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/wicked-gifts-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Better handling of ecosystem smart accounts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 ===
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -166,7 +170,7 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
}

smartAccountOptions = {
chainIds: data.chainIds,
chainIds: [], // unused - TODO remove from service
sponsorGas: data.sponsorGas,
accountFactoryAddress,
};
Expand Down Expand Up @@ -403,27 +407,6 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
/>
{form.watch("useSmartAccount") && (
<div className="mt-1 flex flex-col gap-4">
<FormField
control={form.control}
name="chainIds"
render={({ field }) => (
<FormItem>
<FormLabel>Supported Chains</FormLabel>
<FormDescription>
Select the chains you want to support for smart accounts
</FormDescription>
<FormControl>
<div className="w-full bg-background">
<MultiNetworkSelector
selectedChainIds={field.value}
onChange={field.onChange}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="sponsorGas"
Expand Down
6 changes: 4 additions & 2 deletions packages/thirdweb/src/react/core/utils/isSmartWallet.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { isEcosystemWallet } from "../../../wallets/ecosystem/is-ecosystem-wallet.js";
import type { Wallet } from "../../../wallets/interfaces/wallet.js";

export function hasSmartAccount(activeWallet?: Wallet): boolean {
const config = activeWallet?.getConfig();
return (
!!activeWallet &&
activeWallet !== undefined &&
(activeWallet.id === "smart" ||
(activeWallet.id === "inApp" && !!config && "smartAccount" in config))
(activeWallet.id === "inApp" && !!config && "smartAccount" in config) ||
(isEcosystemWallet(activeWallet) && !!config && "smartAccount" in config))
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useMemo, useState } from "react";
import type { Chain } from "../../../../chains/types.js";
import type { ThirdwebClient } from "../../../../client/client.js";
import { webLocalStorage } from "../../../../utils/storage/webStorage.js";
import { getEcosystemOptions } from "../../../../wallets/ecosystem/get-ecosystem-wallet-auth-options.js";
import { getEcosystemInfo } from "../../../../wallets/ecosystem/get-ecosystem-wallet-auth-options.js";
import { isEcosystemWallet } from "../../../../wallets/ecosystem/is-ecosystem-wallet.js";
import type { Profile } from "../../../../wallets/in-app/core/authentication/types.js";
import { linkProfile } from "../../../../wallets/in-app/web/lib/auth/index.js";
Expand Down Expand Up @@ -123,7 +123,7 @@ export const ConnectWalletSocialOptions = (
queryKey: ["auth-options", wallet.id],
queryFn: async () => {
if (isEcosystemWallet(wallet)) {
const options = await getEcosystemOptions(wallet.id);
const options = await getEcosystemInfo(wallet.id);
return options?.authOptions ?? null;
}
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
};
Expand All @@ -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<EcosystemOptions | null> {
const res = await fetch(
`${getThirdwebBaseUrl("inAppWallet")}/api/2024-05-05/ecosystem-wallet`,
{
headers: {
"x-ecosystem-id": walletId,
},
},
);
): Promise<EcosystemOptions> {
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
},
);
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -14,29 +14,13 @@ import type { EcosystemWalletId } from "../wallet-types.js";
export async function getEcosystemWalletInfo(
walletId: EcosystemWalletId,
): Promise<Prettify<WalletInfo>> {
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,
Expand Down
66 changes: 62 additions & 4 deletions packages/thirdweb/src/wallets/in-app/core/wallet/in-app-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -38,9 +39,10 @@ export function createInAppWallet(args: {
connectorFactory: (client: ThirdwebClient) => Promise<InAppConnector>;
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;
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -139,14 +182,29 @@ 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,
client,
},
createOptions,
connector,
ecosystem,
);
account = connectedAccount;
chain = connectedChain;
Expand Down
Loading