diff options
| author | Adam Soltys <[email protected]> | 2026-08-08 08:21:11 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-08 08:21:11 -0700 |
| commit | 19c14131ce3fc69a06d3c0af8d445b056d703bf9 (patch) | |
| tree | 2d116ff73bccf2af784d8700ed6fd825837928a4 /src/nostr.js | |
| parent | 47919c94e861e98fc5c21956c3c53e896052aea4 (diff) | |
| download | coinosv3-19c14131ce3fc69a06d3c0af8d445b056d703bf9.tar.gz coinosv3-19c14131ce3fc69a06d3c0af8d445b056d703bf9.zip | |
Profiles wear their latest notes, each one zappable; a zap with no ark address to land on becomes a gift only their key can open
Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'src/nostr.js')
| -rw-r--r-- | src/nostr.js | 36 |
1 files changed, 36 insertions, 0 deletions
diff --git a/src/nostr.js b/src/nostr.js index 98515fe..8615f7d 100644 --- a/src/nostr.js +++ b/src/nostr.js @@ -14,6 +14,7 @@ import * as nip44 from 'nostr-tools/nip44'; import * as nip04 from 'nostr-tools/nip04'; import { getPublicKey, finalizeEvent, generateSecretKey } from 'nostr-tools/pure'; import { decode as nip19decode, npubEncode, nsecEncode } from 'nostr-tools/nip19'; +import { wrapEvent as nip17WrapEvent } from 'nostr-tools/nip17'; import { SimplePool } from 'nostr-tools/pool'; import { randomBytes } from '@noble/hashes/utils'; import { base64urlnopad } from '@scure/base'; @@ -152,6 +153,25 @@ export async function fetchInboxRelays(pubkeyHex, relays = PROFILE_RELAYS) { return []; } +// Encrypt a gift payload two ways: (1) under a fresh one-time code, delivered to +// the recipient out-of-band via a nostr DM — the manual path; and (2) to the +// recipient's nostr pubkey via an ephemeral key, so a NIP-07 browser extension +// can decrypt it in-place with no code. Both decrypt to the same payload. +// (NIP-44 takes raw 32-byte key material, so the one-time code works as a key.) +export function encryptGiftPayload(plaintext, recipientPkHex) { + const codeKey = randomBytes(32); + const ephSk = generateSecretKey(); + return { + code: base64urlnopad.encode(codeKey), + ctCode: nip44.encrypt(plaintext, codeKey), + eph: getPublicKey(ephSk), + ctKey: nip44.encrypt(plaintext, nip44.getConversationKey(ephSk, recipientPkHex)), + }; +} +export function decryptWithCode(code, ct) { + return nip44.decrypt(ct, base64urlnopad.decode((code || '').trim())); +} + const DTAG = 'bitcoin-wallet'; // Each network syncs under its own d-tag base — a shared one would let a signet // snapshot overwrite mainnet state (and vice versa). Mainnet keeps the bare tag @@ -285,6 +305,22 @@ export class NostrSync { } catch { return null; } } + // Deliver an encrypted DM (a locked gift's claim code) as a NIP-17 gift wrap + // (kind 1059) — the modern standard every current client supports. Goes to the + // recipient's published inbox relays (NIP-17/65) plus a broad fallback. + async sendDM(recipientPkHex, text, relays = null) { + if (!this.sk) return false; + let evt; + try { evt = nip17WrapEvent(this.sk, { publicKey: recipientPkHex }, text); } catch { return false; } + let targets = relays; + if (!targets) { + const inbox = await fetchInboxRelays(recipientPkHex); + targets = [...new Set([...inbox, ...PROFILE_RELAYS])].slice(0, 8); + } + const res = await Promise.allSettled(pool.publish(targets, evt)); + return res.some((x) => x.status === 'fulfilled'); + } + // 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. |
