diff --git a/.circleci/config.yml b/.circleci/config.yml index 5a65e02568e..9d9c26a0842 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -915,6 +915,17 @@ jobs: name: "Test" command: cond_spot_run_tests end-to-end e2e_aztec_js_browser.test.ts docker-compose-e2e-sandbox.yml + aztec-rpc-sandbox: + docker: + - image: aztecprotocol/alpine-build-image + resource_class: small + steps: + - *checkout + - *setup_env + - run: + name: "Test" + command: cond_spot_run_tests end-to-end aztec_rpc_sandbox.test.ts docker-compose-e2e-sandbox.yml + e2e-canary-test: docker: - image: aztecprotocol/alpine-build-image @@ -1307,6 +1318,7 @@ workflows: - e2e-p2p: *e2e_test - e2e-canary-test: *e2e_test - e2e-browser-sandbox: *e2e_test + - aztec-rpc-sandbox: *e2e_test - e2e-end: requires: @@ -1332,6 +1344,7 @@ workflows: - e2e-p2p - e2e-browser-sandbox - e2e-canary-test + - aztec-rpc-sandbox <<: *defaults - deploy-dockerhub: diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 17b34b7abe9..d56ee76e16d 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -71,7 +71,7 @@ export class AztecNodeService implements AztecNode { // first create and sync the archiver const archiver = await Archiver.createAndSync(config); - // we idenfity the P2P transaction protocol by using the rollup contract address. + // we identify the P2P transaction protocol by using the rollup contract address. // this may well change in future config.transactionProtocol = `/aztec/tx/${config.rollupContract.toString()}`; diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.test.ts b/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.test.ts deleted file mode 100644 index 0a672a468e5..00000000000 --- a/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { AztecAddress, CompleteAddress, Fr } from '@aztec/circuits.js'; -import { Grumpkin } from '@aztec/circuits.js/barretenberg'; -import { ConstantKeyPair, TestKeyStore } from '@aztec/key-store'; -import { AztecNode } from '@aztec/types'; - -import { mock } from 'jest-mock-extended'; - -import { MemoryDB } from '../database/memory_db.js'; -import { RpcServerConfig } from '../index.js'; -import { AztecRPCServer } from './aztec_rpc_server.js'; - -describe('AztecRpcServer', function () { - let rpcServer: AztecRPCServer; - - beforeEach(async () => { - const keyStore = new TestKeyStore(await Grumpkin.new()); - const node = mock(); - const db = new MemoryDB(); - const config: RpcServerConfig = { - l2BlockPollingIntervalMS: 100, - }; - rpcServer = new AztecRPCServer(keyStore, node, db, config); - }); - - it('registers an account and returns it as an account only and not as a recipient', async () => { - const keyPair = ConstantKeyPair.random(await Grumpkin.new()); - const completeAddress = await CompleteAddress.fromPrivateKey(await keyPair.getPrivateKey()); - - await rpcServer.registerAccount(await keyPair.getPrivateKey(), completeAddress); - const accounts = await rpcServer.getAccounts(); - const recipients = await rpcServer.getRecipients(); - expect(accounts).toEqual([completeAddress]); - expect(recipients).toEqual([]); - }); - - it('registers a recipient and returns it as a recipient only and not as an account', async () => { - const completeAddress = await CompleteAddress.random(); - - await rpcServer.registerRecipient(completeAddress); - const accounts = await rpcServer.getAccounts(); - const recipients = await rpcServer.getRecipients(); - expect(accounts).toEqual([]); - expect(recipients).toEqual([completeAddress]); - }); - - it('cannot register the same account twice', async () => { - const keyPair = ConstantKeyPair.random(await Grumpkin.new()); - const completeAddress = await CompleteAddress.fromPrivateKey(await keyPair.getPrivateKey()); - - await rpcServer.registerAccount(await keyPair.getPrivateKey(), completeAddress); - await expect(async () => rpcServer.registerAccount(await keyPair.getPrivateKey(), completeAddress)).rejects.toThrow( - `Complete address corresponding to ${completeAddress.address} already exists`, - ); - }); - - it('cannot register the same recipient twice', async () => { - const completeAddress = await CompleteAddress.random(); - - await rpcServer.registerRecipient(completeAddress); - await expect(() => rpcServer.registerRecipient(completeAddress)).rejects.toThrow( - `Complete address corresponding to ${completeAddress.address} already exists`, - ); - }); - - it('throws when getting public storage for non-existent contract', async () => { - const contract = AztecAddress.random(); - await expect(async () => await rpcServer.getPublicStorageAt(contract, new Fr(0n))).rejects.toThrow( - `Contract ${contract.toString()} is not deployed`, - ); - }); -}); 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 bdf4488a2ff..672ef0e9e08 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 @@ -100,12 +100,9 @@ export class AztecRPCServer implements AztecRPC { return accounts; } - public async getAccount(address: AztecAddress): Promise { + public async getAccount(address: AztecAddress): Promise { const result = await this.getAccounts(); const account = result.find(r => r.address.equals(address)); - if (!account) { - throw new Error(`Unable to get complete address for address ${address.toString()}`); - } return Promise.resolve(account); } @@ -123,12 +120,9 @@ export class AztecRPCServer implements AztecRPC { return recipients; } - public async getRecipient(address: AztecAddress): Promise { + public async getRecipient(address: AztecAddress): Promise { const result = await this.getRecipients(); const recipient = result.find(r => r.address.equals(address)); - if (!recipient) { - throw new Error(`Unable to get complete address for address ${address.toString()}`); - } return Promise.resolve(recipient); } @@ -142,6 +136,10 @@ export class AztecRPCServer implements AztecRPC { } } + public async getContracts(): Promise { + return (await this.db.getContracts()).map(c => c.address); + } + public async getPublicStorageAt(contract: AztecAddress, storageSlot: Fr) { if ((await this.getContractData(contract)) === undefined) { throw new Error(`Contract ${contract.toString()} is not deployed`); diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_server/index.ts b/yarn-project/aztec-rpc/src/aztec_rpc_server/index.ts index 3af43b1a499..1cc9adbe3cb 100644 --- a/yarn-project/aztec-rpc/src/aztec_rpc_server/index.ts +++ b/yarn-project/aztec-rpc/src/aztec_rpc_server/index.ts @@ -1,2 +1,3 @@ export * from './aztec_rpc_server.js'; export * from './create_aztec_rpc_server.js'; +export { aztecRpcTestSuite } from './test/aztec_rpc_test_suite.js'; diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_server.test.ts b/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_server.test.ts new file mode 100644 index 00000000000..cc572ee3003 --- /dev/null +++ b/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_server.test.ts @@ -0,0 +1,29 @@ +import { Grumpkin } from '@aztec/circuits.js/barretenberg'; +import { TestKeyStore } from '@aztec/key-store'; +import { AztecNode, AztecRPC } from '@aztec/types'; + +import { mock } from 'jest-mock-extended'; + +import { MemoryDB } from '../../database/memory_db.js'; +import { EthAddress, RpcServerConfig } from '../../index.js'; +import { AztecRPCServer } from '../aztec_rpc_server.js'; +import { aztecRpcTestSuite } from './aztec_rpc_test_suite.js'; + +async function createAztecRpcServer(): Promise { + const keyStore = new TestKeyStore(await Grumpkin.new()); + const node = mock(); + const db = new MemoryDB(); + const config: RpcServerConfig = { + l2BlockPollingIntervalMS: 100, + }; + + // Setup the relevant mocks + node.getBlockHeight.mockResolvedValue(2); + node.getVersion.mockResolvedValue(1); + node.getChainId.mockResolvedValue(1); + node.getRollupAddress.mockResolvedValue(EthAddress.random()); + + return new AztecRPCServer(keyStore, node, db, config); +} + +aztecRpcTestSuite('AztecRPCServer', createAztecRpcServer); diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_test_suite.ts b/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_test_suite.ts new file mode 100644 index 00000000000..7b12753bf5c --- /dev/null +++ b/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_test_suite.ts @@ -0,0 +1,131 @@ +import { AztecAddress, CompleteAddress, Fr, FunctionData, TxContext } from '@aztec/circuits.js'; +import { Grumpkin } from '@aztec/circuits.js/barretenberg'; +import { ConstantKeyPair } from '@aztec/key-store'; +import { + AztecRPC, + DeployedContract, + INITIAL_L2_BLOCK_NUM, + TxExecutionRequest, + randomDeployedContract, +} from '@aztec/types'; + +export const aztecRpcTestSuite = (testName: string, aztecRpcSetup: () => Promise) => { + describe(testName, function () { + let rpc: AztecRPC; + + beforeAll(async () => { + rpc = await aztecRpcSetup(); + }, 120_000); + + it('registers an account and returns it as an account only and not as a recipient', async () => { + const keyPair = ConstantKeyPair.random(await Grumpkin.new()); + const completeAddress = await CompleteAddress.fromPrivateKey(await keyPair.getPrivateKey()); + + await rpc.registerAccount(await keyPair.getPrivateKey(), completeAddress); + + // Check that the account is correctly registered using the getAccounts and getRecipients methods + const accounts = await rpc.getAccounts(); + const recipients = await rpc.getRecipients(); + expect(accounts).toContainEqual(completeAddress); + expect(recipients).not.toContainEqual(completeAddress); + + // Check that the account is correctly registered using the getAccount and getRecipient methods + const account = await rpc.getAccount(completeAddress.address); + const recipient = await rpc.getRecipient(completeAddress.address); + expect(account).toEqual(completeAddress); + expect(recipient).toBeUndefined(); + }); + + it('registers a recipient and returns it as a recipient only and not as an account', async () => { + const completeAddress = await CompleteAddress.random(); + + await rpc.registerRecipient(completeAddress); + + // Check that the recipient is correctly registered using the getAccounts and getRecipients methods + const accounts = await rpc.getAccounts(); + const recipients = await rpc.getRecipients(); + expect(accounts).not.toContainEqual(completeAddress); + expect(recipients).toContainEqual(completeAddress); + + // Check that the recipient is correctly registered using the getAccount and getRecipient methods + const account = await rpc.getAccount(completeAddress.address); + const recipient = await rpc.getRecipient(completeAddress.address); + expect(account).toBeUndefined(); + expect(recipient).toEqual(completeAddress); + }); + + it('cannot register the same account twice', async () => { + const keyPair = ConstantKeyPair.random(await Grumpkin.new()); + const completeAddress = await CompleteAddress.fromPrivateKey(await keyPair.getPrivateKey()); + + await rpc.registerAccount(await keyPair.getPrivateKey(), completeAddress); + await expect(async () => rpc.registerAccount(await keyPair.getPrivateKey(), completeAddress)).rejects.toThrow( + `Complete address corresponding to ${completeAddress.address} already exists`, + ); + }); + + it('cannot register the same recipient twice', async () => { + const completeAddress = await CompleteAddress.random(); + + await rpc.registerRecipient(completeAddress); + await expect(() => rpc.registerRecipient(completeAddress)).rejects.toThrow( + `Complete address corresponding to ${completeAddress.address} already exists`, + ); + }); + + it('successfully adds a contract', async () => { + const contracts: DeployedContract[] = [randomDeployedContract(), randomDeployedContract()]; + await rpc.addContracts(contracts); + + const expectedContractAddresses = contracts.map(contract => contract.address); + const contractAddresses = await rpc.getContracts(); + + // check if all the contracts were returned + expect(contractAddresses).toEqual(expect.arrayContaining(expectedContractAddresses)); + }); + + it('throws when simulating a tx targeting public entrypoint', async () => { + const functionData = FunctionData.empty(); + functionData.isPrivate = false; + const txExecutionRequest = new TxExecutionRequest( + AztecAddress.random(), + functionData, + new Fr(0), + TxContext.empty(), + [], + ); + + await expect(async () => await rpc.simulateTx(txExecutionRequest)).rejects.toThrow( + 'Public entrypoints are not allowed', + ); + }); + + // Note: Not testing a successful run of `simulateTx`, `sendTx`, `getTxReceipt` and `viewTx` here as it requires + // a larger setup and it's sufficiently tested in the e2e tests. + + it('throws when getting public storage for non-existent contract', async () => { + const contract = AztecAddress.random(); + await expect(async () => await rpc.getPublicStorageAt(contract, new Fr(0n))).rejects.toThrow( + `Contract ${contract.toString()} is not deployed`, + ); + }); + + // Note: Not testing `getContractDataAndBytecode`, `getContractData` and `getUnencryptedLogs` here as these + // functions only call AztecNode and these methods are frequently used by the e2e tests. + + it('successfully gets a block number', async () => { + const blockNum = await rpc.getBlockNum(); + expect(blockNum).toBeGreaterThanOrEqual(INITIAL_L2_BLOCK_NUM); + }); + + it('successfully gets node info', async () => { + const nodeInfo = await rpc.getNodeInfo(); + expect(nodeInfo.version).toBeDefined(); + expect(nodeInfo.chainId).toBeDefined(); + expect(nodeInfo.rollupAddress).toBeDefined(); + }); + + // Note: Not testing `isGlobalStateSynchronised`, `isAccountStateSynchronised` and `getSyncStatus` as these methods + // only call synchroniser. + }); +}; diff --git a/yarn-project/aztec-rpc/src/contract_database/memory_contract_database.ts b/yarn-project/aztec-rpc/src/contract_database/memory_contract_database.ts index 2940faa6fd2..1f7efbb2395 100644 --- a/yarn-project/aztec-rpc/src/contract_database/memory_contract_database.ts +++ b/yarn-project/aztec-rpc/src/contract_database/memory_contract_database.ts @@ -33,10 +33,14 @@ export class MemoryContractDatabase implements ContractDatabase { * @param address - The AztecAddress to search for in the stored contracts. * @returns A Promise resolving to the ContractDao instance matching the given address or undefined. */ - public getContract(address: AztecAddress) { + public getContract(address: AztecAddress): Promise { return Promise.resolve(this.contracts.find(c => c.address.equals(address))); } + public getContracts(): Promise { + return Promise.resolve(this.contracts); + } + /** * Retrieve the bytecode associated with a given contract address and function selector. * This function searches through the stored contracts to find a matching contract and function, diff --git a/yarn-project/aztec.js/src/account/entrypoint/entrypoint_collection.ts b/yarn-project/aztec.js/src/account/entrypoint/entrypoint_collection.ts index 0be7bd65ad2..1da44c3c766 100644 --- a/yarn-project/aztec.js/src/account/entrypoint/entrypoint_collection.ts +++ b/yarn-project/aztec.js/src/account/entrypoint/entrypoint_collection.ts @@ -32,7 +32,7 @@ export class EntrypointCollection implements Entrypoint { /** * Registers an entrypoint against an aztec address - * @param addr - The aztec address agianst which to register the implementation. + * @param addr - The aztec address against which to register the implementation. * @param impl - The entrypoint to be registered. */ public registerAccount(addr: AztecAddress, impl: Entrypoint) { 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 index 5dcd4d4610b..432d33b80df 100644 --- 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 @@ -13,6 +13,7 @@ import { } from '@aztec/types'; export { mustSucceedFetch } from '@aztec/foundation/json-rpc/client'; +export { mustSucceedFetchUnlessNoRetry } from '@aztec/foundation/json-rpc/client'; export const createAztecRpcClient = (url: string, fetch = defaultFetch): AztecRPC => createJsonRpcClient( diff --git a/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts b/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts index bd01275174b..cc0d1ef6e8b 100644 --- a/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts +++ b/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts @@ -52,6 +52,9 @@ export abstract class BaseWallet implements Wallet { addContracts(contracts: DeployedContract[]): Promise { return this.rpc.addContracts(contracts); } + getContracts(): Promise { + return this.rpc.getContracts(); + } simulateTx(txRequest: TxExecutionRequest): Promise { return this.rpc.simulateTx(txRequest); } diff --git a/yarn-project/aztec.js/src/contract/contract.test.ts b/yarn-project/aztec.js/src/contract/contract.test.ts index 0a67a40b33d..c29107f6baa 100644 --- a/yarn-project/aztec.js/src/contract/contract.test.ts +++ b/yarn-project/aztec.js/src/contract/contract.test.ts @@ -1,7 +1,14 @@ import { AztecAddress, CompleteAddress, EthAddress } from '@aztec/circuits.js'; import { ABIParameterVisibility, ContractAbi, FunctionType } from '@aztec/foundation/abi'; -import { randomBytes } from '@aztec/foundation/crypto'; -import { ContractData, DeployedContract, NodeInfo, Tx, TxExecutionRequest, TxHash, TxReceipt } from '@aztec/types'; +import { + ContractData, + NodeInfo, + Tx, + TxExecutionRequest, + TxHash, + TxReceipt, + randomDeployedContract, +} from '@aztec/types'; import { MockProxy, mock } from 'jest-mock-extended'; @@ -80,17 +87,6 @@ describe('Contract Class', () => { ], }; - const randomContractAbi = (): ContractAbi => ({ - name: randomBytes(4).toString('hex'), - functions: [], - }); - - const randomDeployContract = (): DeployedContract => ({ - abi: randomContractAbi(), - address: AztecAddress.random(), - portalContract: EthAddress.random(), - }); - beforeEach(async () => { account = await CompleteAddress.random(); wallet = mock(); @@ -143,7 +139,7 @@ describe('Contract Class', () => { }); it('should add contract and dependencies to aztec rpc', async () => { - const entry = randomDeployContract(); + const entry = randomDeployedContract(); const contract = await Contract.at(entry.address, entry.abi, wallet); { @@ -154,7 +150,7 @@ describe('Contract Class', () => { } { - const dependencies = [randomDeployContract(), randomDeployContract()]; + const dependencies = [randomDeployedContract(), randomDeployedContract()]; await contract.attach(entry.portalContract, dependencies); expect(wallet.addContracts).toHaveBeenCalledTimes(1); expect(wallet.addContracts).toHaveBeenCalledWith([entry, ...dependencies]); diff --git a/yarn-project/end-to-end/src/aztec_rpc_sandbox.test.ts b/yarn-project/end-to-end/src/aztec_rpc_sandbox.test.ts new file mode 100644 index 00000000000..84082670d0e --- /dev/null +++ b/yarn-project/end-to-end/src/aztec_rpc_sandbox.test.ts @@ -0,0 +1,12 @@ +import { aztecRpcTestSuite } from '@aztec/aztec-rpc'; +import { createAztecRpcClient, mustSucceedFetchUnlessNoRetry, waitForSandbox } from '@aztec/aztec.js'; + +const { SANDBOX_URL = 'http://localhost:8080' } = process.env; + +const setup = async () => { + const aztecRpc = createAztecRpcClient(SANDBOX_URL, mustSucceedFetchUnlessNoRetry); + await waitForSandbox(aztecRpc); + return aztecRpc; +}; + +aztecRpcTestSuite('aztec_rpc_sandbox', setup); diff --git a/yarn-project/end-to-end/src/e2e_aztec_js_browser.test.ts b/yarn-project/end-to-end/src/e2e_aztec_js_browser.test.ts index c052734c543..6d7584dd5f1 100644 --- a/yarn-project/end-to-end/src/e2e_aztec_js_browser.test.ts +++ b/yarn-project/end-to-end/src/e2e_aztec_js_browser.test.ts @@ -126,7 +126,7 @@ conditionalDescribe()('e2e_aztec.js_browser', () => { const accounts = await testClient.getAccounts(); const stringAccounts = accounts.map(acc => acc.address.toString()); expect(stringAccounts.includes(result)).toBeTruthy(); - }); + }, 15_000); it('Deploys Private Token contract', async () => { const txHash = await page.evaluate( diff --git a/yarn-project/foundation/src/json-rpc/client/index.ts b/yarn-project/foundation/src/json-rpc/client/index.ts index 90a8bcc736d..786e07ee69e 100644 --- a/yarn-project/foundation/src/json-rpc/client/index.ts +++ b/yarn-project/foundation/src/json-rpc/client/index.ts @@ -1 +1,6 @@ -export { createJsonRpcClient, mustSucceedFetch, defaultFetch } from './json_rpc_client.js'; +export { + createJsonRpcClient, + mustSucceedFetch, + mustSucceedFetchUnlessNoRetry, + defaultFetch, +} from './json_rpc_client.js'; 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 3be85887024..9163396a482 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 @@ -5,7 +5,7 @@ import { RemoteObject } from 'comlink'; import { createDebugLogger } from '../../log/index.js'; -import { retry } from '../../retry/index.js'; +import { NoRetryError, retry } from '../../retry/index.js'; import { ClassConverter, JsonClassConverterInput, StringClassConverterInput } from '../class_converter.js'; import { JsonStringify, convertFromJsonObj, convertToJsonObj } from '../convert.js'; @@ -18,9 +18,17 @@ const debug = createDebugLogger('json-rpc:json_rpc_client'); * @param host - The host URL. * @param method - The RPC method name. * @param body - The RPC payload. + * @param noRetry - Whether to throw a `NoRetryError` in case the response is not ok and the body contains an error + * message (see `retry` function for more details). * @returns The parsed JSON response, or throws an error. */ -export async function defaultFetch(host: string, rpcMethod: string, body: any, useApiEndpoints: boolean) { +export async function defaultFetch( + host: string, + rpcMethod: string, + body: any, + useApiEndpoints: boolean, + noRetry = false, +) { debug(`JsonRpcClient.fetch`, host, rpcMethod, '->', body); let resp: Response; if (useApiEndpoints) { @@ -37,15 +45,24 @@ export async function defaultFetch(host: string, rpcMethod: string, body: any, u }); } - if (!resp.ok) { - throw new Error(resp.statusText); - } - + let responseJson; try { - return await resp.json(); + responseJson = await resp.json(); } catch (err) { + if (!resp.ok) { + throw new Error(resp.statusText); + } throw new Error(`Failed to parse body as JSON: ${resp.text()}`); } + if (!resp.ok) { + if (noRetry) { + throw new NoRetryError(responseJson.error); + } else { + throw new Error(responseJson.error); + } + } + + return responseJson; } /** @@ -55,6 +72,19 @@ export async function mustSucceedFetch(host: string, rpcMethod: string, body: an return await retry(() => defaultFetch(host, rpcMethod, body, useApiEndpoints), 'JsonRpcClient request'); } +/** + * A fetch function with retries unless the error is a NoRetryError. + */ +export async function mustSucceedFetchUnlessNoRetry( + host: string, + rpcMethod: string, + body: any, + useApiEndpoints: boolean, +) { + const noRetry = true; + return await retry(() => defaultFetch(host, rpcMethod, body, useApiEndpoints, noRetry), 'JsonRpcClient request'); +} + /** * Creates a Proxy object that delegates over RPC and satisfies RemoteObject. * The server should have ran new JsonRpcServer(). 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 8de26421e9f..f217fee70dc 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 @@ -80,13 +80,20 @@ export class JsonRpcServer { } 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: convertBigintsInObj(result), - }; - ctx.status = 200; + try { + const result = await this.proxy.call(method, params); + ctx.body = { + jsonrpc, + id, + result: convertBigintsInObj(result), + }; + ctx.status = 200; + } catch (err: any) { + // Propagate the error message to the client. Plenty of the errors are expected to occur (e.g. adding + // a duplicate recipient) so this is necessary. + ctx.status = 400; + ctx.body = { error: err.message }; + } }); } } else { @@ -101,15 +108,23 @@ export class JsonRpcServer { ) { ctx.status = 400; ctx.body = { error: `Invalid method name: ${method}` }; - } - const result = await this.proxy.call(method, params); + } else { + try { + const result = await this.proxy.call(method, params); - ctx.body = { - jsonrpc, - id, - result: convertBigintsInObj(result), - }; - ctx.status = 200; + ctx.body = { + jsonrpc, + id, + result: convertBigintsInObj(result), + }; + ctx.status = 200; + } catch (err: any) { + // Propagate the error message to the client. Plenty of the errors are expected to occur (e.g. adding + // a duplicate recipient) so this is necessary. + ctx.status = 400; + ctx.body = { error: err.message }; + } + } }); } diff --git a/yarn-project/foundation/src/retry/index.ts b/yarn-project/foundation/src/retry/index.ts index 0a098f45ef2..4287678bffa 100644 --- a/yarn-project/foundation/src/retry/index.ts +++ b/yarn-project/foundation/src/retry/index.ts @@ -2,6 +2,9 @@ import { createDebugLogger } from '../log/index.js'; import { sleep } from '../sleep/index.js'; import { Timer } from '../timer/index.js'; +/** An error that indicates that the operation should not be retried. */ +export class NoRetryError extends Error {} + /** * Generates a backoff sequence for retrying operations with an increasing delay. * The backoff sequence follows this pattern: 1, 1, 1, 2, 4, 8, 16, 32, 64, ... @@ -27,6 +30,7 @@ export function* backoffGenerator() { * @param backoff - The optional backoff generator providing the intervals in seconds between retries. Defaults to a predefined series. * @param log - Logger to use for logging. * @returns A Promise that resolves with the successful result of the provided function, or rejects if backoff generator ends. + * @throws If `NoRetryError` is thrown by the `fn`, it is rethrown. */ export async function retry( fn: () => Promise, @@ -38,6 +42,10 @@ export async function retry( try { return await fn(); } catch (err: any) { + if (err instanceof NoRetryError) { + // A special error that indicates that the operation should not be retried. Rethrow it. + throw err; + } const s = backoff.next().value; if (s === undefined) { throw err; diff --git a/yarn-project/types/src/contract_database.ts b/yarn-project/types/src/contract_database.ts index 3a8a81bb01a..7eb2333ed88 100644 --- a/yarn-project/types/src/contract_database.ts +++ b/yarn-project/types/src/contract_database.ts @@ -24,4 +24,10 @@ export interface ContractDatabase { * @returns A Promise resolving to the ContractDao instance matching the given address or undefined. */ getContract(address: AztecAddress): Promise; + + /** + * Retrieve all ContractDao instances stored in the database. + * @returns A Promise resolving to an array of all stored ContractDao instances. + */ + getContracts(): Promise; } diff --git a/yarn-project/types/src/interfaces/aztec_rpc.ts b/yarn-project/types/src/interfaces/aztec_rpc.ts index 6348b4d88b3..98dd2ac0af7 100644 --- a/yarn-project/types/src/interfaces/aztec_rpc.ts +++ b/yarn-project/types/src/interfaces/aztec_rpc.ts @@ -69,6 +69,7 @@ export interface AztecRPC { * @param privKey - Private key of the corresponding user master public key. * @param account - A complete address of the account. * @returns Empty promise. + * @throws If the account is already registered. */ registerAccount(privKey: PrivateKey, account: CompleteAddress): Promise; @@ -80,6 +81,7 @@ export interface AztecRPC { * This is because we don't have the associated private key and for this reason we can't decrypt * the recipient's notes. We can send notes to this account because we can encrypt them with the recipient's * public key. + * @throws If the recipient is already registered. */ registerRecipient(recipient: CompleteAddress): Promise; @@ -122,6 +124,12 @@ export interface AztecRPC { */ addContracts(contracts: DeployedContract[]): Promise; + /** + * Retrieves the list of addresses of contracts added to this rpc server. + * @returns A promise that resolves to an array of contracts addresses registered on this RPC server. + */ + getContracts(): Promise; + /** * Create a transaction for a contract function call with the provided arguments. * Throws an error if the contract or function is unknown. diff --git a/yarn-project/types/src/mocks.ts b/yarn-project/types/src/mocks.ts index 5ec58ec3e08..9136d6adb8f 100644 --- a/yarn-project/types/src/mocks.ts +++ b/yarn-project/types/src/mocks.ts @@ -1,9 +1,11 @@ -import { MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, Proof } from '@aztec/circuits.js'; +import { AztecAddress, EthAddress, MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, Proof } from '@aztec/circuits.js'; import { makeKernelPublicInputs, makePublicCallRequest } from '@aztec/circuits.js/factories'; +import { ContractAbi } from '@aztec/foundation/abi'; +import { randomBytes } from '@aztec/foundation/crypto'; import times from 'lodash.times'; -import { EncodedContractFunction, FunctionL2Logs, TxL2Logs } from './index.js'; +import { DeployedContract, EncodedContractFunction, FunctionL2Logs, TxL2Logs } from './index.js'; import { Tx } from './tx/index.js'; /** @@ -24,3 +26,14 @@ export const mockTx = (seed = 1) => { times(MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, makePublicCallRequest), ); }; + +export const randomContractAbi = (): ContractAbi => ({ + name: randomBytes(4).toString('hex'), + functions: [], +}); + +export const randomDeployedContract = (): DeployedContract => ({ + abi: randomContractAbi(), + address: AztecAddress.random(), + portalContract: EthAddress.random(), +});