diff --git a/packages/thirdweb/src/adapters/ethers5.ts b/packages/thirdweb/src/adapters/ethers5.ts index ec7669bc078..405e708b653 100644 --- a/packages/thirdweb/src/adapters/ethers5.ts +++ b/packages/thirdweb/src/adapters/ethers5.ts @@ -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"; @@ -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 diff --git a/packages/thirdweb/src/chains/types.ts b/packages/thirdweb/src/chains/types.ts index 7a9298c92eb..d4741fb47f9 100644 --- a/packages/thirdweb/src/chains/types.ts +++ b/packages/thirdweb/src/chains/types.ts @@ -1,3 +1,5 @@ +import type { FeeType } from "../gas/fee-data.js"; + /** * @chain */ @@ -26,6 +28,7 @@ export type ChainOptions = { increaseZeroByteCount?: boolean; }; faucets?: Array; + feeType?: FeeType; }; /** diff --git a/packages/thirdweb/src/gas/fee-data.ts b/packages/thirdweb/src/gas/fee-data.ts index 3e38d35a59c..b351193e05b 100644 --- a/packages/thirdweb/src/gas/fee-data.ts +++ b/packages/thirdweb/src/gas/fee-data.ts @@ -52,11 +52,13 @@ export async function getGasOverridesForTransaction( transaction: PreparedTransaction, ): Promise { // 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) { @@ -65,6 +67,7 @@ export async function getGasOverridesForTransaction( maxPriorityFeePerGas, }; } + if (typeof gasPrice === "bigint") { return { gasPrice }; } @@ -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) { @@ -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. @@ -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? return { gasPrice: await getGasPrice({ client, chain, percentMultiplier: 10 }), }; @@ -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 @@ -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 }; } diff --git a/packages/thirdweb/src/transaction/actions/to-serializable-transaction.ts b/packages/thirdweb/src/transaction/actions/to-serializable-transaction.ts index c8ddfc9f5f1..f2ed26a5039 100644 --- a/packages/thirdweb/src/transaction/actions/to-serializable-transaction.ts +++ b/packages/thirdweb/src/transaction/actions/to-serializable-transaction.ts @@ -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) { @@ -126,6 +136,7 @@ export async function toSerializableTransaction( accessList, value, authorizationList, + type, ...feeData, } satisfies SerializableTransaction; } diff --git a/packages/thirdweb/src/transaction/prepare-transaction.ts b/packages/thirdweb/src/transaction/prepare-transaction.ts index aef9a876a82..f309be24dcd 100644 --- a/packages/thirdweb/src/transaction/prepare-transaction.ts +++ b/packages/thirdweb/src/transaction/prepare-transaction.ts @@ -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 @@ -34,6 +35,16 @@ export type StaticPrepareTransactionOptions = { }; }; +type TransactionType = "legacy" | "eip1559" | "eip2930" | "eip4844" | "eip7702"; + +export const TransactionTypeMap: Record = { + legacy: 0, + eip1559: 1, + eip2930: 2, + eip4844: 3, + eip7702: 4, +}; + export type EIP712TransactionOptions = { // constant or user input gasPerPubdata?: bigint | undefined;