diff options
| author | Adam Soltys <[email protected]> | 2026-09-02 08:32:13 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-02 08:32:13 -0700 |
| commit | acd6ca748f1fc4b31648fb999e2f9342cbf5c8a1 (patch) | |
| tree | bd1f3108ecf9cce7bd0bdd1f0ca849925b9404d7 /src | |
| parent | 21418741adfa8d16c53a20a5cc0d2d78a758ed89 (diff) | |
| download | coinosv3-acd6ca748f1fc4b31648fb999e2f9342cbf5c8a1.tar.gz coinosv3-acd6ca748f1fc4b31648fb999e2f9342cbf5c8a1.zip | |
The tester with 233 sats gets to spend them: invoice coverage judges the SUM of spendable coins (the single-coin gate silently declined anything over the largest coin, though multi-input pays gather 24), an uncoverable invoice now owns its paste — board the shortfall from Savings, or say what's short — instead of dropping the bolt11 into the address field like a dead click, and chain calls gain a last-resort relay through the registrar (/esplora passthrough, tip cached) for the network that blackholes every public esplora
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
Diffstat (limited to 'src')
| -rw-r--r-- | src/ark/manager.js | 6 | ||||
| -rw-r--r-- | src/features/ark.js | 32 | ||||
| -rw-r--r-- | src/i18n.js | 1 |
3 files changed, 32 insertions, 7 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js index e9f79b0..9731dd6 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -200,7 +200,11 @@ export class ArkManager { async _esplora(path, init) { const hosts = (this._esploraHosts ||= [ this.esploraUrl, - ...(this.network === 'mainnet' ? ['https://blockstream.info/api', 'https://mempool.space/api'] : []), + // last resort: our own registrar relays chain calls — the one host a + // wallet that can reach coinos at all provably reaches + ...(this.network === 'mainnet' + ? ['https://blockstream.info/api', 'https://mempool.space/api', 'https://names.coinos.io/esplora'] + : []), ].filter((h, i, a) => h && a.indexOf(h) === i)); const start = this._esploraIdx || 0; let lastErr; diff --git a/src/features/ark.js b/src/features/ark.js index ba566ee..829cba9 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -728,12 +728,15 @@ export function arkFeature(ctx) { const dec = maybeLnInvoice(invoice); if (!dec || !arkAvailable() || wallet.watchOnly) return false; const s = arkStateNow(); - const covers = (state, sats) => - !!s && (s.vtxos || []).some((v) => v.state === state && v.amountSat >= sats); + // Coverage by SUM: multi-input pays gather up to 24 coins, so judging by + // the single largest coin declined invoices the wallet could pay — a + // tester holding 204+29 sats pasted a ~220-sat invoice and nothing + // happened at all. Fees are the quote's business, not this gate's. + const sum = (state) => + (s ? (s.vtxos || []) : []).filter((v) => v.state === state).reduce((n, v) => n + v.amountSat, 0); // pending funds count for the take-over decision: an in-flight payment or - // revocation frees them in seconds, and the Boltz fallback can't do small - // amounts at all — better to wait for ark than bounce off Boltz's minimum - if (!covers('spendable', dec.amountSat || 1) && !covers('pending', dec.amountSat || 1)) return false; + // revocation frees them in seconds + if (sum('spendable') < (dec.amountSat || 1) && sum('pending') < (dec.amountSat || 1)) return false; ui.arkLnPay = { invoice, meta: meta || null, amountSat: dec.amountSat, amount: '', feeSat: null, status: 'quote' }; ui.sendError = ''; render(); @@ -2835,7 +2838,24 @@ export function arkFeature(ctx) { return true; } } - if (maybeLnInvoice(inv)) return startArkLnPay(inv); + if (maybeLnInvoice(inv)) { + if (startArkLnPay(inv)) return true; + if (arkAvailable() && !wallet.watchOnly) { + // An invoice Spending can't cover used to fall through SILENTLY — + // the bolt11 landed in the address field looking like a dead + // paste. Own it: offer to board the shortfall from Savings when + // there is one, else say plainly what's short. + const need = (maybeLnInvoice(inv) || {}).amountSat || 0; + if (need && wallet.spendable > 1000 && ctx.hook('arkOfferBoard', need)) return true; + ui.sendError = t('arkLnExceedsSpending', { + need: fmtAmount(need) + ' ' + unitLabel(), + have: fmtAmount(arkBalance()?.spendableSat || 0) + ' ' + unitLabel(), + }); + render(); + return true; + } + return false; + } const pk = npubToHex(text); if (!pk || !arkAvailable() || !wallet.nostrFetch) return false; startNpubPay(pk, String(text).trim()); diff --git a/src/i18n.js b/src/i18n.js index f9f8567..fc23c19 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -852,6 +852,7 @@ const STR = { arkSendTitle: 'Send', arkSendHint: 'Ark address — sent instantly off-chain, no mining fee.', arkOwnAddrBoard: "That's your own Spending address — moving from Savings instead.", + arkLnExceedsSpending: 'That invoice asks for {need} but Spending holds {have}.', arkPayFromSpending: 'Ark payments come from your Spending balance — switch to Spending to send this.', arkPayTo: 'To', arkNoFee: 'none — instant off-chain send', |
