Skip to content

Commit bb58424

Browse files
expose estimate APIs
1 parent b6f72c5 commit bb58424

File tree

4 files changed

+207
-48
lines changed

4 files changed

+207
-48
lines changed

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

Lines changed: 158 additions & 23 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,7 @@ 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";
2830

2931
export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
3032
protected config: SmartWalletConfig;
@@ -165,6 +167,8 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
165167
return restrictions.approvedCallTargets.includes(transaction.getTarget());
166168
}
167169

170+
/// PREPARED TRANSACTIONS
171+
168172
/**
169173
* Send a single transaction without waiting for confirmations
170174
* @param transactions
@@ -199,18 +203,11 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
199203
throw new Error("Personal wallet not connected");
200204
}
201205
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-
);
206+
const batchData = await this.prepareBatchTx(transactions);
210207
return await signer.sendTransaction(
211208
{
212209
to: await signer.getAddress(),
213-
data: callData,
210+
data: batchData.encode(),
214211
value: 0,
215212
},
216213
true, // batched tx flag
@@ -232,6 +229,8 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
232229
};
233230
}
234231

232+
/// RAW TRANSACTIONS
233+
235234
async sendRaw(
236235
transaction: utils.Deferrable<providers.TransactionRequest>,
237236
): Promise<providers.TransactionResponse> {
@@ -259,23 +258,11 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
259258
throw new Error("Personal wallet not connected");
260259
}
261260
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-
);
261+
const batchData = await this.prepareBatchRaw(transactions);
275262
return signer.sendTransaction(
276263
{
277264
to: await signer.getAddress(),
278-
data: callData,
265+
data: batchData.encode(),
279266
value: 0,
280267
},
281268
true, // batched tx flag
@@ -292,6 +279,75 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
292279
};
293280
}
294281

282+
/// ESTIMATION
283+
284+
async estimate(transaction: Transaction) {
285+
if (!this.accountApi) {
286+
throw new Error("Personal wallet not connected");
287+
}
288+
console.log("single", transaction.getTarget(), transaction.encode().length);
289+
return this.estimateTx(
290+
{
291+
target: transaction.getTarget(),
292+
data: transaction.encode(),
293+
value: await transaction.getValue(),
294+
},
295+
false,
296+
);
297+
}
298+
299+
async estimateRaw(
300+
transaction: utils.Deferrable<providers.TransactionRequest>,
301+
) {
302+
if (!this.accountApi) {
303+
throw new Error("Personal wallet not connected");
304+
}
305+
const tx = await ethers.utils.resolveProperties(transaction);
306+
return this.estimateTx(
307+
{
308+
target: tx.to || AddressZero,
309+
data: tx.data?.toString() || "",
310+
value: tx.value || BigNumber.from(0),
311+
},
312+
false,
313+
);
314+
}
315+
316+
async estimateBatch(transactions: Transaction<any>[]) {
317+
if (!this.accountApi) {
318+
throw new Error("Personal wallet not connected");
319+
}
320+
const batch = await this.prepareBatchTx(transactions);
321+
console.log("batch", batch.getTarget(), batch.encode().length);
322+
return this.estimateTx(
323+
{
324+
target: batch.getTarget(),
325+
data: batch.encode(),
326+
value: await batch.getValue(),
327+
},
328+
true,
329+
);
330+
}
331+
332+
async estimateBatchRaw(
333+
transactions: utils.Deferrable<providers.TransactionRequest>[],
334+
) {
335+
if (!this.accountApi) {
336+
throw new Error("Personal wallet not connected");
337+
}
338+
const batch = await this.prepareBatchRaw(transactions);
339+
return this.estimateTx(
340+
{
341+
target: batch.getTarget(),
342+
data: batch.encode(),
343+
value: await batch.getValue(),
344+
},
345+
true,
346+
);
347+
}
348+
349+
//// DEPLOYMENT
350+
295351
/**
296352
* Manually deploy the smart wallet contract. If already deployed this will throw an error.
297353
* Note that this is not necessary as the smart wallet will be deployed automatically on the first transaction the user makes.
@@ -334,6 +390,8 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
334390
}
335391
}
336392

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

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> {

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

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
CeloBaklavaTestnet,
2020
Celo,
2121
} from "@thirdweb-dev/chains";
22-
import { getDynamicFeeData } from "@thirdweb-dev/sdk";
22+
import { Transaction, getDynamicFeeData } from "@thirdweb-dev/sdk";
2323

2424
export interface BaseApiParams {
2525
provider: providers.Provider;
@@ -103,11 +103,11 @@ export abstract class BaseAccountAPI {
103103
* @param value
104104
* @param data
105105
*/
106-
abstract encodeExecute(
106+
abstract prepareExecute(
107107
target: string,
108108
value: BigNumberish,
109109
data: string,
110-
): Promise<string>;
110+
): Promise<Transaction<any>>;
111111

112112
/**
113113
* sign a userOp's hash (userOpHash).
@@ -188,21 +188,14 @@ export abstract class BaseAccountAPI {
188188
detailsForUserOp: TransactionDetailsForUserOp,
189189
batched: boolean,
190190
): Promise<{ callData: string; callGasLimit: BigNumber }> {
191-
function parseNumber(a: any): BigNumber | null {
192-
if (!a || a === "") {
193-
return null;
194-
}
195-
return BigNumber.from(a.toString());
196-
}
197-
198191
const value = parseNumber(detailsForUserOp.value) ?? BigNumber.from(0);
199192
const callData = batched
200193
? detailsForUserOp.data
201-
: await this.encodeExecute(
194+
: await this.prepareExecute(
202195
detailsForUserOp.target,
203196
value,
204197
detailsForUserOp.data,
205-
);
198+
).then((tx) => tx.encode());
206199

207200
let callGasLimit;
208201
const isPhantom = await this.checkAccountPhantom();
@@ -213,6 +206,7 @@ export abstract class BaseAccountAPI {
213206
to: detailsForUserOp.target,
214207
data: detailsForUserOp.data,
215208
});
209+
console.log("estimated gas limit", callGasLimit.toString());
216210
// add 20% overhead for entrypoint checks
217211
callGasLimit = callGasLimit.mul(120).div(100);
218212
// if the estimation is too low, we use a fixed value of 500k
@@ -429,3 +423,10 @@ export abstract class BaseAccountAPI {
429423
return null;
430424
}
431425
}
426+
427+
function parseNumber(a: any): BigNumber | null {
428+
if (!a || a === "") {
429+
return null;
430+
}
431+
return BigNumber.from(a.toString());
432+
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,30 @@ export class SmartWallet
181181
return connector.executeRaw(transaction);
182182
}
183183

184+
async estimate(transaction: Transaction<any>) {
185+
const connector = await this.getConnector();
186+
return connector.estimate(transaction);
187+
}
188+
189+
async estimateBatch(transactions: Transaction<any>[]) {
190+
const connector = await this.getConnector();
191+
return connector.estimateBatch(transactions);
192+
}
193+
194+
async estimateRaw(
195+
transactions: utils.Deferrable<providers.TransactionRequest>,
196+
) {
197+
const connector = await this.getConnector();
198+
return connector.estimateRaw(transactions);
199+
}
200+
201+
async estimateBatchRaw(
202+
transactions: utils.Deferrable<providers.TransactionRequest>[],
203+
) {
204+
const connector = await this.getConnector();
205+
return connector.estimateBatchRaw(transactions);
206+
}
207+
184208
/**
185209
* Send multiple raw transaction in a batch without waiting for confirmations
186210
* @param transaction

0 commit comments

Comments
 (0)