From e1310ee5530b72a699c875598794fa6ed12644ca Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Fri, 18 Sep 2026 16:03:21 +0200 Subject: [PATCH] Pass zapRequest into the NWC transaction desc --- __tests__/nwcListTransactions.test.ts | 50 +++++++++++++++++++++++++-- src/models/NwcTransaction.ts | 8 ++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/__tests__/nwcListTransactions.test.ts b/__tests__/nwcListTransactions.test.ts index cc25aacb..a86e0c84 100644 --- a/__tests__/nwcListTransactions.test.ts +++ b/__tests__/nwcListTransactions.test.ts @@ -29,6 +29,25 @@ import { const MINT = 'https://mint.test' +const SENDER_PUBKEY = '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245' +const ZAPPED_NOTE = '1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100' + +/** A real-shaped kind-9734, as the lnurl server stores it against the paid invoice. */ +const ZAP_REQUEST = JSON.stringify({ + id: '9e1a1b0c2d3e4f50617283940a1b2c3d4e5f60718293a4b5c6d7e8f901234567', + pubkey: SENDER_PUBKEY, + created_at: 1757342998, + kind: 9734, + tags: [ + ['p', '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9'], + ['e', ZAPPED_NOTE], + ['relays', 'wss://relay.damus.io', 'wss://nos.lol'], + ['amount', '21000'], + ], + content: 'nice one', + sig: 'c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00', +}) + let nextId = 1 const addTx = (tx: { @@ -43,12 +62,13 @@ const addTx = (tx: { paymentId?: string proof?: string expiresAt?: string + zapRequest?: string }) => { const id = nextId++ Database.getInstance().execute( `INSERT INTO transactions - (id, type, amount, fee, unit, data, memo, mint, mintId, status, paymentRequest, paymentId, proof, expiresAt, createdAt) - VALUES (?, ?, ?, ?, ?, '[]', ?, ?, 'mint1111', ?, ?, ?, ?, ?, ?)`, + (id, type, amount, fee, unit, data, memo, mint, mintId, status, paymentRequest, paymentId, proof, expiresAt, zapRequest, createdAt) + VALUES (?, ?, ?, ?, ?, '[]', ?, ?, 'mint1111', ?, ?, ?, ?, ?, ?, ?)`, [ id, tx.type, @@ -62,6 +82,7 @@ const addTx = (tx: { tx.paymentId ?? null, tx.proof ?? null, tx.expiresAt ?? null, + tx.zapRequest ?? null, tx.createdAt ?? new Date().toISOString(), ], ) @@ -243,6 +264,31 @@ describe('list_transactions', () => { }) }) + it('carries the zap request as the description, so a received zap can be attributed', () => { + // A kind-9735 receipt is published to the relays the SENDER named, which the + // recipient often does not read, so clients recover zaps from list_transactions + // instead. That only works if the signed zap request is in `description` — + // with the memo there, the payment can only ever render as an anonymous credit. + addTx({type: TransactionType.RECEIVE, amount: 21, memo: 'Received zap', zapRequest: ZAP_REQUEST}) + + const [tx] = listTransactions() + + const zapRequest = JSON.parse(tx.description as string) + + expect(zapRequest.kind).toBe(9734) + expect(zapRequest.pubkey).toBe(SENDER_PUBKEY) + expect(zapRequest.tags).toContainEqual(['e', ZAPPED_NOTE]) + expect(zapRequest.sig).toEqual(expect.any(String)) + }) + + it('still reports the memo when a receive is not a zap', () => { + addTx({type: TransactionType.RECEIVE, memo: 'Received to lightning address'}) + + const [tx] = listTransactions() + + expect(tx.description).toBe('Received to lightning address') + }) + it('carries the invoice, payment hash and preimage of a lightning payment', () => { addTx({ type: TransactionType.TRANSFER, diff --git a/src/models/NwcTransaction.ts b/src/models/NwcTransaction.ts index d2286c7f..f0640b8a 100644 --- a/src/models/NwcTransaction.ts +++ b/src/models/NwcTransaction.ts @@ -17,6 +17,7 @@ export type NwcTransaction = { type: TransactionDirection /** Null for ecash that never had a bolt11 invoice — a token, or a payment request. */ invoice: string | null + /** The serialized zap request for a zap, per NIP-57; the wallet memo otherwise. */ description: string | null preimage: string | null payment_hash: string | null @@ -51,7 +52,12 @@ export const toNwcTransaction = function (t: Transaction): NwcTransaction { return { type: getTransactionDirection(t.type), invoice: t.paymentRequest ?? null, - description: t.memo ?? null, + // NIP-57: a zap's invoice description IS the signed kind-9734 zap request, and + // clients recover the sender, comment and zapped note from it. Ecash received at + // a lightning address has no bolt11 to carry it, so it would otherwise reach the + // client as an anonymous credit — the memo alone attributes nothing. Falls back to + // the memo, which is the zap request's own `content` when there is one. + description: t.zapRequest ?? t.memo ?? null, preimage: t.proof ?? null, payment_hash: t.paymentId ?? null, amount: t.amount * 1000,