diff --git a/README.md b/README.md index ce610a6..1937669 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Other optional arguments are: - `--owner-profile`, Specifies the owner limit, example: --owner-profile 0x123456=12 . Will override the 'OWNER_PROFILE' in env variables - `--public-rpc`, Allows to use public RPCs as fallbacks, default is false. Will override the 'PUBLIC_RPC' 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 +- `--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 - `-V` or `--version`, output the version number - `-h` or `--help`, output usage information @@ -262,7 +262,7 @@ 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% +# Option to multiply the gas limit estimation from the rpc as percentage, default is 108, ie +8% GAS_LIMIT_MULTIPLIER= # Option to set a static gas limit for all submitting txs diff --git a/example.env b/example.env index 61d8b14..c3b9a35 100644 --- a/example.env +++ b/example.env @@ -88,7 +88,7 @@ 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% +# Option to multiply the gas limit estimation from the rpc as percentage, default is 108, ie +8% GAS_LIMIT_MULTIPLIER= # Option to set a static gas limit for all submitting txs diff --git a/src/account.ts b/src/account.ts index 0185e38..e78797e 100644 --- a/src/account.ts +++ b/src/account.ts @@ -525,11 +525,11 @@ export async function sweepToMainWallet( balance, ]) as `0x${string}`, }; - txs.push({ tx, bounty, balance: ethers.utils.formatUnits(balance, bounty.decimals) }); const gas = await fromWallet.estimateGas(tx); + txs.push({ tx, bounty, balance: ethers.utils.formatUnits(balance, bounty.decimals) }); cumulativeGasLimit = cumulativeGasLimit.add(gas); } catch { - failedBounties.push(bounty); + addWatchedToken(bounty, failedBounties); } } @@ -612,7 +612,7 @@ export async function sweepToMainWallet( code: SpanStatusCode.ERROR, message: "Failed to sweep back to main wallet: tx reverted", }); - failedBounties.push(txs[i].bounty); + addWatchedToken(txs[i].bounty, failedBounties); } fromWallet.BALANCE = fromWallet.BALANCE.sub(txCost); } catch (error) { @@ -621,14 +621,14 @@ export async function sweepToMainWallet( code: SpanStatusCode.ERROR, message: "Failed to sweep back to main wallet: " + errorSnapshot("", error), }); - failedBounties.push(txs[i].bounty); + addWatchedToken(txs[i].bounty, failedBounties); } span?.end(); } // empty gas if all tokens are swept if (!failedBounties.length) { - const span = tracer?.startSpan("sweep-gas-to-main-wallet", undefined, mainCtx); + const span = tracer?.startSpan("sweep-remaining-gas-to-main-wallet", undefined, mainCtx); span?.setAttribute("details.wallet", fromWallet.account.address); try { const gasLimit = ethers.BigNumber.from( @@ -889,7 +889,22 @@ export async function sweepToEth(config: BotConfig, tracer?: Tracer, ctx?: Conte } export async function setWatchedTokens(account: ViemClient, watchedTokens: TokenDetails[]) { - account.BOUNTY = watchedTokens; + account.BOUNTY = [...watchedTokens]; +} + +export function addWatchedToken( + token: TokenDetails, + watchedTokens: TokenDetails[], + account?: ViemClient, +) { + if (!watchedTokens.find((v) => v.address.toLowerCase() === token.address.toLowerCase())) { + watchedTokens.push(token); + } + if (account) { + if (!account.BOUNTY.find((v) => v.address.toLowerCase() === token.address.toLowerCase())) { + account.BOUNTY.push(token); + } + } } /** diff --git a/src/cli.ts b/src/cli.ts index 73a6fdd..591c82e 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -194,7 +194,7 @@ const getOptions = async (argv: any, version?: string) => { ) .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 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", ) .option( "--tx-gas ", @@ -446,7 +446,7 @@ export async function startup(argv: any, version?: string, tracer?: Tracer, ctx? 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; + options.gasLimitMultiplier = 108; } if (options.txGas) { if (typeof options.txGas === "number") { @@ -478,7 +478,7 @@ export async function startup(argv: any, version?: string, tracer?: Tracer, ctx? } const lastReadOrdersTimestamp = Math.floor(Date.now() / 1000); const tokens = getOrdersTokens(ordersDetails); - options.tokens = [...tokens]; + options.tokens = tokens; // get config const config = await getConfig( diff --git a/src/error.ts b/src/error.ts index 14c734b..40d0220 100644 --- a/src/error.ts +++ b/src/error.ts @@ -102,9 +102,12 @@ export function errorSnapshot(header: string, err: any): string { export function containsNodeError(err: BaseError): boolean { try { const snapshot = errorSnapshot("", err); + const parsed = parseRevertError(err); return ( // err instanceof TransactionRejectedRpcError || // err instanceof InvalidInputRpcError || + !!parsed.decoded || + !!parsed.raw.data || err instanceof FeeCapTooLowError || err instanceof ExecutionRevertedError || err instanceof InsufficientFundsError || diff --git a/src/order.ts b/src/order.ts index bf9af1c..d27bf8c 100644 --- a/src/order.ts +++ b/src/order.ts @@ -2,6 +2,7 @@ import { OrderV3 } from "./abis"; import { SgOrder } from "./query"; import { Span } from "@opentelemetry/api"; import { hexlify } from "ethers/lib/utils"; +import { addWatchedToken } from "./account"; import { getTokenSymbol, shuffleArray } from "./utils"; import { decodeAbiParameters, parseAbiParameters } from "viem"; import { @@ -67,13 +68,14 @@ export async function getOrderPairs( _outputSymbol = symbol; } } else { - if (!tokens.find((v) => v.address.toLowerCase() === _output.token.toLowerCase())) { - tokens.push({ + addWatchedToken( + { address: _output.token.toLowerCase(), symbol: _outputSymbol, decimals: _output.decimals, - }); - } + }, + tokens, + ); } for (let k = 0; k < orderStruct.validInputs.length; k++) { @@ -91,13 +93,14 @@ export async function getOrderPairs( _inputSymbol = symbol; } } else { - if (!tokens.find((v) => v.address.toLowerCase() === _input.token.toLowerCase())) { - tokens.push({ + addWatchedToken( + { address: _input.token.toLowerCase(), symbol: _inputSymbol, decimals: _input.decimals, - }); - } + }, + tokens, + ); } if (_input.token.toLowerCase() !== _output.token.toLowerCase()) @@ -278,29 +281,30 @@ export function prepareOrdersForRound( for (const [, ownerProfile] of ownersProfileMap) { let remainingLimit = ownerProfile.limit; const activeOrdersProfiles = Array.from(ownerProfile.orders).filter((v) => v[1].active); - const remainingOrdersPairs = activeOrdersProfiles.filter( + let remainingOrdersPairs = activeOrdersProfiles.filter( (v) => v[1].takeOrders.length > 0, ); + // reset if all orders are already consumed if (remainingOrdersPairs.length === 0) { - for (const [orderHash, orderProfile] of activeOrdersProfiles) { + for (const [, orderProfile] of activeOrdersProfiles) { orderProfile.takeOrders.push(...orderProfile.consumedTakeOrders.splice(0)); - if (remainingLimit > 0) { - const consumingOrderPairs = orderProfile.takeOrders.splice( - 0, - remainingLimit, - ); - remainingLimit -= consumingOrderPairs.length; - orderProfile.consumedTakeOrders.push(...consumingOrderPairs); - gatherPairs( - orderbook, - orderHash, - consumingOrderPairs, - orderbookBundledOrders, - ); - } } - } else { - for (const [orderHash, orderProfile] of remainingOrdersPairs) { + remainingOrdersPairs = activeOrdersProfiles; + } + // consume orders limits + for (const [orderHash, orderProfile] of remainingOrdersPairs) { + if (remainingLimit > 0) { + const consumingOrderPairs = orderProfile.takeOrders.splice(0, remainingLimit); + remainingLimit -= consumingOrderPairs.length; + orderProfile.consumedTakeOrders.push(...consumingOrderPairs); + gatherPairs(orderbook, orderHash, consumingOrderPairs, orderbookBundledOrders); + } + } + // if all orders are consumed and still there is limit remaining, + // reset and start consuming again from top until limit is reached + if (remainingLimit > 0) { + for (const [orderHash, orderProfile] of activeOrdersProfiles) { + orderProfile.takeOrders.push(...orderProfile.consumedTakeOrders.splice(0)); if (remainingLimit > 0) { const consumingOrderPairs = orderProfile.takeOrders.splice( 0, @@ -351,6 +355,7 @@ function gatherPairs( v.sellToken.toLowerCase() === pair.sellToken.toLowerCase(), ); if (bundleOrder) { + // make sure to not duplicate if ( !bundleOrder.takeOrders.find((v) => v.id.toLowerCase() === orderHash.toLowerCase()) ) { diff --git a/src/processOrders.ts b/src/processOrders.ts index d9eac75..33849ab 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -8,7 +8,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 { fundOwnedOrders, getNonce, rotateAccounts } from "./account"; +import { addWatchedToken, fundOwnedOrders, getNonce, rotateAccounts } from "./account"; import { containsNodeError, ErrorSeverity, errorSnapshot, handleRevert, isTimeout } from "./error"; import { Report, @@ -762,27 +762,21 @@ export async function processPair(args: { // keep track of gas consumption of the account and bounty token result.gasCost = actualGasCost; - if ( - inputTokenIncome && - inputTokenIncome.gt(0) && - !signer.BOUNTY.find((v) => v.address === orderPairObject.buyToken) - ) { - signer.BOUNTY.push({ + 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) && - !signer.BOUNTY.find((v) => v.address === orderPairObject.sellToken) - ) { - signer.BOUNTY.push({ + 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 { diff --git a/test/orders.test.js b/test/orders.test.js index 26d6375..a2ea81f 100644 --- a/test/orders.test.js +++ b/test/orders.test.js @@ -753,7 +753,7 @@ describe("Test order details", async function () { [order1, order2, order3, order4, order5, order6, order7, order8], undefined, [], - { [owner1]: 3, [owner2]: 1 }, // set owner1 limit as 3, owner2 to 1 + { [owner1]: 4, [owner2]: 1 }, // set owner1 limit as 4, owner2 to 1 ); // prepare orders for first round @@ -768,8 +768,8 @@ describe("Test order details", async function () { sellTokenSymbol: token2.symbol, sellTokenDecimals: token2.decimals, orderbook, - // first 3 owner1 orders for round1, owner1 limit is 3 - takeOrders: owner1Orders.slice(0, 3).map((v) => ({ + // first 4 owner1 orders for round1, owner1 limit is 4 + takeOrders: owner1Orders.slice(0, 4).map((v) => ({ id: v.id, takeOrder: { order: v.struct, @@ -814,8 +814,11 @@ describe("Test order details", async function () { sellTokenSymbol: token2.symbol, sellTokenDecimals: token2.decimals, orderbook, - // second 3 owner1 orders for round2, owner1 limit is 3 - takeOrders: owner1Orders.slice(3, owner1Orders.length).map((v) => ({ + // first2 and last 2 owner1 orders for round2, owner1 limit is 4 + takeOrders: [ + ...owner1Orders.slice(4, owner1Orders.length), + ...owner1Orders.slice(0, 2), + ].map((v) => ({ id: v.id, takeOrder: { order: v.struct, @@ -861,8 +864,8 @@ describe("Test order details", async function () { sellTokenSymbol: token2.symbol, sellTokenDecimals: token2.decimals, orderbook, - // first 3 owner1 orders again for round3, owner1 limit is 3 - takeOrders: owner1Orders.slice(0, 3).map((v) => ({ + // last 4 owner1 orders again for round3, owner1 limit is 4 + takeOrders: owner1Orders.slice(2).map((v) => ({ id: v.id, takeOrder: { order: v.struct, @@ -894,6 +897,52 @@ describe("Test order details", async function () { ], ]; assert.deepEqual(result3, expected3); + + // prepare orders for 4th round + const result4 = prepareOrdersForRound(allOrders, false); + const expected4 = [ + [ + { + buyToken: token1.address, + buyTokenSymbol: token1.symbol, + buyTokenDecimals: token1.decimals, + sellToken: token2.address, + sellTokenSymbol: token2.symbol, + sellTokenDecimals: token2.decimals, + orderbook, + // back to first 4 owner1 orders for round4, owner1 limit is 4 + takeOrders: owner1Orders.slice(0, 4).map((v) => ({ + id: v.id, + takeOrder: { + order: v.struct, + inputIOIndex: 0, + outputIOIndex: 0, + signedContext: [], + }, + })), + }, + { + buyToken: token2.address, + buyTokenSymbol: token2.symbol, + buyTokenDecimals: token2.decimals, + sellToken: token1.address, + sellTokenSymbol: token1.symbol, + sellTokenDecimals: token1.decimals, + orderbook, + // second 1 owner2 orders for round4, owner2 limit is 1 + takeOrders: owner2Orders.slice(1).map((v) => ({ + id: v.id, + takeOrder: { + order: v.struct, + inputIOIndex: 0, + outputIOIndex: 0, + signedContext: [], + }, + })), + }, + ], + ]; + assert.deepEqual(result4, expected4); }); });