Skip to content

Commit

Permalink
chore: add 10% min on gas retries (#425)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
arcoraven authored Feb 24, 2024
1 parent 9468cc5 commit 0f70e16
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 28 deletions.
11 changes: 8 additions & 3 deletions src/db/configuration/getConfiguration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Configuration } from "@prisma/client";
import { LocalWallet } from "@thirdweb-dev/wallets";
import { ethers } from "ethers";
import { WalletType } from "../../schema/wallet";
import { mandatoryAllowedCorsUrls } from "../../server/utils/cors-urls";
import { decrypt } from "../../utils/crypto";
Expand Down Expand Up @@ -174,13 +175,17 @@ export const getConfiguration = async (): Promise<Config> => {
},
create: {
minTxsToProcess: 1,
maxTxsToProcess: 10,
maxTxsToProcess: 30,
minedTxListenerCronSchedule: "*/5 * * * * *",
maxTxsToUpdate: 50,
retryTxListenerCronSchedule: "*/30 * * * * *",
minEllapsedBlocksBeforeRetry: 15,
maxFeePerGasForRetries: "55000000000",
maxPriorityFeePerGasForRetries: "55000000000",
maxFeePerGasForRetries: ethers.utils
.parseUnits("1000", "gwei")
.toString(),
maxPriorityFeePerGasForRetries: ethers.utils
.parseUnits("1000", "gwei")
.toString(),
maxRetriesPerTx: 3,
authDomain: "thirdweb.com",
authWalletEncryptedJson: await createAuthWalletEncryptedJson(),
Expand Down
141 changes: 141 additions & 0 deletions src/tests/gas.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { Transactions } from ".prisma/client";
import { getDefaultGasOverrides } from "@thirdweb-dev/sdk";
import { BigNumber, ethers, providers } from "ethers";
import { getGasSettingsForRetry } from "../utils/gas";

jest.mock("@thirdweb-dev/sdk");
const mockGetDefaultGasOverrides =
getDefaultGasOverrides as jest.MockedFunction<typeof getDefaultGasOverrides>;

describe("getGasSettingsForRetry", () => {
let mockProvider: ethers.providers.StaticJsonRpcProvider;
let mockTransaction: Transactions;

beforeEach(() => {
mockProvider = new providers.StaticJsonRpcProvider();

// @ts-ignore
mockTransaction = {
gasPrice: "1000",
retryGasValues: false,
maxFeePerGas: "500",
maxPriorityFeePerGas: "100",
};
});

afterEach(() => {
jest.clearAllMocks();
});

it("new gas settings for legacy gas format", async () => {
mockGetDefaultGasOverrides.mockResolvedValue({
gasPrice: BigNumber.from(1000),
});

const gasSettings = await getGasSettingsForRetry(
mockTransaction,
mockProvider,
);

expect(gasSettings).toEqual({
gasPrice: BigNumber.from(2000),
});
});

it("new gas settings for legacy gas format, fallback to 110% of previous attempt", async () => {
mockGetDefaultGasOverrides.mockResolvedValue({
gasPrice: BigNumber.from(520),
});

const gasSettings = await getGasSettingsForRetry(
mockTransaction,
mockProvider,
);

expect(gasSettings).toEqual({
gasPrice: BigNumber.from(1100), // uses 1100 instead of 1040
});
});

it("new gas settings for EIP 1559 gas format", async () => {
mockGetDefaultGasOverrides.mockResolvedValueOnce({
maxFeePerGas: BigNumber.from(500),
maxPriorityFeePerGas: BigNumber.from(100),
});

const gasSettings = await getGasSettingsForRetry(
mockTransaction,
mockProvider,
);

expect(gasSettings).toEqual({
maxFeePerGas: BigNumber.from(1000),
maxPriorityFeePerGas: BigNumber.from(200),
});
});

it("new gas settings for EIP 1559 gas format, fallback to 110% of previous attempt", async () => {
mockGetDefaultGasOverrides.mockResolvedValueOnce({
maxFeePerGas: BigNumber.from(500),
maxPriorityFeePerGas: BigNumber.from(100),
});

const gasSettings = await getGasSettingsForRetry(
mockTransaction,
mockProvider,
);

expect(gasSettings).toEqual({
maxFeePerGas: BigNumber.from(1000),
maxPriorityFeePerGas: BigNumber.from(200),
});
});

it("new gas settings for EIP 1559 gas format, use manual overrides", async () => {
mockGetDefaultGasOverrides.mockResolvedValueOnce({
maxFeePerGas: BigNumber.from(500),
maxPriorityFeePerGas: BigNumber.from(100),
});

mockTransaction = {
...mockTransaction,
retryGasValues: true,
retryMaxFeePerGas: "2222",
retryMaxPriorityFeePerGas: "444",
};

const gasSettings = await getGasSettingsForRetry(
mockTransaction,
mockProvider,
);

expect(gasSettings).toEqual({
maxFeePerGas: BigNumber.from(2222),
maxPriorityFeePerGas: BigNumber.from(444),
});
});

it("new gas settings for EIP 1559 gas format, manual overrides but fall back to 110% previous attempt", async () => {
mockGetDefaultGasOverrides.mockResolvedValueOnce({
maxFeePerGas: BigNumber.from(500),
maxPriorityFeePerGas: BigNumber.from(100),
});

mockTransaction = {
...mockTransaction,
retryGasValues: true,
retryMaxFeePerGas: "505",
retryMaxPriorityFeePerGas: "105",
};

const gasSettings = await getGasSettingsForRetry(
mockTransaction,
mockProvider,
);

expect(gasSettings).toEqual({
maxFeePerGas: BigNumber.from(550),
maxPriorityFeePerGas: BigNumber.from(110),
});
});
});
4 changes: 4 additions & 0 deletions src/utils/bigNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { BigNumber } from "ethers";

export const maxBN = (a: BigNumber, b: BigNumber) => (a.gt(b) ? a : b);
export const minBN = (a: BigNumber, b: BigNumber) => (a.lt(b) ? a : b);
54 changes: 54 additions & 0 deletions src/utils/gas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { Transactions } from ".prisma/client";
import { getDefaultGasOverrides } from "@thirdweb-dev/sdk";
import { BigNumber, providers } from "ethers";
import { maxBN } from "./bigNumber";

/**
*
* @param tx
* @param provider
* @returns
*/
export const getGasSettingsForRetry = async (
tx: Transactions,
provider: providers.StaticJsonRpcProvider,
): ReturnType<typeof getDefaultGasOverrides> => {
// Default: get gas settings from chain.
const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } =
await getDefaultGasOverrides(provider);

// Handle legacy gas format.
if (gasPrice) {
const newGasPrice = gasPrice.mul(2);
// Gas settings must be 10% higher than a previous attempt.
const minGasPrice = BigNumber.from(tx.gasPrice!).mul(110).div(100);

return {
gasPrice: maxBN(newGasPrice, minGasPrice),
};
}

// Handle EIP 1559 gas format.
let newMaxFeePerGas = maxFeePerGas.mul(2);
let newMaxPriorityFeePerGas = maxPriorityFeePerGas.mul(2);

if (tx.retryGasValues) {
// If this tx is manually retried, override with provided gas settings.
newMaxFeePerGas = BigNumber.from(tx.retryMaxFeePerGas!);
newMaxPriorityFeePerGas = BigNumber.from(tx.retryMaxPriorityFeePerGas!);
}

// Gas settings muset be 10% higher than a previous attempt.
const minMaxFeePerGas = BigNumber.from(tx.maxFeePerGas!).mul(110).div(100);
const minMaxPriorityFeePerGas = BigNumber.from(tx.maxPriorityFeePerGas!)
.mul(110)
.div(100);

return {
maxFeePerGas: maxBN(newMaxFeePerGas, minMaxFeePerGas),
maxPriorityFeePerGas: maxBN(
newMaxPriorityFeePerGas,
minMaxPriorityFeePerGas,
),
};
};
37 changes: 12 additions & 25 deletions src/worker/tasks/retryTx.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import {
StaticJsonRpcBatchProvider,
getDefaultGasOverrides,
} from "@thirdweb-dev/sdk";
import { StaticJsonRpcBatchProvider } from "@thirdweb-dev/sdk";
import { ethers } from "ethers";
import { prisma } from "../../db/client";
import { getTxToRetry } from "../../db/transactions/getTxToRetry";
import { updateTx } from "../../db/transactions/updateTx";
import { TransactionStatusEnum } from "../../server/schemas/transaction";
import { getConfig } from "../../utils/cache/getConfig";
import { getSdk } from "../../utils/cache/getSdk";
import { getGasSettingsForRetry } from "../../utils/gas";
import { logger } from "../../utils/logger";
import {
ReportUsageParams,
Expand All @@ -20,7 +18,6 @@ export const retryTx = async () => {
try {
await prisma.$transaction(
async (pgtx) => {
// Get one transaction to retry at a time
const tx = await getTxToRetry({ pgtx });
if (!tx) {
return;
Expand All @@ -33,45 +30,38 @@ export const retryTx = async () => {
walletAddress: tx.fromAddress!,
});
const provider = sdk.getProvider() as StaticJsonRpcBatchProvider;

const blockNumber = await sdk.getProvider().getBlockNumber();
// Only retry if more than the elapsed blocks before retry has passed.

if (
blockNumber - tx.sentAtBlockNumber! <=
config.minEllapsedBlocksBeforeRetry
) {
// Return if too few blocks have passed since submitted. Try again later.
return;
}

const receipt = await sdk
.getProvider()
.getTransactionReceipt(tx.transactionHash!);

// If the transaction is mined, update the DB.
const receipt = await provider.getTransactionReceipt(
tx.transactionHash!,
);
if (receipt) {
// Return if the tx is already mined.
return;
}

// TODO: We should still retry anyway
const gasOverrides = await getDefaultGasOverrides(sdk.getProvider());

if (tx.retryGasValues) {
// If a retry has been triggered manually
tx.maxFeePerGas = tx.retryMaxFeePerGas!;
tx.maxPriorityFeePerGas = tx.maxPriorityFeePerGas!;
} else if (
const gasOverrides = await getGasSettingsForRetry(tx, provider);
if (
gasOverrides.maxFeePerGas?.gt(config.maxFeePerGasForRetries) ||
gasOverrides.maxPriorityFeePerGas?.gt(
config.maxPriorityFeePerGasForRetries,
)
) {
// Return if gas settings exceed configured limits. Try again later.
logger({
service: "worker",
level: "warn",
queueId: tx.id,
message: `${tx.chainId} chain gas price is higher than maximum threshold.`,
});

return;
}

Expand All @@ -92,9 +82,6 @@ export const retryTx = async () => {
nonce: tx.nonce!,
value: tx.value!,
...gasOverrides,
gasPrice: gasOverrides.gasPrice?.mul(2),
maxFeePerGas: gasOverrides.maxFeePerGas?.mul(2),
maxPriorityFeePerGas: gasOverrides.maxPriorityFeePerGas?.mul(2),
});
} catch (err: any) {
logger({
Expand Down Expand Up @@ -170,7 +157,7 @@ export const retryTx = async () => {
service: "worker",
level: "info",
queueId: tx.id,
message: `Retried with hash ${res.hash} for Nonce ${res.nonce}`,
message: `Retried with hash ${res.hash} for nonce ${res.nonce}`,
});
},
{
Expand Down

0 comments on commit 0f70e16

Please sign in to comment.