diff --git a/.circleci/config.yml b/.circleci/config.yml index e895fe9f8ed7..0ce618009bb0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -319,6 +319,17 @@ jobs: name: "Build and test" command: build aztec-rpc + aztec-sandbox: + machine: + image: ubuntu-2004:202010-01 + resource_class: large + steps: + - *checkout + - *setup_env + - run: + name: "Build and test" + command: build aztec-sandbox + circuits-js: machine: image: ubuntu-2004:202010-01 @@ -535,6 +546,20 @@ jobs: name: "Noop" command: echo Noop + deploy: + machine: + image: ubuntu-2004:202010-01 + resource_class: medium + steps: + - *checkout + - *setup_env + - run: + name: "deploy-sandbox" + working_directory: aztec-sandbox + command: | + deploy_ecr aztec-sandbox + deploy_dockerhub aztec-sandbox master + # Repeatable config for defining the workflow below. tag_regex: &tag_regex /v[0-9]+(\.[0-9]+)*(-[a-zA-Z-]+\.[0-9]+)?/ defaults: &defaults @@ -615,6 +640,7 @@ workflows: - types: *yarn_project - circuits-js: *yarn_project - rollup-provider: *yarn_project + - aztec-sandbox: *yarn_project - e2e-join: requires: @@ -667,3 +693,12 @@ workflows: - integration-archiver-l1-to-l2 - e2e-p2p <<: *defaults + + - deploy: + requires: + - e2e-end + - aztec-sandbox + filters: + branches: + only: master + <<: *defaults diff --git a/build-system b/build-system index 40e2f4b4b04b..5d3ed11e8017 160000 --- a/build-system +++ b/build-system @@ -1 +1 @@ -Subproject commit 40e2f4b4b04bdef9f66d909bc3b2c49b1444b42d +Subproject commit 5d3ed11e8017e3daf574e3b287e62c7a99c24bb4 diff --git a/build_manifest.json b/build_manifest.json index fbb69a31408e..124a8cdbfb0d 100644 --- a/build_manifest.json +++ b/build_manifest.json @@ -142,6 +142,21 @@ "types" ] }, + "aztec-sandbox": { + "buildDir": "yarn-project", + "projectDir": "yarn-project/aztec-sandbox", + "dockerfile": "aztec-sandbox/Dockerfile", + "rebuildPatterns": [ + "^yarn-project/aztec-sandbox/" + ], + "dependencies": [ + "aztec-node", + "aztec-rpc", + "aztec.js", + "ethereum", + "foundation" + ] + }, "aztec.js": { "buildDir": "yarn-project", "projectDir": "yarn-project/aztec.js", diff --git a/build_manifest.sh b/build_manifest.sh index 22ba06a341e8..7f7227117a29 100755 --- a/build_manifest.sh +++ b/build_manifest.sh @@ -14,6 +14,8 @@ PROJECTS=( circuits:circuits/cpp:./dockerfiles/Dockerfile.wasm-linux-clang:circuits-wasm-linux-clang + l1-contracts:l1-contracts yarn-project-base:yarn-project end-to-end:yarn-project + aztec-sandbox:yarn-project ) diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index b516b8096cb2..d0aed6daef9e 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -47,6 +47,11 @@ export class Archiver implements L2BlockSource, L2LogsSource, ContractDataSource */ private lastProcessedBlockNumber = 0n; + /** + * Use this to track logged block in order to avoid repeating the same message. + */ + private lastLoggedBlockNumber = 0n; + /** * Creates a new instance of the Archiver. * @param publicClient - A client for interacting with the Ethereum node. @@ -123,7 +128,11 @@ export class Archiver implements L2BlockSource, L2LogsSource, ContractDataSource private async sync(blockUntilSynced: boolean) { const currentBlockNumber = await this.publicClient.getBlockNumber(); if (currentBlockNumber <= this.lastProcessedBlockNumber) { - this.log(`No new blocks to process, current block number: ${currentBlockNumber}`); + // reducing logs, otherwise this gets triggered on every loop (1s) + if (currentBlockNumber !== this.lastLoggedBlockNumber) { + this.log(`No new blocks to process, current block number: ${currentBlockNumber}`); + this.lastLoggedBlockNumber = currentBlockNumber; + } return; } diff --git a/yarn-project/aztec-node/src/aztec-node/http-node.ts b/yarn-project/aztec-node/src/aztec-node/http-node.ts index e235edf25255..93d618195ed5 100644 --- a/yarn-project/aztec-node/src/aztec-node/http-node.ts +++ b/yarn-project/aztec-node/src/aztec-node/http-node.ts @@ -33,6 +33,7 @@ export function txToJson(tx: Tx) { return { data: tx.data?.toBuffer().toString('hex'), encryptedLogs: tx.encryptedLogs?.toBuffer().toString('hex'), + unencryptedLogs: tx.unencryptedLogs?.toBuffer().toString('hex'), proof: tx.proof?.toBuffer().toString('hex'), newContractPublicFunctions: tx.newContractPublicFunctions?.map(f => f.toBuffer().toString('hex')) ?? [], enqueuedPublicFunctions: tx.enqueuedPublicFunctionCalls?.map(f => f.toBuffer().toString('hex')) ?? [], @@ -49,10 +50,10 @@ export function txFromJson(json: any) { const encryptedLogs = TxL2Logs.fromBuffer(Buffer.from(json.encryptedLogs, 'hex')); const unencryptedLogs = TxL2Logs.fromBuffer(Buffer.from(json.unencryptedLogs, 'hex')); const proof = Buffer.from(json.proof, 'hex'); - const newContractPublicFunctions = json.newContractPublicFunctions + const newContractPublicFunctions = json.newContractPublicFunctions?.length ? json.newContractPublicFunctions.map((x: string) => EncodedContractFunction.fromBuffer(Buffer.from(x, 'hex'))) : []; - const enqueuedPublicFunctions = json.enqueuedPublicFunctions + const enqueuedPublicFunctions = json.enqueuedPublicFunctions?.length ? json.enqueuedPublicFunctions.map((x: string) => PublicCallRequest.fromBuffer(Buffer.from(x, 'hex'))) : []; return Tx.createTx( diff --git a/yarn-project/aztec-rpc/package.json b/yarn-project/aztec-rpc/package.json index 0b25e5b08005..5433c624add8 100644 --- a/yarn-project/aztec-rpc/package.json +++ b/yarn-project/aztec-rpc/package.json @@ -16,7 +16,8 @@ "clean": "rm -rf ./dest .tsbuildinfo", "formatting": "run -T prettier --check ./src && run -T eslint ./src", "formatting:fix": "run -T prettier -w ./src", - "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --passWithNoTests" + "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --passWithNoTests", + "start:http": "DEBUG='aztec:*' && node ./dest/aztec_rpc_http/aztec_rpc_http_server.js" }, "inherits": [ "../package.common.json" diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_client/aztec_rpc_client.ts b/yarn-project/aztec-rpc/src/aztec_rpc/aztec_rpc.ts similarity index 81% rename from yarn-project/aztec-rpc/src/aztec_rpc_client/aztec_rpc_client.ts rename to yarn-project/aztec-rpc/src/aztec_rpc/aztec_rpc.ts index bbcac17deea2..6ad0ffbf529a 100644 --- a/yarn-project/aztec-rpc/src/aztec_rpc_client/aztec_rpc_client.ts +++ b/yarn-project/aztec-rpc/src/aztec_rpc/aztec_rpc.ts @@ -1,7 +1,15 @@ import { AztecAddress, EthAddress, Fr } from '@aztec/circuits.js'; import { ContractAbi } from '@aztec/foundation/abi'; import { Point } from '@aztec/foundation/fields'; -import { ContractDeploymentTx, PartialContractAddress, Tx, TxHash } from '@aztec/types'; +import { + ContractData, + ContractDeploymentTx, + ContractPublicData, + L2BlockL2Logs, + PartialContractAddress, + Tx, + TxHash, +} from '@aztec/types'; import { TxReceipt } from '../tx/index.js'; import { CurveType, SignerType } from '../crypto/types.js'; @@ -25,12 +33,12 @@ export interface DeployedContract { } /** - * Represents an Aztec RPC client implementation. + * Represents an Aztec RPC implementation. * Provides functionality for all the operations needed to interact with the Aztec network, * including account management, contract deployment, transaction creation, and execution, * as well as storage and view functions for smart contracts. */ -export interface AztecRPCClient { +export interface AztecRPC { createSmartAccount( privKey?: Buffer, curve?: CurveType, @@ -66,4 +74,8 @@ export interface AztecRPCClient { getTxReceipt(txHash: TxHash): Promise; getStorageAt(contract: AztecAddress, storageSlot: Fr): Promise; viewTx(functionName: string, args: any[], to: AztecAddress, from?: AztecAddress): Promise; + getContractData(contractAddress: AztecAddress): Promise; + getContractInfo(contractAddress: AztecAddress): Promise; + getUnencryptedLogs(from: number, take: number): Promise; + getBlockNum(): Promise; } diff --git a/yarn-project/aztec-rpc/src/aztec_rpc/index.ts b/yarn-project/aztec-rpc/src/aztec_rpc/index.ts new file mode 100644 index 000000000000..1314dd7a93ca --- /dev/null +++ b/yarn-project/aztec-rpc/src/aztec_rpc/index.ts @@ -0,0 +1 @@ +export * from './aztec_rpc.js'; 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 new file mode 100644 index 000000000000..dd08f287f1cd --- /dev/null +++ b/yarn-project/aztec-rpc/src/aztec_rpc_http/aztec_rpc_http_server.ts @@ -0,0 +1,33 @@ +import { foundry } from 'viem/chains'; +import { Tx, TxHash, ContractDeploymentTx } from '@aztec/types'; +import { JsonRpcServer } from '@aztec/foundation/json-rpc'; +import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { Fr, Point } from '@aztec/foundation/fields'; +import { AztecNodeConfig, AztecNodeService } from '@aztec/aztec-node'; + +import { EthAddress, createAztecRPCServer } from '../index.js'; + +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 async function getHttpRpcServer(nodeConfig: AztecNodeConfig): Promise { + const aztecNode = await AztecNodeService.createAndSync(nodeConfig); + const aztecRpcServer = await createAztecRPCServer(aztecNode); + const generatedRpcServer = new JsonRpcServer( + aztecRpcServer, + { + AztecAddress, + TxHash, + EthAddress, + Point, + Fr, + }, + { Tx, ContractDeploymentTx }, + false, + ['start', 'stop'], + ); + return generatedRpcServer; +} diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_http/index.ts b/yarn-project/aztec-rpc/src/aztec_rpc_http/index.ts new file mode 100644 index 000000000000..939d0ac64c84 --- /dev/null +++ b/yarn-project/aztec-rpc/src/aztec_rpc_http/index.ts @@ -0,0 +1 @@ +export * from './aztec_rpc_http_server.js'; diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts b/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts index 5f46336c99fd..57cd331363f7 100644 --- a/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts +++ b/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts @@ -14,8 +14,11 @@ import { createDebugLogger } from '@aztec/foundation/log'; import { KeyStore, PublicKey } from '@aztec/key-store'; import { SchnorrAccountContractAbi } from '@aztec/noir-contracts/examples'; import { + ContractData, ContractDeploymentTx, + ContractPublicData, ExecutionRequest, + L2BlockL2Logs, PartialContractAddress, Tx, TxExecutionRequest, @@ -24,7 +27,7 @@ import { import { AccountContract } from '../account_impl/account_contract.js'; import { AccountImplementation } from '../account_impl/index.js'; import { AccountState } from '../account_state/account_state.js'; -import { AztecRPCClient, DeployedContract } from '../aztec_rpc_client/index.js'; +import { AztecRPC, DeployedContract } from '../aztec_rpc/index.js'; import { ContractDao, toContractDao } from '../contract_database/index.js'; import { ContractTree } from '../contract_tree/index.js'; import { Database, TxDao } from '../database/index.js'; @@ -37,7 +40,7 @@ import { CurveType, SignerType, createCurve, createSigner } from '../crypto/type /** * A remote Aztec RPC Client implementation. */ -export class AztecRPCServer implements AztecRPCClient { +export class AztecRPCServer implements AztecRPC { private synchroniser: Synchroniser; constructor( @@ -95,7 +98,7 @@ export class AztecRPCServer implements AztecRPCClient { const contractAddressSalt = Fr.random(); const args: any[] = []; - const { txRequest, contract, partialContractAddress } = await this.prepareDeploy( + const { txRequest, contract, partialContractAddress } = await this.#prepareDeploy( abi, args, portalContract, @@ -103,7 +106,7 @@ export class AztecRPCServer implements AztecRPCClient { pubKey, ); - const account = await this.initAccountState( + const account = await this.#initAccountState( pubKey, contract.address, partialContractAddress, @@ -144,7 +147,7 @@ export class AztecRPCServer implements AztecRPCClient { const accountCurve = await createCurve(curve); const accountSigner = await createSigner(signer); const pubKey = this.keyStore.addAccount(accountCurve, accountSigner, privKey); - await this.initAccountState(pubKey, address, partialContractAddress, accountCurve, accountSigner, abi); + await this.#initAccountState(pubKey, address, partialContractAddress, accountCurve, accountSigner, abi); return address; } @@ -179,7 +182,7 @@ export class AztecRPCServer implements AztecRPCClient { * @returns A Promise resolving to the Point instance representing the public key. */ public getAccountPublicKey(address: AztecAddress): Promise { - const account = this.ensureAccount(address); + const account = this.#ensureAccount(address); return Promise.resolve(account.getPublicKey()); } @@ -225,10 +228,10 @@ export class AztecRPCServer implements AztecRPCClient { contractAddressSalt = Fr.random(), from?: AztecAddress, ) { - const account = this.ensureAccountOrDefault(from); + const account = this.#ensureAccountOrDefault(from); const pubKey = account.getPublicKey(); - const { txRequest, contract, partialContractAddress } = await this.prepareDeploy( + const { txRequest, contract, partialContractAddress } = await this.#prepareDeploy( abi, args, portalContract, @@ -245,7 +248,7 @@ export class AztecRPCServer implements AztecRPCClient { return new ContractDeploymentTx(tx, partialContractAddress); } - private async prepareDeploy( + async #prepareDeploy( abi: ContractAbi, args: any[], portalContract: EthAddress, @@ -305,11 +308,11 @@ export class AztecRPCServer implements AztecRPCClient { * @returns A Tx ready to send to the p2p pool for execution. */ public async createTx(functionName: string, args: any[], to: AztecAddress, optionalFromAddress?: AztecAddress) { - const account = this.ensureAccountOrDefault(optionalFromAddress); + const account = this.#ensureAccountOrDefault(optionalFromAddress); const accountContract = await this.db.getContract(account.getAddress()); - const entrypoint: AccountImplementation = this.getAccountImplementation(account, accountContract); + const entrypoint: AccountImplementation = this.#getAccountImplementation(account, accountContract); - const executionRequest = await this.getExecutionRequest(account, functionName, args, to); + const executionRequest = await this.#getExecutionRequest(account, functionName, args, to); const txContext = TxContext.empty(await this.node.getChainId(), await this.node.getVersion()); const authedTxRequest = await entrypoint.createAuthenticatedTxRequest([executionRequest], txContext); @@ -356,7 +359,7 @@ export class AztecRPCServer implements AztecRPCClient { } // TODO: Store the kind of account in account state - private getAccountImplementation(accountState: AccountState, contract: ContractDao | undefined) { + #getAccountImplementation(accountState: AccountState, contract: ContractDao | undefined) { const address = accountState.getAddress(); const pubKey = accountState.getPublicKey(); const partialContractAddress = accountState.getPartialContractAddress(); @@ -395,8 +398,8 @@ export class AztecRPCServer implements AztecRPCClient { * @returns The result of the view function call, structured based on the function ABI. */ public async viewTx(functionName: string, args: any[], to: AztecAddress, from?: AztecAddress) { - const account = this.ensureAccountOrDefault(from); - const txRequest = await this.getExecutionRequest(account, functionName, args, to); + const account = this.#ensureAccountOrDefault(from); + const txRequest = await this.#getExecutionRequest(account, functionName, args, to); const executionResult = await account.simulateUnconstrained(txRequest); @@ -457,6 +460,76 @@ export class AztecRPCServer implements AztecRPCClient { }; } + /** + * Get latest L2 block number. + * @returns The latest block number. + */ + async getBlockNum(): Promise { + return await this.node.getBlockHeight(); + } + + /** + * Lookup the L2 contract data for this contract. + * Contains the ethereum portal address and bytecode. + * @param contractAddress - The contract data address. + * @returns The complete contract data including portal address & bytecode (if we didn't throw an error). + */ + public async getContractData(contractAddress: AztecAddress): Promise { + return await this.node.getContractData(contractAddress); + } + + /** + * Lookup the L2 contract info for this contract. + * Contains the ethereum portal address . + * @param contractAddress - The contract data address. + * @returns The contract's address & portal address. + */ + public async getContractInfo(contractAddress: AztecAddress): Promise { + return await this.node.getContractInfo(contractAddress); + } + + /** + * Gets L2 block unencrypted logs. + * @param from - Number of the L2 block to which corresponds the first unencrypted logs to be returned. + * @param take - The number of unencrypted logs to return. + * @returns The requested unencrypted logs. + */ + public async getUnencryptedLogs(from: number, take: number): Promise { + return await this.node.getUnencryptedLogs(from, take); + } + + async #getExecutionRequest( + account: AccountState, + functionName: string, + args: any[], + to: AztecAddress, + ): Promise { + const contract = await this.db.getContract(to); + if (!contract) { + throw new Error('Unknown contract.'); + } + + const functionDao = contract.functions.find(f => f.name === functionName); + if (!functionDao) { + throw new Error('Unknown function.'); + } + + const flatArgs = encodeArguments(functionDao, args); + + const functionData = new FunctionData( + functionDao.selector, + functionDao.functionType === FunctionType.SECRET, + false, + ); + + return { + args: flatArgs, + from: account.getAddress(), + functionData, + to, + }; + } + /** * Initializes the account state for a given address. * It retrieves the private key from the key store and adds the account to the synchroniser. @@ -469,7 +542,7 @@ export class AztecRPCServer implements AztecRPCClient { * @param signer - The signer to be used for transaction signing. * @param abi - Implementation of the account contract backing the account. */ - private async initAccountState( + async #initAccountState( pubKey: PublicKey, address: AztecAddress, partialContractAddress: PartialContractAddress, @@ -499,13 +572,13 @@ export class AztecRPCServer implements AztecRPCClient { * @param account - (Optional) Address of the account to ensure its existence. * @returns The ensured account instance. */ - private ensureAccountOrDefault(account?: AztecAddress) { + #ensureAccountOrDefault(account?: AztecAddress) { const address = account || this.synchroniser.getAccounts()[0]?.getAddress(); if (!address) { throw new Error('No accounts available in the key store.'); } - return this.ensureAccount(address); + return this.#ensureAccount(address); } /** @@ -516,7 +589,7 @@ export class AztecRPCServer implements AztecRPCClient { * @returns The account state associated with the given address. * @throws If the account is unknown or not found in the synchroniser. */ - private ensureAccount(account: AztecAddress) { + #ensureAccount(account: AztecAddress) { const accountState = this.synchroniser.getAccount(account); if (!accountState) { throw new Error(`Unknown account: ${account.toShortString()}.`); diff --git a/yarn-project/aztec-rpc/src/index.ts b/yarn-project/aztec-rpc/src/index.ts index 556d40ff2641..c334cd7e0b52 100644 --- a/yarn-project/aztec-rpc/src/index.ts +++ b/yarn-project/aztec-rpc/src/index.ts @@ -1,6 +1,7 @@ export * from './abi_coder/index.js'; -export * from './aztec_rpc_client/index.js'; +export * from './aztec_rpc/index.js'; export * from './aztec_rpc_server/index.js'; +export * from './aztec_rpc_http/index.js'; export * from './tx/index.js'; export * from './crypto/types.js'; diff --git a/yarn-project/aztec-sandbox/.eslintrc.cjs b/yarn-project/aztec-sandbox/.eslintrc.cjs new file mode 100644 index 000000000000..e659927475c0 --- /dev/null +++ b/yarn-project/aztec-sandbox/.eslintrc.cjs @@ -0,0 +1 @@ +module.exports = require('@aztec/foundation/eslint'); diff --git a/yarn-project/aztec-sandbox/Dockerfile b/yarn-project/aztec-sandbox/Dockerfile new file mode 100644 index 000000000000..93c54d5f39b6 --- /dev/null +++ b/yarn-project/aztec-sandbox/Dockerfile @@ -0,0 +1,18 @@ +FROM 278380418400.dkr.ecr.eu-west-2.amazonaws.com/yarn-project-base AS builder + +COPY . . + +WORKDIR /usr/src/yarn-project/aztec-sandbox +RUN yarn build +# && yarn formatting && yarn test + +# Prune dev dependencies. See comment in base image. +RUN yarn cache clean +RUN yarn workspaces focus --production > /dev/null + +FROM node:18-alpine +COPY --from=builder /usr/src/ /usr/src/ +WORKDIR /usr/src/yarn-project/aztec-sandbox +ENTRYPOINT ["yarn"] +CMD [ "start" ] +EXPOSE 8080 \ No newline at end of file diff --git a/yarn-project/aztec-sandbox/README.md b/yarn-project/aztec-sandbox/README.md new file mode 100644 index 000000000000..8b61e013fde2 --- /dev/null +++ b/yarn-project/aztec-sandbox/README.md @@ -0,0 +1 @@ +# aztec-sandbox diff --git a/yarn-project/aztec-sandbox/docker-compose-fork.yml b/yarn-project/aztec-sandbox/docker-compose-fork.yml new file mode 100644 index 000000000000..399ea2bb8b0c --- /dev/null +++ b/yarn-project/aztec-sandbox/docker-compose-fork.yml @@ -0,0 +1,21 @@ +version: '3' +services: + fork: + image: ghcr.io/foundry-rs/foundry:nightly-a44aa13cfc23491ba32aaedc093e9488c1a6db43 + entrypoint: 'anvil --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c --host 0.0.0.0 --fork-block-number 17514288 --chain-id 31337' + ports: + - '8545:8545' + + rpc-server: + image: aztecprotocol/aztec-sandbox:master + ports: + - '8080:8080' + environment: + DEBUG: 'aztec:*' + ETHEREUM_HOST: http://fork:8545 + CHAIN_ID: 31337 + ARCHIVER_POLLING_INTERVAL: 500 + P2P_CHECK_INTERVAL: 500 + SEQ_TX_POLLING_INTERVAL: 500 + WS_CHECK_INTERVAL: 500 + SEARCH_START_BLOCK: 17514288 diff --git a/yarn-project/aztec-sandbox/docker-compose.yml b/yarn-project/aztec-sandbox/docker-compose.yml new file mode 100644 index 000000000000..20c717278e9a --- /dev/null +++ b/yarn-project/aztec-sandbox/docker-compose.yml @@ -0,0 +1,20 @@ +version: '3' +services: + fork: + image: ghcr.io/foundry-rs/foundry:nightly-a44aa13cfc23491ba32aaedc093e9488c1a6db43 + entrypoint: 'anvil -p 8545 --host 0.0.0.0 --chain-id 31337' + ports: + - '8545:8545' + + rpc-server: + image: aztecprotocol/aztec-sandbox:master + ports: + - '8080:8080' + environment: + DEBUG: 'aztec:*' + ETHEREUM_HOST: http://fork:8545 + CHAIN_ID: 31337 + ARCHIVER_POLLING_INTERVAL: 500 + P2P_CHECK_INTERVAL: 500 + SEQ_TX_POLLING_INTERVAL: 500 + WS_CHECK_INTERVAL: 500 diff --git a/yarn-project/aztec-sandbox/package.json b/yarn-project/aztec-sandbox/package.json new file mode 100644 index 000000000000..df0bba648e2f --- /dev/null +++ b/yarn-project/aztec-sandbox/package.json @@ -0,0 +1,59 @@ +{ + "name": "@aztec/aztec-sandbox", + "version": "0.0.0", + "type": "module", + "exports": "./dest/index.js", + "typedoc": { + "entryPoint": "./src/index.ts", + "displayName": "Sandbox", + "tsconfig": "./tsconfig.json" + }, + "scripts": { + "prepare": "node ../yarn-project-base/scripts/update_build_manifest.mjs package.json", + "build": "yarn clean && tsc -b", + "start": "DEBUG='aztec:*' node ./dest", + "clean": "rm -rf ./dest .tsbuildinfo", + "formatting": "run -T prettier --check ./src && run -T eslint ./src", + "formatting:fix": "run -T prettier -w ./src", + "prepare:check": "node ../yarn-project-base/scripts/update_build_manifest.mjs package.json --check", + "build:dev": "tsc -b --watch", + "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --passWithNoTests", + "run:example:token": "DEBUG='aztec:*' node ./dest/examples/zk_contract_deployment.js", + "run:example:uniswap": "DEBUG='aztec:*' node ./dest/examples/uniswap_trade_on_l1_from_l2.js" + }, + "inherits": [ + "../package.common.json" + ], + "dependencies": { + "@aztec/aztec-node": "workspace:^", + "@aztec/aztec-rpc": "workspace:^", + "@aztec/aztec.js": "workspace:^", + "@aztec/ethereum": "workspace:^", + "@aztec/foundation": "workspace:^", + "abitype": "^0.8.11", + "koa-router": "^12.0.0", + "viem": "^0.3.14" + }, + "files": [ + "dest", + "src", + "!*.test.*" + ], + "types": "./dest/index.d.ts", + "devDependencies": { + "@jest/globals": "^29.5.0", + "@types/jest": "^29.5.0", + "jest": "^29.5.0", + "ts-jest": "^29.1.0", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" + }, + "jest": { + "preset": "ts-jest/presets/default-esm", + "moduleNameMapper": { + "^(\\.{1,2}/.*)\\.js$": "$1" + }, + "testRegex": "./src/.*\\.test\\.ts$", + "rootDir": "./src" + } +} diff --git a/yarn-project/aztec-sandbox/src/examples/uniswap_trade_on_l1_from_l2.ts b/yarn-project/aztec-sandbox/src/examples/uniswap_trade_on_l1_from_l2.ts new file mode 100644 index 000000000000..83c7fa24738c --- /dev/null +++ b/yarn-project/aztec-sandbox/src/examples/uniswap_trade_on_l1_from_l2.ts @@ -0,0 +1,361 @@ +import { + AztecRPC, + Contract, + ContractDeployer, + TxStatus, + computeMessageSecretHash, + createAccounts, + createAztecRpcClient, + getL1ContractAddresses, + pointToPublicKey, + AztecAddress, + EthAddress, + Fr, +} from '@aztec/aztec.js'; +import { createDebugLogger } from '@aztec/foundation/log'; +import { UniswapContractAbi } from '@aztec/noir-contracts/examples'; +import { createPublicClient, createWalletClient, getContract, http, parseEther } from 'viem'; +import { mnemonicToAccount } from 'viem/accounts'; +import { foundry } from 'viem/chains'; +import { delay, deployAndInitializeNonNativeL2TokenContracts, deployL1Contract } from './util.js'; +import { UniswapPortalAbi, UniswapPortalBytecode } from '@aztec/l1-artifacts'; + +/** + * Type representation of a Public key's coordinates. + */ +type PublicKey = { + /** Public key X coord */ + x: bigint; + /** Public key Y coord */ + y: bigint; +}; + +const logger = createDebugLogger('aztec:http-rpc-client'); + +// const { SEARCH_START_BLOCK } = process.env; + +export const MNEMONIC = 'test test test test test test test test test test test junk'; + +const INITIAL_BALANCE = 333n; +const wethAmountToBridge = parseEther('1'); + +const WETH9_ADDRESS = EthAddress.fromString('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); +const DAI_ADDRESS = EthAddress.fromString('0x6B175474E89094C44Da98b954EedeAC495271d0F'); + +const EXPECTED_FORKED_BLOCK = 17514288; + +// if (parseInt(SEARCH_START_BLOCK || '') !== EXPECTED_FORKED_BLOCK) { +// throw Error(`You need to set env variable SEARCH_START_BLOCK to ${EXPECTED_FORKED_BLOCK}`); +// } + +// export const privateKey = Buffer.from('ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', 'hex'); + +const aztecRpcUrl = 'http://localhost:8080'; +const ethRpcUrl = 'http://localhost:8545'; + +const hdAccount = mnemonicToAccount(MNEMONIC); +const privateKey = Buffer.from(hdAccount.getHdKey().privateKey!); + +const walletClient = createWalletClient({ + account: hdAccount, + chain: foundry, + transport: http(ethRpcUrl), +}); +const publicClient = createPublicClient({ + chain: foundry, + transport: http(ethRpcUrl), +}); + +if (Number(await publicClient.getBlockNumber()) < EXPECTED_FORKED_BLOCK) { + throw new Error('This test must be run on a fork of mainnet with the expected fork block'); +} + +const ethAccount = EthAddress.fromString((await walletClient.getAddresses())[0]); + +const aztecRpcClient = createAztecRpcClient(aztecRpcUrl); + +/** + * Deploys all l1 / l2 contracts + * @param ownerPub - Public key of deployer. + */ +async function deployAllContracts(ownerPub: PublicKey) { + const l1ContractsAddresses = await getL1ContractAddresses(aztecRpcUrl); + logger('Deploying DAI Portal, initializing and deploying l2 contract...'); + const daiContracts = await deployAndInitializeNonNativeL2TokenContracts( + aztecRpcClient, + walletClient, + publicClient, + l1ContractsAddresses!.registry, + INITIAL_BALANCE, + ownerPub, + DAI_ADDRESS, + ); + const daiL2Contract = daiContracts.l2Contract; + const daiContract = daiContracts.underlyingERC20; + const daiTokenPortalAddress = daiContracts.tokenPortalAddress; + + logger('Deploying WETH Portal, initializing and deploying l2 contract...'); + const wethContracts = await deployAndInitializeNonNativeL2TokenContracts( + aztecRpcClient, + walletClient, + publicClient, + l1ContractsAddresses!.registry, + INITIAL_BALANCE, + ownerPub, + WETH9_ADDRESS, + ); + const wethL2Contract = wethContracts.l2Contract; + const wethContract = wethContracts.underlyingERC20; + const wethTokenPortal = wethContracts.tokenPortal; + const wethTokenPortalAddress = wethContracts.tokenPortalAddress; + + logger('Deploy Uniswap portal on L1 and L2...'); + const uniswapPortalAddress = await deployL1Contract( + walletClient, + publicClient, + UniswapPortalAbi, + UniswapPortalBytecode, + ); + const uniswapPortal = getContract({ + address: uniswapPortalAddress.toString(), + abi: UniswapPortalAbi, + walletClient, + publicClient, + }); + + // deploy l2 uniswap contract and attach to portal + const deployer = new ContractDeployer(UniswapContractAbi, aztecRpcClient); + const tx = deployer.deploy().send({ portalContract: uniswapPortalAddress }); + await tx.isMined(0, 0.5); + const receipt = await tx.getReceipt(); + const uniswapL2Contract = new Contract(receipt.contractAddress!, UniswapContractAbi, aztecRpcClient); + await uniswapL2Contract.attach(uniswapPortalAddress); + + await uniswapPortal.write.initialize( + [l1ContractsAddresses!.registry.toString(), uniswapL2Contract.address.toString()], + {} as any, + ); + + return { + daiL2Contract, + daiContract, + daiTokenPortalAddress, + wethL2Contract, + wethContract, + wethTokenPortal, + wethTokenPortalAddress, + uniswapL2Contract, + uniswapPortal, + uniswapPortalAddress, + }; +} + +const getL2BalanceOf = async (aztecRpcClient: AztecRPC, owner: AztecAddress, l2Contract: any) => { + const ownerPublicKey = await aztecRpcClient.getAccountPublicKey(owner); + const [balance] = await l2Contract.methods.getBalance(pointToPublicKey(ownerPublicKey)).view({ from: owner }); + return balance; +}; + +const logExpectedBalanceOnL2 = async ( + aztecRpcClient: AztecRPC, + owner: AztecAddress, + expectedBalance: bigint, + l2Contract: any, +) => { + const balance = await getL2BalanceOf(aztecRpcClient, owner, l2Contract); + logger(`Account ${owner} balance: ${balance}. Expected to be: ${expectedBalance}`); +}; + +const transferWethOnL2 = async ( + aztecRpcClient: AztecRPC, + wethL2Contract: Contract, + ownerAddress: AztecAddress, + receiver: AztecAddress, + transferAmount: bigint, +) => { + const transferTx = wethL2Contract.methods + .transfer( + transferAmount, + pointToPublicKey(await aztecRpcClient.getAccountPublicKey(ownerAddress)), + pointToPublicKey(await aztecRpcClient.getAccountPublicKey(receiver)), + ) + .send({ from: ownerAddress }); + await transferTx.isMined(0, 0.5); + const transferReceipt = await transferTx.getReceipt(); + // expect(transferReceipt.status).toBe(TxStatus.MINED); + logger(`WETH to L2 Transfer Receipt status: ${transferReceipt.status} should be ${TxStatus.MINED}`); +}; + +/** + * main fn + */ +async function main() { + logger('Running L1/L2 messaging test on HTTP interface.'); + + const accounts = await createAccounts(aztecRpcClient, privateKey!, 2); + const [[owner], [receiver]] = accounts; + const ownerPub = pointToPublicKey(await aztecRpcClient.getAccountPublicKey(owner)); + + const result = await deployAllContracts(ownerPub); + const { + daiL2Contract, + daiContract, + daiTokenPortalAddress, + wethL2Contract, + wethContract, + wethTokenPortal, + wethTokenPortalAddress, + uniswapL2Contract, + uniswapPortal, + uniswapPortalAddress, + } = result; + + // Give me some WETH so I can deposit to L2 and do the swap... + logger('Getting some weth'); + await walletClient.sendTransaction({ to: WETH9_ADDRESS.toString(), value: parseEther('1') }); + + const meBeforeBalance = await wethContract.read.balanceOf([ethAccount.toString()]); + // 1. Approve weth to be bridged + await wethContract.write.approve([wethTokenPortalAddress.toString(), wethAmountToBridge], {} as any); + + // 2. Deposit weth into the portal and move to L2 + // generate secret + const secret = Fr.random(); + const secretHash = await computeMessageSecretHash(secret); + const secretString = `0x${secretHash.toBuffer().toString('hex')}` as `0x${string}`; + const deadline = 2 ** 32 - 1; // max uint32 - 1 + logger('Sending messages to L1 portal'); + const args = [owner.toString(), wethAmountToBridge, deadline, secretString, ethAccount.toString()] as const; + const { result: messageKeyHex } = await wethTokenPortal.simulate.depositToAztec(args, { + account: ethAccount.toString(), + } as any); + await wethTokenPortal.write.depositToAztec(args, {} as any); + // expect(await wethContract.read.balanceOf([ethAccount.toString()])).toBe(meBeforeBalance - wethAmountToBridge); + + const currentBalance = await wethContract.read.balanceOf([ethAccount.toString()]); + logger(`Current Balance: ${currentBalance}. Should be: ${meBeforeBalance - wethAmountToBridge}`); + const messageKey = Fr.fromString(messageKeyHex); + + // Wait for the archiver to process the message + await delay(5000); + // send a transfer tx to force through rollup with the message included + const transferAmount = 1n; + await transferWethOnL2(aztecRpcClient, wethL2Contract, owner, receiver, transferAmount); + + // 3. Claim WETH on L2 + logger('Minting weth on L2'); + // Call the mint tokens function on the noir contract + const consumptionTx = wethL2Contract.methods + .mint(wethAmountToBridge, ownerPub, owner, messageKey, secret, ethAccount.toField()) + .send({ from: owner }); + await consumptionTx.isMined(0, 0.5); + const consumptionReceipt = await consumptionTx.getReceipt(); + // expect(consumptionReceipt.status).toBe(TxStatus.MINED); + logger(`Consumption Receipt status: ${consumptionReceipt.status} should be ${TxStatus.MINED}`); + // await expectBalanceOnL2(ownerAddress, wethAmountToBridge + initialBalance - transferAmount, wethL2Contract); + + // Store balances + const wethBalanceBeforeSwap = await getL2BalanceOf(aztecRpcClient, owner, wethL2Contract); + const daiBalanceBeforeSwap = await getL2BalanceOf(aztecRpcClient, owner, daiL2Contract); + + // 4. Send L2 to L1 message to withdraw funds and another message to swap assets. + logger('Send L2 tx to withdraw WETH to uniswap portal and send message to swap assets on L1'); + // recipient is the uniswap portal + const selector = Fr.fromBuffer(wethL2Contract.methods.withdraw.selector); + const minimumOutputAmount = 0n; + + const withdrawTx = uniswapL2Contract.methods + .swap( + selector, + wethL2Contract.address.toField(), + wethTokenPortalAddress.toField(), + wethAmountToBridge, + new Fr(3000), + daiL2Contract.address.toField(), + daiTokenPortalAddress.toField(), + new Fr(minimumOutputAmount), + ownerPub, + owner, + secretHash, + new Fr(2 ** 32 - 1), + ethAccount.toField(), + uniswapPortalAddress, + ethAccount.toField(), + ) + .send({ from: owner }); + await withdrawTx.isMined(0, 0.5); + const withdrawReceipt = await withdrawTx.getReceipt(); + // expect(withdrawReceipt.status).toBe(TxStatus.MINED); + logger(`Withdraw receipt status: ${withdrawReceipt.status} should be ${TxStatus.MINED}`); + + // check weth balance of owner on L2 (we first briedged `wethAmountToBridge` into L2 and now withdrew it!) + await logExpectedBalanceOnL2(aztecRpcClient, owner, INITIAL_BALANCE - transferAmount, wethL2Contract); + + // 5. Consume L2 to L1 message by calling uniswapPortal.swap() + logger('Execute withdraw and swap on the uniswapPortal!'); + const daiBalanceOfPortalBefore = await daiContract.read.balanceOf([daiTokenPortalAddress.toString()]); + logger(`DAI balance of portal: ${daiBalanceOfPortalBefore}`); + const swapArgs = [ + wethTokenPortalAddress.toString(), + wethAmountToBridge, + 3000, + daiTokenPortalAddress.toString(), + minimumOutputAmount, + owner.toString(), + secretString, + deadline, + ethAccount.toString(), + true, + ] as const; + const { result: depositDaiMessageKeyHex } = await uniswapPortal.simulate.swap(swapArgs, { + account: ethAccount.toString(), + } as any); + // this should also insert a message into the inbox. + await uniswapPortal.write.swap(swapArgs, {} as any); + const depositDaiMessageKey = Fr.fromString(depositDaiMessageKeyHex); + // weth was swapped to dai and send to portal + const daiBalanceOfPortalAfter = await daiContract.read.balanceOf([daiTokenPortalAddress.toString()]); + // expect(daiBalanceOfPortalAfter).toBeGreaterThan(daiBalanceOfPortalBefore); + logger( + `DAI balance in Portal: ${daiBalanceOfPortalAfter} should be bigger than ${daiBalanceOfPortalBefore}. ${ + daiBalanceOfPortalAfter > daiBalanceOfPortalBefore + }`, + ); + const daiAmountToBridge = daiBalanceOfPortalAfter - daiBalanceOfPortalBefore; + + // Wait for the archiver to process the message + await delay(5000); + // send a transfer tx to force through rollup with the message included + await transferWethOnL2(aztecRpcClient, wethL2Contract, owner, receiver, transferAmount); + + // 6. claim dai on L2 + logger('Consuming messages to mint dai on L2'); + // Call the mint tokens function on the noir contract + const daiMintTx = daiL2Contract.methods + .mint(daiAmountToBridge, ownerPub, owner, depositDaiMessageKey, secret, ethAccount.toField()) + .send({ from: owner }); + await daiMintTx.isMined(0, 0.5); + const daiMintTxReceipt = await daiMintTx.getReceipt(); + // expect(daiMintTxReceipt.status).toBe(TxStatus.MINED); + logger(`DAI mint TX status: ${daiMintTxReceipt.status} should be ${TxStatus.MINED}`); + await logExpectedBalanceOnL2(aztecRpcClient, owner, INITIAL_BALANCE + BigInt(daiAmountToBridge), daiL2Contract); + + const wethBalanceAfterSwap = await getL2BalanceOf(aztecRpcClient, owner, wethL2Contract); + const daiBalanceAfterSwap = await getL2BalanceOf(aztecRpcClient, owner, daiL2Contract); + + logger('WETH balance before swap: ', wethBalanceBeforeSwap.toString()); + logger('DAI balance before swap : ', daiBalanceBeforeSwap.toString()); + logger('***** 🧚‍♀️ SWAP L2 assets on L1 Uniswap 🧚‍♀️ *****'); + logger('WETH balance after swap : ', wethBalanceAfterSwap.toString()); + logger('DAI balance after swap : ', daiBalanceAfterSwap.toString()); +} + +main() + .then(() => { + logger('Finished running successfuly.'); + process.exit(0); + }) + .catch(err => { + logger('Error in main fn: ', err); + process.exit(1); + }); diff --git a/yarn-project/aztec-sandbox/src/examples/util.ts b/yarn-project/aztec-sandbox/src/examples/util.ts new file mode 100644 index 000000000000..f6505d85320c --- /dev/null +++ b/yarn-project/aztec-sandbox/src/examples/util.ts @@ -0,0 +1,103 @@ +import { Account, Chain, Hex, HttpTransport, PublicClient, WalletClient, getContract } from 'viem'; +import type { Abi, Narrow } from 'abitype'; +import { PortalERC20Abi, PortalERC20Bytecode, TokenPortalAbi, TokenPortalBytecode } from '@aztec/l1-artifacts'; +import { NonNativeTokenContractAbi } from '@aztec/noir-contracts/examples'; +import { Contract, ContractDeployer, EthAddress, AztecRPC } from '@aztec/aztec.js'; + +/** + * Deploy L1 token and portal, initialize portal, deploy a non native l2 token contract and attach is to the portal. + * @param aztecRpcServer - the aztec rpc server instance + * @param walletClient - A viem WalletClient. + * @param publicClient - A viem PublicClient. + * @param rollupRegistryAddress - address of rollup registry to pass to initialize the token portal + * @param initialBalance - initial balance of the owner of the L2 contract + * @param owner - owner of the L2 contract + * @param underlyingERC20Address - address of the underlying ERC20 contract to use (if noone supplied, it deploys one) + * @returns l2 contract instance, token portal instance, token portal address and the underlying ERC20 instance + */ +export async function deployAndInitializeNonNativeL2TokenContracts( + aztecRpcServer: AztecRPC, + walletClient: WalletClient, + publicClient: PublicClient, + rollupRegistryAddress: EthAddress, + initialBalance = 0n, + owner = { x: 0n, y: 0n }, + underlyingERC20Address?: EthAddress, +) { + // deploy underlying contract if no address supplied + if (!underlyingERC20Address) { + underlyingERC20Address = await deployL1Contract(walletClient, publicClient, PortalERC20Abi, PortalERC20Bytecode); + } + const underlyingERC20: any = getContract({ + address: underlyingERC20Address.toString(), + abi: PortalERC20Abi, + walletClient, + publicClient, + }); + + // deploy the token portal + const tokenPortalAddress = await deployL1Contract(walletClient, publicClient, TokenPortalAbi, TokenPortalBytecode); + const tokenPortal: any = getContract({ + address: tokenPortalAddress.toString(), + abi: TokenPortalAbi, + walletClient, + publicClient, + }); + + // deploy l2 contract and attach to portal + const deployer = new ContractDeployer(NonNativeTokenContractAbi, aztecRpcServer); + const tx = deployer.deploy(initialBalance, owner).send({ + portalContract: tokenPortalAddress, + }); + await tx.isMined(0, 0.1); + const receipt = await tx.getReceipt(); + const l2Contract = new Contract(receipt.contractAddress!, NonNativeTokenContractAbi, aztecRpcServer); + await l2Contract.attach(tokenPortalAddress); + const l2TokenAddress = l2Contract.address.toString() as `0x${string}`; + + // initialize portal + await tokenPortal.write.initialize( + [rollupRegistryAddress.toString(), underlyingERC20Address.toString(), l2TokenAddress], + {} as any, + ); + return { l2Contract, tokenPortalAddress, tokenPortal, underlyingERC20 }; +} + +/** + * Helper function to deploy ETH contracts. + * @param walletClient - A viem WalletClient. + * @param publicClient - A viem PublicClient. + * @param abi - The ETH contract's ABI (as abitype's Abi). + * @param bytecode - The ETH contract's bytecode. + * @param args - Constructor arguments for the contract. + * @returns The ETH address the contract was deployed to. + */ +export async function deployL1Contract( + walletClient: WalletClient, + publicClient: PublicClient, + abi: Narrow, + bytecode: Hex, + args: readonly unknown[] = [], +): Promise { + const hash = await walletClient.deployContract({ + abi, + bytecode, + args, + }); + + const receipt = await publicClient.waitForTransactionReceipt({ hash }); + const contractAddress = receipt.contractAddress; + if (!contractAddress) { + throw new Error(`No contract address found in receipt: ${JSON.stringify(receipt)}`); + } + + return EthAddress.fromString(receipt.contractAddress!); +} + +/** + * Sleep for a given number of milliseconds. + * @param ms - the number of milliseconds to sleep for + */ +export function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/yarn-project/aztec-sandbox/src/examples/zk_contract_deployment.ts b/yarn-project/aztec-sandbox/src/examples/zk_contract_deployment.ts new file mode 100644 index 000000000000..f39cb27bb00e --- /dev/null +++ b/yarn-project/aztec-sandbox/src/examples/zk_contract_deployment.ts @@ -0,0 +1,56 @@ +import { Contract, ContractDeployer, createAccounts, createAztecRpcClient, pointToPublicKey } from '@aztec/aztec.js'; +import { Point } from '@aztec/circuits.js'; +import { createDebugLogger } from '@aztec/foundation/log'; +import { ZkTokenContractAbi } from '@aztec/noir-contracts/examples'; + +const logger = createDebugLogger('aztec:http-rpc-client'); + +export const privateKey = Buffer.from('ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', 'hex'); + +const url = 'http://localhost:8080'; + +const aztecRpcClient = createAztecRpcClient(url); + +const INITIAL_BALANCE = 333n; + +/** + * Deploys the ZK Token contract. + * @param pubKeyPoint - The public key Point that the initial balance will belong to. + * @returns An Aztec Contract object with the zk token's ABI. + */ +async function deployZKContract(pubKeyPoint: Point) { + logger('Deploying L2 contract...'); + const deployer = new ContractDeployer(ZkTokenContractAbi, aztecRpcClient); + const tx = deployer.deploy(INITIAL_BALANCE, pointToPublicKey(pubKeyPoint)).send(); + const receipt = await tx.getReceipt(); + const contract = new Contract(receipt.contractAddress!, ZkTokenContractAbi, aztecRpcClient); + await tx.isMined(); + await tx.getReceipt(); + logger('L2 contract deployed'); + return contract; +} + +/** + * Main function. + */ +async function main() { + logger('Running ZK contract test on HTTP interface.'); + + const [address, pubKeyPoint] = (await createAccounts(aztecRpcClient, privateKey, 1))[0]; + logger(`Created account ${address.toString()} with public key ${pubKeyPoint.toString()}`); + const zkContract = await deployZKContract(pubKeyPoint); + const accounts = await aztecRpcClient.getAccounts(); + logger(`Created ${accounts.length} accounts`); + const [balance1] = await zkContract.methods.getBalance(pointToPublicKey(pubKeyPoint)).view({ from: address }); + logger(`Initial owner balance: ${balance1}`); +} + +main() + .then(() => { + logger('Finished running successfuly.'); + process.exit(0); + }) + .catch(err => { + logger('Error in main fn: ', err); + process.exit(1); + }); diff --git a/yarn-project/aztec-sandbox/src/index.ts b/yarn-project/aztec-sandbox/src/index.ts new file mode 100644 index 000000000000..c37aba117bdb --- /dev/null +++ b/yarn-project/aztec-sandbox/src/index.ts @@ -0,0 +1,83 @@ +import http from 'http'; +import { foundry } from 'viem/chains'; +import { http as httpViemTransport, createPublicClient, HDAccount } from 'viem'; + +import { mnemonicToAccount } from 'viem/accounts'; +import { getHttpRpcServer } from '@aztec/aztec-rpc'; +import { createDebugLogger } from '@aztec/foundation/log'; +import { retryUntil } from '@aztec/foundation/retry'; +import { AztecNodeConfig, getConfigEnvVars } from '@aztec/aztec-node'; +import { deployL1Contracts } from '@aztec/ethereum'; + +import { createApiRouter } from './routes.js'; + +const { SERVER_PORT = 8080, MNEMONIC = 'test test test test test test test test test test test junk' } = process.env; + +const logger = createDebugLogger('aztec:sandbox'); + +export const localAnvil = foundry; + +/** + * Helper function that waits for the Ethereum RPC server to respond before deploying L1 contracts. + */ +async function waitThenDeploy(rpcUrl: string, hdAccount: HDAccount) { + // wait for ETH RPC to respond to a request. + const publicClient = createPublicClient({ + chain: foundry, + transport: httpViemTransport(rpcUrl), + }); + const chainID = await retryUntil( + async () => { + let chainId = 0; + try { + chainId = await publicClient.getChainId(); + } catch (err) { + logger(`Failed to get Chain ID. Retrying...`); + } + return chainId; + }, + 'isEthRpcReady', + 30, + 1, + ); + + if (!chainID) { + throw Error(`ETH RPC server unresponsive at ${rpcUrl}.`); + } + + // Deploy L1 contracts + const deployedL1Contracts = await deployL1Contracts(rpcUrl, hdAccount, localAnvil, logger); + return deployedL1Contracts; +} + +/** + * Create and start a new Aztec RCP HTTP Server + */ +async function main() { + const aztecNodeConfig: AztecNodeConfig = getConfigEnvVars(); + const hdAccount = mnemonicToAccount(MNEMONIC); + const privKey = hdAccount.getHdKey().privateKey; + + const deployedL1Contracts = await waitThenDeploy(aztecNodeConfig.rpcUrl, hdAccount); + aztecNodeConfig.publisherPrivateKey = Buffer.from(privKey!); + aztecNodeConfig.rollupContract = deployedL1Contracts.rollupAddress; + aztecNodeConfig.contractDeploymentEmitterContract = deployedL1Contracts.contractDeploymentEmitterAddress; + aztecNodeConfig.inboxContract = deployedL1Contracts.inboxAddress; + + const rpcServer = await getHttpRpcServer(aztecNodeConfig); + + 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); +} + +main() + .then(() => logger(`Aztec JSON RPC listening on port ${SERVER_PORT}`)) + .catch(err => { + logger(err); + process.exit(1); + }); diff --git a/yarn-project/aztec-sandbox/src/routes.ts b/yarn-project/aztec-sandbox/src/routes.ts new file mode 100644 index 000000000000..c037a1547dd5 --- /dev/null +++ b/yarn-project/aztec-sandbox/src/routes.ts @@ -0,0 +1,28 @@ +import Koa from 'koa'; +import Router from 'koa-router'; +import { DeployL1Contracts } from '@aztec/ethereum'; + +/** + * Creates a router for helper API endpoints of the Aztec RPC Server. + * @param aztecNode - An instance of the aztec node. + * @param config - The aztec node's configuration variables. + */ +export function createApiRouter(l1Contracts: DeployL1Contracts) { + const router = new Router({ prefix: '/api' }); + router.get('/status', (ctx: Koa.Context) => { + // TODO: add `status` to Aztec node. + ctx.status = 200; + }); + + router.get('/l1-contract-addresses', (ctx: Koa.Context) => { + ctx.body = { + rollup: l1Contracts.rollupAddress.toString(), + contractDeploymentEmitter: l1Contracts.contractDeploymentEmitterAddress.toString(), + inbox: l1Contracts.inboxAddress.toString(), + registry: l1Contracts.registryAddress.toString(), + }; + ctx.status = 200; + }); + + return router; +} diff --git a/yarn-project/aztec-sandbox/tsconfig.json b/yarn-project/aztec-sandbox/tsconfig.json new file mode 100644 index 000000000000..e92265f2b6d8 --- /dev/null +++ b/yarn-project/aztec-sandbox/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "..", + "compilerOptions": { + "outDir": "dest", + "rootDir": "src", + "tsBuildInfoFile": ".tsbuildinfo" + }, + "references": [ + { + "path": "../aztec-node" + }, + { + "path": "../aztec-rpc" + }, + { + "path": "../aztec.js" + }, + { + "path": "../ethereum" + }, + { + "path": "../foundation" + } + ], + "include": ["src"] +} diff --git a/yarn-project/aztec.js/src/aztec_rpc_client/aztec_rpc_client.ts b/yarn-project/aztec.js/src/aztec_rpc_client/aztec_rpc_client.ts new file mode 100644 index 000000000000..96dd4ef03644 --- /dev/null +++ b/yarn-project/aztec.js/src/aztec_rpc_client/aztec_rpc_client.ts @@ -0,0 +1,56 @@ +import { AztecAddress, AztecRPC, AztecRPCServer, EthAddress, Fr, Point, Tx, TxHash } from '@aztec/aztec-rpc'; +import { createJsonRpcClient } from '@aztec/foundation/json-rpc'; +import { ContractDeploymentTx } from '@aztec/types'; + +/** + * A dictionary of the Aztec-deployed L1 contracts. + */ +export type L1ContractAddresses = { + /** + * Address fo the main Aztec rollup contract. + */ + rollup: EthAddress; + /** + * Address of the contract that emits events on public contract deployment. + */ + contractDeploymentEmitter: EthAddress; + /** + * Address of the L1/L2 messaging inbox contract. + */ + inbox: EthAddress; + + /** + * Registry Address. + */ + registry: EthAddress; +}; + +/** + * string dictionary of aztec contract addresses that we receive over http. + */ +type L1ContractAddressesResp = { + [K in keyof L1ContractAddresses]: string; +}; + +export const createAztecRpcClient = (url: string): AztecRPC => + createJsonRpcClient( + url, + { + AztecAddress, + TxHash, + EthAddress, + Point, + Fr, + }, + { Tx, ContractDeploymentTx }, + false, + ); + +export const getL1ContractAddresses = async (url: string): Promise => { + const reqUrl = new URL(`${url}/api/l1-contract-addresses`); + const response = (await (await fetch(reqUrl.toString())).json()) as unknown as L1ContractAddressesResp; + const result = Object.fromEntries( + Object.entries(response).map(([key, value]) => [key, EthAddress.fromString(value)]), + ); + return result as L1ContractAddresses; +}; diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_client/index.ts b/yarn-project/aztec.js/src/aztec_rpc_client/index.ts similarity index 100% rename from yarn-project/aztec-rpc/src/aztec_rpc_client/index.ts rename to yarn-project/aztec.js/src/aztec_rpc_client/index.ts diff --git a/yarn-project/aztec.js/src/contract/contract.test.ts b/yarn-project/aztec.js/src/contract/contract.test.ts index e2602deb65b9..7e5ed828720c 100644 --- a/yarn-project/aztec.js/src/contract/contract.test.ts +++ b/yarn-project/aztec.js/src/contract/contract.test.ts @@ -1,4 +1,4 @@ -import { AztecAddress, AztecRPCClient, DeployedContract, EthAddress, Tx, TxHash, TxReceipt } from '@aztec/aztec-rpc'; +import { AztecAddress, AztecRPC, DeployedContract, EthAddress, Tx, TxHash, TxReceipt } from '@aztec/aztec-rpc'; import { mock } from 'jest-mock-extended'; import { ABIParameterVisibility, ContractAbi, FunctionType } from '@aztec/foundation/abi'; @@ -7,7 +7,7 @@ import { Contract } from './contract.js'; import { ContractDeploymentTx } from '@aztec/types'; describe('Contract Class', () => { - let arc: ReturnType>; + let arc: ReturnType>; const contractAddress = AztecAddress.random(); const account = AztecAddress.random(); @@ -86,7 +86,7 @@ describe('Contract Class', () => { }); beforeEach(() => { - arc = mock(); + arc = mock(); arc.createDeploymentTx.mockResolvedValue(mockDeploymentTx); arc.createTx.mockResolvedValue(mockTx); arc.sendTx.mockResolvedValue(mockTxHash); diff --git a/yarn-project/aztec.js/src/contract/contract.ts b/yarn-project/aztec.js/src/contract/contract.ts index 6b2416260349..1513144fdfda 100644 --- a/yarn-project/aztec.js/src/contract/contract.ts +++ b/yarn-project/aztec.js/src/contract/contract.ts @@ -1,4 +1,4 @@ -import { AztecRPCClient, DeployedContract, generateFunctionSelector } from '@aztec/aztec-rpc'; +import { AztecRPC, DeployedContract, generateFunctionSelector } from '@aztec/aztec-rpc'; import { ContractAbi, FunctionAbi } from '@aztec/foundation/abi'; import { ContractFunctionInteraction } from './contract_function_interaction.js'; import { AztecAddress } from '@aztec/foundation/aztec-address'; @@ -37,7 +37,7 @@ export class Contract { * The Application Binary Interface for the contract. */ public readonly abi: ContractAbi, - private arc: AztecRPCClient, + private arc: AztecRPC, ) { abi.functions.forEach((f: FunctionAbi) => { const interactionFunction = (...args: any[]) => { diff --git a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts index b7dbb072ea39..51b7e94931f4 100644 --- a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts +++ b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts @@ -1,4 +1,4 @@ -import { AztecRPCClient, Tx, TxHash } from '@aztec/aztec-rpc'; +import { AztecRPC, Tx, TxHash } from '@aztec/aztec-rpc'; import { AztecAddress, Fr } from '@aztec/circuits.js'; import { FunctionType } from '@aztec/foundation/abi'; import { SentTx } from './sent_tx.js'; @@ -37,7 +37,7 @@ export class ContractFunctionInteraction { protected tx?: Tx; constructor( - protected arc: AztecRPCClient, + protected arc: AztecRPC, protected contractAddress: AztecAddress, protected functionName: string, protected args: any[], diff --git a/yarn-project/aztec.js/src/contract/sent_tx.ts b/yarn-project/aztec.js/src/contract/sent_tx.ts index f7ca73186ace..298cf8224c6c 100644 --- a/yarn-project/aztec.js/src/contract/sent_tx.ts +++ b/yarn-project/aztec.js/src/contract/sent_tx.ts @@ -1,4 +1,4 @@ -import { AztecRPCClient, TxReceipt, TxHash, TxStatus } from '@aztec/aztec-rpc'; +import { AztecRPC, TxReceipt, TxHash, TxStatus } from '@aztec/aztec-rpc'; import { retryUntil } from '@aztec/foundation/retry'; /** @@ -6,7 +6,7 @@ import { retryUntil } from '@aztec/foundation/retry'; * its hash, receipt, and mining status. */ export class SentTx { - constructor(private arc: AztecRPCClient, private txHashPromise: Promise) {} + constructor(private arc: AztecRPC, private txHashPromise: Promise) {} /** * Retrieves the transaction hash of the SentTx instance. diff --git a/yarn-project/aztec.js/src/contract_deployer/contract_deployer.test.ts b/yarn-project/aztec.js/src/contract_deployer/contract_deployer.test.ts index 686770556381..08110e3e23e1 100644 --- a/yarn-project/aztec.js/src/contract_deployer/contract_deployer.test.ts +++ b/yarn-project/aztec.js/src/contract_deployer/contract_deployer.test.ts @@ -1,4 +1,4 @@ -import { AztecRPCClient, Tx, TxHash, TxReceipt } from '@aztec/aztec-rpc'; +import { AztecRPC, Tx, TxHash, TxReceipt } from '@aztec/aztec-rpc'; import { AztecAddress, EthAddress, Fr } from '@aztec/circuits.js'; import { ContractAbi, FunctionType } from '@aztec/foundation/abi'; import { randomBytes } from 'crypto'; @@ -7,7 +7,7 @@ import { ContractDeployer } from './contract_deployer.js'; import { ContractDeploymentTx } from '@aztec/types'; describe('Contract Deployer', () => { - let arc: ReturnType>; + let arc: ReturnType>; const abi: ContractAbi = { name: 'MyContract', @@ -33,7 +33,7 @@ describe('Contract Deployer', () => { const mockTxReceipt = { type: 'TxReceipt' } as any as TxReceipt; beforeEach(() => { - arc = mock(); + arc = mock(); arc.createDeploymentTx.mockResolvedValue(contractDeploymentTx); arc.createTx.mockResolvedValue(mockTx); arc.sendTx.mockResolvedValue(mockTxHash); diff --git a/yarn-project/aztec.js/src/contract_deployer/contract_deployer.ts b/yarn-project/aztec.js/src/contract_deployer/contract_deployer.ts index 38a111a8b810..8b64e7aafff8 100644 --- a/yarn-project/aztec.js/src/contract_deployer/contract_deployer.ts +++ b/yarn-project/aztec.js/src/contract_deployer/contract_deployer.ts @@ -1,4 +1,4 @@ -import { AztecRPCClient } from '@aztec/aztec-rpc'; +import { AztecRPC } from '@aztec/aztec-rpc'; import { ContractAbi } from '@aztec/foundation/abi'; import { DeployMethod } from './deploy_method.js'; @@ -6,7 +6,7 @@ import { DeployMethod } from './deploy_method.js'; * A class for deploying contract. */ export class ContractDeployer { - constructor(private abi: ContractAbi, private arc: AztecRPCClient) {} + constructor(private abi: ContractAbi, private arc: AztecRPC) {} /** * Deploy a contract using the provided ABI and constructor arguments. diff --git a/yarn-project/aztec.js/src/contract_deployer/deploy_method.ts b/yarn-project/aztec.js/src/contract_deployer/deploy_method.ts index e42b678dfd06..abb7d3945fea 100644 --- a/yarn-project/aztec.js/src/contract_deployer/deploy_method.ts +++ b/yarn-project/aztec.js/src/contract_deployer/deploy_method.ts @@ -1,4 +1,4 @@ -import { AztecRPCClient } from '@aztec/aztec-rpc'; +import { AztecRPC } from '@aztec/aztec-rpc'; import { ContractAbi, FunctionType } from '@aztec/foundation/abi'; import { ContractFunctionInteraction, SendMethodOptions } from '../contract/index.js'; import { EthAddress } from '@aztec/foundation/eth-address'; @@ -31,7 +31,7 @@ export class DeployMethod extends ContractFunctionInteraction { */ public partialContractAddress?: PartialContractAddress = undefined; - constructor(arc: AztecRPCClient, private abi: ContractAbi, args: any[] = []) { + constructor(arc: AztecRPC, private abi: ContractAbi, args: any[] = []) { const constructorAbi = abi.functions.find(f => f.name === 'constructor'); if (!constructorAbi) { throw new Error('Cannot find constructor in the ABI.'); diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index 3d4b94f9f2d6..8fc2ac3df68c 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -1,6 +1,7 @@ export * from './contract/index.js'; export * from './contract_deployer/index.js'; export * from './utils/index.js'; +export * from './aztec_rpc_client/index.js'; // TODO - only export necessary stuffs export * from '@aztec/aztec-rpc'; diff --git a/yarn-project/aztec.js/src/utils/account.ts b/yarn-project/aztec.js/src/utils/account.ts new file mode 100644 index 000000000000..f4e1385ecd49 --- /dev/null +++ b/yarn-project/aztec.js/src/utils/account.ts @@ -0,0 +1,31 @@ +import { AztecRPC } from '@aztec/aztec-rpc'; +import { AztecAddress, Point } from '@aztec/circuits.js'; +import { SentTx } from '../index.js'; +import { createDebugLogger } from '@aztec/foundation/log'; + +/** + * Creates an Aztec Account. + * @returns The account's address & public key. + */ +export async function createAccounts( + aztecRpcClient: AztecRPC, + privateKey: Buffer, + numberOfAccounts = 1, + logger = createDebugLogger('aztec:aztec.js:accounts'), +): Promise<[AztecAddress, Point][]> { + const results: [AztecAddress, Point][] = []; + for (let i = 0; i < numberOfAccounts; ++i) { + // We use the well-known private key and the validating account contract for the first account, + // and generate random keypairs with gullible account contracts (ie no sig validation) for the rest. + // TODO(#662): Let the aztec rpc server generate the keypair rather than hardcoding the private key + const privKey = i == 0 ? privateKey : undefined; + const [txHash, newAddress] = await aztecRpcClient.createSmartAccount(privKey); + // wait for tx to be mined + await new SentTx(aztecRpcClient, Promise.resolve(txHash)).isMined(); + const address = newAddress; + const pubKey = await aztecRpcClient.getAccountPublicKey(address); + logger(`Created account ${address.toString()} with public key ${pubKey.toString()}`); + results.push([newAddress, pubKey]); + } + return results; +} diff --git a/yarn-project/aztec.js/src/utils/index.ts b/yarn-project/aztec.js/src/utils/index.ts index 670591ba413e..1edb3e480fb7 100644 --- a/yarn-project/aztec.js/src/utils/index.ts +++ b/yarn-project/aztec.js/src/utils/index.ts @@ -1 +1,3 @@ export * from './secrets.js'; +export * from './account.js'; +export * from './pub_key.js'; diff --git a/yarn-project/aztec.js/src/utils/pub_key.ts b/yarn-project/aztec.js/src/utils/pub_key.ts new file mode 100644 index 000000000000..11e8ac609918 --- /dev/null +++ b/yarn-project/aztec.js/src/utils/pub_key.ts @@ -0,0 +1,15 @@ +import { Point } from '../index.js'; + +/** + * Converts a Point type to a public key represented by BigInt coordinates + * @param point - The Point to convert. + * @returns An object with x & y coordinates represented as bigints. + */ +export function pointToPublicKey(point: Point) { + const x = point.x.toBigInt(); + const y = point.y.toBigInt(); + return { + x, + y, + }; +} diff --git a/yarn-project/circuits.js/src/structs/kernel/combined_accumulated_data.ts b/yarn-project/circuits.js/src/structs/kernel/combined_accumulated_data.ts index 3b183e758686..846a41f42be4 100644 --- a/yarn-project/circuits.js/src/structs/kernel/combined_accumulated_data.ts +++ b/yarn-project/circuits.js/src/structs/kernel/combined_accumulated_data.ts @@ -362,6 +362,10 @@ export class CombinedAccumulatedData { ); } + toString() { + return this.toBuffer().toString(); + } + /** * Deserializes from a buffer or reader, corresponding to a write in cpp. * @param buffer - Buffer or reader to read from. @@ -387,6 +391,15 @@ export class CombinedAccumulatedData { ); } + /** + * Deserializes from a string, corresponding to a write in cpp. + * @param str - String to read from. + * @returns Deserialized object. + */ + static fromString(str: string) { + return CombinedAccumulatedData.fromBuffer(Buffer.from(str, 'hex')); + } + static empty() { return new CombinedAccumulatedData( AggregationObject.makeFake(), diff --git a/yarn-project/circuits.js/src/structs/kernel/combined_constant_data.ts b/yarn-project/circuits.js/src/structs/kernel/combined_constant_data.ts index 0113ba64c047..e1f0796541f0 100644 --- a/yarn-project/circuits.js/src/structs/kernel/combined_constant_data.ts +++ b/yarn-project/circuits.js/src/structs/kernel/combined_constant_data.ts @@ -49,6 +49,10 @@ export class PrivateHistoricTreeRoots { return serializeToBuffer(...PrivateHistoricTreeRoots.getFields(this)); } + toString() { + return this.toBuffer().toString(); + } + isEmpty() { return ( this.privateDataTreeRoot.isZero() && @@ -75,6 +79,10 @@ export class PrivateHistoricTreeRoots { ); } + static fromString(str: string): PrivateHistoricTreeRoots { + return PrivateHistoricTreeRoots.fromBuffer(Buffer.from(str, 'hex')); + } + static empty() { return new PrivateHistoricTreeRoots(Fr.ZERO, Fr.ZERO, Fr.ZERO, Fr.ZERO, Fr.ZERO); } @@ -95,6 +103,10 @@ export class CombinedHistoricTreeRoots { return serializeToBuffer(this.privateHistoricTreeRoots); } + toString() { + return this.toBuffer().toString(); + } + static fromBuffer(buffer: Buffer | BufferReader) { const reader = BufferReader.asReader(buffer); return new CombinedHistoricTreeRoots(reader.readObject(PrivateHistoricTreeRoots)); diff --git a/yarn-project/end-to-end/src/e2e_zk_token_contract.test.ts b/yarn-project/end-to-end/src/e2e_zk_token_contract.test.ts index 5be384c72869..e68ff84f0b13 100644 --- a/yarn-project/end-to-end/src/e2e_zk_token_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_zk_token_contract.test.ts @@ -1,11 +1,11 @@ import { AztecNodeService } from '@aztec/aztec-node'; import { AztecAddress, AztecRPCServer, Contract, ContractDeployer, TxStatus } from '@aztec/aztec.js'; import { ZkTokenContractAbi } from '@aztec/noir-contracts/examples'; - import { DebugLogger } from '@aztec/foundation/log'; -import { pointToPublicKey, setup } from './utils.js'; import { L2BlockL2Logs } from '@aztec/types'; +import { pointToPublicKey, setup } from './utils.js'; + describe('e2e_zk_token_contract', () => { let aztecNode: AztecNodeService; let aztecRpcServer: AztecRPCServer; diff --git a/yarn-project/foundation/src/json-rpc/class_converter.ts b/yarn-project/foundation/src/json-rpc/class_converter.ts index dc6fab4cdd69..86dd9c408e7a 100644 --- a/yarn-project/foundation/src/json-rpc/class_converter.ts +++ b/yarn-project/foundation/src/json-rpc/class_converter.ts @@ -19,7 +19,12 @@ interface IOClass { /** * Creates an IOClass from a given string. */ - fromString: (str: string) => any; + fromString?: (str: string) => any; + + /** + * Creates an IOClass from a given JSON object. + */ + fromJSON?: (obj: object) => any; } /** @@ -32,7 +37,7 @@ export interface ClassConverterInput { /** * Represents a class in a JSON-friendly encoding. */ -export interface JsonEncodedClass { +export interface StringEncodedClass { /** * The class type. */ @@ -43,35 +48,69 @@ export interface JsonEncodedClass { data: string; } +/** + * Represents a class in a JSON-friendly encoding. + */ +export interface JsonEncodedClass { + /** + * The class type. + */ + type: string; + /** + * The class data string. + */ + data: object; +} +/** + * Whether a class is a complex object or simply represented by a string. + */ +export type ClassEncoding = 'string' | 'object'; + /** * Handles mapping of classes to names, and calling toString and fromString to convert to and from JSON-friendly formats. * Takes a class map as input. */ export class ClassConverter { - private toClass = new Map(); - private toName = new Map(); + private toClass = new Map(); + private toName = new Map(); /** * Create a class converter from a table of classes. - * @param input - The class table. + * @param stringClassMap - The class table of string encoded classes. + * @param objectClassMap - The class table of complex object classes */ - constructor(input: ClassConverterInput) { - for (const key of Object.keys(input)) { - this.register(key, input[key]); + constructor(stringClassMap?: ClassConverterInput, objectClassMap?: ClassConverterInput) { + if (stringClassMap) { + for (const key of Object.keys(stringClassMap)) { + this.register(key, stringClassMap[key], 'string'); + } + } + if (objectClassMap) { + for (const key of Object.keys(objectClassMap)) { + this.register(key, objectClassMap[key], 'object'); + } } } + /** * Register a class with a certain name. * This name is used for conversion from and to this class. * @param type - The class name to use for serialization. * @param class_ - The class object. + * @param encoding - Whether the class is a complex object or simply represented by a string. */ - register(type: string, class_: IOClass) { + register(type: string, class_: IOClass, encoding: ClassEncoding) { assert(type !== 'Buffer', "'Buffer' handling is hardcoded. Cannot use as name."); - assert(hasOwnProperty(class_.prototype, 'toString'), `Class ${type} must define a toString() method.`); - assert(class_['fromString'], `Class ${type} must define a fromString() static method.`); - this.toName.set(class_, type); - this.toClass.set(type, class_); + assert( + hasOwnProperty(class_.prototype, 'toString') || hasOwnProperty(class_.prototype, 'toJSON'), + `Class ${type} must define a toString() OR toJSON() method.`, + ); + assert( + class_['fromString'] || class_['fromJSON'], + `Class ${type} must define a fromString() OR fromJSON() static method.`, + ); + this.toName.set(class_, [type, encoding]); + this.toClass.set(type, [class_, encoding]); } /** @@ -95,19 +134,27 @@ export class ClassConverter { * @param jsonObj - An object encoding a class. * @returns The class object. */ - toClassObj(jsonObj: JsonEncodedClass): any { - const class_ = this.toClass.get(jsonObj.type); - assert(class_, `Could not find type in lookup.`); - return class_!.fromString(jsonObj.data); + toClassObj(jsonObj: JsonEncodedClass | StringEncodedClass): any { + const result = this.toClass.get(jsonObj.type); + assert(result, `Could not find type in lookup.`); + + const [class_, encoding] = result; + if (encoding === 'string' && typeof jsonObj.data === 'string') { + return class_!.fromString!(jsonObj.data); + } else { + return class_!.fromJSON!(jsonObj.data as object); + } } /** - * Convert a JSON-like object to a class object. + * Convert a class object to a JSON object. * @param classObj - A JSON encoding a class. * @returns The class object. */ - toJsonObj(classObj: any): JsonEncodedClass { - const type = this.toName.get(classObj.constructor); - assert(type, `Could not find class in lookup.`); - return { type: type!, data: classObj.toString() }; + toJsonObj(classObj: any): JsonEncodedClass | StringEncodedClass { + const result = this.toName.get(classObj.constructor); + assert(result, `Could not find class in lookup.`); + const [type, encoding] = result; + const data = encoding === 'string' ? classObj.toString() : classObj.toJSON(); + return { type: type!, data }; } } diff --git a/yarn-project/foundation/src/json-rpc/client/json_rpc_client.test.ts b/yarn-project/foundation/src/json-rpc/client/json_rpc_client.test.ts index 60d3b8069bd4..4c6b2c4a413f 100644 --- a/yarn-project/foundation/src/json-rpc/client/json_rpc_client.test.ts +++ b/yarn-project/foundation/src/json-rpc/client/json_rpc_client.test.ts @@ -5,11 +5,11 @@ import { createJsonRpcClient } from './json_rpc_client.js'; test('test an RPC function over client', async () => { const mockFetch = async (host: string, method: string, body: any) => { - const server = new JsonRpcServer(new TestState([new TestNote('a'), new TestNote('b')]), { TestNote }); + const server = new JsonRpcServer(new TestState([new TestNote('a'), new TestNote('b')]), { TestNote }, {}, true); const result = await request(server.getApp().callback()).post(`/${method}`).send(body); return JSON.parse(result.text); }; - const client = createJsonRpcClient('', { TestNote }, mockFetch); + const client = createJsonRpcClient('', { TestNote }, {}, true, mockFetch); const result = await client.addNotes([new TestNote('c')]); expect(result[0]).toBeInstanceOf(TestNote); expect(result[1]).toBeInstanceOf(TestNote); diff --git a/yarn-project/foundation/src/json-rpc/client/json_rpc_client.ts b/yarn-project/foundation/src/json-rpc/client/json_rpc_client.ts index 3d1cc7ae5290..f24dea4ae0c8 100644 --- a/yarn-project/foundation/src/json-rpc/client/json_rpc_client.ts +++ b/yarn-project/foundation/src/json-rpc/client/json_rpc_client.ts @@ -6,7 +6,7 @@ import { RemoteObject } from 'comlink'; import { createDebugLogger } from '../../log/index.js'; import { retry } from '../../retry/index.js'; import { ClassConverter, ClassConverterInput } from '../class_converter.js'; -import { convertFromJsonObj, convertToJsonObj } from '../convert.js'; +import { JsonStringify, convertFromJsonObj, convertToJsonObj } from '../convert.js'; const debug = createDebugLogger('json-rpc:json_rpc_client'); /** @@ -17,13 +17,22 @@ const debug = createDebugLogger('json-rpc:json_rpc_client'); * @param body - The RPC payload. * @returns The parsed JSON response, or throws an error. */ -export async function defaultFetch(host: string, rpcMethod: string, body: any) { - debug(`JsonRpcClient.fetch`, host, rpcMethod, '<-', body); - const resp = await fetch(`${host}/${rpcMethod}`, { - method: 'POST', - body: JSON.stringify(body), - headers: { 'content-type': 'application/json' }, - }); +export async function defaultFetch(host: string, rpcMethod: string, body: any, useApiEndpoints: boolean) { + debug(`JsonRpcClient.fetch`, host, rpcMethod, '->', body); + let resp: Response; + if (useApiEndpoints) { + resp = await fetch(`${host}/${rpcMethod}`, { + method: 'POST', + body: JsonStringify(body), + headers: { 'content-type': 'application/json' }, + }); + } else { + resp = await fetch(host, { + method: 'POST', + body: JsonStringify({ ...body, method: rpcMethod }), + headers: { 'content-type': 'application/json' }, + }); + } if (!resp.ok) { throw new Error(resp.statusText); @@ -40,8 +49,8 @@ export async function defaultFetch(host: string, rpcMethod: string, body: any) { /** * A fetch function with retries. */ -export async function mustSucceedFetch(host: string, rpcMethod: string, body: any) { - return await retry(() => defaultFetch(host, rpcMethod, body), 'JsonRpcClient request'); +export async function mustSucceedFetch(host: string, rpcMethod: string, body: any, useApiEndpoints: boolean) { + return await retry(() => defaultFetch(host, rpcMethod, body, useApiEndpoints), 'JsonRpcClient request'); } /** @@ -50,10 +59,12 @@ export async function mustSucceedFetch(host: string, rpcMethod: string, body: an */ export function createJsonRpcClient( host: string, - classMap: ClassConverterInput, + stringClassMap: ClassConverterInput, + objectClassMap: ClassConverterInput, + useApiEndpoints: boolean, fetch = defaultFetch, ) { - const classConverter = new ClassConverter(classMap); + const classConverter = new ClassConverter(stringClassMap, objectClassMap); let id = 0; const request = async (method: string, params: any[]): Promise => { const body = { @@ -63,12 +74,15 @@ export function createJsonRpcClient( params: params.map(param => convertToJsonObj(classConverter, param)), }; debug(`JsonRpcClient.request`, method, '<-', params); - const res = await fetch(host, method, body); - debug(`JsonRpcClient.request`, method, '->', res); + const res = await fetch(host, method, body, useApiEndpoints); + debug(`JsonRpcClient.result`, method, '->', res); if (res.error) { throw res.error; } - return convertFromJsonObj(classConverter, res.result); + if ([null, undefined, 'null', 'undefined'].includes(res.result)) { + return; + } + return convertFromJsonObj(classConverter, JSON.parse(res.result)); }; // Intercept any RPC methods with a proxy diff --git a/yarn-project/foundation/src/json-rpc/convert.ts b/yarn-project/foundation/src/json-rpc/convert.ts index 10292584697c..17c5391f5f15 100644 --- a/yarn-project/foundation/src/json-rpc/convert.ts +++ b/yarn-project/foundation/src/json-rpc/convert.ts @@ -1,6 +1,22 @@ import { ClassConverter } from './class_converter.js'; import { Buffer } from 'buffer'; +/** + * JSON.stringify helper that handles bigints. + * @param obj - The object to be stringified. + * @returns The resulting string. + */ +export function JsonStringify(obj: object): string { + return JSON.stringify(obj, (key, value) => + typeof value === 'bigint' + ? JSON.stringify({ + type: 'bigint', + data: value.toString(), + }) + : value, + ); +} + /** * Convert a JSON-friendly object, which may encode a class object. * @param cc - The class converter. @@ -8,6 +24,10 @@ import { Buffer } from 'buffer'; * @returns The decoded object. */ export function convertFromJsonObj(cc: ClassConverter, obj: any): any { + if (obj === null) { + return undefined; // `null` doesn't work with default args. + } + if (!obj) { return obj; // Primitive type } @@ -15,6 +35,11 @@ export function convertFromJsonObj(cc: ClassConverter, obj: any): any { if (obj.type === 'Buffer' && typeof obj.data === 'string') { return Buffer.from(obj.data, 'base64'); } + + if (obj.type === 'bigint' && typeof obj.data === 'string') { + return BigInt(obj.data); + } + // Is this a convertible type? if (typeof obj.type === 'string' && cc.isRegisteredClassName(obj.type)) { return cc.toClassObj(obj); @@ -51,6 +76,14 @@ export function convertToJsonObj(cc: ClassConverter, obj: any): any { if (obj instanceof Buffer) { return { type: 'Buffer', data: obj.toString('base64') }; } + + if (typeof obj === 'bigint') { + return { + type: 'bigint', + data: obj.toString(), + }; + } + // Is this a convertible type? if (cc.isRegisteredClass(obj.constructor)) { return cc.toJsonObj(obj); diff --git a/yarn-project/foundation/src/json-rpc/js_utils.ts b/yarn-project/foundation/src/json-rpc/js_utils.ts index c8e12fc5d506..c24b3d7b8974 100644 --- a/yarn-project/foundation/src/json-rpc/js_utils.ts +++ b/yarn-project/foundation/src/json-rpc/js_utils.ts @@ -8,8 +8,13 @@ export const hasOwnProperty = (obj: any, propertyName: string) => Object.prototype.hasOwnProperty.call(obj, propertyName); -export const assert = (x: any, err: string) => { +/** + * Helper function to assert a condition is truthy + * @param x - A boolean condition to assert. + * @param err - Error message to throw if x isn't met. + */ +export function assert(x: any, err: string): asserts x { if (!x) { throw new Error(err); } -}; +} diff --git a/yarn-project/foundation/src/json-rpc/server/json_proxy.ts b/yarn-project/foundation/src/json-rpc/server/json_proxy.ts index 0e5a3ae244b0..d51d68a4e10a 100644 --- a/yarn-project/foundation/src/json-rpc/server/json_proxy.ts +++ b/yarn-project/foundation/src/json-rpc/server/json_proxy.ts @@ -11,8 +11,8 @@ const debug = createDebugLogger('json-rpc:json_proxy'); */ export class JsonProxy { classConverter: ClassConverter; - constructor(private handler: object, input: ClassConverterInput) { - this.classConverter = new ClassConverter(input); + constructor(private handler: object, stringClassMap: ClassConverterInput, objectClassMap: ClassConverterInput) { + this.classConverter = new ClassConverter(stringClassMap, objectClassMap); } /** * Call an RPC method. @@ -27,10 +27,10 @@ export class JsonProxy { assert(Array.isArray(jsonParams), 'JsonProxy: Params not an array!'); // convert the params from json representation to classes const convertedParams = jsonParams.map(param => convertFromJsonObj(this.classConverter, param)); - debug('JsonProxy:call', this.handler, methodName, '<-', convertedParams); + debug('JsonProxy:call', methodName, '<-', convertedParams); const rawRet = await (this.handler as any)[methodName](...convertedParams); const ret = convertToJsonObj(this.classConverter, rawRet); - debug('JsonProxy:call', this.handler, methodName, '->', ret); + debug('JsonProxy:call', methodName, '->', ret); return ret; } } diff --git a/yarn-project/foundation/src/json-rpc/server/json_rpc_server.test.ts b/yarn-project/foundation/src/json-rpc/server/json_rpc_server.test.ts index 2a1d054d584c..9aa25756093a 100644 --- a/yarn-project/foundation/src/json-rpc/server/json_rpc_server.test.ts +++ b/yarn-project/foundation/src/json-rpc/server/json_rpc_server.test.ts @@ -3,27 +3,21 @@ import { TestState, TestNote } from '../fixtures/test_state.js'; import { JsonRpcServer } from './json_rpc_server.js'; test('test an RPC function with a primitive parameter', async () => { - const server = new JsonRpcServer(new TestState([new TestNote('a'), new TestNote('b')]), { TestNote }); + const server = new JsonRpcServer(new TestState([new TestNote('a'), new TestNote('b')]), { TestNote }, {}, true); const response = await request(server.getApp().callback()) .post('/getNote') .send({ params: [0] }); expect(response.status).toBe(200); - expect(response.text).toBe('{"result":{"type":"TestNote","data":"a"}}'); + expect(response.text).toBe(JSON.stringify({ result: JSON.stringify({ type: 'TestNote', data: 'a' }) })); }); test('test an RPC function with an array of classes', async () => { - const server = new JsonRpcServer(new TestState([]), { TN: TestNote }); + const server = new JsonRpcServer(new TestState([]), { TestNote }, {}, true); const response = await request(server.getApp().callback()) .post('/addNotes') .send({ - params: [ - [ - { type: 'TN', data: 'a' }, - { type: 'TN', data: 'b' }, - { type: 'TN', data: 'c' }, - ], - ], + params: [[{ data: 'a' }, { data: 'b' }, { data: 'c' }]], }); expect(response.status).toBe(200); - expect(response.text).toBe('{"result":[{"type":"TN","data":"a"},{"type":"TN","data":"b"},{"type":"TN","data":"c"}]}'); + expect(response.text).toBe(JSON.stringify({ result: JSON.stringify([{ data: 'a' }, { data: 'b' }, { data: 'c' }]) })); }); diff --git a/yarn-project/foundation/src/json-rpc/server/json_rpc_server.ts b/yarn-project/foundation/src/json-rpc/server/json_rpc_server.ts index 06e318f5307d..a0ccf40c8dad 100644 --- a/yarn-project/foundation/src/json-rpc/server/json_rpc_server.ts +++ b/yarn-project/foundation/src/json-rpc/server/json_rpc_server.ts @@ -2,11 +2,13 @@ import http from 'http'; import Router from 'koa-router'; import cors from '@koa/cors'; import compress from 'koa-compress'; -import { ClassConverterInput } from '../class_converter.js'; import Koa from 'koa'; import bodyParser from 'koa-bodyparser'; -import { JsonProxy } from './json_proxy.js'; + import { createLogger } from '../../log/index.js'; +import { ClassConverterInput } from '../class_converter.js'; +import { JsonProxy } from './json_proxy.js'; +import { JsonStringify } from '../convert.js'; /** * JsonRpcServer. @@ -16,10 +18,13 @@ export class JsonRpcServer { proxy: JsonProxy; constructor( private handler: object, - input: ClassConverterInput, + stringClassMap: ClassConverterInput, + objectClassMap: ClassConverterInput, + private createApi: boolean, + private disallowedMethods: string[] = [], private log = createLogger('aztec:foundation:json-rpc:server'), ) { - this.proxy = new JsonProxy(handler, input); + this.proxy = new JsonProxy(handler, stringClassMap, objectClassMap); } /** @@ -40,7 +45,7 @@ export class JsonRpcServer { }; const app = new Koa(); app.on('error', error => { - this.log(`KOA app-level error. ${JSON.stringify({ error })}`); + this.log(`KOA app-level error. ${JsonStringify({ error })}`); }); app.use(compress({ br: false } as any)); app.use(bodyParser()); @@ -61,18 +66,49 @@ export class JsonRpcServer { const router = new Router({ prefix }); const proto = Object.getPrototypeOf(this.handler); // Find all our endpoints from the handler methods - for (const method of Object.getOwnPropertyNames(proto)) { - // Ignore if not a function - if (method === 'constructor' || typeof proto[method] !== 'function') { - continue; + + if (this.createApi) { + // "API mode" where an endpoint is created for each method + for (const method of Object.getOwnPropertyNames(proto)) { + // Ignore if not a function or function is not allowed + if ( + method === 'constructor' || + typeof proto[method] !== 'function' || + this.disallowedMethods.includes(method) + ) { + continue; + } + router.post(`/${method}`, async (ctx: Koa.Context) => { + const { params = [], jsonrpc, id } = ctx.request.body as any; + const result = await this.proxy.call(method, params); + ctx.body = { jsonrpc, id, result: JsonStringify(result) }; + ctx.status = 200; + }); } - router.post(`/${method}`, async (ctx: Koa.Context) => { - const { params = [], jsonrpc, id } = ctx.request.body as any; + } else { + // "JSON RPC mode" where a single endpoint is used and the method is given in the request body + router.post('/', async (ctx: Koa.Context) => { + const { params = [], jsonrpc, id, method } = ctx.request.body as any; + // Ignore if not a function + if ( + method === 'constructor' || + typeof proto[method] !== 'function' || + this.disallowedMethods.includes(method) + ) { + ctx.status = 400; + ctx.body = { error: `Invalid method name: ${method}` }; + } const result = await this.proxy.call(method, params); - ctx.body = { jsonrpc, id, result }; + + ctx.body = { + jsonrpc, + id, + result: JsonStringify(result), + }; ctx.status = 200; }); } + return router; } diff --git a/yarn-project/package.json b/yarn-project/package.json index b5a9cbc71cd9..e50eeb70e5fa 100644 --- a/yarn-project/package.json +++ b/yarn-project/package.json @@ -20,6 +20,7 @@ "archiver", "aztec-cli", "aztec-rpc", + "aztec-sandbox", "aztec.js", "circuits.js", "docs", @@ -45,7 +46,7 @@ "devDependencies": { "@monorepo-utils/workspaces-to-typescript-project-references": "^2.9.0", "eslint": "^8.21.0", - "prettier": "^2.7.1", + "prettier": "^2.8.8", "typedoc": "^0.23.26", "typescript": "^5.0.4" } diff --git a/yarn-project/tsconfig.json b/yarn-project/tsconfig.json index bfd1d34d5fba..1c73cfdd8511 100644 --- a/yarn-project/tsconfig.json +++ b/yarn-project/tsconfig.json @@ -24,6 +24,7 @@ { "path": "aztec.js/tsconfig.json" }, { "path": "aztec-node/tsconfig.json" }, { "path": "aztec-rpc/tsconfig.json" }, + { "path": "aztec-sandbox/tsconfig.json" }, { "path": "circuits.js/tsconfig.json" }, { "path": "end-to-end/tsconfig.json" }, { "path": "foundation/tsconfig.json" }, diff --git a/yarn-project/typedoc.json b/yarn-project/typedoc.json index 70ff58a3a74d..559bb768dfd7 100644 --- a/yarn-project/typedoc.json +++ b/yarn-project/typedoc.json @@ -6,6 +6,7 @@ "archiver", "aztec-cli", "aztec-rpc", + "aztec-sandbox", "aztec.js", "key-store", "noir-contracts", @@ -18,4 +19,4 @@ "world-state", "merkle-tree" ] -} +} \ No newline at end of file diff --git a/yarn-project/types/src/logs/tx_l2_logs.ts b/yarn-project/types/src/logs/tx_l2_logs.ts index 424e70e2a834..3f9fca77f268 100644 --- a/yarn-project/types/src/logs/tx_l2_logs.ts +++ b/yarn-project/types/src/logs/tx_l2_logs.ts @@ -45,8 +45,8 @@ export class TxL2Logs { * @param isLengthPrefixed - Whether the buffer is prefixed with 4 bytes for its total length. * @returns A new L2Logs object. */ - public static fromBuffer(buf: Buffer, isLengthPrefixed = true): TxL2Logs { - const reader = new BufferReader(buf, 0); + public static fromBuffer(buf: Buffer | BufferReader, isLengthPrefixed = true): TxL2Logs { + const reader = BufferReader.asReader(buf); // If the buffer is length prefixed use the length to read the array. Otherwise, the entire buffer is consumed. const logsBufLength = isLengthPrefixed ? reader.readNumber() : -1; diff --git a/yarn-project/types/src/tx.ts b/yarn-project/types/src/tx.ts index 11af9cefadc5..e0874c159b11 100644 --- a/yarn-project/types/src/tx.ts +++ b/yarn-project/types/src/tx.ts @@ -1,4 +1,4 @@ -import { KernelCircuitPublicInputs, Proof, PublicCallRequest } from '@aztec/circuits.js'; +import { Fr, KernelCircuitPublicInputs, Proof, PublicCallRequest } from '@aztec/circuits.js'; import { arrayNonEmptyLength } from '@aztec/foundation/collection'; import { EncodedContractFunction } from './contract_data.js'; @@ -10,7 +10,7 @@ import { PartialContractAddress } from './partial_contract_address.js'; * The interface of an L2 transaction. */ export class Tx { - protected constructor( + constructor( /** * Output of the private kernel circuit for this tx. */ @@ -42,7 +42,7 @@ export class Tx { // both public and private function invocations create unencrypted FunctionL2Logs object. Hence "num unencrypted" // >= "num encrypted". throw new Error( - `Number of function logs in unencrypted logs (${this.unencryptedLogs.functionLogs.length}) has to be equal + `Number of function logs in unencrypted logs (${this.unencryptedLogs.functionLogs.length}) has to be equal or larger than number function logs in encrypted logs (${this.encryptedLogs.functionLogs.length})`, ); } @@ -51,12 +51,53 @@ export class Tx { data?.end.publicCallStack && arrayNonEmptyLength(data.end.publicCallStack, item => item.isZero()); if (kernelPublicCallStackSize && kernelPublicCallStackSize > (enqueuedPublicFunctionCalls?.length ?? 0)) { throw new Error( - `Missing preimages for enqueued public function calls in kernel circuit public inputs (expected + `Missing preimages for enqueued public function calls in kernel circuit public inputs (expected ${kernelPublicCallStackSize}, got ${enqueuedPublicFunctionCalls?.length})`, ); } } + /** + * Convert a Tx class object to a plain JSON object. + * @returns A plain object with Tx properties. + */ + public toJSON() { + return { + data: this.data.toBuffer().toString('hex'), + encryptedLogs: this.encryptedLogs.toBuffer().toString('hex'), + unencryptedLogs: this.unencryptedLogs.toBuffer().toString('hex'), + proof: this.proof.toBuffer().toString('hex'), + newContractPublicFunctions: this.newContractPublicFunctions.map(f => f.toBuffer().toString('hex')) ?? [], + enqueuedPublicFunctions: this.enqueuedPublicFunctionCalls.map(f => f.toBuffer().toString('hex')) ?? [], + }; + } + + /** + * Convert a plain JSON object to a Tx class object. + * @param obj - A plain Tx JSON object. + * @returns A Tx class object. + */ + public static fromJSON(obj: any) { + const publicInputs = KernelCircuitPublicInputs.fromBuffer(Buffer.from(obj.data, 'hex')); + const encryptedLogs = TxL2Logs.fromBuffer(Buffer.from(obj.encryptedLogs, 'hex')); + const unencryptedLogs = TxL2Logs.fromBuffer(Buffer.from(obj.unencryptedLogs, 'hex')); + const proof = Buffer.from(obj.proof, 'hex'); + const newContractPublicFunctions = obj.newContractPublicFunctions + ? obj.newContractPublicFunctions.map((x: string) => EncodedContractFunction.fromBuffer(Buffer.from(x, 'hex'))) + : []; + const enqueuedPublicFunctions = obj.enqueuedPublicFunctions + ? obj.enqueuedPublicFunctions.map((x: string) => PublicCallRequest.fromBuffer(Buffer.from(x, 'hex'))) + : []; + return Tx.createTx( + publicInputs, + Proof.fromBuffer(proof), + encryptedLogs, + unencryptedLogs, + newContractPublicFunctions, + enqueuedPublicFunctions, + ); + } + /** * Creates a new private transaction. * @param data - Public inputs of the private kernel circuit. @@ -141,4 +182,15 @@ export class ContractDeploymentTx { */ public readonly partialContractAddress: PartialContractAddress, ) {} + + toJSON() { + return { + tx: this.tx.toJSON(), + partialContractAddress: this.partialContractAddress.toBuffer().toString(), + }; + } + + static fromJSON(obj: any) { + return new ContractDeploymentTx(Tx.fromJSON(obj.tx), Fr.fromBuffer(Buffer.from(obj.partialContractAddress, 'hex'))); + } } diff --git a/yarn-project/types/src/tx_hash.ts b/yarn-project/types/src/tx_hash.ts index c87e88d86fa6..d85583b17aa1 100644 --- a/yarn-project/types/src/tx_hash.ts +++ b/yarn-project/types/src/tx_hash.ts @@ -48,6 +48,7 @@ export class TxHash { public toString() { return this.buffer.toString('hex'); } + /** * Convert this hash to a big int. * @returns The big int. @@ -76,4 +77,13 @@ export class TxHash { const padded = Buffer.concat([Buffer.alloc(this.SIZE - 28), buffer]); return new TxHash(padded); } + + /** + * Converts a string into a TxHash object. + * @param str - The TX hash in string format. + * @returns A new TxHash object. + */ + public static fromString(str: string): TxHash { + return new TxHash(Buffer.from(str, 'hex')); + } } diff --git a/yarn-project/yarn-project-base/Dockerfile b/yarn-project/yarn-project-base/Dockerfile index b23b7b0e6e92..ed613f80612f 100644 --- a/yarn-project/yarn-project-base/Dockerfile +++ b/yarn-project/yarn-project-base/Dockerfile @@ -22,6 +22,7 @@ COPY archiver/package.json archiver/package.json COPY aztec-cli/package.json aztec-cli/package.json COPY aztec-rpc/package.json aztec-rpc/package.json COPY aztec-node/package.json aztec-node/package.json +COPY aztec-sandbox/package.json aztec-sandbox/package.json COPY aztec.js/package.json aztec.js/package.json COPY docs/package.json docs/package.json COPY end-to-end/package.json end-to-end/package.json @@ -80,6 +81,7 @@ COPY archiver/tsconfig.json archiver/tsconfig.json COPY aztec-cli/tsconfig.json aztec-cli/tsconfig.json COPY aztec-rpc/tsconfig.json aztec-rpc/tsconfig.json COPY aztec-node/tsconfig.json aztec-node/tsconfig.json +COPY aztec-sandbox/tsconfig.json aztec-sandbox/tsconfig.json COPY aztec.js/tsconfig.json aztec.js/tsconfig.json COPY end-to-end/tsconfig.json end-to-end/tsconfig.json COPY ethereum/tsconfig.json ethereum/tsconfig.json diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index d0bd3eed7b75..4346ff453126 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -196,6 +196,27 @@ __metadata: languageName: unknown linkType: soft +"@aztec/aztec-sandbox@workspace:aztec-sandbox": + version: 0.0.0-use.local + resolution: "@aztec/aztec-sandbox@workspace:aztec-sandbox" + dependencies: + "@aztec/aztec-node": "workspace:^" + "@aztec/aztec-rpc": "workspace:^" + "@aztec/aztec.js": "workspace:^" + "@aztec/ethereum": "workspace:^" + "@aztec/foundation": "workspace:^" + "@jest/globals": ^29.5.0 + "@types/jest": ^29.5.0 + abitype: ^0.8.11 + jest: ^29.5.0 + koa-router: ^12.0.0 + ts-jest: ^29.1.0 + ts-node: ^10.9.1 + typescript: ^5.0.4 + viem: ^0.3.14 + languageName: unknown + linkType: soft + "@aztec/aztec.js@workspace:^, @aztec/aztec.js@workspace:aztec.js": version: 0.0.0-use.local resolution: "@aztec/aztec.js@workspace:aztec.js" @@ -223,7 +244,7 @@ __metadata: dependencies: "@monorepo-utils/workspaces-to-typescript-project-references": ^2.9.0 eslint: ^8.21.0 - prettier: ^2.7.1 + prettier: ^2.8.8 typedoc: ^0.23.26 typescript: ^5.0.4 languageName: unknown @@ -3535,6 +3556,19 @@ __metadata: languageName: node linkType: hard +"abitype@npm:^0.8.11": + version: 0.8.11 + resolution: "abitype@npm:0.8.11" + peerDependencies: + typescript: ">=5.0.4" + zod: ^3 >=3.19.1 + peerDependenciesMeta: + zod: + optional: true + checksum: 94e6ad5d3d3851f68ea54d090312d35e38aa15d19b65d25f02d8c54400b184a87b121adb23e930f2e92d597e9290e8ca4f2ff70751e6dada4e8f1440948e0c44 + languageName: node + linkType: hard + "abortable-iterator@npm:^5.0.0, abortable-iterator@npm:^5.0.1": version: 5.0.1 resolution: "abortable-iterator@npm:5.0.1" @@ -8234,6 +8268,15 @@ __metadata: languageName: node linkType: hard +"prettier@npm:^2.8.8": + version: 2.8.8 + resolution: "prettier@npm:2.8.8" + bin: + prettier: bin-prettier.js + checksum: b49e409431bf129dd89238d64299ba80717b57ff5a6d1c1a8b1a28b590d998a34e083fa13573bc732bb8d2305becb4c9a4407f8486c81fa7d55100eb08263cf8 + languageName: node + linkType: hard + "pretty-format@npm:^29.0.0, pretty-format@npm:^29.5.0": version: 29.5.0 resolution: "pretty-format@npm:29.5.0"