Skip to content

Commit

Permalink
[SDK] expose estimateUserOpGasCost (#6370)
Browse files Browse the repository at this point in the history
  • Loading branch information
joaquim-verges authored Feb 28, 2025
1 parent 062504f commit 5625ff1
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 33 deletions.
5 changes: 5 additions & 0 deletions .changeset/eighty-pens-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": minor
---

Added `estimateUserOpGasCost()` utility function for estimating the total gas cost in wei/ether of user operations
19 changes: 17 additions & 2 deletions packages/thirdweb/src/chains/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ const opChains = [
* TODO this should be in the chain definition itself
* @internal
*/
export function isOpStackChain(chain: Chain) {
return opChains.includes(chain.id);
export async function isOpStackChain(chain: Chain) {
if (chain.id === 1337 || chain.id === 31337) {
return false;
}

if (opChains.includes(chain.id)) {
return true;
}
// fallback to checking the stack on rpc
try {
const { getChainMetadata } = await import("./utils.js");
const chainMetadata = await getChainMetadata(chain);
return chainMetadata.stackType === "optimism_bedrock";
} catch {
// If the network check fails, assume it's not a OP chain
return false;
}
}
1 change: 1 addition & 0 deletions packages/thirdweb/src/exports/wallets/smart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export {
bundleUserOp,
getUserOpGasFees,
estimateUserOpGas,
estimateUserOpGasCost,
} from "../../wallets/smart/lib/bundler.js";

export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export async function estimateGasCost(
);
}
let l1Fee: bigint;
if (isOpStackChain(transaction.chain)) {
if (await isOpStackChain(transaction.chain)) {
const { estimateL1Fee } = await import("../../gas/estimate-l1-fee.js");
l1Fee = await estimateL1Fee({
transaction,
Expand Down
2 changes: 1 addition & 1 deletion packages/thirdweb/src/wallets/smart/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@ async function _sendUserOp(args: {
}
}

async function getEntrypointFromFactory(
export async function getEntrypointFromFactory(
factoryAddress: string,
client: ThirdwebClient,
chain: Chain,
Expand Down
95 changes: 95 additions & 0 deletions packages/thirdweb/src/wallets/smart/lib/bundler.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
import { decodeErrorResult } from "viem";
import type { ThirdwebClient } from "../../../client/client.js";
import { getContract } from "../../../contract/contract.js";
import { parseEventLogs } from "../../../event/actions/parse-logs.js";
import { userOperationRevertReasonEvent } from "../../../extensions/erc4337/__generated__/IEntryPoint/events/UserOperationRevertReason.js";
import { postOpRevertReasonEvent } from "../../../extensions/erc4337/__generated__/IEntryPoint_v07/events/PostOpRevertReason.js";
import type { PreparedTransaction } from "../../../transaction/prepare-transaction.js";
import type { SerializableTransaction } from "../../../transaction/serialize-transaction.js";
import type { TransactionReceipt } from "../../../transaction/types.js";
import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed.js";
import { type Hex, hexToBigInt } from "../../../utils/encoding/hex.js";
import { getClientFetch } from "../../../utils/fetch.js";
import { stringify } from "../../../utils/json.js";
import { toEther } from "../../../utils/units.js";
import type { Account } from "../../interfaces/wallet.js";
import { getEntrypointFromFactory } from "../index.js";
import {
type BundlerOptions,
type EstimationResult,
type GasPriceResult,
type PmTransactionData,
type SmartWalletOptions,
type UserOperationReceipt,
type UserOperationV06,
type UserOperationV07,
formatUserOperationReceipt,
} from "../types.js";
import { predictSmartAccountAddress } from "./calls.js";
import {
ENTRYPOINT_ADDRESS_v0_6,
MANAGED_ACCOUNT_GAS_BUFFER,
getDefaultBundlerUrl,
} from "./constants.js";
import { prepareUserOp } from "./userop.js";
import { hexlifyUserOp } from "./utils.js";

/**
Expand Down Expand Up @@ -111,6 +121,91 @@ export async function estimateUserOpGas(
};
}

/**
* Estimate the gas cost of a user operation.
* @param args - The options for estimating the gas cost of a user operation.
* @returns The estimated gas cost of the user operation.
* @example
* ```ts
* import { estimateUserOpGasCost } from "thirdweb/wallets/smart";
*
* const gasCost = await estimateUserOpGasCost({
* transactions,
* adminAccount,
* client,
* smartWalletOptions,
* });
* ```
* @walletUtils
*/
export async function estimateUserOpGasCost(args: {
transactions: PreparedTransaction[];
adminAccount: Account;
client: ThirdwebClient;
smartWalletOptions: SmartWalletOptions;
}) {
// if factory is passed, but no entrypoint, try to resolve entrypoint from factory
if (
args.smartWalletOptions.factoryAddress &&
!args.smartWalletOptions.overrides?.entrypointAddress
) {
const entrypointAddress = await getEntrypointFromFactory(
args.smartWalletOptions.factoryAddress,
args.client,
args.smartWalletOptions.chain,
);
if (entrypointAddress) {
args.smartWalletOptions.overrides = {
...args.smartWalletOptions.overrides,
entrypointAddress,
};
}
}

const userOp = await prepareUserOp({
transactions: args.transactions,
adminAccount: args.adminAccount,
client: args.client,
smartWalletOptions: args.smartWalletOptions,
isDeployedOverride: await isContractDeployed(
getContract({
address: await predictSmartAccountAddress({
adminAddress: args.adminAccount.address,
factoryAddress: args.smartWalletOptions.factoryAddress,
chain: args.smartWalletOptions.chain,
client: args.client,
}),
chain: args.smartWalletOptions.chain,
client: args.client,
}),
),
});

let gasLimit = 0n;
if ("paymasterVerificationGasLimit" in userOp) {
// v0.7
gasLimit =
BigInt(userOp.paymasterVerificationGasLimit ?? 0) +
BigInt(userOp.paymasterPostOpGasLimit ?? 0) +
BigInt(userOp.verificationGasLimit ?? 0) +
BigInt(userOp.preVerificationGas ?? 0) +
BigInt(userOp.callGasLimit ?? 0);
} else {
// v0.6
gasLimit =
BigInt(userOp.verificationGasLimit ?? 0) +
BigInt(userOp.preVerificationGas ?? 0) +
BigInt(userOp.callGasLimit ?? 0);
}

const gasCost = gasLimit * (userOp.maxFeePerGas ?? 0n);

return {
ether: toEther(gasCost),
wei: gasCost,
};
}

/**
* Get the gas fees of a user operation.
* @param args - The options for getting the gas price of a user operation.
Expand Down
57 changes: 42 additions & 15 deletions packages/thirdweb/src/wallets/smart/lib/userop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,32 @@ export async function createAndSignUserOp(options: {
smartWalletOptions: SmartWalletOptions;
waitForDeployment?: boolean;
isDeployedOverride?: boolean;
}) {
const unsignedUserOp = await prepareUserOp({
transactions: options.transactions,
adminAccount: options.adminAccount,
client: options.client,
smartWalletOptions: options.smartWalletOptions,
waitForDeployment: options.waitForDeployment,
isDeployedOverride: options.isDeployedOverride,
});
const signedUserOp = await signUserOp({
client: options.client,
chain: options.smartWalletOptions.chain,
adminAccount: options.adminAccount,
entrypointAddress: options.smartWalletOptions.overrides?.entrypointAddress,
userOp: unsignedUserOp,
});
return signedUserOp;
}

export async function prepareUserOp(options: {
transactions: PreparedTransaction[];
adminAccount: Account;
client: ThirdwebClient;
smartWalletOptions: SmartWalletOptions;
waitForDeployment?: boolean;
isDeployedOverride?: boolean;
}) {
const config = options.smartWalletOptions;
const factoryContract = getContract({
Expand All @@ -731,6 +757,7 @@ export async function createAndSignUserOp(options: {
let executeTx: PreparedTransaction;
if (options.transactions.length === 1) {
const tx = options.transactions[0] as PreparedTransaction;
// for single tx, simulate fully
const serializedTx = await toSerializableTransaction({
transaction: tx,
from: accountAddress,
Expand All @@ -741,13 +768,21 @@ export async function createAndSignUserOp(options: {
executeOverride: config.overrides?.execute,
});
} else {
// for multiple txs, we can't simulate, just encode
const serializedTxs = await Promise.all(
options.transactions.map((tx) =>
toSerializableTransaction({
transaction: tx,
from: accountAddress,
}),
),
options.transactions.map(async (tx) => {
const [data, to, value] = await Promise.all([
encode(tx),
resolvePromisedValue(tx.to),
resolvePromisedValue(tx.value),
]);
return {
data,
to,
value,
chainId: tx.chain.id,
};
}),
);
executeTx = prepareBatchExecute({
accountContract,
Expand All @@ -756,7 +791,7 @@ export async function createAndSignUserOp(options: {
});
}

const unsignedUserOp = await createUnsignedUserOp({
return createUnsignedUserOp({
transaction: executeTx,
factoryContract,
accountContract,
Expand All @@ -766,14 +801,6 @@ export async function createAndSignUserOp(options: {
waitForDeployment: options.waitForDeployment,
isDeployedOverride: options.isDeployedOverride,
});
const signedUserOp = await signUserOp({
client: options.client,
chain: config.chain,
adminAccount: options.adminAccount,
entrypointAddress: config.overrides?.entrypointAddress,
userOp: unsignedUserOp,
});
return signedUserOp;
}

async function waitForAccountDeployed(accountContract: ThirdwebContract) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
} from "../../exports/extensions/erc4337.js";
import { claimTo } from "../../extensions/erc1155/drops/write/claimTo.js";
import { setContractURI } from "../../extensions/marketplace/__generated__/IMarketplace/write/setContractURI.js";
import { estimateGasCost } from "../../transaction/actions/estimate-gas-cost.js";
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
import { sendBatchTransaction } from "../../transaction/actions/send-batch-transaction.js";
import { waitForReceipt } from "../../transaction/actions/wait-for-tx-receipt.js";
Expand All @@ -27,6 +26,7 @@ import { hashTypedData } from "../../utils/hashing/hashTypedData.js";
import { sleep } from "../../utils/sleep.js";
import type { Account, Wallet } from "../interfaces/wallet.js";
import { generateAccount } from "../utils/generateAccount.js";
import { estimateUserOpGasCost } from "./lib/bundler.js";
import { predictSmartAccountAddress } from "./lib/calls.js";
import { DEFAULT_ACCOUNT_FACTORY_V0_7 } from "./lib/constants.js";
import {
Expand Down Expand Up @@ -87,6 +87,27 @@ describe.sequential(
expect(predictedAddress).toEqual(smartWalletAddress);
});

it("can estimate gas cost", async () => {
const gasCost = await estimateUserOpGasCost({
transactions: [
claimTo({
contract,
quantity: 1n,
to: smartWalletAddress,
tokenId: 0n,
}),
],
adminAccount: personalAccount,
client: TEST_CLIENT,
smartWalletOptions: {
chain,
sponsorGas: true,
factoryAddress: DEFAULT_ACCOUNT_FACTORY_V0_7,
},
});
expect(gasCost.ether).not.toBe("0");
});

it("can sign a msg", async () => {
const signature = await smartAccount.signMessage({
message: "hello world",
Expand Down Expand Up @@ -202,19 +223,6 @@ describe.sequential(
expect(isDeployed).toEqual(true);
});

it("can estimate a tx", async () => {
const estimates = await estimateGasCost({
transaction: claimTo({
contract,
quantity: 1n,
to: smartWalletAddress,
tokenId: 0n,
}),
account: smartAccount,
});
expect(estimates.wei.toString()).not.toBe("0");
});

it("can execute a batched tx", async () => {
const tx = await sendBatchTransaction({
account: smartAccount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { hashTypedData } from "../../utils/hashing/hashTypedData.js";
import { sleep } from "../../utils/sleep.js";
import type { Account, Wallet } from "../interfaces/wallet.js";
import { generateAccount } from "../utils/generateAccount.js";
import { estimateUserOpGasCost } from "./lib/bundler.js";
import { predictSmartAccountAddress } from "./lib/calls.js";
import { deploySmartAccount } from "./lib/signing.js";
import { smartWallet } from "./smart-wallet.js";
Expand Down Expand Up @@ -74,6 +75,26 @@ describe.runIf(process.env.TW_SECRET_KEY).sequential(
});
});

it("can estimate gas cost", async () => {
const gasCost = await estimateUserOpGasCost({
transactions: [
claimTo({
contract,
quantity: 1n,
to: smartWalletAddress,
tokenId: 0n,
}),
],
adminAccount: personalAccount,
client: TEST_CLIENT,
smartWalletOptions: {
chain,
sponsorGas: true,
},
});
expect(gasCost.ether).not.toBe("0");
});

it("can connect", async () => {
expect(smartWalletAddress).toHaveLength(42);
const predictedAddress = await predictSmartAccountAddress({
Expand Down

0 comments on commit 5625ff1

Please sign in to comment.