diff --git a/.circleci/config.yml b/.circleci/config.yml index 92500e093c3..59a5e0d0964 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -881,6 +881,18 @@ jobs: command: ./scripts/cond_run_script end-to-end $JOB_NAME ./scripts/run_tests_local e2e_public_token_contract.test.ts working_directory: yarn-project/end-to-end + e2e-cli: + machine: + image: ubuntu-2004:202010-01 + resource_class: large + steps: + - *checkout + - *setup_env + - run: + name: "Test" + command: ./scripts/cond_run_script end-to-end $JOB_NAME ./scripts/run_tests_local e2e_cli.test.ts + working_directory: yarn-project/end-to-end + e2e-p2p: docker: - image: aztecprotocol/alpine-build-image @@ -1225,6 +1237,7 @@ workflows: - e2e-non-contract-account: *e2e_test - e2e-multiple-accounts-1-enc-key: *e2e_test - e2e-public-token-contract: *e2e_test + - e2e-cli: *e2e_test - e2e-cross-chain-messaging: *e2e_test - e2e-public-cross-chain-messaging: *e2e_test - e2e-public-to-private-messaging: *e2e_test @@ -1249,6 +1262,7 @@ workflows: - e2e-non-contract-account - e2e-multiple-accounts-1-enc-key - e2e-public-token-contract + - e2e-cli - e2e-cross-chain-messaging - e2e-public-cross-chain-messaging - e2e-public-to-private-messaging diff --git a/build_manifest.json b/build_manifest.json index 96f7d2de625..7d994bb3751 100644 --- a/build_manifest.json +++ b/build_manifest.json @@ -300,8 +300,10 @@ "archiver", "aztec-node", "aztec-rpc", + "aztec-sandbox", "aztec.js", "circuits.js", + "cli", "ethereum", "foundation", "l1-artifacts", diff --git a/yarn-project/aztec-cli/package.json b/yarn-project/aztec-cli/package.json index 94b8112b128..09aa42df0a0 100644 --- a/yarn-project/aztec-cli/package.json +++ b/yarn-project/aztec-cli/package.json @@ -4,7 +4,7 @@ "type": "module", "main": "./dest/index.js", "bin": { - "aztec-cli": "./dest/index.js" + "aztec-cli": "./dest/bin/index.js" }, "typedocOptions": { "entryPoints": [ diff --git a/yarn-project/aztec-cli/src/bin/index.ts b/yarn-project/aztec-cli/src/bin/index.ts new file mode 100644 index 00000000000..014d5e05a24 --- /dev/null +++ b/yarn-project/aztec-cli/src/bin/index.ts @@ -0,0 +1,20 @@ +#!/usr/bin/env -S node --no-warnings +import { createDebugLogger } from '@aztec/aztec.js'; +import { createConsoleLogger } from '@aztec/foundation/log'; + +import { getProgram } from '../index.js'; + +const debugLogger = createDebugLogger('aztec:cli'); +const log = createConsoleLogger(); + +/** CLI main entrypoint */ +async function main() { + const program = getProgram(log, debugLogger); + await program.parseAsync(process.argv); +} + +main().catch(err => { + log(`Error in command execution`); + log(err); + process.exit(1); +}); diff --git a/yarn-project/aztec-cli/src/cli_encoder.ts b/yarn-project/aztec-cli/src/encoding.ts similarity index 100% rename from yarn-project/aztec-cli/src/cli_encoder.ts rename to yarn-project/aztec-cli/src/encoding.ts diff --git a/yarn-project/aztec-cli/src/index.ts b/yarn-project/aztec-cli/src/index.ts index 98c2d3f5fc8..23132d69caf 100644 --- a/yarn-project/aztec-cli/src/index.ts +++ b/yarn-project/aztec-cli/src/index.ts @@ -1,4 +1,3 @@ -#!/usr/bin/env -S node --no-warnings import { AztecAddress, Contract, @@ -13,15 +12,18 @@ import { } from '@aztec/aztec.js'; import { StructType } from '@aztec/foundation/abi'; import { JsonStringify } from '@aztec/foundation/json-rpc'; -import { createConsoleLogger, createDebugLogger } from '@aztec/foundation/log'; +import { DebugLogger, LogFn } from '@aztec/foundation/log'; import { compileContract } from '@aztec/noir-compiler/cli'; import { SchnorrAccountContractAbi } from '@aztec/noir-contracts/artifacts'; import { CompleteAddress, ContractData, L2BlockL2Logs, PrivateKey, TxHash } from '@aztec/types'; import { Command } from 'commander'; +import { readFileSync } from 'fs'; +import { dirname, resolve } from 'path'; +import { fileURLToPath } from 'url'; import { mnemonicToAccount } from 'viem/accounts'; -import { encodeArgs, parseStructString } from './cli_encoder.js'; +import { encodeArgs, parseStructString } from './encoding.js'; import { deployAztecContracts, getAbiFunction, @@ -33,8 +35,6 @@ import { const accountCreationSalt = Fr.ZERO; -const debugLogger = createDebugLogger('aztec:cli'); -const log = createConsoleLogger(); const stripLeadingHex = (hex: string) => { if (hex.length > 2 && hex.startsWith('0x')) { return hex.substring(2); @@ -42,16 +42,22 @@ const stripLeadingHex = (hex: string) => { return hex; }; -const program = new Command(); - -program.name('aztec-cli').description('CLI for interacting with Aztec.').version('0.1.0'); - const { ETHEREUM_HOST, AZTEC_RPC_HOST, PRIVATE_KEY, API_KEY } = process.env; /** - * Main function for the Aztec CLI. + * Returns commander program that defines the CLI. + * @param log - Console logger. + * @param debugLogger - Debug logger. + * @returns The CLI. */ -async function main() { +export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { + const program = new Command(); + + const packageJsonPath = resolve(dirname(fileURLToPath(import.meta.url)), '../package.json'); + const version: string = JSON.parse(readFileSync(packageJsonPath).toString()).version; + + program.name('aztec-cli').description('CLI for interacting with Aztec.').version(version); + program .command('deploy-l1-contracts') .description('Deploys all necessary Ethereum contracts for Aztec.') @@ -474,11 +480,5 @@ async function main() { compileContract(program, 'compile', log); - await program.parseAsync(process.argv); + return program; } - -main().catch(err => { - log(`Error in command execution`); - log(err); - process.exit(1); -}); diff --git a/yarn-project/aztec-cli/src/utils.ts b/yarn-project/aztec-cli/src/utils.ts index 3587cdabac1..a1fd71648b2 100644 --- a/yarn-project/aztec-cli/src/utils.ts +++ b/yarn-project/aztec-cli/src/utils.ts @@ -6,7 +6,7 @@ import { DebugLogger, LogFn } from '@aztec/foundation/log'; import fs from 'fs'; import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; -import { encodeArgs } from './cli_encoder.js'; +import { encodeArgs } from './encoding.js'; /** * Helper type to dynamically import contracts. diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_http/aztec_rpc_http_server.ts b/yarn-project/aztec-rpc/src/aztec_rpc_http/aztec_rpc_http_server.ts index ef0fe709462..c3e8510e12a 100644 --- a/yarn-project/aztec-rpc/src/aztec_rpc_http/aztec_rpc_http_server.ts +++ b/yarn-project/aztec-rpc/src/aztec_rpc_http/aztec_rpc_http_server.ts @@ -2,6 +2,7 @@ import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr, Point } from '@aztec/foundation/fields'; import { JsonRpcServer } from '@aztec/foundation/json-rpc/server'; import { + AztecRPC, CompleteAddress, ContractData, ContractDataAndBytecode, @@ -16,7 +17,7 @@ import { import { foundry } from 'viem/chains'; -import { AztecRPCServer, EthAddress } from '../index.js'; +import { EthAddress } from '../index.js'; export const localAnvil = foundry; @@ -24,7 +25,7 @@ export const localAnvil = foundry; * Wraps an instance of the Aztec RPC Server implementation to a JSON RPC HTTP interface. * @returns A new instance of the HTTP server. */ -export function getHttpRpcServer(aztecRpcServer: AztecRPCServer): JsonRpcServer { +export function getHttpRpcServer(aztecRpcServer: AztecRPC): JsonRpcServer { const generatedRpcServer = new JsonRpcServer( aztecRpcServer, { diff --git a/yarn-project/aztec-sandbox/package.json b/yarn-project/aztec-sandbox/package.json index 1639c088c87..92d59790e7a 100644 --- a/yarn-project/aztec-sandbox/package.json +++ b/yarn-project/aztec-sandbox/package.json @@ -2,7 +2,10 @@ "name": "@aztec/aztec-sandbox", "version": "0.0.0", "type": "module", - "exports": "./dest/index.js", + "exports": { + ".": "./dest/index.js", + "./http": "./dest/server.js" + }, "typedocOptions": { "entryPoints": [ "./src/index.ts" diff --git a/yarn-project/aztec-sandbox/src/index.ts b/yarn-project/aztec-sandbox/src/index.ts index 7edfbc5fdce..a4e07d4ebd2 100644 --- a/yarn-project/aztec-sandbox/src/index.ts +++ b/yarn-project/aztec-sandbox/src/index.ts @@ -1,17 +1,16 @@ import { AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '@aztec/aztec-node'; -import { createAztecRPCServer, getHttpRpcServer, getConfigEnvVars as getRpcConfigEnvVars } from '@aztec/aztec-rpc'; +import { createAztecRPCServer, getConfigEnvVars as getRpcConfigEnvVars } from '@aztec/aztec-rpc'; import { deployInitialSandboxAccounts } from '@aztec/aztec.js'; import { PrivateKey } from '@aztec/circuits.js'; import { deployL1Contracts } from '@aztec/ethereum'; import { createDebugLogger } from '@aztec/foundation/log'; import { retryUntil } from '@aztec/foundation/retry'; -import http from 'http'; import { HDAccount, createPublicClient, http as httpViemTransport } from 'viem'; import { mnemonicToAccount } from 'viem/accounts'; import { foundry } from 'viem/chains'; -import { createApiRouter } from './routes.js'; +import { startHttpRpcServer } from './server.js'; import { github, splash } from './splash.js'; const { SERVER_PORT = 8080, MNEMONIC = 'test test test test test test test test test test test junk' } = process.env; @@ -84,15 +83,7 @@ async function main() { process.once('SIGINT', shutdown); process.once('SIGTERM', shutdown); - const rpcServer = getHttpRpcServer(aztecRpcServer); - - const app = rpcServer.getApp(); - const apiRouter = createApiRouter(deployedL1Contracts); - app.use(apiRouter.routes()); - app.use(apiRouter.allowedMethods()); - - const httpServer = http.createServer(app.callback()); - httpServer.listen(SERVER_PORT); + startHttpRpcServer(aztecRpcServer, deployedL1Contracts, SERVER_PORT); logger.info(`Aztec JSON RPC listening on port ${SERVER_PORT}`); const accountStrings = [`Initial Accounts:\n\n`]; diff --git a/yarn-project/aztec-sandbox/src/server.ts b/yarn-project/aztec-sandbox/src/server.ts new file mode 100644 index 00000000000..7d8893e05c2 --- /dev/null +++ b/yarn-project/aztec-sandbox/src/server.ts @@ -0,0 +1,31 @@ +import { getHttpRpcServer } from '@aztec/aztec-rpc'; +import { DeployL1Contracts } from '@aztec/ethereum'; +import { AztecRPC } from '@aztec/types'; + +import http from 'http'; + +import { createApiRouter } from './routes.js'; + +/** + * Creates an http server that forwards calls to the rpc server and starts it on the given port. + * @param aztecRpcServer - RPC server that answers queries to the created HTTP server. + * @param deployedL1Contracts - Info on L1 deployed contracts. + * @param port - Port to listen in. + * @returns A running http server. + */ +export function startHttpRpcServer( + aztecRpcServer: AztecRPC, + deployedL1Contracts: DeployL1Contracts, + port: string | number, +): http.Server { + const rpcServer = getHttpRpcServer(aztecRpcServer); + + const app = rpcServer.getApp(); + const apiRouter = createApiRouter(deployedL1Contracts); + app.use(apiRouter.routes()); + app.use(apiRouter.allowedMethods()); + + const httpServer = http.createServer(app.callback()); + httpServer.listen(port); + return httpServer; +} diff --git a/yarn-project/end-to-end/package.json b/yarn-project/end-to-end/package.json index b7cde35303c..29cbedb5584 100644 --- a/yarn-project/end-to-end/package.json +++ b/yarn-project/end-to-end/package.json @@ -27,8 +27,10 @@ "@aztec/archiver": "workspace:^", "@aztec/aztec-node": "workspace:^", "@aztec/aztec-rpc": "workspace:^", + "@aztec/aztec-sandbox": "workspace:^", "@aztec/aztec.js": "workspace:^", "@aztec/circuits.js": "workspace:^", + "@aztec/cli": "workspace:^", "@aztec/ethereum": "workspace:^", "@aztec/foundation": "workspace:^", "@aztec/l1-artifacts": "workspace:^", @@ -51,11 +53,13 @@ "koa": "^2.14.2", "koa-static": "^5.0.0", "levelup": "^5.1.1", + "lodash.compact": "^3.0.1", "lodash.every": "^4.6.0", "lodash.times": "^4.3.2", "lodash.zip": "^4.2.0", "lodash.zipwith": "^4.2.0", "puppeteer": "^20.9.0", + "string-argv": "^0.3.2", "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "tslib": "^2.4.0", @@ -64,6 +68,7 @@ }, "devDependencies": { "@rushstack/eslint-patch": "^1.1.4", + "@types/lodash.compact": "^3.0.7", "concurrently": "^7.6.0" }, "files": [ diff --git a/yarn-project/end-to-end/src/e2e_cli.test.ts b/yarn-project/end-to-end/src/e2e_cli.test.ts new file mode 100644 index 00000000000..e793fc93c4e --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_cli.test.ts @@ -0,0 +1,74 @@ +import { AztecNodeService } from '@aztec/aztec-node'; +import { AztecAddress, 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 } from '@aztec/types'; + +import stringArgv from 'string-argv'; +import { format } from 'util'; + +import { setup } from './fixtures/utils.js'; + +const HTTP_PORT = 9009; + +// 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; + + // 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}`); + const log = (...args: any[]) => { + logs.push(format(...args)); + debug(...args); + }; + cli = getProgram(log, debug); + }); + + afterAll(async () => { + http.close(); + await aztecNode?.stop(); + await (aztecRpcServer as AztecRPCServer).stop(); + }); + + beforeEach(() => { + logs.splice(0); + }); + + // Run a command on the CLI + const run = (cmd: string) => + cli.parseAsync(stringArgv(cmd, 'node', 'dest/bin/index.js').concat(['--rpc-url', `http://localhost:${HTTP_PORT}`])); + + // 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; + } + }; + + it('creates an account', async () => { + const accountsBefore = await aztecRpcServer.getAccounts(); + await run(`create-account`); + const newAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(newAddress).toBeDefined(); + + const accountsAfter = await aztecRpcServer.getAccounts(); + const expectedAccounts = [...accountsBefore.map(a => a.address), AztecAddress.fromString(newAddress!)]; + expect(accountsAfter.map(a => a.address)).toEqual(expectedAccounts); + }); +}); diff --git a/yarn-project/end-to-end/tsconfig.json b/yarn-project/end-to-end/tsconfig.json index 3fac0d7c0ac..402777d34bd 100644 --- a/yarn-project/end-to-end/tsconfig.json +++ b/yarn-project/end-to-end/tsconfig.json @@ -15,12 +15,18 @@ { "path": "../aztec-rpc" }, + { + "path": "../aztec-sandbox" + }, { "path": "../aztec.js" }, { "path": "../circuits.js" }, + { + "path": "../aztec-cli" + }, { "path": "../ethereum" }, diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 75b450f85b3..6bcc989cd89 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -181,7 +181,7 @@ __metadata: languageName: unknown linkType: soft -"@aztec/aztec-sandbox@workspace:aztec-sandbox": +"@aztec/aztec-sandbox@workspace:^, @aztec/aztec-sandbox@workspace:aztec-sandbox": version: 0.0.0-use.local resolution: "@aztec/aztec-sandbox@workspace:aztec-sandbox" dependencies: @@ -310,7 +310,7 @@ __metadata: languageName: unknown linkType: soft -"@aztec/cli@workspace:aztec-cli": +"@aztec/cli@workspace:^, @aztec/cli@workspace:aztec-cli": version: 0.0.0-use.local resolution: "@aztec/cli@workspace:aztec-cli" dependencies: @@ -332,7 +332,7 @@ __metadata: typescript: ^5.0.4 viem: ^1.2.5 bin: - aztec-cli: ./dest/index.js + aztec-cli: ./dest/bin/index.js languageName: unknown linkType: soft @@ -351,8 +351,10 @@ __metadata: "@aztec/archiver": "workspace:^" "@aztec/aztec-node": "workspace:^" "@aztec/aztec-rpc": "workspace:^" + "@aztec/aztec-sandbox": "workspace:^" "@aztec/aztec.js": "workspace:^" "@aztec/circuits.js": "workspace:^" + "@aztec/cli": "workspace:^" "@aztec/ethereum": "workspace:^" "@aztec/foundation": "workspace:^" "@aztec/l1-artifacts": "workspace:^" @@ -367,6 +369,7 @@ __metadata: "@types/jest": ^29.5.0 "@types/koa-static": ^4.0.2 "@types/levelup": ^5.1.2 + "@types/lodash.compact": ^3.0.7 "@types/lodash.every": ^4.6.7 "@types/lodash.times": ^4.3.7 "@types/lodash.zip": ^4.2.7 @@ -377,11 +380,13 @@ __metadata: koa: ^2.14.2 koa-static: ^5.0.0 levelup: ^5.1.1 + lodash.compact: ^3.0.1 lodash.every: ^4.6.0 lodash.times: ^4.3.2 lodash.zip: ^4.2.0 lodash.zipwith: ^4.2.0 puppeteer: ^20.9.0 + string-argv: ^0.3.2 ts-jest: ^29.1.0 ts-node: ^10.9.1 tslib: ^2.4.0 @@ -11144,7 +11149,7 @@ __metadata: languageName: node linkType: hard -"string-argv@npm:^0.3.1": +"string-argv@npm:^0.3.1, string-argv@npm:^0.3.2": version: 0.3.2 resolution: "string-argv@npm:0.3.2" checksum: 8703ad3f3db0b2641ed2adbb15cf24d3945070d9a751f9e74a924966db9f325ac755169007233e8985a39a6a292f14d4fee20482989b89b96e473c4221508a0f