aboutsummaryrefslogtreecommitdiffstats
path: root/names
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-17 22:34:53 -0700
committerAdam Soltys <[email protected]>2026-08-17 22:34:53 -0700
commit0ee50c5f64feb9419f799fb53c99b4b852e52452 (patch)
treedfa186b63c41d218b5a712b9a6419d2addfe9144 /names
parent7045711d9d6a639d0180a049fe0fd7ad61a2fcf3 (diff)
downloadcoinosv3-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.js64
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) ------------------------------