wallet_rpc_server: expose change amount in transfer entries
What changed, and why it matters
This commit adds a new 'change_amount' field to Monero wallet RPC responses that describe transfers. When you send Monero, part of the input often comes back to you as change. Previously, callers of the wallet RPC had to calculate this themselves from other fields; now it is exposed directly. This is a straightforward feature addition, not a security fix or vulnerability.
No security action required. Treat as a normal feature/API update; review downstream consumers for compatibility if they parse RPC responses strictly.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch extends the wallet RPC server to populate and serialize a new change_amount field in transfer_entry structures. It updates two population sites in wallet_rpc_server.cpp (one for in/out/pool entries, one for pending entries), bumps the wallet RPC minor version from 31 to 32, adds the field to the struct and serialization macros in wallet_rpc_server_commands_defs.h, and adds functional tests in transfer.py asserting the field is present and consistent. The change is additive and backward-compatible via KV_SERIALIZE_OPT with a default of 0.
Changed components
src/wallet/wallet_rpc_server.cppsrc/wallet/wallet_rpc_server_commands_defs.htests/functional_tests/transfer.pyInspect captured patch +14 / −1
diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp
index e64df58..6357c15 100644
--- a/src/wallet/wallet_rpc_server.cpp
+++ b/src/wallet/wallet_rpc_server.cpp
@@ -520,6 +520,7 @@ namespace tools
entry.fee = pd.m_amount_in - pd.m_amount_out;
uint64_t change = pd.m_change == (uint64_t)-1 ? 0 : pd.m_change; // change may not be known
entry.amount = pd.m_amount_in - change - entry.fee;
+ entry.change_amount = change;
entry.note = m_wallet->get_tx_note(txid);
for (const auto &d: pd.m_dests) {
@@ -548,6 +549,7 @@ namespace tools
entry.timestamp = pd.m_timestamp;
entry.fee = pd.m_amount_in - pd.m_amount_out;
entry.amount = pd.m_amount_in - pd.m_change - entry.fee;
+ entry.change_amount = pd.m_change;
entry.unlock_time = pd.m_tx.unlock_time;
entry.locked = true;
entry.note = m_wallet->get_tx_note(txid);
diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h
index 09d4ae9..065f635 100644
--- a/src/wallet/wallet_rpc_server_commands_defs.h
+++ b/src/wallet/wallet_rpc_server_commands_defs.h
@@ -47,7 +47,7 @@
// advance which version they will stop working with
// Don't go over 32767 for any of these
#define WALLET_RPC_VERSION_MAJOR 1
-#define WALLET_RPC_VERSION_MINOR 31
+#define WALLET_RPC_VERSION_MINOR 32
#define MAKE_WALLET_RPC_VERSION(major,minor) (((major)<<16)|(minor))
#define WALLET_RPC_VERSION MAKE_WALLET_RPC_VERSION(WALLET_RPC_VERSION_MAJOR, WALLET_RPC_VERSION_MINOR)
namespace tools
@@ -1454,6 +1454,7 @@ namespace wallet_rpc
uint64_t amount;
amounts_container amounts;
uint64_t fee;
+ uint64_t change_amount;
std::string note;
std::list<transfer_destination> destinations;
std::string type;
@@ -1474,6 +1475,7 @@ namespace wallet_rpc
KV_SERIALIZE(amount)
KV_SERIALIZE(amounts)
KV_SERIALIZE(fee)
+ KV_SERIALIZE_OPT(change_amount, (uint64_t)0)
KV_SERIALIZE(note)
KV_SERIALIZE(destinations)
KV_SERIALIZE(type)
diff --git a/tests/functional_tests/transfer.py b/tests/functional_tests/transfer.py
index 3f2eedc..eb4fba0 100755
--- a/tests/functional_tests/transfer.py
+++ b/tests/functional_tests/transfer.py
@@ -204,6 +204,13 @@ class TransferTest():
assert e.address == '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm'
assert e.double_spend_seen == False
assert not 'confirmations' in e or e.confirmations == 0
+ assert e.change_amount > 0
+
+ res = self.wallet[0].get_transfer_by_txid(txid)
+ assert len(res.transfers) == 1
+ assert res.transfers[0] == res.transfer
+ assert res.transfer.type == 'pending'
+ assert res.transfer.change_amount == e.change_amount
running_balances[0] -= fee
@@ -235,6 +242,7 @@ class TransferTest():
assert e.address == '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm'
assert e.double_spend_seen == False
assert e.confirmations == 1
+ assert e.change_amount > 0
res = self.wallet[0].get_height()
wallet_height = res.height
@@ -256,6 +264,7 @@ class TransferTest():
assert t.address == '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm'
assert t.double_spend_seen == False
assert t.confirmations == 1
+ assert t.change_amount == e.change_amount
res = self.wallet[0].get_balance()
assert res.balance == running_balances[0]
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.