From e09b76ea0ed13840426a56f031769f8d4154f34b Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 1 Aug 2026 09:25:23 -0700 Subject: Add Nostr Wallet Connect (NIP-47) — hal as a wallet service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Other apps can connect over nostr and pay from this wallet without touching its keys, within a per-payment and daily budget the user sets, revocable at any time. hal holds no native Lightning balance, so every operation runs over Ark: pay_invoice takes the swap bridge when configured and falls back to the ASP's lightning-send, make_invoice mints on the ASP. NWC availability therefore tracks Ark availability. - kinds 13194/23194/23195, nip44_v2 and nip04, replying in whichever scheme the request used; every request gets a reply so clients never hang - each connection gets its own service keypair, so revoking one can't affect the others and none is tied to the user's nostr identity - budgets are checked BEFORE spending, not reconciled after - requests are ignored unless signed by that connection's client key, and replayed/stale events (>120s) can't re-trigger a payment - Settings card to create, show (URI + QR) and revoke connections, and it says plainly that this only works while a tab is open tools/nwc-test.js drives it as a real client: both encryption schemes, budget refusal with a genuine over-limit invoice, unsupported-method errors, and a request from an unauthorized key being ignored. Co-Authored-By: Claude Fable 5 --- src/nostr.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'src/nostr.js') diff --git a/src/nostr.js b/src/nostr.js index 407663c..652c579 100644 --- a/src/nostr.js +++ b/src/nostr.js @@ -11,6 +11,7 @@ import * as nip06 from 'nostr-tools/nip06'; import * as nip44 from 'nostr-tools/nip44'; +import * as nip04 from 'nostr-tools/nip04'; import { wrapEvent as nip17WrapEvent } from 'nostr-tools/nip17'; import { getPublicKey, finalizeEvent, generateSecretKey } from 'nostr-tools/pure'; import { decode as nip19decode, npubEncode } from 'nostr-tools/nip19'; @@ -24,6 +25,11 @@ const pool = new SimplePool(); // --- locking a gift to a recipient's Nostr key --------------------------- // An npub or 64-hex nostr pubkey → hex, or null if it isn't one. +// NIP-47 wallet services sign with their own per-connection key and must +// speak whichever encryption the client asked for, so export both schemes. +export { nip04, nip44 }; +export { getPublicKey, finalizeEvent, generateSecretKey }; + export function parseNostrPubkey(input) { const s = (input || '').trim(); if (/^[0-9a-f]{64}$/i.test(s)) return s.toLowerCase(); @@ -212,6 +218,13 @@ export class NostrSync { } catch { return null; } } + // Publish an event signed by some other key (the NWC service key). The + // wallet's own key must not sign wallet-service traffic — connections are + // revocable and shouldn't be tied to the user's nostr identity. + async publishSigned(evt, relays = this.relays) { + try { await Promise.allSettled(pool.publish(relays, evt)); return true; } catch { return false; } + } + // Fetch events matching a filter from the relays (best-effort). async fetchEvents(filter, maxWait = 5000) { try { return await pool.querySync(this.relays, filter, { maxWait }); } catch { return []; } -- cgit v1.2.3