diff options
Diffstat (limited to 'src/ark')
| -rw-r--r-- | src/ark/manager.js | 43 |
1 files changed, 39 insertions, 4 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js index 0370a77..8aa8707 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -310,6 +310,9 @@ export class ArkManager { seen.add(m.vtxoId); return true; }); + // (the handshake goes out beside the info call, not after it: a wake of + // the phone's worker pays for every round trip in the zap it answers) + const hsP = handshake(this.arkUrl).catch(() => null); try { this.info = await getArkInfo(this.arkUrl); this.offline = false; @@ -323,7 +326,7 @@ export class ArkManager { this.info = this.infoHint; this.offline = true; } - const hs = await handshake(this.arkUrl).catch(() => null); + const hs = await hsP; this.psa = hs?.psa; this.serverPub = hex.decode(this.info.serverPubkey); @@ -387,6 +390,20 @@ export class ArkManager { return Date.now() - age * 600_000; } + // The tip for a payment about to be built: one asked for moments ago (a + // worker warms it while the manager starts) is as good as asking again. + warmTip() { + const p = this.chain.tipHeight().then((h) => { this._tipH = h; this._tipAt = Date.now(); this._tipNow = Date.now(); return h; }); + this._tipWarm = p; + p.catch(() => {}).finally(() => { if (this._tipWarm === p) this._tipWarm = null; }); + return p; + } + async _tipFresh() { + if (this._tipNow && Date.now() - this._tipNow < 5000 && this._tipH) return this._tipH; + if (this._tipWarm) { try { return await this._tipWarm; } catch { /* ask again */ } } + return this.chain.tipHeight(); + } + async _tipMemo() { if (this._tipAt && Date.now() - this._tipAt < 60_000) return this._tipH; try { @@ -1421,10 +1438,17 @@ export class ArkManager { async awaitLnPay(id, timeoutMs = 90_000) { const a = this.lnAction(id); if (!a || a.step !== 'initiated') return this.driveLn(id); - await Promise.race([ + const res = await Promise.race([ checkLightningPayment(this.arkUrl, hex.decode(a.paymentHash), true).catch(() => null), new Promise((r) => setTimeout(r, timeoutMs)), ]); + // the held call's own answer settles it: asking once more for the same + // preimage was a round trip between the payment and its reply + if (res && res.status === 'success' && res.preimage && a.step === 'initiated' + && hex.encode(sha256(hex.decode(res.preimage))) === a.paymentHash) { + this._settleLnPay(a, res.preimage); + return a; + } return this.driveLn(id); } @@ -1556,8 +1580,14 @@ export class ArkManager { // as paid, hours before the money moved) throw new Error(prior.step === 'done' ? 'this invoice was already paid' : 'a payment for this invoice is already in progress'); } - const tip = await this.chain.tipHeight(); - const routing = routingFeeSat != null ? routingFeeSat : await this.lnRouteFee(invoice, amountSat); + const [tip, routing] = await Promise.all([ + this._tipFresh(), + routingFeeSat != null ? routingFeeSat : this.lnRouteFee(invoice, amountSat), + ]); + // the two awaits above let a second call for the same invoice in + const twin = this.state.actions.find((a) => + a.type === 'ln-pay' && a.paymentHash === dec.paymentHash && a.step !== 'failed'); + if (twin) throw new Error(twin.step === 'done' ? 'this invoice was already paid' : 'a payment for this invoice is already in progress'); // smallest single vtxo that covers amount + routing + its expiry-dependent // 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. @@ -1721,6 +1751,11 @@ export class ArkManager { }); action.step = 'initiated'; this._save(); + // Just handed to the server: nothing to learn from asking this + // instant (it answered "pending", and the chain tip was fetched to + // go with it — two round trips before the caller's held call could + // even start). The caller waits on it (awaitLnPay) or polls. + return; } 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 |
