@aztec/bot 0.81.0 → 0.82.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/config.ts CHANGED
@@ -25,6 +25,8 @@ export enum SupportedTokenContracts {
25
25
  export type BotConfig = {
26
26
  /** The URL to the Aztec node to check for tx pool status. */
27
27
  nodeUrl: string | undefined;
28
+ /** The URL to the Aztec node admin API to force-flush txs if configured. */
29
+ nodeAdminUrl: string | undefined;
28
30
  /** URL to the PXE for sending txs, or undefined if an in-proc PXE is used. */
29
31
  pxeUrl: string | undefined;
30
32
  /** Url of the ethereum host. */
@@ -35,6 +37,8 @@ export type BotConfig = {
35
37
  l1PrivateKey: string | undefined;
36
38
  /** Signing private key for the sender account. */
37
39
  senderPrivateKey: Fr | undefined;
40
+ /** Optional salt to use to deploy the sender account */
41
+ senderSalt: Fr | undefined;
38
42
  /** Encryption secret for a recipient account. */
39
43
  recipientEncryptionSecret: Fr;
40
44
  /** Salt for the token contract deployment. */
@@ -69,16 +73,20 @@ export type BotConfig = {
69
73
  maxConsecutiveErrors: number;
70
74
  /** Stops the bot if service becomes unhealthy */
71
75
  stopWhenUnhealthy: boolean;
76
+ /** Deploy an AMM contract and do swaps instead of transfers */
77
+ ammTxs: boolean;
72
78
  };
73
79
 
74
80
  export const BotConfigSchema = z
75
81
  .object({
76
82
  nodeUrl: z.string().optional(),
83
+ nodeAdminUrl: z.string().optional(),
77
84
  pxeUrl: z.string().optional(),
78
85
  l1RpcUrls: z.array(z.string()).optional(),
79
86
  l1Mnemonic: z.string().optional(),
80
87
  l1PrivateKey: z.string().optional(),
81
88
  senderPrivateKey: schemas.Fr.optional(),
89
+ senderSalt: schemas.Fr.optional(),
82
90
  recipientEncryptionSecret: schemas.Fr,
83
91
  tokenSalt: schemas.Fr,
84
92
  txIntervalSeconds: z.number(),
@@ -96,14 +104,17 @@ export const BotConfigSchema = z
96
104
  contract: z.nativeEnum(SupportedTokenContracts),
97
105
  maxConsecutiveErrors: z.number().int().nonnegative(),
98
106
  stopWhenUnhealthy: z.boolean(),
107
+ ammTxs: z.boolean().default(false),
99
108
  })
100
109
  .transform(config => ({
101
110
  nodeUrl: undefined,
111
+ nodeAdminUrl: undefined,
102
112
  pxeUrl: undefined,
103
113
  l1RpcUrls: undefined,
104
114
  l1Mnemonic: undefined,
105
115
  l1PrivateKey: undefined,
106
116
  senderPrivateKey: undefined,
117
+ senderSalt: undefined,
107
118
  l2GasLimit: undefined,
108
119
  daGasLimit: undefined,
109
120
  ...config,
@@ -114,6 +125,10 @@ export const botConfigMappings: ConfigMappingsType<BotConfig> = {
114
125
  env: 'AZTEC_NODE_URL',
115
126
  description: 'The URL to the Aztec node to check for tx pool status.',
116
127
  },
128
+ nodeAdminUrl: {
129
+ env: 'AZTEC_NODE_ADMIN_URL',
130
+ description: 'The URL to the Aztec node admin API to force-flush txs if configured.',
131
+ },
117
132
  pxeUrl: {
118
133
  env: 'BOT_PXE_URL',
119
134
  description: 'URL to the PXE for sending txs, or undefined if an in-proc PXE is used.',
@@ -136,6 +151,11 @@ export const botConfigMappings: ConfigMappingsType<BotConfig> = {
136
151
  description: 'Signing private key for the sender account.',
137
152
  parseEnv: (val: string) => (val ? Fr.fromHexString(val) : undefined),
138
153
  },
154
+ senderSalt: {
155
+ env: 'BOT_ACCOUNT_SALT',
156
+ description: 'The salt to use to deploys the sender account.',
157
+ parseEnv: (val: string) => (val ? Fr.fromHexString(val) : undefined),
158
+ },
139
159
  recipientEncryptionSecret: {
140
160
  env: 'BOT_RECIPIENT_ENCRYPTION_SECRET',
141
161
  description: 'Encryption secret for a recipient account.',
@@ -240,6 +260,11 @@ export const botConfigMappings: ConfigMappingsType<BotConfig> = {
240
260
  description: 'Stops the bot if service becomes unhealthy',
241
261
  ...booleanConfigHelper(false),
242
262
  },
263
+ ammTxs: {
264
+ env: 'BOT_AMM_TXS',
265
+ description: 'Deploy an AMM and send swaps to it',
266
+ ...booleanConfigHelper(false),
267
+ },
243
268
  };
244
269
 
245
270
  export function getBotConfigFromEnv(): BotConfig {
package/src/factory.ts CHANGED
@@ -3,8 +3,8 @@ import { getDeployedTestAccountsWallets, getInitialTestAccounts } from '@aztec/a
3
3
  import {
4
4
  type AccountWallet,
5
5
  AztecAddress,
6
- type AztecNode,
7
6
  BatchCall,
7
+ ContractBase,
8
8
  ContractFunctionInteraction,
9
9
  type DeployMethod,
10
10
  type DeployOptions,
@@ -17,8 +17,10 @@ import {
17
17
  } from '@aztec/aztec.js';
18
18
  import { createEthereumChain, createL1Clients } from '@aztec/ethereum';
19
19
  import { Fr } from '@aztec/foundation/fields';
20
+ import { AMMContract } from '@aztec/noir-contracts.js/AMM';
20
21
  import { EasyPrivateTokenContract } from '@aztec/noir-contracts.js/EasyPrivateToken';
21
22
  import { TokenContract } from '@aztec/noir-contracts.js/Token';
23
+ import type { AztecNode, AztecNodeAdmin } from '@aztec/stdlib/interfaces/client';
22
24
  import { deriveSigningKey } from '@aztec/stdlib/keys';
23
25
  import { makeTracedFetch } from '@aztec/telemetry-client';
24
26
 
@@ -31,11 +33,17 @@ const MIN_BALANCE = 1e3;
31
33
  export class BotFactory {
32
34
  private pxe: PXE;
33
35
  private node?: AztecNode;
36
+ private nodeAdmin?: AztecNodeAdmin;
34
37
  private log = createLogger('bot');
35
38
 
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
+ constructor(
40
+ private readonly config: BotConfig,
41
+ dependencies: { pxe?: PXE; nodeAdmin?: AztecNodeAdmin; node?: AztecNode },
42
+ ) {
43
+ if (config.flushSetupTransactions && !dependencies.nodeAdmin) {
44
+ throw new Error(
45
+ `Either a node admin client or node admin url must be provided if transaction flushing is requested`,
46
+ );
39
47
  }
40
48
  if (config.senderPrivateKey && !dependencies.node) {
41
49
  throw new Error(
@@ -47,6 +55,7 @@ export class BotFactory {
47
55
  }
48
56
 
49
57
  this.node = dependencies.node;
58
+ this.nodeAdmin = dependencies.nodeAdmin;
50
59
 
51
60
  if (dependencies.pxe) {
52
61
  this.log.info(`Using local PXE`);
@@ -69,6 +78,19 @@ export class BotFactory {
69
78
  return { wallet, token, pxe: this.pxe, recipient };
70
79
  }
71
80
 
81
+ public async setupAmm() {
82
+ const wallet = await this.setupAccount();
83
+ const token0 = await this.setupTokenContract(wallet, this.config.tokenSalt, 'BotToken0', 'BOT0');
84
+ const token1 = await this.setupTokenContract(wallet, this.config.tokenSalt, 'BotToken1', 'BOT1');
85
+ const liquidityToken = await this.setupTokenContract(wallet, this.config.tokenSalt, 'BotLPToken', 'BOTLP');
86
+ const amm = await this.setupAmmContract(wallet, this.config.tokenSalt, token0, token1, liquidityToken);
87
+
88
+ await this.fundAmm(wallet, amm, token0, token1);
89
+ this.log.info(`AMM initialized and funded`);
90
+
91
+ return { wallet, amm, token0, token1, pxe: this.pxe };
92
+ }
93
+
72
94
  /**
73
95
  * Checks if the sender account contract is initialized, and initializes it if necessary.
74
96
  * @returns The sender wallet.
@@ -82,7 +104,7 @@ export class BotFactory {
82
104
  }
83
105
 
84
106
  private async setupAccountWithPrivateKey(privateKey: Fr) {
85
- const salt = Fr.ONE;
107
+ const salt = this.config.senderSalt ?? Fr.ONE;
86
108
  const signingKey = deriveSigningKey(privateKey);
87
109
  const account = await getSchnorrAccount(this.pxe, privateKey, signingKey, salt);
88
110
  const isInit = (await this.pxe.getContractMetadata(account.getAddress())).isContractInitialized;
@@ -94,12 +116,14 @@ export class BotFactory {
94
116
  const address = account.getAddress();
95
117
  this.log.info(`Deploying account at ${address}`);
96
118
 
97
- const claim = await this.bridgeL1FeeJuice(address, 10n ** 22n);
119
+ const claim = await this.bridgeL1FeeJuice(address);
98
120
 
121
+ // docs:start:claim_and_deploy
99
122
  const wallet = await account.getWallet();
100
123
  const paymentMethod = new FeeJuicePaymentMethodWithClaim(wallet, claim);
101
124
  const sentTx = account.deploy({ fee: { paymentMethod } });
102
125
  const txHash = await sentTx.getTxHash();
126
+ // docs:end:claim_and_deploy
103
127
  this.log.info(`Sent tx with hash ${txHash.toString()}`);
104
128
  await this.tryFlushTxs();
105
129
  this.log.verbose('Waiting for account deployment to settle');
@@ -166,6 +190,104 @@ export class BotFactory {
166
190
  }
167
191
  }
168
192
 
193
+ /**
194
+ * Checks if the token contract is deployed and deploys it if necessary.
195
+ * @param wallet - Wallet to deploy the token contract from.
196
+ * @returns The TokenContract instance.
197
+ */
198
+ private setupTokenContract(
199
+ wallet: AccountWallet,
200
+ contractAddressSalt: Fr,
201
+ name: string,
202
+ ticker: string,
203
+ decimals = 18,
204
+ ): Promise<TokenContract> {
205
+ const deployOpts: DeployOptions = { contractAddressSalt, universalDeploy: true };
206
+ const deploy = TokenContract.deploy(wallet, wallet.getAddress(), name, ticker, decimals);
207
+ return this.registerOrDeployContract('Token - ' + name, deploy, deployOpts);
208
+ }
209
+
210
+ private async setupAmmContract(
211
+ wallet: AccountWallet,
212
+ contractAddressSalt: Fr,
213
+ token0: TokenContract,
214
+ token1: TokenContract,
215
+ lpToken: TokenContract,
216
+ ): Promise<AMMContract> {
217
+ const deployOpts: DeployOptions = { contractAddressSalt, universalDeploy: true };
218
+ const deploy = AMMContract.deploy(wallet, token0.address, token1.address, lpToken.address);
219
+ const amm = await this.registerOrDeployContract('AMM', deploy, deployOpts);
220
+
221
+ this.log.info(`AMM deployed at ${amm.address}`);
222
+ const minterTx = lpToken.methods.set_minter(amm.address, true).send();
223
+ this.log.info(`Set LP token minter to AMM txHash=${await minterTx.getTxHash()}`);
224
+ await minterTx.wait({ timeout: this.config.txMinedWaitSeconds });
225
+ this.log.info(`Liquidity token initialized`);
226
+
227
+ return amm;
228
+ }
229
+
230
+ private async fundAmm(
231
+ wallet: AccountWallet,
232
+ amm: AMMContract,
233
+ token0: TokenContract,
234
+ token1: TokenContract,
235
+ ): Promise<void> {
236
+ const nonce = Fr.random();
237
+
238
+ // keep some tokens for swapping
239
+ const amount0Max = MINT_BALANCE / 2;
240
+ const amount0Min = MINT_BALANCE / 4;
241
+ const amount1Max = MINT_BALANCE / 2;
242
+ const amount1Min = MINT_BALANCE / 4;
243
+
244
+ const token0Authwit = await wallet.createAuthWit({
245
+ caller: amm.address,
246
+ action: token0.methods.transfer_to_public(wallet.getAddress(), amm.address, amount0Max, nonce),
247
+ });
248
+ const token1Authwit = await wallet.createAuthWit({
249
+ caller: amm.address,
250
+ action: token1.methods.transfer_to_public(wallet.getAddress(), amm.address, amount1Max, nonce),
251
+ });
252
+
253
+ this.log.info(`Minting tokens`);
254
+ const mintTx = new BatchCall(wallet, [
255
+ token0.methods.mint_to_private(wallet.getAddress(), wallet.getAddress(), MINT_BALANCE),
256
+ token1.methods.mint_to_private(wallet.getAddress(), wallet.getAddress(), MINT_BALANCE),
257
+ ]).send();
258
+
259
+ this.log.info(`Sent mint tx: ${await mintTx.getTxHash()}`);
260
+ await mintTx.wait({ timeout: this.config.txMinedWaitSeconds });
261
+
262
+ this.log.info(`Funding AMM`);
263
+ const addLiquidityTx = amm.methods.add_liquidity(amount0Max, amount1Max, amount0Min, amount1Min, nonce).send({
264
+ authWitnesses: [token0Authwit, token1Authwit],
265
+ });
266
+
267
+ this.log.info(`Sent tx to add liquidity to the AMM: ${await addLiquidityTx.getTxHash()}`);
268
+ await addLiquidityTx.wait({ timeout: this.config.txMinedWaitSeconds });
269
+ }
270
+
271
+ private async registerOrDeployContract<T extends ContractBase>(
272
+ name: string,
273
+ deploy: DeployMethod<T>,
274
+ deployOpts: DeployOptions,
275
+ ): Promise<T> {
276
+ const address = (await deploy.getInstance(deployOpts)).address;
277
+ if ((await this.pxe.getContractMetadata(address)).isContractPubliclyDeployed) {
278
+ this.log.info(`Contract ${name} at ${address.toString()} already deployed`);
279
+ return deploy.register();
280
+ } else {
281
+ this.log.info(`Deploying contract ${name} at ${address.toString()}`);
282
+ const sentTx = deploy.send(deployOpts);
283
+ const txHash = await sentTx.getTxHash();
284
+ this.log.info(`Sent tx with hash ${txHash.toString()}`);
285
+ await this.tryFlushTxs();
286
+ this.log.verbose(`Waiting for contract ${name} setup to settle`);
287
+ return sentTx.deployed({ timeout: this.config.txMinedWaitSeconds });
288
+ }
289
+ }
290
+
169
291
  /**
170
292
  * Mints private and public tokens for the sender if their balance is below the minimum.
171
293
  * @param token - Token contract.
@@ -209,7 +331,7 @@ export class BotFactory {
209
331
  await sentTx.wait({ timeout: this.config.txMinedWaitSeconds });
210
332
  }
211
333
 
212
- private async bridgeL1FeeJuice(recipient: AztecAddress, amount: bigint) {
334
+ private async bridgeL1FeeJuice(recipient: AztecAddress) {
213
335
  const l1RpcUrls = this.config.l1RpcUrls;
214
336
  if (!l1RpcUrls?.length) {
215
337
  throw new Error('L1 Rpc url is required to bridge the fee juice to fund the deployment of the account.');
@@ -226,12 +348,13 @@ export class BotFactory {
226
348
  const { publicClient, walletClient } = createL1Clients(chain.rpcUrls, mnemonicOrPrivateKey, chain.chainInfo);
227
349
 
228
350
  const portal = await L1FeeJuicePortalManager.new(this.pxe, publicClient, walletClient, this.log);
229
- const claim = await portal.bridgeTokensPublic(recipient, amount, true /* mint */);
351
+ const mintAmount = await portal.getTokenManager().getMintAmount();
352
+ const claim = await portal.bridgeTokensPublic(recipient, mintAmount, true /* mint */);
230
353
 
231
354
  const isSynced = async () => await this.pxe.isL1ToL2MessageSynced(Fr.fromHexString(claim.messageHash));
232
355
  await retryUntil(isSynced, `message ${claim.messageHash} sync`, 24, 1);
233
356
 
234
- this.log.info(`Created a claim for ${amount} L1 fee juice to ${recipient}.`, claim);
357
+ this.log.info(`Created a claim for ${mintAmount} L1 fee juice to ${recipient}.`, claim);
235
358
 
236
359
  // Progress by 2 L2 blocks so that the l1ToL2Message added above will be available to use on L2.
237
360
  await this.advanceL2Block();
@@ -250,7 +373,7 @@ export class BotFactory {
250
373
  if (this.config.flushSetupTransactions) {
251
374
  this.log.verbose('Flushing transactions');
252
375
  try {
253
- await this.node!.flushTxs();
376
+ await this.nodeAdmin!.flushTxs();
254
377
  } catch (err) {
255
378
  this.log.error(`Failed to flush transactions: ${err}`);
256
379
  }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Bot } from './bot.js';
2
+ export { AmmBot } from './amm_bot.js';
2
3
  export { BotRunner } from './runner.js';
3
4
  export {
4
5
  type BotConfig,
package/src/runner.ts CHANGED
@@ -1,16 +1,20 @@
1
1
  import { type AztecNode, type PXE, createAztecNodeClient, createLogger } from '@aztec/aztec.js';
2
2
  import { RunningPromise } from '@aztec/foundation/running-promise';
3
+ import { type AztecNodeAdmin, createAztecNodeAdminClient } from '@aztec/stdlib/interfaces/client';
3
4
  import { type TelemetryClient, type Traceable, type Tracer, makeTracedFetch, trackSpan } from '@aztec/telemetry-client';
4
5
 
6
+ import { AmmBot } from './amm_bot.js';
7
+ import type { BaseBot } from './base_bot.js';
5
8
  import { Bot } from './bot.js';
6
9
  import { type BotConfig, getVersions } from './config.js';
7
10
  import type { BotRunnerApi } from './interface.js';
8
11
 
9
12
  export class BotRunner implements BotRunnerApi, Traceable {
10
13
  private log = createLogger('bot');
11
- private bot?: Promise<Bot>;
14
+ private bot?: Promise<BaseBot>;
12
15
  private pxe?: PXE;
13
16
  private node: AztecNode;
17
+ private nodeAdmin?: AztecNodeAdmin;
14
18
  private runningPromise: RunningPromise;
15
19
  private consecutiveErrors = 0;
16
20
  private healthy = true;
@@ -19,15 +23,19 @@ export class BotRunner implements BotRunnerApi, Traceable {
19
23
 
20
24
  public constructor(
21
25
  private config: BotConfig,
22
- dependencies: { pxe?: PXE; node?: AztecNode; telemetry: TelemetryClient },
26
+ dependencies: { pxe?: PXE; node?: AztecNode; nodeAdmin?: AztecNodeAdmin; telemetry: TelemetryClient },
23
27
  ) {
24
28
  this.tracer = dependencies.telemetry.getTracer('Bot');
25
29
  this.pxe = dependencies.pxe;
26
30
  if (!dependencies.node && !config.nodeUrl) {
27
31
  throw new Error(`Missing node URL in config or dependencies`);
28
32
  }
29
- this.node =
30
- dependencies.node ?? createAztecNodeClient(config.nodeUrl!, getVersions(), makeTracedFetch([1, 2, 3], true));
33
+ const versions = getVersions();
34
+ const fetch = makeTracedFetch([1, 2, 3], true);
35
+ this.node = dependencies.node ?? createAztecNodeClient(config.nodeUrl!, versions, fetch);
36
+ this.nodeAdmin =
37
+ dependencies.nodeAdmin ??
38
+ (config.nodeAdminUrl ? createAztecNodeAdminClient(config.nodeAdminUrl, versions, fetch) : undefined);
31
39
  this.runningPromise = new RunningPromise(() => this.#work(), this.log, config.txIntervalSeconds * 1000);
32
40
  }
33
41
 
@@ -131,7 +139,9 @@ export class BotRunner implements BotRunnerApi, Traceable {
131
139
 
132
140
  async #createBot() {
133
141
  try {
134
- this.bot = Bot.create(this.config, { pxe: this.pxe, node: this.node });
142
+ this.bot = this.config.ammTxs
143
+ ? AmmBot.create(this.config, { pxe: this.pxe, node: this.node, nodeAdmin: this.nodeAdmin })
144
+ : Bot.create(this.config, { pxe: this.pxe, node: this.node, nodeAdmin: this.nodeAdmin });
135
145
  await this.bot;
136
146
  } catch (err) {
137
147
  this.log.error(`Error setting up bot: ${err}`);
package/src/utils.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { ContractBase } from '@aztec/aztec.js';
2
+ import type { AMMContract } from '@aztec/noir-contracts.js/AMM';
1
3
  import type { EasyPrivateTokenContract } from '@aztec/noir-contracts.js/EasyPrivateToken';
2
4
  import type { TokenContract } from '@aztec/noir-contracts.js/Token';
3
5
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
@@ -22,6 +24,10 @@ export async function getPrivateBalance(token: EasyPrivateTokenContract, who: Az
22
24
  return privateBalance;
23
25
  }
24
26
 
25
- export function isStandardTokenContract(token: TokenContract | EasyPrivateTokenContract): token is TokenContract {
27
+ export function isStandardTokenContract(token: ContractBase): token is TokenContract {
26
28
  return 'mint_to_public' in token.methods;
27
29
  }
30
+
31
+ export function isAMMContract(contract: ContractBase): contract is AMMContract {
32
+ return 'add_liquidity' in contract.methods;
33
+ }