From 0ee50c5f64feb9419f799fb53c99b4b852e52452 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 17 Aug 2026 22:34:53 -0700 Subject: 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 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz --- src/features/hats.js | 62 +++++++++++++++++++++++++++++----------------------- src/i18n.js | 4 ++-- 2 files changed, 37 insertions(+), 29 deletions(-) (limited to 'src') diff --git a/src/features/hats.js b/src/features/hats.js index 5f12ed0..0bd4545 100644 --- a/src/features/hats.js +++ b/src/features/hats.js @@ -17,6 +17,9 @@ import { nip98Header } from '../nostr-login.js'; import { getNetwork } from '../api.js'; const REGISTRAR = 'https://names.coinos.io'; +// Hats exist per network: mainnet hats are the real supporter hats; mutinynet +// runs a parallel play-money economy so staging can exercise the whole flow. +const hatNet = () => (getNetwork() === 'mutinynet' ? 'mutinynet' : 'mainnet'); const ADMIN_PK = '98ae4da926c471c23fd12d1ebdd5839ba82917baa618e184e0c9916d93dcf4f7'; // One drawing per hat, tuned to perch on a circle: `w` scales against the @@ -25,7 +28,7 @@ const ADMIN_PK = '98ae4da926c471c23fd12d1ebdd5839ba82917baa618e184e0c9916d93dcf4 const svg = (inner) => `${inner}`; const HATS = [ { - id: 'beanie', nameKey: 'hatBeanie', sat: 21, w: 96, b: 56, r: -8, + id: 'beanie', nameKey: 'hatBeanie', sat: 21, w: 96, b: 60, r: -8, art: svg(` @@ -33,14 +36,14 @@ const HATS = [ `), }, { - id: 'party', nameKey: 'hatParty', sat: 2100, w: 66, b: 66, r: 11, + id: 'party', nameKey: 'hatParty', sat: 2100, w: 78, b: 70, r: -11, art: svg(` `), }, { - id: 'trucker', nameKey: 'hatTrucker', sat: 2100, w: 106, b: 58, r: -6, + id: 'trucker', nameKey: 'hatTrucker', sat: 2100, w: 106, b: 62, r: -6, art: svg(` ₿ @@ -49,7 +52,7 @@ const HATS = [ `), }, { - id: 'cowboy', nameKey: 'hatCowboy', sat: 21000, w: 122, b: 58, r: -7, + id: 'cowboy', nameKey: 'hatCowboy', sat: 21000, w: 122, b: 62, r: -7, art: svg(` @@ -57,21 +60,27 @@ const HATS = [ `), }, { - id: 'fedora', nameKey: 'hatFedora', sat: 21000, w: 118, b: 60, r: -8, + id: 'fedora', nameKey: 'hatFedora', sat: 21000, w: 118, b: 64, r: -8, art: svg(` `), }, { - id: 'bowler', nameKey: 'hatBowler', sat: 210000, w: 104, b: 60, r: -7, + id: 'bowler', nameKey: 'hatBowler', sat: 210000, w: 104, b: 64, r: -7, art: svg(` `), }, { - id: 'wizard', nameKey: 'hatWizard', sat: 210000, w: 112, b: 62, r: -4, + id: 'top', nameKey: 'hatTop', sat: 210000, w: 96, b: 66, r: -7, + art: svg(` + + `), + }, + { + id: 'wizard', nameKey: 'hatWizard', sat: 2100000, w: 112, b: 66, r: -4, dx: -8, art: svg(` @@ -79,13 +88,7 @@ const HATS = [ `), }, { - id: 'top', nameKey: 'hatTop', sat: 2100000, w: 96, b: 62, r: -7, - art: svg(` - - `), - }, - { - id: 'crown', nameKey: 'hatCrown', sat: null, w: 86, b: 62, r: 0, + id: 'crown', nameKey: 'hatCrown', sat: null, w: 86, b: 66, r: -7, art: svg(` @@ -93,7 +96,7 @@ const HATS = [ }, ]; const hatById = (id) => HATS.find((x) => x.id === id) || null; -const posStyle = (hat) => `width:${hat.w}%;bottom:${hat.b}%;transform:translateX(-50%) rotate(${hat.r}deg)`; +const posStyle = (hat) => `width:${hat.w}%;bottom:${hat.b}%;transform:translateX(${-50 + (hat.dx || 0)}%) rotate(${hat.r}deg)`; export function hatsFeature(ctx) { const { h, ui, render, wallet, hook, toast, brandHeader } = ctx; @@ -135,11 +138,12 @@ export function hatsFeature(ctx) { pending.clear(); if (!pks.length) return; try { - const r = await fetch(`${REGISTRAR}/hats?pks=${pks.join(',')}`); + const net = hatNet(); + const r = await fetch(`${REGISTRAR}/hats?pks=${pks.join(',')}&net=${net}`); const j = await r.json(); if (!r.ok) throw new Error('hats lookup failed'); const now = Date.now(); - for (const pk of pks) worn.set(pk, { hat: (j.hats || {})[pk] || null, t: now }); + for (const pk of pks) worn.set(net + ':' + pk, { hat: (j.hats || {})[pk] || null, t: now }); saveStore(); if ([...worn.entries()].some(([pk, v]) => v.hat && v.t === now)) render(); } catch { @@ -151,7 +155,7 @@ export function hatsFeature(ctx) { function hatFor(pk) { if (!pk || !/^[0-9a-f]{64}$/.test(pk)) return null; loadStore(); - const v = worn.get(pk); + const v = worn.get(hatNet() + ':' + pk); if (!v || Date.now() - v.t > TTL) queueFetch(pk); return v ? v.hat : null; } @@ -195,11 +199,11 @@ export function hatsFeature(ctx) { render(); const me = ctx.shownPubkey(); if (!me) { ui.hatShopData = { owned: [], equipped: null }; render(); return; } - fetch(`${REGISTRAR}/hats/${me}`).then((r) => r.json()).then((j) => { + fetch(`${REGISTRAR}/hats/${me}?net=${hatNet()}`).then((r) => r.json()).then((j) => { if (!ui.hatShop) return; ui.hatShopData = { owned: j.owned || [], equipped: j.equipped || null, prices: j.prices || null }; const now = Date.now(); - worn.set(me, { hat: j.equipped || null, t: now }); + worn.set(hatNet() + ':' + me, { hat: j.equipped || null, t: now }); saveStore(); render(); }).catch(() => { @@ -210,7 +214,7 @@ export function hatsFeature(ctx) { async function buy(hat) { const price = priceOf(hat); - if (getNetwork() !== 'mainnet') { toast(t('hatMainnetOnly')); return; } + if (!['mainnet', 'mutinynet'].includes(getNetwork())) { toast(t('hatMainnetOnly')); return; } if (!hook('arkReady')) { toast(t('hatNeedSpending')); return; } const spendable = hook('arkSpendableSat') || 0; // routing fee headroom: the ASP charges the quoted route fee (~1%) @@ -223,11 +227,11 @@ export function hatsFeature(ctx) { try { const id = await identity(); if (!id) throw new Error(t('msgNoIdentity')); - const inv = await post('/hats/invoice', { hat: hat.id }, id.signer); + const inv = await post('/hats/invoice', { hat: hat.id, net: hatNet() }, id.signer); await hook('arkPayInvoice', inv.invoice, { maxAmountSat: inv.sat }); - const rec = await post('/hats/claim', { paymentHash: inv.paymentHash, hat: hat.id }, id.signer); + const rec = await post('/hats/claim', { paymentHash: inv.paymentHash, hat: hat.id, net: hatNet() }, id.signer); ui.hatShopData = { ...ui.hatShopData, owned: rec.owned, equipped: rec.equipped }; - worn.set(id.pubkey, { hat: rec.equipped, t: Date.now() }); + worn.set(hatNet() + ':' + id.pubkey, { hat: rec.equipped, t: Date.now() }); saveStore(); toast(t('hatYours', { name: t(hat.nameKey) })); } catch (e) { @@ -245,9 +249,9 @@ export function hatsFeature(ctx) { try { const id = await identity(); if (!id) throw new Error(t('msgNoIdentity')); - const rec = await post('/hats/equip', { hat: hatId }, id.signer); + const rec = await post('/hats/equip', { hat: hatId, net: hatNet() }, id.signer); ui.hatShopData = { ...ui.hatShopData, owned: rec.owned, equipped: rec.equipped }; - worn.set(id.pubkey, { hat: rec.equipped, t: Date.now() }); + worn.set(hatNet() + ':' + id.pubkey, { hat: rec.equipped, t: Date.now() }); saveStore(); } catch (e) { toast(e.message || String(e)); @@ -307,7 +311,11 @@ export function hatsFeature(ctx) { h('p', { class: 'small muted', style: 'margin:0' }, t('hatShopBlurb')), d === null ? h('div', { style: 'text-align:center;padding:16px' }, h('span', { class: 'spinner' })) - : h('div', { class: 'col', style: 'gap:2px' }, ...HATS.map((hat) => shopRow(hat, d, me))), + : h('div', { class: 'col', style: 'gap:2px' }, + // the crown stays off the menu — it appears only for the head + // that already wears it + ...HATS.filter((hat) => hat.id !== 'crown' || me === ADMIN_PK || (d.owned || []).includes('crown')) + .map((hat) => shopRow(hat, d, me))), d && d.offline ? h('div', { class: 'small faint', style: 'text-align:center' }, t('hatShopOffline')) : null), h('button', { class: 'btn-ghost btn-block', onClick: closeShop }, t('back'))); } diff --git a/src/i18n.js b/src/i18n.js index 8615124..a0357ab 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -223,7 +223,7 @@ const STR = { hatShopTitle: 'Hat shop', hatShopBlurb: 'Support coinos and wear the proof: your hat shows above your avatar everywhere you appear.', hatShopOffline: 'The hat stand is unreachable right now.', - hatBeanie: 'Beanie', + hatBeanie: 'Toque', hatParty: 'Party hat', hatTrucker: 'Trucker cap', hatCowboy: 'Cowboy hat', @@ -241,7 +241,7 @@ const STR = { hatYours: '🎩 The {name} is yours!', hatNoFunds: 'You need {sats} sats in Spending for that hat', hatNeedSpending: 'Open your Spending balance first', - hatMainnetOnly: 'Hats are sold on mainnet only', + hatMainnetOnly: 'Hats are sold on mainnet and Mutinynet only', profNotesTitle: 'Latest posts', profNotesNone: 'No recent posts found.', profReplyTag: 'reply', -- cgit v1.2.3