Skip to content

Commit e108230

Browse files
authored
Merge pull request #245 from AztecProtocol/feat/simulate-before-send
Add simulate-before-send pattern to all scripts, tests, and docs
2 parents f43b686 + b36804b commit e108230

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)