Add zaps: NWC wallet connect + NIP-57 zap flow

- NWC client (nwc.ts): parse URI, encrypt/send kind 23194, await kind 23195 response
- Lightning store: persist NWC URI to localStorage, zap() via NDKZapper + lnPay callback
- ZapModal: amount presets (21/100/500/1000/5000 sats), custom amount, optional comment,
  paying/success/error states, prompts to Settings if no wallet connected
-  zap button on NoteCard (action row) and ProfileView (header, next to follow)
- Settings > Lightning Wallet section: paste NWC URI, connect/disconnect

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jure
2026-03-09 17:45:39 +01:00
parent b324db6bac
commit 8fcabac450
6 changed files with 458 additions and 11 deletions
+97
View File
@@ -0,0 +1,97 @@
import NDK, { NDKEvent, NDKFilter, NDKKind, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
interface NwcConnection {
walletPubkey: string;
relayUrl: string;
secret: string;
}
export function parseNwcUri(uri: string): NwcConnection {
const cleaned = uri.replace("nostr+walletconnect://", "https://");
const url = new URL(cleaned);
const walletPubkey = url.hostname;
const relayUrl = url.searchParams.get("relay");
const secret = url.searchParams.get("secret");
if (!walletPubkey || !relayUrl || !secret) {
throw new Error("Invalid NWC URI: missing pubkey, relay, or secret");
}
return { walletPubkey, relayUrl, secret };
}
export function isValidNwcUri(uri: string): boolean {
try {
parseNwcUri(uri);
return uri.startsWith("nostr+walletconnect://");
} catch {
return false;
}
}
export async function payInvoiceViaNWC(nwcUri: string, bolt11: string): Promise<string> {
const { walletPubkey, relayUrl, secret } = parseNwcUri(nwcUri);
const ndk = new NDK({ explicitRelayUrls: [relayUrl] });
const signer = new NDKPrivateKeySigner(secret);
ndk.signer = signer;
await ndk.connect();
// Wait briefly for relay connection
await new Promise<void>((resolve) => {
const check = () => {
const relays = Array.from(ndk.pool?.relays?.values() ?? []);
if (relays.some((r) => r.connected)) resolve();
else setTimeout(check, 200);
};
setTimeout(() => resolve(), 8000); // timeout fallback
check();
});
const walletUser = ndk.getUser({ pubkey: walletPubkey });
const requestContent = JSON.stringify({
method: "pay_invoice",
params: { invoice: bolt11 },
});
const encrypted = await signer.encrypt(walletUser, requestContent, "nip04");
const requestEvent = new NDKEvent(ndk);
requestEvent.kind = NDKKind.NostrWalletConnectReq;
requestEvent.content = encrypted;
requestEvent.tags = [["p", walletPubkey]];
await requestEvent.sign();
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
sub.stop();
reject(new Error("NWC payment timed out (30s)"));
}, 30000);
const filter: NDKFilter = {
kinds: [NDKKind.NostrWalletConnectRes],
authors: [walletPubkey],
"#e": [requestEvent.id!],
};
const sub = ndk.subscribe(filter, { closeOnEose: false });
sub.on("event", async (event: NDKEvent) => {
clearTimeout(timeout);
sub.stop();
try {
const decrypted = await signer.decrypt(walletUser, event.content, "nip04");
const response = JSON.parse(decrypted);
if (response.error) {
reject(new Error(response.error.message || "NWC payment failed"));
} else {
resolve(response.result?.preimage ?? "");
}
} catch (err) {
reject(err);
}
});
requestEvent.publish();
});
}