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/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/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-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",
diff --git a/packages/thirdweb/src/wallets/smart/smart-wallet.ts b/packages/thirdweb/src/wallets/smart/smart-wallet.ts
index 0e2e7c6d887..1643f46cf95 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
* });
@@ -131,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;
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;