Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/nervous-masks-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

add fallback chain for ecosystem smart accounts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ type AuthOptionsFormData = {
customAuthEndpoint: string;
customHeaders: { key: string; value: string }[];
useSmartAccount: boolean;
chainIds: number[];
sponsorGas: boolean;
defaultChainId: number;
accountFactoryType: "v0.6" | "v0.7" | "custom";
customAccountFactoryAddress: string;
};
Expand All @@ -57,8 +57,8 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
customAuthEndpoint: ecosystem.customAuthOptions?.authEndpoint?.url || "",
customHeaders: ecosystem.customAuthOptions?.authEndpoint?.headers || [],
useSmartAccount: !!ecosystem.smartAccountOptions,
chainIds: [], // unused - TODO: remove from service
sponsorGas: ecosystem.smartAccountOptions?.sponsorGas || false,
defaultChainId: ecosystem.smartAccountOptions?.defaultChainId,
accountFactoryType:
ecosystem.smartAccountOptions?.accountFactoryAddress ===
DEFAULT_ACCOUNT_FACTORY_V0_7
Expand All @@ -85,8 +85,10 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
)
.optional(),
useSmartAccount: z.boolean(),
chainIds: z.array(z.number()),
sponsorGas: z.boolean(),
defaultChainId: z.coerce.number({
invalid_type_error: "Please enter a valid chain ID",
}),
accountFactoryType: z.enum(["v0.6", "v0.7", "custom"]),
customAccountFactoryAddress: z.string().optional(),
})
Expand Down Expand Up @@ -165,12 +167,16 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
accountFactoryAddress = DEFAULT_ACCOUNT_FACTORY_V0_7;
break;
case "custom":
if (!data.customAccountFactoryAddress) {
toast.error("Please enter a custom account factory address");
return;
}
accountFactoryAddress = data.customAccountFactoryAddress;
break;
}

smartAccountOptions = {
chainIds: [], // unused - TODO remove from service
defaultChainId: data.defaultChainId,
sponsorGas: data.sponsorGas,
accountFactoryAddress,
};
Expand Down Expand Up @@ -427,6 +433,33 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
</FormItem>
)}
/>
<FormField
control={form.control}
name="defaultChainId"
render={({ field }) => (
<FormItem>
<FormLabel>Default Chain ID</FormLabel>
<FormControl>
<Input {...field} placeholder="1" />
</FormControl>
<FormDescription>
This will be the chain ID the smart account will be
initialized to on your{" "}
<a
href={`https://${ecosystem.slug}.ecosystem.thirdweb.com`}
className="text-link-foreground"
target="_blank"
rel="noreferrer"
>
ecosystem page
</a>
.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="accountFactoryType"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export type Ecosystem = {
};
} | null;
smartAccountOptions?: {
chainIds: number[];
defaultChainId: number;
sponsorGas: boolean;
accountFactoryAddress: string;
} | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type EcosystemOptions = {
};

type SmartAccountOptions = {
chainIds: number[];
defaultChainId: number;
sponsorGas: boolean;
accountFactoryAddress: string;
};
Expand Down
270 changes: 270 additions & 0 deletions packages/thirdweb/src/wallets/in-app/core/wallet/in-app-core.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { baseSepolia } from "../../../../chains/chain-definitions/base-sepolia.js";
import { createThirdwebClient } from "../../../../client/client.js";
import { getEcosystemInfo } from "../../../ecosystem/get-ecosystem-wallet-auth-options.js";
import type { Account } from "../../../interfaces/wallet.js";
import type { InAppConnector } from "../interfaces/connector.js";
import { createInAppWallet } from "./in-app-core.js";
import { autoConnectInAppWallet, connectInAppWallet } from "./index.js";

vi.mock("../../../../analytics/track/connect.js", () => ({
trackConnect: vi.fn(),
}));

vi.mock("./index.js", () => ({
autoConnectInAppWallet: vi.fn(),
connectInAppWallet: vi.fn(),
}));

vi.mock("../../../ecosystem/get-ecosystem-wallet-auth-options.js", () => ({
getEcosystemInfo: vi.fn(),
}));

describe("createInAppWallet", () => {
const mockClient = createThirdwebClient({
clientId: "test-client",
});
const mockChain = baseSepolia;
const mockAccount = { address: "0x123" } as Account;

const mockConnectorFactory = vi.fn(() =>
Promise.resolve({
connect: vi.fn(),
logout: vi.fn(() => Promise.resolve({ success: true })),
authenticate: vi.fn(),
getAccounts: vi.fn(),
getAccount: vi.fn(),
getProfiles: vi.fn(),
getUser: vi.fn(),
linkProfile: vi.fn(),
preAuthenticate: vi.fn(),
} as InAppConnector),
);

beforeEach(() => {
vi.clearAllMocks();
});

it("should connect successfully", async () => {
vi.mocked(connectInAppWallet).mockResolvedValue([mockAccount, mockChain]);

const wallet = createInAppWallet({
connectorFactory: mockConnectorFactory,
});

const result = await wallet.connect({
client: mockClient,
chain: mockChain,
strategy: "email",
email: "",
verificationCode: "",
});

expect(result).toBe(mockAccount);
expect(connectInAppWallet).toHaveBeenCalledWith(
expect.objectContaining({
client: mockClient,
chain: mockChain,
}),
undefined,
expect.any(Object),
);
});

it("should auto connect successfully", async () => {
vi.mocked(autoConnectInAppWallet).mockResolvedValue([
mockAccount,
mockChain,
]);

const wallet = createInAppWallet({
connectorFactory: mockConnectorFactory,
});

const result = await wallet.autoConnect({
client: mockClient,
chain: mockChain,
});

expect(result).toBe(mockAccount);
expect(autoConnectInAppWallet).toHaveBeenCalledWith(
expect.objectContaining({
client: mockClient,
chain: mockChain,
}),
undefined,
expect.any(Object),
);
});

it("should handle ecosystem wallet connection with smart account settings", async () => {
vi.mocked(getEcosystemInfo).mockResolvedValue({
smartAccountOptions: {
defaultChainId: mockChain.id,
sponsorGas: true,
accountFactoryAddress: "0x456",
},
authOptions: [],
name: "hello world",
slug: "test-ecosystem",
});

vi.mocked(connectInAppWallet).mockResolvedValue([mockAccount, mockChain]);

const wallet = createInAppWallet({
connectorFactory: mockConnectorFactory,
ecosystem: { id: "ecosystem.test-ecosystem" },
});

const result = await wallet.connect({
client: mockClient,
chain: mockChain,
strategy: "email",
email: "",
verificationCode: "",
});

expect(result).toBe(mockAccount);
expect(connectInAppWallet).toHaveBeenCalledWith(
expect.objectContaining({
client: mockClient,
chain: mockChain,
}),
expect.objectContaining({
smartAccount: expect.objectContaining({
chain: mockChain,
sponsorGas: true,
factoryAddress: "0x456",
}),
}),
expect.any(Object),
);
});
it("should handle ecosystem wallet connection with smart account settings even when no chain is set", async () => {
vi.mocked(getEcosystemInfo).mockResolvedValue({
smartAccountOptions: {
defaultChainId: mockChain.id,
sponsorGas: true,
accountFactoryAddress: "0x456",
},
authOptions: [],
name: "hello world",
slug: "test-ecosystem",
});

vi.mocked(connectInAppWallet).mockResolvedValue([mockAccount, mockChain]);

const wallet = createInAppWallet({
connectorFactory: mockConnectorFactory,
ecosystem: { id: "ecosystem.test-ecosystem" },
});

const result = await wallet.connect({
client: mockClient,
strategy: "email",
email: "",
verificationCode: "",
});

expect(result).toBe(mockAccount);
expect(connectInAppWallet).toHaveBeenCalledWith(
expect.objectContaining({
client: mockClient,
}),
expect.objectContaining({
smartAccount: expect.objectContaining({
chain: mockChain,
sponsorGas: true,
factoryAddress: "0x456",
}),
}),
expect.any(Object),
);
});

it("should handle ecosystem wallet auto connection with smart account settings", async () => {
vi.mocked(getEcosystemInfo).mockResolvedValue({
smartAccountOptions: {
defaultChainId: mockChain.id,
sponsorGas: true,
accountFactoryAddress: "0x456",
},
authOptions: [],
name: "hello world",
slug: "test-ecosystem",
});

vi.mocked(autoConnectInAppWallet).mockResolvedValue([
mockAccount,
mockChain,
]);

const wallet = createInAppWallet({
connectorFactory: mockConnectorFactory,
ecosystem: { id: "ecosystem.test-ecosystem" },
});

const result = await wallet.autoConnect({
client: mockClient,
chain: mockChain,
});

expect(result).toBe(mockAccount);
expect(autoConnectInAppWallet).toHaveBeenCalledWith(
expect.objectContaining({
client: mockClient,
chain: mockChain,
}),
expect.objectContaining({
smartAccount: expect.objectContaining({
chain: mockChain,
sponsorGas: true,
factoryAddress: "0x456",
}),
}),
expect.any(Object),
);
});

it("should handle ecosystem wallet auto connection with smart account settings even when no chain is set", async () => {
vi.mocked(getEcosystemInfo).mockResolvedValue({
smartAccountOptions: {
defaultChainId: mockChain.id,
sponsorGas: true,
accountFactoryAddress: "0x456",
},
authOptions: [],
name: "hello world",
slug: "test-ecosystem",
});

vi.mocked(autoConnectInAppWallet).mockResolvedValue([
mockAccount,
mockChain,
]);

const wallet = createInAppWallet({
connectorFactory: mockConnectorFactory,
ecosystem: { id: "ecosystem.test-ecosystem" },
});

const result = await wallet.autoConnect({
client: mockClient,
});

expect(result).toBe(mockAccount);
expect(autoConnectInAppWallet).toHaveBeenCalledWith(
expect.objectContaining({
client: mockClient,
}),
expect.objectContaining({
smartAccount: expect.objectContaining({
chain: mockChain,
sponsorGas: true,
factoryAddress: "0x456",
}),
}),
expect.any(Object),
);
});
});
Loading
Loading