Skip to content

Commit b36804b

Browse files
critesjoshclaude
andcommitted
Add simulate-before-send pattern to all scripts, tests, and docs
Transactions sent without prior simulation hang for the full timeout (up to 600s) on failure with opaque errors. Adding .simulate() before .send() surfaces revert reasons immediately, saving significant debugging time — especially for AI agents using this repo as reference. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f43b686 commit b36804b

10 files changed

Lines changed: 137 additions & 7 deletions

File tree

AGENTS.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,29 @@ There are two independent test systems:
2424
- **TypeScript E2E tests** (`yarn test:js`): Require a running local network.
2525
- `yarn test` runs both. Ensure tests pass before committing.
2626

27+
## Simulate Before Send
28+
29+
**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.
30+
31+
```typescript
32+
// Simulate first
33+
await contract.methods.create_game(gameId).simulate({ from: address });
34+
// Then send
35+
await contract.methods.create_game(gameId).send({
36+
from: address,
37+
fee: { paymentMethod },
38+
wait: { timeout }
39+
});
40+
```
41+
42+
For deployments, store the deploy request to reuse it:
43+
44+
```typescript
45+
const deployRequest = MyContract.deploy(wallet, ...args);
46+
await deployRequest.simulate({ from: address });
47+
const contract = await deployRequest.send({ ... });
48+
```
49+
2750
## Pull Requests
2851
- Use clear commit messages and provide a concise description in the PR body about the change.
2952
- Mention which tests were executed.

CLAUDE.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,36 @@ yarn profile # Profile a transaction deployment
105105
- **Wallet setup**: `EmbeddedWallet.create()` with `ephemeral: true` for tests; prover is enabled only on devnet.
106106
- **PXE store**: Data persists in `./store`. Must delete after local network restart to avoid stale state errors.
107107

108+
## Simulate Before Send (IMPORTANT)
109+
110+
**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.
111+
112+
```typescript
113+
// Simulate first — surfaces revert reasons instantly
114+
await contract.methods.create_game(gameId).simulate({ from: address });
115+
116+
// Then send — only after simulation succeeds
117+
await contract.methods.create_game(gameId).send({
118+
from: address,
119+
fee: { paymentMethod },
120+
wait: { timeout: timeouts.txTimeout }
121+
});
122+
```
123+
124+
For deployments, store the deploy request to avoid constructing it twice:
125+
126+
```typescript
127+
const deployRequest = MyContract.deploy(wallet, ...args);
128+
await deployRequest.simulate({ from: address });
129+
const contract = await deployRequest.send({ ... });
130+
```
131+
132+
**Checklist:**
133+
134+
- Every `.send()` call must be preceded by a `.simulate()` call
135+
- `.simulate()` does not need fee parameters — only `from` is required
136+
- View/read-only calls (e.g. `balance_of_private`) already use `.simulate()` to return values — no `.send()` needed for those
137+
108138
## Version Update Procedure
109139

110140
When updating the Aztec version, update all of these locations:

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,30 @@ The `./src/utils/` folder contains utility functions:
185185
- `./src/utils/sponsored_fpc.ts` provides functions to deploy and manage the SponsoredFPC (Fee Payment Contract) for handling sponsored transaction fees.
186186
- `./config/config.ts` provides environment-aware configuration loading, automatically selecting the correct JSON config file based on the `ENV` variable.
187187

188+
## Simulate Before Send
189+
190+
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.
191+
192+
```typescript
193+
// Simulate first — surfaces revert reasons instantly
194+
await contract.methods.create_game(gameId).simulate({ from: address });
195+
196+
// Then send — only after simulation succeeds
197+
await contract.methods.create_game(gameId).send({
198+
from: address,
199+
fee: { paymentMethod },
200+
wait: { timeout }
201+
});
202+
```
203+
204+
For deployments, store the deploy request to avoid constructing it twice:
205+
206+
```typescript
207+
const deployRequest = MyContract.deploy(wallet, ...args);
208+
await deployRequest.simulate({ from: address });
209+
const contract = await deployRequest.send({ ... });
210+
```
211+
188212
## **Error Resolution**
189213

190214
: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.

scripts/deploy_contract.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,13 @@ async function main() {
4040
logger.info('🏎️ Starting pod racing contract deployment...');
4141
logger.info(`📋 Admin address for pod racing contract: ${address}`);
4242

43-
logger.info('⏳ Waiting for deployment transaction to be mined...');
44-
const { contract: podRacingContract, instance } = await PodRacingContract.deploy(wallet, address).send({
43+
logger.info('⏳ Simulating deployment transaction...');
44+
const deployRequest = PodRacingContract.deploy(wallet, address);
45+
await deployRequest.simulate({
46+
from: address,
47+
});
48+
logger.info('✅ Simulation successful, sending transaction...');
49+
const { contract: podRacingContract, instance } = await deployRequest.send({
4550
from: address,
4651
fee: { paymentMethod: sponsoredPaymentMethod },
4752
wait: { timeout: timeouts.deployTimeout, returnReceipt: true }

scripts/fees.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,18 @@ async function main() {
7171
const timeouts = getTimeouts();
7272

7373
// Two arbitrary txs to make the L1 message available on L2
74-
const podRacingContract = await PodRacingContract.deploy(wallet, account1.address).send({
74+
// Simulate before sending to surface revert reasons
75+
const podRacingDeploy = PodRacingContract.deploy(wallet, account1.address);
76+
await podRacingDeploy.simulate({ from: account1.address });
77+
const podRacingContract = await podRacingDeploy.send({
7578
from: account1.address,
7679
fee: { paymentMethod },
7780
wait: { timeout: timeouts.deployTimeout }
7881
});
79-
const bananaCoin = await TokenContract.deploy(wallet, account1.address, "bananaCoin", "BNC", 18).send({
82+
83+
const bananaCoinDeploy = TokenContract.deploy(wallet, account1.address, "bananaCoin", "BNC", 18);
84+
await bananaCoinDeploy.simulate({ from: account1.address });
85+
const bananaCoin = await bananaCoinDeploy.send({
8086
from: account1.address,
8187
fee: { paymentMethod },
8288
wait: { timeout: timeouts.deployTimeout }
@@ -93,6 +99,7 @@ async function main() {
9399

94100
// Create a new game on the pod racing contract, interacting from the newWallet
95101
const gameId = Fr.random();
102+
await podRacingContract.methods.create_game(gameId).simulate({ from: account2.address });
96103
await podRacingContract.methods.create_game(gameId).send({
97104
from: account2.address,
98105
wait: { timeout: timeouts.txTimeout }
@@ -105,20 +112,24 @@ async function main() {
105112
// Need to deploy an FPC to use Private Fee payment methods
106113

107114
// This uses bananaCoin as the fee paying asset that will be exchanged for fee juice
108-
const fpc = await FPCContract.deploy(wallet, bananaCoin.address, account1.address).send({
115+
const fpcDeploy = FPCContract.deploy(wallet, bananaCoin.address, account1.address);
116+
await fpcDeploy.simulate({ from: account1.address });
117+
const fpc = await fpcDeploy.send({
109118
from: account1.address,
110119
fee: { paymentMethod },
111120
wait: { timeout: timeouts.deployTimeout }
112121
});
113122
const fpcClaim = await feeJuicePortalManager.bridgeTokensPublic(fpc.address, FEE_FUNDING_FOR_TESTER_ACCOUNT, true);
114123
// 2 public txs to make the bridged fee juice available
115124
// Mint some bananaCoin and send to the newWallet to pay fees privately
125+
await bananaCoin.methods.mint_to_private(account2.address, FEE_FUNDING_FOR_TESTER_ACCOUNT).simulate({ from: account1.address });
116126
await bananaCoin.methods.mint_to_private(account2.address, FEE_FUNDING_FOR_TESTER_ACCOUNT).send({
117127
from: account1.address,
118128
fee: { paymentMethod },
119129
wait: { timeout: timeouts.txTimeout }
120130
});
121131
// mint some public bananaCoin to the newWallet to pay fees publicly
132+
await bananaCoin.methods.mint_to_public(account2.address, FEE_FUNDING_FOR_TESTER_ACCOUNT).simulate({ from: account1.address });
122133
await bananaCoin.methods.mint_to_public(account2.address, FEE_FUNDING_FOR_TESTER_ACCOUNT).send({
123134
from: account1.address,
124135
fee: { paymentMethod },
@@ -144,6 +155,7 @@ async function main() {
144155
const gasSettings = GasSettings.default({ maxFeesPerGas });
145156

146157
const privateFee = new PrivateFeePaymentMethod(fpc.address, account2.address, wallet, gasSettings);
158+
await bananaCoin.methods.transfer_in_private(account2.address, account1.address, 10, 0).simulate({ from: account2.address });
147159
await bananaCoin.methods.transfer_in_private(account2.address, account1.address, 10, 0).send({
148160
from: account2.address,
149161
fee: { paymentMethod: privateFee },
@@ -155,6 +167,7 @@ async function main() {
155167
// Public Fee Payments via FPC
156168

157169
const publicFee = new PublicFeePaymentMethod(fpc.address, account2.address, wallet, gasSettings);
170+
await bananaCoin.methods.transfer_in_private(account2.address, account1.address, 10, 0).simulate({ from: account2.address });
158171
await bananaCoin.methods.transfer_in_private(account2.address, account1.address, 10, 0).send({
159172
from: account2.address,
160173
fee: { paymentMethod: publicFee },
@@ -166,6 +179,7 @@ async function main() {
166179

167180
// This method will only work in environments where there is a sponsored fee contract deployed
168181
const sponsoredPaymentMethod = new SponsoredFeePaymentMethod(sponsoredFPC.address);
182+
await bananaCoin.methods.transfer_in_private(account2.address, account1.address, 10, 0).simulate({ from: account2.address });
169183
await bananaCoin.methods.transfer_in_private(account2.address, account1.address, 10, 0).send({
170184
from: account2.address,
171185
fee: { paymentMethod: sponsoredPaymentMethod },

scripts/interaction_existing_contract.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ async function main() {
8888
const gameId = Fr.random();
8989
logger.info(`Creating new game with ID: ${gameId}`);
9090

91+
// Simulate first to surface revert reasons before sending
92+
await podRacingContract.methods.create_game(gameId).simulate({
93+
from: address,
94+
});
95+
logger.info("Simulation successful, sending transaction...");
96+
9197
await podRacingContract.methods.create_game(gameId)
9298
.send({
9399
from: address,

scripts/multiple_wallet.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ async function main() {
5454
const deployMethod = await schnorrAccount.getDeployMethod();
5555
await deployMethod.send({ from: AztecAddress.ZERO, fee: { paymentMethod }, wait: { timeout: timeouts.deployTimeout } });
5656
let ownerAddress = schnorrAccount.address;
57-
const token = await TokenContract.deploy(wallet1, ownerAddress, 'Clean USDC', 'USDC', 6).send({
57+
58+
// Simulate before sending to surface revert reasons
59+
const tokenDeploy = TokenContract.deploy(wallet1, ownerAddress, 'Clean USDC', 'USDC', 6);
60+
await tokenDeploy.simulate({ from: ownerAddress });
61+
const token = await tokenDeploy.send({
5862
from: ownerAddress,
5963
contractAddressSalt: L2_TOKEN_CONTRACT_SALT,
6064
fee: { paymentMethod },
@@ -78,12 +82,16 @@ async function main() {
7882

7983
// mint to account on 2nd pxe
8084

85+
// Simulate before sending to surface revert reasons
86+
await token.methods.mint_to_private(schnorrAccount2.address, 100).simulate({ from: ownerAddress });
8187
const private_mint_tx = await token.methods.mint_to_private(schnorrAccount2.address, 100).send({
8288
from: ownerAddress,
8389
fee: { paymentMethod },
8490
wait: { timeout: timeouts.txTimeout }
8591
});
8692
console.log(await node.getTxEffect(private_mint_tx.txHash))
93+
94+
await token.methods.mint_to_public(schnorrAccount2.address, 100).simulate({ from: ownerAddress });
8795
await token.methods.mint_to_public(schnorrAccount2.address, 100).send({
8896
from: ownerAddress,
8997
fee: { paymentMethod },

src/test/e2e/index.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ async function playRound(
4848
sponsoredPaymentMethod: SponsoredFeePaymentMethod,
4949
timeout: number
5050
) {
51+
// Simulate first to surface revert reasons before sending
52+
await contract.methods.play_round(
53+
gameId, round,
54+
strategy.track1, strategy.track2, strategy.track3, strategy.track4, strategy.track5
55+
).simulate({ from: playerAccount });
56+
5157
return await contract.methods.play_round(
5258
gameId,
5359
round,
@@ -72,12 +78,15 @@ async function setupGame(
7278
sponsoredPaymentMethod: SponsoredFeePaymentMethod,
7379
timeout: number
7480
) {
81+
// Simulate first to surface revert reasons before sending
82+
await contract.methods.create_game(gameId).simulate({ from: player1Address });
7583
await contract.methods.create_game(gameId).send({
7684
from: player1Address,
7785
fee: { paymentMethod: sponsoredPaymentMethod },
7886
wait: { timeout }
7987
});
8088

89+
await contract.methods.join_game(gameId).simulate({ from: player2Address });
8190
await contract.methods.join_game(gameId).send({
8291
from: player2Address,
8392
fee: { paymentMethod: sponsoredPaymentMethod },
@@ -304,12 +313,14 @@ describe("Pod Racing Game", () => {
304313
logger.info('Player 2 completed all rounds');
305314

306315
// Both players reveal their scores
316+
await contract.methods.finish_game(gameId).simulate({ from: player1Account.address });
307317
await contract.methods.finish_game(gameId).send({
308318
from: player1Account.address,
309319
fee: { paymentMethod: sponsoredPaymentMethod },
310320
wait: { timeout: getTimeouts().txTimeout }
311321
});
312322

323+
await contract.methods.finish_game(gameId).simulate({ from: player2Account.address });
313324
await contract.methods.finish_game(gameId).send({
314325
from: player2Account.address,
315326
fee: { paymentMethod: sponsoredPaymentMethod },

src/utils/deploy_account.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export async function deploySchnorrAccount(wallet?: EmbeddedWallet): Promise<Acc
4040
const sponsoredPaymentMethod = new SponsoredFeePaymentMethod(sponsoredFPC.address);
4141
logger.info('✅ Sponsored fee payment method configured for account deployment');
4242

43+
// Simulate before sending to surface revert reasons
44+
await deployMethod.simulate({
45+
from: AztecAddress.ZERO,
46+
});
47+
logger.info('✅ Simulation successful, sending deployment transaction...');
48+
4349
// Deploy account
4450
await deployMethod.send({
4551
from: AztecAddress.ZERO,

src/utils/sponsored_fpc.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ export async function getSponsoredFPCAddress() {
2020

2121
export async function setupSponsoredFPC(deployer: Wallet, log: LogFn) {
2222
const [{ item: from }] = await deployer.getAccounts();
23-
const deployed = await SponsoredFPCContract.deploy(deployer)
23+
const deployRequest = SponsoredFPCContract.deploy(deployer);
24+
// Simulate before sending to surface revert reasons
25+
await deployRequest.simulate({ from });
26+
const deployed = await deployRequest
2427
.send({
2528
from,
2629
contractAddressSalt: new Fr(SPONSORED_FPC_SALT),

0 commit comments

Comments
 (0)