Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/six-baboons-begin.md
Original file line number Diff line number Diff line change
@@ -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
});
```
Original file line number Diff line number Diff line change
Expand Up @@ -948,8 +948,6 @@ export type ConnectButtonProps = {
* ```tsx
* <ConnectButton
* accountAbstraction={{
* factoryAddress: "0x123...",
* chain: sepolia,
* gasless: true;
* }}
* />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
22 changes: 19 additions & 3 deletions packages/thirdweb/src/wallets/smart/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import type {
SmartWalletConnectionOptions,
SmartWalletOptions,
TokenPaymasterConfig,
UserOpOptions,
UserOperationV06,
UserOperationV07,
} from "./types.js";
Expand Down Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One issue I see is that below this we get the factory and account contracts based on this chain variable.

It works with our default factory because it's deterministic (though need to double check it's on mainnet) but might error if you pass another factory address and no chain.

Also would double check we don't use this chain car for other assumptions in this connect flow


// if factory is passed, but no entrypoint, try to resolve entrypoint from factory
if (options.factoryAddress && !options.overrides?.entrypointAddress) {
Expand Down Expand Up @@ -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,
},
});
Expand Down Expand Up @@ -365,6 +380,7 @@ async function approveERC20(args: {
executeTx,
options: {
...options,
chain: getCachedChain(transaction.chainId),
overrides: {
...options.overrides,
tokenPaymaster: undefined,
Expand Down Expand Up @@ -460,7 +476,7 @@ function createZkSyncAccount(args: {

async function _sendUserOp(args: {
executeTx: PreparedTransaction;
options: SmartAccountOptions;
options: UserOpOptions;
}): Promise<WaitForReceiptOptions> {
const { executeTx, options } = args;
try {
Expand Down
5 changes: 3 additions & 2 deletions packages/thirdweb/src/wallets/smart/lib/calls.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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,
}),
});
Expand Down
4 changes: 2 additions & 2 deletions packages/thirdweb/src/wallets/smart/lib/signing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/thirdweb/src/wallets/smart/lib/userop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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",
Expand Down
10 changes: 7 additions & 3 deletions packages/thirdweb/src/wallets/smart/smart-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import type { SmartWalletOptions } from "./types.js";
* import { sendTransaction } from "thirdweb";
*
* const wallet = smartWallet({
* chain: sepolia,
* sponsorGas: true, // enable sponsored transactions
* });
*
Expand Down Expand Up @@ -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
* });
Expand All @@ -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
* });
Expand Down Expand Up @@ -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");
}
Comment on lines +132 to +137
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition appears to be checking the wrong logic - it currently throws when factoryAddress is undefined but chain is defined. The intended validation is to require chain when factoryAddress is provided. The condition should be:

if (typeof createOptions.factoryAddress !== 'undefined' && typeof createOptions.chain === 'undefined')

This ensures that a chain is specified whenever a custom factory address is used.

Spotted by Graphite Reviewer

Is this helpful? React 👍 or 👎 to let us know.

Comment on lines +132 to +137
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation logic appears to be inverted. The current check allows a chain to be specified without a factoryAddress, but throws when neither is provided. To enforce that a chain must be provided when factoryAddress is specified, the condition should be:

if (typeof createOptions.factoryAddress !== 'undefined' && typeof createOptions.chain === 'undefined')

This ensures that smart wallets with custom factories have the required chain configuration.

Spotted by Graphite Reviewer

Is this helpful? React 👍 or 👎 to let us know.

const emitter = createWalletEmitter<"smart">();
let account: Account | undefined = undefined;
let adminAccount: Account | undefined = undefined;
Expand Down
16 changes: 14 additions & 2 deletions packages/thirdweb/src/wallets/smart/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -81,7 +81,7 @@ export type SmartWalletOptions = Prettify<
// internal type
export type SmartAccountOptions = Prettify<
Omit<SmartWalletOptions, "chain" | "gasless" | "sponsorGas"> & {
chain: Chain;
chain?: Chain;
sponsorGas: boolean;
personalAccount: Account;
factoryContract: ThirdwebContract;
Expand All @@ -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;
Expand Down
Loading