clone

nostr relays on demand at <name>.bind.ws: claim one with your key, pay in zaps past the free allowance

nip11: auth_required whenever a fresh socket cannot read, 46 in supported_nips; the card's QR is scanned in the test

Dami committed b143f05
commit
b143f053edbecd7564ba2738ca96587d98f8da80
parent
8745d13982ee67d67ac5c78cb7c5f6ba65c76075
author
Dami <dami.etoile@gmail.com>
3 files changed, +33 -15
Msrc/relay.ts5++--
Mtest/card.test.ts39++++++++++++++++++++----------
Mtest/nip46.test.ts4+++
M src/relay.ts +3 -2
M test/card.test.ts +26 -13
@@ -18,6 +18,23 @@ async function rpc(host: string, sk: Uint8Array, method: string, ...params: unkn
1818 }1919 const get = (host: string, path: string) => SELF.fetch(`http://${host}${path}`);2020 21+// scan rasterizes a module grid and reads it with jsQR, a decoder that22+// shares nothing with the encoder, so it proves a phone can read it.23+function scan(size: number, dark: (x: number, y: number) => boolean): string | undefined {24+ const px = 4, margin = 4, n = size + margin * 2, w = n * px;25+ const img = new Uint8ClampedArray(w * w * 4).fill(255);26+ for (let y = 0; y < size; y++) {27+ for (let x = 0; x < size; x++) {28+ if (!dark(x, y)) continue;29+ for (let dy = 0; dy < px; dy++) for (let dx = 0; dx < px; dx++) {30+ const i = (((y + margin) * px + dy) * w + (x + margin) * px + dx) * 4;31+ img[i] = img[i + 1] = img[i + 2] = 0;32+ }33+ }34+ }35+ return jsQR(img, w, w)?.data;36+}37+2138 // readBack decodes a symbol without any error correction: it must be exact.2239 function readBack(sym: ReturnType<typeof encode>): { mask: number; level: number; bytes: Uint8Array } {2340 const { size, modules, version } = sym;
@@ -113,19 +130,7 @@ describe("qr encoder", () => {
113130 const full = Array.from({ length: 20 }, (_, i) => Array.from({ length: capacity(i + 1) }, (_, j) => String.fromCharCode(48 + ((j * 7 + i) % 74))).join(""));114131 for (const text of ["nostr:npub1abc", uri, ...full]) {115132 const sym = encode(text);116- const px = 4, margin = 4, n = sym.size + margin * 2, w = n * px;117- const img = new Uint8ClampedArray(w * w * 4).fill(255);118- for (let y = 0; y < sym.size; y++) {119- for (let x = 0; x < sym.size; x++) {120- if (!sym.modules[y * sym.size + x]) continue;121- for (let dy = 0; dy < px; dy++) for (let dx = 0; dx < px; dx++) {122- const i = (((y + margin) * px + dy) * w + (x + margin) * px + dx) * 4;123- img[i] = img[i + 1] = img[i + 2] = 0;124- }125- }126- }127- const r = jsQR(img, w, w);128- expect(r?.data, `version ${sym.version}`).toBe(text);133+ expect(scan(sym.size, (x, y) => sym.modules[y * sym.size + x] === 1), `version ${sym.version}`).toBe(text);129134 }130135 });131136 });
@@ -185,6 +190,14 @@ describe("relay card", () => {
185190 expect(svg).toContain("members write");186191 expect(svg).toContain("group naddr");187192 expect(svg).toContain('<path d="M');193+ // The QR on the card scans to the naddr: the card scales the modules194+ // into a 150 unit box, so read the cells back from the path.195+ const path = svg.match(/<g transform="translate\(418 58\)">.*?<path d="([^"]+)"/)![1];196+ const cells = [...path.matchAll(/M([\d.]+) ([\d.]+)h([\d.]+)/g)].map((m) => ({ x: +m[1], y: +m[2], cell: +m[3] }));197+ const size = encode(c.naddr).size;198+ expect(cells.length).toBeGreaterThan(100);199+ const dark = new Set(cells.map((k) => Math.round(k.x / k.cell) + "," + Math.round(k.y / k.cell)));200+ expect(scan(size, (x, y) => dark.has(x + "," + y))).toBe(c.naddr);188201 });189202 190203 it("says so on unclaimed and leased relays, and does not sign for them", async () => {
M test/nip46.test.ts +4 -0