aboutsummaryrefslogtreecommitdiffstats
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
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
-rw-r--r--names/server.js64
-rw-r--r--src/features/hats.js62
-rw-r--r--src/i18n.js4
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',