diff --git a/src/tests/math.test.ts b/src/tests/math.test.ts index 6303d6be..7fe88c30 100644 --- a/src/tests/math.test.ts +++ b/src/tests/math.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getPercentile } from "../utils/math"; +import { bigMath, getPercentile } from "../utils/math"; describe("getPercentile", () => { it("should correctly calculate the p50 (median) of a sorted array", () => { @@ -27,3 +27,81 @@ describe("getPercentile", () => { expect(getPercentile(numbers, 50)).toBe(0); }); }); + +describe("bigMath", () => { + describe("min", () => { + it("should return the smaller of two positive numbers", () => { + const a = 5n; + const b = 10n; + expect(bigMath.min(a, b)).toBe(5n); + }); + + it("should return the smaller of two negative numbers", () => { + const a = -10n; + const b = -5n; + expect(bigMath.min(a, b)).toBe(-10n); + }); + + it("should handle equal numbers", () => { + const a = 5n; + const b = 5n; + expect(bigMath.min(a, b)).toBe(5n); + }); + + it("should handle zero and positive number", () => { + const a = 0n; + const b = 5n; + expect(bigMath.min(a, b)).toBe(0n); + }); + + it("should handle zero and negative number", () => { + const a = 0n; + const b = -5n; + expect(bigMath.min(a, b)).toBe(-5n); + }); + + it("should handle very large numbers", () => { + const a = BigInt(Number.MAX_SAFE_INTEGER) * 2n; + const b = BigInt(Number.MAX_SAFE_INTEGER); + expect(bigMath.min(a, b)).toBe(b); + }); + }); + + describe("max", () => { + it("should return the larger of two positive numbers", () => { + const a = 5n; + const b = 10n; + expect(bigMath.max(a, b)).toBe(10n); + }); + + it("should return the larger of two negative numbers", () => { + const a = -10n; + const b = -5n; + expect(bigMath.max(a, b)).toBe(-5n); + }); + + it("should handle equal numbers", () => { + const a = 5n; + const b = 5n; + expect(bigMath.max(a, b)).toBe(5n); + }); + + it("should handle zero and positive number", () => { + const a = 0n; + const b = 5n; + expect(bigMath.max(a, b)).toBe(5n); + }); + + it("should handle zero and negative number", () => { + const a = 0n; + const b = -5n; + expect(bigMath.max(a, b)).toBe(0n); + }); + + it("should handle very large numbers", () => { + const a = BigInt(Number.MAX_SAFE_INTEGER) * 2n; + const b = BigInt(Number.MAX_SAFE_INTEGER); + expect(bigMath.max(a, b)).toBe(a); + }); + }); +}); diff --git a/src/utils/env.ts b/src/utils/env.ts index e93a0320..3fa84dcc 100644 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -102,10 +102,12 @@ export const env = createEnv({ /** * Experimental env vars. These may be renamed or removed in future non-major releases. */ - // Sets how long the mine worker waits for a transaction receipt before considering the transaction dropped (default: 30 minutes). + // Sets how long the mine worker waits for a transaction receipt before considering the transaction dropped. Default: 30 minutes EXPERIMENTAL__MINE_WORKER_TIMEOUT_SECONDS: z.coerce .number() .default(30 * 60), + // Sets the max gas price for a transaction attempt. Most RPCs reject transactions above a certain gas price. Default: 10^18 wei. + EXPERIMENTAL__MAX_GAS_PRICE_WEI: z.coerce.number().default(10 ** 18), }, clientPrefix: "NEVER_USED", client: {}, @@ -144,6 +146,8 @@ export const env = createEnv({ NONCE_MAP_COUNT: process.env.NONCE_MAP_COUNT, EXPERIMENTAL__MINE_WORKER_TIMEOUT_SECONDS: process.env.EXPERIMENTAL__MINE_WORKER_TIMEOUT_SECONDS, + EXPERIMENTAL__MAX_GAS_PRICE_WEI: + process.env.EXPERIMENTAL__MAX_GAS_PRICE_WEI, METRICS_PORT: process.env.METRICS_PORT, METRICS_ENABLED: process.env.METRICS_ENABLED, }, diff --git a/src/utils/math.ts b/src/utils/math.ts index 2f693d66..5ec56723 100644 --- a/src/utils/math.ts +++ b/src/utils/math.ts @@ -7,3 +7,8 @@ export const getPercentile = (arr: number[], percentile: number): number => { const index = Math.floor((percentile / 100) * (arr.length - 1)); return arr[index]; }; + +export const BigIntMath = { + min: (a: bigint, b: bigint) => (a < b ? a : b), + max: (a: bigint, b: bigint) => (a > b ? a : b), +}; diff --git a/src/worker/tasks/sendTransactionWorker.ts b/src/worker/tasks/sendTransactionWorker.ts index c8d25458..9dff3c4b 100644 --- a/src/worker/tasks/sendTransactionWorker.ts +++ b/src/worker/tasks/sendTransactionWorker.ts @@ -39,6 +39,7 @@ import { isReplacementGasFeeTooLow, wrapError, } from "../../utils/error"; +import { BigIntMath } from "../../utils/math"; import { getChecksumAddress } from "../../utils/primitiveTypes"; import { recordMetrics } from "../../utils/prometheus"; import { redis } from "../../utils/redis/redis"; @@ -565,34 +566,37 @@ const _minutesFromNow = (minutes: number) => * @param populatedTransaction The transaction with estimated gas from RPC. * @param resendCount The resend attempt #. Example: 2 = the transaction was initially sent, then resent once. This is the second resend attempt. */ -export const _updateGasFees = ( +export function _updateGasFees( populatedTransaction: PopulatedTransaction, resendCount: number, overrides: SentTransaction["overrides"], -): PopulatedTransaction => { +): PopulatedTransaction { if (resendCount === 0) { return populatedTransaction; } - const multiplier = BigInt(Math.min(10, resendCount * 2)); - + const multiplier = BigIntMath.min(10n, BigInt(resendCount) * 2n); const updated = { ...populatedTransaction }; // Update gas fees (unless they were explicitly overridden). + // Do not exceed MAX_GAS_PRICE_WEI. + const MAX_GAS_PRICE_WEI = BigInt(env.EXPERIMENTAL__MAX_GAS_PRICE_WEI); if (updated.gasPrice && !overrides?.gasPrice) { - updated.gasPrice *= multiplier; + const newGasPrice = updated.gasPrice * multiplier; + updated.gasPrice = BigIntMath.min(newGasPrice, MAX_GAS_PRICE_WEI); } if (updated.maxPriorityFeePerGas && !overrides?.maxPriorityFeePerGas) { updated.maxPriorityFeePerGas *= multiplier; } if (updated.maxFeePerGas && !overrides?.maxFeePerGas) { - updated.maxFeePerGas = + const newMaxFeePerGas = updated.maxFeePerGas * 2n + (updated.maxPriorityFeePerGas ?? 0n); + updated.maxFeePerGas = BigIntMath.min(newMaxFeePerGas, MAX_GAS_PRICE_WEI); } return updated; -}; +} // Must be explicitly called for the worker to run on this host. export const initSendTransactionWorker = () => {