aboutsummaryrefslogtreecommitdiffstats
path: root/src/ark
diff options
context:
space:
mode:
Diffstat (limited to 'src/ark')
-rw-r--r--src/ark/lightning.js27
-rw-r--r--src/ark/manager.js8
2 files changed, 33 insertions, 2 deletions
diff --git a/src/ark/lightning.js b/src/ark/lightning.js
index 568d0ab..0d23baa 100644
--- a/src/ark/lightning.js
+++ b/src/ark/lightning.js
@@ -111,6 +111,33 @@ export function lnSendFee(amountSat, fees, inputs, tip) {
return Math.max(fees.minFeeSat || 0, (fees.baseFeeSat || 0) + ppmFee);
}
+// Why the spendable coins can't cover a payment, so the wallet can say so
+// instead of "Payment failed". Coins a renewal took are 'pending' until its
+// round's transaction confirms on-chain — ten minutes, or an hour — which is
+// nothing like the seconds an in-flight payment or a revocation holds a coin
+// for. (A user renewed their whole balance from the coins page, tried to pay
+// three minutes later, and was told only that the payment failed.)
+// 'settling' — money held by another payment frees itself shortly: wait
+// 'renewing' — the balance would cover it, but a renewal holds it
+// 'short' — there isn't enough, whatever is pending
+export function lnShortfall(vtxos, actions, amountSat) {
+ const live = (actions || []).filter((a) => !['done', 'failed'].includes(a.step));
+ const idsOf = (a) => [...(a.inputIds || []), ...(a.vtxoId ? [a.vtxoId] : [])];
+ const renewIds = new Set(live.filter((a) => a.type === 'refresh').flatMap(idsOf));
+ // coins on their way OUT (to Savings, or a unilateral exit) are pending
+ // too, and are never coming back to pay with
+ const leavingIds = new Set(live.filter((a) => a.type === 'offboard' || a.type === 'exit').flatMap(idsOf));
+ const sum = (list) => list.reduce((n, v) => n + v.amountSat, 0);
+ const pending = (vtxos || []).filter((v) => v.state === 'pending' && !leavingIds.has(v.id));
+ const spendableSat = sum((vtxos || []).filter((v) => v.state === 'spendable'));
+ const renewingSat = sum(pending.filter((v) => renewIds.has(v.id)));
+ const settlingSat = sum(pending.filter((v) => !renewIds.has(v.id)));
+ const kind = settlingSat > 0 && spendableSat + settlingSat >= amountSat ? 'settling'
+ : renewingSat > 0 && spendableSat + settlingSat + renewingSat >= amountSat ? 'renewing'
+ : 'short';
+ return { kind, spendableSat, renewingSat, settlingSat };
+}
+
export const lnReceiveFee = (amountSat, fees) =>
(fees.baseFeeSat || 0) + Math.ceil((amountSat * (fees.ppm || 0)) / 1_000_000);
diff --git a/src/ark/manager.js b/src/ark/manager.js
index dc80ca3..a114702 100644
--- a/src/ark/manager.js
+++ b/src/ark/manager.js
@@ -33,6 +33,7 @@ import {
requestLightningPayHtlcRevocation, startLightningReceive, checkLightningReceive,
prepareLightningReceiveClaim, claimLightningReceive, cancelLightningReceive,
lightningReceiveAttestation,
+ lnShortfall,
} from './lightning.js';
import {
boardFee, p2trAddress, buildBoard, requestBoardCosign,
@@ -868,8 +869,11 @@ export class ArkManager {
const expiredSat = this.state.vtxos
.filter((v) => v.state === 'spendable' && this._expired(v, tip))
.reduce((n, v) => n + v.amountSat, 0);
- return expiredSat
- ? `insufficient ark balance (${expiredSat} sat expired and unusable)`
+ if (expiredSat) return `insufficient ark balance (${expiredSat} sat expired and unusable)`;
+ // 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
+ ? `insufficient ark balance (${renewing} sat are being renewed and can't be spent until the renewal confirms on-chain)`
: 'insufficient ark balance';
}