@aztec/bot 0.0.0-test.0

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 ADDED
@@ -0,0 +1,151 @@
1
+ import {
2
+ type AztecAddress,
3
+ BatchCall,
4
+ FeeJuicePaymentMethod,
5
+ type SendMethodOptions,
6
+ type Wallet,
7
+ createLogger,
8
+ } from '@aztec/aztec.js';
9
+ import { timesParallel } from '@aztec/foundation/collection';
10
+ import type { EasyPrivateTokenContract } from '@aztec/noir-contracts.js/EasyPrivateToken';
11
+ import type { TokenContract } from '@aztec/noir-contracts.js/Token';
12
+ import type { FunctionCall } from '@aztec/stdlib/abi';
13
+ import { Gas } from '@aztec/stdlib/gas';
14
+ import type { AztecNode, PXE } from '@aztec/stdlib/interfaces/client';
15
+
16
+ import type { BotConfig } from './config.js';
17
+ import { BotFactory } from './factory.js';
18
+ import { getBalances, getPrivateBalance, isStandardTokenContract } from './utils.js';
19
+
20
+ const TRANSFER_AMOUNT = 1;
21
+
22
+ export class Bot {
23
+ private log = createLogger('bot');
24
+
25
+ private attempts: number = 0;
26
+ private successes: number = 0;
27
+
28
+ protected constructor(
29
+ public readonly wallet: Wallet,
30
+ public readonly token: TokenContract | EasyPrivateTokenContract,
31
+ public readonly recipient: AztecAddress,
32
+ public config: BotConfig,
33
+ ) {}
34
+
35
+ static async create(config: BotConfig, dependencies: { pxe?: PXE; node?: AztecNode } = {}): Promise<Bot> {
36
+ const { wallet, token, recipient } = await new BotFactory(config, dependencies).setup();
37
+ return new Bot(wallet, token, recipient, config);
38
+ }
39
+
40
+ public updateConfig(config: Partial<BotConfig>) {
41
+ this.log.info(`Updating bot config ${Object.keys(config).join(', ')}`);
42
+ this.config = { ...this.config, ...config };
43
+ }
44
+
45
+ public async run() {
46
+ this.attempts++;
47
+ const logCtx = { runId: Date.now() * 1000 + Math.floor(Math.random() * 1000) };
48
+ const { privateTransfersPerTx, publicTransfersPerTx, feePaymentMethod, followChain, txMinedWaitSeconds } =
49
+ this.config;
50
+ const { token, recipient, wallet } = this;
51
+ const sender = wallet.getAddress();
52
+
53
+ this.log.verbose(
54
+ `Preparing tx with ${feePaymentMethod} fee with ${privateTransfersPerTx} private and ${publicTransfersPerTx} public transfers`,
55
+ logCtx,
56
+ );
57
+
58
+ const calls: FunctionCall[] = [];
59
+ if (isStandardTokenContract(token)) {
60
+ calls.push(
61
+ ...(await timesParallel(privateTransfersPerTx, () =>
62
+ token.methods.transfer(recipient, TRANSFER_AMOUNT).request(),
63
+ )),
64
+ );
65
+ calls.push(
66
+ ...(await timesParallel(publicTransfersPerTx, () =>
67
+ token.methods.transfer_in_public(sender, recipient, TRANSFER_AMOUNT, 0).request(),
68
+ )),
69
+ );
70
+ } else {
71
+ calls.push(
72
+ ...(await timesParallel(privateTransfersPerTx, () =>
73
+ token.methods.transfer(TRANSFER_AMOUNT, sender, recipient).request(),
74
+ )),
75
+ );
76
+ }
77
+
78
+ const opts = this.getSendMethodOpts();
79
+ const batch = new BatchCall(wallet, calls);
80
+
81
+ this.log.verbose(`Simulating transaction with ${calls.length}`, logCtx);
82
+ await batch.simulate();
83
+
84
+ this.log.verbose(`Proving transaction`, logCtx);
85
+ const provenTx = await batch.prove(opts);
86
+
87
+ this.log.verbose(`Sending tx`, logCtx);
88
+ const tx = provenTx.send();
89
+
90
+ const txHash = await tx.getTxHash();
91
+
92
+ if (followChain === 'NONE') {
93
+ this.log.info(`Transaction ${txHash} sent, not waiting for it to be mined`);
94
+ return;
95
+ }
96
+
97
+ this.log.verbose(
98
+ `Awaiting tx ${txHash} to be on the ${followChain} chain (timeout ${txMinedWaitSeconds}s)`,
99
+ logCtx,
100
+ );
101
+ const receipt = await tx.wait({
102
+ timeout: txMinedWaitSeconds,
103
+ provenTimeout: txMinedWaitSeconds,
104
+ proven: followChain === 'PROVEN',
105
+ });
106
+ this.log.info(
107
+ `Tx #${this.attempts} ${receipt.txHash} successfully mined in block ${receipt.blockNumber} (stats: ${this.successes}/${this.attempts} success)`,
108
+ logCtx,
109
+ );
110
+ this.successes++;
111
+ }
112
+
113
+ public async getBalances() {
114
+ if (isStandardTokenContract(this.token)) {
115
+ return {
116
+ sender: await getBalances(this.token, this.wallet.getAddress()),
117
+ recipient: await getBalances(this.token, this.recipient),
118
+ };
119
+ } else {
120
+ return {
121
+ sender: {
122
+ privateBalance: await getPrivateBalance(this.token, this.wallet.getAddress()),
123
+ publicBalance: 0n,
124
+ },
125
+ recipient: {
126
+ privateBalance: await getPrivateBalance(this.token, this.recipient),
127
+ publicBalance: 0n,
128
+ },
129
+ };
130
+ }
131
+ }
132
+
133
+ private getSendMethodOpts(): SendMethodOptions {
134
+ const sender = this.wallet.getAddress();
135
+ const { l2GasLimit, daGasLimit, skipPublicSimulation } = this.config;
136
+ const paymentMethod = new FeeJuicePaymentMethod(sender);
137
+
138
+ let gasSettings, estimateGas;
139
+ if (l2GasLimit !== undefined && l2GasLimit > 0 && daGasLimit !== undefined && daGasLimit > 0) {
140
+ gasSettings = { gasLimits: Gas.from({ l2Gas: l2GasLimit, daGas: daGasLimit }) };
141
+ estimateGas = false;
142
+ this.log.verbose(`Using gas limits ${l2GasLimit} L2 gas ${daGasLimit} DA gas`);
143
+ } else {
144
+ estimateGas = true;
145
+ this.log.verbose(`Estimating gas for transaction`);
146
+ }
147
+ const baseFeePadding = 2; // Send 3x the current base fee
148
+ this.log.verbose(skipPublicSimulation ? `Skipping public simulation` : `Simulating public transfers`);
149
+ return { fee: { estimateGas, paymentMethod, gasSettings, baseFeePadding }, skipPublicSimulation };
150
+ }
151
+ }
package/src/config.ts ADDED
@@ -0,0 +1,258 @@
1
+ import {
2
+ type ConfigMappingsType,
3
+ booleanConfigHelper,
4
+ getConfigFromMappings,
5
+ getDefaultConfig,
6
+ numberConfigHelper,
7
+ optionalNumberConfigHelper,
8
+ } from '@aztec/foundation/config';
9
+ import { Fr } from '@aztec/foundation/fields';
10
+ import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree';
11
+ import { protocolContractTreeRoot } from '@aztec/protocol-contracts';
12
+ import { type ZodFor, schemas } from '@aztec/stdlib/schemas';
13
+ import type { ComponentsVersions } from '@aztec/stdlib/versioning';
14
+
15
+ import { z } from 'zod';
16
+
17
+ const BotFollowChain = ['NONE', 'PENDING', 'PROVEN'] as const;
18
+ type BotFollowChain = (typeof BotFollowChain)[number];
19
+
20
+ export enum SupportedTokenContracts {
21
+ TokenContract = 'TokenContract',
22
+ EasyPrivateTokenContract = 'EasyPrivateTokenContract',
23
+ }
24
+
25
+ export type BotConfig = {
26
+ /** The URL to the Aztec node to check for tx pool status. */
27
+ nodeUrl: string | undefined;
28
+ /** URL to the PXE for sending txs, or undefined if an in-proc PXE is used. */
29
+ pxeUrl: string | undefined;
30
+ /** Url of the ethereum host. */
31
+ l1RpcUrls: string[] | undefined;
32
+ /** The mnemonic for the account to bridge fee juice from L1. */
33
+ l1Mnemonic: string | undefined;
34
+ /** The private key for the account to bridge fee juice from L1. */
35
+ l1PrivateKey: string | undefined;
36
+ /** Signing private key for the sender account. */
37
+ senderPrivateKey: Fr | undefined;
38
+ /** Encryption secret for a recipient account. */
39
+ recipientEncryptionSecret: Fr;
40
+ /** Salt for the token contract deployment. */
41
+ tokenSalt: Fr;
42
+ /** Every how many seconds should a new tx be sent. */
43
+ txIntervalSeconds: number;
44
+ /** How many private token transfers are executed per tx. */
45
+ privateTransfersPerTx: number;
46
+ /** How many public token transfers are executed per tx. */
47
+ publicTransfersPerTx: number;
48
+ /** How to handle fee payments. */
49
+ feePaymentMethod: 'fee_juice';
50
+ /** True to not automatically setup or start the bot on initialization. */
51
+ noStart: boolean;
52
+ /** How long to wait for a tx to be mined before reporting an error. */
53
+ txMinedWaitSeconds: number;
54
+ /** Whether to wait for txs to be proven, to be mined, or no wait at all. */
55
+ followChain: BotFollowChain;
56
+ /** Do not send a tx if the node's tx pool already has this many pending txs. */
57
+ maxPendingTxs: number;
58
+ /** Whether to flush after sending each 'setup' transaction */
59
+ flushSetupTransactions: boolean;
60
+ /** Whether to skip public simulation of txs before sending them. */
61
+ skipPublicSimulation: boolean;
62
+ /** L2 gas limit for the tx (empty to have the bot trigger an estimate gas). */
63
+ l2GasLimit: number | undefined;
64
+ /** DA gas limit for the tx (empty to have the bot trigger an estimate gas). */
65
+ daGasLimit: number | undefined;
66
+ /** Token contract to use */
67
+ contract: SupportedTokenContracts;
68
+ /** The maximum number of consecutive errors before the bot shuts down */
69
+ maxConsecutiveErrors: number;
70
+ /** Stops the bot if service becomes unhealthy */
71
+ stopWhenUnhealthy: boolean;
72
+ };
73
+
74
+ export const BotConfigSchema = z
75
+ .object({
76
+ nodeUrl: z.string().optional(),
77
+ pxeUrl: z.string().optional(),
78
+ l1RpcUrls: z.array(z.string()).optional(),
79
+ l1Mnemonic: z.string().optional(),
80
+ l1PrivateKey: z.string().optional(),
81
+ senderPrivateKey: schemas.Fr.optional(),
82
+ recipientEncryptionSecret: schemas.Fr,
83
+ tokenSalt: schemas.Fr,
84
+ txIntervalSeconds: z.number(),
85
+ privateTransfersPerTx: z.number().int().nonnegative(),
86
+ publicTransfersPerTx: z.number().int().nonnegative(),
87
+ feePaymentMethod: z.literal('fee_juice'),
88
+ noStart: z.boolean(),
89
+ txMinedWaitSeconds: z.number(),
90
+ followChain: z.enum(BotFollowChain),
91
+ maxPendingTxs: z.number().int().nonnegative(),
92
+ flushSetupTransactions: z.boolean(),
93
+ skipPublicSimulation: z.boolean(),
94
+ l2GasLimit: z.number().int().nonnegative().optional(),
95
+ daGasLimit: z.number().int().nonnegative().optional(),
96
+ contract: z.nativeEnum(SupportedTokenContracts),
97
+ maxConsecutiveErrors: z.number().int().nonnegative(),
98
+ stopWhenUnhealthy: z.boolean(),
99
+ })
100
+ .transform(config => ({
101
+ nodeUrl: undefined,
102
+ pxeUrl: undefined,
103
+ l1RpcUrls: undefined,
104
+ l1Mnemonic: undefined,
105
+ l1PrivateKey: undefined,
106
+ senderPrivateKey: undefined,
107
+ l2GasLimit: undefined,
108
+ daGasLimit: undefined,
109
+ ...config,
110
+ })) satisfies ZodFor<BotConfig>;
111
+
112
+ export const botConfigMappings: ConfigMappingsType<BotConfig> = {
113
+ nodeUrl: {
114
+ env: 'AZTEC_NODE_URL',
115
+ description: 'The URL to the Aztec node to check for tx pool status.',
116
+ },
117
+ pxeUrl: {
118
+ env: 'BOT_PXE_URL',
119
+ description: 'URL to the PXE for sending txs, or undefined if an in-proc PXE is used.',
120
+ },
121
+ l1RpcUrls: {
122
+ env: 'ETHEREUM_HOSTS',
123
+ description: 'URL of the ethereum host.',
124
+ parseEnv: (val: string) => val.split(',').map(url => url.trim()),
125
+ },
126
+ l1Mnemonic: {
127
+ env: 'BOT_L1_MNEMONIC',
128
+ description: 'The mnemonic for the account to bridge fee juice from L1.',
129
+ },
130
+ l1PrivateKey: {
131
+ env: 'BOT_L1_PRIVATE_KEY',
132
+ description: 'The private key for the account to bridge fee juice from L1.',
133
+ },
134
+ senderPrivateKey: {
135
+ env: 'BOT_PRIVATE_KEY',
136
+ description: 'Signing private key for the sender account.',
137
+ parseEnv: (val: string) => (val ? Fr.fromHexString(val) : undefined),
138
+ },
139
+ recipientEncryptionSecret: {
140
+ env: 'BOT_RECIPIENT_ENCRYPTION_SECRET',
141
+ description: 'Encryption secret for a recipient account.',
142
+ parseEnv: (val: string) => Fr.fromHexString(val),
143
+ defaultValue: Fr.fromHexString('0xcafecafe'),
144
+ },
145
+ tokenSalt: {
146
+ env: 'BOT_TOKEN_SALT',
147
+ description: 'Salt for the token contract deployment.',
148
+ parseEnv: (val: string) => Fr.fromHexString(val),
149
+ defaultValue: Fr.fromHexString('1'),
150
+ },
151
+ txIntervalSeconds: {
152
+ env: 'BOT_TX_INTERVAL_SECONDS',
153
+ description: 'Every how many seconds should a new tx be sent.',
154
+ ...numberConfigHelper(60),
155
+ },
156
+ privateTransfersPerTx: {
157
+ env: 'BOT_PRIVATE_TRANSFERS_PER_TX',
158
+ description: 'How many private token transfers are executed per tx.',
159
+ ...numberConfigHelper(1),
160
+ },
161
+ publicTransfersPerTx: {
162
+ env: 'BOT_PUBLIC_TRANSFERS_PER_TX',
163
+ description: 'How many public token transfers are executed per tx.',
164
+ ...numberConfigHelper(1),
165
+ },
166
+ feePaymentMethod: {
167
+ env: 'BOT_FEE_PAYMENT_METHOD',
168
+ description: 'How to handle fee payments. (Options: fee_juice)',
169
+ parseEnv: val => (val as 'fee_juice') || undefined,
170
+ defaultValue: 'fee_juice',
171
+ },
172
+ noStart: {
173
+ env: 'BOT_NO_START',
174
+ description: 'True to not automatically setup or start the bot on initialization.',
175
+ ...booleanConfigHelper(),
176
+ },
177
+ txMinedWaitSeconds: {
178
+ env: 'BOT_TX_MINED_WAIT_SECONDS',
179
+ description: 'How long to wait for a tx to be mined before reporting an error.',
180
+ ...numberConfigHelper(180),
181
+ },
182
+ followChain: {
183
+ env: 'BOT_FOLLOW_CHAIN',
184
+ description: 'Which chain the bot follows',
185
+ defaultValue: 'NONE',
186
+ parseEnv(val) {
187
+ if (!(BotFollowChain as readonly string[]).includes(val.toUpperCase())) {
188
+ throw new Error(`Invalid value for BOT_FOLLOW_CHAIN: ${val}`);
189
+ }
190
+ return val as BotFollowChain;
191
+ },
192
+ },
193
+ maxPendingTxs: {
194
+ env: 'BOT_MAX_PENDING_TXS',
195
+ description: "Do not send a tx if the node's tx pool already has this many pending txs.",
196
+ ...numberConfigHelper(128),
197
+ },
198
+ flushSetupTransactions: {
199
+ env: 'BOT_FLUSH_SETUP_TRANSACTIONS',
200
+ description: 'Make a request for the sequencer to build a block after each setup transaction.',
201
+ ...booleanConfigHelper(false),
202
+ },
203
+ skipPublicSimulation: {
204
+ env: 'BOT_SKIP_PUBLIC_SIMULATION',
205
+ description: 'Whether to skip public simulation of txs before sending them.',
206
+ ...booleanConfigHelper(false),
207
+ },
208
+ l2GasLimit: {
209
+ env: 'BOT_L2_GAS_LIMIT',
210
+ description: 'L2 gas limit for the tx (empty to have the bot trigger an estimate gas).',
211
+ ...optionalNumberConfigHelper(),
212
+ },
213
+ daGasLimit: {
214
+ env: 'BOT_DA_GAS_LIMIT',
215
+ description: 'DA gas limit for the tx (empty to have the bot trigger an estimate gas).',
216
+ ...optionalNumberConfigHelper(),
217
+ },
218
+ contract: {
219
+ env: 'BOT_TOKEN_CONTRACT',
220
+ description: 'Token contract to use',
221
+ defaultValue: SupportedTokenContracts.TokenContract,
222
+ parseEnv(val) {
223
+ if (!Object.values(SupportedTokenContracts).includes(val as any)) {
224
+ throw new Error(
225
+ `Invalid value for BOT_TOKEN_CONTRACT: ${val}. Valid values: ${Object.values(SupportedTokenContracts).join(
226
+ ', ',
227
+ )}`,
228
+ );
229
+ }
230
+ return val as SupportedTokenContracts;
231
+ },
232
+ },
233
+ maxConsecutiveErrors: {
234
+ env: 'BOT_MAX_CONSECUTIVE_ERRORS',
235
+ description: 'The maximum number of consecutive errors before the bot shuts down',
236
+ ...numberConfigHelper(0),
237
+ },
238
+ stopWhenUnhealthy: {
239
+ env: 'BOT_STOP_WHEN_UNHEALTHY',
240
+ description: 'Stops the bot if service becomes unhealthy',
241
+ ...booleanConfigHelper(false),
242
+ },
243
+ };
244
+
245
+ export function getBotConfigFromEnv(): BotConfig {
246
+ return getConfigFromMappings<BotConfig>(botConfigMappings);
247
+ }
248
+
249
+ export function getBotDefaultConfig(): BotConfig {
250
+ return getDefaultConfig<BotConfig>(botConfigMappings);
251
+ }
252
+
253
+ export function getVersions(): Partial<ComponentsVersions> {
254
+ return {
255
+ l2ProtocolContractsTreeRoot: protocolContractTreeRoot.toString(),
256
+ l2CircuitsVkTreeRoot: getVKTreeRoot().toString(),
257
+ };
258
+ }
package/src/factory.ts ADDED
@@ -0,0 +1,259 @@
1
+ import { getSchnorrAccount } from '@aztec/accounts/schnorr';
2
+ import { getDeployedTestAccountsWallets, getInitialTestAccounts } from '@aztec/accounts/testing';
3
+ import {
4
+ type AccountWallet,
5
+ AztecAddress,
6
+ type AztecNode,
7
+ BatchCall,
8
+ type DeployMethod,
9
+ type DeployOptions,
10
+ FeeJuicePaymentMethodWithClaim,
11
+ L1FeeJuicePortalManager,
12
+ type PXE,
13
+ createLogger,
14
+ createPXEClient,
15
+ retryUntil,
16
+ } from '@aztec/aztec.js';
17
+ import { createEthereumChain, createL1Clients } from '@aztec/ethereum';
18
+ import { Fr } from '@aztec/foundation/fields';
19
+ import { EasyPrivateTokenContract } from '@aztec/noir-contracts.js/EasyPrivateToken';
20
+ import { TokenContract } from '@aztec/noir-contracts.js/Token';
21
+ import type { FunctionCall } from '@aztec/stdlib/abi';
22
+ import { deriveSigningKey } from '@aztec/stdlib/keys';
23
+ import { makeTracedFetch } from '@aztec/telemetry-client';
24
+
25
+ import { type BotConfig, SupportedTokenContracts, getVersions } from './config.js';
26
+ import { getBalances, getPrivateBalance, isStandardTokenContract } from './utils.js';
27
+
28
+ const MINT_BALANCE = 1e12;
29
+ const MIN_BALANCE = 1e3;
30
+
31
+ export class BotFactory {
32
+ private pxe: PXE;
33
+ private node?: AztecNode;
34
+ private log = createLogger('bot');
35
+
36
+ constructor(private readonly config: BotConfig, dependencies: { pxe?: PXE; node?: AztecNode } = {}) {
37
+ if (config.flushSetupTransactions && !dependencies.node) {
38
+ throw new Error(`Either a node client or node url must be provided if transaction flushing is requested`);
39
+ }
40
+ if (config.senderPrivateKey && !dependencies.node) {
41
+ throw new Error(
42
+ `Either a node client or node url must be provided for bridging L1 fee juice to deploy an account with private key`,
43
+ );
44
+ }
45
+ if (!dependencies.pxe && !config.pxeUrl) {
46
+ throw new Error(`Either a PXE client or a PXE URL must be provided`);
47
+ }
48
+
49
+ this.node = dependencies.node;
50
+
51
+ if (dependencies.pxe) {
52
+ this.log.info(`Using local PXE`);
53
+ this.pxe = dependencies.pxe;
54
+ return;
55
+ }
56
+ this.log.info(`Using remote PXE at ${config.pxeUrl!}`);
57
+ this.pxe = createPXEClient(config.pxeUrl!, getVersions(), makeTracedFetch([1, 2, 3], false));
58
+ }
59
+
60
+ /**
61
+ * Initializes a new bot by setting up the sender account, registering the recipient,
62
+ * deploying the token contract, and minting tokens if necessary.
63
+ */
64
+ public async setup() {
65
+ const recipient = await this.registerRecipient();
66
+ const wallet = await this.setupAccount();
67
+ const token = await this.setupToken(wallet);
68
+ await this.mintTokens(token);
69
+ return { wallet, token, pxe: this.pxe, recipient };
70
+ }
71
+
72
+ /**
73
+ * Checks if the sender account contract is initialized, and initializes it if necessary.
74
+ * @returns The sender wallet.
75
+ */
76
+ private async setupAccount() {
77
+ if (this.config.senderPrivateKey) {
78
+ return await this.setupAccountWithPrivateKey(this.config.senderPrivateKey);
79
+ } else {
80
+ return await this.setupTestAccount();
81
+ }
82
+ }
83
+
84
+ private async setupAccountWithPrivateKey(privateKey: Fr) {
85
+ const salt = Fr.ONE;
86
+ const signingKey = deriveSigningKey(privateKey);
87
+ const account = await getSchnorrAccount(this.pxe, privateKey, signingKey, salt);
88
+ const isInit = (await this.pxe.getContractMetadata(account.getAddress())).isContractInitialized;
89
+ if (isInit) {
90
+ this.log.info(`Account at ${account.getAddress().toString()} already initialized`);
91
+ const wallet = await account.register();
92
+ return wallet;
93
+ } else {
94
+ const address = account.getAddress();
95
+ this.log.info(`Deploying account at ${address}`);
96
+
97
+ const claim = await this.bridgeL1FeeJuice(address, 10n ** 22n);
98
+
99
+ const wallet = await account.getWallet();
100
+ const paymentMethod = new FeeJuicePaymentMethodWithClaim(wallet, claim);
101
+ const sentTx = account.deploy({ fee: { paymentMethod } });
102
+ const txHash = await sentTx.getTxHash();
103
+ this.log.info(`Sent tx with hash ${txHash.toString()}`);
104
+ await this.tryFlushTxs();
105
+ this.log.verbose('Waiting for account deployment to settle');
106
+ await sentTx.wait({ timeout: this.config.txMinedWaitSeconds });
107
+ this.log.info(`Account deployed at ${address}`);
108
+ return wallet;
109
+ }
110
+ }
111
+
112
+ private async setupTestAccount() {
113
+ let [wallet] = await getDeployedTestAccountsWallets(this.pxe);
114
+ if (wallet) {
115
+ this.log.info(`Using funded test account: ${wallet.getAddress()}`);
116
+ } else {
117
+ this.log.info('Registering funded test account');
118
+ const [account] = await getInitialTestAccounts();
119
+ const manager = await getSchnorrAccount(this.pxe, account.secret, account.signingKey, account.salt);
120
+ wallet = await manager.register();
121
+ this.log.info(`Funded test account registered: ${wallet.getAddress()}`);
122
+ }
123
+ return wallet;
124
+ }
125
+
126
+ /**
127
+ * Registers the recipient for txs in the pxe.
128
+ */
129
+ private async registerRecipient() {
130
+ const recipient = await this.pxe.registerAccount(this.config.recipientEncryptionSecret, Fr.ONE);
131
+ return recipient.address;
132
+ }
133
+
134
+ /**
135
+ * Checks if the token contract is deployed and deploys it if necessary.
136
+ * @param wallet - Wallet to deploy the token contract from.
137
+ * @returns The TokenContract instance.
138
+ */
139
+ private async setupToken(wallet: AccountWallet): Promise<TokenContract | EasyPrivateTokenContract> {
140
+ let deploy: DeployMethod<TokenContract | EasyPrivateTokenContract>;
141
+ const deployOpts: DeployOptions = { contractAddressSalt: this.config.tokenSalt, universalDeploy: true };
142
+ if (this.config.contract === SupportedTokenContracts.TokenContract) {
143
+ deploy = TokenContract.deploy(wallet, wallet.getAddress(), 'BotToken', 'BOT', 18);
144
+ } else if (this.config.contract === SupportedTokenContracts.EasyPrivateTokenContract) {
145
+ deploy = EasyPrivateTokenContract.deploy(wallet, MINT_BALANCE, wallet.getAddress());
146
+ deployOpts.skipPublicDeployment = true;
147
+ deployOpts.skipClassRegistration = true;
148
+ deployOpts.skipInitialization = false;
149
+ deployOpts.skipPublicSimulation = true;
150
+ } else {
151
+ throw new Error(`Unsupported token contract type: ${this.config.contract}`);
152
+ }
153
+
154
+ const address = (await deploy.getInstance(deployOpts)).address;
155
+ if ((await this.pxe.getContractMetadata(address)).isContractPubliclyDeployed) {
156
+ this.log.info(`Token at ${address.toString()} already deployed`);
157
+ return deploy.register();
158
+ } else {
159
+ this.log.info(`Deploying token contract at ${address.toString()}`);
160
+ const sentTx = deploy.send(deployOpts);
161
+ const txHash = await sentTx.getTxHash();
162
+ this.log.info(`Sent tx with hash ${txHash.toString()}`);
163
+ await this.tryFlushTxs();
164
+ this.log.verbose('Waiting for token setup to settle');
165
+ return sentTx.deployed({ timeout: this.config.txMinedWaitSeconds });
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Mints private and public tokens for the sender if their balance is below the minimum.
171
+ * @param token - Token contract.
172
+ */
173
+ private async mintTokens(token: TokenContract | EasyPrivateTokenContract) {
174
+ const sender = token.wallet.getAddress();
175
+ const isStandardToken = isStandardTokenContract(token);
176
+ let privateBalance = 0n;
177
+ let publicBalance = 0n;
178
+
179
+ if (isStandardToken) {
180
+ ({ privateBalance, publicBalance } = await getBalances(token, sender));
181
+ } else {
182
+ privateBalance = await getPrivateBalance(token, sender);
183
+ }
184
+
185
+ const calls: FunctionCall[] = [];
186
+ if (privateBalance < MIN_BALANCE) {
187
+ this.log.info(`Minting private tokens for ${sender.toString()}`);
188
+
189
+ const from = sender; // we are setting from to sender here because we need a sender to calculate the tag
190
+ calls.push(
191
+ isStandardToken
192
+ ? await token.methods.mint_to_private(from, sender, MINT_BALANCE).request()
193
+ : await token.methods.mint(MINT_BALANCE, sender).request(),
194
+ );
195
+ }
196
+ if (isStandardToken && publicBalance < MIN_BALANCE) {
197
+ this.log.info(`Minting public tokens for ${sender.toString()}`);
198
+ calls.push(await token.methods.mint_to_public(sender, MINT_BALANCE).request());
199
+ }
200
+ if (calls.length === 0) {
201
+ this.log.info(`Skipping minting as ${sender.toString()} has enough tokens`);
202
+ return;
203
+ }
204
+ const sentTx = new BatchCall(token.wallet, calls).send();
205
+ const txHash = await sentTx.getTxHash();
206
+ this.log.info(`Sent tx with hash ${txHash.toString()}`);
207
+ await this.tryFlushTxs();
208
+ this.log.verbose('Waiting for token mint to settle');
209
+ await sentTx.wait({ timeout: this.config.txMinedWaitSeconds });
210
+ }
211
+
212
+ private async bridgeL1FeeJuice(recipient: AztecAddress, amount: bigint) {
213
+ const l1RpcUrls = this.config.l1RpcUrls;
214
+ if (!l1RpcUrls?.length) {
215
+ throw new Error('L1 Rpc url is required to bridge the fee juice to fund the deployment of the account.');
216
+ }
217
+ const mnemonicOrPrivateKey = this.config.l1PrivateKey || this.config.l1Mnemonic;
218
+ if (!mnemonicOrPrivateKey) {
219
+ throw new Error(
220
+ 'Either a mnemonic or private key of an L1 account is required to bridge the fee juice to fund the deployment of the account.',
221
+ );
222
+ }
223
+
224
+ const { l1ChainId } = await this.pxe.getNodeInfo();
225
+ const chain = createEthereumChain(l1RpcUrls, l1ChainId);
226
+ const { publicClient, walletClient } = createL1Clients(chain.rpcUrls, mnemonicOrPrivateKey, chain.chainInfo);
227
+
228
+ const portal = await L1FeeJuicePortalManager.new(this.pxe, publicClient, walletClient, this.log);
229
+ const claim = await portal.bridgeTokensPublic(recipient, amount, true /* mint */);
230
+
231
+ const isSynced = async () => await this.pxe.isL1ToL2MessageSynced(Fr.fromHexString(claim.messageHash));
232
+ await retryUntil(isSynced, `message ${claim.messageHash} sync`, 24, 1);
233
+
234
+ this.log.info(`Created a claim for ${amount} L1 fee juice to ${recipient}.`, claim);
235
+
236
+ // Progress by 2 L2 blocks so that the l1ToL2Message added above will be available to use on L2.
237
+ await this.advanceL2Block();
238
+ await this.advanceL2Block();
239
+
240
+ return claim;
241
+ }
242
+
243
+ private async advanceL2Block() {
244
+ const initialBlockNumber = await this.node!.getBlockNumber();
245
+ await this.tryFlushTxs();
246
+ await retryUntil(async () => (await this.node!.getBlockNumber()) >= initialBlockNumber + 1);
247
+ }
248
+
249
+ private async tryFlushTxs() {
250
+ if (this.config.flushSetupTransactions) {
251
+ this.log.verbose('Flushing transactions');
252
+ try {
253
+ await this.node!.flushTxs();
254
+ } catch (err) {
255
+ this.log.error(`Failed to flush transactions: ${err}`);
256
+ }
257
+ }
258
+ }
259
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { Bot } from './bot.js';
2
+ export { BotRunner } from './runner.js';
3
+ export {
4
+ type BotConfig,
5
+ getBotConfigFromEnv,
6
+ getBotDefaultConfig,
7
+ botConfigMappings,
8
+ SupportedTokenContracts,
9
+ } from './config.js';
10
+ export { createBotRunnerRpcServer, getBotRunnerApiHandler } from './rpc.js';
11
+ export * from './interface.js';