diff --git a/src/wallet/lite_result_parsers.cpp b/src/wallet/lite_result_parsers.cpp index 80ebf5c..8f4210d 100644 --- a/src/wallet/lite_result_parsers.cpp +++ b/src/wallet/lite_result_parsers.cpp @@ -349,6 +349,15 @@ bool parseTransactionRecord(const json& value, output.amount += parsedOutput.value; output.outgoingMetadata.push_back(std::move(parsedOutput)); } + // A send this wallet did not author (seed restore / other device) has empty outgoing_metadata, + // so the sum is 0. Fall back to the record's net 'amount' magnitude (includes the fee) so Sent + // totals aren't understated. Display-only; tolerant of a missing/odd field. + if (output.amount == 0 && value.contains("amount") && !value.at("amount").is_null()) { + std::int64_t net = 0; + if (jsonToSigned(value.at("amount"), net) && net != std::numeric_limits::min()) { + output.amount = net < 0 ? -net : net; + } + } return true; } diff --git a/src/wallet/lite_wallet_controller.cpp b/src/wallet/lite_wallet_controller.cpp index 0f858bd..59c171b 100644 --- a/src/wallet/lite_wallet_controller.cpp +++ b/src/wallet/lite_wallet_controller.cpp @@ -435,7 +435,9 @@ void applyLiteRefreshModelToWalletState(const LiteWalletAppRefreshModel& model, } else { tx.type = record.signedAmountZatoshis < 0 ? "send" : "receive"; } - tx.amount = static_cast(record.amountZatoshis) / kZatoshisPerCoin; + // Signed like the full node: a send is a negative debit, a receive positive. Consumers + // (History/Details/CSV) pick sign + colour from this; the summary cards use std::abs. + tx.amount = static_cast(record.signedAmountZatoshis) / kZatoshisPerCoin; tx.timestamp = record.timestamp; tx.address = record.address; tx.memo = record.memo; diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 7a29eab..d6148f7 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -11736,6 +11736,50 @@ void testLiteSendShowsRecipientFromOutgoing() EXPECT_EQ(state.transactions[0].memo, std::string("thanks")); } +// Lite sends must carry a NEGATIVE amount (full-node debit semantics) so History/Details/CSV render a +// red "-" instead of a green "+"; receives stay positive. +void testLiteSendAmountIsNegative() +{ + using namespace dragonx::wallet; + LiteWalletRefreshBundle bundle; + bundle.complete = true; + bundle.hasTransactions = true; + LiteTransactionRecord snd; + snd.txid = "s"; snd.datetime = 1700000000; snd.direction = LiteTransactionDirection::Send; + LiteTransactionOutput out; out.address = "zs1r"; out.value = 75000000; + snd.outgoingMetadata.push_back(out); + snd.amount = 75000000; + LiteTransactionRecord rcv; + rcv.txid = "r"; rcv.datetime = 1700000100; rcv.direction = LiteTransactionDirection::Receive; + rcv.address = "zs1me"; rcv.amount = 50000000; + bundle.transactions.transactions.push_back(snd); + bundle.transactions.transactions.push_back(rcv); + dragonx::WalletState state; + applyLiteRefreshModelToWalletState(mapLiteWalletRefreshBundle(bundle).model, state); + EXPECT_EQ(static_cast(state.transactions.size()), 2); + for (const auto& t : state.transactions) { + if (t.txid == "s") { EXPECT_EQ(t.type, std::string("send")); EXPECT_NEAR(t.amount, -0.75, 1e-9); } + else { EXPECT_EQ(t.type, std::string("receive")); EXPECT_NEAR(t.amount, 0.5, 1e-9); } + } +} + +// A send with empty outgoing_metadata (seed restore / other device) falls back to |record.amount|. +void testLiteForeignSendFallsBackToRecordAmount() +{ + using namespace dragonx::wallet; + const auto parsed = parseLiteTransactionsResponse(std::string( + R"([{"txid":"f","datetime":1700000000,"outgoing_metadata":[],"amount":-123456789}])")); + EXPECT_TRUE(parsed.ok); + EXPECT_EQ(static_cast(parsed.transactions.transactions.size()), 1); + EXPECT_TRUE(parsed.transactions.transactions[0].direction == LiteTransactionDirection::Send); + EXPECT_EQ(parsed.transactions.transactions[0].amount, static_cast(123456789)); + // Explicit outgoing_metadata still wins over the record amount. + const auto own = parseLiteTransactionsResponse(std::string( + R"([{"txid":"o","datetime":1,"outgoing_metadata":[{"address":"zs1","value":5}],"amount":-99}])")); + EXPECT_TRUE(own.ok); + EXPECT_EQ(own.transactions.transactions[0].amount, static_cast(5)); +} + // A tolerated partial refresh where the notes/utxo command failed (hasAddresses but // !hasSpendableOutputs) must NOT zero every per-address balance — it keeps the last-known // values so a correct total isn't accompanied by a misleading all-zero breakdown. @@ -19393,6 +19437,8 @@ int main() testWalletStateTxRevision(); testTransactionRefreshRevision(); testLiteSendShowsRecipientFromOutgoing(); + testLiteSendAmountIsNegative(); + testLiteForeignSendFallsBackToRecordAmount(); testLitePartialRefreshKeepsPriorAddressBalances(); testLitePerAddressBalances(); testLiteWalletControllerNewAddress();