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] expose estimateUserOpGasCost #6370

Merged
merged 1 commit into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
* 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;
}

Check warning on line 31 in packages/thirdweb/src/chains/constants.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/chains/constants.ts#L30-L31

Added lines #L30 - L31 were not covered by tests

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;
}

Check warning on line 44 in packages/thirdweb/src/chains/constants.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/chains/constants.ts#L43-L44

Added lines #L43 - L44 were not covered by tests
}
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 @@
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;
}

Check warning on line 726 in packages/thirdweb/src/wallets/smart/lib/userop.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/smart/lib/userop.ts#L709-L726

Added lines #L709 - L726 were not covered by tests

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 @@
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 @@
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,
};
}),

Check warning on line 785 in packages/thirdweb/src/wallets/smart/lib/userop.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/smart/lib/userop.ts#L773-L785

Added lines #L773 - L785 were not covered by tests
);
executeTx = prepareBatchExecute({
accountContract,
Expand All @@ -756,7 +791,7 @@
});
}

const unsignedUserOp = await createUnsignedUserOp({
return createUnsignedUserOp({
transaction: executeTx,
factoryContract,
accountContract,
Expand All @@ -766,14 +801,6 @@
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