diff --git a/README.md b/README.md index 974d6cb5..1f7e163e 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ Other optional arguments are: - `-w` or `--wallet-count`, Number of wallet to submit transactions with, requirs `--mnemonic`. Will override the 'WALLET_COUNT' in env variables - `-t` or `--topup-amount`, The initial topup amount of excess wallets, requirs `--mnemonic`. Will override the 'TOPUP_AMOUNT' in env variables - `--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 105, ie +5%. Will override the 'GAS_LIMIT_MULTIPLIER' in env variables - `-V` or `--version`, output the version number - `-h` or `--help`, output usage information @@ -256,6 +257,9 @@ ROUTE="single" # Option to multiply the gas price fetched from the rpc as percentage, default is 107, ie +7% GAS_PRICE_MULTIPLIER= + +# Option to multiply the gas limit estimation from the rpc as percentage, default is 105, ie +5% +GAS_LIMIT_MULTIPLIER= ``` 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 f4f0a84c..94fc0c01 100644 --- a/example.env +++ b/example.env @@ -85,6 +85,9 @@ ROUTE="single" # Option to multiply the gas price fetched from the rpc as percentage, default is 107, ie +7% GAS_PRICE_MULTIPLIER= +# Option to multiply the gas limit estimation from the rpc as percentage, default is 105, ie +5% +GAS_LIMIT_MULTIPLIER= + # test rpcs vars TEST_POLYGON_RPC= diff --git a/src/cli.ts b/src/cli.ts index 6bd1810b..a33e3163 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -57,6 +57,8 @@ const ENV_OPTIONS = { botMinBalance: process?.env?.BOT_MIN_BALANCE, selfFundOrders: process?.env?.SELF_FUND_ORDERS, gasPriceMultiplier: process?.env?.GAS_PRICE_MULTIPLIER, + gasLimitMultiplier: process?.env?.GAS_LIMIT_MULTIPLIER, + txGas: process?.env?.TX_GAS, route: process?.env?.ROUTE, rpc: process?.env?.RPC_URL ? Array.from(process?.env?.RPC_URL.matchAll(/[^,\s]+/g)).map((v) => v[0]) @@ -168,6 +170,14 @@ const getOptions = async (argv: any, version?: string) => { "--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", ) + .option( + "--gas-limit-multiplier ", + "Option to multiply the gas limit estimation from the rpc as percentage, default is 105, ie +5%. Will override the 'GAS_LIMIT_MULTIPLIER' in env variables", + ) + .option( + "--tx-gas ", + "Option to set a static gas limit for all submitting txs. Will override the 'TX_GAS' 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.", @@ -203,6 +213,8 @@ const getOptions = async (argv: any, version?: string) => { cmdOptions.topupAmount = cmdOptions.topupAmount || ENV_OPTIONS.topupAmount; cmdOptions.selfFundOrders = cmdOptions.selfFundOrders || ENV_OPTIONS.selfFundOrders; cmdOptions.gasPriceMultiplier = cmdOptions.gasPriceMultiplier || ENV_OPTIONS.gasPriceMultiplier; + cmdOptions.gasLimitMultiplier = cmdOptions.gasLimitMultiplier || ENV_OPTIONS.gasLimitMultiplier; + cmdOptions.txGas = cmdOptions.txGas || ENV_OPTIONS.txGas; cmdOptions.botMinBalance = cmdOptions.botMinBalance || ENV_OPTIONS.botMinBalance; cmdOptions.route = cmdOptions.route || ENV_OPTIONS.route; cmdOptions.bundle = cmdOptions.bundle ? ENV_OPTIONS.bundle : false; @@ -251,7 +263,7 @@ export const arbRound = async ( if (!ordersDetails.length) { span.setStatus({ code: SpanStatusCode.OK, message: "found no orders" }); span.end(); - return { txs: [], foundOpp: false, avgGasCost: undefined }; + return { txs: [], foundOpp: false, didClear: false, avgGasCost: undefined }; } } catch (e: any) { const snapshot = errorSnapshot("", e); @@ -260,12 +272,13 @@ export const arbRound = async ( span.setAttribute("didClear", false); span.setAttribute("foundOpp", false); span.end(); - return { txs: [], foundOpp: false, avgGasCost: undefined }; + return { txs: [], foundOpp: false, didClear: false, avgGasCost: undefined }; } try { let txs; let foundOpp = false; + let didClear = false; const { reports = [], avgGasCost = undefined } = await clear( config, ordersDetails, @@ -277,7 +290,6 @@ export const arbRound = async ( if (txs.length) { foundOpp = true; span.setAttribute("txUrls", txs); - span.setAttribute("didClear", true); span.setAttribute("foundOpp", true); } else if ( reports.some((v) => v.status === ProcessPairReportStatus.FoundOpportunity) @@ -285,6 +297,15 @@ export const arbRound = async ( foundOpp = true; span.setAttribute("foundOpp", true); } + if ( + reports.some( + (v) => + v.status === ProcessPairReportStatus.FoundOpportunity && !v.reason, + ) + ) { + didClear = true; + span.setAttribute("didClear", true); + } } else { span.setAttribute("didClear", false); } @@ -293,7 +314,7 @@ export const arbRound = async ( } span.setStatus({ code: SpanStatusCode.OK }); span.end(); - return { txs, foundOpp, avgGasCost }; + return { txs, foundOpp, didClear, avgGasCost }; } catch (e: any) { if (e?.startsWith?.("Failed to batch quote orders")) { span.setAttribute("severity", ErrorSeverity.LOW); @@ -307,7 +328,7 @@ export const arbRound = async ( span.setAttribute("didClear", false); span.setAttribute("foundOpp", false); span.end(); - return { txs: [], foundOpp: false, avgGasCost: undefined }; + return { txs: [], foundOpp: false, didClear: false, avgGasCost: undefined }; } } catch (e: any) { const snapshot = errorSnapshot("Unexpected error occured", e); @@ -317,7 +338,7 @@ export const arbRound = async ( span.setAttribute("didClear", false); span.setAttribute("foundOpp", false); span.end(); - return { txs: [], foundOpp: false, avgGasCost: undefined }; + return { txs: [], foundOpp: false, didClear: false, avgGasCost: undefined }; } }); }; @@ -389,6 +410,32 @@ export async function startup(argv: any, version?: string, tracer?: Tracer, ctx? } else { options.gasPriceMultiplier = 107; } + if (options.gasLimitMultiplier) { + if (typeof options.gasLimitMultiplier === "number") { + if (options.gasLimitMultiplier <= 0 || !Number.isInteger(options.gasLimitMultiplier)) + throw "invalid gasLimitMultiplier value, must be an integer greater than zero"; + } else if ( + typeof options.gasLimitMultiplier === "string" && + /^[0-9]+$/.test(options.gasLimitMultiplier) + ) { + options.gasLimitMultiplier = Number(options.gasLimitMultiplier); + if (options.gasLimitMultiplier <= 0) + throw "invalid gasLimitMultiplier value, must be an integer greater than zero"; + } else throw "invalid gasLimitMultiplier value, must be an integer greater than zero"; + } else { + options.gasLimitMultiplier = 105; + } + if (options.txGas) { + if (typeof options.txGas === "number") { + if (options.txGas <= 0 || !Number.isInteger(options.txGas)) + throw "invalid txGas value, must be an integer greater than zero"; + else options.txGas = BigInt(options.txGas); + } else if (typeof options.txGas === "string" && /^[0-9]+$/.test(options.txGas)) { + options.txGas = BigInt(options.txGas); + if (options.txGas <= 0n) + throw "invalid txGas value, must be an integer greater than zero"; + } else throw "invalid txGas value, must be an integer greater than zero"; + } const poolUpdateInterval = _poolUpdateInterval * 60 * 1000; let ordersDetails: any[] = []; if (!process?.env?.TEST) @@ -559,16 +606,19 @@ export const main = async (argv: any, version?: string) => { try { await rotateProviders(config, update); const roundResult = await arbRound(tracer, roundCtx, options, config); - let txs, foundOpp, roundAvgGasCost; + let txs, foundOpp, didClear, roundAvgGasCost; if (roundResult) { txs = roundResult.txs; foundOpp = roundResult.foundOpp; + didClear = roundResult.didClear; roundAvgGasCost = roundResult.avgGasCost; } if (txs && txs.length) { roundSpan.setAttribute("txUrls", txs); - roundSpan.setAttribute("didClear", true); roundSpan.setAttribute("foundOpp", true); + } else if (didClear) { + roundSpan.setAttribute("foundOpp", true); + roundSpan.setAttribute("didClear", true); } else if (foundOpp) { roundSpan.setAttribute("foundOpp", true); roundSpan.setAttribute("didClear", false); diff --git a/src/index.ts b/src/index.ts index a189aa02..2e08ca27 100644 --- a/src/index.ts +++ b/src/index.ts @@ -211,6 +211,7 @@ export async function getConfig( config.route = route; config.rpcRecords = rpcRecords; config.gasPriceMultiplier = options.gasPriceMultiplier; + config.gasLimitMultiplier = options.gasLimitMultiplier; // 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 a1c61b95..3d066895 100644 --- a/src/modes/index.ts +++ b/src/modes/index.ts @@ -82,6 +82,12 @@ export async function findOpp({ if (allResults.some((v) => v.status === "fulfilled")) { // pick and return the highest profit + allResults.forEach((v, i) => { + if (v.status === "fulfilled") { + v.value.spanAttributes["clearModePick"] = + i === 0 ? "rp4" : i === 1 ? "intra" : "inter"; + } + }); const res = allResults.filter( (v) => v.status === "fulfilled", ) as PromiseFulfilledResult[]; diff --git a/src/modes/interOrderbook.ts b/src/modes/interOrderbook.ts index 602567da..3293ebbb 100644 --- a/src/modes/interOrderbook.ts +++ b/src/modes/interOrderbook.ts @@ -82,7 +82,14 @@ export async function dryrun({ evaluable: { interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, - bytecode: "0x", + bytecode: + config.gasCoveragePercentage === "0" + ? "0x" + : getBountyEnsureBytecode( + ethers.utils.parseUnits(inputToEthPrice), + ethers.utils.parseUnits(outputToEthPrice), + ethers.constants.Zero, + ), }, signedContext: [], }; @@ -102,7 +109,9 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)); + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + .mul(config.gasLimitMultiplier) + .div(100); } catch (e) { const isNodeError = containsNodeError(e as BaseError); const errMsg = errorSnapshot("", e); @@ -144,7 +153,9 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)); + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + .mul(config.gasLimitMultiplier) + .div(100); rawtx.gas = gasLimit.toBigInt(); gasCost = gasLimit.mul(gasPrice); task.evaluable.bytecode = getBountyEnsureBytecode( @@ -179,6 +190,9 @@ export async function dryrun({ } } rawtx.gas = gasLimit.toBigInt(); + if (typeof config.txGas === "bigint") { + rawtx.gas = config.txGas; + } // if reached here, it means there was a success and found opp spanAttributes["oppBlockNumber"] = blockNumber; diff --git a/src/modes/intraOrderbook.ts b/src/modes/intraOrderbook.ts index d70a7349..498e4880 100644 --- a/src/modes/intraOrderbook.ts +++ b/src/modes/intraOrderbook.ts @@ -49,6 +49,23 @@ export async function dryrun({ const inputBountyVaultId = "1"; const outputBountyVaultId = "1"; const obInterface = new ethers.utils.Interface(orderbookAbi); + const task = { + evaluable: { + interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, + store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, + bytecode: getWithdrawEnsureBytecode( + signer.account.address, + orderPairObject.buyToken, + orderPairObject.sellToken, + inputBalance, + outputBalance, + ethers.utils.parseUnits(inputToEthPrice), + ethers.utils.parseUnits(outputToEthPrice), + ethers.constants.Zero, + ), + }, + signedContext: [], + }; const withdrawInputCalldata = obInterface.encodeFunctionData("withdraw2", [ orderPairObject.buyToken, inputBountyVaultId, @@ -59,7 +76,7 @@ export async function dryrun({ orderPairObject.sellToken, outputBountyVaultId, ethers.constants.MaxUint256, - [], + config.gasCoveragePercentage === "0" ? [] : [task], ]); const clear2Calldata = obInterface.encodeFunctionData("clear2", [ orderPairObject.takeOrders[0].takeOrder.order, @@ -89,7 +106,9 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)); + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + .mul(config.gasLimitMultiplier) + .div(100); } catch (e) { // reason, code, method, transaction, error, stack, message const isNodeError = containsNodeError(e as BaseError); @@ -118,23 +137,16 @@ export async function dryrun({ // sender output which is already called above if (config.gasCoveragePercentage !== "0") { const headroom = (Number(config.gasCoveragePercentage) * 1.03).toFixed(); - const task = { - evaluable: { - interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, - store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, - bytecode: getWithdrawEnsureBytecode( - signer.account.address, - orderPairObject.buyToken, - orderPairObject.sellToken, - inputBalance, - outputBalance, - ethers.utils.parseUnits(inputToEthPrice), - ethers.utils.parseUnits(outputToEthPrice), - gasCost.mul(headroom).div("100"), - ), - }, - signedContext: [], - }; + task.evaluable.bytecode = getWithdrawEnsureBytecode( + signer.account.address, + orderPairObject.buyToken, + orderPairObject.sellToken, + inputBalance, + outputBalance, + ethers.utils.parseUnits(inputToEthPrice), + ethers.utils.parseUnits(outputToEthPrice), + gasCost.mul(headroom).div("100"), + ); withdrawOutputCalldata = obInterface.encodeFunctionData("withdraw2", [ orderPairObject.sellToken, outputBountyVaultId, @@ -148,7 +160,9 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)); + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + .mul(config.gasLimitMultiplier) + .div(100); rawtx.gas = gasLimit.toBigInt(); gasCost = gasLimit.mul(gasPrice); task.evaluable.bytecode = getWithdrawEnsureBytecode( @@ -192,6 +206,9 @@ export async function dryrun({ } } rawtx.gas = gasLimit.toBigInt(); + if (typeof config.txGas === "bigint") { + rawtx.gas = config.txGas; + } // if reached here, it means there was a success and found opp spanAttributes["oppBlockNumber"] = blockNumber; diff --git a/src/modes/routeProcessor.ts b/src/modes/routeProcessor.ts index a4419f0a..29311ea8 100644 --- a/src/modes/routeProcessor.ts +++ b/src/modes/routeProcessor.ts @@ -147,7 +147,14 @@ export async function dryrun({ evaluable: { interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, - bytecode: "0x", + bytecode: + config.gasCoveragePercentage === "0" + ? "0x" + : getBountyEnsureBytecode( + ethers.utils.parseUnits(ethPrice), + ethers.constants.Zero, + ethers.constants.Zero, + ), }, signedContext: [], }; @@ -167,7 +174,9 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)); + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + .mul(config.gasLimitMultiplier) + .div(100); } catch (e) { // reason, code, method, transaction, error, stack, message const isNodeError = containsNodeError(e as BaseError); @@ -211,7 +220,9 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)); + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + .mul(config.gasLimitMultiplier) + .div(100); rawtx.gas = gasLimit.toBigInt(); gasCost = gasLimit.mul(gasPrice); task.evaluable.bytecode = getBountyEnsureBytecode( @@ -247,6 +258,9 @@ export async function dryrun({ } } rawtx.gas = gasLimit.toBigInt(); + if (typeof config.txGas === "bigint") { + rawtx.gas = config.txGas; + } // if reached here, it means there was a success and found opp // rest of span attr are not needed since they are present in the result.data diff --git a/src/processOrders.ts b/src/processOrders.ts index 2b1b1544..439a3537 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -47,7 +47,8 @@ export enum ProcessPairHaltReason { FailedToGetPools = 4, TxFailed = 5, TxMineFailed = 6, - UnexpectedError = 7, + TxReverted = 7, + UnexpectedError = 8, } /** @@ -310,21 +311,49 @@ export const processOrders = async ( span.setAttribute("errorDetails", message); } span.setStatus({ code: SpanStatusCode.OK, message }); - } else { - // set the otel span status as OK as an unsuccessfull clear, this can happen for example + } 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); + } + span.setAttribute("severity", ErrorSeverity.MEDIUM); + span.setStatus({ code: SpanStatusCode.ERROR, message }); + span.setAttribute("unsuccessfulClear", true); + span.setAttribute("txSendFailed", true); + } else if (e.reason === ProcessPairHaltReason.TxReverted) { + // set the severity to LOW, this can happen for example // because of mev front running or false positive opportunities, etc - let code = SpanStatusCode.OK; + span.setAttribute("severity", ErrorSeverity.LOW); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: "transaction reverted onchain", + }); + 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 (e.spanAttributes["txNoneNodeError"]) { - code = SpanStatusCode.ERROR; - span.setAttribute("severity", ErrorSeverity.MEDIUM); + span.setAttribute("severity", ErrorSeverity.MEDIUM); + span.setStatus({ code: SpanStatusCode.ERROR, message }); + span.setAttribute("unsuccessfulClear", true); + span.setAttribute("txMineFailed", true); + } else { + // record the error for the span + let message = pair + "unexpected error"; + if (e.error) { + message = errorSnapshot(message, e.error); + span.recordException(e.error); } - span.setStatus({ code, message }); - span.setAttribute("unsuccessfullClear", true); + // 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 @@ -756,7 +785,7 @@ export async function processPair(args: { sellToken: orderPairObject.sellToken, actualGasCost: ethers.utils.formatUnits(actualGasCost), }; - result.reason = ProcessPairHaltReason.TxMineFailed; + result.reason = ProcessPairHaltReason.TxReverted; return Promise.reject(result); } } catch (e: any) { @@ -781,6 +810,13 @@ export async function processPair(args: { 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; diff --git a/src/types.ts b/src/types.ts index f351bebf..d8c1076f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -46,6 +46,8 @@ export type CliOptions = { tokens?: TokenDetails[]; route?: string; gasPriceMultiplier: number; + gasLimitMultiplier: number; + txGas?: bigint; }; export type TokenDetails = { @@ -145,6 +147,8 @@ export type BotConfig = { route?: "multi" | "single"; rpcRecords: Record; gasPriceMultiplier: number; + gasLimitMultiplier: number; + txGas?: bigint; onFetchRequest?: (request: Request) => void; onFetchResponse?: (request: Response) => void; }; @@ -162,6 +166,8 @@ export type Report = { clearedOrders?: string[]; income?: BigNumber; netProfit?: BigNumber; + reason?: ProcessPairHaltReason; + error?: any; }; export type RoundReport = { diff --git a/test/cli.test.js b/test/cli.test.js index 2689bc78..23a927fe 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -48,7 +48,7 @@ describe("Test cli", async function () { }; const response = await arbRound(tracer, ctx, options, { mainAccount: {} }); - const expected = { txs: [], foundOpp: false, avgGasCost: undefined }; + const expected = { txs: [], foundOpp: false, didClear: false, avgGasCost: undefined }; assert.deepEqual(response, expected); testSpan.end(); @@ -185,6 +185,8 @@ describe("Test cli", async function () { "0.123", "--gas-price-multiplier", "120", + "--gas-limit-multiplier", + "110", ]); const expected = { roundGap: 10000, @@ -204,10 +206,12 @@ describe("Test cli", async function () { }, }, gasPriceMultiplier: 120, + gasLimitMultiplier: 110, }, options: { botMinBalance: "0.123", gasPriceMultiplier: 120, + gasLimitMultiplier: 110, }, }; await sleep(1000); @@ -221,5 +225,7 @@ describe("Test cli", async function () { assert.equal(result.options.botMinBalance, expected.options.botMinBalance); assert.equal(result.options.gasPriceMultiplier, expected.options.gasPriceMultiplier); assert.equal(result.config.gasPriceMultiplier, expected.config.gasPriceMultiplier); + assert.equal(result.options.gasLimitMultiplier, expected.options.gasLimitMultiplier); + assert.equal(result.config.gasLimitMultiplier, expected.config.gasLimitMultiplier); }); }); diff --git a/test/data.js b/test/data.js index 17e3cfeb..e5bd1ea2 100644 --- a/test/data.js +++ b/test/data.js @@ -67,6 +67,7 @@ const config = { symbol: token2.symbol, }, gasPriceMultiplier: 107, + gasLimitMultiplier: 100, }; const vaultBalance1 = BigNumber.from("10000000000000000000"); diff --git a/test/e2e/e2e.test.js b/test/e2e/e2e.test.js index 81cd52ac..336a61fb 100644 --- a/test/e2e/e2e.test.js +++ b/test/e2e/e2e.test.js @@ -74,7 +74,7 @@ for (let i = 0; i < testData.length; i++) { for (let j = 0; j < rpVersions.length; j++) { const rpVersion = rpVersions[j]; - it.only(`should clear orders successfully using route processor v${rpVersion}`, async function () { + it(`should clear orders successfully using route processor v${rpVersion}`, async function () { config.rpc = [rpc]; const viemClient = await viem.getPublicClient(); const dataFetcher = await getDataFetcher(config, liquidityProviders, false); @@ -250,6 +250,7 @@ for (let i = 0; i < testData.length; i++) { config.mainAccount = bot; config.quoteRpc = [mockServer.url + "/rpc"]; config.gasPriceMultiplier = 107; + config.gasLimitMultiplier = 100; const { reports } = await clear(config, orders, tracer, ctx); // should have cleared correct number of orders @@ -330,7 +331,7 @@ for (let i = 0; i < testData.length; i++) { testSpan.end(); }); - it.only("should clear orders successfully using inter-orderbook", async function () { + it("should clear orders successfully using inter-orderbook", async function () { config.rpc = [rpc]; const viemClient = await viem.getPublicClient(); const dataFetcher = await getDataFetcher(config, liquidityProviders, false); @@ -581,6 +582,7 @@ for (let i = 0; i < testData.length; i++) { config.mainAccount = bot; config.quoteRpc = [mockServer.url + "/rpc"]; config.gasPriceMultiplier = 107; + config.gasLimitMultiplier = 100; const { reports } = await clear(config, orders, tracer, ctx); // should have cleared correct number of orders @@ -674,7 +676,7 @@ for (let i = 0; i < testData.length; i++) { testSpan.end(); }); - it.only("should clear orders successfully using intra-orderbook", async function () { + it("should clear orders successfully using intra-orderbook", async function () { config.rpc = [rpc]; const viemClient = await viem.getPublicClient(); const dataFetcher = await getDataFetcher(config, liquidityProviders, false); @@ -933,6 +935,7 @@ for (let i = 0; i < testData.length; i++) { config.mainAccount = bot; config.quoteRpc = [mockServer.url + "/rpc"]; config.gasPriceMultiplier = 107; + config.gasLimitMultiplier = 100; const { reports } = await clear(config, orders, tracer, ctx); // should have cleared correct number of orders diff --git a/test/findOpp.test.js b/test/findOpp.test.js index efea40c7..5d15c616 100644 --- a/test/findOpp.test.js +++ b/test/findOpp.test.js @@ -124,6 +124,7 @@ describe("Test find opp", async function () { amountOut: formatUnits(getAmountOut(vaultBalance), 6), marketPrice: formatUnits(getCurrentPrice(vaultBalance)), route: expectedRouteVisual, + clearModePick: "rp4", }, }; assert.deepEqual(result, expected); @@ -214,6 +215,7 @@ describe("Test find opp", async function () { oppBlockNumber, foundOpp: true, maxInput: vaultBalance.toString(), + clearModePick: "inter", }, }; assert.deepEqual(result, expected); @@ -309,6 +311,7 @@ describe("Test find opp", async function () { spanAttributes: { oppBlockNumber, foundOpp: true, + clearModePick: "intra", }, }; assert.deepEqual(result, expected); diff --git a/test/mode-interOrderbook.test.js b/test/mode-interOrderbook.test.js index 884bd652..b7ac2a7e 100644 --- a/test/mode-interOrderbook.test.js +++ b/test/mode-interOrderbook.test.js @@ -404,7 +404,11 @@ describe("Test inter-orderbook find opp", async function () { interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, - bytecode: "0x", + bytecode: getBountyEnsureBytecode( + ethers.utils.parseUnits(inputToEthPrice), + ethers.utils.parseUnits(outputToEthPrice), + ethers.constants.Zero, + ), }, signedContext: [], }; diff --git a/test/mode-intraOrderbook.test.js b/test/mode-intraOrderbook.test.js index d34f6387..a7c84110 100644 --- a/test/mode-intraOrderbook.test.js +++ b/test/mode-intraOrderbook.test.js @@ -270,6 +270,7 @@ describe("Test intra-orderbook find opp", async function () { }); assert.fail("expected to reject, but resolved"); } catch (error) { + const balance = ethers.BigNumber.from("1000000000000000000"); const withdrawInputCalldata = orderbook.interface.encodeFunctionData("withdraw2", [ orderPairObject.buyToken, "1", @@ -280,7 +281,26 @@ describe("Test intra-orderbook find opp", async function () { orderPairObject.sellToken, "1", ethers.constants.MaxUint256, - [], + [ + { + evaluable: { + interpreter: + orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, + store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, + bytecode: getWithdrawEnsureBytecode( + signer.account.address, + orderPairObject.buyToken, + orderPairObject.sellToken, + balance, + balance, + ethers.utils.parseUnits(inputToEthPrice), + ethers.utils.parseUnits(outputToEthPrice), + ethers.constants.Zero, + ), + }, + signedContext: [], + }, + ], ]); const clear2Calldata = orderbook.interface.encodeFunctionData("clear2", [ orderPairObject.takeOrders[0].takeOrder.order, diff --git a/test/mode-routeProcessor.test.js b/test/mode-routeProcessor.test.js index 3a671319..bd20ce12 100644 --- a/test/mode-routeProcessor.test.js +++ b/test/mode-routeProcessor.test.js @@ -199,7 +199,11 @@ describe("Test route processor dryrun", async function () { interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, - bytecode: "0x", + bytecode: getBountyEnsureBytecode( + ethers.utils.parseUnits(ethPrice), + ethers.constants.Zero, + ethers.constants.Zero, + ), }, signedContext: [], }; @@ -440,7 +444,11 @@ describe("Test route processor find opp", async function () { interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, - bytecode: "0x", + bytecode: getBountyEnsureBytecode( + ethers.utils.parseUnits(ethPrice), + ethers.constants.Zero, + ethers.constants.Zero, + ), }, signedContext: [], }; @@ -638,7 +646,11 @@ describe("Test find opp with retries", async function () { interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, - bytecode: "0x", + bytecode: getBountyEnsureBytecode( + ethers.utils.parseUnits(ethPrice), + ethers.constants.Zero, + ethers.constants.Zero, + ), }, signedContext: [], }; diff --git a/test/processPair.test.js b/test/processPair.test.js index fa0833b9..a65de918 100644 --- a/test/processPair.test.js +++ b/test/processPair.test.js @@ -150,6 +150,7 @@ describe("Test process pair", async function () { didClear: true, "details.inputToEthPrice": formatUnits(getCurrentInputToEthPrice()), "details.outputToEthPrice": "1", + "details.clearModePick": "rp4", "details.quote": JSON.stringify({ maxOutput: formatUnits(vaultBalance), ratio: formatUnits(ethers.constants.Zero), @@ -226,6 +227,7 @@ describe("Test process pair", async function () { "details.marketQuote.str": "0.99699", "details.inputToEthPrice": formatUnits(getCurrentInputToEthPrice()), "details.outputToEthPrice": "1", + "details.clearModePick": "inter", "details.quote": JSON.stringify({ maxOutput: formatUnits(vaultBalance), ratio: formatUnits(ethers.constants.Zero), @@ -546,6 +548,7 @@ describe("Test process pair", async function () { "details.outputToEthPrice": "1", "details.marketQuote.num": 0.99699, "details.marketQuote.str": "0.99699", + "details.clearModePick": "rp4", txNoneNodeError: true, "details.quote": JSON.stringify({ maxOutput: formatUnits(vaultBalance), @@ -567,7 +570,7 @@ describe("Test process pair", async function () { } }); - it("should fail to mine tx", async function () { + it("should revert tx", async function () { await mockServer.forPost("/rpc").thenSendJsonRpcResult(quoteResponse); const errorReceipt = { status: "reverted", @@ -606,7 +609,7 @@ describe("Test process pair", async function () { txUrl: scannerUrl + "/tx/" + txHash, actualGasCost: formatUnits(effectiveGasPrice.mul(gasUsed)), }, - reason: ProcessPairHaltReason.TxMineFailed, + reason: ProcessPairHaltReason.TxReverted, error: undefined, gasCost: undefined, spanAttributes: { @@ -626,6 +629,111 @@ describe("Test process pair", async function () { "details.outputToEthPrice": "1", "details.marketQuote.num": 0.99699, "details.marketQuote.str": "0.99699", + "details.clearModePick": "rp4", + "details.quote": JSON.stringify({ + maxOutput: formatUnits(vaultBalance), + ratio: formatUnits(ethers.constants.Zero), + }), + "details.estimatedProfit": formatUnits( + estimateProfit( + orderPairObject, + getCurrentInputToEthPrice(), + ethers.utils.parseUnits("1"), + undefined, + getCurrentPrice(vaultBalance), + vaultBalance, + ), + ), + }, + }; + assert.deepEqual(error, expected); + } + }); + + it("should fail to mine tx", async function () { + await mockServer.forPost("/rpc").thenSendJsonRpcResult(quoteResponse); + const errorRejection = new Error("timeout"); + dataFetcher.getCurrentPoolCodeMap = () => { + return poolCodeMap; + }; + signer.sendTransaction = async () => txHash; + viemClient.waitForTransactionReceipt = async () => Promise.reject(errorRejection); + try { + 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 = { + minimumInput: ethers.constants.One, + maximumInput: vaultBalance, + maximumIORatio: ethers.constants.MaxUint256, + orders: [orderPairObject.takeOrders[0].takeOrder], + data: expectedRouteData, + }; + const task = { + evaluable: { + interpreter: + orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, + store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, + bytecode: "0x", + }, + signedContext: [], + }; + const rawtx = { + data: arb.interface.encodeFunctionData("arb3", [ + orderPairObject.orderbook, + expectedTakeOrdersConfigStruct, + task, + ]), + to: arb.address, + gasPrice: gasPrice.mul(107).div(100).toString(), + gas: gasLimitEstimation.toString(), + nonce: 0, + from: signer.account.address, + }; + const expected = { + report: { + status: ProcessPairReportStatus.FoundOpportunity, + tokenPair: pair, + buyToken: orderPairObject.buyToken, + sellToken: orderPairObject.sellToken, + txUrl: scannerUrl + "/tx/" + txHash, + }, + reason: ProcessPairHaltReason.TxMineFailed, + error: errorRejection, + gasCost: undefined, + spanAttributes: { + "details.pair": pair, + "details.orders": [orderPairObject.takeOrders[0].id], + "details.gasPrice": gasPrice.mul(107).div(100).toString(), + "details.blockNumber": 123456, + "details.blockNumberDiff": 0, + "details.marketPrice": formatUnits(getCurrentPrice(vaultBalance)), + "details.amountIn": formatUnits(vaultBalance), + "details.amountOut": formatUnits(getAmountOut(vaultBalance), 6), + oppBlockNumber: 123456, + "details.route": expectedRouteVisual, + foundOpp: true, + "details.rawTx": JSON.stringify(rawtx), + "details.txUrl": scannerUrl + "/tx/" + txHash, + "details.inputToEthPrice": formatUnits(getCurrentInputToEthPrice()), + "details.outputToEthPrice": "1", + "details.marketQuote.num": 0.99699, + "details.marketQuote.str": "0.99699", + "details.clearModePick": "rp4", + txNoneNodeError: true, "details.quote": JSON.stringify({ maxOutput: formatUnits(vaultBalance), ratio: formatUnits(ethers.constants.Zero),