/** * TON Connect JS Bridge Discovery * https://github.com/ton-blockchain/ton-connect/blob/main/bridge.md#js-bridge * * Per the TON Connect spec, wallet extensions inject a bridge object at * window[jsBridgeKey].tonconnect. Unlike EVM (EIP-6963), there is no * announcement event — we check known keys from wallet config. */ import type { WalletMetadata } from '@meshconnect/uwc-types' import { BRIDGE_WALLET_POLL_MS } from './bridge-poll' interface TonConnectBridge { deviceInfo: { platform: string; appName: string } connect: (protocolVersion: number, message: string) => Promise send: (message: string) => Promise listen: (callback: (event: string) => void) => () => void disconnect?: () => Promise } export interface DetectedTonWallet { uuid: string name: string icon: string jsBridgeKey: string provider: TonConnectBridge } function isTonConnectBridge(value: unknown): value is TonConnectBridge { if (!value || typeof value !== 'object') return false const bridge = value as Record return ( typeof bridge['connect'] === 'function' && typeof bridge['send'] === 'function' && typeof bridge['listen'] === 'function' ) } function resolveTonBridge( key: string ): { bridge: TonConnectBridge; name: string } | null { const root = (window as unknown as Record)[key] if (!root || typeof root !== 'object') return null const bridge = (root as Record)['tonconnect'] if (isTonConnectBridge(bridge)) { return { bridge, name: bridge.deviceInfo?.appName || key } } return null } function discoverTonProviders(jsBridgeKeys: string[]): DetectedTonWallet[] { if (typeof window === 'undefined') return [] const wallets: DetectedTonWallet[] = [] const seen = new Set() for (const key of jsBridgeKeys) { if (seen.has(key)) continue const resolved = resolveTonBridge(key) if (resolved) { seen.add(key) wallets.push({ uuid: `ton-${key}`.toLowerCase(), name: resolved.name, icon: '', jsBridgeKey: key, provider: resolved.bridge }) } } return wallets } async function pollForBridgeTonWallets(): Promise { const pollInterval = 50 // Consumer poll budget — see BRIDGE_WALLET_POLL_MS for rationale + the // must-exceed-child-budget invariant. Ceiling, not a fixed wait. const maxPollTime = BRIDGE_WALLET_POLL_MS const startTime = Date.now() return new Promise(resolve => { const poll = () => { if (Date.now() - startTime > maxPollTime) { resolve([]) return } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - window.tonWallets is set by the bridge if (window.tonWallets && Array.isArray(window.tonWallets)) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const bridgeWallets = window.tonWallets as DetectedTonWallet[] resolve(bridgeWallets) return } setTimeout(poll, pollInterval) } poll() }) } export async function getTonWallets( expectedWallets: WalletMetadata[] = [] ): Promise { try { const jsBridgeKeys = expectedWallets .map( w => w.extensionInjectedProvider?.namespaceMetaData?.tvm?.jsBridgeKey ) .filter((key): key is string => !!key) // Bridge mode: prefer this frame's own provider (window[jsBridgeKey]) so connect // attributes to the iframe origin; fall back to the parent list only // when none is present. // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - window.UWCBridgeChildInitialized is set by the bridge if (window.UWCBridgeChildInitialized === true) { const local = discoverTonProviders(jsBridgeKeys) if (local.length > 0) { return local } return pollForBridgeTonWallets() } // Native discovery: direct window[key].tonconnect access return discoverTonProviders(jsBridgeKeys) } catch { return [] } }