diff --git a/.circleci/config.yml b/.circleci/config.yml index 1df6587602a..fd239494034 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1168,7 +1168,18 @@ jobs: - *setup_env - run: name: "Test" - command: spot_run_test_script ./scripts/run_tests canary aztec_js_browser.test.test.ts canary docker-compose.yml + command: spot_run_test_script ./scripts/run_tests canary aztec_js_browser.test.ts canary docker-compose.yml + + run-deployment-canary-cli: + docker: + - image: aztecprotocol/alpine-build-image + resource_class: small + steps: + - *checkout + - *setup_env + - run: + name: "Test" + command: spot_run_test_script ./scripts/run_tests canary cli.test.ts canary docker-compose.yml # Repeatable config for defining the workflow below. tag_regex: &tag_regex /^v.*/ @@ -1403,7 +1414,6 @@ workflows: - deploy-dockerhub: requires: - e2e-end - - aztec-sandbox <<: *deploy_defaults - deploy-npm: requires: @@ -1431,3 +1441,9 @@ workflows: - build-deployment-canary <<: *deploy_defaults + - run-deployment-canary-cli: + requires: + - build-deployment-canary + <<: *deploy_defaults + + diff --git a/build_manifest.json b/build_manifest.json index c3ac0c9dd8c..7ae4ef62ae6 100644 --- a/build_manifest.json +++ b/build_manifest.json @@ -274,6 +274,8 @@ ], "dependencies": [ "aztec.js", + "cli", + "foundation", "l1-artifacts", "noir-contracts" ] diff --git a/yarn-project/aztec-cli/package.json b/yarn-project/aztec-cli/package.json index 8e82eac0e2a..defce57b8cb 100644 --- a/yarn-project/aztec-cli/package.json +++ b/yarn-project/aztec-cli/package.json @@ -54,6 +54,7 @@ "@types/node": "^18.7.23", "jest": "^29.5.0", "jest-mock-extended": "^3.0.5", + "string-argv": "^0.3.2", "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "typescript": "^5.0.4" diff --git a/yarn-project/aztec-cli/src/index.ts b/yarn-project/aztec-cli/src/index.ts index d19200d8f61..18c849a9d49 100644 --- a/yarn-project/aztec-cli/src/index.ts +++ b/yarn-project/aztec-cli/src/index.ts @@ -33,6 +33,8 @@ import { prepTx, } from './utils.js'; +export { cliTestSuite } from './test/cli_test_suite.js'; + const accountCreationSalt = Fr.ZERO; const stripLeadingHex = (hex: string) => { diff --git a/yarn-project/aztec-cli/src/test/cli_test_suite.ts b/yarn-project/aztec-cli/src/test/cli_test_suite.ts new file mode 100644 index 00000000000..0dcb4998a3c --- /dev/null +++ b/yarn-project/aztec-cli/src/test/cli_test_suite.ts @@ -0,0 +1,175 @@ +import { AztecAddress } from '@aztec/aztec.js'; +import { DebugLogger } from '@aztec/foundation/log'; +import { AztecRPC, CompleteAddress } from '@aztec/types'; + +import stringArgv from 'string-argv'; +import { format } from 'util'; + +import { getProgram } from '../index.js'; + +const INITIAL_BALANCE = 33000; +const TRANSFER_BALANCE = 3000; + +export const cliTestSuite = ( + testName: string, + aztecRpcSetup: () => Promise, + cleanup: () => Promise, + rpcUrl: string, + debug: DebugLogger, +) => { + return describe(testName, () => { + let cli: ReturnType; + let aztecRpcClient: AztecRPC; + let existingAccounts: CompleteAddress[]; + let contractAddress: AztecAddress; + let log: (...args: any[]) => void; + + // All logs emitted by the cli will be collected here, and reset between tests + const logs: string[] = []; + + beforeAll(async () => { + aztecRpcClient = await aztecRpcSetup(); + log = (...args: any[]) => { + logs.push(format(...args)); + debug(...args); + }; + }); + + afterAll(async () => { + await cleanup(); + }); + + // in order to run the same command twice, we need to create a new CLI instance + const resetCli = () => { + cli = getProgram(log, debug); + }; + + beforeEach(() => { + logs.splice(0); + resetCli(); + }); + + // Run a command on the CLI + const run = (cmd: string, addRpcUrl = true) => { + const args = stringArgv(cmd, 'node', 'dest/bin/index.js'); + if (addRpcUrl) { + args.push('--rpc-url', rpcUrl); + } + return cli.parseAsync(args); + }; + + // Returns first match across all logs collected so far + const findInLogs = (regex: RegExp) => { + for (const log of logs) { + const match = regex.exec(log); + if (match) return match; + } + }; + + const findMultipleInLogs = (regex: RegExp) => { + const matches = []; + for (const log of logs) { + const match = regex.exec(log); + if (match) matches.push(match); + } + return matches; + }; + + const clearLogs = () => { + logs.splice(0); + }; + + it('creates & retrieves an account', async () => { + existingAccounts = await aztecRpcClient.getAccounts(); + debug('Create an account'); + await run(`create-account`); + const foundAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(foundAddress).toBeDefined(); + const newAddress = AztecAddress.fromString(foundAddress!); + + const accountsAfter = await aztecRpcClient.getAccounts(); + const expectedAccounts = [...existingAccounts.map(a => a.address), newAddress]; + expect(accountsAfter.map(a => a.address)).toEqual(expectedAccounts); + const newCompleteAddress = accountsAfter[accountsAfter.length - 1]; + + // Test get-accounts + debug('Check that account was added to the list of accs in RPC'); + await run('get-accounts'); + const fetchedAddresses = findMultipleInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/); + const foundFetchedAddress = fetchedAddresses.find(match => match.groups?.address === newAddress.toString()); + expect(foundFetchedAddress).toBeDefined(); + + // Test get-account + debug('Check we can retrieve the specific account'); + clearLogs(); + await run(`get-account ${newAddress.toString()}`); + const fetchedAddress = findInLogs(/Public Key:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(fetchedAddress).toEqual(newCompleteAddress.publicKey.toString()); + }); + + it('deploys a contract & sends transactions', async () => { + // generate a private key + debug('Create an account using a private key'); + await run('generate-private-key', false); + const privKey = findInLogs(/Private\sKey:\s+(?[a-fA-F0-9]+)/)?.groups?.privKey; + expect(privKey).toHaveLength(64); + await run(`create-account --private-key ${privKey}`); + const foundAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(foundAddress).toBeDefined(); + const ownerAddress = AztecAddress.fromString(foundAddress!); + + debug('Deploy Private Token Contract using created account.'); + await run(`deploy PrivateTokenContractAbi --args ${INITIAL_BALANCE} ${ownerAddress} --salt 0`); + const loggedAddress = findInLogs(/Contract\sdeployed\sat\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(loggedAddress).toBeDefined(); + contractAddress = AztecAddress.fromString(loggedAddress!); + + const deployedContract = await aztecRpcClient.getContractData(contractAddress); + expect(deployedContract?.contractAddress).toEqual(contractAddress); + + debug('Check contract can be found in returned address'); + await run(`check-deploy -ca ${loggedAddress}`); + const checkResult = findInLogs(/Contract\sfound\sat\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(checkResult).toEqual(deployedContract?.contractAddress.toString()); + + // clear logs + clearLogs(); + await run(`get-contract-data ${loggedAddress}`); + const contractDataAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(contractDataAddress).toEqual(deployedContract?.contractAddress.toString()); + + debug("Check owner's balance"); + await run( + `call getBalance --args ${ownerAddress} --contract-abi PrivateTokenContractAbi --contract-address ${contractAddress.toString()}`, + ); + const balance = findInLogs(/View\sresult:\s+(?\S+)/)?.groups?.data; + expect(balance!).toEqual(`${BigInt(INITIAL_BALANCE).toString()}n`); + + debug('Transfer some tokens'); + const existingAccounts = await aztecRpcClient.getAccounts(); + // ensure we pick a different acc + const receiver = existingAccounts.find(acc => acc.address.toString() !== ownerAddress.toString()); + + await run( + `send transfer --args ${TRANSFER_BALANCE} ${receiver?.address.toString()} --contract-address ${contractAddress.toString()} --contract-abi PrivateTokenContractAbi --private-key ${privKey}`, + ); + const txHash = findInLogs(/Transaction\shash:\s+(?\S+)/)?.groups?.txHash; + + debug('Check the transfer receipt'); + await run(`get-tx-receipt ${txHash}`); + const txResult = findInLogs(/Transaction receipt:\s*(?[\s\S]*?\})/)?.groups?.txHash; + const parsedResult = JSON.parse(txResult!); + expect(parsedResult.txHash).toEqual(txHash); + expect(parsedResult.status).toEqual('mined'); + debug("Check Receiver's balance"); + // Reset CLI as we're calling getBalance again + resetCli(); + clearLogs(); + await run( + `call getBalance --args ${receiver?.address.toString()} --contract-abi PrivateTokenContractAbi --contract-address ${contractAddress.toString()}`, + ); + const receiverBalance = findInLogs(/View\sresult:\s+(?\S+)/)?.groups?.data; + expect(receiverBalance).toEqual(`${BigInt(TRANSFER_BALANCE).toString()}n`); + }); + }); +}; diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_test_suite.ts b/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_test_suite.ts index 20f5969f5bb..94ec2c629d0 100644 --- a/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_test_suite.ts +++ b/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_test_suite.ts @@ -10,7 +10,7 @@ import { } from '@aztec/types'; export const aztecRpcTestSuite = (testName: string, aztecRpcSetup: () => Promise) => { - describe(testName, function () { + describe(testName, () => { let rpc: AztecRPC; beforeAll(async () => { diff --git a/yarn-project/canary/package.json b/yarn-project/canary/package.json index 7d9059f13ce..adf9534ab60 100644 --- a/yarn-project/canary/package.json +++ b/yarn-project/canary/package.json @@ -22,6 +22,8 @@ }, "dependencies": { "@aztec/aztec.js": "workspace:^", + "@aztec/cli": "workspace:^", + "@aztec/foundation": "workspace:^", "@aztec/l1-artifacts": "workspace:^", "@aztec/noir-contracts": "workspace:^", "@jest/globals": "^29.5.0", @@ -32,6 +34,7 @@ "koa": "^2.14.2", "koa-static": "^5.0.0", "puppeteer": "^21.1.0", + "string-argv": "^0.3.2", "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "tslib": "^2.4.0", diff --git a/yarn-project/canary/src/cli.test.ts b/yarn-project/canary/src/cli.test.ts new file mode 100644 index 00000000000..2fa302b8542 --- /dev/null +++ b/yarn-project/canary/src/cli.test.ts @@ -0,0 +1,13 @@ +import { createAztecRpcClient, createDebugLogger, makeFetch } from '@aztec/aztec.js'; +import { cliTestSuite } from '@aztec/cli'; + +const { SANDBOX_URL = 'http://localhost:8080' } = process.env; + +const debug = createDebugLogger('aztec:e2e_cli'); + +const setupRPC = () => { + const aztecRpcClient = createAztecRpcClient(SANDBOX_URL, makeFetch([1, 2, 3], true)); + return Promise.resolve(aztecRpcClient); +}; + +cliTestSuite('CLI canary', setupRPC, () => Promise.resolve(), SANDBOX_URL, debug); diff --git a/yarn-project/canary/tsconfig.json b/yarn-project/canary/tsconfig.json index 6a1fcb002cf..ce5d6bbf22f 100644 --- a/yarn-project/canary/tsconfig.json +++ b/yarn-project/canary/tsconfig.json @@ -22,6 +22,12 @@ { "path": "../aztec.js" }, + { + "path": "../aztec-cli" + }, + { + "path": "../foundation" + }, { "path": "../l1-artifacts" }, diff --git a/yarn-project/end-to-end/src/e2e_cli.test.ts b/yarn-project/end-to-end/src/e2e_cli.test.ts index f86d10daffe..21d038e6f6b 100644 --- a/yarn-project/end-to-end/src/e2e_cli.test.ts +++ b/yarn-project/end-to-end/src/e2e_cli.test.ts @@ -1,184 +1,33 @@ import { AztecNodeService } from '@aztec/aztec-node'; -import { AztecAddress, AztecRPCServer } from '@aztec/aztec-rpc'; +import { AztecRPCServer } from '@aztec/aztec-rpc'; import { startHttpRpcServer } from '@aztec/aztec-sandbox/http'; import { createDebugLogger } from '@aztec/aztec.js'; -import { getProgram } from '@aztec/cli'; -import { DebugLogger } from '@aztec/foundation/log'; -import { AztecRPC, CompleteAddress } from '@aztec/types'; - -import stringArgv from 'string-argv'; -import { format } from 'util'; +import { cliTestSuite } from '@aztec/cli'; +import { AztecRPC } from '@aztec/types'; import { setup } from './fixtures/utils.js'; const HTTP_PORT = 9009; -const INITIAL_BALANCE = 33000; -const TRANSFER_BALANCE = 3000; - -// Spins up a new http server wrapping the set up rpc server, and tests cli commands against it -describe('cli', () => { - let cli: ReturnType; - let http: ReturnType; - let debug: DebugLogger; - let aztecNode: AztecNodeService | undefined; - let aztecRpcServer: AztecRPC; - let existingAccounts: CompleteAddress[]; - let contractAddress: AztecAddress; - let log: (...args: any[]) => void; - - // All logs emitted by the cli will be collected here, and reset between tests - const logs: string[] = []; - - beforeAll(async () => { - debug = createDebugLogger('aztec:e2e_cli'); - const context = await setup(2); - debug(`Environment set up`); - const { deployL1ContractsValues } = context; - ({ aztecNode, aztecRpcServer } = context); - http = startHttpRpcServer(aztecRpcServer, deployL1ContractsValues, HTTP_PORT); - debug(`HTTP RPC server started in port ${HTTP_PORT}`); - log = (...args: any[]) => { - logs.push(format(...args)); - debug(...args); - }; - }); - - // in order to run the same command twice, we need to create a new CLI instance - const resetCli = () => { - cli = getProgram(log, debug); - }; - - afterAll(async () => { - http.close(); - await aztecNode?.stop(); - await (aztecRpcServer as AztecRPCServer).stop(); - }); - - beforeEach(() => { - logs.splice(0); - resetCli(); - }); - - // Run a command on the CLI - const run = (cmd: string, addRpcUrl = true) => { - const args = stringArgv(cmd, 'node', 'dest/bin/index.js'); - if (addRpcUrl) { - args.push('--rpc-url', `http://localhost:${HTTP_PORT}`); - } - return cli.parseAsync(args); - }; - - // Returns first match across all logs collected so far - const findInLogs = (regex: RegExp) => { - for (const log of logs) { - const match = regex.exec(log); - if (match) return match; - } - }; - - const findMultipleInLogs = (regex: RegExp) => { - const matches = []; - for (const log of logs) { - const match = regex.exec(log); - if (match) matches.push(match); - } - return matches; - }; - - const clearLogs = () => { - logs.splice(0); - }; - - it('creates & retrieves an account', async () => { - existingAccounts = await aztecRpcServer.getAccounts(); - debug('Create an account'); - await run(`create-account`); - const foundAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; - expect(foundAddress).toBeDefined(); - const newAddress = AztecAddress.fromString(foundAddress!); - - const accountsAfter = await aztecRpcServer.getAccounts(); - const expectedAccounts = [...existingAccounts.map(a => a.address), newAddress]; - expect(accountsAfter.map(a => a.address)).toEqual(expectedAccounts); - const newCompleteAddress = accountsAfter[accountsAfter.length - 1]; - - // Test get-accounts - debug('Check that account was added to the list of accs in RPC'); - await run('get-accounts'); - const fetchedAddresses = findMultipleInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/); - const foundFetchedAddress = fetchedAddresses.find(match => match.groups?.address === newAddress.toString()); - expect(foundFetchedAddress).toBeDefined(); - - // Test get-account - debug('Check we can retrieve the specific account'); - clearLogs(); - await run(`get-account ${newAddress.toString()}`); - const fetchedAddress = findInLogs(/Public Key:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; - expect(fetchedAddress).toEqual(newCompleteAddress.publicKey.toString()); - }); - - it('deploys a contract & sends transactions', async () => { - // generate a private key - debug('Create an account using a private key'); - await run('generate-private-key', false); - const privKey = findInLogs(/Private\sKey:\s+(?[a-fA-F0-9]+)/)?.groups?.privKey; - expect(privKey).toHaveLength(64); - await run(`create-account --private-key ${privKey}`); - const foundAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; - expect(foundAddress).toBeDefined(); - const ownerAddress = AztecAddress.fromString(foundAddress!); - - debug('Deploy Private Token Contract using created account.'); - await run(`deploy PrivateTokenContractAbi --args ${INITIAL_BALANCE} ${ownerAddress} --salt 0`); - const loggedAddress = findInLogs(/Contract\sdeployed\sat\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; - expect(loggedAddress).toBeDefined(); - contractAddress = AztecAddress.fromString(loggedAddress!); - - const deployedContract = await aztecRpcServer.getContractData(contractAddress); - expect(deployedContract?.contractAddress).toEqual(contractAddress); - - debug('Check contract can be found in returned address'); - await run(`check-deploy -ca ${loggedAddress}`); - const checkResult = findInLogs(/Contract\sfound\sat\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; - expect(checkResult).toEqual(deployedContract?.contractAddress.toString()); - - // clear logs - clearLogs(); - await run(`get-contract-data ${loggedAddress}`); - const contractDataAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; - expect(contractDataAddress).toEqual(deployedContract?.contractAddress.toString()); - - debug("Check owner's balance"); - await run( - `call getBalance --args ${ownerAddress} --contract-abi PrivateTokenContractAbi --contract-address ${contractAddress.toString()}`, - ); - const balance = findInLogs(/View\sresult:\s+(?\S+)/)?.groups?.data; - expect(balance!).toEqual(`${BigInt(INITIAL_BALANCE).toString()}n`); - - debug('Transfer some tokens'); - const existingAccounts = await aztecRpcServer.getAccounts(); - // ensure we pick a different acc - const receiver = existingAccounts.find(acc => acc.address.toString() !== ownerAddress.toString()); - - await run( - `send transfer --args ${TRANSFER_BALANCE} ${receiver?.address.toString()} --contract-address ${contractAddress.toString()} --contract-abi PrivateTokenContractAbi --private-key ${privKey}`, - ); - const txHash = findInLogs(/Transaction\shash:\s+(?\S+)/)?.groups?.txHash; - - debug('Check the transfer receipt'); - await run(`get-tx-receipt ${txHash}`); - const txResult = findInLogs(/Transaction receipt:\s*(?[\s\S]*?\})/)?.groups?.txHash; - const parsedResult = JSON.parse(txResult!); - expect(parsedResult.txHash).toEqual(txHash); - expect(parsedResult.status).toEqual('mined'); - debug("Check Receiver's balance"); - // Reset CLI as we're calling getBalance again - resetCli(); - clearLogs(); - await run( - `call getBalance --args ${receiver?.address.toString()} --contract-abi PrivateTokenContractAbi --contract-address ${contractAddress.toString()}`, - ); - const receiverBalance = findInLogs(/View\sresult:\s+(?\S+)/)?.groups?.data; - expect(receiverBalance).toEqual(`${BigInt(TRANSFER_BALANCE).toString()}n`); - }); -}); +const debug = createDebugLogger('aztec:e2e_cli'); + +let http: ReturnType; +let aztecNode: AztecNodeService | undefined; +let aztecRpcServer: AztecRPC; + +const testSetup = async () => { + const context = await setup(2); + debug(`Environment set up`); + const { deployL1ContractsValues } = context; + ({ aztecNode, aztecRpcServer } = context); + http = startHttpRpcServer(aztecRpcServer, deployL1ContractsValues, HTTP_PORT); + debug(`HTTP RPC server started in port ${HTTP_PORT}`); + return aztecRpcServer; +}; + +const cleanup = async () => { + http.close(); + await aztecNode?.stop(); + await (aztecRpcServer as AztecRPCServer).stop(); +}; + +cliTestSuite('CLI e2e test', testSetup, cleanup, `http://localhost:${HTTP_PORT}`, debug); diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 9ce39dfd1b4..f9a3923961e 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -256,6 +256,8 @@ __metadata: resolution: "@aztec/canary@workspace:canary" dependencies: "@aztec/aztec.js": "workspace:^" + "@aztec/cli": "workspace:^" + "@aztec/foundation": "workspace:^" "@aztec/l1-artifacts": "workspace:^" "@aztec/noir-contracts": "workspace:^" "@jest/globals": ^29.5.0 @@ -267,6 +269,7 @@ __metadata: koa: ^2.14.2 koa-static: ^5.0.0 puppeteer: ^21.1.0 + string-argv: ^0.3.2 ts-jest: ^29.1.0 ts-node: ^10.9.1 tslib: ^2.4.0 @@ -326,6 +329,7 @@ __metadata: jest: ^29.5.0 jest-mock-extended: ^3.0.5 semver: ^7.5.4 + string-argv: ^0.3.2 ts-jest: ^29.1.0 ts-node: ^10.9.1 tslib: ^2.4.0