From e449bd803fde51de525ea7846d18c599f6541d44 Mon Sep 17 00:00:00 2001 From: Enes Date: Fri, 26 Jun 2026 16:10:40 +0300 Subject: [PATCH 1/5] feat: expose headless wallet list imperatively on the AppKit client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The headless wallet flow (list/search/paginate WalletGuide wallets, fetch the WC URI, and programmatic connect for injected/API/mobile wallets) lived only inside the `useAppKitWallets` React hook, so non-React hosts couldn't drive it. Lift the imperative core into a new `HeadlessWalletUtil` (@reown/appkit-controllers) and expose it on the AppKit client: - AppKit instance methods: fetchWallets / getWalletList / subscribeWalletList / getWalletConnectUri / connectWallet. - HeadlessWalletUtil holds the shared logic (reads/writes the global controllers, no AppKit instance — same pattern as ConnectionControllerUtil), so the client methods and the React hook share one code path. Follow-up: refactor useAppKitWallets to delegate to HeadlessWalletUtil (dedup); kept out of this change to keep the diff additive. + HeadlessWalletUtil unit tests, changeset. Co-Authored-By: Claude Opus 4.8 --- .changeset/headless-wallets-on-client.md | 8 + .../appkit/src/client/appkit-base-client.ts | 48 ++++ packages/controllers/exports/index.ts | 6 + .../src/utils/HeadlessWalletUtil.ts | 204 ++++++++++++++++ .../tests/utils/HeadlessWalletUtil.test.ts | 226 ++++++++++++++++++ 5 files changed, 492 insertions(+) create mode 100644 .changeset/headless-wallets-on-client.md create mode 100644 packages/controllers/src/utils/HeadlessWalletUtil.ts create mode 100644 packages/controllers/tests/utils/HeadlessWalletUtil.test.ts diff --git a/.changeset/headless-wallets-on-client.md b/.changeset/headless-wallets-on-client.md new file mode 100644 index 0000000000..9e40782778 --- /dev/null +++ b/.changeset/headless-wallets-on-client.md @@ -0,0 +1,8 @@ +--- +'@reown/appkit-controllers': minor +'@reown/appkit': minor +--- + +Expose the headless wallet list imperatively on the AppKit client, so a non-React host can list / search / connect wallets without the `useAppKitWallets` React hook. + +New `AppKit` instance methods: `fetchWallets(options?)`, `getWalletList()`, `subscribeWalletList(cb)`, `getWalletConnectUri(options?)`, and `connectWallet(wallet, namespace?, options?)`. The shared imperative logic lives in a new `HeadlessWalletUtil` (`@reown/appkit-controllers`), which both the client methods and the React hook can use — one tested code path for headless wallet listing, search, pagination, the WalletConnect URI, and programmatic connect (injected / API / mobile-deeplink). diff --git a/packages/appkit/src/client/appkit-base-client.ts b/packages/appkit/src/client/appkit-base-client.ts index 7e702aaa44..54c10d7a50 100644 --- a/packages/appkit/src/client/appkit-base-client.ts +++ b/packages/appkit/src/client/appkit-base-client.ts @@ -21,12 +21,14 @@ import type { ConnectExternalOptions, ConnectMethod, ConnectedWalletInfo, + ConnectOptions, ConnectionControllerClient, ConnectionControllerState, ConnectorType, EstimateGasTransactionArgs, EventsControllerState, Features, + FetchWalletsOptions, ModalControllerState, NamespaceTypeMap, OptionsControllerState, @@ -41,6 +43,8 @@ import type { UseAppKitNetworkReturn, User, WalletFeature, + WalletItem, + WalletListSnapshot, WriteContractArgs, WriteSolanaTransactionArgs } from '@reown/appkit-controllers' @@ -59,6 +63,7 @@ import { CoreHelperUtil, EnsController, EventsController, + HeadlessWalletUtil, ModalController, OnRampController, OptionsController, @@ -2317,6 +2322,49 @@ export abstract class AppKitBaseClient { await ConnectionController.disconnect({ namespace: chainNamespace }) } + // -- Headless wallet list ------------------------------------------------ // + // Imperative counterparts of the `useAppKitWallets` React hook, so a non-React host + // (e.g. `@walletconnect/pay-appkit`) can list / search / connect wallets headlessly + // through the AppKit instance. Both share the same code path via `HeadlessWalletUtil`. + + /** + * Fetch / search / paginate the WalletConnect wallet list (WalletGuide explorer). Read + * the results with {@link getWalletList}; subscribe with {@link subscribeWalletList}. + */ + public async fetchWallets(options?: FetchWalletsOptions) { + await HeadlessWalletUtil.fetchWallets(options) + } + + /** The current headless wallet list (initial view + WalletConnect list + pagination). */ + public getWalletList(): WalletListSnapshot { + return HeadlessWalletUtil.getWalletList() + } + + /** Subscribe to wallet-list changes. Returns an unsubscribe. */ + public subscribeWalletList(callback: () => void) { + return HeadlessWalletUtil.subscribeWalletList(callback) + } + + /** + * Pre-fetch the WalletConnect URI (read from {@link getState} / `subscribeConnections`). + * Call when a wallet is selected so a later connect can deeplink synchronously (iOS). + */ + public async getWalletConnectUri(options?: ConnectOptions) { + await HeadlessWalletUtil.getWalletConnectUri(options) + } + + /** + * Connect a chosen wallet programmatically (headless — no modal). Handles injected, + * API ("all wallets"), and mobile-deeplink wallets. + */ + public async connectWallet( + wallet: WalletItem, + namespace?: ChainNamespace, + options?: ConnectOptions + ) { + await HeadlessWalletUtil.connect(wallet, namespace, options) + } + public getSIWX() { return OptionsController.state.siwx as SIWXConfigInterface | undefined } diff --git a/packages/controllers/exports/index.ts b/packages/controllers/exports/index.ts index 5677558ee6..ecc8984c43 100644 --- a/packages/controllers/exports/index.ts +++ b/packages/controllers/exports/index.ts @@ -130,5 +130,11 @@ export { MobileWalletUtil } from '../src/utils/MobileWallet.js' export type * from '../src/utils/TypeUtil.js' export type * from '../src/utils/SIWXUtil.js' export type { WalletItem } from '../src/utils/ConnectUtil.js' +export { HeadlessWalletUtil } from '../src/utils/HeadlessWalletUtil.js' +export type { + ConnectOptions, + FetchWalletsOptions, + WalletListSnapshot +} from '../src/utils/HeadlessWalletUtil.js' export * from '../src/utils/ChainControllerUtil.js' export * from '../src/utils/WalletConnectUtil.js' diff --git a/packages/controllers/src/utils/HeadlessWalletUtil.ts b/packages/controllers/src/utils/HeadlessWalletUtil.ts new file mode 100644 index 0000000000..14dd9cb091 --- /dev/null +++ b/packages/controllers/src/utils/HeadlessWalletUtil.ts @@ -0,0 +1,204 @@ +import { subscribeKey as subKey } from 'valtio/vanilla/utils' + +import type { ChainNamespace } from '@reown/appkit-common' + +import { ApiController } from '../controllers/ApiController.js' +import { ChainController } from '../controllers/ChainController.js' +import { ConnectionController } from '../controllers/ConnectionController.js' +import { ConnectorController } from '../controllers/ConnectorController.js' +import { OptionsController } from '../controllers/OptionsController.js' +import { PublicStateController } from '../controllers/PublicStateController.js' +import { ConnectUtil, type WalletItem } from './ConnectUtil.js' +import { ConnectionControllerUtil } from './ConnectionControllerUtil.js' +import { ConnectorControllerUtil } from './ConnectorControllerUtil.js' +import { CoreHelperUtil } from './CoreHelperUtil.js' +import { MobileWalletUtil } from './MobileWallet.js' +import type { WcWallet } from './TypeUtil.js' + +// -- Types ------------------------------------------------------------------ // + +/** Options for {@link HeadlessWalletUtil.fetchWallets}. */ +export interface FetchWalletsOptions { + /** Page number to fetch (default: 1) */ + page?: number + /** @deprecated Use `search` instead */ + query?: string + /** Search query to filter wallets. When provided, switches to search mode. */ + search?: string + /** Number of entries per page. Defaults to 40 for list mode, 100 for search mode. */ + entries?: number + /** Filter wallets by badge type ('none' | 'certified') */ + badge?: 'none' | 'certified' + /** Wallet IDs to include. Overrides the global includeWalletIds config when provided. */ + include?: string[] + /** Wallet IDs to exclude. Overrides the default exclude list when provided. */ + exclude?: string[] + /** + * Include wallets that support WalletConnect Pay but are not v2-compatible. + * By default these are filtered out. Enable for WalletConnect Pay surfaces. + */ + includePayOnly?: boolean + /** Sort mode. 'wcpay' bubbles WalletConnect Pay-supporting wallets to the top. */ + sort?: 'default' | 'wcpay' +} + +/** Options for {@link HeadlessWalletUtil.connect} / {@link HeadlessWalletUtil.getWalletConnectUri}. */ +export interface ConnectOptions { + /** + * A WalletConnect Pay deeplink appended to the WC URI, so a WCPay-capable wallet + * returns to — and processes — this payment after pairing. + */ + wcPayUrl?: string +} + +/** The headless wallet list, read imperatively. */ +export interface WalletListSnapshot { + /** Initial connect-view wallets: installed extensions + top-ranked WalletConnect wallets. */ + wallets: WalletItem[] + /** The full WalletGuide WalletConnect list (with the current search results applied). */ + wcWallets: WalletItem[] + /** Current page of the WalletConnect list. */ + page: number + /** Total number of available WalletConnect wallets for the current parameters. */ + count: number +} + +/** + * Framework-agnostic headless wallet-list logic — the imperative core behind the + * `useAppKitWallets` React hook and the `AppKit` client's wallet methods. + * + * AppKit runs headless (no modal): the host renders its own picker and connects a + * chosen wallet programmatically. That orchestration (list / search / paginate the + * WalletGuide wallets, fetch the WalletConnect URI, and connect injected / API / mobile + * wallets) lived only inside the React hook; lifting it here lets a non-React host + * (e.g. `@walletconnect/pay-appkit`) drive the same flow through `appKit.*` methods, + * with the hook and the client sharing one tested code path. + * + * Reads/writes the global controllers directly (valtio singletons), so it takes no + * AppKit instance — exactly like {@link ConnectionControllerUtil}. + */ +export const HeadlessWalletUtil = { + /** + * Fetch / search / paginate the WalletConnect wallet list from the explorer API. + * With a `search` (or the deprecated `query`), switches to search mode; otherwise + * lists/paginates. Reads results from `ApiController.state` (see {@link getWalletList}). + */ + async fetchWallets(fetchOptions?: FetchWalletsOptions): Promise { + const { query, ...options } = fetchOptions ?? {} + const search = options.search ?? query + + if (search) { + await ApiController.searchWallet({ ...options, search }) + } else { + ApiController.state.search = [] + await ApiController.fetchWalletsByPage({ page: 1, ...options }) + } + }, + + /** Read the current wallet list (initial view + WalletConnect list + pagination). */ + getWalletList(): WalletListSnapshot { + return { + wallets: ConnectUtil.getInitialWallets(), + wcWallets: ConnectUtil.getWalletConnectWallets( + ApiController.state.wallets as WcWallet[], + ApiController.state.search as WcWallet[] + ), + page: ApiController.state.page, + count: ApiController.state.count + } + }, + + /** Subscribe to wallet-list changes (the WalletGuide list + search results). */ + subscribeWalletList(callback: () => void): () => void { + const unsubscribers = [ + subKey(ApiController.state, 'wallets', callback), + subKey(ApiController.state, 'search', callback), + subKey(ApiController.state, 'page', callback), + subKey(ApiController.state, 'count', callback) + ] + + return () => unsubscribers.forEach(unsubscribe => unsubscribe()) + }, + + /** + * Pre-fetch the WalletConnect URI (read from `ConnectionController.state.wcUri`). + * Call when a wallet is selected so a later connect can deeplink synchronously + * (required for iOS Safari). Uses 'auto' cache to reuse a valid URI or fetch a new one. + */ + async getWalletConnectUri(_options?: ConnectOptions): Promise { + this.resetWcUri() + await ConnectionController.connectWalletConnect({ cache: 'auto' }) + }, + + /** + * Connect a chosen wallet programmatically (headless — no modal). + * + * Handles injected wallets, API wallets (from the "all wallets" list), and mobile + * deeplinks. For API wallets without pre-populated connectors, falls back to a lookup + * by the wallet's ID via `explorerId` matching (e.g. Coinbase/Base from "all wallets"). + */ + async connect( + wallet: WalletItem, + namespace?: ChainNamespace, + options?: ConnectOptions + ): Promise { + PublicStateController.set({ connectingWallet: wallet }) + const isMobileDevice = CoreHelperUtil.isMobile() + + // Fall back to the active chain if no namespace is given (matches headful behavior). + const activeNamespace = namespace || ChainController.state.activeChain + + try { + const walletConnector = wallet?.connectors.find(c => c.chain === activeNamespace) + + const connector = + walletConnector && activeNamespace + ? ConnectorController.getConnector({ + id: walletConnector?.id, + namespace: activeNamespace + }) + : undefined + + // Fallback connector lookup for API wallets (empty `connectors`): find a connector + // by the wallet's API id (getConnector matches both `c.id` and `c.explorerId`), so + // e.g. the Base Account connector opens Coinbase's web wallet instead of falling + // through to WalletConnect. Matches headful `ConnectorController.selectWalletConnector`. + const fallbackConnector = + !connector && activeNamespace + ? ConnectorController.getConnector({ id: wallet?.id, namespace: activeNamespace }) + : undefined + + if (wallet?.isInjected && connector) { + await ConnectorControllerUtil.connectExternal(connector) + } else if (fallbackConnector) { + await ConnectorControllerUtil.connectExternal(fallbackConnector) + } else if (isMobileDevice) { + const wcWallet = ConnectUtil.mapWalletItemToWcWallet(wallet) + + if (wcWallet.mobile_link) { + ConnectionControllerUtil.onConnectMobile(wcWallet, options?.wcPayUrl) + } else { + MobileWalletUtil.handleMobileDeeplinkRedirect(wallet.id, activeNamespace, { + isCoinbaseDisabled: OptionsController.state.enableCoinbase === false + }) + } + } else { + await ConnectionController.connectWalletConnect({ cache: 'never' }) + } + } catch (error) { + PublicStateController.set({ connectingWallet: undefined }) + throw error + } + }, + + /** Reset the WalletConnect URI + linking state (e.g. when a QR is closed). */ + resetWcUri(): void { + ConnectionController.resetUri() + ConnectionController.setWcLinking(undefined) + }, + + /** Clear the `connectingWallet` state. */ + resetConnectingWallet(): void { + PublicStateController.set({ connectingWallet: undefined }) + } +} diff --git a/packages/controllers/tests/utils/HeadlessWalletUtil.test.ts b/packages/controllers/tests/utils/HeadlessWalletUtil.test.ts new file mode 100644 index 0000000000..147b535ec7 --- /dev/null +++ b/packages/controllers/tests/utils/HeadlessWalletUtil.test.ts @@ -0,0 +1,226 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import type { ChainNamespace } from '@reown/appkit-common' + +import { ApiController } from '../../src/controllers/ApiController.js' +import { ChainController } from '../../src/controllers/ChainController.js' +import { ConnectionController } from '../../src/controllers/ConnectionController.js' +import { ConnectorController } from '../../src/controllers/ConnectorController.js' +import { OptionsController } from '../../src/controllers/OptionsController.js' +import { PublicStateController } from '../../src/controllers/PublicStateController.js' +import { ConnectUtil, type WalletItem } from '../../src/utils/ConnectUtil.js' +import { ConnectionControllerUtil } from '../../src/utils/ConnectionControllerUtil.js' +import { ConnectorControllerUtil } from '../../src/utils/ConnectorControllerUtil.js' +import { CoreHelperUtil } from '../../src/utils/CoreHelperUtil.js' +import { HeadlessWalletUtil } from '../../src/utils/HeadlessWalletUtil.js' +import { MobileWalletUtil } from '../../src/utils/MobileWallet.js' + +const injectedWallet: WalletItem = { + id: 'mm', + name: 'MetaMask', + imageUrl: 'icon', + connectors: [{ id: 'io.metamask', chain: 'eip155' }], + walletInfo: {}, + isInjected: true, + isRecent: false +} + +const apiWallet: WalletItem = { + id: 'wc_wallet', + name: 'Some Wallet', + imageUrl: 'icon', + connectors: [], + walletInfo: {}, + isInjected: false, + isRecent: false +} + +beforeEach(() => { + vi.restoreAllMocks() + vi.spyOn(ChainController, 'state', 'get').mockReturnValue({ + activeChain: 'eip155' + } as never) + vi.spyOn(PublicStateController, 'set').mockImplementation(vi.fn()) +}) + +describe('HeadlessWalletUtil.fetchWallets', () => { + it('searches when a search term is given', async () => { + const searchWallet = vi.spyOn(ApiController, 'searchWallet').mockResolvedValue(undefined as never) + const fetchByPage = vi.spyOn(ApiController, 'fetchWalletsByPage') + + await HeadlessWalletUtil.fetchWallets({ search: 'rainbow', includePayOnly: true }) + + expect(searchWallet).toHaveBeenCalledWith({ includePayOnly: true, search: 'rainbow' }) + expect(fetchByPage).not.toHaveBeenCalled() + }) + + it('maps the deprecated `query` to `search`', async () => { + const searchWallet = vi.spyOn(ApiController, 'searchWallet').mockResolvedValue(undefined as never) + + await HeadlessWalletUtil.fetchWallets({ query: 'rbw' }) + + expect(searchWallet).toHaveBeenCalledWith({ search: 'rbw' }) + }) + + it('lists from page 1 and clears prior search when no search term', async () => { + const fetchByPage = vi + .spyOn(ApiController, 'fetchWalletsByPage') + .mockResolvedValue(undefined as never) + ApiController.state.search = [{ id: 'stale' }] as never + + await HeadlessWalletUtil.fetchWallets({ sort: 'wcpay' }) + + expect(ApiController.state.search).toEqual([]) + expect(fetchByPage).toHaveBeenCalledWith({ page: 1, sort: 'wcpay' }) + }) +}) + +describe('HeadlessWalletUtil.getWalletList', () => { + it('projects the initial + WalletConnect lists with pagination', () => { + vi.spyOn(ConnectUtil, 'getInitialWallets').mockReturnValue([injectedWallet]) + vi.spyOn(ConnectUtil, 'getWalletConnectWallets').mockReturnValue([apiWallet]) + ApiController.state.page = 2 + ApiController.state.count = 99 + + expect(HeadlessWalletUtil.getWalletList()).toEqual({ + wallets: [injectedWallet], + wcWallets: [apiWallet], + page: 2, + count: 99 + }) + }) +}) + +describe('HeadlessWalletUtil.connect', () => { + it('connects an injected wallet via its connector', async () => { + const connector = { id: 'io.metamask' } + vi.spyOn(ConnectorController, 'getConnector').mockReturnValue(connector as never) + const connectExternal = vi + .spyOn(ConnectorControllerUtil, 'connectExternal') + .mockResolvedValue(undefined as never) + + await HeadlessWalletUtil.connect(injectedWallet, 'eip155') + + expect(PublicStateController.set).toHaveBeenCalledWith({ connectingWallet: injectedWallet }) + expect(connectExternal).toHaveBeenCalledWith(connector) + }) + + it('falls back to a connector found by wallet id (API wallet)', async () => { + const fallback = { id: 'baseAccount', explorerId: 'wc_wallet' } + // First lookup (by connector) → none; fallback lookup (by wallet id) → fallback. + vi.spyOn(ConnectorController, 'getConnector') + .mockReturnValueOnce(undefined as never) + .mockReturnValueOnce(fallback as never) + const connectExternal = vi + .spyOn(ConnectorControllerUtil, 'connectExternal') + .mockResolvedValue(undefined as never) + + await HeadlessWalletUtil.connect(apiWallet, 'eip155') + + expect(connectExternal).toHaveBeenCalledWith(fallback) + }) + + it('deeplinks on mobile when the wallet declares a mobile link', async () => { + vi.spyOn(ConnectorController, 'getConnector').mockReturnValue(undefined as never) + vi.spyOn(CoreHelperUtil, 'isMobile').mockReturnValue(true) + vi.spyOn(ConnectUtil, 'mapWalletItemToWcWallet').mockReturnValue({ + id: 'wc_wallet', + name: 'Some Wallet', + mobile_link: 'somewallet://' + } as never) + const onConnectMobile = vi + .spyOn(ConnectionControllerUtil, 'onConnectMobile') + .mockImplementation(vi.fn()) + + await HeadlessWalletUtil.connect(apiWallet, 'eip155', { wcPayUrl: 'https://pay/x' }) + + expect(onConnectMobile).toHaveBeenCalledWith( + expect.objectContaining({ mobile_link: 'somewallet://' }), + 'https://pay/x' + ) + }) + + it('redirects via MobileWalletUtil on mobile when there is no mobile link', async () => { + vi.spyOn(ConnectorController, 'getConnector').mockReturnValue(undefined as never) + vi.spyOn(CoreHelperUtil, 'isMobile').mockReturnValue(true) + vi.spyOn(ConnectUtil, 'mapWalletItemToWcWallet').mockReturnValue({ + id: 'wc_wallet', + name: 'Some Wallet' + } as never) + vi.spyOn(OptionsController, 'state', 'get').mockReturnValue({ enableCoinbase: true } as never) + const redirect = vi + .spyOn(MobileWalletUtil, 'handleMobileDeeplinkRedirect') + .mockImplementation(vi.fn()) + + await HeadlessWalletUtil.connect(apiWallet, 'eip155') + + expect(redirect).toHaveBeenCalledWith('wc_wallet', 'eip155', { isCoinbaseDisabled: false }) + }) + + it('falls back to WalletConnect on desktop', async () => { + vi.spyOn(ConnectorController, 'getConnector').mockReturnValue(undefined as never) + vi.spyOn(CoreHelperUtil, 'isMobile').mockReturnValue(false) + const connectWc = vi + .spyOn(ConnectionController, 'connectWalletConnect') + .mockResolvedValue(undefined as never) + + await HeadlessWalletUtil.connect(apiWallet, 'eip155') + + expect(connectWc).toHaveBeenCalledWith({ cache: 'never' }) + }) + + it('clears connectingWallet and rethrows on failure', async () => { + vi.spyOn(ConnectorController, 'getConnector').mockReturnValue({ id: 'io.metamask' } as never) + vi.spyOn(ConnectorControllerUtil, 'connectExternal').mockRejectedValue(new Error('boom')) + + await expect(HeadlessWalletUtil.connect(injectedWallet, 'eip155')).rejects.toThrow('boom') + expect(PublicStateController.set).toHaveBeenLastCalledWith({ connectingWallet: undefined }) + }) + + it('falls back to the active chain when no namespace is given', async () => { + const getConnector = vi + .spyOn(ConnectorController, 'getConnector') + .mockReturnValue({ id: 'io.metamask' } as never) + vi.spyOn(ConnectorControllerUtil, 'connectExternal').mockResolvedValue(undefined as never) + + await HeadlessWalletUtil.connect(injectedWallet) + + expect(getConnector).toHaveBeenCalledWith( + expect.objectContaining({ namespace: 'eip155' as ChainNamespace }) + ) + }) +}) + +describe('HeadlessWalletUtil.getWalletConnectUri', () => { + it('resets then fetches the URI with auto cache', async () => { + const resetUri = vi.spyOn(ConnectionController, 'resetUri').mockImplementation(vi.fn()) + const setWcLinking = vi.spyOn(ConnectionController, 'setWcLinking').mockImplementation(vi.fn()) + const connectWc = vi + .spyOn(ConnectionController, 'connectWalletConnect') + .mockResolvedValue(undefined as never) + + await HeadlessWalletUtil.getWalletConnectUri() + + expect(resetUri).toHaveBeenCalled() + expect(setWcLinking).toHaveBeenCalledWith(undefined) + expect(connectWc).toHaveBeenCalledWith({ cache: 'auto' }) + }) +}) + +describe('HeadlessWalletUtil resets', () => { + it('resetWcUri resets the URI + linking', () => { + const resetUri = vi.spyOn(ConnectionController, 'resetUri').mockImplementation(vi.fn()) + const setWcLinking = vi.spyOn(ConnectionController, 'setWcLinking').mockImplementation(vi.fn()) + + HeadlessWalletUtil.resetWcUri() + + expect(resetUri).toHaveBeenCalled() + expect(setWcLinking).toHaveBeenCalledWith(undefined) + }) + + it('resetConnectingWallet clears the connecting wallet', () => { + HeadlessWalletUtil.resetConnectingWallet() + + expect(PublicStateController.set).toHaveBeenCalledWith({ connectingWallet: undefined }) + }) +}) From 572cf78ca8ed4ab5cbca656547af2a6fb2796c96 Mon Sep 17 00:00:00 2001 From: Enes Date: Fri, 26 Jun 2026 16:20:33 +0300 Subject: [PATCH 2/5] refactor: single-source ConnectOptions/FetchWalletsOptions + lint The hook redefined ConnectOptions/FetchWalletsOptions that now live in HeadlessWalletUtil; re-export them from there (one declaration) to resolve the ambiguous re-export in @reown/appkit's react build. Drop the now-unused BadgeType import + tidy comment/cast lint. Co-Authored-By: Claude Opus 4.8 --- packages/controllers/exports/react.ts | 37 +++++-------------- .../src/utils/HeadlessWalletUtil.ts | 15 ++++---- .../tests/utils/HeadlessWalletUtil.test.ts | 11 ++++-- 3 files changed, 24 insertions(+), 39 deletions(-) diff --git a/packages/controllers/exports/react.ts b/packages/controllers/exports/react.ts index 35b3fa9ac2..f86c8e5227 100644 --- a/packages/controllers/exports/react.ts +++ b/packages/controllers/exports/react.ts @@ -23,9 +23,12 @@ import { ConnectUtil, type WalletItem } from '../src/utils/ConnectUtil.js' import { ConnectionControllerUtil } from '../src/utils/ConnectionControllerUtil.js' import { ConnectorControllerUtil } from '../src/utils/ConnectorControllerUtil.js' import { CoreHelperUtil } from '../src/utils/CoreHelperUtil.js' +import { + type ConnectOptions, + type FetchWalletsOptions +} from '../src/utils/HeadlessWalletUtil.js' import { MobileWalletUtil } from '../src/utils/MobileWallet.js' import type { - BadgeType, UseAppKitAccountReturn, UseAppKitNetworkReturn, WcWallet @@ -62,9 +65,11 @@ interface DeleteRecentConnectionProps { connectorId: string } -export interface ConnectOptions { - wcPayUrl?: string -} +/* + * `ConnectOptions` / `FetchWalletsOptions` are defined once in `HeadlessWalletUtil` + * (the framework-neutral home) and re-exported here for the hook's public surface. + */ +export type { ConnectOptions, FetchWalletsOptions } from '../src/utils/HeadlessWalletUtil.js' // -- Hooks ------------------------------------------------------------ export function useAppKitProvider(chainNamespace: ChainNamespace) { @@ -310,30 +315,6 @@ export function useAppKitConnection({ namespace, onSuccess, onError }: UseAppKit } } -export interface FetchWalletsOptions { - /** Page number to fetch (default: 1) */ - page?: number - /** @deprecated Use `search` instead */ - query?: string - /** Search query to filter wallets. When provided, switches to search mode. */ - search?: string - /** Number of entries per page. Defaults to 40 for list mode, 100 for search mode. */ - entries?: number - /** Filter wallets by badge type ('none' | 'certified') */ - badge?: BadgeType - /** Wallet IDs to include. Overrides the global includeWalletIds config when provided. */ - include?: string[] - /** Wallet IDs to exclude. Overrides the default exclude list when provided. */ - exclude?: string[] - /** - * Include wallets that support WalletConnect Pay but are not v2-compatible. - * By default these are filtered out. Enable for WalletConnect Pay surfaces. - */ - includePayOnly?: boolean - /** Sort mode. 'wcpay' bubbles WalletConnect Pay-supporting wallets to the top. */ - sort?: 'default' | 'wcpay' -} - export interface UseAppKitWalletsReturn { /** * List of wallets for the initial connect view including WalletConnect wallet and injected wallets together. If user doesn't have any injected wallets, it'll fill the list with most ranked WalletConnect wallets. diff --git a/packages/controllers/src/utils/HeadlessWalletUtil.ts b/packages/controllers/src/utils/HeadlessWalletUtil.ts index 14dd9cb091..1ecf1c3dd7 100644 --- a/packages/controllers/src/utils/HeadlessWalletUtil.ts +++ b/packages/controllers/src/utils/HeadlessWalletUtil.ts @@ -13,7 +13,6 @@ import { ConnectionControllerUtil } from './ConnectionControllerUtil.js' import { ConnectorControllerUtil } from './ConnectorControllerUtil.js' import { CoreHelperUtil } from './CoreHelperUtil.js' import { MobileWalletUtil } from './MobileWallet.js' -import type { WcWallet } from './TypeUtil.js' // -- Types ------------------------------------------------------------------ // @@ -100,8 +99,8 @@ export const HeadlessWalletUtil = { return { wallets: ConnectUtil.getInitialWallets(), wcWallets: ConnectUtil.getWalletConnectWallets( - ApiController.state.wallets as WcWallet[], - ApiController.state.search as WcWallet[] + ApiController.state.wallets, + ApiController.state.search ), page: ApiController.state.page, count: ApiController.state.count @@ -159,10 +158,12 @@ export const HeadlessWalletUtil = { }) : undefined - // Fallback connector lookup for API wallets (empty `connectors`): find a connector - // by the wallet's API id (getConnector matches both `c.id` and `c.explorerId`), so - // e.g. the Base Account connector opens Coinbase's web wallet instead of falling - // through to WalletConnect. Matches headful `ConnectorController.selectWalletConnector`. + /* + * Fallback connector lookup for API wallets (empty `connectors`): find a connector + * by the wallet's API id (getConnector matches both `c.id` and `c.explorerId`), so + * e.g. the Base Account connector opens Coinbase's web wallet instead of falling + * through to WalletConnect. Matches headful `ConnectorController.selectWalletConnector`. + */ const fallbackConnector = !connector && activeNamespace ? ConnectorController.getConnector({ id: wallet?.id, namespace: activeNamespace }) diff --git a/packages/controllers/tests/utils/HeadlessWalletUtil.test.ts b/packages/controllers/tests/utils/HeadlessWalletUtil.test.ts index 147b535ec7..b7e547f8fc 100644 --- a/packages/controllers/tests/utils/HeadlessWalletUtil.test.ts +++ b/packages/controllers/tests/utils/HeadlessWalletUtil.test.ts @@ -107,16 +107,19 @@ describe('HeadlessWalletUtil.connect', () => { it('falls back to a connector found by wallet id (API wallet)', async () => { const fallback = { id: 'baseAccount', explorerId: 'wc_wallet' } - // First lookup (by connector) → none; fallback lookup (by wallet id) → fallback. - vi.spyOn(ConnectorController, 'getConnector') - .mockReturnValueOnce(undefined as never) - .mockReturnValueOnce(fallback as never) + // apiWallet has no connectors, so the first (guarded) lookup is skipped — only the + // fallback lookup by wallet id runs, and it resolves the Base Account connector. + const getConnector = vi + .spyOn(ConnectorController, 'getConnector') + .mockReturnValue(fallback as never) const connectExternal = vi .spyOn(ConnectorControllerUtil, 'connectExternal') .mockResolvedValue(undefined as never) await HeadlessWalletUtil.connect(apiWallet, 'eip155') + expect(getConnector).toHaveBeenCalledTimes(1) + expect(getConnector).toHaveBeenCalledWith({ id: 'wc_wallet', namespace: 'eip155' }) expect(connectExternal).toHaveBeenCalledWith(fallback) }) From d5d9655d2983bbc11a4a09dee2e88bd63d507abb Mon Sep 17 00:00:00 2001 From: Enes Date: Fri, 26 Jun 2026 16:28:35 +0300 Subject: [PATCH 3/5] chore: bump all packages in changeset + dogfood in examples/html-headless MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - changeset: list all packages (repo lockstep convention), per review. - examples/html-headless: a vanilla-JS wallet modal driven entirely by the new imperative client API — first page via appKit.fetchWallets({ page }), infinite loading on scroll (appends), live re-render via subscribeWalletList, and connectWallet on click. No React, no AppKit modal. Builds clean. Co-Authored-By: Claude Opus 4.8 --- .changeset/headless-wallets-on-client.md | 28 +++- examples/html-headless/index.html | 46 ++++++ examples/html-headless/package.json | 16 +++ examples/html-headless/src/main.css | 172 +++++++++++++++++++++++ examples/html-headless/src/main.js | 140 ++++++++++++++++++ pnpm-lock.yaml | 13 ++ 6 files changed, 413 insertions(+), 2 deletions(-) create mode 100644 examples/html-headless/index.html create mode 100644 examples/html-headless/package.json create mode 100644 examples/html-headless/src/main.css create mode 100644 examples/html-headless/src/main.js diff --git a/.changeset/headless-wallets-on-client.md b/.changeset/headless-wallets-on-client.md index 9e40782778..1769051e83 100644 --- a/.changeset/headless-wallets-on-client.md +++ b/.changeset/headless-wallets-on-client.md @@ -1,6 +1,30 @@ --- -'@reown/appkit-controllers': minor -'@reown/appkit': minor +'@reown/appkit-utils': patch +'@reown/appkit': patch +'@reown/appkit-cdn': patch +'@reown/appkit-cli': patch +'@reown/appkit-codemod': patch +'@reown/appkit-common': patch +'@reown/appkit-core': patch +'@reown/appkit-experimental': patch +'@reown/appkit-pay': patch +'@reown/appkit-polyfills': patch +'@reown/appkit-scaffold-ui': patch +'@reown/appkit-siwe': patch +'@reown/appkit-siwx': patch +'@reown/appkit-testing': patch +'@reown/appkit-ui': patch +'@reown/appkit-universal-connector': patch +'@reown/appkit-wallet-button': patch +'@reown/appkit-wallet': patch +'@reown/appkit-controllers': patch +'@reown/appkit-adapter-bitcoin': patch +'@reown/appkit-adapter-ethers': patch +'@reown/appkit-adapter-ethers5': patch +'@reown/appkit-adapter-solana': patch +'@reown/appkit-adapter-ton': patch +'@reown/appkit-adapter-tron': patch +'@reown/appkit-adapter-wagmi': patch --- Expose the headless wallet list imperatively on the AppKit client, so a non-React host can list / search / connect wallets without the `useAppKitWallets` React hook. diff --git a/examples/html-headless/index.html b/examples/html-headless/index.html new file mode 100644 index 0000000000..43bfa5ac39 --- /dev/null +++ b/examples/html-headless/index.html @@ -0,0 +1,46 @@ + + + + + + AppKit Headless (vanilla) Example + + + +
+

AppKit Headless — vanilla JS

+

+ A custom wallet modal driven entirely by the imperative AppKit client API (appKit.fetchWallets + / getWalletList / subscribeWalletList / + connectWallet) — no React, no AppKit modal. +

+ + + + + + + + + +
+ + + + diff --git a/examples/html-headless/package.json b/examples/html-headless/package.json new file mode 100644 index 0000000000..c91c909ccd --- /dev/null +++ b/examples/html-headless/package.json @@ -0,0 +1,16 @@ +{ + "name": "@examples/html-headless", + "private": true, + "version": "1.2.0", + "scripts": { + "dev": "vite --port 3013", + "build": "vite build" + }, + "dependencies": { + "@reown/appkit-adapter-wagmi": "workspace:*", + "@reown/appkit": "workspace:*" + }, + "devDependencies": { + "vite": "5.4.20" + } +} diff --git a/examples/html-headless/src/main.css b/examples/html-headless/src/main.css new file mode 100644 index 0000000000..4ec22e7442 --- /dev/null +++ b/examples/html-headless/src/main.css @@ -0,0 +1,172 @@ +:root { + --bg: #0b0d12; + --panel: #151821; + --border: #262b38; + --text: #e7e9ee; + --muted: #9aa3b2; + --accent: #3396ff; + font-family: + ui-sans-serif, + system-ui, + -apple-system, + sans-serif; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + background: var(--bg); + color: var(--text); + display: flex; + justify-content: center; + padding: 3rem 1rem; +} + +.page { + width: 100%; + max-width: 30rem; +} + +.title { + margin: 0; + font-size: 1.5rem; +} + +.subtitle { + color: var(--muted); + font-size: 0.9rem; + line-height: 1.5; +} + +.subtitle code { + color: var(--accent); + font-size: 0.82rem; +} + +.btn { + padding: 0.7rem 1.1rem; + border: 0; + border-radius: 10px; + background: var(--accent); + color: #fff; + font-size: 0.95rem; + cursor: pointer; +} + +.btn--outline { + background: transparent; + border: 1px solid var(--border); + color: var(--text); + padding: 0.4rem 0.75rem; + font-size: 0.85rem; +} + +.account { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.85rem 1rem; + margin-bottom: 1rem; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 12px; +} + +.account__address { + font-family: ui-monospace, monospace; +} + +/* -- Modal ----------------------------------------------------------------- */ +.modal { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; +} + +.modal__backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.55); +} + +.modal__card { + position: relative; + width: 100%; + max-width: 24rem; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 16px; + padding: 1.25rem; +} + +.modal__header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.75rem; +} + +.modal__header h2 { + margin: 0; + font-size: 1.1rem; +} + +.modal__close { + background: none; + border: 0; + color: var(--muted); + font-size: 1.4rem; + cursor: pointer; + line-height: 1; +} + +.wallet-list { + max-height: 22rem; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.wallet { + display: flex; + align-items: center; + gap: 0.75rem; + width: 100%; + padding: 0.6rem 0.7rem; + background: transparent; + border: 1px solid var(--border); + border-radius: 10px; + color: var(--text); + cursor: pointer; + font-size: 0.95rem; + text-align: left; +} + +.wallet:hover { + border-color: var(--accent); +} + +.wallet__icon { + width: 2rem; + height: 2rem; + border-radius: 8px; + object-fit: cover; + background: var(--bg); + flex-shrink: 0; +} + +.wallet-list__status { + margin: 0.75rem 0 0; + text-align: center; + font-size: 0.8rem; + color: var(--muted); +} diff --git a/examples/html-headless/src/main.js b/examples/html-headless/src/main.js new file mode 100644 index 0000000000..6b32ba28d2 --- /dev/null +++ b/examples/html-headless/src/main.js @@ -0,0 +1,140 @@ +import { createAppKit } from '@reown/appkit' +import { WagmiAdapter } from '@reown/appkit-adapter-wagmi' +import { mainnet, polygon } from '@reown/appkit/networks' + +// Public projectId for localhost only — create your own at https://dashboard.reown.com +const projectId = import.meta.env.VITE_PROJECT_ID || 'b56e18d47c72ab683b10814fe9495694' +const networks = [mainnet, polygon] + +const wagmiAdapter = new WagmiAdapter({ networks, projectId }) + +// Headless: no AppKit modal — this app renders its own wallet list and connects +// programmatically through the imperative client API. +const appKit = createAppKit({ + adapters: [wagmiAdapter], + networks, + projectId, + features: { headless: true }, + metadata: { + name: 'AppKit Headless HTML', + description: 'Headless wallet list driven by the imperative AppKit client API', + url: window.location.origin, + icons: [] + } +}) + +// -- DOM --------------------------------------------------------------------- // +const openBtn = document.getElementById('open') +const modal = document.getElementById('modal') +const listEl = document.getElementById('wallet-list') +const statusEl = document.getElementById('list-status') +const accountEl = document.getElementById('account') +const accountAddressEl = document.getElementById('account-address') +const disconnectBtn = document.getElementById('disconnect') + +// -- Wallet list (infinite loading) ----------------------------------------- // +let page = 0 +let loading = false + +/** Whether more pages are available (the explorer reports a higher total than loaded). */ +function hasMore() { + const { wcWallets, count } = appKit.getWalletList() + + return wcWallets.length < count +} + +/** Fetch the next page of WalletConnect wallets (appends to the list). */ +async function loadMore() { + if (loading || (page > 0 && !hasMore())) { + return + } + loading = true + statusEl.textContent = 'Loading…' + try { + page += 1 + await appKit.fetchWallets({ page }) // ← imperative client API; appends to the list + } catch (error) { + statusEl.textContent = 'Failed to load wallets.' + // eslint-disable-next-line no-console + console.error(error) + } finally { + loading = false + } +} + +/** Render the current wallet list (called on every wallet-list change). */ +function renderWallets() { + const { wcWallets, count } = appKit.getWalletList() // ← imperative client API + listEl.replaceChildren( + ...wcWallets.map(wallet => { + const item = document.createElement('button') + item.className = 'wallet' + item.innerHTML = ` + + ${wallet.name}` + item.addEventListener('click', () => connect(wallet)) + + return item + }) + ) + statusEl.textContent = wcWallets.length + ? `${wcWallets.length} of ${count} wallets` + : 'No wallets yet' +} + +// Re-render whenever the wallet list / search / pagination state changes. +appKit.subscribeWalletList(renderWallets) // ← imperative client API + +// Infinite scroll: load the next page as the user nears the bottom. +listEl.addEventListener('scroll', () => { + if (listEl.scrollTop + listEl.clientHeight >= listEl.scrollHeight - 120 && hasMore()) { + void loadMore() + } +}) + +// -- Connect ----------------------------------------------------------------- // +async function connect(wallet) { + statusEl.textContent = `Connecting ${wallet.name}…` + try { + await appKit.connectWallet(wallet, 'eip155') // ← imperative client API + } catch (error) { + statusEl.textContent = `Failed to connect ${wallet.name}.` + // eslint-disable-next-line no-console + console.error(error) + } +} + +// -- Account state ----------------------------------------------------------- // +function renderAccount() { + const account = appKit.getAccount('eip155') + const connected = Boolean(account?.address) + accountEl.hidden = !connected + openBtn.hidden = connected + if (connected) { + const a = account.address + accountAddressEl.textContent = `${a.slice(0, 6)}…${a.slice(-4)}` + closeModal() + } +} + +appKit.subscribeAccount(renderAccount, 'eip155') +disconnectBtn.addEventListener('click', () => appKit.disconnect('eip155')) + +// -- Modal open/close -------------------------------------------------------- // +function openModal() { + modal.hidden = false + if (page === 0) { + void loadMore() // first page on first open + } +} + +function closeModal() { + modal.hidden = true +} + +openBtn.addEventListener('click', openModal) +modal.querySelectorAll('[data-close]').forEach(el => el.addEventListener('click', closeModal)) + +// Initial paint. +renderAccount() +renderWallets() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 809e0bde2b..3dca2acb08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -896,6 +896,19 @@ importers: specifier: 5.4.20 version: 5.4.20(@types/node@22.13.9)(lightningcss@1.30.2)(terser@5.44.1) + examples/html-headless: + dependencies: + '@reown/appkit': + specifier: workspace:* + version: link:../../packages/appkit + '@reown/appkit-adapter-wagmi': + specifier: workspace:* + version: link:../../packages/adapters/wagmi + devDependencies: + vite: + specifier: 5.4.20 + version: 5.4.20(@types/node@22.13.9)(lightningcss@1.30.2)(terser@5.44.1) + examples/html-solana: dependencies: '@reown/appkit': From 52393a5c586b6b36fe9a187ddee7bb6569a8a0f6 Mon Sep 17 00:00:00 2001 From: Enes Date: Fri, 26 Jun 2026 16:53:05 +0300 Subject: [PATCH 4/5] style: prettier + lint fixes (block comments) Apply prettier to the touched files and convert the new client-method header to a block comment (multiline-comment-style / capitalized-comments lint). Co-Authored-By: Claude Opus 4.8 --- packages/appkit/src/client/appkit-base-client.ts | 12 +++++++----- packages/controllers/exports/react.ts | 5 +---- .../tests/utils/HeadlessWalletUtil.test.ts | 8 ++++++-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/appkit/src/client/appkit-base-client.ts b/packages/appkit/src/client/appkit-base-client.ts index 54c10d7a50..c79d601427 100644 --- a/packages/appkit/src/client/appkit-base-client.ts +++ b/packages/appkit/src/client/appkit-base-client.ts @@ -20,8 +20,8 @@ import type { ChainAdapterConnector, ConnectExternalOptions, ConnectMethod, - ConnectedWalletInfo, ConnectOptions, + ConnectedWalletInfo, ConnectionControllerClient, ConnectionControllerState, ConnectorType, @@ -2322,10 +2322,12 @@ export abstract class AppKitBaseClient { await ConnectionController.disconnect({ namespace: chainNamespace }) } - // -- Headless wallet list ------------------------------------------------ // - // Imperative counterparts of the `useAppKitWallets` React hook, so a non-React host - // (e.g. `@walletconnect/pay-appkit`) can list / search / connect wallets headlessly - // through the AppKit instance. Both share the same code path via `HeadlessWalletUtil`. + /* + * Headless wallet list — imperative counterparts of the `useAppKitWallets` React hook, + * so a non-React host (e.g. `@walletconnect/pay-appkit`) can list / search / connect + * wallets headlessly through the AppKit instance. Both share the same code path via + * `HeadlessWalletUtil`. + */ /** * Fetch / search / paginate the WalletConnect wallet list (WalletGuide explorer). Read diff --git a/packages/controllers/exports/react.ts b/packages/controllers/exports/react.ts index f86c8e5227..024be73d6e 100644 --- a/packages/controllers/exports/react.ts +++ b/packages/controllers/exports/react.ts @@ -23,10 +23,7 @@ import { ConnectUtil, type WalletItem } from '../src/utils/ConnectUtil.js' import { ConnectionControllerUtil } from '../src/utils/ConnectionControllerUtil.js' import { ConnectorControllerUtil } from '../src/utils/ConnectorControllerUtil.js' import { CoreHelperUtil } from '../src/utils/CoreHelperUtil.js' -import { - type ConnectOptions, - type FetchWalletsOptions -} from '../src/utils/HeadlessWalletUtil.js' +import { type ConnectOptions, type FetchWalletsOptions } from '../src/utils/HeadlessWalletUtil.js' import { MobileWalletUtil } from '../src/utils/MobileWallet.js' import type { UseAppKitAccountReturn, diff --git a/packages/controllers/tests/utils/HeadlessWalletUtil.test.ts b/packages/controllers/tests/utils/HeadlessWalletUtil.test.ts index b7e547f8fc..ccb4bab764 100644 --- a/packages/controllers/tests/utils/HeadlessWalletUtil.test.ts +++ b/packages/controllers/tests/utils/HeadlessWalletUtil.test.ts @@ -45,7 +45,9 @@ beforeEach(() => { describe('HeadlessWalletUtil.fetchWallets', () => { it('searches when a search term is given', async () => { - const searchWallet = vi.spyOn(ApiController, 'searchWallet').mockResolvedValue(undefined as never) + const searchWallet = vi + .spyOn(ApiController, 'searchWallet') + .mockResolvedValue(undefined as never) const fetchByPage = vi.spyOn(ApiController, 'fetchWalletsByPage') await HeadlessWalletUtil.fetchWallets({ search: 'rainbow', includePayOnly: true }) @@ -55,7 +57,9 @@ describe('HeadlessWalletUtil.fetchWallets', () => { }) it('maps the deprecated `query` to `search`', async () => { - const searchWallet = vi.spyOn(ApiController, 'searchWallet').mockResolvedValue(undefined as never) + const searchWallet = vi + .spyOn(ApiController, 'searchWallet') + .mockResolvedValue(undefined as never) await HeadlessWalletUtil.fetchWallets({ query: 'rbw' }) From f633c681352f54f439ea1675bc8001709b9297de Mon Sep 17 00:00:00 2001 From: Enes Date: Fri, 26 Jun 2026 17:03:19 +0300 Subject: [PATCH 5/5] style: fix pre-existing block-comment lint in tron connector `TronWalletConnectConnector.ts` (from #5694, current main) has 4 consecutive line comments that violate `multiline-comment-style`, failing `code_style (lint)` for every branch off main. Convert to a block comment to unblock CI. Unrelated to this PR's changes, but main's lint is red without it. Co-Authored-By: Claude Opus 4.8 --- .../tron/src/connectors/TronWalletConnectConnector.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/adapters/tron/src/connectors/TronWalletConnectConnector.ts b/packages/adapters/tron/src/connectors/TronWalletConnectConnector.ts index b3b384bee8..aab659645a 100644 --- a/packages/adapters/tron/src/connectors/TronWalletConnectConnector.ts +++ b/packages/adapters/tron/src/connectors/TronWalletConnectConnector.ts @@ -110,10 +110,12 @@ export class TronWalletConnectConnector throw new Error(unsignedTx?.Error || 'Failed to create transaction') } - // Step 2: Send full transaction to wallet for signing via WalletConnect. - // Wallets opt into the simplified (v1) payload shape by advertising - // `tron_method_version: "v1"` in sessionProperties during the handshake. - // Otherwise the spec mandates the legacy nested `transaction.transaction` shape. + /* + * Step 2: Send full transaction to wallet for signing via WalletConnect. + * Wallets opt into the simplified (v1) payload shape by advertising + * `tron_method_version: "v1"` in sessionProperties during the handshake. + * Otherwise the spec mandates the legacy nested `transaction.transaction` shape. + */ // See https://docs.reown.com/advanced/multichain/rpc-reference/tron-rpc const usesV1Format = this.provider.session?.sessionProperties?.['tron_method_version'] === 'v1' const signRequest = {