diff options
| -rw-r--r-- | names/server.js | 65 |
1 files changed, 49 insertions, 16 deletions
diff --git a/names/server.js b/names/server.js index 58e5228..1048e4d 100644 --- a/names/server.js +++ b/names/server.js @@ -29,7 +29,7 @@ import { lnBackend } from '../bridge/ln.js'; import { ArkManager } from '../coinosv3/src/ark/manager.js'; import { fetchPreview } from './preview.js'; import { translate, translateDeepl, languages } from './translate.js'; -import { decodeAddress } from '../coinosv3/src/ark/proto.js'; +import { decodeAddress, arkIdFromServerPubkey } from '../coinosv3/src/ark/proto.js'; import { decodeNoffer } from '../coinosv3/src/noffer.js'; import { decodeBolt11 } from '../coinosv3/src/ark/lightning.js'; import { decodeOffer } from '../coinosv3/src/bolt12.js'; @@ -379,9 +379,19 @@ async function forward(name, sat, hash, pubkey) { const clink = clinkDest(rec); // stays queued (and loud): a returned "success" here once dropped a payment if (!dest && !clink) throw new Error(`nowhere to send ${sat} sat for ${name}`); + // Where the name's wallet lives decides the routes. The float can only + // arkoor-send on its own server; a wallet on another one (Second, say) is + // reachable by its own Lightning invoice alone — the float step used to + // throw first and the Lightning step never ran (manna, rogerv3: 100 tries + // each since 10-18). A wallet on another NETWORK (a staging name's tark + // address, paid real sats through its mainnet offer) can't be reached at all. + let destArk = null, ownServer = false; + try { destArk = dest && decodeAddress(dest); } catch {} + const wrongNetwork = !!destArk && (dest.startsWith('tark') !== ((CFG.forwarder?.network || 'mainnet') !== 'mainnet')); + try { ownServer = !!destArk && !!fwd?.serverPub && destArk.arkId === Buffer.from(arkIdFromServerPubkey(fwd.serverPub)).toString('hex'); } catch {} const viaFloat = async () => { - if (!dest || !fwd || fwd.balance().spendableSat < sat) return false; + if (!ownServer || fwd.balance().spendableSat < sat) return false; try { await fwd.send(dest, sat); } catch (e) { // enough sats, too many coins: merge them, and the queue's next try goes @@ -425,9 +435,29 @@ async function forward(name, sat, hash, pubkey) { for (const step of rec?.noffer ? [viaClink, viaFloat] : [viaFloat, viaClink]) { if (await step()) return; } + if (wrongNetwork) { + throw Object.assign(new Error(`${name}'s wallet is on another network (${dest.slice(0, 5)}…) — no route for mainnet sats`), { permanent: true }); + } + if (destArk && !ownServer) { + throw Object.assign(new Error(`${name}'s wallet is on another ark server and isn't listening`), { foreign: true }); + } throw new Error(fwd && dest ? 'insufficient float and the wallet is not listening' : 'no delivery route'); } +// A queued forward that can never go (the wallet is on another network) is +// set aside with its reason instead of retried forever: the sats stay on the +// node, and state.parked is the ledger to refund or write them off from. +function queueFailedForward(p, e) { + if (e.permanent) { + (state.parked ||= []).push({ ...p, reason: e.message, parkedAt: Date.now() }); + log(`forward parked for ${p.name} (${p.sat} sat): ${e.message}`); + return false; + } + p.lastError = e.message; + p.foreign = !!e.foreign; + return true; +} + // --------------------------------------------------------------------------- // Point of sale — a coinos POS terminal (coinos-pos, an ESP32 keypad) works // against a name with a bearer token its owner mints from Settings. The @@ -615,9 +645,11 @@ async function settleLoop() { try { await forward(name, sat, inv.payment_hash, state.names[name]?.pubkey); } catch (e) { - log(`forward failed for ${name} (${sat} sat): ${e.message} — queued`); - state.pending = state.pending || []; - state.pending.push({ name, sat, hash: inv.payment_hash, pubkey: state.names[name]?.pubkey, ts: Date.now(), lastError: e.message }); + const p = { name, sat, hash: inv.payment_hash, pubkey: state.names[name]?.pubkey, ts: Date.now() }; + if (queueFailedForward(p, e)) { + log(`forward failed for ${name} (${sat} sat): ${e.message} — queued`); + (state.pending ||= []).push(p); + } persist(); } } @@ -787,14 +819,18 @@ async function consolidateFloat() { // forever. One tick at a time: a top-up or a circular delivery can outlast // the interval, and an overlapping tick would forward the same queued // payment twice. -const foreignServer = (p) => /different ark server/.test(p.lastError || ''); +// What /health reports about the queue. Payments for wallets on another ark +// server only go when that wallet answers, so they wait without paging; +// parked ones (another network) need a person, not a top-up. function pendingSummary() { - const q = state.pending || []; - const live = q.filter((p) => !foreignServer(p)), foreign = q.filter(foreignServer); + const q = state.pending || [], parked = state.parked || []; + const ours = q.filter((p) => !p.foreign), foreign = q.filter((p) => p.foreign); + const sum = (a) => a.reduce((n, p) => n + p.sat, 0); return { - pendingSat: live.reduce((n, p) => n + p.sat, 0), - oldestPendingAt: live.reduce((m, p) => (p.ts && (!m || p.ts < m) ? p.ts : m), null), - undeliverable: { count: foreign.length, sat: foreign.reduce((n, p) => n + p.sat, 0) }, + pendingSat: sum(ours), + oldestPendingAt: ours.reduce((m, p) => (p.ts && (!m || p.ts < m) ? p.ts : m), null), + waitingOnWallet: { count: foreign.length, sat: sum(foreign) }, + parked: { count: parked.length, sat: sum(parked) }, }; } @@ -822,9 +858,8 @@ async function retryTick() { catch (e) { log(`forward retry failed for ${p.name} (${p.sat} sat, try ${(p.tries || 0) + 1}): ${e.message}`); p.tries = (p.tries || 0) + 1; - p.lastError = e.message; p.next = Date.now() + Math.min(3_600_000, 60_000 * 2 ** Math.min(p.tries - 1, 6)); - still.push(p); + if (queueFailedForward(p, e)) still.push(p); } } state.pending = still; @@ -1189,9 +1224,7 @@ Bun.serve({ floatAddress: fwd ? fwd.address() : null, pendingForwards: (state.pending || []).length, // for the desk monitor (scripts/float-monitor.ts): stuck money and a - // float that can't refill are what page us. A payment for a wallet - // that moved to another ark server can't go by float at all, so it's - // counted apart — it needs a person, not a top-up. + // float that can't refill are what page us ...pendingSummary(), floatTopup: { ...topupStatus, nextChunkSat: topupChunk }, }); |
