@aztec/bot 0.0.1-commit.b6e433891 → 0.0.1-commit.b9865e97

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/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 { GasFees, 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,21 +230,14 @@ 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
-
227
- const { estimatedGas } = await deployMethod.simulate({
228
- from: AztecAddress.ZERO,
229
- fee: { estimateGas: true, paymentMethod },
230
- });
231
- const gasSettings = GasSettings.from({ ...estimatedGas!, maxFeesPerGas, maxPriorityFeesPerGas: GasFees.empty() });
232
233
 
233
234
  await this.withNoMinTxsPerBlock(async () => {
234
235
  const { txHash } = await deployMethod.send({
235
- from: AztecAddress.ZERO,
236
- fee: { gasSettings, paymentMethod },
236
+ from: NO_FROM,
237
+ fee: { paymentMethod },
237
238
  wait: NO_WAIT,
238
239
  });
239
- this.log.info(`Sent tx for account deployment with hash ${txHash.toString()}`, { gasSettings });
240
+ this.log.info(`Sent tx for account deployment with hash ${txHash.toString()}`);
240
241
  return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds });
241
242
  });
242
243
  this.log.info(`Account deployed at ${address}`);
@@ -258,35 +259,95 @@ export class BotFactory {
258
259
  return accountManager.address;
259
260
  }
260
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
+
261
320
  /**
262
321
  * Checks if the token contract is deployed and deploys it if necessary.
263
- * @param wallet - Wallet to deploy the token contract from.
264
- * @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.
265
326
  */
266
327
  private async setupToken(sender: AztecAddress): Promise<TokenContract | PrivateTokenContract> {
267
328
  let deploy: DeployMethod<TokenContract | PrivateTokenContract>;
268
- let tokenInstance: ContractInstanceWithAddress | undefined;
269
- const deployOpts: DeployOptions = {
270
- from: sender,
271
- contractAddressSalt: this.config.tokenSalt,
272
- universalDeploy: true,
273
- };
329
+ const salt = this.config.tokenSalt;
330
+ const deployOpts: DeployOptions = { from: sender };
274
331
  let token: TokenContract | PrivateTokenContract;
275
332
  if (this.config.contract === SupportedTokenContracts.TokenContract) {
276
- deploy = TokenContract.deploy(this.wallet, sender, 'BotToken', 'BOT', 18);
277
- tokenInstance = await deploy.getInstance(deployOpts);
278
- 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);
279
336
  } else if (this.config.contract === SupportedTokenContracts.PrivateTokenContract) {
280
337
  // Generate keys for the contract since PrivateToken uses SinglePrivateMutable which requires keys
281
338
  const tokenSecretKey = Fr.random();
282
339
  const tokenPublicKeys = (await deriveKeys(tokenSecretKey)).publicKeys;
283
- 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
+ });
284
345
  deployOpts.skipInstancePublication = true;
285
346
  deployOpts.skipClassPublication = true;
286
347
  deployOpts.skipInitialization = false;
287
348
 
288
349
  // Register the contract with the secret key before deployment
289
- tokenInstance = await deploy.getInstance(deployOpts);
350
+ const tokenInstance = await deploy.getInstance();
290
351
  token = PrivateTokenContract.at(tokenInstance.address, this.wallet);
291
352
  await this.wallet.registerContract(tokenInstance, PrivateTokenContract.artifact, tokenSecretKey);
292
353
  // The contract constructor initializes private storage vars that need the contract's own nullifier key.
@@ -295,21 +356,7 @@ export class BotFactory {
295
356
  throw new Error(`Unsupported token contract type: ${this.config.contract}`);
296
357
  }
297
358
 
298
- const address = tokenInstance?.address ?? (await deploy.getInstance(deployOpts)).address;
299
- const metadata = await this.wallet.getContractMetadata(address);
300
- if (metadata.isContractPublished) {
301
- this.log.info(`Token at ${address.toString()} already deployed`);
302
- await deploy.register();
303
- } else {
304
- this.log.info(`Deploying token contract at ${address.toString()}`);
305
- const { estimatedGas } = await deploy.simulate({ ...deployOpts, fee: { estimateGas: true } });
306
- const { txHash } = await deploy.send({ ...deployOpts, fee: { gasSettings: estimatedGas }, wait: NO_WAIT });
307
- this.log.info(`Sent tx for token setup with hash ${txHash.toString()}`, { estimatedGas });
308
- await this.withNoMinTxsPerBlock(async () => {
309
- await waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds });
310
- return token;
311
- });
312
- }
359
+ await this.registerOrDeployContract('token', deploy, deployOpts);
313
360
  return token;
314
361
  }
315
362
 
@@ -320,43 +367,39 @@ export class BotFactory {
320
367
  */
321
368
  private async setupTokenContract(
322
369
  deployer: AztecAddress,
323
- contractAddressSalt: Fr,
370
+ salt: Fr,
324
371
  name: string,
325
372
  ticker: string,
326
373
  decimals = 18,
327
374
  ): Promise<TokenContract> {
328
- const deployOpts: DeployOptions = { from: deployer, contractAddressSalt, universalDeploy: true };
329
- 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 });
330
377
  const instance = await this.registerOrDeployContract('Token - ' + name, deploy, deployOpts);
331
378
  return TokenContract.at(instance.address, this.wallet);
332
379
  }
333
380
 
334
381
  private async setupAmmContract(
335
382
  deployer: AztecAddress,
336
- contractAddressSalt: Fr,
383
+ salt: Fr,
337
384
  token0: TokenContract,
338
385
  token1: TokenContract,
339
386
  lpToken: TokenContract,
340
387
  ): Promise<AMMContract> {
341
- const deployOpts: DeployOptions = { from: deployer, contractAddressSalt, universalDeploy: true };
342
- 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
+ });
343
393
  const instance = await this.registerOrDeployContract('AMM', deploy, deployOpts);
344
394
  const amm = AMMContract.at(instance.address, this.wallet);
345
395
 
346
396
  this.log.info(`AMM deployed at ${amm.address}`);
347
397
  const setMinterInteraction = lpToken.methods.set_minter(amm.address, true);
348
- const { estimatedGas: setMinterGas } = await setMinterInteraction.simulate({
349
- from: deployer,
350
- fee: { estimateGas: true },
351
- });
352
398
  const { receipt: minterReceipt } = await setMinterInteraction.send({
353
399
  from: deployer,
354
- fee: { gasSettings: setMinterGas },
355
400
  wait: { timeout: this.config.txMinedWaitSeconds },
356
401
  });
357
- this.log.info(`Set LP token minter to AMM txHash=${minterReceipt.txHash.toString()}`, {
358
- estimatedGas: setMinterGas,
359
- });
402
+ this.log.info(`Set LP token minter to AMM txHash=${minterReceipt.txHash.toString()}`);
360
403
  this.log.info(`Liquidity token initialized`);
361
404
 
362
405
  return amm;
@@ -428,17 +471,12 @@ export class BotFactory {
428
471
  token0.methods.mint_to_private(liquidityProvider, MINT_BALANCE),
429
472
  token1.methods.mint_to_private(liquidityProvider, MINT_BALANCE),
430
473
  ]);
431
- const { estimatedGas: mintGas } = await mintBatch.simulate({
432
- from: liquidityProvider,
433
- fee: { estimateGas: true },
434
- });
435
474
  const { receipt: mintReceipt } = await mintBatch.send({
436
475
  from: liquidityProvider,
437
- fee: { gasSettings: mintGas },
438
476
  wait: { timeout: this.config.txMinedWaitSeconds },
439
477
  });
440
478
 
441
- this.log.info(`Sent mint tx: ${mintReceipt.txHash.toString()}`, { estimatedGas: mintGas });
479
+ this.log.info(`Sent mint tx: ${mintReceipt.txHash.toString()}`);
442
480
 
443
481
  const addLiquidityInteraction = amm.methods.add_liquidity(
444
482
  amount0Max,
@@ -447,21 +485,13 @@ export class BotFactory {
447
485
  amount1Min,
448
486
  authwitNonce,
449
487
  );
450
- const { estimatedGas: addLiquidityGas } = await addLiquidityInteraction.simulate({
451
- from: liquidityProvider,
452
- fee: { estimateGas: true },
453
- authWitnesses: [token0Authwit, token1Authwit],
454
- });
455
488
  const { receipt: addLiquidityReceipt } = await addLiquidityInteraction.send({
456
489
  from: liquidityProvider,
457
- fee: { gasSettings: addLiquidityGas },
458
490
  authWitnesses: [token0Authwit, token1Authwit],
459
491
  wait: { timeout: this.config.txMinedWaitSeconds },
460
492
  });
461
493
 
462
- this.log.info(`Sent tx to add liquidity to the AMM: ${addLiquidityReceipt.txHash.toString()}`, {
463
- estimatedGas: addLiquidityGas,
464
- });
494
+ this.log.info(`Sent tx to add liquidity to the AMM: ${addLiquidityReceipt.txHash.toString()}`);
465
495
  this.log.info(`Liquidity added`);
466
496
 
467
497
  const [newT0Bal, newT1Bal, newLPBal] = await getPrivateBalances();
@@ -475,20 +505,49 @@ export class BotFactory {
475
505
  deploy: DeployMethod<T>,
476
506
  deployOpts: DeployOptions,
477
507
  ): Promise<ContractInstanceWithAddress> {
478
- const instance = await deploy.getInstance(deployOpts);
508
+ const instance = await deploy.getInstance();
479
509
  const address = instance.address;
480
510
  const metadata = await this.wallet.getContractMetadata(address);
481
511
  if (metadata.isContractPublished) {
482
512
  this.log.info(`Contract ${name} at ${address.toString()} already deployed`);
483
513
  await deploy.register();
484
514
  } else {
485
- const { estimatedGas } = await deploy.simulate({ ...deployOpts, fee: { estimateGas: true } });
486
- this.log.info(`Deploying contract ${name} at ${address.toString()}`, { estimatedGas });
487
- await this.withNoMinTxsPerBlock(async () => {
488
- const { txHash } = await deploy.send({ ...deployOpts, fee: { gasSettings: estimatedGas }, wait: NO_WAIT });
489
- this.log.info(`Sent contract ${name} setup tx with hash ${txHash.toString()}`);
490
- return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds });
491
- });
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
+ }
492
551
  }
493
552
  return instance;
494
553
  }
@@ -497,6 +556,66 @@ export class BotFactory {
497
556
  * Mints private and public tokens for the sender if their balance is below the minimum.
498
557
  * @param token - Token contract.
499
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
+
500
619
  private async mintTokens(token: TokenContract | PrivateTokenContract, minter: AztecAddress) {
501
620
  const isStandardToken = isStandardTokenContract(token);
502
621
  let privateBalance = 0n;
@@ -530,15 +649,13 @@ export class BotFactory {
530
649
  // PrivateToken's mint accesses contract-level private storage vars (admin, total_supply).
531
650
  const additionalScopes = isStandardToken ? undefined : [token.address];
532
651
  const mintBatch = new BatchCall(token.wallet, calls);
533
- const { estimatedGas } = await mintBatch.simulate({ from: minter, fee: { estimateGas: true }, additionalScopes });
534
652
  await this.withNoMinTxsPerBlock(async () => {
535
653
  const { txHash } = await mintBatch.send({
536
654
  from: minter,
537
655
  additionalScopes,
538
- fee: { gasSettings: estimatedGas },
539
656
  wait: NO_WAIT,
540
657
  });
541
- this.log.info(`Sent token mint tx with hash ${txHash.toString()}`, { estimatedGas });
658
+ this.log.info(`Sent token mint tx with hash ${txHash.toString()}`);
542
659
  return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds });
543
660
  });
544
661
  }
@@ -606,6 +723,19 @@ export class BotFactory {
606
723
  return claim as L2AmountClaim;
607
724
  }
608
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
+
609
739
  private async withNoMinTxsPerBlock<T>(fn: () => Promise<T>): Promise<T> {
610
740
  if (!this.aztecNodeAdmin || !this.config.flushSetupTransactions) {
611
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
  };