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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
// A gift link can name its sender: #f=<base32 pubkey|created_at|sig>, a
// nostr signature over the gift's code. The claim screen shows who it's
// from (and that claiming follows them); a link whose signature doesn't
// match its code — passed on as someone else's gift — shows no sender.
// Run: bun run build && bun tools/gift-from-test.js
import puppeteer from './puppeteer.js';
import { base32nopad } from '@scure/base';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { sha256 } from '@noble/hashes/sha256';
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let ok = true;
const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; };
const server = Bun.serve({ port: 0, async fetch(req) {
const p = new URL(req.url).pathname;
const f = Bun.file('dist' + (p === '/' ? '/index.html' : p));
return (await f.exists()) ? new Response(f) : new Response(Bun.file('dist/index.html'), { headers: { 'content-type': 'text/html' } });
} });
const giftCode = () => {
const b = new Uint8Array(42);
b[0] = 0x11; b[1] = 3; // ark gift, mutinynet
new DataView(b.buffer).setBigUint64(2, 1000n, true);
b.set(crypto.getRandomValues(new Uint8Array(32)), 10);
return base32nopad.encode(b);
};
const sk = generateSecretKey();
const fromFrag = (code) => {
const ev = finalizeEvent({ kind: 27121, created_at: Math.floor(Date.now() / 1000), content: '',
tags: [['x', bytesToHex(sha256(new TextEncoder().encode(code.toUpperCase())))]] }, sk);
const b = new Uint8Array(100);
b.set(hexToBytes(ev.pubkey), 0);
new DataView(b.buffer).setUint32(32, ev.created_at);
b.set(hexToBytes(ev.sig), 36);
return base32nopad.encode(b);
};
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
const text = (page) => page.evaluate(() => document.body.innerText);
const waitText = async (page, re, ms = 30000) => {
for (let t = 0; t < ms; t += 250) { if (re.test(await text(page))) return true; await sleep(250); }
return false;
};
async function open(path) {
const page = await browser.newPage();
await page.goto(server.url.href);
await page.evaluate(() => { localStorage.clear(); localStorage.setItem('btc-wallet-network', 'mutinynet'); });
await page.goto(server.url.href + path);
await waitText(page, /aren.t visible/i);
await sleep(500);
return page;
}
try {
const code = giftCode();
let page = await open(`ag/${code}#m=hi&f=${fromFrag(code)}`);
let tx = await text(page);
check('a signed link names its sender', /\bFrom\b/.test(tx) && /Claiming it follows them/.test(tx), tx.slice(0, 300).replace(/\n/g, ' | '));
check('the note still shows', /\bhi\b/.test(tx));
await page.close();
// an upper-case key, as a scanner may hand it over
page = await open(`ag/${code}#F=${fromFrag(code)}`);
{ const tx = await text(page); check("an upper-cased link still names its sender", /Claiming it follows them/.test(tx), tx.slice(0, 200).replace(/\n/g, " | ")); }
await page.close();
// the same signature on a different gift: no sender
page = await open(`ag/${giftCode()}#f=${fromFrag(code)}`);
check('a signature over another code names nobody', !/Claiming it follows them/.test(await text(page)));
await page.close();
} finally {
await browser.close();
server.stop();
}
console.log(ok ? 'PASS' : 'FAIL');
process.exit(ok ? 0 : 1);
|