From 8eb45b79355d2583f9e4fb71bc06d7f464ff954f Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Sat, 31 May 2025 21:23:40 +1200 Subject: [PATCH] Handle smart account detection for inApp and ecosystem wallets --- .changeset/two-bugs-rescue.md | 5 + .../hooks/transaction/useSendTransaction.ts | 2 +- .../react/core/utils/isSmartWallet.test.ts | 19 -- .../src/react/core/utils/isSmartWallet.ts | 12 -- .../src/react/core/utils/wallet.test.ts | 77 -------- .../thirdweb/src/react/core/utils/wallet.ts | 50 +----- .../native/ui/connect/ConnectedModal.tsx | 10 +- .../react/web/ui/ConnectWallet/Details.tsx | 14 +- .../screens/Buy/TransactionModeScreen.tsx | 2 +- .../ui/TransactionButton/DepositScreen.tsx | 2 +- .../thirdweb/src/wallets/manager/index.ts | 3 +- .../smart/get-smart-wallet-config.test.ts | 67 ------- .../wallets/smart/get-smart-wallet-config.ts | 24 --- .../src/wallets/smart/is-smart-wallet.test.ts | 164 +++++++++++++++++- .../src/wallets/smart/is-smart-wallet.ts | 67 ++++++- 15 files changed, 247 insertions(+), 271 deletions(-) create mode 100644 .changeset/two-bugs-rescue.md delete mode 100644 packages/thirdweb/src/react/core/utils/isSmartWallet.test.ts delete mode 100644 packages/thirdweb/src/react/core/utils/isSmartWallet.ts delete mode 100644 packages/thirdweb/src/react/core/utils/wallet.test.ts delete mode 100644 packages/thirdweb/src/wallets/smart/get-smart-wallet-config.test.ts delete mode 100644 packages/thirdweb/src/wallets/smart/get-smart-wallet-config.ts diff --git a/.changeset/two-bugs-rescue.md b/.changeset/two-bugs-rescue.md new file mode 100644 index 00000000000..a99f339ac94 --- /dev/null +++ b/.changeset/two-bugs-rescue.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Handle smart account detection for inApp and ecosystem wallets diff --git a/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts b/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts index 3807597cf9b..a0c51a3a364 100644 --- a/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts +++ b/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts @@ -13,12 +13,12 @@ import { getTransactionGasCost } from "../../../../transaction/utils.js"; import type { Hex } from "../../../../utils/encoding/hex.js"; import { resolvePromisedValue } from "../../../../utils/promise/resolve-promised-value.js"; import type { Wallet } from "../../../../wallets/interfaces/wallet.js"; +import { hasSponsoredTransactionsEnabled } from "../../../../wallets/smart/is-smart-wallet.js"; import { getTokenBalance } from "../../../../wallets/utils/getTokenBalance.js"; import { getWalletBalance } from "../../../../wallets/utils/getWalletBalance.js"; import type { LocaleId } from "../../../web/ui/types.js"; import type { Theme } from "../../design-system/index.js"; import type { SupportedTokens } from "../../utils/defaultTokens.js"; -import { hasSponsoredTransactionsEnabled } from "../../utils/wallet.js"; /** * Configuration for the "Pay Modal" that opens when the user doesn't have enough funds to send a transaction. diff --git a/packages/thirdweb/src/react/core/utils/isSmartWallet.test.ts b/packages/thirdweb/src/react/core/utils/isSmartWallet.test.ts deleted file mode 100644 index c803e8eb215..00000000000 --- a/packages/thirdweb/src/react/core/utils/isSmartWallet.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { ANVIL_CHAIN } from "~test/chains.js"; -import { createWallet } from "../../../wallets/create-wallet.js"; -import { hasSmartAccount } from "./isSmartWallet.js"; - -describe("isSmartWallet", () => { - it("should work if id is inApp and has smartAccount in wallet config", () => { - const wallet = createWallet("inApp", { - smartAccount: { - chain: ANVIL_CHAIN, - sponsorGas: true, - overrides: { - bundlerUrl: "your-bundler-url", - }, - }, - }); - expect(hasSmartAccount(wallet)).toBe(true); - }); -}); diff --git a/packages/thirdweb/src/react/core/utils/isSmartWallet.ts b/packages/thirdweb/src/react/core/utils/isSmartWallet.ts deleted file mode 100644 index f1dffffd247..00000000000 --- a/packages/thirdweb/src/react/core/utils/isSmartWallet.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 !== undefined && - (activeWallet.id === "smart" || - (activeWallet.id === "inApp" && !!config && "smartAccount" in config) || - (isEcosystemWallet(activeWallet) && !!config && "smartAccount" in config)) - ); -} diff --git a/packages/thirdweb/src/react/core/utils/wallet.test.ts b/packages/thirdweb/src/react/core/utils/wallet.test.ts deleted file mode 100644 index 1afd6022943..00000000000 --- a/packages/thirdweb/src/react/core/utils/wallet.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { describe, expect, it } from "vitest"; -import type { Wallet } from "../../../wallets/interfaces/wallet"; -import { hasSponsoredTransactionsEnabled } from "./wallet"; - -describe("hasSponsoredTransactionsEnabled", () => { - it("should return false for undefined wallet", () => { - expect(hasSponsoredTransactionsEnabled(undefined)).toBe(false); - }); - - it("should handle smart wallet with sponsorGas config", () => { - const mockSmartWallet = { - id: "smart", - getConfig: () => ({ sponsorGas: true }), - } as Wallet; - expect(hasSponsoredTransactionsEnabled(mockSmartWallet)).toBe(true); - - const mockSmartWalletDisabled = { - id: "smart", - getConfig: () => ({ sponsorGas: false }), - } as Wallet; - expect(hasSponsoredTransactionsEnabled(mockSmartWalletDisabled)).toBe( - false, - ); - }); - - it("should handle smart wallet with gasless config", () => { - const mockSmartWallet = { - id: "smart", - getConfig: () => ({ gasless: true }), - } as Wallet; - expect(hasSponsoredTransactionsEnabled(mockSmartWallet)).toBe(true); - }); - - it("should handle inApp wallet with smartAccount config", () => { - const mockInAppWallet = { - id: "inApp", - getConfig: () => ({ - smartAccount: { - sponsorGas: true, - }, - }), - } as Wallet; - expect(hasSponsoredTransactionsEnabled(mockInAppWallet)).toBe(true); - - const mockInAppWalletDisabled = { - id: "inApp", - getConfig: () => ({ - smartAccount: { - sponsorGas: false, - }, - }), - } as Wallet; - expect(hasSponsoredTransactionsEnabled(mockInAppWalletDisabled)).toBe( - false, - ); - }); - - it("should handle inApp wallet with gasless config", () => { - const mockInAppWallet = { - id: "inApp", - getConfig: () => ({ - smartAccount: { - gasless: true, - }, - }), - } as Wallet; - expect(hasSponsoredTransactionsEnabled(mockInAppWallet)).toBe(true); - }); - - it("should return false for regular wallet without smart account config", () => { - const mockRegularWallet = { - id: "inApp", - getConfig: () => ({}), - } as Wallet; - expect(hasSponsoredTransactionsEnabled(mockRegularWallet)).toBe(false); - }); -}); diff --git a/packages/thirdweb/src/react/core/utils/wallet.ts b/packages/thirdweb/src/react/core/utils/wallet.ts index 6e0503bd753..4d22c18cc1f 100644 --- a/packages/thirdweb/src/react/core/utils/wallet.ts +++ b/packages/thirdweb/src/react/core/utils/wallet.ts @@ -7,8 +7,7 @@ import { resolveName } from "../../../extensions/ens/resolve-name.js"; import { shortenAddress } from "../../../utils/address.js"; import { parseAvatarRecord } from "../../../utils/ens/avatar.js"; import { getWalletInfo } from "../../../wallets/__generated__/getWalletInfo.js"; -import { isEcosystemWallet } from "../../../wallets/ecosystem/is-ecosystem-wallet.js"; -import type { Account, Wallet } from "../../../wallets/interfaces/wallet.js"; +import type { Account } from "../../../wallets/interfaces/wallet.js"; import type { WalletInfo } from "../../../wallets/wallet-info.js"; import type { WalletId } from "../../../wallets/wallet-types.js"; import { useWalletBalance } from "../hooks/others/useWalletBalance.js"; @@ -215,50 +214,3 @@ export function useWalletImage(id: WalletId | undefined) { enabled: !!id, }); } - -/** - * @internal - */ -export function hasSponsoredTransactionsEnabled(wallet: Wallet | undefined) { - if (!wallet) { - return false; - } - let sponsoredTransactionsEnabled = false; - if (wallet && wallet.id === "smart") { - const options = (wallet as Wallet<"smart">).getConfig(); - if ("sponsorGas" in options) { - sponsoredTransactionsEnabled = options.sponsorGas; - } - if ("gasless" in options) { - sponsoredTransactionsEnabled = options.gasless; - } - } - if (wallet && (wallet.id === "inApp" || isEcosystemWallet(wallet))) { - const options = (wallet as Wallet<"inApp">).getConfig(); - if (options && "smartAccount" in options && options.smartAccount) { - const smartOptions = options.smartAccount; - if ("sponsorGas" in smartOptions) { - sponsoredTransactionsEnabled = smartOptions.sponsorGas; - } - if ("gasless" in smartOptions) { - sponsoredTransactionsEnabled = smartOptions.gasless; - } - } - if (options?.executionMode) { - const execMode = options.executionMode; - if (execMode.mode === "EIP4337") { - const smartOptions = execMode.smartAccount; - if (smartOptions && "sponsorGas" in smartOptions) { - sponsoredTransactionsEnabled = smartOptions.sponsorGas; - } - if (smartOptions && "gasless" in smartOptions) { - sponsoredTransactionsEnabled = smartOptions.gasless; - } - } - if (execMode.mode === "EIP7702") { - sponsoredTransactionsEnabled = execMode.sponsorGas || false; - } - } - } - return sponsoredTransactionsEnabled; -} diff --git a/packages/thirdweb/src/react/native/ui/connect/ConnectedModal.tsx b/packages/thirdweb/src/react/native/ui/connect/ConnectedModal.tsx index c48312a1158..434cb5ace72 100644 --- a/packages/thirdweb/src/react/native/ui/connect/ConnectedModal.tsx +++ b/packages/thirdweb/src/react/native/ui/connect/ConnectedModal.tsx @@ -5,6 +5,7 @@ import { getContract } from "../../../../contract/contract.js"; import { isContractDeployed } from "../../../../utils/bytecode/is-contract-deployed.js"; import { formatNumber } from "../../../../utils/formatNumber.js"; import type { Account, Wallet } from "../../../../wallets/interfaces/wallet.js"; +import { isSmartWallet } from "../../../../wallets/smart/is-smart-wallet.js"; import type { Theme } from "../../../core/design-system/index.js"; import { useSiweAuth } from "../../../core/hooks/auth/useSiweAuth.js"; import type { ConnectButtonProps } from "../../../core/hooks/connection/ConnectButtonProps.js"; @@ -13,7 +14,6 @@ import { useActiveAccount } from "../../../core/hooks/wallets/useActiveAccount.j import { useActiveWallet } from "../../../core/hooks/wallets/useActiveWallet.js"; import { useActiveWalletChain } from "../../../core/hooks/wallets/useActiveWalletChain.js"; import { useDisconnect } from "../../../core/hooks/wallets/useDisconnect.js"; -import { hasSmartAccount } from "../../../core/utils/isSmartWallet.js"; import { useConnectedWalletDetails } from "../../../core/utils/wallet.js"; import { fontSize, radius, spacing } from "../../design-system/index.js"; import { Address } from "../components/Address.js"; @@ -326,14 +326,14 @@ function SmartAccountBadge(props: { }) { const activeAccount = useActiveAccount(); const activeWallet = useActiveWallet(); - const isSmartWallet = hasSmartAccount(activeWallet); + const isSW = isSmartWallet(activeWallet); const chain = useActiveWalletChain(); const { client, theme } = props; const [isSmartWalletDeployed, setIsSmartWalletDeployed] = useState(false); useEffect(() => { - if (activeAccount && isSmartWallet && activeAccount.address && chain) { + if (activeAccount && isSW && activeAccount.address && chain) { const contract = getContract({ address: activeAccount.address, chain, @@ -346,7 +346,7 @@ function SmartAccountBadge(props: { } else { setIsSmartWalletDeployed(false); } - }, [activeAccount, chain, client, isSmartWallet]); + }, [activeAccount, chain, client, isSW]); const content = ( ); - if (chain && activeAccount && isSmartWallet) { + if (chain && activeAccount && isSW) { return ( <> diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx index fe7230bc36e..7e6efc62e12 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx @@ -30,6 +30,7 @@ import { webLocalStorage } from "../../../../utils/storage/webStorage.js"; import { isEcosystemWallet } from "../../../../wallets/ecosystem/is-ecosystem-wallet.js"; import type { Ecosystem } from "../../../../wallets/in-app/core/wallet/types.js"; import type { Account, Wallet } from "../../../../wallets/interfaces/wallet.js"; +import { isSmartWallet } from "../../../../wallets/smart/is-smart-wallet.js"; import type { SmartWalletOptions } from "../../../../wallets/smart/types.js"; import { type AppMetadata, @@ -77,7 +78,6 @@ import type { SupportedNFTs, SupportedTokens, } from "../../../core/utils/defaultTokens.js"; -import { hasSmartAccount } from "../../../core/utils/isSmartWallet.js"; import { useWalletInfo } from "../../../core/utils/wallet.js"; import { WalletUIStatesProvider } from "../../providers/wallet-ui-states-provider.js"; import { ChainActiveDot } from "../components/ChainActiveDot.js"; @@ -1186,14 +1186,14 @@ export function ConnectedToSmartWallet(props: { }) { const activeAccount = useActiveAccount(); const activeWallet = useActiveWallet(); - const isSmartWallet = hasSmartAccount(activeWallet); + const isSW = isSmartWallet(activeWallet); const chain = useActiveWalletChain(); const { client, connectLocale: locale } = props; const [isSmartWalletDeployed, setIsSmartWalletDeployed] = useState(false); useEffect(() => { - if (activeAccount && isSmartWallet && activeAccount.address && chain) { + if (activeAccount && isSW && activeAccount.address && chain) { const contract = getContract({ address: activeAccount.address, chain, @@ -1206,7 +1206,7 @@ export function ConnectedToSmartWallet(props: { } else { setIsSmartWalletDeployed(false); } - }, [activeAccount, chain, client, isSmartWallet]); + }, [activeAccount, chain, client, isSW]); const content = ( @@ -1216,7 +1216,7 @@ export function ConnectedToSmartWallet(props: { ); - if (chain && activeAccount && isSmartWallet) { + if (chain && activeAccount && isSW) { return ( <> {isSmartWalletDeployed ? ( @@ -1251,7 +1251,7 @@ export function InAppWalletUserInfo(props: { const activeWallet = useActiveWallet(); const adminWallet = useAdminWallet(); const { data: walletInfo } = useWalletInfo(activeWallet?.id); - const isSmartWallet = hasSmartAccount(activeWallet); + const isSW = isSmartWallet(activeWallet); const { data: walletName } = useQuery({ queryKey: [ "wallet-name", @@ -1317,7 +1317,7 @@ export function InAppWalletUserInfo(props: { enabled: !!adminWallet, }); - if (!userInfoQuery.data && isSmartWallet) { + if (!userInfoQuery.data && isSW) { return ; } diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx index f9134a47b3f..ee1ddcddd0c 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx @@ -5,6 +5,7 @@ import { NATIVE_TOKEN_ADDRESS } from "../../../../../../constants/addresses.js"; import { formatNumber } from "../../../../../../utils/formatNumber.js"; import { toTokens } from "../../../../../../utils/units.js"; import type { Account } from "../../../../../../wallets/interfaces/wallet.js"; +import { hasSponsoredTransactionsEnabled } from "../../../../../../wallets/smart/is-smart-wallet.js"; import { useCustomTheme } from "../../../../../core/design-system/CustomThemeProvider.js"; import { fontSize, @@ -16,7 +17,6 @@ import { useChainMetadata } from "../../../../../core/hooks/others/useChainQuery import { useWalletBalance } from "../../../../../core/hooks/others/useWalletBalance.js"; import { useActiveAccount } from "../../../../../core/hooks/wallets/useActiveAccount.js"; import { useActiveWallet } from "../../../../../core/hooks/wallets/useActiveWallet.js"; -import { hasSponsoredTransactionsEnabled } from "../../../../../core/utils/wallet.js"; import { ErrorState } from "../../../../wallets/shared/ErrorState.js"; import { LoadingScreen } from "../../../../wallets/shared/LoadingScreen.js"; import type { PayEmbedConnectOptions } from "../../../PayEmbed.js"; diff --git a/packages/thirdweb/src/react/web/ui/TransactionButton/DepositScreen.tsx b/packages/thirdweb/src/react/web/ui/TransactionButton/DepositScreen.tsx index 899bfd21d80..3bfd1a51644 100644 --- a/packages/thirdweb/src/react/web/ui/TransactionButton/DepositScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/TransactionButton/DepositScreen.tsx @@ -4,6 +4,7 @@ import type { PreparedTransaction } from "../../../../transaction/prepare-transa import { shortenAddress } from "../../../../utils/address.js"; import { formatNumber } from "../../../../utils/formatNumber.js"; import { toTokens } from "../../../../utils/units.js"; +import { hasSponsoredTransactionsEnabled } from "../../../../wallets/smart/is-smart-wallet.js"; import { useCustomTheme } from "../../../core/design-system/CustomThemeProvider.js"; import { fontSize, @@ -13,7 +14,6 @@ import { } from "../../../core/design-system/index.js"; import { useActiveAccount } from "../../../core/hooks/wallets/useActiveAccount.js"; import { useActiveWallet } from "../../../core/hooks/wallets/useActiveWallet.js"; -import { hasSponsoredTransactionsEnabled } from "../../../core/utils/wallet.js"; import { ErrorState } from "../../wallets/shared/ErrorState.js"; import { LoadingScreen } from "../../wallets/shared/LoadingScreen.js"; import { CoinsIcon } from "../ConnectWallet/icons/CoinsIcon.js"; diff --git a/packages/thirdweb/src/wallets/manager/index.ts b/packages/thirdweb/src/wallets/manager/index.ts index baa8e420264..f7a896d3e15 100644 --- a/packages/thirdweb/src/wallets/manager/index.ts +++ b/packages/thirdweb/src/wallets/manager/index.ts @@ -1,7 +1,6 @@ import type { Chain } from "../../chains/types.js"; import { cacheChains } from "../../chains/utils.js"; import type { ThirdwebClient } from "../../client/client.js"; -import { hasSmartAccount } from "../../react/core/utils/isSmartWallet.js"; import { computedStore } from "../../reactive/computedStore.js"; import { effect } from "../../reactive/effect.js"; import { createStore } from "../../reactive/store.js"; @@ -135,7 +134,7 @@ export function createConnectionManager(storage: AsyncStorage) { } const activeWallet = await (async () => { - if (options?.accountAbstraction && !hasSmartAccount(wallet)) { + if (options?.accountAbstraction && !isSmartWallet(wallet)) { return await handleSmartWalletConnection( wallet, options.client, diff --git a/packages/thirdweb/src/wallets/smart/get-smart-wallet-config.test.ts b/packages/thirdweb/src/wallets/smart/get-smart-wallet-config.test.ts deleted file mode 100644 index a30674c95c3..00000000000 --- a/packages/thirdweb/src/wallets/smart/get-smart-wallet-config.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { optimism } from "../../chains/chain-definitions/optimism.js"; -import type { Wallet } from "../interfaces/wallet.js"; -import { getSmartWallet } from "./get-smart-wallet-config.js"; -import type { SmartWalletOptions } from "./types.js"; - -describe("getSmartWallet", () => { - const mockSmartWalletConfig: SmartWalletOptions = { - chain: optimism, - sponsorGas: false, - }; - - it("should return config for smart wallet ID", () => { - const wallet = { - id: "smart", - getConfig: () => mockSmartWalletConfig, - } as Wallet<"smart">; - - expect(getSmartWallet(wallet)).toBe(mockSmartWalletConfig); - }); - - it("should return smartAccount config for wallet with smartAccount", () => { - const wallet = { - id: "inApp", - getConfig: () => ({ - smartAccount: mockSmartWalletConfig, - }), - } as Wallet; - - expect(getSmartWallet(wallet)).toBe(mockSmartWalletConfig); - }); - - it("should throw error for non-smart wallet", () => { - const wallet = { - id: "inApp", - getConfig: () => ({}), - } as Wallet; - - expect(() => getSmartWallet(wallet)).toThrow( - "Wallet is not a smart wallet", - ); - }); - - it("should throw error when getConfig returns null", () => { - const wallet = { - id: "inApp", - getConfig: () => null, - // biome-ignore lint/suspicious/noExplicitAny: Testing invalid config - } as any as Wallet; - - expect(() => getSmartWallet(wallet)).toThrow( - "Wallet is not a smart wallet", - ); - }); - - it("should throw error when smartAccount is null", () => { - const wallet = { - id: "inApp", - getConfig: () => ({ smartAccount: null }), - // biome-ignore lint/suspicious/noExplicitAny: Testing invalid config - } as any as Wallet; - - expect(() => getSmartWallet(wallet)).toThrow( - "Wallet is not a smart wallet", - ); - }); -}); diff --git a/packages/thirdweb/src/wallets/smart/get-smart-wallet-config.ts b/packages/thirdweb/src/wallets/smart/get-smart-wallet-config.ts deleted file mode 100644 index 9711fdbabb7..00000000000 --- a/packages/thirdweb/src/wallets/smart/get-smart-wallet-config.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Wallet } from "../interfaces/wallet.js"; -import type { SmartWalletOptions } from "./types.js"; - -/** - * Gets the smart wallet configuration for a given wallet. - * - * @param {Wallet} wallet - The wallet to check. - * @returns {SmartWalletOptions} The smart wallet configuration. - * - * @throws {Error} If the wallet is not a smart wallet. - * @internal - */ -export function getSmartWallet(wallet: Wallet): SmartWalletOptions { - if (wallet.id === "smart") { - return (wallet as Wallet<"smart">).getConfig(); - } - - const config = wallet.getConfig(); - if (!!config && "smartAccount" in config && !!config?.smartAccount) { - return config.smartAccount; - } - - throw new Error("Wallet is not a smart wallet"); -} diff --git a/packages/thirdweb/src/wallets/smart/is-smart-wallet.test.ts b/packages/thirdweb/src/wallets/smart/is-smart-wallet.test.ts index 85e6e97f1f5..1eb33cbe275 100644 --- a/packages/thirdweb/src/wallets/smart/is-smart-wallet.test.ts +++ b/packages/thirdweb/src/wallets/smart/is-smart-wallet.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from "vitest"; import type { Wallet } from "../interfaces/wallet.js"; -import { isSmartWallet } from "./is-smart-wallet.js"; +import { + hasSponsoredTransactionsEnabled, + isSmartWallet, +} from "./is-smart-wallet.js"; describe("isSmartWallet", () => { it("should return true for smart wallet ID", () => { @@ -22,6 +25,34 @@ describe("isSmartWallet", () => { expect(isSmartWallet(wallet)).toBe(true); }); + it("should return true for wallet with EIP7702 config", () => { + const wallet = { + id: "inApp", + getConfig: () => ({ + executionMode: { + mode: "EIP7702", + }, + }), + } as Wallet; + expect(isSmartWallet(wallet)).toBe(true); + }); + + it("should return true for wallet with EIP4337 config", () => { + const wallet = { + id: "inApp", + getConfig: () => ({ + executionMode: { + mode: "EIP4337", + smartAccount: { + chain: { id: 1, name: "test", rpc: "test" }, + sponsorGas: true, + }, + }, + }), + } as Wallet; + expect(isSmartWallet(wallet)).toBe(true); + }); + it("should return false for non-smart wallet", () => { const wallet = { id: "inApp", @@ -56,3 +87,134 @@ describe("isSmartWallet", () => { expect(isSmartWallet(wallet)).toBe(false); }); }); + +describe("hasSponsoredTransactionsEnabled", () => { + it("should return false for undefined wallet", () => { + expect(hasSponsoredTransactionsEnabled(undefined)).toBe(false); + }); + + it("should handle smart wallet with sponsorGas config", () => { + const mockSmartWallet = { + id: "smart", + getConfig: () => ({ sponsorGas: true }), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockSmartWallet)).toBe(true); + + const mockSmartWalletDisabled = { + id: "smart", + getConfig: () => ({ sponsorGas: false }), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockSmartWalletDisabled)).toBe( + false, + ); + }); + + it("should handle smart wallet with gasless config", () => { + const mockSmartWallet = { + id: "smart", + getConfig: () => ({ gasless: true }), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockSmartWallet)).toBe(true); + }); + + it("should handle inApp wallet with smartAccount config", () => { + const mockInAppWallet = { + id: "inApp", + getConfig: () => ({ + smartAccount: { + sponsorGas: true, + }, + }), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockInAppWallet)).toBe(true); + + const mockInAppWalletDisabled = { + id: "inApp", + getConfig: () => ({ + smartAccount: { + sponsorGas: false, + }, + }), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockInAppWalletDisabled)).toBe( + false, + ); + }); + + it("should handle inApp wallet with EIP7702 config", () => { + const mockInAppWallet = { + id: "inApp", + getConfig: () => ({ + executionMode: { + mode: "EIP7702", + sponsorGas: true, + }, + }), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockInAppWallet)).toBe(true); + + const mockInAppWalletDisabled = { + id: "inApp", + getConfig: () => ({ + executionMode: { + mode: "EIP7702", + }, + }), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockInAppWalletDisabled)).toBe( + false, + ); + }); + + it("should handle inApp wallet with EIP4337 config", () => { + const mockInAppWallet = { + id: "inApp", + getConfig: () => ({ + executionMode: { + mode: "EIP4337", + smartAccount: { + chain: { id: 1, name: "test", rpc: "test" }, + sponsorGas: true, + }, + }, + }), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockInAppWallet)).toBe(true); + + const mockInAppWalletDisabled = { + id: "inApp", + getConfig: () => ({ + executionMode: { + mode: "EIP4337", + smartAccount: { + chain: { id: 1, name: "test", rpc: "test" }, + sponsorGas: false, + }, + }, + }), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockInAppWalletDisabled)).toBe( + false, + ); + }); + + it("should handle inApp wallet with gasless config", () => { + const mockInAppWallet = { + id: "inApp", + getConfig: () => ({ + smartAccount: { + gasless: true, + }, + }), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockInAppWallet)).toBe(true); + }); + + it("should return false for regular wallet without smart account config", () => { + const mockRegularWallet = { + id: "inApp", + getConfig: () => ({}), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockRegularWallet)).toBe(false); + }); +}); diff --git a/packages/thirdweb/src/wallets/smart/is-smart-wallet.ts b/packages/thirdweb/src/wallets/smart/is-smart-wallet.ts index abc97bb0210..07c2752058d 100644 --- a/packages/thirdweb/src/wallets/smart/is-smart-wallet.ts +++ b/packages/thirdweb/src/wallets/smart/is-smart-wallet.ts @@ -1,3 +1,4 @@ +import { isEcosystemWallet } from "../ecosystem/is-ecosystem-wallet.js"; import type { Wallet } from "../interfaces/wallet.js"; /** @@ -7,15 +8,71 @@ import type { Wallet } from "../interfaces/wallet.js"; * @returns {boolean} True if the wallet is a smart wallet, false otherwise. * @internal */ -export function isSmartWallet(wallet: Wallet): boolean { - if (wallet.id === "smart") { - return true; +export function isSmartWallet(activeWallet?: Wallet): boolean { + if (!activeWallet) { + return false; } - const config = wallet.getConfig(); - if (!!config && "smartAccount" in config && !!config.smartAccount) { + if (activeWallet.id === "smart") { return true; } + if (activeWallet.id === "inApp" || isEcosystemWallet(activeWallet)) { + const options = (activeWallet as Wallet<"inApp">).getConfig(); + if (options && "smartAccount" in options && options.smartAccount) { + return true; + } + if (options?.executionMode) { + const execMode = options.executionMode; + return execMode.mode === "EIP4337" || execMode.mode === "EIP7702"; + } + } return false; } + +/** + * @internal + */ +export function hasSponsoredTransactionsEnabled(wallet: Wallet | undefined) { + if (!wallet) { + return false; + } + let sponsoredTransactionsEnabled = false; + if (wallet && wallet.id === "smart") { + const options = (wallet as Wallet<"smart">).getConfig(); + if ("sponsorGas" in options) { + sponsoredTransactionsEnabled = options.sponsorGas; + } + if ("gasless" in options) { + sponsoredTransactionsEnabled = options.gasless; + } + } + if (wallet && (wallet.id === "inApp" || isEcosystemWallet(wallet))) { + const options = (wallet as Wallet<"inApp">).getConfig(); + if (options && "smartAccount" in options && options.smartAccount) { + const smartOptions = options.smartAccount; + if ("sponsorGas" in smartOptions) { + sponsoredTransactionsEnabled = smartOptions.sponsorGas; + } + if ("gasless" in smartOptions) { + sponsoredTransactionsEnabled = smartOptions.gasless; + } + } + if (options?.executionMode) { + const execMode = options.executionMode; + if (execMode.mode === "EIP4337") { + const smartOptions = execMode.smartAccount; + if (smartOptions && "sponsorGas" in smartOptions) { + sponsoredTransactionsEnabled = smartOptions.sponsorGas; + } + if (smartOptions && "gasless" in smartOptions) { + sponsoredTransactionsEnabled = smartOptions.gasless; + } + } + if (execMode.mode === "EIP7702") { + sponsoredTransactionsEnabled = execMode.sponsorGas || false; + } + } + } + return sponsoredTransactionsEnabled; +}