diff options
| author | Adam Soltys <[email protected]> | 2026-10-03 16:00:39 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-03 16:00:39 -0700 |
| commit | 328f2db9af46cda473318bb3eada0f6e83bb42f1 (patch) | |
| tree | b4e850022ba260eff90940ebc465eebded023b63 | |
| parent | 141779ab97efad6abefaec0545bba5330fd1e12d (diff) | |
| download | coinosv3-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.js | 27 | ||||
| -rw-r--r-- | src/ark/manager.js | 8 | ||||
| -rw-r--r-- | src/features/ark.js | 22 | ||||
| -rw-r--r-- | src/i18n.js | 3 | ||||
| -rw-r--r-- | src/locales/ar.json | 5 | ||||
| -rw-r--r-- | src/locales/bn.json | 5 | ||||
| -rw-r--r-- | src/locales/de.json | 5 | ||||
| -rw-r--r-- | src/locales/es.json | 5 | ||||
| -rw-r--r-- | src/locales/fr.json | 5 | ||||
| -rw-r--r-- | src/locales/hi.json | 5 | ||||
| -rw-r--r-- | src/locales/id.json | 5 | ||||
| -rw-r--r-- | src/locales/ja.json | 5 | ||||
| -rw-r--r-- | src/locales/ko.json | 5 | ||||
| -rw-r--r-- | src/locales/mr.json | 5 | ||||
| -rw-r--r-- | src/locales/pt.json | 5 | ||||
| -rw-r--r-- | src/locales/ru.json | 5 | ||||
| -rw-r--r-- | src/locales/sw.json | 5 | ||||
| -rw-r--r-- | src/locales/ta.json | 5 | ||||
| -rw-r--r-- | src/locales/te.json | 5 | ||||
| -rw-r--r-- | src/locales/tr.json | 5 | ||||
| -rw-r--r-- | src/locales/ur.json | 5 | ||||
| -rw-r--r-- | src/locales/vi.json | 5 | ||||
| -rw-r--r-- | src/locales/zh.json | 5 | ||||
| -rw-r--r-- | tools/ark/ln-shortfall-test.js | 52 |
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); |
