import { useState, useEffect, useCallback } from 'react'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useToast } from '@/hooks/useToast'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { nip04 } from 'nostr-tools'; import { useNostr } from '@nostrify/react'; export interface NWCConnection { walletPubkey: string; secret: string; relayUrls: string[]; lud16?: string; alias?: string; } interface NWCInfo { alias?: string; color?: string; pubkey?: string; network?: string; methods?: string[]; notifications?: string[]; } export function useNWC() { const { nostr } = useNostr(); const { toast } = useToast(); const { user } = useCurrentUser(); const [connections, setConnections] = useLocalStorage('nwc-connections', []); const [activeConnection, setActiveConnection] = useLocalStorage('nwc-active-connection', null); const [connectionInfo, setConnectionInfo] = useState>({}); // Parse NWC URI const parseNWCUri = (uri: string): NWCConnection | null => { try { const url = new URL(uri); if (url.protocol !== 'nostr+walletconnect:') { return null; } const walletPubkey = url.pathname.replace('//', ''); const secret = url.searchParams.get('secret'); const relayParam = url.searchParams.getAll('relay'); const lud16 = url.searchParams.get('lud16') || undefined; if (!walletPubkey || !secret || relayParam.length === 0) { return null; } return { walletPubkey, secret, relayUrls: relayParam, lud16, }; } catch { return null; } }; // Add new connection const addConnection = async (uri: string, alias?: string): Promise => { const connection = parseNWCUri(uri); if (!connection) { toast({ title: 'Invalid NWC URI', description: 'Please check the connection string and try again.', variant: 'destructive', }); return false; } // Check if connection already exists const existingConnection = connections.find(c => c.walletPubkey === connection.walletPubkey); if (existingConnection) { toast({ title: 'Connection already exists', description: 'This wallet is already connected.', variant: 'destructive', }); return false; } if (alias) { connection.alias = alias; } try { // Test connection by fetching info await fetchWalletInfo(connection); setConnections(prev => [...prev, connection]); // Set as active if it's the first connection if (connections.length === 0) { setActiveConnection(connection.walletPubkey); } toast({ title: 'Wallet connected', description: `Successfully connected to ${alias || 'wallet'}.`, }); return true; } catch { toast({ title: 'Connection failed', description: 'Could not connect to the wallet. Please check your connection.', variant: 'destructive', }); return false; } }; // Remove connection const removeConnection = (walletPubkey: string) => { setConnections(prev => prev.filter(c => c.walletPubkey !== walletPubkey)); if (activeConnection === walletPubkey) { const remaining = connections.filter(c => c.walletPubkey !== walletPubkey); setActiveConnection(remaining.length > 0 ? remaining[0].walletPubkey : null); } setConnectionInfo(prev => { const newInfo = { ...prev }; delete newInfo[walletPubkey]; return newInfo; }); toast({ title: 'Wallet disconnected', description: 'The wallet connection has been removed.', }); }; // Get active connection const getActiveConnection = (): NWCConnection | null => { if (!activeConnection) return null; return connections.find(c => c.walletPubkey === activeConnection) || null; }; // Send NWC request const sendNWCRequest = useCallback(async ( connection: NWCConnection, request: { method: string; params: Record } ): Promise<{ result_type: string; error?: { code: string; message: string }; result?: unknown }> => { if (!user?.signer) { throw new Error('User not logged in or signer not available'); } // Create request event const requestEvent = { kind: 23194, created_at: Math.floor(Date.now() / 1000), tags: [['p', connection.walletPubkey]], content: await nip04.encrypt(connection.secret, connection.walletPubkey, JSON.stringify(request)), }; // Sign and publish request const signedRequest = await user.signer.signEvent(requestEvent); if (!signedRequest) { throw new Error('Failed to sign NWC request'); } // Publish to NWC relays try { await nostr.event(signedRequest, { signal: AbortSignal.timeout(10000), relays: connection.relayUrls }); } catch (error) { console.warn('Failed to publish NWC request:', error); throw new Error('Failed to publish NWC request'); } // Listen for response return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('NWC request timeout')); }, 30000); // 30 second timeout // Query for response events const checkForResponse = async () => { try { const responseEvents = await nostr.query([ { kinds: [23195], authors: [connection.walletPubkey], '#p': [user.pubkey], '#e': [signedRequest.id], since: Math.floor(Date.now() / 1000) - 60, }, ], { signal: AbortSignal.timeout(30000) }); for (const event of responseEvents) { try { const decrypted = await nip04.decrypt( connection.secret, connection.walletPubkey, event.content ); const response = JSON.parse(decrypted); clearTimeout(timeout); resolve(response); return; } catch (error) { console.error('Failed to decrypt NWC response:', error); } } // If no response found, wait and try again setTimeout(checkForResponse, 2000); } catch (error) { clearTimeout(timeout); reject(error); } }; // Start checking for responses setTimeout(checkForResponse, 1000); // Wait 1 second before first check }); }, [nostr, user]); // Fetch wallet info const fetchWalletInfo = useCallback(async (connection: NWCConnection): Promise => { // First, try to get the info event (kind 13194) try { const infoEvents = await nostr.query([ { kinds: [13194], authors: [connection.walletPubkey], limit: 1, } ], { signal: AbortSignal.timeout(5000) }); if (infoEvents.length > 0) { const infoEvent = infoEvents[0]; const capabilities = infoEvent.content.split(' '); const notificationsTag = infoEvent.tags.find(tag => tag[0] === 'notifications'); const notifications = notificationsTag ? notificationsTag[1].split(' ') : []; const info: NWCInfo = { methods: capabilities, notifications, }; setConnectionInfo(prev => ({ ...prev, [connection.walletPubkey]: info, })); return info; } } catch (error) { console.warn('Failed to fetch NWC info event:', error); } // Fallback: try to send a get_info request try { const response = await sendNWCRequest(connection, { method: 'get_info', params: {} }); if (response.error) { throw new Error(response.error.message); } const info = response.result as NWCInfo; setConnectionInfo(prev => ({ ...prev, [connection.walletPubkey]: info, })); return info; } catch (error) { console.error('Failed to fetch wallet info:', error); throw error; } }, [nostr, sendNWCRequest]); // Fetch info for all connections on mount useEffect(() => { connections.forEach(connection => { if (!connectionInfo[connection.walletPubkey]) { fetchWalletInfo(connection).catch(console.error); } }); }, [connections, connectionInfo, fetchWalletInfo]); return { connections, activeConnection, connectionInfo, addConnection, removeConnection, setActiveConnection, getActiveConnection, fetchWalletInfo, sendNWCRequest, parseNWCUri, }; }