aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--names/server.js68
1 files changed, 66 insertions, 2 deletions
diff --git a/names/server.js b/names/server.js
index fbb7737..a0a0bb7 100644
--- a/names/server.js
+++ b/names/server.js
@@ -333,6 +333,18 @@ if (CFG.forwarder?.mnemonic) {
log(`forwarder ark wallet ready — float ${fwd.balance().spendableSat} sat, receive ${fwd.address().slice(0, 24)}…`);
}
+// Is this name's wallet on another network than this registrar's node? Then
+// nothing payable may be minted for it here: a mainnet offer for a mutinynet
+// wallet takes real sats that have no route onward (2,092 sat parked
+// 2026-10-22, paid through staging names' mainnet offers). The ark address
+// decides; without one, a staging name is a mutinynet wallet.
+const REGISTRAR_TESTNET = (CFG.forwarder?.network || 'mainnet') !== 'mainnet';
+function otherNetwork(uri, domain) {
+ const ark = arkParamOf(uri || '');
+ if (ark) return ark.startsWith('tark') !== REGISTRAR_TESTNET;
+ return domain === 'staging.coinos.io' && !REGISTRAR_TESTNET;
+}
+
async function makeOffer(address) { // "name@domain"
if (!ln) return null;
const o = await ln.call('offer', { amount: 'any', description: address });
@@ -1130,6 +1142,9 @@ Bun.serve({
}
// Ring up a sale: {invoice:{fiat}} (the terminal's shape) or {amount} in sats.
if (url.pathname === '/pos/invoice' && req.method === 'POST') {
+ if (otherNetwork(state.names[a.key]?.uri, state.names[a.key]?.domain)) {
+ return json({ error: 'this name\'s wallet is on another network' }, 400);
+ }
if (!rateOk(ip, 60)) return json({ error: 'rate limited' }, 429);
if (!ln) return json({ error: 'invoicing is closed right now' }, 503);
const body = await req.json().catch(() => ({}));
@@ -1361,6 +1376,9 @@ Bun.serve({
const dup = mine.find(([, o]) => o.memo === memo);
if (dup) return json({ ok: true, offerId: dup[0], memo, bolt12: dup[1].bolt12, existing: true });
if (mine.length >= 10) return json({ error: 'too many offers' }, 400);
+ if (otherNetwork(state.names[key]?.uri, state.names[key]?.domain)) {
+ return json({ error: 'this name\'s wallet is on another network — mint the offer in the wallet' }, 400);
+ }
let o;
try {
// `issuer` keeps two users' identical memos from collapsing onto one
@@ -1904,8 +1922,10 @@ Bun.serve({
// The record's lno=: the owner's own offer when they set one, else a
// static offer on our node with the settle forwarder behind it.
let offer = existing?.offerId ? { offerId: existing.offerId, bolt12: existing.bolt12 } : null;
- if (lno) {
- // their node answers fetchinvoice directly now — retire our stand-in
+ if (lno || otherNetwork(claim.uri, domain)) {
+ // their node answers fetchinvoice directly now, or their wallet is on
+ // another network and our offer could only strand what it takes —
+ // retire our stand-in either way
if (offer && ln) await ln.call('disableoffer', { offer_id: offer.offerId }).catch(() => {});
offer = null;
} else if (!offer) {
@@ -2023,4 +2043,48 @@ Bun.serve({
});
settleLoop().catch((e) => log('settle loop died:', e.message));
+retireOtherNetworkOffers().catch((e) => log('retiring other-network offers failed: ' + e.message));
+
+// Names that got one of our offers before otherNetwork() existed (staging
+// names: 84 of them, plus 15 custom offers, on 2026-10-22): disable the offer
+// on the node, take its lno= out of the published record, drop custom ones.
+// Idempotent — a name it fixed has no offerId left — so every start re-runs
+// it and a name that failed (Cloudflare down, say) gets another go.
+async function retireOtherNetworkOffers() {
+ if (!ln) return;
+ let names = 0, customs = 0;
+ const gone = (e) => /unknown offer|already disabled|not found/i.test(e.message);
+ for (const [key, rec] of Object.entries(state.names)) {
+ if (!rec.offerId || !otherNetwork(rec.uri, rec.domain)) continue;
+ try {
+ await ln.call('disableoffer', { offer_id: rec.offerId }).catch((e) => { if (!gone(e)) throw e; });
+ const uri = rec.bolt12 ? withoutLno(rec.uri, rec.bolt12) : rec.uri;
+ if (uri !== rec.uri) rec.recordId = await cfWrite(key.split('@')[0], rec.domain, uri, rec.recordId);
+ rec.uri = uri;
+ delete rec.offerId;
+ delete rec.bolt12;
+ rec.updated = Date.now();
+ persist();
+ names++;
+ } catch (e) { log(`retiring the offer for ${key} failed: ${e.message}`); }
+ }
+ for (const [id, o] of Object.entries(state.offers || {})) {
+ const rec = state.names[o.key];
+ if (!rec || !otherNetwork(rec.uri, rec.domain)) continue;
+ try {
+ await ln.call('disableoffer', { offer_id: id }).catch((e) => { if (!gone(e)) throw e; });
+ delete state.offers[id];
+ persist();
+ customs++;
+ } catch (e) { log(`retiring custom offer ${id} for ${o.key} failed: ${e.message}`); }
+ }
+ if (names || customs) log(`retired our offers for wallets on another network: ${names} names, ${customs} custom offers`);
+}
+
+// The published uri minus our lno= parameter (the owner's other params kept).
+function withoutLno(uri, bolt12) {
+ const [base, query = ''] = uri.split('?');
+ const kept = query.split('&').filter((kv) => kv && kv.toLowerCase() !== `lno=${bolt12.toLowerCase()}`);
+ return kept.length ? `${base}?${kept.join('&')}` : base;
+}
log(`names registrar for ${DOMAIN} on :${CFG.port || 8798} — ${Object.keys(state.names).length} name(s), offers ${ln ? 'on' : 'off'}, forwarder ${fwd ? 'on' : 'off'}`);