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/cross_chain/test_harness.ts b/yarn-project/end-to-end/src/cross_chain/test_harness.ts index a9e56bcb95a5..20c97fe4dbb2 100644 --- a/yarn-project/end-to-end/src/cross_chain/test_harness.ts +++ b/yarn-project/end-to-end/src/cross_chain/test_harness.ts @@ -4,7 +4,7 @@ import { AztecAddress, EthAddress, Fr, Point } from '@aztec/circuits.js'; import { DeployL1Contracts } from '@aztec/ethereum'; import { DebugLogger } from '@aztec/foundation/log'; import { PublicClient, HttpTransport, Chain, getContract } from 'viem'; -import { deployAndInitializeNonNativeL2TokenContracts, pointToPublicKey } from '../utils.js'; +import { deployAndInitializeNonNativeL2TokenContracts, expectStorageSlot, pointToPublicKey } from '../utils.js'; import { OutboxAbi } from '@aztec/l1-artifacts'; import { sha256ToField } from '@aztec/foundation/crypto'; import { toBufferBE } from '@aztec/foundation/bigint-buffer'; @@ -120,6 +120,10 @@ export class CrossChainTestHarness { expect(await this.underlyingERC20.read.balanceOf([this.ethAccount.toString()])).toBe(amount); } + async getL1BalanceOf(address: EthAddress) { + return await this.underlyingERC20.read.balanceOf([address.toString()]); + } + async sendTokensToPortal(bridgeAmount: bigint, secretHash: Fr) { await this.underlyingERC20.write.approve([this.tokenPortalAddress.toString(), bridgeAmount], {} as any); @@ -158,6 +162,53 @@ export class CrossChainTestHarness { expect(transferReceipt.status).toBe(TxStatus.MINED); } + async consumeMessageOnAztecAndMintSecretly(bridgeAmount: bigint, messageKey: Fr, secret: Fr) { + this.logger('Consuming messages on L2 secretively'); + // Call the mint tokens function on the noir contract + const consumptionTx = this.l2Contract.methods + .mint(bridgeAmount, this.ownerPub, this.ownerAddress, messageKey, secret, this.ethAccount.toField()) + .send({ from: this.ownerAddress }); + + await consumptionTx.isMined(0, 0.1); + const consumptionReceipt = await consumptionTx.getReceipt(); + expect(consumptionReceipt.status).toBe(TxStatus.MINED); + } + + async consumeMessageOnAztecAndMintPublicly(bridgeAmount: bigint, messageKey: Fr, secret: Fr) { + this.logger('Consuming messages on L2 Publicly'); + // Call the mint tokens function on the noir contract + const consumptionTx = this.l2Contract.methods + .mintPublic(bridgeAmount, this.ownerAddress, messageKey, secret, this.ethAccount.toField()) + .send({ from: this.ownerAddress }); + + await consumptionTx.isMined(0, 0.1); + const consumptionReceipt = await consumptionTx.getReceipt(); + expect(consumptionReceipt.status).toBe(TxStatus.MINED); + } + + async getL2BalanceOf(owner: AztecAddress) { + const ownerPublicKey = await this.aztecRpcServer.getAccountPublicKey(owner); + const [balance] = await this.l2Contract.methods.getBalance(pointToPublicKey(ownerPublicKey)).view({ from: owner }); + return balance; + } + + async expectBalanceOnL2(owner: AztecAddress, expectedBalance: bigint) { + const balance = await this.getL2BalanceOf(owner); + this.logger(`Account ${owner} balance: ${balance}`); + expect(balance).toBe(expectedBalance); + } + + async expectPublicBalanceOnL2(owner: AztecAddress, expectedBalance: bigint, publicBalanceSlot: bigint) { + await expectStorageSlot( + this.logger, + this.aztecNode, + this.l2Contract, + publicBalanceSlot, + owner.toField(), + expectedBalance, + ); + } + async checkEntryIsNotInOutbox(withdrawAmount: bigint, callerOnL1: EthAddress = EthAddress.ZERO): Promise { this.logger('Ensure that the entry is not in outbox yet'); const contractInfo = await this.aztecNode.getContractInfo(this.l2Contract.address); diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts index 31fa7bdc30ef..23d13a2d340d 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts @@ -2,7 +2,7 @@ import { AztecNodeService } from '@aztec/aztec-node'; import { AztecAddress, AztecRPCServer, Contract, TxStatus } from '@aztec/aztec.js'; import { EthAddress } from '@aztec/foundation/eth-address'; -import { Fr, Point } from '@aztec/foundation/fields'; +import { Point } from '@aztec/foundation/fields'; import { DebugLogger } from '@aztec/foundation/log'; import { delay, pointToPublicKey, setup } from './utils.js'; import { CrossChainTestHarness } from './cross_chain/test_harness.js'; @@ -65,19 +65,6 @@ describe('e2e_cross_chain_messaging', () => { expect(balance).toBe(expectedBalance); }; - const consumeMessageOnAztecAndMint = async (bridgeAmount: bigint, messageKey: Fr, secret: Fr) => { - logger('Consuming messages on L2 secretively'); - // Call the mint tokens function on the noir contract - const consumptionTx = l2Contract.methods - .mint(bridgeAmount, ownerPub, ownerAddress, messageKey, secret, ethAccount.toField()) - .send({ from: ownerAddress }); - - await consumptionTx.isMined(0, 0.1); - const consumptionReceipt = await consumptionTx.getReceipt(); - - expect(consumptionReceipt.status).toBe(TxStatus.MINED); - }; - const withdrawFundsFromAztec = async (withdrawAmount: bigint) => { logger('Send L2 tx to withdraw funds'); const withdrawTx = l2Contract.methods @@ -108,7 +95,7 @@ describe('e2e_cross_chain_messaging', () => { const transferAmount = 1n; await crossChainTestHarness.performL2Transfer(transferAmount); - await consumeMessageOnAztecAndMint(bridgeAmount, messageKey, secret); + await crossChainTestHarness.consumeMessageOnAztecAndMintSecretly(bridgeAmount, messageKey, secret); await expectBalance(ownerAddress, bridgeAmount + initialBalance - transferAmount); // time to withdraw the funds again! diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts index 9ebb9f97c5e2..f5ad44d3eec6 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts @@ -2,9 +2,8 @@ import { AztecNodeService } from '@aztec/aztec-node'; import { AztecAddress, AztecRPCServer, Contract, TxStatus } from '@aztec/aztec.js'; import { EthAddress } from '@aztec/foundation/eth-address'; -import { Fr } from '@aztec/foundation/fields'; import { DebugLogger } from '@aztec/foundation/log'; -import { delay, expectStorageSlot, setup } from './utils.js'; +import { delay, setup } from './utils.js'; import { CrossChainTestHarness } from './cross_chain/test_harness.js'; describe('e2e_public_cross_chain_messaging', () => { @@ -58,19 +57,6 @@ describe('e2e_public_cross_chain_messaging', () => { await crossChainTestHarness?.stop(); }); - const consumeMessageOnAztec = async (bridgeAmount: bigint, messageKey: Fr, secret: Fr) => { - logger('Consuming messages on L2 Publicly'); - // Call the mint tokens function on the noir contract - const consumptionTx = l2Contract.methods - .mintPublic(bridgeAmount, ownerAddress, messageKey, secret, ethAccount.toField()) - .send({ from: ownerAddress }); - - await consumptionTx.isMined(0, 0.1); - const consumptionReceipt = await consumptionTx.getReceipt(); - - expect(consumptionReceipt.status).toBe(TxStatus.MINED); - }; - const withdrawFundsFromAztec = async (withdrawAmount: bigint) => { logger('Send L2 tx to withdraw funds'); const withdrawTx = l2Contract.methods @@ -102,22 +88,15 @@ describe('e2e_public_cross_chain_messaging', () => { const transferAmount = 1n; await crossChainTestHarness.performL2Transfer(transferAmount); - await consumeMessageOnAztec(bridgeAmount, messageKey, secret); - await expectStorageSlot(logger, aztecNode, l2Contract, publicBalanceSlot, ownerAddress.toField(), bridgeAmount); + await crossChainTestHarness.consumeMessageOnAztecAndMintPublicly(bridgeAmount, messageKey, secret); + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount, publicBalanceSlot); // time to withdraw the funds again! logger('Withdrawing funds from L2'); const withdrawAmount = 9n; const entryKey = await crossChainTestHarness.checkEntryIsNotInOutbox(withdrawAmount); await withdrawFundsFromAztec(withdrawAmount); - await expectStorageSlot( - logger, - aztecNode, - l2Contract, - publicBalanceSlot, - ownerAddress.toField(), - bridgeAmount - withdrawAmount, - ); + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount - withdrawAmount, publicBalanceSlot); // Check balance before and after exit. expect(await underlyingERC20.read.balanceOf([ethAccount.toString()])).toBe(l1TokenBalance - bridgeAmount); 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/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts b/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts index c70fbbfb19d0..dcca1928fe86 100644 --- a/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts +++ b/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts @@ -1,19 +1,12 @@ import { AztecNodeService } from '@aztec/aztec-node'; -import { - AztecAddress, - AztecRPCServer, - Contract, - ContractDeployer, - Fr, - TxStatus, - computeMessageSecretHash, -} from '@aztec/aztec.js'; +import { AztecAddress, AztecRPCServer, Contract, ContractDeployer, Fr, TxStatus } from '@aztec/aztec.js'; import { deployL1Contract } from '@aztec/ethereum'; import { EthAddress } from '@aztec/foundation/eth-address'; import { delay, deployAndInitializeNonNativeL2TokenContracts, pointToPublicKey, setup } from './utils.js'; +import { CrossChainTestHarness } from './cross_chain/test_harness.js'; import { DebugLogger } from '@aztec/foundation/log'; -import { Chain, HttpTransport, PublicClient, getContract, parseEther } from 'viem'; +import { getContract, parseEther } from 'viem'; import { DeployL1Contracts } from '@aztec/ethereum'; import { UniswapPortalAbi, UniswapPortalBytecode } from '@aztec/l1-artifacts'; import { UniswapContractAbi } from '@aztec/noir-contracts/examples'; @@ -36,9 +29,6 @@ describe('uniswap_trade_on_l1_from_l2', () => { let accounts: AztecAddress[]; let logger: DebugLogger; - let publicClient: PublicClient; - let walletClient: any; - let ethAccount: EthAddress; let ownerAddress: AztecAddress; let receiver: AztecAddress; @@ -46,25 +36,19 @@ describe('uniswap_trade_on_l1_from_l2', () => { const initialBalance = 10n; const wethAmountToBridge = parseEther('1'); + let daiCrossChainHarness: CrossChainTestHarness; + let wethCrossChainHarness: CrossChainTestHarness; + let uniswapPortal: any; let uniswapPortalAddress: EthAddress; let uniswapL2Contract: Contract; - let wethContract: any; - let wethTokenPortalAddress: EthAddress; - let wethTokenPortal: any; - let wethL2Contract: Contract; - - let daiContract: any; - let daiTokenPortalAddress: EthAddress; - let daiL2Contract: Contract; - beforeEach(async () => { let deployL1ContractsValues: DeployL1Contracts; ({ aztecNode, aztecRpcServer, deployL1ContractsValues, accounts, logger } = await setup(2)); - walletClient = deployL1ContractsValues.walletClient; - publicClient = deployL1ContractsValues.publicClient; + const walletClient = deployL1ContractsValues.walletClient; + const publicClient = deployL1ContractsValues.publicClient; 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'); @@ -72,7 +56,8 @@ describe('uniswap_trade_on_l1_from_l2', () => { ethAccount = EthAddress.fromString((await walletClient.getAddresses())[0]); [ownerAddress, receiver] = accounts; - ownerPub = pointToPublicKey(await aztecRpcServer.getAccountPublicKey(ownerAddress)); + const ownerPubPoint = await aztecRpcServer.getAccountPublicKey(ownerAddress); + ownerPub = pointToPublicKey(ownerPubPoint); logger('Deploying DAI Portal, initializing and deploying l2 contract...'); const daiContracts = await deployAndInitializeNonNativeL2TokenContracts( @@ -84,9 +69,23 @@ describe('uniswap_trade_on_l1_from_l2', () => { ownerPub, DAI_ADDRESS, ); - daiL2Contract = daiContracts.l2Contract; - daiContract = daiContracts.underlyingERC20; - daiTokenPortalAddress = daiContracts.tokenPortalAddress; + daiCrossChainHarness = new CrossChainTestHarness( + aztecNode, + aztecRpcServer, + accounts, + logger, + daiContracts.l2Contract, + ethAccount, + daiContracts.tokenPortalAddress, + daiContracts.tokenPortal, + daiContracts.underlyingERC20, + null, + publicClient, + walletClient, + ownerAddress, + receiver, + ownerPubPoint, + ); logger('Deploying WETH Portal, initializing and deploying l2 contract...'); const wethContracts = await deployAndInitializeNonNativeL2TokenContracts( @@ -98,10 +97,23 @@ describe('uniswap_trade_on_l1_from_l2', () => { ownerPub, WETH9_ADDRESS, ); - wethL2Contract = wethContracts.l2Contract; - wethContract = wethContracts.underlyingERC20; - wethTokenPortal = wethContracts.tokenPortal; - wethTokenPortalAddress = wethContracts.tokenPortalAddress; + wethCrossChainHarness = new CrossChainTestHarness( + aztecNode, + aztecRpcServer, + accounts, + logger, + wethContracts.l2Contract, + ethAccount, + wethContracts.tokenPortalAddress, + wethContracts.tokenPortal, + wethContracts.underlyingERC20, + null, + publicClient, + walletClient, + ownerAddress, + receiver, + ownerPubPoint, + ); logger('Deploy Uniswap portal on L1 and L2...'); uniswapPortalAddress = await deployL1Contract(walletClient, publicClient, UniswapPortalAbi, UniswapPortalBytecode); @@ -130,91 +142,50 @@ describe('uniswap_trade_on_l1_from_l2', () => { }, 100_000); afterEach(async () => { - await aztecNode?.stop(); - await aztecRpcServer?.stop(); + await aztecNode.stop(); + await aztecRpcServer.stop(); + await wethCrossChainHarness.stop(); + await daiCrossChainHarness.stop(); }); - const getL2BalanceOf = async (owner: AztecAddress, l2Contract: any) => { - const ownerPublicKey = await aztecRpcServer.getAccountPublicKey(owner); - const [balance] = await l2Contract.methods.getBalance(pointToPublicKey(ownerPublicKey)).view({ from: owner }); - return balance; - }; - - const expectBalanceOnL2 = async (owner: AztecAddress, expectedBalance: bigint, l2Contract: any) => { - const balance = await getL2BalanceOf(owner, l2Contract); - logger(`Account ${owner} balance: ${balance}`); - expect(balance).toBe(expectedBalance); - }; - - const transferWethOnL2 = async (transferAmount: bigint) => { - const transferTx = wethL2Contract.methods - .transfer( - transferAmount, - pointToPublicKey(await aztecRpcServer.getAccountPublicKey(ownerAddress)), - pointToPublicKey(await aztecRpcServer.getAccountPublicKey(receiver)), - ) - .send({ from: accounts[0] }); - await transferTx.isMined(0, 0.1); - const transferReceipt = await transferTx.getReceipt(); - expect(transferReceipt.status).toBe(TxStatus.MINED); - }; - it('should uniswap trade on L1 from L2 funds privately (swaps WETH -> DAI)', async () => { - const meBeforeBalance = await wethContract.read.balanceOf([ethAccount.toString()]); - // 1. Approve weth to be bridged - await wethContract.write.approve([wethTokenPortalAddress.toString(), wethAmountToBridge], {} as any); + const meBeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ethAccount); - // 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 = [ownerAddress.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 messageKey = Fr.fromString(messageKeyHex); + // 1. Approve and deposit weth to the portal and move to L2 + const [secret, secretHash] = await wethCrossChainHarness.generateClaimSecret(); + const messageKey = await wethCrossChainHarness.sendTokensToPortal(wethAmountToBridge, secretHash); + expect(await wethCrossChainHarness.getL1BalanceOf(ethAccount)).toBe(meBeforeBalance - wethAmountToBridge); // 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(transferAmount); + await wethCrossChainHarness.performL2Transfer(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, ownerAddress, messageKey, secret, ethAccount.toField()) - .send({ from: ownerAddress }); - await consumptionTx.isMined(0, 0.1); - const consumptionReceipt = await consumptionTx.getReceipt(); - expect(consumptionReceipt.status).toBe(TxStatus.MINED); - await expectBalanceOnL2(ownerAddress, wethAmountToBridge + initialBalance - transferAmount, wethL2Contract); + await wethCrossChainHarness.consumeMessageOnAztecAndMintSecretly(wethAmountToBridge, messageKey, secret); + await wethCrossChainHarness.expectBalanceOnL2(ownerAddress, wethAmountToBridge + initialBalance - transferAmount); // Store balances - const wethBalanceBeforeSwap = await getL2BalanceOf(ownerAddress, wethL2Contract); - const daiBalanceBeforeSwap = await getL2BalanceOf(ownerAddress, daiL2Contract); + const wethBalanceBeforeSwap = await wethCrossChainHarness.getL2BalanceOf(ownerAddress); + const daiBalanceBeforeSwap = await daiCrossChainHarness.getL2BalanceOf(ownerAddress); // 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 selector = Fr.fromBuffer(wethCrossChainHarness.l2Contract.methods.withdraw.selector); const minimumOutputAmount = 0; const withdrawTx = uniswapL2Contract.methods .swap( selector, - wethL2Contract.address.toField(), - wethTokenPortalAddress.toField(), + wethCrossChainHarness.l2Contract.address.toField(), + wethCrossChainHarness.tokenPortalAddress.toField(), wethAmountToBridge, new Fr(3000), - daiL2Contract.address.toField(), - daiTokenPortalAddress.toField(), + daiCrossChainHarness.l2Contract.address.toField(), + daiCrossChainHarness.tokenPortalAddress.toField(), new Fr(minimumOutputAmount), ownerPub, ownerAddress, @@ -230,19 +201,20 @@ describe('uniswap_trade_on_l1_from_l2', () => { expect(withdrawReceipt.status).toBe(TxStatus.MINED); // check weth balance of owner on L2 (we first briedged `wethAmountToBridge` into L2 and now withdrew it!) - await expectBalanceOnL2(ownerAddress, initialBalance - transferAmount, wethL2Contract); + await wethCrossChainHarness.expectBalanceOnL2(ownerAddress, initialBalance - transferAmount); // 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()]); + const daiBalanceOfPortalBefore = await daiCrossChainHarness.getL1BalanceOf(daiCrossChainHarness.tokenPortalAddress); + const deadline = 2 ** 32 - 1; // max uint32 - 1 const swapArgs = [ - wethTokenPortalAddress.toString(), + wethCrossChainHarness.tokenPortalAddress.toString(), wethAmountToBridge, 3000, - daiTokenPortalAddress.toString(), + daiCrossChainHarness.tokenPortalAddress.toString(), minimumOutputAmount, ownerAddress.toString(), - secretString, + secretHash.toString(true), deadline, ethAccount.toString(), true, @@ -254,28 +226,22 @@ describe('uniswap_trade_on_l1_from_l2', () => { 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()]); + const daiBalanceOfPortalAfter = await daiCrossChainHarness.getL1BalanceOf(daiCrossChainHarness.tokenPortalAddress); expect(daiBalanceOfPortalAfter).toBeGreaterThan(daiBalanceOfPortalBefore); - const daiAmountToBridge = daiBalanceOfPortalAfter - daiBalanceOfPortalBefore; + const daiAmountToBridge = BigInt(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(transferAmount); + await wethCrossChainHarness.performL2Transfer(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, ownerAddress, depositDaiMessageKey, secret, ethAccount.toField()) - .send({ from: ownerAddress }); - await daiMintTx.isMined(0, 0.1); - const daiMintTxReceipt = await daiMintTx.getReceipt(); - expect(daiMintTxReceipt.status).toBe(TxStatus.MINED); - await expectBalanceOnL2(ownerAddress, initialBalance + BigInt(daiAmountToBridge), daiL2Contract); + await daiCrossChainHarness.consumeMessageOnAztecAndMintSecretly(daiAmountToBridge, depositDaiMessageKey, secret); + await daiCrossChainHarness.expectBalanceOnL2(ownerAddress, initialBalance + daiAmountToBridge); - const wethBalanceAfterSwap = await getL2BalanceOf(ownerAddress, wethL2Contract); - const daiBalanceAfterSwap = await getL2BalanceOf(ownerAddress, daiL2Contract); + const wethBalanceAfterSwap = await wethCrossChainHarness.getL2BalanceOf(ownerAddress); + const daiBalanceAfterSwap = await daiCrossChainHarness.getL2BalanceOf(ownerAddress); logger('WETH balance before swap: ', wethBalanceBeforeSwap.toString()); logger('DAI balance before swap : ', daiBalanceBeforeSwap.toString()); 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"