From ecb3d1e56eb387ceba3fb5477a4c4090a810c5ec Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Tue, 20 Feb 2024 11:26:50 -0800 Subject: [PATCH] sendTx, estimateGas and simulate now all accept `wallet` OR `account` --- packages/thirdweb/src/chains/types.ts | 3 ++ packages/thirdweb/src/gas/fee-data.ts | 47 ++++++++++++++++--- .../src/gas/op-gas-fee-reducer.test.ts | 10 ++++ .../thirdweb/src/gas/op-gas-fee-reducer.ts | 24 ++++++++++ .../src/transaction/actions/estimate-gas.ts | 44 +++++++++++++---- .../actions/send-batch-transaction.ts | 16 +++++-- .../transaction/actions/send-transaction.ts | 34 +++++++++----- .../src/transaction/actions/simulate.test.ts | 2 +- .../src/transaction/actions/simulate.ts | 40 +++++++++++++--- 9 files changed, 178 insertions(+), 42 deletions(-) create mode 100644 packages/thirdweb/src/gas/op-gas-fee-reducer.test.ts create mode 100644 packages/thirdweb/src/gas/op-gas-fee-reducer.ts diff --git a/packages/thirdweb/src/chains/types.ts b/packages/thirdweb/src/chains/types.ts index f1465dc2240..7f9a735480f 100644 --- a/packages/thirdweb/src/chains/types.ts +++ b/packages/thirdweb/src/chains/types.ts @@ -17,6 +17,9 @@ export type ChainOptions = { apiUrl?: string; }>; testnet?: true; + experimental?: { + increaseZeroByteCount?: boolean; + }; }; type Icon = { diff --git a/packages/thirdweb/src/gas/fee-data.ts b/packages/thirdweb/src/gas/fee-data.ts index 7cb7396add3..f8b2118cd61 100644 --- a/packages/thirdweb/src/gas/fee-data.ts +++ b/packages/thirdweb/src/gas/fee-data.ts @@ -8,6 +8,8 @@ import { } from "../rpc/index.js"; import { parseUnits } from "../utils/units.js"; import type { PreparedTransaction } from "../transaction/prepare-transaction.js"; +import { resolvePromisedValue } from "../utils/promise/resolve-promised-value.js"; +import { roundUpGas } from "./op-gas-fee-reducer.js"; type FeeData = { maxFeePerGas: null | bigint; @@ -34,23 +36,54 @@ export async function getGasOverridesForTransaction( transaction: PreparedTransaction, ): Promise { // if we have a `gasPrice` param in the transaction, use that. - if ("gasPrice" in transaction && !transaction.gasPrice) { - return { gasPrice: transaction.gasPrice }; + if ("gasPrice" in transaction) { + const resolvedGasPrice = await resolvePromisedValue(transaction.gasPrice); + // if the value ends up being "undefined" -> continue to getting the real data + if (resolvedGasPrice !== undefined) { + return { gasPrice: resolvedGasPrice }; + } } // if we have a maxFeePerGas and maxPriorityFeePerGas, use those if ( "maxFeePerGas" in transaction && "maxPriorityFeePerGas" in transaction && - !transaction.maxFeePerGas && - !transaction.maxPriorityFeePerGas + transaction.maxFeePerGas && + transaction.maxPriorityFeePerGas ) { + const [resolvedMaxFee, resolvedMaxPriorityFee] = await Promise.all([ + resolvePromisedValue(transaction.maxFeePerGas), + resolvePromisedValue(transaction.maxPriorityFeePerGas), + ]); return { - maxFeePerGas: transaction.maxFeePerGas, - maxPriorityFeePerGas: transaction.maxPriorityFeePerGas, + maxFeePerGas: resolvedMaxFee, + maxPriorityFeePerGas: resolvedMaxPriorityFee, }; } // otherwise call getDefaultGasOverrides - return getDefaultGasOverrides(transaction.client, transaction.chain); + const defaultGasOverrides = await getDefaultGasOverrides( + transaction.client, + transaction.chain, + ); + if (!transaction.chain.experimental?.increaseZeroByteCount) { + // return as is + return defaultGasOverrides; + } + // otherwise adjust each value + if (defaultGasOverrides.gasPrice) { + return { gasPrice: roundUpGas(defaultGasOverrides.gasPrice) }; + } else if ( + defaultGasOverrides.maxFeePerGas && + defaultGasOverrides.maxPriorityFeePerGas + ) { + return { + maxFeePerGas: roundUpGas(defaultGasOverrides.maxFeePerGas), + maxPriorityFeePerGas: roundUpGas( + defaultGasOverrides.maxPriorityFeePerGas, + ), + }; + } + // this should never happen + return defaultGasOverrides; } /** diff --git a/packages/thirdweb/src/gas/op-gas-fee-reducer.test.ts b/packages/thirdweb/src/gas/op-gas-fee-reducer.test.ts new file mode 100644 index 00000000000..9d04303316e --- /dev/null +++ b/packages/thirdweb/src/gas/op-gas-fee-reducer.test.ts @@ -0,0 +1,10 @@ +import { describe, test, expect } from "vitest"; +import { roundUpGas } from "./op-gas-fee-reducer.js"; +import { hexToBigInt, numberToHex } from "viem"; + +describe("opGasFeeReducer", () => { + test("should turn '0x3F1234' into '0x400000'", () => { + const result = roundUpGas(hexToBigInt("0x3F1234")); + expect(numberToHex(result)).toBe("0x400000"); + }); +}); diff --git a/packages/thirdweb/src/gas/op-gas-fee-reducer.ts b/packages/thirdweb/src/gas/op-gas-fee-reducer.ts new file mode 100644 index 00000000000..036a81f74aa --- /dev/null +++ b/packages/thirdweb/src/gas/op-gas-fee-reducer.ts @@ -0,0 +1,24 @@ +/** + * Via: https://twitter.com/0xjustadev/status/1758973668011434062 + * + * Increases the gas fee value to the nearest power of 2. + * If the value is already a power of 2 or 0, it returns the value as is. + * Otherwise, it finds the highest power of 2 that is bigger than the given value. + * @param value - The gas fee value to be "rounded up". + * @returns The *increased* gas value which will result in a lower L1 gas fee, overall reducing the gas fee. + * @internal + */ +export function roundUpGas(value: bigint): bigint { + if (value === 0n || (value & (value - 1n)) === 0n) { + return value; + } + + // Find the highest set bit by shifting until the value is 0. + let highestBit = 1n; + while (value > 0n) { + value >>= 1n; + highestBit <<= 1n; + } + + return highestBit; +} diff --git a/packages/thirdweb/src/transaction/actions/estimate-gas.ts b/packages/thirdweb/src/transaction/actions/estimate-gas.ts index 35629c00d0f..9b4c1b9c18d 100644 --- a/packages/thirdweb/src/transaction/actions/estimate-gas.ts +++ b/packages/thirdweb/src/transaction/actions/estimate-gas.ts @@ -1,19 +1,30 @@ import { formatTransactionRequest } from "viem"; -import type { Wallet } from "../../wallets/interfaces/wallet.js"; +import type { Account, Wallet } from "../../wallets/interfaces/wallet.js"; import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js"; import type { PreparedTransaction } from "../prepare-transaction.js"; import { extractError as parseEstimationError } from "../extract-error.js"; import type { Prettify } from "../../utils/type-utils.js"; +import { roundUpGas } from "../../gas/op-gas-fee-reducer.js"; -type EstimateGasOptions = Prettify< +export type EstimateGasOptions = Prettify< { transaction: PreparedTransaction; } & ( | { + account: Account; + from?: never; + wallet?: never; + } + | { + account?: never; from?: string; wallet?: never; } - | { from?: never; wallet?: Wallet } + | { + account?: never; + from?: never; + wallet?: Wallet; + } ) >; @@ -51,7 +62,11 @@ export async function estimateGas( // if the wallet itself overrides the estimateGas function, use that if (options.wallet && options.wallet.estimateGas) { try { - return await options.wallet.estimateGas(options.transaction); + let gas = await options.wallet.estimateGas(options.transaction); + if (options.transaction.chain.experimental?.increaseZeroByteCount) { + gas = roundUpGas(gas); + } + return gas; } catch (error) { throw await parseEstimationError({ error, @@ -74,19 +89,28 @@ export async function estimateGas( ]); const rpcRequest = getRpcClient(options.transaction); + // from is: + // 1. the user specified from address + // 2. the passed in account address + // 3. the passed in wallet's account address + const from = + options.from ?? + options.account?.address ?? + options.wallet?.getAccount()?.address ?? + undefined; try { - return await eth_estimateGas( + let gas = await eth_estimateGas( rpcRequest, formatTransactionRequest({ to: toAddress, data: encodedData, - from: - // if the user has specified a from address, use that - // otherwise use the wallet's account address - // if the wallet is not provided, use undefined - options.from ?? options.wallet?.getAccount()?.address ?? undefined, + from, }), ); + if (options.transaction.chain.experimental?.increaseZeroByteCount) { + gas = roundUpGas(gas); + } + return gas; } catch (error) { throw await parseEstimationError({ error, diff --git a/packages/thirdweb/src/transaction/actions/send-batch-transaction.ts b/packages/thirdweb/src/transaction/actions/send-batch-transaction.ts index 2d77ec4aa1b..814f1cb55f1 100644 --- a/packages/thirdweb/src/transaction/actions/send-batch-transaction.ts +++ b/packages/thirdweb/src/transaction/actions/send-batch-transaction.ts @@ -1,16 +1,22 @@ import type { WaitForReceiptOptions } from "./wait-for-tx-receipt.js"; import type { + Account, SendTransactionOption, Wallet, } from "../../wallets/interfaces/wallet.js"; import type { PreparedTransaction } from "../prepare-transaction.js"; import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js"; import { encode } from "./encode.js"; +import type { Prettify } from "../../utils/type-utils.js"; -type SendBatchTransactionOptions = { - transactions: PreparedTransaction[]; - wallet: Wallet; -}; +export type SendBatchTransactionOptions = Prettify< + { + transactions: PreparedTransaction[]; + } & ( + | { account?: never; wallet: Wallet } + | { account: Account; wallet?: never } + ) +>; /** * Sends a transaction using the provided wallet. @@ -30,7 +36,7 @@ type SendBatchTransactionOptions = { export async function sendBatchTransaction( options: SendBatchTransactionOptions, ): Promise { - const account = options.wallet.getAccount(); + const account = options.account ?? options.wallet.getAccount(); if (!account) { throw new Error("not connected"); } diff --git a/packages/thirdweb/src/transaction/actions/send-transaction.ts b/packages/thirdweb/src/transaction/actions/send-transaction.ts index 5d97fbf86c7..d5cdc6bb2ee 100644 --- a/packages/thirdweb/src/transaction/actions/send-transaction.ts +++ b/packages/thirdweb/src/transaction/actions/send-transaction.ts @@ -1,13 +1,24 @@ import type { TransactionSerializable } from "viem"; import type { WaitForReceiptOptions } from "./wait-for-tx-receipt.js"; -import type { Wallet } from "../../wallets/interfaces/wallet.js"; +import type { Account, Wallet } from "../../wallets/interfaces/wallet.js"; import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js"; import type { PreparedTransaction } from "../prepare-transaction.js"; +import type { Prettify } from "../../utils/type-utils.js"; -type SendTransactionOptions = { - transaction: PreparedTransaction; - wallet: Wallet; -}; +export type SendTransactionOptions = Prettify< + { + transaction: PreparedTransaction; + } & ( + | { + account?: never; + wallet: Wallet; + } + | { + account: Account; + wallet?: never; + } + ) +>; /** * Sends a transaction using the provided wallet. @@ -27,7 +38,7 @@ type SendTransactionOptions = { export async function sendTransaction( options: SendTransactionOptions, ): Promise { - const account = options.wallet.getAccount(); + const account = options.account ?? options.wallet.getAccount(); if (!account) { throw new Error("not connected"); } @@ -56,18 +67,15 @@ export async function sendTransaction( address: account.address, blockTag: "pending", }), - // if user has specified a gas value, use that - estimateGas({ - transaction: options.transaction, - wallet: options.wallet, - }), + // takes the same options as the sendTransaction function thankfully! + estimateGas(options), getGasOverridesForTransaction(options.transaction), resolvePromisedValue(options.transaction.to), resolvePromisedValue(options.transaction.accessList), resolvePromisedValue(options.transaction.value), ]); - const walletChainId = options.wallet.getChain()?.id; + const walletChainId = options.wallet?.getChain()?.id; const chainId = options.transaction.chain.id; // only if: // 1. the wallet has a chainId @@ -75,7 +83,7 @@ export async function sendTransaction( // 3. the wallet's chainId is not the same as the transaction's chainId // => switch tot he wanted chain if ( - options.wallet.switchChain && + options.wallet?.switchChain && walletChainId && walletChainId !== chainId ) { diff --git a/packages/thirdweb/src/transaction/actions/simulate.test.ts b/packages/thirdweb/src/transaction/actions/simulate.test.ts index ddf0af981bd..24c85f666e3 100644 --- a/packages/thirdweb/src/transaction/actions/simulate.test.ts +++ b/packages/thirdweb/src/transaction/actions/simulate.test.ts @@ -18,7 +18,7 @@ describe("transaction: simulate", () => { }); const result = await simulateTransaction({ transaction: tx, - account: { address: TEST_WALLET_A }, + from: TEST_WALLET_A, }); expect(result).toMatchInlineSnapshot(`true`); diff --git a/packages/thirdweb/src/transaction/actions/simulate.ts b/packages/thirdweb/src/transaction/actions/simulate.ts index 165a4533ea0..f03a8df7571 100644 --- a/packages/thirdweb/src/transaction/actions/simulate.ts +++ b/packages/thirdweb/src/transaction/actions/simulate.ts @@ -1,5 +1,5 @@ import { formatTransactionRequest } from "viem"; -import type { Account } from "../../wallets/interfaces/wallet.js"; +import type { Account, Wallet } from "../../wallets/interfaces/wallet.js"; import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js"; import type { PreparedTransaction } from "../prepare-transaction.js"; import { eth_call } from "../../rpc/index.js"; @@ -7,11 +7,29 @@ import type { Abi, AbiFunction } from "abitype"; import type { ReadContractResult } from "../read-contract.js"; import { decodeFunctionResult } from "../../abi/decode.js"; import { extractError } from "../extract-error.js"; +import type { Prettify } from "../../utils/type-utils.js"; -type SimulateOptions = { - transaction: PreparedTransaction; - account?: Partial | undefined; -}; +type SimulateOptions = Prettify< + { + transaction: PreparedTransaction; + } & ( + | { + account: Account; + from?: never; + wallet?: never; + } + | { + account?: never; + from?: string; + wallet?: never; + } + | { + account?: never; + from?: never; + wallet?: Wallet; + } + ) +>; /** * Simulates the execution of a transaction. @@ -44,9 +62,19 @@ export async function simulateTransaction< resolvePromisedValue(options.transaction.value), ]); + // from is: + // 1. the user specified from address + // 2. the passed in account address + // 3. the passed in wallet's account address + const from = + options.from ?? + options.account?.address ?? + options.wallet?.getAccount()?.address ?? + undefined; + const serializedTx = formatTransactionRequest({ data, - from: options.account?.address, + from, to, value, accessList,