diff options
| author | Adam Soltys <[email protected]> | 2026-08-02 09:04:52 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-02 09:04:52 -0700 |
| commit | 468932b1c3a70bc384bf9d123bf681cb2e7ce0d2 (patch) | |
| tree | 3db064a19770d78228bbb1cdc0b5079a278fb776 | |
| parent | a60f2a090b12f62649ddd55d1a8605e433d2c75a (diff) | |
| download | coinosv3-468932b1c3a70bc384bf9d123bf681cb2e7ce0d2.tar.gz coinosv3-468932b1c3a70bc384bf9d123bf681cb2e7ce0d2.zip | |
Deliver incoming Lightning without depending on an operator float
A payment to an address settled on our node and then sat in a retry
queue because the float wallet that forwards it was empty — the money
was safe but invisible to the recipient. Delivery now has a second
route: ask the recipient's wallet for an invoice (CLINK) and pay it
over Lightning, which needs no float at all. The float path stays first
because it works while they're offline; the queue is now the last
resort rather than the only one.
Existing addresses never carried the wallet's offer key, so that route
was unavailable to them. The wallet now records what it published and
re-registers whenever the registrar is missing the current key.
Co-Authored-By: Claude Fable 5 <[email protected]>
| -rw-r--r-- | names/server.js | 28 | ||||
| -rw-r--r-- | src/features/names.js | 9 |
2 files changed, 30 insertions, 7 deletions
diff --git a/names/server.js b/names/server.js index 0005782..bd62f81 100644 --- a/names/server.js +++ b/names/server.js @@ -189,14 +189,32 @@ const arkParamOf = (uri) => { return m ? decodeURIComponent(m[1]) : null; }; -// Forward one settled offer payment; queue it on failure (empty float, etc). +// Deliver a settled payment to the name's owner. Two ways, and we try the +// one that works while they're asleep first: +// 1. an arkoor send from the float — instant, free, needs no cooperation +// 2. ask their wallet for an invoice and pay it over Lightning — needs no +// float at all, but only works while something of theirs is listening +// Failing both, it queues and retries; nothing is ever dropped silently. async function forward(name, sat) { const rec = state.names[name]; const dest = rec && arkParamOf(rec.uri); - if (!dest) { log(`forward: no ark destination for ${name}, dropping ${sat} sat`); return; } - if (!fwd) throw new Error('no forwarder wallet'); - await fwd.send(dest, sat); - log(`forwarded ${sat} sat to ${name}`); + if (!dest && !rec?.offerPk) { log(`forward: nowhere to send ${sat} sat for ${name}`); return; } + + if (dest && fwd && fwd.balance().spendableSat >= sat) { + await fwd.send(dest, sat); + log(`forwarded ${sat} sat to ${name}`); + return; + } + + if (rec?.offerPk && ln) { + const pr = await requestInvoiceFromWallet(rec.offerPk, { amountSat: sat, description: `${name} payment` }); + if (pr) { + await ln.pay(pr, { maxfeeSat: Math.max(2, Math.ceil(sat * 0.005)) }); + log(`delivered ${sat} sat to ${name} over lightning (no float needed)`); + return; + } + } + throw new Error(fwd && dest ? 'insufficient float and the wallet is not listening' : 'no delivery route'); } // NIP-57: a zap we invoiced is only "a zap" once we publish the receipt, diff --git a/src/features/names.js b/src/features/names.js index 4c0ffaf..54f15f7 100644 --- a/src/features/names.js +++ b/src/features/names.js @@ -74,7 +74,7 @@ export function namesFeature(ctx) { ...(manager ? { manager } : {}), }, signer); const prev = load().name; - save({ name, domain: DOMAIN, uri, updated: Date.now() }); + save({ name, domain: DOMAIN, uri, offerPk: hook('nwcOfferPubkey') || null, updated: Date.now() }); if (prev && prev !== name) { post('/register', 'DELETE', { name: prev, domain: DOMAIN }).catch(() => {}); } @@ -147,7 +147,12 @@ export function namesFeature(ctx) { render(); if (!st.name) return; const uri = await currentUri(); - if (uri && uri !== st.uri) await claim(st.name); + // Re-publish when the address changed OR when the registrar doesn't yet + // know this wallet's offer key — that key is what lets an incoming + // Lightning payment be delivered straight to us instead of waiting on + // the operator's float. + const offerPk = hook('nwcOfferPubkey') || null; + if (uri && (uri !== st.uri || (offerPk && st.offerPk !== offerPk))) await claim(st.name); } catch (e) { console.warn('names: refresh failed —', e.message); lastError = e.message; |
