Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b576c60
daemon rpc: add test for get_outputs method
everoddandeven Sep 10, 2026
1e72a81
daemon rpc: add test for set_poll_period
everoddandeven Sep 10, 2026
834693e
daemon model: add unit test for MoneroKeyImage.deserialize_key_images
everoddandeven Sep 10, 2026
3bbef19
daemon model: improve test_block_deserialize
everoddandeven Sep 10, 2026
4ee011f
daemon model: add and improve test for blocks
everoddandeven Sep 10, 2026
40776f3
daemon model: test tx inputs merge
everoddandeven Sep 10, 2026
3eb0401
daemon model: add test_output_merge_recurses_into_tx_merge
everoddandeven Sep 10, 2026
43262b3
daemon model: add test for mining status deserialization consinstency
everoddandeven Sep 10, 2026
4068052
wallet model: test tx wallet input deserialization
everoddandeven Sep 10, 2026
1a5b10d
wallet model: test tx filter inputs/outputs wallet
everoddandeven Sep 10, 2026
5d62df9
wallet model: test query deserialize from block
everoddandeven Sep 11, 2026
fb1456c
wallet model: test invalid message signature type deserialization
everoddandeven Sep 11, 2026
f1ec278
wallet model: add tests for output comparator
everoddandeven Sep 11, 2026
7a09cec
wallet model: add more unit tests for tx config
everoddandeven Sep 11, 2026
a22ced3
wallet model: test destination deserialization
everoddandeven Sep 11, 2026
06c05a4
wallet model: improve deserialize from block unit tests
everoddandeven Sep 11, 2026
bac4247
wallet model: test MoneroTxWallet.get_transfers
everoddandeven Sep 11, 2026
45a92ae
wallet model: add unit test for tx query decontextualize
everoddandeven Sep 11, 2026
a3f037c
wallet model: add unit tests for query meets_criteria
everoddandeven Sep 11, 2026
28e4768
utils: test MoneroUtils.get_vlocks_from
everoddandeven Sep 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/cpp/daemon/py_monero_daemon_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,9 @@ void py_monero_bind_daemon(py::module_& m, PyMoneroTypes& t) {
.def("get_rpc_connection", [](const monero_daemon_rpc& self) {
MONERO_CATCH_AND_RETHROW(self.get_rpc_connection());
})
.def("set_poll_period_in_ms", [](monero_daemon_rpc& self, uint64_t period_ms) {
MONERO_CATCH_AND_RETHROW(self.set_poll_period_in_ms(period_ms));
}, py::arg("period_ms"), py::call_guard<py::gil_scoped_release>())
.def("is_connected", [](monero_daemon_rpc& self) {
MONERO_CATCH_AND_RETHROW(self.is_connected());
}, py::call_guard<py::gil_scoped_release>());
Expand Down
5 changes: 4 additions & 1 deletion src/cpp/wallet/py_monero_wallet_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,10 @@ void py_monero_bind_wallet(py::module_& m, PyMoneroTypes& t) {
})
.def("meets_criteria", [](monero_tx_query& self, monero_tx_wallet* tx, bool query_children) {
MONERO_CATCH_AND_RETHROW(self.meets_criteria(tx, query_children));
}, py::arg("tx"), py::arg("query_children") = false);
}, py::arg("tx"), py::arg("query_children") = true)
.def_static("decontextualize", [](const std::shared_ptr<monero_tx_query>& query) {
MONERO_CATCH_AND_RETHROW(monero_tx_query::decontextualize(query));
}, py::arg("query"));

// monero_tx_set
t.py_monero_tx_set
Expand Down
8 changes: 8 additions & 0 deletions src/python/monero_daemon_rpc.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ class MoneroDaemonRpc(MoneroDaemon):
"""
...

def set_poll_period_in_ms(self, period_ms: int) -> None:
"""
Set the period between polls of the daemon for block notifications.

:param int period_ms: the poll period in milliseconds.
"""
...

def is_connected(self) -> bool:
"""
Indicates if the client is connected to the daemon via RPC.
Expand Down
15 changes: 14 additions & 1 deletion src/python/monero_tx_query.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class MoneroTxQuery(MoneroTxWallet):
:returns MoneroTxQuery: tx query copy.
"""
...
def meets_criteria(self, tx: MoneroTxWallet, query_children: bool = False) -> bool:
def meets_criteria(self, tx: MoneroTxWallet, query_children: bool = True) -> bool:
"""
Check if transaction wallet meets all criteria defined in this query.

Expand All @@ -74,3 +74,16 @@ class MoneroTxQuery(MoneroTxWallet):
:returns bool: `True` if `tx` meets all criteria defined in this query, `False` otherwise.
"""
...

@staticmethod
def decontextualize(query: MoneroTxQuery) -> MoneroTxQuery:
"""
Remove query criteria which require looking up other transfers/outputs to fulfill the query.

Clears `is_incoming`, `is_outgoing`, `transfer_query`, `input_query` and `output_query`.
Mutates `query` in place.

:param MoneroTxQuery query: the query to decontextualize.
:returns MoneroTxQuery: a reference to `query`, for convenience.
"""
...
1 change: 1 addition & 0 deletions tests/config/config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ rpc_password=abc123
zmq_uri=tcp://127.0.0.1:18085
zmq_pub_uri=tcp://127.0.0.1:18086
log_level=3
poll_period_in_ms=10000

[wallet]
name=test_wallet_1
Expand Down
145 changes: 144 additions & 1 deletion tests/test_monero_daemon_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,20 @@ def test_mining_status_deserialize(self) -> None:
status.num_threads = 4
AssertUtils.assert_serialization_integrity(status)

def test_mining_status_inactive_clears_background_and_address_deserialize(self) -> None:
# when isActive is false from_property_tree drops isBackground and address
# even if present in the json
json_str: str = (
'{"isActive": false, "isBackground": true, '
'"address": "' + "9" + "a" * 94 + '", "speed": 500, "numThreads": 4}'
)
status: MoneroMiningStatus = MoneroMiningStatus.deserialize(json_str)
assert status.is_active is False
assert status.is_background is None
assert status.address is None
assert status.speed == 500
assert status.num_threads == 4

def test_miner_tx_sum_deserialize(self) -> None:
summ: MoneroMinerTxSum = MoneroMinerTxSum()
summ.emission_sum_low = 1000
Expand Down Expand Up @@ -165,7 +179,26 @@ def test_block_deserialize(self) -> None:
block.height = 12345
block.hex = "deadbeef"
block.tx_hashes = ["b" * 64, "c" * 64]
AssertUtils.assert_serialization_integrity(block)

miner_tx: MoneroTx = MoneroTx()
miner_tx.hash = "d" * 64
miner_tx.is_miner_tx = True
block.miner_tx = miner_tx

tx: MoneroTx = MoneroTx()
tx.hash = "e" * 64
block.txs = [tx]

restored: MoneroBlock = AssertUtils.assert_serialization_integrity(block)

# nested txs are parsed and back-linked to the block
assert restored.miner_tx is not None
assert restored.miner_tx.hash == "d" * 64
assert restored.miner_tx.is_miner_tx is True
assert restored.miner_tx.block is not None and restored.miner_tx.block.height == 12345
assert len(restored.txs) == 1
assert restored.txs[0].hash == "e" * 64
assert restored.txs[0].block is not None and restored.txs[0].block.height == 12345

def test_connection_span_deserialize(self) -> None:
span: MoneroConnectionSpan = MoneroConnectionSpan()
Expand Down Expand Up @@ -516,6 +549,28 @@ def test_key_image_deserialize(self) -> None:
key_image.signature = "b" * 128
AssertUtils.assert_serialization_integrity(key_image)

def test_key_image_deserialize_key_images(self) -> None:
json_str: str = (
'{"keyImages": ['
'{"hex": "' + "a" * 64 + '", "signature": "' + "b" * 128 + '"}, '
'{"hex": "' + "c" * 64 + '"}'
']}'
)
key_images: list[MoneroKeyImage] = MoneroKeyImage.deserialize_key_images(json_str)
assert len(key_images) == 2
assert key_images[0].hex == "a" * 64
assert key_images[0].signature == "b" * 128
assert key_images[1].hex == "c" * 64
assert key_images[1].signature is None

# a missing or empty keyImages array yields no key images
assert MoneroKeyImage.deserialize_key_images("{}") == []
assert MoneroKeyImage.deserialize_key_images('{"keyImages": []}') == []

# an empty string is not valid json
with pytest.raises(Exception):
MoneroKeyImage.deserialize_key_images("")

def test_output_deserialize(self) -> None:
output: MoneroOutput = MoneroOutput()
output.amount = 1000000
Expand Down Expand Up @@ -667,10 +722,28 @@ def test_block_copy(self) -> None:
block.hex = "deadbeef"
block.tx_hashes = ["b" * 64, "c" * 64]

miner_tx: MoneroTx = MoneroTx()
miner_tx.hash = "d" * 64
miner_tx.is_miner_tx = True
block.miner_tx = miner_tx

tx: MoneroTx = MoneroTx()
tx.hash = "e" * 64
block.txs = [tx]

copy: MoneroBlock = block.copy()
assert copy is not block
assert copy.serialize() == block.serialize()

# miner tx and non-wallet txs are deep copied and back-linked to the copy
assert copy.miner_tx is not None and copy.miner_tx is not block.miner_tx
assert copy.miner_tx.hash == "d" * 64
assert copy.miner_tx.block is copy
assert len(copy.txs) == 1
assert copy.txs[0] is not block.txs[0]
assert copy.txs[0].hash == "e" * 64
assert copy.txs[0].block is copy

def test_block_merge(self) -> None:
a: MoneroBlock = MoneroBlock()
a.hash = "a" * 64
Expand Down Expand Up @@ -723,6 +796,24 @@ def test_tx_merge(self) -> None:
a.merge(b)
assert a.num_confirmations == 5

def test_tx_merge_adopts_other_block(self) -> None:
block: MoneroBlock = MoneroBlock()
block.height = 200

other: MoneroTx = MoneroTx()
other.hash = "a" * 64
other.is_confirmed = True
other.block = block
block.txs = [other]

tx: MoneroTx = MoneroTx()
tx.hash = "a" * 64
tx.is_confirmed = True
# tx.block is unset -> merge adopts other's block and repoints the block at tx
tx.merge(other)
assert tx.block is block
assert block.txs[0] is tx

def test_tx_merge_is_confirmed_can_become_true(self) -> None:
a: MoneroTx = MoneroTx()
a.hash = "a" * 64
Expand Down Expand Up @@ -803,6 +894,30 @@ def test_output_merge(self) -> None:
assert a.key_image is not None
assert a.key_image.hex == "a" * 64

def test_output_merge_recurses_into_tx_merge(self) -> None:
tx_a: MoneroTx = MoneroTx()
tx_a.hash = "a" * 64
tx_a.is_confirmed = True

tx_b: MoneroTx = MoneroTx()
tx_b.hash = "a" * 64
tx_b.is_confirmed = True
tx_b.num_confirmations = 9 # only tx merge reconciles this

out_a: MoneroOutput = MoneroOutput()
out_a.tx = tx_a
out_a.stealth_public_key = "s" * 64
tx_a.outputs = [out_a]

out_b: MoneroOutput = MoneroOutput()
out_b.tx = tx_b
out_b.stealth_public_key = "s" * 64
tx_b.outputs = [out_b]

# outputs on different txs -> merge delegates to tx merge (which comes back to merging outputs)
out_a.merge(out_b)
assert tx_a.num_confirmations == 9

def test_tx_merge_extra_and_output_indices(self) -> None:
a: MoneroTx = MoneroTx()
a.hash = "a" * 64
Expand All @@ -814,6 +929,34 @@ def test_tx_merge_extra_and_output_indices(self) -> None:
assert a.extra == [1, 2, 3, 255]
assert a.output_indices == [100, 101]

def test_tx_merge_inputs(self) -> None:
def make_input(tx: MoneroTx, key_image_hex: str) -> MoneroOutput:
key_image: MoneroKeyImage = MoneroKeyImage()
key_image.hex = key_image_hex
tx_input: MoneroOutput = MoneroOutput()
tx_input.tx = tx
tx_input.key_image = key_image
return tx_input

a: MoneroTx = MoneroTx()
a.hash = "a" * 64
a.is_confirmed = True # required: merge() dereferences is_confirmed directly
a.inputs = [make_input(a, "1" * 64)]

b: MoneroTx = MoneroTx()
b.hash = "a" * 64
b.is_confirmed = True
shared_input: MoneroOutput = make_input(b, "1" * 64) # same key image -> merged into a's input
shared_input.amount = 5000
b.inputs = [shared_input, make_input(b, "2" * 64)] # new key image -> appended

a.merge(b)
assert len(a.inputs) == 2
assert a.inputs[0].key_image.hex == "1" * 64 # type: ignore
assert a.inputs[0].amount == 5000 # filled from b's matching input
assert a.inputs[1].key_image.hex == "2" * 64 # type: ignore
assert a.inputs[1].tx is a # appended input is repointed at a

def test_output_merge_ring_output_indices_and_stealth_public_key(self) -> None:
a: MoneroOutput = MoneroOutput()
a.amount = 1000000
Expand Down
46 changes: 45 additions & 1 deletion tests/test_monero_daemon_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
MoneroWalletRpc, MoneroKeyImageSpentStatus, MoneroRpcConnection,
MoneroOutputHistogramEntry, MoneroOutputDistributionEntry, MoneroFeeEstimate,
MoneroMinerData, MoneroDaemonNetworkStats, MoneroAuxiliaryPow,
MoneroAddAuxiliaryPowResult, MoneroGetBlocksByHashResult, MoneroGetBlockHashesResult
MoneroAddAuxiliaryPowResult, MoneroGetBlocksByHashResult, MoneroGetBlockHashesResult,
MoneroOutput
)
from utils import (
TestUtils as Utils, TestContext, BinaryBlockContext, RpcConnectionUtils,
Expand Down Expand Up @@ -1159,6 +1160,40 @@ def test_get_output_indices(self, daemon: MoneroDaemonRpc) -> None:
daemon.get_output_indices("")
assert str(exc_info.value) == "Must provide a transaction hash"

# Can get outputs given a list of output amounts and indices (binary)
@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled")
def test_get_outputs(self, daemon: MoneroDaemonRpc) -> None:
tx_hashes: list[str] = DaemonUtils.get_confirmed_tx_hashes(daemon)
assert len(tx_hashes) > 0, "No confirmed txs found"

# identify each output by amount 0 (RingCT) and its global index
requested: list[MoneroOutput] = []
for tx_hash in tx_hashes:
for index in daemon.get_output_indices(tx_hash):
output: MoneroOutput = MoneroOutput()
output.amount = 0
output.index = index
requested.append(output)

num_requested: int = len(requested)
assert num_requested > 0

fetched: list[MoneroOutput] = daemon.get_outputs(requested)
num_fetched: int = len(fetched)
assert num_fetched == num_requested

for i, output in enumerate(fetched):
logger.debug(f"Testing fetched output: {output.serialize()}")
assert output.index == requested[i].index
assert output.stealth_public_key is not None and len(output.stealth_public_key) == 64
assert output.mask is not None and len(output.mask) == 64
assert output.tx is not None
assert output.tx.block is not None and output.tx.block.height is not None

with pytest.raises(Exception) as exc_info:
daemon.get_outputs([])
assert str(exc_info.value) == "Must provide outputs to fetch"

# Can get network (bandwidth) statistics
@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled")
def test_get_network_stats(self, daemon: MoneroDaemonRpc) -> None:
Expand Down Expand Up @@ -1221,6 +1256,15 @@ def test_flush_cache(self, daemon: MoneroDaemonRpc) -> None:
def test_remove_bootstrap_daemon(self, daemon: MoneroDaemonRpc) -> None:
daemon.remove_bootstrap_daemon()

# Can set the poll period for block notifications
@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled")
def test_set_poll_period_in_ms(self, daemon: MoneroDaemonRpc) -> None:
try:
daemon.set_poll_period_in_ms(1000)
finally:
# restore the configured period so later tests poll at the expected rate
daemon.set_poll_period_in_ms(Utils.DAEMON_POLL_PERIOD_IN_MS)

# Can set the daemon's log level
@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled")
def test_set_log_level(self, daemon: MoneroDaemonRpc) -> None:
Expand Down
Loading
Loading