diff --git a/package.json b/package.json index 8f37bd6f..415ab7cc 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "scripts": { "prepublish": "npm run build", "test": "npm run unit-test && npm run e2e-test", - "unit-test": "hardhat test ./test/*.test.js", + "unit-test": "hardhat test ./test/*.test.*", "e2e-test": "hardhat test", "lint": "eslint ./src ./test ./arb-bot.js", "lint-fix": "eslint ./src ./test ./arb-bot.js --fix", diff --git a/src/error.ts b/src/error.ts index de3b136f..ed1447a9 100644 --- a/src/error.ts +++ b/src/error.ts @@ -96,6 +96,8 @@ export function errorSnapshot( if (gasErr) { message.push("Gas Error: " + gasErr); } + } else { + message.push("Comment: Found no additional info") } } } @@ -181,13 +183,9 @@ export async function handleRevert( gasPrice: tx.gasPrice, blockNumber: tx.blockNumber, }); - return { - err: "transaction reverted onchain and simulation failed to find out what was the revert reason, please try to simulate the tx manualy for more details", - nodeError: false, - snapshot: - "transaction reverted onchain and simulation failed to find out what was the revert reason, please try to simulate the tx manualy for more details", - }; - return undefined; + 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) { return { err, diff --git a/src/processOrders.ts b/src/processOrders.ts index 089a5b08..4ff660f2 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -185,6 +185,9 @@ export const processOrders = async ( }; // await for first available signer + if (accounts.length) { + shuffleArray(accounts); + } const signer = await getSigner(accounts, mainAccount); const writeSigner = config.writeRpc @@ -236,7 +239,7 @@ export const processOrders = async ( } } - for (const { settle, pair, orderPairObject } of results) { + for (const { settle, pair, orderPairObject } of results) { // instantiate a span for this pair const span = tracer.startSpan(`order_${pair}`, undefined, ctx); try { @@ -704,9 +707,6 @@ export async function processPair(args: { * Returns the first available signer by constantly polling the signers in 30ms intervals */ export async function getSigner(accounts: ViemClient[], mainAccount: ViemClient): Promise { - if (accounts.length) { - shuffleArray(accounts); - } const accs = accounts.length ? accounts : [mainAccount]; for (;;) { const acc = accs.find((v) => !v.BUSY); diff --git a/src/tx.ts b/src/tx.ts index b68fabb4..2ff99f13 100644 --- a/src/tx.ts +++ b/src/tx.ts @@ -36,8 +36,9 @@ export async function handleTransaction( writeSigner?: ViemClient, ): Promise<() => Promise> { // submit the tx - let txhash, txUrl; - try { + 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; @@ -56,34 +57,18 @@ export async function handleTransaction( 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 console.log("\x1b[33m%s\x1b[0m", txUrl, "\n"); spanAttributes["details.txUrl"] = txUrl; + }; + try { + await sendTx(); } catch (e) { try { // retry again after 5 seconds if first attempt failed await sleep(5000); - 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({ - ...rawtx, - type: "legacy", - }) - : await signer.sendTransaction({ - ...rawtx, - type: "legacy", - }); - - writeSigner !== undefined ? (writeSigner.BUSY = false) : (signer.BUSY = false); - txUrl = config.chain.blockExplorers?.default.url + "/tx/" + txhash; - // eslint-disable-next-line no-console - console.log("\x1b[33m%s\x1b[0m", txUrl, "\n"); - spanAttributes["details.txUrl"] = txUrl; + await sendTx(); } catch { writeSigner !== undefined ? (writeSigner.BUSY = false) : (signer.BUSY = false); // record rawtx in case it is not already present in the error @@ -105,7 +90,7 @@ export async function handleTransaction( // start getting tx receipt in background and return the resolver fn const receipt = viemClient.waitForTransactionReceipt({ - hash: txhash, + hash: txhash!, confirmations: 1, timeout: 120_000, }); @@ -116,7 +101,6 @@ export async function handleTransaction( txhash, await receipt, signer, - viemClient as any as ViemClient, spanAttributes, rawtx, orderbook, @@ -129,6 +113,7 @@ export async function handleTransaction( toToken, fromToken, config, + time, ); } catch (e: any) { try { @@ -138,7 +123,6 @@ export async function handleTransaction( txhash, newReceipt, signer, - viemClient as any as ViemClient, spanAttributes, rawtx, orderbook, @@ -151,6 +135,7 @@ export async function handleTransaction( toToken, fromToken, config, + time, ); } } catch {} @@ -194,7 +179,6 @@ export async function handleReceipt( txhash: string, receipt: TransactionReceipt, signer: ViemClient, - viemClient: ViemClient, spanAttributes: any, rawtx: RawTx, orderbook: Contract, @@ -207,15 +191,16 @@ export async function handleReceipt( toToken: Token, fromToken: Token, config: BotConfig, -) { + time: number, +): Promise { const actualGasCost = ethers.BigNumber.from(receipt.effectiveGasPrice).mul(receipt.gasUsed); const signerBalance = signer.BALANCE; signer.BALANCE = signer.BALANCE.sub(actualGasCost); + if (receipt.status === "success") { spanAttributes["didClear"] = true; const clearActualAmount = getActualClearAmount(rawtx.to, orderbook.address, receipt); - const inputTokenIncome = getIncome( signer.account.address, receipt, @@ -293,8 +278,14 @@ export async function handleReceipt( } return result; } else { + // wait at least 60s before simulating the revert tx + // in order for rpcs to catch up + const wait = 60000 - Date.now() + time; + if (wait > 0) { + await sleep(wait); + } const simulation = await handleRevert( - viemClient as any, + signer, txhash as `0x${string}`, receipt, rawtx, diff --git a/test/tx.test.ts b/test/tx.test.ts index 5d644916..f3ba1b2c 100644 --- a/test/tx.test.ts +++ b/test/tx.test.ts @@ -1,43 +1,35 @@ -import { ethers } from "ethers"; -import { getLocal } from "mockttp"; +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"; describe("Test tx", async function () { - const mockServer = getLocal(); - let signer = {}; - let viemClient = {}; + let signer: any = {}; + let viemClient: any = {}; const { - gasPrice, - gasLimitEstimation, - arb, - vaultBalance, orderPairObject1: orderPairObject, - config: fixtureConfig, - poolCodeMap, - expectedRouteVisual, + config, pair, orderbook, txHash, - effectiveGasPrice, - gasUsed, scannerUrl, - getCurrentPrice, - expectedRouteData, - getCurrentInputToEthPrice, - orderbooksOrders, - getAmountOut, + toToken, + fromToken, } = fixtures; + const gasUsed = 10n; + const effectiveGasPrice = 44n; + const inputToEthPrice = "1.5"; + const outputToEthPrice = "0.5"; beforeEach(() => { - mockServer.start(8090); - // config.gasCoveragePercentage = "0"; + // mock signer and viemClient before each test signer = { + chain: config.chain, account: { address: "0x1F1E4c845183EF6d50E9609F16f6f9cAE43BC9Cb" }, BALANCE: ethers.BigNumber.from(0), BOUNTY: [], - getAddress: () => "0x1F1E4c845183EF6d50E9609F16f6f9cAE43BC9Cb", - getBlockNumber: async () => 123456, - getGasPrice: async () => gasPrice, - estimateGas: async () => gasLimitEstimation, + BUSY: false, sendTransaction: async () => txHash, getTransactionCount: async () => 0, waitForTransactionReceipt: async () => { @@ -51,10 +43,7 @@ describe("Test tx", async function () { }, }; viemClient = { - chain: { id: 137 }, - multicall: async () => [vaultBalance.toBigInt()], - getGasPrice: async () => gasPrice.toBigInt(), - getBlockNumber: async () => 123456n, + chain: config.chain, getTransactionCount: async () => 0, waitForTransactionReceipt: async () => { return { @@ -67,15 +56,272 @@ describe("Test tx", async function () { }, }; }); - afterEach(() => mockServer.stop()); - it("handle transaction successfuly", async function () {}); - - it("handle fail to submit transaction", async function () {}); + it("handle transaction successfuly", async function () { + const spanAttributes = {}; + const rawtx = { + to: "0x" + "1".repeat(40), + data: "", + }; + const res = { + reason: undefined, + error: undefined, + gasCost: undefined, + spanAttributes, + report: { + status: ProcessPairReportStatus.FoundOpportunity, + tokenPair: pair, + buyToken: orderPairObject.buyToken, + sellToken: orderPairObject.sellToken, + }, + }; + const result = await ( + await handleTransaction( + signer, + viemClient, + spanAttributes, + rawtx as any, + orderbook, + orderPairObject, + inputToEthPrice, + outputToEthPrice, + res, + pair, + toToken, + fromToken, + config as any, + ) + )(); + const expected = { + reason: undefined, + error: undefined, + gasCost: ethers.BigNumber.from(gasUsed * effectiveGasPrice), + spanAttributes: { + "details.txUrl": scannerUrl + "/tx/" + txHash, + didClear: true, + }, + report: { + status: ProcessPairReportStatus.FoundOpportunity, + txUrl: scannerUrl + "/tx/" + txHash, + tokenPair: pair, + buyToken: orderPairObject.buyToken, + sellToken: orderPairObject.sellToken, + clearedAmount: undefined, + actualGasCost: ethers.utils.formatUnits(gasUsed * effectiveGasPrice), + income: undefined, + inputTokenIncome: undefined, + outputTokenIncome: undefined, + netProfit: undefined, + clearedOrders: [orderPairObject.takeOrders[0].id], + }, + }; + assert.deepEqual(result, expected); + }); - it("should handle success transaction successfuly", async function () {}); + it("handle fail to submit transaction", async function () { + // mock signer to reject the sendTransaction + signer.sendTransaction = async () => { + throw new InsufficientFundsError(); + }; + const spanAttributes = {}; + const rawtx = { + to: "0x" + "1".repeat(40), + data: "", + }; + const res = { + reason: undefined, + error: undefined, + gasCost: undefined, + spanAttributes, + report: { + status: ProcessPairReportStatus.FoundOpportunity, + tokenPair: pair, + buyToken: orderPairObject.buyToken, + sellToken: orderPairObject.sellToken, + }, + }; + try { + await ( + await handleTransaction( + signer, + viemClient, + spanAttributes, + rawtx as any, + orderbook, + orderPairObject, + inputToEthPrice, + outputToEthPrice, + res, + pair, + toToken, + fromToken, + config as any, + ) + )(); + assert.fail("expected to fail, but resolved"); + } catch (e) { + const x = { + error: new InsufficientFundsError(), + gasCost: undefined, + reason: ProcessPairHaltReason.TxFailed, + report: { + buyToken: orderPairObject.buyToken, + sellToken: orderPairObject.sellToken, + status: ProcessPairReportStatus.FoundOpportunity, + tokenPair: pair, + }, + spanAttributes: { + "details.rawTx": JSON.stringify({ + to: rawtx.to, + data: "", + nonce: 0, + from: signer.account.address, + }), + txNoneNodeError: false, + }, + }; + assert.deepEqual(e, x); + } + }); - it("should handle revert transaction successfuly", async function () {}); + it("should handle success transaction receipt successfuly", async function () { + const receipt: Promise = (async () => ({ + status: "success", // success tx receipt + effectiveGasPrice, + gasUsed, + logs: [], + events: [], + }))() as any; + const spanAttributes = {}; + const rawtx: any = { + to: "0x" + "1".repeat(40), + data: "", + }; + const txUrl = scannerUrl + "/tx/" + txHash; + const res = { + reason: undefined, + error: undefined, + gasCost: undefined, + spanAttributes, + report: { + status: ProcessPairReportStatus.FoundOpportunity, + tokenPair: pair, + buyToken: orderPairObject.buyToken, + sellToken: orderPairObject.sellToken, + }, + }; + const result = await handleReceipt( + txHash, + await receipt, + signer, + spanAttributes, + rawtx, + orderbook, + orderPairObject, + inputToEthPrice, + outputToEthPrice, + res, + txUrl, + pair, + toToken, + fromToken, + config as any, + 0, + ); + const expected = { + reason: undefined, + error: undefined, + gasCost: ethers.BigNumber.from(gasUsed * effectiveGasPrice), + spanAttributes: { didClear: true }, + report: { + status: 3, + txUrl, + tokenPair: pair, + buyToken: orderPairObject.buyToken, + sellToken: orderPairObject.sellToken, + clearedAmount: undefined, + actualGasCost: ethers.utils.formatUnits(gasUsed * effectiveGasPrice), + income: undefined, + inputTokenIncome: undefined, + outputTokenIncome: undefined, + netProfit: undefined, + clearedOrders: [orderPairObject.takeOrders[0].id], + }, + }; + assert.deepEqual(result, expected); + }); - it("should handle dropped transaction successfuly", async function () {}); + it("should handle revert transaction receipt successfuly", async function () { + // mock signer to throw on tx simulation + signer.call = async () => { + throw new InsufficientFundsError(); + }; + const receipt: Promise = (async () => ({ + status: "revert", // revert tx receipt + effectiveGasPrice, + gasUsed, + logs: [], + events: [], + }))() as any; + const spanAttributes = {}; + const rawtx: any = { + to: "0x" + "1".repeat(40), + data: "", + }; + const txUrl = scannerUrl + "/tx/" + txHash; + const res = { + reason: undefined, + error: undefined, + gasCost: undefined, + spanAttributes, + report: { + status: ProcessPairReportStatus.FoundOpportunity, + tokenPair: pair, + buyToken: orderPairObject.buyToken, + sellToken: orderPairObject.sellToken, + }, + }; + try { + await handleReceipt( + txHash, + await receipt, + signer, + spanAttributes, + rawtx, + orderbook, + orderPairObject, + inputToEthPrice, + outputToEthPrice, + res, + txUrl, + pair, + toToken, + fromToken, + config as any, + 0, + ); + assert.fail("expected to fail, but resolved"); + } catch (e) { + const expected = { + reason: ProcessPairHaltReason.TxReverted, + error: { + err: "transaction reverted onchain, account ran out of gas for transaction gas cost", + nodeError: false, + snapshot: + "transaction reverted onchain, account ran out of gas for transaction gas cost", + }, + gasCost: undefined, + spanAttributes: { txNoneNodeError: true }, + report: { + status: ProcessPairReportStatus.FoundOpportunity, + txUrl, + tokenPair: pair, + buyToken: orderPairObject.buyToken, + sellToken: orderPairObject.sellToken, + actualGasCost: ethers.utils.formatUnits(gasUsed * effectiveGasPrice), + }, + }; + assert.deepEqual(e, expected); + } + }); });