import type { NetworkId, AvailableAddress, WalletLifecycleEvent, WalletMetadata } from '@meshconnect/uwc-types' import { getSolanaWallets, type SolanaAdapter, type WalletStandardInfo, type LegacyInjectedSolanaDiscoveryFailure } from '../../wallet-standard-discovery' import { StorageService } from '../storage-service' /** * Service for managing Solana wallet connections */ export class SolanaWalletService { private detectedWallets: WalletStandardInfo[] = [] private connectedAdapter: SolanaAdapter | null = null private account: string | null = null private storageService: StorageService constructor(storageService: StorageService) { this.storageService = storageService } /** * Initialize wallet discovery */ async initializeDiscovery( expectedWallets: WalletMetadata[] = [], usingIntegratedBrowser = false, onLegacyDiscoveryFailure?: LegacyInjectedSolanaDiscoveryFailure ): Promise { this.detectedWallets = await getSolanaWallets( expectedWallets, usingIntegratedBrowser, onLegacyDiscoveryFailure ) } /** * Get detected wallets */ getDetectedWallets(): WalletStandardInfo[] { return this.detectedWallets } /** * Find wallet by UUID */ findWalletByUuid(uuid: string): WalletStandardInfo | undefined { return this.detectedWallets.find(w => w.uuid === uuid) } /** * Check if a Solana wallet is already connected. * * `allowSilentReconnect` (default true) gates the storage-flag reconnect * below: even a "silent" `adapter.connect()` is a REAL provider connect — * for a legacy-`injectedId` wallet in its in-app browser it parks the * wallet's active namespace on Solana and swallows the next EVM send * (ONC-3536 follow-up). Pass false to make this check purely passive. */ async checkExistingConnection( adapter: SolanaAdapter, walletUuid: string, options?: { allowSilentReconnect?: boolean } ): Promise { const allowSilentReconnect = options?.allowSilentReconnect ?? true try { const base58PublicKey = await adapter.publicKey?.toBase58() // Check if adapter has a publicKey (means it's connected) if (base58PublicKey) { return base58PublicKey } // If this wallet was previously connected, try to reconnect // This should work without prompting for wallets that support it if ( allowSilentReconnect && this.storageService.wasSolanaWalletPreviouslyConnected(walletUuid) ) { const walletIsConnecting = await adapter.connecting if (!walletIsConnecting) { try { await adapter.connect() const base58PublicKey = await adapter.publicKey?.toBase58() if (base58PublicKey) { return base58PublicKey } } catch { // Silent fail - wallet requires user interaction or doesn't support silent reconnect } } } return null } catch { return null } } /** * Connect to wallet */ async connect(adapter: SolanaAdapter, walletUuid: string): Promise { // Connect to the Solana wallet await adapter.connect() const address = await adapter.publicKey?.toBase58() if (!address) { throw new Error('No address returned from wallet') } this.connectedAdapter = adapter this.account = address // Store this wallet as connected this.storageService.storeSolanaWalletUUID(walletUuid) return address } /** * Build available addresses for all supported networks */ buildAvailableAddresses( supportedNetworkIds: string[], address: string ): AvailableAddress[] { const addresses: AvailableAddress[] = [] supportedNetworkIds.forEach(networkId => { if (networkId.startsWith('solana:')) { addresses.push({ address, networkId: networkId as NetworkId }) } }) return addresses } /** * Set connection state */ setConnectionState( adapter: SolanaAdapter | null, account: string | null, walletUuid?: string ): void { this.connectedAdapter = adapter this.account = account // Store wallet UUID if provided and connected if (adapter && account && walletUuid) { this.storageService.storeSolanaWalletUUID(walletUuid) } } /** * Get current account */ getAccount(): string | null { return this.account } /** * Get connected adapter */ getConnectedAdapter(): SolanaAdapter | null { return this.connectedAdapter } /** * Subscribe to the connected wallet-standard adapter's lifecycle. Only the * adapter's typed `disconnect` event is forwarded (`WalletAdapterEvents` doesn't * expose a guaranteed account-change event, so we don't invent one). Returns a * teardown, or undefined when there is no connected adapter / it can't be * observed. * * Bridge guard: in the production iframe the adapter is a Comlink proxy — a raw * handler can't ride the bridge — so we skip (the core reports lifecycle * unavailable), mirroring the EVM path. */ onLifecycleEvent( listener: (event: WalletLifecycleEvent) => void ): (() => void) | undefined { const adapter = this.connectedAdapter // A `.on`-only guard is safe HERE (unlike raw EIP-1193, where removeListener // is only a SHOULD): the adapter is a UWC-constructed StandardWalletAdapter // backed by eventemitter3, where `.off` exists whenever `.on` does. if (!adapter || typeof adapter.on !== 'function') return undefined if ( typeof window !== 'undefined' && (window as unknown as { UWCBridgeChildInitialized?: boolean }) .UWCBridgeChildInitialized === true ) { return undefined } const onDisconnect = () => listener({ type: 'disconnect' }) adapter.on('disconnect', onDisconnect) return () => { adapter.off('disconnect', onDisconnect) } } /** * Disconnect (clear state only, don't actually disconnect from wallet) */ disconnect(): void { this.connectedAdapter = null this.account = null } }