// Lists (NIP-51 follow sets) and linkable posts. A kind-30000 list of yours // on the relays turns up as a feed; a thread puts its nevent in the address // bar and its ⋯ menu opens (with Copy link); a profile is /npub1…; "Add to // a list" from a profile starts a new list that goes out as a kind 30000; // unticking "Keep as a nostr list" sends the NIP-09 deletion. No relay is // written for real — the WebSocket shim swallows EVENT and answers OK. // Run: bun tools/feed-list-test.js import puppeteer from 'puppeteer-core'; import { cacheKeyFor } from '../src/wallet.js'; import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure'; import { privateKeyFromSeedWords } from 'nostr-tools/nip06'; import { decode } from 'nostr-tools/nip19'; import { buildHtml } from '../build.js'; 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 SK = generateSecretKey(); const AUTHOR = getPublicKey(SK); const MEMBER = getPublicKey(generateSecretKey()); const signed = (content, ago, tags = [], sk = SK) => finalizeEvent({ kind: 1, created_at: Math.floor(Date.now() / 1000) - ago, tags, content }, sk); const seeded = [signed('a post to open', 300), signed('another post', 600)]; const html = await buildHtml({ minify: true, pwa: false }); const server = Bun.serve({ port: 5298, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) }); const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] }); const page = await browser.newPage(); await page.evaluateOnNewDocument(() => { window.__reqs = []; window.__published = []; const Real = window.WebSocket; window.WebSocket = function (...a) { const ws = new Real(...a); const send = ws.send.bind(ws); ws.send = (data) => { try { const m = JSON.parse(data); if (m[0] === 'REQ' && m[2]) { window.__reqs.push(m[2]); const list = window.__list; if (list && Array.isArray(m[2].kinds) && m[2].kinds.includes(30000) && m[2].authors && m[2].authors.includes(list.pubkey)) { setTimeout(() => { ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EVENT', m[1], list]) })); ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EOSE', m[1]]) })); }, 0); } } // nothing is written to a real relay: swallow it, say OK if (m[0] === 'EVENT' && m[1]) { window.__published.push(m[1]); setTimeout(() => ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['OK', m[1].id, true, '']) })), 0); return; } } catch {} return send(data); }; return ws; }; window.WebSocket.prototype = Real.prototype; Object.assign(window.WebSocket, Real); }); const errs = []; page.on('pageerror', (e) => errs.push(String(e))); const click = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); if (e) { e.click(); return true; } return false; }, t); const waitText = async (x, ms = 25000) => { for (let i = 0; i < ms / 250; i++) { if ((await page.evaluate(() => document.body.innerText)).toLowerCase().includes(x)) return true; await sleep(250); } return false; }; const waitFor = async (fn, ms = 10000) => { for (let i = 0; i < ms / 250; i++) { if (await page.evaluate(fn)) return true; await sleep(250); } return false; }; const chips = () => page.evaluate(() => [...document.querySelectorAll('.feed-chip')].map((c) => (c.classList.contains('on') ? '*' : '') + c.textContent.trim())); const path = () => page.evaluate(() => location.pathname); const type = async (sel, text) => { await page.evaluate((q) => { const i = document.querySelector(q); i.value = ''; i.dispatchEvent(new Event('input', { bubbles: true })); }, sel); await page.click(sel); await page.type(sel, text); }; const openFeed = async () => { await page.evaluate(() => { const b = [...document.querySelectorAll('button')].find((e) => /message/i.test(e.getAttribute('aria-label') || '')); if (b) b.click(); }); await sleep(800); await page.evaluate(() => { const e = [...document.querySelectorAll('.item')].find((n) => (n.querySelector('.chat-name') || {}).textContent === 'Feed'); if (e) e.click(); }); await sleep(1200); }; try { await page.setViewport({ width: 390, height: 844 }); await page.goto('http://localhost:5298/', { waitUntil: 'domcontentloaded' }); await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest')); await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400); await click('create a new wallet'); await sleep(600); await page.waitForSelector('.words .w .t'); const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' '); await click('skip verification'); await waitText('receive', 20000); const base = cacheKeyFor(mn + '\n'); const ME_SK = privateKeyFromSeedWords(mn); const ME = getPublicKey(ME_SK); // a list made in another client, under this wallet's own nostr key const LIST = finalizeEvent({ kind: 30000, created_at: Math.floor(Date.now() / 1000) - 50, tags: [['d', 'from-coracle'], ['title', 'From Coracle'], ['p', MEMBER]], content: '' }, ME_SK); await page.evaluateOnNewDocument((list) => { window.__list = list; }, LIST); await page.evaluate(([k, pk, ns]) => { localStorage.setItem(k + ':follows', JSON.stringify({ tags: [['p', pk]], c: '', at: Math.floor(Date.now() / 1000) })); localStorage.setItem(k + ':feedNotes', JSON.stringify(ns)); }, [base, AUTHOR, seeded]); await page.reload({ waitUntil: 'domcontentloaded' }); await waitText('receive', 20000); await openFeed(); console.log('\n[a list made elsewh
import { emit } from "$lib/sockets";
import config from "$config";
import { notify } from "$lib/notifications";
import { callWebhook } from "$lib/webhooks";
import ln from "$lib/ln";
import { l, err, warn } from "$lib/logging";
import { g, s, t, db } from "$lib/db";
import { credit, types } from "$lib/payments";
export async function listenForLightning() {
let event = await ln.waitanyinvoice(await g("pay_index"));
let {
payment_hash: hash,
bolt11,
pay_index,
status,
amount_received_msat,
payment_preimage: preimage
} = event;
await s("pay_index", pay_index);
setTimeout(listenForLightning);
let received = Math.round(amount_received_msat / 1000);
try {
l("incoming lightning payment", received);
if (!preimage) return;
let invoice = await g(`invoice:${hash}`);
if (!invoice) return warn("received lightning with no invoice", bolt11);
let payment = await g(`payment:${hash}`);
if (payment) return warn("already processed", hash);
await credit(hash, received, invoice.memo, preimage, types.lightning);
} catch (e) {
console.log(e);
err("problem receiving lightning payment", e.message);
}
}