diff --git a/src/modes/index.ts b/src/modes/index.ts index 2a53796b..7021ff1f 100644 --- a/src/modes/index.ts +++ b/src/modes/index.ts @@ -6,6 +6,7 @@ import { findOpp as findInterObOpp } from "./interOrderbook"; import { findOpp as findIntraObOpp } from "./intraOrderbook"; import { findOppWithRetries as findRpOpp } from "./routeProcessor"; import { BotConfig, BundledOrders, ViemClient, DryrunResult, SpanAttrs } from "../types"; +import { extendSpanAttributes } from "../utils"; /** * The main entrypoint for the main logic to find opps. @@ -116,18 +117,24 @@ export async function findOpp({ noneNodeError: undefined, }; if ((allResults[0] as any)?.reason?.spanAttributes) { - spanAttributes["routeProcessor"] = JSON.stringify( - (allResults[0] as any).reason.spanAttributes, + extendSpanAttributes( + spanAttributes, + JSON.stringify((allResults[0] as any).reason.spanAttributes), + "routeProcessor", ); } if ((allResults[1] as any)?.reason?.spanAttributes) { - spanAttributes["intraOrderbook"] = JSON.stringify( - (allResults[1] as any).reason.spanAttributes["intraOrderbook"], + extendSpanAttributes( + spanAttributes, + JSON.stringify((allResults[1] as any).reason.spanAttributes), + "intraOrderbook", ); } if ((allResults[2] as any)?.reason?.spanAttributes) { - spanAttributes["interOrderbook"] = JSON.stringify( - (allResults[2] as any).reason.spanAttributes, + extendSpanAttributes( + spanAttributes, + JSON.stringify((allResults[2] as any).reason.spanAttributes), + "interOrderbook", ); } if ((allResults[0] as any)?.reason?.value?.noneNodeError) { diff --git a/src/modes/interOrderbook.ts b/src/modes/interOrderbook.ts index 3b978cc3..38098a17 100644 --- a/src/modes/interOrderbook.ts +++ b/src/modes/interOrderbook.ts @@ -4,8 +4,14 @@ import { BigNumber, Contract, ethers } from "ethers"; import { containsNodeError, errorSnapshot } from "../error"; import { getBountyEnsureRainlang, parseRainlang } from "../task"; import { BaseError, ExecutionRevertedError, PublicClient } from "viem"; -import { ONE18, scale18To, estimateProfit, withBigintSerializer } from "../utils"; import { BotConfig, BundledOrders, ViemClient, DryrunResult, SpanAttrs } from "../types"; +import { + ONE18, + scale18To, + estimateProfit, + withBigintSerializer, + extendSpanAttributes, +} from "../utils"; const obInterface = new ethers.utils.Interface(orderbookAbi); @@ -122,13 +128,21 @@ export async function dryrun({ gasLimit = ethers.BigNumber.from(estimation.gas).mul(config.gasLimitMultiplier).div(100); // include dryrun headroom gas estimation in otel logs - spanAttributes["gasEst.headroom.gasLimit"] = estimation.gas.toString(); - spanAttributes["gasEst.headroom.totalCost"] = estimation.totalGasCost.toString(); - spanAttributes["gasEst.headroom.gasPrice"] = estimation.gasPrice.toString(); - if (config.isSpecialL2) { - spanAttributes["gasEst.headroom.l1Cost"] = estimation.l1Cost.toString(); - spanAttributes["gasEst.headroom.l1GasPrice"] = estimation.l1GasPrice.toString(); - } + extendSpanAttributes( + spanAttributes, + JSON.stringify({ + gasLimit: estimation.gas.toString(), + totalCost: estimation.totalGasCost.toString(), + gasPrice: estimation.gasPrice.toString(), + ...(config.isSpecialL2 + ? { + l1Cost: estimation.l1Cost.toString(), + l1GasPrice: estimation.l1GasPrice.toString(), + } + : {}), + }), + "gasEst.headroom", + ); } catch (e) { const isNodeError = containsNodeError(e as BaseError); const errMsg = errorSnapshot("", e); @@ -198,13 +212,21 @@ export async function dryrun({ gasCost = gasLimit.mul(gasPrice).add(estimation.l1Cost); // include dryrun final gas estimation in otel logs - spanAttributes["gasEst.final.gasLimit"] = estimation.gas.toString(); - spanAttributes["gasEst.final.totalCost"] = estimation.totalGasCost.toString(); - spanAttributes["gasEst.final.gasPrice"] = estimation.gasPrice.toString(); - if (config.isSpecialL2) { - spanAttributes["gasEst.final.l1Cost"] = estimation.l1Cost.toString(); - spanAttributes["gasEst.final.l1GasPrice"] = estimation.l1GasPrice.toString(); - } + extendSpanAttributes( + spanAttributes, + JSON.stringify({ + gasLimit: estimation.gas.toString(), + totalCost: estimation.totalGasCost.toString(), + gasPrice: estimation.gasPrice.toString(), + ...(config.isSpecialL2 + ? { + l1Cost: estimation.l1Cost.toString(), + l1GasPrice: estimation.l1GasPrice.toString(), + } + : {}), + }), + "gasEst.final", + ); task.evaluable.bytecode = await parseRainlang( await getBountyEnsureRainlang( ethers.utils.parseUnits(inputToEthPrice), @@ -401,12 +423,13 @@ export async function findOpp({ // } catch { // /**/ // } - const allOrderbooksAttributes: any = {}; for (let i = 0; i < e.errors.length; i++) { - allOrderbooksAttributes[opposingOrderbookOrders[i].orderbook] = - e.errors[i].spanAttributes; + extendSpanAttributes( + spanAttributes, + JSON.stringify(e.errors[i].spanAttributes), + "againstOrderbooks." + opposingOrderbookOrders[i].orderbook, + ); } - spanAttributes["againstOrderbooks"] = JSON.stringify(allOrderbooksAttributes); const noneNodeErrors = allNoneNodeErrors.filter((v) => !!v); if (allNoneNodeErrors.length && noneNodeErrors.length / allNoneNodeErrors.length > 0.5) { result.value = { diff --git a/src/modes/intraOrderbook.ts b/src/modes/intraOrderbook.ts index 9d1c95b7..f694412f 100644 --- a/src/modes/intraOrderbook.ts +++ b/src/modes/intraOrderbook.ts @@ -3,8 +3,8 @@ import { estimateGasCost } from "../gas"; import { BigNumber, ethers } from "ethers"; import { containsNodeError, errorSnapshot } from "../error"; import { getWithdrawEnsureRainlang, parseRainlang } from "../task"; -import { estimateProfit, scale18, withBigintSerializer } from "../utils"; import { BaseError, erc20Abi, ExecutionRevertedError, PublicClient } from "viem"; +import { estimateProfit, scale18, withBigintSerializer, extendSpanAttributes } from "../utils"; import { SpanAttrs, BotConfig, @@ -120,13 +120,21 @@ export async function dryrun({ gasLimit = ethers.BigNumber.from(estimation.gas).mul(config.gasLimitMultiplier).div(100); // include dryrun headroom gas estimation in otel logs - spanAttributes["gasEst.headroom.gasLimit"] = estimation.gas.toString(); - spanAttributes["gasEst.headroom.totalCost"] = estimation.totalGasCost.toString(); - spanAttributes["gasEst.headroom.gasPrice"] = estimation.gasPrice.toString(); - if (config.isSpecialL2) { - spanAttributes["gasEst.headroom.l1Cost"] = estimation.l1Cost.toString(); - spanAttributes["gasEst.headroom.l1GasPrice"] = estimation.l1GasPrice.toString(); - } + extendSpanAttributes( + spanAttributes, + JSON.stringify({ + gasLimit: estimation.gas.toString(), + totalCost: estimation.totalGasCost.toString(), + gasPrice: estimation.gasPrice.toString(), + ...(config.isSpecialL2 + ? { + l1Cost: estimation.l1Cost.toString(), + l1GasPrice: estimation.l1GasPrice.toString(), + } + : {}), + }), + "gasEst.headroom", + ); } catch (e) { // reason, code, method, transaction, error, stack, message const isNodeError = containsNodeError(e as BaseError); @@ -206,13 +214,21 @@ export async function dryrun({ gasCost = gasLimit.mul(gasPrice).add(estimation.l1Cost); // include dryrun final gas estimation in otel logs - spanAttributes["gasEst.final.gasLimit"] = estimation.gas.toString(); - spanAttributes["gasEst.final.totalCost"] = estimation.totalGasCost.toString(); - spanAttributes["gasEst.final.gasPrice"] = estimation.gasPrice.toString(); - if (config.isSpecialL2) { - spanAttributes["gasEst.final.l1Cost"] = estimation.l1Cost.toString(); - spanAttributes["gasEst.final.l1GasPrice"] = estimation.l1GasPrice.toString(); - } + extendSpanAttributes( + spanAttributes, + JSON.stringify({ + gasLimit: estimation.gas.toString(), + totalCost: estimation.totalGasCost.toString(), + gasPrice: estimation.gasPrice.toString(), + ...(config.isSpecialL2 + ? { + l1Cost: estimation.l1Cost.toString(), + l1GasPrice: estimation.l1GasPrice.toString(), + } + : {}), + }), + "gasEst.final", + ); task.evaluable.bytecode = await parseRainlang( await getWithdrawEnsureRainlang( signer.account.address, @@ -342,7 +358,6 @@ export async function findOpp({ ); if (!opposingOrders || !opposingOrders.length) throw undefined; - const allErrorAttributes: string[] = []; const allNoneNodeErrors: (string | undefined)[] = []; const inputBalance = scale18( await viemClient.readContract({ @@ -379,10 +394,13 @@ export async function findOpp({ }); } catch (e: any) { allNoneNodeErrors.push(e?.value?.noneNodeError); - allErrorAttributes.push(JSON.stringify(e.spanAttributes)); + extendSpanAttributes( + spanAttributes, + JSON.stringify(e.spanAttributes), + "intraOrderbook." + i, + ); } } - spanAttributes["intraOrderbook"] = allErrorAttributes; const noneNodeErrors = allNoneNodeErrors.filter((v) => !!v); if (allNoneNodeErrors.length && noneNodeErrors.length / allNoneNodeErrors.length > 0.5) { result.value = { diff --git a/src/modes/routeProcessor.ts b/src/modes/routeProcessor.ts index a503d31d..6d21edf3 100644 --- a/src/modes/routeProcessor.ts +++ b/src/modes/routeProcessor.ts @@ -14,6 +14,7 @@ import { estimateProfit, visualizeRoute, withBigintSerializer, + extendSpanAttributes, } from "../utils"; /** @@ -190,13 +191,21 @@ export async function dryrun({ .div(100); // include dryrun headroom gas estimation in otel logs - spanAttributes["gasEst.headroom.gasLimit"] = estimation.gas.toString(); - spanAttributes["gasEst.headroom.totalCost"] = estimation.totalGasCost.toString(); - spanAttributes["gasEst.headroom.gasPrice"] = estimation.gasPrice.toString(); - if (config.isSpecialL2) { - spanAttributes["gasEst.headroom.l1Cost"] = estimation.l1Cost.toString(); - spanAttributes["gasEst.headroom.l1GasPrice"] = estimation.l1GasPrice.toString(); - } + extendSpanAttributes( + spanAttributes, + JSON.stringify({ + gasLimit: estimation.gas.toString(), + totalCost: estimation.totalGasCost.toString(), + gasPrice: estimation.gasPrice.toString(), + ...(config.isSpecialL2 + ? { + l1Cost: estimation.l1Cost.toString(), + l1GasPrice: estimation.l1GasPrice.toString(), + } + : {}), + }), + "gasEst.headroom", + ); } catch (e) { // reason, code, method, transaction, error, stack, message const isNodeError = containsNodeError(e as BaseError); @@ -268,13 +277,21 @@ export async function dryrun({ gasCost = gasLimit.mul(gasPrice).add(estimation.l1Cost); // include dryrun final gas estimation in otel logs - spanAttributes["gasEst.final.gasLimit"] = estimation.gas.toString(); - spanAttributes["gasEst.final.totalCost"] = estimation.totalGasCost.toString(); - spanAttributes["gasEst.final.gasPrice"] = estimation.gasPrice.toString(); - if (config.isSpecialL2) { - spanAttributes["gasEst.final.l1Cost"] = estimation.l1Cost.toString(); - spanAttributes["gasEst.final.l1GasPrice"] = estimation.l1GasPrice.toString(); - } + extendSpanAttributes( + spanAttributes, + JSON.stringify({ + gasLimit: estimation.gas.toString(), + totalCost: estimation.totalGasCost.toString(), + gasPrice: estimation.gasPrice.toString(), + ...(config.isSpecialL2 + ? { + l1Cost: estimation.l1Cost.toString(), + l1GasPrice: estimation.l1GasPrice.toString(), + } + : {}), + }), + "gasEst.final", + ); task.evaluable.bytecode = await parseRainlang( await getBountyEnsureRainlang( ethers.utils.parseUnits(ethPrice), @@ -413,7 +430,7 @@ export async function findOpp({ // the fail reason can only be no route in case all hops fail reasons are no route if (e.reason !== RouteProcessorDryrunHaltReason.NoRoute) noRoute = false; allNoneNodeErrors.push(e?.value?.noneNodeError); - spanAttributes["full"] = JSON.stringify(e.spanAttributes); + extendSpanAttributes(spanAttributes, JSON.stringify(e.spanAttributes), "full"); } if (!hasPriceMatch.value) { const maxTradeSize = findMaxInput({ @@ -446,7 +463,7 @@ export async function findOpp({ // the fail reason can only be no route in case all hops fail reasons are no route if (e.reason !== RouteProcessorDryrunHaltReason.NoRoute) noRoute = false; allNoneNodeErrors.push(e?.value?.noneNodeError); - spanAttributes["partial"] = JSON.stringify(e.spanAttributes); + extendSpanAttributes(spanAttributes, JSON.stringify(e.spanAttributes), "partial"); } } } diff --git a/src/processOrders.ts b/src/processOrders.ts index c003521b..8b0223fb 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -602,40 +602,9 @@ export async function processPair(args: { } } } catch (e: any) { - // record all span attributes in their scopes + // record all span attributes for (const attrKey in e.spanAttributes) { - if (attrKey === "routeProcessor") { - const rpAttrs = JSON.parse(e.spanAttributes[attrKey]); - for (const key in rpAttrs) { - const innerAttrs = JSON.parse(rpAttrs[key]); - for (const innerKey in innerAttrs) { - spanAttributes["details.routeProcessor." + key + "." + innerKey] = - innerAttrs[innerKey]; - } - } - } else if (attrKey === "intraOrderbook") { - const intraAttrs = JSON.parse(e.spanAttributes[attrKey]); - for (let i = 0; i < intraAttrs.length; i++) { - const innerAttrs = JSON.parse(intraAttrs[i]); - for (const innerKey in innerAttrs) { - spanAttributes["details.intraOrderbook." + i + "." + innerKey] = - innerAttrs[innerKey]; - } - } - } else if (attrKey === "interOrderbook") { - const interAttrs = JSON.parse( - JSON.parse(e.spanAttributes[attrKey])["againstOrderbooks"], - ); - for (const key in interAttrs) { - for (const innerKey in interAttrs[key]) { - spanAttributes[ - "details.interOrderbook.againstOrderbooks." + key + "." + innerKey - ] = interAttrs[key][innerKey]; - } - } - } else { - spanAttributes["details." + attrKey] = e.spanAttributes[attrKey]; - } + spanAttributes["details." + attrKey] = e.spanAttributes[attrKey]; } if (e.noneNodeError) { spanAttributes["details.noneNodeError"] = true; diff --git a/src/utils.ts b/src/utils.ts index 82f9f2ad..09e2657c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -816,16 +816,18 @@ export function scale18To(value: BigNumberish, targetDecimals: BigNumberish): Bi */ export function extendSpanAttributes( spanAttributes: Record, - newAttributes: Record, + newAttributes: string, header: string, excludeHeaderForKeys: string[] = [], ) { - for (const attrKey in newAttributes) { + const attrs = JSON.parse(newAttributes); + for (const attrKey in attrs) { if (!excludeHeaderForKeys.includes(attrKey)) { - spanAttributes[header + "." + attrKey] = newAttributes[attrKey]; + spanAttributes[header + "." + attrKey] = attrs[attrKey]; } else { - spanAttributes[attrKey] = newAttributes[attrKey]; + spanAttributes[attrKey] = attrs[attrKey]; } + delete attrs[attrKey]; } } diff --git a/test/findOpp.test.js b/test/findOpp.test.js index 57cae604..1d71152f 100644 --- a/test/findOpp.test.js +++ b/test/findOpp.test.js @@ -504,31 +504,28 @@ describe("Test find opp", async function () { oppBlockNumber: undefined, noneNodeError: errorSnapshot("", err), spanAttributes: { - routeProcessor: JSON.stringify({ - full: JSON.stringify({ - amountIn: formatUnits(vaultBalance), - amountOut: formatUnits(getAmountOut(vaultBalance), 6), - marketPrice: formatUnits(getCurrentPrice(vaultBalance)), - route: expectedRouteVisual, - blockNumber: oppBlockNumber, - stage: 1, - isNodeError: false, - error: errorSnapshot("", err), - rawtx: rawtx, - }), - }), - interOrderbook: JSON.stringify({ - againstOrderbooks: JSON.stringify({ - [opposingOrderbookAddress]: { - maxInput: vaultBalance.toString(), - blockNumber: oppBlockNumber, - stage: 1, - isNodeError: false, - error: errorSnapshot("", err), - rawtx: JSON.stringify(rawtx2, withBigintSerializer), - }, - }), - }), + // rp span attrs + "routeProcessor.full.stage": 1, + "routeProcessor.full.rawtx": rawtx, + "routeProcessor.full.isNodeError": false, + "routeProcessor.full.route": expectedRouteVisual, + "routeProcessor.full.blockNumber": oppBlockNumber, + "routeProcessor.full.error": errorSnapshot("", err), + "routeProcessor.full.amountIn": formatUnits(vaultBalance), + "routeProcessor.full.amountOut": formatUnits(getAmountOut(vaultBalance), 6), + "routeProcessor.full.marketPrice": formatUnits(getCurrentPrice(vaultBalance)), + + // inter-ob span attrs + [`interOrderbook.againstOrderbooks.${opposingOrderbookAddress}.stage`]: 1, + [`interOrderbook.againstOrderbooks.${opposingOrderbookAddress}.isNodeError`]: false, + [`interOrderbook.againstOrderbooks.${opposingOrderbookAddress}.blockNumber`]: + oppBlockNumber, + [`interOrderbook.againstOrderbooks.${opposingOrderbookAddress}.rawtx`]: + JSON.stringify(rawtx2, withBigintSerializer), + [`interOrderbook.againstOrderbooks.${opposingOrderbookAddress}.maxInput`]: + vaultBalance.toString(), + [`interOrderbook.againstOrderbooks.${opposingOrderbookAddress}.error`]: + errorSnapshot("", err), }, }; assert.deepEqual(error, expected); diff --git a/test/mode-interOrderbook.test.js b/test/mode-interOrderbook.test.js index 7991895f..f5b3a198 100644 --- a/test/mode-interOrderbook.test.js +++ b/test/mode-interOrderbook.test.js @@ -375,16 +375,13 @@ describe("Test inter-orderbook find opp", async function () { }, reason: undefined, spanAttributes: { - againstOrderbooks: JSON.stringify({ - [opposingOrderbookAddress]: { - maxInput: vaultBalance.toString(), - blockNumber: oppBlockNumber, - stage: 1, - isNodeError: false, - error: errorSnapshot("", err), - rawtx: JSON.stringify(rawtx), - }, - }), + [`againstOrderbooks.${opposingOrderbookAddress}.blockNumber`]: oppBlockNumber, + [`againstOrderbooks.${opposingOrderbookAddress}.stage`]: 1, + [`againstOrderbooks.${opposingOrderbookAddress}.isNodeError`]: false, + [`againstOrderbooks.${opposingOrderbookAddress}.error`]: errorSnapshot("", err), + [`againstOrderbooks.${opposingOrderbookAddress}.rawtx`]: JSON.stringify(rawtx), + [`againstOrderbooks.${opposingOrderbookAddress}.maxInput`]: + vaultBalance.toString(), }, }; assert.deepEqual(error, expected); diff --git a/test/mode-intraOrderbook.test.js b/test/mode-intraOrderbook.test.js index b0d40eee..fd713563 100644 --- a/test/mode-intraOrderbook.test.js +++ b/test/mode-intraOrderbook.test.js @@ -375,15 +375,11 @@ describe("Test intra-orderbook find opp", async function () { }, reason: undefined, spanAttributes: { - intraOrderbook: [ - JSON.stringify({ - blockNumber: oppBlockNumber, - stage: 1, - isNodeError: false, - error: errorSnapshot("", err), - rawtx: JSON.stringify(rawtx), - }), - ], + "intraOrderbook.0.blockNumber": oppBlockNumber, + "intraOrderbook.0.stage": 1, + "intraOrderbook.0.isNodeError": false, + "intraOrderbook.0.error": errorSnapshot("", err), + "intraOrderbook.0.rawtx": JSON.stringify(rawtx), }, }; assert.deepEqual(error, expected); diff --git a/test/mode-routeProcessor.test.js b/test/mode-routeProcessor.test.js index 7e4f4e75..0c983006 100644 --- a/test/mode-routeProcessor.test.js +++ b/test/mode-routeProcessor.test.js @@ -532,17 +532,15 @@ describe("Test route processor find opp", async function () { }, reason: RouteProcessorDryrunHaltReason.NoOpportunity, spanAttributes: { - full: JSON.stringify({ - amountIn: formatUnits(vaultBalance), - amountOut: formatUnits(getAmountOut(vaultBalance), 6), - marketPrice: formatUnits(getCurrentPrice(vaultBalance)), - route: expectedRouteVisual, - blockNumber: oppBlockNumber, - stage: 1, - isNodeError: false, - error: errorSnapshot("", ethers.errors.UNPREDICTABLE_GAS_LIMIT), - rawtx: rawtx, - }), + "full.amountIn": formatUnits(vaultBalance), + "full.amountOut": formatUnits(getAmountOut(vaultBalance), 6), + "full.marketPrice": formatUnits(getCurrentPrice(vaultBalance)), + "full.route": expectedRouteVisual, + "full.blockNumber": oppBlockNumber, + "full.stage": 1, + "full.isNodeError": false, + "full.error": errorSnapshot("", ethers.errors.UNPREDICTABLE_GAS_LIMIT), + "full.rawtx": rawtx, }, }; assert.deepEqual(error, expected); @@ -573,10 +571,8 @@ describe("Test route processor find opp", async function () { value: undefined, reason: RouteProcessorDryrunHaltReason.NoRoute, spanAttributes: { - full: JSON.stringify({ - amountIn: formatUnits(vaultBalance), - route: "no-way", - }), + "full.amountIn": formatUnits(vaultBalance), + "full.route": "no-way", }, }; assert.deepEqual(error, expected); @@ -760,17 +756,15 @@ describe("Test find opp with retries", async function () { }, reason: RouteProcessorDryrunHaltReason.NoOpportunity, spanAttributes: { - full: JSON.stringify({ - amountIn: formatUnits(vaultBalance), - amountOut: formatUnits(getAmountOut(vaultBalance), 6), - marketPrice: formatUnits(getCurrentPrice(vaultBalance)), - route: expectedRouteVisual, - blockNumber: oppBlockNumber, - stage: 1, - isNodeError: false, - error: errorSnapshot("", ethers.errors.UNPREDICTABLE_GAS_LIMIT), - rawtx: rawtx, - }), + "full.amountIn": formatUnits(vaultBalance), + "full.amountOut": formatUnits(getAmountOut(vaultBalance), 6), + "full.marketPrice": formatUnits(getCurrentPrice(vaultBalance)), + "full.route": expectedRouteVisual, + "full.blockNumber": oppBlockNumber, + "full.stage": 1, + "full.isNodeError": false, + "full.error": errorSnapshot("", ethers.errors.UNPREDICTABLE_GAS_LIMIT), + "full.rawtx": rawtx, }, }; assert.deepEqual(error, expected);