aboutsummaryrefslogtreecommitdiffstats
path: root/src/ark
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-25 07:46:55 -0700
committerAdam Soltys <[email protected]>2026-10-25 07:46:55 -0700
commit43a35d0fe6abc56b6594015ae1028842c04a2c19 (patch)
treebe56c2bb8f71826df0360b0842ee1bd7fa4b3a3b /src/ark
parent9270bc52a33ef007d57d15b5e6aee95248c368a2 (diff)
downloadcoinosv3-43a35d0fe6abc56b6594015ae1028842c04a2c19.tar.gz
coinosv3-43a35d0fe6abc56b6594015ae1028842c04a2c19.zip
A closed wallet's NWC payments fit iOS's ten-second wake: a paid send is never marked for revocation by a second wake, lookups report outgoing sends and finish them, and pushes no longer run side by side
PullCap's night of 2026-10-24: every wake's diary stopped 8-12 s after its push (iOS stops the worker there, not at 30 s). Two wakes two seconds apart both drove one send; the second's InitiateLightningPayment was refused ("state: ln-spent") and the paid send went to 'revoking', where every later wake spent its whole budget on it, so no request was answered after midnight. - manager: an initiate refused as ln-spent follows the payment ('initiated'); a send in 'revoking' asks the ASP first (paid: settle; in flight before expiry: back to waiting; not found: revoke) - worker: 9 s budget on Apple endpoints (27 s elsewhere) for answers, earlier sends and renewals; every ASP step held to it - lookup_invoice asks every copy of the connection and drives an outgoing send in flight, answering settled with the preimage - a lease in the worker's IndexedDB store serialises pushes (the Web Lock didn't); diary notes a missing Web Locks API - the reply and the mirror save run together; stuck sends are finished before a new payment only when they hold its coins Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
Diffstat (limited to 'src/ark')
-rw-r--r--src/ark/manager.js26
1 files changed, 24 insertions, 2 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js
index a1c1390..810808c 100644
--- a/src/ark/manager.js
+++ b/src/ark/manager.js
@@ -1682,9 +1682,16 @@ export class ArkManager {
action.step = 'initiated';
this._save();
} catch (e) {
+ // the HTLC already went to this payment: another context (a second
+ // wake of the phone's worker, seconds apart, over its own copy of the
+ // state) initiated it first. That payment is this one — follow it.
+ // (PullCap 2026-10-24 23:21: a paid send was marked 'revoking' here.)
+ if (e instanceof GrpcError && /state: ln-spent/i.test(e.message)) {
+ action.step = 'initiated';
+ this._save();
// INVALID_ARGUMENT: the server itself deems the invoice unpayable —
// retrying can never succeed, revoke the HTLC instead
- if (e instanceof GrpcError && e.grpcStatus === 3) {
+ } else if (e instanceof GrpcError && e.grpcStatus === 3) {
action.error = e.message;
action.step = 'revoking';
this._save();
@@ -1711,7 +1718,22 @@ export class ArkManager {
this._save();
}
}
- if (action.step === 'revoking') await this._revokeLnPay(action);
+ if (action.step === 'revoking') {
+ // Ask first: a send can land in 'revoking' over a stale copy of the
+ // state while the payment went through (or is still going) — a
+ // settled payment is settled here too, and one in flight waits.
+ const res = await checkLightningPayment(this.arkUrl, hex.decode(action.paymentHash), false).catch(() => null);
+ if (res && res.status === 'success' && res.preimage
+ && hex.encode(sha256(hex.decode(res.preimage))) === action.paymentHash) {
+ this._settleLnPay(action, res.preimage);
+ return;
+ }
+ if (res && res.status === 'pending' && action.htlcExpiry) {
+ const tip = await this.chain.tipHeight().catch(() => 0);
+ if (tip && tip <= action.htlcExpiry) { action.step = 'initiated'; delete action.error; this._save(); return; }
+ }
+ await this._revokeLnPay(action);
+ }
}
_settleLnPay(action, preimageHex) {