aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
Diffstat (limited to 'src/features')
-rw-r--r--src/features/ark.js58
-rw-r--r--src/features/zaps.js2
2 files changed, 46 insertions, 14 deletions
diff --git a/src/features/ark.js b/src/features/ark.js
index d1fa356..5d3b293 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -491,6 +491,23 @@ export function arkFeature(ctx) {
localStorage.setItem(ARK_INFO_KEY + url, JSON.stringify(tip ? { info, tip, at: Date.now() } : { info, tip: (prev && prev.tip) || 0, at: (prev && prev.at) || Date.now() }));
} catch {}
}
+ // Whether Savings can fund a move into Spending at all: the server's board
+ // minimum (1,000 sats on coinos) plus the board transaction's chain fee.
+ // A Savings balance under that — 546 sats, say — must not be offered a
+ // move the board panel then refuses with "Needs at least 1,000 sats"; the
+ // way into Spending for it is a Lightning receive, which lands there
+ // directly. The minimum comes from the live manager or the last copy of
+ // the server's info seen, so the answer is right before connect too.
+ function savingsBoardMin() {
+ const info = (ark && ark.info) || ((cachedArkInfo((getArkConfig() || {}).ark) || {}).info) || null;
+ return info ? (info.minBoardAmountSat || 0) : 1000;
+ }
+ function savingsCanBoard() {
+ if (!arkAvailable()) return false;
+ const min = savingsBoardMin();
+ if (min >= 2_100_000_000_000_000 || wallet.spendable < min) return false; // paused, or short
+ try { return previewBoardFunding(wallet, ark, min).fundingSat >= min; } catch { return false; } // the fee on top
+ }
// the tip, live or estimated from the last one seen (a block per ten minutes)
function arkTipNow(mgr) {
if (mgr && mgr._tipH) return mgr._tipH;
@@ -930,7 +947,7 @@ export function arkFeature(ctx) {
h('button', { class: 'btn-ghost', onClick: () => { ui.arkSend = null; ui.sendError = ''; render(); } }, t('back')),
ui.busy
? h('button', { class: 'btn-primary grow', disabled: true }, h('span', { class: 'spinner' }))
- : a.amountSat > (arkBalance()?.spendableSat || 0) && wallet.spendable > 0 && !wallet.watchOnly
+ : a.amountSat > (arkBalance()?.spendableSat || 0) && savingsCanBoard()
? h('button', { class: 'btn-primary grow', onClick: () => {
const need = a.amountSat;
ui.arkSend = null; ui.sendError = '';
@@ -1953,11 +1970,16 @@ export function arkFeature(ctx) {
}
// The ask itself: switch and continue, move Savings over first, or not.
+ // When Savings can't fund a move either (under the board minimum, as a
+ // 546-sat Savings against coinos's 1,000 is), the door offered is the one
+ // that works: a Lightning receive, which lands in Spending directly.
function arkLnFromSavingsView() {
const p = ui.arkLnFromSavings;
const have = arkBalance()?.spendableSat || 0;
const need = p.amountSat || 0;
const enough = !need || have >= need;
+ const canMove = savingsCanBoard();
+ const close = () => { ui.arkLnFromSavings = null; ui.sendError = ''; ui.send = blankSend(); };
const proceed = () => {
ui.arkLnFromSavings = null;
ctx.setAccount('spending', 'right');
@@ -1965,20 +1987,27 @@ export function arkFeature(ctx) {
if (startArkLnPay(p.invoice, p.meta)) return;
ui.arkLnFromSavingsOk = false;
// Spending can't carry it: the same offer the Spending side makes
- if (need && wallet.spendable > 1000 && ctx.hook('arkOfferBoard', need)) return;
+ if (need && canMove && ctx.hook('arkOfferBoard', need)) return;
ui.sendError = t('arkLnExceedsSpending', { need: fmtAmount(need) + ' ' + unitLabel(), have: fmtAmount(have) + ' ' + unitLabel() });
render();
};
+ const moveFirst = () => { ui.arkLnFromSavings = null; ctx.hook('arkOfferBoard', need || 0); };
+ const receiveInstead = () => { close(); ctx.setAccount('spending', 'right'); render(); };
+ const sats = (n) => fmtAmount(n) + ' ' + unitLabel();
return h('div', { class: 'card col', style: 'gap:12px' },
h('h3', { style: 'margin:0' }, t('arkLnFromSavingsTitle')),
- h('p', { class: 'small muted', style: 'margin:0' }, t('arkLnFromSavingsBody', { have: fmtAmount(have) + ' ' + unitLabel() })),
- need ? h('div', { class: 'row between' }, h('span', { class: 'small muted' }, t('lnPayAmount')), h('span', { class: 'small' }, fmtAmount(need) + ' ' + unitLabel())) : null,
- !enough ? h('div', { class: 'notice info small' }, t('arkLnFromSavingsShort')) : null,
- h('button', { class: 'btn-primary btn-block', onClick: proceed }, enough ? t('arkLnUseSpending') : t('arkLnMoveFirst')),
- enough && wallet.spendable > 1000
- ? h('button', { class: 'btn-block', onClick: () => { ui.arkLnFromSavings = null; ctx.hook('arkOfferBoard', need || 0); } }, t('arkLnMoveFirst'))
+ h('p', { class: 'small muted', style: 'margin:0' }, t('arkLnFromSavingsBody', { have: sats(have) })),
+ need ? h('div', { class: 'row between' }, h('span', { class: 'small muted' }, t('lnPayAmount')), h('span', { class: 'small' }, sats(need))) : null,
+ enough ? null
+ : canMove ? h('div', { class: 'notice info small' }, t('arkLnFromSavingsShort'))
+ : h('div', { class: 'notice info small' }, t('arkLnSavingsTooSmall', { have: sats(wallet.spendable || 0), min: sats(savingsBoardMin()) })),
+ enough ? h('button', { class: 'btn-primary btn-block', onClick: proceed }, t('arkLnUseSpending'))
+ : canMove ? h('button', { class: 'btn-primary btn-block', onClick: moveFirst }, t('arkLnMoveFirst'))
+ : h('button', { class: 'btn-primary btn-block', onClick: receiveInstead }, t('arkLnReceiveInstead')),
+ enough && canMove
+ ? h('button', { class: 'btn-block', onClick: moveFirst }, t('arkLnMoveFirst'))
: null,
- h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.arkLnFromSavings = null; ui.sendError = ''; ui.send = blankSend(); render(); } }, t('cancel')));
+ h('button', { class: 'btn-ghost btn-block', onClick: () => { close(); render(); } }, t('cancel')));
}
// shared by the ark and lightning zap flows (see zaps.js for the twin)
@@ -3666,7 +3695,7 @@ export function arkFeature(ctx) {
const boardsPaused = () => ((ark && ark.info && ark.info.minBoardAmountSat) || 0) >= 2_100_000_000_000_000;
function boardForm() {
if (boardsPaused()) return h('div', { class: 'notice small' }, t('arkBoardPaused'));
- const minBoard = (ark && ark.info && ark.info.minBoardAmountSat) || 0;
+ const minBoard = savingsBoardMin();
const canBoard = wallet.spendable >= minBoard;
return h('div', { class: 'col', style: 'width:100%;gap:8px' },
h('div', { class: 'input-group' },
@@ -4035,6 +4064,7 @@ export function arkFeature(ctx) {
return mgr.fetchBolt12(offerStr, amountSat);
},
lnSpendableSat() { const b = arkBalance(); return b ? b.spendableSat : 0; },
+ arkSavingsCanBoard() { return savingsCanBoard(); },
spendingRenewingSat() { return renewingSat(); },
// Max for any Lightning amount form: spendable minus estimated fees.
lnMaxSendSat() { return lnMaxSat(); },
@@ -4058,7 +4088,7 @@ export function arkFeature(ctx) {
const short = Math.max(0, Math.ceil(needSat * 1.01) + 2 - have);
let gross = short;
try { for (let i = 0; i < 4; i++) gross = short + boardFee(gross, ark.info.boardFees); } catch {}
- const minBoard = (ark && ark.info && ark.info.minBoardAmountSat) || 0;
+ const minBoard = savingsBoardMin(); // remembered info too: the offer can come before connect
ctx.goHome();
ui.arkMoveOpen = true;
ui.arkMoveDir = 'toSpending';
@@ -4209,11 +4239,13 @@ export function arkFeature(ctx) {
// 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;
+ if (need && savingsCanBoard() && ctx.hook('arkOfferBoard', need)) return true;
ui.sendError = t('arkLnExceedsSpending', {
need: fmtAmount(need) + ' ' + unitLabel(),
have: fmtAmount(arkBalance()?.spendableSat || 0) + ' ' + unitLabel(),
- });
+ }) + (wallet.spendable > 0 && !savingsCanBoard() // Savings has sats, just too few to move
+ ? ' ' + t('arkLnSavingsTooSmall', { have: fmtAmount(wallet.spendable) + ' ' + unitLabel(), min: fmtAmount(savingsBoardMin()) + ' ' + unitLabel() })
+ : '');
render();
return true;
}
diff --git a/src/features/zaps.js b/src/features/zaps.js
index 76296cf..fbdc2e9 100644
--- a/src/features/zaps.js
+++ b/src/features/zaps.js
@@ -487,7 +487,7 @@ export function zapsFeature(ctx) {
// what this payment needs (extra is one edit away).
const typedSats = parseAmount(z.amount, getUnit()) || 0;
const theirMaxSat = Math.floor(p.maxSendable / 1000);
- const boardable = !!hook('arkReady') && ours != null;
+ const boardable = !!hook('arkReady') && ours != null && !!hook('arkSavingsCanBoard');
const renewing = hook('spendingRenewingSat') || 0;
const waitingOnRenewal = renewing > 0 && broke && ours + renewing >= Math.max(min, p.fixedSat || typedSats);
const needsBoard = !waitingOnRenewal && boardable && (broke || (typedSats > 0 && typedSats > ours && typedSats <= theirMaxSat));