Skip to content

Commit 2faf3c8

Browse files
committed
update gas retry logic
1 parent 58ee495 commit 2faf3c8

File tree

2 files changed

+152
-20
lines changed

2 files changed

+152
-20
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { Hex } from "thirdweb";
2+
import { describe, expect, it } from "vitest";
3+
import { _updateGasFees } from "../worker/tasks/sendTransactionWorker";
4+
5+
describe("_updateGasFees", () => {
6+
const base = {
7+
// Irrelevant values for testing.
8+
chainId: 1,
9+
data: "0x0" as Hex,
10+
gas: 21000n,
11+
to: undefined,
12+
nonce: undefined,
13+
accessList: undefined,
14+
value: undefined,
15+
};
16+
17+
// const legacyTransaction: PopulatedTransaction = {
18+
// ...base,
19+
// gasPrice: 35000000000n,
20+
// };
21+
22+
// const eip1559Transaction: PopulatedTransaction = {
23+
// ...base,
24+
// maxPriorityFeePerGas: 500000000n,
25+
// maxFeePerGas: 35000000000n,
26+
// };
27+
28+
it("returns the original transaction on first send (resendCount = 0)", () => {
29+
let result = _updateGasFees({ ...base, gasPrice: 100n }, 0, undefined);
30+
expect(result.gasPrice).toEqual(100n);
31+
32+
result = _updateGasFees(
33+
{ ...base, maxFeePerGas: 100n, maxPriorityFeePerGas: 10n },
34+
0,
35+
undefined,
36+
);
37+
expect(result.maxFeePerGas).toEqual(100n);
38+
expect(result.maxPriorityFeePerGas).toEqual(10n);
39+
});
40+
41+
it("doubles gasPrice for legacy transactions", () => {
42+
const result = _updateGasFees({ ...base, gasPrice: 100n }, 1, {});
43+
expect(result.gasPrice).toBe(200n);
44+
});
45+
46+
it("caps gasPrice multiplier at 10x", () => {
47+
const result = _updateGasFees({ ...base, gasPrice: 100n }, 10, {});
48+
expect(result.gasPrice).toBe(1000n);
49+
});
50+
51+
it("updates maxPriorityFeePerGas and maxFeePerGas for EIP-1559 transactions", () => {
52+
const result = _updateGasFees(
53+
{ ...base, maxFeePerGas: 100n, maxPriorityFeePerGas: 10n },
54+
3,
55+
{},
56+
);
57+
expect(result.maxPriorityFeePerGas).toBe(60n);
58+
expect(result.maxFeePerGas).toBe(260n);
59+
});
60+
61+
it("respects overrides for maxPriorityFeePerGas", () => {
62+
const result = _updateGasFees(
63+
{ ...base, maxFeePerGas: 100n, maxPriorityFeePerGas: 10n },
64+
3,
65+
{ maxPriorityFeePerGas: 10n },
66+
);
67+
expect(result.maxPriorityFeePerGas).toBe(10n); // matches override
68+
expect(result.maxFeePerGas).toBe(210n);
69+
});
70+
71+
it("respects overrides for maxFeePerGas", () => {
72+
const result = _updateGasFees(
73+
{ ...base, maxFeePerGas: 100n, maxPriorityFeePerGas: 10n },
74+
3,
75+
{ maxFeePerGas: 100n },
76+
);
77+
expect(result.maxPriorityFeePerGas).toBe(60n);
78+
expect(result.maxFeePerGas).toBe(100n); // matches override
79+
});
80+
81+
it("returns correct values when only maxPriorityFeePerGas is set", () => {
82+
const result = _updateGasFees(
83+
{ ...base, maxPriorityFeePerGas: 10n },
84+
3,
85+
{},
86+
);
87+
expect(result.maxPriorityFeePerGas).toBe(60n);
88+
expect(result.maxFeePerGas).toBeUndefined();
89+
});
90+
91+
it("returns correct values when only maxFeePerGas is set", () => {
92+
const result = _updateGasFees({ ...base, maxFeePerGas: 80n }, 3, {});
93+
expect(result.maxFeePerGas).toBe(160n);
94+
});
95+
});

src/worker/tasks/sendTransactionWorker.ts

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,15 @@ const handler: Processor<string, void, string> = async (job: Job<string>) => {
7575
return;
7676
}
7777

78-
// The transaction may be errored if it is manually retried.
79-
// For example, the developer retried all failed transactions during an RPC outage.
80-
// An errored queued transaction (resendCount = 0) is safe to retry: the transaction wasn't sent to RPC.
81-
if (transaction.status === "errored" && resendCount === 0) {
78+
// If the transaction is errored and has not yet been sent,
79+
// reset it to a QueuedTransaction to try again.
80+
// No transaction hashes means the transaction is not in-flight.
81+
if (
82+
transaction.status === "errored" &&
83+
!transaction.isUserOp &&
84+
"sentTransactionHashes" in transaction &&
85+
transaction.sentTransactionHashes.length > 0
86+
) {
8287
const { errorMessage, ...omitted } = transaction;
8388
transaction = {
8489
...omitted,
@@ -438,7 +443,7 @@ const _resendTransaction = async (
438443

439444
// Populate the transaction with double gas.
440445
const { chainId, from, overrides, sentTransactionHashes } = sentTransaction;
441-
const populatedTransaction = await toSerializableTransaction({
446+
let populatedTransaction = await toSerializableTransaction({
442447
from: getChecksumAddress(from),
443448
transaction: {
444449
client: thirdwebClient,
@@ -452,21 +457,12 @@ const _resendTransaction = async (
452457
},
453458
});
454459

455-
// Double the gas fee settings each attempt up to 10x.
456-
// Do not update gas if overrides were provided.
457-
const gasMultiple = BigInt(clamp(job.attemptsMade * 2, { min: 2, max: 10 }));
458-
if (populatedTransaction.gasPrice) {
459-
populatedTransaction.gasPrice *= gasMultiple;
460-
}
461-
if (populatedTransaction.maxFeePerGas && !overrides?.maxFeePerGas) {
462-
populatedTransaction.maxFeePerGas *= gasMultiple;
463-
}
464-
if (
465-
populatedTransaction.maxPriorityFeePerGas &&
466-
!overrides?.maxPriorityFeePerGas
467-
) {
468-
populatedTransaction.maxPriorityFeePerGas *= gasMultiple;
469-
}
460+
// Increase gas fees for this resend attempt.
461+
populatedTransaction = _updateGasFees(
462+
populatedTransaction,
463+
sentTransaction.resendCount + 1,
464+
sentTransaction.overrides,
465+
);
470466

471467
job.log(`Populated transaction: ${stringify(populatedTransaction)}`);
472468

@@ -577,6 +573,47 @@ const _hasExceededTimeout = (
577573
const _minutesFromNow = (minutes: number) =>
578574
new Date(Date.now() + minutes * 60_000);
579575

576+
/**
577+
* Computes the aggressive gas fees to use when resending a transaction.
578+
*
579+
* For legacy transactions (pre-EIP1559):
580+
* - Set gas price to 2 * current attempt, capped at 10x.
581+
*
582+
* For transactions with maxFeePerGas + maxPriorityFeePerGas:
583+
* - Set maxPriorityFeePerGas to 2x current attempt, capped at 10x.
584+
* - Set maxFeePerGas to 2 * current max fee, plus the maxPriorityFeePerGas.
585+
*
586+
* @param populatedTransaction The transaction with estimated gas from RPC.
587+
* @param resendCount The resend attempt #. Example: 2 = the transaction was initially sent, then resent once. This is the second resend attempt.
588+
*/
589+
export const _updateGasFees = (
590+
populatedTransaction: PopulatedTransaction,
591+
resendCount: number,
592+
overrides: SentTransaction["overrides"],
593+
): PopulatedTransaction => {
594+
if (resendCount === 0) {
595+
return populatedTransaction;
596+
}
597+
598+
const multiplier = BigInt(clamp(resendCount * 2, { min: 2, max: 10 }));
599+
600+
const updated = { ...populatedTransaction };
601+
if (updated.gasPrice) {
602+
updated.gasPrice *= multiplier;
603+
}
604+
// Don't update gas fees that are explicitly overridden.
605+
if (updated.maxPriorityFeePerGas && !overrides?.maxPriorityFeePerGas) {
606+
updated.maxPriorityFeePerGas *= multiplier;
607+
}
608+
// Don't update gas fees that are explicitly overridden.
609+
if (updated.maxFeePerGas && !overrides?.maxFeePerGas) {
610+
updated.maxFeePerGas =
611+
updated.maxFeePerGas * 2n + (updated.maxPriorityFeePerGas ?? 0n);
612+
}
613+
614+
return updated;
615+
};
616+
580617
// Must be explicitly called for the worker to run on this host.
581618
export const initSendTransactionWorker = () => {
582619
const _worker = new Worker(SendTransactionQueue.q.name, handler, {

0 commit comments

Comments
 (0)