From 8811b322ff50cb4af3a4b0650b46f5edda5f3e17 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Thu, 5 Dec 2024 03:10:25 +0000 Subject: [PATCH 01/15] init [skip ci] --- src/error.ts | 2 +- src/processOrders.ts | 633 +++++++++++++-------------------------- src/tx.ts | 318 ++++++++++++++++++++ src/types.ts | 4 +- test/processPair.test.js | 44 +-- test/tx.test.ts | 81 +++++ 6 files changed, 640 insertions(+), 442 deletions(-) create mode 100644 src/tx.ts create mode 100644 test/tx.test.ts diff --git a/src/error.ts b/src/error.ts index 712dee45..de3b136f 100644 --- a/src/error.ts +++ b/src/error.ts @@ -170,7 +170,7 @@ 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({ diff --git a/src/processOrders.ts b/src/processOrders.ts index 87bf906d..5c5c3489 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -1,17 +1,17 @@ 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 { privateKeyToAccount } from "viem/accounts"; import { BigNumber, Contract, ethers } from "ethers"; import { Tracer } from "@opentelemetry/sdk-trace-base"; -import { Context, SpanStatusCode } from "@opentelemetry/api"; -import { BaseError, PublicClient, TransactionReceipt } from "viem"; -import { addWatchedToken, fundOwnedOrders, getNonce, rotateAccounts } from "./account"; -import { containsNodeError, ErrorSeverity, errorSnapshot, handleRevert, isTimeout } from "./error"; +import { Context, Span, SpanStatusCode } from "@opentelemetry/api"; +import { ErrorSeverity, errorSnapshot, isTimeout } from "./error"; import { - RawTx, Report, BotConfig, SpanAttrs, @@ -22,18 +22,16 @@ import { ProcessPairResult, } from "./types"; import { + sleep, toNumber, - getIncome, getEthPrice, quoteOrders, routeExists, + shuffleArray, PoolBlackList, getMarketQuote, - getTotalIncome, checkOwnedOrders, quoteSingleOrder, - getActualClearAmount, - withBigintSerializer, } from "./utils"; /** @@ -160,9 +158,16 @@ export const processOrders = async ( } catch (e) { throw errorSnapshot("Failed to batch quote orders", e); } + let avgGasCost: BigNumber | undefined; const reports: Report[] = []; + const results: { + settle: () => Promise; + span: Span; + pair: string; + orderPairObject: BundledOrders; + }[] = []; for (const orderbookOrders of bundledOrders) { for (const pairOrders of orderbookOrders) { // instantiating orderbook contract @@ -179,7 +184,8 @@ export const processOrders = async ( sellTokenDecimals: pairOrders.sellTokenDecimals, takeOrders: [pairOrders.takeOrders[i]], }; - const signer = accounts.length ? accounts[0] : mainAccount; + // const signer = accounts.length ? accounts[0] : mainAccount; + const signer = await getSigner(accounts, mainAccount); const writeSigner = config.writeRpc ? await createViemClient( config.chain.id as ChainId, @@ -207,7 +213,7 @@ export const processOrders = async ( // process the pair try { - const result = await processPair({ + const settle = await processPair({ config, orderPairObject, viemClient, @@ -219,186 +225,191 @@ export const processOrders = async ( orderbook, pair, orderbooksOrders: bundledOrders, + span, }); + results.push({ settle, span, pair, orderPairObject }); + } catch {} + } + } + } - // keep track of avggas cost - if (result.gasCost) { - if (!avgGasCost) { - avgGasCost = result.gasCost; - } else { - avgGasCost = avgGasCost.add(result.gasCost).div(2); - } - } + for (const { settle, span, pair, orderPairObject } of results) { + try { + const result = await settle(); + + // keep track of avg gas cost + if (result.gasCost) { + if (!avgGasCost) { + avgGasCost = result.gasCost; + } else { + avgGasCost = avgGasCost.add(result.gasCost).div(2); + } + } - reports.push(result.report); + reports.push(result.report); - // set the span attributes with the values gathered at processPair() - span.setAttributes(result.spanAttributes); + // set the span attributes with the values gathered at processPair() + span.setAttributes(result.spanAttributes); - // set the otel span status based on report status - if (result.report.status === ProcessPairReportStatus.ZeroOutput) { - span.setStatus({ code: SpanStatusCode.OK, message: "zero max output" }); - } else if (result.report.status === ProcessPairReportStatus.NoOpportunity) { - if (result.error && typeof result.error === "string") { - span.setStatus({ code: SpanStatusCode.ERROR, message: result.error }); + // set the otel span status based on report status + if (result.report.status === ProcessPairReportStatus.ZeroOutput) { + span.setStatus({ code: SpanStatusCode.OK, message: "zero max output" }); + } else if (result.report.status === ProcessPairReportStatus.NoOpportunity) { + if (result.error && typeof result.error === "string") { + span.setStatus({ code: SpanStatusCode.ERROR, message: result.error }); + } else { + span.setStatus({ code: SpanStatusCode.OK, message: "no opportunity" }); + } + } else if (result.report.status === ProcessPairReportStatus.FoundOpportunity) { + span.setStatus({ code: SpanStatusCode.OK, message: "found opportunity" }); + } else { + // set the span status to unexpected error + span.setAttribute("severity", ErrorSeverity.HIGH); + span.setStatus({ code: SpanStatusCode.ERROR, message: "unexpected error" }); + } + } catch (e: any) { + // keep track of avg gas cost + if (e.gasCost) { + if (!avgGasCost) { + avgGasCost = e.gasCost; + } else { + avgGasCost = avgGasCost.add(e.gasCost).div(2); + } + } + + // set the span attributes with the values gathered at processPair() + span.setAttributes(e.spanAttributes); + + // record otel span status based on reported reason + if (e.reason) { + // report the error reason along with the rest of report + reports.push({ + ...e.report, + error: e.error, + reason: e.reason, + }); + + // set the otel span status based on returned reason + if (e.reason === ProcessPairHaltReason.FailedToQuote) { + let message = + "failed to quote order: " + orderPairObject.takeOrders[0].id; + if (e.error) { + message = errorSnapshot(message, e.error); + } + span.setStatus({ code: SpanStatusCode.OK, message }); + } else if (e.reason === ProcessPairHaltReason.FailedToGetGasPrice) { + let message = pair + ": failed to get gas price"; + if (e.error) { + message = errorSnapshot(message, e.error); + span.recordException(e.error); + } + span.setAttribute("severity", ErrorSeverity.LOW); + span.setStatus({ code: SpanStatusCode.ERROR, message }); + } else if (e.reason === ProcessPairHaltReason.FailedToGetPools) { + let message = pair + ": failed to get pool details"; + if (e.error) { + message = errorSnapshot(message, e.error); + span.recordException(e.error); + } + span.setAttribute("severity", ErrorSeverity.MEDIUM); + span.setStatus({ code: SpanStatusCode.ERROR, message }); + } else if (e.reason === ProcessPairHaltReason.FailedToGetEthPrice) { + // set OK status because a token might not have a pool and as a result eth price cannot + // be fetched for it and if it is set to ERROR it will constantly error on each round + // resulting in lots of false positives + let message = "failed to get eth price"; + if (e.error) { + message = errorSnapshot(message, e.error); + span.setAttribute("errorDetails", message); + } + span.setStatus({ code: SpanStatusCode.OK, message }); + } else if (e.reason === ProcessPairHaltReason.TxFailed) { + // failed to submit the tx to mempool, this can happen for example when rpc rejects + // the tx for example because of low gas or invalid parameters, etc + let message = "failed to submit the transaction"; + if (e.error) { + message = errorSnapshot(message, e.error); + span.setAttribute("errorDetails", message); + if (isTimeout(e.error)) { + span.setAttribute("severity", ErrorSeverity.LOW); } else { - span.setStatus({ code: SpanStatusCode.OK, message: "no opportunity" }); + span.setAttribute("severity", ErrorSeverity.HIGH); } - } else if (result.report.status === ProcessPairReportStatus.FoundOpportunity) { - span.setStatus({ code: SpanStatusCode.OK, message: "found opportunity" }); } else { - // set the span status to unexpected error span.setAttribute("severity", ErrorSeverity.HIGH); - span.setStatus({ code: SpanStatusCode.ERROR, message: "unexpected error" }); } - } catch (e: any) { - // keep track of avg gas cost - if (e.gasCost) { - if (!avgGasCost) { - avgGasCost = e.gasCost; + span.setStatus({ code: SpanStatusCode.ERROR, message }); + span.setAttribute("unsuccessfulClear", true); + span.setAttribute("txSendFailed", true); + } else if (e.reason === ProcessPairHaltReason.TxReverted) { + // Tx reverted onchain, this can happen for example + // because of mev front running or false positive opportunities, etc + let message = ""; + if (e.error) { + if ("snapshot" in e.error) { + message = e.error.snapshot; } else { - avgGasCost = avgGasCost.add(e.gasCost).div(2); + message = errorSnapshot( + "transaction reverted onchain", + e.error.err, + ); } + span.setAttribute("errorDetails", message); } - - // set the span attributes with the values gathered at processPair() - span.setAttributes(e.spanAttributes); - - // record otel span status based on reported reason - if (e.reason) { - // report the error reason along with the rest of report - reports.push({ - ...e.report, - error: e.error, - reason: e.reason, - }); - - // set the otel span status based on returned reason - if (e.reason === ProcessPairHaltReason.FailedToQuote) { - let message = - "failed to quote order: " + orderPairObject.takeOrders[0].id; - if (e.error) { - message = errorSnapshot(message, e.error); - } - span.setStatus({ code: SpanStatusCode.OK, message }); - } else if (e.reason === ProcessPairHaltReason.FailedToGetGasPrice) { - let message = pair + ": failed to get gas price"; - if (e.error) { - message = errorSnapshot(message, e.error); - span.recordException(e.error); - } + if (e.spanAttributes["txNoneNodeError"]) { + span.setAttribute("severity", ErrorSeverity.HIGH); + } + span.setStatus({ code: SpanStatusCode.ERROR, message }); + span.setAttribute("unsuccessfulClear", true); + span.setAttribute("txReverted", true); + } else if (e.reason === ProcessPairHaltReason.TxMineFailed) { + // tx failed to get included onchain, this can happen as result of timeout, rpc dropping the tx, etc + let message = "transaction failed"; + if (e.error) { + message = errorSnapshot(message, e.error); + span.setAttribute("errorDetails", message); + if (isTimeout(e.error)) { span.setAttribute("severity", ErrorSeverity.LOW); - span.setStatus({ code: SpanStatusCode.ERROR, message }); - } else if (e.reason === ProcessPairHaltReason.FailedToGetPools) { - let message = pair + ": failed to get pool details"; - if (e.error) { - message = errorSnapshot(message, e.error); - span.recordException(e.error); - } - span.setAttribute("severity", ErrorSeverity.MEDIUM); - span.setStatus({ code: SpanStatusCode.ERROR, message }); - } else if (e.reason === ProcessPairHaltReason.FailedToGetEthPrice) { - // set OK status because a token might not have a pool and as a result eth price cannot - // be fetched for it and if it is set to ERROR it will constantly error on each round - // resulting in lots of false positives - let message = "failed to get eth price"; - if (e.error) { - message = errorSnapshot(message, e.error); - span.setAttribute("errorDetails", message); - } - span.setStatus({ code: SpanStatusCode.OK, message }); - } else if (e.reason === ProcessPairHaltReason.TxFailed) { - // failed to submit the tx to mempool, this can happen for example when rpc rejects - // the tx for example because of low gas or invalid parameters, etc - let message = "failed to submit the transaction"; - if (e.error) { - message = errorSnapshot(message, e.error); - span.setAttribute("errorDetails", message); - if (isTimeout(e.error)) { - span.setAttribute("severity", ErrorSeverity.LOW); - } else { - span.setAttribute("severity", ErrorSeverity.HIGH); - } - } else { - span.setAttribute("severity", ErrorSeverity.HIGH); - } - span.setStatus({ code: SpanStatusCode.ERROR, message }); - span.setAttribute("unsuccessfulClear", true); - span.setAttribute("txSendFailed", true); - } else if (e.reason === ProcessPairHaltReason.TxReverted) { - // Tx reverted onchain, this can happen for example - // because of mev front running or false positive opportunities, etc - let message = ""; - if (e.error) { - if ("snapshot" in e.error) { - message = e.error.snapshot; - } else { - message = errorSnapshot( - "transaction reverted onchain", - e.error.err, - ); - } - span.setAttribute("errorDetails", message); - } - if (e.spanAttributes["txNoneNodeError"]) { - span.setAttribute("severity", ErrorSeverity.HIGH); - } - span.setStatus({ code: SpanStatusCode.ERROR, message }); - span.setAttribute("unsuccessfulClear", true); - span.setAttribute("txReverted", true); - } else if (e.reason === ProcessPairHaltReason.TxMineFailed) { - // tx failed to get included onchain, this can happen as result of timeout, rpc dropping the tx, etc - let message = "transaction failed"; - if (e.error) { - message = errorSnapshot(message, e.error); - span.setAttribute("errorDetails", message); - if (isTimeout(e.error)) { - span.setAttribute("severity", ErrorSeverity.LOW); - } else { - span.setAttribute("severity", ErrorSeverity.HIGH); - } - } else { - span.setAttribute("severity", ErrorSeverity.HIGH); - } - span.setStatus({ code: SpanStatusCode.ERROR, message }); - span.setAttribute("unsuccessfulClear", true); - span.setAttribute("txMineFailed", true); } else { - // record the error for the span - let message = "unexpected error"; - if (e.error) { - message = errorSnapshot(message, e.error); - span.recordException(e.error); - } - // set the span status to unexpected error span.setAttribute("severity", ErrorSeverity.HIGH); - span.setStatus({ code: SpanStatusCode.ERROR, message }); } } else { - // record the error for the span - let message = "unexpected error"; - if (e.error) { - message = errorSnapshot(message, e.error); - span.recordException(e.error); - } - - // report the unexpected error reason - reports.push({ - ...e.report, - error: e.error, - reason: ProcessPairHaltReason.UnexpectedError, - }); - // set the span status to unexpected error span.setAttribute("severity", ErrorSeverity.HIGH); - span.setStatus({ code: SpanStatusCode.ERROR, message }); } + span.setStatus({ code: SpanStatusCode.ERROR, message }); + span.setAttribute("unsuccessfulClear", true); + span.setAttribute("txMineFailed", true); + } else { + // record the error for the span + let message = "unexpected error"; + if (e.error) { + message = errorSnapshot(message, e.error); + span.recordException(e.error); + } + // set the span status to unexpected error + span.setAttribute("severity", ErrorSeverity.HIGH); + span.setStatus({ code: SpanStatusCode.ERROR, message }); + } + } else { + // record the error for the span + let message = "unexpected error"; + if (e.error) { + message = errorSnapshot(message, e.error); + span.recordException(e.error); } - span.end(); - // rotate the accounts once they are used once - rotateAccounts(accounts); + // report the unexpected error reason + reports.push({ + ...e.report, + error: e.error, + reason: ProcessPairHaltReason.UnexpectedError, + }); + // set the span status to unexpected error + span.setAttribute("severity", ErrorSeverity.HIGH); + span.setStatus({ code: SpanStatusCode.ERROR, message }); } } + span.end(); } return { reports, avgGasCost }; }; @@ -418,7 +429,8 @@ export async function processPair(args: { orderbook: Contract; pair: string; orderbooksOrders: BundledOrders[][]; -}): Promise { + span: Span, +}): Promise<() => Promise> { const { config, orderPairObject, @@ -475,12 +487,12 @@ export async function processPair(args: { buyToken: orderPairObject.buyToken, sellToken: orderPairObject.sellToken, }; - return result; + return async () => { return result; }; } } catch (e) { result.error = e; result.reason = ProcessPairHaltReason.FailedToQuote; - throw result; + return async () => { throw result; }; } spanAttributes["details.quote"] = JSON.stringify({ @@ -497,7 +509,7 @@ export async function processPair(args: { } catch (e) { result.reason = ProcessPairHaltReason.FailedToGetGasPrice; result.error = e; - throw result; + return async () => { throw result; }; } // get pool details @@ -521,7 +533,7 @@ export async function processPair(args: { } catch (e) { result.reason = ProcessPairHaltReason.FailedToGetPools; result.error = e; - throw result; + return async () => { throw result; }; } } @@ -572,7 +584,7 @@ export async function processPair(args: { } else { result.reason = ProcessPairHaltReason.FailedToGetEthPrice; result.error = "no-route"; - return Promise.reject(result); + return async () => { return Promise.reject(result); }; } } else { const p1 = `${orderPairObject.buyTokenSymbol}/${config.nativeWrappedToken.symbol}`; @@ -589,7 +601,7 @@ export async function processPair(args: { } else { result.reason = ProcessPairHaltReason.FailedToGetEthPrice; result.error = e; - throw result; + return async () => { throw result; }; } } @@ -641,7 +653,7 @@ export async function processPair(args: { buyToken: orderPairObject.buyToken, sellToken: orderPairObject.sellToken, }; - 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 @@ -665,252 +677,39 @@ export async function processPair(args: { spanAttributes["details.blockNumberError"] = errorSnapshot("failed to get block number", e); } - // submit the tx - let txhash, txUrl; - try { - rawtx.nonce = await getNonce(writeSigner !== undefined ? writeSigner : signer); - if (writeSigner !== undefined) { - rawtx.gas = undefined; - } - txhash = - writeSigner !== undefined - ? await writeSigner.sendTransaction({ - ...rawtx, - type: "legacy", - }) - : await signer.sendTransaction({ - ...rawtx, - type: "legacy", - }); - - 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; - } catch (e) { - // record rawtx in case it is not already present in the error - spanAttributes["details.rawTx"] = JSON.stringify( - { - ...rawtx, - from: signer.account.address, - }, - withBigintSerializer, - ); - spanAttributes["txNoneNodeError"] = !containsNodeError(e as BaseError); - result.error = e; - result.reason = ProcessPairHaltReason.TxFailed; - throw result; - } - - // wait for tx receipt - try { - const receipt = await viemClient.waitForTransactionReceipt({ - hash: txhash, - confirmations: 1, - timeout: 120_000, - }); - return handleReceipt( - txhash, - receipt, - signer, - viemClient as any as ViemClient, - spanAttributes, - rawtx, - orderbook, - orderPairObject, - inputToEthPrice, - outputToEthPrice, - result, - txUrl, - pair, - toToken, - fromToken, - config, - ); - } catch (e: any) { - try { - const newReceipt = await viemClient.getTransactionReceipt({ hash: txhash }); - if (newReceipt) { - return handleReceipt( - txhash, - newReceipt, - signer, - viemClient as any as ViemClient, - spanAttributes, - rawtx, - orderbook, - orderPairObject, - inputToEthPrice, - outputToEthPrice, - result, - txUrl, - pair, - toToken, - fromToken, - config, - ); - } - } catch {} - // keep track of gas consumption of the account - let actualGasCost; - try { - actualGasCost = ethers.BigNumber.from(e.receipt.effectiveGasPrice).mul( - e.receipt.gasUsed, - ); - signer.BALANCE = signer.BALANCE.sub(actualGasCost); - } catch { - /**/ - } - result.report = { - status: ProcessPairReportStatus.FoundOpportunity, - txUrl, - tokenPair: pair, - buyToken: orderPairObject.buyToken, - sellToken: orderPairObject.sellToken, - }; - if (actualGasCost) { - result.report.actualGasCost = ethers.utils.formatUnits(actualGasCost); - } - result.error = e; - spanAttributes["details.rawTx"] = JSON.stringify( - { - ...rawtx, - from: signer.account.address, - }, - withBigintSerializer, - ); - spanAttributes["txNoneNodeError"] = !containsNodeError(e); - result.reason = ProcessPairHaltReason.TxMineFailed; - throw result; - } + // call and return the tx processor + return handleTransaction( + signer, + viemClient as any as ViemClient, + spanAttributes, + rawtx, + orderbook, + orderPairObject, + inputToEthPrice, + outputToEthPrice, + result, + pair, + toToken, + fromToken, + config, + writeSigner, + ); } /** - * Handles the tx receipt + * Returns the first available signer by constantly polling the signers in 30ms intervals */ -export async function handleReceipt( - txhash: string, - receipt: TransactionReceipt, - signer: ViemClient, - viemClient: ViemClient, - spanAttributes: any, - rawtx: RawTx, - orderbook: Contract, - orderPairObject: BundledOrders, - inputToEthPrice: string, - outputToEthPrice: string, - result: any, - txUrl: string, - pair: string, - toToken: Token, - fromToken: Token, - config: BotConfig, -) { - 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, - orderPairObject.buyToken, - ); - const outputTokenIncome = getIncome( - signer.account.address, - receipt, - orderPairObject.sellToken, - ); - const income = getTotalIncome( - inputTokenIncome, - outputTokenIncome, - inputToEthPrice, - outputToEthPrice, - orderPairObject.buyTokenDecimals, - orderPairObject.sellTokenDecimals, - ); - const netProfit = income ? income.sub(actualGasCost) : undefined; - - if (income) { - spanAttributes["details.income"] = toNumber(income); - spanAttributes["details.netProfit"] = toNumber(netProfit!); - spanAttributes["details.actualGasCost"] = toNumber(actualGasCost); - } - if (inputTokenIncome) { - spanAttributes["details.inputTokenIncome"] = ethers.utils.formatUnits( - inputTokenIncome, - orderPairObject.buyTokenDecimals, - ); - } - if (outputTokenIncome) { - spanAttributes["details.outputTokenIncome"] = ethers.utils.formatUnits( - outputTokenIncome, - orderPairObject.sellTokenDecimals, - ); - } - - result.report = { - status: ProcessPairReportStatus.FoundOpportunity, - txUrl, - tokenPair: pair, - buyToken: orderPairObject.buyToken, - sellToken: orderPairObject.sellToken, - clearedAmount: clearActualAmount?.toString(), - actualGasCost: ethers.utils.formatUnits(actualGasCost), - income, - inputTokenIncome: inputTokenIncome - ? ethers.utils.formatUnits(inputTokenIncome, toToken.decimals) - : undefined, - outputTokenIncome: outputTokenIncome - ? ethers.utils.formatUnits(outputTokenIncome, fromToken.decimals) - : undefined, - netProfit, - clearedOrders: orderPairObject.takeOrders.map((v) => v.id), - }; - - // keep track of gas consumption of the account and bounty token - result.gasCost = actualGasCost; - if (inputTokenIncome && inputTokenIncome.gt(0)) { - const tkn = { - address: orderPairObject.buyToken.toLowerCase(), - decimals: orderPairObject.buyTokenDecimals, - symbol: orderPairObject.buyTokenSymbol, - }; - addWatchedToken(tkn, config.watchedTokens ?? [], signer); - } - if (outputTokenIncome && outputTokenIncome.gt(0)) { - const tkn = { - address: orderPairObject.sellToken.toLowerCase(), - decimals: orderPairObject.sellTokenDecimals, - symbol: orderPairObject.sellTokenSymbol, - }; - addWatchedToken(tkn, config.watchedTokens ?? [], signer); - } - return result; - } else { - const simulation = await handleRevert( - viemClient as any, - txhash as `0x${string}`, - receipt, - rawtx, - signerBalance, - ); - if (simulation) { - result.error = simulation; - spanAttributes["txNoneNodeError"] = !simulation.nodeError; +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); + if (acc) { + return acc; + } else { + await sleep(30); } - result.report = { - status: ProcessPairReportStatus.FoundOpportunity, - txUrl, - tokenPair: pair, - buyToken: orderPairObject.buyToken, - sellToken: orderPairObject.sellToken, - actualGasCost: ethers.utils.formatUnits(actualGasCost), - }; - result.reason = ProcessPairHaltReason.TxReverted; - return Promise.reject(result); } } diff --git a/src/tx.ts b/src/tx.ts new file mode 100644 index 00000000..b68fabb4 --- /dev/null +++ b/src/tx.ts @@ -0,0 +1,318 @@ +import { Token } from "sushi/currency"; +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 { + toNumber, + getIncome, + getTotalIncome, + withBigintSerializer, + getActualClearAmount, + sleep, +} from "./utils"; + +/** + * Handles the given transaction, starts by sending the transaction and + * then tries to get the receipt and process that in async manner, returns + * a function that resolves with the ProcessOrderResult type when called + */ +export async function handleTransaction( + signer: ViemClient, + viemClient: ViemClient, + spanAttributes: any, + rawtx: RawTx, + orderbook: Contract, + orderPairObject: BundledOrders, + inputToEthPrice: string, + outputToEthPrice: string, + result: ProcessPairResult, + pair: string, + toToken: Token, + fromToken: Token, + config: BotConfig, + writeSigner?: ViemClient, +): Promise<() => Promise> { + // submit the tx + let txhash, txUrl; + try { + 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; + } 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; + } catch { + writeSigner !== undefined ? (writeSigner.BUSY = false) : (signer.BUSY = false); + // record rawtx in case it is not already present in the error + spanAttributes["details.rawTx"] = JSON.stringify( + { + ...rawtx, + from: signer.account.address, + }, + withBigintSerializer, + ); + spanAttributes["txNoneNodeError"] = !containsNodeError(e as BaseError); + result.error = e; + result.reason = ProcessPairHaltReason.TxFailed; + return async () => { + throw result; + }; + } + } + + // start getting tx receipt in background and return the resolver fn + const receipt = viemClient.waitForTransactionReceipt({ + hash: txhash, + confirmations: 1, + timeout: 120_000, + }); + return async () => { + // wait for tx receipt + try { + return handleReceipt( + txhash, + await receipt, + signer, + viemClient as any as ViemClient, + spanAttributes, + rawtx, + orderbook, + orderPairObject, + inputToEthPrice, + outputToEthPrice, + result, + txUrl, + pair, + toToken, + fromToken, + config, + ); + } catch (e: any) { + try { + const newReceipt = await viemClient.getTransactionReceipt({ hash: txhash }); + if (newReceipt) { + return handleReceipt( + txhash, + newReceipt, + signer, + viemClient as any as ViemClient, + spanAttributes, + rawtx, + orderbook, + orderPairObject, + inputToEthPrice, + outputToEthPrice, + result, + txUrl, + pair, + toToken, + fromToken, + config, + ); + } + } catch {} + // keep track of gas consumption of the account + let actualGasCost; + try { + actualGasCost = BigNumber.from(e.receipt.effectiveGasPrice).mul(e.receipt.gasUsed); + signer.BALANCE = signer.BALANCE.sub(actualGasCost); + } catch { + /**/ + } + result.report = { + status: ProcessPairReportStatus.FoundOpportunity, + txUrl, + tokenPair: pair, + buyToken: orderPairObject.buyToken, + sellToken: orderPairObject.sellToken, + }; + if (actualGasCost) { + result.report.actualGasCost = ethers.utils.formatUnits(actualGasCost); + } + result.error = e; + spanAttributes["details.rawTx"] = JSON.stringify( + { + ...rawtx, + from: signer.account.address, + }, + withBigintSerializer, + ); + spanAttributes["txNoneNodeError"] = !containsNodeError(e); + result.reason = ProcessPairHaltReason.TxMineFailed; + throw result; + } + }; +} + +/** + * Handles the tx receipt + */ +export async function handleReceipt( + txhash: string, + receipt: TransactionReceipt, + signer: ViemClient, + viemClient: ViemClient, + spanAttributes: any, + rawtx: RawTx, + orderbook: Contract, + orderPairObject: BundledOrders, + inputToEthPrice: string, + outputToEthPrice: string, + result: ProcessPairResult, + txUrl: string, + pair: string, + toToken: Token, + fromToken: Token, + config: BotConfig, +) { + 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, + orderPairObject.buyToken, + ); + const outputTokenIncome = getIncome( + signer.account.address, + receipt, + orderPairObject.sellToken, + ); + const income = getTotalIncome( + inputTokenIncome, + outputTokenIncome, + inputToEthPrice, + outputToEthPrice, + orderPairObject.buyTokenDecimals, + orderPairObject.sellTokenDecimals, + ); + const netProfit = income ? income.sub(actualGasCost) : undefined; + + if (income) { + spanAttributes["details.income"] = toNumber(income); + spanAttributes["details.netProfit"] = toNumber(netProfit!); + spanAttributes["details.actualGasCost"] = toNumber(actualGasCost); + } + if (inputTokenIncome) { + spanAttributes["details.inputTokenIncome"] = ethers.utils.formatUnits( + inputTokenIncome, + orderPairObject.buyTokenDecimals, + ); + } + if (outputTokenIncome) { + spanAttributes["details.outputTokenIncome"] = ethers.utils.formatUnits( + outputTokenIncome, + orderPairObject.sellTokenDecimals, + ); + } + + result.report = { + status: ProcessPairReportStatus.FoundOpportunity, + txUrl, + tokenPair: pair, + buyToken: orderPairObject.buyToken, + sellToken: orderPairObject.sellToken, + clearedAmount: clearActualAmount?.toString(), + actualGasCost: ethers.utils.formatUnits(actualGasCost), + income, + inputTokenIncome: inputTokenIncome + ? ethers.utils.formatUnits(inputTokenIncome, toToken.decimals) + : undefined, + outputTokenIncome: outputTokenIncome + ? ethers.utils.formatUnits(outputTokenIncome, fromToken.decimals) + : undefined, + netProfit, + clearedOrders: orderPairObject.takeOrders.map((v) => v.id), + }; + + // keep track of gas consumption of the account and bounty token + result.gasCost = actualGasCost; + if (inputTokenIncome && inputTokenIncome.gt(0)) { + const tkn = { + address: orderPairObject.buyToken.toLowerCase(), + decimals: orderPairObject.buyTokenDecimals, + symbol: orderPairObject.buyTokenSymbol, + }; + addWatchedToken(tkn, config.watchedTokens ?? [], signer); + } + if (outputTokenIncome && outputTokenIncome.gt(0)) { + const tkn = { + address: orderPairObject.sellToken.toLowerCase(), + decimals: orderPairObject.sellTokenDecimals, + symbol: orderPairObject.sellTokenSymbol, + }; + addWatchedToken(tkn, config.watchedTokens ?? [], signer); + } + return result; + } else { + const simulation = await handleRevert( + viemClient as any, + txhash as `0x${string}`, + receipt, + rawtx, + signerBalance, + ); + if (simulation) { + result.error = simulation; + spanAttributes["txNoneNodeError"] = !simulation.nodeError; + } + result.report = { + status: ProcessPairReportStatus.FoundOpportunity, + txUrl, + tokenPair: pair, + buyToken: orderPairObject.buyToken, + sellToken: orderPairObject.sellToken, + actualGasCost: ethers.utils.formatUnits(actualGasCost), + }; + result.reason = ProcessPairHaltReason.TxReverted; + return Promise.reject(result); + } +} diff --git a/src/types.ts b/src/types.ts index 0db97980..b88e4556 100644 --- a/src/types.ts +++ b/src/types.ts @@ -129,11 +129,11 @@ export type OwnersProfileMap = Map; export type OrderbooksOwnersProfileMap = Map; export type ViemClient = WalletClient & - PublicActions & { BALANCE: BigNumber; BOUNTY: TokenDetails[] }; + PublicActions & { BALANCE: BigNumber; BOUNTY: TokenDetails[]; BUSY: boolean }; export type TestViemClient = TestClient<"hardhat"> & PublicActions & - WalletActions & { BALANCE: BigNumber; BOUNTY: TokenDetails[] }; + WalletActions & { BALANCE: BigNumber; BOUNTY: TokenDetails[]; BUSY: boolean }; export type BotDataFetcher = DataFetcher & { fetchedPairPools: string[] }; diff --git a/test/processPair.test.js b/test/processPair.test.js index aaf74cce..0fde5299 100644 --- a/test/processPair.test.js +++ b/test/processPair.test.js @@ -100,7 +100,7 @@ describe("Test process pair", async function () { dataFetcher.getCurrentPoolCodeMap = () => { return poolCodeMap; }; - const result = await processPair({ + const result = await (await processPair({ config, orderPairObject, viemClient, @@ -113,7 +113,7 @@ describe("Test process pair", async function () { accounts: [signer], fetchedPairPools: [], orderbooksOrders, - }); + }))(); const expected = { report: { status: ProcessPairReportStatus.FoundOpportunity, @@ -179,7 +179,7 @@ describe("Test process pair", async function () { return poolCodeMap; } else return new Map(); }; - const result = await processPair({ + const result = await (await processPair({ config, orderPairObject, viemClient, @@ -193,7 +193,7 @@ describe("Test process pair", async function () { accounts: [signer], fetchedPairPools: [], orderbooksOrders, - }); + }))(); const expected = { report: { status: ProcessPairReportStatus.FoundOpportunity, @@ -255,7 +255,7 @@ describe("Test process pair", async function () { encodeQuoteResponse([[true, ethers.constants.Zero, ethers.constants.Zero]]), ); const orderPairObjectCopy = clone(orderPairObject); - const result = await processPair({ + const result = await (await processPair({ config, orderPairObject: orderPairObjectCopy, viemClient, @@ -268,7 +268,7 @@ describe("Test process pair", async function () { mainAccount: signer, accounts: [signer], fetchedPairPools: [], - }); + }))(); const expected = { reason: undefined, error: undefined, @@ -290,7 +290,7 @@ describe("Test process pair", async function () { it("should fail to quote order", async function () { await mockServer.forPost("/rpc").thenSendJsonRpcError(); try { - await processPair({ + await (await processPair({ config, orderPairObject, viemClient, @@ -303,7 +303,7 @@ describe("Test process pair", async function () { mainAccount: signer, accounts: [signer], fetchedPairPools: [], - }); + }))(); assert.fail("expected to reject, but resolved"); } catch (error) { const expected = { @@ -332,7 +332,7 @@ describe("Test process pair", async function () { return Promise.reject(evmError); }; try { - await processPair({ + await (await processPair({ config, orderPairObject, viemClient, @@ -345,7 +345,7 @@ describe("Test process pair", async function () { mainAccount: signer, accounts: [signer], fetchedPairPools: [], - }); + }))(); assert.fail("expected to reject, but resolved"); } catch (error) { const expected = { @@ -378,7 +378,7 @@ describe("Test process pair", async function () { return new Map(); }; try { - await processPair({ + await (await processPair({ config, orderPairObject, viemClient, @@ -391,7 +391,7 @@ describe("Test process pair", async function () { mainAccount: signer, accounts: [signer], fetchedPairPools: [], - }); + }))(); assert.fail("expected to reject, but resolved"); } catch (error) { const expected = { @@ -427,7 +427,7 @@ describe("Test process pair", async function () { return Promise.reject(evmError); }; try { - await processPair({ + await (await processPair({ config, orderPairObject, viemClient, @@ -440,7 +440,7 @@ describe("Test process pair", async function () { mainAccount: signer, accounts: [signer], fetchedPairPools: [], - }); + }))(); assert.fail("expected to reject, but resolved"); } catch (error) { const expected = { @@ -477,7 +477,7 @@ describe("Test process pair", async function () { return Promise.reject(evmError); }; try { - await processPair({ + await (await processPair({ config, orderPairObject, viemClient, @@ -490,7 +490,7 @@ describe("Test process pair", async function () { mainAccount: signer, accounts: [signer], fetchedPairPools: [], - }); + }))(); assert.fail("expected to reject, but resolved"); } catch (error) { const expectedTakeOrdersConfigStruct = { @@ -586,7 +586,7 @@ describe("Test process pair", async function () { viemClient.getTransaction = async () => ({}); viemClient.call = async () => Promise.reject("out of gas"); try { - await processPair({ + await (await processPair({ config, orderPairObject, viemClient, @@ -599,7 +599,7 @@ describe("Test process pair", async function () { mainAccount: signer, accounts: [signer], fetchedPairPools: [], - }); + }))(); assert.fail("expected to reject, but resolved"); } catch (error) { const expected = { @@ -613,10 +613,10 @@ describe("Test process pair", async function () { }, reason: ProcessPairHaltReason.TxReverted, error: { - err: "transaction reverted onchainaccount ran out of gas for transaction gas cost", + err: "transaction reverted onchain, account ran out of gas for transaction gas cost", nodeError: false, snapshot: - "transaction reverted onchainaccount ran out of gas for transaction gas cost", + "transaction reverted onchain, account ran out of gas for transaction gas cost", }, gasCost: undefined, spanAttributes: { @@ -667,7 +667,7 @@ describe("Test process pair", async function () { signer.sendTransaction = async () => txHash; viemClient.waitForTransactionReceipt = async () => Promise.reject(errorRejection); try { - await processPair({ + await (await processPair({ config, orderPairObject, viemClient, @@ -680,7 +680,7 @@ describe("Test process pair", async function () { mainAccount: signer, accounts: [signer], fetchedPairPools: [], - }); + }))(); assert.fail("expected to reject, but resolved"); } catch (error) { const expectedTakeOrdersConfigStruct = { diff --git a/test/tx.test.ts b/test/tx.test.ts new file mode 100644 index 00000000..5d644916 --- /dev/null +++ b/test/tx.test.ts @@ -0,0 +1,81 @@ +import { ethers } from "ethers"; +import { getLocal } from "mockttp"; +import fixtures from "./data"; + +describe("Test tx", async function () { + const mockServer = getLocal(); + let signer = {}; + let viemClient = {}; + const { + gasPrice, + gasLimitEstimation, + arb, + vaultBalance, + orderPairObject1: orderPairObject, + config: fixtureConfig, + poolCodeMap, + expectedRouteVisual, + pair, + orderbook, + txHash, + effectiveGasPrice, + gasUsed, + scannerUrl, + getCurrentPrice, + expectedRouteData, + getCurrentInputToEthPrice, + orderbooksOrders, + getAmountOut, + } = fixtures; + beforeEach(() => { + mockServer.start(8090); + // config.gasCoveragePercentage = "0"; + signer = { + account: { address: "0x1F1E4c845183EF6d50E9609F16f6f9cAE43BC9Cb" }, + BALANCE: ethers.BigNumber.from(0), + BOUNTY: [], + getAddress: () => "0x1F1E4c845183EF6d50E9609F16f6f9cAE43BC9Cb", + getBlockNumber: async () => 123456, + getGasPrice: async () => gasPrice, + estimateGas: async () => gasLimitEstimation, + sendTransaction: async () => txHash, + getTransactionCount: async () => 0, + waitForTransactionReceipt: async () => { + return { + status: "success", + effectiveGasPrice, + gasUsed, + logs: [], + events: [], + }; + }, + }; + viemClient = { + chain: { id: 137 }, + multicall: async () => [vaultBalance.toBigInt()], + getGasPrice: async () => gasPrice.toBigInt(), + getBlockNumber: async () => 123456n, + getTransactionCount: async () => 0, + waitForTransactionReceipt: async () => { + return { + status: "success", + effectiveGasPrice, + gasUsed, + logs: [], + events: [], + }; + }, + }; + }); + afterEach(() => mockServer.stop()); + + it("handle transaction successfuly", async function () {}); + + it("handle fail to submit transaction", async function () {}); + + it("should handle success transaction successfuly", async function () {}); + + it("should handle revert transaction successfuly", async function () {}); + + it("should handle dropped transaction successfuly", async function () {}); +}); From a744f25a626401a5f42013931600426607f428e9 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Thu, 5 Dec 2024 04:09:55 +0000 Subject: [PATCH 02/15] update [skip ci] --- src/processOrders.ts | 52 ++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/processOrders.ts b/src/processOrders.ts index 5c5c3489..0ab934cd 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -164,7 +164,6 @@ export const processOrders = async ( const reports: Report[] = []; const results: { settle: () => Promise; - span: Span; pair: string; orderPairObject: BundledOrders; }[] = []; @@ -184,8 +183,10 @@ export const processOrders = async ( sellTokenDecimals: pairOrders.sellTokenDecimals, takeOrders: [pairOrders.takeOrders[i]], }; - // const signer = accounts.length ? accounts[0] : mainAccount; + + // await for first available signer const signer = await getSigner(accounts, mainAccount); + const writeSigner = config.writeRpc ? await createViemClient( config.chain.id as ChainId, @@ -207,33 +208,38 @@ export const processOrders = async ( : undefined; const pair = `${pairOrders.buyTokenSymbol}/${pairOrders.sellTokenSymbol}`; - - // instantiate a span for this pair - const span = tracer.startSpan(`order_${pair}`, undefined, ctx); + const span = tracer.startSpan("checkpoint", undefined, ctx); + span.setAttributes({ + "details.pair": pair, + "details.order": orderPairObject.takeOrders[0].id, + "details.orderbook": orderbook.address, + "details.account": signer.account.address, + }); // process the pair - try { - const settle = await processPair({ - config, - orderPairObject, - viemClient, - dataFetcher, - signer, - writeSigner, - arb, - genericArb, - orderbook, - pair, - orderbooksOrders: bundledOrders, - span, - }); - results.push({ settle, span, pair, orderPairObject }); - } catch {} + const settle = await processPair({ + config, + orderPairObject, + viemClient, + dataFetcher, + signer, + writeSigner, + arb, + genericArb, + orderbook, + pair, + orderbooksOrders: bundledOrders, + span, + }); + results.push({ settle, pair, orderPairObject }); + span.end(); } } } - for (const { settle, span, 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 { const result = await settle(); From e257b12fbe177ec7a22400a44a474edd67ba3f08 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Thu, 5 Dec 2024 04:11:45 +0000 Subject: [PATCH 03/15] update [skip ci] --- src/processOrders.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/processOrders.ts b/src/processOrders.ts index 0ab934cd..089a5b08 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -9,7 +9,7 @@ import { arbAbis, orderbookAbi } from "./abis"; import { privateKeyToAccount } from "viem/accounts"; import { BigNumber, Contract, ethers } from "ethers"; import { Tracer } from "@opentelemetry/sdk-trace-base"; -import { Context, Span, SpanStatusCode } from "@opentelemetry/api"; +import { Context, SpanStatusCode } from "@opentelemetry/api"; import { ErrorSeverity, errorSnapshot, isTimeout } from "./error"; import { Report, @@ -229,7 +229,6 @@ export const processOrders = async ( orderbook, pair, orderbooksOrders: bundledOrders, - span, }); results.push({ settle, pair, orderPairObject }); span.end(); @@ -435,7 +434,6 @@ export async function processPair(args: { orderbook: Contract; pair: string; orderbooksOrders: BundledOrders[][]; - span: Span, }): Promise<() => Promise> { const { config, From 9ea3944e5bce211542e757233c2e2f1537b0c5f1 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Fri, 6 Dec 2024 03:56:14 +0000 Subject: [PATCH 04/15] update [skip ci] --- package.json | 2 +- src/error.ts | 12 +- src/processOrders.ts | 8 +- src/tx.ts | 51 +++---- test/tx.test.ts | 318 ++++++++++++++++++++++++++++++++++++++----- 5 files changed, 313 insertions(+), 78 deletions(-) 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); + } + }); }); From ddf3d4e9cf8732bcdcbf2a3882ac60c323069b63 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Fri, 6 Dec 2024 16:22:23 +0000 Subject: [PATCH 05/15] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 19376695..4dc78440 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ Other optional arguments are: - `-V` or `--version`, output the version number - `-h` or `--help`, output usage information -
+
### List of available supported dexes (decentralized exchanges) - all of the below names are case INSENSITIVE: From 7b67b76d28f9faaee1963314f8bdbf1bffd0de7c Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sat, 7 Dec 2024 04:10:02 +0000 Subject: [PATCH 06/15] update --- src/account.ts | 52 ++----- src/config.ts | 11 +- src/error.ts | 11 +- src/processOrders.ts | 80 +++++----- src/tx.ts | 92 ++++++++++-- src/types.ts | 20 ++- test/account.test.js | 12 ++ test/e2e/e2e.test.js | 10 ++ test/processPair.test.js | 311 +++++++++++++++++++++------------------ test/tx.test.ts | 105 ++++++++++++- 10 files changed, 446 insertions(+), 258 deletions(-) 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]); + }); }); From fce0605ad206ac8fbf4dfe5abc4d52b09fe56b54 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sat, 7 Dec 2024 04:46:21 +0000 Subject: [PATCH 07/15] fix --- src/tx.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tx.ts b/src/tx.ts index bea73f78..1c4d19a0 100644 --- a/src/tx.ts +++ b/src/tx.ts @@ -319,7 +319,7 @@ export async function sendTransaction Date: Tue, 10 Dec 2024 03:10:17 +0000 Subject: [PATCH 08/15] update --- src/config.ts | 4 ++-- src/error.ts | 22 ++++++++++++++++++++-- src/processOrders.ts | 5 ++++- src/tx.ts | 16 ++++++++++------ 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/config.ts b/src/config.ts index 954fd2bb..bb7101fc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -237,7 +237,7 @@ export function getBountyEnsureBytecode( const minimum = minimumExcepted.toHexString().substring(2).padStart(64, "0"); const msgSender = sender.substring(2).padStart(64, "0").toLowerCase(); // rainlang bytecode: - // :ensure(sender context<0 0>()), + // :ensure(equal-to(sender context<0 0>()) \"unknown sender\"), // :ensure( // greater-than-or-equal-to( // add( @@ -283,7 +283,7 @@ export function getWithdrawEnsureBytecode( const minimum = minimumExcepted.toHexString().substring(2).padStart(64, "0"); const msgSender = sender.substring(2).padStart(64, "0").toLowerCase(); // rainlang bytecode: - // :ensure(sender context<0 0>()), + // :ensure(equal-to(sender context<0 0>()) \"unknown sender\"), // :ensure( // greater-than-or-equal-to( // add( diff --git a/src/error.ts b/src/error.ts index c510500c..65c0922b 100644 --- a/src/error.ts +++ b/src/error.ts @@ -35,6 +35,15 @@ export enum ErrorSeverity { HIGH = "HIGH", } +/** + * Known errors + */ +export const KnownErrors = [ + "unknown sender", + "minimumSenderOutput", + "MinimalOutputBalanceViolation", +] as const; + /** * Specifies a decoded contract error */ @@ -167,7 +176,15 @@ export async function handleRevert( receipt: TransactionReceipt, rawtx: RawTx, signerBalance: BigNumber, -): Promise<{ err: any; nodeError: boolean; snapshot: string } | undefined> { +): Promise< + | { + err: any; + nodeError: boolean; + snapshot: string; + rawRevertError?: TxRevertError; + } + | undefined +> { const header = "transaction reverted onchain"; try { const gasErr = checkGasIssue(receipt, rawtx, signerBalance); @@ -189,13 +206,14 @@ export async function handleRevert( }); const msg = header + - " and simulation failed to find out what was the revert reason, please try to simulate the tx manualy for more details"; + " and simulation failed to find 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, nodeError: containsNodeError(err), snapshot: errorSnapshot(header, err, { receipt, rawtx, signerBalance }), + rawRevertError: parseRevertError(err), }; } } diff --git a/src/processOrders.ts b/src/processOrders.ts index 883a0dc8..5911c9ea 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -10,7 +10,7 @@ import { privateKeyToAccount } from "viem/accounts"; import { BigNumber, Contract, ethers } from "ethers"; import { Tracer } from "@opentelemetry/sdk-trace-base"; import { Context, SpanStatusCode } from "@opentelemetry/api"; -import { ErrorSeverity, errorSnapshot, isTimeout } from "./error"; +import { ErrorSeverity, errorSnapshot, isTimeout, KnownErrors } from "./error"; import { Report, BotConfig, @@ -359,6 +359,9 @@ export const processOrders = async ( } else { message = errorSnapshot("transaction reverted onchain", e.error.err); } + if (KnownErrors.some((v) => message.includes(v))) { + span.setAttribute("severity", ErrorSeverity.HIGH); + } span.setAttribute("errorDetails", message); } if (e.spanAttributes["txNoneNodeError"]) { diff --git a/src/tx.ts b/src/tx.ts index 1c4d19a0..1f764d3a 100644 --- a/src/tx.ts +++ b/src/tx.ts @@ -113,7 +113,14 @@ export async function handleTransaction( ); } catch (e: any) { try { - const newReceipt = await viemClient.getTransactionReceipt({ hash: txhash }); + const newReceipt = await (async () => { + try { + return await viemClient.getTransactionReceipt({ hash: txhash }); + } catch { + await sleep(Math.max(90_000 + time - Date.now(), 0)); + return await viemClient.getTransactionReceipt({ hash: txhash }); + } + })(); if (newReceipt) { return handleReceipt( txhash, @@ -274,12 +281,9 @@ export async function handleReceipt( } return result; } else { - // wait at least 60s before simulating the revert tx + // wait at least 90s before simulating the revert tx // in order for rpcs to catch up - const wait = 60000 - Date.now() + time; - if (wait > 0) { - await sleep(wait); - } + await sleep(Math.max(90_000 + time - Date.now(), 0)); const simulation = await handleRevert( signer, txhash as `0x${string}`, From 2641528f29b5b870813586e91b4aadf2efb6b181 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 10 Dec 2024 03:54:56 +0000 Subject: [PATCH 09/15] update --- src/error.ts | 15 ++++++--------- src/tx.ts | 33 +++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/error.ts b/src/error.ts index 65c0922b..881fc0f1 100644 --- a/src/error.ts +++ b/src/error.ts @@ -176,15 +176,12 @@ export async function handleRevert( receipt: TransactionReceipt, rawtx: RawTx, signerBalance: BigNumber, -): Promise< - | { - err: any; - nodeError: boolean; - snapshot: string; - rawRevertError?: TxRevertError; - } - | undefined -> { +): Promise<{ + err: any; + nodeError: boolean; + snapshot: string; + rawRevertError?: TxRevertError; +}> { const header = "transaction reverted onchain"; try { const gasErr = checkGasIssue(receipt, rawtx, signerBalance); diff --git a/src/tx.ts b/src/tx.ts index 1f764d3a..64b0b1fb 100644 --- a/src/tx.ts +++ b/src/tx.ts @@ -281,16 +281,29 @@ export async function handleReceipt( } return result; } else { - // wait at least 90s before simulating the revert tx - // in order for rpcs to catch up - await sleep(Math.max(90_000 + time - Date.now(), 0)); - const simulation = await handleRevert( - signer, - txhash as `0x${string}`, - receipt, - rawtx, - signerBalance, - ); + const simulation = await (async () => { + const result = await handleRevert( + signer, + txhash as `0x${string}`, + receipt, + rawtx, + signerBalance, + ); + if (result.snapshot.includes("simulation failed to find the revert reason")) { + // wait at least 90s before simulating the revert tx + // in order for rpcs to catch up + await sleep(Math.max(90_000 + time - Date.now(), 0)); + return await handleRevert( + signer, + txhash as `0x${string}`, + receipt, + rawtx, + signerBalance, + ); + } else { + return result; + } + })(); if (simulation) { result.error = simulation; spanAttributes["txNoneNodeError"] = !simulation.nodeError; From 6237416898345c948f1d5002ae663f3e2384f5ae Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 10 Dec 2024 04:22:34 +0000 Subject: [PATCH 10/15] Update processOrders.ts --- src/processOrders.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/processOrders.ts b/src/processOrders.ts index 5911c9ea..11295067 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -359,11 +359,11 @@ export const processOrders = async ( } else { message = errorSnapshot("transaction reverted onchain", e.error.err); } - if (KnownErrors.some((v) => message.includes(v))) { - span.setAttribute("severity", ErrorSeverity.HIGH); - } span.setAttribute("errorDetails", message); } + if (KnownErrors.some((v) => !message.includes(v))) { + span.setAttribute("severity", ErrorSeverity.HIGH); + } if (e.spanAttributes["txNoneNodeError"]) { span.setAttribute("severity", ErrorSeverity.HIGH); } From bfe9d28a7371f49e2338de2af85ae4f1488f3733 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 10 Dec 2024 14:12:32 +0000 Subject: [PATCH 11/15] Update error.ts --- src/error.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/error.ts b/src/error.ts index 881fc0f1..52366a86 100644 --- a/src/error.ts +++ b/src/error.ts @@ -41,6 +41,7 @@ export enum ErrorSeverity { export const KnownErrors = [ "unknown sender", "minimumSenderOutput", + "minimum sender output", "MinimalOutputBalanceViolation", ] as const; From 22ad8db210a69b750442caaa832bdf6f8ec09c30 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 10 Dec 2024 15:42:51 +0000 Subject: [PATCH 12/15] Update processOrders.ts --- src/processOrders.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/processOrders.ts b/src/processOrders.ts index 11295067..30d4e90d 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -361,7 +361,7 @@ export const processOrders = async ( } span.setAttribute("errorDetails", message); } - if (KnownErrors.some((v) => !message.includes(v))) { + if (KnownErrors.every((v) => !message.includes(v))) { span.setAttribute("severity", ErrorSeverity.HIGH); } if (e.spanAttributes["txNoneNodeError"]) { From c99dfc681db6f34b28b4e65e18688dde6a71de8a Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 10 Dec 2024 19:34:44 +0000 Subject: [PATCH 13/15] update --- src/abis.ts | 5 +++ src/error.ts | 98 ++++++++++++++++++++++++++++++++++++++++------------ src/tx.ts | 2 ++ 3 files changed, 82 insertions(+), 23 deletions(-) diff --git a/src/abis.ts b/src/abis.ts index 8ecaa9a2..b376267f 100644 --- a/src/abis.ts +++ b/src/abis.ts @@ -1,3 +1,5 @@ +import { parseAbi } from "viem"; + /** * Minimal ABI for ERC20 contract only including Transfer event */ @@ -45,6 +47,7 @@ export const orderbookAbi = [ "function multicall(bytes[] calldata data) external returns (bytes[] memory results)", `function takeOrders2(${TakeOrdersConfigV3} memory config) external returns (uint256 totalInput, uint256 totalOutput)`, `function clear2(${OrderV3} memory aliceOrder, ${OrderV3} memory bobOrder, ${ClearConfig} calldata clearConfig, ${SignedContextV1}[] memory aliceSignedContext, ${SignedContextV1}[] memory bobSignedContext) external`, + `event TakeOrderV2(address sender, ${TakeOrderConfigV3} config, uint256 input, uint256 output)`, ] as const; /** @@ -89,3 +92,5 @@ export const DefaultArbEvaluable = { store: "0x" + "0".repeat(40), bytecode: "0x", } as const; + +export const TakeOrderV2EventAbi = parseAbi([orderbookAbi[13]]); diff --git a/src/error.ts b/src/error.ts index 52366a86..83bebcf1 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,6 +1,8 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { BigNumber } from "ethers"; +import { isDeepStrictEqual } from "util"; import { RawTx, ViemClient } from "./types"; +import { TakeOrderV2EventAbi } from "./abis"; // @ts-ignore import { abi as obAbi } from "../test/abis/OrderBook.json"; // @ts-ignore @@ -17,6 +19,7 @@ import { FeeCapTooLowError, decodeErrorResult, TransactionReceipt, + decodeFunctionData, ExecutionRevertedError, InsufficientFundsError, TransactionNotFoundError, @@ -80,35 +83,33 @@ export function errorSnapshot( receipt: TransactionReceipt; rawtx: RawTx; signerBalance: BigNumber; + frontrun?: string; }, ): string { const message = [header]; if (err instanceof BaseError) { if (err.shortMessage) message.push("Reason: " + err.shortMessage); if (err.name) message.push("Error: " + err.name); - if (err.details) { - message.push("Details: " + err.details); - if ( - message.some( - (v) => v.includes("unknown reason") || v.includes("execution reverted"), - ) - ) { - const { raw, decoded } = parseRevertError(err); - if (decoded) { - message.push("Error Name: " + decoded.name); - if (decoded.args.length) { - message.push("Error Args: " + JSON.stringify(decoded.args)); - } - } else if (raw.data) { - message.push("Error Raw Data: " + raw.data); - } else if (data) { - const gasErr = checkGasIssue(data.receipt, data.rawtx, data.signerBalance); - if (gasErr) { - message.push("Gas Error: " + gasErr); - } - } else { - message.push("Comment: Found no additional info"); + if (err.details) message.push("Details: " + err.details); + if (message.some((v) => v.includes("unknown reason") || v.includes("execution reverted"))) { + const { raw, decoded } = parseRevertError(err); + if (decoded) { + message.push("Error Name: " + decoded.name); + if (decoded.args.length) { + message.push("Error Args: " + JSON.stringify(decoded.args)); } + } else if (raw.data) { + message.push("Error Raw Data: " + raw.data); + } else if (data) { + const gasErr = checkGasIssue(data.receipt, data.rawtx, data.signerBalance); + if (gasErr) { + message.push("Gas Error: " + gasErr); + } + } else { + message.push("Comment: Found no additional info"); + } + if (data?.frontrun) { + message.push("Actual Cause: " + data.frontrun); } } } else if (err instanceof Error) { @@ -177,6 +178,7 @@ export async function handleRevert( receipt: TransactionReceipt, rawtx: RawTx, signerBalance: BigNumber, + orderbook: `0x${string}`, ): Promise<{ err: any; nodeError: boolean; @@ -207,10 +209,16 @@ export async function handleRevert( " and simulation failed to find the revert reason, please try to simulate the tx manualy for more details"; return { err: msg, nodeError: false, snapshot: msg }; } catch (err: any) { + let frontrun: string | undefined = await hasFrontrun(viemClient, rawtx, receipt, orderbook); + if (frontrun) { + frontrun = `current transaction with hash ${ + receipt.transactionHash + } has been actually frontrun by transaction with hash ${frontrun}`; + } return { err, nodeError: containsNodeError(err), - snapshot: errorSnapshot(header, err, { receipt, rawtx, signerBalance }), + snapshot: errorSnapshot(header, err, { receipt, rawtx, signerBalance, frontrun }), rawRevertError: parseRevertError(err), }; } @@ -304,3 +312,47 @@ export function checkGasIssue(receipt: TransactionReceipt, rawtx: RawTx, signerB } return undefined; } + +/** + * Checks if the given transaction has been frontrun by another transaction. + * This is done by checking previouse transaction on the same block that emitted + * the target event with the same TakeOrderConfigV3 struct. + */ +export async function hasFrontrun( + viemClient: ViemClient, + rawtx: RawTx, + receipt: TransactionReceipt, + orderbook: `0x${string}`, +) { + try { + const orderConfig = (() => { + try { + const result = decodeFunctionData({ + abi: arbRp4Abi, + data: rawtx.data, + }) as any; + return result?.args?.[1]?.orders?.[0]; + } catch { + return undefined; + } + })(); + if (orderConfig) { + const logs = await viemClient.getLogs({ + event: TakeOrderV2EventAbi[0], + address: orderbook, + blockHash: receipt.blockHash, + }); + const otherLogs = logs.filter( + (v) => + receipt.transactionIndex > v.transactionIndex && + v.transactionHash.toLowerCase() !== receipt.transactionHash.toLowerCase(), + ); + if (otherLogs.length) { + for (const log of otherLogs) { + if (isDeepStrictEqual(log.args.config, orderConfig)) return log.transactionHash; + } + } + } + } catch {} + return undefined; +} diff --git a/src/tx.ts b/src/tx.ts index 64b0b1fb..af73f053 100644 --- a/src/tx.ts +++ b/src/tx.ts @@ -288,6 +288,7 @@ export async function handleReceipt( receipt, rawtx, signerBalance, + orderbook.address as `0x${string}`, ); if (result.snapshot.includes("simulation failed to find the revert reason")) { // wait at least 90s before simulating the revert tx @@ -299,6 +300,7 @@ export async function handleReceipt( receipt, rawtx, signerBalance, + orderbook.address as `0x${string}`, ); } else { return result; From 6e07cfa52f28974fa0d68e5094a4fe950ae083f0 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 10 Dec 2024 21:54:21 +0000 Subject: [PATCH 14/15] add frontrun finder test --- test/error.test.js | 88 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/test/error.test.js b/test/error.test.js index 633c0c34..bed89704 100644 --- a/test/error.test.js +++ b/test/error.test.js @@ -1,6 +1,7 @@ const { assert } = require("chai"); -const { BaseError } = require("viem"); -const { tryDecodeError, parseRevertError } = require("../src/error"); +const { BaseError, encodeFunctionData } = require("viem"); +const { tryDecodeError, parseRevertError, hasFrontrun } = require("../src/error"); +const { abi: arbRp4Abi } = require("./abis/RouteProcessorOrderBookV4ArbOrderTaker.json"); describe("Test error", async function () { const data = "0x963b34a500000000000000000000000000000000000000000000000340bda9d7e155feb0"; @@ -31,4 +32,87 @@ describe("Test error", async function () { }; assert.deepEqual(result, expected); }); + + it("should find frontrun tx", async function () { + const takeOrderV3Config = { + order: { + owner: "0x7177b9d00bB5dbcaaF069CC63190902763783b09", + evaluable: { + interpreter: "0xC1A14cE2fd58A3A2f99deCb8eDd866204eE07f8D", + store: "0xFA4989F5D49197FD9673cE4B7Fe2A045A0F2f9c8", + bytecode: "0x1234", + }, + validInputs: [ + { + token: "0x4Aa9AEf59C7B63CD5C4B2eDE81F65A4225a99d9d", + decimals: 18, + vaultId: 1n, + }, + ], + validOutputs: [ + { + token: "0xc92be5C1a82da1Ab3984a3923dCC5d8576279c7d", + decimals: 18, + vaultId: 1n, + }, + ], + nonce: "0x8170f8b00c92678800474dfc16369f1cb5ca8a8c70b620a3b9a103e04f750ae2", + }, + inputIOIndex: 0n, + outputIOIndex: 0n, + signedContext: [], + }; + const rawtx = { + to: "0x7D2f700b1f6FD75734824EA4578960747bdF269A", + data: encodeFunctionData({ + abi: arbRp4Abi, + functionName: "arb3", + args: [ + "0x245fCcE2d5D0E365C2777B5984460742cE438e7e", + { + minimumInput: 1n, + maximumInput: 100000000000n, + maximumIORatio: 115792089237316195423570985n, + orders: [takeOrderV3Config], + data: "0x", + }, + { + evaluable: { + interpreter: "0x0000000000000000000000000000000000000000", + store: "0x0000000000000000000000000000000000000000", + bytecode: "0x", + }, + signedContext: [], + }, + ], + }), + }; + const receipt = { + transactionIndex: 2, + transactionHash: "0x2", + }; + const expectedReceipt = { + transactionIndex: 1, + transactionHash: "0x1", + }; + const viemClient = { + getLogs: async () => [ + { + ...expectedReceipt, + args: { + config: takeOrderV3Config, + }, + }, + { + ...receipt, + args: { + config: takeOrderV3Config, + }, + }, + ], + }; + const result = await hasFrontrun(viemClient, rawtx, receipt, ""); + + assert.equal(result, expectedReceipt.transactionHash); + }); }); From 74f715c564316cfc01fa280eac27e8bc40bc792d Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 11 Dec 2024 01:08:12 +0000 Subject: [PATCH 15/15] update --- README.md | 4 ++++ example.env | 3 +++ src/cli.ts | 6 ++++++ src/index.ts | 1 + src/modes/index.ts | 46 +++++++++++++++++++++++++--------------------- src/types.ts | 2 ++ test/cli.test.js | 5 +++++ 7 files changed, 46 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 4dc78440..ef29e2b1 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ Other optional arguments are: - `--gas-price-multiplier`, Option to multiply the gas price fetched from the rpc as percentage, default is 107, ie +7%. Will override the 'GAS_PRICE_MULTIPLIER' in env variables - `--gas-limit-multiplier`, Option to multiply the gas limit estimation from the rpc as percentage, default is 108, ie +8%. Will override the 'GAS_LIMIT_MULTIPLIER' in env variables - `--tx-gas`, Option to set a static gas limit for all submitting txs. Will override the 'TX_GAS' in env variables +- `--rp-only`, Only clear orders through RP4, excludes intra and inter orderbook clears. Will override the 'RP_ONLY' in env variablesin env variables - `-V` or `--version`, output the version number - `-h` or `--help`, output usage information @@ -267,6 +268,9 @@ GAS_LIMIT_MULTIPLIER= # Option to set a static gas limit for all submitting txs TX_GAS= + +# Only clear orders through RP4, excludes intra and inter orderbook clears +RP_ONLY="true" ``` If both env variables and CLI argument are set, the CLI arguments will be prioritized and override the env variables. diff --git a/example.env b/example.env index c3b9a355..25e38948 100644 --- a/example.env +++ b/example.env @@ -94,6 +94,9 @@ GAS_LIMIT_MULTIPLIER= # Option to set a static gas limit for all submitting txs TX_GAS= +# Only clear orders through RP4, excludes intra and inter orderbook clears +RP_ONLY="true" + # test rpcs vars TEST_POLYGON_RPC= diff --git a/src/cli.ts b/src/cli.ts index 591c82e5..8e6b5b67 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -72,6 +72,7 @@ const ENV_OPTIONS = { gasLimitMultiplier: process?.env?.GAS_LIMIT_MULTIPLIER, txGas: process?.env?.TX_GAS, route: process?.env?.ROUTE, + rpOnly: process?.env?.RP_ONLY?.toLowerCase() === "true" ? true : false, ownerProfile: process?.env?.OWNER_PROFILE ? Array.from(process?.env?.OWNER_PROFILE.matchAll(/[^,\s]+/g)).map((v) => v[0]) : undefined, @@ -200,6 +201,10 @@ const getOptions = async (argv: any, version?: string) => { "--tx-gas ", "Option to set a static gas limit for all submitting txs. Will override the 'TX_GAS' in env variables", ) + .option( + "--rp-only", + "Only clear orders through RP4, excludes intra and inter orderbook clears. Will override the 'RP_ONLY' in env variables", + ) .description( [ "A NodeJS app to find and take arbitrage trades for Rain Orderbook orders against some DeFi liquidity providers, requires NodeJS v18 or higher.", @@ -246,6 +251,7 @@ const getOptions = async (argv: any, version?: string) => { cmdOptions.ownerProfile = cmdOptions.ownerProfile || getEnv(ENV_OPTIONS.ownerProfile); cmdOptions.route = cmdOptions.route || getEnv(ENV_OPTIONS.route); cmdOptions.publicRpc = cmdOptions.publicRpc || getEnv(ENV_OPTIONS.publicRpc); + cmdOptions.rpOnly = cmdOptions.rpOnly || getEnv(ENV_OPTIONS.rpOnly); if (cmdOptions.ownerProfile) { const profiles: Record = {}; cmdOptions.ownerProfile.forEach((v: string) => { diff --git a/src/index.ts b/src/index.ts index b533d20d..94e3579a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -209,6 +209,7 @@ export async function getConfig( config.gasPriceMultiplier = options.gasPriceMultiplier; config.gasLimitMultiplier = options.gasLimitMultiplier; config.txGas = options.txGas; + config.rpOnly = options.rpOnly; // init accounts const { mainAccount, accounts } = await initAccounts(walletKey, config, options, tracer, ctx); diff --git a/src/modes/index.ts b/src/modes/index.ts index 92cb2b82..80e08cdd 100644 --- a/src/modes/index.ts +++ b/src/modes/index.ts @@ -64,27 +64,31 @@ export async function findOpp({ config, viemClient, }), - findIntraObOpp({ - orderPairObject, - signer, - gasPrice, - inputToEthPrice, - outputToEthPrice, - config, - viemClient, - orderbooksOrders, - }), - findInterObOpp({ - orderPairObject, - signer, - gasPrice, - arb: genericArb!, - inputToEthPrice, - outputToEthPrice, - config, - viemClient, - orderbooksOrders, - }), + ...(!config.rpOnly + ? [ + findIntraObOpp({ + orderPairObject, + signer, + gasPrice, + inputToEthPrice, + outputToEthPrice, + config, + viemClient, + orderbooksOrders, + }), + findInterObOpp({ + orderPairObject, + signer, + gasPrice, + arb: genericArb!, + inputToEthPrice, + outputToEthPrice, + config, + viemClient, + orderbooksOrders, + }), + ] + : []), ]; const allResults = await Promise.allSettled(promises); diff --git a/src/types.ts b/src/types.ts index c11bd8d0..4284019e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -51,6 +51,7 @@ export type CliOptions = { gasPriceMultiplier: number; gasLimitMultiplier: number; txGas?: bigint; + rpOnly?: boolean; }; export type TokenDetails = { @@ -190,6 +191,7 @@ export type BotConfig = { gasPriceMultiplier: number; gasLimitMultiplier: number; txGas?: bigint; + rpOnly?: boolean; onFetchRequest?: (request: Request) => void; onFetchResponse?: (request: Response) => void; }; diff --git a/test/cli.test.js b/test/cli.test.js index 228c15ac..4d4ed3fd 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -189,6 +189,7 @@ describe("Test cli", async function () { "110", "--tx-gas", "123456789", + "--rp-only", ]); const expected = { roundGap: 10000, @@ -210,12 +211,14 @@ describe("Test cli", async function () { gasPriceMultiplier: 120, gasLimitMultiplier: 110, txGas: 123456789n, + rpOnly: true, }, options: { botMinBalance: "0.123", gasPriceMultiplier: 120, gasLimitMultiplier: 110, txGas: 123456789n, + rpOnly: true, }, }; await sleep(1000); @@ -233,5 +236,7 @@ describe("Test cli", async function () { assert.equal(result.config.gasLimitMultiplier, expected.config.gasLimitMultiplier); assert.equal(result.options.txGas, expected.options.txGas); assert.equal(result.config.txGas, expected.config.txGas); + assert.equal(result.options.rpOnly, expected.options.rpOnly); + assert.equal(result.config.rpOnly, expected.config.rpOnly); }); });