aboutsummaryrefslogtreecommitdiffstats
path: root/src/wallet.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-28 09:57:18 -0700
committerAdam Soltys <[email protected]>2026-06-28 09:57:18 -0700
commit7dd7f1678c9f294b7a42bfbca263c53c08a83ac4 (patch)
tree96ebd94f304e0e80f44c5a2af738e53538164877 /src/wallet.js
parent1e43db6e50e5e1dc7ccb8fb53e7ce68cc580967d (diff)
downloadcoinosv3-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.js23
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