Skip to content

Commit c893239

Browse files
committed
[SDK] Fix: Enable default smart wallet chain for ecosystem page (#5596)
https://linear.app/thirdweb/issue/CNCT-2522/unable-to-log-in-to-ecosystem-wallet-and-xai-connect <!-- start pr-codex --> --- ## PR-Codex overview This PR introduces a fallback chain option for ecosystem smart accounts in the `thirdweb` wallet, enhancing the configuration options for users. ### Detailed summary - Added `defaultChainId` to `SmartAccountOptions` type. - Updated `smartAccountOptions` to use `defaultChainId`. - Modified error messages to clarify chain requirements. - Added new form field for `defaultChainId` in the `AuthOptionsForm`. - Enhanced tests to cover scenarios with `defaultChainId`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 13d63ab commit c893239

File tree

6 files changed

+330
-11
lines changed

6 files changed

+330
-11
lines changed

.changeset/nervous-masks-beam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
add fallback chain for ecosystem smart accounts

apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/auth-options-form.client.tsx

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ type AuthOptionsFormData = {
4343
customAuthEndpoint: string;
4444
customHeaders: { key: string; value: string }[];
4545
useSmartAccount: boolean;
46-
chainIds: number[];
4746
sponsorGas: boolean;
47+
defaultChainId: number;
4848
accountFactoryType: "v0.6" | "v0.7" | "custom";
4949
customAccountFactoryAddress: string;
5050
};
@@ -57,8 +57,8 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
5757
customAuthEndpoint: ecosystem.customAuthOptions?.authEndpoint?.url || "",
5858
customHeaders: ecosystem.customAuthOptions?.authEndpoint?.headers || [],
5959
useSmartAccount: !!ecosystem.smartAccountOptions,
60-
chainIds: [], // unused - TODO: remove from service
6160
sponsorGas: ecosystem.smartAccountOptions?.sponsorGas || false,
61+
defaultChainId: ecosystem.smartAccountOptions?.defaultChainId,
6262
accountFactoryType:
6363
ecosystem.smartAccountOptions?.accountFactoryAddress ===
6464
DEFAULT_ACCOUNT_FACTORY_V0_7
@@ -85,8 +85,10 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
8585
)
8686
.optional(),
8787
useSmartAccount: z.boolean(),
88-
chainIds: z.array(z.number()),
8988
sponsorGas: z.boolean(),
89+
defaultChainId: z.coerce.number({
90+
invalid_type_error: "Please enter a valid chain ID",
91+
}),
9092
accountFactoryType: z.enum(["v0.6", "v0.7", "custom"]),
9193
customAccountFactoryAddress: z.string().optional(),
9294
})
@@ -165,12 +167,16 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
165167
accountFactoryAddress = DEFAULT_ACCOUNT_FACTORY_V0_7;
166168
break;
167169
case "custom":
170+
if (!data.customAccountFactoryAddress) {
171+
toast.error("Please enter a custom account factory address");
172+
return;
173+
}
168174
accountFactoryAddress = data.customAccountFactoryAddress;
169175
break;
170176
}
171177

172178
smartAccountOptions = {
173-
chainIds: [], // unused - TODO remove from service
179+
defaultChainId: data.defaultChainId,
174180
sponsorGas: data.sponsorGas,
175181
accountFactoryAddress,
176182
};
@@ -427,6 +433,33 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
427433
</FormItem>
428434
)}
429435
/>
436+
<FormField
437+
control={form.control}
438+
name="defaultChainId"
439+
render={({ field }) => (
440+
<FormItem>
441+
<FormLabel>Default Chain ID</FormLabel>
442+
<FormControl>
443+
<Input {...field} placeholder="1" />
444+
</FormControl>
445+
<FormDescription>
446+
This will be the chain ID the smart account will be
447+
initialized to on your{" "}
448+
<a
449+
href={`https://${ecosystem.slug}.ecosystem.thirdweb.com`}
450+
className="text-link-foreground"
451+
target="_blank"
452+
rel="noreferrer"
453+
>
454+
ecosystem page
455+
</a>
456+
.
457+
</FormDescription>
458+
<FormMessage />
459+
</FormItem>
460+
)}
461+
/>
462+
430463
<FormField
431464
control={form.control}
432465
name="accountFactoryType"

apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export type Ecosystem = {
3636
};
3737
} | null;
3838
smartAccountOptions?: {
39-
chainIds: number[];
39+
defaultChainId: number;
4040
sponsorGas: boolean;
4141
accountFactoryAddress: string;
4242
} | null;

packages/thirdweb/src/wallets/ecosystem/get-ecosystem-wallet-auth-options.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type EcosystemOptions = {
1313
};
1414

1515
type SmartAccountOptions = {
16-
chainIds: number[];
16+
defaultChainId: number;
1717
sponsorGas: boolean;
1818
accountFactoryAddress: string;
1919
};
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { baseSepolia } from "../../../../chains/chain-definitions/base-sepolia.js";
3+
import { createThirdwebClient } from "../../../../client/client.js";
4+
import { getEcosystemInfo } from "../../../ecosystem/get-ecosystem-wallet-auth-options.js";
5+
import type { Account } from "../../../interfaces/wallet.js";
6+
import type { InAppConnector } from "../interfaces/connector.js";
7+
import { createInAppWallet } from "./in-app-core.js";
8+
import { autoConnectInAppWallet, connectInAppWallet } from "./index.js";
9+
10+
vi.mock("../../../../analytics/track/connect.js", () => ({
11+
trackConnect: vi.fn(),
12+
}));
13+
14+
vi.mock("./index.js", () => ({
15+
autoConnectInAppWallet: vi.fn(),
16+
connectInAppWallet: vi.fn(),
17+
}));
18+
19+
vi.mock("../../../ecosystem/get-ecosystem-wallet-auth-options.js", () => ({
20+
getEcosystemInfo: vi.fn(),
21+
}));
22+
23+
describe("createInAppWallet", () => {
24+
const mockClient = createThirdwebClient({
25+
clientId: "test-client",
26+
});
27+
const mockChain = baseSepolia;
28+
const mockAccount = { address: "0x123" } as Account;
29+
30+
const mockConnectorFactory = vi.fn(() =>
31+
Promise.resolve({
32+
connect: vi.fn(),
33+
logout: vi.fn(() => Promise.resolve({ success: true })),
34+
authenticate: vi.fn(),
35+
getAccounts: vi.fn(),
36+
getAccount: vi.fn(),
37+
getProfiles: vi.fn(),
38+
getUser: vi.fn(),
39+
linkProfile: vi.fn(),
40+
preAuthenticate: vi.fn(),
41+
} as InAppConnector),
42+
);
43+
44+
beforeEach(() => {
45+
vi.clearAllMocks();
46+
});
47+
48+
it("should connect successfully", async () => {
49+
vi.mocked(connectInAppWallet).mockResolvedValue([mockAccount, mockChain]);
50+
51+
const wallet = createInAppWallet({
52+
connectorFactory: mockConnectorFactory,
53+
});
54+
55+
const result = await wallet.connect({
56+
client: mockClient,
57+
chain: mockChain,
58+
strategy: "email",
59+
email: "",
60+
verificationCode: "",
61+
});
62+
63+
expect(result).toBe(mockAccount);
64+
expect(connectInAppWallet).toHaveBeenCalledWith(
65+
expect.objectContaining({
66+
client: mockClient,
67+
chain: mockChain,
68+
}),
69+
undefined,
70+
expect.any(Object),
71+
);
72+
});
73+
74+
it("should auto connect successfully", async () => {
75+
vi.mocked(autoConnectInAppWallet).mockResolvedValue([
76+
mockAccount,
77+
mockChain,
78+
]);
79+
80+
const wallet = createInAppWallet({
81+
connectorFactory: mockConnectorFactory,
82+
});
83+
84+
const result = await wallet.autoConnect({
85+
client: mockClient,
86+
chain: mockChain,
87+
});
88+
89+
expect(result).toBe(mockAccount);
90+
expect(autoConnectInAppWallet).toHaveBeenCalledWith(
91+
expect.objectContaining({
92+
client: mockClient,
93+
chain: mockChain,
94+
}),
95+
undefined,
96+
expect.any(Object),
97+
);
98+
});
99+
100+
it("should handle ecosystem wallet connection with smart account settings", async () => {
101+
vi.mocked(getEcosystemInfo).mockResolvedValue({
102+
smartAccountOptions: {
103+
defaultChainId: mockChain.id,
104+
sponsorGas: true,
105+
accountFactoryAddress: "0x456",
106+
},
107+
authOptions: [],
108+
name: "hello world",
109+
slug: "test-ecosystem",
110+
});
111+
112+
vi.mocked(connectInAppWallet).mockResolvedValue([mockAccount, mockChain]);
113+
114+
const wallet = createInAppWallet({
115+
connectorFactory: mockConnectorFactory,
116+
ecosystem: { id: "ecosystem.test-ecosystem" },
117+
});
118+
119+
const result = await wallet.connect({
120+
client: mockClient,
121+
chain: mockChain,
122+
strategy: "email",
123+
email: "",
124+
verificationCode: "",
125+
});
126+
127+
expect(result).toBe(mockAccount);
128+
expect(connectInAppWallet).toHaveBeenCalledWith(
129+
expect.objectContaining({
130+
client: mockClient,
131+
chain: mockChain,
132+
}),
133+
expect.objectContaining({
134+
smartAccount: expect.objectContaining({
135+
chain: mockChain,
136+
sponsorGas: true,
137+
factoryAddress: "0x456",
138+
}),
139+
}),
140+
expect.any(Object),
141+
);
142+
});
143+
it("should handle ecosystem wallet connection with smart account settings even when no chain is set", async () => {
144+
vi.mocked(getEcosystemInfo).mockResolvedValue({
145+
smartAccountOptions: {
146+
defaultChainId: mockChain.id,
147+
sponsorGas: true,
148+
accountFactoryAddress: "0x456",
149+
},
150+
authOptions: [],
151+
name: "hello world",
152+
slug: "test-ecosystem",
153+
});
154+
155+
vi.mocked(connectInAppWallet).mockResolvedValue([mockAccount, mockChain]);
156+
157+
const wallet = createInAppWallet({
158+
connectorFactory: mockConnectorFactory,
159+
ecosystem: { id: "ecosystem.test-ecosystem" },
160+
});
161+
162+
const result = await wallet.connect({
163+
client: mockClient,
164+
strategy: "email",
165+
email: "",
166+
verificationCode: "",
167+
});
168+
169+
expect(result).toBe(mockAccount);
170+
expect(connectInAppWallet).toHaveBeenCalledWith(
171+
expect.objectContaining({
172+
client: mockClient,
173+
}),
174+
expect.objectContaining({
175+
smartAccount: expect.objectContaining({
176+
chain: mockChain,
177+
sponsorGas: true,
178+
factoryAddress: "0x456",
179+
}),
180+
}),
181+
expect.any(Object),
182+
);
183+
});
184+
185+
it("should handle ecosystem wallet auto connection with smart account settings", async () => {
186+
vi.mocked(getEcosystemInfo).mockResolvedValue({
187+
smartAccountOptions: {
188+
defaultChainId: mockChain.id,
189+
sponsorGas: true,
190+
accountFactoryAddress: "0x456",
191+
},
192+
authOptions: [],
193+
name: "hello world",
194+
slug: "test-ecosystem",
195+
});
196+
197+
vi.mocked(autoConnectInAppWallet).mockResolvedValue([
198+
mockAccount,
199+
mockChain,
200+
]);
201+
202+
const wallet = createInAppWallet({
203+
connectorFactory: mockConnectorFactory,
204+
ecosystem: { id: "ecosystem.test-ecosystem" },
205+
});
206+
207+
const result = await wallet.autoConnect({
208+
client: mockClient,
209+
chain: mockChain,
210+
});
211+
212+
expect(result).toBe(mockAccount);
213+
expect(autoConnectInAppWallet).toHaveBeenCalledWith(
214+
expect.objectContaining({
215+
client: mockClient,
216+
chain: mockChain,
217+
}),
218+
expect.objectContaining({
219+
smartAccount: expect.objectContaining({
220+
chain: mockChain,
221+
sponsorGas: true,
222+
factoryAddress: "0x456",
223+
}),
224+
}),
225+
expect.any(Object),
226+
);
227+
});
228+
229+
it("should handle ecosystem wallet auto connection with smart account settings even when no chain is set", async () => {
230+
vi.mocked(getEcosystemInfo).mockResolvedValue({
231+
smartAccountOptions: {
232+
defaultChainId: mockChain.id,
233+
sponsorGas: true,
234+
accountFactoryAddress: "0x456",
235+
},
236+
authOptions: [],
237+
name: "hello world",
238+
slug: "test-ecosystem",
239+
});
240+
241+
vi.mocked(autoConnectInAppWallet).mockResolvedValue([
242+
mockAccount,
243+
mockChain,
244+
]);
245+
246+
const wallet = createInAppWallet({
247+
connectorFactory: mockConnectorFactory,
248+
ecosystem: { id: "ecosystem.test-ecosystem" },
249+
});
250+
251+
const result = await wallet.autoConnect({
252+
client: mockClient,
253+
});
254+
255+
expect(result).toBe(mockAccount);
256+
expect(autoConnectInAppWallet).toHaveBeenCalledWith(
257+
expect.objectContaining({
258+
client: mockClient,
259+
}),
260+
expect.objectContaining({
261+
smartAccount: expect.objectContaining({
262+
chain: mockChain,
263+
sponsorGas: true,
264+
factoryAddress: "0x456",
265+
}),
266+
}),
267+
expect.any(Object),
268+
);
269+
});
270+
});

0 commit comments

Comments
 (0)