diff --git a/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr index d87e47208e64..d1f8d053c40c 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr @@ -47,6 +47,8 @@ pub unconstrained fn do_process_log( recipient: AztecAddress, compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, ) { + debug_log_format("Processing log with tag {0}", [log.get(0)]); + let log_plaintext = decrypt_log(log, recipient); // The first thing to do after decrypting the log is to determine what type of private log we're processing. We diff --git a/noir-projects/aztec-nr/uint-note/src/uint_note.nr b/noir-projects/aztec-nr/uint-note/src/uint_note.nr index 7f273717b2e2..e0889c1167af 100644 --- a/noir-projects/aztec-nr/uint-note/src/uint_note.nr +++ b/noir-projects/aztec-nr/uint-note/src/uint_note.nr @@ -217,6 +217,13 @@ impl PartialUintNote { impl PartialUintNote { /// Completes the partial note, creating a new note that can be used like any other UintNote. pub fn complete(self, value: u128, context: &mut PublicContext) { + // A note with a value of zero is valid, but we cannot currently complete a partial note with such a value + // because this will result in the completion log having its last field set to 0. Public logs currently do not + // track their length, and so trailing zeros are simply trimmed. This results in the completion log missing its + // last field (the value), and note discovery failing. + // TODO(#11636): remove this + assert(value != 0, "Cannot complete a PartialUintNote with a value of 0"); + // We need to do two things: // - emit a public log containing the public fields (the value). The contract will later find it by searching // for the expected tag (which is simply the partial note commitment). diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/types/nft_note.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/types/nft_note.nr index d5eb63638c89..386b80ba2ab5 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/types/nft_note.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/types/nft_note.nr @@ -218,6 +218,13 @@ impl PartialNFTNote { impl PartialNFTNote { /// Completes the partial note, creating a new note that can be used like any other NFTNote. pub fn complete(self, token_id: Field, context: &mut PublicContext) { + // A note with a value of zero is valid, but we cannot currently complete a partial note with such a value + // because this will result in the completion log having its last field set to 0. Public logs currently do not + // track their length, and so trailing zeros are simply trimmed. This results in the completion log missing its + // last field (the value), and note discovery failing. + // TODO(#11636): remove this + assert(token_id != 0, "Cannot complete a PartialNFTNote with a value of 0"); + // We need to do two things: // - emit a public log containing the public fields (the token id). The contract will later find it by // searching for the expected tag (which is simply the partial note commitment). diff --git a/spartan/aztec-network/templates/transaction-bot.yaml b/spartan/aztec-network/templates/transaction-bot.yaml index 270616cfc906..08097767ee75 100644 --- a/spartan/aztec-network/templates/transaction-bot.yaml +++ b/spartan/aztec-network/templates/transaction-bot.yaml @@ -159,6 +159,8 @@ spec: value: "{{ .Values.bot.botNoStart }}" - name: BOT_FEE_PAYMENT_METHOD value: "{{ .Values.bot.feePaymentMethod }}" + - name: BOT_AMM_TXS + value: "{{ .Values.bot.ammTxs }}" - name: PXE_PROVER_ENABLED value: "{{ .Values.aztec.realProofs }}" - name: PROVER_REAL_PROOFS diff --git a/spartan/aztec-network/values.yaml b/spartan/aztec-network/values.yaml index 15600c5971e6..476bc8f178c3 100644 --- a/spartan/aztec-network/values.yaml +++ b/spartan/aztec-network/values.yaml @@ -235,6 +235,7 @@ bot: followChain: "NONE" botNoStart: false feePaymentMethod: "fee_juice" + ammTxs: false maxErrors: 3 stopIfUnhealthy: true service: diff --git a/spartan/aztec-network/values/rc-1.yaml b/spartan/aztec-network/values/rc-1.yaml index 6a073e35604b..82d8e06cd91b 100644 --- a/spartan/aztec-network/values/rc-1.yaml +++ b/spartan/aztec-network/values/rc-1.yaml @@ -40,10 +40,10 @@ proverAgent: cpu: "31" bot: - # 2 bots each building txs as fast as possible - # It takes about 16 seconds to build a tx so each bot should produce ~2 txs per block - replicas: 2 - followChain: "NONE" + # 4 bots each building txs as fast as possible + replicas: 4 + ammTxs: true + followChain: "PENDING" enabled: true txIntervalSeconds: 1 resources: diff --git a/yarn-project/aztec.js/src/contract/batch_call.ts b/yarn-project/aztec.js/src/contract/batch_call.ts index e0844f93c414..13b748a3baf2 100644 --- a/yarn-project/aztec.js/src/contract/batch_call.ts +++ b/yarn-project/aztec.js/src/contract/batch_call.ts @@ -1,4 +1,4 @@ -import type { ExecutionPayload } from '@aztec/entrypoints/payload'; +import { ExecutionPayload } from '@aztec/entrypoints/payload'; import { mergeExecutionPayloads } from '@aztec/entrypoints/payload'; import { type FunctionCall, FunctionType, decodeFromAbi } from '@aztec/stdlib/abi'; import type { TxExecutionRequest } from '@aztec/stdlib/tx'; @@ -38,8 +38,14 @@ export class BatchCall extends BaseContractInteraction { * @returns An execution payload wrapped in promise. */ public async request(options: RequestMethodOptions = {}): Promise { - const requests = await this.getRequests(options); - return mergeExecutionPayloads(requests); + const requests = await this.getRequests(); + const combinedPayload = mergeExecutionPayloads(requests); + return new ExecutionPayload( + combinedPayload.calls, + combinedPayload.authWitnesses.concat(options.authWitnesses ?? []), + combinedPayload.capsules.concat(options.capsules ?? []), + combinedPayload.extraHashedArgs, + ); } /** @@ -52,7 +58,7 @@ export class BatchCall extends BaseContractInteraction { * @returns The result of the transaction as returned by the contract function. */ public async simulate(options: SimulateMethodOptions = {}): Promise { - const { indexedExecutionPayloads, unconstrained } = (await this.getRequests(options)).reduce<{ + const { indexedExecutionPayloads, unconstrained } = (await this.getRequests()).reduce<{ /** Keep track of the number of private calls to retrieve the return values */ privateIndex: 0; /** Keep track of the number of public calls to retrieve the return values */ @@ -79,7 +85,13 @@ export class BatchCall extends BaseContractInteraction { ); const payloads = indexedExecutionPayloads.map(([request]) => request); - const requestWithoutFee = mergeExecutionPayloads(payloads); + const combinedPayload = mergeExecutionPayloads(payloads); + const requestWithoutFee = new ExecutionPayload( + combinedPayload.calls, + combinedPayload.authWitnesses.concat(options.authWitnesses ?? []), + combinedPayload.capsules.concat(options.capsules ?? []), + combinedPayload.extraHashedArgs, + ); const { fee: userFee, nonce, cancellable } = options; const fee = await this.getFeeOptions(requestWithoutFee, userFee, {}); const txRequest = await this.wallet.createTxExecutionRequest(requestWithoutFee, fee, { nonce, cancellable }); @@ -117,7 +129,7 @@ export class BatchCall extends BaseContractInteraction { return results; } - private async getRequests(options: RequestMethodOptions = {}) { - return await Promise.all(this.calls.map(c => c.request(options))); + private async getRequests() { + return await Promise.all(this.calls.map(c => c.request())); } } diff --git a/yarn-project/bot/src/amm_bot.ts b/yarn-project/bot/src/amm_bot.ts new file mode 100644 index 000000000000..8859beebad0d --- /dev/null +++ b/yarn-project/bot/src/amm_bot.ts @@ -0,0 +1,93 @@ +import { AztecAddress, Fr, SentTx, type Wallet } from '@aztec/aztec.js'; +import type { AMMContract } from '@aztec/noir-contracts.js/AMM'; +import type { TokenContract } from '@aztec/noir-contracts.js/Token'; +import type { AztecNode, AztecNodeAdmin, PXE } from '@aztec/stdlib/interfaces/client'; + +import { BaseBot } from './base_bot.js'; +import type { BotConfig } from './config.js'; +import { BotFactory } from './factory.js'; + +const TRANSFER_AMOUNT = 1_000; + +type Balances = { token0: bigint; token1: bigint }; + +export class AmmBot extends BaseBot { + protected constructor( + pxe: PXE, + wallet: Wallet, + public readonly amm: AMMContract, + public readonly token0: TokenContract, + public readonly token1: TokenContract, + config: BotConfig, + ) { + super(pxe, wallet, config); + } + + static async create( + config: BotConfig, + dependencies: { pxe?: PXE; node?: AztecNode; nodeAdmin?: AztecNodeAdmin }, + ): Promise { + const { pxe, wallet, token0, token1, amm } = await new BotFactory(config, dependencies).setupAmm(); + return new AmmBot(pxe, wallet, amm, token0, token1, config); + } + + protected async createAndSendTx(logCtx: object): Promise { + const { feePaymentMethod } = this.config; + const { wallet, amm, token0, token1 } = this; + + this.log.verbose(`Preparing tx with ${feePaymentMethod} fee to swap tokens`, logCtx); + + const ammBalances = await this.getAmmBalances(); + const amountIn = TRANSFER_AMOUNT; + const nonce = Fr.random(); + + const swapAuthwit = await wallet.createAuthWit({ + caller: amm.address, + action: token0.methods.transfer_to_public(wallet.getAddress(), amm.address, amountIn, nonce), + }); + + const amountOutMin = await amm.methods + .get_amount_out_for_exact_in(ammBalances.token0, ammBalances.token1, amountIn) + .simulate(); + + const swapExactTokensInteraction = amm.methods.swap_exact_tokens_for_tokens( + token0.address, + token1.address, + amountIn, + amountOutMin, + nonce, + ); + + const opts = this.getSendMethodOpts(swapAuthwit); + + this.log.verbose(`Proving transaction`, logCtx); + const tx = await swapExactTokensInteraction.prove(opts); + + return tx.send(); + } + + public getAmmBalances(): Promise { + return this.getPublicBalanceFor(this.amm.address); + } + + public async getBalances(): Promise<{ senderPublic: Balances; senderPrivate: Balances; amm: Balances }> { + return { + senderPublic: await this.getPublicBalanceFor(this.wallet.getAddress()), + senderPrivate: await this.getPrivateBalanceFor(this.wallet.getAddress()), + amm: await this.getPublicBalanceFor(this.amm.address), + }; + } + + private async getPublicBalanceFor(address: AztecAddress): Promise { + return { + token0: await this.token0.methods.balance_of_public(address).simulate(), + token1: await this.token1.methods.balance_of_public(address).simulate(), + }; + } + private async getPrivateBalanceFor(address: AztecAddress): Promise { + return { + token0: await this.token0.methods.balance_of_private(address).simulate(), + token1: await this.token1.methods.balance_of_private(address).simulate(), + }; + } +} diff --git a/yarn-project/bot/src/base_bot.ts b/yarn-project/bot/src/base_bot.ts new file mode 100644 index 000000000000..29e440149b81 --- /dev/null +++ b/yarn-project/bot/src/base_bot.ts @@ -0,0 +1,75 @@ +import { + AuthWitness, + FeeJuicePaymentMethod, + type SendMethodOptions, + SentTx, + type Wallet, + createLogger, + waitForProven, +} from '@aztec/aztec.js'; +import { Gas } from '@aztec/stdlib/gas'; +import type { PXE } from '@aztec/stdlib/interfaces/client'; + +import type { BotConfig } from './config.js'; + +export abstract class BaseBot { + protected log = createLogger('bot'); + + protected attempts: number = 0; + protected successes: number = 0; + + protected constructor(public readonly pxe: PXE, public readonly wallet: Wallet, public config: BotConfig) {} + + public async run() { + this.attempts++; + const logCtx = { runId: Date.now() * 1000 + Math.floor(Math.random() * 1000) }; + const { followChain, txMinedWaitSeconds } = this.config; + + this.log.verbose(`Creating tx`, logCtx); + const tx = await this.createAndSendTx(logCtx); + + const txHash = await tx.getTxHash(); + + if (followChain === 'NONE') { + this.log.info(`Transaction ${txHash} sent, not waiting for it to be mined`); + return; + } + + this.log.verbose( + `Awaiting tx ${txHash} to be on the ${followChain} chain (timeout ${txMinedWaitSeconds}s)`, + logCtx, + ); + const receipt = await tx.wait({ + timeout: txMinedWaitSeconds, + }); + if (followChain === 'PROVEN') { + await waitForProven(this.pxe, receipt, { provenTimeout: txMinedWaitSeconds }); + } + this.successes++; + this.log.info( + `Tx #${this.attempts} ${receipt.txHash} successfully mined in block ${receipt.blockNumber} (stats: ${this.successes}/${this.attempts} success)`, + logCtx, + ); + } + + protected abstract createAndSendTx(logCtx: object): Promise; + + protected getSendMethodOpts(...authWitnesses: AuthWitness[]): SendMethodOptions { + const sender = this.wallet.getAddress(); + const { l2GasLimit, daGasLimit, skipPublicSimulation } = this.config; + const paymentMethod = new FeeJuicePaymentMethod(sender); + + let gasSettings, estimateGas; + if (l2GasLimit !== undefined && l2GasLimit > 0 && daGasLimit !== undefined && daGasLimit > 0) { + gasSettings = { gasLimits: Gas.from({ l2Gas: l2GasLimit, daGas: daGasLimit }) }; + estimateGas = false; + this.log.verbose(`Using gas limits ${l2GasLimit} L2 gas ${daGasLimit} DA gas`); + } else { + estimateGas = true; + this.log.verbose(`Estimating gas for transaction`); + } + const baseFeePadding = 2; // Send 3x the current base fee + this.log.verbose(skipPublicSimulation ? `Skipping public simulation` : `Simulating public transfers`); + return { fee: { estimateGas, paymentMethod, gasSettings, baseFeePadding }, skipPublicSimulation, authWitnesses }; + } +} diff --git a/yarn-project/bot/src/bot.ts b/yarn-project/bot/src/bot.ts index d94ea0c8d539..760c0937f60e 100644 --- a/yarn-project/bot/src/bot.ts +++ b/yarn-project/bot/src/bot.ts @@ -1,37 +1,26 @@ -import { - type AztecAddress, - BatchCall, - FeeJuicePaymentMethod, - type SendMethodOptions, - type Wallet, - createLogger, - waitForProven, -} from '@aztec/aztec.js'; +import { type AztecAddress, BatchCall, SentTx, type Wallet } from '@aztec/aztec.js'; import { times } from '@aztec/foundation/collection'; import type { EasyPrivateTokenContract } from '@aztec/noir-contracts.js/EasyPrivateToken'; import type { TokenContract } from '@aztec/noir-contracts.js/Token'; -import { Gas } from '@aztec/stdlib/gas'; import type { AztecNode, AztecNodeAdmin, PXE } from '@aztec/stdlib/interfaces/client'; +import { BaseBot } from './base_bot.js'; import type { BotConfig } from './config.js'; import { BotFactory } from './factory.js'; import { getBalances, getPrivateBalance, isStandardTokenContract } from './utils.js'; const TRANSFER_AMOUNT = 1; -export class Bot { - private log = createLogger('bot'); - - private attempts: number = 0; - private successes: number = 0; - +export class Bot extends BaseBot { protected constructor( - public readonly pxe: PXE, - public readonly wallet: Wallet, + pxe: PXE, + wallet: Wallet, public readonly token: TokenContract | EasyPrivateTokenContract, public readonly recipient: AztecAddress, - public config: BotConfig, - ) {} + config: BotConfig, + ) { + super(pxe, wallet, config); + } static async create( config: BotConfig, @@ -46,11 +35,8 @@ export class Bot { this.config = { ...this.config, ...config }; } - public async run() { - this.attempts++; - const logCtx = { runId: Date.now() * 1000 + Math.floor(Math.random() * 1000) }; - const { privateTransfersPerTx, publicTransfersPerTx, feePaymentMethod, followChain, txMinedWaitSeconds } = - this.config; + protected async createAndSendTx(logCtx: object): Promise { + const { privateTransfersPerTx, publicTransfersPerTx, feePaymentMethod } = this.config; const { token, recipient, wallet } = this; const sender = wallet.getAddress(); @@ -74,32 +60,7 @@ export class Bot { this.log.verbose(`Proving transaction`, logCtx); const provenTx = await batch.prove(opts); - - this.log.verbose(`Sending tx`, logCtx); - const tx = provenTx.send(); - - const txHash = await tx.getTxHash(); - - if (followChain === 'NONE') { - this.log.info(`Transaction ${txHash} sent, not waiting for it to be mined`); - return; - } - - this.log.verbose( - `Awaiting tx ${txHash} to be on the ${followChain} chain (timeout ${txMinedWaitSeconds}s)`, - logCtx, - ); - const receipt = await tx.wait({ - timeout: txMinedWaitSeconds, - }); - if (followChain === 'PROVEN') { - await waitForProven(this.pxe, receipt, { provenTimeout: txMinedWaitSeconds }); - } - this.log.info( - `Tx #${this.attempts} ${receipt.txHash} successfully mined in block ${receipt.blockNumber} (stats: ${this.successes}/${this.attempts} success)`, - logCtx, - ); - this.successes++; + return provenTx.send(); } public async getBalances() { @@ -121,23 +82,4 @@ export class Bot { }; } } - - private getSendMethodOpts(): SendMethodOptions { - const sender = this.wallet.getAddress(); - const { l2GasLimit, daGasLimit, skipPublicSimulation } = this.config; - const paymentMethod = new FeeJuicePaymentMethod(sender); - - let gasSettings, estimateGas; - if (l2GasLimit !== undefined && l2GasLimit > 0 && daGasLimit !== undefined && daGasLimit > 0) { - gasSettings = { gasLimits: Gas.from({ l2Gas: l2GasLimit, daGas: daGasLimit }) }; - estimateGas = false; - this.log.verbose(`Using gas limits ${l2GasLimit} L2 gas ${daGasLimit} DA gas`); - } else { - estimateGas = true; - this.log.verbose(`Estimating gas for transaction`); - } - const baseFeePadding = 2; // Send 3x the current base fee - this.log.verbose(skipPublicSimulation ? `Skipping public simulation` : `Simulating public transfers`); - return { fee: { estimateGas, paymentMethod, gasSettings, baseFeePadding }, skipPublicSimulation }; - } } diff --git a/yarn-project/bot/src/config.ts b/yarn-project/bot/src/config.ts index 78e30d926009..5829123f32ef 100644 --- a/yarn-project/bot/src/config.ts +++ b/yarn-project/bot/src/config.ts @@ -71,6 +71,8 @@ export type BotConfig = { maxConsecutiveErrors: number; /** Stops the bot if service becomes unhealthy */ stopWhenUnhealthy: boolean; + /** Deploy an AMM contract and do swaps instead of transfers */ + ammTxs: boolean; }; export const BotConfigSchema = z @@ -99,6 +101,7 @@ export const BotConfigSchema = z contract: z.nativeEnum(SupportedTokenContracts), maxConsecutiveErrors: z.number().int().nonnegative(), stopWhenUnhealthy: z.boolean(), + ammTxs: z.boolean().default(false), }) .transform(config => ({ nodeUrl: undefined, @@ -248,6 +251,11 @@ export const botConfigMappings: ConfigMappingsType = { description: 'Stops the bot if service becomes unhealthy', ...booleanConfigHelper(false), }, + ammTxs: { + env: 'BOT_AMM_TXS', + description: 'Deploy an AMM and send swaps to it', + ...booleanConfigHelper(false), + }, }; export function getBotConfigFromEnv(): BotConfig { diff --git a/yarn-project/bot/src/factory.ts b/yarn-project/bot/src/factory.ts index 632e41138fb8..97d02b997528 100644 --- a/yarn-project/bot/src/factory.ts +++ b/yarn-project/bot/src/factory.ts @@ -4,6 +4,7 @@ import { type AccountWallet, AztecAddress, BatchCall, + ContractBase, ContractFunctionInteraction, type DeployMethod, type DeployOptions, @@ -16,6 +17,7 @@ import { } from '@aztec/aztec.js'; import { createEthereumChain, createL1Clients } from '@aztec/ethereum'; import { Fr } from '@aztec/foundation/fields'; +import { AMMContract } from '@aztec/noir-contracts.js/AMM'; import { EasyPrivateTokenContract } from '@aztec/noir-contracts.js/EasyPrivateToken'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import type { AztecNode, AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; @@ -76,6 +78,19 @@ export class BotFactory { return { wallet, token, pxe: this.pxe, recipient }; } + public async setupAmm() { + const wallet = await this.setupAccount(); + const token0 = await this.setupTokenContract(wallet, this.config.tokenSalt, 'BotToken0', 'BOT0'); + const token1 = await this.setupTokenContract(wallet, this.config.tokenSalt, 'BotToken1', 'BOT1'); + const liquidityToken = await this.setupTokenContract(wallet, this.config.tokenSalt, 'BotLPToken', 'BOTLP'); + const amm = await this.setupAmmContract(wallet, this.config.tokenSalt, token0, token1, liquidityToken); + + await this.fundAmm(wallet, amm, token0, token1); + this.log.info(`AMM initialized and funded`); + + return { wallet, amm, token0, token1, pxe: this.pxe }; + } + /** * Checks if the sender account contract is initialized, and initializes it if necessary. * @returns The sender wallet. @@ -175,6 +190,104 @@ export class BotFactory { } } + /** + * Checks if the token contract is deployed and deploys it if necessary. + * @param wallet - Wallet to deploy the token contract from. + * @returns The TokenContract instance. + */ + private setupTokenContract( + wallet: AccountWallet, + contractAddressSalt: Fr, + name: string, + ticker: string, + decimals = 18, + ): Promise { + const deployOpts: DeployOptions = { contractAddressSalt, universalDeploy: true }; + const deploy = TokenContract.deploy(wallet, wallet.getAddress(), name, ticker, decimals); + return this.registerOrDeployContract('Token - ' + name, deploy, deployOpts); + } + + private async setupAmmContract( + wallet: AccountWallet, + contractAddressSalt: Fr, + token0: TokenContract, + token1: TokenContract, + lpToken: TokenContract, + ): Promise { + const deployOpts: DeployOptions = { contractAddressSalt, universalDeploy: true }; + const deploy = AMMContract.deploy(wallet, token0.address, token1.address, lpToken.address); + const amm = await this.registerOrDeployContract('AMM', deploy, deployOpts); + + this.log.info(`AMM deployed at ${amm.address}`); + const minterTx = lpToken.methods.set_minter(amm.address, true).send(); + this.log.info(`Set LP token minter to AMM txHash=${await minterTx.getTxHash()}`); + await minterTx.wait({ timeout: this.config.txMinedWaitSeconds }); + this.log.info(`Liquidity token initialized`); + + return amm; + } + + private async fundAmm( + wallet: AccountWallet, + amm: AMMContract, + token0: TokenContract, + token1: TokenContract, + ): Promise { + const nonce = Fr.random(); + + // keep some tokens for swapping + const amount0Max = MINT_BALANCE / 2; + const amount0Min = MINT_BALANCE / 4; + const amount1Max = MINT_BALANCE / 2; + const amount1Min = MINT_BALANCE / 4; + + const token0Authwit = await wallet.createAuthWit({ + caller: amm.address, + action: token0.methods.transfer_to_public(wallet.getAddress(), amm.address, amount0Max, nonce), + }); + const token1Authwit = await wallet.createAuthWit({ + caller: amm.address, + action: token1.methods.transfer_to_public(wallet.getAddress(), amm.address, amount1Max, nonce), + }); + + this.log.info(`Minting tokens`); + const mintTx = new BatchCall(wallet, [ + token0.methods.mint_to_private(wallet.getAddress(), wallet.getAddress(), MINT_BALANCE), + token1.methods.mint_to_private(wallet.getAddress(), wallet.getAddress(), MINT_BALANCE), + ]).send(); + + this.log.info(`Sent mint tx: ${await mintTx.getTxHash()}`); + await mintTx.wait({ timeout: this.config.txMinedWaitSeconds }); + + this.log.info(`Funding AMM`); + const addLiquidityTx = amm.methods.add_liquidity(amount0Max, amount1Max, amount0Min, amount1Min, nonce).send({ + authWitnesses: [token0Authwit, token1Authwit], + }); + + this.log.info(`Sent tx to add liquidity to the AMM: ${await addLiquidityTx.getTxHash()}`); + await addLiquidityTx.wait({ timeout: this.config.txMinedWaitSeconds }); + } + + private async registerOrDeployContract( + name: string, + deploy: DeployMethod, + deployOpts: DeployOptions, + ): Promise { + const address = (await deploy.getInstance(deployOpts)).address; + if ((await this.pxe.getContractMetadata(address)).isContractPubliclyDeployed) { + this.log.info(`Contract ${name} at ${address.toString()} already deployed`); + return deploy.register(); + } else { + this.log.info(`Deploying contract ${name} at ${address.toString()}`); + const sentTx = deploy.send(deployOpts); + const txHash = await sentTx.getTxHash(); + this.log.info(`Sent tx with hash ${txHash.toString()}`); + await this.tryFlushTxs(); + this.log.verbose(`Waiting for contract ${name} setup to settle`); + return sentTx.deployed({ timeout: this.config.txMinedWaitSeconds }); + } + } + /** * Mints private and public tokens for the sender if their balance is below the minimum. * @param token - Token contract. diff --git a/yarn-project/bot/src/index.ts b/yarn-project/bot/src/index.ts index 689d3feb5e53..3d70e243aa91 100644 --- a/yarn-project/bot/src/index.ts +++ b/yarn-project/bot/src/index.ts @@ -1,4 +1,5 @@ export { Bot } from './bot.js'; +export { AmmBot } from './amm_bot.js'; export { BotRunner } from './runner.js'; export { type BotConfig, diff --git a/yarn-project/bot/src/runner.ts b/yarn-project/bot/src/runner.ts index f434172d6980..0bcef9188477 100644 --- a/yarn-project/bot/src/runner.ts +++ b/yarn-project/bot/src/runner.ts @@ -3,13 +3,15 @@ import { RunningPromise } from '@aztec/foundation/running-promise'; import { type AztecNodeAdmin, createAztecNodeAdminClient } from '@aztec/stdlib/interfaces/client'; import { type TelemetryClient, type Traceable, type Tracer, makeTracedFetch, trackSpan } from '@aztec/telemetry-client'; +import { AmmBot } from './amm_bot.js'; +import type { BaseBot } from './base_bot.js'; import { Bot } from './bot.js'; import { type BotConfig, getVersions } from './config.js'; import type { BotRunnerApi } from './interface.js'; export class BotRunner implements BotRunnerApi, Traceable { private log = createLogger('bot'); - private bot?: Promise; + private bot?: Promise; private pxe?: PXE; private node: AztecNode; private nodeAdmin?: AztecNodeAdmin; @@ -137,7 +139,9 @@ export class BotRunner implements BotRunnerApi, Traceable { async #createBot() { try { - this.bot = Bot.create(this.config, { pxe: this.pxe, node: this.node, nodeAdmin: this.nodeAdmin }); + this.bot = this.config.ammTxs + ? AmmBot.create(this.config, { pxe: this.pxe, node: this.node, nodeAdmin: this.nodeAdmin }) + : Bot.create(this.config, { pxe: this.pxe, node: this.node, nodeAdmin: this.nodeAdmin }); await this.bot; } catch (err) { this.log.error(`Error setting up bot: ${err}`); diff --git a/yarn-project/bot/src/utils.ts b/yarn-project/bot/src/utils.ts index e7101863a00a..4bab0c31c2be 100644 --- a/yarn-project/bot/src/utils.ts +++ b/yarn-project/bot/src/utils.ts @@ -1,3 +1,5 @@ +import type { ContractBase } from '@aztec/aztec.js'; +import type { AMMContract } from '@aztec/noir-contracts.js/AMM'; import type { EasyPrivateTokenContract } from '@aztec/noir-contracts.js/EasyPrivateToken'; import type { TokenContract } from '@aztec/noir-contracts.js/Token'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; @@ -22,6 +24,10 @@ export async function getPrivateBalance(token: EasyPrivateTokenContract, who: Az return privateBalance; } -export function isStandardTokenContract(token: TokenContract | EasyPrivateTokenContract): token is TokenContract { +export function isStandardTokenContract(token: ContractBase): token is TokenContract { return 'mint_to_public' in token.methods; } + +export function isAMMContract(contract: ContractBase): contract is AMMContract { + return 'add_liquidity' in contract.methods; +} diff --git a/yarn-project/end-to-end/src/e2e_bot.test.ts b/yarn-project/end-to-end/src/e2e_bot.test.ts index 4c2805622c7c..a8b04dfb7d92 100644 --- a/yarn-project/end-to-end/src/e2e_bot.test.ts +++ b/yarn-project/end-to-end/src/e2e_bot.test.ts @@ -1,6 +1,6 @@ import { getInitialTestAccounts } from '@aztec/accounts/testing'; import type { PXE } from '@aztec/aztec.js'; -import { Bot, type BotConfig, SupportedTokenContracts, getBotDefaultConfig } from '@aztec/bot'; +import { AmmBot, Bot, type BotConfig, SupportedTokenContracts, getBotDefaultConfig } from '@aztec/bot'; import { setup } from './fixtures/utils.js'; @@ -8,61 +8,87 @@ describe('e2e_bot', () => { let pxe: PXE; let teardown: () => Promise; - let bot: Bot; let config: BotConfig; beforeAll(async () => { const initialFundedAccounts = await getInitialTestAccounts(); ({ teardown, pxe } = await setup(1, { initialFundedAccounts })); - config = { - ...getBotDefaultConfig(), - followChain: 'PENDING', - }; - bot = await Bot.create(config, { pxe }); }); afterAll(() => teardown()); - it('sends token transfers from the bot', async () => { - const { recipient: recipientBefore } = await bot.getBalances(); + describe('transaction-bot', () => { + let bot: Bot; + beforeAll(async () => { + config = { + ...getBotDefaultConfig(), + followChain: 'PENDING', + ammTxs: false, + }; + bot = await Bot.create(config, { pxe }); + }); - await bot.run(); - const { recipient: recipientAfter } = await bot.getBalances(); - expect(recipientAfter.privateBalance - recipientBefore.privateBalance).toEqual(1n); - expect(recipientAfter.publicBalance - recipientBefore.publicBalance).toEqual(1n); - }); + it('sends token transfers from the bot', async () => { + const { recipient: recipientBefore } = await bot.getBalances(); - it('sends token transfers with hardcoded gas and no simulation', async () => { - bot.updateConfig({ daGasLimit: 1e9, l2GasLimit: 1e9, skipPublicSimulation: true }); - const { recipient: recipientBefore } = await bot.getBalances(); + await bot.run(); + const { recipient: recipientAfter } = await bot.getBalances(); + expect(recipientAfter.privateBalance - recipientBefore.privateBalance).toEqual(1n); + expect(recipientAfter.publicBalance - recipientBefore.publicBalance).toEqual(1n); + }); - await bot.run(); - const { recipient: recipientAfter } = await bot.getBalances(); - expect(recipientAfter.privateBalance - recipientBefore.privateBalance).toEqual(1n); - expect(recipientAfter.publicBalance - recipientBefore.publicBalance).toEqual(1n); - }); + it('sends token transfers with hardcoded gas and no simulation', async () => { + bot.updateConfig({ daGasLimit: 1e9, l2GasLimit: 1e9, skipPublicSimulation: true }); + const { recipient: recipientBefore } = await bot.getBalances(); + + await bot.run(); + const { recipient: recipientAfter } = await bot.getBalances(); + expect(recipientAfter.privateBalance - recipientBefore.privateBalance).toEqual(1n); + expect(recipientAfter.publicBalance - recipientBefore.publicBalance).toEqual(1n); + }); + + it('reuses the same account and token contract', async () => { + const { wallet, token, recipient } = bot; + const bot2 = await Bot.create(config, { pxe }); + expect(bot2.wallet.getAddress().toString()).toEqual(wallet.getAddress().toString()); + expect(bot2.token.address.toString()).toEqual(token.address.toString()); + expect(bot2.recipient.toString()).toEqual(recipient.toString()); + }); + + it('sends token from the bot using EasyPrivateToken', async () => { + const easyBot = await Bot.create( + { + ...config, + contract: SupportedTokenContracts.EasyPrivateTokenContract, + }, + { pxe }, + ); + const { recipient: recipientBefore } = await easyBot.getBalances(); - it('reuses the same account and token contract', async () => { - const { wallet, token, recipient } = bot; - const bot2 = await Bot.create(config, { pxe }); - expect(bot2.wallet.getAddress().toString()).toEqual(wallet.getAddress().toString()); - expect(bot2.token.address.toString()).toEqual(token.address.toString()); - expect(bot2.recipient.toString()).toEqual(recipient.toString()); + await easyBot.run(); + const { recipient: recipientAfter } = await easyBot.getBalances(); + expect(recipientAfter.privateBalance - recipientBefore.privateBalance).toEqual(1n); + expect(recipientAfter.publicBalance - recipientBefore.publicBalance).toEqual(0n); + }); }); - it('sends token from the bot using EasyPrivateToken', async () => { - const easyBot = await Bot.create( - { - ...config, - contract: SupportedTokenContracts.EasyPrivateTokenContract, - }, - { pxe }, - ); - const { recipient: recipientBefore } = await easyBot.getBalances(); + describe('amm-bot', () => { + let bot: AmmBot; + beforeAll(async () => { + config = { + ...getBotDefaultConfig(), + followChain: 'PENDING', + ammTxs: true, + }; + bot = await AmmBot.create(config, { pxe }); + }); - await easyBot.run(); - const { recipient: recipientAfter } = await easyBot.getBalances(); - expect(recipientAfter.privateBalance - recipientBefore.privateBalance).toEqual(1n); - expect(recipientAfter.publicBalance - recipientBefore.publicBalance).toEqual(0n); + it('swaps tokens from the bot', async () => { + const balancesBefore = await bot.getBalances(); + await expect(bot.run()).resolves.toBeUndefined(); + const balancesAfter = await bot.getBalances(); + expect(balancesAfter.senderPrivate.token0).toBeLessThan(balancesBefore.senderPrivate.token0); + expect(balancesAfter.senderPrivate.token1).toBeGreaterThan(balancesBefore.senderPrivate.token1); + }); }); }); diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 28e61a0c87a8..b6d4dce212d1 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -43,6 +43,7 @@ export type EnvVar = | 'BOT_TX_MINED_WAIT_SECONDS' | 'BOT_MAX_CONSECUTIVE_ERRORS' | 'BOT_STOP_WHEN_UNHEALTHY' + | 'BOT_AMM_TXS' | 'COINBASE' | 'DATA_DIRECTORY' | 'DATA_STORE_MAP_SIZE_KB'