aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
Diffstat (limited to 'src/features')
-rw-r--r--src/features/ark.js61
1 files changed, 47 insertions, 14 deletions
diff --git a/src/features/ark.js b/src/features/ark.js
index 30ca90c..4693336 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -570,17 +570,26 @@ export function arkFeature(ctx) {
// payment picks the stale coin and the ASP rejects it ("state: spent").
await mgr.reconcile().catch(() => {});
- const id = await mgr.payLnInvoice(invoice);
- for (let i = 0; i < 60; i++) {
+ // Up to three attempts: each failure refunds in full, and the next try
+ // brings one extra sat of routing budget for liquidity that shifted
+ // between the quote and the send.
+ let last = null;
+ for (let attempt = 0; attempt < 3; attempt++) {
+ const routingFeeSat = attempt === 0 ? undefined : (last?.routingFeeSat ?? 0) + 1;
+ const id = await mgr.payLnInvoice(invoice, { routingFeeSat });
+ for (let i = 0; i < 60; i++) {
+ const a = mgr.lnAction(id);
+ if (!a || ['done', 'failed'].includes(a.step)) break;
+ await new Promise((r) => setTimeout(r, 2000));
+ await mgr.driveLn(id).catch(() => {});
+ }
const a = mgr.lnAction(id);
- if (!a || a.step === 'done') break;
- if (a.step === 'failed') throw new Error(a.error || 'payment failed');
- await new Promise((r) => setTimeout(r, 2000));
- await mgr.driveLn(id).catch(() => {});
+ if (a && a.step === 'done') return { preimage: a.preimage, feeSat: a.feeSat, amountSat: a.amountSat };
+ last = a;
+ if (a && a.step === 'failed' && !lnRetryable(a)) break;
+ if (!a || a.step !== 'failed') break; // still pending after 2 min — don't double-pay
}
- const a = mgr.lnAction(id);
- if (!a || a.step !== 'done') throw new Error(a?.error || a?.lastError || 'payment did not complete');
- return { preimage: a.preimage, feeSat: a.feeSat, amountSat: a.amountSat };
+ throw new Error(last?.error || last?.lastError || 'payment did not complete');
}
// A bolt11 lands in Send (via the swaps feature's delegation hook, or
@@ -614,11 +623,15 @@ export function arkFeature(ctx) {
if (!p || p.invoice !== invoice) return;
if (!p.amountSat) { p.status = 'ready'; render(); return; }
const tip = await mgr.chain.tipHeight();
+ // what the lightning network itself will charge (0 for coinos-to-coinos)
+ const routing = await mgr.lnRouteFee(invoice, p.amountSat);
+ if (!ui.arkLnPay || ui.arkLnPay !== p || p.invoice !== invoice) return;
+ p.routingFeeSat = routing;
const candidates = mgr.vtxos().filter((v) => v.state === 'spendable')
.sort((a, b) => a.amountSat - b.amountSat);
let fee = null;
for (const v of candidates) {
- const f = lnSendFee(p.amountSat, mgr.info.lnSendFees, [v], tip);
+ const f = lnSendFee(p.amountSat, mgr.info.lnSendFees, [v], tip) + routing;
if (v.amountSat >= p.amountSat + f) { fee = f; break; }
}
if (fee == null) {
@@ -650,27 +663,47 @@ export function arkFeature(ctx) {
ui.busy = true; ui.sendError = ''; render();
try {
const mgr = await connectArk();
- const id = await mgr.payLnInvoice(p.invoice, { amountSat: sats });
+ // Routing conditions can shift between quote and send: a failed
+ // (and fully refunded) attempt re-tries with one extra sat of routing
+ // budget before giving up — askrene fees are exact, liquidity isn't.
+ let tries = 0;
+ const attempt = async (routingFeeSat) => {
+ const id = await mgr.payLnInvoice(p.invoice, { amountSat: sats, routingFeeSat });
+ const a = mgr.lnAction(id);
+ if (['done', 'failed'].includes(a.step)) settle(a);
+ else { p.actionId = id; p.status = 'paying'; pollArkLn(id, settle); }
+ };
const settle = (a) => {
if (a.step === 'done') {
ui.arkLnPaid = { amountSat: a.amountSat, meta: p.meta };
if (p.meta && p.meta.pk) noteZap('inv:' + p.invoice, p.meta.pk);
ui.arkLnPay = null;
} else if (ui.arkLnPay) {
+ if (tries < 2 && lnRetryable(a)) {
+ tries += 1;
+ attempt((a.routingFeeSat ?? 0) + 1).catch((e) => {
+ ui.arkLnPay.status = 'ready'; ui.sendError = e.message; render();
+ });
+ return;
+ }
ui.arkLnPay.status = 'ready';
ui.sendError = a.error === 'payment failed' ? t('arkLnRefunded') : (a.error || t('arkLnPayFailed'));
}
render();
};
- const a = mgr.lnAction(id);
- if (['done', 'failed'].includes(a.step)) settle(a);
- else { p.actionId = id; p.status = 'paying'; pollArkLn(id, settle); }
+ await attempt(p.routingFeeSat);
} catch (e) {
ui.sendError = e.message;
}
ui.busy = false; render();
}
+ // A failed lightning pay is worth re-trying with a bumped routing budget
+ // unless the error says the attempt itself was ill-formed.
+ function lnRetryable(a) {
+ return !a?.error || !/already|invalid|expired|unpayable|insufficient|consolidate|no amount|exceeds/i.test(a.error);
+ }
+
function arkLnPayView() {
const u = ' ' + unitLabel();
if (ui.arkLnPaid) {