aboutsummaryrefslogtreecommitdiffstats
path: root/tools/gift-from-test.js
blob: a0ffaf82f2474d6d9b04a6c5eb8d05d66a6534c7 (plain) (blame)
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);