// A zap is signed as the person zapping: with a login identity (extension, // bunker, passkey…) the zap request's pubkey is that identity, not the // wallet's own key — a signed-in person's zaps showed in stream chats (and // streamers' overlays) under the wallet key's stranger npub. Without a // login, the wallet key signs as before. // Run: bun tools/zap-signer-test.js import assert from 'node:assert/strict'; import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure'; import { zapsFeature } from '../src/features/zaps.js'; const check = (name, ok, detail = '') => { assert(ok, name + (detail ? ' — ' + detail : '')); console.log(' ✓ ' + name); }; const walletSk = generateSecretKey(), loginSk = generateSecretKey(), host = getPublicKey(generateSecretKey()); let asked = null; globalThis.fetch = async (url) => { const u = new URL(url); if (u.pathname.startsWith('/.well-known/lnurlp/')) return Response.json({ tag: 'payRequest', callback: 'https://ln.example/cb', minSendable: 1000, maxSendable: 1e9, allowsNostr: true, nostrPubkey: host }); if (u.pathname === '/cb') { asked = JSON.parse(u.searchParams.get('nostr')); return Response.json({ pr: 'lnbc210n1fake' }); } return new Response('no', { status: 404 }); }; async function zapWith(login) { asked = null; let settled = null; const ctx = { h: () => null, ui: {}, render: () => {}, toast: (m) => console.log('toast:', m), fmtAmount: String, unitLabel: () => 'sats', parseAmount: Number, getUnit: () => 'sat', unitTag: () => '', blankSend: () => {}, wallet: { watchOnly: false, nostrRelays: () => ['wss://relay.example'], nostrProfile: async () => ({ lud16: 'streamer@ln.example' }), nostrSign: (e) => finalizeEvent(e, walletSk) }, hook(name, ...a) { if (name === 'canLnPay' || name === 'arkReady') return true; if (name === 'nostrLoginIdentity') return login; if (name === 'arkPayInvoice') return Promise.resolve({}); if (name === 'zapSettled') { settled = a; return null; } return null; }, }; zapsFeature(ctx).lnZapNpub(host, null, 'e'.repeat(64), 21); for (let i = 0; i < 50 && !settled; i++) await new Promise((r) => setTimeout(r, 20)); return { req: asked, settled }; } const signedIn = await zapWith({ pubkey: getPublicKey(loginSk), signer: { signEvent: async (e) => finalizeEvent(e, loginSk) } }); check('signed in: the zap goes through', signedIn.settled && signedIn.settled[1] === true, JSON.stringify(signedIn.settled)); check('...and its request is signed by the login identity', signedIn.req && signedIn.req.kind === 9734 && signedIn.req.pubkey === getPublicKey(loginSk), signedIn.req && signedIn.req.pubkey); const plain = await zapWith(null); check('no login: the wallet\'s own key signs it', plain.req && plain.req.pubkey === getPublicKey(walletSk)); console.log('\n✅ zaps are signed as the person zapping');