Skip to content

Commit 0f70e16

Browse files
authored
chore: add 10% min on gas retries (#425)
* chore: add 10% min on gas retries * Make sure values are integers before casting to BN * bignumber * update default max gas for retries * gas * Add gas unit tests, bignumber helpers
1 parent 9468cc5 commit 0f70e16

File tree

5 files changed

+219
-28
lines changed

5 files changed

+219
-28
lines changed

src/db/configuration/getConfiguration.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Configuration } from "@prisma/client";
22
import { LocalWallet } from "@thirdweb-dev/wallets";
3+
import { ethers } from "ethers";
34
import { WalletType } from "../../schema/wallet";
45
import { mandatoryAllowedCorsUrls } from "../../server/utils/cors-urls";
56
import { decrypt } from "../../utils/crypto";
@@ -174,13 +175,17 @@ export const getConfiguration = async (): Promise<Config> => {
174175
},
175176
create: {
176177
minTxsToProcess: 1,
177-
maxTxsToProcess: 10,
178+
maxTxsToProcess: 30,
178179
minedTxListenerCronSchedule: "*/5 * * * * *",
179180
maxTxsToUpdate: 50,
180181
retryTxListenerCronSchedule: "*/30 * * * * *",
181182
minEllapsedBlocksBeforeRetry: 15,
182-
maxFeePerGasForRetries: "55000000000",
183-
maxPriorityFeePerGasForRetries: "55000000000",
183+
maxFeePerGasForRetries: ethers.utils
184+
.parseUnits("1000", "gwei")
185+
.toString(),
186+
maxPriorityFeePerGasForRetries: ethers.utils
187+
.parseUnits("1000", "gwei")
188+
.toString(),
184189
maxRetriesPerTx: 3,
185190
authDomain: "thirdweb.com",
186191
authWalletEncryptedJson: await createAuthWalletEncryptedJson(),

src/tests/gas.test.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { Transactions } from ".prisma/client";
2+
import { getDefaultGasOverrides } from "@thirdweb-dev/sdk";
3+
import { BigNumber, ethers, providers } from "ethers";
4+
import { getGasSettingsForRetry } from "../utils/gas";
5+
6+
jest.mock("@thirdweb-dev/sdk");
7+
const mockGetDefaultGasOverrides =
8+
getDefaultGasOverrides as jest.MockedFunction<typeof getDefaultGasOverrides>;
9+
10+
describe("getGasSettingsForRetry", () => {
11+
let mockProvider: ethers.providers.StaticJsonRpcProvider;
12+
let mockTransaction: Transactions;
13+
14+
beforeEach(() => {
15+
mockProvider = new providers.StaticJsonRpcProvider();
16+
17+
// @ts-ignore
18+
mockTransaction = {
19+
gasPrice: "1000",
20+
retryGasValues: false,
21+
maxFeePerGas: "500",
22+
maxPriorityFeePerGas: "100",
23+
};
24+
});
25+
26+
afterEach(() => {
27+
jest.clearAllMocks();
28+
});
29+
30+
it("new gas settings for legacy gas format", async () => {
31+
mockGetDefaultGasOverrides.mockResolvedValue({
32+
gasPrice: BigNumber.from(1000),
33+
});
34+
35+
const gasSettings = await getGasSettingsForRetry(
36+
mockTransaction,
37+
mockProvider,
38+
);
39+
40+
expect(gasSettings).toEqual({
41+
gasPrice: BigNumber.from(2000),
42+
});
43+
});
44+
45+
it("new gas settings for legacy gas format, fallback to 110% of previous attempt", async () => {
46+
mockGetDefaultGasOverrides.mockResolvedValue({
47+
gasPrice: BigNumber.from(520),
48+
});
49+
50+
const gasSettings = await getGasSettingsForRetry(
51+
mockTransaction,
52+
mockProvider,
53+
);
54+
55+
expect(gasSettings).toEqual({
56+
gasPrice: BigNumber.from(1100), // uses 1100 instead of 1040
57+
});
58+
});
59+
60+
it("new gas settings for EIP 1559 gas format", async () => {
61+
mockGetDefaultGasOverrides.mockResolvedValueOnce({
62+
maxFeePerGas: BigNumber.from(500),
63+
maxPriorityFeePerGas: BigNumber.from(100),
64+
});
65+
66+
const gasSettings = await getGasSettingsForRetry(
67+
mockTransaction,
68+
mockProvider,
69+
);
70+
71+
expect(gasSettings).toEqual({
72+
maxFeePerGas: BigNumber.from(1000),
73+
maxPriorityFeePerGas: BigNumber.from(200),
74+
});
75+
});
76+
77+
it("new gas settings for EIP 1559 gas format, fallback to 110% of previous attempt", async () => {
78+
mockGetDefaultGasOverrides.mockResolvedValueOnce({
79+
maxFeePerGas: BigNumber.from(500),
80+
maxPriorityFeePerGas: BigNumber.from(100),
81+
});
82+
83+
const gasSettings = await getGasSettingsForRetry(
84+
mockTransaction,
85+
mockProvider,
86+
);
87+
88+
expect(gasSettings).toEqual({
89+
maxFeePerGas: BigNumber.from(1000),
90+
maxPriorityFeePerGas: BigNumber.from(200),
91+
});
92+
});
93+
94+
it("new gas settings for EIP 1559 gas format, use manual overrides", async () => {
95+
mockGetDefaultGasOverrides.mockResolvedValueOnce({
96+
maxFeePerGas: BigNumber.from(500),
97+
maxPriorityFeePerGas: BigNumber.from(100),
98+
});
99+
100+
mockTransaction = {
101+
...mockTransaction,
102+
retryGasValues: true,
103+
retryMaxFeePerGas: "2222",
104+
retryMaxPriorityFeePerGas: "444",
105+
};
106+
107+
const gasSettings = await getGasSettingsForRetry(
108+
mockTransaction,
109+
mockProvider,
110+
);
111+
112+
expect(gasSettings).toEqual({
113+
maxFeePerGas: BigNumber.from(2222),
114+
maxPriorityFeePerGas: BigNumber.from(444),
115+
});
116+
});
117+
118+
it("new gas settings for EIP 1559 gas format, manual overrides but fall back to 110% previous attempt", async () => {
119+
mockGetDefaultGasOverrides.mockResolvedValueOnce({
120+
maxFeePerGas: BigNumber.from(500),
121+
maxPriorityFeePerGas: BigNumber.from(100),
122+
});
123+
124+
mockTransaction = {
125+
...mockTransaction,
126+
retryGasValues: true,
127+
retryMaxFeePerGas: "505",
128+
retryMaxPriorityFeePerGas: "105",
129+
};
130+
131+
const gasSettings = await getGasSettingsForRetry(
132+
mockTransaction,
133+
mockProvider,
134+
);
135+
136+
expect(gasSettings).toEqual({
137+
maxFeePerGas: BigNumber.from(550),
138+
maxPriorityFeePerGas: BigNumber.from(110),
139+
});
140+
});
141+
});

src/utils/bigNumber.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import type { BigNumber } from "ethers";
2+
3+
export const maxBN = (a: BigNumber, b: BigNumber) => (a.gt(b) ? a : b);
4+
export const minBN = (a: BigNumber, b: BigNumber) => (a.lt(b) ? a : b);

src/utils/gas.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { Transactions } from ".prisma/client";
2+
import { getDefaultGasOverrides } from "@thirdweb-dev/sdk";
3+
import { BigNumber, providers } from "ethers";
4+
import { maxBN } from "./bigNumber";
5+
6+
/**
7+
*
8+
* @param tx
9+
* @param provider
10+
* @returns
11+
*/
12+
export const getGasSettingsForRetry = async (
13+
tx: Transactions,
14+
provider: providers.StaticJsonRpcProvider,
15+
): ReturnType<typeof getDefaultGasOverrides> => {
16+
// Default: get gas settings from chain.
17+
const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } =
18+
await getDefaultGasOverrides(provider);
19+
20+
// Handle legacy gas format.
21+
if (gasPrice) {
22+
const newGasPrice = gasPrice.mul(2);
23+
// Gas settings must be 10% higher than a previous attempt.
24+
const minGasPrice = BigNumber.from(tx.gasPrice!).mul(110).div(100);
25+
26+
return {
27+
gasPrice: maxBN(newGasPrice, minGasPrice),
28+
};
29+
}
30+
31+
// Handle EIP 1559 gas format.
32+
let newMaxFeePerGas = maxFeePerGas.mul(2);
33+
let newMaxPriorityFeePerGas = maxPriorityFeePerGas.mul(2);
34+
35+
if (tx.retryGasValues) {
36+
// If this tx is manually retried, override with provided gas settings.
37+
newMaxFeePerGas = BigNumber.from(tx.retryMaxFeePerGas!);
38+
newMaxPriorityFeePerGas = BigNumber.from(tx.retryMaxPriorityFeePerGas!);
39+
}
40+
41+
// Gas settings muset be 10% higher than a previous attempt.
42+
const minMaxFeePerGas = BigNumber.from(tx.maxFeePerGas!).mul(110).div(100);
43+
const minMaxPriorityFeePerGas = BigNumber.from(tx.maxPriorityFeePerGas!)
44+
.mul(110)
45+
.div(100);
46+
47+
return {
48+
maxFeePerGas: maxBN(newMaxFeePerGas, minMaxFeePerGas),
49+
maxPriorityFeePerGas: maxBN(
50+
newMaxPriorityFeePerGas,
51+
minMaxPriorityFeePerGas,
52+
),
53+
};
54+
};

src/worker/tasks/retryTx.ts

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
import {
2-
StaticJsonRpcBatchProvider,
3-
getDefaultGasOverrides,
4-
} from "@thirdweb-dev/sdk";
1+
import { StaticJsonRpcBatchProvider } from "@thirdweb-dev/sdk";
52
import { ethers } from "ethers";
63
import { prisma } from "../../db/client";
74
import { getTxToRetry } from "../../db/transactions/getTxToRetry";
85
import { updateTx } from "../../db/transactions/updateTx";
96
import { TransactionStatusEnum } from "../../server/schemas/transaction";
107
import { getConfig } from "../../utils/cache/getConfig";
118
import { getSdk } from "../../utils/cache/getSdk";
9+
import { getGasSettingsForRetry } from "../../utils/gas";
1210
import { logger } from "../../utils/logger";
1311
import {
1412
ReportUsageParams,
@@ -20,7 +18,6 @@ export const retryTx = async () => {
2018
try {
2119
await prisma.$transaction(
2220
async (pgtx) => {
23-
// Get one transaction to retry at a time
2421
const tx = await getTxToRetry({ pgtx });
2522
if (!tx) {
2623
return;
@@ -33,45 +30,38 @@ export const retryTx = async () => {
3330
walletAddress: tx.fromAddress!,
3431
});
3532
const provider = sdk.getProvider() as StaticJsonRpcBatchProvider;
36-
3733
const blockNumber = await sdk.getProvider().getBlockNumber();
38-
// Only retry if more than the elapsed blocks before retry has passed.
34+
3935
if (
4036
blockNumber - tx.sentAtBlockNumber! <=
4137
config.minEllapsedBlocksBeforeRetry
4238
) {
39+
// Return if too few blocks have passed since submitted. Try again later.
4340
return;
4441
}
4542

46-
const receipt = await sdk
47-
.getProvider()
48-
.getTransactionReceipt(tx.transactionHash!);
49-
50-
// If the transaction is mined, update the DB.
43+
const receipt = await provider.getTransactionReceipt(
44+
tx.transactionHash!,
45+
);
5146
if (receipt) {
47+
// Return if the tx is already mined.
5248
return;
5349
}
5450

55-
// TODO: We should still retry anyway
56-
const gasOverrides = await getDefaultGasOverrides(sdk.getProvider());
57-
58-
if (tx.retryGasValues) {
59-
// If a retry has been triggered manually
60-
tx.maxFeePerGas = tx.retryMaxFeePerGas!;
61-
tx.maxPriorityFeePerGas = tx.maxPriorityFeePerGas!;
62-
} else if (
51+
const gasOverrides = await getGasSettingsForRetry(tx, provider);
52+
if (
6353
gasOverrides.maxFeePerGas?.gt(config.maxFeePerGasForRetries) ||
6454
gasOverrides.maxPriorityFeePerGas?.gt(
6555
config.maxPriorityFeePerGasForRetries,
6656
)
6757
) {
58+
// Return if gas settings exceed configured limits. Try again later.
6859
logger({
6960
service: "worker",
7061
level: "warn",
7162
queueId: tx.id,
7263
message: `${tx.chainId} chain gas price is higher than maximum threshold.`,
7364
});
74-
7565
return;
7666
}
7767

@@ -92,9 +82,6 @@ export const retryTx = async () => {
9282
nonce: tx.nonce!,
9383
value: tx.value!,
9484
...gasOverrides,
95-
gasPrice: gasOverrides.gasPrice?.mul(2),
96-
maxFeePerGas: gasOverrides.maxFeePerGas?.mul(2),
97-
maxPriorityFeePerGas: gasOverrides.maxPriorityFeePerGas?.mul(2),
9885
});
9986
} catch (err: any) {
10087
logger({
@@ -170,7 +157,7 @@ export const retryTx = async () => {
170157
service: "worker",
171158
level: "info",
172159
queueId: tx.id,
173-
message: `Retried with hash ${res.hash} for Nonce ${res.nonce}`,
160+
message: `Retried with hash ${res.hash} for nonce ${res.nonce}`,
174161
});
175162
},
176163
{

0 commit comments

Comments
 (0)