aboutsummaryrefslogtreecommitdiffstats
path: root/names
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-03 09:25:47 -0700
committerAdam Soltys <[email protected]>2026-08-03 09:25:47 -0700
commit6cce1f7fdbf487222e919bc2f50870d539f19b8c (patch)
tree24b1d80d2ebd1c61216b46dfc8f90b11724d47a4 /names
parentc7ba5d5f0c8e80da1133fc6f53cc37bfedc281f2 (diff)
downloadcoinosv3-6cce1f7fdbf487222e919bc2f50870d539f19b8c.tar.gz
coinosv3-6cce1f7fdbf487222e919bc2f50870d539f19b8c.zip
BOLT 12 offers with a custom memo on the Spending receive page
Mining pools (Ocean) and payroll pay to a reusable offer rather than a lightning address, and the memo is how the payer labels it. A disclosure under the payment address mints one: the registrar's new NIP-98'd /offer endpoint asks the ASP's CLN for an `amount: any` offer carrying the memo, keyed to the caller's name (issuer = their address, so two users' identical memos can't collapse onto one CLN offer). Up to ten each, identical memos reuse the existing offer, and GET lists them. Settled offer payments already had a home: the registrar's settle loop now resolves a custom offer id to its owner and hands it to the same forwarder the default offer uses — an instant, free arkoor send into the user's Spending balance. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'names')
-rw-r--r--names/server.js41
1 files changed, 41 insertions, 0 deletions
diff --git a/names/server.js b/names/server.js
index 99552dc..74bcb9b 100644
--- a/names/server.js
+++ b/names/server.js
@@ -295,6 +295,8 @@ async function settleLoop() {
persist();
const offerId = inv.local_offer_id;
let name = offerId && Object.keys(state.names).find((n) => state.names[n].offerId === offerId);
+ // custom offers (a memo'd bolt12 the user minted for e.g. a mining pool)
+ if (!name && offerId && state.offers && state.offers[offerId]) name = state.offers[offerId].key;
// LNURL invoices we minted: recorded against their payment hash
const pending = state.invoices && state.invoices[inv.payment_hash];
if (!name && pending) name = pending.key;
@@ -440,6 +442,45 @@ Bun.serve({
return json({ results });
}
+ // A user's own BOLT 12 offers with a custom memo — what a mining pool or
+ // payroll wants to send to. Settled payments ride the same forwarder as
+ // the name's default offer, so they land in the user's Spending balance.
+ if (url.pathname === '/offer' && (req.method === 'POST' || req.method === 'GET')) {
+ if (!rateOk(ip)) return json({ error: 'rate limited' }, 429);
+ const bodyText = req.method === 'POST' ? await req.text() : '';
+ const a = await checkNip98(req, url, bodyText);
+ if (a.error) return json({ error: a.error }, 401);
+ const key = Object.keys(state.names)
+ .find((k) => state.names[k].pubkey === a.pubkey || state.names[k].manager === a.pubkey);
+ if (!key) return json({ error: 'claim a name first' }, 400);
+ state.offers = state.offers || {};
+ if (req.method === 'GET') {
+ return json({ offers: Object.entries(state.offers)
+ .filter(([, o]) => o.key === key)
+ .map(([offerId, o]) => ({ offerId, memo: o.memo, bolt12: o.bolt12, created: o.created })) });
+ }
+ if (!ln) return json({ error: 'offers unavailable' }, 503);
+ let body;
+ try { body = JSON.parse(bodyText); } catch { return json({ error: 'bad body' }, 400); }
+ const memo = String(body.memo || '').slice(0, 120).trim();
+ const mine = Object.entries(state.offers).filter(([, o]) => o.key === key);
+ 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);
+ let o;
+ try {
+ // `issuer` keeps two users' identical memos from collapsing onto one
+ // CLN offer (it dedupes by the offer's contents).
+ o = await ln.call('offer', { amount: 'any', description: memo || key, issuer: key });
+ } catch (e) {
+ return json({ error: e.message }, 500);
+ }
+ state.offers[o.offer_id] = { key, memo, bolt12: o.bolt12, created: Date.now() };
+ persist();
+ log(`offer for ${key}: ${memo ? JSON.stringify(memo) : '(no memo)'}`);
+ return json({ ok: true, offerId: o.offer_id, memo, bolt12: o.bolt12 });
+ }
+
// Availability / lookup. Public data (it's DNS).
const m = url.pathname.match(/^\/name\/([a-z0-9._-]{1,30})$/);
if (m && req.method === 'GET') {