diff --git a/src/account.ts b/src/account.ts index e78797e8..3117e0ae 100644 --- a/src/account.ts +++ b/src/account.ts @@ -103,10 +103,9 @@ export async function initAccounts( span?.setAttribute("details.wallet", accounts[i].account.address); span?.setAttribute("details.amount", ethers.utils.formatUnits(transferAmount)); try { - const hash = await mainAccount.sendTransaction({ + const hash = await mainAccount.sendTx({ to: accounts[i].account.address, value: transferAmount.toBigInt(), - nonce: await getNonce(mainAccount), }); const receipt = await mainAccount.waitForTransactionReceipt({ hash, @@ -248,10 +247,9 @@ export async function manageAccounts( continue; } try { - const hash = await config.mainAccount.sendTransaction({ + const hash = await config.mainAccount.sendTx({ to: acc.account.address, value: transferAmount.toBigInt(), - nonce: await getNonce(config.mainAccount), }); const receipt = await config.mainAccount.waitForTransactionReceipt({ hash, @@ -543,10 +541,9 @@ export async function sweepToMainWallet( .div(100) .sub(fromWallet.BALANCE); span?.setAttribute("details.amount", ethers.utils.formatUnits(transferAmount)); - const hash = await toWallet.sendTransaction({ + const hash = await toWallet.sendTx({ to: fromWallet.account.address, value: transferAmount.toBigInt(), - nonce: await getNonce(toWallet), }); const receipt = await toWallet.waitForTransactionReceipt({ hash, @@ -589,9 +586,7 @@ export async function sweepToMainWallet( span?.setAttribute("details.tokenAddress", txs[i].bounty.address); span?.setAttribute("details.balance", txs[i].balance); try { - const nonce = await getNonce(fromWallet); - (txs[i].tx as any).nonce = nonce; - const hash = await fromWallet.sendTransaction(txs[i].tx); + const hash = await fromWallet.sendTx(txs[i].tx); const receipt = await fromWallet.waitForTransactionReceipt({ hash, confirmations: 4, @@ -643,12 +638,11 @@ export async function sweepToMainWallet( const transferAmount = remainingGas.sub(gasLimit.mul(gasPrice)); if (transferAmount.gt(0)) { span?.setAttribute("details.amount", ethers.utils.formatUnits(transferAmount)); - const hash = await fromWallet.sendTransaction({ + const hash = await fromWallet.sendTx({ gasPrice, to: toWallet.account.address, value: transferAmount.toBigInt(), gas: gasLimit.toBigInt(), - nonce: await getNonce(fromWallet), }); const receipt = await fromWallet.waitForTransactionReceipt({ hash, @@ -791,13 +785,12 @@ export async function sweepToEth(config: BotConfig, tracer?: Tracer, ctx?: Conte ).data; if (allowance && balance.gt(allowance)) { span?.addEvent("Approving spend limit"); - const hash = await config.mainAccount.sendTransaction({ + const hash = await config.mainAccount.sendTx({ to: bounty.address as `0x${string}`, data: erc20.encodeFunctionData("approve", [ rp4Address, balance.mul(100), ]) as `0x${string}`, - nonce: await getNonce(config.mainAccount), }); await config.mainAccount.waitForTransactionReceipt({ hash, @@ -838,9 +831,7 @@ export async function sweepToEth(config: BotConfig, tracer?: Tracer, ctx?: Conte span?.end(); continue; } else { - const nonce = await getNonce(config.mainAccount); - (rawtx as any).nonce = nonce; - const hash = await config.mainAccount.sendTransaction(rawtx); + const hash = await config.mainAccount.sendTx(rawtx); span?.setAttribute("txHash", hash); const receipt = await config.mainAccount.waitForTransactionReceipt({ hash, @@ -1012,11 +1003,10 @@ export async function fundOwnedOrders( finalRpParams!.to, finalRpParams!.routeCode, ]) as `0x${string}`; - const swapHash = await config.mainAccount.sendTransaction({ + const swapHash = await config.mainAccount.sendTx({ to: rp4Address, value: sellAmount!.toBigInt(), data, - nonce: await getNonce(config.mainAccount), }); const swapReceipt = await config.mainAccount.waitForTransactionReceipt({ hash: swapHash, @@ -1046,13 +1036,12 @@ export async function fundOwnedOrders( }) ).data; if (allowance && topupAmount.gt(allowance)) { - const approveHash = await config.mainAccount.sendTransaction({ + const approveHash = await config.mainAccount.sendTx({ to: ownedOrder.token as `0x${string}`, data: erc20.encodeFunctionData("approve", [ ownedOrder.orderbook, topupAmount.mul(20), ]) as `0x${string}`, - nonce: await getNonce(config.mainAccount), }); const approveReceipt = await config.mainAccount.waitForTransactionReceipt({ @@ -1070,7 +1059,7 @@ export async function fundOwnedOrders( } } - const hash = await config.mainAccount.sendTransaction({ + const hash = await config.mainAccount.sendTx({ to: ownedOrder.orderbook as `0x${string}`, data: ob.encodeFunctionData("deposit2", [ ownedOrder.token, @@ -1078,7 +1067,6 @@ export async function fundOwnedOrders( topupAmount, [], ]) as `0x${string}`, - nonce: await getNonce(config.mainAccount), }); const receipt = await config.mainAccount.waitForTransactionReceipt({ hash, @@ -1138,23 +1126,3 @@ async function getTransactionCount( ); return hexToNumber(count); } - -/** - * Get an account's nonce - * @param client - The viem client - * @param address - account address - */ -export async function getNonce(client: ViemClient): Promise { - for (let i = 0; i < 3; i++) { - try { - return await client.getTransactionCount({ - address: client.account.address, - blockTag: "latest", - }); - } catch (error) { - if (i === 2) throw error; - else await sleep((i + 1) * 5000); - } - } - throw "Failed to get account's nonce"; -} diff --git a/src/config.ts b/src/config.ts index ba6a715d..954fd2bb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -30,6 +30,7 @@ import { ROUTE_PROCESSOR_3_1_ADDRESS, ROUTE_PROCESSOR_3_2_ADDRESS, } from "sushi/config"; +import { sendTransaction } from "./tx"; /** * Get the chain config for a given chain id @@ -114,7 +115,7 @@ export async function createViemClient( ? fallback([...topRpcs, ...fallbacks], configuration) : fallback(topRpcs, configuration); - return testClient + const client = testClient ? ((await testClient({ account })) .extend(publicActions) .extend(walletActions) as any as ViemClient) @@ -123,6 +124,14 @@ export async function createViemClient( chain: publicClientConfig[chainId]?.chain, transport, }).extend(publicActions) as any as ViemClient); + + // set injected properties + client.BUSY = false; + client.sendTx = async (tx) => { + return await sendTransaction(client, tx); + }; + + return client; } /** diff --git a/src/error.ts b/src/error.ts index ed1447a9..c510500c 100644 --- a/src/error.ts +++ b/src/error.ts @@ -97,7 +97,7 @@ export function errorSnapshot( message.push("Gas Error: " + gasErr); } } else { - message.push("Comment: Found no additional info") + message.push("Comment: Found no additional info"); } } } @@ -172,7 +172,11 @@ export async function handleRevert( try { const gasErr = checkGasIssue(receipt, rawtx, signerBalance); if (gasErr) { - return { err: header + ", " + gasErr, nodeError: false, snapshot: header + ", " + gasErr }; + return { + err: header + ", " + gasErr, + nodeError: false, + snapshot: header + ", " + gasErr, + }; } const tx = await viemClient.getTransaction({ hash }); await viemClient.call({ @@ -183,7 +187,8 @@ export async function handleRevert( gasPrice: tx.gasPrice, blockNumber: tx.blockNumber, }); - const msg = header + + const msg = + header + " and simulation failed to find out what was the revert reason, please try to simulate the tx manualy for more details"; return { err: msg, nodeError: false, snapshot: msg }; } catch (err: any) { diff --git a/src/processOrders.ts b/src/processOrders.ts index 4ff660f2..883a0dc8 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -2,10 +2,10 @@ import { ChainId } from "sushi"; import { findOpp } from "./modes"; import { PublicClient } from "viem"; import { Token } from "sushi/currency"; -import { handleTransaction } from "./tx"; import { createViemClient } from "./config"; import { fundOwnedOrders } from "./account"; import { arbAbis, orderbookAbi } from "./abis"; +import { getSigner, handleTransaction } from "./tx"; import { privateKeyToAccount } from "viem/accounts"; import { BigNumber, Contract, ethers } from "ethers"; import { Tracer } from "@opentelemetry/sdk-trace-base"; @@ -22,12 +22,10 @@ import { ProcessPairResult, } from "./types"; import { - sleep, toNumber, getEthPrice, quoteOrders, routeExists, - shuffleArray, PoolBlackList, getMarketQuote, checkOwnedOrders, @@ -158,7 +156,6 @@ export const processOrders = async ( } catch (e) { throw errorSnapshot("Failed to batch quote orders", e); } - let avgGasCost: BigNumber | undefined; const reports: Report[] = []; @@ -184,11 +181,8 @@ export const processOrders = async ( takeOrders: [pairOrders.takeOrders[i]], }; - // await for first available signer - if (accounts.length) { - shuffleArray(accounts); - } - const signer = await getSigner(accounts, mainAccount); + // await for first available signer to get free + const signer = await getSigner(accounts, mainAccount, true); const writeSigner = config.writeRpc ? await createViemClient( @@ -211,15 +205,18 @@ export const processOrders = async ( : undefined; const pair = `${pairOrders.buyTokenSymbol}/${pairOrders.sellTokenSymbol}`; - const span = tracer.startSpan("checkpoint", undefined, ctx); + const span = tracer.startSpan(`checkpoint_${pair}`, undefined, ctx); span.setAttributes({ "details.pair": pair, - "details.order": orderPairObject.takeOrders[0].id, + "details.orderHash": orderPairObject.takeOrders[0].id, "details.orderbook": orderbook.address, - "details.account": signer.account.address, + "details.sender": signer.account.address, + "details.owner": orderPairObject.takeOrders[0].takeOrder.order.owner, }); - // process the pair + // call process pair and save the settlement fn + // to later settle without needing to pause if + // there are more signers available const settle = await processPair({ config, orderPairObject, @@ -243,6 +240,10 @@ export const processOrders = async ( // instantiate a span for this pair const span = tracer.startSpan(`order_${pair}`, undefined, ctx); try { + // settle the process results + // this will return the report of the operation and in case + // there was a revert tx, it will try to simulate it and find + // the root cause as well const result = await settle(); // keep track of avg gas cost @@ -299,8 +300,7 @@ export const processOrders = async ( // set the otel span status based on returned reason if (e.reason === ProcessPairHaltReason.FailedToQuote) { - let message = - "failed to quote order: " + orderPairObject.takeOrders[0].id; + let message = "failed to quote order: " + orderPairObject.takeOrders[0].id; if (e.error) { message = errorSnapshot(message, e.error); } @@ -357,10 +357,7 @@ export const processOrders = async ( if ("snapshot" in e.error) { message = e.error.snapshot; } else { - message = errorSnapshot( - "transaction reverted onchain", - e.error.err, - ); + message = errorSnapshot("transaction reverted onchain", e.error.err); } span.setAttribute("errorDetails", message); } @@ -494,12 +491,16 @@ export async function processPair(args: { buyToken: orderPairObject.buyToken, sellToken: orderPairObject.sellToken, }; - return async () => { return result; }; + return async () => { + return result; + }; } } catch (e) { result.error = e; result.reason = ProcessPairHaltReason.FailedToQuote; - return async () => { throw result; }; + return async () => { + throw result; + }; } spanAttributes["details.quote"] = JSON.stringify({ @@ -516,7 +517,9 @@ export async function processPair(args: { } catch (e) { result.reason = ProcessPairHaltReason.FailedToGetGasPrice; result.error = e; - return async () => { throw result; }; + return async () => { + throw result; + }; } // get pool details @@ -540,7 +543,9 @@ export async function processPair(args: { } catch (e) { result.reason = ProcessPairHaltReason.FailedToGetPools; result.error = e; - return async () => { throw result; }; + return async () => { + throw result; + }; } } @@ -591,7 +596,9 @@ export async function processPair(args: { } else { result.reason = ProcessPairHaltReason.FailedToGetEthPrice; result.error = "no-route"; - return async () => { return Promise.reject(result); }; + return async () => { + return Promise.reject(result); + }; } } else { const p1 = `${orderPairObject.buyTokenSymbol}/${config.nativeWrappedToken.symbol}`; @@ -608,7 +615,9 @@ export async function processPair(args: { } else { result.reason = ProcessPairHaltReason.FailedToGetEthPrice; result.error = e; - return async () => { throw result; }; + return async () => { + throw result; + }; } } @@ -660,7 +669,9 @@ export async function processPair(args: { buyToken: orderPairObject.buyToken, sellToken: orderPairObject.sellToken, }; - return async () => { return result; }; + return async () => { + return result; + }; } // from here on we know an opp is found, so record it in report and in otel span attributes @@ -684,7 +695,7 @@ export async function processPair(args: { spanAttributes["details.blockNumberError"] = errorSnapshot("failed to get block number", e); } - // call and return the tx processor + // handle the found transaction opportunity return handleTransaction( signer, viemClient as any as ViemClient, @@ -702,18 +713,3 @@ export async function processPair(args: { writeSigner, ); } - -/** - * Returns the first available signer by constantly polling the signers in 30ms intervals - */ -export async function getSigner(accounts: ViemClient[], mainAccount: ViemClient): Promise { - const accs = accounts.length ? accounts : [mainAccount]; - for (;;) { - const acc = accs.find((v) => !v.BUSY); - if (acc) { - return acc; - } else { - await sleep(30); - } - } -} diff --git a/src/tx.ts b/src/tx.ts index 2ff99f13..bea73f78 100644 --- a/src/tx.ts +++ b/src/tx.ts @@ -1,17 +1,18 @@ import { Token } from "sushi/currency"; +import { addWatchedToken } from "./account"; import { BigNumber, Contract, ethers } from "ethers"; -import { BaseError, TransactionReceipt } from "viem"; -import { addWatchedToken, getNonce } from "./account"; import { containsNodeError, handleRevert } from "./error"; import { ProcessPairHaltReason, ProcessPairReportStatus } from "./processOrders"; import { BotConfig, BundledOrders, ProcessPairResult, RawTx, ViemClient } from "./types"; +import { Account, BaseError, Chain, SendTransactionParameters, TransactionReceipt } from "viem"; import { + sleep, toNumber, getIncome, + shuffleArray, getTotalIncome, withBigintSerializer, getActualClearAmount, - sleep, } from "./utils"; /** @@ -39,23 +40,19 @@ export async function handleTransaction( let txhash: `0x${string}`, txUrl: string; let time = 0; const sendTx = async () => { - rawtx.nonce = await getNonce(writeSigner !== undefined ? writeSigner : signer); if (writeSigner !== undefined) { rawtx.gas = undefined; } - writeSigner !== undefined ? (writeSigner.BUSY = true) : (signer.BUSY = true); txhash = writeSigner !== undefined - ? await writeSigner.sendTransaction({ + ? await writeSigner.sendTx({ ...rawtx, type: "legacy", }) - : await signer.sendTransaction({ + : await signer.sendTx({ ...rawtx, type: "legacy", }); - - writeSigner !== undefined ? (writeSigner.BUSY = false) : (signer.BUSY = false); txUrl = config.chain.blockExplorers?.default.url + "/tx/" + txhash; time = Date.now(); // eslint-disable-next-line no-console @@ -70,8 +67,7 @@ export async function handleTransaction( await sleep(5000); await sendTx(); } catch { - writeSigner !== undefined ? (writeSigner.BUSY = false) : (signer.BUSY = false); - // record rawtx in case it is not already present in the error + // record rawtx in logs spanAttributes["details.rawTx"] = JSON.stringify( { ...rawtx, @@ -88,18 +84,18 @@ export async function handleTransaction( } } - // start getting tx receipt in background and return the resolver fn - const receipt = viemClient.waitForTransactionReceipt({ + // start getting tx receipt in background and return the settler fn + const receiptPromise = viemClient.waitForTransactionReceipt({ hash: txhash!, confirmations: 1, timeout: 120_000, }); return async () => { - // wait for tx receipt try { + const receipt = await receiptPromise; return handleReceipt( txhash, - await receipt, + receipt, signer, spanAttributes, rawtx, @@ -307,3 +303,69 @@ export async function handleReceipt( return Promise.reject(result); } } + +/** + * A wrapper for sending transactions that handles nonce and keeps + * signer busy while the transaction is being sent + */ +export async function sendTransaction( + signer: ViemClient, + tx: SendTransactionParameters, +): Promise<`0x${string}`> { + // make sure signer is free + await pollSigners([signer]); + + // start sending tranaction process + signer.BUSY = true; + try { + const nonce = await getNonce(signer); + const result = await signer.sendTransaction({ ...tx, nonce }); + signer.BUSY = false; + return result; + } catch (error) { + signer.BUSY = false; + throw error; + } +} + +/** + * A wrapper fn to get an signer's nonce at latest mined block + */ +export async function getNonce(client: ViemClient): Promise { + if (!client?.account?.address) throw "undefined account"; + return await client.getTransactionCount({ + address: client.account.address, + blockTag: "latest", + }); +} + +/** + * Returns the first available signer by polling the + * signers until first one becomes available + */ +export async function getSigner( + accounts: ViemClient[], + mainAccount: ViemClient, + shuffle = false, +): Promise { + if (shuffle && accounts.length) { + shuffleArray(accounts); + } + const accs = accounts.length ? accounts : [mainAccount]; + return await pollSigners(accs); +} + +/** + * Polls an array of given signers in 30ms intervals + * until the first one becomes free for consumption + */ +export async function pollSigners(accounts: ViemClient[]): Promise { + for (;;) { + const acc = accounts.find((v) => !v.BUSY); + if (acc) { + return acc; + } else { + await sleep(30); + } + } +} diff --git a/src/types.ts b/src/types.ts index b88e4556..c11bd8d0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,6 +5,7 @@ import { DataFetcher, LiquidityProviders } from "sushi/router"; import { ProcessPairHaltReason, ProcessPairReportStatus } from "./processOrders"; import { Chain, + Account, HDAccount, TestClient, WalletClient, @@ -12,6 +13,7 @@ import { PublicActions, WalletActions, FallbackTransport, + SendTransactionParameters, } from "viem"; export type BotError = { @@ -129,11 +131,25 @@ export type OwnersProfileMap = Map; export type OrderbooksOwnersProfileMap = Map; export type ViemClient = WalletClient & - PublicActions & { BALANCE: BigNumber; BOUNTY: TokenDetails[]; BUSY: boolean }; + PublicActions & { + BALANCE: BigNumber; + BOUNTY: TokenDetails[]; + BUSY: boolean; + sendTx: ( + tx: SendTransactionParameters, + ) => Promise<`0x${string}`>; + }; export type TestViemClient = TestClient<"hardhat"> & PublicActions & - WalletActions & { BALANCE: BigNumber; BOUNTY: TokenDetails[]; BUSY: boolean }; + WalletActions & { + BALANCE: BigNumber; + BOUNTY: TokenDetails[]; + BUSY: boolean; + sendTx: ( + tx: SendTransactionParameters, + ) => Promise<`0x${string}`>; + }; export type BotDataFetcher = DataFetcher & { fetchedPairPools: string[] }; diff --git a/test/account.test.js b/test/account.test.js index af22f5a6..f8b2113e 100644 --- a/test/account.test.js +++ b/test/account.test.js @@ -1,5 +1,6 @@ const { assert } = require("chai"); const { ethers, viem } = require("hardhat"); +const { sendTransaction } = require("../src/tx"); const { publicActions, walletActions } = require("viem"); const { BridgeUnlimited, ConstantProductRPool } = require("sushi/tines"); const { WNATIVE, WNATIVE_ADDRESS, Native, DAI } = require("sushi/currency"); @@ -139,6 +140,15 @@ describe("Test accounts", async function () { ]); acc1.BOUNTY = ["0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270"]; acc2.BOUNTY = ["0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"]; + mainAccount.sendTx = async (tx) => { + return await sendTransaction(mainAccount, tx); + }; + acc1.sendTx = async (tx) => { + return await sendTransaction(acc1, tx); + }; + acc2.sendTx = async (tx) => { + return await sendTransaction(acc2, tx); + }; mainAccount.BALANCE = ethers.BigNumber.from("0x4563918244F40000"); acc1.BALANCE = ethers.BigNumber.from("10"); @@ -277,6 +287,7 @@ describe("Test accounts", async function () { estimateGas: async () => 25n, getBalance: async () => 10000n, sendTransaction: async () => "0x1234", + sendTx: async () => "0x1234", getTransactionCount: async () => 0, waitForTransactionReceipt: async () => ({ status: "success", @@ -360,6 +371,7 @@ describe("Test accounts", async function () { estimateGas: async () => 25n, getBalance: async () => 10000n, sendTransaction: async () => "0x1234", + sendTx: async () => "0x1234", getTransactionCount: async () => 0, call: async () => ({ data: "0x00" }), waitForTransactionReceipt: async () => ({ diff --git a/test/e2e/e2e.test.js b/test/e2e/e2e.test.js index c67d3b7d..3f0a2a73 100644 --- a/test/e2e/e2e.test.js +++ b/test/e2e/e2e.test.js @@ -5,6 +5,7 @@ const { ChainKey } = require("sushi"); const { clear } = require("../../src"); const { arbAbis } = require("../../src/abis"); const mockServer = require("mockttp").getLocal(); +const { sendTransaction } = require("../../src/tx"); const { ethers, viem, network } = require("hardhat"); const { Resource } = require("@opentelemetry/resources"); const { trace, context } = require("@opentelemetry/api"); @@ -96,6 +97,9 @@ for (let i = 0; i < testData.length; i++) { ) .extend(publicActions) .extend(walletActions); + bot.sendTx = async (tx) => { + return await sendTransaction(bot, tx); + }; bot.impersonateAccount({ address: botAddress ?? "0x22025257BeF969A81eDaC0b343ce82d777931327", }); @@ -357,6 +361,9 @@ for (let i = 0; i < testData.length; i++) { ) .extend(publicActions) .extend(walletActions); + bot.sendTx = async (tx) => { + return await sendTransaction(bot, tx); + }; bot.impersonateAccount({ address: botAddress ?? "0x22025257BeF969A81eDaC0b343ce82d777931327", }); @@ -705,6 +712,9 @@ for (let i = 0; i < testData.length; i++) { ) .extend(publicActions) .extend(walletActions); + bot.sendTx = async (tx) => { + return await sendTransaction(bot, tx); + }; bot.impersonateAccount({ address: botAddress ?? "0x22025257BeF969A81eDaC0b343ce82d777931327", }); diff --git a/test/processPair.test.js b/test/processPair.test.js index 0fde5299..141778eb 100644 --- a/test/processPair.test.js +++ b/test/processPair.test.js @@ -56,6 +56,7 @@ describe("Test process pair", async function () { getGasPrice: async () => gasPrice, estimateGas: async () => gasLimitEstimation, sendTransaction: async () => txHash, + sendTx: async () => txHash, getTransactionCount: async () => 0, waitForTransactionReceipt: async () => { return { @@ -100,20 +101,22 @@ describe("Test process pair", async function () { dataFetcher.getCurrentPoolCodeMap = () => { return poolCodeMap; }; - const result = await (await processPair({ - config, - orderPairObject, - viemClient, - dataFetcher, - signer, - arb, - orderbook, - pair, - mainAccount: signer, - accounts: [signer], - fetchedPairPools: [], - orderbooksOrders, - }))(); + const result = await ( + await processPair({ + config, + orderPairObject, + viemClient, + dataFetcher, + signer, + arb, + orderbook, + pair, + mainAccount: signer, + accounts: [signer], + fetchedPairPools: [], + orderbooksOrders, + }) + )(); const expected = { report: { status: ProcessPairReportStatus.FoundOpportunity, @@ -179,21 +182,23 @@ describe("Test process pair", async function () { return poolCodeMap; } else return new Map(); }; - const result = await (await processPair({ - config, - orderPairObject, - viemClient, - dataFetcher, - signer, - arb, - genericArb: arb, - orderbook, - pair, - mainAccount: signer, - accounts: [signer], - fetchedPairPools: [], - orderbooksOrders, - }))(); + const result = await ( + await processPair({ + config, + orderPairObject, + viemClient, + dataFetcher, + signer, + arb, + genericArb: arb, + orderbook, + pair, + mainAccount: signer, + accounts: [signer], + fetchedPairPools: [], + orderbooksOrders, + }) + )(); const expected = { report: { status: ProcessPairReportStatus.FoundOpportunity, @@ -255,20 +260,22 @@ describe("Test process pair", async function () { encodeQuoteResponse([[true, ethers.constants.Zero, ethers.constants.Zero]]), ); const orderPairObjectCopy = clone(orderPairObject); - const result = await (await processPair({ - config, - orderPairObject: orderPairObjectCopy, - viemClient, - dataFetcher, - signer, - flashbotSigner: undefined, - arb, - orderbook, - pair, - mainAccount: signer, - accounts: [signer], - fetchedPairPools: [], - }))(); + const result = await ( + await processPair({ + config, + orderPairObject: orderPairObjectCopy, + viemClient, + dataFetcher, + signer, + flashbotSigner: undefined, + arb, + orderbook, + pair, + mainAccount: signer, + accounts: [signer], + fetchedPairPools: [], + }) + )(); const expected = { reason: undefined, error: undefined, @@ -290,20 +297,22 @@ describe("Test process pair", async function () { it("should fail to quote order", async function () { await mockServer.forPost("/rpc").thenSendJsonRpcError(); try { - await (await processPair({ - config, - orderPairObject, - viemClient, - dataFetcher, - signer, - flashbotSigner: undefined, - arb, - orderbook, - pair, - mainAccount: signer, - accounts: [signer], - fetchedPairPools: [], - }))(); + await ( + await processPair({ + config, + orderPairObject, + viemClient, + dataFetcher, + signer, + flashbotSigner: undefined, + arb, + orderbook, + pair, + mainAccount: signer, + accounts: [signer], + fetchedPairPools: [], + }) + )(); assert.fail("expected to reject, but resolved"); } catch (error) { const expected = { @@ -332,20 +341,22 @@ describe("Test process pair", async function () { return Promise.reject(evmError); }; try { - await (await processPair({ - config, - orderPairObject, - viemClient, - dataFetcher, - signer, - flashbotSigner: undefined, - arb, - orderbook, - pair, - mainAccount: signer, - accounts: [signer], - fetchedPairPools: [], - }))(); + await ( + await processPair({ + config, + orderPairObject, + viemClient, + dataFetcher, + signer, + flashbotSigner: undefined, + arb, + orderbook, + pair, + mainAccount: signer, + accounts: [signer], + fetchedPairPools: [], + }) + )(); assert.fail("expected to reject, but resolved"); } catch (error) { const expected = { @@ -378,20 +389,22 @@ describe("Test process pair", async function () { return new Map(); }; try { - await (await processPair({ - config, - orderPairObject, - viemClient, - dataFetcher, - signer, - flashbotSigner: undefined, - arb, - orderbook, - pair, - mainAccount: signer, - accounts: [signer], - fetchedPairPools: [], - }))(); + await ( + await processPair({ + config, + orderPairObject, + viemClient, + dataFetcher, + signer, + flashbotSigner: undefined, + arb, + orderbook, + pair, + mainAccount: signer, + accounts: [signer], + fetchedPairPools: [], + }) + )(); assert.fail("expected to reject, but resolved"); } catch (error) { const expected = { @@ -427,20 +440,22 @@ describe("Test process pair", async function () { return Promise.reject(evmError); }; try { - await (await processPair({ - config, - orderPairObject, - viemClient, - dataFetcher, - signer, - flashbotSigner: undefined, - arb, - orderbook, - pair, - mainAccount: signer, - accounts: [signer], - fetchedPairPools: [], - }))(); + await ( + await processPair({ + config, + orderPairObject, + viemClient, + dataFetcher, + signer, + flashbotSigner: undefined, + arb, + orderbook, + pair, + mainAccount: signer, + accounts: [signer], + fetchedPairPools: [], + }) + )(); assert.fail("expected to reject, but resolved"); } catch (error) { const expected = { @@ -473,24 +488,26 @@ describe("Test process pair", async function () { dataFetcher.getCurrentPoolCodeMap = () => { return poolCodeMap; }; - signer.sendTransaction = async () => { + signer.sendTx = async () => { return Promise.reject(evmError); }; try { - await (await processPair({ - config, - orderPairObject, - viemClient, - dataFetcher, - signer, - flashbotSigner: undefined, - arb, - orderbook, - pair, - mainAccount: signer, - accounts: [signer], - fetchedPairPools: [], - }))(); + await ( + await processPair({ + config, + orderPairObject, + viemClient, + dataFetcher, + signer, + flashbotSigner: undefined, + arb, + orderbook, + pair, + mainAccount: signer, + accounts: [signer], + fetchedPairPools: [], + }) + )(); assert.fail("expected to reject, but resolved"); } catch (error) { const expectedTakeOrdersConfigStruct = { @@ -518,7 +535,6 @@ describe("Test process pair", async function () { to: arb.address, gasPrice: gasPrice.mul(107).div(100).toString(), gas: gasLimitEstimation.toString(), - nonce: 0, from: signer.account.address, }; const expected = { @@ -581,25 +597,27 @@ describe("Test process pair", async function () { dataFetcher.getCurrentPoolCodeMap = () => { return poolCodeMap; }; - signer.sendTransaction = async () => txHash; + signer.sendTx = async () => txHash; viemClient.waitForTransactionReceipt = async () => errorReceipt; viemClient.getTransaction = async () => ({}); viemClient.call = async () => Promise.reject("out of gas"); try { - await (await processPair({ - config, - orderPairObject, - viemClient, - dataFetcher, - signer, - flashbotSigner: undefined, - arb, - orderbook, - pair, - mainAccount: signer, - accounts: [signer], - fetchedPairPools: [], - }))(); + await ( + await processPair({ + config, + orderPairObject, + viemClient, + dataFetcher, + signer, + flashbotSigner: undefined, + arb, + orderbook, + pair, + mainAccount: signer, + accounts: [signer], + fetchedPairPools: [], + }) + )(); assert.fail("expected to reject, but resolved"); } catch (error) { const expected = { @@ -664,23 +682,25 @@ describe("Test process pair", async function () { dataFetcher.getCurrentPoolCodeMap = () => { return poolCodeMap; }; - signer.sendTransaction = async () => txHash; + signer.sendTx = async () => txHash; viemClient.waitForTransactionReceipt = async () => Promise.reject(errorRejection); try { - await (await processPair({ - config, - orderPairObject, - viemClient, - dataFetcher, - signer, - flashbotSigner: undefined, - arb, - orderbook, - pair, - mainAccount: signer, - accounts: [signer], - fetchedPairPools: [], - }))(); + await ( + await processPair({ + config, + orderPairObject, + viemClient, + dataFetcher, + signer, + flashbotSigner: undefined, + arb, + orderbook, + pair, + mainAccount: signer, + accounts: [signer], + fetchedPairPools: [], + }) + )(); assert.fail("expected to reject, but resolved"); } catch (error) { const expectedTakeOrdersConfigStruct = { @@ -708,7 +728,6 @@ describe("Test process pair", async function () { to: arb.address, gasPrice: gasPrice.mul(107).div(100).toString(), gas: gasLimitEstimation.toString(), - nonce: 0, from: signer.account.address, }; const expected = { diff --git a/test/tx.test.ts b/test/tx.test.ts index f3ba1b2c..1432e166 100644 --- a/test/tx.test.ts +++ b/test/tx.test.ts @@ -1,9 +1,16 @@ import { assert } from "chai"; import fixtures from "./data"; import { ethers } from "ethers"; -import { handleReceipt, handleTransaction } from "../src/tx"; -import { InsufficientFundsError, TransactionReceipt } from "viem"; import { ProcessPairHaltReason, ProcessPairReportStatus } from "../src/processOrders"; +import { InsufficientFundsError, InvalidInputRpcError, TransactionReceipt } from "viem"; +import { + getNonce, + getSigner, + pollSigners, + handleReceipt, + sendTransaction, + handleTransaction, +} from "../src/tx"; describe("Test tx", async function () { let signer: any = {}; @@ -31,6 +38,7 @@ describe("Test tx", async function () { BOUNTY: [], BUSY: false, sendTransaction: async () => txHash, + sendTx: async () => txHash, getTransactionCount: async () => 0, waitForTransactionReceipt: async () => { return { @@ -57,7 +65,7 @@ describe("Test tx", async function () { }; }); - it("handle transaction successfuly", async function () { + it("should handle transaction successfully", async function () { const spanAttributes = {}; const rawtx = { to: "0x" + "1".repeat(40), @@ -120,7 +128,7 @@ describe("Test tx", async function () { it("handle fail to submit transaction", async function () { // mock signer to reject the sendTransaction - signer.sendTransaction = async () => { + signer.sendTx = async () => { throw new InsufficientFundsError(); }; const spanAttributes = {}; @@ -174,7 +182,6 @@ describe("Test tx", async function () { "details.rawTx": JSON.stringify({ to: rawtx.to, data: "", - nonce: 0, from: signer.account.address, }), txNoneNodeError: false, @@ -184,7 +191,7 @@ describe("Test tx", async function () { } }); - it("should handle success transaction receipt successfuly", async function () { + it("should handle success transaction receipt successfully", async function () { const receipt: Promise = (async () => ({ status: "success", // success tx receipt effectiveGasPrice, @@ -251,7 +258,7 @@ describe("Test tx", async function () { assert.deepEqual(result, expected); }); - it("should handle revert transaction receipt successfuly", async function () { + it("should handle revert transaction receipt successfully", async function () { // mock signer to throw on tx simulation signer.call = async () => { throw new InsufficientFundsError(); @@ -324,4 +331,88 @@ describe("Test tx", async function () { assert.deepEqual(e, expected); } }); + + it("should test sendTransaction happy", async function () { + const rawtx: any = { + to: "0x" + "1".repeat(40), + data: "", + }; + const result = await sendTransaction(signer, rawtx); + assert.equal(result, txHash); + }); + + it("should test sendTransaction unhappy", async function () { + const rawtx: any = { + to: "0x" + "1".repeat(40), + data: "", + }; + try { + // mock inner send call to throw an error + signer.sendTransaction = async () => { + throw new InsufficientFundsError(); + }; + await sendTransaction(signer, rawtx); + assert.fail("expected to fail, but resolved"); + } catch (error) { + assert.deepEqual(error, new InsufficientFundsError()); + } + }); + + it("should test getNonce happy", async function () { + // should get nonce succesfully + const result = await getNonce(signer); + assert.equal(result, 0); + }); + + it("should test getNonce unhappy", async function () { + try { + // mock inner getTransactionCount to throw an error + signer.getTransactionCount = async () => { + throw new InvalidInputRpcError(new Error("some error")); + }; + await getNonce(signer); + assert.fail("expected to fail, but resolved"); + } catch (error) { + assert.deepEqual(error, new InvalidInputRpcError(new Error("some error"))); + } + }); + + it("should test getSigner", async function () { + // mock some signer accounts and main signer + const mainSigner: any = { BUSY: true }; + const someMockedSigners: any[] = [ + { BUSY: true }, + { BUSY: true }, + { BUSY: true }, + { BUSY: true }, + ]; + + // set timeout to free a signer after 2s + setTimeout(() => (someMockedSigners[2].BUSY = false), 2000); + // test multi account + let result = await getSigner(someMockedSigners, mainSigner); + assert.equal(result, someMockedSigners[2]); + + // set timeout to free main signer after 2s + setTimeout(() => (mainSigner.BUSY = false), 2000); + // test single account + result = await getSigner([], mainSigner); + assert.equal(result, mainSigner); + }); + + it("should test pollSigners", async function () { + // mock some signer accounts + const someMockedSigners: any[] = [ + { BUSY: true }, + { BUSY: true }, + { BUSY: true }, + { BUSY: true }, + ]; + + // set timeout to free a signer after 2s + setTimeout(() => (someMockedSigners[2].BUSY = false), 2000); + + const result = await pollSigners(someMockedSigners); + assert.equal(result, someMockedSigners[2]); + }); });