From 6f1bb12df82e56b76d83a7c1a1c85d45dacdc1e9 Mon Sep 17 00:00:00 2001 From: everoddandeven Date: Fri, 20 Mar 2026 01:49:14 +0100 Subject: [PATCH] Wallet and daemon rpc fixes * Fix `PyMoneroTx::from_property_tree` tx fee deserialization * Fix `PyMoneroDaemon::get_miner_tx_sum` * Fix `PyMoneroTxWallet::from_property_tree_with_transfer` * Fix `PyMoneroWalletRpc::check_tx_proof` * Fix `PyMoneroWalletRpc::sweep_account` * Fix `PyMoneroWalletRpc::get_transfers_aux` * Fix `PyMoneroWalletRpc::close` * Implement `PyMoneroWalletRpc::is_closed` * Consolidate `PyMoneroDaemonRpc::get_tx_pool_hashes` * Remove `AssertUtils.assert_equals` for simple comparison * Set `--rpc-max-connections-per-private-ip=100` in `monero-wallet-rpc` docker containers * Re-enable rpc wallet tests that were failing for Network error * Set config `sync_period_in_ms=2500` --- src/cpp/daemon/py_monero_daemon_model.cpp | 2 +- src/cpp/daemon/py_monero_daemon_rpc.cpp | 2 +- src/cpp/py_monero.cpp | 15 +-- src/cpp/wallet/py_monero_wallet_model.cpp | 18 ++-- src/cpp/wallet/py_monero_wallet_rpc.cpp | 70 +++++++++----- src/cpp/wallet/py_monero_wallet_rpc.h | 26 ++--- tests/config/config.ini | 2 +- tests/docker-compose.yml | 2 + tests/test_monero_daemon_rpc.py | 17 +++- tests/test_monero_wallet_common.py | 111 ++++++++++++---------- tests/test_monero_wallet_full.py | 7 +- tests/test_monero_wallet_keys.py | 12 ++- tests/test_monero_wallet_rpc.py | 38 +++----- tests/utils/daemon_utils.py | 6 +- tests/utils/test_utils.py | 11 ++- tests/utils/tx_utils.py | 3 +- 16 files changed, 190 insertions(+), 152 deletions(-) diff --git a/src/cpp/daemon/py_monero_daemon_model.cpp b/src/cpp/daemon/py_monero_daemon_model.cpp index abb67ee..f083966 100644 --- a/src/cpp/daemon/py_monero_daemon_model.cpp +++ b/src/cpp/daemon/py_monero_daemon_model.cpp @@ -260,7 +260,7 @@ void PyMoneroTx::from_property_tree(const boost::property_tree::ptree& node, con for(auto it2 = node2.begin(); it2 != node2.end(); ++it2) { std::string _key = it2->first; - if (_key == std::string("txnfee")) { + if (_key == std::string("txnFee")) { tx->m_fee = it2->second.get_value(); } } diff --git a/src/cpp/daemon/py_monero_daemon_rpc.cpp b/src/cpp/daemon/py_monero_daemon_rpc.cpp index a7ed323..7bda45a 100644 --- a/src/cpp/daemon/py_monero_daemon_rpc.cpp +++ b/src/cpp/daemon/py_monero_daemon_rpc.cpp @@ -370,7 +370,7 @@ std::vector PyMoneroDaemonRpc::get_tx_hexes(const std::vector PyMoneroDaemonRpc::get_miner_tx_sum(uint64_t height, uint64_t num_blocks) { - auto params = std::make_shared(); + auto params = std::make_shared(height, num_blocks); PyMoneroJsonRequest request("get_coinbase_tx_sum", params); std::shared_ptr response = m_rpc->send_json_request(request); if (response->m_result == boost::none) throw std::runtime_error("Invalid Monero JSONRPC response"); diff --git a/src/cpp/py_monero.cpp b/src/cpp/py_monero.cpp index 0f495e5..db31127 100644 --- a/src/cpp/py_monero.cpp +++ b/src/cpp/py_monero.cpp @@ -2106,9 +2106,9 @@ PYBIND11_MODULE(monero, m) { // monero_wallet_rpc py_monero_wallet_rpc - .def(py::init>(), py::arg("rpc_connection")) + .def(py::init&>(), py::arg("rpc_connection")) .def(py::init(), py::arg("uri") = "", py::arg("username") = "", py::arg("password") = "") - .def("create_wallet", [](PyMoneroWalletRpc& self, const std::shared_ptr config) { + .def("create_wallet", [](PyMoneroWalletRpc& self, const std::shared_ptr& config) { try { self.create_wallet(config); return &self; @@ -2120,10 +2120,10 @@ PYBIND11_MODULE(monero, m) { throw; } catch(const std::exception& ex) { - throw py::value_error(ex.what()); + throw PyMoneroError(ex.what()); } }, py::arg("config")) - .def("open_wallet", [](PyMoneroWalletRpc& self, const std::shared_ptr config) { + .def("open_wallet", [](PyMoneroWalletRpc& self, const std::shared_ptr& config) { try { self.open_wallet(config); return &self; @@ -2135,7 +2135,7 @@ PYBIND11_MODULE(monero, m) { throw; } catch(const std::exception& ex) { - throw py::value_error(ex.what()); + throw PyMoneroError(ex.what()); } }, py::arg("config")) .def("open_wallet", [](PyMoneroWalletRpc& self, const std::string& name, const std::string& password) { @@ -2150,9 +2150,12 @@ PYBIND11_MODULE(monero, m) { throw; } catch(const std::exception& ex) { - throw py::value_error(ex.what()); + throw PyMoneroError(ex.what()); } }, py::arg("name"), py::arg("password")) + .def("is_closed", [](const PyMoneroWalletRpc& self) { + MONERO_CATCH_AND_RETHROW(self.is_closed()); + }) .def("get_seed_languages", [](PyMoneroWalletRpc& self) { MONERO_CATCH_AND_RETHROW(self.get_seed_languages()); }) diff --git a/src/cpp/wallet/py_monero_wallet_model.cpp b/src/cpp/wallet/py_monero_wallet_model.cpp index 02e4bb4..c6c38e1 100644 --- a/src/cpp/wallet/py_monero_wallet_model.cpp +++ b/src/cpp/wallet/py_monero_wallet_model.cpp @@ -395,6 +395,7 @@ void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tr outgoing_transfer->m_tx = tx; } auto node2 = it->second; + outgoing_transfer->m_destinations.clear(); for(auto it2 = node2.begin(); it2 != node2.end(); ++it2) { auto node3 = it2->second; @@ -413,10 +414,7 @@ void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tr } else if (key == std::string("amount_in")) tx->m_input_sum = it->second.get_value(); else if (key == std::string("amount_out")) tx->m_input_sum = it->second.get_value(); - else if (key == std::string("change_address")) { - std::string change_address = it->second.data(); - if (change_address != std::string("")) tx->m_change_address = it->second.data(); - } + else if (key == std::string("change_address") && !it->second.data().empty()) tx->m_change_address = it->second.data(); else if (key == std::string("change_amount")) tx->m_change_amount = it->second.get_value(); else if (key == std::string("dummy_outputs")) tx->m_num_dummy_outputs = it->second.get_value(); //else if (key == std::string("extra")) tx->m_extra = it->second.data(); @@ -467,7 +465,7 @@ void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tr auto destinations = config.get_normalized_destinations(); size_t num_destinations = destinations.size(); if (num_destinations != amounts_by_dest.size()) throw std::runtime_error("Expected destinations size equal to amounts by dest size"); - + outgoing_transfer->m_destinations.clear(); for(uint64_t i = 0; i < num_destinations; i++) { auto dest = std::make_shared(); dest->m_address = destinations[i]->m_address; @@ -493,7 +491,7 @@ void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tr if (tx->m_outgoing_transfer != boost::none) { // overwrite to avoid reconcile error TODO: remove after >18.3.1 when amounts_by_dest supported - if (tx->m_outgoing_transfer.get()->m_destinations.size() != 0) { + if (!outgoing_transfer->m_destinations.empty()) { tx->m_outgoing_transfer.get()->m_destinations.clear(); } tx->m_outgoing_transfer.get()->merge(tx->m_outgoing_transfer.get(), outgoing_transfer); @@ -826,6 +824,7 @@ void PyMoneroTxSet::from_sent_txs(const boost::property_tree::ptree& node, const if (conf == boost::none) throw std::runtime_error("Expected tx configuration"); auto config = conf.get(); if (config.m_destinations.size() == 1) { + // sweeping can create multiple withone address auto dest = std::make_shared(); dest->m_address = config.m_destinations[0]->m_address; dest->m_amount = amount; @@ -1786,6 +1785,13 @@ void PyMoneroCheckTxProof::from_property_tree(const boost::property_tree::ptree& else if (key == std::string("confirmations")) check->m_num_confirmations = it->second.get_value(); else if (key == std::string("received")) check->m_received_amount = it->second.get_value(); } + + if (!bool_equals_2(true, check->m_is_good)) { + // normalize invalid tx proof + check->m_in_tx_pool = boost::none; + check->m_num_confirmations = boost::none; + check->m_received_amount = boost::none; + } } std::string PyMoneroReserveProofSignature::from_property_tree(const boost::property_tree::ptree& node) { diff --git a/src/cpp/wallet/py_monero_wallet_rpc.cpp b/src/cpp/wallet/py_monero_wallet_rpc.cpp index 13287cf..f153ff5 100644 --- a/src/cpp/wallet/py_monero_wallet_rpc.cpp +++ b/src/cpp/wallet/py_monero_wallet_rpc.cpp @@ -1,6 +1,11 @@ #include "py_monero_wallet_rpc.h" #include "utils/monero_utils.h" +PyMoneroWalletPoller::PyMoneroWalletPoller(PyMoneroWallet *wallet) { + m_wallet = wallet; + m_is_polling = false; + m_num_polling = 0; +} PyMoneroWalletPoller::~PyMoneroWalletPoller() { set_is_polling(false); @@ -214,7 +219,23 @@ bool PyMoneroWalletPoller::check_for_changed_balances() { return false; } +PyMoneroWalletRpc::PyMoneroWalletRpc() { + m_rpc = std::make_shared(); +} + +PyMoneroWalletRpc::PyMoneroWalletRpc(const std::shared_ptr& rpc_connection) { + m_rpc = rpc_connection; + if (!m_rpc->is_online() && !m_rpc->m_uri->empty()) m_rpc->check_connection(); +} + +PyMoneroWalletRpc::PyMoneroWalletRpc(const std::string& uri, const std::string& username, const std::string& password) { + m_rpc = std::make_shared(uri, username, password); + if (!m_rpc->m_uri->empty()) m_rpc->check_connection(); +} + PyMoneroWalletRpc::~PyMoneroWalletRpc() { + MTRACE("~PyMoneroWalletRpc()"); + clear(); } void PyMoneroWalletRpc::add_listener(monero_wallet_listener& listener) { @@ -1091,6 +1112,7 @@ std::vector> PyMoneroWalletRpc::sweep_unlocked } else { std::vector subaddress_indices; for (const monero_subaddress& subaddress : monero_wallet::get_subaddresses(config.m_account_index.get())) { + // TODO wallet rpc sweep_all now supports req.subaddr_indices_all if (subaddress.m_unlocked_balance.get() > 0) subaddress_indices.push_back(subaddress.m_index.get()); } indices[config.m_account_index.get()] = subaddress_indices; @@ -1141,7 +1163,6 @@ std::vector> PyMoneroWalletRpc::sweep_unlocked return txs; } - std::shared_ptr PyMoneroWalletRpc::sweep_output(const monero_tx_config& config) { // validate request std::vector> destinations = config.get_normalized_destinations(); @@ -1347,11 +1368,11 @@ std::shared_ptr PyMoneroWalletRpc::check_tx_proof(const std::st PyMoneroCheckTxProof::from_property_tree(node, check); return check; } catch (const PyMoneroRpcError& ex) { - if (ex.code == -1 && ex.what() == std::string("basic_string")) { + // normalize error message + if (ex.code == -1 && std::string(ex.what()).find("basic_string") != std::string::npos) { throw PyMoneroRpcError(-1, "Must provide signature to check tx proof"); } if (ex.code == -8 && ex.what() == std::string("TX ID has invalid format")) { - // normalize error message throw PyMoneroRpcError(-8, "TX hash has invalid format"); } throw; @@ -1709,7 +1730,18 @@ void PyMoneroWalletRpc::save() { m_rpc->send_json_request(request); } +bool PyMoneroWalletRpc::is_closed() const { + try { + get_primary_address(); + } catch (const PyMoneroRpcError& ex) { + return ex.code == -8 && ex.what() == std::string("No wallet file"); + } + + return false; +} + void PyMoneroWalletRpc::close(bool save) { + clear(); auto params = std::make_shared(save); PyMoneroJsonRequest request("close_wallet", params); m_rpc->send_json_request(request); @@ -1839,15 +1871,15 @@ std::string PyMoneroWalletRpc::query_key(const std::string& key_type) const { std::vector> PyMoneroWalletRpc::sweep_account(const monero_tx_config &conf) { auto config = conf.copy(); + // validate config if (config.m_account_index == boost::none) throw std::runtime_error("Must specify an account index to sweep from"); std::vector> destinations = config.get_normalized_destinations(); - if (destinations.size() != 1) throw std::runtime_error("Must specify exactly one destination to sweep to"); + if (destinations.size() != 1) throw std::runtime_error("Must provide exactly one destination address to sweep output to"); if (destinations[0]->m_address == boost::none) throw std::runtime_error("Must specify destination address to sweep to"); - if (destinations[0]->m_amount != boost::none) throw std::runtime_error("Cannot specify amount in sweep request"); - if (config.m_key_image != boost::none) throw std::runtime_error("Key image defined; use sweepOutput() to sweep an output by its key image"); - //if (config.m_subaddress_indices.size() == 0) throw std::runtime_error("Empty list given for subaddresses indices to sweep"); + if (destinations[0]->m_amount != boost::none) throw std::runtime_error("Cannot specify destination amount to sweep"); + if (config.m_key_image != boost::none) throw std::runtime_error("Cannot define key image in sweep_account(); use sweep_output() to sweep an output by its key image"); if (bool_equals_2(true, config.m_sweep_each_subaddress)) throw std::runtime_error("Cannot sweep each subaddress with RPC `sweep_all`"); - if (config.m_subtract_fee_from.size() > 0) throw std::runtime_error("Sweeping output does not support subtracting fees from destinations"); + if (config.m_subtract_fee_from.size() > 0) throw std::runtime_error("Sweep transactions do not support subtracting fees from destinations"); // sweep from all subaddresses if not otherwise defined if (config.m_subaddress_indices.empty()) { @@ -1911,21 +1943,13 @@ void PyMoneroWalletRpc::clear_address_cache() { } void PyMoneroWalletRpc::refresh_listening() { - if (m_rpc->m_zmq_uri == boost::none || m_rpc->m_zmq_uri.get().empty()) { - if (m_poller == nullptr && m_listeners.size() > 0) { - m_poller = std::make_shared(this); - if (m_sync_period_in_ms != boost::none) m_poller->set_period_in_ms(m_sync_period_in_ms.get()); - } - if (m_poller != nullptr) { - m_poller->set_is_polling(m_listeners.size() > 0); - } + if (m_poller == nullptr && !m_listeners.empty()) { + m_poller = std::make_unique(this); + if (m_sync_period_in_ms != boost::none) m_poller->set_period_in_ms(m_sync_period_in_ms.get()); } - /* - else { - if (m_zmq_listener == nullptr && m_listeners.size() > 0) m_zmq_listener = std::make_shared(); - if (m_zmq_listener != nullptr) m_zmq_listener.set_is_polling(m_listeners.size() > 0); + if (m_poller != nullptr) { + m_poller->set_is_polling(!m_listeners.empty()); } - */ } void PyMoneroWalletRpc::poll() { @@ -2178,7 +2202,7 @@ std::vector> PyMoneroWalletRpc::get_transfers_a } if (_query->m_account_index == boost::none) { - if (_query->m_subaddress_index != boost::none) throw std::runtime_error("Filter specifies a subaddress index but not an account index"); + if (_query->m_subaddress_index != boost::none || !_query->m_subaddress_indices.empty()) throw std::runtime_error("Filter specifies a subaddress index but not an account index"); params->m_all_accounts = true; } else { params->m_account_index = _query->m_account_index; @@ -2269,6 +2293,7 @@ std::vector> PyMoneroWalletRpc::get_output for (const auto& subaddress_idx : _query->m_subaddress_indices) { subaddress_indices.push_back(subaddress_idx); } + // null will fetch from all subaddresses indices[_query->m_account_index.get()] = subaddress_indices; } else { @@ -2295,6 +2320,7 @@ std::vector> PyMoneroWalletRpc::get_output uint32_t account_idx = kv.first; params->m_account_index = account_idx; params->m_subaddr_indices = kv.second; + // send request PyMoneroJsonRequest request("incoming_transfers", params); auto response = m_rpc->send_json_request(request); if (response->m_result == boost::none) throw std::runtime_error("Invalid Monero JSONRPC response"); diff --git a/src/cpp/wallet/py_monero_wallet_rpc.h b/src/cpp/wallet/py_monero_wallet_rpc.h index f99cfd2..610ac0b 100644 --- a/src/cpp/wallet/py_monero_wallet_rpc.h +++ b/src/cpp/wallet/py_monero_wallet_rpc.h @@ -5,13 +5,9 @@ class PyMoneroWalletPoller { public: - explicit PyMoneroWalletPoller(PyMoneroWallet *wallet) { - m_wallet = wallet; - m_is_polling = false; - m_num_polling = 0; - } ~PyMoneroWalletPoller(); + PyMoneroWalletPoller(PyMoneroWallet *wallet); bool is_polling() const { return m_is_polling; } void set_is_polling(bool is_polling); @@ -42,21 +38,10 @@ class PyMoneroWalletPoller { class PyMoneroWalletRpc : public PyMoneroWallet { public: - PyMoneroWalletRpc() { - m_rpc = std::make_shared(); - } - - PyMoneroWalletRpc(std::shared_ptr rpc_connection) { - m_rpc = rpc_connection; - if (!m_rpc->is_online() && !m_rpc->m_uri->empty()) m_rpc->check_connection(); - } - - PyMoneroWalletRpc(const std::string& uri = "", const std::string& username = "", const std::string& password = "") { - m_rpc = std::make_shared(uri, username, password); - if (!m_rpc->m_uri->empty()) m_rpc->check_connection(); - } - ~PyMoneroWalletRpc(); + PyMoneroWalletRpc(); + PyMoneroWalletRpc(const std::shared_ptr& rpc_connection); + PyMoneroWalletRpc(const std::string& uri = "", const std::string& username = "", const std::string& password = ""); PyMoneroWalletRpc* open_wallet(const std::shared_ptr &config); PyMoneroWalletRpc* open_wallet(const std::string& name, const std::string& password); @@ -177,6 +162,7 @@ class PyMoneroWalletRpc : public PyMoneroWallet { std::vector submit_multisig_tx_hex(const std::string& signed_multisig_tx_hex); void change_password(const std::string& old_password, const std::string& new_password) override; void save() override; + bool is_closed() const override; void close(bool save = false) override; std::shared_ptr get_balances(boost::optional account_idx, boost::optional subaddress_idx) const override; @@ -186,7 +172,7 @@ class PyMoneroWalletRpc : public PyMoneroWallet { std::string m_path = ""; std::shared_ptr m_rpc; std::shared_ptr m_daemon_connection; - std::shared_ptr m_poller; + std::unique_ptr m_poller; mutable boost::recursive_mutex m_sync_mutex; mutable std::unordered_map> m_address_cache; diff --git a/tests/config/config.ini b/tests/config/config.ini index c56c522..ae88d26 100644 --- a/tests/config/config.ini +++ b/tests/config/config.ini @@ -33,7 +33,7 @@ rpc_zmq_enabled=False rpc_zmq_port_start=48083 rpc_zmq_bind_port_start=48083 rpc_zmq_domain=127.0.0.1 -sync_period_in_ms=5000 +sync_period_in_ms=2500 [mining_wallet] name=mining_wallet diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 7c96885..14d0ac3 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -86,6 +86,7 @@ services: "--daemon-address=node_2:18081", "--daemon-login=rpc_daemon_user:abc123", "--rpc-login=rpc_user:abc123", + "--rpc-max-connections-per-private-ip=100", "--wallet-dir=/wallet", "--rpc-access-control-origins=*", "--non-interactive" @@ -112,6 +113,7 @@ services: "--daemon-address=node_2:18081", "--daemon-login=rpc_daemon_user:abc123", "--rpc-login=rpc_user:abc123", + "--rpc-max-connections-per-private-ip=100", "--wallet-dir=/wallet", "--rpc-access-control-origins=*", "--non-interactive" diff --git a/tests/test_monero_daemon_rpc.py b/tests/test_monero_daemon_rpc.py index 96292ce..6004ec9 100644 --- a/tests/test_monero_daemon_rpc.py +++ b/tests/test_monero_daemon_rpc.py @@ -29,6 +29,7 @@ class TestMoneroDaemonRpc: """Rpc daemon integration tests""" BINARY_BLOCK_CTX: BinaryBlockContext = BinaryBlockContext() + _test_wallet: MoneroWalletRpc | None = None #region Fixtures @@ -50,7 +51,10 @@ def daemon(self) -> MoneroDaemonRpc: @pytest.fixture(scope="class") def wallet(self) -> MoneroWalletRpc: """Test rpc wallet instance""" - return Utils.get_wallet_rpc() + if self._test_wallet is None: + self._test_wallet = Utils.get_wallet_rpc() + + return self._test_wallet #endregion @@ -482,7 +486,7 @@ def test_get_tx_hexes_by_hashes(self, daemon: MoneroDaemonRpc) -> None: @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_miner_tx_sum(self, daemon: MoneroDaemonRpc) -> None: tx_sum = daemon.get_miner_tx_sum(0, min(5000, daemon.get_height())) - DaemonUtils.test_miner_tx_sum(tx_sum, Utils.REGTEST) + 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") @@ -509,6 +513,11 @@ def test_get_txs_in_pool(self, daemon: MoneroDaemonRpc, wallet: MoneroWalletRpc) # fetch txs in pool txs: list[MoneroTx] = daemon.get_tx_pool() + num_txs: int = len(txs) + + # fetch tx hashes in pool + tx_hashes: list[str] = daemon.get_tx_pool_hashes() + num_tx_hashes: int = len(tx_hashes) # context for testing tx ctx: TestContext = TestContext() @@ -517,9 +526,11 @@ def test_get_txs_in_pool(self, daemon: MoneroDaemonRpc, wallet: MoneroWalletRpc) ctx.from_get_tx_pool = True # test txs - assert len(txs) > 0, "Test requires an unconfirmed tx in the tx pool" + assert num_txs > 0, "Test requires an unconfirmed tx in the tx pool" + assert num_txs == num_tx_hashes, f"Txs in pool {num_txs} != Txs hashes in pool {num_tx_hashes}" for a_tx in txs: TxUtils.test_tx(a_tx, ctx) + assert a_tx.hash in tx_hashes # flush the tx from the pool, gg daemon.flush_tx_pool(tx.hash) diff --git a/tests/test_monero_wallet_common.py b/tests/test_monero_wallet_common.py index e2a0683..a7968a0 100644 --- a/tests/test_monero_wallet_common.py +++ b/tests/test_monero_wallet_common.py @@ -39,8 +39,8 @@ class BaseTestMoneroWallet(ABC): CREATED_WALLET_KEYS_ERROR: str = "Wallet created from keys is not connected to authenticated daemon" _test_wallet: Optional[MoneroWallet] = None - @property - def wallet_type(self) -> WalletType: + @classmethod + def get_wallet_type(cls) -> WalletType: """Wallet type to test""" return WalletType.UNDEFINED @@ -77,25 +77,25 @@ def _get_test_daemon(cls) -> MoneroDaemonRpc: """ return TestUtils.get_daemon_rpc() - def get_test_wallet(self) -> MoneroWallet: + @classmethod + def get_test_wallet(cls) -> MoneroWallet: """ Get the main wallet to test. :return MoneroWallet: the wallet to test """ - if self._test_wallet is not None: - return self._test_wallet - wallet_type: WalletType = self.wallet_type - if wallet_type == WalletType.FULL: - self._test_wallet = TestUtils.get_wallet_full() - elif wallet_type == WalletType.RPC: - self._test_wallet = TestUtils.get_wallet_rpc() - elif wallet_type == WalletType.KEYS: - self._test_wallet = TestUtils.get_wallet_keys() - else: - raise Exception("Cannot get test wallet: No wallet type setup for tests") + if cls._test_wallet is None: + wallet_type: WalletType = cls.get_wallet_type() + if wallet_type == WalletType.FULL: + cls._test_wallet = TestUtils.get_wallet_full() + elif wallet_type == WalletType.RPC: + cls._test_wallet = TestUtils.get_wallet_rpc() + elif wallet_type == WalletType.KEYS: + cls._test_wallet = TestUtils.get_wallet_keys() + else: + raise Exception("Cannot get test wallet: No wallet type setup for tests") - return self._test_wallet + return cls._test_wallet @abstractmethod def _open_wallet(self, config: Optional[MoneroWalletConfig]) -> MoneroWallet: @@ -195,7 +195,7 @@ def setup_and_teardown(self, request: pytest.FixtureRequest): def before_all(self) -> None: """Executed once before all tests""" logger.info(f"Setup test class {type(self).__name__}") - IntegrationTestUtils.setup(self.wallet_type) + IntegrationTestUtils.setup(self.get_wallet_type()) # After all tests def after_all(self) -> None: @@ -593,7 +593,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 - AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + assert MoneroWallet.DEFAULT_LANGUAGE == wallet.get_seed_language() finally: self._close_wallet(wallet) @@ -603,7 +603,8 @@ def test_create_wallet_random(self) -> None: config.path = path self._create_wallet(config) except Exception as e: - AssertUtils.assert_equals("Wallet already exists: " + path, str(e)) + e_msg: str = str(e) + assert "Wallet already exists: " + path == e_msg, e_msg # attempt to create wallet with unknown language try: @@ -612,7 +613,8 @@ def test_create_wallet_random(self) -> None: self._create_wallet(config) raise Exception("Should have thrown error") except Exception as e: - AssertUtils.assert_equals("Unknown language: english", str(e)) + e_msg: str = str(e) + assert "Unknown language: english" == e_msg, e_msg # Can create a wallet from a seed @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -630,12 +632,12 @@ def test_create_wallet_from_seed(self, wallet: MoneroWallet, test_config: BaseTe w: MoneroWallet = self._create_wallet(config) path = w.get_path() try: - AssertUtils.assert_equals(primary_address, w.get_primary_address()) - AssertUtils.assert_equals(private_view_key, w.get_private_view_key()) - AssertUtils.assert_equals(private_spend_key, w.get_private_spend_key()) - AssertUtils.assert_equals(TestUtils.SEED, w.get_seed()) + assert primary_address == w.get_primary_address() + assert private_view_key == w.get_private_view_key() + assert private_spend_key == w.get_private_spend_key() + assert TestUtils.SEED, w.get_seed() if not isinstance(w, MoneroWalletRpc): - AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, w.get_seed_language()) + assert MoneroWallet.DEFAULT_LANGUAGE == w.get_seed_language() finally: self._close_wallet(w) @@ -646,7 +648,8 @@ def test_create_wallet_from_seed(self, wallet: MoneroWallet, test_config: BaseTe config.restore_height = TestUtils.FIRST_RECEIVE_HEIGHT self._create_wallet(config) except Exception as e: - AssertUtils.assert_equals("Invalid mnemonic", str(e)) + e_msg: str = str(e) + assert "Invalid mnemonic" == e_msg, e_msg # attempt to create wallet at same path try: @@ -655,7 +658,8 @@ def test_create_wallet_from_seed(self, wallet: MoneroWallet, test_config: BaseTe self._create_wallet(config) raise Exception("Should have thrown error") except Exception as e: - AssertUtils.assert_equals("Wallet already exists: " + path, str(e)) + e_msg: str = str(e) + assert "Wallet already exists: " + path == e_msg, e_msg # Can create a wallet from a seed with offset @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -673,7 +677,7 @@ def test_create_wallet_from_seed_with_offset(self) -> None: assert TestUtils.ADDRESS != wallet.get_primary_address() if not isinstance(wallet, MoneroWalletRpc): # TODO monero-wallet-rpc: support - AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + assert MoneroWallet.DEFAULT_LANGUAGE == wallet.get_seed_language() finally: self._close_wallet(wallet) @@ -695,9 +699,9 @@ def test_create_wallet_from_keys(self, daemon: MoneroDaemonRpc, wallet: MoneroWa path = w.get_path() try: - AssertUtils.assert_equals(primary_address, w.get_primary_address()) - AssertUtils.assert_equals(private_view_key, w.get_private_view_key()) - AssertUtils.assert_equals(private_spend_key, w.get_private_spend_key()) + assert primary_address == w.get_primary_address() + assert private_view_key == w.get_private_view_key() + assert private_spend_key == w.get_private_spend_key() if not w.is_connected_to_daemon(): # TODO monero-project: keys wallets not connected logger.warning(f"WARNING: {self.CREATED_WALLET_KEYS_ERROR}") @@ -705,7 +709,7 @@ def test_create_wallet_from_keys(self, daemon: MoneroDaemonRpc, wallet: MoneroWa if not isinstance(w, MoneroWalletRpc): # TODO monero-wallet-rpc: cannot get seed from wallet created from keys? MoneroUtils.validate_mnemonic(w.get_seed()) - AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, w.get_seed_language()) + assert MoneroWallet.DEFAULT_LANGUAGE == w.get_seed_language() finally: self._close_wallet(w) @@ -717,9 +721,9 @@ def test_create_wallet_from_keys(self, daemon: MoneroDaemonRpc, wallet: MoneroWa w = self._create_wallet(config) try: - AssertUtils.assert_equals(primary_address, w.get_primary_address()) - AssertUtils.assert_equals(private_view_key, w.get_private_view_key()) - AssertUtils.assert_equals(private_spend_key, w.get_private_spend_key()) + assert primary_address == w.get_primary_address() + assert private_view_key == w.get_private_view_key() + assert private_spend_key == w.get_private_spend_key() if not w.is_connected_to_daemon(): # TODO monero-project: keys wallets not connected logger.warning(f"{self.CREATED_WALLET_KEYS_ERROR}") @@ -727,7 +731,7 @@ def test_create_wallet_from_keys(self, daemon: MoneroDaemonRpc, wallet: MoneroWa if not isinstance(w, MoneroWalletRpc): # TODO monero-wallet-rpc: cannot get seed from wallet created from keys? MoneroUtils.validate_mnemonic(w.get_seed()) - AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, w.get_seed_language()) + assert MoneroWallet.DEFAULT_LANGUAGE == w.get_seed_language() finally: self._close_wallet(w) @@ -738,7 +742,8 @@ def test_create_wallet_from_keys(self, daemon: MoneroDaemonRpc, wallet: MoneroWa self._create_wallet(config) raise Exception("Should have thrown error") except Exception as e: - AssertUtils.assert_equals("Wallet already exists: " + path, str(e)) + e_msg: str = str(e) + assert "Wallet already exists: " + path == e_msg, e_msg # Can create wallets with subaddress lookahead @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -798,7 +803,7 @@ def test_get_path(self) -> None: wallet = self._open_wallet_from_path(path, None) # test the attribute - AssertUtils.assert_equals(uuid, wallet.get_attribute("uuid")) + assert uuid == wallet.get_attribute("uuid") self._close_wallet(wallet) # Can set the daemon connection @@ -847,12 +852,12 @@ def test_set_daemon_connection(self) -> None: # nullify daemon connection wallet.set_daemon_connection(None) - AssertUtils.assert_equals(None, wallet.get_daemon_connection()) + assert wallet.get_daemon_connection() is None wallet.set_daemon_connection(daemon_rpc_uri) connection = MoneroRpcConnection(daemon_rpc_uri) AssertUtils.assert_equals(connection, wallet.get_daemon_connection()) wallet.set_daemon_connection(None) - AssertUtils.assert_equals(None, wallet.get_daemon_connection()) + assert wallet.get_daemon_connection() is None # set daemon uri to non-daemon if not TestUtils.IN_CONTAINER: # TODO sometimes this fails in container... @@ -870,7 +875,8 @@ def test_set_daemon_connection(self) -> None: wallet.sync() raise Exception("Exception expected") except Exception as e: - AssertUtils.assert_equals("Wallet is not connected to daemon", str(e)) + e_msg: str = str(e) + assert "Wallet is not connected to daemon" == e_msg, e_msg finally: self._close_wallet(wallet) @@ -879,13 +885,13 @@ def test_set_daemon_connection(self) -> None: def test_get_seed(self, wallet: MoneroWallet) -> None: seed = wallet.get_seed() MoneroUtils.validate_mnemonic(seed) - AssertUtils.assert_equals(TestUtils.SEED, seed) + assert TestUtils.SEED == seed # Can get the language of the seed @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_seed_language(self, wallet: MoneroWallet) -> None: language = wallet.get_seed_language() - AssertUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, language) + assert MoneroWallet.DEFAULT_LANGUAGE == language # Can get a list of supported languages for the seed @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -924,17 +930,17 @@ def test_get_public_spend_key(self, wallet: MoneroWallet) -> None: def test_get_primary_address(self, wallet: MoneroWallet) -> None: primary_address = wallet.get_primary_address() MoneroUtils.validate_address(primary_address, TestUtils.NETWORK_TYPE) - AssertUtils.assert_equals(wallet.get_address(0, 0), primary_address) + assert wallet.get_address(0, 0) == primary_address # Can get the address of a subaddress at a specified account and subaddress index @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_subaddress_address(self, wallet: MoneroWallet) -> None: - AssertUtils.assert_equals(wallet.get_primary_address(), (wallet.get_address(0, 0))) + assert wallet.get_primary_address() == wallet.get_address(0, 0) for account in wallet.get_accounts(True): for subaddress in account.subaddresses: assert account.index is not None assert subaddress.index is not None - AssertUtils.assert_equals(subaddress.address, wallet.get_address(account.index, subaddress.index)) + assert subaddress.address == wallet.get_address(account.index, subaddress.index) # Can get addresses out of range of used accounts and subaddresses @pytest.mark.skipif(TestUtils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -959,8 +965,8 @@ def test_get_address_indices(self, wallet: MoneroWallet) -> None: # get address index subaddress = wallet.get_address_index(address) - AssertUtils.assert_equals(account_idx, subaddress.account_index) - AssertUtils.assert_equals(subaddress_idx, subaddress.index) + assert account_idx == subaddress.account_index + assert subaddress_idx == subaddress.index # test valid but unfound address non_wallet_address: str = WalletUtils.get_external_wallet_address() @@ -968,7 +974,8 @@ def test_get_address_indices(self, wallet: MoneroWallet) -> None: wallet.get_address_index(non_wallet_address) raise Exception("Should have thrown exception") except Exception as e: - AssertUtils.assert_equals("Address doesn't belong to the wallet", str(e)) + e_msg: str = str(e) + assert "Address doesn't belong to the wallet" == e_msg, e_msg # test invalid address try: @@ -1568,9 +1575,6 @@ def test_get_txs_with_query(self, wallet: MoneroWallet) -> None: assert tx.is_locked is False # get confirmed transactions sent from/to same wallet with a transfer with destinations - # TODO wallet-rpc is not returning destinations - if isinstance(wallet, MoneroWalletRpc): - return tx_query = MoneroTxQuery() tx_query.is_incoming = True @@ -3735,6 +3739,11 @@ def test_scan_txs(self, wallet: MoneroWallet) -> None: logger.debug("Created scan wallet") TxUtils.test_scan_txs(wallet, scan_wallet) + # Can rescan spent + @pytest.mark.skipif(TestUtils.TEST_RESETS is False, reason="TEST_RESETS disabled") + def test_rescan_spent(self, wallet: MoneroWallet) -> None: + wallet.rescan_spent() + # Can rescan the blockchain @pytest.mark.skipif(TestUtils.TEST_RESETS is False, reason="TEST_RELAYS disabled") @pytest.mark.skip(reason="Disabled so tests don't delete local cache") diff --git a/tests/test_monero_wallet_full.py b/tests/test_monero_wallet_full.py index 8fa22ec..ffe98ce 100644 --- a/tests/test_monero_wallet_full.py +++ b/tests/test_monero_wallet_full.py @@ -29,9 +29,9 @@ class TestMoneroWalletFull(BaseTestMoneroWallet): #region Overrides - @property + @classmethod @override - def wallet_type(self) -> WalletType: + def get_wallet_type(cls) -> WalletType: return WalletType.FULL @pytest.fixture(scope="class") @@ -102,7 +102,8 @@ def _get_seed_languages(self) -> list[str]: return self.get_test_wallet().get_seed_languages() @override - def get_test_wallet(self) -> MoneroWalletFull: + @classmethod + def get_test_wallet(cls) -> MoneroWalletFull: return super().get_test_wallet() # type: ignore #endregion diff --git a/tests/test_monero_wallet_keys.py b/tests/test_monero_wallet_keys.py index 779b280..f8e5ed1 100644 --- a/tests/test_monero_wallet_keys.py +++ b/tests/test_monero_wallet_keys.py @@ -39,9 +39,9 @@ def daemon(self) -> MoneroDaemonRpc: #region Overrides - @property + @classmethod @override - def wallet_type(self) -> WalletType: + def get_wallet_type(cls) -> WalletType: return WalletType.KEYS @override @@ -99,8 +99,9 @@ def _close_wallet(self, wallet: MoneroWallet, save: bool = False) -> None: def _get_seed_languages(self) -> list[str]: return self.get_test_wallet().get_seed_languages() + @classmethod @override - def get_test_wallet(self) -> MoneroWalletKeys: + def get_test_wallet(cls) -> MoneroWalletKeys: return super().get_test_wallet() # type: ignore #endregion @@ -522,6 +523,11 @@ def test_create_and_receive(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) def test_is_multisig_needed(self, wallet: MoneroWallet) -> None: return super().test_is_multisig_needed(wallet) + @pytest.mark.not_supported + @override + def test_rescan_spent(self, wallet: MoneroWallet) -> None: + return super().test_rescan_spent(wallet) + #endregion #region Tests diff --git a/tests/test_monero_wallet_rpc.py b/tests/test_monero_wallet_rpc.py index 1ae1b87..55a5c6f 100644 --- a/tests/test_monero_wallet_rpc.py +++ b/tests/test_monero_wallet_rpc.py @@ -18,9 +18,9 @@ class TestMoneroWalletRpc(BaseTestMoneroWallet): """Rpc wallet integration tests""" - @property + @classmethod @override - def wallet_type(self) -> WalletType: + def get_wallet_type(cls) -> WalletType: return WalletType.RPC #region Overrides @@ -29,10 +29,11 @@ def wallet_type(self) -> WalletType: @override def wallet(self) -> MoneroWalletRpc: """Test rpc wallet instance""" - return Utils.get_wallet_rpc() + return self.get_test_wallet() + @classmethod @override - def get_test_wallet(self) -> MoneroWalletRpc: + def get_test_wallet(cls) -> MoneroWalletRpc: return super().get_test_wallet() # type: ignore @override @@ -97,7 +98,7 @@ def test_get_subaddress_address_out_of_range(self, wallet: MoneroWallet) -> None # Can create a wallet with a randomly generated seed @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - @pytest.mark.skip(reason="TODO setup another docker monero-wallet-rpc resource") + @pytest.mark.xfail(reason="TODO setup another docker wallet-rpc") def test_create_wallet_random_rpc(self) -> None: # create random wallet with defaults path: str = StringUtils.get_random_string() @@ -137,7 +138,6 @@ def test_create_wallet_random_rpc(self) -> None: # Can create a RPC wallet from a seed @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - @pytest.mark.xfail(reason="TODO this test is failing after running all tests together for Network error") def test_create_wallet_from_seed_rpc(self, daemon: MoneroDaemonRpc) -> None: # create wallet with seed and defaults path: str = StringUtils.get_random_string() @@ -240,9 +240,7 @@ def test_save(self, wallet: MoneroWallet) -> None: wallet.save() # Can close a wallet - # TODO this test is failing after running all tests @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - @pytest.mark.xfail(reason="TODO this test is failing after running all tests together for Network error") def test_close(self, daemon: MoneroDaemonRpc) -> None: # create a test wallet path: str = StringUtils.get_random_string() @@ -308,6 +306,11 @@ def test_get_seed_language(self, wallet: MoneroWallet) -> None: def test_get_height_by_date(self, wallet: MoneroWallet) -> None: return super().test_get_height_by_date(wallet) + @pytest.mark.not_supported + @override + def test_subaddress_lookahead(self, wallet: MoneroWallet) -> None: + return super().test_subaddress_lookahead(wallet) + @pytest.mark.xfail(reason="TODO monero-project") @override def test_get_public_view_key(self, wallet: MoneroWallet) -> None: @@ -332,25 +335,8 @@ def test_import_key_images(self, wallet: MoneroWallet) -> None: def test_get_new_key_images_from_last_import(self, wallet: MoneroWallet) -> None: return super().test_get_new_key_images_from_last_import(wallet) - @pytest.mark.skip(reason="TODO") - @override - def test_subaddress_lookahead(self, wallet: MoneroWallet) -> None: - return super().test_subaddress_lookahead(wallet) - - @pytest.mark.skip(reason="TODO wallet-rpc can't find txs with payment ids") - @override - def test_get_txs_with_payment_ids(self, wallet: MoneroWallet) -> None: - return super().test_get_txs_with_payment_ids(wallet) - - @pytest.mark.skip(reason="TODO wallet rpc can't find destinations in outgoing transfers") - def test_check_tx_key(self, wallet: MoneroWallet) -> None: - return super().test_check_tx_key(wallet) - - @pytest.mark.skip(reason="TODO wallet rpc can't find destinations in outgoing transfers") - def test_check_tx_proof(self, wallet: MoneroWallet) -> None: - return super().test_check_tx_proof(wallet) - @pytest.mark.skip(reason="TODO setup another docker monero-wallet-rpc resource") + @override def test_view_only_and_offline_wallets(self, wallet: MoneroWallet) -> None: return super().test_view_only_and_offline_wallets(wallet) diff --git a/tests/utils/daemon_utils.py b/tests/utils/daemon_utils.py index f9432d0..24005f0 100644 --- a/tests/utils/daemon_utils.py +++ b/tests/utils/daemon_utils.py @@ -220,10 +220,10 @@ def test_ban(cls, ban: Optional[MoneroBan]) -> None: assert ban.seconds is not None @classmethod - def test_miner_tx_sum(cls, tx_sum: Optional[MoneroMinerTxSum], regtest: bool) -> None: + 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, not regtest) # TODO regtest daemon returning zero, why? - GenUtils.test_unsigned_big_integer(tx_sum.fee_sum, not regtest) # TODO regtest daemon returing zero, why? + GenUtils.test_unsigned_big_integer(tx_sum.emission_sum) + GenUtils.test_unsigned_big_integer(tx_sum.fee_sum) @classmethod def test_tx_pool_stats(cls, stats: Optional[MoneroTxPoolStats]) -> None: diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index 870afd5..95a382b 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -519,16 +519,19 @@ def get_wallets(cls, wallet_type: str) -> list[MoneroWallet]: @classmethod def free_wallet_rpc_resources(cls, save: bool = False) -> None: """Free all docker wallet rpc resources""" - if cls._WALLET_RPC_2 is not None: + if cls._WALLET_RPC_2 is None: + return + + if not cls._WALLET_RPC_2.is_closed(): try: cls._WALLET_RPC_2.close(save) except Exception as e: e_str: str = str(e) if "No wallet file" != e_str: - logger.debug(str(e)) + logger.warning(str(e)) - logger.debug(f"FREE WALLET RPC RESOURCE") - cls._WALLET_RPC_2 = None + logger.debug(f"FREE WALLET RPC RESOURCE") + cls._WALLET_RPC_2 = None @classmethod def is_wallet_rpc_resource(cls, wallet: MoneroWallet) -> bool: diff --git a/tests/utils/tx_utils.py b/tests/utils/tx_utils.py index 9a2da2b..d421797 100644 --- a/tests/utils/tx_utils.py +++ b/tests/utils/tx_utils.py @@ -516,8 +516,7 @@ def test_tx(cls, tx: Optional[MoneroTx], ctx: Optional[TestContext]) -> None: assert tx.unlock_time >= 0 assert tx.extra is not None assert len(tx.extra) > 0 - # TODO regtest daemon not returning tx fee... - # GenUtils.test_unsigned_big_integer(tx.fee, True) + GenUtils.test_unsigned_big_integer(tx.fee, True) # test presence of output indices # TODO change this over to outputs only