aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/names.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-29 00:31:12 -0700
committerAdam Soltys <[email protected]>2026-08-29 00:31:12 -0700
commit8ea0416c08535750839088c060d3252415510cc5 (patch)
tree2908f20062ba516464ddfc5489a65082ac7a6b6a /src/features/names.js
parentf06644e729ab5be2fcfa45e32f8b1de52ea36eb3 (diff)
downloadcoinosv3-8ea0416c08535750839088c060d3252415510cc5.tar.gz
coinosv3-8ea0416c08535750839088c060d3252415510cc5.zip
An address can point home: a name's owner may register their own CLINK offer code and the registrar asks THEIR node for invoices — delivered there first, float as fallback, and no invoice is relayed or paid unless it demands exactly the sats owed on the right network. hal learns to pay pasted noffer1 codes through the zap flow (fixed, variable and payer-chosen pricing, amount verified before the HTLC locks). The depth advisory slims to one line — an uncooperative exit's price and a Manage link — and the new coins screen shows each vtxo's amount, expiry and arkoor depth beside what renewal costs and buys
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019WboeoetMF3MTmgT7Ew8yi
Diffstat (limited to 'src/features/names.js')
-rw-r--r--src/features/names.js66
1 files changed, 60 insertions, 6 deletions
diff --git a/src/features/names.js b/src/features/names.js
index 0291b40..2c69a84 100644
--- a/src/features/names.js
+++ b/src/features/names.js
@@ -12,6 +12,7 @@
// record falls back to the Lightning-address flow (zaps feature).
import { resolveBip353, parsePaymentName, parseBip21 } from '../bip353.js';
+import { decodeNoffer } from '../noffer.js';
import { nip98Header } from '../nip98.js';
import { qrSvg } from '../qr.js';
import { isArkAddress } from './ark.js';
@@ -88,9 +89,12 @@ export function namesFeature(ctx) {
return addr ? `bitcoin:?ark=${encodeURIComponent(addr)}` : null;
}
- async function claim(name, { signer, manager, quietProfile } = {}) {
+ async function claim(name, { signer, manager, quietProfile, noffer } = {}) {
const uri = await currentUri();
if (!uri) throw new Error(t('namesNeedArk'));
+ // Delivery noffer: undefined keeps whatever the record carries (here or
+ // registrar-side), '' clears it, a string points delivery at that code.
+ const nof = noffer !== undefined ? noffer : (load().noffer || undefined);
const j = await post('/register', 'POST', {
name, uri, domain: DOMAIN(),
// lets the address take Lightning payments from LNURL-only wallets:
@@ -99,9 +103,10 @@ export function namesFeature(ctx) {
// when the login identity claims its own npub name, it nominates the
// wallet key to keep the record updated afterwards
...(manager ? { manager } : {}),
+ ...(nof !== undefined ? { noffer: nof } : {}),
}, signer);
const prev = load().name;
- save({ name, domain: DOMAIN(), uri, offerPk: hook('nwcOfferPubkey') || null, updated: Date.now() });
+ save({ ...load(), name, domain: DOMAIN(), uri, offerPk: hook('nwcOfferPubkey') || null, noffer: nof || null, updated: Date.now() });
// Releasing the previous name is for renames the user meant. A CUSTOM
// name must never be released because a placeholder claim raced in: that
// exact race once deleted a user's claimed name — adoptIdentity's npub
@@ -137,7 +142,8 @@ export function namesFeature(ctx) {
if (!pk) return;
const r = await withTimeout(
fetch(`${REGISTRAR}/pubkey/${pk}?domain=${encodeURIComponent(DOMAIN())}`).then((x) => x.json()), 12000, 'registrar');
- if (r && r.name) save({ ...load(), name: r.name, domain: r.domain || DOMAIN(), uri: r.uri });
+ if (r && r.name) save({ ...load(), name: r.name, domain: r.domain || DOMAIN(), uri: r.uri,
+ ...(r.noffer !== undefined ? { noffer: r.noffer } : {}) });
} catch (e) { console.warn('names: lookup failed —', e.message); }
}
@@ -177,7 +183,8 @@ export function namesFeature(ctx) {
fetch(`${REGISTRAR}/pubkey/${login.pubkey}?domain=${encodeURIComponent(DOMAIN())}`).then((x) => x.json()),
8000, 'registrar');
if (r && r.name && !/^npub1/.test(r.name)) {
- save({ ...st, name: r.name, domain: r.domain || DOMAIN(), uri: r.uri });
+ save({ ...st, name: r.name, domain: r.domain || DOMAIN(), uri: r.uri,
+ ...(r.noffer !== undefined ? { noffer: r.noffer } : {}) });
st = load();
}
} catch {}
@@ -582,6 +589,52 @@ export function namesFeature(ctx) {
copyBtn(st.address, t('copy')));
}
+ // Point the address's Lightning delivery at the user's own node: any
+ // CLINK-capable service beside their node (Lightning.Pub etc.) hands out
+ // its own noffer; registered here, the registrar asks THAT service for
+ // invoices, so payments land on their node and never touch this wallet.
+ async function setDeliveryNoffer(raw) {
+ const st = load();
+ if (!st.name) { ui.namesNofferError = t('namesNeedArk'); render(); return; }
+ ui.namesNofferBusy = true; ui.namesNofferError = null; render();
+ try {
+ if (raw) {
+ const d = decodeNoffer(raw);
+ if (!d.relay) throw new Error(t('namesOwnNodeNoRelay'));
+ }
+ await claim(st.name, { noffer: raw || '' });
+ ui.namesNofferInput = '';
+ toast(raw ? t('namesOwnNodeSet') : t('namesOwnNodeCleared'));
+ } catch (e) { ui.namesNofferError = e.message; }
+ ui.namesNofferBusy = false; render();
+ }
+
+ function ownNodeSection() {
+ const st = load();
+ if (!st.name) return null;
+ return h('div', { class: 'col', style: 'gap:8px;width:100%;margin-top:6px' },
+ h('strong', { class: 'small' }, t('namesOwnNode')),
+ st.noffer
+ ? h('div', { class: 'col', style: 'gap:8px' },
+ h('div', { class: 'small muted' }, t('namesOwnNodeActive')),
+ h('div', { class: 'addr-box break', style: 'font-size:10px' }, st.noffer),
+ h('button', { class: 'btn-ghost btn-sm', disabled: !!ui.namesNofferBusy,
+ onClick: () => setDeliveryNoffer('') },
+ ui.namesNofferBusy ? h('span', { class: 'spinner sm' }) : t('namesOwnNodeClear')))
+ : h('div', { class: 'col', style: 'gap:8px' },
+ h('div', { class: 'small muted' }, t('namesOwnNodeHow')),
+ h('div', { class: 'row gap6' },
+ h('input', {
+ type: 'text', class: 'grow mono-input', placeholder: 'noffer1…',
+ value: ui.namesNofferInput || '',
+ onInput: (e) => { ui.namesNofferInput = e.target.value; },
+ }),
+ h('button', { class: 'btn-sm', disabled: !!ui.namesNofferBusy,
+ onClick: () => setDeliveryNoffer((ui.namesNofferInput || '').trim()) },
+ ui.namesNofferBusy ? h('span', { class: 'spinner sm' }) : t('save')))),
+ ui.namesNofferError ? h('div', { class: 'notice err small' }, ui.namesNofferError) : null);
+ }
+
function clinkPane() {
const code = hook('nwcOfferString');
if (!code) return h('div', { class: 'notice err small', style: 'width:100%' }, t('namesNeedArk'));
@@ -589,7 +642,8 @@ export function namesFeature(ctx) {
h('div', { html: qrSvg(code) }),
h('div', { class: 'addr-box break', style: 'width:100%;font-size:10px' }, code),
copyBtn(code, t('copy')),
- h('p', { class: 'small muted', style: 'margin:0' }, t('namesZapCodeHow')));
+ h('p', { class: 'small muted', style: 'margin:0' }, t('namesZapCodeHow')),
+ ownNodeSection());
}
// ---- more options -------------------------------------------------------
@@ -677,7 +731,7 @@ export function namesFeature(ctx) {
namesSeed(rec) {
if (!rec || !rec.name) return null;
const st = load();
- if (!st.name) save({ name: rec.name, domain: rec.domain || DOMAIN(), uri: rec.uri });
+ if (!st.name) save({ name: rec.name, domain: rec.domain || DOMAIN(), uri: rec.uri, noffer: rec.noffer ?? null });
return true;
},
// 'pending' while the restore/claim pass is still running — the wallet