/ src / hooks / useNWC.ts
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  }