aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/features/hats.js62
-rw-r--r--src/i18n.js4
2 files changed, 37 insertions, 29 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')));
}
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',