Add fallback for gas fee estimation in zkSync (#8637)

This commit is contained in:
Firekeeper
2026-01-14 22:54:24 +07:00
committed by GitHub
parent 621f187a8a
commit 6390052f8b
3 changed files with 93 additions and 20 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---
newer zkos chains no longer support zks\_ rpc endpoints, fallback to evm std
@@ -5,9 +5,13 @@ import {
TEST_ACCOUNT_B,
} from "../../../../test/src/test-wallets.js";
import { zkSyncSepolia } from "../../../chains/chain-definitions/zksync-sepolia.js";
import { defineChain } from "../../../chains/utils.js";
import { deployPublishedContract } from "../../../extensions/prebuilts/deploy-published.js";
import { prepareTransaction } from "../../prepare-transaction.js";
import { sendEip712Transaction } from "./send-eip712-transaction.js";
import {
getZkGasFees,
sendEip712Transaction,
} from "./send-eip712-transaction.js";
describe("sendEip712Transaction", () => {
// re-enable for testing, but disable for CI since it requires testnet funds
@@ -52,4 +56,34 @@ describe("sendEip712Transaction", () => {
expect(address).toBeDefined();
expect(address.length).toBe(42);
});
it("should fallback to standard EVM methods when zks_estimateFee is not available", async () => {
// Chain 278701 is a zkSync chain that doesn't support zks_estimateFee
const zkSyncChainWithoutZksSupport = defineChain(278701);
// Use a transaction with pre-defined gas to skip estimation (which requires balance)
// This tests that the fallback path is taken for fee estimation
const transaction = prepareTransaction({
chain: zkSyncChainWithoutZksSupport,
client: TEST_CLIENT,
gas: 21000n, // pre-define gas to skip eth_estimateGas
to: TEST_ACCOUNT_B.address,
value: 0n,
});
const gasFees = await getZkGasFees({
transaction,
from: TEST_ACCOUNT_A.address as `0x${string}`,
});
// Verify fallback worked - should have valid gas values
expect(gasFees.gas).toBeDefined();
expect(gasFees.gas).toBeGreaterThan(0n);
expect(gasFees.maxFeePerGas).toBeDefined();
expect(gasFees.maxFeePerGas).toBeGreaterThan(0n);
expect(gasFees.maxPriorityFeePerGas).toBeDefined();
expect(gasFees.maxPriorityFeePerGas).toBeGreaterThan(0n);
// Fallback should use 100k for gasPerPubdata
expect(gasFees.gasPerPubdata).toBe(100000n);
});
});
@@ -180,25 +180,59 @@ export async function getZkGasFees(args: {
) {
const rpc = getRpcClient(transaction);
const params = await formatTransaction({ from, transaction });
const result = (await rpc({
// biome-ignore lint/suspicious/noExplicitAny: TODO add to RPC method types
method: "zks_estimateFee" as any,
// biome-ignore lint/suspicious/noExplicitAny: TODO add to RPC method types
params: [replaceBigInts(params, toHex)] as any,
})) as {
gas_limit: string;
max_fee_per_gas: string;
max_priority_fee_per_gas: string;
gas_per_pubdata_limit: string;
};
gas = toBigInt(result.gas_limit) * 2n; // overestimating to avoid issues when not accounting for paymaster extra gas ( we should really pass the paymaster input above for better accuracy )
const baseFee = toBigInt(result.max_fee_per_gas);
maxFeePerGas = baseFee * 2n; // bumping the base fee per gas to ensure fast inclusion
maxPriorityFeePerGas = toBigInt(result.max_priority_fee_per_gas) || 1n;
gasPerPubdata = toBigInt(result.gas_per_pubdata_limit) * 2n; // doubling for fast inclusion;
if (gasPerPubdata < 50000n) {
// enforce a minimum gas per pubdata limit
gasPerPubdata = 50000n;
// Try zkSync-specific fee estimation first, fallback to standard EVM methods
try {
const result = (await rpc({
// biome-ignore lint/suspicious/noExplicitAny: TODO add to RPC method types
method: "zks_estimateFee" as any,
// biome-ignore lint/suspicious/noExplicitAny: TODO add to RPC method types
params: [replaceBigInts(params, toHex)] as any,
})) as {
gas_limit: string;
max_fee_per_gas: string;
max_priority_fee_per_gas: string;
gas_per_pubdata_limit: string;
};
gas = toBigInt(result.gas_limit) * 2n; // overestimating to avoid issues when not accounting for paymaster extra gas ( we should really pass the paymaster input above for better accuracy )
const baseFee = toBigInt(result.max_fee_per_gas);
maxFeePerGas = baseFee * 2n; // bumping the base fee per gas to ensure fast inclusion
maxPriorityFeePerGas = toBigInt(result.max_priority_fee_per_gas) || 1n;
gasPerPubdata = toBigInt(result.gas_per_pubdata_limit) * 2n; // doubling for fast inclusion;
if (gasPerPubdata < 50000n) {
// enforce a minimum gas per pubdata limit
gasPerPubdata = 50000n;
}
} catch {
// Fallback to standard EVM methods if zks_estimateFee is not available
const [{ estimateGas }, { getDefaultGasOverrides }] = await Promise.all([
import("../estimate-gas.js"),
import("../../../gas/fee-data.js"),
]);
const [estimatedGas, gasOverrides] = await Promise.all([
gas === undefined
? estimateGas({ transaction, from })
: Promise.resolve(gas),
getDefaultGasOverrides(transaction.client, transaction.chain),
]);
gas = estimatedGas * 2n; // overestimating similar to zkSync estimation
if ("maxFeePerGas" in gasOverrides && gasOverrides.maxFeePerGas) {
maxFeePerGas = gasOverrides.maxFeePerGas * 2n; // bumping for fast inclusion
} else if ("gasPrice" in gasOverrides && gasOverrides.gasPrice) {
maxFeePerGas = gasOverrides.gasPrice * 2n;
}
if (
"maxPriorityFeePerGas" in gasOverrides &&
gasOverrides.maxPriorityFeePerGas
) {
maxPriorityFeePerGas = gasOverrides.maxPriorityFeePerGas;
} else {
maxPriorityFeePerGas = 1n;
}
// Use 100k as default gasPerPubdata for non-zkSync chains
gasPerPubdata = 100000n;
}
}
return {