Skip to content

Commit 5b0c37a

Browse files
feat: default account factory for smart accounts + switch chain support (#2917)
1 parent d77b6f6 commit 5b0c37a

File tree

14 files changed

+214
-113
lines changed

14 files changed

+214
-113
lines changed

.changeset/brown-bikes-lick.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Factory address is now optional in `accountAbstraction` and `smartWallet` options.
6+
7+
- Defaults to a global permissionless factory deployed on all chains.
8+
- Also enables switching chains for smart wallets, as long as the factory is deployed

packages/thirdweb/src/contract/verification/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ type VerifyContractOptions = {
3333
* @example
3434
* ```ts
3535
* import { getContract } from "thirdweb/contract";
36-
* import { verifyContract } from "thirdweb/contract/verification";
36+
* import { verifyContract } from "thirdweb/contract";
3737
*
3838
* const contract = getContract({ ... });
3939
* const verificationResult = await verifyContract({
@@ -162,7 +162,7 @@ type CheckVerificationStatusOptions = {
162162
* @throws An error if the verification status check fails.
163163
* @example
164164
* ```ts
165-
* import { checkVerificationStatus } from "thirdweb/contract/verification";
165+
* import { checkVerificationStatus } from "thirdweb/contract";
166166
* const verificationStatus = await checkVerificationStatus({
167167
* explorerApiUrl: "https://api.polygonscan.com/api",
168168
* explorerApiKey: "YOUR_API_KEY",

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,7 @@ export type {
77
PaymasterResult,
88
} from "../../wallets/smart/types.js";
99

10-
export { ENTRYPOINT_ADDRESS_v0_6 } from "../../wallets/smart/lib/constants.js";
10+
export {
11+
ENTRYPOINT_ADDRESS_v0_6,
12+
DEFAULT_ACCOUNT_FACTORY,
13+
} from "../../wallets/smart/lib/constants.js";

packages/thirdweb/src/react/web/ui/TransactionButton/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useSendTransactionCore } from "../../../core/hooks/contract/useSendTran
1111
import {
1212
useActiveAccount,
1313
useActiveWallet,
14+
useSwitchActiveWalletChain,
1415
} from "../../../core/hooks/wallets/wallet-hooks.js";
1516
import { Spinner } from "../components/Spinner.js";
1617
import { Button } from "../components/buttons.js";
@@ -105,6 +106,7 @@ export function TransactionButton(props: TransactionButtonProps) {
105106
const account = useActiveAccount();
106107
const wallet = useActiveWallet();
107108
const [isPending, setIsPending] = useState(false);
109+
const switchChain = useSwitchActiveWalletChain();
108110

109111
const sendTransaction = useSendTransactionCore(undefined, gasless);
110112

@@ -123,7 +125,7 @@ export function TransactionButton(props: TransactionButtonProps) {
123125
const resolvedTx = await transaction();
124126

125127
if (wallet && wallet.getChain()?.id !== resolvedTx.chain.id) {
126-
await wallet?.switchChain(resolvedTx.chain);
128+
await switchChain(resolvedTx.chain);
127129
}
128130

129131
const result = await sendTransaction.mutateAsync(resolvedTx);

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

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ import type {
99
CreateWalletArgs,
1010
InjectedConnectOptions,
1111
WalletAutoConnectionOption,
12+
WalletConnectionOption,
1213
WalletId,
1314
} from "./wallet-types.js";
1415

1516
import { trackConnect } from "../analytics/track.js";
17+
import { getContract } from "../contract/contract.js";
18+
import { isContractDeployed } from "../exports/utils.js";
1619
import { COINBASE } from "./constants.js";
20+
import { DEFAULT_ACCOUNT_FACTORY } from "./smart/lib/constants.js";
1721
import type { WCConnectOptions } from "./wallet-connect/types.js";
1822
import { createWalletEmitter } from "./wallet-emitter.js";
1923

@@ -300,9 +304,10 @@ export function walletConnect() {
300304
export function smartWallet(
301305
createOptions: CreateWalletArgs<"smart">[1],
302306
): Wallet<"smart"> {
303-
const emitter = createWalletEmitter<"inApp">();
307+
const emitter = createWalletEmitter<"smart">();
304308
let account: Account | undefined = undefined;
305309
let chain: Chain | undefined = undefined;
310+
let lastConnectOptions: WalletConnectionOption<"smart"> | undefined;
306311

307312
const _smartWallet: Wallet<"smart"> = {
308313
id: "smart",
@@ -318,6 +323,7 @@ export function smartWallet(
318323
createOptions,
319324
);
320325
// set the states
326+
lastConnectOptions = options;
321327
account = connectedAccount;
322328
chain = connectedChain;
323329
trackConnect({
@@ -336,6 +342,7 @@ export function smartWallet(
336342
createOptions,
337343
);
338344
// set the states
345+
lastConnectOptions = options;
339346
account = connectedAccount;
340347
chain = connectedChain;
341348
trackConnect({
@@ -344,17 +351,42 @@ export function smartWallet(
344351
walletAddress: account.address,
345352
});
346353
// return account
354+
emitter.emit("accountChanged", account);
347355
return account;
348356
},
349357
disconnect: async () => {
350358
account = undefined;
351359
chain = undefined;
352-
emitter.emit("disconnect", undefined);
353360
const { disconnectSmartWallet } = await import("./smart/index.js");
354361
await disconnectSmartWallet(_smartWallet);
362+
emitter.emit("disconnect", undefined);
355363
},
356-
switchChain: async () => {
357-
throw new Error("Not implemented yet");
364+
switchChain: async (newChain: Chain) => {
365+
if (!lastConnectOptions) {
366+
throw new Error("Cannot switch chain without a previous connection");
367+
}
368+
// check if factory is deployed
369+
const factory = getContract({
370+
address: createOptions.factoryAddress || DEFAULT_ACCOUNT_FACTORY,
371+
chain: newChain,
372+
client: lastConnectOptions.client,
373+
});
374+
const isDeployed = await isContractDeployed(factory);
375+
if (!isDeployed) {
376+
throw new Error(
377+
`Factory contract not deployed on chain: ${newChain.id}`,
378+
);
379+
}
380+
const { connectSmartWallet } = await import("./smart/index.js");
381+
const [connectedAccount, connectedChain] = await connectSmartWallet(
382+
_smartWallet,
383+
{ ...lastConnectOptions, chain: newChain },
384+
createOptions,
385+
);
386+
// set the states
387+
account = connectedAccount;
388+
chain = connectedChain;
389+
emitter.emit("chainChanged", newChain);
358390
},
359391
};
360392

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,24 @@ export function createConnectionManager(storage: AsyncStorage) {
218218
throw new Error("wallet does not support switching chains");
219219
}
220220

221-
await wallet.switchChain(chain);
221+
if (wallet.id === "smart") {
222+
// also switch personal wallet
223+
const personalWalletId = await getStoredActiveWalletId(storage);
224+
if (personalWalletId) {
225+
const personalWallet = connectedWallets
226+
.getValue()
227+
.find((w) => w.id === personalWalletId);
228+
if (personalWallet) {
229+
await personalWallet.switchChain(chain);
230+
}
231+
}
232+
await wallet.switchChain(chain);
233+
// reset the active wallet as switch chain recreates a new smart account
234+
handleSetActiveWallet(wallet);
235+
} else {
236+
await wallet.switchChain(chain);
237+
}
238+
222239
// for wallets that dont implement events, just set it manually
223240
activeWalletChainStore.setValue(wallet.getChain());
224241
};

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

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type {
55
TypedDataDomain,
66
} from "viem";
77
import type { Chain } from "../../chains/types.js";
8-
import type { ThirdwebClient } from "../../client/client.js";
98
import { type ThirdwebContract, getContract } from "../../contract/contract.js";
109
import type { WaitForReceiptOptions } from "../../transaction/actions/wait-for-tx-receipt.js";
1110
import type { PreparedTransaction } from "../../transaction/prepare-transaction.js";
@@ -26,8 +25,9 @@ import {
2625
prepareBatchExecute,
2726
prepareExecute,
2827
} from "./lib/calls.js";
28+
import { DEFAULT_ACCOUNT_FACTORY } from "./lib/constants.js";
2929
import { createUnsignedUserOp, signUserOp } from "./lib/userop.js";
30-
import type { SmartWalletOptions } from "./types.js";
30+
import type { SmartAccountOptions } from "./types.js";
3131

3232
/**
3333
* We can get the personal account for given smart account but not the other way around - this map gives us the reverse lookup
@@ -55,8 +55,8 @@ export async function connectSmartWallet(
5555
}
5656

5757
const options = creationOptions;
58+
const factoryAddress = options.factoryAddress ?? DEFAULT_ACCOUNT_FACTORY;
5859
const chain = connectChain ?? options.chain;
59-
const factoryAddress = options.factoryAddress;
6060

6161
const factoryContract = getContract({
6262
client: client,
@@ -82,8 +82,13 @@ export async function connectSmartWallet(
8282
chain,
8383
});
8484

85+
const sponsorGas =
86+
"gasless" in options ? options.gasless : options.sponsorGas;
87+
8588
const account = await createSmartAccount({
8689
...options,
90+
chain,
91+
sponsorGas,
8792
personalAccount,
8893
accountContract,
8994
factoryContract,
@@ -112,14 +117,9 @@ export async function disconnectSmartWallet(
112117
}
113118

114119
async function createSmartAccount(
115-
options: SmartWalletOptions & {
116-
personalAccount: Account;
117-
factoryContract: ThirdwebContract;
118-
accountContract: ThirdwebContract;
119-
client: ThirdwebClient;
120-
},
120+
options: SmartAccountOptions,
121121
): Promise<Account> {
122-
const { accountContract, factoryContract } = options;
122+
const { accountContract } = options;
123123
const account = {
124124
address: accountContract.address,
125125
async sendTransaction(transaction: SendTransactionOption) {
@@ -129,8 +129,6 @@ async function createSmartAccount(
129129
transaction,
130130
});
131131
return _sendUserOp({
132-
factoryContract,
133-
accountContract,
134132
executeTx,
135133
options,
136134
});
@@ -142,8 +140,6 @@ async function createSmartAccount(
142140
transactions,
143141
});
144142
return _sendUserOp({
145-
factoryContract,
146-
accountContract,
147143
executeTx,
148144
options,
149145
});
@@ -321,7 +317,7 @@ async function createSmartAccount(
321317
}
322318

323319
async function _deployAccount(args: {
324-
options: SmartWalletOptions & { client: ThirdwebClient };
320+
options: SmartAccountOptions;
325321
account: Account;
326322
accountContract: ThirdwebContract;
327323
}) {
@@ -344,18 +340,11 @@ async function _deployAccount(args: {
344340
}
345341

346342
async function _sendUserOp(args: {
347-
factoryContract: ThirdwebContract;
348-
accountContract: ThirdwebContract;
349343
executeTx: PreparedTransaction;
350-
options: SmartWalletOptions & {
351-
personalAccount: Account;
352-
client: ThirdwebClient;
353-
};
344+
options: SmartAccountOptions;
354345
}): Promise<WaitForReceiptOptions> {
355-
const { factoryContract, accountContract, executeTx, options } = args;
346+
const { executeTx, options } = args;
356347
const unsignedUserOp = await createUnsignedUserOp({
357-
factoryContract,
358-
accountContract,
359348
executeTx,
360349
options,
361350
});
@@ -381,10 +370,7 @@ async function _sendUserOp(args: {
381370
}
382371

383372
async function waitForUserOpReceipt(args: {
384-
options: SmartWalletOptions & {
385-
personalAccount: Account;
386-
client: ThirdwebClient;
387-
};
373+
options: SmartAccountOptions;
388374
userOpHash: Hex;
389375
}): Promise<TransactionReceipt> {
390376
const { options, userOpHash } = args;

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { decodeErrorResult } from "viem";
2-
import type { ThirdwebClient } from "../../../client/client.js";
32
import { parseEventLogs } from "../../../event/actions/parse-logs.js";
43
import { userOperationRevertReasonEvent } from "../../../extensions/erc4337/__generated__/IEntryPoint/events/UserOperationRevertReason.js";
54
import type { TransactionReceipt } from "../../../transaction/types.js";
@@ -8,7 +7,7 @@ import { getClientFetch } from "../../../utils/fetch.js";
87
import type {
98
EstimationResult,
109
GasPriceResult,
11-
SmartWalletOptions,
10+
SmartAccountOptions,
1211
UserOperation,
1312
} from "../types.js";
1413
import {
@@ -24,7 +23,7 @@ import { hexlifyUserOp } from "./utils.js";
2423
*/
2524
export async function bundleUserOp(args: {
2625
userOp: UserOperation;
27-
options: SmartWalletOptions & { client: ThirdwebClient };
26+
options: SmartAccountOptions;
2827
}): Promise<Hex> {
2928
return sendBundlerRequest({
3029
...args,
@@ -41,7 +40,7 @@ export async function bundleUserOp(args: {
4140
*/
4241
export async function estimateUserOpGas(args: {
4342
userOp: UserOperation;
44-
options: SmartWalletOptions & { client: ThirdwebClient };
43+
options: SmartAccountOptions;
4544
}): Promise<EstimationResult> {
4645
const res = await sendBundlerRequest({
4746
...args,
@@ -65,7 +64,7 @@ export async function estimateUserOpGas(args: {
6564
* @internal
6665
*/
6766
export async function getUserOpGasPrice(args: {
68-
options: SmartWalletOptions & { client: ThirdwebClient };
67+
options: SmartAccountOptions;
6968
}): Promise<GasPriceResult> {
7069
const res = await sendBundlerRequest({
7170
...args,
@@ -84,7 +83,7 @@ export async function getUserOpGasPrice(args: {
8483
*/
8584
export async function getUserOpReceipt(args: {
8685
userOpHash: Hex;
87-
options: SmartWalletOptions & { client: ThirdwebClient };
86+
options: SmartAccountOptions;
8887
}): Promise<TransactionReceipt | undefined> {
8988
const res = await sendBundlerRequest({
9089
...args,
@@ -117,7 +116,7 @@ export async function getUserOpReceipt(args: {
117116
}
118117

119118
async function sendBundlerRequest(args: {
120-
options: SmartWalletOptions & { client: ThirdwebClient };
119+
options: SmartAccountOptions;
121120
operation:
122121
| "eth_estimateUserOperationGas"
123122
| "eth_sendUserOperation"

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ import { prepareContractCall } from "../../../transaction/prepare-contract-call.
33
import type { PreparedTransaction } from "../../../transaction/prepare-transaction.js";
44
import { readContract } from "../../../transaction/read-contract.js";
55
import { stringToHex } from "../../../utils/encoding/hex.js";
6-
import type {
7-
Account,
8-
SendTransactionOption,
9-
} from "../../interfaces/wallet.js";
10-
import type { SmartWalletOptions } from "../types.ts";
6+
import type { SendTransactionOption } from "../../interfaces/wallet.js";
7+
import type { SmartAccountOptions, SmartWalletOptions } from "../types.ts";
118

129
/**
1310
* @internal
@@ -39,7 +36,7 @@ export async function predictAddress(
3936
*/
4037
export function prepareCreateAccount(args: {
4138
factoryContract: ThirdwebContract;
42-
options: SmartWalletOptions & { personalAccount: Account };
39+
options: SmartAccountOptions;
4340
}): PreparedTransaction {
4441
const { factoryContract, options } = args;
4542
if (options.overrides?.createAccount) {
@@ -60,7 +57,7 @@ export function prepareCreateAccount(args: {
6057
*/
6158
export function prepareExecute(args: {
6259
accountContract: ThirdwebContract;
63-
options: SmartWalletOptions;
60+
options: SmartAccountOptions;
6461
transaction: SendTransactionOption;
6562
}): PreparedTransaction {
6663
const { accountContract, options, transaction } = args;
@@ -83,7 +80,7 @@ export function prepareExecute(args: {
8380
*/
8481
export function prepareBatchExecute(args: {
8582
accountContract: ThirdwebContract;
86-
options: SmartWalletOptions;
83+
options: SmartAccountOptions;
8784
transactions: SendTransactionOption[];
8885
}): PreparedTransaction {
8986
const { accountContract, options, transactions } = args;

0 commit comments

Comments
 (0)