diff --git a/AGENTS.md b/AGENTS.md index 5afc3dd..d4d35ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,6 +24,29 @@ There are two independent test systems: - **TypeScript E2E tests** (`yarn test:js`): Require a running local network. - `yarn test` runs both. Ensure tests pass before committing. +## Simulate Before Send + +**Always call `.simulate()` before `.send()` for every state-changing transaction.** Simulation runs the transaction locally and surfaces revert reasons immediately instead of waiting for the send timeout with an opaque error. + +```typescript +// Simulate first +await contract.methods.create_game(gameId).simulate({ from: address }); +// Then send +await contract.methods.create_game(gameId).send({ + from: address, + fee: { paymentMethod }, + wait: { timeout } +}); +``` + +For deployments, store the deploy request to reuse it: + +```typescript +const deployRequest = MyContract.deploy(wallet, ...args); +await deployRequest.simulate({ from: address }); +const contract = await deployRequest.send({ ... }); +``` + ## Pull Requests - Use clear commit messages and provide a concise description in the PR body about the change. - Mention which tests were executed. diff --git a/CLAUDE.md b/CLAUDE.md index 8a653cb..8f0d5b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,6 +105,36 @@ yarn profile # Profile a transaction deployment - **Wallet setup**: `EmbeddedWallet.create()` with `ephemeral: true` for tests; prover is enabled only on devnet. - **PXE store**: Data persists in `./store`. Must delete after local network restart to avoid stale state errors. +## Simulate Before Send (IMPORTANT) + +**Always call `.simulate()` before `.send()` for every state-changing transaction.** Simulation runs the transaction locally and surfaces revert reasons immediately. Without it, a failing transaction will hang until the send timeout (up to 600s) with an opaque error. + +```typescript +// Simulate first — surfaces revert reasons instantly +await contract.methods.create_game(gameId).simulate({ from: address }); + +// Then send — only after simulation succeeds +await contract.methods.create_game(gameId).send({ + from: address, + fee: { paymentMethod }, + wait: { timeout: timeouts.txTimeout } +}); +``` + +For deployments, store the deploy request to avoid constructing it twice: + +```typescript +const deployRequest = MyContract.deploy(wallet, ...args); +await deployRequest.simulate({ from: address }); +const contract = await deployRequest.send({ ... }); +``` + +**Checklist:** + +- Every `.send()` call must be preceded by a `.simulate()` call +- `.simulate()` does not need fee parameters — only `from` is required +- View/read-only calls (e.g. `balance_of_private`) already use `.simulate()` to return values — no `.send()` needed for those + ## Version Update Procedure When updating the Aztec version, update all of these locations: diff --git a/README.md b/README.md index 3b2928f..f546caf 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,30 @@ The `./src/utils/` folder contains utility functions: - `./src/utils/sponsored_fpc.ts` provides functions to deploy and manage the SponsoredFPC (Fee Payment Contract) for handling sponsored transaction fees. - `./config/config.ts` provides environment-aware configuration loading, automatically selecting the correct JSON config file based on the `ENV` variable. +## Simulate Before Send + +Always call `.simulate()` before `.send()` for every state-changing transaction. Simulation runs the transaction locally and surfaces revert reasons immediately. Without it, a failing transaction will hang until the send timeout with an opaque error. + +```typescript +// Simulate first — surfaces revert reasons instantly +await contract.methods.create_game(gameId).simulate({ from: address }); + +// Then send — only after simulation succeeds +await contract.methods.create_game(gameId).send({ + from: address, + fee: { paymentMethod }, + wait: { timeout } +}); +``` + +For deployments, store the deploy request to avoid constructing it twice: + +```typescript +const deployRequest = MyContract.deploy(wallet, ...args); +await deployRequest.simulate({ from: address }); +const contract = await deployRequest.send({ ... }); +``` + ## ❗ **Error Resolution** :warning: Tests and scripts set up and run the Private Execution Environment (PXE) and store PXE data in the `./store` directory. If you restart the local network, you will need to delete the `./store` directory to avoid errors. diff --git a/scripts/deploy_contract.ts b/scripts/deploy_contract.ts index 8d59281..ca3a6e2 100644 --- a/scripts/deploy_contract.ts +++ b/scripts/deploy_contract.ts @@ -40,8 +40,13 @@ async function main() { logger.info('🏎️ Starting pod racing contract deployment...'); logger.info(`📋 Admin address for pod racing contract: ${address}`); - logger.info('⏳ Waiting for deployment transaction to be mined...'); - const { contract: podRacingContract, instance } = await PodRacingContract.deploy(wallet, address).send({ + logger.info('⏳ Simulating deployment transaction...'); + const deployRequest = PodRacingContract.deploy(wallet, address); + await deployRequest.simulate({ + from: address, + }); + logger.info('✅ Simulation successful, sending transaction...'); + const { contract: podRacingContract, instance } = await deployRequest.send({ from: address, fee: { paymentMethod: sponsoredPaymentMethod }, wait: { timeout: timeouts.deployTimeout, returnReceipt: true } diff --git a/scripts/fees.ts b/scripts/fees.ts index 0a3edf2..990b050 100644 --- a/scripts/fees.ts +++ b/scripts/fees.ts @@ -71,12 +71,18 @@ async function main() { const timeouts = getTimeouts(); // Two arbitrary txs to make the L1 message available on L2 - const podRacingContract = await PodRacingContract.deploy(wallet, account1.address).send({ + // Simulate before sending to surface revert reasons + const podRacingDeploy = PodRacingContract.deploy(wallet, account1.address); + await podRacingDeploy.simulate({ from: account1.address }); + const podRacingContract = await podRacingDeploy.send({ from: account1.address, fee: { paymentMethod }, wait: { timeout: timeouts.deployTimeout } }); - const bananaCoin = await TokenContract.deploy(wallet, account1.address, "bananaCoin", "BNC", 18).send({ + + const bananaCoinDeploy = TokenContract.deploy(wallet, account1.address, "bananaCoin", "BNC", 18); + await bananaCoinDeploy.simulate({ from: account1.address }); + const bananaCoin = await bananaCoinDeploy.send({ from: account1.address, fee: { paymentMethod }, wait: { timeout: timeouts.deployTimeout } @@ -93,6 +99,7 @@ async function main() { // Create a new game on the pod racing contract, interacting from the newWallet const gameId = Fr.random(); + await podRacingContract.methods.create_game(gameId).simulate({ from: account2.address }); await podRacingContract.methods.create_game(gameId).send({ from: account2.address, wait: { timeout: timeouts.txTimeout } @@ -105,7 +112,9 @@ async function main() { // Need to deploy an FPC to use Private Fee payment methods // This uses bananaCoin as the fee paying asset that will be exchanged for fee juice - const fpc = await FPCContract.deploy(wallet, bananaCoin.address, account1.address).send({ + const fpcDeploy = FPCContract.deploy(wallet, bananaCoin.address, account1.address); + await fpcDeploy.simulate({ from: account1.address }); + const fpc = await fpcDeploy.send({ from: account1.address, fee: { paymentMethod }, wait: { timeout: timeouts.deployTimeout } @@ -113,12 +122,14 @@ async function main() { const fpcClaim = await feeJuicePortalManager.bridgeTokensPublic(fpc.address, FEE_FUNDING_FOR_TESTER_ACCOUNT, true); // 2 public txs to make the bridged fee juice available // Mint some bananaCoin and send to the newWallet to pay fees privately + await bananaCoin.methods.mint_to_private(account2.address, FEE_FUNDING_FOR_TESTER_ACCOUNT).simulate({ from: account1.address }); await bananaCoin.methods.mint_to_private(account2.address, FEE_FUNDING_FOR_TESTER_ACCOUNT).send({ from: account1.address, fee: { paymentMethod }, wait: { timeout: timeouts.txTimeout } }); // mint some public bananaCoin to the newWallet to pay fees publicly + await bananaCoin.methods.mint_to_public(account2.address, FEE_FUNDING_FOR_TESTER_ACCOUNT).simulate({ from: account1.address }); await bananaCoin.methods.mint_to_public(account2.address, FEE_FUNDING_FOR_TESTER_ACCOUNT).send({ from: account1.address, fee: { paymentMethod }, @@ -144,6 +155,7 @@ async function main() { const gasSettings = GasSettings.default({ maxFeesPerGas }); const privateFee = new PrivateFeePaymentMethod(fpc.address, account2.address, wallet, gasSettings); + await bananaCoin.methods.transfer_in_private(account2.address, account1.address, 10, 0).simulate({ from: account2.address }); await bananaCoin.methods.transfer_in_private(account2.address, account1.address, 10, 0).send({ from: account2.address, fee: { paymentMethod: privateFee }, @@ -155,6 +167,7 @@ async function main() { // Public Fee Payments via FPC const publicFee = new PublicFeePaymentMethod(fpc.address, account2.address, wallet, gasSettings); + await bananaCoin.methods.transfer_in_private(account2.address, account1.address, 10, 0).simulate({ from: account2.address }); await bananaCoin.methods.transfer_in_private(account2.address, account1.address, 10, 0).send({ from: account2.address, fee: { paymentMethod: publicFee }, @@ -166,6 +179,7 @@ async function main() { // This method will only work in environments where there is a sponsored fee contract deployed const sponsoredPaymentMethod = new SponsoredFeePaymentMethod(sponsoredFPC.address); + await bananaCoin.methods.transfer_in_private(account2.address, account1.address, 10, 0).simulate({ from: account2.address }); await bananaCoin.methods.transfer_in_private(account2.address, account1.address, 10, 0).send({ from: account2.address, fee: { paymentMethod: sponsoredPaymentMethod }, diff --git a/scripts/interaction_existing_contract.ts b/scripts/interaction_existing_contract.ts index dc493b8..874889b 100644 --- a/scripts/interaction_existing_contract.ts +++ b/scripts/interaction_existing_contract.ts @@ -88,6 +88,12 @@ async function main() { const gameId = Fr.random(); logger.info(`Creating new game with ID: ${gameId}`); + // Simulate first to surface revert reasons before sending + await podRacingContract.methods.create_game(gameId).simulate({ + from: address, + }); + logger.info("Simulation successful, sending transaction..."); + await podRacingContract.methods.create_game(gameId) .send({ from: address, diff --git a/scripts/multiple_wallet.ts b/scripts/multiple_wallet.ts index dc59a2b..6d5f92d 100644 --- a/scripts/multiple_wallet.ts +++ b/scripts/multiple_wallet.ts @@ -54,7 +54,11 @@ async function main() { const deployMethod = await schnorrAccount.getDeployMethod(); await deployMethod.send({ from: AztecAddress.ZERO, fee: { paymentMethod }, wait: { timeout: timeouts.deployTimeout } }); let ownerAddress = schnorrAccount.address; - const token = await TokenContract.deploy(wallet1, ownerAddress, 'Clean USDC', 'USDC', 6).send({ + + // Simulate before sending to surface revert reasons + const tokenDeploy = TokenContract.deploy(wallet1, ownerAddress, 'Clean USDC', 'USDC', 6); + await tokenDeploy.simulate({ from: ownerAddress }); + const token = await tokenDeploy.send({ from: ownerAddress, contractAddressSalt: L2_TOKEN_CONTRACT_SALT, fee: { paymentMethod }, @@ -78,12 +82,16 @@ async function main() { // mint to account on 2nd pxe + // Simulate before sending to surface revert reasons + await token.methods.mint_to_private(schnorrAccount2.address, 100).simulate({ from: ownerAddress }); const private_mint_tx = await token.methods.mint_to_private(schnorrAccount2.address, 100).send({ from: ownerAddress, fee: { paymentMethod }, wait: { timeout: timeouts.txTimeout } }); console.log(await node.getTxEffect(private_mint_tx.txHash)) + + await token.methods.mint_to_public(schnorrAccount2.address, 100).simulate({ from: ownerAddress }); await token.methods.mint_to_public(schnorrAccount2.address, 100).send({ from: ownerAddress, fee: { paymentMethod }, diff --git a/src/test/e2e/index.test.ts b/src/test/e2e/index.test.ts index 924fd3d..706fec9 100644 --- a/src/test/e2e/index.test.ts +++ b/src/test/e2e/index.test.ts @@ -48,6 +48,12 @@ async function playRound( sponsoredPaymentMethod: SponsoredFeePaymentMethod, timeout: number ) { + // Simulate first to surface revert reasons before sending + await contract.methods.play_round( + gameId, round, + strategy.track1, strategy.track2, strategy.track3, strategy.track4, strategy.track5 + ).simulate({ from: playerAccount }); + return await contract.methods.play_round( gameId, round, @@ -72,12 +78,15 @@ async function setupGame( sponsoredPaymentMethod: SponsoredFeePaymentMethod, timeout: number ) { + // Simulate first to surface revert reasons before sending + await contract.methods.create_game(gameId).simulate({ from: player1Address }); await contract.methods.create_game(gameId).send({ from: player1Address, fee: { paymentMethod: sponsoredPaymentMethod }, wait: { timeout } }); + await contract.methods.join_game(gameId).simulate({ from: player2Address }); await contract.methods.join_game(gameId).send({ from: player2Address, fee: { paymentMethod: sponsoredPaymentMethod }, @@ -304,12 +313,14 @@ describe("Pod Racing Game", () => { logger.info('Player 2 completed all rounds'); // Both players reveal their scores + await contract.methods.finish_game(gameId).simulate({ from: player1Account.address }); await contract.methods.finish_game(gameId).send({ from: player1Account.address, fee: { paymentMethod: sponsoredPaymentMethod }, wait: { timeout: getTimeouts().txTimeout } }); + await contract.methods.finish_game(gameId).simulate({ from: player2Account.address }); await contract.methods.finish_game(gameId).send({ from: player2Account.address, fee: { paymentMethod: sponsoredPaymentMethod }, diff --git a/src/utils/deploy_account.ts b/src/utils/deploy_account.ts index 5fa6f58..e87b2d6 100644 --- a/src/utils/deploy_account.ts +++ b/src/utils/deploy_account.ts @@ -40,6 +40,12 @@ export async function deploySchnorrAccount(wallet?: EmbeddedWallet): Promise