diff options
| author | Adam Soltys <[email protected]> | 2026-08-17 22:34:53 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-17 22:34:53 -0700 |
| commit | 0ee50c5f64feb9419f799fb53c99b4b852e52452 (patch) | |
| tree | dfa186b63c41d218b5a712b9a6419d2addfe9144 /names | |
| parent | 7045711d9d6a639d0180a049fe0fd7ad61a2fcf3 (diff) | |
| download | coinosv3-0ee50c5f64feb9419f799fb53c99b4b852e52452.tar.gz coinosv3-0ee50c5f64feb9419f799fb53c99b4b852e52452.zip | |
Staging gets a hat economy of its own: mutinynet mints on its own CLN into its own registry, play sats never buy mainnet flex — plus the toque earns its Canadian name, the wizard takes the throne at 2.1M, the crown leaves the menu, and every hat sits a little higher and straighter
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
Diffstat (limited to 'names')
| -rw-r--r-- | names/server.js | 64 |
1 files changed, 39 insertions, 25 deletions
diff --git a/names/server.js b/names/server.js index b484be1..c6b092b 100644 --- a/names/server.js +++ b/names/server.js @@ -209,6 +209,9 @@ function requestInvoiceFromWallet(offerPk, { amountSat, description, zap }) { } const ln = CFG.ln ? lnBackend(CFG.ln) : null; +// Optional mutinynet CLN — lets staging sell (play-money) hats. Nothing else +// uses it: names, offers and forwards remain mainnet affairs. +const lnMut = CFG.lnMut ? lnBackend(CFG.lnMut) : null; // -- routing-fee quotes (the /lnquote endpoint) ------------------------------ @@ -331,20 +334,26 @@ async function publishZapReceipt(pending, inv) { const HAT_ADMIN_PK = '98ae4da926c471c23fd12d1ebdd5839ba82917baa618e184e0c9916d93dcf4f7'; const HAT_PRICES = { beanie: 21, party: 2100, trucker: 2100, cowboy: 21000, fedora: 21000, - bowler: 210000, wizard: 210000, top: 2100000, + bowler: 210000, top: 210000, wizard: 2100000, }; -function hatRec(pk) { - const r = (state.hats || {})[pk]; +// Mutinynet runs a parallel hat economy (invoices minted on the mutinynet +// CLN, grants in their own registry) so play-money purchases can exercise +// the whole flow on staging without ever buying a hat mainnet users see. +const hatNetOf = (v) => (v === 'mutinynet' ? 'mutinynet' : 'mainnet'); +const hatStore = (net) => (net === 'mutinynet' ? (state.hatsMut ||= {}) : (state.hats ||= {})); +const hatLn = (net) => (net === 'mutinynet' ? lnMut : ln); + +function hatRec(pk, net = 'mainnet') { + const r = hatStore(net)[pk]; const owned = r ? [...(r.owned || [])] : []; if (pk === HAT_ADMIN_PK && !owned.includes('crown')) owned.push('crown'); const equipped = r ? (r.equipped || null) : (pk === HAT_ADMIN_PK ? 'crown' : null); return { owned, equipped }; } -function grantHat(pk, hat) { - state.hats ||= {}; - const r = state.hats[pk] ||= { owned: [], equipped: null }; +function grantHat(pk, hat, net = 'mainnet') { + const r = hatStore(net)[pk] ||= { owned: [], equipped: null }; if (!r.owned.includes(hat)) r.owned.push(hat); r.equipped = hat; // a fresh purchase goes straight on the head } @@ -365,10 +374,10 @@ async function settleLoop() { // hat purchases settle here too — grant and keep the sats (that's the point) const hatPending = state.hatInvoices && state.hatInvoices[inv.payment_hash]; if (hatPending) { - grantHat(hatPending.pubkey, hatPending.hat); + grantHat(hatPending.pubkey, hatPending.hat, hatNetOf(hatPending.net)); delete state.hatInvoices[inv.payment_hash]; persist(); - log(`hat sold: ${hatPending.hat} to ${hatPending.pubkey.slice(0, 12)}`); + log(`hat sold: ${hatPending.hat} (${hatNetOf(hatPending.net)}) to ${hatPending.pubkey.slice(0, 12)}`); continue; } const offerId = inv.local_offer_id; @@ -670,10 +679,11 @@ Bun.serve({ // Batched display lookup: which hat is each of these heads wearing? // Public data (a hat is worn in public by construction). if (url.pathname === '/hats' && req.method === 'GET') { + const net = hatNetOf(url.searchParams.get('net')); const pks = (url.searchParams.get('pks') || '') .split(',').filter((p) => /^[0-9a-f]{64}$/.test(p)).slice(0, 100); const hats = {}; - for (const pk of pks) hats[pk] = hatRec(pk).equipped; + for (const pk of pks) hats[pk] = hatRec(pk, net).equipped; return json({ hats }); } @@ -681,7 +691,7 @@ Bun.serve({ // client shows what the server would actually charge). const hm = url.pathname.match(/^\/hats\/([0-9a-f]{64})$/); if (hm && req.method === 'GET') { - return json({ ...hatRec(hm[1]), prices: HAT_PRICES }); + return json({ ...hatRec(hm[1], hatNetOf(url.searchParams.get('net'))), prices: HAT_PRICES }); } if (url.pathname === '/hats/invoice' && req.method === 'POST') { @@ -692,24 +702,26 @@ Bun.serve({ let body; try { body = JSON.parse(bodyText); } catch { return json({ error: 'bad body' }, 400); } const hat = String(body.hat || ''); + const net = hatNetOf(body.net); const sat = HAT_PRICES[hat]; if (!sat) return json({ error: 'no such hat' }, 400); - if (hatRec(a.pubkey).owned.includes(hat)) return json({ error: 'you already own that hat' }, 400); - if (!ln) return json({ error: 'the hat stand is closed right now' }, 503); + if (hatRec(a.pubkey, net).owned.includes(hat)) return json({ error: 'you already own that hat' }, 400); + const node = hatLn(net); + if (!node) return json({ error: 'the hat stand is closed right now' }, 503); try { - const inv = await ln.call('invoice', { + const inv = await node.call('invoice', { amount_msat: sat * 1000, label: `hat-${hat}-${a.pubkey.slice(0, 8)}-${Date.now()}`, description: `coinos hat: ${hat}`, expiry: 900, }); state.hatInvoices ||= {}; - state.hatInvoices[inv.payment_hash] = { pubkey: a.pubkey, hat, sat, ts: Date.now() }; + state.hatInvoices[inv.payment_hash] = { pubkey: a.pubkey, hat, sat, net, ts: Date.now() }; for (const [h2, v] of Object.entries(state.hatInvoices)) { if (Date.now() - v.ts > 7 * 86400_000) delete state.hatInvoices[h2]; } persist(); - log(`hat invoice: ${hat} (${sat} sat) for ${a.pubkey.slice(0, 12)}`); + log(`hat invoice: ${hat} (${sat} sat, ${net}) for ${a.pubkey.slice(0, 12)}`); return json({ invoice: inv.bolt11, paymentHash: inv.payment_hash, sat }); } catch (e) { log('hat invoice failed: ' + e.message); @@ -728,25 +740,27 @@ Bun.serve({ try { body = JSON.parse(bodyText); } catch { return json({ error: 'bad body' }, 400); } const hash = String(body.paymentHash || ''); const hat = String(body.hat || ''); + const net = hatNetOf(body.net); const pending = (state.hatInvoices || {})[hash]; if (!pending) { // the settle loop may have beaten us to it - if (hat && hatRec(a.pubkey).owned.includes(hat)) return json(hatRec(a.pubkey)); + if (hat && hatRec(a.pubkey, net).owned.includes(hat)) return json(hatRec(a.pubkey, net)); return json({ error: 'unknown invoice' }, 404); } if (pending.pubkey !== a.pubkey) return json({ error: 'not your invoice' }, 403); - if (!ln) return json({ error: 'the hat stand is closed right now' }, 503); + const node = hatLn(hatNetOf(pending.net)); + if (!node) return json({ error: 'the hat stand is closed right now' }, 503); try { - const r = await ln.call('listinvoices', { payment_hash: hash }); + const r = await node.call('listinvoices', { payment_hash: hash }); if ((r.invoices || [])[0]?.status !== 'paid') return json({ error: 'invoice not paid yet' }, 402); } catch (e) { return json({ error: 'could not check the invoice' }, 500); } - grantHat(pending.pubkey, pending.hat); + grantHat(pending.pubkey, pending.hat, hatNetOf(pending.net)); delete state.hatInvoices[hash]; persist(); - log(`hat sold (claim): ${pending.hat} to ${pending.pubkey.slice(0, 12)}`); - return json(hatRec(a.pubkey)); + log(`hat sold (claim): ${pending.hat} (${hatNetOf(pending.net)}) to ${pending.pubkey.slice(0, 12)}`); + return json(hatRec(a.pubkey, hatNetOf(pending.net))); } if (url.pathname === '/hats/equip' && req.method === 'POST') { @@ -756,12 +770,12 @@ Bun.serve({ let body; try { body = JSON.parse(bodyText); } catch { return json({ error: 'bad body' }, 400); } const hat = body.hat ? String(body.hat) : null; - if (hat && !hatRec(a.pubkey).owned.includes(hat)) return json({ error: 'you do not own that hat' }, 403); - state.hats ||= {}; - const r = state.hats[a.pubkey] ||= { owned: [], equipped: null }; + const net = hatNetOf(body.net); + if (hat && !hatRec(a.pubkey, net).owned.includes(hat)) return json({ error: 'you do not own that hat' }, 403); + const r = hatStore(net)[a.pubkey] ||= { owned: [], equipped: null }; r.equipped = hat; persist(); - return json(hatRec(a.pubkey)); + return json(hatRec(a.pubkey, net)); } // --- LNURL-pay (LUD-06/16, NIP-57 zaps) ------------------------------ |
