@aztec/bot 0.0.1-commit.b33fc05d0 → 0.0.1-commit.b3d3157a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/bot.ts CHANGED
@@ -70,10 +70,7 @@ export class Bot extends BaseBot {
70
70
  );
71
71
 
72
72
  const batch = new BatchCall(wallet, calls);
73
- const opts = await this.getSendMethodOpts(batch);
74
-
75
- this.log.verbose(`Simulating transaction with ${calls.length}`, logCtx);
76
- await batch.simulate({ from: this.defaultAccountAddress });
73
+ const opts = this.getSendMethodOpts();
77
74
 
78
75
  this.log.verbose(`Sending transaction`, logCtx);
79
76
  const { txHash } = await batch.send({ ...opts, wait: NO_WAIT });
package/src/config.ts CHANGED
@@ -69,9 +69,9 @@ export type BotConfig = {
69
69
  maxPendingTxs: number;
70
70
  /** Whether to flush after sending each 'setup' transaction */
71
71
  flushSetupTransactions: boolean;
72
- /** L2 gas limit for the tx (empty to have the bot trigger an estimate gas). */
72
+ /** L2 gas limit for the tx (empty to let the bot's wallet estimate). */
73
73
  l2GasLimit: number | undefined;
74
- /** DA gas limit for the tx (empty to have the bot trigger an estimate gas). */
74
+ /** DA gas limit for the tx (empty to let the bot's wallet estimate). */
75
75
  daGasLimit: number | undefined;
76
76
  /** Token contract to use */
77
77
  contract: SupportedTokenContracts;
@@ -243,12 +243,12 @@ export const botConfigMappings: ConfigMappingsType<BotConfig> = {
243
243
  },
244
244
  l2GasLimit: {
245
245
  env: 'BOT_L2_GAS_LIMIT',
246
- description: 'L2 gas limit for the tx (empty to have the bot trigger an estimate gas).',
246
+ description: "L2 gas limit for the tx (empty to let the bot's wallet estimate).",
247
247
  ...optionalNumberConfigHelper(),
248
248
  },
249
249
  daGasLimit: {
250
250
  env: 'BOT_DA_GAS_LIMIT',
251
- description: 'DA gas limit for the tx (empty to have the bot trigger an estimate gas).',
251
+ description: "DA gas limit for the tx (empty to let the bot's wallet estimate).",
252
252
  ...optionalNumberConfigHelper(),
253
253
  },
254
254
  contract: {
@@ -26,7 +26,7 @@
26
26
  import { AztecAddress } from '@aztec/aztec.js/addresses';
27
27
  import { BatchCall, NO_WAIT } from '@aztec/aztec.js/contracts';
28
28
  import { isL1ToL2MessageReady } from '@aztec/aztec.js/messaging';
29
- import { TxHash, TxReceipt } from '@aztec/aztec.js/tx';
29
+ import type { TxHash, TxReceipt } from '@aztec/aztec.js/tx';
30
30
  import type { ExtendedViemWalletClient } from '@aztec/ethereum/types';
31
31
  import { Fr } from '@aztec/foundation/curves/bn254';
32
32
  import { EthAddress } from '@aztec/foundation/eth-address';
@@ -137,7 +137,7 @@ export class CrossChainBot extends BaseBot {
137
137
  }
138
138
 
139
139
  const batch = new BatchCall(this.wallet, calls);
140
- const opts = await this.getSendMethodOpts(batch);
140
+ const opts = this.getSendMethodOpts();
141
141
 
142
142
  this.log.verbose(`Sending cross-chain batch with ${calls.length} calls`, logCtx);
143
143
  const { txHash } = await batch.send({ ...opts, wait: NO_WAIT });
@@ -146,9 +146,10 @@ export class CrossChainBot extends BaseBot {
146
146
 
147
147
  protected override async onTxMined(receipt: TxReceipt, logCtx: object): Promise<void> {
148
148
  // Verify L2→L1 messages appeared in this tx's effects
149
- const indexed = await this.node.getTxEffect(receipt.txHash);
150
- if (indexed) {
151
- const l2ToL1Msgs = indexed.data.l2ToL1Msgs.filter(m => !m.isZero());
149
+ const minedReceipt = await this.node.getTxReceipt(receipt.txHash, { includeTxEffect: true });
150
+ const l2ToL1MsgsRaw = minedReceipt.txEffect?.l2ToL1Msgs;
151
+ if (l2ToL1MsgsRaw) {
152
+ const l2ToL1Msgs = l2ToL1MsgsRaw.filter(m => !m.isZero());
152
153
  if (l2ToL1Msgs.length >= this.config.l2ToL1MessagesPerTx) {
153
154
  this.l2ToL1Sent += l2ToL1Msgs.length;
154
155
  } else {
package/src/factory.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getInitialTestAccountsData } from '@aztec/accounts/testing';
2
+ import { NO_FROM } from '@aztec/aztec.js/account';
2
3
  import { AztecAddress } from '@aztec/aztec.js/addresses';
3
4
  import {
4
5
  BatchCall,
@@ -15,6 +16,8 @@ import { deriveKeys } from '@aztec/aztec.js/keys';
15
16
  import { createLogger } from '@aztec/aztec.js/log';
16
17
  import { waitForL1ToL2MessageReady } from '@aztec/aztec.js/messaging';
17
18
  import { waitForTx } from '@aztec/aztec.js/node';
19
+ import { getFeeJuiceBalance } from '@aztec/aztec.js/utils';
20
+ import { ContractInitializationStatus } from '@aztec/aztec.js/wallet';
18
21
  import { createEthereumChain } from '@aztec/ethereum/chain';
19
22
  import { createExtendedL1Client } from '@aztec/ethereum/client';
20
23
  import { RollupContract } from '@aztec/ethereum/contracts';
@@ -27,7 +30,7 @@ import { PrivateTokenContract } from '@aztec/noir-contracts.js/PrivateToken';
27
30
  import { TokenContract } from '@aztec/noir-contracts.js/Token';
28
31
  import { TestContract } from '@aztec/noir-test-contracts.js/Test';
29
32
  import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract';
30
- import { GasSettings } from '@aztec/stdlib/gas';
33
+ import { GasFees, GasSettings, ManaUsageEstimate } from '@aztec/stdlib/gas';
31
34
  import type { AztecNode, AztecNodeAdmin } from '@aztec/stdlib/interfaces/client';
32
35
  import { deriveSigningKey } from '@aztec/stdlib/keys';
33
36
  import { EmbeddedWallet } from '@aztec/wallets/embedded';
@@ -39,6 +42,8 @@ import { getBalances, getPrivateBalance, isStandardTokenContract } from './utils
39
42
 
40
43
  const MINT_BALANCE = 1e12;
41
44
  const MIN_BALANCE = 1e3;
45
+ const FEE_JUICE_TOP_UP_THRESHOLD = 100n * 10n ** 18n;
46
+ const FEE_JUICE_TOP_UP_TARGET = 10_000n * 10n ** 18n;
42
47
 
43
48
  export class BotFactory {
44
49
  private log = createLogger('bot');
@@ -68,7 +73,8 @@ export class BotFactory {
68
73
  }> {
69
74
  const defaultAccountAddress = await this.setupAccount();
70
75
  const recipient = (await this.wallet.createSchnorrAccount(Fr.random(), Fr.random())).address;
71
- const token = await this.setupToken(defaultAccountAddress);
76
+ const token = await this.setupTokenWithOptionalEarlyRefuel(defaultAccountAddress);
77
+ await this.ensureFeeJuiceBalance(defaultAccountAddress, token);
72
78
  await this.mintTokens(token, defaultAccountAddress);
73
79
  return { wallet: this.wallet, defaultAccountAddress, token, node: this.aztecNode, recipient };
74
80
  }
@@ -82,7 +88,13 @@ export class BotFactory {
82
88
  node: AztecNode;
83
89
  }> {
84
90
  const defaultAccountAddress = await this.setupAccount();
85
- const token0 = await this.setupTokenContract(defaultAccountAddress, this.config.tokenSalt, 'BotToken0', 'BOT0');
91
+ const token0 = await this.setupTokenContractWithOptionalEarlyRefuel(
92
+ defaultAccountAddress,
93
+ this.config.tokenSalt,
94
+ 'BotToken0',
95
+ 'BOT0',
96
+ );
97
+ await this.ensureFeeJuiceBalance(defaultAccountAddress, token0);
86
98
  const token1 = await this.setupTokenContract(defaultAccountAddress, this.config.tokenSalt, 'BotToken1', 'BOT1');
87
99
  const liquidityToken = await this.setupTokenContract(
88
100
  defaultAccountAddress,
@@ -177,12 +189,8 @@ export class BotFactory {
177
189
  }
178
190
 
179
191
  private async setupTestContract(deployer: AztecAddress): Promise<TestContract> {
180
- const deployOpts: DeployOptions = {
181
- from: deployer,
182
- contractAddressSalt: this.config.tokenSalt,
183
- universalDeploy: true,
184
- };
185
- const deploy = TestContract.deploy(this.wallet);
192
+ const deployOpts: DeployOptions = { from: deployer };
193
+ const deploy = TestContract.deploy(this.wallet, { salt: this.config.tokenSalt, universalDeploy: true });
186
194
  const instance = await this.registerOrDeployContract('TestContract', deploy, deployOpts);
187
195
  return TestContract.at(instance.address, this.wallet);
188
196
  }
@@ -207,7 +215,7 @@ export class BotFactory {
207
215
  const signingKey = deriveSigningKey(secret);
208
216
  const accountManager = await this.wallet.createSchnorrAccount(secret, salt, signingKey);
209
217
  const metadata = await this.wallet.getContractMetadata(accountManager.address);
210
- if (metadata.isContractInitialized) {
218
+ if (metadata.initializationStatus === ContractInitializationStatus.INITIALIZED) {
211
219
  this.log.info(`Account at ${accountManager.address.toString()} already initialized`);
212
220
  const timer = new Timer();
213
221
  const address = accountManager.address;
@@ -222,13 +230,11 @@ export class BotFactory {
222
230
 
223
231
  const paymentMethod = new FeeJuicePaymentMethodWithClaim(accountManager.address, claim);
224
232
  const deployMethod = await accountManager.getDeployMethod();
225
- const maxFeesPerGas = (await this.aztecNode.getCurrentMinFees()).mul(1 + this.config.minFeePadding);
226
- const gasSettings = GasSettings.default({ maxFeesPerGas });
227
233
 
228
234
  await this.withNoMinTxsPerBlock(async () => {
229
235
  const { txHash } = await deployMethod.send({
230
- from: AztecAddress.ZERO,
231
- fee: { gasSettings, paymentMethod },
236
+ from: NO_FROM,
237
+ fee: { paymentMethod },
232
238
  wait: NO_WAIT,
233
239
  });
234
240
  this.log.info(`Sent tx for account deployment with hash ${txHash.toString()}`);
@@ -253,35 +259,95 @@ export class BotFactory {
253
259
  return accountManager.address;
254
260
  }
255
261
 
262
+ /**
263
+ * Setup token and refuel first: if the token already exists (restart scenario),
264
+ * run ensureFeeJuiceBalance before any step that might need fee juice. When deploying,
265
+ * use a bridge claim if balance is below threshold.
266
+ */
267
+ private async setupTokenWithOptionalEarlyRefuel(sender: AztecAddress): Promise<TokenContract | PrivateTokenContract> {
268
+ const token = await this.getTokenInstance(sender);
269
+ const address = token.address;
270
+ const metadata = await this.wallet.getContractMetadata(address);
271
+ if (metadata.isContractPublished) {
272
+ this.log.info(`Token at ${address.toString()} already deployed, refueling before setup`);
273
+ await this.ensureFeeJuiceBalance(sender, token);
274
+ }
275
+ return this.setupToken(sender);
276
+ }
277
+
278
+ /**
279
+ * Setup token0 for AMM with refuel-first behaviour when token already exists.
280
+ */
281
+ private async setupTokenContractWithOptionalEarlyRefuel(
282
+ deployer: AztecAddress,
283
+ salt: Fr,
284
+ name: string,
285
+ ticker: string,
286
+ decimals = 18,
287
+ ): Promise<TokenContract> {
288
+ const deploy = TokenContract.deploy(this.wallet, deployer, name, ticker, decimals, { salt, universalDeploy: true });
289
+ const instance = await deploy.getInstance();
290
+ const metadata = await this.wallet.getContractMetadata(instance.address);
291
+ if (metadata.isContractPublished) {
292
+ this.log.info(`Token ${name} at ${instance.address.toString()} already deployed, refueling before setup`);
293
+ const token = TokenContract.at(instance.address, this.wallet);
294
+ await this.ensureFeeJuiceBalance(deployer, token);
295
+ }
296
+ return this.setupTokenContract(deployer, salt, name, ticker, decimals);
297
+ }
298
+
299
+ private async getTokenInstance(sender: AztecAddress): Promise<TokenContract | PrivateTokenContract> {
300
+ const salt = this.config.tokenSalt;
301
+ if (this.config.contract === SupportedTokenContracts.TokenContract) {
302
+ const deploy = TokenContract.deploy(this.wallet, sender, 'BotToken', 'BOT', 18, { salt, universalDeploy: true });
303
+ const instance = await deploy.getInstance();
304
+ return TokenContract.at(instance.address, this.wallet);
305
+ }
306
+ if (this.config.contract === SupportedTokenContracts.PrivateTokenContract) {
307
+ const tokenSecretKey = Fr.random();
308
+ const tokenPublicKeys = (await deriveKeys(tokenSecretKey)).publicKeys;
309
+ const deploy = PrivateTokenContract.deploy(this.wallet, MINT_BALANCE, sender, {
310
+ salt,
311
+ universalDeploy: true,
312
+ publicKeys: tokenPublicKeys,
313
+ });
314
+ const instance = await deploy.getInstance();
315
+ return PrivateTokenContract.at(instance.address, this.wallet);
316
+ }
317
+ throw new Error(`Unsupported token contract type: ${this.config.contract}`);
318
+ }
319
+
256
320
  /**
257
321
  * Checks if the token contract is deployed and deploys it if necessary.
258
- * @param wallet - Wallet to deploy the token contract from.
259
- * @returns The TokenContract instance.
322
+ * Uses a bridge claim for deploy when balance is below threshold to avoid failing before refuel.
323
+ * @param sender - Aztec address to deploy the token contract from.
324
+ * @param existingToken - Optional token instance when called from setupTokenWithOptionalEarlyRefuel.
325
+ * @returns The TokenContract or PrivateTokenContract instance.
260
326
  */
261
327
  private async setupToken(sender: AztecAddress): Promise<TokenContract | PrivateTokenContract> {
262
328
  let deploy: DeployMethod<TokenContract | PrivateTokenContract>;
263
- let tokenInstance: ContractInstanceWithAddress | undefined;
264
- const deployOpts: DeployOptions = {
265
- from: sender,
266
- contractAddressSalt: this.config.tokenSalt,
267
- universalDeploy: true,
268
- };
329
+ const salt = this.config.tokenSalt;
330
+ const deployOpts: DeployOptions = { from: sender };
269
331
  let token: TokenContract | PrivateTokenContract;
270
332
  if (this.config.contract === SupportedTokenContracts.TokenContract) {
271
- deploy = TokenContract.deploy(this.wallet, sender, 'BotToken', 'BOT', 18);
272
- tokenInstance = await deploy.getInstance(deployOpts);
273
- token = TokenContract.at(tokenInstance.address, this.wallet);
333
+ deploy = TokenContract.deploy(this.wallet, sender, 'BotToken', 'BOT', 18, { salt, universalDeploy: true });
334
+ const instance = await deploy.getInstance();
335
+ token = TokenContract.at(instance.address, this.wallet);
274
336
  } else if (this.config.contract === SupportedTokenContracts.PrivateTokenContract) {
275
337
  // Generate keys for the contract since PrivateToken uses SinglePrivateMutable which requires keys
276
338
  const tokenSecretKey = Fr.random();
277
339
  const tokenPublicKeys = (await deriveKeys(tokenSecretKey)).publicKeys;
278
- deploy = PrivateTokenContract.deployWithPublicKeys(tokenPublicKeys, this.wallet, MINT_BALANCE, sender);
340
+ deploy = PrivateTokenContract.deploy(this.wallet, MINT_BALANCE, sender, {
341
+ salt,
342
+ universalDeploy: true,
343
+ publicKeys: tokenPublicKeys,
344
+ });
279
345
  deployOpts.skipInstancePublication = true;
280
346
  deployOpts.skipClassPublication = true;
281
347
  deployOpts.skipInitialization = false;
282
348
 
283
349
  // Register the contract with the secret key before deployment
284
- tokenInstance = await deploy.getInstance(deployOpts);
350
+ const tokenInstance = await deploy.getInstance();
285
351
  token = PrivateTokenContract.at(tokenInstance.address, this.wallet);
286
352
  await this.wallet.registerContract(tokenInstance, PrivateTokenContract.artifact, tokenSecretKey);
287
353
  // The contract constructor initializes private storage vars that need the contract's own nullifier key.
@@ -290,20 +356,7 @@ export class BotFactory {
290
356
  throw new Error(`Unsupported token contract type: ${this.config.contract}`);
291
357
  }
292
358
 
293
- const address = tokenInstance?.address ?? (await deploy.getInstance(deployOpts)).address;
294
- const metadata = await this.wallet.getContractMetadata(address);
295
- if (metadata.isContractPublished) {
296
- this.log.info(`Token at ${address.toString()} already deployed`);
297
- await deploy.register();
298
- } else {
299
- this.log.info(`Deploying token contract at ${address.toString()}`);
300
- const { txHash } = await deploy.send({ ...deployOpts, wait: NO_WAIT });
301
- this.log.info(`Sent tx for token setup with hash ${txHash.toString()}`);
302
- await this.withNoMinTxsPerBlock(async () => {
303
- await waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds });
304
- return token;
305
- });
306
- }
359
+ await this.registerOrDeployContract('token', deploy, deployOpts);
307
360
  return token;
308
361
  }
309
362
 
@@ -314,33 +367,38 @@ export class BotFactory {
314
367
  */
315
368
  private async setupTokenContract(
316
369
  deployer: AztecAddress,
317
- contractAddressSalt: Fr,
370
+ salt: Fr,
318
371
  name: string,
319
372
  ticker: string,
320
373
  decimals = 18,
321
374
  ): Promise<TokenContract> {
322
- const deployOpts: DeployOptions = { from: deployer, contractAddressSalt, universalDeploy: true };
323
- const deploy = TokenContract.deploy(this.wallet, deployer, name, ticker, decimals);
375
+ const deployOpts: DeployOptions = { from: deployer };
376
+ const deploy = TokenContract.deploy(this.wallet, deployer, name, ticker, decimals, { salt, universalDeploy: true });
324
377
  const instance = await this.registerOrDeployContract('Token - ' + name, deploy, deployOpts);
325
378
  return TokenContract.at(instance.address, this.wallet);
326
379
  }
327
380
 
328
381
  private async setupAmmContract(
329
382
  deployer: AztecAddress,
330
- contractAddressSalt: Fr,
383
+ salt: Fr,
331
384
  token0: TokenContract,
332
385
  token1: TokenContract,
333
386
  lpToken: TokenContract,
334
387
  ): Promise<AMMContract> {
335
- const deployOpts: DeployOptions = { from: deployer, contractAddressSalt, universalDeploy: true };
336
- const deploy = AMMContract.deploy(this.wallet, token0.address, token1.address, lpToken.address);
388
+ const deployOpts: DeployOptions = { from: deployer };
389
+ const deploy = AMMContract.deploy(this.wallet, token0.address, token1.address, lpToken.address, {
390
+ salt,
391
+ universalDeploy: true,
392
+ });
337
393
  const instance = await this.registerOrDeployContract('AMM', deploy, deployOpts);
338
394
  const amm = AMMContract.at(instance.address, this.wallet);
339
395
 
340
396
  this.log.info(`AMM deployed at ${amm.address}`);
341
- const { receipt: minterReceipt } = await lpToken.methods
342
- .set_minter(amm.address, true)
343
- .send({ from: deployer, wait: { timeout: this.config.txMinedWaitSeconds } });
397
+ const setMinterInteraction = lpToken.methods.set_minter(amm.address, true);
398
+ const { receipt: minterReceipt } = await setMinterInteraction.send({
399
+ from: deployer,
400
+ wait: { timeout: this.config.txMinedWaitSeconds },
401
+ });
344
402
  this.log.info(`Set LP token minter to AMM txHash=${minterReceipt.txHash.toString()}`);
345
403
  this.log.info(`Liquidity token initialized`);
346
404
 
@@ -409,20 +467,29 @@ export class BotFactory {
409
467
  .getFunctionCall(),
410
468
  });
411
469
 
412
- const { receipt: mintReceipt } = await new BatchCall(this.wallet, [
470
+ const mintBatch = new BatchCall(this.wallet, [
413
471
  token0.methods.mint_to_private(liquidityProvider, MINT_BALANCE),
414
472
  token1.methods.mint_to_private(liquidityProvider, MINT_BALANCE),
415
- ]).send({ from: liquidityProvider, wait: { timeout: this.config.txMinedWaitSeconds } });
473
+ ]);
474
+ const { receipt: mintReceipt } = await mintBatch.send({
475
+ from: liquidityProvider,
476
+ wait: { timeout: this.config.txMinedWaitSeconds },
477
+ });
416
478
 
417
479
  this.log.info(`Sent mint tx: ${mintReceipt.txHash.toString()}`);
418
480
 
419
- const { receipt: addLiquidityReceipt } = await amm.methods
420
- .add_liquidity(amount0Max, amount1Max, amount0Min, amount1Min, authwitNonce)
421
- .send({
422
- from: liquidityProvider,
423
- authWitnesses: [token0Authwit, token1Authwit],
424
- wait: { timeout: this.config.txMinedWaitSeconds },
425
- });
481
+ const addLiquidityInteraction = amm.methods.add_liquidity(
482
+ amount0Max,
483
+ amount1Max,
484
+ amount0Min,
485
+ amount1Min,
486
+ authwitNonce,
487
+ );
488
+ const { receipt: addLiquidityReceipt } = await addLiquidityInteraction.send({
489
+ from: liquidityProvider,
490
+ authWitnesses: [token0Authwit, token1Authwit],
491
+ wait: { timeout: this.config.txMinedWaitSeconds },
492
+ });
426
493
 
427
494
  this.log.info(`Sent tx to add liquidity to the AMM: ${addLiquidityReceipt.txHash.toString()}`);
428
495
  this.log.info(`Liquidity added`);
@@ -438,19 +505,49 @@ export class BotFactory {
438
505
  deploy: DeployMethod<T>,
439
506
  deployOpts: DeployOptions,
440
507
  ): Promise<ContractInstanceWithAddress> {
441
- const instance = await deploy.getInstance(deployOpts);
508
+ const instance = await deploy.getInstance();
442
509
  const address = instance.address;
443
510
  const metadata = await this.wallet.getContractMetadata(address);
444
511
  if (metadata.isContractPublished) {
445
512
  this.log.info(`Contract ${name} at ${address.toString()} already deployed`);
446
513
  await deploy.register();
447
514
  } else {
448
- this.log.info(`Deploying contract ${name} at ${address.toString()}`);
449
- await this.withNoMinTxsPerBlock(async () => {
450
- const { txHash } = await deploy.send({ ...deployOpts, wait: NO_WAIT });
451
- this.log.info(`Sent contract ${name} setup tx with hash ${txHash.toString()}`);
452
- return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds });
453
- });
515
+ const sender = deployOpts.from === NO_FROM ? undefined : deployOpts.from;
516
+ const balance = sender ? await getFeeJuiceBalance(sender, this.aztecNode) : 0n;
517
+ const useClaim =
518
+ sender &&
519
+ balance < FEE_JUICE_TOP_UP_THRESHOLD &&
520
+ this.config.feePaymentMethod === 'fee_juice' &&
521
+ !!this.config.l1RpcUrls?.length;
522
+ const mnemonicOrPrivateKey = this.config.l1PrivateKey?.getValue() ?? this.config.l1Mnemonic?.getValue();
523
+
524
+ if (useClaim && mnemonicOrPrivateKey) {
525
+ const claim = await this.getOrCreateBridgeClaim(sender!);
526
+ const paymentMethod = new FeeJuicePaymentMethodWithClaim(sender!, claim);
527
+ const { estimatedGas } = await deploy.simulate({ ...deployOpts, fee: { estimateGas: true, paymentMethod } });
528
+ const maxFeesPerGas = (await this.getMinFees()).mul(1 + this.config.minFeePadding);
529
+ const gasSettings = GasSettings.from({
530
+ ...estimatedGas!,
531
+ maxFeesPerGas,
532
+ maxPriorityFeesPerGas: GasFees.empty(),
533
+ });
534
+ await this.withNoMinTxsPerBlock(async () => {
535
+ const { txHash } = await deploy.send({ ...deployOpts, fee: { gasSettings, paymentMethod }, wait: NO_WAIT });
536
+ this.log.info(
537
+ `Sent contract ${name} deploy tx ${txHash.toString()} (using bridge claim, balance was ${balance})`,
538
+ );
539
+ return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds });
540
+ });
541
+ await this.store.deleteBridgeClaim(sender!);
542
+ } else {
543
+ const { estimatedGas } = await deploy.simulate({ ...deployOpts, fee: { estimateGas: true } });
544
+ this.log.info(`Deploying contract ${name} at ${address.toString()}`, { estimatedGas });
545
+ await this.withNoMinTxsPerBlock(async () => {
546
+ const { txHash } = await deploy.send({ ...deployOpts, fee: { gasSettings: estimatedGas }, wait: NO_WAIT });
547
+ this.log.info(`Sent contract ${name} setup tx with hash ${txHash.toString()}`);
548
+ return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds });
549
+ });
550
+ }
454
551
  }
455
552
  return instance;
456
553
  }
@@ -459,6 +556,66 @@ export class BotFactory {
459
556
  * Mints private and public tokens for the sender if their balance is below the minimum.
460
557
  * @param token - Token contract.
461
558
  */
559
+ /**
560
+ * Ensures the account has sufficient fee juice by bridging from L1 if balance is below threshold.
561
+ * Bridges repeatedly until balance reaches the target (10k FJ).
562
+ * Used on startup/restart to top up when the account has run out after previous runs.
563
+ */
564
+ private async ensureFeeJuiceBalance(
565
+ account: AztecAddress,
566
+ token: TokenContract | PrivateTokenContract,
567
+ ): Promise<void> {
568
+ const { feePaymentMethod, l1RpcUrls } = this.config;
569
+ if (feePaymentMethod !== 'fee_juice' || !l1RpcUrls?.length) {
570
+ return;
571
+ }
572
+ const mnemonicOrPrivateKey = this.config.l1PrivateKey?.getValue() ?? this.config.l1Mnemonic?.getValue();
573
+ if (!mnemonicOrPrivateKey) {
574
+ return;
575
+ }
576
+
577
+ let balance = await getFeeJuiceBalance(account, this.aztecNode);
578
+ if (balance >= FEE_JUICE_TOP_UP_THRESHOLD) {
579
+ this.log.info(`Fee juice balance ${balance} above threshold ${FEE_JUICE_TOP_UP_THRESHOLD}, skipping top-up`);
580
+ return;
581
+ }
582
+
583
+ this.log.info(
584
+ `Fee juice balance ${balance} below threshold ${FEE_JUICE_TOP_UP_THRESHOLD}, bridging from L1 until ${FEE_JUICE_TOP_UP_TARGET}`,
585
+ );
586
+ const maxFeesPerGas = (await this.getMinFees()).mul(1 + this.config.minFeePadding);
587
+ const minimalInteraction = isStandardTokenContract(token)
588
+ ? token.methods.transfer_in_public(account, account, 0n, 0)
589
+ : token.methods.transfer(0n, account, account);
590
+
591
+ while (balance < FEE_JUICE_TOP_UP_TARGET) {
592
+ const claim = await this.bridgeL1FeeJuice(account);
593
+ const paymentMethod = new FeeJuicePaymentMethodWithClaim(account, claim);
594
+ const { estimatedGas } = await minimalInteraction.simulate({
595
+ from: account,
596
+ fee: { estimateGas: true, paymentMethod },
597
+ });
598
+ const gasSettings = GasSettings.from({
599
+ ...estimatedGas!,
600
+ maxFeesPerGas,
601
+ maxPriorityFeesPerGas: GasFees.empty(),
602
+ });
603
+
604
+ await this.withNoMinTxsPerBlock(async () => {
605
+ const { txHash } = await minimalInteraction.send({
606
+ from: account,
607
+ fee: { gasSettings, paymentMethod },
608
+ wait: NO_WAIT,
609
+ });
610
+ this.log.info(`Sent fee juice top-up tx ${txHash.toString()}`);
611
+ return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds });
612
+ });
613
+ balance = await getFeeJuiceBalance(account, this.aztecNode);
614
+ this.log.info(`Fee juice balance after top-up: ${balance}`);
615
+ }
616
+ this.log.info(`Fee juice top-up complete for ${account.toString()}`);
617
+ }
618
+
462
619
  private async mintTokens(token: TokenContract | PrivateTokenContract, minter: AztecAddress) {
463
620
  const isStandardToken = isStandardTokenContract(token);
464
621
  let privateBalance = 0n;
@@ -491,8 +648,9 @@ export class BotFactory {
491
648
 
492
649
  // PrivateToken's mint accesses contract-level private storage vars (admin, total_supply).
493
650
  const additionalScopes = isStandardToken ? undefined : [token.address];
651
+ const mintBatch = new BatchCall(token.wallet, calls);
494
652
  await this.withNoMinTxsPerBlock(async () => {
495
- const { txHash } = await new BatchCall(token.wallet, calls).send({
653
+ const { txHash } = await mintBatch.send({
496
654
  from: minter,
497
655
  additionalScopes,
498
656
  wait: NO_WAIT,
@@ -565,6 +723,19 @@ export class BotFactory {
565
723
  return claim as L2AmountClaim;
566
724
  }
567
725
 
726
+ /** Returns worst-case min fees across predicted slots, with fallback to current min fees. */
727
+ private async getMinFees(): Promise<GasFees> {
728
+ try {
729
+ const predicted = await this.aztecNode.getPredictedMinFees(ManaUsageEstimate.Limit);
730
+ if (predicted.length === 0) {
731
+ return this.aztecNode.getCurrentMinFees();
732
+ }
733
+ return predicted.reduce((worst, fees) => (fees.feePerL2Gas > worst.feePerL2Gas ? fees : worst));
734
+ } catch {
735
+ return this.aztecNode.getCurrentMinFees();
736
+ }
737
+ }
738
+
568
739
  private async withNoMinTxsPerBlock<T>(fn: () => Promise<T>): Promise<T> {
569
740
  if (!this.aztecNodeAdmin || !this.config.flushSetupTransactions) {
570
741
  this.log.verbose(`No node admin client or flushing not requested (not setting minTxsPerBlock to 0)`);
package/src/interface.ts CHANGED
@@ -22,11 +22,11 @@ export interface BotRunnerApi {
22
22
  }
23
23
 
24
24
  export const BotRunnerApiSchema: ApiSchemaFor<BotRunnerApi> = {
25
- start: z.function().args().returns(z.void()),
26
- stop: z.function().args().returns(z.void()),
27
- run: z.function().args().returns(z.void()),
28
- setup: z.function().args().returns(z.void()),
29
- getInfo: z.function().args().returns(BotInfoSchema),
30
- getConfig: z.function().args().returns(BotConfigSchema),
31
- update: z.function().args(BotConfigSchema).returns(z.void()),
25
+ start: z.function({ input: z.tuple([]), output: z.void() }),
26
+ stop: z.function({ input: z.tuple([]), output: z.void() }),
27
+ run: z.function({ input: z.tuple([]), output: z.void() }),
28
+ setup: z.function({ input: z.tuple([]), output: z.void() }),
29
+ getInfo: z.function({ input: z.tuple([]), output: BotInfoSchema }),
30
+ getConfig: z.function({ input: z.tuple([]), output: BotConfigSchema }),
31
+ update: z.function({ input: z.tuple([BotConfigSchema]), output: z.void() }),
32
32
  };