diff options
| author | Adam Soltys <[email protected]> | 2026-06-28 09:57:18 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-28 09:57:18 -0700 |
| commit | 7dd7f1678c9f294b7a42bfbca263c53c08a83ac4 (patch) | |
| tree | 96ebd94f304e0e80f44c5a2af738e53538164877 /src/wallet.js | |
| parent | 1e43db6e50e5e1dc7ccb8fb53e7ce68cc580967d (diff) | |
| download | coinosv3-7dd7f1678c9f294b7a42bfbca263c53c08a83ac4.tar.gz coinosv3-7dd7f1678c9f294b7a42bfbca263c53c08a83ac4.zip | |
Consolidate coins: a Settings action + a gift-time recommendation
- Settings gains a 'Consolidate coins' card (shown when you hold >1 spendable
coin): wallet.consolidate() does a max self-send merging them into one, so
future sends/gifts use a single input — cheaper and a smaller gift QR.
- At gift creation, detect when the gift would bundle several coins
(giftCoinSummary returns the input count) and trigger the split/consolidate
offer when count ≥ 3 (not just when it frees change). The offer now explains it
plainly ('would bundle N coins, costly to claim') and recommends splitting into
one coin first, with 'Use N coins anyway' as the explicit opt-out.
Verified on regtest: 6 small coins → a 1000-sat gift needs 3 inputs and triggers
the offer; consolidate merges them to 1 coin, after which the same gift is a clean
single input.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src/wallet.js')
| -rw-r--r-- | src/wallet.js | 23 |
1 files changed, 23 insertions, 0 deletions
diff --git a/src/wallet.js b/src/wallet.js index e2b5b35..6e3ea81 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -1577,6 +1577,29 @@ export class Wallet { return sel ? sel.reduce((s, u) => s + u.value, 0) : null; } + // The coins a gift of this amount would lock: total value + how many inputs. + // A high input count means a costly-to-claim gift (each input adds to the + // claimer's fee) — the UI uses it to recommend splitting/consolidating first. + giftCoinSummary(amountSats) { + const sel = this._selectGiftCoins(BigInt(Math.round(amountSats))); + return sel ? { lock: sel.reduce((s, u) => s + u.value, 0), count: sel.length } : null; + } + + // Number of unreserved spendable coins (for the consolidate prompt/action). + spendableCoinCount() { + return this.utxos.filter((u) => !this.isReserved(utxoId(u)) && (u.confirmed || u.chain === 1)).length; + } + + // Merge all unreserved spendable coins into one, via a max self-send. Leaves + // the wallet with a single coin so future sends/gifts use one input. + async consolidate(feeRate) { + const rate = Math.max(1, Math.round(feeRate)); + const draft = this.buildTx({ recipients: [{ address: this.freshReceive().address }], feeRate: rate, sendMax: true }); + const hex = this.sign(draft.tx); + await this.broadcast(hex); + return { txid: draft.tx.id, inputs: draft.inputsCount }; + } + // Build a signed gift from already-chosen coins. If the gift leaves a viable // (>= dust) change, output0 is that change back to us, cryptographically fixed // by signing input0 SIGHASH_SINGLE (the rest SIGHASH_NONE) — so our change is |
