diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/ark/manager.js | 13 | ||||
| -rw-r--r-- | src/nwc-respond.js | 21 |
2 files changed, 27 insertions, 7 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js index aea6020..853ca84 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -1182,7 +1182,10 @@ export class ArkManager { .reduce((n, v) => n + v.amountSat, 0); if (expiredSat) return `insufficient ark balance (${expiredSat} sat expired and unusable)`; const cappedSat = this.state.vtxos.filter((v) => this._free(v) && this._capped(v)).reduce((n, v) => n + v.amountSat, 0); - if (cappedSat) return `insufficient ark balance (${cappedSat} sat have a history too long to spend and are waiting to be renewed)`; + // under dust a round won't take it either: only new money pays from here + if (cappedSat) return cappedSat < 330 + ? `insufficient ark balance (${cappedSat} sat have a history too long to spend and are too few to renew; receive more to keep paying)` + : `insufficient ark balance (${cappedSat} sat have a history too long to spend and are waiting to be renewed)`; // coins a renewal took stay unspendable until its round confirms on-chain const renewing = lnShortfall(this.state.vtxos, this.state.actions, Infinity).renewingSat; return renewing @@ -1524,8 +1527,9 @@ export class ArkManager { // fee — else gather largest-first: each input funds its own HTLC vtxo with // the same payment hash, and the server sums them into one payment. const busy = this._remoteBusy(); + // a coin at the depth cap can't be an input: the ASP refuses the cosign const spendable = this.state.vtxos - .filter((v) => this._free(v, busy) && !this._expired(v, tip)); + .filter((v) => this._free(v, busy) && !this._expired(v, tip) && !this._capped(v)); let inputs = null, feeSat = 0; for (const v of this._spendOrder(spendable, tip, (a, b) => a.amountSat - b.amountSat)) { const fee = lnSendFee(amountSat, this.info.lnSendFees, [v], tip) + routing; @@ -2711,8 +2715,11 @@ export class ArkManager { if (!tip) return 0; const covered = new Set((this.state.scheduled || []).flatMap((s) => s.inputIds)); const cap = this.info.maxVtxoAmountSat || Infinity; + // a round output under dust (330 sat) is refused, so a coin that small + // can't be renewed on its own — asking only spends the wake's budget const coins = deep - .filter((v) => !covered.has(v.id) && v.expiryHeight > tip + 2 && v.amountSat <= cap) + .filter((v) => !covered.has(v.id) && v.expiryHeight > tip + 2 && v.amountSat <= cap + && v.amountSat - this.refreshFee([v], tip + 2) >= 330) .sort((a, b) => this.exitDepth(b) - this.exitDepth(a)) .slice(0, maxCoins); let n = 0; diff --git a/src/nwc-respond.js b/src/nwc-respond.js index 6ea23ab..04340d0 100644 --- a/src/nwc-respond.js +++ b/src/nwc-respond.js @@ -30,6 +30,7 @@ const MAX_AGE_SEC = 120; // subscription; with an error it can tell the payer their wallet is waiting // for them). The notifier holds the reply this long before sending it. const DECLINE_AFTER_MS = 20_000; +const FAIL_AFTER_MS = 5_000; // the notifier's shortest hold const WAIT_MSG = 'Coinos is waiting for its owner to open the app and approve. Look this request up again later.'; const PAY_WAIT_MS = 20_000; // inside the SW's ~30s budget, with reply margin const nowSec = () => Math.floor(Date.now() / 1000); @@ -474,13 +475,25 @@ export async function respondFromBg(data, { // the manager's start (the ASP's info, the chain tip) runs while the // notifier is asked whether anyone else answered ready().catch(() => {}); - // A local failure carries no urgency — wait out the grace period and - // re-check before publishing, so a sibling device's slower SUCCESS reply - // wins the client's eyes instead of being shouted over. + // A local failure carries no urgency — a sibling device's slower SUCCESS + // reply must win the client's eyes instead of being shouted over. The + // error is left with the notifier, which sends it only if nobody answers + // in FAIL_AFTER_MS: waiting here instead ran past iOS's ten seconds after + // a failed send, and the other app heard nothing at all (PullCap, + // 2026-10-29). Waiting here is the fallback when the notifier won't take it. const failQuietly = async (message) => { + const code = /insufficient/i.test(message || '') ? 'INSUFFICIENT_BALANCE' : 'PAYMENT_FAILED'; + try { + const evt = await buildReply(conn, ev, scheme, errRes('pay_invoice', code, message)); + const r = await fetchFn(`${notifier}/decline`, { + method: 'POST', headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ event: evt, afterMs: FAIL_AFTER_MS }), + }); + if (r.ok) { log(`payment failed (${message}) — ${code} goes out unless someone answers first`); return true; } + } catch {} await new Promise((r) => setTimeout(r, errGraceMs)); if (!(await answered())) { - await publish(errRes('pay_invoice', 'INTERNAL', message)).catch(() => {}); + await publish(errRes('pay_invoice', code, message)).catch(() => {}); } return true; }; |
