/** * EIP-6963: Multi Injected Provider Discovery * https://eips.ethereum.org/EIPS/eip-6963 */ import type { WalletMetadata } from '@meshconnect/uwc-types' import { v4 as uuid } from 'uuid' import { BRIDGE_WALLET_POLL_MS } from './bridge-poll' export interface EthereumProvider { // eslint-disable-next-line @typescript-eslint/no-explicit-any request: (args: { method: string; params?: unknown[] }) => Promise // eslint-disable-next-line @typescript-eslint/no-explicit-any on?: (event: string, handler: (...args: any[]) => void) => void // eslint-disable-next-line @typescript-eslint/no-explicit-any removeListener?: (event: string, handler: (...args: any[]) => void) => void } /** * EIP-1193 compatible provider interface extending the base EthereumProvider * This interface includes the standard EIP-1193 methods and properties */ export interface EIP1193Provider extends EthereumProvider { // EIP-1193 standard methods // eslint-disable-next-line @typescript-eslint/no-explicit-any request: (args: { method: string; params?: unknown[] }) => Promise // eslint-disable-next-line @typescript-eslint/no-explicit-any on: (event: string, handler: (...args: any[]) => void) => void // eslint-disable-next-line @typescript-eslint/no-explicit-any removeListener: (event: string, handler: (...args: any[]) => void) => void // Add common wallet detection properties as needed (e.g. isMetaMask, isCoinbaseWallet, etc.) // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any } export interface EIP6963ProviderInfo { uuid: string name: string icon: string rdns: string } export interface EIP6963ProviderDetail { info: EIP6963ProviderInfo provider: EthereumProvider } export interface EIP6963AnnounceProviderEvent extends CustomEvent { type: 'eip6963:announceProvider' detail: EIP6963ProviderDetail } export interface EIP6963RequestProviderEvent extends Event { type: 'eip6963:requestProvider' } export interface DetectedWallet { uuid: string name: string icon: string rdns: string provider: EthereumProvider } /** * Polls for bridge wallets when UWCBridgeInitialized is true * @returns Promise that resolves to an array of detected wallets from the bridge */ async function pollForBridgeWallets(): Promise { const pollInterval = 50 // Poll every 50ms // Consumer budget — see BRIDGE_WALLET_POLL_MS for rationale (couples this // to the bridge child's MAX_POLL_MS + grace so we don't resolve `[]` while // the child is still marshalling). const maxPollTime = BRIDGE_WALLET_POLL_MS const startTime = Date.now() return new Promise(resolve => { const poll = () => { // Check if we've exceeded the max poll time if (Date.now() - startTime > maxPollTime) { resolve([]) return } // Check if window.eip6963Wallets is available // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - window.eip6963Wallets is set by the bridge if (window.eip6963Wallets && Array.isArray(window.eip6963Wallets)) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const bridgeWallets = window.eip6963Wallets as DetectedWallet[] resolve(bridgeWallets) return } // Continue polling setTimeout(poll, pollInterval) } // Start polling poll() }) } /** * Discovers wallets using EIP-6963 protocol * @returns Promise that resolves to an array of detected wallets */ export async function discoverWallets(): Promise { // Skip discovery in non-browser environments if (typeof window === 'undefined') { return [] } // UWCBridgeChildInitialized is only initialized if app runs in an iframe. We can skip checking for bridge proxy providers when it's not necessary. // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - window.UWCBridgeInitialized is set by the bridge if (window.UWCBridgeChildInitialized === true) { const wallets = await pollForBridgeWallets() if (wallets.length > 0) { return wallets } } // Original EIP-6963 discovery return new Promise(resolve => { const detectedWallets: DetectedWallet[] = [] const timeout = 100 // Wait up to 100 ms for wallets to announce // Set up listener for wallet announcements const handleAnnouncement = (event: Event) => { const announcementEvent = event as EIP6963AnnounceProviderEvent const { info, provider } = announcementEvent.detail // Check if wallet is already detected (by uuid) const existingIndex = detectedWallets.findIndex(w => w.uuid === info.uuid) if (existingIndex === -1) { detectedWallets.push({ uuid: info.uuid, name: info.name, icon: info.icon, rdns: info.rdns, provider }) } } // Listen for wallet announcements window.addEventListener('eip6963:announceProvider', handleAnnouncement) // Request wallets to announce themselves window.dispatchEvent(new Event('eip6963:requestProvider')) // Clean up and resolve after timeout setTimeout(() => { window.removeEventListener('eip6963:announceProvider', handleAnnouncement) resolve(detectedWallets) }, timeout) }) } /** * Gets available wallets by discovering them via EIP-6963 * This function can be called multiple times to refresh the list */ export async function getAvailableWallets( expectedWallets: WalletMetadata[] = [] ): Promise { try { const detectedWallets = await discoverWallets() // Check expected wallets with legacy EIP-1193 provider support // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - window.ethereum is set by the browser if (window.ethereum && expectedWallets.length > 0) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - window.ethereum is set by the browser const legacyProvider = window.ethereum as EIP1193Provider // Match expected wallets with legacy provider using isWallet detection const matchedWallet = expectedWallets.find(wallet => { const injectedId = wallet.extensionInjectedProvider?.namespaceMetaData.eip155 ?.injectedId ?? '' // Check if the legacy provider has the corresponding isWallet property // This supports detection of wallets like MetaMask (isMetaMask), Coinbase Wallet (isCoinbaseWallet), etc. return injectedId && legacyProvider[injectedId] === true }) if (matchedWallet) { // Check if this wallet is already detected via EIP-6963 to avoid duplicates const isAlreadyDetected = detectedWallets.some( detectedWallet => detectedWallet.name.toLowerCase() === matchedWallet.extensionInjectedProvider?.namespaceMetaData.eip155?.eip155Name?.toLowerCase() ) if (!isAlreadyDetected) { detectedWallets.push({ uuid: matchedWallet.metadata['uuid'] || uuid(), name: matchedWallet.extensionInjectedProvider?.namespaceMetaData.eip155 ?.eip155Name || matchedWallet.name, icon: matchedWallet.metadata['icon'] || '', rdns: matchedWallet.metadata['rdns'] || '', provider: legacyProvider }) } } } return detectedWallets } catch { // Return empty array if discovery fails return [] } }