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

@@ -4687,6 +4687,37 @@ void testLiteSyncStatusParserRealShapes()
}
}
// Per-address balances: unspent notes/utxos are summed per address (spent ones excluded)
// into the WalletState address list, instead of aggregate-only zeros.
void testLitePerAddressBalances()
{
using namespace dragonx::wallet;
LiteWalletRefreshBundle bundle;
bundle.complete = true;
bundle.hasAddresses = true;
bundle.addresses.zAddresses = {"zs1aaa"};
bundle.addresses.tAddresses = {"t1bbb"};
bundle.hasNotes = true;
LiteSpendableOutput note; note.address = "zs1aaa"; note.value = 150000000; note.createdInTxid = "n1";
LiteSpendableOutput utxo; utxo.address = "t1bbb"; utxo.value = 50000000; utxo.createdInTxid = "u1";
LiteSpendableOutput spentNote; spentNote.address = "zs1aaa"; spentNote.value = 999; spentNote.spent = true; spentNote.createdInTxid = "n2";
bundle.notes.unspentNotes.push_back(note);
bundle.notes.unspentNotes.push_back(spentNote); // excluded: spent
bundle.notes.utxos.push_back(utxo);
const auto mapped = mapLiteWalletRefreshBundle(bundle);
EXPECT_TRUE(mapped.ok);
dragonx::WalletState state;
applyLiteRefreshModelToWalletState(mapped.model, state);
EXPECT_EQ(static_cast<int>(state.z_addresses.size()), 1);
EXPECT_EQ(static_cast<int>(state.t_addresses.size()), 1);
EXPECT_NEAR(state.z_addresses[0].balance, 1.5, 1e-9); // 150000000; the spent note is excluded
EXPECT_NEAR(state.t_addresses[0].balance, 0.5, 1e-9); // 50000000
}
// Gateway hardening: one command's parse failure must not abort the whole refresh — the
// other commands still populate the bundle (graceful degradation against real-shape drift).
void testLiteWalletGatewayRefreshSkipsFailedCommand()
@@ -4809,6 +4840,7 @@ int main()
testLiteWalletControllerLifecycle();
testLiteChainNameMigration();
testLiteRefreshModelAppliesToWalletState();
testLitePerAddressBalances();
testLiteSyncStatusParserRealShapes();
testLiteWalletControllerRefreshPopulatesState();
testLiteWalletGatewayRefreshSkipsFailedCommand();