Skip to content

Commit 4a5b4b4

Browse files
committed
feat(sdk): adds eagerDeployment option to smart wallets
feat(sdk): add eagerDeployment option to smart wallets
1 parent 7293dfa commit 4a5b4b4

File tree

9 files changed

+377
-72
lines changed

9 files changed

+377
-72
lines changed

.changeset/polite-trains-kick.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Feature: Adds eagerDeployment option for smart accounts that need EIP-1271 signatures.
6+
7+
When setting `eagerDeployment` to `true`, smart accounts will use the legacy behavior of deploying prior to signing a message or typed data.
8+
9+
```ts
10+
const wallet = smartWallet({
11+
chain,
12+
gasless: true,
13+
eagerDeployment: true,
14+
});
15+
```

packages/thirdweb/src/auth/verify-hash.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,7 @@ export async function verifyHash({
129129
try {
130130
const result = await eth_call(rpcRequest, verificationData);
131131
return hexToBool(result);
132-
} catch (err) {
133-
console.error("Error verifying ERC-6492 signature", err);
132+
} catch {
134133
// Some chains do not support the eth_call simulation and will fail, so we fall back to regular EIP1271 validation
135134
const validEip1271 = await verifyEip1271Signature({
136135
hash,
@@ -154,7 +153,7 @@ export async function verifyHash({
154153
}
155154

156155
const EIP_1271_MAGIC_VALUE = "0x1626ba7e";
157-
async function verifyEip1271Signature({
156+
export async function verifyEip1271Signature({
158157
hash,
159158
signature,
160159
contract,
@@ -163,10 +162,14 @@ async function verifyEip1271Signature({
163162
signature: Hex;
164163
contract: ThirdwebContract;
165164
}): Promise<boolean> {
166-
const result = await isValidSignature({
167-
hash,
168-
signature,
169-
contract,
170-
});
171-
return result === EIP_1271_MAGIC_VALUE;
165+
try {
166+
const result = await isValidSignature({
167+
hash,
168+
signature,
169+
contract,
170+
});
171+
return result === EIP_1271_MAGIC_VALUE;
172+
} catch {
173+
return false;
174+
}
172175
}

packages/thirdweb/src/wallets/create-wallet.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,22 @@ import { createWalletEmitter } from "./wallet-emitter.js";
116116
*
117117
* [View Coinbase wallet creation options](https://portal.thirdweb.com/references/typescript/v5/CoinbaseWalletCreationOptions)
118118
*
119+
* ## Connecting with a smart wallet
120+
*
121+
* ```ts
122+
* import { createWallet } from "thirdweb/wallets";
123+
*
124+
* const wallet = createWallet("smart", {
125+
* chain: sepolia,
126+
* sponsorGas: true,
127+
* });
128+
*
129+
* const account = await wallet.connect({
130+
* client,
131+
* personalAccount, // pass the admin account
132+
* });
133+
* ```
134+
*
119135
* @wallet
120136
*/
121137
export function createWallet<const ID extends WalletId>(

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,9 @@ async function createSmartAccount(
277277
});
278278
}
279279

280-
const { deployAndSignMessage } = await import("./lib/signing.js");
281-
return deployAndSignMessage({
280+
const { smartAccountSignMessage } = await import("./lib/signing.js");
281+
return smartAccountSignMessage({
282+
smartAccount: account,
282283
accountContract,
283284
factoryContract: options.factoryContract,
284285
options,
@@ -298,8 +299,9 @@ async function createSmartAccount(
298299
});
299300
}
300301

301-
const { deployAndSignTypedData } = await import("./lib/signing.js");
302-
return deployAndSignTypedData({
302+
const { smartAccountSignTypedData } = await import("./lib/signing.js");
303+
return smartAccountSignTypedData({
304+
smartAccount: account,
303305
accountContract,
304306
factoryContract: options.factoryContract,
305307
options,

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

Lines changed: 157 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,41 @@
11
import type * as ox__TypedData from "ox/TypedData";
22
import { serializeErc6492Signature } from "../../../auth/serialize-erc6492-signature.js";
3-
import { verifyHash } from "../../../auth/verify-hash.js";
3+
import {
4+
verifyEip1271Signature,
5+
verifyHash,
6+
} from "../../../auth/verify-hash.js";
7+
import type { Chain } from "../../../chains/types.js";
8+
import type { ThirdwebClient } from "../../../client/client.js";
49
import {
510
type ThirdwebContract,
611
getContract,
712
} from "../../../contract/contract.js";
813
import { encode } from "../../../transaction/actions/encode.js";
914
import { readContract } from "../../../transaction/read-contract.js";
1015
import { encodeAbiParameters } from "../../../utils/abi/encodeAbiParameters.js";
16+
import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed.js";
1117
import type { Hex } from "../../../utils/encoding/hex.js";
1218
import { hashMessage } from "../../../utils/hashing/hashMessage.js";
1319
import { hashTypedData } from "../../../utils/hashing/hashTypedData.js";
1420
import type { SignableMessage } from "../../../utils/types.js";
21+
import type { Account } from "../../../wallets/interfaces/wallet.js";
1522
import type { SmartAccountOptions } from "../types.js";
1623
import { prepareCreateAccount } from "./calls.js";
1724

18-
export async function deployAndSignMessage({
25+
/**
26+
* If the account is already deployed, generate an ERC-1271 signature.
27+
* If the account is not deployed, generate an ERC-6492 signature unless otherwise specified.
28+
*
29+
* @internal
30+
*/
31+
export async function smartAccountSignMessage({
32+
smartAccount,
1933
accountContract,
2034
factoryContract,
2135
options,
2236
message,
2337
}: {
38+
smartAccount: Account;
2439
accountContract: ThirdwebContract;
2540
factoryContract: ThirdwebContract;
2641
options: SmartAccountOptions;
@@ -55,48 +70,73 @@ export async function deployAndSignMessage({
5570
sig = await options.personalAccount.signMessage({ message });
5671
}
5772

58-
const deployTx = prepareCreateAccount({
59-
factoryContract,
60-
adminAddress: options.personalAccount.address,
61-
accountSalt: options.overrides?.accountSalt,
62-
createAccountOverride: options.overrides?.createAccount,
63-
});
64-
if (!deployTx) {
65-
throw new Error("Create account override not provided");
73+
if (options.eagerDeployment) {
74+
await forceDeploySmartAccount({
75+
chain: options.chain,
76+
client: options.client,
77+
smartAccount,
78+
accountContract,
79+
});
80+
await confirmContractDeployment({
81+
accountContract,
82+
});
6683
}
67-
const initCode = await encode(deployTx);
68-
const erc6492Sig = serializeErc6492Signature({
69-
address: factoryContract.address,
70-
data: initCode,
71-
signature: sig,
72-
});
7384

74-
// check if the signature is valid
75-
const isValid = await verifyHash({
76-
hash: originalMsgHash,
77-
signature: erc6492Sig,
78-
address: accountContract.address,
79-
chain: accountContract.chain,
80-
client: accountContract.client,
81-
});
85+
const isDeployed = await isContractDeployed(accountContract);
86+
if (isDeployed) {
87+
const isValid = await verifyEip1271Signature({
88+
hash: originalMsgHash,
89+
signature: sig,
90+
contract: accountContract,
91+
});
92+
if (isValid) {
93+
return sig;
94+
}
95+
throw new Error("Failed to verify signature");
96+
} else {
97+
const deployTx = prepareCreateAccount({
98+
factoryContract,
99+
adminAddress: options.personalAccount.address,
100+
accountSalt: options.overrides?.accountSalt,
101+
createAccountOverride: options.overrides?.createAccount,
102+
});
103+
if (!deployTx) {
104+
throw new Error("Create account override not provided");
105+
}
106+
const initCode = await encode(deployTx);
107+
const erc6492Sig = serializeErc6492Signature({
108+
address: factoryContract.address,
109+
data: initCode,
110+
signature: sig,
111+
});
82112

83-
if (isValid) {
84-
return erc6492Sig;
113+
// check if the signature is valid
114+
const isValid = await verifyHash({
115+
hash: originalMsgHash,
116+
signature: erc6492Sig,
117+
address: accountContract.address,
118+
chain: accountContract.chain,
119+
client: accountContract.client,
120+
});
121+
122+
if (isValid) {
123+
return erc6492Sig;
124+
}
125+
throw new Error("Unable to verify ERC-6492 signature after signing.");
85126
}
86-
throw new Error(
87-
"Unable to verify signature on smart account, please make sure the admin wallet has permissions and the signature is valid.",
88-
);
89127
}
90128

91-
export async function deployAndSignTypedData<
129+
export async function smartAccountSignTypedData<
92130
const typedData extends ox__TypedData.TypedData | Record<string, unknown>,
93131
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
94132
>({
133+
smartAccount,
95134
accountContract,
96135
factoryContract,
97136
options,
98137
typedData,
99138
}: {
139+
smartAccount: Account;
100140
accountContract: ThirdwebContract;
101141
factoryContract: ThirdwebContract;
102142
options: SmartAccountOptions;
@@ -142,37 +182,62 @@ export async function deployAndSignTypedData<
142182
sig = await options.personalAccount.signTypedData(typedData);
143183
}
144184

145-
const deployTx = prepareCreateAccount({
146-
factoryContract,
147-
adminAddress: options.personalAccount.address,
148-
accountSalt: options.overrides?.accountSalt,
149-
createAccountOverride: options.overrides?.createAccount,
150-
});
151-
if (!deployTx) {
152-
throw new Error("Create account override not provided");
185+
if (options.eagerDeployment) {
186+
await forceDeploySmartAccount({
187+
chain: options.chain,
188+
client: options.client,
189+
smartAccount,
190+
accountContract,
191+
});
192+
await confirmContractDeployment({
193+
accountContract,
194+
});
153195
}
154-
const initCode = await encode(deployTx);
155-
const erc6492Sig = serializeErc6492Signature({
156-
address: factoryContract.address,
157-
data: initCode,
158-
signature: sig,
159-
});
160196

161-
// check if the signature is valid
162-
const isValid = await verifyHash({
163-
hash: originalMsgHash,
164-
signature: erc6492Sig,
165-
address: accountContract.address,
166-
chain: accountContract.chain,
167-
client: accountContract.client,
168-
});
197+
const isDeployed = await isContractDeployed(accountContract);
198+
if (isDeployed) {
199+
const isValid = await verifyEip1271Signature({
200+
hash: originalMsgHash,
201+
signature: sig,
202+
contract: accountContract,
203+
});
204+
if (isValid) {
205+
return sig;
206+
}
207+
throw new Error("Failed to verify signature");
208+
} else {
209+
const deployTx = prepareCreateAccount({
210+
factoryContract,
211+
adminAddress: options.personalAccount.address,
212+
accountSalt: options.overrides?.accountSalt,
213+
createAccountOverride: options.overrides?.createAccount,
214+
});
215+
if (!deployTx) {
216+
throw new Error("Create account override not provided");
217+
}
218+
const initCode = await encode(deployTx);
219+
const erc6492Sig = serializeErc6492Signature({
220+
address: factoryContract.address,
221+
data: initCode,
222+
signature: sig,
223+
});
169224

170-
if (isValid) {
171-
return erc6492Sig;
225+
// check if the signature is valid
226+
const isValid = await verifyHash({
227+
hash: originalMsgHash,
228+
signature: erc6492Sig,
229+
address: accountContract.address,
230+
chain: accountContract.chain,
231+
client: accountContract.client,
232+
});
233+
234+
if (isValid) {
235+
return erc6492Sig;
236+
}
237+
throw new Error(
238+
"Unable to verify signature on smart account, please make sure the admin wallet has permissions and the signature is valid.",
239+
);
172240
}
173-
throw new Error(
174-
"Unable to verify signature on smart account, please make sure the admin wallet has permissions and the signature is valid.",
175-
);
176241
}
177242

178243
export async function confirmContractDeployment(args: {
@@ -229,3 +294,38 @@ async function checkFor712Factory({
229294
return false;
230295
}
231296
}
297+
298+
/**
299+
* Forces smart account deployment via a dummy transaction
300+
*
301+
* @internal
302+
*/
303+
export async function forceDeploySmartAccount(args: {
304+
smartAccount: Account;
305+
chain: Chain;
306+
client: ThirdwebClient;
307+
accountContract: ThirdwebContract;
308+
}) {
309+
const { chain, client, smartAccount, accountContract } = args;
310+
const isDeployed = await isContractDeployed(accountContract);
311+
if (isDeployed) {
312+
return;
313+
}
314+
315+
const [{ sendTransaction }, { prepareTransaction }] = await Promise.all([
316+
import("../../../transaction/actions/send-transaction.js"),
317+
import("../../../transaction/prepare-transaction.js"),
318+
]);
319+
const dummyTx = prepareTransaction({
320+
client: client,
321+
chain: chain,
322+
to: accountContract.address,
323+
value: 0n,
324+
gas: 50000n, // force gas to avoid simulation error
325+
});
326+
const deployResult = await sendTransaction({
327+
transaction: dummyTx,
328+
account: smartAccount,
329+
});
330+
return deployResult;
331+
}

0 commit comments

Comments
 (0)