diff --git a/.changeset/wicked-gifts-joke.md b/.changeset/wicked-gifts-joke.md new file mode 100644 index 00000000000..1b49ec5df9e --- /dev/null +++ b/.changeset/wicked-gifts-joke.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Better handling of ecosystem smart accounts diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/auth-options-form.client.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/auth-options-form.client.tsx index dcc7c558bde..fc13b31cb83 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/auth-options-form.client.tsx +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/auth-options-form.client.tsx @@ -1,5 +1,4 @@ "use client"; -import { MultiNetworkSelector } from "@/components/blocks/NetworkSelectors"; import { SettingsCard } from "@/components/blocks/SettingsCard"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -27,6 +26,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { PlusIcon } from "lucide-react"; import { useFieldArray, useForm } from "react-hook-form"; import { toast } from "sonner"; +import { isAddress } from "thirdweb"; import { getSocialIcon } from "thirdweb/wallets/in-app"; import { DEFAULT_ACCOUNT_FACTORY_V0_6, @@ -57,7 +57,7 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) { customAuthEndpoint: ecosystem.customAuthOptions?.authEndpoint?.url || "", customHeaders: ecosystem.customAuthOptions?.authEndpoint?.headers || [], useSmartAccount: !!ecosystem.smartAccountOptions, - chainIds: ecosystem.smartAccountOptions?.chainIds || [], + chainIds: [], // unused - TODO: remove from service sponsorGas: ecosystem.smartAccountOptions?.sponsorGas || false, accountFactoryType: ecosystem.smartAccountOptions?.accountFactoryAddress === @@ -92,14 +92,18 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) { }) .refine( (data) => { - if (data.useSmartAccount && data.chainIds.length === 0) { + if ( + data.useSmartAccount && + data.customAccountFactoryAddress && + !isAddress(data.customAccountFactoryAddress) + ) { return false; } return true; }, { - message: "Please select at least one chain for smart accounts", - path: ["chainIds"], + message: "Please enter a valid custom account factory address", + path: ["customAccountFactoryAddress"], }, ) .refine( @@ -166,7 +170,7 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) { } smartAccountOptions = { - chainIds: data.chainIds, + chainIds: [], // unused - TODO remove from service sponsorGas: data.sponsorGas, accountFactoryAddress, }; @@ -403,27 +407,6 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) { /> {form.watch("useSmartAccount") && (
- ( - - Supported Chains - - Select the chains you want to support for smart accounts - - -
- -
-
- -
- )} - /> { if (isEcosystemWallet(wallet)) { - const options = await getEcosystemOptions(wallet.id); + const options = await getEcosystemInfo(wallet.id); return options?.authOptions ?? null; } return null; diff --git a/packages/thirdweb/src/wallets/ecosystem/get-ecosystem-wallet-auth-options.ts b/packages/thirdweb/src/wallets/ecosystem/get-ecosystem-wallet-auth-options.ts index 075318fcd4b..1f3303c2e53 100644 --- a/packages/thirdweb/src/wallets/ecosystem/get-ecosystem-wallet-auth-options.ts +++ b/packages/thirdweb/src/wallets/ecosystem/get-ecosystem-wallet-auth-options.ts @@ -1,8 +1,13 @@ import { getThirdwebBaseUrl } from "../../utils/domains.js"; +import { withCache } from "../../utils/promise/withCache.js"; import type { AuthOption } from "../types.js"; import type { EcosystemWalletId } from "../wallet-types.js"; export type EcosystemOptions = { + name: string; + imageUrl?: string; + slug: string; + homepage?: string; authOptions: AuthOption[]; smartAccountOptions: SmartAccountOptions; }; @@ -19,32 +24,40 @@ type SmartAccountOptions = { * @returns {AuthOption[] | undefined} The auth options for the ecosystem wallet. * @internal */ -export async function getEcosystemOptions( +export async function getEcosystemInfo( walletId: EcosystemWalletId, -): Promise { - const res = await fetch( - `${getThirdwebBaseUrl("inAppWallet")}/api/2024-05-05/ecosystem-wallet`, - { - headers: { - "x-ecosystem-id": walletId, - }, - }, - ); +): Promise { + return withCache( + async () => { + const res = await fetch( + `${getThirdwebBaseUrl("inAppWallet")}/api/2024-05-05/ecosystem-wallet`, + { + headers: { + "x-ecosystem-id": walletId, + }, + }, + ); - const data = await res.json(); + const data = await res.json(); - if (!data || data.code === "UNAUTHORIZED") { - throw new Error( - data.message || - `Could not find ecosystem wallet with id ${walletId}, please check your ecosystem wallet configuration.`, - ); - } + if (!data || data.code === "UNAUTHORIZED") { + throw new Error( + data.message || + `Could not find ecosystem wallet with id ${walletId}, please check your ecosystem wallet configuration.`, + ); + } - // siwe is the auth option in the backend, but we want to use wallet as the auth option in the frontend - if (data.authOptions?.includes("siwe")) { - data.authOptions = data.authOptions.filter((o: string) => o !== "siwe"); - data.authOptions.push("wallet"); - } + // siwe is the auth option in the backend, but we want to use wallet as the auth option in the frontend + if (data.authOptions?.includes("siwe")) { + data.authOptions = data.authOptions.filter((o: string) => o !== "siwe"); + data.authOptions.push("wallet"); + } - return data ?? null; + return data; + }, + { + cacheKey: `ecosystem-wallet-options-${walletId}`, + cacheTime: 1000 * 60 * 5, // 5 mins + }, + ); } diff --git a/packages/thirdweb/src/wallets/ecosystem/get-ecosystem-wallet-info.ts b/packages/thirdweb/src/wallets/ecosystem/get-ecosystem-wallet-info.ts index 62a71531506..9536c8d698c 100644 --- a/packages/thirdweb/src/wallets/ecosystem/get-ecosystem-wallet-info.ts +++ b/packages/thirdweb/src/wallets/ecosystem/get-ecosystem-wallet-info.ts @@ -1,7 +1,7 @@ -import { getThirdwebBaseUrl } from "../../utils/domains.js"; import type { Prettify } from "../../utils/type-utils.js"; import type { WalletInfo } from "../wallet-info.js"; import type { EcosystemWalletId } from "../wallet-types.js"; +import { getEcosystemInfo } from "./get-ecosystem-wallet-auth-options.js"; /** * Fetches metadata for a given ecosystem wallet. @@ -14,29 +14,13 @@ import type { EcosystemWalletId } from "../wallet-types.js"; export async function getEcosystemWalletInfo( walletId: EcosystemWalletId, ): Promise> { - const res = await fetch( - `${getThirdwebBaseUrl("inAppWallet")}/api/2024-05-05/ecosystem-wallet`, - { - headers: { - "x-ecosystem-id": walletId, - }, - }, - ); - - const data = await res.json(); - - if (!data || data.code === "UNAUTHORIZED") { - throw new Error( - data.message || - `Could not find ecosystem wallet with id ${walletId}, please check your ecosystem wallet configuration.`, - ); - } + const data = await getEcosystemInfo(walletId); return { id: walletId, - name: data.name as string, - image_id: data.imageUrl as string, - homepage: data.homepage as string, + name: data.name, + image_id: data.imageUrl || "", + homepage: data.homepage || "", rdns: null, app: { browser: null, diff --git a/packages/thirdweb/src/wallets/in-app/core/wallet/in-app-core.ts b/packages/thirdweb/src/wallets/in-app/core/wallet/in-app-core.ts index eec4833136e..921dbd1cb83 100644 --- a/packages/thirdweb/src/wallets/in-app/core/wallet/in-app-core.ts +++ b/packages/thirdweb/src/wallets/in-app/core/wallet/in-app-core.ts @@ -2,6 +2,7 @@ import { trackConnect } from "../../../../analytics/track/connect.js"; import type { Chain } from "../../../../chains/types.js"; import { getCachedChainIfExists } from "../../../../chains/utils.js"; import type { ThirdwebClient } from "../../../../client/client.js"; +import { getEcosystemInfo } from "../../../ecosystem/get-ecosystem-wallet-auth-options.js"; import type { Account, Wallet } from "../../../interfaces/wallet.js"; import { createWalletEmitter } from "../../../wallet-emitter.js"; import type { @@ -38,9 +39,10 @@ export function createInAppWallet(args: { connectorFactory: (client: ThirdwebClient) => Promise; ecosystem?: Ecosystem; }): Wallet<"inApp" | EcosystemWalletId> { - const { createOptions, connectorFactory, ecosystem } = args; + const { createOptions: _createOptions, connectorFactory, ecosystem } = args; const walletId = ecosystem ? ecosystem.id : "inApp"; const emitter = createWalletEmitter<"inApp">(); + let createOptions = _createOptions; let account: Account | undefined = undefined; let chain: Chain | undefined = undefined; let client: ThirdwebClient | undefined; @@ -66,11 +68,32 @@ export function createInAppWallet(args: { connectorFactory, ecosystem, ); + + if (ecosystem) { + const ecosystemOptions = await getEcosystemInfo(ecosystem.id); + const smartAccountOptions = ecosystemOptions?.smartAccountOptions; + if (smartAccountOptions) { + const preferredChain = options.chain; + if (!preferredChain) { + throw new Error( + "Chain is required for ecosystem smart accounts, pass it via connect() or via UI components", + ); + } + createOptions = { + ...createOptions, + smartAccount: { + chain: preferredChain, + sponsorGas: smartAccountOptions.sponsorGas, + factoryAddress: smartAccountOptions.accountFactoryAddress, + }, + }; + } + } + const [connectedAccount, connectedChain] = await autoConnectInAppWallet( options, createOptions, connector, - ecosystem, ); // set the states @@ -94,11 +117,31 @@ export function createInAppWallet(args: { ecosystem, ); + if (ecosystem) { + const ecosystemOptions = await getEcosystemInfo(ecosystem.id); + const smartAccountOptions = ecosystemOptions?.smartAccountOptions; + if (smartAccountOptions) { + const preferredChain = options.chain; + if (!preferredChain) { + throw new Error( + "Chain is required for ecosystem smart accounts, pass it via connect() or via UI components", + ); + } + createOptions = { + ...createOptions, + smartAccount: { + chain: preferredChain, + sponsorGas: smartAccountOptions.sponsorGas, + factoryAddress: smartAccountOptions.accountFactoryAddress, + }, + }; + } + } + const [connectedAccount, connectedChain] = await connectInAppWallet( options, createOptions, connector, - ecosystem, ); // set the states client = options.client; @@ -139,6 +182,22 @@ export function createInAppWallet(args: { connectorFactory, ecosystem, ); + + if (ecosystem) { + const ecosystemOptions = await getEcosystemInfo(ecosystem.id); + const smartAccountOptions = ecosystemOptions?.smartAccountOptions; + if (smartAccountOptions) { + createOptions = { + ...createOptions, + smartAccount: { + chain: newChain, + sponsorGas: smartAccountOptions.sponsorGas, + factoryAddress: smartAccountOptions.accountFactoryAddress, + }, + }; + } + } + const [connectedAccount, connectedChain] = await autoConnectInAppWallet( { chain: newChain, @@ -146,7 +205,6 @@ export function createInAppWallet(args: { }, createOptions, connector, - ecosystem, ); account = connectedAccount; chain = connectedChain; diff --git a/packages/thirdweb/src/wallets/in-app/core/wallet/index.ts b/packages/thirdweb/src/wallets/in-app/core/wallet/index.ts index 9b508436a01..cd21db10632 100644 --- a/packages/thirdweb/src/wallets/in-app/core/wallet/index.ts +++ b/packages/thirdweb/src/wallets/in-app/core/wallet/index.ts @@ -1,12 +1,10 @@ import { ethereum } from "../../../../chains/chain-definitions/ethereum.js"; import type { Chain } from "../../../../chains/types.js"; -import { getCachedChain } from "../../../../chains/utils.js"; import type { ThirdwebClient } from "../../../../client/client.js"; import { type SocialAuthOption, socialAuthOptions, } from "../../../../wallets/types.js"; -import { getEcosystemOptions } from "../../../ecosystem/get-ecosystem-wallet-auth-options.js"; import type { Account, Wallet } from "../../../interfaces/wallet.js"; import type { EcosystemWalletId, WalletId } from "../../../wallet-types.js"; import type { @@ -15,7 +13,6 @@ import type { WalletConnectionOption, } from "../../../wallet-types.js"; import type { InAppConnector } from "../interfaces/connector.js"; -import type { Ecosystem } from "./types.js"; /** * Checks if the provided wallet is an in-app wallet. @@ -40,7 +37,6 @@ export async function connectInAppWallet( | CreateWalletArgs<"inApp">[1] | CreateWalletArgs[1], connector: InAppConnector, - ecosystem: Ecosystem | undefined, ): Promise<[Account, Chain]> { if ( // if auth mode is not specified, the default is popup @@ -77,33 +73,6 @@ export async function connectInAppWallet( }); } - if (ecosystem) { - const ecosystemOptions = await getEcosystemOptions(ecosystem.id); - const smartAccountOptions = ecosystemOptions?.smartAccountOptions; - if (smartAccountOptions) { - const allowedChains = smartAccountOptions.chainIds; - const firstAllowedChain = allowedChains[0]; - if (!firstAllowedChain) { - throw new Error( - "At least one chain must be allowed for ecosystem smart account", - ); - } - const preferredChain = - options.chain && allowedChains.includes(options.chain.id) - ? options.chain - : getCachedChain(firstAllowedChain); - return convertToSmartAccount({ - client: options.client, - authAccount, - smartAccountOptions: { - chain: preferredChain, - sponsorGas: smartAccountOptions.sponsorGas, - factoryAddress: smartAccountOptions.accountFactoryAddress, - }, - }); - } - } - return [authAccount, options.chain || ethereum] as const; } @@ -118,7 +87,6 @@ export async function autoConnectInAppWallet( | CreateWalletArgs<"inApp">[1] | CreateWalletArgs[1], connector: InAppConnector, - ecosystem: Ecosystem | undefined, ): Promise<[Account, Chain]> { if (options.authResult && connector.loginWithAuthToken) { await connector.loginWithAuthToken(options.authResult); @@ -144,33 +112,6 @@ export async function autoConnectInAppWallet( }); } - if (ecosystem) { - const ecosystemOptions = await getEcosystemOptions(ecosystem.id); - const smartAccountOptions = ecosystemOptions?.smartAccountOptions; - if (smartAccountOptions) { - const allowedChains = smartAccountOptions.chainIds; - const firstAllowedChain = allowedChains[0]; - if (!firstAllowedChain) { - throw new Error( - "At least one chain must be allowed for ecosystem smart account", - ); - } - const preferredChain = - options.chain && allowedChains.includes(options.chain.id) - ? options.chain - : getCachedChain(firstAllowedChain); - return convertToSmartAccount({ - client: options.client, - authAccount, - smartAccountOptions: { - chain: preferredChain, - sponsorGas: smartAccountOptions.sponsorGas, - factoryAddress: smartAccountOptions.accountFactoryAddress, - }, - }); - } - } - return [authAccount, options.chain || ethereum] as const; }