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 /src/features | |
| 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 'src/features')
| -rw-r--r-- | src/features/hats.js | 62 |
1 files changed, 35 insertions, 27 deletions
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) => `<svg viewBox="0 0 64 44" xmlns="http://www.w3.org/2000/svg">${inner}</svg>`; 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(`<circle cx="32" cy="9" r="5.5" fill="#f3e6c9"/> <path d="M11 34 Q11 12 32 12 Q53 12 53 34 Z" fill="#e0554a"/> <path d="M20 14.5 Q26 25 25 32 M32 12.5 Q32 23 32 32 M44 14.5 Q38 25 39 32" stroke="#c9443b" stroke-width="2" fill="none"/> @@ -33,14 +36,14 @@ const HATS = [ <path d="M15 33 V39.5 M23 32.5 V40.5 M31 32.5 V40.5 M39 32.5 V40.5 M47 33 V39.5" stroke="#b23a32" stroke-width="2"/>`), }, { - 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(`<path d="M32 5 L46 39 Q32 44 18 39 Z" fill="#2fb3a5"/> <path d="M26.5 18.5 Q32 20.5 37.5 18.5 L40 24.5 Q32 27 24 24.5 Z" fill="#ff7bac"/> <path d="M21.5 30.5 Q32 34 42.5 30.5 L44.5 35.5 Q32 40 19.5 35.5 Z" fill="#ffd166"/> <circle cx="32" cy="5" r="4.2" fill="#ffd166"/>`), }, { - 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(`<path d="M9 33 Q9 10 29 10 Q49 10 49 33 Z" fill="#3579c2"/> <path d="M17 33 Q17 14 29 14 Q41 14 41 33 Z" fill="#f2f5f8"/> <text x="29" y="28" font-size="14" font-weight="700" text-anchor="middle" fill="#f7931a" font-family="system-ui,sans-serif">₿</text> @@ -49,7 +52,7 @@ const HATS = [ <rect x="9" y="32" width="39" height="5" rx="2.5" fill="#2a5f9c"/>`), }, { - 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(`<path d="M21 26 Q21 5 32 5 Q43 5 43 26 Z" fill="#a97844"/> <path d="M32 5.5 Q29.5 14 31 24" stroke="#8a5f33" stroke-width="1.8" fill="none"/> <rect x="20" y="20.5" width="24" height="5" rx="2" fill="#6f4a26"/> @@ -57,21 +60,27 @@ const HATS = [ <path d="M8 28 Q17 32.5 32 32.5 Q47 32.5 56 28" stroke="#8a5f33" stroke-width="1.5" fill="none"/>`), }, { - 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(`<path d="M18 29 L20 11 Q32 6 44 11 L46 29 Z" fill="#4c525b" stroke="rgba(255,255,255,.1)" stroke-width="0.8"/> <path d="M20 11 Q32 15.5 44 11" stroke="#3a3f47" stroke-width="2" fill="none"/> <path d="M17.4 23 L46.6 23 L47 29 L17 29 Z" fill="#23262c"/> <path d="M6 31 Q32 25 58 31 Q58 37 32 37.5 Q6 37 6 31 Z" fill="#3a3f47" stroke="rgba(255,255,255,.1)" stroke-width="0.8"/>`), }, { - 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(`<path d="M15 29 Q15 7 32 7 Q49 7 49 29 Z" fill="#2b2e35" stroke="rgba(255,255,255,.14)" stroke-width="0.8"/> <path d="M22 11 Q27 8.5 33 9.5" stroke="#464b55" stroke-width="2" fill="none" stroke-linecap="round"/> <path d="M14.6 24 L49.4 24 L49.6 29 L14.4 29 Z" fill="#17191d"/> <path d="M7 31 Q7 26.5 13 29 L51 29 Q57 26.5 57 31 Q57 36 32 36 Q7 36 7 31 Z" fill="#17191d" stroke="rgba(255,255,255,.14)" stroke-width="0.8"/>`), }, { - 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(`<path d="M17 5 Q32 1.5 47 5 L45.5 28 L18.5 28 Z" fill="#26282f" stroke="rgba(255,255,255,.14)" stroke-width="0.8"/> + <path d="M18.7 22 L45.3 22 L45.6 28 L18.4 28 Z" fill="#b6382e"/> + <path d="M7 30.5 Q7 26.5 14 29 L50 29 Q57 26.5 57 30.5 Q57 36 32 36 Q7 36 7 30.5 Z" fill="#17191d" stroke="rgba(255,255,255,.14)" stroke-width="0.8"/>`), + }, + { + id: 'wizard', nameKey: 'hatWizard', sat: 2100000, w: 112, b: 66, r: -4, dx: -8, art: svg(`<path d="M25 35 Q33 21 36.5 11 Q37.5 5.5 44 3 Q41 9 40 13 Q44 25 50 35 Z" fill="#6d4bb8"/> <circle cx="44.5" cy="3.5" r="2.6" fill="#ffd166"/> <ellipse cx="37" cy="36" rx="21" ry="5" fill="#59389f"/> @@ -79,13 +88,7 @@ const HATS = [ <path d="M41.5 26 l0.9 2 2 0.9 -2 0.9 -0.9 2 -0.9 -2 -2 -0.9 2 -0.9 Z" fill="#ffe08a"/>`), }, { - id: 'top', nameKey: 'hatTop', sat: 2100000, w: 96, b: 62, r: -7, - art: svg(`<path d="M17 5 Q32 1.5 47 5 L45.5 28 L18.5 28 Z" fill="#26282f" stroke="rgba(255,255,255,.14)" stroke-width="0.8"/> - <path d="M18.7 22 L45.3 22 L45.6 28 L18.4 28 Z" fill="#b6382e"/> - <path d="M7 30.5 Q7 26.5 14 29 L50 29 Q57 26.5 57 30.5 Q57 36 32 36 Q7 36 7 30.5 Z" fill="#17191d" stroke="rgba(255,255,255,.14)" stroke-width="0.8"/>`), - }, - { - 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(`<path d="M10 38 L7 13 L20 25 L32 6 L44 25 L57 13 L54 38 Z" fill="#f2b32a"/> <circle cx="7" cy="12" r="3" fill="#ffd97a"/><circle cx="32" cy="6" r="3" fill="#ffd97a"/><circle cx="57" cy="12" r="3" fill="#ffd97a"/> <rect x="9" y="34" width="46" height="8" rx="2.5" fill="#d9930f"/> @@ -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'))); } |
