From d3433b93fb05e65e11c57d8d1a7cebcd165802a8 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Mon, 4 Sep 2023 18:24:35 -0300 Subject: [PATCH 01/11] fix(rpc): Fixes getNodeInfo serialisation --- .../test/aztec_rpc_test_suite.ts | 6 ++-- .../foundation/src/json-rpc/convert.ts | 28 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_test_suite.ts b/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_test_suite.ts index 94ec2c629d0..16e3ec57de4 100644 --- a/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_test_suite.ts +++ b/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_test_suite.ts @@ -132,9 +132,9 @@ export const aztecRpcTestSuite = (testName: string, aztecRpcSetup: () => Promise it('successfully gets node info', async () => { const nodeInfo = await rpc.getNodeInfo(); - expect(nodeInfo.version).toBeDefined(); - expect(nodeInfo.chainId).toBeDefined(); - expect(nodeInfo.rollupAddress).toBeDefined(); + expect(typeof nodeInfo.version).toEqual('number'); + expect(typeof nodeInfo.chainId).toEqual('number'); + expect(nodeInfo.rollupAddress.toString()).toMatch(/0x[a-fA-F0-9]+/); }); // Note: Not testing `isGlobalStateSynchronised`, `isAccountStateSynchronised` and `getSyncStatus` as these methods diff --git a/yarn-project/foundation/src/json-rpc/convert.ts b/yarn-project/foundation/src/json-rpc/convert.ts index 7ca545ddbd4..aec26c8f5cf 100644 --- a/yarn-project/foundation/src/json-rpc/convert.ts +++ b/yarn-project/foundation/src/json-rpc/convert.ts @@ -96,8 +96,9 @@ export function convertFromJsonObj(cc: ClassConverter, obj: any): any { if (Array.isArray(obj)) { return obj.map((x: any) => convertFromJsonObj(cc, x)); } + // Is this a dictionary? - if (obj.constructor === Object) { + if (typeof obj === 'object') { const newObj: any = {}; for (const key of Object.keys(obj)) { newObj[key] = convertFromJsonObj(cc, obj[key]); @@ -119,6 +120,7 @@ export function convertToJsonObj(cc: ClassConverter, obj: any): any { if (!obj) { return obj; // Primitive type } + // Is this a Node buffer? if (obj instanceof Buffer) { return { type: 'Buffer', data: obj.toString('base64') }; @@ -135,22 +137,26 @@ export function convertToJsonObj(cc: ClassConverter, obj: any): any { if (cc.isRegisteredClass(obj.constructor)) { return cc.toJsonObj(obj); } + // Is this an array? if (Array.isArray(obj)) { return obj.map((x: any) => convertToJsonObj(cc, x)); } - // Is this a dictionary? - if (obj.constructor === Object) { - const newObj: any = {}; - for (const key of Object.keys(obj)) { - newObj[key] = convertToJsonObj(cc, obj[key]); + + if (typeof obj === 'object') { + // Is this a dictionary? + if (isPlainObject(obj)) { + const newObj: any = {}; + for (const key of Object.keys(obj)) { + newObj[key] = convertToJsonObj(cc, obj[key]); + } + return newObj; + } else { + // Throw if this is a non-primitive class that was not registered + throw new Error(`Object ${obj.constructor.name} not registered for serialisation`); } - return newObj; - } - // Throw if this is a non-primitive class that was not registered - if (typeof obj === 'object' && !isPlainObject(obj)) { - throw new Error(`Object ${obj.constructor.name} not registered for serialisation`); } + // Leave alone, assume JSON primitive return obj; } From 1e39da86f45f8e53b515e1122e04627953695df2 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Mon, 4 Sep 2023 18:33:46 -0300 Subject: [PATCH 02/11] feat(docs): Testing guide and getPrivateStorage method --- docs/docs/dev_docs/dapps/testing.md | 162 ++++++++++++++++++ .../aztec_rpc_http/aztec_rpc_http_server.ts | 2 + .../src/aztec_rpc_server/aztec_rpc_server.ts | 12 ++ .../src/aztec_rpc_client/aztec_rpc_client.ts | 2 + .../aztec.js/src/aztec_rpc_client/wallet.ts | 9 + .../aztec.js/src/utils/cheat_codes.ts | 13 +- .../src/guides/dapp_testing.test.ts | 160 +++++++++++++++++ yarn-project/foundation/src/fields/fields.ts | 5 +- .../src/contract-interface-gen/typescript.ts | 4 - .../native_token_contract/src/main.nr | 11 +- .../src/contracts/test_contract/src/main.nr | 3 +- .../noir-libs/value-note/src/value_note.nr | 2 + .../types/src/interfaces/aztec_rpc.ts | 12 ++ .../logs/note_spending_info/note_preimage.ts | 18 ++ 14 files changed, 397 insertions(+), 18 deletions(-) create mode 100644 docs/docs/dev_docs/dapps/testing.md create mode 100644 yarn-project/end-to-end/src/guides/dapp_testing.test.ts diff --git a/docs/docs/dev_docs/dapps/testing.md b/docs/docs/dev_docs/dapps/testing.md new file mode 100644 index 00000000000..3444933c251 --- /dev/null +++ b/docs/docs/dev_docs/dapps/testing.md @@ -0,0 +1,162 @@ +# Testing + +Testing is an integral part of any piece of software, and especially important for any blockchain application. In this page we will cover how to interact with your Noir contracts in a testing environment to write automated tests for your apps. + +We will be using typescript to write our tests, and rely on the [`aztec.js`](https://www.npmjs.com/package/@aztec/aztec.js) client library to interact with a local Aztec network. We will use [`jest`](https://jestjs.io/) as a testing library, though feel free to use whatever you work with. Configuring the nodejs testing framework is out of scope for this guide. + +## A simple example + +Let's start with a simple example for a test using the [Sandbox](../sandbox/main.md). We will create two accounts and deploy a token contract in a setup step, and then issue a transfer from one user to another. + +#include_code sandbox-example /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +This test sets up the environment by creating a client to the Aztec RPC server running on the Sandbox on port 8080. It then creates two new accounts, dubbed `owner` and `recipient`. Last, it deploys an instance of the [`PrivateTokenContract`](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/noir-contracts/src/contracts/private_token_contract/src/main.nr), minting an initial 100 tokens to the owner. + +Once we have this setup, the test itself is simple. We check the balance of the `recipient` user to ensure it has no tokens, send and await a deployment transaction, and then check the balance again to ensure it was increased. Note that all numeric values are represented as [native bigints](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) to avoid loss of precision. + +:::info +We are using the `PrivateTokenContract` [typescript interface](../contracts/compiling.md#typescript-interfaces) to get type-safe methods for deploying and interacting with the token contract. +::: + +To run the test, first make sure the Sandbox is running on port 8080, and then [run your tests using jest](https://jestjs.io/docs/getting-started#running-from-command-line). Your test should pass, and you should see the following output in the Sandbox logs, where each chunk corresponds to a transaction. Note how this test run has a total of four transactions: two for deploying the account contracts for the `owner` and `recipient`, another for deploying the token contract, and a last one for actually executing the transfer. + +```text +rpc_server Registered account 0x2efa51d2e67581aef4578e8cc647a1af2e3f40e9872deeda0919e5f77cb8b2d2 +rpc_server Added contract SchnorrAccount at 0x2efa51d2e67581aef4578e8cc647a1af2e3f40e9872deeda0919e5f77cb8b2d2 +node Simulating tx 19bfe4fb2569be2168f01eefe5e5a4284d6c1678f17ab5e94c6ba9c811bcb214 +node Simulated tx 19bfe4fb2569be2168f01eefe5e5a4284d6c1678f17ab5e94c6ba9c811bcb214 succeeds +rpc_server Executed local simulation for 19bfe4fb2569be2168f01eefe5e5a4284d6c1678f17ab5e94c6ba9c811bcb214 +rpc_server Sending transaction 19bfe4fb2569be2168f01eefe5e5a4284d6c1678f17ab5e94c6ba9c811bcb214 +node Received tx 19bfe4fb2569be2168f01eefe5e5a4284d6c1678f17ab5e94c6ba9c811bcb214 +sequencer Submitted rollup block 2 with 1 transactions + +rpc_server Registered account 0x12ef7ceb5064da3a729f598a6a50585059794fdcf347a6fc9bb317002162e3db +rpc_server Added contract SchnorrAccount at 0x12ef7ceb5064da3a729f598a6a50585059794fdcf347a6fc9bb317002162e3db +node Simulating tx 0f195f8f6fb8fe29cf8159c5c664c1288788f1151a5413ec0e35cf378de74794 +node Simulated tx 0f195f8f6fb8fe29cf8159c5c664c1288788f1151a5413ec0e35cf378de74794 succeeds +rpc_server Executed local simulation for 0f195f8f6fb8fe29cf8159c5c664c1288788f1151a5413ec0e35cf378de74794 +rpc_server Sending transaction 0f195f8f6fb8fe29cf8159c5c664c1288788f1151a5413ec0e35cf378de74794 +node Received tx 0f195f8f6fb8fe29cf8159c5c664c1288788f1151a5413ec0e35cf378de74794 +sequencer Submitted rollup block 3 with 1 transactions + +rpc_server Added contract PrivateToken at 0x24e691d8bde970ab9e84fe669ea5ac8019c32c199a55aaa8a3e704db763af88f +node Simulating tx 0101e1a3d73c3a112a18b7e4954edfe611d74ae0dc59e1688221ecda982ba943 +node Simulated tx 0101e1a3d73c3a112a18b7e4954edfe611d74ae0dc59e1688221ecda982ba943 succeeds +rpc_server Executed local simulation for 0101e1a3d73c3a112a18b7e4954edfe611d74ae0dc59e1688221ecda982ba943 +rpc_server Sending transaction 0101e1a3d73c3a112a18b7e4954edfe611d74ae0dc59e1688221ecda982ba943 +node Received tx 0101e1a3d73c3a112a18b7e4954edfe611d74ae0dc59e1688221ecda982ba943 +sequencer Submitted rollup block 4 with 1 transactions + +node Simulating tx 2132767911fbbe67e24a3e51bc769ba2ae874eb1ba56e69cef8fc9e2c5eba04c +node Simulated tx 2132767911fbbe67e24a3e51bc769ba2ae874eb1ba56e69cef8fc9e2c5eba04c succeeds +rpc_server Executed local simulation for 2132767911fbbe67e24a3e51bc769ba2ae874eb1ba56e69cef8fc9e2c5eba04c +rpc_server Sending transaction 2132767911fbbe67e24a3e51bc769ba2ae874eb1ba56e69cef8fc9e2c5eba04c +node Received tx 2132767911fbbe67e24a3e51bc769ba2ae874eb1ba56e69cef8fc9e2c5eba04c +sequencer Submitted rollup block 5 with 1 transactions +``` + +## Assertions + +We will now see how to use `aztec.js` to write assertions about transaction statuses, about chain state both public and private, and about logs. + +### Transactions + +In the example above we used `contract.methods.method().send().wait()` to create a function call for a contract, send it, and await it to be mined successfully. But what if we want to assert failing scenarios? + +#### A private call fails + +We can check that a call to a private function would fail by simulating it locally and expecting a rejection. Remember that all private function calls are only executed locally in order to preserve privacy. As an example, we can try transferring more tokens than we have. + +#include_code local-tx-fails /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +Under the hood, the `send()` method executes a simulation, so we can just call the usual `send().wait()` to catch the same failure. + +#include_code local-tx-fails-send /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +#### A transaction is dropped + +We can have private transactions that work fine locally, but are dropped by the sequencer when tried to be included due to a double-spend. In this example, we simulate two different transfers that would succeed individually, but not when both are tried to be mined. Here we need to `send()` the transaction and `wait()` for it to be mined. + +#include_code tx-dropped /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +#### A public call fails locally + +Public function calls can be caught failing locally similar to how we catch private function calls. For this example, we use a [`NativeTokenContract`](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/noir-contracts/src/contracts/native_token_contract/src/main.nr) instead of a private one. + +:::info +Keep in mind that public function calls behave as in EVM blockchains, in that they are executed by the sequencer and not locally. Local simulation helps alert the user of a potential failure, but the actual execution path of a public function call will depend on when it gets mined. +::: + +#include_code local-pub-fails /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +#### A public call fails on the sequencer + +We can ignore a local simulation error for a public function via the `skipPublicSimulation`. This will submit a failing call to the sequencer, who will then reject it and drop the transaction. + +#include_code pub-dropped /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +If you run the snippet above, you'll see the following error in the Sandbox logs, where `0x1cbcdf5094fe2f1cda9a47621c4528c30a13a10e610942f656b95130015f44f8` is the address of the contract and `3c5e7ad4` is the selector of the `transfer_pub` function we called. + +``` +WARN Error processing tx 06dc87c4d64462916ea58426ffcfaf20017880b353c9ec3e0f0ee5fab3ea923f: Simulation error: Cannot satisfy constraint 0.19085 at 0x1cbcdf5094fe2f1cda9a47621c4528c30a13a10e610942f656b95130015f44f8.3c5e7ad4 +``` + +:::info +In the near future, transactions where a public function call fails will get mined but flagged as reverted, instead of dropped. +::: + +### State + +We can check private or public state directly rather than going through view-only methods, as we did in the initial example by calling `token.methods.balance().view()`. Bear in mind that directly accessing contract storage will break any kind of encapsulation. + +To query storage directly, you'll need to know the slot you want to access. This can be checked in the [contract's `Storage` definition](../contracts/storage.md) directly for most data types. However, when it comes to mapping types, as in most EVM languages, we'll need to calculate the slot for a given key. To do this, we'll use the `CheatCodes` utility class: + +#include_code calc-slot /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +#### Querying private state + +Private state in the Aztec Network is represented via sets of [private notes](../../concepts/foundation/state_model.md#private-state). In our token contract example, the balance of a user is represented as a set of unspent value notes, each with their own corresponding numeric value. + +#include_code value-note-def yarn-project/noir-libs/value-note/src/value_note.nr rust + +We can query the RPC server for all notes encrypted for a given user in a contract slot. For this example, we'll get all notes encrypted for the `owner` user that are stored on the token contract address and on the slot we calculated earlier. To calculate the actual balance, we extract the `value` of each note, which is the first element, and sum them up. + +#include_code private-storage /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +#### Querying public state + +[Public state](../../concepts/foundation/state_model.md#public-state) behaves as a key-value store, much like in the EVM. This scenario is much more straightforward, in that we can directly query the target slot and get the result back as a buffer. Note that we use the [`NativeTokenContract`](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/noir-contracts/src/contracts/native_token_contract/src/main.nr) in this example, which defines a mapping of public balances on slot 4. + +#include_code public-storage /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +### Logs + +Last but not least, we can check the logs of [events](../contracts/events.md) emitted by our contracts. Contracts in Aztec can emit both [encrypted](../contracts/events.md#encrypted-events) and [unencrypted](../contracts/events.md#unencrypted-events) events. + +:::info +At the time of this writing, only unencrypted events can be queried directly. Encrypted events are always assumed to be encrypted notes. +::: + +#### Querying unencrypted logs + +We can query the RPC server for the unencrypted logs emitted in the block where our transaction is mined. Note that logs need to be unrolled and formatted as strings for consumption. + +#include_code unencrypted-storage /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +## Cheats + +The `CheatCodes` class, which we used for [calculating the storage slot above](#state), also includes a set of cheat methods for modifying the chain state that can be handy for testing. + +### Set next block timestamp + +The `warp` method sets the time for next execution, both on L1 and L2. We can test this using an `isTimeEqual` function in a `Test` contract defined like the following: + +#include_code is-time-equal yarn-project/noir-contracts/src/contracts/test_contract/src/main.nr rust + +We can then call `warp` and rely on the `isTimeEqual` function to check that the timestamp was properly modified. + +#include_code warp /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +:::info +The `warp` method calls `evm_setNextBlockTimestamp` under the hood on L1. +::: diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_http/aztec_rpc_http_server.ts b/yarn-project/aztec-rpc/src/aztec_rpc_http/aztec_rpc_http_server.ts index 509de59af8a..a5d3d296d5b 100644 --- a/yarn-project/aztec-rpc/src/aztec_rpc_http/aztec_rpc_http_server.ts +++ b/yarn-project/aztec-rpc/src/aztec_rpc_http/aztec_rpc_http_server.ts @@ -7,6 +7,7 @@ import { ContractData, ExtendedContractData, L2BlockL2Logs, + NotePreimage, PrivateKey, Tx, TxExecutionRequest, @@ -38,6 +39,7 @@ export function getHttpRpcServer(aztecRpcServer: AztecRPC): JsonRpcServer { Point, PrivateKey, Fr, + NotePreimage, }, { Tx, TxReceipt, L2BlockL2Logs }, false, 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 f4d5610890a..1aa46b803dc 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 @@ -171,6 +171,18 @@ export class AztecRPCServer implements AztecRPC { return await this.node.getPublicStorageAt(contract, storageSlot.value); } + public async getPrivateStorageAt(owner: AztecAddress, contract: AztecAddress, storageSlot: Fr) { + if ((await this.getContractData(contract)) === undefined) { + throw new Error(`Contract ${contract.toString()} is not deployed`); + } + const notes = await this.db.getNoteSpendingInfo(contract, storageSlot); + const ownerCompleteAddress = await this.db.getCompleteAddress(owner); + if (!ownerCompleteAddress) throw new Error(`Owner ${owner} not registered in RPC server`); + const { publicKey: ownerPublicKey } = ownerCompleteAddress; + const ownerNotes = notes.filter(n => n.publicKey.equals(ownerPublicKey)); + return ownerNotes.map(n => n.notePreimage); + } + public async getBlock(blockNumber: number): Promise { // If a negative block number is provided the current block number is fetched. if (blockNumber < 0) { 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 3add6a538d5..075a14c9703 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 @@ -5,6 +5,7 @@ import { ContractData, ExtendedContractData, L2BlockL2Logs, + NotePreimage, Tx, TxExecutionRequest, TxHash, @@ -27,6 +28,7 @@ export const createAztecRpcClient = (url: string, fetch = defaultFetch): AztecRP Point, PrivateKey, Fr, + NotePreimage, }, { Tx, TxReceipt, L2BlockL2Logs }, false, 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 c9450bbf887..d069edc8476 100644 --- a/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts +++ b/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts @@ -7,6 +7,7 @@ import { FunctionCall, L2BlockL2Logs, NodeInfo, + NotePreimage, PackedArguments, SyncStatus, Tx, @@ -64,6 +65,9 @@ export abstract class BaseWallet implements Wallet { getTxReceipt(txHash: TxHash): Promise { return this.rpc.getTxReceipt(txHash); } + getPrivateStorageAt(owner: AztecAddress, contract: AztecAddress, storageSlot: Fr): Promise { + return this.rpc.getPrivateStorageAt(owner, contract, storageSlot); + } getPublicStorageAt(contract: AztecAddress, storageSlot: Fr): Promise { return this.rpc.getPublicStorageAt(contract, storageSlot); } @@ -120,6 +124,11 @@ export class AccountWallet extends EntrypointWallet { public getCompleteAddress() { return this.address; } + + /** Returns the address of the account that implements this wallet. */ + public getAddress() { + return this.address.address; + } } /** diff --git a/yarn-project/aztec.js/src/utils/cheat_codes.ts b/yarn-project/aztec.js/src/utils/cheat_codes.ts index d94b73b28e3..16b93aa6f1d 100644 --- a/yarn-project/aztec.js/src/utils/cheat_codes.ts +++ b/yarn-project/aztec.js/src/utils/cheat_codes.ts @@ -5,11 +5,10 @@ import { keccak } from '@aztec/foundation/crypto'; import { createDebugLogger } from '@aztec/foundation/log'; import { AztecRPC } from '@aztec/types'; + + import fs from 'fs'; -const toFr = (value: Fr | bigint): Fr => { - return typeof value === 'bigint' ? new Fr(value) : value; -}; /** * A class that provides utility functions for interacting with the chain. @@ -236,13 +235,13 @@ export class AztecCheatCodes { * @param key - The key to lookup in the map * @returns The storage slot of the value in the map */ - public computeSlotInMap(baseSlot: Fr | bigint, key: Fr | bigint): Fr { + public computeSlotInMap(baseSlot: Fr | bigint, key: Fr | bigint | AztecAddress): Fr { // Based on `at` function in // aztec3-packages/yarn-project/noir-contracts/src/contracts/noir-aztec/src/state_vars/map.nr return Fr.fromBuffer( pedersenPlookupCommitInputs( this.wasm, - [toFr(baseSlot), toFr(key)].map(f => f.toBuffer()), + [new Fr(baseSlot), new Fr(key)].map(f => f.toBuffer()), ), ); } @@ -277,10 +276,10 @@ export class AztecCheatCodes { * @returns The value stored at the given slot */ public async loadPublic(who: AztecAddress, slot: Fr | bigint): Promise { - const storageValue = await this.aztecRpc.getPublicStorageAt(who, toFr(slot)); + const storageValue = await this.aztecRpc.getPublicStorageAt(who, new Fr(slot)); if (storageValue === undefined) { throw new Error(`Storage slot ${slot} not found`); } return Fr.fromBuffer(storageValue); } -} +} \ No newline at end of file diff --git a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts new file mode 100644 index 00000000000..588d5019980 --- /dev/null +++ b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts @@ -0,0 +1,160 @@ +import { + AccountWallet, + AztecRPC, + CheatCodes, + Fr, + L2BlockL2Logs, + PrivateKey, + createAztecRpcClient, + getSchnorrAccount, +} from '@aztec/aztec.js'; +import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; +import { NativeTokenContract, PrivateTokenContract, TestContract } from '@aztec/noir-contracts/types'; + +describe('guides/dapp/testing', () => { + describe('on sandbox', () => { + // docs:start:sandbox-example + describe('private token contract', () => { + const AZTEC_URL = 'http://localhost:8080'; + + let rpc: AztecRPC; + let owner: AccountWallet; + let recipient: AccountWallet; + let token: PrivateTokenContract; + + beforeEach(async () => { + rpc = createAztecRpcClient(AZTEC_URL); + owner = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); + recipient = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); + token = await PrivateTokenContract.deploy(owner, 100n, owner.getAddress()).send().deployed(); + }, 30_000); + + it('increases recipient funds on transfer', async () => { + expect(await token.methods.getBalance(recipient.getAddress()).view()).toEqual(0n); + await token.methods.transfer(20n, recipient.getAddress()).send().wait(); + expect(await token.methods.getBalance(recipient.getAddress()).view()).toEqual(20n); + }); + }); + // docs:end:sandbox-example + + describe('cheats', () => { + const AZTEC_URL = 'http://localhost:8080'; + const ETH_RPC_URL = 'http://localhost:8545'; + + let rpc: AztecRPC; + let owner: AccountWallet; + let testContract: TestContract; + let cheats: CheatCodes; + + beforeAll(async () => { + rpc = createAztecRpcClient(AZTEC_URL); + owner = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); + testContract = await TestContract.deploy(owner).send().deployed(); + cheats = await CheatCodes.create(ETH_RPC_URL, rpc); + }, 30_000); + + it('warps time to 1h into the future', async () => { + // docs:start:warp + const newTimestamp = Math.floor(Date.now() / 1000) + 60 * 60 * 24; + await cheats.aztec.warp(newTimestamp); + await testContract.methods.isTimeEqual(newTimestamp).send().wait(); + // docs:end:warp + }); + }); + + describe('assertions', () => { + const AZTEC_URL = 'http://localhost:8080'; + const ETH_RPC_URL = 'http://localhost:8545'; + + let rpc: AztecRPC; + let owner: AccountWallet; + let recipient: AccountWallet; + let token: PrivateTokenContract; + let nativeToken: NativeTokenContract; + let cheats: CheatCodes; + let ownerSlot: Fr; + + beforeAll(async () => { + rpc = createAztecRpcClient(AZTEC_URL); + owner = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); + recipient = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); + token = await PrivateTokenContract.deploy(owner, 100n, owner.getAddress()).send().deployed(); + nativeToken = await NativeTokenContract.deploy(owner, 100n, owner.getAddress()).send().deployed(); + + // docs:start:calc-slot + cheats = await CheatCodes.create(ETH_RPC_URL, rpc); + // The balances mapping is defined on storage slot 1 and is indexed by user address + ownerSlot = cheats.aztec.computeSlotInMap(1n, owner.getAddress()); + // docs:end:calc-slot + }, 30_000); + + it('checks private storage', async () => { + // docs:start:private-storage + const notes = await rpc.getPrivateStorageAt(owner.getAddress(), token.address, ownerSlot); + const values = notes.map(note => note.items[0]); + const balance = values.reduce((sum, current) => sum + current.toBigInt(), 0n); + expect(balance).toEqual(100n); + // docs:end:private-storage + }); + + it('checks public storage', async () => { + // docs:start:public-storage + await nativeToken.methods.owner_mint_pub(owner.getAddress(), 100n).send().wait(); + const ownerPublicBalanceSlot = cheats.aztec.computeSlotInMap(4n, owner.getAddress()); + const balance = await rpc.getPublicStorageAt(nativeToken.address, ownerPublicBalanceSlot); + expect(toBigIntBE(balance!)).toEqual(100n); + // docs:end:public-storage + }); + + it('checks unencrypted logs', async () => { + // docs:start:unencrypted-logs + const tx = await nativeToken.methods.owner_mint_pub(owner.getAddress(), 100n).send().wait(); + const logs = await rpc.getUnencryptedLogs(tx.blockNumber!, 1); + const textLogs = L2BlockL2Logs.unrollLogs(logs).map(log => log.toString('ascii')); + expect(textLogs).toEqual(['Coins minted']); + // docs:end:unencrypted-logs + }); + + it('asserts a local transaction simulation fails by calling simulate', async () => { + // docs:start:local-tx-fails + const call = token.methods.transfer(200n, recipient.getAddress()); + await expect(call.simulate()).rejects.toThrowError(/Assertion failed/); + // docs:end:local-tx-fails + }); + + it('asserts a local transaction simulation fails by calling send', async () => { + // docs:start:local-tx-fails-send + const call = token.methods.transfer(200n, recipient.getAddress()); + await expect(call.send().wait()).rejects.toThrowError(/Assertion failed/); + // docs:end:local-tx-fails-send + }); + + it('asserts a transaction is dropped', async () => { + // docs:start:tx-dropped + const call1 = token.methods.transfer(80n, recipient.getAddress()); + const call2 = token.methods.transfer(50n, recipient.getAddress()); + + await call1.simulate(); + await call2.simulate(); + + await call1.send().wait(); + await expect(call2.send().wait()).rejects.toThrowError(/dropped/); + // docs:end:tx-dropped + }); + + it('asserts a simulation for a public function call fails', async () => { + // docs:start:local-pub-fails + const call = nativeToken.methods.transfer_pub(recipient.getAddress(), 200n); + await expect(call.simulate()).rejects.toThrowError(/Assertion failed/); + // docs:end:local-pub-fails + }); + + it('asserts a transaction with a failing public call is dropped (until we get public reverts)', async () => { + // docs:start:pub-dropped + const call = nativeToken.methods.transfer_pub(recipient.getAddress(), 200n); + await expect(call.send({ skipPublicSimulation: true }).wait()).rejects.toThrowError(/dropped/); + // docs:end:pub-dropped + }); + }); + }); +}); diff --git a/yarn-project/foundation/src/fields/fields.ts b/yarn-project/foundation/src/fields/fields.ts index 284677ddac8..b0ecb881538 100644 --- a/yarn-project/foundation/src/fields/fields.ts +++ b/yarn-project/foundation/src/fields/fields.ts @@ -1,3 +1,4 @@ +import { AztecAddress } from '../aztec-address/index.js'; import { toBigIntBE, toBufferBE, toHex } from '../bigint-buffer/index.js'; import { randomBytes } from '../crypto/index.js'; import { BufferReader } from '../serialize/buffer_reader.js'; @@ -18,8 +19,8 @@ export class Fr { */ public readonly value: bigint; - constructor(value: bigint | number | Fr) { - const isFr = (value: bigint | number | Fr): value is Fr => !!(value as Fr).toBigInt; + constructor(value: bigint | number | Fr | AztecAddress) { + const isFr = (value: bigint | number | Fr | AztecAddress): value is Fr | AztecAddress => !!(value as Fr).toBigInt; this.value = isFr(value) ? value.toBigInt() : BigInt(value); if (this.value > Fr.MAX_VALUE) { throw new Error(`Fr out of range ${value}.`); diff --git a/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts b/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts index 003dae14cce..ba88a253519 100644 --- a/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts +++ b/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts @@ -182,10 +182,6 @@ ${abiStatement} export class ${input.name}Contract extends ContractBase { ${ctor} - get address() { - return this.completeAddress.address; - } - ${at} ${deploy} diff --git a/yarn-project/noir-contracts/src/contracts/native_token_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/native_token_contract/src/main.nr index 0155a592d3e..9658f2d4110 100644 --- a/yarn-project/noir-contracts/src/contracts/native_token_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/native_token_contract/src/main.nr @@ -24,16 +24,16 @@ contract NativeToken { use crate::storage::Storage; - use dep::aztec::types::point::Point; use dep::aztec::{ note::{ note_header::NoteHeader, utils as note_utils, }, + oracle::logs::emit_unencrypted_log, + public_call_stack_item::PublicCallStackItem, + types::point::Point }; - use dep::aztec::public_call_stack_item::PublicCallStackItem; - #[aztec(private)] fn constructor( initial_supply: Field, @@ -55,7 +55,8 @@ contract NativeToken { let new_balance = storage.public_balances.at(to).read() + amount; storage.public_balances.at(to).write(new_balance); storage.total_supply.write(storage.total_supply.read() + amount); - + let _hash = emit_unencrypted_log("Coins minted"); + 1 } @@ -207,6 +208,8 @@ contract NativeToken { // User has sufficient balance so we decrement it by `amount` sender_balance.write(current_sender_balance - amount); to_balance.write(current_to_balance + amount); + + let _hash = emit_unencrypted_log("Coins transferred"); } // uint256(keccak256("transfer_from_pub(field,field,field)")) >> 224 -> 1602017294 diff --git a/yarn-project/noir-contracts/src/contracts/test_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/test_contract/src/main.nr index 0c3573009af..41f398ead1c 100644 --- a/yarn-project/noir-contracts/src/contracts/test_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/test_contract/src/main.nr @@ -105,14 +105,15 @@ contract Test { context.push_new_nullifier(note.get_commitment(), 0); } + // docs:start:is-time-equal #[aztec(public)] fn isTimeEqual( time: Field, ) -> Field { assert(context.timestamp() == time); - time } + // docs:end:is-time-equal // Purely exists for testing unconstrained fn getRandom( diff --git a/yarn-project/noir-libs/value-note/src/value_note.nr b/yarn-project/noir-libs/value-note/src/value_note.nr index 79f0213edb1..b4b79c1bceb 100644 --- a/yarn-project/noir-libs/value-note/src/value_note.nr +++ b/yarn-project/noir-libs/value-note/src/value_note.nr @@ -10,12 +10,14 @@ use dep::aztec::oracle::{ global VALUE_NOTE_LEN: Field = 3; // 3 plus a header. +// docs:start:value-note-def struct ValueNote { value: Field, owner: Field, randomness: Field, header: NoteHeader, } +// docs:end:value-note-def impl ValueNote { fn new(value: Field, owner: Field) -> Self { diff --git a/yarn-project/types/src/interfaces/aztec_rpc.ts b/yarn-project/types/src/interfaces/aztec_rpc.ts index 5eded121bfe..0cfab2d07a0 100644 --- a/yarn-project/types/src/interfaces/aztec_rpc.ts +++ b/yarn-project/types/src/interfaces/aztec_rpc.ts @@ -5,6 +5,7 @@ import { ContractData, ExtendedContractData, L2BlockL2Logs, + NotePreimage, Tx, TxExecutionRequest, TxHash, @@ -158,6 +159,17 @@ export interface AztecRPC { */ getTxReceipt(txHash: TxHash): Promise; + /** + * Retrieves the private storage data at a specified contract address and storage slot. + * The returned data is data at the storage slot or throws an error if the contract is not deployed. + * + * @param owner - The address for whom the private data is encrypted. + * @param contract - The AztecAddress of the target contract. + * @param storageSlot - The Fr representing the storage slot to be fetched. + * @returns A set of note preimages for the owner in that contract and slot. + */ + getPrivateStorageAt(owner: AztecAddress, contract: AztecAddress, storageSlot: Fr): Promise; + /** * Retrieves the public storage data at a specified contract address and storage slot. * The returned data is data at the storage slot or throws an error if the contract is not deployed. diff --git a/yarn-project/types/src/logs/note_spending_info/note_preimage.ts b/yarn-project/types/src/logs/note_spending_info/note_preimage.ts index a2a3c24126d..3d849147984 100644 --- a/yarn-project/types/src/logs/note_spending_info/note_preimage.ts +++ b/yarn-project/types/src/logs/note_spending_info/note_preimage.ts @@ -34,4 +34,22 @@ export class NotePreimage extends Vector { const items = Array.from({ length: numItems }, () => Fr.random()); return new NotePreimage(items); } + + /** + * Returns a hex representation of this preimage. + * @returns A hex string with the vector length as first element. + */ + toString() { + return '0x' + this.toBuffer().toString('hex'); + } + + /** + * Creates a new NotePreimage instance from a hex string. + * @param str - Hex representation. + * @returns A NotePreimage instance. + */ + static fromString(str: string) { + const hex = str.replace(/^0x/, ''); + return NotePreimage.fromBuffer(Buffer.from(hex, 'hex')); + } } From 18770e611b9f4366b6a737cdc4a6457064876038 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Mon, 4 Sep 2023 18:44:51 -0300 Subject: [PATCH 03/11] Fix doc import --- docs/docs/dev_docs/dapps/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/dev_docs/dapps/testing.md b/docs/docs/dev_docs/dapps/testing.md index 3444933c251..1fba8a1f339 100644 --- a/docs/docs/dev_docs/dapps/testing.md +++ b/docs/docs/dev_docs/dapps/testing.md @@ -141,7 +141,7 @@ At the time of this writing, only unencrypted events can be queried directly. En We can query the RPC server for the unencrypted logs emitted in the block where our transaction is mined. Note that logs need to be unrolled and formatted as strings for consumption. -#include_code unencrypted-storage /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript +#include_code unencrypted-logs /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript ## Cheats From b1ac9dbadeb2aef993ab83bb0c3d9ba727a8a579 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 5 Sep 2023 10:02:26 -0300 Subject: [PATCH 04/11] Format --- yarn-project/aztec.js/src/utils/cheat_codes.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/yarn-project/aztec.js/src/utils/cheat_codes.ts b/yarn-project/aztec.js/src/utils/cheat_codes.ts index 16b93aa6f1d..8d05d7ed2f5 100644 --- a/yarn-project/aztec.js/src/utils/cheat_codes.ts +++ b/yarn-project/aztec.js/src/utils/cheat_codes.ts @@ -5,11 +5,8 @@ import { keccak } from '@aztec/foundation/crypto'; import { createDebugLogger } from '@aztec/foundation/log'; import { AztecRPC } from '@aztec/types'; - - import fs from 'fs'; - /** * A class that provides utility functions for interacting with the chain. */ @@ -282,4 +279,4 @@ export class AztecCheatCodes { } return Fr.fromBuffer(storageValue); } -} \ No newline at end of file +} From 4a85a1af301d6f963133b4570966d129393a4d7d Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 5 Sep 2023 10:28:16 -0300 Subject: [PATCH 05/11] Add code to CI --- .circleci/config.yml | 13 +++++++++ .../src/guides/dapp_testing.test.ts | 29 +++++++++++-------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fc5c8ddadd8..48d9220f71d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -959,6 +959,17 @@ jobs: command: ./scripts/cond_run_script end-to-end $JOB_NAME ./scripts/run_tests_local guides/writing_an_account_contract.test.ts working_directory: yarn-project/end-to-end + guides-dapp-testing: + docker: + - image: aztecprotocol/alpine-build-image + resource_class: small + steps: + - *checkout + - *setup_env + - run: + name: "Test" + command: cond_spot_run_tests end-to-end guides/dapp_testing.test.ts docker-compose-e2e-sandbox.yml + e2e-canary-test: docker: - image: aztecprotocol/alpine-build-image @@ -1393,6 +1404,7 @@ workflows: - e2e-browser-sandbox: *e2e_test - aztec-rpc-sandbox: *e2e_test - guides-writing-an-account-contract: *e2e_test + - guides-dapp-testing: *e2e_test - e2e-end: requires: @@ -1423,6 +1435,7 @@ workflows: - e2e-canary-test - aztec-rpc-sandbox - guides-writing-an-account-contract + - guides-dapp-testing <<: *defaults - deploy-dockerhub: diff --git a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts index 588d5019980..65e209d46bd 100644 --- a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts +++ b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts @@ -7,15 +7,22 @@ import { PrivateKey, createAztecRpcClient, getSchnorrAccount, + waitForSandbox, } from '@aztec/aztec.js'; import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; import { NativeTokenContract, PrivateTokenContract, TestContract } from '@aztec/noir-contracts/types'; describe('guides/dapp/testing', () => { + beforeAll(async () => { + const { SANDBOX_URL = 'http://localhost:8080' } = process.env; + const rpc = createAztecRpcClient(SANDBOX_URL); + await waitForSandbox(rpc); + }); + describe('on sandbox', () => { // docs:start:sandbox-example describe('private token contract', () => { - const AZTEC_URL = 'http://localhost:8080'; + const { SANDBOX_URL = 'http://localhost:8080' } = process.env; let rpc: AztecRPC; let owner: AccountWallet; @@ -23,7 +30,7 @@ describe('guides/dapp/testing', () => { let token: PrivateTokenContract; beforeEach(async () => { - rpc = createAztecRpcClient(AZTEC_URL); + rpc = createAztecRpcClient(SANDBOX_URL); owner = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); recipient = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); token = await PrivateTokenContract.deploy(owner, 100n, owner.getAddress()).send().deployed(); @@ -38,8 +45,7 @@ describe('guides/dapp/testing', () => { // docs:end:sandbox-example describe('cheats', () => { - const AZTEC_URL = 'http://localhost:8080'; - const ETH_RPC_URL = 'http://localhost:8545'; + const { SANDBOX_URL = 'http://localhost:8080', ETHEREUM_HOST = 'http://localhost:8545' } = process.env; let rpc: AztecRPC; let owner: AccountWallet; @@ -47,10 +53,10 @@ describe('guides/dapp/testing', () => { let cheats: CheatCodes; beforeAll(async () => { - rpc = createAztecRpcClient(AZTEC_URL); + rpc = createAztecRpcClient(SANDBOX_URL); owner = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); testContract = await TestContract.deploy(owner).send().deployed(); - cheats = await CheatCodes.create(ETH_RPC_URL, rpc); + cheats = await CheatCodes.create(ETHEREUM_HOST, rpc); }, 30_000); it('warps time to 1h into the future', async () => { @@ -63,8 +69,7 @@ describe('guides/dapp/testing', () => { }); describe('assertions', () => { - const AZTEC_URL = 'http://localhost:8080'; - const ETH_RPC_URL = 'http://localhost:8545'; + const { SANDBOX_URL = 'http://localhost:8080', ETHEREUM_HOST = 'http://localhost:8545' } = process.env; let rpc: AztecRPC; let owner: AccountWallet; @@ -75,14 +80,14 @@ describe('guides/dapp/testing', () => { let ownerSlot: Fr; beforeAll(async () => { - rpc = createAztecRpcClient(AZTEC_URL); + rpc = createAztecRpcClient(SANDBOX_URL); owner = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); recipient = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); token = await PrivateTokenContract.deploy(owner, 100n, owner.getAddress()).send().deployed(); nativeToken = await NativeTokenContract.deploy(owner, 100n, owner.getAddress()).send().deployed(); // docs:start:calc-slot - cheats = await CheatCodes.create(ETH_RPC_URL, rpc); + cheats = await CheatCodes.create(ETHEREUM_HOST, rpc); // The balances mapping is defined on storage slot 1 and is indexed by user address ownerSlot = cheats.aztec.computeSlotInMap(1n, owner.getAddress()); // docs:end:calc-slot @@ -144,14 +149,14 @@ describe('guides/dapp/testing', () => { it('asserts a simulation for a public function call fails', async () => { // docs:start:local-pub-fails - const call = nativeToken.methods.transfer_pub(recipient.getAddress(), 200n); + const call = nativeToken.methods.transfer_pub(recipient.getAddress(), 1000n); await expect(call.simulate()).rejects.toThrowError(/Assertion failed/); // docs:end:local-pub-fails }); it('asserts a transaction with a failing public call is dropped (until we get public reverts)', async () => { // docs:start:pub-dropped - const call = nativeToken.methods.transfer_pub(recipient.getAddress(), 200n); + const call = nativeToken.methods.transfer_pub(recipient.getAddress(), 1000n); await expect(call.send({ skipPublicSimulation: true }).wait()).rejects.toThrowError(/dropped/); // docs:end:pub-dropped }); From ea871601b38dfb56f21b51f50349562aabddf38c Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 5 Sep 2023 10:32:32 -0300 Subject: [PATCH 06/11] Add loadPrivate to cheat codes --- yarn-project/aztec.js/src/utils/cheat_codes.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/yarn-project/aztec.js/src/utils/cheat_codes.ts b/yarn-project/aztec.js/src/utils/cheat_codes.ts index 8d05d7ed2f5..6321e0ff135 100644 --- a/yarn-project/aztec.js/src/utils/cheat_codes.ts +++ b/yarn-project/aztec.js/src/utils/cheat_codes.ts @@ -3,7 +3,7 @@ import { pedersenPlookupCommitInputs } from '@aztec/circuits.js/barretenberg'; import { toBigIntBE, toHex } from '@aztec/foundation/bigint-buffer'; import { keccak } from '@aztec/foundation/crypto'; import { createDebugLogger } from '@aztec/foundation/log'; -import { AztecRPC } from '@aztec/types'; +import { AztecRPC, NotePreimage } from '@aztec/types'; import fs from 'fs'; @@ -279,4 +279,15 @@ export class AztecCheatCodes { } return Fr.fromBuffer(storageValue); } + + /** + * Loads the value stored at the given slot in the private storage of the given contract. + * @param contract - The address of the contract + * @param owner - The owner for whom the notes are encrypted + * @param slot - The storage slot to lookup + * @returns The notes stored at the given slot + */ + public loadPrivate(owner: AztecAddress, contract: AztecAddress, slot: Fr | bigint): Promise { + return this.aztecRpc.getPrivateStorageAt(owner, contract, new Fr(slot)); + } } From f95b83dd7f66ac4f72715aacdb16286c7207f0af Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 5 Sep 2023 15:37:30 -0300 Subject: [PATCH 07/11] Test using sandbox existing accounts --- docs/docs/dev_docs/dapps/testing.md | 6 +++++ yarn-project/aztec.js/package.json | 2 ++ yarn-project/aztec.js/src/sandbox/index.ts | 27 ++++++++++++++----- .../src/guides/dapp_testing.test.ts | 24 +++++++++++++++++ yarn-project/yarn.lock | 2 ++ 5 files changed, 55 insertions(+), 6 deletions(-) diff --git a/docs/docs/dev_docs/dapps/testing.md b/docs/docs/dev_docs/dapps/testing.md index 1fba8a1f339..169efe3d0d1 100644 --- a/docs/docs/dev_docs/dapps/testing.md +++ b/docs/docs/dev_docs/dapps/testing.md @@ -55,6 +55,12 @@ node Received tx 2132767911fbbe67e24a3e51bc769ba2ae874eb1ba56e69cef8fc9e2c5eba04 sequencer Submitted rollup block 5 with 1 transactions ``` +### Using Sandbox initial accounts + +Instead of creating new accounts in our test suite, we can use the ones already initialized by the Sandbox upon startup. This can provide a speed boost to your tests setup. However, bear in mind that you may accidentally introduce an interdependency across test suites by reusing the same accounts. + +#include_code use-existing-wallets /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + ## Assertions We will now see how to use `aztec.js` to write assertions about transaction statuses, about chain state both public and private, and about logs. diff --git a/yarn-project/aztec.js/package.json b/yarn-project/aztec.js/package.json index 4266ced7275..956b48c4183 100644 --- a/yarn-project/aztec.js/package.json +++ b/yarn-project/aztec.js/package.json @@ -43,6 +43,7 @@ "@aztec/types": "workspace:^", "lodash.every": "^4.6.0", "lodash.partition": "^4.6.0", + "lodash.zip": "^4.2.0", "tslib": "^2.4.0" }, "devDependencies": { @@ -51,6 +52,7 @@ "@types/jest": "^29.5.0", "@types/lodash.every": "^4.6.7", "@types/lodash.partition": "^4.6.0", + "@types/lodash.zip": "^4.2.7", "@types/node": "^18.7.23", "buffer": "^6.0.3", "crypto-browserify": "^3.12.0", diff --git a/yarn-project/aztec.js/src/sandbox/index.ts b/yarn-project/aztec.js/src/sandbox/index.ts index 8c05e61937b..c0790b63960 100644 --- a/yarn-project/aztec.js/src/sandbox/index.ts +++ b/yarn-project/aztec.js/src/sandbox/index.ts @@ -2,8 +2,10 @@ import { Fr, PrivateKey } from '@aztec/circuits.js'; import { ContractAbi } from '@aztec/foundation/abi'; import { sleep } from '@aztec/foundation/sleep'; +import zip from 'lodash.zip'; + import SchnorrAccountContractAbi from '../abis/schnorr_account_contract.json' assert { type: 'json' }; -import { AztecRPC, getAccountWallets, getSchnorrAccount } from '../index.js'; +import { AccountWallet, AztecRPC, EntrypointWallet, getAccountWallets, getSchnorrAccount } from '../index.js'; export const INITIAL_SANDBOX_ENCRYPTION_KEYS = [ new PrivateKey(Buffer.from('b2803ec899f76f6b2ac011480d24028f1a29587f8a3a92f7ee9d48d8c085c284', 'hex')), @@ -18,11 +20,11 @@ export const INITIAL_SANDBOX_SALTS = [Fr.ZERO, Fr.ZERO, Fr.ZERO]; export const INITIAL_SANDBOX_ACCOUNT_CONTRACT_ABI = SchnorrAccountContractAbi; /** - * Gets a wallet for the Aztec accounts that are initially stored in the sandbox. + * Gets a single wallet that manages all the Aztec accounts that are initially stored in the sandbox. * @param aztecRpc - An instance of the Aztec RPC interface. - * @returns An AccountWallet implementation that includes all the accounts found. + * @returns An AccountWallet implementation that includes all the initial accounts. */ -export async function getSandboxAccountsWallet(aztecRpc: AztecRPC) { +export async function getSandboxAccountsWallet(aztecRpc: AztecRPC): Promise { return await getAccountWallets( aztecRpc, INITIAL_SANDBOX_ACCOUNT_CONTRACT_ABI as unknown as ContractAbi, @@ -32,6 +34,19 @@ export async function getSandboxAccountsWallet(aztecRpc: AztecRPC) { ); } +/** + * Gets a collection of wallets for the Aztec accounts that are initially stored in the sandbox. + * @param aztecRpc - An instance of the Aztec RPC interface. + * @returns A set of AccountWallet implementations for each of the initial accounts. + */ +export function getSandboxAccountsWallets(aztecRpc: AztecRPC): Promise { + return Promise.all( + zip(INITIAL_SANDBOX_ENCRYPTION_KEYS, INITIAL_SANDBOX_SIGNING_KEYS, INITIAL_SANDBOX_SALTS).map( + ([encryptionKey, signingKey, salt]) => getSchnorrAccount(aztecRpc, encryptionKey!, signingKey!, salt).getWallet(), + ), + ); +} + /** * Deploys the initial set of schnorr signature accounts to the sandbox * @param aztecRpc - An instance of the Aztec RPC interface. @@ -71,7 +86,7 @@ export async function deployInitialSandboxAccounts(aztecRpc: AztecRPC) { * Function to wait until the sandbox becomes ready for use. * @param rpcServer - The rpc client connected to the sandbox. */ -export const waitForSandbox = async (rpcServer: AztecRPC) => { +export async function waitForSandbox(rpcServer: AztecRPC) { while (true) { try { await rpcServer.getNodeInfo(); @@ -80,4 +95,4 @@ export const waitForSandbox = async (rpcServer: AztecRPC) => { await sleep(1000); } } -}; +} diff --git a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts index 65e209d46bd..c2535cabf75 100644 --- a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts +++ b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts @@ -6,6 +6,7 @@ import { L2BlockL2Logs, PrivateKey, createAztecRpcClient, + getSandboxAccountsWallets, getSchnorrAccount, waitForSandbox, } from '@aztec/aztec.js'; @@ -44,6 +45,29 @@ describe('guides/dapp/testing', () => { }); // docs:end:sandbox-example + describe('private token contract with initial accounts', () => { + const { SANDBOX_URL = 'http://localhost:8080' } = process.env; + + let rpc: AztecRPC; + let owner: AccountWallet; + let recipient: AccountWallet; + let token: PrivateTokenContract; + + beforeEach(async () => { + // docs:start:use-existing-wallets + rpc = createAztecRpcClient(SANDBOX_URL); + [owner, recipient] = await getSandboxAccountsWallets(rpc); + token = await PrivateTokenContract.deploy(owner, 100n, owner.getAddress()).send().deployed(); + // docs:end:use-existing-wallets + }, 30_000); + + it('increases recipient funds on transfer', async () => { + expect(await token.methods.getBalance(recipient.getAddress()).view()).toEqual(0n); + await token.methods.transfer(20n, recipient.getAddress()).send().wait(); + expect(await token.methods.getBalance(recipient.getAddress()).view()).toEqual(20n); + }); + }); + describe('cheats', () => { const { SANDBOX_URL = 'http://localhost:8080', ETHEREUM_HOST = 'http://localhost:8545' } = process.env; diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index f9a3923961e..f6a6e73449d 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -214,6 +214,7 @@ __metadata: "@types/jest": ^29.5.0 "@types/lodash.every": ^4.6.7 "@types/lodash.partition": ^4.6.0 + "@types/lodash.zip": ^4.2.7 "@types/node": ^18.7.23 buffer: ^6.0.3 crypto-browserify: ^3.12.0 @@ -221,6 +222,7 @@ __metadata: jest-mock-extended: ^3.0.3 lodash.every: ^4.6.0 lodash.partition: ^4.6.0 + lodash.zip: ^4.2.0 process: ^0.11.10 resolve-typescript-plugin: ^2.0.1 stream-browserify: ^3.0.0 From adccf65611a9607c8e30483c19687b20f2dd94c4 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 5 Sep 2023 16:17:59 -0300 Subject: [PATCH 08/11] Start sandbox in-proc --- docs/docs/dev_docs/dapps/testing.md | 10 ++ yarn-project/aztec-sandbox/package.json | 7 +- yarn-project/aztec-sandbox/src/bin/index.ts | 63 ++++++++++ yarn-project/aztec-sandbox/src/index.ts | 119 +----------------- yarn-project/aztec-sandbox/src/sandbox.ts | 84 +++++++++++++ yarn-project/end-to-end/src/e2e_cli.test.ts | 2 +- .../src/guides/dapp_testing.test.ts | 40 +++++- 7 files changed, 198 insertions(+), 127 deletions(-) create mode 100644 yarn-project/aztec-sandbox/src/bin/index.ts create mode 100644 yarn-project/aztec-sandbox/src/sandbox.ts diff --git a/docs/docs/dev_docs/dapps/testing.md b/docs/docs/dev_docs/dapps/testing.md index 169efe3d0d1..8c9a8b4170c 100644 --- a/docs/docs/dev_docs/dapps/testing.md +++ b/docs/docs/dev_docs/dapps/testing.md @@ -61,6 +61,16 @@ Instead of creating new accounts in our test suite, we can use the ones already #include_code use-existing-wallets /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript +### Running Sandbox in the nodejs process + +Instead of connecting to a local running Sandbox instance, you can also start your own Sandbox within the nodejs process running your tests, for an easier setup. To do this, import the `@aztec/aztec-sandbox` package in your project, and run `createSandbox` during setup. Note that this will still require you to run a local Ethereum node like Anvil locally. + +#include_code in-proc-sandbox /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +The `createSandbox` returns a `stop` callback that you should run once your test suite is over to stop all Sandbox services. + +#include_code stop-in-proc-sandbox /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + ## Assertions We will now see how to use `aztec.js` to write assertions about transaction statuses, about chain state both public and private, and about logs. diff --git a/yarn-project/aztec-sandbox/package.json b/yarn-project/aztec-sandbox/package.json index 3bf0880a9a7..f565e533ef1 100644 --- a/yarn-project/aztec-sandbox/package.json +++ b/yarn-project/aztec-sandbox/package.json @@ -3,10 +3,9 @@ "version": "0.1.0", "type": "module", "exports": { - ".": "./dest/index.js", - "./http": "./dest/server.js" + ".": "./dest/index.js" }, - "bin": "./dest/index.js", + "bin": "./dest/bin/index.js", "typedocOptions": { "entryPoints": [ "./src/index.ts" @@ -17,7 +16,7 @@ "scripts": { "prepare": "node ../yarn-project-base/scripts/update_build_manifest.mjs package.json", "build": "yarn clean && tsc -b", - "start": "node --no-warnings ./dest", + "start": "node --no-warnings ./dest/bin", "clean": "rm -rf ./dest .tsbuildinfo", "formatting": "run -T prettier --check ./src && run -T eslint ./src", "formatting:fix": "run -T prettier -w ./src", diff --git a/yarn-project/aztec-sandbox/src/bin/index.ts b/yarn-project/aztec-sandbox/src/bin/index.ts new file mode 100644 index 00000000000..c4acb394119 --- /dev/null +++ b/yarn-project/aztec-sandbox/src/bin/index.ts @@ -0,0 +1,63 @@ +#!/usr/bin/env -S node --no-warnings +import { deployInitialSandboxAccounts } from '@aztec/aztec.js'; +import { createDebugLogger } from '@aztec/foundation/log'; +import { fileURLToPath } from '@aztec/foundation/url'; + +import { readFileSync } from 'fs'; +import { dirname, resolve } from 'path'; + +import { setupFileDebugLog } from '../logging.js'; +import { createSandbox } from '../sandbox.js'; +import { startHttpRpcServer } from '../server.js'; +import { github, splash } from '../splash.js'; + +const { SERVER_PORT = 8080 } = process.env; + +const logger = createDebugLogger('aztec:sandbox'); + +/** + * Create and start a new Aztec RCP HTTP Server + */ +async function main() { + const logPath = setupFileDebugLog(); + const packageJsonPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'); + const version: string = JSON.parse(readFileSync(packageJsonPath).toString()).version; + + logger.info(`Setting up Aztec Sandbox v${version}, please stand by...`); + + const { l1Contracts, rpcServer, stop } = await createSandbox(); + + logger.info('Setting up test accounts...'); + const accounts = await deployInitialSandboxAccounts(rpcServer); + + const shutdown = async () => { + logger.info('Shutting down...'); + await stop(); + process.exit(0); + }; + + process.once('SIGINT', shutdown); + process.once('SIGTERM', shutdown); + + startHttpRpcServer(rpcServer, l1Contracts, SERVER_PORT); + logger.info(`Aztec Sandbox JSON-RPC Server listening on port ${SERVER_PORT}`); + logger.info(`Debug logs will be written to ${logPath}`); + const accountStrings = [`Initial Accounts:\n\n`]; + + const registeredAccounts = await rpcServer.getAccounts(); + for (const account of accounts) { + const completeAddress = await account.account.getCompleteAddress(); + if (registeredAccounts.find(a => a.equals(completeAddress))) { + accountStrings.push(` Address: ${completeAddress.address.toString()}\n`); + accountStrings.push(` Partial Address: ${completeAddress.partialAddress.toString()}\n`); + accountStrings.push(` Private Key: ${account.privateKey.toString()}\n`); + accountStrings.push(` Public Key: ${completeAddress.publicKey.toString()}\n\n`); + } + } + logger.info(`${splash}\n${github}\n\n`.concat(...accountStrings).concat(`Aztec Sandbox is now ready for use!`)); +} + +main().catch(err => { + logger.error(err); + process.exit(1); +}); diff --git a/yarn-project/aztec-sandbox/src/index.ts b/yarn-project/aztec-sandbox/src/index.ts index 093353350f2..a9a6669f682 100644 --- a/yarn-project/aztec-sandbox/src/index.ts +++ b/yarn-project/aztec-sandbox/src/index.ts @@ -1,117 +1,2 @@ -#!/usr/bin/env -S node --no-warnings -import { AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '@aztec/aztec-node'; -import { createAztecRPCServer, getConfigEnvVars as getRpcConfigEnvVars } from '@aztec/aztec-rpc'; -import { deployInitialSandboxAccounts } from '@aztec/aztec.js'; -import { PrivateKey } from '@aztec/circuits.js'; -import { deployL1Contracts } from '@aztec/ethereum'; -import { createDebugLogger } from '@aztec/foundation/log'; -import { retryUntil } from '@aztec/foundation/retry'; -import { fileURLToPath } from '@aztec/foundation/url'; - -import { readFileSync } from 'fs'; -import { dirname, resolve } from 'path'; -import { HDAccount, createPublicClient, http as httpViemTransport } from 'viem'; -import { mnemonicToAccount } from 'viem/accounts'; -import { foundry } from 'viem/chains'; - -import { setupFileDebugLog } from './logging.js'; -import { startHttpRpcServer } from './server.js'; -import { github, splash } from './splash.js'; - -const { SERVER_PORT = 8080, MNEMONIC = 'test test test test test test test test test test test junk' } = process.env; - -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.warn(`Failed to connect to Ethereum node at ${rpcUrl}. Retrying...`); - } - return chainId; - }, - 'isEthRpcReady', - 600, - 1, - ); - - if (!chainID) { - throw Error(`Ethereum node 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 logPath = setupFileDebugLog(); - const aztecNodeConfig: AztecNodeConfig = getConfigEnvVars(); - const rpcConfig = getRpcConfigEnvVars(); - const hdAccount = mnemonicToAccount(MNEMONIC); - const privKey = hdAccount.getHdKey().privateKey; - const packageJsonPath = resolve(dirname(fileURLToPath(import.meta.url)), '../package.json'); - const version: string = JSON.parse(readFileSync(packageJsonPath).toString()).version; - - logger.info(`Setting up Aztec Sandbox v${version}, please stand by...`); - logger.info('Deploying rollup contracts to L1...'); - const deployedL1Contracts = await waitThenDeploy(aztecNodeConfig.rpcUrl, hdAccount); - aztecNodeConfig.publisherPrivateKey = new PrivateKey(Buffer.from(privKey!)); - aztecNodeConfig.rollupContract = deployedL1Contracts.rollupAddress; - aztecNodeConfig.contractDeploymentEmitterContract = deployedL1Contracts.contractDeploymentEmitterAddress; - aztecNodeConfig.inboxContract = deployedL1Contracts.inboxAddress; - - const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig); - const aztecRpcServer = await createAztecRPCServer(aztecNode, rpcConfig); - - logger.info('Setting up test accounts...'); - const accounts = await deployInitialSandboxAccounts(aztecRpcServer); - - const shutdown = async () => { - logger.info('Shutting down...'); - await aztecRpcServer.stop(); - await aztecNode.stop(); - process.exit(0); - }; - - process.once('SIGINT', shutdown); - process.once('SIGTERM', shutdown); - - startHttpRpcServer(aztecRpcServer, deployedL1Contracts, SERVER_PORT); - logger.info(`Aztec Sandbox JSON-RPC Server listening on port ${SERVER_PORT}`); - logger.info(`Debug logs will be written to ${logPath}`); - const accountStrings = [`Initial Accounts:\n\n`]; - - const registeredAccounts = await aztecRpcServer.getAccounts(); - for (const account of accounts) { - const completeAddress = await account.account.getCompleteAddress(); - if (registeredAccounts.find(a => a.equals(completeAddress))) { - accountStrings.push(` Address: ${completeAddress.address.toString()}\n`); - accountStrings.push(` Partial Address: ${completeAddress.partialAddress.toString()}\n`); - accountStrings.push(` Private Key: ${account.privateKey.toString()}\n`); - accountStrings.push(` Public Key: ${completeAddress.publicKey.toString()}\n\n`); - } - } - logger.info(`${splash}\n${github}\n\n`.concat(...accountStrings).concat(`Aztec Sandbox is now ready for use!`)); -} - -main().catch(err => { - logger.error(err); - process.exit(1); -}); +export { createSandbox } from './sandbox.js'; +export { startHttpRpcServer } from './server.js'; diff --git a/yarn-project/aztec-sandbox/src/sandbox.ts b/yarn-project/aztec-sandbox/src/sandbox.ts new file mode 100644 index 00000000000..ff1f7c7bc21 --- /dev/null +++ b/yarn-project/aztec-sandbox/src/sandbox.ts @@ -0,0 +1,84 @@ +#!/usr/bin/env -S node --no-warnings +import { AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '@aztec/aztec-node'; +import { createAztecRPCServer, getConfigEnvVars as getRpcConfigEnvVars } from '@aztec/aztec-rpc'; +import { PrivateKey } from '@aztec/circuits.js'; +import { deployL1Contracts } from '@aztec/ethereum'; +import { createDebugLogger } from '@aztec/foundation/log'; +import { retryUntil } from '@aztec/foundation/retry'; + +import { HDAccount, createPublicClient, http as httpViemTransport } from 'viem'; +import { mnemonicToAccount } from 'viem/accounts'; +import { foundry } from 'viem/chains'; + +const { MNEMONIC = 'test test test test test test test test test test test junk' } = process.env; + +const logger = createDebugLogger('aztec:sandbox'); + +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.warn(`Failed to connect to Ethereum node at ${rpcUrl}. Retrying...`); + } + return chainId; + }, + 'isEthRpcReady', + 600, + 1, + ); + + if (!chainID) { + throw Error(`Ethereum node unresponsive at ${rpcUrl}.`); + } + + // Deploy L1 contracts + const deployedL1Contracts = await deployL1Contracts(rpcUrl, hdAccount, localAnvil, logger); + return deployedL1Contracts; +} + +/** Sandbox settings. */ +export type SandboxConfig = AztecNodeConfig & { + /** Mnemonic used to derive the L1 deployer private key.*/ + l1Mnemonic: string; +}; + +/** + * Create and start a new Aztec Node and RPC Server. Deploys L1 contracts. + * Does not start any HTTP services nor populate any initial accounts. + * @param config - Optional Sandbox settings. + */ +export async function createSandbox(config: Partial = {}) { + const aztecNodeConfig: AztecNodeConfig = { ...getConfigEnvVars(), ...config }; + const rpcConfig = getRpcConfigEnvVars(); + const hdAccount = mnemonicToAccount(config.l1Mnemonic ?? MNEMONIC); + const privKey = hdAccount.getHdKey().privateKey; + + const l1Contracts = await waitThenDeploy(aztecNodeConfig.rpcUrl, hdAccount); + aztecNodeConfig.publisherPrivateKey = new PrivateKey(Buffer.from(privKey!)); + aztecNodeConfig.rollupContract = l1Contracts.rollupAddress; + aztecNodeConfig.contractDeploymentEmitterContract = l1Contracts.contractDeploymentEmitterAddress; + aztecNodeConfig.inboxContract = l1Contracts.inboxAddress; + + const node = await AztecNodeService.createAndSync(aztecNodeConfig); + const rpcServer = await createAztecRPCServer(node, rpcConfig); + + const stop = async () => { + await rpcServer.stop(); + await node.stop(); + }; + + return { node, rpcServer, l1Contracts, stop }; +} diff --git a/yarn-project/end-to-end/src/e2e_cli.test.ts b/yarn-project/end-to-end/src/e2e_cli.test.ts index 21d038e6f6b..2bffc76917a 100644 --- a/yarn-project/end-to-end/src/e2e_cli.test.ts +++ b/yarn-project/end-to-end/src/e2e_cli.test.ts @@ -1,6 +1,6 @@ import { AztecNodeService } from '@aztec/aztec-node'; import { AztecRPCServer } from '@aztec/aztec-rpc'; -import { startHttpRpcServer } from '@aztec/aztec-sandbox/http'; +import { startHttpRpcServer } from '@aztec/aztec-sandbox'; import { createDebugLogger } from '@aztec/aztec.js'; import { cliTestSuite } from '@aztec/cli'; import { AztecRPC } from '@aztec/types'; diff --git a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts index c2535cabf75..7887835a601 100644 --- a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts +++ b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts @@ -1,3 +1,4 @@ +import { createSandbox } from '@aztec/aztec-sandbox'; import { AccountWallet, AztecRPC, @@ -14,13 +15,42 @@ import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; import { NativeTokenContract, PrivateTokenContract, TestContract } from '@aztec/noir-contracts/types'; describe('guides/dapp/testing', () => { - beforeAll(async () => { - const { SANDBOX_URL = 'http://localhost:8080' } = process.env; - const rpc = createAztecRpcClient(SANDBOX_URL); - await waitForSandbox(rpc); + describe('on in-proc sandbox', () => { + describe('private token contract', () => { + let rpc: AztecRPC; + let stop: () => Promise; + let owner: AccountWallet; + let recipient: AccountWallet; + let token: PrivateTokenContract; + + beforeAll(async () => { + // docs:start:in-proc-sandbox + ({ rpcServer: rpc, stop } = await createSandbox()); + // docs:end:in-proc-sandbox + owner = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); + recipient = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); + token = await PrivateTokenContract.deploy(owner, 100n, owner.getAddress()).send().deployed(); + }, 30_000); + + // docs:start:stop-in-proc-sandbox + afterAll(() => stop()); + // docs:end:stop-in-proc-sandbox + + it('increases recipient funds on transfer', async () => { + expect(await token.methods.getBalance(recipient.getAddress()).view()).toEqual(0n); + await token.methods.transfer(20n, recipient.getAddress()).send().wait(); + expect(await token.methods.getBalance(recipient.getAddress()).view()).toEqual(20n); + }); + }); }); - describe('on sandbox', () => { + describe('on local sandbox', () => { + beforeAll(async () => { + const { SANDBOX_URL = 'http://localhost:8080' } = process.env; + const rpc = createAztecRpcClient(SANDBOX_URL); + await waitForSandbox(rpc); + }); + // docs:start:sandbox-example describe('private token contract', () => { const { SANDBOX_URL = 'http://localhost:8080' } = process.env; From d77c000efd507426330cc52cd2f12e35d35e9c53 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 5 Sep 2023 16:35:44 -0300 Subject: [PATCH 09/11] Update yarn.lock --- yarn-project/yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 245167154fe..1a0496d21a3 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -198,7 +198,7 @@ __metadata: winston: ^3.10.0 winston-daily-rotate-file: ^4.7.1 bin: - aztec-sandbox: ./dest/index.js + aztec-sandbox: ./dest/bin/index.js languageName: unknown linkType: soft From 977f8be1cc12926aeacaed5dfef1ca4a79c6248a Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 5 Sep 2023 17:51:27 -0300 Subject: [PATCH 10/11] Update error messages after merge --- yarn-project/end-to-end/src/guides/dapp_testing.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts index 7887835a601..0a12a0428d5 100644 --- a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts +++ b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts @@ -177,14 +177,14 @@ describe('guides/dapp/testing', () => { it('asserts a local transaction simulation fails by calling simulate', async () => { // docs:start:local-tx-fails const call = token.methods.transfer(200n, recipient.getAddress()); - await expect(call.simulate()).rejects.toThrowError(/Assertion failed/); + await expect(call.simulate()).rejects.toThrowError(/Cannot satisfy constraint/); // docs:end:local-tx-fails }); it('asserts a local transaction simulation fails by calling send', async () => { // docs:start:local-tx-fails-send const call = token.methods.transfer(200n, recipient.getAddress()); - await expect(call.send().wait()).rejects.toThrowError(/Assertion failed/); + await expect(call.send().wait()).rejects.toThrowError(/Cannot satisfy constraint/); // docs:end:local-tx-fails-send }); @@ -204,7 +204,7 @@ describe('guides/dapp/testing', () => { it('asserts a simulation for a public function call fails', async () => { // docs:start:local-pub-fails const call = nativeToken.methods.transfer_pub(recipient.getAddress(), 1000n); - await expect(call.simulate()).rejects.toThrowError(/Assertion failed/); + await expect(call.simulate()).rejects.toThrowError(/Failed to solve/); // docs:end:local-pub-fails }); From aa4fbbba2ac9ef045ad7b062ba8225d3f24ca048 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 5 Sep 2023 18:08:06 -0300 Subject: [PATCH 11/11] Use assertion messages in expectations --- docs/docs/dev_docs/dapps/testing.md | 6 +++--- yarn-project/end-to-end/src/guides/dapp_testing.test.ts | 6 +++--- .../src/contracts/native_token_contract/src/main.nr | 2 +- yarn-project/noir-libs/value-note/src/utils.nr | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/docs/dev_docs/dapps/testing.md b/docs/docs/dev_docs/dapps/testing.md index 8c9a8b4170c..93fe000049e 100644 --- a/docs/docs/dev_docs/dapps/testing.md +++ b/docs/docs/dev_docs/dapps/testing.md @@ -81,7 +81,7 @@ In the example above we used `contract.methods.method().send().wait()` to create #### A private call fails -We can check that a call to a private function would fail by simulating it locally and expecting a rejection. Remember that all private function calls are only executed locally in order to preserve privacy. As an example, we can try transferring more tokens than we have. +We can check that a call to a private function would fail by simulating it locally and expecting a rejection. Remember that all private function calls are only executed locally in order to preserve privacy. As an example, we can try transferring more tokens than we have, which will fail an assertion with the `Balance too low` error message. #include_code local-tx-fails /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript @@ -111,10 +111,10 @@ We can ignore a local simulation error for a public function via the `skipPublic #include_code pub-dropped /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript -If you run the snippet above, you'll see the following error in the Sandbox logs, where `0x1cbcdf5094fe2f1cda9a47621c4528c30a13a10e610942f656b95130015f44f8` is the address of the contract and `3c5e7ad4` is the selector of the `transfer_pub` function we called. +If you run the snippet above, you'll see the following error in the Sandbox logs: ``` -WARN Error processing tx 06dc87c4d64462916ea58426ffcfaf20017880b353c9ec3e0f0ee5fab3ea923f: Simulation error: Cannot satisfy constraint 0.19085 at 0x1cbcdf5094fe2f1cda9a47621c4528c30a13a10e610942f656b95130015f44f8.3c5e7ad4 +WARN Error processing tx 06dc87c4d64462916ea58426ffcfaf20017880b353c9ec3e0f0ee5fab3ea923f: Assertion failed: Balance too low. ``` :::info diff --git a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts index 0a12a0428d5..e65a655da32 100644 --- a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts +++ b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts @@ -177,14 +177,14 @@ describe('guides/dapp/testing', () => { it('asserts a local transaction simulation fails by calling simulate', async () => { // docs:start:local-tx-fails const call = token.methods.transfer(200n, recipient.getAddress()); - await expect(call.simulate()).rejects.toThrowError(/Cannot satisfy constraint/); + await expect(call.simulate()).rejects.toThrowError(/Balance too low/); // docs:end:local-tx-fails }); it('asserts a local transaction simulation fails by calling send', async () => { // docs:start:local-tx-fails-send const call = token.methods.transfer(200n, recipient.getAddress()); - await expect(call.send().wait()).rejects.toThrowError(/Cannot satisfy constraint/); + await expect(call.send().wait()).rejects.toThrowError(/Balance too low/); // docs:end:local-tx-fails-send }); @@ -204,7 +204,7 @@ describe('guides/dapp/testing', () => { it('asserts a simulation for a public function call fails', async () => { // docs:start:local-pub-fails const call = nativeToken.methods.transfer_pub(recipient.getAddress(), 1000n); - await expect(call.simulate()).rejects.toThrowError(/Failed to solve/); + await expect(call.simulate()).rejects.toThrowError(/Balance too low/); // docs:end:local-pub-fails }); diff --git a/yarn-project/noir-contracts/src/contracts/native_token_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/native_token_contract/src/main.nr index bc6a5edb43a..f5875b04687 100644 --- a/yarn-project/noir-contracts/src/contracts/native_token_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/native_token_contract/src/main.nr @@ -191,7 +191,7 @@ contract NativeToken { let sender = context.msg_sender(); let sender_balance = storage.public_balances.at(sender); let current_sender_balance: Field = sender_balance.read(); - assert(current_sender_balance as u120 >= amount as u120); + assert(current_sender_balance as u120 >= amount as u120, "Balance too low"); let to_balance = storage.public_balances.at(to); let current_to_balance: Field = to_balance.read(); diff --git a/yarn-project/noir-libs/value-note/src/utils.nr b/yarn-project/noir-libs/value-note/src/utils.nr index 13e1d42db53..f431895e956 100644 --- a/yarn-project/noir-libs/value-note/src/utils.nr +++ b/yarn-project/noir-libs/value-note/src/utils.nr @@ -45,7 +45,7 @@ fn decrement( owner: Field, ) { let sum = decrement_by_at_most(balance, amount, owner); - assert(sum == amount); + assert(sum == amount, "Balance too low"); } // Similar to `decrement`, except that it doesn't fail if the decremented amount is less than max_amount.