1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
// 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: '[email protected]' }), 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');
|