From cadb36ff07bd2ab4f8d3b04bbc638a66d5d24757 Mon Sep 17 00:00:00 2001 From: gregfromstl Date: Sun, 19 Jan 2025 18:33:45 -0800 Subject: [PATCH 1/3] feat(sdk): makes chain optional in smart wallet config --- .changeset/six-baboons-begin.md | 13 +++++++++++ .../hooks/connection/ConnectButtonProps.ts | 2 -- .../Modal/SmartWalletConnectUI.tsx | 4 +++- packages/thirdweb/src/wallets/smart/index.ts | 22 ++++++++++++++++--- .../thirdweb/src/wallets/smart/lib/signing.ts | 4 ++-- .../thirdweb/src/wallets/smart/lib/userop.ts | 2 +- .../src/wallets/smart/smart-wallet.ts | 4 +--- packages/thirdweb/src/wallets/smart/types.ts | 16 ++++++++++++-- 8 files changed, 53 insertions(+), 14 deletions(-) create mode 100644 .changeset/six-baboons-begin.md diff --git a/.changeset/six-baboons-begin.md b/.changeset/six-baboons-begin.md new file mode 100644 index 00000000000..6f6f3fca48d --- /dev/null +++ b/.changeset/six-baboons-begin.md @@ -0,0 +1,13 @@ +--- +"thirdweb": minor +--- + +Feature: Chain is no longer required for smart accounts + +```ts +import { smartWallet } from "thirdweb"; + +const wallet = smartWallet({ + sponsorGas: true, // enable sponsored transactions +}); +``` diff --git a/packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts b/packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts index 9bc568ed7a6..0c8edf90829 100644 --- a/packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts +++ b/packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts @@ -948,8 +948,6 @@ export type ConnectButtonProps = { * ```tsx * diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/SmartWalletConnectUI.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/SmartWalletConnectUI.tsx index 877dc017b7e..d68436c57e9 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/SmartWalletConnectUI.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/SmartWalletConnectUI.tsx @@ -129,7 +129,9 @@ function SmartWalletConnecting(props: { }; }, [personalWallet]); - const wrongNetwork = personalWalletChainId !== smartWalletChain.id; + const wrongNetwork = + typeof smartWalletChain !== "undefined" && + personalWalletChainId !== smartWalletChain.id; const [smartWalletConnectionStatus, setSmartWalletConnectionStatus] = useState<"connecting" | "connect-error" | "idle">("idle"); diff --git a/packages/thirdweb/src/wallets/smart/index.ts b/packages/thirdweb/src/wallets/smart/index.ts index d7019ec11b3..d7d6e9578c5 100644 --- a/packages/thirdweb/src/wallets/smart/index.ts +++ b/packages/thirdweb/src/wallets/smart/index.ts @@ -53,6 +53,7 @@ import type { SmartWalletConnectionOptions, SmartWalletOptions, TokenPaymasterConfig, + UserOpOptions, UserOperationV06, UserOperationV07, } from "./types.js"; @@ -92,7 +93,8 @@ export async function connectSmartAccount( } const options = creationOptions; - const chain = connectChain ?? options.chain; + // Fallback to mainnet if no chain is provided (we only need this for pre-deploy signatures since transactions and deployments must define their own chain) + const chain = connectChain ?? options.chain ?? getCachedChain(1); // if factory is passed, but no entrypoint, try to resolve entrypoint from factory if (options.factoryAddress && !options.overrides?.entrypointAddress) { @@ -265,16 +267,29 @@ async function createSmartAccount( }); }, async sendBatchTransaction(transactions: SendTransactionOption[]) { + // The latter half of this OR is purely to satisfy TS + if (transactions.length === 0 || typeof transactions[0] === "undefined") { + throw new Error("You must provide at least one transaction in a batch"); + } + + const chain = getCachedChain(transactions[0].chainId); + if (transactions.some((tx) => tx.chainId !== chain.id)) { + throw new Error( + "All transactions in a batch must have the same chain ID", + ); + } + const executeTx = prepareBatchExecute({ accountContract, transactions, executeBatchOverride: options.overrides?.executeBatch, }); + return _sendUserOp({ executeTx, options: { ...options, - chain: getCachedChain(transactions[0]?.chainId ?? options.chain.id), + chain, accountContract, }, }); @@ -365,6 +380,7 @@ async function approveERC20(args: { executeTx, options: { ...options, + chain: getCachedChain(transaction.chainId), overrides: { ...options.overrides, tokenPaymaster: undefined, @@ -460,7 +476,7 @@ function createZkSyncAccount(args: { async function _sendUserOp(args: { executeTx: PreparedTransaction; - options: SmartAccountOptions; + options: UserOpOptions; }): Promise { const { executeTx, options } = args; try { diff --git a/packages/thirdweb/src/wallets/smart/lib/signing.ts b/packages/thirdweb/src/wallets/smart/lib/signing.ts index 33847690dad..e7f55f9c442 100644 --- a/packages/thirdweb/src/wallets/smart/lib/signing.ts +++ b/packages/thirdweb/src/wallets/smart/lib/signing.ts @@ -57,7 +57,7 @@ export async function smartAccountSignMessage({ domain: { name: "Account", version: "1", - chainId: options.chain.id, + chainId: options.chain?.id ?? 1, verifyingContract: accountContract.address, }, primaryType: "AccountMessage", @@ -155,7 +155,7 @@ export async function smartAccountSignTypedData< domain: { name: "Account", version: "1", - chainId: options.chain.id, + chainId: options.chain?.id ?? 1, verifyingContract: accountContract.address, }, primaryType: "AccountMessage", diff --git a/packages/thirdweb/src/wallets/smart/lib/userop.ts b/packages/thirdweb/src/wallets/smart/lib/userop.ts index db00983de90..77d8a45efa4 100644 --- a/packages/thirdweb/src/wallets/smart/lib/userop.ts +++ b/packages/thirdweb/src/wallets/smart/lib/userop.ts @@ -697,7 +697,7 @@ export async function createAndSignUserOp(options: { transactions: PreparedTransaction[]; adminAccount: Account; client: ThirdwebClient; - smartWalletOptions: SmartWalletOptions; + smartWalletOptions: SmartWalletOptions & { chain: Chain }; waitForDeployment?: boolean; }) { const config = options.smartWalletOptions; diff --git a/packages/thirdweb/src/wallets/smart/smart-wallet.ts b/packages/thirdweb/src/wallets/smart/smart-wallet.ts index 0e2e7c6d887..d6e9424f59e 100644 --- a/packages/thirdweb/src/wallets/smart/smart-wallet.ts +++ b/packages/thirdweb/src/wallets/smart/smart-wallet.ts @@ -43,7 +43,6 @@ import type { SmartWalletOptions } from "./types.js"; * import { sendTransaction } from "thirdweb"; * * const wallet = smartWallet({ - * chain: sepolia, * sponsorGas: true, // enable sponsored transactions * }); * @@ -77,7 +76,7 @@ import type { SmartWalletOptions } from "./types.js"; * import { sepolia } from "thirdweb/chains"; * * const wallet = smartWallet({ - * chain: sepolia, + * chain: sepolia, // specify a chain if your factory only exists on one chain * sponsorGas: true, // enable sponsored transactions * factoryAddress: "0x...", // custom factory address * }); @@ -94,7 +93,6 @@ import type { SmartWalletOptions } from "./types.js"; * import { sepolia } from "thirdweb/chains"; * * const wallet = smartWallet({ - * chain: sepolia, * sponsorGas: true, // enable sponsored transactions * factoryAddress: DEFAULT_ACCOUNT_FACTORY_V0_7, // 0.7 factory address * }); diff --git a/packages/thirdweb/src/wallets/smart/types.ts b/packages/thirdweb/src/wallets/smart/types.ts index 6be9384a4f7..f2926d3b2d4 100644 --- a/packages/thirdweb/src/wallets/smart/types.ts +++ b/packages/thirdweb/src/wallets/smart/types.ts @@ -19,7 +19,7 @@ export type TokenPaymasterConfig = { export type SmartWalletOptions = Prettify< { - chain: Chain; // TODO consider making default chain optional + chain?: Chain; factoryAddress?: string; overrides?: { bundlerUrl?: string; @@ -81,7 +81,7 @@ export type SmartWalletOptions = Prettify< // internal type export type SmartAccountOptions = Prettify< Omit & { - chain: Chain; + chain?: Chain; sponsorGas: boolean; personalAccount: Account; factoryContract: ThirdwebContract; @@ -90,6 +90,18 @@ export type SmartAccountOptions = Prettify< } >; +export type UserOpOptions = Omit< + SmartWalletOptions, + "chain" | "gasless" | "sponsorGas" +> & { + chain: Chain; + sponsorGas: boolean; + personalAccount: Account; + factoryContract: ThirdwebContract; + accountContract: ThirdwebContract; + client: ThirdwebClient; +}; + export type BundlerOptions = { bundlerUrl?: string; entrypointAddress?: string; From d1e756015630f09602bf207fad4055b0485009d0 Mon Sep 17 00:00:00 2001 From: gregfromstl Date: Sun, 19 Jan 2025 19:08:33 -0800 Subject: [PATCH 2/3] fixup! feat(sdk): makes chain optional in smart wallet config --- .../thirdweb/src/wallets/smart/lib/calls.ts | 5 +-- .../smart-wallet-integration-v07.test.ts | 35 ++++++++++++++++++- .../smart/smart-wallet-integration.test.ts | 32 ++++++++++++++++- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/packages/thirdweb/src/wallets/smart/lib/calls.ts b/packages/thirdweb/src/wallets/smart/lib/calls.ts index f90c5582e43..636fcdb04de 100644 --- a/packages/thirdweb/src/wallets/smart/lib/calls.ts +++ b/packages/thirdweb/src/wallets/smart/lib/calls.ts @@ -1,4 +1,5 @@ import type { Chain } from "../../../chains/types.js"; +import { getCachedChain } from "../../../chains/utils.js"; import type { ThirdwebClient } from "../../../client/client.js"; import { type ThirdwebContract, @@ -29,7 +30,7 @@ import { DEFAULT_ACCOUNT_FACTORY_V0_6 } from "./constants.js"; */ export async function predictSmartAccountAddress(args: { client: ThirdwebClient; - chain: Chain; + chain?: Chain; adminAddress: string; factoryAddress?: string; accountSalt?: string; @@ -39,7 +40,7 @@ export async function predictSmartAccountAddress(args: { accountSalt: args.accountSalt, factoryContract: getContract({ address: args.factoryAddress ?? DEFAULT_ACCOUNT_FACTORY_V0_6, - chain: args.chain, + chain: args.chain ?? getCachedChain(1), client: args.client, }), }); diff --git a/packages/thirdweb/src/wallets/smart/smart-wallet-integration-v07.test.ts b/packages/thirdweb/src/wallets/smart/smart-wallet-integration-v07.test.ts index 10a773ca52c..1e9683734d4 100644 --- a/packages/thirdweb/src/wallets/smart/smart-wallet-integration-v07.test.ts +++ b/packages/thirdweb/src/wallets/smart/smart-wallet-integration-v07.test.ts @@ -77,7 +77,31 @@ describe.runIf(process.env.TW_SECRET_KEY)( }); }); - it("can connect", async () => { + it("can deploy with default chain", async () => { + const wallet = smartWallet({ + gasless: true, + factoryAddress: DEFAULT_ACCOUNT_FACTORY_V0_7, + }); + const smartAccount = await wallet.connect({ + client: TEST_CLIENT, + personalAccount, + }); + expect(smartAccount.address).toHaveLength(42); + + const signature = await smartAccount.signMessage({ + message: "hello world", + }); + const isValid = await verifySignature({ + message: "hello world", + signature, + address: smartWalletAddress, + chain: wallet.getChain(), + client, + }); + expect(isValid).toEqual(true); + }); + + it("can predict account address", async () => { expect(smartWalletAddress).toHaveLength(42); const predictedAddress = await predictSmartAccountAddress({ client, @@ -88,6 +112,15 @@ describe.runIf(process.env.TW_SECRET_KEY)( expect(predictedAddress).toEqual(smartWalletAddress); }); + it("can predict account address with default chain", async () => { + const predictedAddress = await predictSmartAccountAddress({ + client, + adminAddress: personalAccount.address, + factoryAddress: DEFAULT_ACCOUNT_FACTORY_V0_7, + }); + expect(predictedAddress).toEqual(smartWalletAddress); + }); + it("can sign a msg", async () => { const signature = await smartAccount.signMessage({ message: "hello world", diff --git a/packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts b/packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts index f7bef6f9999..93936b6952d 100644 --- a/packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts +++ b/packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts @@ -74,7 +74,29 @@ describe.runIf(process.env.TW_SECRET_KEY).sequential( }); }); - it("can connect", async () => { + it("can deploy with default chain", async () => { + const wallet = smartWallet({ + gasless: true, + }); + const smartAccount = await wallet.connect({ + client: TEST_CLIENT, + personalAccount, + }); + expect(smartAccount.address).toHaveLength(42); + const signature = await smartAccount.signMessage({ + message: "hello world", + }); + const isValid = await verifySignature({ + message: "hello world", + signature, + address: smartWalletAddress, + chain: wallet.getChain(), + client, + }); + expect(isValid).toEqual(true); + }); + + it("can predict account address", async () => { expect(smartWalletAddress).toHaveLength(42); const predictedAddress = await predictSmartAccountAddress({ client, @@ -84,6 +106,14 @@ describe.runIf(process.env.TW_SECRET_KEY).sequential( expect(predictedAddress).toEqual(smartWalletAddress); }); + it("can predict account address with default chain", async () => { + const predictedAddress = await predictSmartAccountAddress({ + client, + adminAddress: personalAccount.address, + }); + expect(predictedAddress).toEqual(smartWalletAddress); + }); + it("can sign a msg", async () => { const signature = await smartAccount.signMessage({ message: "hello world", From cbdb7cd2a08361209bb4192f1def38fe35871cf3 Mon Sep 17 00:00:00 2001 From: gregfromstl Date: Mon, 20 Jan 2025 13:36:29 -0800 Subject: [PATCH 3/3] fixup! feat(sdk): makes chain optional in smart wallet config --- packages/thirdweb/src/wallets/smart/smart-wallet.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/thirdweb/src/wallets/smart/smart-wallet.ts b/packages/thirdweb/src/wallets/smart/smart-wallet.ts index d6e9424f59e..1643f46cf95 100644 --- a/packages/thirdweb/src/wallets/smart/smart-wallet.ts +++ b/packages/thirdweb/src/wallets/smart/smart-wallet.ts @@ -129,6 +129,12 @@ import type { SmartWalletOptions } from "./types.js"; export function smartWallet( createOptions: SmartWalletOptions, ): Wallet<"smart"> { + if ( + typeof createOptions.factoryAddress === "undefined" && + typeof createOptions.chain !== "undefined" + ) { + throw new Error("You must provide a chain if factory address is specified"); + } const emitter = createWalletEmitter<"smart">(); let account: Account | undefined = undefined; let adminAccount: Account | undefined = undefined;