Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDK] Feature: Add support for forcing legacy transactions in chain configuration #6180

Merged
merged 9 commits into from
Feb 27, 2025
6 changes: 5 additions & 1 deletion packages/thirdweb/src/adapters/ethers5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import type { ThirdwebClient } from "../client/client.js";
import { type ThirdwebContract, getContract } from "../contract/contract.js";
import { toSerializableTransaction } from "../transaction/actions/to-serializable-transaction.js";
import { waitForReceipt } from "../transaction/actions/wait-for-tx-receipt.js";
import type { PreparedTransaction } from "../transaction/prepare-transaction.js";
import {
type PreparedTransaction,
TransactionTypeMap,
} from "../transaction/prepare-transaction.js";
import type { SerializableTransaction } from "../transaction/serialize-transaction.js";
import { toHex } from "../utils/encoding/hex.js";
import type { Account } from "../wallets/interfaces/wallet.js";
Expand Down Expand Up @@ -483,6 +486,7 @@ export async function toEthersSigner(

const response: ethers5.ethers.providers.TransactionResponse = {
...serialized,
type: serialized.type ? TransactionTypeMap[serialized.type] : undefined,
nonce: Number(serialized.nonce ?? 0),
from: account.address,
maxFeePerGas: serialized.maxFeePerGas
Expand Down
3 changes: 3 additions & 0 deletions packages/thirdweb/src/chains/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { FeeType } from "../gas/fee-data.js";

/**
* @chain
*/
Expand Down Expand Up @@ -26,6 +28,7 @@ export type ChainOptions = {
increaseZeroByteCount?: boolean;
};
faucets?: Array<string>;
feeType?: FeeType;
};

/**
Expand Down
51 changes: 32 additions & 19 deletions packages/thirdweb/src/gas/fee-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ export async function getGasOverridesForTransaction(
transaction: PreparedTransaction,
): Promise<FeeDataParams> {
// first check for explicit values
const [maxFeePerGas, maxPriorityFeePerGas, gasPrice] = await Promise.all([
resolvePromisedValue(transaction.maxFeePerGas),
resolvePromisedValue(transaction.maxPriorityFeePerGas),
resolvePromisedValue(transaction.gasPrice),
]);
const [maxFeePerGas, maxPriorityFeePerGas, gasPrice, type] =
await Promise.all([
resolvePromisedValue(transaction.maxFeePerGas),
resolvePromisedValue(transaction.maxPriorityFeePerGas),
resolvePromisedValue(transaction.gasPrice),
resolvePromisedValue(transaction.type),
]);

// Exit early if the user explicitly provided enough options
if (maxFeePerGas !== undefined && maxPriorityFeePerGas !== undefined) {
Expand All @@ -65,6 +67,7 @@ export async function getGasOverridesForTransaction(
maxPriorityFeePerGas,
};
}

if (typeof gasPrice === "bigint") {
return { gasPrice };
}
Expand All @@ -73,6 +76,7 @@ export async function getGasOverridesForTransaction(
const defaultGasOverrides = await getDefaultGasOverrides(
transaction.client,
transaction.chain,
type === "legacy" ? "legacy" : "eip1559", // 7702, 2930, and eip1559 all qualify as "eip1559" fee type
);

if (transaction.chain.experimental?.increaseZeroByteCount) {
Expand Down Expand Up @@ -103,6 +107,8 @@ export async function getGasOverridesForTransaction(
};
}

export type FeeType = "legacy" | "eip1559";

/**
* Retrieves the default gas overrides for a given client and chain ID.
* If the fee data contains both maxFeePerGas and maxPriorityFeePerGas, it returns an object with those values.
Expand All @@ -115,20 +121,27 @@ export async function getGasOverridesForTransaction(
export async function getDefaultGasOverrides(
client: ThirdwebClient,
chain: Chain,
feeType?: FeeType,
) {
// if chain is in the force gas price list, always use gas price
if (!FORCE_GAS_PRICE_CHAIN_IDS.includes(chain.id)) {
const feeData = await getDynamicFeeData(client, chain);
if (
feeData.maxFeePerGas !== null &&
feeData.maxPriorityFeePerGas !== null
) {
return {
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
};
}
// give priority to the transaction fee type over the chain fee type
const resolvedFeeType = feeType ?? chain.feeType;
// if chain is configured to force legacy transactions or is in the legacy chain list
if (
resolvedFeeType === "legacy" ||
FORCE_GAS_PRICE_CHAIN_IDS.includes(chain.id)
) {
return {
gasPrice: await getGasPrice({ client, chain, percentMultiplier: 10 }),
};
}
const feeData = await getDynamicFeeData(client, chain);
if (feeData.maxFeePerGas !== null && feeData.maxPriorityFeePerGas !== null) {
return {
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
};
}
// TODO: resolvedFeeType here could be "EIP1559", but we could not get EIP1559 fee data. should we throw?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joaquim-verges wdyt about this btw

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets not throw, i think there's some cases where both type=1559 + gasprice are valid. polygon being one example IIRC

return {
gasPrice: await getGasPrice({ client, chain, percentMultiplier: 10 }),
};
Expand Down Expand Up @@ -156,7 +169,7 @@ async function getDynamicFeeData(
eth_maxPriorityFeePerGas(rpcRequest).catch(() => null),
]);

const baseBlockFee = block?.baseFeePerGas ?? 0n;
const baseBlockFee = block?.baseFeePerGas;

const chainId = chain.id;
// flag chain testnet & flag chain
Expand All @@ -174,7 +187,7 @@ async function getDynamicFeeData(
maxPriorityFeePerGas_ = maxPriorityFeePerGas;
}

if (maxPriorityFeePerGas_ == null) {
if (maxPriorityFeePerGas_ == null || baseBlockFee == null) {
// chain does not support eip-1559, return null for both
return { maxFeePerGas: null, maxPriorityFeePerGas: null };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,42 +75,52 @@ export async function toSerializableTransaction(
const rpcRequest = getRpcClient(options.transaction);
const chainId = options.transaction.chain.id;
const from = options.from;
let [data, nonce, gas, feeData, to, accessList, value, authorizationList] =
await Promise.all([
encode(options.transaction),
(async () => {
// if the user has specified a nonce, use that
const resolvedNonce = await resolvePromisedValue(
options.transaction.nonce,
);
if (resolvedNonce !== undefined) {
return resolvedNonce;
}
let [
data,
nonce,
gas,
feeData,
to,
accessList,
value,
authorizationList,
type,
] = await Promise.all([
encode(options.transaction),
(async () => {
// if the user has specified a nonce, use that
const resolvedNonce = await resolvePromisedValue(
options.transaction.nonce,
);
if (resolvedNonce !== undefined) {
return resolvedNonce;
}

return from // otherwise get the next nonce (import the method to do so)
? await import("../../rpc/actions/eth_getTransactionCount.js").then(
({ eth_getTransactionCount }) =>
eth_getTransactionCount(rpcRequest, {
address:
typeof from === "string"
? getAddress(from)
: getAddress(from.address),
blockTag: "pending",
}),
)
: undefined;
})(),
// takes the same options as the sendTransaction function thankfully!
estimateGas({
...options,
from: options.from,
}),
getGasOverridesForTransaction(options.transaction),
resolvePromisedValue(options.transaction.to),
resolvePromisedValue(options.transaction.accessList),
resolvePromisedValue(options.transaction.value),
resolvePromisedValue(options.transaction.authorizationList),
]);
return from // otherwise get the next nonce (import the method to do so)
? await import("../../rpc/actions/eth_getTransactionCount.js").then(
({ eth_getTransactionCount }) =>
eth_getTransactionCount(rpcRequest, {
address:
typeof from === "string"
? getAddress(from)
: getAddress(from.address),
blockTag: "pending",
}),
)
: undefined;
})(),
// takes the same options as the sendTransaction function thankfully!
estimateGas({
...options,
from: options.from,
}),
getGasOverridesForTransaction(options.transaction),
resolvePromisedValue(options.transaction.to),
resolvePromisedValue(options.transaction.accessList),
resolvePromisedValue(options.transaction.value),
resolvePromisedValue(options.transaction.authorizationList),
resolvePromisedValue(options.transaction.type),
]);

const extraGas = await resolvePromisedValue(options.transaction.extraGas);
if (extraGas) {
Expand All @@ -126,6 +136,7 @@ export async function toSerializableTransaction(
accessList,
value,
authorizationList,
type,
...feeData,
} satisfies SerializableTransaction;
}
11 changes: 11 additions & 0 deletions packages/thirdweb/src/transaction/prepare-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type StaticPrepareTransactionOptions = {
maxFeePerGas?: bigint | undefined;
maxPriorityFeePerGas?: bigint | undefined;
maxFeePerBlobGas?: bigint | undefined;
type?: undefined | TransactionType;
nonce?: number | undefined;
extraGas?: bigint | undefined;
// eip7702
Expand All @@ -34,6 +35,16 @@ export type StaticPrepareTransactionOptions = {
};
};

type TransactionType = "legacy" | "eip1559" | "eip2930" | "eip4844" | "eip7702";

export const TransactionTypeMap: Record<TransactionType, number> = {
legacy: 0,
eip1559: 1,
eip2930: 2,
eip4844: 3,
eip7702: 4,
};

export type EIP712TransactionOptions = {
// constant or user input
gasPerPubdata?: bigint | undefined;
Expand Down
Loading