Skip to content

Commit fee5d83

Browse files
[SDK] expose estimateUserOpGasCost
1 parent 36b4ea5 commit fee5d83

File tree

9 files changed

+194
-27
lines changed

9 files changed

+194
-27
lines changed

.changeset/eighty-pens-judge.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Added `estimateUserOpGasCost()` utility function for estimating the total gas cost in wei/ether of user operations

packages/thirdweb/src/chains/constants.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,21 @@ const opChains = [
2525
* TODO this should be in the chain definition itself
2626
* @internal
2727
*/
28-
export function isOpStackChain(chain: Chain) {
29-
return opChains.includes(chain.id);
28+
export async function isOpStackChain(chain: Chain) {
29+
if (chain.id === 1337 || chain.id === 31337) {
30+
return false;
31+
}
32+
33+
if (opChains.includes(chain.id)) {
34+
return true;
35+
}
36+
// fallback to checking the stack on rpc
37+
try {
38+
const { getChainMetadata } = await import("./utils.js");
39+
const chainMetadata = await getChainMetadata(chain);
40+
return chainMetadata.stackType === "optimism_bedrock";
41+
} catch {
42+
// If the network check fails, assume it's not a OP chain
43+
return false;
44+
}
3045
}

packages/thirdweb/src/exports/wallets/smart.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export {
1313
bundleUserOp,
1414
getUserOpGasFees,
1515
estimateUserOpGas,
16+
estimateUserOpGasCost,
1617
} from "../../wallets/smart/lib/bundler.js";
1718

1819
export {

packages/thirdweb/src/transaction/actions/estimate-gas-cost.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export async function estimateGasCost(
4444
);
4545
}
4646
let l1Fee: bigint;
47-
if (isOpStackChain(transaction.chain)) {
47+
if (await isOpStackChain(transaction.chain)) {
4848
const { estimateL1Fee } = await import("../../gas/estimate-l1-fee.js");
4949
l1Fee = await estimateL1Fee({
5050
transaction,

packages/thirdweb/src/wallets/smart/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ async function _sendUserOp(args: {
556556
}
557557
}
558558

559-
async function getEntrypointFromFactory(
559+
export async function getEntrypointFromFactory(
560560
factoryAddress: string,
561561
client: ThirdwebClient,
562562
chain: Chain,

packages/thirdweb/src/wallets/smart/lib/bundler.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,38 @@
11
import { decodeErrorResult } from "viem";
2+
/import type { ThirdwebClient } from "../../../client/client.js";
3+
import type { ThirdwebClient } from "../../../client/client.js";
4+
import { getContract } from "../../../contract/contract.js";
25
import { parseEventLogs } from "../../../event/actions/parse-logs.js";
36
import { userOperationRevertReasonEvent } from "../../../extensions/erc4337/__generated__/IEntryPoint/events/UserOperationRevertReason.js";
47
import { postOpRevertReasonEvent } from "../../../extensions/erc4337/__generated__/IEntryPoint_v07/events/PostOpRevertReason.js";
8+
import type { PreparedTransaction } from "../../../transaction/prepare-transaction.js";
59
import type { SerializableTransaction } from "../../../transaction/serialize-transaction.js";
610
import type { TransactionReceipt } from "../../../transaction/types.js";
11+
import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed.js";
712
import { type Hex, hexToBigInt } from "../../../utils/encoding/hex.js";
813
import { getClientFetch } from "../../../utils/fetch.js";
914
import { stringify } from "../../../utils/json.js";
15+
import { toEther } from "../../../utils/units.js";
16+
import type { Account } from "../../interfaces/wallet.js";
17+
import { getEntrypointFromFactory } from "../index.js";
1018
import {
1119
type BundlerOptions,
1220
type EstimationResult,
1321
type GasPriceResult,
1422
type PmTransactionData,
23+
type SmartWalletOptions,
1524
type UserOperationReceipt,
1625
type UserOperationV06,
1726
type UserOperationV07,
1827
formatUserOperationReceipt,
1928
} from "../types.js";
29+
import { predictSmartAccountAddress } from "./calls.js";
2030
import {
2131
ENTRYPOINT_ADDRESS_v0_6,
2232
MANAGED_ACCOUNT_GAS_BUFFER,
2333
getDefaultBundlerUrl,
2434
} from "./constants.js";
35+
import { prepareUserOp } from "./userop.js";
2536
import { hexlifyUserOp } from "./utils.js";
2637

2738
/**
@@ -111,6 +122,92 @@ export async function estimateUserOpGas(
111122
};
112123
}
113124

125+
/**
126+
* Estimate the gas cost of a user operation.
127+
* @param args - The options for estimating the gas cost of a user operation.
128+
* @returns The estimated gas cost of the user operation.
129+
* @example
130+
* ```ts
131+
* import { estimateUserOpGasCost } from "thirdweb/wallets/smart";
132+
*
133+
* const gasCost = await estimateUserOpGasCost({
134+
* transactions,
135+
* adminAccount,
136+
* client,
137+
* smartWalletOptions,
138+
* });
139+
* ```
140+
* @walletUtils
141+
*/
142+
export async function estimateUserOpGasCost(args: {
143+
transactions: PreparedTransaction[];
144+
adminAccount: Account;
145+
client: ThirdwebClient;
146+
smartWalletOptions: SmartWalletOptions;
147+
}) {
148+
// if factory is passed, but no entrypoint, try to resolve entrypoint from factory
149+
if (
150+
args.smartWalletOptions.factoryAddress &&
151+
!args.smartWalletOptions.overrides?.entrypointAddress
152+
) {
153+
const entrypointAddress = await getEntrypointFromFactory(
154+
args.smartWalletOptions.factoryAddress,
155+
args.client,
156+
args.smartWalletOptions.chain,
157+
);
158+
if (entrypointAddress) {
159+
args.smartWalletOptions.overrides = {
160+
...args.smartWalletOptions.overrides,
161+
entrypointAddress,
162+
};
163+
}
164+
}
165+
166+
const userOp = await prepareUserOp({
167+
transactions: args.transactions,
168+
adminAccount: args.adminAccount,
169+
client: args.client,
170+
smartWalletOptions: args.smartWalletOptions,
171+
isDeployedOverride: await isContractDeployed(
172+
getContract({
173+
address: await predictSmartAccountAddress({
174+
adminAddress: args.adminAccount.address,
175+
factoryAddress: args.smartWalletOptions.factoryAddress,
176+
chain: args.smartWalletOptions.chain,
177+
client: args.client,
178+
}),
179+
chain: args.smartWalletOptions.chain,
180+
client: args.client,
181+
}),
182+
),
183+
});
184+
185+
let gasLimit = 0n;
186+
if ("paymasterVerificationGasLimit" in userOp) {
187+
// v0.7
188+
gasLimit =
189+
BigInt(userOp.paymasterVerificationGasLimit ?? 0) +
190+
BigInt(userOp.paymasterPostOpGasLimit ?? 0) +
191+
BigInt(userOp.verificationGasLimit ?? 0) +
192+
BigInt(userOp.preVerificationGas ?? 0) +
193+
BigInt(userOp.preVerificationGas ?? 0) +
194+
BigInt(userOp.callGasLimit ?? 0);
195+
} else {
196+
// v0.6
197+
gasLimit =
198+
BigInt(userOp.verificationGasLimit ?? 0) +
199+
BigInt(userOp.preVerificationGas ?? 0) +
200+
BigInt(userOp.callGasLimit ?? 0);
201+
}
202+
203+
const gasCost = gasLimit * userOp.maxFeePerGas;
204+
205+
return {
206+
ether: toEther(gasCost),
207+
wei: gasCost,
208+
};
209+
}
210+
114211
/**
115212
* Get the gas fees of a user operation.
116213
* @param args - The options for getting the gas price of a user operation.

packages/thirdweb/src/wallets/smart/lib/userop.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,32 @@ export async function createAndSignUserOp(options: {
706706
smartWalletOptions: SmartWalletOptions;
707707
waitForDeployment?: boolean;
708708
isDeployedOverride?: boolean;
709+
}) {
710+
const unsignedUserOp = await prepareUserOp({
711+
transactions: options.transactions,
712+
adminAccount: options.adminAccount,
713+
client: options.client,
714+
smartWalletOptions: options.smartWalletOptions,
715+
waitForDeployment: options.waitForDeployment,
716+
isDeployedOverride: options.isDeployedOverride,
717+
});
718+
const signedUserOp = await signUserOp({
719+
client: options.client,
720+
chain: options.smartWalletOptions.chain,
721+
adminAccount: options.adminAccount,
722+
entrypointAddress: options.smartWalletOptions.overrides?.entrypointAddress,
723+
userOp: unsignedUserOp,
724+
});
725+
return signedUserOp;
726+
}
727+
728+
export async function prepareUserOp(options: {
729+
transactions: PreparedTransaction[];
730+
adminAccount: Account;
731+
client: ThirdwebClient;
732+
smartWalletOptions: SmartWalletOptions;
733+
waitForDeployment?: boolean;
734+
isDeployedOverride?: boolean;
709735
}) {
710736
const config = options.smartWalletOptions;
711737
const factoryContract = getContract({
@@ -756,7 +782,7 @@ export async function createAndSignUserOp(options: {
756782
});
757783
}
758784

759-
const unsignedUserOp = await createUnsignedUserOp({
785+
return createUnsignedUserOp({
760786
transaction: executeTx,
761787
factoryContract,
762788
accountContract,
@@ -766,14 +792,6 @@ export async function createAndSignUserOp(options: {
766792
waitForDeployment: options.waitForDeployment,
767793
isDeployedOverride: options.isDeployedOverride,
768794
});
769-
const signedUserOp = await signUserOp({
770-
client: options.client,
771-
chain: config.chain,
772-
adminAccount: options.adminAccount,
773-
entrypointAddress: config.overrides?.entrypointAddress,
774-
userOp: unsignedUserOp,
775-
});
776-
return signedUserOp;
777795
}
778796

779797
async function waitForAccountDeployed(accountContract: ThirdwebContract) {

packages/thirdweb/src/wallets/smart/smart-wallet-integration-v07.test.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
} from "../../exports/extensions/erc4337.js";
1717
import { claimTo } from "../../extensions/erc1155/drops/write/claimTo.js";
1818
import { setContractURI } from "../../extensions/marketplace/__generated__/IMarketplace/write/setContractURI.js";
19-
import { estimateGasCost } from "../../transaction/actions/estimate-gas-cost.js";
2019
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
2120
import { sendBatchTransaction } from "../../transaction/actions/send-batch-transaction.js";
2221
import { waitForReceipt } from "../../transaction/actions/wait-for-tx-receipt.js";
@@ -27,6 +26,7 @@ import { hashTypedData } from "../../utils/hashing/hashTypedData.js";
2726
import { sleep } from "../../utils/sleep.js";
2827
import type { Account, Wallet } from "../interfaces/wallet.js";
2928
import { generateAccount } from "../utils/generateAccount.js";
29+
import { estimateUserOpGasCost } from "./lib/bundler.js";
3030
import { predictSmartAccountAddress } from "./lib/calls.js";
3131
import { DEFAULT_ACCOUNT_FACTORY_V0_7 } from "./lib/constants.js";
3232
import {
@@ -87,6 +87,28 @@ describe.sequential(
8787
expect(predictedAddress).toEqual(smartWalletAddress);
8888
});
8989

90+
it("can estimate gas cost", async () => {
91+
const gasCost = await estimateUserOpGasCost({
92+
transactions: [
93+
claimTo({
94+
contract,
95+
quantity: 1n,
96+
to: smartWalletAddress,
97+
tokenId: 0n,
98+
}),
99+
],
100+
adminAccount: personalAccount,
101+
client: TEST_CLIENT,
102+
smartWalletOptions: {
103+
chain,
104+
sponsorGas: true,
105+
factoryAddress: DEFAULT_ACCOUNT_FACTORY_V0_7,
106+
},
107+
});
108+
console.log(gasCost);
109+
expect(gasCost.ether).not.toBe("0");
110+
});
111+
90112
it("can sign a msg", async () => {
91113
const signature = await smartAccount.signMessage({
92114
message: "hello world",
@@ -202,19 +224,6 @@ describe.sequential(
202224
expect(isDeployed).toEqual(true);
203225
});
204226

205-
it("can estimate a tx", async () => {
206-
const estimates = await estimateGasCost({
207-
transaction: claimTo({
208-
contract,
209-
quantity: 1n,
210-
to: smartWalletAddress,
211-
tokenId: 0n,
212-
}),
213-
account: smartAccount,
214-
});
215-
expect(estimates.wei.toString()).not.toBe("0");
216-
});
217-
218227
it("can execute a batched tx", async () => {
219228
const tx = await sendBatchTransaction({
220229
account: smartAccount,

packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { hashTypedData } from "../../utils/hashing/hashTypedData.js";
2929
import { sleep } from "../../utils/sleep.js";
3030
import type { Account, Wallet } from "../interfaces/wallet.js";
3131
import { generateAccount } from "../utils/generateAccount.js";
32+
import { estimateUserOpGasCost } from "./lib/bundler.js";
3233
import { predictSmartAccountAddress } from "./lib/calls.js";
3334
import { deploySmartAccount } from "./lib/signing.js";
3435
import { smartWallet } from "./smart-wallet.js";
@@ -74,6 +75,27 @@ describe.runIf(process.env.TW_SECRET_KEY).sequential(
7475
});
7576
});
7677

78+
it("can estimate gas cost", async () => {
79+
const gasCost = await estimateUserOpGasCost({
80+
transactions: [
81+
claimTo({
82+
contract,
83+
quantity: 1n,
84+
to: smartWalletAddress,
85+
tokenId: 0n,
86+
}),
87+
],
88+
adminAccount: personalAccount,
89+
client: TEST_CLIENT,
90+
smartWalletOptions: {
91+
chain,
92+
sponsorGas: true,
93+
},
94+
});
95+
console.log(gasCost);
96+
expect(gasCost.ether).not.toBe("0");
97+
});
98+
7799
it("can connect", async () => {
78100
expect(smartWalletAddress).toHaveLength(42);
79101
const predictedAddress = await predictSmartAccountAddress({

0 commit comments

Comments
 (0)