@aztec/bot 6.0.0-nightly.20260604 → 6.0.0-nightly.20260721
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/dest/amm_bot.d.ts +3 -2
- package/dest/amm_bot.d.ts.map +1 -1
- package/dest/amm_bot.js +2 -2
- package/dest/bot.d.ts +3 -2
- package/dest/bot.d.ts.map +1 -1
- package/dest/bot.js +2 -2
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +3 -3
- package/dest/cross_chain_bot.d.ts +5 -3
- package/dest/cross_chain_bot.d.ts.map +1 -1
- package/dest/cross_chain_bot.js +7 -6
- package/dest/factory.d.ts +6 -7
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +66 -215
- package/dest/runner.d.ts +3 -2
- package/dest/runner.d.ts.map +1 -1
- package/dest/runner.js +6 -4
- package/package.json +15 -15
- package/src/amm_bot.ts +3 -0
- package/src/bot.ts +3 -0
- package/src/config.ts +3 -2
- package/src/cross_chain_bot.ts +6 -2
- package/src/factory.ts +62 -208
- package/src/runner.ts +26 -3
package/src/cross_chain_bot.ts
CHANGED
|
@@ -31,6 +31,7 @@ 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';
|
|
33
33
|
import type { TestContract } from '@aztec/noir-test-contracts.js/Test';
|
|
34
|
+
import type { BlockTag } from '@aztec/stdlib/block';
|
|
34
35
|
import type { AztecNode, AztecNodeAdmin } from '@aztec/stdlib/interfaces/client';
|
|
35
36
|
import type { EmbeddedWallet } from '@aztec/wallets/embedded';
|
|
36
37
|
|
|
@@ -60,6 +61,7 @@ export class CrossChainBot extends BaseBot {
|
|
|
60
61
|
private readonly rollupVersion: bigint,
|
|
61
62
|
private readonly store: BotStore,
|
|
62
63
|
config: BotConfig,
|
|
64
|
+
private readonly syncChainTip?: BlockTag,
|
|
63
65
|
) {
|
|
64
66
|
super(node, wallet, defaultAccountAddress, config);
|
|
65
67
|
}
|
|
@@ -70,11 +72,12 @@ export class CrossChainBot extends BaseBot {
|
|
|
70
72
|
aztecNode: AztecNode,
|
|
71
73
|
aztecNodeAdmin: AztecNodeAdmin | undefined,
|
|
72
74
|
store: BotStore,
|
|
75
|
+
syncChainTip?: BlockTag,
|
|
73
76
|
): Promise<CrossChainBot> {
|
|
74
77
|
if (config.followChain === 'NONE') {
|
|
75
78
|
throw new Error(`CrossChainBot requires followChain to be set (got NONE)`);
|
|
76
79
|
}
|
|
77
|
-
const factory = new BotFactory(config, wallet, store, aztecNode, aztecNodeAdmin);
|
|
80
|
+
const factory = new BotFactory(config, wallet, store, aztecNode, aztecNodeAdmin, syncChainTip);
|
|
78
81
|
const { defaultAccountAddress, contract, l1Client, rollupVersion } = await factory.setupCrossChain();
|
|
79
82
|
const l1Recipient = EthAddress.fromString(l1Client.account!.address);
|
|
80
83
|
const { l1ContractAddresses } = await aztecNode.getNodeInfo();
|
|
@@ -90,6 +93,7 @@ export class CrossChainBot extends BaseBot {
|
|
|
90
93
|
rollupVersion,
|
|
91
94
|
store,
|
|
92
95
|
config,
|
|
96
|
+
syncChainTip,
|
|
93
97
|
);
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -176,7 +180,7 @@ export class CrossChainBot extends BaseBot {
|
|
|
176
180
|
): Promise<PendingL1ToL2Message | undefined> {
|
|
177
181
|
const now = Date.now();
|
|
178
182
|
for (const msg of pendingMessages) {
|
|
179
|
-
const ready = await isL1ToL2MessageReady(this.node, Fr.fromHexString(msg.msgHash));
|
|
183
|
+
const ready = await isL1ToL2MessageReady(this.node, Fr.fromHexString(msg.msgHash), this.syncChainTip);
|
|
180
184
|
if (ready) {
|
|
181
185
|
return msg;
|
|
182
186
|
}
|
package/src/factory.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getInitialTestAccountsData } from '@aztec/accounts/testing';
|
|
2
|
-
import { NO_FROM } from '@aztec/aztec.js/account';
|
|
3
2
|
import { AztecAddress } from '@aztec/aztec.js/addresses';
|
|
4
3
|
import {
|
|
5
4
|
BatchCall,
|
|
@@ -17,20 +16,18 @@ import { createLogger } from '@aztec/aztec.js/log';
|
|
|
17
16
|
import { waitForL1ToL2MessageReady } from '@aztec/aztec.js/messaging';
|
|
18
17
|
import { waitForTx } from '@aztec/aztec.js/node';
|
|
19
18
|
import { getFeeJuiceBalance } from '@aztec/aztec.js/utils';
|
|
20
|
-
import { ContractInitializationStatus } from '@aztec/aztec.js/wallet';
|
|
21
19
|
import { createEthereumChain } from '@aztec/ethereum/chain';
|
|
22
20
|
import { createExtendedL1Client } from '@aztec/ethereum/client';
|
|
23
21
|
import { RollupContract } from '@aztec/ethereum/contracts';
|
|
24
22
|
import type { ExtendedViemWalletClient } from '@aztec/ethereum/types';
|
|
25
23
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
26
24
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
27
|
-
import { Timer } from '@aztec/foundation/timer';
|
|
28
25
|
import { AMMContract } from '@aztec/noir-contracts.js/AMM';
|
|
29
26
|
import { PrivateTokenContract } from '@aztec/noir-contracts.js/PrivateToken';
|
|
30
27
|
import { TokenContract } from '@aztec/noir-contracts.js/Token';
|
|
31
28
|
import { TestContract } from '@aztec/noir-test-contracts.js/Test';
|
|
29
|
+
import type { BlockTag } from '@aztec/stdlib/block';
|
|
32
30
|
import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract';
|
|
33
|
-
import { GasFees, GasSettings, ManaUsageEstimate } from '@aztec/stdlib/gas';
|
|
34
31
|
import type { AztecNode, AztecNodeAdmin } from '@aztec/stdlib/interfaces/client';
|
|
35
32
|
import { deriveSigningKey } from '@aztec/stdlib/keys';
|
|
36
33
|
import { EmbeddedWallet } from '@aztec/wallets/embedded';
|
|
@@ -43,7 +40,6 @@ import { getBalances, getPrivateBalance, isStandardTokenContract } from './utils
|
|
|
43
40
|
const MINT_BALANCE = 1e12;
|
|
44
41
|
const MIN_BALANCE = 1e3;
|
|
45
42
|
const FEE_JUICE_TOP_UP_THRESHOLD = 100n * 10n ** 18n;
|
|
46
|
-
const FEE_JUICE_TOP_UP_TARGET = 10_000n * 10n ** 18n;
|
|
47
43
|
|
|
48
44
|
export class BotFactory {
|
|
49
45
|
private log = createLogger('bot');
|
|
@@ -54,6 +50,7 @@ export class BotFactory {
|
|
|
54
50
|
private readonly store: BotStore,
|
|
55
51
|
private readonly aztecNode: AztecNode,
|
|
56
52
|
private readonly aztecNodeAdmin?: AztecNodeAdmin,
|
|
53
|
+
private readonly syncChainTip?: BlockTag,
|
|
57
54
|
) {
|
|
58
55
|
// Set fee padding on the wallet so that all transactions during setup
|
|
59
56
|
// (token deploy, minting, etc.) use the configured padding, not the default.
|
|
@@ -73,8 +70,8 @@ export class BotFactory {
|
|
|
73
70
|
}> {
|
|
74
71
|
const defaultAccountAddress = await this.setupAccount();
|
|
75
72
|
const recipient = (await this.wallet.createSchnorrAccount(Fr.random(), Fr.random())).address;
|
|
76
|
-
|
|
77
|
-
await this.
|
|
73
|
+
await this.ensureFeeJuiceBalance(defaultAccountAddress);
|
|
74
|
+
const token = await this.setupToken(defaultAccountAddress);
|
|
78
75
|
await this.mintTokens(token, defaultAccountAddress);
|
|
79
76
|
return { wallet: this.wallet, defaultAccountAddress, token, node: this.aztecNode, recipient };
|
|
80
77
|
}
|
|
@@ -88,13 +85,8 @@ export class BotFactory {
|
|
|
88
85
|
node: AztecNode;
|
|
89
86
|
}> {
|
|
90
87
|
const defaultAccountAddress = await this.setupAccount();
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
this.config.tokenSalt,
|
|
94
|
-
'BotToken0',
|
|
95
|
-
'BOT0',
|
|
96
|
-
);
|
|
97
|
-
await this.ensureFeeJuiceBalance(defaultAccountAddress, token0);
|
|
88
|
+
await this.ensureFeeJuiceBalance(defaultAccountAddress);
|
|
89
|
+
const token0 = await this.setupTokenContract(defaultAccountAddress, this.config.tokenSalt, 'BotToken0', 'BOT0');
|
|
98
90
|
const token1 = await this.setupTokenContract(defaultAccountAddress, this.config.tokenSalt, 'BotToken1', 'BOT1');
|
|
99
91
|
const liquidityToken = await this.setupTokenContract(
|
|
100
92
|
defaultAccountAddress,
|
|
@@ -129,6 +121,7 @@ export class BotFactory {
|
|
|
129
121
|
rollupVersion: bigint;
|
|
130
122
|
}> {
|
|
131
123
|
const defaultAccountAddress = await this.setupAccount();
|
|
124
|
+
await this.ensureFeeJuiceBalance(defaultAccountAddress);
|
|
132
125
|
|
|
133
126
|
// Create L1 client (same pattern as bridgeL1FeeJuice)
|
|
134
127
|
const l1RpcUrls = this.config.l1RpcUrls;
|
|
@@ -147,7 +140,7 @@ export class BotFactory {
|
|
|
147
140
|
const rollupContract = new RollupContract(l1Client, l1ContractAddresses.rollupAddress.toString());
|
|
148
141
|
const rollupVersion = await rollupContract.getVersion();
|
|
149
142
|
|
|
150
|
-
// Deploy TestContract
|
|
143
|
+
// Deploy TestContract (pays from the standing balance funded above).
|
|
151
144
|
const contract = await this.setupTestContract(defaultAccountAddress);
|
|
152
145
|
|
|
153
146
|
// Recover any pending messages from store (clean up stale ones first)
|
|
@@ -174,6 +167,7 @@ export class BotFactory {
|
|
|
174
167
|
const firstMsg = allMessages[0];
|
|
175
168
|
await waitForL1ToL2MessageReady(this.aztecNode, Fr.fromHexString(firstMsg.msgHash), {
|
|
176
169
|
timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds,
|
|
170
|
+
chainTip: this.syncChainTip,
|
|
177
171
|
});
|
|
178
172
|
this.log.info(`First L1→L2 message is ready`);
|
|
179
173
|
}
|
|
@@ -210,48 +204,16 @@ export class BotFactory {
|
|
|
210
204
|
}
|
|
211
205
|
}
|
|
212
206
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const timer = new Timer();
|
|
221
|
-
const address = accountManager.address;
|
|
222
|
-
this.log.info(`Account at ${address} registered. duration=${timer.ms()}`);
|
|
223
|
-
await this.store.deleteBridgeClaim(address);
|
|
224
|
-
return address;
|
|
225
|
-
} else {
|
|
226
|
-
const address = accountManager.address;
|
|
227
|
-
this.log.info(`Deploying account at ${address}`);
|
|
228
|
-
|
|
229
|
-
const claim = await this.getOrCreateBridgeClaim(address);
|
|
230
|
-
|
|
231
|
-
const paymentMethod = new FeeJuicePaymentMethodWithClaim(accountManager.address, claim);
|
|
232
|
-
const deployMethod = await accountManager.getDeployMethod();
|
|
233
|
-
|
|
234
|
-
await this.withNoMinTxsPerBlock(async () => {
|
|
235
|
-
const { txHash } = await deployMethod.send({
|
|
236
|
-
from: NO_FROM,
|
|
237
|
-
fee: { paymentMethod },
|
|
238
|
-
wait: NO_WAIT,
|
|
239
|
-
});
|
|
240
|
-
this.log.info(`Sent tx for account deployment with hash ${txHash.toString()}`);
|
|
241
|
-
return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds });
|
|
242
|
-
});
|
|
243
|
-
this.log.info(`Account deployed at ${address}`);
|
|
244
|
-
|
|
245
|
-
// Clean up the consumed bridge claim
|
|
246
|
-
await this.store.deleteBridgeClaim(address);
|
|
247
|
-
|
|
248
|
-
return accountManager.address;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Keyless fallback for tests and local dev: reuses the first genesis test account, whose address is
|
|
209
|
+
* pre-funded with fee juice via `initialFundedAccounts`. The test accounts are initializerless, so this
|
|
210
|
+
* must create an initializerless account for the address to match the funded one. Production bots set a
|
|
211
|
+
* sender private key and fund the resulting initializerless account from L1 instead; see
|
|
212
|
+
* setupAccountWithPrivateKey.
|
|
213
|
+
*/
|
|
252
214
|
private async setupTestAccount() {
|
|
253
215
|
const [initialAccountData] = await getInitialTestAccountsData();
|
|
254
|
-
const accountManager = await this.wallet.
|
|
216
|
+
const accountManager = await this.wallet.createSchnorrInitializerlessAccount(
|
|
255
217
|
initialAccountData.secret,
|
|
256
218
|
initialAccountData.salt,
|
|
257
219
|
initialAccountData.signingKey,
|
|
@@ -259,62 +221,11 @@ export class BotFactory {
|
|
|
259
221
|
return accountManager.address;
|
|
260
222
|
}
|
|
261
223
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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}`);
|
|
224
|
+
private async setupAccountWithPrivateKey(secret: Fr) {
|
|
225
|
+
const salt = this.config.senderSalt ?? Fr.ONE;
|
|
226
|
+
const signingKey = deriveSigningKey(secret);
|
|
227
|
+
const accountManager = await this.wallet.createSchnorrInitializerlessAccount(secret, salt, signingKey);
|
|
228
|
+
return accountManager.address;
|
|
318
229
|
}
|
|
319
230
|
|
|
320
231
|
/**
|
|
@@ -511,66 +422,39 @@ export class BotFactory {
|
|
|
511
422
|
if (metadata.isContractPublished) {
|
|
512
423
|
this.log.info(`Contract ${name} at ${address.toString()} already deployed`);
|
|
513
424
|
await deploy.register();
|
|
514
|
-
|
|
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
|
-
}
|
|
425
|
+
return instance;
|
|
551
426
|
}
|
|
427
|
+
|
|
428
|
+
// Setup always runs ensureFeeJuiceBalance before any deploy, so the account pays from its standing
|
|
429
|
+
// balance here. No manual gas estimation: the embedded wallet simulates before sending and derives
|
|
430
|
+
// the gas limits and padded maxFeesPerGas itself.
|
|
431
|
+
this.log.info(`Deploying contract ${name} at ${address.toString()}`);
|
|
432
|
+
await this.withNoMinTxsPerBlock(async () => {
|
|
433
|
+
const { txHash } = await deploy.send({ ...deployOpts, wait: NO_WAIT });
|
|
434
|
+
this.log.info(`Sent contract ${name} deploy tx ${txHash.toString()}`);
|
|
435
|
+
return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds });
|
|
436
|
+
});
|
|
437
|
+
|
|
552
438
|
return instance;
|
|
553
439
|
}
|
|
554
440
|
|
|
441
|
+
/** True when the config allows bridging fee juice from L1 (fee_juice mode, an L1 RPC, and an L1 key). */
|
|
442
|
+
private isL1BridgingConfigured(): boolean {
|
|
443
|
+
const mnemonicOrPrivateKey = this.config.l1PrivateKey?.getValue() ?? this.config.l1Mnemonic?.getValue();
|
|
444
|
+
return this.config.feePaymentMethod === 'fee_juice' && !!this.config.l1RpcUrls?.length && !!mnemonicOrPrivateKey;
|
|
445
|
+
}
|
|
446
|
+
|
|
555
447
|
/**
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
448
|
+
* Ensures the account holds enough fee juice before any other setup step. The account starts empty
|
|
449
|
+
* (initializerless accounts have no deployment tx) and the runtime loop pays fees from this balance and
|
|
450
|
+
* never refuels itself, so every flow funds the account up front. Bridges claims from L1 and consumes
|
|
451
|
+
* each with a claim-only tx until the balance clears the threshold, working from a zero (fresh run) or
|
|
452
|
+
* drained (restart) balance. Each bridge mints a fixed amount well above the threshold, so this is a
|
|
453
|
+
* single bridge in practice. No-op when L1 bridging is not configured or the balance is already above
|
|
454
|
+
* the threshold.
|
|
563
455
|
*/
|
|
564
|
-
private async ensureFeeJuiceBalance(
|
|
565
|
-
|
|
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) {
|
|
456
|
+
private async ensureFeeJuiceBalance(account: AztecAddress): Promise<void> {
|
|
457
|
+
if (!this.isL1BridgingConfigured()) {
|
|
574
458
|
return;
|
|
575
459
|
}
|
|
576
460
|
|
|
@@ -580,36 +464,21 @@ export class BotFactory {
|
|
|
580
464
|
return;
|
|
581
465
|
}
|
|
582
466
|
|
|
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);
|
|
467
|
+
this.log.info(`Fee juice balance ${balance} below threshold ${FEE_JUICE_TOP_UP_THRESHOLD}, bridging from L1`);
|
|
590
468
|
|
|
591
|
-
while (balance <
|
|
592
|
-
|
|
469
|
+
while (balance < FEE_JUICE_TOP_UP_THRESHOLD) {
|
|
470
|
+
// Persist the claim before consuming it: if the top-up tx fails or the bot crashes mid-loop, the
|
|
471
|
+
// next run reuses the pending claim instead of bridging again (and wasting the bridged funds).
|
|
472
|
+
const claim = await this.getOrCreateBridgeClaim(account);
|
|
593
473
|
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
474
|
|
|
604
475
|
await this.withNoMinTxsPerBlock(async () => {
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
fee: { gasSettings, paymentMethod },
|
|
608
|
-
wait: NO_WAIT,
|
|
609
|
-
});
|
|
476
|
+
const executionPayload = await paymentMethod.getExecutionPayload();
|
|
477
|
+
const { txHash } = await this.wallet.sendTx(executionPayload, { from: account, wait: NO_WAIT });
|
|
610
478
|
this.log.info(`Sent fee juice top-up tx ${txHash.toString()}`);
|
|
611
479
|
return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds });
|
|
612
480
|
});
|
|
481
|
+
await this.store.deleteBridgeClaim(account);
|
|
613
482
|
balance = await getFeeJuiceBalance(account, this.aztecNode);
|
|
614
483
|
this.log.info(`Fee juice balance after top-up: ${balance}`);
|
|
615
484
|
}
|
|
@@ -661,22 +530,20 @@ export class BotFactory {
|
|
|
661
530
|
}
|
|
662
531
|
|
|
663
532
|
/**
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
533
|
+
* Returns a usable bridge claim for the recipient, reusing a persisted one when its L1→L2 message is
|
|
534
|
+
* still available (resuming a top-up that failed or crashed before the claim was consumed) and bridging
|
|
535
|
+
* a fresh claim otherwise. The caller deletes the claim from the store once it has been consumed.
|
|
667
536
|
*/
|
|
668
537
|
private async getOrCreateBridgeClaim(recipient: AztecAddress): Promise<L2AmountClaim> {
|
|
669
|
-
// Check if we have an existing claim in the store
|
|
670
538
|
const existingClaim = await this.store.getBridgeClaim(recipient);
|
|
671
539
|
if (existingClaim) {
|
|
672
540
|
this.log.info(`Found existing bridge claim for ${recipient.toString()}, checking validity...`);
|
|
673
|
-
|
|
674
|
-
// Check if the message is ready on L2
|
|
675
541
|
try {
|
|
676
542
|
const messageHash = Fr.fromHexString(existingClaim.claim.messageHash);
|
|
677
543
|
await this.withNoMinTxsPerBlock(() =>
|
|
678
544
|
waitForL1ToL2MessageReady(this.aztecNode, messageHash, {
|
|
679
545
|
timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds,
|
|
546
|
+
chainTip: this.syncChainTip,
|
|
680
547
|
}),
|
|
681
548
|
);
|
|
682
549
|
return existingClaim.claim;
|
|
@@ -688,7 +555,6 @@ export class BotFactory {
|
|
|
688
555
|
|
|
689
556
|
const claim = await this.bridgeL1FeeJuice(recipient);
|
|
690
557
|
await this.store.saveBridgeClaim(recipient, claim);
|
|
691
|
-
|
|
692
558
|
return claim;
|
|
693
559
|
}
|
|
694
560
|
|
|
@@ -715,6 +581,7 @@ export class BotFactory {
|
|
|
715
581
|
await this.withNoMinTxsPerBlock(() =>
|
|
716
582
|
waitForL1ToL2MessageReady(this.aztecNode, Fr.fromHexString(claim.messageHash), {
|
|
717
583
|
timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds,
|
|
584
|
+
chainTip: this.syncChainTip,
|
|
718
585
|
}),
|
|
719
586
|
);
|
|
720
587
|
|
|
@@ -723,19 +590,6 @@ export class BotFactory {
|
|
|
723
590
|
return claim as L2AmountClaim;
|
|
724
591
|
}
|
|
725
592
|
|
|
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
|
-
|
|
739
593
|
private async withNoMinTxsPerBlock<T>(fn: () => Promise<T>): Promise<T> {
|
|
740
594
|
if (!this.aztecNodeAdmin || !this.config.flushSetupTransactions) {
|
|
741
595
|
this.log.verbose(`No node admin client or flushing not requested (not setting minTxsPerBlock to 0)`);
|
package/src/runner.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createLogger } from '@aztec/aztec.js/log';
|
|
|
2
2
|
import type { AztecNode } from '@aztec/aztec.js/node';
|
|
3
3
|
import { omit } from '@aztec/foundation/collection';
|
|
4
4
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
5
|
+
import type { BlockTag } from '@aztec/stdlib/block';
|
|
5
6
|
import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client';
|
|
6
7
|
import { type TelemetryClient, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
|
|
7
8
|
import type { EmbeddedWallet } from '@aztec/wallets/embedded';
|
|
@@ -30,6 +31,7 @@ export class BotRunner implements BotRunnerApi, Traceable {
|
|
|
30
31
|
private readonly telemetry: TelemetryClient,
|
|
31
32
|
private readonly aztecNodeAdmin: AztecNodeAdmin | undefined,
|
|
32
33
|
private readonly store: BotStore,
|
|
34
|
+
private readonly syncChainTip?: BlockTag,
|
|
33
35
|
) {
|
|
34
36
|
this.tracer = telemetry.getTracer('Bot');
|
|
35
37
|
|
|
@@ -149,13 +151,34 @@ export class BotRunner implements BotRunnerApi, Traceable {
|
|
|
149
151
|
try {
|
|
150
152
|
switch (this.config.botMode) {
|
|
151
153
|
case 'crosschain':
|
|
152
|
-
this.bot = CrossChainBot.create(
|
|
154
|
+
this.bot = CrossChainBot.create(
|
|
155
|
+
this.config,
|
|
156
|
+
this.wallet,
|
|
157
|
+
this.aztecNode,
|
|
158
|
+
this.aztecNodeAdmin,
|
|
159
|
+
this.store,
|
|
160
|
+
this.syncChainTip,
|
|
161
|
+
);
|
|
153
162
|
break;
|
|
154
163
|
case 'amm':
|
|
155
|
-
this.bot = AmmBot.create(
|
|
164
|
+
this.bot = AmmBot.create(
|
|
165
|
+
this.config,
|
|
166
|
+
this.wallet,
|
|
167
|
+
this.aztecNode,
|
|
168
|
+
this.aztecNodeAdmin,
|
|
169
|
+
this.store,
|
|
170
|
+
this.syncChainTip,
|
|
171
|
+
);
|
|
156
172
|
break;
|
|
157
173
|
case 'transfer':
|
|
158
|
-
this.bot = Bot.create(
|
|
174
|
+
this.bot = Bot.create(
|
|
175
|
+
this.config,
|
|
176
|
+
this.wallet,
|
|
177
|
+
this.aztecNode,
|
|
178
|
+
this.aztecNodeAdmin,
|
|
179
|
+
this.store,
|
|
180
|
+
this.syncChainTip,
|
|
181
|
+
);
|
|
159
182
|
break;
|
|
160
183
|
default: {
|
|
161
184
|
const _exhaustive: never = this.config.botMode;
|