diff --git a/src/tests/sendTransactionWorker.test.ts b/src/tests/sendTransactionWorker.test.ts new file mode 100644 index 000000000..5fb0a5dcb --- /dev/null +++ b/src/tests/sendTransactionWorker.test.ts @@ -0,0 +1,84 @@ +import type { Hex } from "thirdweb"; +import { describe, expect, it } from "vitest"; +import { _updateGasFees } from "../worker/tasks/sendTransactionWorker"; + +describe("_updateGasFees", () => { + const base = { + // Irrelevant values for testing. + chainId: 1, + data: "0x0" as Hex, + gas: 21000n, + to: undefined, + nonce: undefined, + accessList: undefined, + value: undefined, + }; + + it("returns the original transaction on first send (resendCount = 0)", () => { + let result = _updateGasFees({ ...base, gasPrice: 100n }, 0, undefined); + expect(result.gasPrice).toEqual(100n); + + result = _updateGasFees( + { ...base, maxFeePerGas: 100n, maxPriorityFeePerGas: 10n }, + 0, + undefined, + ); + expect(result.maxFeePerGas).toEqual(100n); + expect(result.maxPriorityFeePerGas).toEqual(10n); + }); + + it("doubles gasPrice for legacy transactions", () => { + const result = _updateGasFees({ ...base, gasPrice: 100n }, 1, {}); + expect(result.gasPrice).toBe(200n); + }); + + it("caps gasPrice multiplier at 10x", () => { + const result = _updateGasFees({ ...base, gasPrice: 100n }, 10, {}); + expect(result.gasPrice).toBe(1000n); + }); + + it("updates maxPriorityFeePerGas and maxFeePerGas for EIP-1559 transactions", () => { + const result = _updateGasFees( + { ...base, maxFeePerGas: 100n, maxPriorityFeePerGas: 10n }, + 3, + {}, + ); + expect(result.maxPriorityFeePerGas).toBe(60n); + expect(result.maxFeePerGas).toBe(260n); + }); + + it("respects overrides for maxPriorityFeePerGas", () => { + const result = _updateGasFees( + { ...base, maxFeePerGas: 100n, maxPriorityFeePerGas: 10n }, + 3, + { maxPriorityFeePerGas: 10n }, + ); + expect(result.maxPriorityFeePerGas).toBe(10n); // matches override + expect(result.maxFeePerGas).toBe(210n); + }); + + it("respects overrides for maxFeePerGas", () => { + const result = _updateGasFees( + { ...base, maxFeePerGas: 100n, maxPriorityFeePerGas: 10n }, + 3, + { maxFeePerGas: 100n }, + ); + expect(result.maxPriorityFeePerGas).toBe(60n); + expect(result.maxFeePerGas).toBe(100n); // matches override + }); + + it("returns correct values when only maxPriorityFeePerGas is set", () => { + const result = _updateGasFees( + { ...base, maxPriorityFeePerGas: 10n }, + 3, + {}, + ); + expect(result.maxPriorityFeePerGas).toBe(60n); + expect(result.maxFeePerGas).toBeUndefined(); + }); + + it("returns correct values when only maxFeePerGas is set", () => { + const result = _updateGasFees({ ...base, maxFeePerGas: 80n }, 3, {}); + expect(result.maxFeePerGas).toBe(160n); + }); +}); diff --git a/src/worker/tasks/sendTransactionWorker.ts b/src/worker/tasks/sendTransactionWorker.ts index 783530ba0..c8d254581 100644 --- a/src/worker/tasks/sendTransactionWorker.ts +++ b/src/worker/tasks/sendTransactionWorker.ts @@ -422,7 +422,7 @@ const _resendTransaction = async ( // Populate the transaction with double gas. const { chainId, from, overrides, sentTransactionHashes } = sentTransaction; - const populatedTransaction = await toSerializableTransaction({ + let populatedTransaction = await toSerializableTransaction({ from: getChecksumAddress(from), transaction: { client: thirdwebClient, @@ -436,19 +436,12 @@ const _resendTransaction = async ( }, }); - // Double gas fee settings if they were not provded in `overrides`. - if (populatedTransaction.gasPrice) { - populatedTransaction.gasPrice *= 2n; - } - if (populatedTransaction.maxFeePerGas && !overrides?.maxFeePerGas) { - populatedTransaction.maxFeePerGas *= 2n; - } - if ( - populatedTransaction.maxPriorityFeePerGas && - !overrides?.maxPriorityFeePerGas - ) { - populatedTransaction.maxPriorityFeePerGas *= 2n; - } + // Increase gas fees for this resend attempt. + populatedTransaction = _updateGasFees( + populatedTransaction, + sentTransaction.resendCount + 1, + sentTransaction.overrides, + ); job.log(`Populated transaction: ${stringify(populatedTransaction)}`); @@ -559,6 +552,48 @@ const _hasExceededTimeout = ( const _minutesFromNow = (minutes: number) => new Date(Date.now() + minutes * 60_000); +/** + * Computes aggressive gas fees when resending a transaction. + * + * For legacy transactions (pre-EIP1559): + * - Gas price = (2 * attempt) * estimatedGasPrice, capped at 10x. + * + * For other transactions: + * - maxPriorityFeePerGas = (2 * attempt) * estimatedMaxPriorityFeePerGas, capped at 10x. + * - maxFeePerGas = (2 * estimatedMaxFeePerGas) + maxPriorityFeePerGas. + * + * @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 = ( + populatedTransaction: PopulatedTransaction, + resendCount: number, + overrides: SentTransaction["overrides"], +): PopulatedTransaction => { + if (resendCount === 0) { + return populatedTransaction; + } + + const multiplier = BigInt(Math.min(10, resendCount * 2)); + + const updated = { ...populatedTransaction }; + + // Update gas fees (unless they were explicitly overridden). + + if (updated.gasPrice && !overrides?.gasPrice) { + updated.gasPrice *= multiplier; + } + if (updated.maxPriorityFeePerGas && !overrides?.maxPriorityFeePerGas) { + updated.maxPriorityFeePerGas *= multiplier; + } + if (updated.maxFeePerGas && !overrides?.maxFeePerGas) { + updated.maxFeePerGas = + updated.maxFeePerGas * 2n + (updated.maxPriorityFeePerGas ?? 0n); + } + + return updated; +}; + // Must be explicitly called for the worker to run on this host. export const initSendTransactionWorker = () => { const _worker = new Worker(SendTransactionQueue.q.name, handler, {