diff --git a/src/cpp/py_monero.cpp b/src/cpp/py_monero.cpp index 593ea09..976cf43 100644 --- a/src/cpp/py_monero.cpp +++ b/src/cpp/py_monero.cpp @@ -704,6 +704,7 @@ PYBIND11_MODULE(monero, m) { // monero_tx py_monero_tx .def(py::init<>()) + .def_property_readonly_static("DEFAULT_PAYMENT_ID", [](py::object /* self */) { return monero::monero_tx::DEFAULT_PAYMENT_ID; }) .def_readwrite("block", &monero::monero_tx::m_block) .def_readwrite("hash", &monero::monero_tx::m_hash) .def_readwrite("version", &monero::monero_tx::m_version) @@ -983,6 +984,20 @@ PYBIND11_MODULE(monero, m) { .def_readwrite("change_amount", &monero::monero_tx_wallet::m_change_amount) .def_readwrite("num_dummy_outputs", &monero::monero_tx_wallet::m_num_dummy_outputs) .def_readwrite("extra_hex", &monero::monero_tx_wallet::m_extra_hex) + .def("get_incoming_amount", [](monero::monero_tx_wallet& self) { + uint64_t amount = 0; + for (const auto& transfer : self.m_incoming_transfers) { + if (transfer->m_amount != boost::none) + amount += transfer->m_amount.get(); + } + return amount; + }) + .def("get_outgoing_amount", [](monero::monero_tx_wallet& self) { + uint64_t amount = 0; + if (self.m_outgoing_transfer != boost::none && self.m_outgoing_transfer.value()->m_amount != boost::none) + amount = self.m_outgoing_transfer.value()->m_amount.get(); + return amount; + }) .def("get_transfers", [](monero::monero_tx_wallet& self) { MONERO_CATCH_AND_RETHROW(self.get_transfers()); }) @@ -992,6 +1007,16 @@ PYBIND11_MODULE(monero, m) { .def("filter_transfers", [](monero::monero_tx_wallet& self, const monero_transfer_query& query) { MONERO_CATCH_AND_RETHROW(self.filter_transfers(query)); }, py::arg("query")) + .def("get_inputs_wallet", [](monero::monero_tx_wallet& self, const boost::optional& query) { + std::vector> inputs; + for(const auto& i : self.m_inputs) { + auto input = std::dynamic_pointer_cast(i); + if (!input) continue; + if (query == boost::none || query.value().meets_criteria(input.get())) + inputs.push_back(input); + } + return inputs; + }, py::arg("query") = py::none()) .def("get_outputs_wallet", [](monero::monero_tx_wallet& self) { MONERO_CATCH_AND_RETHROW(self.get_outputs_wallet()); }) @@ -2007,13 +2032,13 @@ PYBIND11_MODULE(monero, m) { .def_static("is_valid_language", [](const std::string& language) { MONERO_CATCH_AND_RETHROW(PyMoneroUtils::is_valid_language(language)); }, py::arg("language")) - .def_static("get_blocks_from_txs", [](std::vector> txs) { + .def_static("get_blocks_from_txs", [](const std::vector>& txs) { MONERO_CATCH_AND_RETHROW(PyMoneroUtils::get_blocks_from_txs(txs)); }, py::arg("txs")) - .def_static("get_blocks_from_transfers", [](std::vector> transfers) { + .def_static("get_blocks_from_transfers", [](const std::vector>& transfers) { MONERO_CATCH_AND_RETHROW(PyMoneroUtils::get_blocks_from_transfers(transfers)); }, py::arg("transfers")) - .def_static("get_blocks_from_outputs", [](std::vector> outputs) { + .def_static("get_blocks_from_outputs", [](const std::vector>& outputs) { MONERO_CATCH_AND_RETHROW(PyMoneroUtils::get_blocks_from_outputs(outputs)); }, py::arg("outputs")) .def_static("get_payment_uri", [](const monero::monero_tx_config &config) { diff --git a/src/cpp/wallet/py_monero_wallet_rpc.cpp b/src/cpp/wallet/py_monero_wallet_rpc.cpp index 7513aa0..81138ee 100644 --- a/src/cpp/wallet/py_monero_wallet_rpc.cpp +++ b/src/cpp/wallet/py_monero_wallet_rpc.cpp @@ -680,7 +680,7 @@ std::vector PyMoneroWalletRpc::get_accounts(bool include_subaddr // merge info auto account = &accounts[subaddress->m_account_index.get()]; if (account->m_index != subaddress->m_account_index) throw std::runtime_error("RPC accounts are out of order"); - auto tgt_subaddress = &account->m_subaddresses[subaddress->m_account_index.get()]; + auto tgt_subaddress = &account->m_subaddresses[subaddress->m_index.get()]; if (tgt_subaddress->m_index != subaddress->m_index) throw std::runtime_error("RPC subaddresses are out of order"); if (subaddress->m_balance != boost::none) tgt_subaddress->m_balance = subaddress->m_balance; diff --git a/src/python/monero_tx.pyi b/src/python/monero_tx.pyi index 4f42097..f602d5e 100644 --- a/src/python/monero_tx.pyi +++ b/src/python/monero_tx.pyi @@ -7,6 +7,8 @@ class MoneroTx(SerializableStruct): """ Models a Monero transaction on the blockchain. """ + DEFAULT_PAYMENT_ID: str + """Default tx payment id""" block: MoneroBlock | None """Block including the transaction.""" common_tx_sets: str | None diff --git a/src/python/monero_tx_wallet.pyi b/src/python/monero_tx_wallet.pyi index caf9b51..6080705 100644 --- a/src/python/monero_tx_wallet.pyi +++ b/src/python/monero_tx_wallet.pyi @@ -55,12 +55,18 @@ class MoneroTxWallet(MoneroTx): @typing.overload def get_outputs_wallet(self, query: MoneroOutputQuery) -> list[MoneroOutputWallet]: ... + def get_inputs_wallet(self, query: MoneroOutputQuery | None = None) -> list[MoneroOutputWallet]: + ... @typing.overload def get_transfers(self) -> list[MoneroTransfer]: ... @typing.overload def get_transfers(self, query: MoneroTransferQuery) -> list[MoneroTransfer]: ... + def get_incoming_amount(self) -> int: + ... + def get_outgoing_amount(self) -> int: + ... @typing.overload def merge(self, tgt: MoneroTxWallet) -> None: ... diff --git a/tests/test_monero_connection_manager.py b/tests/test_monero_connection_manager.py index 14329ce..cb55025 100644 --- a/tests/test_monero_connection_manager.py +++ b/tests/test_monero_connection_manager.py @@ -5,7 +5,7 @@ from monero import ( MoneroWallet, MoneroConnectionManager, MoneroRpcConnection, MoneroConnectionPollType ) -from utils import ConnectionChangeCollector, TestUtils as Utils +from utils import ConnectionChangeCollector, TestUtils as Utils, AssertUtils, GenUtils logger: logging.Logger = logging.getLogger("TestMoneroConnectionManager") @@ -14,7 +14,7 @@ class TestMoneroConnectionManager: @pytest.fixture(autouse=True) - def before_each(self, request: pytest.FixtureRequest): + def setup_and_teardown(self, request: pytest.FixtureRequest): logger.info(f"Before {request.node.name}") # type: ignore yield logger.info(f"After {request.node.name}") # type: ignore @@ -55,13 +55,13 @@ def test_connection_manager(self): # test connections and order ordered_connections: list[MoneroRpcConnection] = connection_manager.get_connections() - Utils.assert_true(ordered_connections[0] == wallet_rpcs[4].get_daemon_connection()) - Utils.assert_true(ordered_connections[1] == wallet_rpcs[2].get_daemon_connection()) - Utils.assert_true(ordered_connections[2] == wallet_rpcs[3].get_daemon_connection()) - Utils.assert_true(ordered_connections[3] == wallet_rpcs[0].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[0] == wallet_rpcs[4].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[1] == wallet_rpcs[2].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[2] == wallet_rpcs[3].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[3] == wallet_rpcs[0].get_daemon_connection()) connection = wallet_rpcs[1].get_daemon_connection() assert connection is not None - Utils.assert_equals(ordered_connections[4].uri, connection.uri) + AssertUtils.assert_equals(ordered_connections[4].uri, connection.uri) for connection in ordered_connections: assert connection.is_online() is None @@ -70,35 +70,35 @@ def test_connection_manager(self): connection = wallet_rpcs[0].get_daemon_connection() assert connection is not None assert connection.uri is not None - Utils.assert_true(connection_manager.has_connection(connection.uri)) - Utils.assert_true( + AssertUtils.assert_true(connection_manager.has_connection(connection.uri)) + AssertUtils.assert_true( connection_manager.get_connection_by_uri(connection.uri) == wallet_rpcs[0].get_daemon_connection() ) # test unknown connection num_expected_changes: int = 0 connection_manager.set_connection(ordered_connections[0]) - Utils.assert_equals(None, connection_manager.is_connected()) + AssertUtils.assert_equals(None, connection_manager.is_connected()) num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) # auto connect to the best available connection connection_manager.start_polling(Utils.SYNC_PERIOD_IN_MS) - Utils.wait_for(Utils.AUTO_CONNECT_TIMEOUT_MS) - Utils.assert_true(connection_manager.is_connected()) + GenUtils.wait_for(Utils.AUTO_CONNECT_TIMEOUT_MS) + AssertUtils.assert_true(connection_manager.is_connected()) connection = connection_manager.get_connection() assert connection is not None - Utils.assert_true(connection.is_online()) - Utils.assert_true(connection == wallet_rpcs[4].get_daemon_connection()) + AssertUtils.assert_true(connection.is_online()) + AssertUtils.assert_true(connection == wallet_rpcs[4].get_daemon_connection()) num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) - Utils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == connection) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == connection) connection_manager.set_auto_switch(False) connection_manager.stop_polling() connection_manager.disconnect() num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) - Utils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) is None) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) is None) # start periodically checking connection without auto switch connection_manager.start_polling(Utils.SYNC_PERIOD_IN_MS, False) @@ -106,83 +106,84 @@ def test_connection_manager(self): # connect to the best available connection in order of priority and response time connection = connection_manager.get_best_available_connection() connection_manager.set_connection(connection) - Utils.assert_true(connection == wallet_rpcs[4].get_daemon_connection()) - Utils.assert_true(connection.is_online()) - Utils.assert_true(connection.is_authenticated()) + AssertUtils.assert_true(connection == wallet_rpcs[4].get_daemon_connection()) + AssertUtils.assert_true(connection.is_online()) + AssertUtils.assert_true(connection.is_authenticated()) num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) - Utils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == connection) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == connection) # test connections and order ordered_connections = connection_manager.get_connections() - Utils.assert_true(ordered_connections[0] == wallet_rpcs[4].get_daemon_connection()) - Utils.assert_true(ordered_connections[1] == wallet_rpcs[2].get_daemon_connection()) - Utils.assert_true(ordered_connections[2] == wallet_rpcs[3].get_daemon_connection()) - Utils.assert_true(ordered_connections[3] == wallet_rpcs[0].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[0] == wallet_rpcs[4].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[1] == wallet_rpcs[2].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[2] == wallet_rpcs[3].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[3] == wallet_rpcs[0].get_daemon_connection()) connection = wallet_rpcs[1].get_daemon_connection() assert connection is not None - Utils.assert_equals(ordered_connections[4].uri, connection.uri) + AssertUtils.assert_equals(ordered_connections[4].uri, connection.uri) for orderedConnection in ordered_connections: - Utils.assert_is_none(orderedConnection.is_online()) + AssertUtils.assert_is_none(orderedConnection.is_online()) # check all connections connection_manager.check_connections() # test connection order ordered_connections = connection_manager.get_connections() - Utils.assert_true(ordered_connections[0] == wallet_rpcs[4].get_daemon_connection()) - Utils.assert_true(ordered_connections[1] == wallet_rpcs[0].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[0] == wallet_rpcs[4].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[1] == wallet_rpcs[0].get_daemon_connection()) connection = wallet_rpcs[1].get_daemon_connection() assert connection is not None - Utils.assert_equals(ordered_connections[2].uri, connection.uri) - Utils.assert_true(ordered_connections[3] == wallet_rpcs[2].get_daemon_connection()) - Utils.assert_true(ordered_connections[4] == wallet_rpcs[3].get_daemon_connection()) + AssertUtils.assert_equals(ordered_connections[2].uri, connection.uri) + AssertUtils.assert_true(ordered_connections[3] == wallet_rpcs[2].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[4] == wallet_rpcs[3].get_daemon_connection()) # test online and authentication status for orderedConnection in ordered_connections: is_online = orderedConnection.is_online() is_authenticated = orderedConnection.is_authenticated() if i == 1 or i == 2: - Utils.assert_true(is_online) + AssertUtils.assert_true(is_online) else: - Utils.assert_false(is_online) + AssertUtils.assert_false(is_online) if i == 1: - Utils.assert_true(is_authenticated) + AssertUtils.assert_true(is_authenticated) elif i == 2: - Utils.assert_false(is_authenticated) + AssertUtils.assert_false(is_authenticated) else: - Utils.assert_is_none(is_authenticated) + AssertUtils.assert_is_none(is_authenticated) + i += 1 # test auto switch when disconnected connection_manager.set_auto_switch(True) - Utils.wait_for(Utils.SYNC_PERIOD_IN_MS + 100) - Utils.assert_true(connection_manager.is_connected()) + GenUtils.wait_for(Utils.SYNC_PERIOD_IN_MS + 100) + AssertUtils.assert_true(connection_manager.is_connected()) connection = connection_manager.get_connection() assert connection is not None - Utils.assert_true(connection.is_online()) - Utils.assert_true(connection == wallet_rpcs[0].get_daemon_connection()) + AssertUtils.assert_true(connection.is_online()) + AssertUtils.assert_true(connection == wallet_rpcs[0].get_daemon_connection()) num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) - Utils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == connection) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == connection) # test connection order ordered_connections = connection_manager.get_connections() - Utils.assert_true(ordered_connections[0] == connection) - Utils.assert_true(ordered_connections[0] == wallet_rpcs[0].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[0] == connection) + AssertUtils.assert_true(ordered_connections[0] == wallet_rpcs[0].get_daemon_connection()) connection = wallet_rpcs[1].get_daemon_connection() assert connection is not None - Utils.assert_equals(ordered_connections[1].uri, connection.uri) - Utils.assert_true(ordered_connections[2] == wallet_rpcs[4].get_daemon_connection()) - Utils.assert_true(ordered_connections[3] == wallet_rpcs[2].get_daemon_connection()) - Utils.assert_true(ordered_connections[4] == wallet_rpcs[3].get_daemon_connection()) + AssertUtils.assert_equals(ordered_connections[1].uri, connection.uri) + AssertUtils.assert_true(ordered_connections[2] == wallet_rpcs[4].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[3] == wallet_rpcs[2].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[4] == wallet_rpcs[3].get_daemon_connection()) # connect to specific endpoint without authentication connection = ordered_connections[1] - Utils.assert_false(connection.is_authenticated()) + AssertUtils.assert_false(connection.is_authenticated()) connection_manager.set_connection(connection) - Utils.assert_false(connection_manager.is_connected()) + AssertUtils.assert_false(connection_manager.is_connected()) num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) # connect to specific endpoint with authentication ordered_connections[1].set_credentials("rpc_user", "abc123") @@ -191,44 +192,44 @@ def test_connection_manager(self): assert connection is not None connection = wallet_rpcs[1].get_daemon_connection() assert connection is not None - Utils.assert_equals(connection.uri, connection.uri) - Utils.assert_true(connection.is_online()) - Utils.assert_true(connection.is_authenticated()) + AssertUtils.assert_equals(connection.uri, connection.uri) + AssertUtils.assert_true(connection.is_online()) + AssertUtils.assert_true(connection.is_authenticated()) num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) - Utils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == connection) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == connection) # test connection order ordered_connections = connection_manager.get_connections() - Utils.assert_true(ordered_connections[0] == connection_manager.get_connection()) + AssertUtils.assert_true(ordered_connections[0] == connection_manager.get_connection()) connection = wallet_rpcs[1].get_daemon_connection() assert connection is not None - Utils.assert_equals(ordered_connections[0].uri, connection.uri) - Utils.assert_true(ordered_connections[1] == wallet_rpcs[0].get_daemon_connection()) - Utils.assert_true(ordered_connections[2] == wallet_rpcs[4].get_daemon_connection()) - Utils.assert_true(ordered_connections[3] == wallet_rpcs[2].get_daemon_connection()) - Utils.assert_true(ordered_connections[4] == wallet_rpcs[3].get_daemon_connection()) + AssertUtils.assert_equals(ordered_connections[0].uri, connection.uri) + AssertUtils.assert_true(ordered_connections[1] == wallet_rpcs[0].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[2] == wallet_rpcs[4].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[3] == wallet_rpcs[2].get_daemon_connection()) + AssertUtils.assert_true(ordered_connections[4] == wallet_rpcs[3].get_daemon_connection()) first: bool = True for orderedConnection in ordered_connections: if i <= 1: - Utils.assert_true(orderedConnection.is_online() if first else not orderedConnection.is_online() ) + AssertUtils.assert_true(orderedConnection.is_online() if first else not orderedConnection.is_online() ) - Utils.assert_false(ordered_connections[4].is_online()) + AssertUtils.assert_false(ordered_connections[4].is_online()) # set connection to existing uri connection = wallet_rpcs[0].get_daemon_connection() assert connection is not None connection_manager.set_connection(connection.uri) - Utils.assert_true(connection_manager.is_connected()) - Utils.assert_true(wallet_rpcs[0].get_daemon_connection() == connection_manager.get_connection()) + AssertUtils.assert_true(connection_manager.is_connected()) + AssertUtils.assert_true(wallet_rpcs[0].get_daemon_connection() == connection_manager.get_connection()) connection = connection_manager.get_connection() assert connection is not None - Utils.assert_equals(Utils.WALLET_RPC_USERNAME, connection.username) - Utils.assert_equals(Utils.WALLET_RPC_PASSWORD, connection.password) + AssertUtils.assert_equals(Utils.WALLET_RPC_USERNAME, connection.username) + AssertUtils.assert_equals(Utils.WALLET_RPC_PASSWORD, connection.password) num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) - Utils.assert_equals( + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_equals( listener.changed_connections.get(listener.changed_connections.size() - 1), wallet_rpcs[0].get_daemon_connection() ) @@ -239,24 +240,24 @@ def test_connection_manager(self): connection_manager.set_connection(uri) connection = connection_manager.get_connection() assert connection is not None - Utils.assert_equals(uri, connection.uri) + AssertUtils.assert_equals(uri, connection.uri) num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) connection = listener.changed_connections.get(listener.changed_connections.size() -1) assert connection is not None - Utils.assert_equals(uri, connection.uri) + AssertUtils.assert_equals(uri, connection.uri) # set connection to empty string connection_manager.set_connection("") - Utils.assert_equals(None, connection_manager.get_connection()) + AssertUtils.assert_equals(None, connection_manager.get_connection()) num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) # check all connections and test auto switch connection_manager.check_connections() num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) - Utils.assert_true(connection_manager.is_connected()) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_true(connection_manager.is_connected()) # remove current connection and test auto switch connection = connection_manager.get_connection() @@ -264,51 +265,51 @@ def test_connection_manager(self): assert connection.uri is not None connection_manager.remove_connection(connection.uri) num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) - Utils.assert_false(connection_manager.is_connected()) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_false(connection_manager.is_connected()) connection_manager.check_connections() num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) - Utils.assert_true(connection_manager.is_connected()) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_true(connection_manager.is_connected()) # test polling current connection connection_manager.set_connection(None) - Utils.assert_false(connection_manager.is_connected()) + AssertUtils.assert_false(connection_manager.is_connected()) num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) connection_manager.start_polling( period_ms=Utils.SYNC_PERIOD_IN_MS, poll_type=MoneroConnectionPollType.CURRENT ) - Utils.wait_for(Utils.AUTO_CONNECT_TIMEOUT_MS) - Utils.assert_true(connection_manager.is_connected()) + GenUtils.wait_for(Utils.AUTO_CONNECT_TIMEOUT_MS) + AssertUtils.assert_true(connection_manager.is_connected()) num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) # test polling all connections connection_manager.set_connection(None) num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) connection_manager.start_polling(period_ms=Utils.SYNC_PERIOD_IN_MS, poll_type=MoneroConnectionPollType.ALL) - Utils.wait_for(Utils.AUTO_CONNECT_TIMEOUT_MS) - Utils.assert_true(connection_manager.is_connected()) + GenUtils.wait_for(Utils.AUTO_CONNECT_TIMEOUT_MS) + AssertUtils.assert_true(connection_manager.is_connected()) num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) # shut down all connections connection = connection_manager.get_connection() assert connection is not None - Utils.wait_for(Utils.SYNC_PERIOD_IN_MS + 100) - Utils.assert_false(connection.is_online()) + GenUtils.wait_for(Utils.SYNC_PERIOD_IN_MS + 100) + AssertUtils.assert_false(connection.is_online()) num_expected_changes += 1 - Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) - Utils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == connection) + AssertUtils.assert_equals(num_expected_changes, listener.changed_connections.size()) + AssertUtils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == connection) # reset connection_manager.reset() - Utils.assert_equals(0, len(connection_manager.get_connections())) - Utils.assert_equals(None, connection_manager.get_connection()) + AssertUtils.assert_equals(0, len(connection_manager.get_connections())) + AssertUtils.assert_equals(None, connection_manager.get_connection()) finally: # stop connection manager diff --git a/tests/test_monero_daemon_interface.py b/tests/test_monero_daemon_interface.py index a65855c..befd084 100644 --- a/tests/test_monero_daemon_interface.py +++ b/tests/test_monero_daemon_interface.py @@ -5,7 +5,7 @@ from monero import ( MoneroDaemon, MoneroBan ) -from utils import TestUtils as Utils +from utils import AssertUtils logger: logging.Logger = logging.getLogger("TestMoneroDaemonInterface") @@ -22,10 +22,10 @@ def _get_daemon(self) -> MoneroDaemon: return self._daemon @pytest.fixture(autouse=True) - def before_each(self, request: pytest.FixtureRequest): - logger.info(f"Before test {request.node.name}") # type: ignore + def setup_and_teardown(self, request: pytest.FixtureRequest): + logger.info(f"Before {request.node.name}") # type: ignore yield - logger.info(f"After test {request.node.name}") # type: ignore + logger.info(f"After {request.node.name}") # type: ignore #region Tests @@ -36,71 +36,71 @@ def test_info(self) -> None: try: daemon.get_version() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.is_trusted() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_fee_estimate() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_info() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_hard_fork_info except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_alt_block_hashes() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_alt_chains() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test node sync info def test_sync_info(self) -> None: daemon = self._get_daemon() - + try: daemon.get_height() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_last_block_header() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_sync_info() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.wait_for_next_block_header() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_key_image_spent_status("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_key_image_spent_statuses([]) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test blockchain def test_blocks(self) -> None: @@ -109,72 +109,72 @@ def test_blocks(self) -> None: try: daemon.get_block_hash(1) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_block_template("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_block_header_by_hash("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_block_header_by_height(1) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_block_headers_by_range(1, 100) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_block_by_hash("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_blocks_by_hash([], 0, False) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_block_by_height(1) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_blocks_by_height([1]) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_blocks_by_range(1, 100) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_blocks_by_range_chunked(1, 100) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_block_hashes([], 0) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.submit_block("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.submit_blocks([]) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test txs def test_txs(self) -> None: @@ -183,42 +183,42 @@ def test_txs(self) -> None: try: daemon.get_tx("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_txs([]) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_tx_hex("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_tx_hexes([]) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_miner_tx_sum(0, 100) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.submit_tx_hex("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.relay_tx_by_hash("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.relay_txs_by_hash([]) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test tx pool def test_tx_pool(self) -> None: @@ -227,37 +227,37 @@ def test_tx_pool(self) -> None: try: daemon.get_tx_pool() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_tx_pool_hashes() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_tx_pool_backlog() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_tx_pool_stats() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.flush_tx_pool() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.flush_tx_pool("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.flush_tx_pool([""]) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test outputs def test_outputs(self) -> None: @@ -266,17 +266,17 @@ def test_outputs(self) -> None: try: daemon.get_outputs([]) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_output_histogram([], 0, 100, False, 1000) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_output_distribution([], False, 0, 1) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test network and peers def test_network(self) -> None: @@ -285,37 +285,37 @@ def test_network(self) -> None: try: daemon.get_peers() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_known_peers() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.set_outgoing_peer_limit(1000) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.set_incoming_peer_limit(10000) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_peer_bans() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.set_peer_bans([]) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.set_peer_ban(MoneroBan()) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test mining def test_mining(self) -> None: @@ -324,17 +324,17 @@ def test_mining(self) -> None: try: daemon.start_mining("", 1, False, False) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.stop_mining() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.get_mining_status() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test other utilities def test_utilities(self) -> None: @@ -343,26 +343,26 @@ def test_utilities(self) -> None: try: daemon.prune_blockchain(False) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.check_for_update() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.download_update() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.download_update("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: daemon.wait_for_next_block_header() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) #endregion diff --git a/tests/test_monero_daemon_rpc.py b/tests/test_monero_daemon_rpc.py index 913ec26..73549eb 100644 --- a/tests/test_monero_daemon_rpc.py +++ b/tests/test_monero_daemon_rpc.py @@ -14,7 +14,8 @@ from utils import ( TestUtils as Utils, TestContext, BinaryBlockContext, MiningUtils, - OsUtils + OsUtils, AssertUtils, TxUtils, + BlockUtils, GenUtils, DaemonUtils ) logger: logging.Logger = logging.getLogger("TestMoneroDaemonRpc") @@ -30,13 +31,11 @@ class TestMoneroDaemonRpc: @pytest.fixture(scope="class", autouse=True) def before_all(self): - # TODO setup test environment for windows if not OsUtils.is_windows(): MiningUtils.wait_until_blockchain_ready() - MiningUtils.try_stop_mining() @pytest.fixture(autouse=True) - def before_each(self, request: pytest.FixtureRequest): + def setup_and_teardown(self, request: pytest.FixtureRequest): logger.info(f"Before {request.node.name}") # type: ignore yield logger.info(f"After {request.node.name}") # type: ignore @@ -50,8 +49,8 @@ def before_each(self, request: pytest.FixtureRequest): def test_get_version(self): version: MoneroVersion = self._daemon.get_version() assert version.number is not None - Utils.assert_true(version.number > 0) - Utils.assert_not_none(version.is_release) + AssertUtils.assert_true(version.number > 0) + AssertUtils.assert_not_none(version.is_release) # Can indicate if it's trusted @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -62,7 +61,7 @@ def test_is_trusted(self): @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_height(self): height = self._daemon.get_height() - Utils.assert_true(height > 0, "Height must be greater than 0") + AssertUtils.assert_true(height > 0, "Height must be greater than 0") # Can get a block hash by height @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -70,20 +69,20 @@ def test_get_block_id_by_height(self): last_header: MoneroBlockHeader = self._daemon.get_last_block_header() assert last_header.height is not None hash_str: str = self._daemon.get_block_hash(last_header.height) - Utils.assert_not_none(hash_str) - Utils.assert_equals(64, len(hash_str)) + AssertUtils.assert_not_none(hash_str) + AssertUtils.assert_equals(64, len(hash_str)) # Can get a block template @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_block_template(self): template: MoneroBlockTemplate = self._daemon.get_block_template(Utils.ADDRESS, 2) - Utils.test_block_template(template) + DaemonUtils.test_block_template(template) # Can get the last block's header @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_last_block_header(self): last_header: MoneroBlockHeader = self._daemon.get_last_block_header() - Utils.test_block_header(last_header, True) + BlockUtils.test_block_header(last_header, True) # Can get a block header by hash @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -93,14 +92,14 @@ def test_get_block_header_by_hash(self): assert last_header.height is not None hash_str: str = self._daemon.get_block_hash(last_header.height) header: MoneroBlockHeader = self._daemon.get_block_header_by_hash(hash_str) - Utils.test_block_header(header, True) - Utils.assert_equals(last_header, header) + BlockUtils.test_block_header(header, True) + AssertUtils.assert_equals(last_header, header) # retrieve by hash of previous to last block hash_str = self._daemon.get_block_hash(last_header.height - 1) header = self._daemon.get_block_header_by_hash(hash_str) - Utils.test_block_header(header, True) - Utils.assert_equals(last_header.height - 1, header.height) + BlockUtils.test_block_header(header, True) + AssertUtils.assert_equals(last_header.height - 1, header.height) # Can get a block header by height @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -109,13 +108,13 @@ def test_get_block_header_by_height(self): last_header: MoneroBlockHeader = self._daemon.get_last_block_header() assert last_header.height is not None header: MoneroBlockHeader = self._daemon.get_block_header_by_height(last_header.height) - Utils.test_block_header(header, True) - Utils.assert_equals(last_header, header) + BlockUtils.test_block_header(header, True) + AssertUtils.assert_equals(last_header, header) # retrieve by height of previous to last block header = self._daemon.get_block_header_by_height(last_header.height - 1) - Utils.test_block_header(header, True) - Utils.assert_equals(last_header.height - 1, header.height) + BlockUtils.test_block_header(header, True) + AssertUtils.assert_equals(last_header.height - 1, header.height) # Can get block headers by range # TODO: test start with no end, vice versa, inclusivity @@ -132,12 +131,12 @@ def test_get_block_headers_by_range(self): headers: list[MoneroBlockHeader] = self._daemon.get_block_headers_by_range(start_height, end_height) # test headers - Utils.assert_equals(num_blocks, len(headers)) + AssertUtils.assert_equals(num_blocks, len(headers)) i: int = 0 while i < num_blocks: header: MoneroBlockHeader = headers[i] - Utils.assert_equals(start_height + i, header.height) - Utils.test_block_header(header, True) + AssertUtils.assert_equals(start_height + i, header.height) + BlockUtils.test_block_header(header, True) i += 1 # Can get a block by hash @@ -155,15 +154,15 @@ def test_get_block_by_hash(self): hash_str: str = self._daemon.get_block_hash(last_header.height) block: MoneroBlock = self._daemon.get_block_by_hash(hash_str) assert block.height is not None - Utils.test_block(block, ctx) - Utils.assert_equals(self._daemon.get_block_by_height(block.height), block) + BlockUtils.test_block(block, ctx) + AssertUtils.assert_equals(self._daemon.get_block_by_height(block.height), block) assert len(block.txs) == 0, f"No block tx expected, found: {len(block.txs)}" # retrieve by hash of previous to last block hash_str = self._daemon.get_block_hash(last_header.height - 1) block = self._daemon.get_block_by_hash(hash_str) - Utils.test_block(block, ctx) - Utils.assert_equals(self._daemon.get_block_by_height(last_header.height - 1), block) + BlockUtils.test_block(block, ctx) + AssertUtils.assert_equals(self._daemon.get_block_by_height(last_header.height - 1), block) assert len(block.txs) == 0, f"No block tx expected, found: {len(block.txs)}" # Can get blocks by hash which includes transactions (binary) @@ -186,13 +185,13 @@ def test_get_block_by_height(self): assert last_header.height is not None block: MoneroBlock = self._daemon.get_block_by_height(last_header.height) assert block.height is not None - Utils.test_block(block, ctx) - Utils.assert_equals(self._daemon.get_block_by_height(block.height), block) + BlockUtils.test_block(block, ctx) + AssertUtils.assert_equals(self._daemon.get_block_by_height(block.height), block) # retrieve by height of previous to last block block = self._daemon.get_block_by_height(last_header.height - 1) - Utils.test_block(block, ctx) - Utils.assert_equals(last_header.height - 1, block.height) + BlockUtils.test_block(block, ctx) + AssertUtils.assert_equals(last_header.height - 1, block.height) # Can get blocks by height which includes transactions (binary) #@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -221,18 +220,18 @@ def test_get_blocks_by_height_binary(self): # test blocks tx_found: bool = False - Utils.assert_equals(num_blocks, len(blocks)) + AssertUtils.assert_equals(num_blocks, len(blocks)) i = 0 while i < len(heights): block: MoneroBlock = blocks[i] if len(block.txs) > 0: tx_found = True - Utils.test_block(block, self.BINARY_BLOCK_CTX) - Utils.assert_equals(block.height, heights[i]) + BlockUtils.test_block(block, self.BINARY_BLOCK_CTX) + AssertUtils.assert_equals(block.height, heights[i]) i += 1 - Utils.assert_true(tx_found, "No transactions found to test") + AssertUtils.assert_true(tx_found, "No transactions found to test") # Can get blocks by range in a single request @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -240,41 +239,41 @@ def test_get_blocks_by_range(self): # get height range num_blocks = 100 num_blocks_ago = 102 - Utils.assert_true(num_blocks > 0) - Utils.assert_true(num_blocks_ago >= num_blocks) + AssertUtils.assert_true(num_blocks > 0) + AssertUtils.assert_true(num_blocks_ago >= num_blocks) height = self._daemon.get_height() - Utils.assert_true(height - num_blocks_ago + num_blocks - 1 < height) + AssertUtils.assert_true(height - num_blocks_ago + num_blocks - 1 < height) start_height = height - num_blocks_ago end_height = height - num_blocks_ago + num_blocks - 1 # test known start and end heights - Utils.test_get_blocks_range(self._daemon, start_height, end_height, height, False, self.BINARY_BLOCK_CTX) + BlockUtils.test_get_blocks_range(self._daemon, start_height, end_height, height, False, self.BINARY_BLOCK_CTX) # test unspecified start - Utils.test_get_blocks_range(self._daemon, None, num_blocks - 1, height, False, self.BINARY_BLOCK_CTX) + BlockUtils.test_get_blocks_range(self._daemon, None, num_blocks - 1, height, False, self.BINARY_BLOCK_CTX) # test unspecified end - Utils.test_get_blocks_range(self._daemon, height - num_blocks - 1, None, height, False, self.BINARY_BLOCK_CTX) + BlockUtils.test_get_blocks_range(self._daemon, height - num_blocks - 1, None, height, False, self.BINARY_BLOCK_CTX) # Can get blocks by range using chunked requests @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_blocks_by_range_chunked(self): # get long height range num_blocks = min(self._daemon.get_height() - 2, 1440) # test up to ~2 days of blocks - Utils.assert_true(num_blocks > 0) + AssertUtils.assert_true(num_blocks > 0) height = self._daemon.get_height() - Utils.assert_true(height - num_blocks - 1 < height) + AssertUtils.assert_true(height - num_blocks - 1 < height) start_height = height - num_blocks end_height = height - 1 # test known start and end heights - Utils.test_get_blocks_range(self._daemon, start_height, end_height, height, True, self.BINARY_BLOCK_CTX) + BlockUtils.test_get_blocks_range(self._daemon, start_height, end_height, height, True, self.BINARY_BLOCK_CTX) # test unspecified start - Utils.test_get_blocks_range(self._daemon, None, num_blocks - 1, height, True, self.BINARY_BLOCK_CTX) + BlockUtils.test_get_blocks_range(self._daemon, None, num_blocks - 1, height, True, self.BINARY_BLOCK_CTX) # test unspecified end - Utils.test_get_blocks_range(self._daemon, end_height - num_blocks - 1, None, height, True, self.BINARY_BLOCK_CTX) + BlockUtils.test_get_blocks_range(self._daemon, end_height - num_blocks - 1, None, height, True, self.BINARY_BLOCK_CTX) # Can get block hashes (binary) #@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -287,7 +286,7 @@ def test_get_block_ids_binary(self) -> None: @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_tx_by_hash(self) -> None: # fetch tx hashses to test - tx_hashes = Utils.get_confirmed_tx_hashes(self._daemon) + tx_hashes = TxUtils.get_confirmed_tx_hashes(self._daemon) # context for creating txs ctx = TestContext() @@ -298,27 +297,27 @@ def test_get_tx_by_hash(self) -> None: # fetch each tx by hash without pruning for tx_hash in tx_hashes: tx = self._daemon.get_tx(tx_hash) - Utils.test_tx(tx, ctx) + TxUtils.test_tx(tx, ctx) # fetch each tx by hash with pruning for tx_hash in tx_hashes: tx = self._daemon.get_tx(tx_hash, True) ctx.is_pruned = True - Utils.test_tx(tx, ctx) + TxUtils.test_tx(tx, ctx) # fetch invalid hash try: self._daemon.get_tx("invalid tx hash") raise Exception("fail") except Exception as e: - Utils.assert_equals("Invalid transaction hash", str(e)) + AssertUtils.assert_equals("Invalid transaction hash", str(e)) # Can get transactions by hashes with and without pruning #@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @pytest.mark.skip(reason="TODO fund wallet") def test_get_txs_by_hashes(self) -> None: # fetch tx hashses to test - tx_hashes = Utils.get_confirmed_tx_hashes(self._daemon) + tx_hashes = TxUtils.get_confirmed_tx_hashes(self._daemon) assert len(tx_hashes) > 0, "No tx hashes found" # context for creating txs @@ -331,19 +330,19 @@ def test_get_txs_by_hashes(self) -> None: txs = self._daemon.get_txs(tx_hashes) assert len(txs) == len(tx_hashes), f"Expected len(txs) == len(tx_hashes), got {len(txs)} == {len(tx_hashes)}" for tx in txs: - Utils.test_tx(tx, ctx) + TxUtils.test_tx(tx, ctx) # fetch each tx by hash with pruning txs = self._daemon.get_txs(tx_hashes, True) ctx.is_pruned = True assert len(txs) == len(tx_hashes), f"Expected len(txs) == len(tx_hashes), got {len(txs)} == {len(tx_hashes)}" for tx in txs: - Utils.test_tx(tx, ctx) + TxUtils.test_tx(tx, ctx) # fetch missing hash dest = MoneroDestination() dest.address = self._wallet.get_primary_address() - dest.amount = Utils.MAX_FEE + dest.amount = TxUtils.MAX_FEE config = MoneroTxConfig() config.account_index = 0 config.destinations.append(dest) @@ -360,7 +359,7 @@ def test_get_txs_by_hashes(self) -> None: self._daemon.get_txs(["invalid tx hash"]) raise Exception("fail") except Exception as e: - Utils.assert_equals("Invalid transaction hash", str(e)) + AssertUtils.assert_equals("Invalid transaction hash", str(e)) # Can get transaction pool statistics #@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -375,18 +374,18 @@ def test_get_tx_pool_statistics(self): i = 1 while 1 < 3: # submit tx hex - tx: MoneroTx = Utils.get_unrelayed_tx(wallet, i) + tx: MoneroTx = TxUtils.get_unrelayed_tx(wallet, i) assert tx.full_hex is not None result: MoneroSubmitTxResult = self._daemon.submit_tx_hex(tx.full_hex, True) - Utils.assert_true(result.is_good, json.dumps(result)) + AssertUtils.assert_true(result.is_good, json.dumps(result)) assert tx.hash is not None tx_ids.append(tx.hash) # get tx pool stats stats: MoneroTxPoolStats = self._daemon.get_tx_pool_stats() assert stats.num_txs is not None - Utils.assert_true(stats.num_txs > i - 1) - Utils.test_tx_pool_stats(stats) + AssertUtils.assert_true(stats.num_txs > i - 1) + DaemonUtils.test_tx_pool_stats(stats) i += 1 except Exception as e: @@ -398,110 +397,110 @@ def test_get_tx_pool_statistics(self): @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_miner_tx_sum(self) -> None: tx_sum = self._daemon.get_miner_tx_sum(0, min(5000, self._daemon.get_height())) - Utils.test_miner_tx_sum(tx_sum) + DaemonUtils.test_miner_tx_sum(tx_sum) # Can get fee estimate @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_fee_estimate(self) -> None: fee_estimate = self._daemon.get_fee_estimate() - Utils.test_unsigned_big_integer(fee_estimate.fee, True) + GenUtils.test_unsigned_big_integer(fee_estimate.fee, True) assert len(fee_estimate.fees) == 4, "Exptected 4 fees" for fee in fee_estimate.fees: - Utils.test_unsigned_big_integer(fee, True) - Utils.test_unsigned_big_integer(fee_estimate.quantization_mask, True) + GenUtils.test_unsigned_big_integer(fee, True) + GenUtils.test_unsigned_big_integer(fee_estimate.quantization_mask, True) # Can get general information @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_general_information(self): info: MoneroDaemonInfo = self._daemon.get_info() - Utils.test_info(info) + DaemonUtils.test_info(info) # Can get sync information @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_sync_information(self): sync_info: MoneroDaemonSyncInfo = self._daemon.get_sync_info() - Utils.test_sync_info(sync_info) + DaemonUtils.test_sync_info(sync_info) # Can get hard fork information @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_hard_fork_information(self): hard_fork_info: MoneroHardForkInfo = self._daemon.get_hard_fork_info() - Utils.test_hard_fork_info(hard_fork_info) + DaemonUtils.test_hard_fork_info(hard_fork_info) # Can get alternative chains @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_alternative_chains(self): alt_chains: list[MoneroAltChain] = self._daemon.get_alt_chains() for altChain in alt_chains: - Utils.test_alt_chain(altChain) + DaemonUtils.test_alt_chain(altChain) # Can get alternative block hashes @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_alternative_block_ids(self): alt_block_ids: list[str] = self._daemon.get_alt_block_hashes() for altBlockId in alt_block_ids: - Utils.assert_not_none(altBlockId) - Utils.assert_equals(64, len(altBlockId)) # TODO: common validation + AssertUtils.assert_not_none(altBlockId) + AssertUtils.assert_equals(64, len(altBlockId)) # TODO: common validation # Can get, set, and reset a download bandwidth limit @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_set_download_bandwidth(self): init_val: int = self._daemon.get_download_limit() - Utils.assert_true(init_val > 0) + AssertUtils.assert_true(init_val > 0) set_val: int = init_val * 2 self._daemon.set_download_limit(set_val) - Utils.assert_equals(set_val, self._daemon.get_download_limit()) + AssertUtils.assert_equals(set_val, self._daemon.get_download_limit()) reset_val: int = self._daemon.reset_download_limit() - Utils.assert_equals(init_val, reset_val) + AssertUtils.assert_equals(init_val, reset_val) # test invalid limits try: self._daemon.set_download_limit(0) raise Exception("Should have thrown error on invalid input") except Exception as e: - Utils.assert_equals("Download limit must be an integer greater than 0", str(e)) + AssertUtils.assert_equals("Download limit must be an integer greater than 0", str(e)) - Utils.assert_equals(self._daemon.get_download_limit(), init_val) + AssertUtils.assert_equals(self._daemon.get_download_limit(), init_val) # Can get, set, and reset an upload bandwidth limit @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_set_upload_bandwidth(self): init_val: int = self._daemon.get_upload_limit() - Utils.assert_true(init_val > 0) + AssertUtils.assert_true(init_val > 0) set_val: int = init_val * 2 self._daemon.set_upload_limit(set_val) - Utils.assert_equals(set_val, self._daemon.get_upload_limit()) + AssertUtils.assert_equals(set_val, self._daemon.get_upload_limit()) reset_val: int = self._daemon.reset_upload_limit() - Utils.assert_equals(init_val, reset_val) + AssertUtils.assert_equals(init_val, reset_val) # test invalid limits try: self._daemon.set_upload_limit(0) raise Exception("Should have thrown error on invalid input") except Exception as e: - Utils.assert_equals("Upload limit must be an integer greater than 0", str(e)) + AssertUtils.assert_equals("Upload limit must be an integer greater than 0", str(e)) - Utils.assert_equals(init_val, self._daemon.get_upload_limit()) + AssertUtils.assert_equals(init_val, self._daemon.get_upload_limit()) # Can get peers with active incoming or outgoing connections @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_peers(self): peers: list[MoneroPeer] = self._daemon.get_peers() - Utils.assert_false(len(peers) == 0, "Daemon has no incoming or outgoing peers to test") + AssertUtils.assert_false(len(peers) == 0, "Daemon has no incoming or outgoing peers to test") for peer in peers: - Utils.test_peer(peer) + DaemonUtils.test_peer(peer) # Can get all known peers which may be online or offline @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_known_peers(self): peers: list[MoneroPeer] = self._daemon.get_known_peers() if Utils.REGTEST: - Utils.assert_true(len(peers) == 0, "Regtest daemon should not have known peers to test") + AssertUtils.assert_true(len(peers) == 0, "Regtest daemon should not have known peers to test") else: - Utils.assert_false(len(peers) == 0, "Daemon has no known peers to test") + AssertUtils.assert_false(len(peers) == 0, "Daemon has no known peers to test") for peer in peers: - Utils.test_known_peer(peer, False) + DaemonUtils.test_known_peer(peer, False) # Can limit the number of outgoing peers @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -535,10 +534,10 @@ def test_block_listener(self): # wait for next block notification header: MoneroBlockHeader = self._daemon.wait_for_next_block_header() self._daemon.remove_listener(listener) # unregister listener so daemon does not keep polling - Utils.test_block_header(header, True) + BlockUtils.test_block_header(header, True) # test that listener was called with equivalent header - Utils.assert_equals(header, listener.last_header) + AssertUtils.assert_equals(header, listener.last_header) except Exception as e: raise e finally: @@ -563,7 +562,7 @@ def test_ban_peer(self): bans = self._daemon.get_peer_bans() found: bool = False for peer_ban in bans: - Utils.test_ban(peer_ban) + DaemonUtils.test_ban(peer_ban) if host == peer_ban.host: found = True @@ -593,7 +592,7 @@ def test_ban_peers(self): found1: bool = False found2: bool = False for aBan in bans: - Utils.test_ban(aBan) + DaemonUtils.test_ban(aBan) if addr1 == aBan.host: found1 = True if addr2 == aBan.host: @@ -632,11 +631,11 @@ def test_get_mining_status(self): # test status without mining status: MoneroMiningStatus = self._daemon.get_mining_status() - Utils.assert_equals(False, status.is_active) - Utils.assert_is_none(status.address, f"Mining address is not None: {status.address}") - Utils.assert_equals(0, status.speed) - Utils.assert_equals(0, status.num_threads) - Utils.assert_is_none(status.is_background) + AssertUtils.assert_equals(False, status.is_active) + AssertUtils.assert_is_none(status.address, f"Mining address is not None: {status.address}") + AssertUtils.assert_equals(0, status.speed) + AssertUtils.assert_equals(0, status.num_threads) + AssertUtils.assert_is_none(status.is_background) # test status with mining address: str = self._wallet.get_primary_address() @@ -645,11 +644,11 @@ def test_get_mining_status(self): self._daemon.start_mining(address, thread_count, is_background, True) status = self._daemon.get_mining_status() assert status.speed is not None - Utils.assert_equals(True, status.is_active) - Utils.assert_equals(address, status.address) - Utils.assert_true(status.speed >= 0) - Utils.assert_equals(thread_count, status.num_threads) - Utils.assert_equals(is_background, status.is_background) + AssertUtils.assert_equals(True, status.is_active) + AssertUtils.assert_equals(address, status.address) + AssertUtils.assert_true(status.speed >= 0) + AssertUtils.assert_equals(thread_count, status.num_threads) + AssertUtils.assert_equals(is_background, status.is_background) except Exception as e: raise e finally: @@ -674,8 +673,8 @@ def test_submit_mined_block(self): self._daemon.submit_block(template.block_template_blob) raise Exception("Should have thrown error") except Exception as e: - # Utils.assert_equals(-7, (int) e.getCode()) - Utils.assert_equals("Block not accepted", str(e)) + # AssertUtils.assert_equals(-7, (int) e.getCode()) + AssertUtils.assert_equals("Block not accepted", str(e)) # Can prune the blockchain @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -684,16 +683,16 @@ def test_prune_blockchain(self): if result.is_pruned: assert result.pruning_seed is not None - Utils.assert_true(result.pruning_seed > 0) + AssertUtils.assert_true(result.pruning_seed > 0) else: - Utils.assert_equals(0, result.pruning_seed) + AssertUtils.assert_equals(0, result.pruning_seed) # Can check for an update @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @pytest.mark.flaky(reruns=5, reruns_delay=5) def test_check_for_update(self): result: MoneroDaemonUpdateCheckResult = self._daemon.check_for_update() - Utils.test_update_check_result(result) + DaemonUtils.test_update_check_result(result) # Can download an update @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -701,12 +700,12 @@ def test_check_for_update(self): def test_download_update(self): # download to default path result: MoneroDaemonUpdateDownloadResult = self._daemon.download_update() - Utils.test_update_download_result(result, None) + DaemonUtils.test_update_download_result(result, None) # download to defined path path: str = "test_download_" + str(time.time()) + ".tar.bz2" result = self._daemon.download_update(path) - Utils.test_update_download_result(result, path) + DaemonUtils.test_update_download_result(result, path) # test invalid path if result.is_update_available: @@ -714,8 +713,8 @@ def test_download_update(self): self._daemon.download_update("./ohhai/there") raise Exception("Should have thrown error") except Exception as e: - Utils.assert_not_equals(str(e), "Should have thrown error") - # Utils.assert_equals(500, (int) e.getCode()) # TODO monerod: this causes a 500 in daemon rpc + AssertUtils.assert_not_equals(str(e), "Should have thrown error") + # AssertUtils.assert_equals(500, (int) e.getCode()) # TODO monerod: this causes a 500 in daemon rpc # Can be stopped #@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -732,6 +731,6 @@ def test_stop(self): self._daemon.get_height() raise Exception("Should have thrown error") except Exception as e: - Utils.assert_not_equals("Should have thrown error", str(e)) + AssertUtils.assert_not_equals("Should have thrown error", str(e)) #endregion diff --git a/tests/test_monero_rpc_connection.py b/tests/test_monero_rpc_connection.py index 2f31043..be9b89d 100644 --- a/tests/test_monero_rpc_connection.py +++ b/tests/test_monero_rpc_connection.py @@ -2,7 +2,7 @@ import logging from monero import MoneroRpcConnection -from utils import TestUtils as Utils, OsUtils +from utils import TestUtils as Utils, OsUtils, DaemonUtils logger: logging.Logger = logging.getLogger("TestMoneroRpcConnection") @@ -11,7 +11,7 @@ class TestMoneroRpcConnection: @pytest.fixture(autouse=True) - def before_each(self, request: pytest.FixtureRequest): + def setup_and_teardown(self, request: pytest.FixtureRequest): logger.info(f"Before {request.node.name}") # type: ignore yield logger.info(f"After {request.node.name}") # type: ignore @@ -19,16 +19,16 @@ def before_each(self, request: pytest.FixtureRequest): # Test monerod rpc connection def test_node_rpc_connection(self): connection = MoneroRpcConnection(Utils.DAEMON_RPC_URI, Utils.DAEMON_RPC_USERNAME, Utils.DAEMON_RPC_PASSWORD) - Utils.test_rpc_connection(connection, Utils.DAEMON_RPC_URI) + DaemonUtils.test_rpc_connection(connection, Utils.DAEMON_RPC_URI) # Test wallet rpc connection @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_wallet_rpc_connection(self): connection = MoneroRpcConnection(Utils.WALLET_RPC_URI, Utils.WALLET_RPC_USERNAME, Utils.WALLET_RPC_PASSWORD) - Utils.test_rpc_connection(connection, Utils.WALLET_RPC_URI) + DaemonUtils.test_rpc_connection(connection, Utils.WALLET_RPC_URI) # Test invalid connection @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_invalid_connection(self): connection = MoneroRpcConnection(Utils.OFFLINE_SERVER_URI) - Utils.test_rpc_connection(connection, Utils.OFFLINE_SERVER_URI, False) + DaemonUtils.test_rpc_connection(connection, Utils.OFFLINE_SERVER_URI, False) diff --git a/tests/test_monero_utils.py b/tests/test_monero_utils.py index 69b4974..a69f9ff 100644 --- a/tests/test_monero_utils.py +++ b/tests/test_monero_utils.py @@ -8,7 +8,7 @@ from monero import ( MoneroNetworkType, MoneroIntegratedAddress, MoneroUtils, MoneroTxConfig ) -from utils import TestUtils, AddressBook, KeysBook +from utils import AddressBook, KeysBook, AssertUtils, WalletUtils logger: logging.Logger = logging.getLogger("TestMoneroUtils") @@ -41,7 +41,7 @@ def config(self) -> TestMoneroUtils.Config: return TestMoneroUtils.Config.parse(parser) @pytest.fixture(autouse=True) - def before_each(self, request: pytest.FixtureRequest): + def setup_and_teardown(self, request: pytest.FixtureRequest): logger.info(f"Before {request.node.name}") # type: ignore yield logger.info(f"After {request.node.name}") # type: ignore @@ -88,7 +88,7 @@ def test_serialize_heights_small(self) -> None: binary: bytes = MoneroUtils.dict_to_binary(json_map) - TestUtils.assert_true(len(binary) > 0) + AssertUtils.assert_true(len(binary) > 0) json_map2: dict[Any, Any] = MoneroUtils.binary_to_dict(binary) @@ -101,7 +101,7 @@ def test_serialize_heights_big(self) -> None: } binary: bytes = MoneroUtils.dict_to_binary(json_map) - TestUtils.assert_true(len(binary) > 0) + AssertUtils.assert_true(len(binary) > 0) json_map2: dict[Any, Any] = MoneroUtils.binary_to_dict(binary) assert json_map == json_map2 @@ -114,7 +114,7 @@ def test_serialize_text_short(self, config: TestMoneroUtils.Config) -> None: } binary: bytes = MoneroUtils.dict_to_binary(json_map) - TestUtils.assert_true(len(binary) > 0) + AssertUtils.assert_true(len(binary) > 0) json_map2: dict[Any, Any] = MoneroUtils.binary_to_dict(binary) assert json_map == json_map2 @@ -140,7 +140,7 @@ def test_serialize_text_long(self, config: TestMoneroUtils.Config) -> None: } binary: bytes = MoneroUtils.dict_to_binary(json_map) - TestUtils.assert_true(len(binary) > 0) + AssertUtils.assert_true(len(binary) > 0) json_map2: dict[Any, Any] = MoneroUtils.binary_to_dict(binary) assert json_map == json_map2 @@ -194,64 +194,64 @@ def test_address_validation(self, config: TestMoneroUtils.Config) -> None: assert (MoneroUtils.is_valid_address(config.stagenet.subaddress_3, MoneroNetworkType.STAGENET)) is True # test invalid addresses on mainnet - TestUtils.test_invalid_address(None, MoneroNetworkType.MAINNET) - TestUtils.test_invalid_address("", MoneroNetworkType.MAINNET) - TestUtils.test_invalid_address(config.mainnet.invalid_1, MoneroNetworkType.MAINNET) - TestUtils.test_invalid_address(config.mainnet.invalid_2, MoneroNetworkType.MAINNET) - TestUtils.test_invalid_address(config.mainnet.invalid_3, MoneroNetworkType.MAINNET) + WalletUtils.test_invalid_address(None, MoneroNetworkType.MAINNET) + WalletUtils.test_invalid_address("", MoneroNetworkType.MAINNET) + WalletUtils.test_invalid_address(config.mainnet.invalid_1, MoneroNetworkType.MAINNET) + WalletUtils.test_invalid_address(config.mainnet.invalid_2, MoneroNetworkType.MAINNET) + WalletUtils.test_invalid_address(config.mainnet.invalid_3, MoneroNetworkType.MAINNET) # test invalid addresses on testnet - TestUtils.test_invalid_address(None, MoneroNetworkType.TESTNET) - TestUtils.test_invalid_address("", MoneroNetworkType.TESTNET) - TestUtils.test_invalid_address(config.testnet.invalid_1, MoneroNetworkType.TESTNET) - TestUtils.test_invalid_address(config.testnet.invalid_2, MoneroNetworkType.TESTNET) - TestUtils.test_invalid_address(config.testnet.invalid_3, MoneroNetworkType.TESTNET) + WalletUtils.test_invalid_address(None, MoneroNetworkType.TESTNET) + WalletUtils.test_invalid_address("", MoneroNetworkType.TESTNET) + WalletUtils.test_invalid_address(config.testnet.invalid_1, MoneroNetworkType.TESTNET) + WalletUtils.test_invalid_address(config.testnet.invalid_2, MoneroNetworkType.TESTNET) + WalletUtils.test_invalid_address(config.testnet.invalid_3, MoneroNetworkType.TESTNET) # test invalid addresses on stagenet - TestUtils.test_invalid_address(None, MoneroNetworkType.STAGENET) - TestUtils.test_invalid_address("", MoneroNetworkType.STAGENET) - TestUtils.test_invalid_address(config.stagenet.invalid_1, MoneroNetworkType.STAGENET) - TestUtils.test_invalid_address(config.stagenet.invalid_2, MoneroNetworkType.STAGENET) - TestUtils.test_invalid_address(config.stagenet.invalid_3, MoneroNetworkType.STAGENET) + WalletUtils.test_invalid_address(None, MoneroNetworkType.STAGENET) + WalletUtils.test_invalid_address("", MoneroNetworkType.STAGENET) + WalletUtils.test_invalid_address(config.stagenet.invalid_1, MoneroNetworkType.STAGENET) + WalletUtils.test_invalid_address(config.stagenet.invalid_2, MoneroNetworkType.STAGENET) + WalletUtils.test_invalid_address(config.stagenet.invalid_3, MoneroNetworkType.STAGENET) # Can validate keys def test_key_validation(self, config: TestMoneroUtils.Config) -> None: # test private view key validation - TestUtils.assert_true(MoneroUtils.is_valid_private_view_key(config.keys.private_view_key)) - TestUtils.test_invalid_private_view_key("") - TestUtils.test_invalid_private_view_key(None) - TestUtils.test_invalid_private_view_key(config.keys.invalid_private_view_key) + AssertUtils.assert_true(MoneroUtils.is_valid_private_view_key(config.keys.private_view_key)) + WalletUtils.test_invalid_private_view_key("") + WalletUtils.test_invalid_private_view_key(None) + WalletUtils.test_invalid_private_view_key(config.keys.invalid_private_view_key) # test public view key validation - TestUtils.assert_true(MoneroUtils.is_valid_public_view_key(config.keys.public_view_key)) - TestUtils.test_invalid_public_view_key("") - TestUtils.test_invalid_public_view_key(None) - TestUtils.test_invalid_public_view_key(config.keys.invalid_public_view_key) + AssertUtils.assert_true(MoneroUtils.is_valid_public_view_key(config.keys.public_view_key)) + WalletUtils.test_invalid_public_view_key("") + WalletUtils.test_invalid_public_view_key(None) + WalletUtils.test_invalid_public_view_key(config.keys.invalid_public_view_key) # test private spend key validation - TestUtils.assert_true(MoneroUtils.is_valid_private_spend_key(config.keys.private_spend_key)) - TestUtils.test_invalid_private_spend_key("") - TestUtils.test_invalid_private_spend_key(None) - TestUtils.test_invalid_private_spend_key(config.keys.invalid_private_spend_key) + AssertUtils.assert_true(MoneroUtils.is_valid_private_spend_key(config.keys.private_spend_key)) + WalletUtils.test_invalid_private_spend_key("") + WalletUtils.test_invalid_private_spend_key(None) + WalletUtils.test_invalid_private_spend_key(config.keys.invalid_private_spend_key) # test public spend key validation - TestUtils.assert_true(MoneroUtils.is_valid_public_spend_key(config.keys.public_spend_key)) - TestUtils.test_invalid_public_spend_key("") - TestUtils.test_invalid_public_spend_key(None) - TestUtils.test_invalid_public_spend_key(config.keys.invalid_public_spend_key) + AssertUtils.assert_true(MoneroUtils.is_valid_public_spend_key(config.keys.public_spend_key)) + WalletUtils.test_invalid_public_spend_key("") + WalletUtils.test_invalid_public_spend_key(None) + WalletUtils.test_invalid_public_spend_key(config.keys.invalid_public_spend_key) # Can validate seed def test_mnemonic_validation(self, config: TestMoneroUtils.Config) -> None: # test valid seed - TestUtils.assert_true(MoneroUtils.is_valid_mnemonic(config.keys.seed), f"Invalid seed: {config.keys.seed}") + AssertUtils.assert_true(MoneroUtils.is_valid_mnemonic(config.keys.seed), f"Invalid seed: {config.keys.seed}") # test invalid seed - TestUtils.assert_false(MoneroUtils.is_valid_mnemonic("invalid monero wallet seed")) + AssertUtils.assert_false(MoneroUtils.is_valid_mnemonic("invalid monero wallet seed")) # test empty seed - TestUtils.assert_false(MoneroUtils.is_valid_mnemonic("")) + AssertUtils.assert_false(MoneroUtils.is_valid_mnemonic("")) # Can validate language def test_seed_language_validation(self) -> None: diff --git a/tests/test_monero_wallet_common.py b/tests/test_monero_wallet_common.py index 177cad5..908e684 100644 --- a/tests/test_monero_wallet_common.py +++ b/tests/test_monero_wallet_common.py @@ -9,11 +9,13 @@ from monero import ( MoneroWallet, MoneroWalletRpc, MoneroDaemonRpc, MoneroWalletConfig, MoneroTxConfig, MoneroDestination, MoneroRpcConnection, MoneroError, - MoneroKeyImage, MoneroTxQuery, MoneroUtils, MoneroWalletFull + MoneroKeyImage, MoneroTxQuery, MoneroUtils, MoneroWalletFull, + MoneroBlock ) from utils import ( TestUtils, WalletEqualityUtils, MiningUtils, - OsUtils, StringUtils + OsUtils, StringUtils, AssertUtils, TxUtils, + TxContext, GenUtils, WalletUtils ) logger: logging.Logger = logging.getLogger("TestMoneroWalletCommon") @@ -23,6 +25,7 @@ class BaseTestMoneroWallet(ABC): CREATED_WALLET_KEYS_ERROR: str = "Wallet created from keys is not connected to authenticated daemon" _wallet: MoneroWallet _daemon: MoneroDaemonRpc + _funded: bool = False class Config: seed: str = "" @@ -72,6 +75,14 @@ def _open_wallet_from_path(self, path: str, password: str | None) -> MoneroWalle def get_daemon_rpc_uri(self) -> str: return TestUtils.DAEMON_RPC_URI + def fund_test_wallet(self) -> None: + if self._funded: + return + + wallet = self.get_test_wallet() + MiningUtils.fund_wallet(wallet, 1) + self._funded = True + @classmethod def is_random_wallet_config(cls, config: Optional[MoneroWalletConfig]) -> bool: assert config is not None @@ -89,15 +100,20 @@ def test_config(self) -> BaseTestMoneroWallet.Config: @pytest.fixture(scope="class", autouse=True) def before_all(self): - # TODO setup test environment for windows - if not OsUtils.is_windows(): + if not OsUtils.is_windows() and not MiningUtils.blockchain_is_ready(): MiningUtils.wait_until_blockchain_ready() - MiningUtils.try_stop_mining() + self.fund_test_wallet() @pytest.fixture(autouse=True) + def setup_and_teardown(self, request: pytest.FixtureRequest): + self.before_each(request) + yield + self.after_each(request) + def before_each(self, request: pytest.FixtureRequest): logger.info(f"Before {request.node.name}") # type: ignore - yield + + def after_each(self, request: pytest.FixtureRequest): logger.info(f"After {request.node.name}") # type: ignore #endregion @@ -137,7 +153,7 @@ def test_create_wallet_random(self) -> None: MoneroUtils.validate_mnemonic(wallet.get_seed()) if not isinstance(wallet, MoneroWalletRpc): # TODO monero-wallet-rpc: get seed language - TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) except Exception as e: e2 = e @@ -151,7 +167,7 @@ def test_create_wallet_random(self) -> None: config.path = path self._create_wallet(config) except Exception as e: - TestUtils.assert_equals("Wallet already exists: " + path, str(e)) + AssertUtils.assert_equals("Wallet already exists: " + path, str(e)) # attempt to create wallet with unknown language try: @@ -160,7 +176,7 @@ def test_create_wallet_random(self) -> None: self._create_wallet(config) raise Exception("Should have thrown error") except Exception as e: - TestUtils.assert_equals("Unknown language: english", str(e)) + AssertUtils.assert_equals("Unknown language: english", str(e)) @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_create_wallet_from_seed(self, test_config: BaseTestMoneroWallet.Config) -> None: @@ -178,12 +194,12 @@ def test_create_wallet_from_seed(self, test_config: BaseTestMoneroWallet.Config) path = wallet.get_path() e2: Exception | None = None try: - TestUtils.assert_equals(primary_address, wallet.get_primary_address()) - TestUtils.assert_equals(private_view_key, wallet.get_private_view_key()) - TestUtils.assert_equals(private_spend_key, wallet.get_private_spend_key()) - TestUtils.assert_equals(TestUtils.SEED, wallet.get_seed()) + AssertUtils.assert_equals(primary_address, wallet.get_primary_address()) + AssertUtils.assert_equals(private_view_key, wallet.get_private_view_key()) + AssertUtils.assert_equals(private_spend_key, wallet.get_private_spend_key()) + AssertUtils.assert_equals(TestUtils.SEED, wallet.get_seed()) if not isinstance(wallet, MoneroWalletRpc): - TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) except Exception as e: e2 = e @@ -198,7 +214,7 @@ def test_create_wallet_from_seed(self, test_config: BaseTestMoneroWallet.Config) config.restore_height = TestUtils.FIRST_RECEIVE_HEIGHT self._create_wallet(config) except Exception as e: - TestUtils.assert_equals("Invalid mnemonic", str(e)) + AssertUtils.assert_equals("Invalid mnemonic", str(e)) # attempt to create wallet at same path try: @@ -207,7 +223,7 @@ def test_create_wallet_from_seed(self, test_config: BaseTestMoneroWallet.Config) self._create_wallet(config) raise Exception("Should have thrown error") except Exception as e: - TestUtils.assert_equals("Wallet already exists: " + path, str(e)) + AssertUtils.assert_equals("Wallet already exists: " + path, str(e)) @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_create_wallet_from_seed_with_offset(self) -> None: @@ -223,12 +239,12 @@ def test_create_wallet_from_seed_with_offset(self) -> None: e2: Exception | None = None try: MoneroUtils.validate_mnemonic(wallet.get_seed()) - TestUtils.assert_not_equals(TestUtils.SEED, wallet.get_seed()) + AssertUtils.assert_not_equals(TestUtils.SEED, wallet.get_seed()) MoneroUtils.validate_address(wallet.get_primary_address(), TestUtils.NETWORK_TYPE) - TestUtils.assert_not_equals(TestUtils.ADDRESS, wallet.get_primary_address()) + AssertUtils.assert_not_equals(TestUtils.ADDRESS, wallet.get_primary_address()) if not isinstance(wallet, MoneroWalletRpc): # TODO monero-wallet-rpc: support - TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) except Exception as e: e2 = e @@ -258,17 +274,17 @@ def test_create_wallet_from_keys(self) -> None: path = wallet.get_path() e2: Exception | None = None try: - TestUtils.assert_equals(primary_address, wallet.get_primary_address()) - TestUtils.assert_equals(private_view_key, wallet.get_private_view_key()) - TestUtils.assert_equals(private_spend_key, wallet.get_private_spend_key()) + AssertUtils.assert_equals(primary_address, wallet.get_primary_address()) + AssertUtils.assert_equals(private_view_key, wallet.get_private_view_key()) + AssertUtils.assert_equals(private_spend_key, wallet.get_private_spend_key()) if not wallet.is_connected_to_daemon(): # TODO monero-project: keys wallets not connected logger.warning(f"WARNING: {self.CREATED_WALLET_KEYS_ERROR}") - TestUtils.assert_true(wallet.is_connected_to_daemon(), self.CREATED_WALLET_KEYS_ERROR) + AssertUtils.assert_true(wallet.is_connected_to_daemon(), self.CREATED_WALLET_KEYS_ERROR) if not isinstance(wallet, MoneroWalletRpc): # TODO monero-wallet-rpc: cannot get seed from wallet created from keys? MoneroUtils.validate_mnemonic(wallet.get_seed()) - TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) except Exception as e: e2 = e @@ -285,17 +301,17 @@ def test_create_wallet_from_keys(self) -> None: wallet = self._create_wallet(config) e2 = None try: - TestUtils.assert_equals(primary_address, wallet.get_primary_address()) - TestUtils.assert_equals(private_view_key, wallet.get_private_view_key()) - TestUtils.assert_equals(private_spend_key, wallet.get_private_spend_key()) + AssertUtils.assert_equals(primary_address, wallet.get_primary_address()) + AssertUtils.assert_equals(private_view_key, wallet.get_private_view_key()) + AssertUtils.assert_equals(private_spend_key, wallet.get_private_spend_key()) if not wallet.is_connected_to_daemon(): # TODO monero-project: keys wallets not connected logger.warning(f"{self.CREATED_WALLET_KEYS_ERROR}") - TestUtils.assert_true(wallet.is_connected_to_daemon(), self.CREATED_WALLET_KEYS_ERROR) + AssertUtils.assert_true(wallet.is_connected_to_daemon(), self.CREATED_WALLET_KEYS_ERROR) if not isinstance(wallet, MoneroWalletRpc): # TODO monero-wallet-rpc: cannot get seed from wallet created from keys? MoneroUtils.validate_mnemonic(wallet.get_seed()) - TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) except Exception as e: e2 = e @@ -311,7 +327,7 @@ def test_create_wallet_from_keys(self) -> None: self._create_wallet(config) raise Exception("Should have thrown error") except Exception as e: - TestUtils.assert_equals("Wallet already exists: " + path, str(e)) + AssertUtils.assert_equals("Wallet already exists: " + path, str(e)) @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_subaddress_lookahead(self) -> None: @@ -329,14 +345,14 @@ def test_subaddress_lookahead(self) -> None: tx_config.account_index = 0 dest = MoneroDestination() dest.address = receiver.get_subaddress(0, 85000).address - dest.amount = TestUtils.MAX_FEE + dest.amount = TxUtils.MAX_FEE tx_config.destinations.append(dest) tx_config.relay = True self._wallet.create_tx(tx_config) # observe unconfirmed funds - TestUtils.wait_for(1000) + GenUtils.wait_for(1000) receiver.sync() assert receiver.get_balance() > 0 except Exception as e: @@ -373,7 +389,7 @@ def test_get_path(self) -> None: wallet = self._open_wallet_from_path(path, None) # test the attribute - TestUtils.assert_equals(uuid, wallet.get_attribute("uuid")) + AssertUtils.assert_equals(uuid, wallet.get_attribute("uuid")) self._close_wallet(wallet) def test_set_daemon_connection(self): @@ -384,29 +400,29 @@ def test_set_daemon_connection(self): connection = MoneroRpcConnection( daemon_rpc_uri, TestUtils.DAEMON_RPC_USERNAME, TestUtils.DAEMON_RPC_PASSWORD ) - TestUtils.assert_equals(connection, wallet.get_daemon_connection()) - TestUtils.assert_true(wallet.is_connected_to_daemon()) # uses default localhost connection + AssertUtils.assert_equals(connection, wallet.get_daemon_connection()) + AssertUtils.assert_true(wallet.is_connected_to_daemon()) # uses default localhost connection # set empty server uri wallet.set_daemon_connection("") - TestUtils.assert_equals(None, wallet.get_daemon_connection()) - TestUtils.assert_false(wallet.is_connected_to_daemon()) + AssertUtils.assert_equals(None, wallet.get_daemon_connection()) + AssertUtils.assert_false(wallet.is_connected_to_daemon()) # set offline server uri wallet.set_daemon_connection(TestUtils.OFFLINE_SERVER_URI) connection = MoneroRpcConnection(TestUtils.OFFLINE_SERVER_URI, "", "") - TestUtils.assert_equals(connection, wallet.get_daemon_connection()) - TestUtils.assert_false(wallet.is_connected_to_daemon()) + AssertUtils.assert_equals(connection, wallet.get_daemon_connection()) + AssertUtils.assert_false(wallet.is_connected_to_daemon()) # set daemon with wrong credentials wallet.set_daemon_connection(daemon_rpc_uri, "wronguser", "wrongpass") connection = MoneroRpcConnection(daemon_rpc_uri, "wronguser", "wrongpass") - TestUtils.assert_equals(connection, wallet.get_daemon_connection()) + AssertUtils.assert_equals(connection, wallet.get_daemon_connection()) if "" == TestUtils.DAEMON_RPC_USERNAME: # TODO: monerod without authentication works with bad credentials? - TestUtils.assert_true(wallet.is_connected_to_daemon()) + AssertUtils.assert_true(wallet.is_connected_to_daemon()) else: - TestUtils.assert_false(wallet.is_connected_to_daemon()) + AssertUtils.assert_false(wallet.is_connected_to_daemon()) # set daemon with authentication wallet.set_daemon_connection( @@ -415,35 +431,35 @@ def test_set_daemon_connection(self): connection = MoneroRpcConnection( daemon_rpc_uri, TestUtils.DAEMON_RPC_USERNAME, TestUtils.DAEMON_RPC_PASSWORD ) - TestUtils.assert_equals(connection, wallet.get_daemon_connection()) - TestUtils.assert_true(wallet.is_connected_to_daemon()) + AssertUtils.assert_equals(connection, wallet.get_daemon_connection()) + AssertUtils.assert_true(wallet.is_connected_to_daemon()) # nullify daemon connection wallet.set_daemon_connection(None) - TestUtils.assert_equals(None, wallet.get_daemon_connection()) + AssertUtils.assert_equals(None, wallet.get_daemon_connection()) wallet.set_daemon_connection(daemon_rpc_uri) connection = MoneroRpcConnection(daemon_rpc_uri) - TestUtils.assert_equals(connection, wallet.get_daemon_connection()) + AssertUtils.assert_equals(connection, wallet.get_daemon_connection()) wallet.set_daemon_connection(None) - TestUtils.assert_equals(None, wallet.get_daemon_connection()) + AssertUtils.assert_equals(None, wallet.get_daemon_connection()) # set daemon uri to non-daemon if not TestUtils.IN_CONTAINER: # TODO sometimes this fails in container... wallet.set_daemon_connection("www.getmonero.org") connection = MoneroRpcConnection("www.getmonero.org") - TestUtils.assert_equals(connection, wallet.get_daemon_connection()) - TestUtils.assert_false(wallet.is_connected_to_daemon()) + AssertUtils.assert_equals(connection, wallet.get_daemon_connection()) + AssertUtils.assert_false(wallet.is_connected_to_daemon()) # set daemon to invalid uri wallet.set_daemon_connection("abc123") - TestUtils.assert_false(wallet.is_connected_to_daemon()) + AssertUtils.assert_false(wallet.is_connected_to_daemon()) # attempt to sync try: wallet.sync() raise Exception("Exception expected") except Exception as e: - TestUtils.assert_equals("Wallet is not connected to daemon", str(e)) + AssertUtils.assert_equals("Wallet is not connected to daemon", str(e)) finally: self._close_wallet(wallet) @@ -451,12 +467,12 @@ def test_set_daemon_connection(self): def test_get_seed(self): seed = self._wallet.get_seed() MoneroUtils.validate_mnemonic(seed) - TestUtils.assert_equals(TestUtils.SEED, seed) + AssertUtils.assert_equals(TestUtils.SEED, seed) @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_seed_language(self): language = self._wallet.get_seed_language() - TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, language) + AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, language) @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_seed_languages(self): @@ -489,16 +505,16 @@ def test_get_public_spend_key(self): def test_get_primary_address(self): primary_address = self._wallet.get_primary_address() MoneroUtils.validate_address(primary_address, TestUtils.NETWORK_TYPE) - TestUtils.assert_equals(self._wallet.get_address(0, 0), primary_address) + AssertUtils.assert_equals(self._wallet.get_address(0, 0), primary_address) @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_subaddress_address(self): - TestUtils.assert_equals(self._wallet.get_primary_address(), (self._wallet.get_address(0, 0))) + AssertUtils.assert_equals(self._wallet.get_primary_address(), (self._wallet.get_address(0, 0))) for account in self._wallet.get_accounts(True): for subaddress in account.subaddresses: assert account.index is not None assert subaddress.index is not None - TestUtils.assert_equals(subaddress.address, self._wallet.get_address(account.index, subaddress.index)) + AssertUtils.assert_equals(subaddress.address, self._wallet.get_address(account.index, subaddress.index)) @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_subaddress_address_out_of_range(self): @@ -506,8 +522,8 @@ def test_get_subaddress_address_out_of_range(self): account_idx = len(accounts) - 1 subaddress_idx = len(accounts[account_idx].subaddresses) address = self._wallet.get_address(account_idx, subaddress_idx) - TestUtils.assert_not_none(address) - TestUtils.assert_true(len(address) > 0) + AssertUtils.assert_not_none(address) + AssertUtils.assert_true(len(address) > 0) @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_address_indices(self): @@ -517,12 +533,12 @@ def test_get_address_indices(self): account_idx = len(accounts) - 1 subaddress_idx = len(accounts[account_idx].subaddresses) - 1 address = wallet.get_address(account_idx, subaddress_idx) - TestUtils.assert_not_none(address) + AssertUtils.assert_not_none(address) # get address index subaddress = wallet.get_address_index(address) - TestUtils.assert_equals(account_idx, subaddress.account_index) - TestUtils.assert_equals(subaddress_idx, subaddress.index) + AssertUtils.assert_equals(account_idx, subaddress.account_index) + AssertUtils.assert_equals(subaddress_idx, subaddress.index) # test valid but unfound address non_wallet_address = TestUtils.get_external_wallet_address() @@ -530,28 +546,28 @@ def test_get_address_indices(self): wallet.get_address_index(non_wallet_address) raise Exception("Should have thrown exception") except Exception as e: - TestUtils.assert_equals("Address doesn't belong to the wallet", str(e)) + AssertUtils.assert_equals("Address doesn't belong to the wallet", str(e)) # test invalid address try: wallet.get_address_index("this is definitely not an address") raise Exception("Should have thrown exception") except Exception as e: - TestUtils.assert_equals("Invalid address", str(e)) + AssertUtils.assert_equals("Invalid address", str(e)) @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_decode_integrated_address(self): wallet = self._wallet integrated_address = wallet.get_integrated_address('', "03284e41c342f036") decoded_address = wallet.decode_integrated_address(integrated_address.integrated_address) - TestUtils.assert_equals(integrated_address, decoded_address) + AssertUtils.assert_equals(integrated_address, decoded_address) # decode invalid address try: wallet.decode_integrated_address("bad address") raise Exception("Should have failed decoding bad address") except Exception as e: - TestUtils.assert_equals("Invalid address", str(e)) + AssertUtils.assert_equals("Invalid address", str(e)) @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_sync_without_progress(self): @@ -559,10 +575,10 @@ def test_sync_without_progress(self): daemon = self._daemon num_blocks = 100 chain_height = daemon.get_height() - TestUtils.assert_true(chain_height >= num_blocks) + AssertUtils.assert_true(chain_height >= num_blocks) result = wallet.sync(chain_height - num_blocks) # sync end of chain - TestUtils.assert_true(result.num_blocks_fetched >= 0) - TestUtils.assert_not_none(result.received_money) + AssertUtils.assert_true(result.num_blocks_fetched >= 0) + AssertUtils.assert_not_none(result.received_money) @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_wallet_equality_ground_truth(self): @@ -589,7 +605,7 @@ def test_get_height_by_date(self): # collect dates to test starting 100 days ago day_ms = 24 * 60 * 60 * 1000 # TODO monero-project: today's date can throw exception as "in future" so we test up to yesterday - yesterday = TestUtils.current_timestamp() - day_ms + yesterday = GenUtils.current_timestamp() - day_ms dates: list[datetime] = [] i = 99 @@ -653,8 +669,8 @@ def test_get_all_balances(self): assert wallet.get_balance(account.index) == subaddresses_balance assert wallet.get_unlocked_balance(account.index) == subaddresses_unlocked_balance - TestUtils.test_unsigned_big_integer(accounts_balance) - TestUtils.test_unsigned_big_integer(accounts_unlocked_balance) + GenUtils.test_unsigned_big_integer(accounts_balance) + GenUtils.test_unsigned_big_integer(accounts_unlocked_balance) assert wallet.get_balance() == accounts_balance assert wallet.get_unlocked_balance() == accounts_unlocked_balance @@ -663,7 +679,7 @@ def test_get_accounts_without_subaddresses(self): accounts = self._wallet.get_accounts() assert len(accounts) > 0 for account in accounts: - TestUtils.test_account(account) + WalletUtils.test_account(account, TestUtils.NETWORK_TYPE) assert len(account.subaddresses) == 0 @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -671,7 +687,7 @@ def test_get_accounts_with_subaddresses(self): accounts = self._wallet.get_accounts(True) assert len(accounts) > 0 for account in accounts: - TestUtils.test_account(account) + WalletUtils.test_account(account, TestUtils.NETWORK_TYPE) assert len(account.subaddresses) > 0 @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -679,7 +695,7 @@ def test_get_account(self): accounts = self._wallet.get_accounts() assert len(accounts) > 0 for account in accounts: - TestUtils.test_account(account) + WalletUtils.test_account(account, TestUtils.NETWORK_TYPE) # test without subaddresses assert account.index is not None @@ -694,7 +710,7 @@ def test_get_account(self): def test_create_account_without_label(self): accounts_before = self._wallet.get_accounts() created_account = self._wallet.create_account() - TestUtils.test_account(created_account) + WalletUtils.test_account(created_account, TestUtils.NETWORK_TYPE) assert len(accounts_before) == len(self._wallet.get_accounts()) - 1 @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -704,25 +720,25 @@ def test_create_account_with_label(self): accounts_before = wallet.get_accounts() label = StringUtils.get_random_string() created_account = wallet.create_account(label) - TestUtils.test_account(created_account) + WalletUtils.test_account(created_account, TestUtils.NETWORK_TYPE) assert created_account.index is not None assert len(accounts_before) == len(wallet.get_accounts()) - 1 assert label == wallet.get_subaddress(created_account.index, 0).label # fetch and test account created_account = wallet.get_account(created_account.index) - TestUtils.test_account(created_account) + WalletUtils.test_account(created_account, TestUtils.NETWORK_TYPE) # create account with same label created_account = wallet.create_account(label) - TestUtils.test_account(created_account) + WalletUtils.test_account(created_account, TestUtils.NETWORK_TYPE) assert len(accounts_before) == len(wallet.get_accounts()) - 2 assert created_account.index is not None assert label == wallet.get_subaddress(created_account.index, 0).label # fetch and test account created_account = wallet.get_account(created_account.index) - TestUtils.test_account(created_account) + WalletUtils.test_account(created_account, TestUtils.NETWORK_TYPE) def test_set_account_label(self): # create account @@ -745,7 +761,7 @@ def test_get_subaddresses(self): subaddresses = wallet.get_subaddresses(account.index) assert len(subaddresses) > 0 for subaddress in subaddresses: - TestUtils.test_subaddress(subaddress) + WalletUtils.test_subaddress(subaddress) assert account.index == subaddress.account_index @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -777,7 +793,7 @@ def test_get_subaddresses_by_indices(self): fetched_subaddresses = wallet.get_subaddresses(account.index, subaddress_indices) # original subaddresses (minus one removed if applicable) is equal to fetched subaddresses - TestUtils.assert_subaddresses_equal(subaddresses, fetched_subaddresses) + AssertUtils.assert_subaddresses_equal(subaddresses, fetched_subaddresses) @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_subaddress_by_index(self): @@ -790,10 +806,10 @@ def test_get_subaddress_by_index(self): assert len(subaddresses) > 0 for subaddress in subaddresses: assert subaddress.index is not None - TestUtils.test_subaddress(subaddress) - TestUtils.assert_subaddress_equal(subaddress, wallet.get_subaddress(account.index, subaddress.index)) + WalletUtils.test_subaddress(subaddress) + AssertUtils.assert_subaddress_equal(subaddress, wallet.get_subaddress(account.index, subaddress.index)) # test plural call with single subaddr number - TestUtils.assert_subaddress_equal( + AssertUtils.assert_subaddress_equal( subaddress, wallet.get_subaddresses(account.index, [subaddress.index])[0] ) @@ -813,20 +829,20 @@ def test_create_subaddress(self): subaddresses = wallet.get_subaddresses(account_idx) subaddress = wallet.create_subaddress(account_idx) assert subaddress.label is None - TestUtils.test_subaddress(subaddress) + WalletUtils.test_subaddress(subaddress) subaddresses_new = wallet.get_subaddresses(account_idx) assert len(subaddresses_new) - 1 == len(subaddresses) - TestUtils.assert_subaddress_equal(subaddress, subaddresses_new[len(subaddresses_new) - 1]) + AssertUtils.assert_subaddress_equal(subaddress, subaddresses_new[len(subaddresses_new) - 1]) # create subaddress with label subaddresses = wallet.get_subaddresses(account_idx) uuid = StringUtils.get_random_string() subaddress = wallet.create_subaddress(account_idx, uuid) assert (uuid == subaddress.label) - TestUtils.test_subaddress(subaddress) + WalletUtils.test_subaddress(subaddress) subaddresses_new = wallet.get_subaddresses(account_idx) assert len(subaddresses) == len(subaddresses_new) - 1 - TestUtils.assert_subaddress_equal(subaddress, subaddresses_new[len(subaddresses_new) - 1]) + AssertUtils.assert_subaddress_equal(subaddress, subaddresses_new[len(subaddresses_new) - 1]) account_idx += 1 @@ -846,10 +862,70 @@ def test_set_subaddress_label(self): #region Txs Tests + # Can get transactions in the wallet + @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_get_txs_wallet(self) -> None: + wallet = self.get_test_wallet() + #non_default_incoming: bool = False + txs = TxUtils.get_and_test_txs(wallet, None, None, True) + assert len(txs) > 0, "Wallet has no txs to test" + # TODO make consistent with test funded wallet + # assert TestUtils.FIRST_RECEIVE_HEIGHT == txs[0].get_height(), "First tx's restore height must match the restore height in TestUtils" + + # build test context + ctx = TxContext() + ctx.wallet = wallet + + # test each transaction + block_per_height: dict[int, MoneroBlock] = {} + for i, tx in enumerate(txs): + TxUtils.test_tx_wallet(tx, ctx) + + # test merging equivalent txs + it = txs[i] # is the same as tx + copy1 = it.copy() + copy2 = it.copy() + + if copy1.is_confirmed: + assert it.block is not None + copy1.block = it.block.copy() + copy1.block.txs = [copy1] + + if copy2.is_confirmed: + assert it.block is not None + copy2.block = it.block.copy() + copy2.block.txs = [copy2] + + copy1.merge(copy2) + TxUtils.test_tx_wallet(copy1, ctx) + + # find non-default incoming + for transfer in it.incoming_transfers: + assert transfer.account_index is not None + assert transfer.subaddress_index is not None + #if transfer.account_index != 0 and transfer.subaddress_index != 0: + # non_default_incoming = True + + # ensure unique block reference per height + if it.is_confirmed: + h = it.get_height() + assert h is not None + block = block_per_height.get(h) + if block is None: + assert it.block is not None + block_per_height[i] = it.block + else: + AssertUtils.assert_equals(block, it.block) + assert block == it.block, "Block references for same height must be same" + + # ensure non-default account and subaddress testes + # TODO enable this after setting send-to-multiple order + #assert non_default_incoming, "No incoming transfers found to non-default account and subaddress; run send-to-multiple tests first" + @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_set_tx_note(self) -> None: wallet = self._wallet - txs = TestUtils.get_random_transactions(wallet, None, 1, 5) + txs = TxUtils.get_random_transactions(wallet, None, 1, 5) # set notes uuid = StringUtils.get_random_string() @@ -951,8 +1027,8 @@ def test_import_key_images(self): has_unspent = balance > 0 # test amounts - TestUtils.test_unsigned_big_integer(result.spent_amount, has_spent) - TestUtils.test_unsigned_big_integer(result.unspent_amount, has_unspent) + GenUtils.test_unsigned_big_integer(result.spent_amount, has_spent) + GenUtils.test_unsigned_big_integer(result.unspent_amount, has_unspent) @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_payment_uri(self): @@ -963,7 +1039,7 @@ def test_get_payment_uri(self): config1.amount = 0 uri = wallet.get_payment_uri(config1) config2 = wallet.parse_payment_uri(uri) - TestUtils.assert_equals(config1, config2) + AssertUtils.assert_equals(config1, config2) # test with subaddress and all fields subaddress = wallet.get_subaddress(0, 1) @@ -975,7 +1051,7 @@ def test_get_payment_uri(self): config2 = wallet.parse_payment_uri(uri) # TODO implement set_address like Java MoneroTxConfig and remove next line config2.destinations.append(MoneroDestination(subaddress.address, 425000000000)) - TestUtils.assert_equals(config1, config2) + AssertUtils.assert_equals(config1, config2) # test with undefined address address = config1.destinations[0].address diff --git a/tests/test_monero_wallet_full.py b/tests/test_monero_wallet_full.py index 06916c8..35190be 100644 --- a/tests/test_monero_wallet_full.py +++ b/tests/test_monero_wallet_full.py @@ -8,7 +8,10 @@ MoneroSubaddress, MoneroDaemonRpc, MoneroWallet ) -from utils import TestUtils as Utils, OsUtils, StringUtils +from utils import ( + TestUtils as Utils, OsUtils, StringUtils, + AssertUtils, WalletUtils +) from test_monero_wallet_common import BaseTestMoneroWallet logger: logging.Logger = logging.getLogger("TestMoneroWalletFull") @@ -43,7 +46,7 @@ def _create_wallet(self, config: Optional[MoneroWalletConfig], start_syncing: bo # create wallet wallet = MoneroWalletFull.create_wallet(config) if not random: - Utils.assert_equals( + AssertUtils.assert_equals( 0 if config.restore_height is None else config.restore_height, wallet.get_restore_height() ) if start_syncing is not False and wallet.is_connected_to_daemon(): @@ -83,13 +86,6 @@ def _get_seed_languages(self) -> list[str]: def get_test_wallet(self) -> MoneroWallet: return Utils.get_wallet_full() - @pytest.fixture(autouse=True) - @override - def before_each(self, request: pytest.FixtureRequest): - logger.info(f"Before test {request.node.name}") # type: ignore - yield - logger.info(f"After test {request.node.name}") # type: ignore - #endregion #region Tests @@ -104,7 +100,7 @@ def test_create_subaddress(self): self._wallet.create_account() accounts = self._wallet.get_accounts() - Utils.assert_true(len(accounts) > 1) + AssertUtils.assert_true(len(accounts) > 1) account_idx: int = 0 while account_idx < 2: # create subaddress with no label @@ -113,20 +109,20 @@ def test_create_subaddress(self): # TODO fix monero-cpp/monero_wallet_full.cpp to return boost::none on empty label #assert subaddress.label is None assert subaddress.label is None or subaddress.label == "" - Utils.test_subaddress(subaddress) + WalletUtils.test_subaddress(subaddress) subaddresses_new: list[MoneroSubaddress] = self._wallet.get_subaddresses(account_idx) - Utils.assert_equals(len(subaddresses_new) - 1, len(subaddresses)) - Utils.assert_equals(subaddress, subaddresses_new[len(subaddresses_new) - 1]) + AssertUtils.assert_equals(len(subaddresses_new) - 1, len(subaddresses)) + AssertUtils.assert_equals(subaddress, subaddresses_new[len(subaddresses_new) - 1]) # create subaddress with label subaddresses = self._wallet.get_subaddresses(account_idx) uuid: str = StringUtils.get_random_string() subaddress = self._wallet.create_subaddress(account_idx, uuid) - Utils.assert_equals(uuid, subaddress.label) - Utils.test_subaddress(subaddress) + AssertUtils.assert_equals(uuid, subaddress.label) + WalletUtils.test_subaddress(subaddress) subaddresses_new = self._wallet.get_subaddresses(account_idx) - Utils.assert_equals(len(subaddresses), len(subaddresses_new) - 1) - Utils.assert_equals(subaddress, subaddresses_new[len(subaddresses_new) - 1]) + AssertUtils.assert_equals(len(subaddresses), len(subaddresses_new) - 1) + AssertUtils.assert_equals(subaddress, subaddresses_new[len(subaddresses_new) - 1]) account_idx += 1 #endregion diff --git a/tests/test_monero_wallet_interface.py b/tests/test_monero_wallet_interface.py index 5229b5b..a5e7b0a 100644 --- a/tests/test_monero_wallet_interface.py +++ b/tests/test_monero_wallet_interface.py @@ -7,7 +7,7 @@ MoneroWalletListener, MoneroTransferQuery, MoneroOutputQuery, MoneroTxConfig, MoneroTxSet, MoneroMessageSignatureType ) -from utils import TestUtils as Utils +from utils import AssertUtils logger: logging.Logger = logging.getLogger("TestMoneroWalletInterface") @@ -24,10 +24,10 @@ def _get_wallet(self) -> MoneroWallet: return self._wallet @pytest.fixture(autouse=True) - def before_each(self, request: pytest.FixtureRequest): - logger.info(f"Before test {request.node.name}") # type: ignore + def setup_and_teardown(self, request: pytest.FixtureRequest): + logger.info(f"Before {request.node.name}") # type: ignore yield - logger.info(f"After test {request.node.name}") # type: ignore + logger.info(f"After {request.node.name}") # type: ignore #region Tests @@ -43,22 +43,22 @@ def test_info(self) -> None: try: wallet.is_view_only() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_version() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_path() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_network_type() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test wallet connection def test_connection(self) -> None: @@ -67,38 +67,38 @@ def test_connection(self) -> None: try: wallet.set_connection_manager(MoneroConnectionManager()) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # TODO segmentation fault # try: # wallet.get_connection_manager() # except Exception as e: -# Utils.assert_not_supported(e) +# AssertUtils.assert_not_supported(e) try: wallet.set_daemon_connection('') except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.set_daemon_connection(MoneroRpcConnection()) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_daemon_connection() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.is_connected_to_daemon() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.is_daemon_trusted() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test get wallet keys def test_keys(self) -> None: @@ -107,32 +107,32 @@ def test_keys(self) -> None: try: wallet.get_seed() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_seed_language() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_public_view_key() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_private_view_key() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_public_spend_key() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_private_spend_key() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test wallet addresses def test_addresses(self) -> None: @@ -141,27 +141,27 @@ def test_addresses(self) -> None: try: wallet.get_primary_address() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_address(0, 0) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_address_index("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_integrated_address() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.decode_integrated_address("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test get wallet sync info def test_sync_utils(self) -> None: @@ -170,97 +170,97 @@ def test_sync_utils(self) -> None: try: wallet.is_synced() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_height() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_restore_height() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.set_restore_height(0) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_daemon_height() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_daemon_max_peer_height() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_height_by_date(0, 0, 0) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.add_listener(MoneroWalletListener()) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.remove_listener(MoneroWalletListener()) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_listeners() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.sync() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.sync(MoneroWalletListener()) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.sync(0) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.sync(0, MoneroWalletListener()) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.start_syncing() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.stop_syncing() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.scan_txs(["", "", ""]) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.rescan_spent() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.rescan_blockchain() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test wallet balance def test_get_balances(self) -> None: @@ -269,32 +269,32 @@ def test_get_balances(self) -> None: try: wallet.get_balance() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_balance(0) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_balance(0, 0) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_unlocked_balance() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_unlocked_balance(0) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_unlocked_balance(0, 0) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test get wallet accounts def test_get_accounts(self) -> None: @@ -303,37 +303,37 @@ def test_get_accounts(self) -> None: try: wallet.get_accounts() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_accounts("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_accounts(True) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_accounts(True, "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_account(0) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_account(0, True) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.create_account() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test get wallet subaddresses def test_get_subaddresses(self) -> None: @@ -342,22 +342,22 @@ def test_get_subaddresses(self) -> None: try: wallet.get_subaddress(0, 0) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_subaddresses(0) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.create_subaddress(0, "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.set_subaddress_label(1, 1, "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test txs wallet data def test_txs_data(self) -> None: @@ -366,63 +366,63 @@ def test_txs_data(self) -> None: try: wallet.get_txs() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_transfers(MoneroTransferQuery()) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_outputs(MoneroOutputQuery()) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.export_outputs(True) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.import_outputs("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.export_key_images(True) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.import_key_images([]) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # TODO segmentation fault # try: # wallet.get_new_key_images_from_last_import() # except Exception as e: -# Utils.assert_not_supported(e) +# AssertUtils.assert_not_supported(e) try: wallet.freeze_output("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.thaw_output("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.is_output_frozen("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_default_fee_priority() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test wallet tx creation def test_txs_creation(self) -> None: @@ -431,68 +431,68 @@ def test_txs_creation(self) -> None: try: wallet.create_tx(MoneroTxConfig()) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.create_txs(MoneroTxConfig()) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.sweep_unlocked(MoneroTxConfig()) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.sweep_output(MoneroTxConfig()) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.sweep_dust(True) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.relay_tx("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # TODO aborted # try: # wallet.relay_tx(MoneroTxWallet()) # except Exception as e: -# Utils.assert_not_supported(e) +# AssertUtils.assert_not_supported(e) try: wallet.relay_txs([]) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.describe_tx_set(MoneroTxSet()) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.sign_txs("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.submit_txs("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_tx_key("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.check_tx_key("", "", "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test wallet proofs def test_proofs(self) -> None: @@ -501,37 +501,37 @@ def test_proofs(self) -> None: try: wallet.get_tx_proof("", "", "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.check_tx_proof("", "", "", "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_spend_proof("", "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.check_spend_proof("", "", "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_reserve_proof_wallet("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_reserve_proof_account(0, 0, "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.check_reserve_proof("", "", "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test wallet notes def test_notes(self) -> None: @@ -540,73 +540,73 @@ def test_notes(self) -> None: try: wallet.get_tx_note("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.set_tx_note("", "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.set_tx_notes([], []) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_address_book_entries([]) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.add_address_book_entry("", "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.edit_address_book_entry(1, False, "", True, "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.delete_address_book_entry(0) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # TODO segmentation fault # try: # wallet.tag_accounts("", []) # except Exception as e: -# Utils.assert_not_supported(e) +# AssertUtils.assert_not_supported(e) # try: # wallet.untag_accounts([]) # except Exception as e: -# Utils.assert_not_supported(e) +# AssertUtils.assert_not_supported(e) # try: # wallet.get_account_tags() # except Exception as e: -# Utils.assert_not_supported(e) +# AssertUtils.assert_not_supported(e) try: wallet.set_account_tag_label("", "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.set_account_label(0, "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_attribute("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.set_attribute("", "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test wallet multisig info def test_multisig(self) -> None: @@ -615,52 +615,52 @@ def test_multisig(self) -> None: try: wallet.is_multisig_import_needed() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.is_multisig() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_multisig_info() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.prepare_multisig() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.make_multisig([], 0, "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.exchange_multisig_keys([], "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.export_multisig_hex() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.import_multisig_hex([]) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.sign_multisig_tx_hex("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.submit_multisig_tx_hex("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) # Test wallet utils def test_utils(self) -> None: @@ -669,56 +669,56 @@ def test_utils(self) -> None: try: wallet.sign_message("", MoneroMessageSignatureType.SIGN_WITH_VIEW_KEY) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.verify_message("", "", "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.get_payment_uri(MoneroTxConfig()) except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.parse_payment_uri("") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.start_mining() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.stop_mining() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.wait_for_next_block() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.change_password("", "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.move_to("", "") except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.save() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) try: wallet.close() except Exception as e: - Utils.assert_not_supported(e) + AssertUtils.assert_not_supported(e) #endregion diff --git a/tests/test_monero_wallet_keys.py b/tests/test_monero_wallet_keys.py index 90ab9c7..0d75411 100644 --- a/tests/test_monero_wallet_keys.py +++ b/tests/test_monero_wallet_keys.py @@ -7,7 +7,7 @@ MoneroWalletKeys, MoneroWalletConfig, MoneroWallet, MoneroUtils, MoneroAccount, MoneroSubaddress ) -from utils import TestUtils as Utils +from utils import TestUtils as Utils, AssertUtils, WalletUtils from test_monero_wallet_common import BaseTestMoneroWallet @@ -20,13 +20,6 @@ class TestMoneroWalletKeys(BaseTestMoneroWallet): _subaddress_indices: list[int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] _wallet: MoneroWalletKeys = Utils.get_wallet_keys() # type: ignore - @pytest.fixture(autouse=True) - @override - def before_each(self, request: pytest.FixtureRequest): - logger.info(f"Before test {request.node.name}") # type: ignore - yield - logger.info(f"After test {request.node.name}") # type: ignore - @pytest.fixture(scope="class", autouse=True) @override def before_all(self): @@ -85,6 +78,10 @@ def get_test_wallet(self) -> MoneroWallet: #region Disabled Tests + @pytest.mark.skip(reason="Txs not supported") + def test_get_txs_wallet(self) -> None: + return super().test_get_txs_wallet() + @pytest.mark.skip(reason="Daemon not supported") @override def test_daemon(self) -> None: @@ -242,7 +239,7 @@ def test_create_wallet_random(self) -> None: MoneroUtils.validate_private_spend_key(wallet.get_private_spend_key()) MoneroUtils.validate_mnemonic(wallet.get_seed()) # TODO monero-wallet-rpc: get seed language - Utils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) except Exception as e: e2 = e @@ -256,7 +253,7 @@ def test_create_wallet_random(self) -> None: self._create_wallet(config) raise Exception("Should have thrown error") except Exception as e: - Utils.assert_equals("Unknown language: english", str(e)) + AssertUtils.assert_equals("Unknown language: english", str(e)) except Exception as e: e1 = e @@ -282,11 +279,11 @@ def test_create_wallet_from_seed(self, test_config: BaseTestMoneroWallet.Config) wallet: MoneroWallet = self._create_wallet(config) e2: Exception | None = None try: - Utils.assert_equals(primary_address, wallet.get_primary_address()) - Utils.assert_equals(private_view_key, wallet.get_private_view_key()) - Utils.assert_equals(private_spend_key, wallet.get_private_spend_key()) - Utils.assert_equals(Utils.SEED, wallet.get_seed()) - Utils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + AssertUtils.assert_equals(primary_address, wallet.get_primary_address()) + AssertUtils.assert_equals(private_view_key, wallet.get_private_view_key()) + AssertUtils.assert_equals(private_spend_key, wallet.get_private_spend_key()) + AssertUtils.assert_equals(Utils.SEED, wallet.get_seed()) + AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) except Exception as e: e2 = e @@ -299,7 +296,7 @@ def test_create_wallet_from_seed(self, test_config: BaseTestMoneroWallet.Config) config.seed = test_config.seed self._create_wallet(config) except Exception as e: - Utils.assert_equals("Invalid mnemonic", str(e)) + AssertUtils.assert_equals("Invalid mnemonic", str(e)) except Exception as e: e1 = e @@ -320,11 +317,11 @@ def test_create_wallet_from_seed_with_offset(self) -> None: e2: Exception | None = None try: MoneroUtils.validate_mnemonic(wallet.get_seed()) - Utils.assert_not_equals(Utils.SEED, wallet.get_seed()) + AssertUtils.assert_not_equals(Utils.SEED, wallet.get_seed()) MoneroUtils.validate_address(wallet.get_primary_address(), Utils.NETWORK_TYPE) - Utils.assert_not_equals(Utils.ADDRESS, wallet.get_primary_address()) + AssertUtils.assert_not_equals(Utils.ADDRESS, wallet.get_primary_address()) # TODO monero-wallet-rpc: support - Utils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) except Exception as e: e2 = e @@ -354,12 +351,12 @@ def test_create_wallet_from_keys(self) -> None: wallet: MoneroWallet = self._create_wallet(config) e2: Exception | None = None try: - Utils.assert_equals(primary_address, wallet.get_primary_address()) - Utils.assert_equals(private_view_key, wallet.get_private_view_key()) - Utils.assert_equals(private_spend_key, wallet.get_private_spend_key()) + AssertUtils.assert_equals(primary_address, wallet.get_primary_address()) + AssertUtils.assert_equals(private_view_key, wallet.get_private_view_key()) + AssertUtils.assert_equals(private_spend_key, wallet.get_private_spend_key()) # TODO monero-wallet-rpc: cannot get seed from wallet created from keys? MoneroUtils.validate_mnemonic(wallet.get_seed()) - Utils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) except Exception as e: e2 = e @@ -375,12 +372,12 @@ def test_create_wallet_from_keys(self) -> None: wallet = self._create_wallet(config) e2 = None try: - Utils.assert_equals(primary_address, wallet.get_primary_address()) - Utils.assert_equals(private_view_key, wallet.get_private_view_key()) - Utils.assert_equals(private_spend_key, wallet.get_private_spend_key()) + AssertUtils.assert_equals(primary_address, wallet.get_primary_address()) + AssertUtils.assert_equals(private_view_key, wallet.get_private_view_key()) + AssertUtils.assert_equals(private_spend_key, wallet.get_private_spend_key()) # TODO monero-wallet-rpc: cannot get seed from wallet created from keys? MoneroUtils.validate_mnemonic(wallet.get_seed()) - Utils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) except Exception as e: e2 = e @@ -398,7 +395,7 @@ def test_create_wallet_from_keys(self) -> None: @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @override def test_get_subaddress_address(self): - Utils.assert_equals(self._wallet.get_primary_address(), (self._wallet.get_address(0, 0))) + AssertUtils.assert_equals(self._wallet.get_primary_address(), (self._wallet.get_address(0, 0))) accounts = self._get_test_accounts(True) for account in accounts: @@ -410,7 +407,7 @@ def test_get_subaddress_address(self): for subaddress in account.subaddresses: assert subaddress is not None assert subaddress.index is not None - Utils.assert_equals(subaddress.address, self._wallet.get_address(account.index, subaddress.index)) + AssertUtils.assert_equals(subaddress.address, self._wallet.get_address(account.index, subaddress.index)) @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @override @@ -419,8 +416,8 @@ def test_get_subaddress_address_out_of_range(self): account_idx = len(accounts) - 1 subaddress_idx = len(accounts[account_idx].subaddresses) address = self._wallet.get_address(account_idx, subaddress_idx) - Utils.assert_not_none(address) - Utils.assert_true(len(address) > 0) + AssertUtils.assert_not_none(address) + AssertUtils.assert_true(len(address) > 0) @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @override @@ -428,7 +425,7 @@ def test_get_account(self): accounts = self._get_test_accounts() assert len(accounts) > 0 for account in accounts: - Utils.test_account(account, False) + WalletUtils.test_account(account, Utils.NETWORK_TYPE, False) # test without subaddresses assert account.index is not None @@ -444,7 +441,7 @@ def test_get_accounts_without_subaddresses(self): accounts = self._get_test_accounts() assert len(accounts) > 0 for account in accounts: - Utils.test_account(account, False) + WalletUtils.test_account(account, Utils.NETWORK_TYPE, False) assert len(account.subaddresses) == 0 @override @@ -452,7 +449,7 @@ def test_get_accounts_with_subaddresses(self): accounts = self._get_test_accounts(True) assert len(accounts) > 0 for account in accounts: - Utils.test_account(account, False) + WalletUtils.test_account(account, Utils.NETWORK_TYPE, False) assert len(account.subaddresses) > 0 @override @@ -465,7 +462,7 @@ def test_get_subaddresses(self): subaddresses = wallet.get_subaddresses(account.index, self._subaddress_indices) assert len(subaddresses) > 0 for subaddress in subaddresses: - Utils.test_subaddress(subaddress, False) + WalletUtils.test_subaddress(subaddress, False) assert account.index == subaddress.account_index @override @@ -479,10 +476,10 @@ def test_get_subaddress_by_index(self): for subaddress in subaddresses: assert subaddress.index is not None - Utils.test_subaddress(subaddress, False) - Utils.assert_subaddress_equal(subaddress, self._get_subaddress(account.index, subaddress.index)) + WalletUtils.test_subaddress(subaddress, False) + AssertUtils.assert_subaddress_equal(subaddress, self._get_subaddress(account.index, subaddress.index)) # test plural call with single subaddr number - Utils.assert_subaddress_equal( + AssertUtils.assert_subaddress_equal( subaddress, self._wallet.get_subaddresses(account.index, [subaddress.index])[0] ) diff --git a/tests/test_monero_wallet_rpc.py b/tests/test_monero_wallet_rpc.py index 5fc58b3..930fb45 100644 --- a/tests/test_monero_wallet_rpc.py +++ b/tests/test_monero_wallet_rpc.py @@ -47,13 +47,10 @@ def _close_wallet(self, wallet: MoneroWallet, save: bool = False) -> None: def _get_seed_languages(self) -> list[str]: return self._wallet.get_seed_languages() # type: ignore - @pytest.fixture(autouse=True) @override - def before_each(self, request: pytest.FixtureRequest): - logger.info(f"Before test {request.node.name}") # type: ignore - yield + def after_each(self, request: pytest.FixtureRequest): Utils.free_wallet_rpc_resources() - logger.info(f"After test {request.node.name}") # type: ignore + super().after_each(request) @override def get_daemon_rpc_uri(self) -> str: @@ -79,6 +76,10 @@ def test_save(self): #region Disabled Tests + @pytest.mark.skip(reason="TODO implement get_txs") + def test_get_txs_wallet(self) -> None: + return super().test_get_txs_wallet() + @pytest.mark.skip(reason="Not implemented for wallet rpc") def test_get_daemon_max_peer_height(self) -> None: return super().test_get_daemon_max_peer_height() diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index b41f406..6ab8e3f 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -1,3 +1,5 @@ +from .gen_utils import GenUtils +from .assert_utils import AssertUtils from .test_utils import TestUtils from .mining_utils import MiningUtils from .os_utils import OsUtils @@ -13,8 +15,16 @@ from .print_height import PrintHeight from .wallet_equality_utils import WalletEqualityUtils from .wallet_tx_tracker import WalletTxTracker +from .tx_utils import TxUtils +from .block_utils import BlockUtils +from .daemon_utils import DaemonUtils +from .wallet_utils import WalletUtils __all__ = [ + 'WalletUtils', + 'DaemonUtils', + 'GenUtils', + 'AssertUtils', 'TestUtils', 'MiningUtils', 'OsUtils', @@ -29,5 +39,7 @@ 'StringUtils', 'PrintHeight', 'WalletEqualityUtils', - 'WalletTxTracker' + 'WalletTxTracker', + 'TxUtils', + 'BlockUtils' ] diff --git a/tests/utils/address_book.py b/tests/utils/address_book.py index d815617..152efac 100644 --- a/tests/utils/address_book.py +++ b/tests/utils/address_book.py @@ -1,7 +1,7 @@ from __future__ import annotations from configparser import ConfigParser from monero import MoneroNetworkType -from .test_utils import TestUtils +from .daemon_utils import DaemonUtils class AddressBook: @@ -42,5 +42,5 @@ def parse(cls, parser: ConfigParser, section: str) -> AddressBook: entry.invalid_1 = parser.get(section, 'invalid_1') entry.invalid_2 = parser.get(section, 'invalid_2') entry.invalid_3 = parser.get(section, 'invalid_3') - entry.network_type = TestUtils.parse_network_type(section) + entry.network_type = DaemonUtils.parse_network_type(section) return entry diff --git a/tests/utils/assert_utils.py b/tests/utils/assert_utils.py new file mode 100644 index 0000000..b35795a --- /dev/null +++ b/tests/utils/assert_utils.py @@ -0,0 +1,104 @@ +import logging + +from abc import ABC +from os import getenv +from typing import Any, Optional +from monero import ( + SerializableStruct, MoneroRpcConnection, + MoneroSubaddress +) + +logger: logging.Logger = logging.getLogger("AssertUtils") +var = getenv("IN_CONTAINER", "true").lower() +IN_CONTAINER: bool = var == "true" or var == "1" + + +class AssertUtils(ABC): + + @classmethod + def assert_false(cls, expr: Any, message: str = "assertion failed"): + assert expr is False, message + + @classmethod + def assert_true(cls, expr: Any, message: str = "assertion failed"): + assert expr is True, message + + @classmethod + def assert_not_none(cls, expr: Any, message: str = "assertion failed"): + assert expr is not None, message + + @classmethod + def assert_is_none(cls, expr: Any, message: str = "assertion failed"): + assert expr is None, message + + @classmethod + def assert_equals(cls, expr1: Any, expr2: Any, message: str = "assertion failed"): + if isinstance(expr1, SerializableStruct) and isinstance(expr2, SerializableStruct): + str1 = expr1.serialize() + str2 = expr2.serialize() + assert str1 == str2, f"{message}: {str1} == {str2}" + elif isinstance(expr1, MoneroRpcConnection) and isinstance(expr2, MoneroRpcConnection): + cls.assert_connection_equals(expr1, expr2) + else: + assert expr1 == expr2, f"{message}: {expr1} == {expr2}" + + @classmethod + def equals(cls, expr1: Any, expr2: Any) -> bool: + try: + cls.assert_equals(expr1, expr2) + return True + except Exception as e: + logger.debug(str(e)) + return False + + @classmethod + def assert_not_equals(cls, expr1: Any, expr2: Any, message: str = "assertion failed"): + assert expr1 != expr2, f"{message}: {expr1} != {expr2}" + + @classmethod + def assert_is(cls, expr: Any, what: Any, message: str = "assertion failed"): + assert expr is what, f"{message}: {expr} is {what}" + + @classmethod + def assert_not_supported(cls, error: Any) -> None: + assert "not supported" in str(error), f"Expected not supported method: {error}" + + @classmethod + def assert_connection_equals(cls, c1: Optional[MoneroRpcConnection], c2: Optional[MoneroRpcConnection]) -> None: + if c1 is None and c2 is None: + return + + assert c1 is not None + assert c2 is not None + if not IN_CONTAINER: # TODO + assert c1.uri == c2.uri + assert c1.username == c2.username + assert c1.password == c2.password + + @classmethod + def assert_subaddress_equal(cls, subaddress: Optional[MoneroSubaddress], other: Optional[MoneroSubaddress]): + if subaddress is None and other is None: + return + assert not (subaddress is None or other is None) + assert subaddress.address == other.address + assert subaddress.account_index == other.account_index + assert subaddress.balance == other.balance + assert subaddress.index == other.index + assert subaddress.is_used == other.is_used + assert subaddress.label == other.label + assert subaddress.num_blocks_to_unlock == other.num_blocks_to_unlock + assert subaddress.num_unspent_outputs == other.num_unspent_outputs + assert subaddress.unlocked_balance == other.unlocked_balance + + @classmethod + def assert_subaddresses_equal(cls, subaddresses1: list[MoneroSubaddress], subaddresses2: list[MoneroSubaddress]): + size1 = len(subaddresses1) + size2 = len(subaddresses2) + if size1 != size2: + raise Exception("Number of subaddresses doesn't match") + + i = 0 + + while i < size1: + cls.assert_subaddress_equal(subaddresses1[i], subaddresses2[i]) + i += 1 diff --git a/tests/utils/block_utils.py b/tests/utils/block_utils.py new file mode 100644 index 0000000..2577c4c --- /dev/null +++ b/tests/utils/block_utils.py @@ -0,0 +1,117 @@ +import logging + +from typing import Optional +from abc import ABC +from monero import ( + MoneroBlockHeader, MoneroBlock, MoneroDaemonRpc +) + +from .binary_block_context import BinaryBlockContext +from .test_context import TestContext +from .assert_utils import AssertUtils +from .tx_utils import TxUtils + +logger: logging.Logger = logging.getLogger("BlockUtils") + + +class BlockUtils(ABC): + + @classmethod + def test_block_header(cls, header: MoneroBlockHeader, is_full: Optional[bool]): + AssertUtils.assert_not_none(header) + assert header.height is not None + AssertUtils.assert_true(header.height >= 0) + assert header.major_version is not None + AssertUtils.assert_true(header.major_version > 0) + assert header.minor_version is not None + AssertUtils.assert_true(header.minor_version >= 0) + assert header.timestamp is not None + if header.height == 0: + AssertUtils.assert_true(header.timestamp == 0) + else: + AssertUtils.assert_true(header.timestamp > 0) + AssertUtils.assert_not_none(header.prev_hash) + AssertUtils.assert_not_none(header.nonce) + if header.nonce == 0: + # TODO (monero-project): why is header nonce 0? + logger.warning(f"header nonce is 0 at height {header.height}") + else: + assert header.nonce is not None + AssertUtils.assert_true(header.nonce > 0) + AssertUtils.assert_is_none(header.pow_hash) # never seen defined + if is_full: + assert header.size is not None + assert header.depth is not None + assert header.difficulty is not None + assert header.cumulative_difficulty is not None + assert header.hash is not None + assert header.miner_tx_hash is not None + assert header.num_txs is not None + assert header.weight is not None + AssertUtils.assert_true(header.size > 0) + AssertUtils.assert_true(header.depth >= 0) + AssertUtils.assert_true(header.difficulty > 0) + AssertUtils.assert_true(header.cumulative_difficulty > 0) + AssertUtils.assert_equals(64, len(header.hash)) + AssertUtils.assert_equals(64, len(header.miner_tx_hash)) + AssertUtils.assert_true(header.num_txs >= 0) + AssertUtils.assert_not_none(header.orphan_status) + AssertUtils.assert_not_none(header.reward) + AssertUtils.assert_not_none(header.weight) + AssertUtils.assert_true(header.weight > 0) + else: + AssertUtils.assert_is_none(header.size) + AssertUtils.assert_is_none(header.depth) + AssertUtils.assert_is_none(header.difficulty) + AssertUtils.assert_is_none(header.cumulative_difficulty) + AssertUtils.assert_is_none(header.hash) + AssertUtils.assert_is_none(header.miner_tx_hash) + AssertUtils.assert_is_none(header.num_txs) + AssertUtils.assert_is_none(header.orphan_status) + AssertUtils.assert_is_none(header.reward) + AssertUtils.assert_is_none(header.weight) + + @classmethod + def test_block(cls, block: Optional[MoneroBlock], ctx: TestContext): + # test required fields + assert block is not None, "Expected MoneroBlock, got None" + assert block.miner_tx is not None, "Expected block miner tx" + TxUtils.test_miner_tx(block.miner_tx) # TODO: miner tx doesn't have as much stuff, can't call testTx? + cls.test_block_header(block, ctx.header_is_full) + + if ctx.has_hex: + assert block.hex is not None + AssertUtils.assert_true(len(block.hex) > 1) + else: + AssertUtils.assert_is_none(block.hex) + + if ctx.has_txs: + AssertUtils.assert_not_none(ctx.tx_context) + for tx in block.txs: + AssertUtils.assert_true(block == tx.block) + TxUtils.test_tx(tx, ctx.tx_context) + + else: + AssertUtils.assert_is_none(ctx.tx_context) + assert len(block.txs) == 0, "No txs expected" + + @classmethod + def test_get_blocks_range( + cls, + daemon: MoneroDaemonRpc, + start_height: Optional[int], + end_height: Optional[int], + chain_height: int, + chunked: bool, + block_ctx: BinaryBlockContext + ) -> None: + # fetch blocks by range + real_start_height = 0 if start_height is None else start_height + real_end_height = chain_height - 1 if end_height is None else end_height + blocks = daemon.get_blocks_by_range_chunked(start_height, end_height) if chunked else daemon.get_blocks_by_range(start_height, end_height) + AssertUtils.assert_equals(real_end_height - real_start_height + 1, len(blocks)) + + # test each block + for i, block in enumerate(blocks): + AssertUtils.assert_equals(real_start_height + i, block.height) + cls.test_block(block, block_ctx) diff --git a/tests/utils/daemon_utils.py b/tests/utils/daemon_utils.py new file mode 100644 index 0000000..1132695 --- /dev/null +++ b/tests/utils/daemon_utils.py @@ -0,0 +1,318 @@ +import logging + +from abc import ABC +from typing import Optional, Union, Any +from monero import ( + MoneroPeer, MoneroDaemonInfo, MoneroDaemonSyncInfo, + MoneroConnectionSpan, MoneroHardForkInfo, + MoneroAltChain, MoneroBan, MoneroMinerTxSum, + MoneroTxPoolStats, MoneroBlockTemplate, + MoneroDaemonUpdateCheckResult, MoneroDaemonUpdateDownloadResult, + MoneroNetworkType, MoneroRpcConnection +) + +from .gen_utils import GenUtils +from .assert_utils import AssertUtils + +logger: logging.Logger = logging.getLogger("DaemonUtils") + + +class DaemonUtils(ABC): + + @classmethod + def network_type_to_str(cls, nettype: MoneroNetworkType) -> str: + if nettype == MoneroNetworkType.MAINNET: + return "mainnet" + elif nettype == MoneroNetworkType.TESTNET: + return "testnet" + elif nettype == MoneroNetworkType.STAGENET: + return "stagenet" + + raise TypeError(f"Invalid network type provided: {str(nettype)}") + + @classmethod + def is_regtest(cls, network_type_str: Optional[str]) -> bool: + if network_type_str is None: + return False + nettype = network_type_str.lower() + return nettype == "regtest" or nettype == "reg" + + @classmethod + def parse_network_type(cls, nettype: str) -> MoneroNetworkType: + net = nettype.lower() + if net == "mainnet" or net == "main" or cls.is_regtest(net): + return MoneroNetworkType.MAINNET + elif net == "testnet" or net == "test": + return MoneroNetworkType.TESTNET + elif net == "stagenet" or net == "stage": + return MoneroNetworkType.STAGENET + + raise TypeError(f"Invalid network type provided: {str(nettype)}") + + @classmethod + def test_known_peer(cls, peer: Optional[MoneroPeer], from_connection: bool): + assert peer is not None, "Peer is null" + assert peer.id is not None + assert peer.host is not None + assert peer.port is not None + AssertUtils.assert_false(len(peer.id) == 0) + AssertUtils.assert_false(len(peer.host) == 0) + AssertUtils.assert_true(peer.port > 0) + AssertUtils.assert_true(peer.rpc_port is None or peer.rpc_port >= 0) + AssertUtils.assert_not_none(peer.is_online) + if peer.rpc_credits_per_hash is not None: + GenUtils.test_unsigned_big_integer(peer.rpc_credits_per_hash) + if from_connection: + AssertUtils.assert_is_none(peer.last_seen_timestamp) + else: + assert peer.last_seen_timestamp is not None + + if peer.last_seen_timestamp < 0: + logger.warning(f"Last seen timestamp is invalid: {peer.last_seen_timestamp}") + AssertUtils.assert_true(peer.last_seen_timestamp >= 0) + + AssertUtils.assert_true(peer.pruning_seed is None or peer.pruning_seed >= 0) + + @classmethod + def test_peer(cls, peer: Union[Any, MoneroPeer]): + AssertUtils.assert_true(isinstance(peer, MoneroPeer)) + cls.test_known_peer(peer, True) + assert peer.hash is not None + assert peer.avg_download is not None + assert peer.avg_upload is not None + assert peer.current_download is not None + assert peer.current_upload is not None + assert peer.height is not None + assert peer.live_time is not None + assert peer.num_receives is not None + assert peer.receive_idle_time is not None + assert peer.num_sends is not None + assert peer.send_idle_time is not None + assert peer.num_support_flags is not None + + AssertUtils.assert_false(len(peer.hash) == 0) + AssertUtils.assert_true(peer.avg_download >= 0) + AssertUtils.assert_true(peer.avg_upload >= 0) + AssertUtils.assert_true(peer.current_download >= 0) + AssertUtils.assert_true(peer.current_upload >= 0) + AssertUtils.assert_true(peer.height >= 0) + AssertUtils.assert_true(peer.live_time >= 0) + AssertUtils.assert_not_none(peer.is_local_ip) + AssertUtils.assert_not_none(peer.is_local_host) + AssertUtils.assert_true(peer.num_receives >= 0) + AssertUtils.assert_true(peer.receive_idle_time >= 0) + AssertUtils.assert_true(peer.num_sends >= 0) + AssertUtils.assert_true(peer.send_idle_time >= 0) + AssertUtils.assert_not_none(peer.state) + AssertUtils.assert_true(peer.num_support_flags >= 0) + AssertUtils.assert_not_none(peer.connection_type) + + @classmethod + def test_info(cls, info: MoneroDaemonInfo): + assert info.num_alt_blocks is not None + assert info.block_size_limit is not None + assert info.block_size_median is not None + assert info.num_offline_peers is not None + assert info.num_online_peers is not None + assert info.height is not None + assert info.height_without_bootstrap is not None + assert info.num_incoming_connections is not None + assert info.num_outgoing_connections is not None + assert info.num_rpc_connections is not None + assert info.start_timestamp is not None + assert info.adjusted_timestamp is not None + assert info.target is not None + assert info.target_height is not None + assert info.num_txs is not None + assert info.num_txs_pool is not None + assert info.block_weight_limit is not None + assert info.block_weight_median is not None + assert info.database_size is not None + AssertUtils.assert_not_none(info.version) + AssertUtils.assert_true(info.num_alt_blocks >= 0) + AssertUtils.assert_true(info.block_size_limit > 0) + AssertUtils.assert_true(info.block_size_median > 0) + AssertUtils.assert_true(info.bootstrap_daemon_address is None or not GenUtils.is_empty(info.bootstrap_daemon_address)) + GenUtils.test_unsigned_big_integer(info.cumulative_difficulty) + GenUtils.test_unsigned_big_integer(info.free_space) + AssertUtils.assert_true(info.num_offline_peers >= 0) + AssertUtils.assert_true(info.num_online_peers >= 0) + AssertUtils.assert_true(info.height >= 0) + AssertUtils.assert_true(info.height_without_bootstrap > 0) + AssertUtils.assert_true(info.num_incoming_connections >= 0) + AssertUtils.assert_not_none(info.network_type) + AssertUtils.assert_not_none(info.is_offline) + AssertUtils.assert_true(info.num_outgoing_connections >= 0) + AssertUtils.assert_true(info.num_rpc_connections >= 0) + AssertUtils.assert_true(info.start_timestamp > 0) + AssertUtils.assert_true(info.adjusted_timestamp > 0) + AssertUtils.assert_true(info.target > 0) + AssertUtils.assert_true(info.target_height >= 0) + AssertUtils.assert_true(info.num_txs >= 0) + AssertUtils.assert_true(info.num_txs_pool >= 0) + AssertUtils.assert_not_none(info.was_bootstrap_ever_used) + AssertUtils.assert_true(info.block_weight_limit > 0) + AssertUtils.assert_true(info.block_weight_median > 0) + AssertUtils.assert_true(info.database_size > 0) + AssertUtils.assert_not_none(info.update_available) + GenUtils.test_unsigned_big_integer(info.credits, False) # 0 credits + AssertUtils.assert_false(GenUtils.is_empty(info.top_block_hash)) + AssertUtils.assert_not_none(info.is_busy_syncing) + AssertUtils.assert_not_none(info.is_synchronized) + + @classmethod + def test_sync_info(cls, sync_info: Union[Any, MoneroDaemonSyncInfo]): + AssertUtils.assert_true(isinstance(sync_info, MoneroDaemonSyncInfo)) + assert sync_info.height is not None + AssertUtils.assert_true(sync_info.height >= 0) + + for connection in sync_info.peers: + cls.test_peer(connection) + + for span in sync_info.spans: + cls.test_connection_span(span) + + assert sync_info.next_needed_pruning_seed is not None + AssertUtils.assert_true(sync_info.next_needed_pruning_seed >= 0) + AssertUtils.assert_is_none(sync_info.overview) + GenUtils.test_unsigned_big_integer(sync_info.credits, False) # 0 credits + AssertUtils.assert_is_none(sync_info.top_block_hash) + + @classmethod + def test_connection_span(cls, span: Union[MoneroConnectionSpan, Any]) -> None: + raise NotImplementedError() + + @classmethod + def test_hard_fork_info(cls, hard_fork_info: MoneroHardForkInfo): + AssertUtils.assert_not_none(hard_fork_info.earliest_height) + AssertUtils.assert_not_none(hard_fork_info.is_enabled) + AssertUtils.assert_not_none(hard_fork_info.state) + AssertUtils.assert_not_none(hard_fork_info.threshold) + AssertUtils.assert_not_none(hard_fork_info.version) + AssertUtils.assert_not_none(hard_fork_info.num_votes) + AssertUtils.assert_not_none(hard_fork_info.voting) + AssertUtils.assert_not_none(hard_fork_info.window) + GenUtils.test_unsigned_big_integer(hard_fork_info.credits, False) # 0 credits + AssertUtils.assert_is_none(hard_fork_info.top_block_hash) + + @classmethod + def test_alt_chain(cls, alt_chain: MoneroAltChain): + AssertUtils.assert_not_none(alt_chain) + AssertUtils.assert_false(len(alt_chain.block_hashes) == 0) + GenUtils.test_unsigned_big_integer(alt_chain.difficulty, True) + assert alt_chain.height is not None + assert alt_chain.length is not None + assert alt_chain.main_chain_parent_block_hash is not None + AssertUtils.assert_true(alt_chain.height > 0) + AssertUtils.assert_true(alt_chain.length > 0) + AssertUtils.assert_equals(64, len(alt_chain.main_chain_parent_block_hash)) + + @classmethod + def test_ban(cls, ban: Optional[MoneroBan]) -> None: + assert ban is not None + assert ban.host is not None + assert ban.ip is not None + assert ban.seconds is not None + + @classmethod + def test_miner_tx_sum(cls, tx_sum: Optional[MoneroMinerTxSum]) -> None: + assert tx_sum is not None + GenUtils.test_unsigned_big_integer(tx_sum.emission_sum, True) + GenUtils.test_unsigned_big_integer(tx_sum.fee_sum, True) + + @classmethod + def test_tx_pool_stats(cls, stats: MoneroTxPoolStats): + AssertUtils.assert_not_none(stats) + assert stats.num_txs is not None + AssertUtils.assert_true(stats.num_txs >= 0) + if stats.num_txs > 0: + + assert stats.bytes_max is not None + assert stats.bytes_med is not None + assert stats.bytes_min is not None + assert stats.bytes_total is not None + assert stats.oldest_timestamp is not None + assert stats.num10m is not None + assert stats.num_double_spends is not None + assert stats.num_failing is not None + assert stats.num_not_relayed is not None + + AssertUtils.assert_true(stats.bytes_max > 0) + AssertUtils.assert_true(stats.bytes_med > 0) + AssertUtils.assert_true(stats.bytes_min > 0) + AssertUtils.assert_true(stats.bytes_total > 0) + AssertUtils.assert_true(stats.histo98pc is None or stats.histo98pc > 0) + AssertUtils.assert_true(stats.oldest_timestamp > 0) + AssertUtils.assert_true(stats.num10m >= 0) + AssertUtils.assert_true(stats.num_double_spends >= 0) + AssertUtils.assert_true(stats.num_failing >= 0) + AssertUtils.assert_true(stats.num_not_relayed >= 0) + + else: + AssertUtils.assert_is_none(stats.bytes_max) + AssertUtils.assert_is_none(stats.bytes_med) + AssertUtils.assert_is_none(stats.bytes_min) + AssertUtils.assert_equals(0, stats.bytes_total) + AssertUtils.assert_is_none(stats.histo98pc) + AssertUtils.assert_is_none(stats.oldest_timestamp) + AssertUtils.assert_equals(0, stats.num10m) + AssertUtils.assert_equals(0, stats.num_double_spends) + AssertUtils.assert_equals(0, stats.num_failing) + AssertUtils.assert_equals(0, stats.num_not_relayed) + #AssertUtils.assert_is_none(stats.histo) + + @classmethod + def test_rpc_connection(cls, connection: Optional[MoneroRpcConnection], uri: Optional[str], connected: bool = True) -> None: + assert connection is not None + assert uri is not None + assert len(uri) > 0 + assert connection.uri == uri + assert connection.check_connection() == connected + assert connection.is_connected() == connected + assert connection.is_online() == connected + + @classmethod + def test_block_template(cls, template: MoneroBlockTemplate): + AssertUtils.assert_not_none(template) + AssertUtils.assert_not_none(template.block_template_blob) + AssertUtils.assert_not_none(template.block_hashing_blob) + AssertUtils.assert_not_none(template.difficulty) + AssertUtils.assert_not_none(template.expected_reward) + AssertUtils.assert_not_none(template.height) + AssertUtils.assert_not_none(template.prev_hash) + AssertUtils.assert_not_none(template.reserved_offset) + AssertUtils.assert_not_none(template.seed_height) + assert template.seed_height is not None + AssertUtils.assert_true(template.seed_height >= 0) + AssertUtils.assert_not_none(template.seed_hash) + AssertUtils.assert_false(template.seed_hash == "") + # next seed hash can be null or initialized TODO: test circumstances for each + + @classmethod + def test_update_check_result(cls, result: Union[Any, MoneroDaemonUpdateCheckResult]): + assert result is not None + AssertUtils.assert_true(isinstance(result, MoneroDaemonUpdateCheckResult)) + AssertUtils.assert_not_none(result.is_update_available) + if result.is_update_available: + AssertUtils.assert_false(GenUtils.is_empty(result.auto_uri), "No auto uri is daemon online?") + AssertUtils.assert_false(GenUtils.is_empty(result.user_uri)) + AssertUtils.assert_false(GenUtils.is_empty(result.version)) + AssertUtils.assert_false(GenUtils.is_empty(result.hash)) + assert result.hash is not None + AssertUtils.assert_equals(64, len(result.hash)) + else: + AssertUtils.assert_is_none(result.auto_uri) + AssertUtils.assert_is_none(result.user_uri) + AssertUtils.assert_is_none(result.version) + AssertUtils.assert_is_none(result.hash) + + @classmethod + def test_update_download_result(cls, result: MoneroDaemonUpdateDownloadResult, path: Optional[str]): + cls.test_update_check_result(result) + if result.is_update_available: + if path is not None: + AssertUtils.assert_equals(path, result.download_path) + else: + AssertUtils.assert_not_none(result.download_path) + else: + AssertUtils.assert_is_none(result.download_path) diff --git a/tests/utils/gen_utils.py b/tests/utils/gen_utils.py new file mode 100644 index 0000000..820a1f5 --- /dev/null +++ b/tests/utils/gen_utils.py @@ -0,0 +1,41 @@ +from typing import Union, Any +from abc import ABC +from time import sleep, time +from os import makedirs +from os.path import exists as path_exists + + + +class GenUtils(ABC): + + @classmethod + def create_dir_if_not_exists(cls, dir_path: str) -> None: + if path_exists(dir_path): + return + + makedirs(dir_path) + + @classmethod + def wait_for(cls, milliseconds: int): + sleep(milliseconds / 1000) + + @classmethod + def is_empty(cls, value: Union[str, list[Any], None]) -> bool: + return value == "" + + @classmethod + def test_unsigned_big_integer(cls, value: Any, bool_val: bool = False): + if not isinstance(value, int): + raise Exception(f"Value is not number: {value}") + + if value < 0: + raise Exception("Value cannot be negative") + + @classmethod + def current_timestamp(cls) -> int: + return round(time() * 1000) + + @classmethod + def current_timestamp_str(cls) -> str: + return f"{cls.current_timestamp()}" + diff --git a/tests/utils/mining_utils.py b/tests/utils/mining_utils.py index 8d92253..55023c6 100644 --- a/tests/utils/mining_utils.py +++ b/tests/utils/mining_utils.py @@ -2,7 +2,10 @@ from typing import Optional from time import sleep -from monero import MoneroDaemonRpc +from monero import ( + MoneroDaemonRpc, MoneroWallet, MoneroUtils, + MoneroDestination, MoneroTxConfig +) from .test_utils import TestUtils as Utils from .string_utils import StringUtils @@ -111,21 +114,96 @@ def wait_for_height(cls, height: int) -> int: block = daemon.wait_for_next_block_header() assert block.height is not None current_height = block.height - sleep(3) + sleep(5) if stop_mining: cls.stop_mining() - sleep(3) + sleep(5) current_height = daemon.get_height() - logger.info(f"Blockchain height: {current_height}") + logger.info(f"[100%] Reached blockchain height: {current_height}") return current_height @classmethod - def wait_until_blockchain_ready(cls) -> None: + def wait_until_blockchain_ready(cls) -> int: """ Wait until blockchain is ready. """ - cls.wait_for_height(Utils.MIN_BLOCK_HEIGHT) + height = cls.wait_for_height(Utils.MIN_BLOCK_HEIGHT) + cls.try_stop_mining() + return height + @classmethod + def has_reached_height(cls, height: int) -> bool: + """Check if blockchain has reached height""" + return height == cls._get_daemon().get_height() + + @classmethod + def blockchain_is_ready(cls) -> bool: + """Indicates if blockchain has reached minimum height for running tests""" + return cls.has_reached_height(Utils.MIN_BLOCK_HEIGHT) + + @classmethod + def is_wallet_funded(cls, wallet: MoneroWallet, xmr_amount_per_address: float, num_subaddresses: int = 10) -> bool: + """Check if wallet has required funds""" + amount_per_address = MoneroUtils.xmr_to_atomic_units(xmr_amount_per_address) + amount_required = amount_per_address * (num_subaddresses + 1) # include primary address + wallet.sync() + + if wallet.get_balance() < amount_required: + return False + + accounts = wallet.get_accounts(True) + subaddresses_found: int = 0 + for account in accounts: + for subaddress in account.subaddresses: + balance = subaddress.unlocked_balance + assert balance is not None + if balance >= amount_per_address: + subaddresses_found += 1 + + return subaddresses_found >= num_subaddresses + 1 + + @classmethod + def fund_wallet(cls, wallet: MoneroWallet, xmr_amount_per_address: float, num_subaddresses: int = 10) -> None: + """Fund a wallet with mined coins""" + primary_addr = wallet.get_primary_address() + if cls.is_wallet_funded(wallet, xmr_amount_per_address, num_subaddresses): + logger.info(f"Already funded wallet {primary_addr}") + return + + amount_per_address = MoneroUtils.xmr_to_atomic_units(xmr_amount_per_address) + amount_required = amount_per_address * (num_subaddresses + 1) # include primary address + amount_required_str = f"{MoneroUtils.atomic_units_to_xmr(amount_required)} XMR" + + logger.info(f"Funding wallet {primary_addr}...") + + tx_config = MoneroTxConfig() + tx_config.account_index = 0 + tx_config.relay = True + + account_idx = 0 + account = wallet.get_account(account_idx) + num_subaddr = len(account.subaddresses) + + while num_subaddr < num_subaddresses: + wallet.create_subaddress(account_idx) + num_subaddr += 1 + + addresses = wallet.get_subaddresses(0, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + for address in addresses: + assert address.address is not None + dest = MoneroDestination(address.address, amount_per_address) + tx_config.destinations.append(dest) + + mining_wallet = Utils.get_mining_wallet() + wallet_balance = mining_wallet.get_balance() + err_msg = f"Mining wallet doesn't have enough balance: {MoneroUtils.atomic_units_to_xmr(wallet_balance)}" + assert wallet_balance > amount_required, err_msg + tx = mining_wallet.create_tx(tx_config) + assert tx.is_failed is False, "Cannot fund wallet: tx failed" + logger.info(f"Funded test wallet {primary_addr} with {amount_required_str} in tx {tx.hash}") + height = cls._get_daemon().get_height() + cls.wait_for_height(height + 11) + wallet.sync() diff --git a/tests/utils/string_utils.py b/tests/utils/string_utils.py index 630c470..7d1d12f 100644 --- a/tests/utils/string_utils.py +++ b/tests/utils/string_utils.py @@ -3,16 +3,20 @@ class StringUtils(ABC): + """Strin utilities""" @classmethod def get_percentage(cls, n: int, m: int, precision: int = 2) -> str: + """Get percentage in readable format""" r: float = (n / m)*100 return cls.get_percentage_float(r, precision) @classmethod def get_percentage_float(cls, n: float, precision: int = 2) -> str: + """Get percentage in readable format""" return f"{round(n, precision)}%" @classmethod def get_random_string(cls, n: int = 25) -> str: + """Generate random string""" return token_hex(n) diff --git a/tests/utils/test_context.py b/tests/utils/test_context.py index 66106c7..b92676b 100644 --- a/tests/utils/test_context.py +++ b/tests/utils/test_context.py @@ -3,12 +3,48 @@ class TestContext: - has_hex: bool = True - has_txs: bool = False - header_is_full: bool = True + """Provides context or configuration for test methods to test a type.""" + + __test__ = False + + has_json: Optional[bool] = None + """Expect presence of json field""" + is_pruned: Optional[bool] = None + """Expect pruning""" + is_full: Optional[bool] = None + """Expect complete model""" + is_confirmed: Optional[bool] = None + """Expect confirmations""" + is_miner_tx: Optional[bool] = None + """Expect miner tx""" + from_get_tx_pool: Optional[bool] = None + """Expect from tx pool""" + from_binary_block: Optional[bool] = None + """Expect from binary block""" + has_output_indices: Optional[bool] = None + """Expect output indices""" + do_not_test_copy: Optional[bool] = None + """Diable copy tests""" + has_txs: Optional[bool] = None + """Expect txs""" + has_hex: Optional[bool] = None + """Expect hex field""" + header_is_full: Optional[bool] = None + """Expect full header""" tx_context: Optional[TestContext] = None - is_pruned: bool = False - is_confirmed: bool = False - from_get_tx_pool: bool = False - has_output_indices: bool = False - from_binary_block: bool = False + """Tx context""" + + def __init__(self, ctx: Optional[TestContext] = None) -> None: + if ctx is not None: + self.has_json = ctx.has_json + self.is_pruned = ctx.is_pruned + self.is_full = ctx.is_full + self.is_confirmed = ctx.is_confirmed + self.is_miner_tx = ctx.is_miner_tx + self.from_get_tx_pool = ctx.from_get_tx_pool + self.from_binary_block = ctx.from_binary_block + self.has_output_indices = ctx.has_output_indices + self.do_not_test_copy = ctx.do_not_test_copy + self.has_txs = ctx.has_txs + self.header_is_full = ctx.header_is_full + self.tx_context = ctx.tx_context diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index fd26134..b93d3ff 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -1,36 +1,33 @@ import logging -from typing import Any, Optional, Union +from typing import Optional from abc import ABC -from random import shuffle -from time import sleep, time from os.path import exists as path_exists from os import makedirs, getenv from configparser import ConfigParser from monero import ( - MoneroNetworkType, MoneroTx, MoneroUtils, MoneroWalletFull, MoneroRpcConnection, - MoneroWalletConfig, MoneroDaemonRpc, MoneroWalletRpc, MoneroBlockHeader, MoneroBlockTemplate, - MoneroBlock, MoneroDaemonUpdateCheckResult, MoneroDaemonUpdateDownloadResult, MoneroWalletKeys, - MoneroSubaddress, MoneroPeer, MoneroDaemonInfo, MoneroDaemonSyncInfo, MoneroHardForkInfo, - MoneroAltChain, MoneroTxPoolStats, MoneroWallet, MoneroRpcError, MoneroTxConfig, MoneroBan, - MoneroAccount, MoneroTxWallet, MoneroTxQuery, MoneroConnectionSpan, SerializableStruct, - MoneroMinerTxSum, MoneroDaemon + MoneroNetworkType, MoneroWalletFull, MoneroRpcConnection, + MoneroWalletConfig, MoneroDaemonRpc, MoneroWalletRpc, MoneroWalletKeys, + MoneroWallet, MoneroRpcError ) from .wallet_sync_printer import WalletSyncPrinter from .wallet_tx_tracker import WalletTxTracker -from .test_context import TestContext -from .tx_context import TxContext -from .binary_block_context import BinaryBlockContext +from .gen_utils import GenUtils from .os_utils import OsUtils from .string_utils import StringUtils +from .assert_utils import AssertUtils +from .daemon_utils import DaemonUtils logger: logging.Logger = logging.getLogger("TestUtils") class TestUtils(ABC): + """Test utilities and constants""" + __test__ = False _LOADED: bool = False + """Indicates if test configuration is loaded""" IN_CONTAINER: bool = True """indicates if tests are running in docker container""" @@ -51,48 +48,74 @@ class TestUtils(ABC): """Additional wallet rpc instance""" DAEMON_RPC_URI: str = "" + """Monero daemon rpc uri""" CONTAINER_DAEMON_RPC_URI: str = "" - """monero daemon rpc endpoint configuration (change per your configuration)""" + """Monero daemon rpc endpoint configuration (change per your configuration)""" DAEMON_RPC_USERNAME: str = "" + """Monero daemon rpc username""" DAEMON_RPC_PASSWORD: str = "" + """Monero daemon rpc password""" TEST_NON_RELAYS: bool = True + """Indicates if relays tests are enabled""" LITE_MODE: bool = False + """Indicates if running tests in light mode""" TEST_NOTIFICATIONS: bool = True + """Indicates if notifications tests are enabled""" WALLET_TX_TRACKER: WalletTxTracker + """Test wallet tx tracker""" # monero wallet rpc configuration (change per your configuration) WALLET_RPC_PORT_START: int = 18082 """test wallet executables will bind to consecutive ports after these""" WALLET_RPC_ZMQ_ENABLED: bool = False + """Indicates if test wallet rpc zmq is enabled""" WALLET_RPC_ZMQ_PORT_START: int = 58083 WALLET_RPC_ZMQ_BIND_PORT_START: int = 48083 # TODO: zmq bind port necessary? WALLET_RPC_USERNAME: str = "" + """Test wallet rpc username""" WALLET_RPC_PASSWORD: str = "" + """Test wallet rpc password""" WALLET_RPC_ZMQ_DOMAIN: str = "" + """Test wallet rpc zmq domain""" WALLET_RPC_DOMAIN: str = "" + """Test wallet rpc domain""" WALLET_RPC_URI: str = "" + """Test wallet rpc uri""" WALLET_RPC_URI_2: str = "" WALLET_RPC_ZMQ_URI: str = "" + """Test wallet rpc zmq uri""" WALLET_RPC_ACCESS_CONTROL_ORIGINS: str = "" """cors access from web browser""" # test wallet config WALLET_NAME: str = "" + """Test wallet name""" WALLET_PASSWORD: str = "" + """Test wallet password""" TEST_WALLETS_DIR: str = "" + """Directory containing wallets used in tests""" WALLET_FULL_PATH: str = "" + """Test wallet full path""" # test wallet constants - MAX_FEE = 7500000*10000 NETWORK_TYPE: MoneroNetworkType = MoneroNetworkType.MAINNET + """Test network type""" REGTEST: bool = False + """Indicates if running on fakechain""" LANGUAGE: str = "" + """Test wallet language""" SEED: str = "" + """Test wallet seed""" ADDRESS: str = "" + """Test wallet primary address""" PRIVATE_VIEW_KEY: str = "" + """Test wallet private view key""" PRIVATE_SPEND_KEY: str = "" + """Test wallet private spend key""" PUBLIC_SPEND_KEY: str = "" + """Test wallet public spend key""" PUBLIC_VIEW_KEY: str = "" + """Test wallet public view key""" FIRST_RECEIVE_HEIGHT: int = 0 """NOTE: this value must be the height of the wallet's first tx for tests""" SYNC_PERIOD_IN_MS: int = 5000 @@ -100,21 +123,32 @@ class TestUtils(ABC): OFFLINE_SERVER_URI: str = "offline_server_uri" """dummy server uri to remain offline because wallet2 connects to default if not given""" AUTO_CONNECT_TIMEOUT_MS: int = 3000 + """Default connection timeout in milliseconds""" # mining wallet config MINING_WALLET_NAME: str = "" + """Mining wallet name""" MINING_WALLET_PASSWORD: str = "" + """Mining wallet password""" MINING_SEED: str = "" + """Mining wallet seed""" MINING_ADDRESS: str = "" + """Mining wallet primary address""" MINING_PRIVATE_VIEW_KEY: str = "" + """Mining wallet private view key""" MINING_PRIVATE_SPEND_KEY: str = "" + """Mining wallet private spend key""" MINING_PUBLIC_SPEND_KEY: str = "" + """Mining wallet public spend key""" MINING_PUBLIC_VIEW_KEY: str = "" + """Mining wallet public view key""" + MINING_WALLET_FULL_PATH: str = "" + """Mining wallet full path""" @classmethod def load_config(cls) -> None: """ - Load utils configuration from tests/config/config.ini + Load tests configuration from `tests/config/config.ini` """ if cls._LOADED: return @@ -133,8 +167,8 @@ def load_config(cls) -> None: cls.TEST_NOTIFICATIONS = parser.getboolean('general', 'test_notifications') cls.LITE_MODE = parser.getboolean('general', 'lite_mode') cls.AUTO_CONNECT_TIMEOUT_MS = parser.getint('general', 'auto_connect_timeout_ms') - cls.NETWORK_TYPE = cls.parse_network_type(nettype_str) - cls.REGTEST = cls.is_regtest(nettype_str) + cls.NETWORK_TYPE = DaemonUtils.parse_network_type(nettype_str) + cls.REGTEST = DaemonUtils.is_regtest(nettype_str) cls.WALLET_TX_TRACKER = WalletTxTracker(cls.MINING_ADDRESS) if cls.REGTEST: @@ -177,6 +211,7 @@ def load_config(cls) -> None: # parse mining wallet config cls.MINING_WALLET_NAME = parser.get('mining_wallet', 'name') + cls.MINING_WALLET_FULL_PATH = cls.TEST_WALLETS_DIR + "/" + cls.MINING_WALLET_NAME cls.MINING_WALLET_PASSWORD = parser.get('mining_wallet', 'password') cls.MINING_ADDRESS = parser.get('mining_wallet', 'address') cls.MINING_PRIVATE_VIEW_KEY = parser.get('mining_wallet', 'private_view_key') @@ -185,119 +220,35 @@ def load_config(cls) -> None: cls.MINING_PUBLIC_SPEND_KEY = parser.get('mining_wallet', 'public_spend_key') cls.MINING_SEED = parser.get('mining_wallet', 'seed') - cls._LOADED = True - - @classmethod - def current_timestamp(cls) -> int: - return round(time() * 1000) - - @classmethod - def current_timestamp_str(cls) -> str: - return f"{cls.current_timestamp()}" - - @classmethod - def network_type_to_str(cls, nettype: MoneroNetworkType) -> str: - if nettype == MoneroNetworkType.MAINNET: - return "mainnet" - elif nettype == MoneroNetworkType.TESTNET: - return "testnet" - elif nettype == MoneroNetworkType.STAGENET: - return "stagenet" - - raise TypeError(f"Invalid network type provided: {str(nettype)}") - - @classmethod - def is_regtest(cls, network_type_str: Optional[str]) -> bool: - if network_type_str is None: - return False - nettype = network_type_str.lower() - return nettype == "regtest" or nettype == "reg" - - @classmethod - def parse_network_type(cls, nettype: str) -> MoneroNetworkType: - net = nettype.lower() - if net == "mainnet" or net == "main" or cls.is_regtest(net): - return MoneroNetworkType.MAINNET - elif net == "testnet" or net == "test": - return MoneroNetworkType.TESTNET - elif net == "stagenet" or net == "stage": - return MoneroNetworkType.STAGENET + # create directory for test wallets if it doesn't exist + cls.initialize_test_wallet_dir() - raise TypeError(f"Invalid network type provided: {str(nettype)}") + cls._LOADED = True @classmethod def get_network_type(cls) -> str: - return cls.network_type_to_str(cls.NETWORK_TYPE) - - @classmethod - def create_dir_if_not_exists(cls, dir_path: str) -> None: - if path_exists(dir_path): - return - - makedirs(dir_path) + """Get test network type""" + return DaemonUtils.network_type_to_str(cls.NETWORK_TYPE) @classmethod def initialize_test_wallet_dir(cls) -> None: - cls.create_dir_if_not_exists(cls.TEST_WALLETS_DIR) - - @classmethod - def assert_false(cls, expr: Any, message: str = "assertion failed"): - assert expr is False, message - - @classmethod - def assert_true(cls, expr: Any, message: str = "assertion failed"): - assert expr is True, message - - @classmethod - def assert_not_none(cls, expr: Any, message: str = "assertion failed"): - assert expr is not None, message - - @classmethod - def assert_is_none(cls, expr: Any, message: str = "assertion failed"): - assert expr is None, message - - @classmethod - def assert_equals(cls, expr1: Any, expr2: Any, message: str = "assertion failed"): - if isinstance(expr1, SerializableStruct) and isinstance(expr2, SerializableStruct): - str1 = expr1.serialize() - str2 = expr2.serialize() - assert str1 == str2, f"{message}: {str1} == {str2}" - elif isinstance(expr1, MoneroRpcConnection) and isinstance(expr2, MoneroRpcConnection): - cls.assert_connection_equals(expr1, expr2) - else: - assert expr1 == expr2, f"{message}: {expr1} == {expr2}" - - @classmethod - def assert_not_equals(cls, expr1: Any, expr2: Any, message: str = "assertion failed"): - assert expr1 != expr2, f"{message}: {expr1} != {expr2}" - - @classmethod - def assert_is(cls, expr: Any, what: Any, message: str = "assertion failed"): - assert expr is what, f"{message}: {expr} is {what}" - - @classmethod - def assert_not_supported(cls, error: Any) -> None: - assert "not supported" in str(error), f"Expected not supported method: {error}" - - @classmethod - def get_wallets(cls, wallet_type: str) -> list[MoneroWallet]: - raise NotImplementedError() - - @classmethod - def wait_for(cls, milliseconds: int): - sleep(milliseconds / 1000) + """Initialize test wallets directory""" + GenUtils.create_dir_if_not_exists(cls.TEST_WALLETS_DIR) @classmethod def check_test_wallets_dir_exists(cls) -> bool: + """Checks if tests wallets directory exists""" return path_exists(cls.TEST_WALLETS_DIR) @classmethod def create_test_wallets_dir(cls) -> None: + """Create test wallets directory""" makedirs(cls.TEST_WALLETS_DIR) @classmethod def get_daemon_rpc(cls) -> MoneroDaemonRpc: - if OsUtils.is_windows(): # TODO enable windows integration test env + """Get test daemon rpc""" + if OsUtils.is_windows(): return None # type: ignore if cls._DAEMON_RPC is None: @@ -305,8 +256,14 @@ def get_daemon_rpc(cls) -> MoneroDaemonRpc: return cls._DAEMON_RPC + @classmethod + def get_daemon_rpc_connection(cls) -> MoneroRpcConnection: + """Get test daemon rpc connection""" + return cls.get_daemon_rpc().get_rpc_connection() + @classmethod def get_wallet_keys_config(cls) -> MoneroWalletConfig: + """Get test wallet keys configuration""" config = MoneroWalletConfig() config.network_type = cls.NETWORK_TYPE config.seed = cls.SEED @@ -314,6 +271,7 @@ def get_wallet_keys_config(cls) -> MoneroWalletConfig: @classmethod def get_wallet_keys(cls) -> MoneroWalletKeys: + """Get test wallet keys""" if cls._WALLET_KEYS is None: config = cls.get_wallet_keys_config() cls._WALLET_KEYS = MoneroWalletKeys.create_wallet_from_seed(config) @@ -322,6 +280,7 @@ def get_wallet_keys(cls) -> MoneroWalletKeys: @classmethod def get_wallet_full_config(cls, daemon_connection: MoneroRpcConnection) -> MoneroWalletConfig: + """Get test wallet full configuration""" config = MoneroWalletConfig() config.path = cls.WALLET_FULL_PATH config.password = cls.WALLET_PASSWORD @@ -334,15 +293,13 @@ def get_wallet_full_config(cls, daemon_connection: MoneroRpcConnection) -> Moner @classmethod def get_wallet_full(cls) -> MoneroWalletFull: - if OsUtils.is_windows(): # TODO enable windows integration test env + """Get test wallet full""" + if OsUtils.is_windows(): return None # type: ignore if cls._WALLET_FULL is None: # create wallet from seed if it doesn't exist if not MoneroWalletFull.wallet_exists(cls.WALLET_FULL_PATH): - # create directory for test wallets if it doesn't exist - cls.initialize_test_wallet_dir() - # create wallet with connection daemon_connection = MoneroRpcConnection( cls.DAEMON_RPC_URI, cls.DAEMON_RPC_USERNAME, cls.DAEMON_RPC_PASSWORD @@ -358,7 +315,7 @@ def get_wallet_full(cls) -> MoneroWalletFull: cls._WALLET_FULL = MoneroWalletFull.open_wallet( cls.WALLET_FULL_PATH, cls.WALLET_PASSWORD, cls.NETWORK_TYPE ) - cls._WALLET_FULL.set_daemon_connection(cls.get_daemon_rpc().get_rpc_connection()) + cls._WALLET_FULL.set_daemon_connection(cls.get_daemon_rpc_connection()) # sync and save wallet if cls._WALLET_FULL.is_connected_to_daemon(): @@ -372,9 +329,44 @@ def get_wallet_full(cls) -> MoneroWalletFull: assert cls.ADDRESS == cls._WALLET_FULL.get_primary_address() return cls._WALLET_FULL + @classmethod + def get_mining_wallet_config(cls) -> MoneroWalletConfig: + """Get mining wallet configuration""" + connection = MoneroRpcConnection( + cls.DAEMON_RPC_URI, + cls.DAEMON_RPC_USERNAME, + cls.DAEMON_RPC_PASSWORD + ) + config = cls.get_wallet_full_config(connection) + config.path = cls.MINING_WALLET_FULL_PATH + config.password = cls.MINING_WALLET_PASSWORD + config.seed = cls.MINING_SEED + config.restore_height = 0 + return config + + @classmethod + def get_mining_wallet(cls) -> MoneroWalletFull: + """Get mining wallet""" + if not MoneroWalletFull.wallet_exists(cls.MINING_WALLET_FULL_PATH): + wallet = MoneroWalletFull.create_wallet(cls.get_mining_wallet_config()) + else: + wallet = MoneroWalletFull.open_wallet(cls.MINING_WALLET_FULL_PATH, cls.MINING_WALLET_PASSWORD, cls.NETWORK_TYPE) + wallet.set_daemon_connection(cls.get_daemon_rpc_connection()) + + assert wallet.is_connected_to_daemon(), "Mining wallet is not connected to daemon" + listener = WalletSyncPrinter() + wallet.sync(listener) + wallet.save() + wallet.start_syncing(cls.SYNC_PERIOD_IN_MS) + + assert cls.MINING_SEED == wallet.get_seed() + assert cls.MINING_ADDRESS == wallet.get_primary_address() + return wallet + @classmethod def get_wallet_rpc(cls) -> MoneroWalletRpc: - if OsUtils.is_windows(): # TODO enable windows integration test env + """Get test wallet rpc""" + if OsUtils.is_windows(): return None # type: ignore if cls._WALLET_RPC is None: @@ -416,6 +408,7 @@ def get_wallet_rpc(cls) -> MoneroWalletRpc: @classmethod def open_wallet_rpc(cls, c: Optional[MoneroWalletConfig]) -> MoneroWalletRpc: + """Open a rpc wallet""" config = c if c is not None else MoneroWalletConfig() # assign defaults @@ -448,6 +441,7 @@ def open_wallet_rpc(cls, c: Optional[MoneroWalletConfig]) -> MoneroWalletRpc: @classmethod def create_wallet_rpc(cls, c: Optional[MoneroWalletConfig]) -> MoneroWalletRpc: + """Create rpc wallet""" # assign defaults config = c if c is not None else MoneroWalletConfig() random = config.seed is None and config.primary_address is None @@ -470,7 +464,7 @@ def create_wallet_rpc(cls, c: Optional[MoneroWalletConfig]) -> MoneroWalletRpc: # create client connected to monero-wallet-rpc process wallet_rpc = cls._WALLET_RPC_2 if wallet_rpc is not None: - raise Exception(f"Cannot open wallet rpc: no resources left") + raise Exception("Cannot open wallet rpc: no resources left") if wallet_rpc is None: rpc = MoneroRpcConnection( @@ -489,21 +483,30 @@ def create_wallet_rpc(cls, c: Optional[MoneroWalletConfig]) -> MoneroWalletRpc: cls._WALLET_RPC_2 = wallet_rpc return cls._WALLET_RPC_2 + @classmethod + def get_wallets(cls, wallet_type: str) -> list[MoneroWallet]: + """Get all test wallets""" + raise NotImplementedError() + @classmethod def free_wallet_rpc_resources(cls) -> None: + """Free all docker wallet rpc resources""" if cls._WALLET_RPC_2 is not None: try: cls._WALLET_RPC_2.close() - except: + except Exception as e: + logger.debug(str(e)) pass cls._WALLET_RPC_2 = None @classmethod def is_wallet_rpc_resource(cls, wallet: MoneroWallet) -> bool: + """Indicates if wallet is using a docker rpc instance""" return wallet is cls._WALLET_RPC_2 @classmethod def free_wallet_rpc_resource(cls, wallet: MoneroWallet) -> None: + """Free docker resource used by wallet""" if cls.is_wallet_rpc_resource(wallet): # TODO free specific wallet rpc resource cls.free_wallet_rpc_resources() @@ -516,13 +519,14 @@ def create_wallet_ground_truth( start_height: int | None, restore_height: int | None ) -> MoneroWalletFull: + """Create a full wallet to use in equality tests""" # create directory for test wallets if it doesn't exist if not cls.check_test_wallets_dir_exists(): cls.create_test_wallets_dir() # create ground truth wallet daemon_connection = MoneroRpcConnection(cls.DAEMON_RPC_URI, cls.DAEMON_RPC_USERNAME, cls.DAEMON_RPC_PASSWORD) - path = cls.TEST_WALLETS_DIR + "/gt_wallet_" + cls.current_timestamp_str() + path = cls.TEST_WALLETS_DIR + "/gt_wallet_" + GenUtils.current_timestamp_str() config = MoneroWalletConfig() config.path = path config.password = cls.WALLET_PASSWORD @@ -535,7 +539,7 @@ def create_wallet_ground_truth( start_height = 0 gt_wallet = MoneroWalletFull.create_wallet(config) - cls.assert_equals(restore_height, gt_wallet.get_restore_height()) + AssertUtils.assert_equals(restore_height, gt_wallet.get_restore_height()) gt_wallet.sync(start_height, WalletSyncPrinter()) gt_wallet.start_syncing(cls.SYNC_PERIOD_IN_MS) @@ -543,551 +547,9 @@ def create_wallet_ground_truth( return gt_wallet - @classmethod - def test_invalid_address(cls, address: Optional[str], network_type: MoneroNetworkType) -> None: - if address is None: - return - - cls.assert_false(MoneroUtils.is_valid_address(address, network_type)) - - try: - MoneroUtils.validate_address(address, network_type) - raise Exception("Should have thrown exception") - except Exception as e: - cls.assert_false(len(str(e)) == 0) - - @classmethod - def test_invalid_private_view_key(cls, private_view_key: Optional[str]): - if private_view_key is None: - return - - cls.assert_false(MoneroUtils.is_valid_private_view_key(private_view_key)) - - try: - MoneroUtils.validate_private_view_key(private_view_key) - raise Exception("Should have thrown exception") - except Exception as e: - cls.assert_false(len(str(e)) == 0) - - @classmethod - def test_invalid_public_view_key(cls, public_view_key: Optional[str]) -> None: - if public_view_key is None: - return - - cls.assert_false(MoneroUtils.is_valid_public_view_key(public_view_key)) - - try: - MoneroUtils.validate_public_view_key(public_view_key) - raise Exception("Should have thrown exception") - except Exception as e: - cls.assert_false(len(str(e)) == 0) - - @classmethod - def test_invalid_private_spend_key(cls, private_spend_key: Optional[str]): - if private_spend_key is None: - return - - cls.assert_false(MoneroUtils.is_valid_private_spend_key(private_spend_key)) - - try: - MoneroUtils.validate_private_spend_key(private_spend_key) - raise Exception("Should have thrown exception") - except Exception as e: - cls.assert_false(len(str(e)) == 0) - - @classmethod - def test_invalid_public_spend_key(cls, public_spend_key: Optional[str]): - if public_spend_key is None: - return - - cls.assert_false(MoneroUtils.is_valid_public_spend_key(public_spend_key)) - try: - MoneroUtils.validate_public_spend_key(public_spend_key) - raise Exception("Should have thrown exception") - except Exception as e: - cls.assert_false(len(str(e)) == 0) - - @classmethod - def test_block_template(cls, template: MoneroBlockTemplate): - cls.assert_not_none(template) - cls.assert_not_none(template.block_template_blob) - cls.assert_not_none(template.block_hashing_blob) - cls.assert_not_none(template.difficulty) - cls.assert_not_none(template.expected_reward) - cls.assert_not_none(template.height) - cls.assert_not_none(template.prev_hash) - cls.assert_not_none(template.reserved_offset) - cls.assert_not_none(template.seed_height) - assert template.seed_height is not None - cls.assert_true(template.seed_height >= 0) - cls.assert_not_none(template.seed_hash) - cls.assert_false(template.seed_hash == "") - # next seed hash can be null or initialized TODO: test circumstances for each - - @classmethod - def test_block_header(cls, header: MoneroBlockHeader, is_full: bool): - cls.assert_not_none(header) - assert header.height is not None - cls.assert_true(header.height >= 0) - assert header.major_version is not None - cls.assert_true(header.major_version > 0) - assert header.minor_version is not None - cls.assert_true(header.minor_version >= 0) - assert header.timestamp is not None - if header.height == 0: - cls.assert_true(header.timestamp == 0) - else: - cls.assert_true(header.timestamp > 0) - cls.assert_not_none(header.prev_hash) - cls.assert_not_none(header.nonce) - if header.nonce == 0: - # TODO (monero-project): why is header nonce 0? - logger.warning(f"header nonce is 0 at height {header.height}") - else: - assert header.nonce is not None - cls.assert_true(header.nonce > 0) - cls.assert_is_none(header.pow_hash) # never seen defined - if is_full: - assert header.size is not None - assert header.depth is not None - assert header.difficulty is not None - assert header.cumulative_difficulty is not None - assert header.hash is not None - assert header.miner_tx_hash is not None - assert header.num_txs is not None - assert header.weight is not None - cls.assert_true(header.size > 0) - cls.assert_true(header.depth >= 0) - cls.assert_true(header.difficulty > 0) - cls.assert_true(header.cumulative_difficulty > 0) - cls.assert_equals(64, len(header.hash)) - cls.assert_equals(64, len(header.miner_tx_hash)) - cls.assert_true(header.num_txs >= 0) - cls.assert_not_none(header.orphan_status) - cls.assert_not_none(header.reward) - cls.assert_not_none(header.weight) - cls.assert_true(header.weight > 0) - else: - cls.assert_is_none(header.size) - cls.assert_is_none(header.depth) - cls.assert_is_none(header.difficulty) - cls.assert_is_none(header.cumulative_difficulty) - cls.assert_is_none(header.hash) - cls.assert_is_none(header.miner_tx_hash) - cls.assert_is_none(header.num_txs) - cls.assert_is_none(header.orphan_status) - cls.assert_is_none(header.reward) - cls.assert_is_none(header.weight) - - @classmethod - def test_miner_tx(cls, miner_tx: MoneroTx): - assert miner_tx is not None - cls.assert_not_none(miner_tx.is_miner_tx) - assert miner_tx.version is not None - cls.assert_true(miner_tx.version >= 0) - cls.assert_not_none(miner_tx.extra) - cls.assert_true(len(miner_tx.extra) > 0) - assert miner_tx.unlock_time is not None - cls.assert_true(miner_tx.unlock_time >= 0) - - # TODO: miner tx does not have hashes in binary requests so this will fail, need to derive using prunable data - # ctx = new TestContext() - # ctx.has_json = false - # ctx.is_pruned = true - # ctx.is_full = false - # ctx.is_confirmed = true - # ctx.is_miner = true - # ctx.from_get_tx_pool = true - # cls.test_tx(miner_tx, ctx) - - @classmethod - def test_tx(cls, tx: Optional[MoneroTx], ctx: Optional[TestContext]) -> None: - raise NotImplementedError() - - # TODO: test block deep copy - - @classmethod - def test_block(cls, block: Optional[MoneroBlock], ctx: TestContext): - # test required fields - assert block is not None, "Expected MoneroBlock, got None" - assert block.miner_tx is not None, "Expected block miner tx" - cls.test_miner_tx(block.miner_tx) # TODO: miner tx doesn't have as much stuff, can't call testTx? - cls.test_block_header(block, ctx.header_is_full) - - if ctx.has_hex: - assert block.hex is not None - cls.assert_true(len(block.hex) > 1) - else: - cls.assert_is_none(block.hex) - - if ctx.has_txs: - cls.assert_not_none(ctx.tx_context) - for tx in block.txs: - cls.assert_true(block == tx.block) - cls.test_tx(tx, ctx.tx_context) - - else: - cls.assert_is_none(ctx.tx_context) - assert len(block.txs) == 0, "No txs expected" - - @classmethod - def is_empty(cls, value: Union[str, list[Any], None]) -> bool: - return value == "" - - @classmethod - def test_update_check_result(cls, result: Union[Any, MoneroDaemonUpdateCheckResult]): - assert result is not None - cls.assert_true(isinstance(result, MoneroDaemonUpdateCheckResult)) - cls.assert_not_none(result.is_update_available) - if result.is_update_available: - cls.assert_false(cls.is_empty(result.auto_uri), "No auto uri is daemon online?") - cls.assert_false(cls.is_empty(result.user_uri)) - cls.assert_false(cls.is_empty(result.version)) - cls.assert_false(cls.is_empty(result.hash)) - assert result.hash is not None - cls.assert_equals(64, len(result.hash)) - else: - cls.assert_is_none(result.auto_uri) - cls.assert_is_none(result.user_uri) - cls.assert_is_none(result.version) - cls.assert_is_none(result.hash) - - @classmethod - def test_update_download_result(cls, result: MoneroDaemonUpdateDownloadResult, path: Optional[str]): - cls.test_update_check_result(result) - if result.is_update_available: - if path is not None: - cls.assert_equals(path, result.download_path) - else: - cls.assert_not_none(result.download_path) - else: - cls.assert_is_none(result.download_path) - - @classmethod - def test_unsigned_big_integer(cls, value: Any, bool_val: bool = False): - if not isinstance(value, int): - raise Exception(f"Value is not number: {value}") - - if value < 0: - raise Exception("Value cannot be negative") - - @classmethod - def test_account(cls, account: Optional[MoneroAccount], full: bool = True): - # test account - assert account is not None - assert account.index is not None - assert account.index >= 0 - assert account.primary_address is not None - - MoneroUtils.validate_address(account.primary_address, cls.NETWORK_TYPE) - if full: - cls.test_unsigned_big_integer(account.balance) - cls.test_unsigned_big_integer(account.unlocked_balance) - - # if given, test subaddresses and that their balances add up to account balances - if len(account.subaddresses) > 0: - balance = 0 - unlocked_balance = 0 - i = 0 - j = len(account.subaddresses) - while i < j: - cls.test_subaddress(account.subaddresses[i]) - assert account.index == account.subaddresses[i].account_index - assert i == account.subaddresses[i].index - address_balance = account.subaddresses[i].balance - assert address_balance is not None - balance += address_balance - address_balance = account.subaddresses[i].unlocked_balance - assert address_balance is not None - unlocked_balance += address_balance - msg1 = f"Subaddress balances {balance} != account {account.index} balance {account.balance}" - msg2 = f"Subaddress unlocked balances {unlocked_balance} != account {account.index} unlocked balance {account.unlocked_balance}" - assert account.balance == balance, msg1 - assert account.unlocked_balance == unlocked_balance, msg2 - i += 1 - - # tag must be undefined or non-empty - tag = account.tag - assert tag is None or len(tag) > 0 - - @classmethod - def test_subaddress(cls, subaddress: MoneroSubaddress, full: bool = True): - assert subaddress.account_index is not None - assert subaddress.index is not None - if full: - assert subaddress.balance is not None - assert subaddress.num_unspent_outputs is not None - assert subaddress.num_blocks_to_unlock is not None - cls.test_unsigned_big_integer(subaddress.balance) - cls.test_unsigned_big_integer(subaddress.unlocked_balance) - cls.assert_true(subaddress.num_unspent_outputs >= 0) - cls.assert_not_none(subaddress.is_used) - if subaddress.balance > 0: - cls.assert_true(subaddress.is_used) - cls.assert_true(subaddress.num_blocks_to_unlock >= 0) - - cls.assert_true(subaddress.account_index >= 0) - cls.assert_true(subaddress.index >= 0) - cls.assert_not_none(subaddress.address) - # TODO fix monero-cpp/monero_wallet_full.cpp to return boost::none on empty label - #cls.assert_true(subaddress.label is None or subaddress.label != "") - - @classmethod - def assert_subaddress_equal(cls, subaddress: Optional[MoneroSubaddress], other: Optional[MoneroSubaddress]): - if subaddress is None and other is None: - return - assert not (subaddress is None or other is None) - assert subaddress.address == other.address - assert subaddress.account_index == other.account_index - assert subaddress.balance == other.balance - assert subaddress.index == other.index - assert subaddress.is_used == other.is_used - assert subaddress.label == other.label - assert subaddress.num_blocks_to_unlock == other.num_blocks_to_unlock - assert subaddress.num_unspent_outputs == other.num_unspent_outputs - assert subaddress.unlocked_balance == other.unlocked_balance - - @classmethod - def assert_subaddresses_equal(cls, subaddresses1: list[MoneroSubaddress], subaddresses2: list[MoneroSubaddress]): - size1 = len(subaddresses1) - size2 = len(subaddresses2) - if size1 != size2: - raise Exception("Number of subaddresses doesn't match") - - i = 0 - - while i < size1: - cls.assert_subaddress_equal(subaddresses1[i], subaddresses2[i]) - i += 1 - - @classmethod - def test_known_peer(cls, peer: Optional[MoneroPeer], from_connection: bool): - assert peer is not None, "Peer is null" - assert peer.id is not None - assert peer.host is not None - assert peer.port is not None - cls.assert_false(len(peer.id) == 0) - cls.assert_false(len(peer.host) == 0) - cls.assert_true(peer.port > 0) - cls.assert_true(peer.rpc_port is None or peer.rpc_port >= 0) - cls.assert_not_none(peer.is_online) - if peer.rpc_credits_per_hash is not None: - cls.test_unsigned_big_integer(peer.rpc_credits_per_hash) - if from_connection: - cls.assert_is_none(peer.last_seen_timestamp) - else: - assert peer.last_seen_timestamp is not None - - if peer.last_seen_timestamp < 0: - logger.warning(f"Last seen timestamp is invalid: {peer.last_seen_timestamp}") - cls.assert_true(peer.last_seen_timestamp >= 0) - - cls.assert_true(peer.pruning_seed is None or peer.pruning_seed >= 0) - - @classmethod - def test_peer(cls, peer: Union[Any, MoneroPeer]): - cls.assert_true(isinstance(peer, MoneroPeer)) - cls.test_known_peer(peer, True) - assert peer.hash is not None - assert peer.avg_download is not None - assert peer.avg_upload is not None - assert peer.current_download is not None - assert peer.current_upload is not None - assert peer.height is not None - assert peer.live_time is not None - assert peer.num_receives is not None - assert peer.receive_idle_time is not None - assert peer.num_sends is not None - assert peer.send_idle_time is not None - assert peer.num_support_flags is not None - - cls.assert_false(len(peer.hash) == 0) - cls.assert_true(peer.avg_download >= 0) - cls.assert_true(peer.avg_upload >= 0) - cls.assert_true(peer.current_download >= 0) - cls.assert_true(peer.current_upload >= 0) - cls.assert_true(peer.height >= 0) - cls.assert_true(peer.live_time >= 0) - cls.assert_not_none(peer.is_local_ip) - cls.assert_not_none(peer.is_local_host) - cls.assert_true(peer.num_receives >= 0) - cls.assert_true(peer.receive_idle_time >= 0) - cls.assert_true(peer.num_sends >= 0) - cls.assert_true(peer.send_idle_time >= 0) - cls.assert_not_none(peer.state) - cls.assert_true(peer.num_support_flags >= 0) - cls.assert_not_none(peer.connection_type) - - @classmethod - def test_info(cls, info: MoneroDaemonInfo): - assert info.num_alt_blocks is not None - assert info.block_size_limit is not None - assert info.block_size_median is not None - assert info.num_offline_peers is not None - assert info.num_online_peers is not None - assert info.height is not None - assert info.height_without_bootstrap is not None - assert info.num_incoming_connections is not None - assert info.num_outgoing_connections is not None - assert info.num_rpc_connections is not None - assert info.start_timestamp is not None - assert info.adjusted_timestamp is not None - assert info.target is not None - assert info.target_height is not None - assert info.num_txs is not None - assert info.num_txs_pool is not None - assert info.block_weight_limit is not None - assert info.block_weight_median is not None - assert info.database_size is not None - cls.assert_not_none(info.version) - cls.assert_true(info.num_alt_blocks >= 0) - cls.assert_true(info.block_size_limit > 0) - cls.assert_true(info.block_size_median > 0) - cls.assert_true(info.bootstrap_daemon_address is None or not cls.is_empty(info.bootstrap_daemon_address)) - cls.test_unsigned_big_integer(info.cumulative_difficulty) - cls.test_unsigned_big_integer(info.free_space) - cls.assert_true(info.num_offline_peers >= 0) - cls.assert_true(info.num_online_peers >= 0) - cls.assert_true(info.height >= 0) - cls.assert_true(info.height_without_bootstrap > 0) - cls.assert_true(info.num_incoming_connections >= 0) - cls.assert_not_none(info.network_type) - cls.assert_not_none(info.is_offline) - cls.assert_true(info.num_outgoing_connections >= 0) - cls.assert_true(info.num_rpc_connections >= 0) - cls.assert_true(info.start_timestamp > 0) - cls.assert_true(info.adjusted_timestamp > 0) - cls.assert_true(info.target > 0) - cls.assert_true(info.target_height >= 0) - cls.assert_true(info.num_txs >= 0) - cls.assert_true(info.num_txs_pool >= 0) - cls.assert_not_none(info.was_bootstrap_ever_used) - cls.assert_true(info.block_weight_limit > 0) - cls.assert_true(info.block_weight_median > 0) - cls.assert_true(info.database_size > 0) - cls.assert_not_none(info.update_available) - cls.test_unsigned_big_integer(info.credits, False) # 0 credits - cls.assert_false(cls.is_empty(info.top_block_hash)) - cls.assert_not_none(info.is_busy_syncing) - cls.assert_not_none(info.is_synchronized) - - @classmethod - def test_sync_info(cls, sync_info: Union[Any, MoneroDaemonSyncInfo]): - cls.assert_true(isinstance(sync_info, MoneroDaemonSyncInfo)) - assert sync_info.height is not None - cls.assert_true(sync_info.height >= 0) - - for connection in sync_info.peers: - cls.test_peer(connection) - - for span in sync_info.spans: - cls.test_connection_span(span) - - assert sync_info.next_needed_pruning_seed is not None - cls.assert_true(sync_info.next_needed_pruning_seed >= 0) - cls.assert_is_none(sync_info.overview) - cls.test_unsigned_big_integer(sync_info.credits, False) # 0 credits - cls.assert_is_none(sync_info.top_block_hash) - - @classmethod - def test_connection_span(cls, span: Union[MoneroConnectionSpan, Any]) -> None: - raise NotImplementedError() - - @classmethod - def test_hard_fork_info(cls, hard_fork_info: MoneroHardForkInfo): - cls.assert_not_none(hard_fork_info.earliest_height) - cls.assert_not_none(hard_fork_info.is_enabled) - cls.assert_not_none(hard_fork_info.state) - cls.assert_not_none(hard_fork_info.threshold) - cls.assert_not_none(hard_fork_info.version) - cls.assert_not_none(hard_fork_info.num_votes) - cls.assert_not_none(hard_fork_info.voting) - cls.assert_not_none(hard_fork_info.window) - cls.test_unsigned_big_integer(hard_fork_info.credits, False) # 0 credits - cls.assert_is_none(hard_fork_info.top_block_hash) - - @classmethod - def test_alt_chain(cls, alt_chain: MoneroAltChain): - cls.assert_not_none(alt_chain) - cls.assert_false(len(alt_chain.block_hashes) == 0) - cls.test_unsigned_big_integer(alt_chain.difficulty, True) - assert alt_chain.height is not None - assert alt_chain.length is not None - assert alt_chain.main_chain_parent_block_hash is not None - cls.assert_true(alt_chain.height > 0) - cls.assert_true(alt_chain.length > 0) - cls.assert_equals(64, len(alt_chain.main_chain_parent_block_hash)) - - @classmethod - def test_ban(cls, ban: Optional[MoneroBan]) -> None: - assert ban is not None - assert ban.host is not None - assert ban.ip is not None - assert ban.seconds is not None - - @classmethod - def test_miner_tx_sum(cls, tx_sum: Optional[MoneroMinerTxSum]) -> None: - assert tx_sum is not None - cls.test_unsigned_big_integer(tx_sum.emission_sum, True) - cls.test_unsigned_big_integer(tx_sum.fee_sum, True) - - @classmethod - def get_unrelayed_tx(cls, wallet: MoneroWallet, account_idx: int): - # TODO monero-project - assert account_idx > 0, "Txs sent from/to same account are not properly synced from the pool" - config = MoneroTxConfig() - config.account_index = account_idx - config.address = wallet.get_primary_address() - config.amount = cls.MAX_FEE - - tx = wallet.create_tx(config) - assert (tx.full_hex is None or tx.full_hex == "") is False - assert tx.relay is False - return tx - - @classmethod - def test_tx_pool_stats(cls, stats: MoneroTxPoolStats): - cls.assert_not_none(stats) - assert stats.num_txs is not None - cls.assert_true(stats.num_txs >= 0) - if stats.num_txs > 0: - - assert stats.bytes_max is not None - assert stats.bytes_med is not None - assert stats.bytes_min is not None - assert stats.bytes_total is not None - assert stats.oldest_timestamp is not None - assert stats.num10m is not None - assert stats.num_double_spends is not None - assert stats.num_failing is not None - assert stats.num_not_relayed is not None - - cls.assert_true(stats.bytes_max > 0) - cls.assert_true(stats.bytes_med > 0) - cls.assert_true(stats.bytes_min > 0) - cls.assert_true(stats.bytes_total > 0) - cls.assert_true(stats.histo98pc is None or stats.histo98pc > 0) - cls.assert_true(stats.oldest_timestamp > 0) - cls.assert_true(stats.num10m >= 0) - cls.assert_true(stats.num_double_spends >= 0) - cls.assert_true(stats.num_failing >= 0) - cls.assert_true(stats.num_not_relayed >= 0) - - else: - cls.assert_is_none(stats.bytes_max) - cls.assert_is_none(stats.bytes_med) - cls.assert_is_none(stats.bytes_min) - cls.assert_equals(0, stats.bytes_total) - cls.assert_is_none(stats.histo98pc) - cls.assert_is_none(stats.oldest_timestamp) - cls.assert_equals(0, stats.num10m) - cls.assert_equals(0, stats.num_double_spends) - cls.assert_equals(0, stats.num_failing) - cls.assert_equals(0, stats.num_not_relayed) - #cls.assert_is_none(stats.histo) - @classmethod def get_external_wallet_address(cls) -> str: + """Return an external wallet address""" network_type: MoneroNetworkType | None = cls.get_daemon_rpc().get_info().network_type if network_type == MoneroNetworkType.STAGENET: @@ -1102,97 +564,4 @@ def get_external_wallet_address(cls) -> str: else: raise Exception("Invalid network type: " + str(network_type)) - @classmethod - def get_and_test_txs(cls, wallet: MoneroWallet, a: Any, b: Any, c: bool) -> list[MoneroTxWallet]: - raise NotImplementedError() - - @classmethod - def get_random_transactions( - cls, - wallet: MoneroWallet, - query: Optional[MoneroTxQuery] = None, - min_txs: Optional[int] = None, - max_txs: Optional[int] = None - ) -> list[MoneroTxWallet]: - txs = wallet.get_txs(query if query is not None else MoneroTxQuery()) - - if min_txs is not None: - assert len(txs) >= min_txs, f"{len(txs)}/{min_txs} transactions found with the query" - - shuffle(txs) - - if max_txs is None: - return txs - - result: list[MoneroTxWallet] = [] - i = 0 - - for tx in txs: - result.append(tx) - if i >= max_txs - 1: - break - i += 1 - - return result - - @classmethod - def test_tx_wallet(cls, tx: MoneroTxWallet, ctx: TxContext) -> None: - raise NotImplementedError() - - @classmethod - def get_confirmed_tx_hashes(cls, daemon: MoneroDaemon) -> list[str]: - hashes: list[str] = [] - height: int = daemon.get_height() - i = 0 - while i < 5 and height > 0: - height -= 1 - block = daemon.get_block_by_height(height) - for tx_hash in block.tx_hashes: - hashes.append(tx_hash) - return hashes - - @classmethod - def test_rpc_connection(cls, connection: Optional[MoneroRpcConnection], uri: Optional[str], connected: bool = True) -> None: - assert connection is not None - assert uri is not None - assert len(uri) > 0 - assert connection.uri == uri - assert connection.check_connection() == connected - assert connection.is_connected() == connected - assert connection.is_online() == connected - - @classmethod - def test_get_blocks_range( - cls, - daemon: MoneroDaemonRpc, - start_height: Optional[int], - end_height: Optional[int], - chain_height: int, - chunked: bool, - block_ctx: BinaryBlockContext - ) -> None: - # fetch blocks by range - real_start_height = 0 if start_height is None else start_height - real_end_height = chain_height - 1 if end_height is None else end_height - blocks = daemon.get_blocks_by_range_chunked(start_height, end_height) if chunked else daemon.get_blocks_by_range(start_height, end_height) - cls.assert_equals(real_end_height - real_start_height + 1, len(blocks)) - - # test each block - for i, block in enumerate(blocks): - cls.assert_equals(real_start_height + i, block.height) - cls.test_block(block, block_ctx) - - @classmethod - def assert_connection_equals(cls, c1: Optional[MoneroRpcConnection], c2: Optional[MoneroRpcConnection]) -> None: - if c1 is None and c2 is None: - return - - assert c1 is not None - assert c2 is not None - if not cls.IN_CONTAINER: # TODO - assert c1.uri == c2.uri - assert c1.username == c2.username - assert c1.password == c2.password - - TestUtils.load_config() diff --git a/tests/utils/tx_context.py b/tests/utils/tx_context.py index 779450d..3be5e91 100644 --- a/tests/utils/tx_context.py +++ b/tests/utils/tx_context.py @@ -19,14 +19,13 @@ class TxContext: is_sweep_output_response: Optional[bool] def __init__(self, ctx: Optional[TxContext] = None) -> None: - if ctx is not None: - self.wallet = ctx.wallet - self.config = ctx.config - self.has_outgoing_transfer = ctx.has_outgoing_transfer - self.has_incoming_transfers = ctx.has_incoming_transfers - self.has_destinations = ctx.has_destinations - self.is_copy = ctx.is_copy - self.include_outputs = ctx.include_outputs - self.is_send_response = ctx.is_send_response - self.is_sweep_response = ctx.is_sweep_response - self.is_sweep_output_response = ctx.is_sweep_output_response + self.wallet = ctx.wallet if ctx is not None else None + self.config = ctx.config if ctx is not None else None + self.has_outgoing_transfer = ctx.has_outgoing_transfer if ctx is not None else None + self.has_incoming_transfers = ctx.has_incoming_transfers if ctx is not None else None + self.has_destinations = ctx.has_destinations if ctx is not None else None + self.is_copy = ctx.is_copy if ctx is not None else None + self.include_outputs = ctx.include_outputs if ctx is not None else None + self.is_send_response = ctx.is_send_response if ctx is not None else None + self.is_sweep_response = ctx.is_sweep_response if ctx is not None else None + self.is_sweep_output_response = ctx.is_sweep_output_response if ctx is not None else None diff --git a/tests/utils/tx_utils.py b/tests/utils/tx_utils.py new file mode 100644 index 0000000..ad06534 --- /dev/null +++ b/tests/utils/tx_utils.py @@ -0,0 +1,875 @@ +import logging + +from abc import ABC +from typing import Optional +from random import shuffle +from monero import ( + MoneroWallet, MoneroTxQuery, MoneroTxWallet, + MoneroBlock, MoneroTransfer, MoneroIncomingTransfer, + MoneroOutgoingTransfer, MoneroDestination, + MoneroUtils, MoneroOutputWallet, MoneroTx, + MoneroOutput, MoneroKeyImage, MoneroDaemon, + MoneroTxConfig +) + +from .tx_context import TxContext +from .gen_utils import GenUtils +from .test_context import TestContext +from .assert_utils import AssertUtils +from .test_utils import TestUtils + +logger: logging.Logger = logging.getLogger("TxUtils") + + +class TxUtils(ABC): + """Tx utils for tests""" + + __test__ = False + + MAX_FEE = 7500000*10000 + """Max tx fee""" + + @classmethod + def test_key_image(cls, image: Optional[MoneroKeyImage], context: Optional[TestContext] = None) -> None: + """Test monero key image""" + assert image is not None + assert image.hex is not None + assert len(image.hex) > 0 + if image.signature is not None: + assert len(image.signature) > 0 + + @classmethod + def test_output(cls, output: Optional[MoneroOutput], context: Optional[TestContext] = None) -> None: + """Test monero output""" + assert output is not None + GenUtils.test_unsigned_big_integer(output.amount) + assert output.tx is not None + ctx = TestContext(context) + if output.tx.in_tx_pool or ctx.has_output_indices is False: + assert output.index is None + else: + assert output.index is not None + assert output.index >= 0 + assert output.stealth_public_key is not None + assert len(output.stealth_public_key) > 0 + + @classmethod + def test_input(cls, xmr_input: Optional[MoneroOutput], ctx: Optional[TestContext]) -> None: + """Test monero input""" + assert xmr_input is not None + cls.test_output(xmr_input, ctx) + cls.test_key_image(xmr_input.key_image, ctx) + assert len(xmr_input.ring_output_indices) > 0 + + @classmethod + def test_input_wallet(cls, xmr_input: Optional[MoneroOutputWallet]) -> None: + """Test monero input wallet""" + assert xmr_input is not None + assert xmr_input.key_image is not None + assert xmr_input.key_image.hex is not None + assert len(xmr_input.key_image.hex) > 0 + assert xmr_input.amount is None + + @classmethod + def test_output_wallet(cls, output: Optional[MoneroOutputWallet]) -> None: + """Test monero output wallet""" + assert output is not None + assert output.account_index is not None + assert output.account_index >= 0 + assert output.subaddress_index is not None + assert output.subaddress_index >= 0 + assert output.index is not None + assert output.index >= 0 + assert output.is_spent is not None + # TODO implement is_locked + #assert output.is_locked is not None + assert output.is_frozen is not None + assert output.key_image is not None + assert output.key_image.hex is not None + assert len(output.key_image.hex) > 0 + GenUtils.test_unsigned_big_integer(output.amount, True) + + # output has circular reference to its transaction which has some initialized fields + tx = output.tx + assert tx is not None + assert output in tx.outputs + assert tx.hash is not None + # TODO implement is_locked + #assert tx.is_locked is not None + # TODO monero-wallet-rpc: possible to get unconfirmed outputs? + assert tx.is_confirmed is True + assert tx.is_relayed is True + assert tx.is_failed is False + tx_height = tx.get_height() + assert tx_height is not None + assert tx_height > 0 + + # test copying + copy = output.copy() + assert copy != output + AssertUtils.assert_equals(copy, output) + # TODO: should output copy do deep copy of tx so models are graph instead of tree? Would need to work out circular references + assert copy.tx is None + + @classmethod + def test_destination(cls, dest: Optional[MoneroDestination]) -> None: + """Test monero destination""" + assert dest is not None + assert dest.address is not None + MoneroUtils.validate_address(dest.address, TestUtils.NETWORK_TYPE) + GenUtils.test_unsigned_big_integer(dest.amount, True) + + @classmethod + def test_incoming_transfer(cls, transfer: Optional[MoneroIncomingTransfer]) -> None: + """Test monero incoming transfer""" + assert transfer is not None + assert transfer.is_incoming() is True + assert transfer.is_outgoing() is False + assert transfer.address is not None + assert transfer.subaddress_index is not None + assert transfer.subaddress_index >= 0 + assert transfer.num_suggested_confirmations is not None + assert transfer.num_suggested_confirmations > 0 + + @classmethod + def test_outgoing_transfer(cls, transfer: Optional[MoneroOutgoingTransfer], ctx: TxContext) -> None: + """Test monero outgoing transfer""" + assert transfer is not None + assert transfer.is_incoming() is False + assert transfer.is_outgoing() is True + if ctx.is_send_response is not True: + assert len(transfer.subaddress_indices) > 0 + + if len(transfer.subaddress_indices) > 0: + assert len(transfer.subaddress_indices) == len(transfer.addresses) + for address in transfer.addresses: + assert address is not None + + # test destinations sum to outgoing amount + if len(transfer.destinations) > 0: + transfer_sum: int = 0 + for destination in transfer.destinations: + cls.test_destination(destination) + assert destination.amount is not None + transfer_sum += destination.amount + + assert transfer_sum == transfer.amount + + @classmethod + def test_transfer(cls, transfer: Optional[MoneroTransfer], context: Optional[TxContext]) -> None: + """Test monero transfer""" + ctx = context if context is not None else TxContext() + assert transfer is not None + GenUtils.test_unsigned_big_integer(transfer.amount) + if ctx.is_sweep_output_response is not True: + assert transfer.account_index is not None + assert transfer.account_index >= 0 + if transfer.is_incoming(): + assert isinstance(transfer, MoneroIncomingTransfer) + cls.test_incoming_transfer(transfer) + else: + assert isinstance(transfer, MoneroOutgoingTransfer) + cls.test_outgoing_transfer(transfer, ctx) + + # transfer and tx reference each other + assert transfer.tx is not None + if not AssertUtils.equals(transfer, transfer.tx.outgoing_transfer): + assert len(transfer.tx.incoming_transfers) != 0 + assert transfer in transfer.tx.incoming_transfers, "Transaction does not reference given transfer" + + @classmethod + def test_tx_wallet(cls, tx: Optional[MoneroTxWallet], context: Optional[TxContext]) -> None: + """Test monero tx wallet""" + # validate / sanitize inputs + ctx = TxContext(context) + ctx.wallet = None # TODO: re-enable + assert tx is not None + if ctx.is_send_response is None or ctx.config is None: + assert ctx.is_send_response is None, "if either sendRequest or isSendResponse is defined, they must both be defined" + assert ctx.config is None, "if either sendRequest or isSendResponse is defined, they must both be defined" + + # test common field types + assert tx.hash is not None + assert tx.is_confirmed is not None + assert tx.is_miner_tx is not None + assert tx.is_failed is not None + assert tx.is_relayed is not None + assert tx.in_tx_pool is not None + assert tx.is_locked is not None + GenUtils.test_unsigned_big_integer(tx.fee) + if tx.payment_id is not None: + # default payment id converted to None + assert MoneroTxWallet.DEFAULT_PAYMENT_ID != tx.payment_id + if tx.note is not None: + # empty notes converted to undefined + assert len(tx.note) > 0 + + assert tx.unlock_time is not None + assert tx.unlock_time >= 0 + assert tx.size is None # TODO monero-wallet-rpc: add tx_size to get_transfers and get_transfer_by_txid + assert tx.received_timestamp is None # TODO monero-wallet-rpc: return received timestamp (asked to file issue if wanted) + + # test send tx + if ctx.is_send_response is True: + assert tx.weight is not None + assert tx.weight > 0 + assert len(tx.inputs) > 0 + for tx_input in tx.inputs: + assert tx_input.tx == tx + else: + assert tx.weight is None + assert len(tx.inputs) == 0 + + # test confirmed + if tx.is_confirmed: + assert tx.block is not None + assert tx in tx.block.txs + assert tx.block.height is not None + assert tx.block.height > 0 + assert tx.block.timestamp is not None + assert tx.block.timestamp > 0 + assert tx.relay is True + assert tx.is_relayed is True + assert tx.is_failed is False + assert tx.in_tx_pool is False + assert tx.is_double_spend_seen is False + assert tx.num_confirmations is not None + assert tx.num_confirmations > 0 + else: + assert tx.block is None + assert tx.num_confirmations is not None + assert tx.num_confirmations == 0 + + # test in tx pool + if tx.in_tx_pool: + assert tx.is_confirmed is False + assert tx.relay is True + assert tx.is_relayed is True + assert tx.is_double_spend_seen is False + assert tx.is_locked is True + + # these should be initialized unless a response from sending + # TODO re-enable when received timestamp returned in wallet rpc + #if ctx.is_send_response: + #assert tx.received_timestamp > 0 + else: + assert tx.last_relayed_timestamp is None + + # test miner tx + if tx.is_miner_tx: + assert tx.fee is not None + assert tx.fee == 0 + + # test failed + # TODO what else to test associated with failed + if tx.is_failed: + assert isinstance(tx.outgoing_transfer, MoneroTransfer) + # TODO re-enable when received timestamp returned in wallet rpc + #assert tx.received_timestamp > 0 + else: + if tx.is_relayed: + assert tx.is_double_spend_seen is False + else: + assert tx.relay is False + assert tx.is_relayed is False + assert tx.is_double_spend_seen is None + + assert tx.last_failed_height is None + assert tx.last_failed_hash is None + + # received time only for tx pool or failed txs + if tx.received_timestamp is not None: + assert tx.in_tx_pool or tx.is_failed + + # test relayed tx + if tx.is_relayed: + assert tx.relay is True + if tx.relay is False: + assert (not tx.is_relayed) is True + + # test outgoing transfer per configuration + if ctx.has_outgoing_transfer is False: + assert tx.outgoing_transfer is None + if ctx.has_destinations is True: + assert tx.outgoing_transfer is not None + assert len(tx.outgoing_transfer.destinations) > 0 + + # test outgoing transfer + if tx.outgoing_transfer is not None: + assert tx.is_outgoing is True + cls.test_transfer(tx.outgoing_transfer, ctx) + if ctx.is_sweep_response is True: + assert len(tx.outgoing_transfer.destinations) == 1 + # TODO handle special cases + else: + assert len(tx.incoming_transfers) > 0 + assert tx.get_outgoing_amount() == 0 + assert tx.outgoing_transfer is None + assert tx.ring_size is None + assert tx.full_hex is None + assert tx.metadata is None + assert tx.key is None + + # test incoming transfers + if len(tx.incoming_transfers) > 0: + assert tx.is_incoming is True + GenUtils.test_unsigned_big_integer(tx.get_incoming_amount()) + assert tx.is_failed is False + + # test each transfer and collect transfer sum + transfer_sum: int = 0 + for transfer in tx.incoming_transfers: + cls.test_transfer(transfer, ctx) + assert transfer.amount is not None + transfer_sum += transfer.amount + if ctx.wallet is not None: + addr = ctx.wallet.get_address(transfer.account_index, transfer.subaddress_index) + assert transfer.address == addr + # TODO special case: transfer amount of 0 + + # incoming transfers add up to incoming tx amount + assert tx.get_incoming_amount() == transfer_sum + else: + assert tx.outgoing_transfer is not None + assert tx.get_incoming_amount() == 0 + assert len(tx.incoming_transfers) == 0 + + # test tx results from send or relay + if ctx.is_send_response is True: + # test tx set + assert tx.tx_set is not None + found: bool = False + for a_tx in tx.tx_set.txs: + if a_tx == tx: + found = True + break + + if ctx.is_copy is True: + assert found is False + else: + assert found + + # test common attributes + assert ctx.config is not None + config = ctx.config + assert tx.is_confirmed is False + cls.test_transfer(tx.outgoing_transfer, ctx) + assert tx.ring_size == MoneroUtils.get_ring_size() + assert tx.unlock_time == 0 + assert tx.block is None + assert tx.key is not None + assert len(tx.key) > 0 + assert tx.full_hex is not None + assert len(tx.full_hex) > 0 + assert tx.metadata is not None + assert tx.received_timestamp is None + assert tx.is_locked is True + + # get locked state + if tx.unlock_time == 0: + assert tx.is_confirmed == (not tx.is_locked) + else: + assert tx.is_locked is True + + # TODO implement is_locked + #for output in tx.get_outputs_wallet(): + # assert tx.is_locked == output.is_locked + + # test destinations of sent tx + assert tx.outgoing_transfer is not None + if len(tx.outgoing_transfer.destinations) == 0: + assert config.can_split is True + # TODO: remove this after >18.3.1 when amounts_by_dest_list official + logger.warning("Destinations not returned from split transactions") + else: + subtract_fee_from_dests = len(config.subtract_fee_from) > 0 + if ctx.is_sweep_response is True: + assert len(config.destinations) == 1 + assert config.destinations[0].amount is None + if not subtract_fee_from_dests: + assert tx.outgoing_transfer.amount == tx.outgoing_transfer.destinations[0].amount + + if config.relay is True: + # test relayed txs + assert tx.in_tx_pool is True + assert tx.relay is True + assert tx.is_relayed is True + assert tx.last_relayed_timestamp is not None + assert tx.last_relayed_timestamp > 0 + assert tx.is_double_spend_seen is False + else: + # test non-relayed txs + assert tx.in_tx_pool is False + assert tx.relay is False + assert tx.is_relayed is False + assert tx.last_relayed_timestamp is None + assert tx.is_double_spend_seen is None + + else: + # test tx result query + # tx set only initialized on send responses + assert tx.tx_set is None + assert tx.ring_size is None + assert tx.key is None + assert tx.full_hex is None + assert tx.metadata is None + assert tx.last_relayed_timestamp is None + + # test inputs + if tx.is_outgoing is True and ctx.is_send_response is True: + assert len(tx.inputs) > 0 + + for wallet_input in tx.get_inputs_wallet(): + cls.test_input_wallet(wallet_input) + + # test outputs + if tx.is_incoming is True and ctx.include_outputs is True: + if tx.is_confirmed is True: + assert len(tx.outputs) > 0 + else: + assert len(tx.outputs) == 0 + + for output in tx.get_outputs_wallet(): + cls.test_output_wallet(output) + + # TODO test deep copy + #if ctx.is_copy is not True: + # cls.test_tx_wallet_copy(tx, ctx) + + @classmethod + def test_tx_copy(cls, tx: Optional[MoneroTx], context: Optional[TestContext]) -> None: + """Test monero tx copy""" + # copy tx and assert deep equality + assert tx is not None + copy = tx.copy() + assert isinstance(copy, MoneroTx) + assert copy.block is None + if tx.block is not None: + block_copy = tx.block.copy() + block_copy.txs = [copy] + + AssertUtils.assert_equals(str(tx), str(copy)) + assert copy != tx + + # test different input references + if len(copy.inputs) == 0: + assert len(tx.inputs) == 0 + else: + assert copy.inputs != tx.inputs + for i, output in enumerate(copy.outputs): + assert tx.outputs[i].amount == output.amount + + # test copied tx + ctx = TestContext(context) + ctx.do_not_test_copy = True # to prevent infinite recursion + if tx.block is not None: + block_copy = tx.block.copy() + block_copy.txs = [copy] + copy.block = block_copy + + cls.test_tx(copy, ctx) + + # test merging with copy + merged = copy + merged.merge(copy.copy()) + assert str(tx) == str(merged) + + @classmethod + def test_tx(cls, tx: Optional[MoneroTx], ctx: Optional[TestContext]) -> None: + """Test monero tx""" + # check inputs + assert tx is not None + assert ctx is not None + assert ctx.is_pruned is not None + assert ctx.is_confirmed is not None + assert ctx.from_get_tx_pool is not None + + # standard across all txs + assert tx.hash is not None + assert len(tx.hash) == 64 + if tx.is_relayed is None: + assert tx.in_tx_pool is True + else: + assert tx.is_relayed is not None + assert tx.is_confirmed is not None + assert tx.in_tx_pool is not None + assert tx.is_miner_tx is not None + assert tx.is_double_spend_seen is not None + assert tx.version is not None + assert tx.version >= 0 + assert tx.unlock_time is not None + assert tx.unlock_time >= 0 + assert tx.extra is not None + assert len(tx.extra) > 0 + GenUtils.test_unsigned_big_integer(tx.fee, True) + + # test presence of output indices + # TODO change this over to outputs only + if tx.is_miner_tx is True: + # TODO how to get output indices for miner transactions? + assert len(tx.output_indices) == 0 + if tx.in_tx_pool or ctx.from_get_tx_pool or ctx.has_output_indices is False: + assert len(tx.output_indices) == 0 + else: + assert len(tx.output_indices) > 0 + + # test confirmed ctx + if ctx.is_confirmed is True: + assert tx.is_confirmed is True + elif ctx.is_confirmed is False: + assert tx.is_confirmed is False + + # test confirmed + if tx.is_confirmed is True: + block = tx.block + assert block is not None + assert tx in block.txs + assert block.height is not None + assert block.height > 0 + assert block.timestamp is not None + assert block.timestamp > 0 + assert tx.relay is True + assert tx.is_relayed is True + assert tx.is_failed is False + assert tx.in_tx_pool is False + assert tx.is_double_spend_seen is False + if ctx.from_binary_block is True: + assert tx.num_confirmations is None + else: + assert tx.num_confirmations is not None + assert tx.num_confirmations > 0 + else: + assert tx.block is None + assert tx.num_confirmations == 0 + + # test in tx pool + if tx.in_tx_pool: + assert tx.is_confirmed is False + assert tx.is_double_spend_seen is False + assert tx.last_failed_height is None + assert tx.last_failed_hash is None + assert tx.received_timestamp is not None + assert tx.received_timestamp > 0 + if ctx.from_get_tx_pool: + assert tx.size is not None + assert tx.size > 0 + assert tx.weight is not None + assert tx.weight > 0 + assert tx.is_kept_by_block is not None + assert tx.max_used_block_height is not None + assert tx.max_used_block_height >= 0 + assert tx.max_used_block_hash is not None + + assert tx.last_failed_height is None + assert tx.last_failed_hash is None + else: + assert tx.last_relayed_timestamp is None + + # test miner tx + if tx.is_miner_tx: + assert tx.fee == 0 + assert len(tx.inputs) == 0 + assert len(tx.signatures) == 0 + + # test failed + # TODO what else to test associated with failed + if tx.is_failed: + assert tx.received_timestamp is not None + assert tx.received_timestamp > 0 + else: + if tx.is_relayed is None: + assert tx.relay is None + elif tx.is_relayed: + assert tx.is_double_spend_seen is False + else: + assert tx.is_relayed is False + if ctx.from_get_tx_pool: + assert tx.relay is False + assert tx.is_double_spend_seen is not None + + assert tx.last_failed_height is None + assert tx.last_failed_hash is None + + # received time only for tx pool or failed txs + if tx.received_timestamp is not None: + assert tx.in_tx_pool or tx.is_failed + + # test inputs and outputs + if not tx.is_miner_tx: + assert len(tx.inputs) > 0 + + for tx_input in tx.inputs: + assert tx == tx_input.tx + cls.test_input(tx_input, ctx) + + assert len(tx.outputs) > 0 + for output in tx.outputs: + assert tx == output.tx + cls.test_output(output, ctx) + + # test pruned vs not pruned + # tx might be pruned regardless of configuration + is_pruned: bool = tx.pruned_hex is not None + if ctx.is_pruned: + assert is_pruned + if ctx.from_get_tx_pool or ctx.from_binary_block: + assert tx.prunable_hash is None + else: + assert tx.prunable_hash is not None + + if is_pruned: + assert tx.rct_sig_prunable is None + assert tx.size is None + assert tx.last_relayed_timestamp is None + assert tx.received_timestamp is None + assert tx.full_hex is None + assert tx.pruned_hex is not None + else: + assert tx.version is not None + assert tx.version >= 0 + assert tx.unlock_time is not None + assert tx.unlock_time >= 0 + assert tx.extra is not None + assert len(tx.extra) > 0 + + if ctx.from_binary_block is True: + # TODO: get_blocks_by_height() has inconsistent client-side pruning + assert tx.full_hex is None + assert tx.rct_sig_prunable is None + else: + assert tx.full_hex is not None + assert len(tx.full_hex) > 0 + # TODO define and test this + #assert tx.rct_sig_prunable is not None + + assert tx.is_double_spend_seen is False + if tx.is_confirmed: + assert tx.last_relayed_timestamp is None + assert tx.received_timestamp is None + else: + if tx.is_relayed: + assert tx.last_relayed_timestamp is not None + assert tx.last_relayed_timestamp > 0 + else: + assert tx.last_relayed_timestamp is None + + assert tx.received_timestamp is not None + assert tx.received_timestamp > 0 + + # TODO test failed tx + + # test deep copy + if ctx.do_not_test_copy is not True: + cls.test_tx_copy(tx, ctx) + + @classmethod + def test_miner_tx(cls, miner_tx: Optional[MoneroTx]) -> None: + """Test monero miner tx""" + assert miner_tx is not None + AssertUtils.assert_not_none(miner_tx.is_miner_tx) + assert miner_tx.version is not None + AssertUtils.assert_true(miner_tx.version >= 0) + AssertUtils.assert_not_none(miner_tx.extra) + AssertUtils.assert_true(len(miner_tx.extra) > 0) + assert miner_tx.unlock_time is not None + AssertUtils.assert_true(miner_tx.unlock_time >= 0) + + # TODO: miner tx does not have hashes in binary requests so this will fail, need to derive using prunable data + # ctx = new TestContext() + # ctx.has_json = false + # ctx.is_pruned = true + # ctx.is_full = false + # ctx.is_confirmed = true + # ctx.is_miner = true + # ctx.from_get_tx_pool = true + # cls.test_tx(miner_tx, ctx) + + @classmethod + def get_and_test_txs(cls, wallet: MoneroWallet, query: Optional[MoneroTxQuery], ctx: Optional[TxContext], is_expected: bool) -> list[MoneroTxWallet]: + """Get and test txs from wallet""" + copy: Optional[MoneroTxQuery] = query.copy() if query is not None else None + txs = wallet.get_txs(query) if query is not None else wallet.get_txs() + assert txs is not None + + if is_expected is False: + assert len(txs) == 0 + + if is_expected is True: + assert len(txs) > 0 + + for tx in txs: + cls.test_tx_wallet(tx, ctx) + + cls.test_get_txs_structure(txs, query) + if query is not None: + AssertUtils.assert_equals(copy, query) + + return txs + + @classmethod + def is_tx_in_block(cls, tx: MoneroTxWallet, block: MoneroBlock) -> bool: + """Check if transaction is included in block""" + for block_tx in block.txs: + if block_tx.hash == tx.hash: + return True + + return False + + @classmethod + def is_block_in_blocks(cls, block: MoneroBlock, blocks: set[MoneroBlock] | list[MoneroBlock]) -> bool: + """Check if block is contained in set or list""" + for b in blocks: + if b == block: + return True + + return False + + @classmethod + def test_get_txs_structure(cls, txs: list[MoneroTxWallet], q: Optional[MoneroTxQuery]) -> None: + """Test txs structure""" + query = q if q is not None else MoneroTxQuery() + # collect unique blocks in order (using set and list instead of TreeSet for direct portability to other languages) + seen_blocks: set[MoneroBlock] = set() + blocks: list[MoneroBlock] = [] + unconfirmed_txs: list[MoneroTxWallet] = [] + + for tx in txs: + if tx.block is None: + unconfirmed_txs.append(tx) + else: + assert cls.is_tx_in_block(tx, tx.block) + if not cls.is_block_in_blocks(tx.block, seen_blocks): + seen_blocks.add(tx.block) + blocks.append(tx.block) + + # tx hashes must be in order if requested + if len(query.hashes) > 0: + assert len(txs) == len(query.hashes) + for i, query_hash in enumerate(query.hashes): + assert query_hash == txs[i].hash + + # test that txs and blocks reference each other and blocks are in ascending order unless specific tx hashes queried + index: int = 0 + prev_block_height: Optional[int] = None + for block in blocks: + if prev_block_height is None: + prev_block_height = block.height + elif len(query.hashes) == 0: + assert block.height is not None + assert block.height > prev_block_height + + for tx in block.txs: + assert tx.block == block + if len(query.hashes) == 0: + # verify tx order is self-consistent with blocks unless txs manually re-ordered by querying by hash + assert txs[index].hash == tx.hash + + index += 1 + + assert len(txs) == index + len(unconfirmed_txs), f"txs: {len(txs)}, unconfirmed txs: {len(unconfirmed_txs)}, index: {index}" + + # test that incoming transfers are in order of ascending accounts and subaddresses + for tx in txs: + if len(tx.incoming_transfers) == 0: + continue + + prev_account_idx: Optional[int] = None + prev_subaddress_idx: Optional[int] = None + for transfer in tx.incoming_transfers: + if prev_account_idx is None: + prev_account_idx = transfer.account_index + + else: + assert prev_account_idx is not None + assert transfer.account_index is not None + assert prev_account_idx <= transfer.account_index + if prev_account_idx < transfer.account_index: + prev_subaddress_idx = None + prev_account_idx = transfer.account_index + if prev_subaddress_idx is None: + prev_subaddress_idx = transfer.subaddress_index + else: + assert transfer.subaddress_index is not None + assert prev_subaddress_idx < transfer.subaddress_index + + # test that outputs are in order of ascending accounts and subaddresses + for tx in txs: + if len(tx.outputs) == 0: + continue + + prev_account_idx: Optional[int] = None + prev_subaddress_idx: Optional[int] = None + for output in tx.get_outputs_wallet(): + if prev_account_idx is None: + prev_account_idx = output.account_index + else: + assert output.account_index is not None + assert prev_account_idx <= output.account_index + if prev_account_idx < output.account_index: + prev_subaddress_idx = None + prev_account_idx = output.account_index + if prev_subaddress_idx: + prev_subaddress_idx = output.subaddress_index + else: + assert prev_subaddress_idx is not None + assert output.subaddress_index is not None + # TODO: this does not test that index < other index if subaddresses are equal + assert prev_subaddress_idx <= output.subaddress_index + + @classmethod + def get_random_transactions( + cls, + wallet: MoneroWallet, + query: Optional[MoneroTxQuery] = None, + min_txs: Optional[int] = None, + max_txs: Optional[int] = None + ) -> list[MoneroTxWallet]: + """Get random transaction from wallet""" + txs = wallet.get_txs(query if query is not None else MoneroTxQuery()) + + if min_txs is not None: + assert len(txs) >= min_txs, f"{len(txs)}/{min_txs} transactions found with the query" + + shuffle(txs) + + if max_txs is None: + return txs + + result: list[MoneroTxWallet] = [] + i = 0 + + for tx in txs: + result.append(tx) + if i >= max_txs - 1: + break + i += 1 + + return result + + @classmethod + def get_confirmed_tx_hashes(cls, daemon: MoneroDaemon) -> list[str]: + """Get confirmed tx hashes from daemon from last 5 blocks""" + hashes: list[str] = [] + height: int = daemon.get_height() + i = 0 + while i < 5 and height > 0: + height -= 1 + block = daemon.get_block_by_height(height) + for tx_hash in block.tx_hashes: + hashes.append(tx_hash) + return hashes + + @classmethod + def get_unrelayed_tx(cls, wallet: MoneroWallet, account_idx: int): + """Get unrelayed tx from wallet account""" + # TODO monero-project + assert account_idx > 0, "Txs sent from/to same account are not properly synced from the pool" + config = MoneroTxConfig() + config.account_index = account_idx + config.address = wallet.get_primary_address() + config.amount = cls.MAX_FEE + + tx = wallet.create_tx(config) + assert (tx.full_hex is None or tx.full_hex == "") is False + assert tx.relay is False + return tx diff --git a/tests/utils/wallet_utils.py b/tests/utils/wallet_utils.py new file mode 100644 index 0000000..c82282a --- /dev/null +++ b/tests/utils/wallet_utils.py @@ -0,0 +1,145 @@ +import logging + +from abc import ABC +from typing import Optional + +from monero import ( + MoneroNetworkType, MoneroUtils, MoneroAccount, + MoneroSubaddress +) + +from .gen_utils import GenUtils +from .assert_utils import AssertUtils + +logger: logging.Logger = logging.getLogger("WalletUtils") + + +class WalletUtils(ABC): + + @classmethod + def test_invalid_address(cls, address: Optional[str], network_type: MoneroNetworkType) -> None: + if address is None: + return + + AssertUtils.assert_false(MoneroUtils.is_valid_address(address, network_type)) + + try: + MoneroUtils.validate_address(address, network_type) + raise Exception("Should have thrown exception") + except Exception as e: + AssertUtils.assert_false(len(str(e)) == 0) + + @classmethod + def test_invalid_private_view_key(cls, private_view_key: Optional[str]): + if private_view_key is None: + return + + AssertUtils.assert_false(MoneroUtils.is_valid_private_view_key(private_view_key)) + + try: + MoneroUtils.validate_private_view_key(private_view_key) + raise Exception("Should have thrown exception") + except Exception as e: + AssertUtils.assert_false(len(str(e)) == 0) + + @classmethod + def test_invalid_public_view_key(cls, public_view_key: Optional[str]) -> None: + if public_view_key is None: + return + + AssertUtils.assert_false(MoneroUtils.is_valid_public_view_key(public_view_key)) + + try: + MoneroUtils.validate_public_view_key(public_view_key) + raise Exception("Should have thrown exception") + except Exception as e: + AssertUtils.assert_false(len(str(e)) == 0) + + @classmethod + def test_invalid_private_spend_key(cls, private_spend_key: Optional[str]): + if private_spend_key is None: + return + + AssertUtils.assert_false(MoneroUtils.is_valid_private_spend_key(private_spend_key)) + + try: + MoneroUtils.validate_private_spend_key(private_spend_key) + raise Exception("Should have thrown exception") + except Exception as e: + AssertUtils.assert_false(len(str(e)) == 0) + + @classmethod + def test_invalid_public_spend_key(cls, public_spend_key: Optional[str]): + if public_spend_key is None: + return + + AssertUtils.assert_false(MoneroUtils.is_valid_public_spend_key(public_spend_key)) + try: + MoneroUtils.validate_public_spend_key(public_spend_key) + raise Exception("Should have thrown exception") + except Exception as e: + AssertUtils.assert_false(len(str(e)) == 0) + + @classmethod + def test_account(cls, account: Optional[MoneroAccount], network_type: MoneroNetworkType, full: bool = True): + """Test a monero wallet account""" + # test account + assert account is not None + assert account.index is not None + assert account.index >= 0 + assert account.primary_address is not None + + MoneroUtils.validate_address(account.primary_address, network_type) + if full: + GenUtils.test_unsigned_big_integer(account.balance) + GenUtils.test_unsigned_big_integer(account.unlocked_balance) + + # if given, test subaddresses and that their balances add up to account balances + if len(account.subaddresses) > 0: + balance = 0 + unlocked_balance = 0 + i = 0 + j = len(account.subaddresses) + while i < j: + cls.test_subaddress(account.subaddresses[i]) + assert account.index == account.subaddresses[i].account_index + assert i == account.subaddresses[i].index + address_balance = account.subaddresses[i].balance + assert address_balance is not None + balance += address_balance + address_balance = account.subaddresses[i].unlocked_balance + assert address_balance is not None + unlocked_balance += address_balance + i += 1 + + msg1 = f"Subaddress balances {balance} != account {account.index} balance {account.balance}" + msg2 = f"Subaddress unlocked balances {unlocked_balance} != account {account.index} unlocked balance {account.unlocked_balance}" + assert account.balance == balance, msg1 + assert account.unlocked_balance == unlocked_balance, msg2 + + # tag must be undefined or non-empty + tag = account.tag + assert tag is None or len(tag) > 0 + + @classmethod + def test_subaddress(cls, subaddress: Optional[MoneroSubaddress], full: bool = True): + assert subaddress is not None + assert subaddress.account_index is not None + assert subaddress.index is not None + if full: + assert subaddress.balance is not None + assert subaddress.num_unspent_outputs is not None + assert subaddress.num_blocks_to_unlock is not None + GenUtils.test_unsigned_big_integer(subaddress.balance) + GenUtils.test_unsigned_big_integer(subaddress.unlocked_balance) + AssertUtils.assert_true(subaddress.num_unspent_outputs >= 0) + AssertUtils.assert_not_none(subaddress.is_used) + if subaddress.balance > 0: + AssertUtils.assert_true(subaddress.is_used) + AssertUtils.assert_true(subaddress.num_blocks_to_unlock >= 0) + + AssertUtils.assert_true(subaddress.account_index >= 0) + AssertUtils.assert_true(subaddress.index >= 0) + AssertUtils.assert_not_none(subaddress.address) + # TODO fix monero-cpp/monero_wallet_full.cpp to return boost::none on empty label + #AssertUtils.assert_true(subaddress.label is None or subaddress.label != "")