aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-03 16:00:39 -0700
committerAdam Soltys <[email protected]>2026-10-03 16:00:39 -0700
commit328f2db9af46cda473318bb3eada0f6e83bb42f1 (patch)
treeb4e850022ba260eff90940ebc465eebded023b63
parent141779ab97efad6abefaec0545bba5330fd1e12d (diff)
downloadcoinosv3-328f2db9af46cda473318bb3eada0f6e83bb42f1.tar.gz
coinosv3-328f2db9af46cda473318bb3eada0f6e83bb42f1.zip
A Lightning payment that can't be covered says why, not 'Payment failed'
A user renewed all ten of their coins (55,611 sats) from the coins page, tried to pay a 55,300-sat invoice three minutes later, and was told only 'Payment failed'. Nothing had failed: the renewal's round ran at once, but its coins stay unspendable until the round's transaction confirms on-chain, and that block took 64 minutes. The quote found no spendable coin and gave up with the generic message; nothing reached the server. - The quote now tells apart (lnShortfall): money held by another payment (wait, as before — now by sum, not one coin covering it all), money a renewal holds ('Nothing was sent. N sats are being renewed…'), and a real shortfall ('needs X sats with its fee, Y available'). Coins leaving in an offboard or exit are never waited for. - The manager's 'insufficient ark balance' names a renewal too. - The coins page says before the tap that renewed coins can't be spent until the round confirms, and that renewing every coin leaves nothing to pay with meanwhile. tools/ark/ln-shortfall-test.js Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
-rw-r--r--src/ark/lightning.js27
-rw-r--r--src/ark/manager.js8
-rw-r--r--src/features/ark.js22
-rw-r--r--src/i18n.js3
-rw-r--r--src/locales/ar.json5
-rw-r--r--src/locales/bn.json5
-rw-r--r--src/locales/de.json5
-rw-r--r--src/locales/es.json5
-rw-r--r--src/locales/fr.json5
-rw-r--r--src/locales/hi.json5
-rw-r--r--src/locales/id.json5
-rw-r--r--src/locales/ja.json5
-rw-r--r--src/locales/ko.json5
-rw-r--r--src/locales/mr.json5
-rw-r--r--src/locales/pt.json5
-rw-r--r--src/locales/ru.json5
-rw-r--r--src/locales/sw.json5
-rw-r--r--src/locales/ta.json5
-rw-r--r--src/locales/te.json5
-rw-r--r--src/locales/tr.json5
-rw-r--r--src/locales/ur.json5
-rw-r--r--src/locales/vi.json5
-rw-r--r--src/locales/zh.json5
-rw-r--r--tools/ark/ln-shortfall-test.js52
24 files changed, 180 insertions, 27 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';
}
diff --git a/src/features/ark.js b/src/features/ark.js
index fb6d2a3..523a3a2 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -9,7 +9,7 @@ import { sha256 } from '@noble/hashes/sha256';
import { ArkManager } from '../ark/manager.js';
import { loadBg, saveBg, buildBg, disarmSiblingRecords } from '../nwc-bg.js';
import { boardFee, p2trAddress } from '../ark/board.js';
-import { maybeBolt11, maybeLnInvoice, lnSendFee } from '../ark/lightning.js';
+import { maybeBolt11, maybeLnInvoice, lnSendFee, lnShortfall } from '../ark/lightning.js';
import { decodeVtxo, getVtxoStatus, VTXO_STATE_SPENT, concatBytes, vtxoBytesFromStr, vtxoBytesToHex, decodeAddress, arkIdFromServerPubkey, getArkInfo } from '../ark/proto.js';
import { arkStore } from '../ark/store.js';
import { signedExitTxs, exitTxVsizes, buildBumpChild, buildExitClaim, submitPackage } from '../ark/exit.js';
@@ -1062,8 +1062,8 @@ export function arkFeature(ctx) {
}
}
if (fee == null) {
- const pendingCover = mgr.vtxos().some((v) => v.state === 'pending' && v.amountSat >= p.amountSat);
- if (pendingCover) {
+ const why = lnShortfall(mgr.vtxos(), mgr.state.actions, p.amountSat);
+ if (why.kind === 'settling') {
p.status = 'fundsPending';
render();
mgr.resumePending().catch(() => {}); // nudge whatever holds them
@@ -1072,9 +1072,16 @@ export function arkFeature(ctx) {
}, 4000);
return;
}
- // can't cover once fees are in
+ // Say why. A renewal holds its coins until the round confirms on-chain
+ // (not seconds: no spinner for that); otherwise there simply isn't
+ // enough once the fee is in — and that is not "Payment failed".
ui.arkLnPay = null;
- ui.sendError = t('arkLnPayFailed');
+ ui.sendError = why.kind === 'renewing'
+ ? t('arkLnRenewing', { sats: fmtSats(why.renewingSat) })
+ : t('arkLnShort', {
+ need: fmtSats(p.amountSat + lnSendFee(p.amountSat, mgr.info.lnSendFees, spendables, tip) + routing),
+ have: fmtSats(why.spendableSat),
+ });
render();
return;
}
@@ -2412,7 +2419,10 @@ export function arkFeature(ctx) {
!selCoins.length ? t('arkCoinsRenewNone')
: selCoins.length < spend.length
? t('arkCoinsRenewSome', { n: selCoins.length, fee: selFee > 0 ? fmtSats(selFee) + ' sats' : t('arkDepthFree') })
- : selFee > 0 ? t('arkCoinsRenewNowFee', { fee: fmtSats(selFee) + ' sats' }) : t('arkDepthRenewBtn'))),
+ : selFee > 0 ? t('arkCoinsRenewNowFee', { fee: fmtSats(selFee) + ' sats' }) : t('arkDepthRenewBtn')),
+ // said before the tap: a renewal takes its coins out of reach until
+ // the round's transaction confirms, and by default it takes them all
+ !spend.length ? null : h('div', { class: 'notice info small ark-renew-lock' }, t('arkCoinsRenewLock'))),
h('div', { class: 'card col', style: 'gap:8px' },
h('h4', { style: 'margin:0' }, t('arkCoinsCoopTitle')),
h('p', { class: 'small muted', style: 'margin:0' }, t('arkCoopDesc')),
diff --git a/src/i18n.js b/src/i18n.js
index e4d2429..4bdf2e6 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -1066,6 +1066,9 @@ const STR = {
arkLnPaying: 'Paying invoice…',
arkLnRetrying: 'Route failed. Retrying with a bigger fee budget ({n}/{of})…',
arkLnPayFailed: 'Payment failed',
+ arkLnRenewing: "Nothing was sent. {sats} sats are being renewed and can’t be spent until the renewal confirms on the Bitcoin network: usually about ten minutes, sometimes an hour or more. Try again then.",
+ arkLnShort: "Not enough in Spending: this payment needs {need} sats with its fee, and {have} are available.",
+ arkCoinsRenewLock: "Coins being renewed can’t be spent until the round’s transaction confirms on the Bitcoin network: usually about ten minutes, sometimes an hour or more. Renewing every coin leaves nothing to pay with in the meantime.",
arkLnRefunded: 'Payment failed. Your money is back in Spending.',
arkLnPaidTitle: 'Paid!',
or: 'or',
diff --git a/src/locales/ar.json b/src/locales/ar.json
index e1e3d67..2bce98d 100644
--- a/src/locales/ar.json
+++ b/src/locales/ar.json
@@ -912,5 +912,8 @@
"videoTapToPlay": "انقر للتشغيل",
"videoLargeTitle": "الفيديوهات",
"videoLargeToggle": "تشغيل الفيديوهات الكبيرة تلقائيًا",
- "videoLargeHelp": "الفيديوهات التي تزيد على {mb} ميغابايت أو تتجاوز 1080p تنتظر نقرة، حتى لا تستهلك الخلاصة بياناتك. الحد أقل على الاتصال البطيء أو عند توفير البيانات."
+ "videoLargeHelp": "الفيديوهات التي تزيد على {mb} ميغابايت أو تتجاوز 1080p تنتظر نقرة، حتى لا تستهلك الخلاصة بياناتك. الحد أقل على الاتصال البطيء أو عند توفير البيانات.",
+ "arkLnRenewing": "لم يُرسل شيء. {sats} ساتس قيد التجديد ولا يمكن إنفاقها حتى يتأكد التجديد على شبكة بيتكوين: عادة نحو عشر دقائق، وأحيانًا ساعة أو أكثر. حاول مجددًا بعد ذلك.",
+ "arkLnShort": "الرصيد غير كافٍ: هذه الدفعة تحتاج {need} ساتس مع رسومها، والمتاح {have}.",
+ "arkCoinsRenewLock": "العملات قيد التجديد لا يمكن إنفاقها حتى تتأكد معاملة الجولة على شبكة بيتكوين: عادة نحو عشر دقائق، وأحيانًا ساعة أو أكثر. تجديد كل العملات لا يترك شيئًا للدفع في تلك الأثناء."
}
diff --git a/src/locales/bn.json b/src/locales/bn.json
index 9687a7e..27ab990 100644
--- a/src/locales/bn.json
+++ b/src/locales/bn.json
@@ -912,5 +912,8 @@
"videoTapToPlay": "চালাতে ট্যাপ করুন",
"videoLargeTitle": "ভিডিও",
"videoLargeToggle": "বড় ভিডিও নিজে থেকে চালান",
- "videoLargeHelp": "{mb} MB-এর বেশি বা 1080p-এর ওপরের ভিডিও ট্যাপের অপেক্ষায় থাকে, যাতে ফিড আপনার ডেটা না খায়। ধীর বা ডেটা-সেভার সংযোগে সীমা কম।"
+ "videoLargeHelp": "{mb} MB-এর বেশি বা 1080p-এর ওপরের ভিডিও ট্যাপের অপেক্ষায় থাকে, যাতে ফিড আপনার ডেটা না খায়। ধীর বা ডেটা-সেভার সংযোগে সীমা কম।",
+ "arkLnRenewing": "কিছুই পাঠানো হয়নি। {sats} sats নবায়ন হচ্ছে এবং নবায়ন Bitcoin নেটওয়ার্কে নিশ্চিত না হওয়া পর্যন্ত খরচ করা যাবে না: সাধারণত প্রায় দশ মিনিট, কখনও এক ঘণ্টা বা তার বেশি। তখন আবার চেষ্টা করুন।",
+ "arkLnShort": "যথেষ্ট নেই: এই পেমেন্টে ফি-সহ {need} sats লাগে, আর {have} পাওয়া যাচ্ছে।",
+ "arkCoinsRenewLock": "নবায়নরত কয়েন রাউন্ডের লেনদেন Bitcoin নেটওয়ার্কে নিশ্চিত না হওয়া পর্যন্ত খরচ করা যায় না: সাধারণত প্রায় দশ মিনিট, কখনও এক ঘণ্টা বা তার বেশি। সব কয়েন নবায়ন করলে এর মধ্যে পেমেন্ট করার কিছু থাকে না।"
}
diff --git a/src/locales/de.json b/src/locales/de.json
index b1e5127..da21d08 100644
--- a/src/locales/de.json
+++ b/src/locales/de.json
@@ -912,5 +912,8 @@
"videoTapToPlay": "Zum Abspielen tippen",
"videoLargeTitle": "Videos",
"videoLargeToggle": "Große Videos automatisch abspielen",
- "videoLargeHelp": "Videos über {mb} MB oder über 1080p warten auf einen Tipp, damit der Feed nicht dein Datenvolumen frisst. Bei langsamer Verbindung oder Datensparmodus ist die Grenze niedriger."
+ "videoLargeHelp": "Videos über {mb} MB oder über 1080p warten auf einen Tipp, damit der Feed nicht dein Datenvolumen frisst. Bei langsamer Verbindung oder Datensparmodus ist die Grenze niedriger.",
+ "arkLnRenewing": "Es wurde nichts gesendet. {sats} Sats werden gerade erneuert und können erst ausgegeben werden, wenn die Erneuerung im Bitcoin-Netzwerk bestätigt ist: meist etwa zehn Minuten, manchmal eine Stunde oder länger. Versuche es dann erneut.",
+ "arkLnShort": "Nicht genug im Ausgabenkonto: Diese Zahlung braucht mit Gebühr {need} Sats, verfügbar sind {have}.",
+ "arkCoinsRenewLock": "Coins in Erneuerung können erst ausgegeben werden, wenn die Transaktion der Runde im Bitcoin-Netzwerk bestätigt ist: meist etwa zehn Minuten, manchmal eine Stunde oder länger. Wer alle Coins erneuert, kann in der Zwischenzeit nichts bezahlen."
}
diff --git a/src/locales/es.json b/src/locales/es.json
index 569e9fa..1b6ed7b 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -912,5 +912,8 @@
"videoTapToPlay": "Toca para reproducir",
"videoLargeTitle": "Vídeos",
"videoLargeToggle": "Reproducir vídeos grandes automáticamente",
- "videoLargeHelp": "Los vídeos de más de {mb} MB o por encima de 1080p esperan a que los toques, para que el feed no se coma tus datos. El límite es menor con una conexión lenta o en ahorro de datos."
+ "videoLargeHelp": "Los vídeos de más de {mb} MB o por encima de 1080p esperan a que los toques, para que el feed no se coma tus datos. El límite es menor con una conexión lenta o en ahorro de datos.",
+ "arkLnRenewing": "No se envió nada. {sats} sats se están renovando y no pueden gastarse hasta que la renovación se confirme en la red Bitcoin: normalmente unos diez minutos, a veces una hora o más. Inténtalo entonces.",
+ "arkLnShort": "No hay suficiente en Gastos: este pago necesita {need} sats con su comisión, y hay {have} disponibles.",
+ "arkCoinsRenewLock": "Las monedas en renovación no pueden gastarse hasta que la transacción de la ronda se confirme en la red Bitcoin: normalmente unos diez minutos, a veces una hora o más. Renovar todas las monedas no deja nada con que pagar mientras tanto."
}
diff --git a/src/locales/fr.json b/src/locales/fr.json
index c345556..ea83805 100644
--- a/src/locales/fr.json
+++ b/src/locales/fr.json
@@ -912,5 +912,8 @@
"videoTapToPlay": "Touchez pour lire",
"videoLargeTitle": "Vidéos",
"videoLargeToggle": "Lire automatiquement les grosses vidéos",
- "videoLargeHelp": "Les vidéos de plus de {mb} Mo ou au-delà de 1080p attendent un toucher, pour que le fil ne dévore pas vos données. La limite est plus basse sur une connexion lente ou en économie de données."
+ "videoLargeHelp": "Les vidéos de plus de {mb} Mo ou au-delà de 1080p attendent un toucher, pour que le fil ne dévore pas vos données. La limite est plus basse sur une connexion lente ou en économie de données.",
+ "arkLnRenewing": "Rien n’a été envoyé. {sats} sats sont en cours de renouvellement et ne peuvent pas être dépensés avant que le renouvellement soit confirmé sur le réseau Bitcoin : en général une dizaine de minutes, parfois une heure ou plus. Réessayez ensuite.",
+ "arkLnShort": "Solde insuffisant : ce paiement demande {need} sats avec ses frais, et {have} sont disponibles.",
+ "arkCoinsRenewLock": "Les pièces en cours de renouvellement ne peuvent pas être dépensées avant que la transaction du tour soit confirmée sur le réseau Bitcoin : en général une dizaine de minutes, parfois une heure ou plus. Tout renouveler ne laisse rien pour payer entre-temps."
}
diff --git a/src/locales/hi.json b/src/locales/hi.json
index cbee487..7be94f6 100644
--- a/src/locales/hi.json
+++ b/src/locales/hi.json
@@ -912,5 +912,8 @@
"videoTapToPlay": "चलाने के लिए टैप करें",
"videoLargeTitle": "वीडियो",
"videoLargeToggle": "बड़े वीडियो अपने आप चलाएँ",
- "videoLargeHelp": "{mb} MB से बड़े या 1080p से ऊपर के वीडियो टैप का इंतज़ार करते हैं, ताकि फ़ीड आपका डेटा न खाए। धीमे या डेटा-सेवर कनेक्शन पर सीमा कम होती है।"
+ "videoLargeHelp": "{mb} MB से बड़े या 1080p से ऊपर के वीडियो टैप का इंतज़ार करते हैं, ताकि फ़ीड आपका डेटा न खाए। धीमे या डेटा-सेवर कनेक्शन पर सीमा कम होती है।",
+ "arkLnRenewing": "कुछ भी नहीं भेजा गया। {sats} sats का नवीनीकरण हो रहा है और जब तक वह Bitcoin नेटवर्क पर पुष्ट नहीं होता, उन्हें खर्च नहीं किया जा सकता: आमतौर पर लगभग दस मिनट, कभी-कभी एक घंटा या ज़्यादा। तब फिर कोशिश करें।",
+ "arkLnShort": "पर्याप्त राशि नहीं: इस भुगतान को शुल्क सहित {need} sats चाहिए, और {have} उपलब्ध हैं।",
+ "arkCoinsRenewLock": "जिन सिक्कों का नवीनीकरण हो रहा है, उन्हें तब तक खर्च नहीं किया जा सकता जब तक राउंड का लेनदेन Bitcoin नेटवर्क पर पुष्ट न हो: आमतौर पर लगभग दस मिनट, कभी-कभी एक घंटा या ज़्यादा। सभी सिक्कों का नवीनीकरण करने पर इस बीच भुगतान के लिए कुछ नहीं बचता।"
}
diff --git a/src/locales/id.json b/src/locales/id.json
index 45be39b..01bc000 100644
--- a/src/locales/id.json
+++ b/src/locales/id.json
@@ -248,5 +248,8 @@
"videoTapToPlay": "Ketuk untuk memutar",
"videoLargeTitle": "Video",
"videoLargeToggle": "Putar otomatis video besar",
- "videoLargeHelp": "Video di atas {mb} MB atau lebih dari 1080p menunggu ketukan, agar feed tidak menghabiskan kuota. Batasnya lebih rendah di koneksi lambat atau mode hemat data."
+ "videoLargeHelp": "Video di atas {mb} MB atau lebih dari 1080p menunggu ketukan, agar feed tidak menghabiskan kuota. Batasnya lebih rendah di koneksi lambat atau mode hemat data.",
+ "arkLnRenewing": "Tidak ada yang terkirim. {sats} sats sedang diperbarui dan tidak bisa dibelanjakan sampai pembaruan terkonfirmasi di jaringan Bitcoin: biasanya sekitar sepuluh menit, kadang satu jam atau lebih. Coba lagi nanti.",
+ "arkLnShort": "Saldo tidak cukup: pembayaran ini butuh {need} sats termasuk biayanya, dan yang tersedia {have}.",
+ "arkCoinsRenewLock": "Koin yang sedang diperbarui tidak bisa dibelanjakan sampai transaksi ronde terkonfirmasi di jaringan Bitcoin: biasanya sekitar sepuluh menit, kadang satu jam atau lebih. Memperbarui semua koin berarti tidak ada yang bisa dipakai membayar untuk sementara."
}
diff --git a/src/locales/ja.json b/src/locales/ja.json
index 56e05f4..3805d70 100644
--- a/src/locales/ja.json
+++ b/src/locales/ja.json
@@ -912,5 +912,8 @@
"videoTapToPlay": "タップして再生",
"videoLargeTitle": "動画",
"videoLargeToggle": "大きな動画を自動再生",
- "videoLargeHelp": "{mb} MBを超える動画や1080pを超える動画は、データを使いすぎないようタップするまで再生しません。低速回線やデータセーバー時は上限が下がります。"
+ "videoLargeHelp": "{mb} MBを超える動画や1080pを超える動画は、データを使いすぎないようタップするまで再生しません。低速回線やデータセーバー時は上限が下がります。",
+ "arkLnRenewing": "何も送金されていません。{sats} satsは更新中で、更新がビットコインネットワークで承認されるまで使えません(通常は約10分、1時間以上かかることもあります)。その後にもう一度お試しください。",
+ "arkLnShort": "残高が足りません。この支払いには手数料込みで{need} sats必要ですが、使えるのは{have} satsです。",
+ "arkCoinsRenewLock": "更新中のコインは、ラウンドのトランザクションがビットコインネットワークで承認されるまで使えません(通常は約10分、1時間以上かかることもあります)。すべてのコインを更新すると、その間は支払いに使えるものがなくなります。"
}
diff --git a/src/locales/ko.json b/src/locales/ko.json
index 79f3cdf..a6b23aa 100644
--- a/src/locales/ko.json
+++ b/src/locales/ko.json
@@ -911,5 +911,8 @@
"videoTapToPlay": "탭하여 재생",
"videoLargeTitle": "동영상",
"videoLargeToggle": "큰 동영상 자동 재생",
- "videoLargeHelp": "{mb}MB를 넘거나 1080p보다 높은 동영상은 데이터를 아끼기 위해 탭할 때까지 기다립니다. 느린 연결이나 데이터 절약 모드에서는 한도가 더 낮습니다."
+ "videoLargeHelp": "{mb}MB를 넘거나 1080p보다 높은 동영상은 데이터를 아끼기 위해 탭할 때까지 기다립니다. 느린 연결이나 데이터 절약 모드에서는 한도가 더 낮습니다.",
+ "arkLnRenewing": "아무것도 보내지지 않았습니다. {sats} sats가 갱신 중이며, 갱신이 비트코인 네트워크에서 확인될 때까지 쓸 수 없습니다. 보통 10분 정도, 때로는 1시간 이상 걸립니다. 그 후에 다시 시도하세요.",
+ "arkLnShort": "잔액이 부족합니다. 이 결제에는 수수료 포함 {need} sats가 필요하고, 사용할 수 있는 것은 {have} sats입니다.",
+ "arkCoinsRenewLock": "갱신 중인 코인은 라운드 거래가 비트코인 네트워크에서 확인될 때까지 쓸 수 없습니다. 보통 10분 정도, 때로는 1시간 이상 걸립니다. 모든 코인을 갱신하면 그동안 결제할 수 있는 돈이 없습니다."
}
diff --git a/src/locales/mr.json b/src/locales/mr.json
index f35613b..88479c1 100644
--- a/src/locales/mr.json
+++ b/src/locales/mr.json
@@ -248,5 +248,8 @@
"videoTapToPlay": "चालवण्यासाठी टॅप करा",
"videoLargeTitle": "व्हिडिओ",
"videoLargeToggle": "मोठे व्हिडिओ आपोआप चालवा",
- "videoLargeHelp": "{mb} MB पेक्षा मोठे किंवा 1080p पेक्षा वरचे व्हिडिओ तुमचा डेटा वाचवण्यासाठी टॅपची वाट पाहतात. धीम्या किंवा डेटा-सेव्हर कनेक्शनवर मर्यादा कमी असते."
+ "videoLargeHelp": "{mb} MB पेक्षा मोठे किंवा 1080p पेक्षा वरचे व्हिडिओ तुमचा डेटा वाचवण्यासाठी टॅपची वाट पाहतात. धीम्या किंवा डेटा-सेव्हर कनेक्शनवर मर्यादा कमी असते.",
+ "arkLnRenewing": "काहीही पाठवले गेले नाही. {sats} sats चे नूतनीकरण सुरू आहे आणि ते Bitcoin नेटवर्कवर निश्चित होईपर्यंत खर्च करता येणार नाहीत: साधारणपणे सुमारे दहा मिनिटे, कधी कधी तासभर किंवा जास्त. नंतर पुन्हा प्रयत्न करा.",
+ "arkLnShort": "पुरेशी शिल्लक नाही: या पेमेंटला शुल्कासह {need} sats लागतात, आणि {have} उपलब्ध आहेत.",
+ "arkCoinsRenewLock": "नूतनीकरण होत असलेली नाणी, राउंडचा व्यवहार Bitcoin नेटवर्कवर निश्चित होईपर्यंत खर्च करता येत नाहीत: साधारणपणे सुमारे दहा मिनिटे, कधी कधी तासभर किंवा जास्त. सर्व नाण्यांचे नूतनीकरण केल्यास तोपर्यंत पैसे देण्यासाठी काहीच उरत नाही."
}
diff --git a/src/locales/pt.json b/src/locales/pt.json
index 665cf73..fdf925e 100644
--- a/src/locales/pt.json
+++ b/src/locales/pt.json
@@ -912,5 +912,8 @@
"videoTapToPlay": "Toque para reproduzir",
"videoLargeTitle": "Vídeos",
"videoLargeToggle": "Reproduzir vídeos grandes automaticamente",
- "videoLargeHelp": "Vídeos acima de {mb} MB ou de 1080p esperam um toque, para o feed não consumir seus dados. O limite é menor em conexões lentas ou com economia de dados."
+ "videoLargeHelp": "Vídeos acima de {mb} MB ou de 1080p esperam um toque, para o feed não consumir seus dados. O limite é menor em conexões lentas ou com economia de dados.",
+ "arkLnRenewing": "Nada foi enviado. {sats} sats estão sendo renovados e não podem ser gastos até a renovação ser confirmada na rede Bitcoin: normalmente uns dez minutos, às vezes uma hora ou mais. Tente de novo depois.",
+ "arkLnShort": "Saldo insuficiente em Gastos: este pagamento precisa de {need} sats com a taxa, e há {have} disponíveis.",
+ "arkCoinsRenewLock": "Moedas em renovação não podem ser gastas até a transação da rodada ser confirmada na rede Bitcoin: normalmente uns dez minutos, às vezes uma hora ou mais. Renovar todas as moedas não deixa nada para pagar nesse meio-tempo."
}
diff --git a/src/locales/ru.json b/src/locales/ru.json
index f7ee649..918eb63 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -912,5 +912,8 @@
"videoTapToPlay": "Нажмите, чтобы воспроизвести",
"videoLargeTitle": "Видео",
"videoLargeToggle": "Автовоспроизведение больших видео",
- "videoLargeHelp": "Видео больше {mb} МБ или выше 1080p ждут нажатия, чтобы лента не съедала трафик. На медленном соединении или в режиме экономии лимит ниже."
+ "videoLargeHelp": "Видео больше {mb} МБ или выше 1080p ждут нажатия, чтобы лента не съедала трафик. На медленном соединении или в режиме экономии лимит ниже.",
+ "arkLnRenewing": "Ничего не отправлено. {sats} сат обновляются, и их нельзя потратить, пока обновление не подтвердится в сети Bitcoin: обычно около десяти минут, иногда час и больше. Попробуйте позже.",
+ "arkLnShort": "Недостаточно средств: для этого платежа с комиссией нужно {need} сат, доступно {have}.",
+ "arkCoinsRenewLock": "Монеты в процессе обновления нельзя потратить, пока транзакция раунда не подтвердится в сети Bitcoin: обычно около десяти минут, иногда час и больше. Если обновить все монеты, платить в это время будет нечем."
}
diff --git a/src/locales/sw.json b/src/locales/sw.json
index 179531a..93076bc 100644
--- a/src/locales/sw.json
+++ b/src/locales/sw.json
@@ -248,5 +248,8 @@
"videoTapToPlay": "Gusa ili kucheza",
"videoLargeTitle": "Video",
"videoLargeToggle": "Cheza video kubwa zenyewe",
- "videoLargeHelp": "Video zaidi ya MB {mb} au juu ya 1080p husubiri uguse, ili mlisho usimalize data yako. Kikomo ni cha chini kwenye muunganisho wa polepole au wa kuokoa data."
+ "videoLargeHelp": "Video zaidi ya MB {mb} au juu ya 1080p husubiri uguse, ili mlisho usimalize data yako. Kikomo ni cha chini kwenye muunganisho wa polepole au wa kuokoa data.",
+ "arkLnRenewing": "Hakuna kilichotumwa. Sats {sats} zinahuishwa na haziwezi kutumika hadi uhuishaji uthibitishwe kwenye mtandao wa Bitcoin: kwa kawaida kama dakika kumi, wakati mwingine saa moja au zaidi. Jaribu tena baadaye.",
+ "arkLnShort": "Salio halitoshi: malipo haya yanahitaji sats {need} pamoja na ada, na zilizopo ni {have}.",
+ "arkCoinsRenewLock": "Sarafu zinazohuishwa haziwezi kutumika hadi muamala wa raundi uthibitishwe kwenye mtandao wa Bitcoin: kwa kawaida kama dakika kumi, wakati mwingine saa moja au zaidi. Kuhuisha sarafu zote hakuachi chochote cha kulipia kwa muda huo."
}
diff --git a/src/locales/ta.json b/src/locales/ta.json
index 068bcc8..c361637 100644
--- a/src/locales/ta.json
+++ b/src/locales/ta.json
@@ -248,5 +248,8 @@
"videoTapToPlay": "இயக்கத் தட்டவும்",
"videoLargeTitle": "வீடியோக்கள்",
"videoLargeToggle": "பெரிய வீடியோக்களைத் தானாக இயக்கு",
- "videoLargeHelp": "{mb} MB-க்கு மேல் அல்லது 1080p-க்கு மேலான வீடியோக்கள் உங்கள் டேட்டாவைச் சேமிக்க, தட்டும் வரை காத்திருக்கும். மெதுவான அல்லது டேட்டா சேமிப்பு இணைப்பில் வரம்பு குறைவு."
+ "videoLargeHelp": "{mb} MB-க்கு மேல் அல்லது 1080p-க்கு மேலான வீடியோக்கள் உங்கள் டேட்டாவைச் சேமிக்க, தட்டும் வரை காத்திருக்கும். மெதுவான அல்லது டேட்டா சேமிப்பு இணைப்பில் வரம்பு குறைவு.",
+ "arkLnRenewing": "எதுவும் அனுப்பப்படவில்லை. {sats} sats புதுப்பிக்கப்படுகின்றன; புதுப்பிப்பு Bitcoin வலையமைப்பில் உறுதியாகும் வரை அவற்றைச் செலவிட முடியாது: பொதுவாக சுமார் பத்து நிமிடங்கள், சில நேரங்களில் ஒரு மணி நேரம் அல்லது அதற்கு மேல். பிறகு மீண்டும் முயலவும்.",
+ "arkLnShort": "போதிய இருப்பு இல்லை: இந்தக் கட்டணத்திற்கு கட்டணத்துடன் {need} sats தேவை, கிடைப்பது {have}.",
+ "arkCoinsRenewLock": "புதுப்பிக்கப்படும் நாணயங்களை, சுற்றின் பரிவர்த்தனை Bitcoin வலையமைப்பில் உறுதியாகும் வரை செலவிட முடியாது: பொதுவாக சுமார் பத்து நிமிடங்கள், சில நேரங்களில் ஒரு மணி நேரம் அல்லது அதற்கு மேல். எல்லா நாணயங்களையும் புதுப்பித்தால் அதுவரை செலுத்த எதுவும் இருக்காது."
}
diff --git a/src/locales/te.json b/src/locales/te.json
index 2c7160b..4199e2e 100644
--- a/src/locales/te.json
+++ b/src/locales/te.json
@@ -246,5 +246,8 @@
"videoTapToPlay": "ప్లే చేయడానికి నొక్కండి",
"videoLargeTitle": "వీడియోలు",
"videoLargeToggle": "పెద్ద వీడియోలను ఆటో‌ప్లే చేయి",
- "videoLargeHelp": "{mb} MB కంటే పెద్దవి లేదా 1080p పైబడిన వీడియోలు మీ డేటా ఆదా కోసం నొక్కే వరకు ఆగుతాయి. నెమ్మదైన లేదా డేటా సేవర్ కనెక్షన్‌లో పరిమితి తక్కువ."
+ "videoLargeHelp": "{mb} MB కంటే పెద్దవి లేదా 1080p పైబడిన వీడియోలు మీ డేటా ఆదా కోసం నొక్కే వరకు ఆగుతాయి. నెమ్మదైన లేదా డేటా సేవర్ కనెక్షన్‌లో పరిమితి తక్కువ.",
+ "arkLnRenewing": "ఏమీ పంపబడలేదు. {sats} sats పునరుద్ధరణలో ఉన్నాయి; అది Bitcoin నెట్‌వర్క్‌లో నిర్ధారణ అయ్యే వరకు వాటిని ఖర్చు చేయలేరు: సాధారణంగా సుమారు పది నిమిషాలు, కొన్నిసార్లు గంట లేదా అంతకంటే ఎక్కువ. తర్వాత మళ్లీ ప్రయత్నించండి.",
+ "arkLnShort": "సరిపడా నిల్వ లేదు: ఈ చెల్లింపుకు రుసుముతో కలిపి {need} sats కావాలి, అందుబాటులో ఉన్నవి {have}.",
+ "arkCoinsRenewLock": "పునరుద్ధరణలో ఉన్న నాణేలను, రౌండ్ లావాదేవీ Bitcoin నెట్‌వర్క్‌లో నిర్ధారణ అయ్యే వరకు ఖర్చు చేయలేరు: సాధారణంగా సుమారు పది నిమిషాలు, కొన్నిసార్లు గంట లేదా అంతకంటే ఎక్కువ. అన్ని నాణేలను పునరుద్ధరిస్తే ఆ సమయంలో చెల్లించడానికి ఏమీ ఉండదు."
}
diff --git a/src/locales/tr.json b/src/locales/tr.json
index b8ebb51..c96eb99 100644
--- a/src/locales/tr.json
+++ b/src/locales/tr.json
@@ -911,5 +911,8 @@
"videoTapToPlay": "Oynatmak için dokun",
"videoLargeTitle": "Videolar",
"videoLargeToggle": "Büyük videoları otomatik oynat",
- "videoLargeHelp": "{mb} MB’den büyük veya 1080p’nin üzerindeki videolar, akış verini tüketmesin diye dokunmanı bekler. Yavaş ya da veri tasarruflu bağlantıda sınır daha düşüktür."
+ "videoLargeHelp": "{mb} MB’den büyük veya 1080p’nin üzerindeki videolar, akış verini tüketmesin diye dokunmanı bekler. Yavaş ya da veri tasarruflu bağlantıda sınır daha düşüktür.",
+ "arkLnRenewing": "Hiçbir şey gönderilmedi. {sats} sats yenileniyor ve yenileme Bitcoin ağında onaylanana kadar harcanamaz: genellikle on dakika kadar, bazen bir saat veya daha fazla. Sonra tekrar dene.",
+ "arkLnShort": "Yeterli bakiye yok: bu ödeme ücretiyle birlikte {need} sats gerektiriyor, kullanılabilir olan {have}.",
+ "arkCoinsRenewLock": "Yenilenen coinler, turun işlemi Bitcoin ağında onaylanana kadar harcanamaz: genellikle on dakika kadar, bazen bir saat veya daha fazla. Tüm coinleri yenilemek, bu sürede ödeme yapacak bir şey bırakmaz."
}
diff --git a/src/locales/ur.json b/src/locales/ur.json
index 82437f2..56b5b90 100644
--- a/src/locales/ur.json
+++ b/src/locales/ur.json
@@ -248,5 +248,8 @@
"videoTapToPlay": "چلانے کے لیے ٹیپ کریں",
"videoLargeTitle": "ویڈیوز",
"videoLargeToggle": "بڑی ویڈیوز خود چلائیں",
- "videoLargeHelp": "{mb} MB سے بڑی یا 1080p سے اوپر کی ویڈیوز ٹیپ کا انتظار کرتی ہیں، تاکہ فیڈ آپ کا ڈیٹا نہ کھائے۔ سست یا ڈیٹا بچت والے کنکشن پر حد کم ہوتی ہے۔"
+ "videoLargeHelp": "{mb} MB سے بڑی یا 1080p سے اوپر کی ویڈیوز ٹیپ کا انتظار کرتی ہیں، تاکہ فیڈ آپ کا ڈیٹا نہ کھائے۔ سست یا ڈیٹا بچت والے کنکشن پر حد کم ہوتی ہے۔",
+ "arkLnRenewing": "کچھ نہیں بھیجا گیا۔ {sats} sats کی تجدید ہو رہی ہے اور جب تک تجدید Bitcoin نیٹ ورک پر تصدیق نہ ہو جائے انہیں خرچ نہیں کیا جا سکتا: عام طور پر تقریباً دس منٹ، کبھی کبھی ایک گھنٹہ یا زیادہ۔ اس کے بعد دوبارہ کوشش کریں۔",
+ "arkLnShort": "رقم ناکافی ہے: اس ادائیگی کو فیس سمیت {need} sats درکار ہیں، اور {have} دستیاب ہیں۔",
+ "arkCoinsRenewLock": "جن سکوں کی تجدید ہو رہی ہے وہ اس وقت تک خرچ نہیں ہو سکتے جب تک راؤنڈ کی ٹرانزیکشن Bitcoin نیٹ ورک پر تصدیق نہ ہو جائے: عام طور پر تقریباً دس منٹ، کبھی کبھی ایک گھنٹہ یا زیادہ۔ تمام سکوں کی تجدید کرنے سے اس دوران ادائیگی کے لیے کچھ نہیں بچتا۔"
}
diff --git a/src/locales/vi.json b/src/locales/vi.json
index 7ad49e0..f01f0a7 100644
--- a/src/locales/vi.json
+++ b/src/locales/vi.json
@@ -248,5 +248,8 @@
"videoTapToPlay": "Chạm để phát",
"videoLargeTitle": "Video",
"videoLargeToggle": "Tự động phát video lớn",
- "videoLargeHelp": "Video trên {mb} MB hoặc cao hơn 1080p sẽ chờ bạn chạm, để bảng tin không ngốn dữ liệu. Giới hạn thấp hơn khi kết nối chậm hoặc tiết kiệm dữ liệu."
+ "videoLargeHelp": "Video trên {mb} MB hoặc cao hơn 1080p sẽ chờ bạn chạm, để bảng tin không ngốn dữ liệu. Giới hạn thấp hơn khi kết nối chậm hoặc tiết kiệm dữ liệu.",
+ "arkLnRenewing": "Chưa gửi gì cả. {sats} sats đang được gia hạn và không thể chi tiêu cho đến khi việc gia hạn được xác nhận trên mạng Bitcoin: thường khoảng mười phút, đôi khi một giờ hoặc hơn. Hãy thử lại sau.",
+ "arkLnShort": "Không đủ số dư: khoản thanh toán này cần {need} sats gồm cả phí, và hiện có {have}.",
+ "arkCoinsRenewLock": "Coin đang gia hạn không thể chi tiêu cho đến khi giao dịch của vòng được xác nhận trên mạng Bitcoin: thường khoảng mười phút, đôi khi một giờ hoặc hơn. Gia hạn mọi coin nghĩa là trong lúc đó không còn gì để thanh toán."
}
diff --git a/src/locales/zh.json b/src/locales/zh.json
index b2cd00d..b64861b 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -912,5 +912,8 @@
"videoTapToPlay": "点按播放",
"videoLargeTitle": "视频",
"videoLargeToggle": "自动播放大视频",
- "videoLargeHelp": "超过 {mb} MB 或高于 1080p 的视频会等你点按再播放,以免动态耗尽流量。在慢速或省流量连接下,上限更低。"
+ "videoLargeHelp": "超过 {mb} MB 或高于 1080p 的视频会等你点按再播放,以免动态耗尽流量。在慢速或省流量连接下,上限更低。",
+ "arkLnRenewing": "没有发送任何款项。{sats} sats 正在续期,在续期于比特币网络确认之前无法使用:通常约十分钟,有时一小时或更久。请到时再试。",
+ "arkLnShort": "余额不足:这笔付款连同手续费需要 {need} sats,可用 {have} sats。",
+ "arkCoinsRenewLock": "续期中的币在该轮交易于比特币网络确认之前无法使用:通常约十分钟,有时一小时或更久。把所有币都续期,期间就没有可用于付款的余额。"
}
diff --git a/tools/ark/ln-shortfall-test.js b/tools/ark/ln-shortfall-test.js
new file mode 100644
index 0000000..f249ae1
--- /dev/null
+++ b/tools/ark/ln-shortfall-test.js
@@ -0,0 +1,52 @@
+// Why a Lightning payment can't be covered — so the wallet says "your coins
+// are being renewed" or "not enough with the fee" instead of "Payment failed".
+// The first case is the 2026-10-03 report: a user renewed all ten coins
+// (55,611 sats) from the coins page, tried to pay 55,300 three minutes later,
+// and the renewal's round took 64 minutes to confirm.
+// Run: bun tools/ark/ln-shortfall-test.js
+import { lnShortfall } from '../../src/ark/lightning.js';
+let ok = true;
+const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; };
+const coin = (id, amountSat, state) => ({ id, amountSat, state });
+
+const amounts = [3, 54, 54, 49500, 1000, 1000, 1000, 1000, 1000, 1000];
+const renewed = amounts.map((a, i) => coin('c' + i, a, 'pending'));
+const refresh = { type: 'refresh', step: 'submitted', inputIds: renewed.map((v) => v.id) };
+let r = lnShortfall(renewed, [refresh], 55300);
+check('every coin in a renewal: the payment is held by the renewal, by name', r.kind === 'renewing' && r.renewingSat === 55611 && r.spendableSat === 0, JSON.stringify(r));
+
+r = lnShortfall(renewed, [{ ...refresh, step: 'done' }], 55300);
+check('a finished renewal holds nothing', r.kind !== 'renewing' && r.renewingSat === 0, JSON.stringify(r));
+
+r = lnShortfall([coin('a', 60000, 'pending')], [{ type: 'lnpay', step: 'htlc', inputIds: ['a'] }], 55300);
+check('coins held by another payment: wait, they free themselves', r.kind === 'settling' && r.settlingSat === 60000, JSON.stringify(r));
+
+r = lnShortfall([coin('a', 30000, 'pending'), coin('b', 30000, 'spendable')], [], 55300);
+check('...counted together with what is spendable (it used to take one pending coin covering it all)', r.kind === 'settling', JSON.stringify(r));
+
+r = lnShortfall([coin('a', 55400, 'spendable')], [], 55300);
+check('enough for the amount but not its fee: short, not "failed"', r.kind === 'short' && r.spendableSat === 55400, JSON.stringify(r));
+
+r = lnShortfall([coin('a', 100, 'spendable'), coin('b', 200, 'pending')], [{ type: 'refresh', step: 'created', inputIds: ['b'] }], 55300);
+check('a renewal that wouldn\'t cover it anyway: short', r.kind === 'short' && r.renewingSat === 200, JSON.stringify(r));
+
+r = lnShortfall([coin('a', 40000, 'pending'), coin('b', 20000, 'pending')], [{ type: 'refresh', step: 'issued', inputIds: ['a'] }, { type: 'lnpay', step: 'htlc', inputIds: ['b'] }], 55300);
+check('part renewing, part settling, together enough: the renewal is what it waits on', r.kind === 'renewing', JSON.stringify(r));
+
+r = lnShortfall([coin('a', 60000, 'pending')], [{ type: 'offboard', step: 'submitted', inputIds: ['a'] }], 55300);
+check('coins leaving for Savings are not waited for', r.kind === 'short' && r.settlingSat === 0, JSON.stringify(r));
+r = lnShortfall([coin('a', 60000, 'pending')], [{ type: 'exit', step: 'chain', vtxoId: 'a' }], 55300);
+check('...nor coins in a unilateral exit', r.kind === 'short' && r.settlingSat === 0, JSON.stringify(r));
+
+// the manager's own error (ark sends, and the pay path behind the quote) names it too
+const { ArkManager } = await import('../../src/ark/manager.js');
+const mgr = new ArkManager({ storage: { save() {} }, arkUrl: 'http://test.invalid' });
+mgr.state = { nextKeyIndex: 1, movements: [], actions: [refresh], vtxos: renewed.map((v) => ({ ...v, expiryHeight: 973921 })) };
+const msg = mgr._insufficientMsg(969751);
+check('the manager\'s "insufficient" says a renewal holds the money', /55611 sat are being renewed/.test(msg), msg);
+mgr.state.actions = [];
+mgr.state.vtxos = [];
+check('...and stays plain when nothing is renewing', mgr._insufficientMsg(969751) === 'insufficient ark balance');
+
+console.log(ok ? '\n✅ a payment that can\'t be covered says why' : '\n❌ failed');
+process.exit(ok ? 0 : 1);