diff options
| author | Adam Soltys <[email protected]> | 2026-06-24 20:31:08 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-24 20:31:08 -0700 |
| commit | a6bfb5bd4429cf7069a5d0c5e61cb5873ddd8230 (patch) | |
| tree | 70b99b7c1bd2a3a356537305e9c435bff8bbfb7a /src | |
| parent | 08d1753e8433fcae6e9068e87cff93813f0ed76d (diff) | |
| download | coinosv3-a6bfb5bd4429cf7069a5d0c5e61cb5873ddd8230.tar.gz coinosv3-a6bfb5bd4429cf7069a5d0c5e61cb5873ddd8230.zip | |
Deduct gift-locked coins from the spendable balance
Coins reserved behind unclaimed gift links were already excluded from
spending, but the headline balance still counted them. Show the spendable
balance (total minus gift locks) as the headline and surface the locked
amount as its own line, so a gift debits immediately rather than at claim.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 17 | ||||
| -rw-r--r-- | src/i18n.js | 1 | ||||
| -rw-r--r-- | src/wallet.js | 13 |
3 files changed, 26 insertions, 5 deletions
@@ -1450,18 +1450,25 @@ function offlineBanner() { function balanceCard() { // Only dim on the very first load; background updates happen silently. const firstLoad = wallet.scanning && !wallet.loaded; + const locked = wallet.lockedValue; return h( 'div', { class: 'card balance' }, h('div', { class: 'small faint', style: 'text-transform:uppercase;letter-spacing:.05em' }, t('balance')), - // Headline is the projected balance (confirmed + mempool), so a pending - // spend is debited immediately rather than waiting for confirmation. - h('div', { class: 'amt', style: firstLoad ? 'opacity:.3' : '' }, fmtAmount(wallet.total), ' ', unitTag('unit')), - wallet.pending > 0 + // Headline is the spendable, projected balance (confirmed + mempool, minus + // coins locked in unclaimed gifts), so both a pending spend and a reserved + // gift are debited immediately rather than waiting to confirm/claim. + h('div', { class: 'amt', style: firstLoad ? 'opacity:.3' : '' }, fmtAmount(wallet.spendable), ' ', unitTag('unit')), + wallet.pending > 0 || locked > 0 ? h( 'div', { class: 'split' }, - h('div', {}, h('div', { class: 'k' }, t('pending')), h('div', { class: 'v pending' }, fmtAmount(wallet.pending), ' ', unitTag())) + wallet.pending > 0 + ? h('div', {}, h('div', { class: 'k' }, t('pending')), h('div', { class: 'v pending' }, fmtAmount(wallet.pending), ' ', unitTag())) + : null, + locked > 0 + ? h('div', {}, h('div', { class: 'k' }, t('lockedInGifts')), h('div', { class: 'v' }, fmtAmount(locked), ' ', unitTag())) + : null ) : null ); diff --git a/src/i18n.js b/src/i18n.js index 42fd34b..8e7cbf4 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -195,6 +195,7 @@ const STR = { giftAmountInvalid: 'Enter at least {n}.', giftMinNote: 'Minimum {n} at current fees, so it clears dust when claimed.', giftAnother: 'Create another', + lockedInGifts: 'Locked in gifts', giftReserved: 'Unclaimed gift links ({n})', giftReclaim: 'Reclaim', giftReclaimPrompt: 'Reclaim frees this coin for your next payment, but the link stays claimable until you spend it. Pay a network fee to revoke it right now instead?', diff --git a/src/wallet.js b/src/wallet.js index 72d33be..2871c5e 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -583,6 +583,19 @@ export class Wallet { return this.confirmed + this.pending; } + // Confirmed coins locked behind unclaimed gift links. The whole source coin is + // committed until the gift is claimed (our change only comes back in the + // claimer's tx), so it's already excluded from spending — and from the + // spendable balance below. + get lockedValue() { + const res = this.reservedSet(); + return this.utxos.reduce((s, u) => s + (res.has(utxoId(u)) ? u.value : 0), 0); + } + // What the user can actually spend right now: everything minus gift locks. + get spendable() { + return this.total - this.lockedValue; + } + _recomputeBalanceFromChains() { let c = 0; let p = 0; |
