Files
eranos/src/hooks/useNWC.ts
T

226 lines
7.1 KiB
TypeScript

import { useState, useCallback } from 'react';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import { useToast } from '@/hooks/useToast';
import { LN } from '@getalby/sdk';
export interface NWCConnection {
connectionString: string;
alias?: string;
isConnected: boolean;
client?: LN;
}
export interface NWCInfo {
alias?: string;
color?: string;
pubkey?: string;
network?: string;
methods?: string[];
notifications?: string[];
}
export function useNWCInternal(userPubkey?: string) {
const { toast } = useToast();
// Scope wallet connections per user so switching accounts doesn't leak wallets.
// When no user is logged in, use a 'global' fallback key (connections won't
// be accessible without a user anyway since zap actions require login).
const storagePrefix = userPubkey ? `nwc-connections:${userPubkey}` : 'nwc-connections';
const activePrefix = userPubkey ? `nwc-active-connection:${userPubkey}` : 'nwc-active-connection';
const [connections, setConnections] = useLocalStorage<NWCConnection[]>(storagePrefix, []);
const [activeConnection, setActiveConnection] = useLocalStorage<string | null>(activePrefix, null);
const [connectionInfo, setConnectionInfo] = useState<Record<string, NWCInfo>>({});
// Add new connection
const addConnection = async (uri: string, alias?: string): Promise<boolean> => {
const parseNWCUri = (uri: string): { connectionString: string } | null => {
try {
if (!uri.startsWith('nostr+walletconnect://') && !uri.startsWith('nostrwalletconnect://')) {
console.error('Invalid NWC URI protocol:', { protocol: uri.split('://')[0] });
return null;
}
return { connectionString: uri };
} catch (error) {
console.error('Failed to parse NWC URI:', error);
return null;
}
};
const parsed = parseNWCUri(uri);
if (!parsed) {
toast({
title: 'Invalid NWC URI',
description: 'Please check the connection string and try again.',
variant: 'destructive',
});
return false;
}
const existingConnection = connections.find(c => c.connectionString === parsed.connectionString);
if (existingConnection) {
toast({
title: 'Connection already exists',
description: 'This wallet is already connected.',
variant: 'destructive',
});
return false;
}
try {
let timeoutId: NodeJS.Timeout | undefined;
const testPromise = new Promise((resolve, reject) => {
try {
const client = new LN(parsed.connectionString);
resolve(client);
} catch (error) {
reject(error);
}
});
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => reject(new Error('Connection test timeout')), 10000);
});
try {
await Promise.race([testPromise, timeoutPromise]) as LN;
if (timeoutId) clearTimeout(timeoutId);
} catch (error) {
if (timeoutId) clearTimeout(timeoutId);
throw error;
}
const connection: NWCConnection = {
connectionString: parsed.connectionString,
alias: alias || 'NWC Wallet',
isConnected: true,
};
setConnectionInfo(prev => ({
...prev,
[parsed.connectionString]: {
alias: connection.alias,
methods: ['pay_invoice'],
},
}));
const newConnections = [...connections, connection];
setConnections(newConnections);
if (connections.length === 0 || !activeConnection)
setActiveConnection(parsed.connectionString);
toast({
title: 'Wallet connected',
description: `Successfully connected to ${connection.alias}.`,
});
return true;
} catch (error) {
console.error('NWC connection failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
toast({
title: 'Connection failed',
description: `Could not connect to the wallet: ${errorMessage}`,
variant: 'destructive',
});
return false;
}
};
// Remove connection
const removeConnection = (connectionString: string) => {
const filtered = connections.filter(c => c.connectionString !== connectionString);
setConnections(filtered);
if (activeConnection === connectionString) {
const newActive = filtered.length > 0 ? filtered[0].connectionString : null;
setActiveConnection(newActive);
}
setConnectionInfo(prev => {
const newInfo = { ...prev };
delete newInfo[connectionString];
return newInfo;
});
toast({
title: 'Wallet disconnected',
description: 'The wallet connection has been removed.',
});
};
// Get active connection
const getActiveConnection = useCallback((): NWCConnection | null => {
if (!activeConnection && connections.length > 0) {
setActiveConnection(connections[0].connectionString);
return connections[0];
}
if (!activeConnection) return null;
const found = connections.find(c => c.connectionString === activeConnection);
return found || null;
}, [activeConnection, connections, setActiveConnection]);
// Send payment using the SDK
const sendPayment = useCallback(async (
connection: NWCConnection,
invoice: string
): Promise<{ preimage: string }> => {
if (!connection.connectionString) {
throw new Error('Invalid connection: missing connection string');
}
let client: LN;
try {
client = new LN(connection.connectionString);
} catch (error) {
console.error('Failed to create NWC client:', error);
throw new Error(`Failed to create NWC client: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
try {
let timeoutId: NodeJS.Timeout | undefined;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => reject(new Error('Payment timeout after 15 seconds')), 15000);
});
const paymentPromise = client.pay(invoice);
try {
const response = await Promise.race([paymentPromise, timeoutPromise]) as { preimage: string };
if (timeoutId) clearTimeout(timeoutId);
return response;
} catch (error) {
if (timeoutId) clearTimeout(timeoutId);
throw error;
}
} catch (error) {
console.error('NWC payment failed:', error);
if (error instanceof Error) {
if (error.message.includes('timeout')) {
throw new Error('Payment timed out. Please try again.');
} else if (error.message.includes('insufficient')) {
throw new Error('Insufficient balance in connected wallet.');
} else if (error.message.includes('invalid')) {
throw new Error('Invalid invoice or connection. Please check your wallet.');
} else {
throw new Error(`Payment failed: ${error.message}`);
}
}
throw new Error('Payment failed with unknown error');
}
}, []);
return {
connections,
activeConnection,
connectionInfo,
addConnection,
removeConnection,
setActiveConnection,
getActiveConnection,
sendPayment,
};
}