Skip to content

Commit 4c32019

Browse files
joaquim-vergesIDubuque
authored andcommitted
[SmartWallet] Expose new estimation functions for smart wallet transactions (#1856)
1 parent d768edc commit 4c32019

File tree

7 files changed

+305
-79
lines changed

7 files changed

+305
-79
lines changed

.changeset/cuddly-kings-listen.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@thirdweb-dev/wallets": patch
3+
---
4+
5+
Expose function to estimate SmartWallet transactions, handling pre and post deployment state
6+
7+
```ts
8+
const cost = smartWallet.estimate(preparedTx);
9+
const costBatch = smartWallet.estimateBatch(preparedTxs);
10+
```
11+
12+
Also works with raw transactions
13+
14+
```ts
15+
const cost = smartWallet.estimateRaw(rawTx);
16+
const costBatch = smartWallet.estimateBatchRaw(rawTxs);
17+
```

packages/wallets/src/evm/connectors/smart-wallet/index.ts

Lines changed: 172 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { ERC4337EthersSigner } from "./lib/erc4337-signer";
1616
import { BigNumber, ethers, providers, utils } from "ethers";
1717
import {
1818
getChainProvider,
19+
getGasPrice,
1920
SignerPermissionsInput,
2021
SignerWithPermissions,
2122
SmartContract,
@@ -25,6 +26,8 @@ import {
2526
} from "@thirdweb-dev/sdk";
2627
import { AccountAPI } from "./lib/account";
2728
import { AddressZero } from "@account-abstraction/utils";
29+
import { TransactionDetailsForUserOp } from "./lib/transaction-details";
30+
import { BatchData } from "./lib/base-api";
2831

2932
export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
3033
protected config: SmartWalletConfig;
@@ -165,6 +168,8 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
165168
return restrictions.approvedCallTargets.includes(transaction.getTarget());
166169
}
167170

171+
/// PREPARED TRANSACTIONS
172+
168173
/**
169174
* Send a single transaction without waiting for confirmations
170175
* @param transactions
@@ -199,21 +204,14 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
199204
throw new Error("Personal wallet not connected");
200205
}
201206
const signer = await this.getSigner();
202-
const targets = transactions.map((tx) => tx.getTarget());
203-
const data = transactions.map((tx) => tx.encode());
204-
const values = await Promise.all(transactions.map((tx) => tx.getValue()));
205-
const callData = await this.accountApi.encodeExecuteBatch(
206-
targets,
207-
values,
208-
data,
209-
);
207+
const { tx, batchData } = await this.prepareBatchTx(transactions);
210208
return await signer.sendTransaction(
211209
{
212210
to: await signer.getAddress(),
213-
data: callData,
211+
data: tx.encode(),
214212
value: 0,
215213
},
216-
true, // batched tx flag
214+
batchData,
217215
);
218216
}
219217

@@ -232,6 +230,8 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
232230
};
233231
}
234232

233+
/// RAW TRANSACTIONS
234+
235235
async sendRaw(
236236
transaction: utils.Deferrable<providers.TransactionRequest>,
237237
): Promise<providers.TransactionResponse> {
@@ -259,26 +259,14 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
259259
throw new Error("Personal wallet not connected");
260260
}
261261
const signer = await this.getSigner();
262-
const resolvedTxs = await Promise.all(
263-
transactions.map((transaction) =>
264-
ethers.utils.resolveProperties(transaction),
265-
),
266-
);
267-
const targets = resolvedTxs.map((tx) => tx.to || AddressZero);
268-
const data = resolvedTxs.map((tx) => tx.data || "0x");
269-
const values = resolvedTxs.map((tx) => tx.value || BigNumber.from(0));
270-
const callData = await this.accountApi.encodeExecuteBatch(
271-
targets,
272-
values,
273-
data,
274-
);
262+
const batch = await this.prepareBatchRaw(transactions);
275263
return signer.sendTransaction(
276264
{
277265
to: await signer.getAddress(),
278-
data: callData,
266+
data: batch.tx.encode(),
279267
value: 0,
280268
},
281-
true, // batched tx flag
269+
batch.batchData, // batched tx flag
282270
);
283271
}
284272

@@ -292,6 +280,67 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
292280
};
293281
}
294282

283+
/// ESTIMATION
284+
285+
async estimate(transaction: Transaction) {
286+
if (!this.accountApi) {
287+
throw new Error("Personal wallet not connected");
288+
}
289+
return this.estimateTx({
290+
target: transaction.getTarget(),
291+
data: transaction.encode(),
292+
value: await transaction.getValue(),
293+
});
294+
}
295+
296+
async estimateRaw(
297+
transaction: utils.Deferrable<providers.TransactionRequest>,
298+
) {
299+
if (!this.accountApi) {
300+
throw new Error("Personal wallet not connected");
301+
}
302+
const tx = await ethers.utils.resolveProperties(transaction);
303+
return this.estimateTx({
304+
target: tx.to || AddressZero,
305+
data: tx.data?.toString() || "",
306+
value: tx.value || BigNumber.from(0),
307+
});
308+
}
309+
310+
async estimateBatch(transactions: Transaction<any>[]) {
311+
if (!this.accountApi) {
312+
throw new Error("Personal wallet not connected");
313+
}
314+
const { tx, batchData } = await this.prepareBatchTx(transactions);
315+
return this.estimateTx(
316+
{
317+
target: tx.getTarget(),
318+
data: tx.encode(),
319+
value: await tx.getValue(),
320+
},
321+
batchData,
322+
);
323+
}
324+
325+
async estimateBatchRaw(
326+
transactions: utils.Deferrable<providers.TransactionRequest>[],
327+
) {
328+
if (!this.accountApi) {
329+
throw new Error("Personal wallet not connected");
330+
}
331+
const { tx, batchData } = await this.prepareBatchRaw(transactions);
332+
return this.estimateTx(
333+
{
334+
target: tx.getTarget(),
335+
data: tx.encode(),
336+
value: await tx.getValue(),
337+
},
338+
batchData,
339+
);
340+
}
341+
342+
//// DEPLOYMENT
343+
295344
/**
296345
* Manually deploy the smart wallet contract. If already deployed this will throw an error.
297346
* Note that this is not necessary as the smart wallet will be deployed automatically on the first transaction the user makes.
@@ -301,16 +350,16 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
301350
if (!this.accountApi) {
302351
throw new Error("Personal wallet not connected");
303352
}
304-
if (await this.accountApi.isAcountDeployed()) {
305-
throw new Error("Smart wallet already deployed");
306-
}
307353
const signer = await this.getSigner();
308354
const tx = await signer.sendTransaction(
309355
{
310356
to: await signer.getAddress(),
311357
data: "0x",
312358
},
313-
true, // batched tx flag to avoid hitting the Router fallback method
359+
{
360+
targets: [],
361+
data: [],
362+
}, // batched tx flag to avoid hitting the Router fallback method
314363
);
315364
const receipt = await tx.wait();
316365
return { receipt };
@@ -334,6 +383,8 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
334383
}
335384
}
336385

386+
//// PERMISSIONS
387+
337388
async grantPermissions(
338389
target: string,
339390
permissions: SignerPermissionsInput,
@@ -472,4 +523,96 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
472523
},
473524
};
474525
}
526+
527+
/// PRIVATE METHODS
528+
529+
private async estimateTx(
530+
tx: TransactionDetailsForUserOp,
531+
batchData?: BatchData,
532+
) {
533+
if (!this.accountApi) {
534+
throw new Error("Personal wallet not connected");
535+
}
536+
let deployGasLimit = BigNumber.from(0);
537+
const [provider, isDeployed] = await Promise.all([
538+
this.getProvider(),
539+
this.isDeployed(),
540+
]);
541+
if (!isDeployed) {
542+
deployGasLimit = await this.estimateDeploymentGasLimit();
543+
}
544+
const [{ callGasLimit: transactionGasLimit }, gasPrice] = await Promise.all(
545+
[
546+
this.accountApi.encodeUserOpCallDataAndGasLimit(tx, batchData),
547+
getGasPrice(provider),
548+
],
549+
);
550+
const transactionCost = transactionGasLimit.mul(gasPrice);
551+
const deployCost = deployGasLimit.mul(gasPrice);
552+
const totalCost = deployCost.add(transactionCost);
553+
554+
return {
555+
ether: utils.formatEther(totalCost),
556+
wei: totalCost,
557+
details: {
558+
deployGasLimit,
559+
transactionGasLimit,
560+
gasPrice,
561+
transactionCost,
562+
deployCost,
563+
totalCost,
564+
},
565+
};
566+
}
567+
568+
private async estimateDeploymentGasLimit() {
569+
if (!this.accountApi) {
570+
throw new Error("Personal wallet not connected");
571+
}
572+
const initCode = await this.accountApi.getInitCode();
573+
const [initGas, verificationGasLimit] = await Promise.all([
574+
this.accountApi.estimateCreationGas(initCode),
575+
this.accountApi.getVerificationGasLimit(),
576+
]);
577+
return BigNumber.from(verificationGasLimit).add(initGas);
578+
}
579+
580+
private async prepareBatchRaw(
581+
transactions: ethers.utils.Deferrable<ethers.providers.TransactionRequest>[],
582+
) {
583+
if (!this.accountApi) {
584+
throw new Error("Personal wallet not connected");
585+
}
586+
const resolvedTxs = await Promise.all(
587+
transactions.map((transaction) =>
588+
ethers.utils.resolveProperties(transaction),
589+
),
590+
);
591+
const targets = resolvedTxs.map((tx) => tx.to || AddressZero);
592+
const data = resolvedTxs.map((tx) => tx.data || "0x");
593+
const values = resolvedTxs.map((tx) => tx.value || BigNumber.from(0));
594+
return {
595+
tx: await this.accountApi.prepareExecuteBatch(targets, values, data),
596+
batchData: {
597+
targets,
598+
data,
599+
},
600+
};
601+
}
602+
603+
private async prepareBatchTx(transactions: Transaction<any>[]) {
604+
if (!this.accountApi) {
605+
throw new Error("Personal wallet not connected");
606+
}
607+
const targets = transactions.map((tx) => tx.getTarget());
608+
const data = transactions.map((tx) => tx.encode());
609+
const values = await Promise.all(transactions.map((tx) => tx.getValue()));
610+
return {
611+
tx: await this.accountApi.prepareExecuteBatch(targets, values, data),
612+
batchData: {
613+
targets,
614+
data,
615+
},
616+
};
617+
}
475618
}

packages/wallets/src/evm/connectors/smart-wallet/lib/account.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { LOCAL_NODE_PKEY, SmartContract, ThirdwebSDK } from "@thirdweb-dev/sdk";
1+
import {
2+
LOCAL_NODE_PKEY,
3+
SmartContract,
4+
ThirdwebSDK,
5+
Transaction,
6+
} from "@thirdweb-dev/sdk";
27
import { BigNumberish, BigNumber, ethers, utils, BytesLike } from "ethers";
38
import { AccountApiParams } from "../types";
49
import { BaseAccountAPI } from "./base-api";
@@ -103,33 +108,27 @@ export class AccountAPI extends BaseAccountAPI {
103108
return this.params.accountInfo.getNonce(accountContract);
104109
}
105110

106-
async encodeExecute(
111+
async prepareExecute(
107112
target: string,
108113
value: BigNumberish,
109114
data: string,
110-
): Promise<string> {
115+
): Promise<Transaction<any>> {
111116
const accountContract = await this.getAccountContract();
112-
const tx = await this.params.accountInfo.execute(
117+
return this.params.accountInfo.execute(
113118
accountContract,
114119
target,
115120
value,
116121
data,
117122
);
118-
return tx.encode();
119123
}
120124

121-
async encodeExecuteBatch(
125+
async prepareExecuteBatch(
122126
targets: string[],
123127
values: BigNumberish[],
124128
datas: BytesLike[],
125-
): Promise<string> {
129+
): Promise<Transaction<any>> {
126130
const accountContract = await this.getAccountContract();
127-
const tx = accountContract.prepare("executeBatch", [
128-
targets,
129-
values,
130-
datas,
131-
]);
132-
return tx.encode();
131+
return accountContract.prepare("executeBatch", [targets, values, datas]);
133132
}
134133

135134
async signUserOpHash(userOpHash: string): Promise<string> {

0 commit comments

Comments
 (0)