Skip to content

Commit 07ea65b

Browse files
feat: improve ecosystem smart account handling (#5212)
1 parent 6cb66c6 commit 07ea65b

File tree

8 files changed

+124
-138
lines changed

8 files changed

+124
-138
lines changed

.changeset/wicked-gifts-joke.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+
Better handling of ecosystem smart accounts

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

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"use client";
2-
import { MultiNetworkSelector } from "@/components/blocks/NetworkSelectors";
32
import { SettingsCard } from "@/components/blocks/SettingsCard";
43
import { Button } from "@/components/ui/button";
54
import { Checkbox } from "@/components/ui/checkbox";
@@ -27,6 +26,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
2726
import { PlusIcon } from "lucide-react";
2827
import { useFieldArray, useForm } from "react-hook-form";
2928
import { toast } from "sonner";
29+
import { isAddress } from "thirdweb";
3030
import { getSocialIcon } from "thirdweb/wallets/in-app";
3131
import {
3232
DEFAULT_ACCOUNT_FACTORY_V0_6,
@@ -57,7 +57,7 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
5757
customAuthEndpoint: ecosystem.customAuthOptions?.authEndpoint?.url || "",
5858
customHeaders: ecosystem.customAuthOptions?.authEndpoint?.headers || [],
5959
useSmartAccount: !!ecosystem.smartAccountOptions,
60-
chainIds: ecosystem.smartAccountOptions?.chainIds || [],
60+
chainIds: [], // unused - TODO: remove from service
6161
sponsorGas: ecosystem.smartAccountOptions?.sponsorGas || false,
6262
accountFactoryType:
6363
ecosystem.smartAccountOptions?.accountFactoryAddress ===
@@ -92,14 +92,18 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
9292
})
9393
.refine(
9494
(data) => {
95-
if (data.useSmartAccount && data.chainIds.length === 0) {
95+
if (
96+
data.useSmartAccount &&
97+
data.customAccountFactoryAddress &&
98+
!isAddress(data.customAccountFactoryAddress)
99+
) {
96100
return false;
97101
}
98102
return true;
99103
},
100104
{
101-
message: "Please select at least one chain for smart accounts",
102-
path: ["chainIds"],
105+
message: "Please enter a valid custom account factory address",
106+
path: ["customAccountFactoryAddress"],
103107
},
104108
)
105109
.refine(
@@ -166,7 +170,7 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
166170
}
167171

168172
smartAccountOptions = {
169-
chainIds: data.chainIds,
173+
chainIds: [], // unused - TODO remove from service
170174
sponsorGas: data.sponsorGas,
171175
accountFactoryAddress,
172176
};
@@ -403,27 +407,6 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
403407
/>
404408
{form.watch("useSmartAccount") && (
405409
<div className="mt-1 flex flex-col gap-4">
406-
<FormField
407-
control={form.control}
408-
name="chainIds"
409-
render={({ field }) => (
410-
<FormItem>
411-
<FormLabel>Supported Chains</FormLabel>
412-
<FormDescription>
413-
Select the chains you want to support for smart accounts
414-
</FormDescription>
415-
<FormControl>
416-
<div className="w-full bg-background">
417-
<MultiNetworkSelector
418-
selectedChainIds={field.value}
419-
onChange={field.onChange}
420-
/>
421-
</div>
422-
</FormControl>
423-
<FormMessage />
424-
</FormItem>
425-
)}
426-
/>
427410
<FormField
428411
control={form.control}
429412
name="sponsorGas"
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { isEcosystemWallet } from "../../../wallets/ecosystem/is-ecosystem-wallet.js";
12
import type { Wallet } from "../../../wallets/interfaces/wallet.js";
23

34
export function hasSmartAccount(activeWallet?: Wallet): boolean {
45
const config = activeWallet?.getConfig();
56
return (
6-
!!activeWallet &&
7+
activeWallet !== undefined &&
78
(activeWallet.id === "smart" ||
8-
(activeWallet.id === "inApp" && !!config && "smartAccount" in config))
9+
(activeWallet.id === "inApp" && !!config && "smartAccount" in config) ||
10+
(isEcosystemWallet(activeWallet) && !!config && "smartAccount" in config))
911
);
1012
}

packages/thirdweb/src/react/web/wallets/shared/ConnectWalletSocialOptions.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useMemo, useState } from "react";
55
import type { Chain } from "../../../../chains/types.js";
66
import type { ThirdwebClient } from "../../../../client/client.js";
77
import { webLocalStorage } from "../../../../utils/storage/webStorage.js";
8-
import { getEcosystemOptions } from "../../../../wallets/ecosystem/get-ecosystem-wallet-auth-options.js";
8+
import { getEcosystemInfo } from "../../../../wallets/ecosystem/get-ecosystem-wallet-auth-options.js";
99
import { isEcosystemWallet } from "../../../../wallets/ecosystem/is-ecosystem-wallet.js";
1010
import type { Profile } from "../../../../wallets/in-app/core/authentication/types.js";
1111
import { linkProfile } from "../../../../wallets/in-app/web/lib/auth/index.js";
@@ -123,7 +123,7 @@ export const ConnectWalletSocialOptions = (
123123
queryKey: ["auth-options", wallet.id],
124124
queryFn: async () => {
125125
if (isEcosystemWallet(wallet)) {
126-
const options = await getEcosystemOptions(wallet.id);
126+
const options = await getEcosystemInfo(wallet.id);
127127
return options?.authOptions ?? null;
128128
}
129129
return null;
Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import { getThirdwebBaseUrl } from "../../utils/domains.js";
2+
import { withCache } from "../../utils/promise/withCache.js";
23
import type { AuthOption } from "../types.js";
34
import type { EcosystemWalletId } from "../wallet-types.js";
45

56
export type EcosystemOptions = {
7+
name: string;
8+
imageUrl?: string;
9+
slug: string;
10+
homepage?: string;
611
authOptions: AuthOption[];
712
smartAccountOptions: SmartAccountOptions;
813
};
@@ -19,32 +24,40 @@ type SmartAccountOptions = {
1924
* @returns {AuthOption[] | undefined} The auth options for the ecosystem wallet.
2025
* @internal
2126
*/
22-
export async function getEcosystemOptions(
27+
export async function getEcosystemInfo(
2328
walletId: EcosystemWalletId,
24-
): Promise<EcosystemOptions | null> {
25-
const res = await fetch(
26-
`${getThirdwebBaseUrl("inAppWallet")}/api/2024-05-05/ecosystem-wallet`,
27-
{
28-
headers: {
29-
"x-ecosystem-id": walletId,
30-
},
31-
},
32-
);
29+
): Promise<EcosystemOptions> {
30+
return withCache(
31+
async () => {
32+
const res = await fetch(
33+
`${getThirdwebBaseUrl("inAppWallet")}/api/2024-05-05/ecosystem-wallet`,
34+
{
35+
headers: {
36+
"x-ecosystem-id": walletId,
37+
},
38+
},
39+
);
3340

34-
const data = await res.json();
41+
const data = await res.json();
3542

36-
if (!data || data.code === "UNAUTHORIZED") {
37-
throw new Error(
38-
data.message ||
39-
`Could not find ecosystem wallet with id ${walletId}, please check your ecosystem wallet configuration.`,
40-
);
41-
}
43+
if (!data || data.code === "UNAUTHORIZED") {
44+
throw new Error(
45+
data.message ||
46+
`Could not find ecosystem wallet with id ${walletId}, please check your ecosystem wallet configuration.`,
47+
);
48+
}
4249

43-
// siwe is the auth option in the backend, but we want to use wallet as the auth option in the frontend
44-
if (data.authOptions?.includes("siwe")) {
45-
data.authOptions = data.authOptions.filter((o: string) => o !== "siwe");
46-
data.authOptions.push("wallet");
47-
}
50+
// siwe is the auth option in the backend, but we want to use wallet as the auth option in the frontend
51+
if (data.authOptions?.includes("siwe")) {
52+
data.authOptions = data.authOptions.filter((o: string) => o !== "siwe");
53+
data.authOptions.push("wallet");
54+
}
4855

49-
return data ?? null;
56+
return data;
57+
},
58+
{
59+
cacheKey: `ecosystem-wallet-options-${walletId}`,
60+
cacheTime: 1000 * 60 * 5, // 5 mins
61+
},
62+
);
5063
}

packages/thirdweb/src/wallets/ecosystem/get-ecosystem-wallet-info.ts

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { getThirdwebBaseUrl } from "../../utils/domains.js";
21
import type { Prettify } from "../../utils/type-utils.js";
32
import type { WalletInfo } from "../wallet-info.js";
43
import type { EcosystemWalletId } from "../wallet-types.js";
4+
import { getEcosystemInfo } from "./get-ecosystem-wallet-auth-options.js";
55

66
/**
77
* Fetches metadata for a given ecosystem wallet.
@@ -14,29 +14,13 @@ import type { EcosystemWalletId } from "../wallet-types.js";
1414
export async function getEcosystemWalletInfo(
1515
walletId: EcosystemWalletId,
1616
): Promise<Prettify<WalletInfo>> {
17-
const res = await fetch(
18-
`${getThirdwebBaseUrl("inAppWallet")}/api/2024-05-05/ecosystem-wallet`,
19-
{
20-
headers: {
21-
"x-ecosystem-id": walletId,
22-
},
23-
},
24-
);
25-
26-
const data = await res.json();
27-
28-
if (!data || data.code === "UNAUTHORIZED") {
29-
throw new Error(
30-
data.message ||
31-
`Could not find ecosystem wallet with id ${walletId}, please check your ecosystem wallet configuration.`,
32-
);
33-
}
17+
const data = await getEcosystemInfo(walletId);
3418

3519
return {
3620
id: walletId,
37-
name: data.name as string,
38-
image_id: data.imageUrl as string,
39-
homepage: data.homepage as string,
21+
name: data.name,
22+
image_id: data.imageUrl || "",
23+
homepage: data.homepage || "",
4024
rdns: null,
4125
app: {
4226
browser: null,

packages/thirdweb/src/wallets/in-app/core/wallet/in-app-core.ts

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { trackConnect } from "../../../../analytics/track/connect.js";
22
import type { Chain } from "../../../../chains/types.js";
33
import { getCachedChainIfExists } from "../../../../chains/utils.js";
44
import type { ThirdwebClient } from "../../../../client/client.js";
5+
import { getEcosystemInfo } from "../../../ecosystem/get-ecosystem-wallet-auth-options.js";
56
import type { Account, Wallet } from "../../../interfaces/wallet.js";
67
import { createWalletEmitter } from "../../../wallet-emitter.js";
78
import type {
@@ -38,9 +39,10 @@ export function createInAppWallet(args: {
3839
connectorFactory: (client: ThirdwebClient) => Promise<InAppConnector>;
3940
ecosystem?: Ecosystem;
4041
}): Wallet<"inApp" | EcosystemWalletId> {
41-
const { createOptions, connectorFactory, ecosystem } = args;
42+
const { createOptions: _createOptions, connectorFactory, ecosystem } = args;
4243
const walletId = ecosystem ? ecosystem.id : "inApp";
4344
const emitter = createWalletEmitter<"inApp">();
45+
let createOptions = _createOptions;
4446
let account: Account | undefined = undefined;
4547
let chain: Chain | undefined = undefined;
4648
let client: ThirdwebClient | undefined;
@@ -66,11 +68,32 @@ export function createInAppWallet(args: {
6668
connectorFactory,
6769
ecosystem,
6870
);
71+
72+
if (ecosystem) {
73+
const ecosystemOptions = await getEcosystemInfo(ecosystem.id);
74+
const smartAccountOptions = ecosystemOptions?.smartAccountOptions;
75+
if (smartAccountOptions) {
76+
const preferredChain = options.chain;
77+
if (!preferredChain) {
78+
throw new Error(
79+
"Chain is required for ecosystem smart accounts, pass it via connect() or via UI components",
80+
);
81+
}
82+
createOptions = {
83+
...createOptions,
84+
smartAccount: {
85+
chain: preferredChain,
86+
sponsorGas: smartAccountOptions.sponsorGas,
87+
factoryAddress: smartAccountOptions.accountFactoryAddress,
88+
},
89+
};
90+
}
91+
}
92+
6993
const [connectedAccount, connectedChain] = await autoConnectInAppWallet(
7094
options,
7195
createOptions,
7296
connector,
73-
ecosystem,
7497
);
7598

7699
// set the states
@@ -94,11 +117,31 @@ export function createInAppWallet(args: {
94117
ecosystem,
95118
);
96119

120+
if (ecosystem) {
121+
const ecosystemOptions = await getEcosystemInfo(ecosystem.id);
122+
const smartAccountOptions = ecosystemOptions?.smartAccountOptions;
123+
if (smartAccountOptions) {
124+
const preferredChain = options.chain;
125+
if (!preferredChain) {
126+
throw new Error(
127+
"Chain is required for ecosystem smart accounts, pass it via connect() or via UI components",
128+
);
129+
}
130+
createOptions = {
131+
...createOptions,
132+
smartAccount: {
133+
chain: preferredChain,
134+
sponsorGas: smartAccountOptions.sponsorGas,
135+
factoryAddress: smartAccountOptions.accountFactoryAddress,
136+
},
137+
};
138+
}
139+
}
140+
97141
const [connectedAccount, connectedChain] = await connectInAppWallet(
98142
options,
99143
createOptions,
100144
connector,
101-
ecosystem,
102145
);
103146
// set the states
104147
client = options.client;
@@ -139,14 +182,29 @@ export function createInAppWallet(args: {
139182
connectorFactory,
140183
ecosystem,
141184
);
185+
186+
if (ecosystem) {
187+
const ecosystemOptions = await getEcosystemInfo(ecosystem.id);
188+
const smartAccountOptions = ecosystemOptions?.smartAccountOptions;
189+
if (smartAccountOptions) {
190+
createOptions = {
191+
...createOptions,
192+
smartAccount: {
193+
chain: newChain,
194+
sponsorGas: smartAccountOptions.sponsorGas,
195+
factoryAddress: smartAccountOptions.accountFactoryAddress,
196+
},
197+
};
198+
}
199+
}
200+
142201
const [connectedAccount, connectedChain] = await autoConnectInAppWallet(
143202
{
144203
chain: newChain,
145204
client,
146205
},
147206
createOptions,
148207
connector,
149-
ecosystem,
150208
);
151209
account = connectedAccount;
152210
chain = connectedChain;

0 commit comments

Comments
 (0)