feat(lite): per-address balances from unspent notes/utxos

applyLiteRefreshModelToWalletState now derives each address's balance by summing its unspent
notes/utxos (excluding spent and unconfirmed-spent outputs) instead of the aggregate-only
zeros, so the Receive/Balance UI shows per-address amounts. The notes parser shape is
confirmed against do_list_notes in the backend source.

testLitePerAddressBalances covers the summing + spent-exclusion. Completes M2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 10:24:36 -05:00
parent c6e28fc4da
commit e6b91ca661
3 changed files with 51 additions and 2 deletions

View File

@@ -7,6 +7,8 @@
#include "../data/wallet_state.h"
#include <chrono>
#include <cstdint>
#include <unordered_map>
#include <utility>
#include <sodium.h>
@@ -37,13 +39,26 @@ void applyLiteRefreshModelToWalletState(const LiteWalletAppRefreshModel& model,
}
if (model.hasAddresses) {
// Per-address balances from unspent notes/utxos (when the notes command succeeded).
// Sum the value of outputs at each address that are neither spent nor unconfirmed-spent.
std::unordered_map<std::string, std::uint64_t> perAddressZatoshis;
if (model.hasSpendableOutputs) {
for (const auto& output : model.spendableOutputs) {
if (output.spent || output.unconfirmedSpent) continue;
perAddressZatoshis[output.address] += output.valueZatoshis;
}
}
state.addresses.clear();
state.z_addresses.clear();
state.t_addresses.clear();
for (const auto& addr : model.addresses) {
AddressInfo info;
info.address = addr.address;
info.balance = 0.0; // lite address listing is aggregate-only; per-address balance is M2b (notes correlation)
const auto it = perAddressZatoshis.find(addr.address);
info.balance = it != perAddressZatoshis.end()
? static_cast<double>(it->second) / kZatoshisPerCoin
: 0.0;
info.type = (addr.kind == LiteWalletAppAddressKind::Shielded) ? "shielded" : "transparent";
info.has_spending_key = addr.spendabilityKnown ? addr.spendable : true;
if (addr.kind == LiteWalletAppAddressKind::Shielded) {