/** * pnpm seed: ingest the initial mint list, run one probe cycle, run one discovery * backfill, print a summary table. * * The list below is every mint URL carried in a kind 38172 announcement on the old * site's relay pool at the time of the rebuild (see NOTES.md: the old project has no * hardcoded list of its own, its directory is built live from Nostr). Seeding is only * a warm start, discovery finds everything here on its own and keeps finding more. */ import { closeDb, getDb } from './db.ts'; import { closePool, runDiscovery } from './discovery.ts'; import { log } from './log.ts'; import { insertLnurlIfNew, insertMintIfNew } from './mints.ts'; import { probeAll } from './probe.ts'; import { listMints } from './queries.ts'; const SEED_MINTS = [ 'https://21mint.me', 'https://bitcointxoko.com/cashu/api/v1/dMk78c5aR7uhHzcqH3Bwqp', 'https://cashu.boats', 'https://cashu.mineracks.com', 'https://forge.flashapp.me', 'https://lnb.bolverker.com/cashu/api/v1/GZjPXyQXsgnZEgKZsLRQyy', 'https://lnwallet.app/cashu/api/v1/9tZLEVyknrtaGxeBjEecc4', 'https://bb.mountainlake.io/cashu/api/v1/WpczZwvNES7wMa8ForKGmh', 'https://mint.0xchat.com', 'https://mint.28waves.com', 'https://mint.belgianbitcoinembassy.org', 'https://mint.btcforplebs.com', 'https://mint.cashu.chat', 'https://mint.coinos.io', 'https://mint.endfiat.money', 'https://mint.fredix.xyz', 'https://mint.gitvid.net', 'https://mint.gwoq.com', 'https://mint.kashir.xyz', 'https://mint.lnfast.com', 'https://mint.lnserver.com', 'https://mint.lnvoltz.com', 'https://mint.lnw.cash', 'https://mint.lnwallet.app', 'https://mint.mikelcalvo.net', 'https://mint.minibits.cash/Bitcoin', 'https://mint.mineracks.com', 'https://mint.mountainlake.io', 'https://mint.nostrich.cc', 'https://mint.quixote.cash/cashu/api/v1/TX6nZECe8r6hjDxtdA6N5b', 'https://mint.sharegap.net', 'https://mint.westernbtc.com', 'https://mint2.nutmix.cash', 'https://obsessedjerky7.lnbits.com/cashu/api/v1/L4bNfiBtTyhEpZWdt6QTb4', 'https://smilemoji.cash:3338', 'https://stpaulinternet.net/cashu/api/v1/TTcmiEDSWmkefef3XqERKv', 'https://kashu.me', ]; /** * LNURL mints, seeded by URL alone. * * Short for the obvious reason: `kind:38174` is a proposed kind (see * `docs/KIND-LNURL-MINT.md`) and there are no announcements on the network yet, so * unlike Cashu — where this list was read off real 38172 events — there is nothing to * read it off. This is the reference instance the ecosystem was built against, and * discovery takes over the moment operators start announcing. * * Seeded with no status: `insertLnurlIfNew` writes `unknown` and the first probe cycle * decides, exactly as it does for a Cashu mint. Being on this list is not a claim that * a mint is up, and is not an endorsement of it. */ const SEED_LNURL = [ 'https://lnurl.21mint.me', ]; function pad(s: string, width: number): string { return s.length > width ? `${s.slice(0, width - 1)}…` : s.padEnd(width); } async function main(): Promise { const db = await getDb(); log.info('seeding', { db: db.label }); let added = 0; for (const url of SEED_MINTS) if (await insertMintIfNew(url)) added++; let addedLnurl = 0; for (const url of SEED_LNURL) if (await insertLnurlIfNew(url)) addedLnurl++; log.info('seed list ingested', { listed: SEED_MINTS.length + SEED_LNURL.length, added: added + addedLnurl, lnurl: addedLnurl, }); await probeAll(); const discovery = await runDiscovery(true); // Discovery almost always turns up mints the seed list did not have. Probe those too, // otherwise they sit at status 'unknown' with no metadata. if (discovery.newMints.length > 0) { log.info('probing newly discovered mints', { count: discovery.newMints.length }); await probeAll(); } const listed = await listMints(); const width = { host: 44, type: 9, status: 10, score: 7, reviews: 8, rating: 7 }; console.log(''); console.log( [ pad('MINT', width.host), pad('TYPE', width.type), pad('STATUS', width.status), pad('SCORE', width.score), pad('REVIEWS', width.reviews), pad('RATING', width.rating), 'VERSION', ].join(' '), ); console.log('-'.repeat(120)); for (const m of listed) { console.log( [ pad(m.host, width.host), pad(m.type, width.type), pad(m.status, width.status), pad(m.score.toFixed(3), width.score), pad(String(m.review_count), width.reviews), pad(m.rating_avg === null ? '-' : m.rating_avg.toFixed(1), width.rating), m.version ?? '-', ].join(' '), ); } const mints = listed.filter((m) => m.type === 'cashu'); const federations = listed.filter((m) => m.type === 'fedimint'); const lnurlMints = listed.filter((m) => m.type === 'lnurl'); const count = (rows: typeof listed, status: string) => rows.filter((m) => m.status === status).length; const reviews = listed.reduce((sum, m) => sum + m.review_count, 0); console.log('-'.repeat(120)); console.log( `${mints.length} mints (${count(mints, 'online')} online, ${count(mints, 'offline')} offline)`, ); /* * Federations are counted with `announced` called out rather than folded into the * offline total. It is the honest word for "nothing checks this one", and rolling it * into either of the other two would be the first place the site started claiming to * know something it does not. */ console.log( `${federations.length} federations (${count(federations, 'online')} confirmed up, ` + `${count(federations, 'offline')} reported down, ` + `${count(federations, 'announced')} announced only)`, ); console.log( `${lnurlMints.length} LNURL mints (${count(lnurlMints, 'online')} online, ` + `${count(lnurlMints, 'offline')} offline)`, ); console.log(`${reviews} reviews indexed`); closePool(); await closeDb(); process.exit(0); } void main();