The Wallet page was Cashu-only and said 'App only' on web, so anyone with an NWC wallet connected saw nothing. Adds: - useNWC.getBalance() (millisats -> sats) and makeInvoice() via NWCClient - NwcWalletCard: live balance + refresh, Receive (create invoice, QR, copy), Send (paste invoice, decoded amount shown, pay via the connected wallet) Renders only when an NWC connection is active.
259 lines
No EOL
8.4 KiB
TypeScript
259 lines
No EOL
8.4 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import type { 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() {
|
|
const { toast } = useToast();
|
|
const [connections, setConnections] = useLocalStorage<NWCConnection[]>('nwc-connections', []);
|
|
const [activeConnection, setActiveConnection] = useLocalStorage<string | null>('nwc-active-connection', 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 {
|
|
// Load the Getalby SDK on demand so it stays out of the initial bundle.
|
|
const { LN } = await import('@getalby/sdk');
|
|
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');
|
|
}
|
|
|
|
// Load the Getalby SDK on demand so it stays out of the initial bundle.
|
|
const { LN } = await import('@getalby/sdk');
|
|
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');
|
|
}
|
|
}, []);
|
|
|
|
/** Read the connected wallet's balance, in sats (NWC reports millisats). */
|
|
const getBalance = useCallback(async (connection: NWCConnection): Promise<number> => {
|
|
if (!connection?.connectionString) throw new Error('Invalid connection: missing connection string');
|
|
const { nwc } = await import('@getalby/sdk');
|
|
const client = new nwc.NWCClient({ nostrWalletConnectUrl: connection.connectionString });
|
|
try {
|
|
const res = await client.getBalance();
|
|
return Math.floor((res?.balance ?? 0) / 1000);
|
|
} finally {
|
|
try { client.close(); } catch { /* ignore */ }
|
|
}
|
|
}, []);
|
|
|
|
/** Create a Lightning invoice with the connected wallet. Returns the bolt11. */
|
|
const makeInvoice = useCallback(async (
|
|
connection: NWCConnection,
|
|
amountSats: number,
|
|
description?: string,
|
|
): Promise<string> => {
|
|
if (!connection?.connectionString) throw new Error('Invalid connection: missing connection string');
|
|
if (!Number.isFinite(amountSats) || amountSats <= 0) throw new Error('Enter an amount greater than zero.');
|
|
const { nwc } = await import('@getalby/sdk');
|
|
const client = new nwc.NWCClient({ nostrWalletConnectUrl: connection.connectionString });
|
|
try {
|
|
const tx = await client.makeInvoice({ amount: Math.round(amountSats * 1000), description });
|
|
if (!tx?.invoice) throw new Error('Wallet did not return an invoice.');
|
|
return tx.invoice;
|
|
} finally {
|
|
try { client.close(); } catch { /* ignore */ }
|
|
}
|
|
}, []);
|
|
|
|
return {
|
|
connections,
|
|
activeConnection,
|
|
connectionInfo,
|
|
addConnection,
|
|
removeConnection,
|
|
setActiveConnection,
|
|
getActiveConnection,
|
|
sendPayment,
|
|
getBalance,
|
|
makeInvoice,
|
|
};
|
|
} |