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 | |
| 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
| -rw-r--r-- | names/server.js | 64 | ||||
| -rw-r--r-- | src/features/hats.js | 62 | ||||
| -rw-r--r-- | src/i18n.js | 4 |
3 files changed, 76 insertions, 54 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) ------------------------------ 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'))); } 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', |
