useNWC.ts
1 import { useState, useCallback } from 'react'; 2 import { useLocalStorage } from '@/hooks/useLocalStorage'; 3 import { useToast } from '@/hooks/useToast'; 4 import { LN } from '@getalby/sdk'; 5 6 export interface NWCConnection { 7 connectionString: string; 8 alias?: string; 9 isConnected: boolean; 10 client?: LN; 11 } 12 13 export interface NWCInfo { 14 alias?: string; 15 color?: string; 16 pubkey?: string; 17 network?: string; 18 methods?: string[]; 19 notifications?: string[]; 20 } 21 22 export function useNWCInternal() { 23 const { toast } = useToast(); 24 const [connections, setConnections] = useLocalStorage<NWCConnection[]>('nwc-connections', []); 25 const [activeConnection, setActiveConnection] = useLocalStorage<string | null>('nwc-active-connection', null); 26 const [connectionInfo, setConnectionInfo] = useState<Record<string, NWCInfo>>({}); 27 28 // Add new connection 29 const addConnection = async (uri: string, alias?: string): Promise<boolean> => { 30 const parseNWCUri = (uri: string): { connectionString: string } | null => { 31 try { 32 if (!uri.startsWith('nostr+walletconnect://') && !uri.startsWith('nostrwalletconnect://')) { 33 console.error('Invalid NWC URI protocol:', { protocol: uri.split('://')[0] }); 34 return null; 35 } 36 return { connectionString: uri }; 37 } catch (error) { 38 console.error('Failed to parse NWC URI:', error); 39 return null; 40 } 41 }; 42 43 const parsed = parseNWCUri(uri); 44 if (!parsed) { 45 toast({ 46 title: 'Invalid NWC URI', 47 description: 'Please check the connection string and try again.', 48 variant: 'destructive', 49 }); 50 return false; 51 } 52 53 const existingConnection = connections.find(c => c.connectionString === parsed.connectionString); 54 if (existingConnection) { 55 toast({ 56 title: 'Connection already exists', 57 description: 'This wallet is already connected.', 58 variant: 'destructive', 59 }); 60 return false; 61 } 62 63 try { 64 let timeoutId: NodeJS.Timeout | undefined; 65 const testPromise = new Promise((resolve, reject) => { 66 try { 67 const client = new LN(parsed.connectionString); 68 resolve(client); 69 } catch (error) { 70 reject(error); 71 } 72 }); 73 const timeoutPromise = new Promise<never>((_, reject) => { 74 timeoutId = setTimeout(() => reject(new Error('Connection test timeout')), 10000); 75 }); 76 77 try { 78 await Promise.race([testPromise, timeoutPromise]) as LN; 79 if (timeoutId) clearTimeout(timeoutId); 80 } catch (error) { 81 if (timeoutId) clearTimeout(timeoutId); 82 throw error; 83 } 84 85 const connection: NWCConnection = { 86 connectionString: parsed.connectionString, 87 alias: alias || 'NWC Wallet', 88 isConnected: true, 89 }; 90 91 setConnectionInfo(prev => ({ 92 ...prev, 93 [parsed.connectionString]: { 94 alias: connection.alias, 95 methods: ['pay_invoice'], 96 }, 97 })); 98 99 const newConnections = [...connections, connection]; 100 setConnections(newConnections); 101 102 if (connections.length === 0 || !activeConnection) 103 setActiveConnection(parsed.connectionString); 104 105 toast({ 106 title: 'Wallet connected', 107 description: `Successfully connected to ${connection.alias}.`, 108 }); 109 110 return true; 111 } catch (error) { 112 console.error('NWC connection failed:', error); 113 const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 114 115 toast({ 116 title: 'Connection failed', 117 description: `Could not connect to the wallet: ${errorMessage}`, 118 variant: 'destructive', 119 }); 120 return false; 121 } 122 }; 123 124 // Remove connection 125 const removeConnection = (connectionString: string) => { 126 const filtered = connections.filter(c => c.connectionString !== connectionString); 127 setConnections(filtered); 128 129 if (activeConnection === connectionString) { 130 const newActive = filtered.length > 0 ? filtered[0].connectionString : null; 131 setActiveConnection(newActive); 132 } 133 134 setConnectionInfo(prev => { 135 const newInfo = { ...prev }; 136 delete newInfo[connectionString]; 137 return newInfo; 138 }); 139 140 toast({ 141 title: 'Wallet disconnected', 142 description: 'The wallet connection has been removed.', 143 }); 144 }; 145 146 // Get active connection 147 const getActiveConnection = useCallback((): NWCConnection | null => { 148 if (!activeConnection && connections.length > 0) { 149 setActiveConnection(connections[0].connectionString); 150 return connections[0]; 151 } 152 153 if (!activeConnection) return null; 154 155 const found = connections.find(c => c.connectionString === activeConnection); 156 return found || null; 157 }, [activeConnection, connections, setActiveConnection]); 158 159 // Send payment using the SDK 160 const sendPayment = useCallback(async ( 161 connection: NWCConnection, 162 invoice: string 163 ): Promise<{ preimage: string }> => { 164 if (!connection.connectionString) { 165 throw new Error('Invalid connection: missing connection string'); 166 } 167 168 let client: LN; 169 try { 170 client = new LN(connection.connectionString); 171 } catch (error) { 172 console.error('Failed to create NWC client:', error); 173 throw new Error(`Failed to create NWC client: ${error instanceof Error ? error.message : 'Unknown error'}`); 174 } 175 176 try { 177 let timeoutId: NodeJS.Timeout | undefined; 178 const timeoutPromise = new Promise<never>((_, reject) => { 179 timeoutId = setTimeout(() => reject(new Error('Payment timeout after 15 seconds')), 15000); 180 }); 181 182 const paymentPromise = client.pay(invoice); 183 184 try { 185 const response = await Promise.race([paymentPromise, timeoutPromise]) as { preimage: string }; 186 if (timeoutId) clearTimeout(timeoutId); 187 return response; 188 } catch (error) { 189 if (timeoutId) clearTimeout(timeoutId); 190 throw error; 191 } 192 } catch (error) { 193 console.error('NWC payment failed:', error); 194 195 if (error instanceof Error) { 196 if (error.message.includes('timeout')) { 197 throw new Error('Payment timed out. Please try again.'); 198 } else if (error.message.includes('insufficient')) { 199 throw new Error('Insufficient balance in connected wallet.'); 200 } else if (error.message.includes('invalid')) { 201 throw new Error('Invalid invoice or connection. Please check your wallet.'); 202 } else { 203 throw new Error(`Payment failed: ${error.message}`); 204 } 205 } 206 207 throw new Error('Payment failed with unknown error'); 208 } 209 }, []); 210 211 return { 212 connections, 213 activeConnection, 214 connectionInfo, 215 addConnection, 216 removeConnection, 217 setActiveConnection, 218 getActiveConnection, 219 sendPayment, 220 }; 221 }