fix(lite): render a lite send as a negative debit, not a green "+"; recover unauthored-send amounts
Lite-parity audit, two History-correctness findings (display/parse only — no send/broadcast call touched): - A lite send carried a POSITIVE tx.amount, so History, the Details dialog and CSV export rendered an outgoing payment as a green "+" receipt (the full node uses a negative debit). The mapper already computes signedAmountZatoshis (negative for sends, positive for receives); applyLiteRefreshModelToWalletState now uses it for tx.amount, so the shared UI shows a red "-" and CSV a negative value. The summary cards use std::abs and are unaffected. - A send this wallet did not author (seed restore / another device) has empty outgoing_metadata, so the parser summed it to 0 and showed amount 0 with a blank address, understating Sent totals. parseTransactionRecord now falls back to |record.amount| (the net debit, incl. fee) when the metadata sum is 0, tolerant of a missing/odd field; explicit outgoing_metadata still wins. Tests: testLiteSendAmountIsNegative (send maps to <0, receive stays >0) and testLiteForeignSendFallsBackToRecordAmount. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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<std::int64_t>::min()) {
|
||||
output.amount = net < 0 ? -net : net;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -435,7 +435,9 @@ void applyLiteRefreshModelToWalletState(const LiteWalletAppRefreshModel& model,
|
||||
} else {
|
||||
tx.type = record.signedAmountZatoshis < 0 ? "send" : "receive";
|
||||
}
|
||||
tx.amount = static_cast<double>(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<double>(record.signedAmountZatoshis) / kZatoshisPerCoin;
|
||||
tx.timestamp = record.timestamp;
|
||||
tx.address = record.address;
|
||||
tx.memo = record.memo;
|
||||
|
||||
@@ -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<int>(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<int>(parsed.transactions.transactions.size()), 1);
|
||||
EXPECT_TRUE(parsed.transactions.transactions[0].direction == LiteTransactionDirection::Send);
|
||||
EXPECT_EQ(parsed.transactions.transactions[0].amount, static_cast<std::int64_t>(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<std::int64_t>(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();
|
||||
|
||||
Reference in New Issue
Block a user