aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--names/server.js65
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 },
});