diff --git a/docker/ds-minimal/docker-compose.yml b/docker/ds-minimal/docker-compose.yml new file mode 100644 index 000000000..838a97104 --- /dev/null +++ b/docker/ds-minimal/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + backend: + image: dm3org/dm3-backend:v0.2.1 + restart: always + depends_on: + - db + environment: + REDIS_URL: redis://db:6379 + SIGNING_PUBLIC_KEY: ${SIGNING_PUBLIC_KEY} + SIGNING_PRIVATE_KEY: ${SIGNING_PRIVATE_KEY} + ENCRYPTION_PUBLIC_KEY: ${ENCRYPTION_PUBLIC_KEY} + ENCRYPTION_PRIVATE_KEY: ${ENCRYPTION_PRIVATE_KEY} + RPC: ${RPC} + PORT: 8081 + LOG_LEVEL: 'debug' + db: + image: redis + restart: always diff --git a/package.json b/package.json index 6d18b8536..655e9ca82 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ ] }, "scripts": { + "cli":"yarn workspace dm3-cli run dm3", "docker:up": "cd packages/backend && docker-compose up -d", "build": " yarn workspaces foreach -pt run build", "start": "yarn workspace dm3-backend start", diff --git a/packages/cli/README.md b/packages/cli/README.md index dcb20d0cd..6b50e29ac 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,14 +1,16 @@ # DM3 CLI -## SETUP +## SETUP BillboardDs + ### Overview -The CLI command "setup" is used to configure a DM3 delivery service for your domain. It initiates all the necessary on-chain transactions and provides an environment configuration that you can use for the delivery service (DS). +The CLI command "setup billboardDs" is used to configure a DM3 delivery service for your domain. It initiates all the necessary on-chain transactions and provides an environment configuration that you can use for the delivery service (DS). +It also configures subdomains for .addr and ds, enabling them to resolve requests via the provided gateway. ### Usage -`yarn cli setup --rpc --pk --domain --gateway --deliveryService --profilePk --ensRegistry --ensResolver --erc3668Resolver ` +`setup billboardDs --rpc --pk --domain --gateway --deliveryService --profilePk --ensRegistry --ensResolver --erc3668Resolver ` ### OPTIONS @@ -24,7 +26,37 @@ The CLI command "setup" is used to configure a DM3 delivery service for your dom ### Example -`setup --rpc http://127.0.0.1:8545 --pk 0x123456789abcdef --domain alice.eth --gateway https://gateway.io/ --deliveryService https://ds.io/ --profilePk 0x987654321fedcba --ensRegistry 0xabcdef123456789 --ensResolver 0xfedcba987654321 --erc3668Resolver 0x123456789abcdef0` +`setup billboardDs --rpc http://127.0.0.1:8545 --pk 0x123456789abcdef --domain alice.eth --gateway https://gateway.io/ --deliveryService https://ds.io/ --profilePk 0x987654321fedcba --ensRegistry 0xabcdef123456789 --ensResolver 0xfedcba987654321 --erc3668Resolver 0x123456789abcdef0` + + +## Setup Offchain DS + +### Overview + +The CLI command "setup onchainDs" can be used to configure the minimal viable version of a Delivery Service (DS). It creates a Delivery Service Profile on-chain and prints the environment variables needed to run your own DS. +To set a DS up run the following steps + +1. Run the command below to create the delivery service profile on-chain. +2. Copy the Docker Compose file to your project using the command: `cp docker/ds-minimal/docker-compose.yml $YOUR_PROJECT_PATH`. +3. Create an .env file at the same directory and copy the values created at step one into that file +4. Start the Delivery Service with `docker compose up` + +### Usage + +`setup offchainDS --rpc --pk --domain --deliveryService --profilePk --ensResolver ` + +### OPTIONS + +- --rpc : The Ethereum RPC URL to connect to. +- --pk : The private key of the account owning the domain. That account will be used to execute the tx and has to be funded accordingly. +- --domain : The ENS (Ethereum Name Service) domain associated with dm3. It has to be owned by the account used as Private Key. +- --deliveryService : The URL of the delivery service. +- --profilePk : The private key used as a seed to create the delivery service. When omitted, a random private key will be created. +- --ensResolver : The address of the ENS public resolver contract. + +### Example + +`setup offchainDS --rpc http://127.0.0.1:8545 --pk 0x123456789abcdef --domain alice.eth --deliveryService https://ds.io/ --profilePk 0x987654321fedcba --ensResolver 0xfedcba987654321 ` ## Profile @@ -36,3 +68,4 @@ The CLI command "setup" is used to create a dm3 user profile based on an existin `yarn cli profile --deliveryService foo.eth --profilePk 0x123` - --deliveryService : The URL of the delivery service. - --profilePk : Optionally, if provided, the profile will be created based on that key. Otherwise, a random key will be generated. + diff --git a/packages/cli/cli.test.ts b/packages/cli/cli.test.ts index 049e35c64..e9c1c4f61 100644 --- a/packages/cli/cli.test.ts +++ b/packages/cli/cli.test.ts @@ -111,23 +111,23 @@ describe('cli', () => { const cli = (argv = ''): any => new Promise((resolve, reject) => { - const subprocess = execa.command(`yarn start ${argv}`); + const subprocess = execa.command(`yarn run ${argv}`); subprocess.stdout!.pipe(process.stdout); subprocess.stderr!.pipe(process.stderr); Promise.resolve(subprocess).then(resolve).catch(resolve); }); - describe('setup', () => { + describe('setup billboardDs', () => { describe('sanitize input', () => { it('reverts for unknown input', async () => { - const res = await cli('setup --efeh'); + const res = await cli('dm3 setup billboardDs --efeh'); expect(res.stderr).to.equal("error: unknown option '--efeh'"); }); it('reverts if rpc url is undefined', async () => { const wallet = ethers.Wallet.createRandom(); const res = await cli( - `setup --pk ${wallet.privateKey} --domain test.eth`, + `dm3 setup billboardDs --pk ${wallet.privateKey} --domain test.eth`, ); expect(res.stderr).to.equal( 'error: option --rpc argument missing', @@ -136,7 +136,7 @@ describe('cli', () => { it('reverts if privateKey is undefined', async () => { const res = await cli( - 'setup --rpc www.rpc.io --domain test.eth', + 'dm3 setup billboardDs --rpc www.rpc.io --domain test.eth', ); expect(res.stderr).to.equal( 'error: option --pk argument missing', @@ -144,7 +144,7 @@ describe('cli', () => { }); it('reverts if privateKey is invalid', async () => { const res = await cli( - 'setup --rpc www.rpc.io --domain test.eth --pk 123', + 'dm3 setup billboardDs --rpc www.rpc.io --domain test.eth --pk 123', ); expect(res.stderr).to.equal( 'error: option --pk argument invalid', @@ -153,7 +153,7 @@ describe('cli', () => { it('reverts if domain is undefined', async () => { const wallet = ethers.Wallet.createRandom(); const res = await cli( - `setup --rpc www.rpc.io --pk ${wallet.privateKey}`, + `dm3 setup billboardDs --rpc www.rpc.io --pk ${wallet.privateKey}`, ); expect(res.stderr).to.equal( 'error: option --domain argument missing', @@ -162,19 +162,19 @@ describe('cli', () => { it('reverts if gateway url is undefined', async () => { const wallet = ethers.Wallet.createRandom(); const res = await cli( - `setup --rpc www.rpc.io --pk ${wallet.privateKey} --domain test.eth`, + `dm3 setup billboardDs --rpc www.rpc.io --pk ${wallet.privateKey} --domain test.eth`, ); expect(res.stderr).to.equal( 'error: option --gateway argument missing', ); }); }); - describe('setupAll', () => { + describe('setup billboardDsAll', () => { it('test all', async () => { const owner = ethers.Wallet.createRandom(); const res = await cli( - `setup + `dm3 setup billboardDs --rpc http://127.0.0.1:8545 --pk ${alice.privateKey} --domain alice.eth @@ -234,7 +234,7 @@ describe('cli', () => { }); it('test all with random profile wallet', async () => { const res = await cli( - `setup + `dm3 setup billboardDs --rpc http://127.0.0.1:8545 --pk ${alice.privateKey} --domain alice.eth @@ -298,7 +298,7 @@ describe('cli', () => { ); const res = await cli( - `setup + `dm3 setup billboardDs --rpc http://127.0.0.1:8545 --pk ${underfundedWallet.privateKey} --domain alice.eth @@ -320,4 +320,140 @@ describe('cli', () => { }); }); }); + describe('setup onChain', () => { + describe('sanitize input', () => { + it('reverts for unknown input', async () => { + const res = await cli('dm3 setup onchainDs --efeh'); + expect(res.stderr).to.equal("error: unknown option '--efeh'"); + }); + + it('reverts if rpc url is undefined', async () => { + const wallet = ethers.Wallet.createRandom(); + const res = await cli( + `dm3 setup onchainDs --pk ${wallet.privateKey} --domain test.eth`, + ); + expect(res.stderr).to.equal( + 'error: option --rpc argument missing', + ); + }); + + it('reverts if privateKey is undefined', async () => { + const res = await cli( + 'dm3 setup onchainDs --rpc www.rpc.io --domain test.eth', + ); + expect(res.stderr).to.equal( + 'error: option --pk argument missing', + ); + }); + it('reverts if privateKey is invalid', async () => { + const res = await cli( + 'dm3 setup onchainDs --rpc www.rpc.io --domain test.eth --pk 123', + ); + expect(res.stderr).to.equal( + 'error: option --pk argument invalid', + ); + }); + it('reverts if domain is undefined', async () => { + const wallet = ethers.Wallet.createRandom(); + const res = await cli( + `dm3 setup onchainDs --rpc www.rpc.io --pk ${wallet.privateKey}`, + ); + expect(res.stderr).to.equal( + 'error: option --domain argument missing', + ); + }); + }); + describe('setup onchainDsAll', () => { + it('test all', async () => { + const owner = ethers.Wallet.createRandom(); + + const res = await cli( + `dm3 setup onchainDs + --rpc http://127.0.0.1:8545 + --pk ${alice.privateKey} + --domain alice.eth + --deliveryService https://ds.io/ + --profilePk ${owner.privateKey} + --ensResolver ${publicResolver.address} + `, + ); + + const profile = await publicResolver.text( + ethers.utils.namehash('alice.eth'), + 'network.dm3.deliveryService', + ); + expect(JSON.parse(profile).url).to.equal('https://ds.io/'); + + const encryptionKeyPair = await createKeyPair( + await createStorageKey(owner.privateKey), + ); + const signingKeyPair = await createSigningKeyPair( + await createStorageKey(owner.privateKey), + ); + + expect(JSON.parse(profile).publicEncryptionKey).to.equal( + encryptionKeyPair.publicKey, + ); + expect(JSON.parse(profile).publicSigningKey).to.equal( + signingKeyPair.publicKey, + ); + }); + it('test all with random profile wallet', async () => { + const res = await cli( + `dm3 setup onchainDs + --rpc http://127.0.0.1:8545 + --pk ${alice.privateKey} + --domain alice.eth + --deliveryService https://ds.io/ + --ensResolver ${publicResolver.address} + `, + ); + + const profile = await publicResolver.text( + ethers.utils.namehash('alice.eth'), + 'network.dm3.deliveryService', + ); + expect(JSON.parse(profile).url).to.equal('https://ds.io/'); + + expect(JSON.parse(profile).publicEncryptionKey).to.not.be + .undefined; + expect(JSON.parse(profile).publicSigningKey).to.not.be + .undefined; + }); + it('rejects with underfunded balance', async () => { + const provider = new ethers.providers.JsonRpcProvider( + 'http://127.0.0.1:8545/', + ); + + const underfundedWallet = ethers.Wallet.createRandom(); + //Send a little bit of ETH to the wallet. Although its to little to pay for the transactions + await owner.connect(provider).sendTransaction({ + to: underfundedWallet.address, + value: ethers.utils.parseEther('0.0001'), + }); + const balanceBefore = await provider.getBalance( + underfundedWallet.address, + ); + + const res = await cli( + `dm3 setup onchainDs + --rpc http://127.0.0.1:8545 + --pk ${underfundedWallet.privateKey} + --domain alice.eth + --deliveryService https://ds.io/ + --ensResolver ${publicResolver.address} + `, + ); + + const balanceAfter = await provider.getBalance( + underfundedWallet.address, + ); + + expect(balanceAfter._hex).to.equal(balanceBefore._hex); + expect(res.stderr).to.include( + 'has insufficient funds to send 1 transactions with total cost of', + ); + }); + }); + }); }); diff --git a/packages/cli/index.ts b/packages/cli/index.ts index cb36ec96c..c49b374ce 100644 --- a/packages/cli/index.ts +++ b/packages/cli/index.ts @@ -44,10 +44,11 @@ const cli = async () => { ); program.parse(); - const [mode] = program.args; + // Every mode is prefixed with setup so we only have to differentiate the second arg + const [, mode] = program.args; switch (mode) { - case 'setup': { + case 'billboardDs': { const args = program.opts(); const { pk, domain, gateway, rpc, profilePk, deliveryService } = @@ -79,7 +80,7 @@ const cli = async () => { 'error: option --deliveryService argument missing', ); } - await Installer.setupAll({ + await Installer.setupBillboardDs({ wallet, profileWallet, domain, @@ -91,10 +92,49 @@ const cli = async () => { break; } + + case 'onchainDs': { + const args = program.opts(); + const { pk, domain, gateway, rpc, profilePk, deliveryService } = + args; + + if (!rpc) { + program.error('error: option --rpc argument missing'); + } + + const wallet = getSanitizedWallet(program, pk, 'pk'); + + const profileWallet = getSanitizedWallet( + program, + profilePk ?? ethers.Wallet.createRandom().privateKey, + 'profilePk', + ); + if (!domain) { + program.error( + 'error: option --domain argument missing', + ); + } + if (!deliveryService) { + program.error( + 'error: option --deliveryService argument missing', + ); + } + await Installer.setupOnChain({ + wallet, + profileWallet, + domain, + rpc, + deliveryService, + ...args, + }); + + break; + } case 'profile': { Profile.newProfile(program); break; } + default: { program.error('error: unknown option'); } diff --git a/packages/cli/installer/index.ts b/packages/cli/installer/index.ts index e9e4ce9d1..55e31141a 100644 --- a/packages/cli/installer/index.ts +++ b/packages/cli/installer/index.ts @@ -1,18 +1,18 @@ import { ethers } from 'ethers'; import { createDsProfile } from './tasks/createDsProfile'; +import { printEnv } from './tasks/printEnv'; import { sendTransactions } from './tasks/sendTransactions'; import { ERC3668Resolver } from './transactions/ERC3668Resolver'; import { EnsRegistry } from './transactions/EnsRegistry'; import { EnsResolver } from './transactions/EnsResolver'; import { SignatureVerifier } from './transactions/SignatureVerifier'; -import { InstallerArgs } from './types'; -import { printEnv } from './tasks/printEnv'; +import { InstallerArgs, SetupBillboardDsArgs, SetupOnchainArgs } from './types'; const ENS_REGISTRY_ADDRESS = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'; const ERC3668RESOLVER_ADDRESSS = ethers.constants.AddressZero; -const setupAll = async (args: InstallerArgs) => { +const setupBillboardDs = async (args: SetupBillboardDsArgs) => { const graphQlurl = ''; const resolverChainId = '1'; const resolverName = 'SignatureCcipVerifier'; @@ -109,4 +109,32 @@ const setupAll = async (args: InstallerArgs) => { await sendTransactions(args, tx); printEnv(args, keys); }; -export { setupAll }; + +const setupOnChain = async (args: SetupOnchainArgs) => { + const provider = new ethers.providers.StaticJsonRpcProvider(args.rpc); + + //Create ds profile + const ensPublicResolverAddress = args.ensResolver + ? args.ensResolver + : (await provider.getResolver(args.wallet.address))!.address; + const publicResolver = EnsResolver(ensPublicResolverAddress); + + const { profile, keys } = await createDsProfile(args); + + const transactionCount = await provider.getTransactionCount( + args.wallet.address, + ); + + const tx = [ + //Set Ds profile on chain + publicResolver.setText( + `${args.domain}`, + 'network.dm3.deliveryService', + JSON.stringify(profile), + transactionCount, + ), + ]; + await sendTransactions(args, tx); + printEnv(args, keys); +}; +export { setupBillboardDs, setupOnChain }; diff --git a/packages/cli/installer/tasks/createDsProfile.ts b/packages/cli/installer/tasks/createDsProfile.ts index 99f37b042..579e67ae9 100644 --- a/packages/cli/installer/tasks/createDsProfile.ts +++ b/packages/cli/installer/tasks/createDsProfile.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { DeliveryServiceProfile, DeliveryServiceProfileKeys, @@ -8,7 +9,6 @@ import { createSigningKeyPair, createStorageKey, } from 'dm3-lib-crypto'; -import { logInfo } from 'dm3-lib-shared'; export const createDsProfile = async (args: InstallerArgs) => { const keys: DeliveryServiceProfileKeys = { @@ -25,8 +25,8 @@ export const createDsProfile = async (args: InstallerArgs) => { url: args.deliveryService, }; - logInfo('Create DeliveryServiceProfile:'); - logInfo(profile); + console.log('Create DeliveryServiceProfile:'); + console.log(profile); return { profile, keys }; }; diff --git a/packages/cli/installer/tasks/sendTransactions.ts b/packages/cli/installer/tasks/sendTransactions.ts index 941409853..f09fb2c75 100644 --- a/packages/cli/installer/tasks/sendTransactions.ts +++ b/packages/cli/installer/tasks/sendTransactions.ts @@ -1,6 +1,6 @@ +/* eslint-disable no-console */ import { BigNumber, ethers } from 'ethers'; import { InstallerArgs } from '../types'; -import { logInfo } from 'dm3-lib-shared'; export const sendTransactions = async ( args: InstallerArgs, @@ -26,7 +26,7 @@ export const sendTransactions = async ( ); } - logInfo( + console.log( `Send ${ tx.length } transaction with total cost of ${ethers.utils.formatEther( @@ -49,5 +49,5 @@ export const sendTransactions = async ( if (hasError) { throw new Error('Failed to send transactions'); } - logInfo('Transactions sent successfully'); + console.log('Transactions sent successfully'); }; diff --git a/packages/cli/installer/transactions/ERC3668Resolver.ts b/packages/cli/installer/transactions/ERC3668Resolver.ts index 3447c3662..6c7b7fc6c 100644 --- a/packages/cli/installer/transactions/ERC3668Resolver.ts +++ b/packages/cli/installer/transactions/ERC3668Resolver.ts @@ -1,4 +1,4 @@ -import { logInfo } from 'dm3-lib-shared'; +/* eslint-disable no-console */ import { ethers } from 'ethers'; export const ERC3668Resolver = (address: string) => { @@ -26,7 +26,7 @@ export const ERC3668Resolver = (address: string) => { nonce, gasLimit: 215000, }; - logInfo( + console.log( `set ccip resolver for ${domain} to ${verifierAddress} at nonce ${nonce}`, ); return tx; diff --git a/packages/cli/installer/transactions/EnsRegistry.ts b/packages/cli/installer/transactions/EnsRegistry.ts index b948bbc5a..386f681cb 100644 --- a/packages/cli/installer/transactions/EnsRegistry.ts +++ b/packages/cli/installer/transactions/EnsRegistry.ts @@ -1,4 +1,4 @@ -import { logInfo } from 'dm3-lib-shared'; +/* eslint-disable no-console */ import { ethers } from 'ethers'; const TTL = 300; @@ -32,7 +32,7 @@ export const EnsRegistry = (address: string) => { gasLimit: 100000, nonce, }; - logInfo( + console.log( `Set Subnode record for ${label}.${domain} owner : ${owner} resolver : ${resolver} at nonce ${nonce}`, ); return tx; diff --git a/packages/cli/installer/transactions/EnsResolver.ts b/packages/cli/installer/transactions/EnsResolver.ts index e619e77f3..6e22bd1d6 100644 --- a/packages/cli/installer/transactions/EnsResolver.ts +++ b/packages/cli/installer/transactions/EnsResolver.ts @@ -1,4 +1,4 @@ -import { logInfo } from 'dm3-lib-shared'; +/* eslint-disable no-console */ import { ethers } from 'ethers'; export const EnsResolver = (address: string) => { @@ -22,7 +22,7 @@ export const EnsResolver = (address: string) => { nonce, gasLimit: 350000, }; - logInfo( + console.log( `Publish delivery service profile for ${domain} at nonce ${nonce}`, ); return tx; diff --git a/packages/cli/installer/transactions/SignatureVerifier.ts b/packages/cli/installer/transactions/SignatureVerifier.ts index 2645c61fe..08a1f1958 100644 --- a/packages/cli/installer/transactions/SignatureVerifier.ts +++ b/packages/cli/installer/transactions/SignatureVerifier.ts @@ -1,8 +1,8 @@ +/* eslint-disable no-console */ import { SignatureCcipVerifier__factory, //@ts-ignore } from 'ccip-resolver/dist/typechain/'; -import { log, logInfo } from 'dm3-lib-shared'; import { BigNumber, ethers } from 'ethers'; import { RLP, keccak256 } from 'ethers/lib/utils'; @@ -36,7 +36,7 @@ export const SignatureVerifier = (deployer: string) => { nonce, gasLimit: 2000000, }; - logInfo( + console.log( `Deploy SignatureVerifier at ${verifierAddress} with nonce ${nonce}`, ); return { verifierAddress, signatureVerifierDeployTransaction }; diff --git a/packages/cli/installer/types.ts b/packages/cli/installer/types.ts index 3043ce1f0..dba54e6ea 100644 --- a/packages/cli/installer/types.ts +++ b/packages/cli/installer/types.ts @@ -4,10 +4,15 @@ export interface InstallerArgs { wallet: ethers.Wallet; profileWallet: ethers.Wallet; domain: string; - gateway: string; + deliveryService: string; rpc: string; ensRegistry?: string; ensResolver?: string; erc3668Resolver?: string; } + +export type SetupOnchainArgs = InstallerArgs; +export type SetupBillboardDsArgs = InstallerArgs & { + gateway: string; +}; diff --git a/packages/cli/package.json b/packages/cli/package.json index 617a8266d..3a3568926 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -2,7 +2,7 @@ "name": "dm3-cli", "version": "0.0.1", "scripts": { - "start": "ts-node index.ts", + "dm3": "ts-node index.ts", "test": "npx hardhat test cli.test.ts --network hardhat", "start-hh-node": "npx hardhat node" },