diff --git a/src/cpp/common/py_monero_common.cpp b/src/cpp/common/py_monero_common.cpp index f32a658..88edb2b 100644 --- a/src/cpp/common/py_monero_common.cpp +++ b/src/cpp/common/py_monero_common.cpp @@ -46,7 +46,7 @@ py::object PyGenUtils::ptree_to_pyobject(const boost::property_tree::ptree& tree lst.append(ptree_to_pyobject(child.second)); } return lst; - } + } else { py::dict d; if (!tree.get_value().empty()) { @@ -212,6 +212,36 @@ rapidjson::Value PyMoneroJsonRequest::to_rapidjson_val(rapidjson::Document::Allo return root; } +PyMoneroRpcConnection::PyMoneroRpcConnection(const std::string& uri, const std::string& username, const std::string& password, const std::string& proxy_uri, const std::string& zmq_uri, int priority, uint64_t timeout) { + if (!uri.empty()) m_uri = uri; + else m_uri = boost::none; + if (!proxy_uri.empty()) m_proxy_uri = proxy_uri; + else m_proxy_uri = boost::none; + if (!zmq_uri.empty()) m_zmq_uri = zmq_uri; + else m_zmq_uri = boost::none; + m_priority = priority; + m_timeout = timeout; + set_credentials(username, password); +} + +PyMoneroRpcConnection::PyMoneroRpcConnection(const monero::monero_rpc_connection& rpc) { + m_uri = rpc.m_uri; + m_proxy_uri = rpc.m_proxy_uri; + set_credentials(rpc.m_username.value_or(""), rpc.m_password.value_or("")); +} + +PyMoneroRpcConnection::PyMoneroRpcConnection(const PyMoneroRpcConnection& rpc) { + m_uri = rpc.m_uri; + m_proxy_uri = rpc.m_proxy_uri; + m_zmq_uri = rpc.m_zmq_uri; + m_priority = rpc.m_priority; + m_timeout = rpc.m_timeout; + m_is_online = rpc.m_is_online; + m_is_authenticated = rpc.m_is_authenticated; + m_response_time = rpc.m_response_time; + set_credentials(rpc.m_username.value_or(""), rpc.m_password.value_or("")); +} + int PyMoneroRpcConnection::compare(std::shared_ptr c1, std::shared_ptr c2, std::shared_ptr current_connection) { // current connection is first if (c1 == current_connection) return -1; @@ -255,37 +285,39 @@ void PyMoneroRpcConnection::set_credentials(const std::string& username, const s if (m_http_client->is_connected()) { m_http_client->disconnect(); } - } - else { + } else { auto factory = new net::http::client_factory(); m_http_client = factory->create(); } - if (username.empty()) { - m_username = boost::none; - } - - if (password.empty()) { - m_password = boost::none; - } + bool username_empty = username.empty(); + bool password_empty = password.empty(); - if (!password.empty() || !username.empty()) { - if (password.empty()) { - throw PyMoneroError("username cannot be empty because password is not empty"); + if (!username_empty || !password_empty) { + if (password_empty) { + throw PyMoneroError("password cannot be empty because username is not empty"); } - if (username.empty()) { - throw PyMoneroError("password cannot be empty because username is not empty"); + if (username_empty) { + throw PyMoneroError("username cannot be empty because password is not empty"); } } - if (m_username != username || m_password != password) { + bool username_equals = (m_username == boost::none && username_empty) || (m_username != boost::none && *m_username == username); + bool password_equals = (m_password == boost::none && password_empty) || (m_password != boost::none && *m_password == password); + + if (!username_equals || !password_equals) { m_is_online = boost::none; m_is_authenticated = boost::none; } - m_username = username; - m_password = password; + if (!username_empty && !password_empty) { + m_username = username; + m_password = password; + } else { + m_username = boost::none; + m_password = boost::none; + } } void PyMoneroRpcConnection::set_attribute(const std::string& key, const std::string& val) { @@ -319,13 +351,14 @@ bool PyMoneroRpcConnection::check_connection(int timeout_ms) { boost::optional is_online_before = m_is_online; boost::optional is_authenticated_before = m_is_authenticated; boost::lock_guard lock(m_mutex); + auto start = std::chrono::high_resolution_clock::now(); try { if (!m_http_client) throw std::runtime_error("http client not set"); if (m_http_client->is_connected()) { m_http_client->disconnect(); } - if (m_proxy_uri != boost::none) { + if (m_proxy_uri != boost::none && !m_proxy_uri.get().empty()) { if(!m_http_client->set_proxy(m_proxy_uri.get())) { throw std::runtime_error("Could not set proxy"); } @@ -333,10 +366,8 @@ bool PyMoneroRpcConnection::check_connection(int timeout_ms) { if(m_username != boost::none && !m_username->empty() && m_password != boost::none && !m_password->empty()) { auto credentials = std::make_shared(); - credentials->username = *m_username; credentials->password = *m_password; - m_credentials = *credentials; } @@ -345,20 +376,14 @@ bool PyMoneroRpcConnection::check_connection(int timeout_ms) { } m_http_client->connect(std::chrono::milliseconds(timeout_ms)); - auto start = std::chrono::high_resolution_clock::now(); - PyMoneroJsonRequest request("get_version"); - auto response = send_json_request(request); - auto end = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(end - start); - if (response->m_result == boost::none) { - throw PyMoneroRpcError(-1, "Invalid JSON RPC response"); - } - + std::vector heights; + heights.reserve(100); + for(long i = 0; i < 100; i++) heights.push_back(i); + py::dict params; + params["heights"] = heights; + send_binary_request("get_blocks_by_height.bin", params); m_is_online = true; m_is_authenticated = true; - m_response_time = duration.count(); - - return is_online_before != m_is_online || is_authenticated_before != m_is_authenticated; } catch (const PyMoneroRpcError& ex) { m_is_online = false; @@ -370,18 +395,24 @@ bool PyMoneroRpcConnection::check_connection(int timeout_ms) { m_is_authenticated = false; } else if (ex.code == 404) { + // fallback to latency check m_is_online = true; m_is_authenticated = true; } - - return false; } catch (const std::exception& ex) { m_is_online = false; m_is_authenticated = boost::none; m_response_time = boost::none; - return false; } + + if (*m_is_online) { + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + m_response_time = duration.count(); + } + + return is_online_before != m_is_online || is_authenticated_before != m_is_authenticated; } void PyMoneroConnectionManager::add_listener(const std::shared_ptr &listener) { diff --git a/src/cpp/common/py_monero_common.h b/src/cpp/common/py_monero_common.h index a4ffd72..9db9cd5 100644 --- a/src/cpp/common/py_monero_common.h +++ b/src/cpp/common/py_monero_common.h @@ -112,7 +112,7 @@ class PyGenUtils { public: PyGenUtils() {} - static py::object convert_value(const std::string& val); + static py::object convert_value(const std::string& val); static py::object ptree_to_pyobject(const boost::property_tree::ptree& tree); static boost::property_tree::ptree pyobject_to_ptree(const py::object& obj); static boost::property_tree::ptree parse_json_string(const std::string &json); @@ -141,7 +141,7 @@ class PyMoneroRequestParams : public PySerializableStruct { class PyMoneroRequestEmptyParams : public PyMoneroRequestParams { public: PyMoneroRequestEmptyParams() {} - + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override { rapidjson::Value root(rapidjson::kObjectType); return root; }; }; @@ -150,7 +150,7 @@ class PyMoneroPathRequest : public PyMoneroRequest { boost::optional> m_params; PyMoneroPathRequest() { } - + PyMoneroPathRequest(std::string method, boost::optional params = boost::none) { m_method = method; if (params != boost::none) m_params = std::make_shared(params); @@ -312,34 +312,9 @@ class PyMoneroRpcConnection : public monero::monero_rpc_connection { static int compare(std::shared_ptr c1, std::shared_ptr c2, std::shared_ptr current_connection); - PyMoneroRpcConnection(const std::string& uri = "", const std::string& username = "", const std::string& password = "", const std::string& proxy_uri = "", const std::string& zmq_uri = "", int priority = 0, uint64_t timeout = 0) { - m_uri = uri; - m_username = username; - m_password = password; - m_zmq_uri = zmq_uri; - m_priority = priority; - m_timeout = timeout; - m_proxy_uri = proxy_uri; - set_credentials(username, password); - } - - PyMoneroRpcConnection(const PyMoneroRpcConnection& rpc) { - m_uri = rpc.m_uri; - m_username = rpc.m_username; - m_password = rpc.m_password; - m_zmq_uri = rpc.m_zmq_uri; - m_proxy_uri = rpc.m_proxy_uri; - m_is_authenticated = rpc.m_is_authenticated; - set_credentials(m_username.value_or(""), m_password.value_or("")); - } - - PyMoneroRpcConnection(const monero::monero_rpc_connection& rpc) { - m_uri = rpc.m_uri; - m_username = rpc.m_username; - m_password = rpc.m_password; - m_proxy_uri = rpc.m_proxy_uri; - set_credentials(m_username.value_or(""), m_password.value_or("")); - } + PyMoneroRpcConnection(const std::string& uri = "", const std::string& username = "", const std::string& password = "", const std::string& proxy_uri = "", const std::string& zmq_uri = "", int priority = 0, uint64_t timeout = 0); + PyMoneroRpcConnection(const PyMoneroRpcConnection& rpc); + PyMoneroRpcConnection(const monero::monero_rpc_connection& rpc); bool is_onion() const; bool is_i2p() const; @@ -389,8 +364,7 @@ class PyMoneroRpcConnection : public monero::monero_rpc_connection { PyMoneroJsonResponse response; int result = invoke_post("/json_rpc", request, response, timeout); - - if (result != 200) throw std::runtime_error("HTTP error: code " + std::to_string(result)); + if (result != 200) throw PyMoneroRpcError(result, "HTTP error: code " + std::to_string(result)); return std::make_shared(response); } @@ -400,8 +374,7 @@ class PyMoneroRpcConnection : public monero::monero_rpc_connection { if (request.m_method == boost::none || request.m_method->empty()) throw std::runtime_error("No RPC method set in path request"); int result = invoke_post(std::string("/") + request.m_method.get(), request, response, timeout); - - if (result != 200) throw std::runtime_error("HTTP error: code " + std::to_string(result)); + if (result != 200) throw PyMoneroRpcError(result, "HTTP error: code " + std::to_string(result)); return std::make_shared(response); } @@ -415,7 +388,7 @@ class PyMoneroRpcConnection : public monero::monero_rpc_connection { const epee::net_utils::http::http_response_info* response = invoke_post(uri, body, timeout); int result = response->m_response_code; - if (result != 200) throw std::runtime_error("HTTP error: code " + std::to_string(result)); + if (result != 200) throw PyMoneroRpcError(result, "HTTP error: code " + std::to_string(result)); auto res = std::make_shared(); res->m_binary = response->m_body; @@ -428,22 +401,19 @@ class PyMoneroRpcConnection : public monero::monero_rpc_connection { inline boost::optional send_json_request(const std::string method, boost::optional parameters) { PyMoneroJsonRequest request(method, parameters); auto response = send_json_request(request); - return response->get_result(); } inline boost::optional send_path_request(const std::string method, boost::optional parameters) { PyMoneroPathRequest request(method, parameters); auto response = send_path_request(request); - return response->get_response(); } - inline boost::optional send_binary_request(const std::string method, boost::optional parameters) { + inline boost::optional send_binary_request(const std::string method, boost::optional parameters) { PyMoneroBinaryRequest request(method, parameters); auto response = send_binary_request(request); - - return response->get_response(); + return response->m_binary; } protected: diff --git a/src/cpp/py_monero.cpp b/src/cpp/py_monero.cpp index 53b8d4c..0f495e5 100644 --- a/src/cpp/py_monero.cpp +++ b/src/cpp/py_monero.cpp @@ -39,7 +39,7 @@ PYBIND11_MODULE(monero, m) { m.doc() = ""; auto py_serializable_struct = py::class_>(m, "SerializableStruct"); - auto py_monero_rpc_connection = py::class_>(m, "MoneroRpcConnection"); + auto py_monero_rpc_connection = py::class_>(m, "MoneroRpcConnection"); auto py_monero_connection_manager_listener = py::class_>(m, "MoneroConnectionManagerListener"); auto py_monero_connection_manager = py::class_>(m, "MoneroConnectionManager"); @@ -301,26 +301,44 @@ PYBIND11_MODULE(monero, m) { // monero_rpc_connection py_monero_rpc_connection .def(py::init(), py::arg("uri") = "", py::arg("username") = "", py::arg("password") = "", py::arg("proxy_uri") = "", py::arg("zmq_uri") = "", py::arg("priority") = 0, py::arg("timeout") = 0) - .def(py::init(), py::arg("rpc")) + .def(py::init(), py::arg("rpc")) .def_static("compare", [](const std::shared_ptr c1, const std::shared_ptr c2, std::shared_ptr current_connection) { MONERO_CATCH_AND_RETHROW(PyMoneroRpcConnection::compare(c1, c2, current_connection)); }, py::arg("c1"), py::arg("c2"), py::arg("current_connection")) - .def_readwrite("uri", &PyMoneroRpcConnection::m_uri) - .def_readwrite("username", &PyMoneroRpcConnection::m_username) - .def_readwrite("password", &PyMoneroRpcConnection::m_password) - .def_readwrite("proxy_uri", &PyMoneroRpcConnection::m_proxy_uri) + .def_property("uri", + [](const PyMoneroRpcConnection& self) { return self.m_uri; }, + [](PyMoneroRpcConnection& self, const boost::optional& val) { + // normalize uri + if (val != boost::none && !val->empty()) { + self.m_uri = val; + } else self.m_uri = boost::none; + }) + .def_readonly("username", &PyMoneroRpcConnection::m_username) + .def_readonly("password", &PyMoneroRpcConnection::m_password) + .def_property_readonly("response_time", + [](const PyMoneroRpcConnection& self) { return self.m_response_time; }) + .def_property("proxy_uri", + [](const PyMoneroRpcConnection& self) { return self.m_proxy_uri; }, + [](PyMoneroRpcConnection& self, const boost::optional& val) { + // normalize proxy uri + if (val != boost::none && !val->empty()) { + self.m_proxy_uri = val; + } else self.m_proxy_uri = boost::none; + }) .def_property("zmq_uri", [](const PyMoneroRpcConnection& self) { return self.m_zmq_uri; }, - [](PyMoneroRpcConnection& self, boost::optional val) { self.m_zmq_uri = val; }) + [](PyMoneroRpcConnection& self, const boost::optional& val) { + // normalize zmq uri + if (val != boost::none && !val->empty()) { + self.m_zmq_uri = val; + } else self.m_zmq_uri = boost::none; + }) .def_property("priority", [](const PyMoneroRpcConnection& self) { return self.m_priority; }, [](PyMoneroRpcConnection& self, int val) { self.m_priority = val; }) .def_property("timeout", [](const PyMoneroRpcConnection& self) { return self.m_timeout; }, [](PyMoneroRpcConnection& self, uint64_t val) { self.m_timeout = val; }) - .def_property("response_time", - [](const PyMoneroRpcConnection& self) { return self.m_response_time; }, - [](PyMoneroRpcConnection& self, boost::optional val) { self.m_response_time = val; }) .def("set_attribute", [](PyMoneroRpcConnection& self, const std::string& key, const std::string& value) { MONERO_CATCH_AND_RETHROW(self.set_attribute(key, value)); }, py::arg("key"), py::arg("value")) @@ -1960,7 +1978,6 @@ PYBIND11_MODULE(monero, m) { MONERO_CATCH_AND_RETHROW(self.delete_address_book_entry(index)); }, py::arg("index")) .def("tag_accounts", [](PyMoneroWallet& self, const std::string& tag, const std::vector& account_indices) { - std::cout << "Indirizzo dell'oggetto A: " << &self << std::endl; MONERO_CATCH_AND_RETHROW(self.tag_accounts(tag, account_indices)); }, py::arg("tag"), py::arg("account_indices")) .def("untag_accounts", [](PyMoneroWallet& self, const std::vector& account_indices) { @@ -2243,9 +2260,15 @@ PYBIND11_MODULE(monero, m) { std::string b{bin}; MONERO_CATCH_AND_RETHROW(PyMoneroUtils::binary_to_json(b)); }, py::arg("bin")) + .def_static("binary_to_json", [](const std::string &bin) { + MONERO_CATCH_AND_RETHROW(PyMoneroUtils::binary_to_json(bin)); + }, py::arg("bin")) .def_static("dict_to_binary", [](const py::dict &dictionary) { MONERO_CATCH_AND_RETHROW(py::bytes(PyMoneroUtils::dict_to_binary(dictionary))); }, py::arg("dictionary")) + .def_static("binary_to_dict", [](const std::string &bin) { + MONERO_CATCH_AND_RETHROW(PyMoneroUtils::binary_to_dict(bin)); + }, py::arg("bin")) .def_static("binary_to_dict", [](const py::bytes &bin) { std::string b{bin}; MONERO_CATCH_AND_RETHROW(PyMoneroUtils::binary_to_dict(b)); diff --git a/src/cpp/wallet/py_monero_wallet_rpc.cpp b/src/cpp/wallet/py_monero_wallet_rpc.cpp index 55db7fd..13287cf 100644 --- a/src/cpp/wallet/py_monero_wallet_rpc.cpp +++ b/src/cpp/wallet/py_monero_wallet_rpc.cpp @@ -353,7 +353,7 @@ void PyMoneroWalletRpc::set_daemon_connection(const std::string& uri, const std: set_daemon_connection(boost::none); return; } - boost::optional rpc = monero_rpc_connection(uri, username, password, proxy_uri); + boost::optional rpc = PyMoneroRpcConnection(uri, username, password, proxy_uri); set_daemon_connection(rpc); } diff --git a/src/python/monero_rpc_connection.pyi b/src/python/monero_rpc_connection.pyi index c0ecb29..2aa44ef 100644 --- a/src/python/monero_rpc_connection.pyi +++ b/src/python/monero_rpc_connection.pyi @@ -1,26 +1,38 @@ import typing +from .serializable_struct import SerializableStruct -class MoneroRpcConnection: + +class MoneroRpcConnection(SerializableStruct): """ Models a connection to a daemon. """ - password: str | None - """Connection authentication password.""" priority: int """Connection priority.""" proxy_uri: str | None """Connection proxy address.""" - response_time: int | None - """Connection response time.""" + zmq_uri: str | None + """ZMQ connection uri.""" timeout: int """Connection timeout (milliseconds).""" uri: str | None """Connection uri.""" - username: str | None - """Connection authentication username.""" - zmq_uri: str | None - """ZMQ connection uri.""" + + @property + def username(self) -> str | None: + """Connection authentication username.""" + ... + + @property + def password(self) -> str | None: + """Connection authentication password.""" + ... + + @property + def response_time(self) -> int | None: + """Connection response time in milliseconds.""" + ... + @staticmethod def compare(c1: MoneroRpcConnection, c2: MoneroRpcConnection, current_connection: MoneroRpcConnection) -> int: """ @@ -42,8 +54,8 @@ class MoneroRpcConnection: :param str password: password used for authentication :param str proxy_uri: proxy uri :param str zmq_uri: ZMQ uri - :param int priority: priorioty of the connection - :param int timeout: connection timeout in milliseconds + :param int priority: priorioty of the connection (default `0`) + :param int timeout: connection timeout in milliseconds (default `0`) """ ... @typing.overload @@ -57,7 +69,7 @@ class MoneroRpcConnection: def check_connection(self, timeout_ms: int = 2000) -> bool: """ Check the connection and update online, authentication, and response time status. - + :param int timeout_ms: the maximum response time before considered offline :return bool: true if there is a change in status, false otherwise """ @@ -75,14 +87,14 @@ class MoneroRpcConnection: Indicates if the connection is authenticated according to the last call to check_connection(). Note: must call check_connection() manually unless using MoneroConnectionManager. - + :return bool: true if authenticated or no authentication, false if not authenticated, or null if check_connection() has not been called """ ... def is_connected(self) -> bool: """ Indicates if the connection is connected according to the last call to check_connection(). - + Note: must call check_connection() manually unless using MoneroConnectionManager. :return bool: true or false to indicate if connected, or null if check_connection() has not been called @@ -101,39 +113,39 @@ class MoneroRpcConnection: def is_online(self) -> bool: """ Indicates if the connection is online according to the last call to check_connection(). - + Note: must call check_connection() manually unless using MoneroConnectionManager. - + :return bool: true or false to indicate if online, or null if check_connection() has not been called """ ... - def send_json_request(self, method: str, parameters: object | None = None) -> object: + def send_json_request(self, method: str, parameters: object | None = None) -> object | None: """ Send a request to the JSON-RPC API. - + :param str method: is the method to request :param object parameters: are the request's input parameters - :return response: the RPC API response as a map + :returns object | None: the RPC API response as a map """ ... - def send_path_request(self, method: str, parameters: object | None = None) -> object: + def send_path_request(self, method: str, parameters: object | None = None) -> object | None: """ Send a RPC request to the given path and with the given paramters. - + E.g. "/get_transactions" with params - + :param str path: is the url path of the request to invoke :param object parameters: are request parameters sent in the body - :return response: the request's deserialized response + :returns object | None: the request's deserialized response """ ... - def send_binary_request(self, method: str, parameters: object | None = None) -> object: + def send_binary_request(self, method: str, parameters: object | None = None) -> str | None: """ Send a binary RPC request. - + :param str path: is the path of the binary RPC method to invoke :param object parameters: are the request parameters - :return response: the request's deserialized binary response + :returns str | None: the request's deserialized binary response """ ... def set_attribute(self, key: str, value: str) -> None: diff --git a/src/python/monero_utils.pyi b/src/python/monero_utils.pyi index 0ae8d63..e923e62 100644 --- a/src/python/monero_utils.pyi +++ b/src/python/monero_utils.pyi @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, overload from .monero_output_wallet import MoneroOutputWallet from .monero_block import MoneroBlock from .monero_transfer import MoneroTransfer @@ -15,13 +15,14 @@ class MoneroUtils: @staticmethod def atomic_units_to_xmr(amount_atomic_units: int) -> float: """ - Convert atomic units to XMR. + Convert atomic units to XMR. :param int amount_atomic_units: amount in atomic units to convert to XMR :return float: amount in XMR """ ... @staticmethod + @overload def binary_to_dict(bin: bytes) -> dict[Any, Any]: """ Deserialize a dictionary from binary format. @@ -31,6 +32,17 @@ class MoneroUtils: """ ... @staticmethod + @overload + def binary_to_dict(bin: str) -> dict[Any, Any]: + """ + Deserialize a dictionary from binary format. + + :param str bin: Dictionary in binary format. + :return dict: Deserialized dictionary. + """ + ... + @staticmethod + @overload def binary_to_json(bin: bytes) -> str: """ Deserialize a JSON string from binary format. @@ -40,10 +52,20 @@ class MoneroUtils: """ ... @staticmethod + @overload + def binary_to_json(bin: str) -> str: + """ + Deserialize a JSON string from binary format. + + :param str bin: JSON string in binary format. + :return str: The deserialized JSON string. + """ + ... + @staticmethod def configure_logging(path: str, console: bool) -> None: """ Initialize logging. - + :param str path: the path to write logs to :param bool console: specifies whether or not to write to the console """ @@ -88,7 +110,7 @@ class MoneroUtils: def get_integrated_address(network_type: MoneroNetworkType, standard_address: str, payment_id: str = '') -> MoneroIntegratedAddress: """ Get an integrated address. - + :param MoneroNetworkType network_type: is the network type of the integrated address :param str standard_address: is the address to derive the integrated address from :param str payment_id: optionally specifies the integrated address's payment id (defaults to random payment id) @@ -99,7 +121,7 @@ class MoneroUtils: def get_payment_uri(config: MoneroTxConfig) -> str: """ Creates a payment URI from a tx configuration. - + :param config: specifies configuration for a payment URI :return: the payment URI """ @@ -114,7 +136,7 @@ class MoneroUtils: def get_version() -> str: """ Get the version of the monero-python library. - + :return version: the version of this monero-python library """ ... @@ -122,7 +144,7 @@ class MoneroUtils: def is_valid_address(address: str, network_type: MoneroNetworkType) -> bool: """ Determine if the given address is valid. - + :param str address: is the address to validate :param MoneroNetworkType network_type: is the address's network type :return bool: true if the address is valid, false otherwise @@ -141,8 +163,8 @@ class MoneroUtils: def is_valid_mnemonic(mnemonic: str) -> bool: """ Indicates if a mnemonic is valid. - - :param str private_spend_key: is the mnemonic to validate + + :param str mnemonic: is the mnemonic to validate :return: true if the mnemonic is valid, false otherwise """ ... @@ -150,7 +172,7 @@ class MoneroUtils: def is_valid_payment_id(payment_id: str) -> bool: """ Indicates if a payment id is valid. - + :param str payment_id: is the payment id to validate :return: true if the payment id is valid, false otherwise """ @@ -159,7 +181,7 @@ class MoneroUtils: def is_valid_private_spend_key(private_spend_key: str) -> bool: """ Indicates if a private spend key is valid. - + :param str private_spend_key: is the private spend key to validate :return: true if the private spend key is valid, false otherwise """ @@ -168,7 +190,7 @@ class MoneroUtils: def is_valid_private_view_key(private_view_key: str) -> bool: """ Indicates if a private view key is valid. - + :param str private_view_key: is the private view key to validate :return: true if the private view key is valid, false otherwise """ @@ -177,7 +199,7 @@ class MoneroUtils: def is_valid_public_spend_key(public_spend_key: str) -> bool: """ Indicates if a public spend key is valid. - + :param str public_spend_key: is the public spend key to validate :return bool: true if the public spend key is valid, false otherwise """ @@ -186,7 +208,7 @@ class MoneroUtils: def is_valid_public_view_key(public_view_key: str) -> bool: """ Indicates if a public view key is valid. - + :param public_view_key: is the public view key to validate :return: true if the public view key is valid, false otherwise """ @@ -211,7 +233,7 @@ class MoneroUtils: def validate_address(address: str, network_type: MoneroNetworkType) -> None: """ Validates the given address. - + :param address: is the address to validate :param network_type: is the address's network type """ @@ -220,7 +242,7 @@ class MoneroUtils: def validate_mnemonic(mnemonic: str) -> None: """ Validates the given mnemonic phrase. - + :param str mnemonic: is the mnemonic to validate :raise MoneroError: if the given mnemonic is invalid """ @@ -229,7 +251,7 @@ class MoneroUtils: def validate_payment_id(payment_id: str) -> None: """ Validate a payment id. - + :param str payment_id: is the payment id to validate :raise MoneroError: if the given payment id is invalid """ @@ -238,7 +260,7 @@ class MoneroUtils: def validate_private_spend_key(private_spend_key: str) -> None: """ Validate a private spend key. - + :param str private_spend_key: is the private spend key to validate :raise MoneroError: if the given private spend key is invalid """ @@ -247,7 +269,7 @@ class MoneroUtils: def validate_private_view_key(private_view_key: str) -> None: """ Validate a private view key. - + :param str private_view_key: is the private view key to validate :raise MoneroError: if the given private view key is invalid """ @@ -256,7 +278,7 @@ class MoneroUtils: def validate_public_spend_key(public_spend_key: str) -> None: """ Validate a public spend key. - + :param str public_spend_key: is the public spend key to validate :raise MoneroError: if the given public spend key is invalid """ @@ -265,7 +287,7 @@ class MoneroUtils: def validate_public_view_key(public_view_key: str) -> None: """ Validate a public view key. - + :param public_view_key: is the public view key to validate :raise MoneroError: if the given public view key is invalid """ @@ -274,7 +296,7 @@ class MoneroUtils: def xmr_to_atomic_units(amount_xmr: float) -> int: """ Convert XMR to atomic units. - + :param float amount_xmr: amount in XMR to convert to atomic units :return int: amount in atomic units """ diff --git a/tests/config/config.ini b/tests/config/config.ini index 2f3cda4..c56c522 100644 --- a/tests/config/config.ini +++ b/tests/config/config.ini @@ -5,12 +5,12 @@ lite_mode=False test_notifications=True test_resets=True network_type=regtest -auto_connect_timeout_ms=3000 +auto_connect_timeout_ms=5000 [daemon] -rpc_uri=127.0.0.1:18081 -rpc_username= -rpc_password= +rpc_uri=http://127.0.0.1:18081 +rpc_username=rpc_daemon_user +rpc_password=abc123 [wallet] name=test_wallet_1 @@ -28,7 +28,7 @@ rpc_port_start=18082 rpc_username=rpc_user rpc_password=abc123 rpc_access_control_origins="http:#localhost:8080" -rpc_domain=localhost +rpc_domain=127.0.0.1 rpc_zmq_enabled=False rpc_zmq_port_start=48083 rpc_zmq_bind_port_start=48083 diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 2d31607..7c96885 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -32,6 +32,7 @@ services: "--rpc-max-connections-per-private-ip=100", "--start-mining=49cvU1JnXFAH7r1RbDLJw78aaDnhW4sCKRbLaTYa9eHvcz9PK1YXwod5npWZvMyQ8L4waVjUhuCp6btFyELkRpA4SWNKeQH", "--mining-threads=1", + "--rpc-login=rpc_daemon_user:abc123", "--non-interactive" ] volumes: @@ -60,6 +61,7 @@ services: "--no-zmq", "--max-connections-per-ip=100", "--rpc-max-connections-per-private-ip=100", + "--rpc-login=rpc_daemon_user:abc123", "--non-interactive" ] volumes: @@ -73,7 +75,21 @@ services: xmr_wallet_1: image: lalanza808/monero:v0.18.4.4 container_name: xmr_wallet_1 - command: monero-wallet-rpc --log-level 2 --allow-mismatched-daemon-version --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=node_2:18081 --wallet-dir=/wallet --rpc-access-control-origins=* --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s" + command: [ + "monero-wallet-rpc", + "--log-level=3", + "--allow-mismatched-daemon-version", + "--rpc-bind-ip=0.0.0.0", + "--confirm-external-bind", + "--rpc-bind-port=18082", + "--trusted-daemon", + "--daemon-address=node_2:18081", + "--daemon-login=rpc_daemon_user:abc123", + "--rpc-login=rpc_user:abc123", + "--wallet-dir=/wallet", + "--rpc-access-control-origins=*", + "--non-interactive" + ] ports: - "18082:18082" volumes: @@ -85,7 +101,21 @@ services: xmr_wallet_2: image: lalanza808/monero:v0.18.4.4 container_name: xmr_wallet_2 - command: monero-wallet-rpc --log-level 2 --allow-mismatched-daemon-version --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18083 --non-interactive --trusted-daemon --daemon-address=node_2:18081 --wallet-dir=/wallet --rpc-access-control-origins=* --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s" + command: [ + "monero-wallet-rpc", + "--log-level=3", + "--allow-mismatched-daemon-version", + "--rpc-bind-ip=0.0.0.0", + "--confirm-external-bind", + "--rpc-bind-port=18083", + "--trusted-daemon", + "--daemon-address=node_2:18081", + "--daemon-login=rpc_daemon_user:abc123", + "--rpc-login=rpc_user:abc123", + "--wallet-dir=/wallet", + "--rpc-access-control-origins=*", + "--non-interactive" + ] ports: - "18083:18083" volumes: diff --git a/tests/test_monero_rpc_connection.py b/tests/test_monero_rpc_connection.py index cf04dee..c8e3508 100644 --- a/tests/test_monero_rpc_connection.py +++ b/tests/test_monero_rpc_connection.py @@ -1,8 +1,8 @@ import pytest import logging -from monero import MoneroRpcConnection -from utils import TestUtils as Utils, DaemonUtils +from monero import MoneroRpcConnection, MoneroConnectionType, MoneroRpcError, MoneroUtils +from utils import TestUtils as Utils, DaemonUtils, StringUtils logger: logging.Logger = logging.getLogger("TestMoneroRpcConnection") @@ -11,14 +11,22 @@ class TestMoneroRpcConnection: """Rpc connection integration tests""" + # region Fixtures + # Setup and teardown of test class @pytest.fixture(scope="class", autouse=True) def global_setup_and_teardown(self): """Executed once before all tests""" logger.info(f"Setup test class {type(self).__name__}") + self.before_all() yield logger.info(f"Teardown test class {type(self).__name__}") + # Before all tests + def before_all(self) -> None: + """Executed once before all tests""" + logger.info(f"Setup test class {type(self).__name__}") + # Setup and teardown of each tests @pytest.fixture(autouse=True) def setup_and_teardown(self, request: pytest.FixtureRequest): @@ -26,19 +34,204 @@ def setup_and_teardown(self, request: pytest.FixtureRequest): yield logger.info(f"After {request.node.name}") # type: ignore + # Node rpc connection fixture + @pytest.fixture(scope="class") + def node_connection(self) -> MoneroRpcConnection: + """Rpc connection test instance.""" + return MoneroRpcConnection(Utils.DAEMON_RPC_URI, Utils.DAEMON_RPC_USERNAME, Utils.DAEMON_RPC_PASSWORD) + + # Wallet rpc connection fixture + @pytest.fixture(scope="class") + def wallet_connection(self) -> MoneroRpcConnection: + """Rpc connection test instance.""" + return MoneroRpcConnection(Utils.WALLET_RPC_URI, Utils.WALLET_RPC_USERNAME, Utils.WALLET_RPC_PASSWORD) + + #endregion + + #region Tests + + # Test rpc connection json serialization + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_rpc_connection_serialization(self, node_connection: MoneroRpcConnection, wallet_connection: MoneroRpcConnection) -> None: + # test node connection serialization + connection_str: str = node_connection.serialize() + assert '{"uri":"http://127.0.0.1:18081","username":"rpc_daemon_user","password":"abc123"}' == connection_str + + # node wallet connection serialization + connection_str = wallet_connection.serialize() + assert '{"uri":"127.0.0.1:18082","username":"rpc_user","password":"abc123"}' == connection_str + + # Can copy a rpc connection + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_connection_copy(self, node_connection: MoneroRpcConnection) -> None: + # test copy + copy: MoneroRpcConnection = MoneroRpcConnection(node_connection) + assert copy.serialize() == node_connection.serialize() + # Test monerod rpc connection - def test_node_rpc_connection(self) -> None: - connection = MoneroRpcConnection(Utils.DAEMON_RPC_URI, Utils.DAEMON_RPC_USERNAME, Utils.DAEMON_RPC_PASSWORD) - DaemonUtils.test_rpc_connection(connection, Utils.DAEMON_RPC_URI) + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_node_rpc_connection(self, node_connection: MoneroRpcConnection) -> None: + DaemonUtils.test_rpc_connection(node_connection, Utils.DAEMON_RPC_URI, True, MoneroConnectionType.IPV4) # Test wallet rpc connection @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - def test_wallet_rpc_connection(self) -> None: - connection = MoneroRpcConnection(Utils.WALLET_RPC_URI, Utils.WALLET_RPC_USERNAME, Utils.WALLET_RPC_PASSWORD) - DaemonUtils.test_rpc_connection(connection, Utils.WALLET_RPC_URI) + def test_wallet_rpc_connection(self, wallet_connection: MoneroRpcConnection) -> None: + DaemonUtils.test_rpc_connection(wallet_connection, Utils.WALLET_RPC_URI, True, MoneroConnectionType.IPV4) # Test invalid connection @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_invalid_connection(self) -> None: connection = MoneroRpcConnection(Utils.OFFLINE_SERVER_URI) - DaemonUtils.test_rpc_connection(connection, Utils.OFFLINE_SERVER_URI, False) + DaemonUtils.test_rpc_connection(connection, Utils.OFFLINE_SERVER_URI, False, MoneroConnectionType.INVALID) + + # Can set credentials + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_set_credentials(self) -> None: + # create connection + connection: MoneroRpcConnection = MoneroRpcConnection(Utils.DAEMON_RPC_URI) + + # test connection without credentials + assert connection.username is None + assert connection.password is None + + # test set credentials + connection.set_credentials(Utils.DAEMON_RPC_USERNAME, Utils.DAEMON_RPC_PASSWORD) + + assert not connection.is_connected(), "Expected not connected" + assert not connection.is_online(), "Expected not online" + assert not connection.is_authenticated(), "Expected not authenticated" + + # test connection + assert connection.username == Utils.DAEMON_RPC_USERNAME + assert connection.password == Utils.DAEMON_RPC_PASSWORD + + assert connection.check_connection(), "Could not check connection" + + assert connection.is_connected(), "Expected connected after check" + assert connection.is_online(), "Expected online after check" + assert connection.is_authenticated(), "Not authenticated" + + # test empty credentials + connection.set_credentials("", "") + + assert not connection.is_connected(), "Expected not connected" + assert not connection.is_online(), "Expected not online" + assert not connection.is_authenticated(), "Expected not authenticated" + + assert connection.username is None + assert connection.password is None + + # Test invalid credentials + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_set_invalid_credentials(self) -> None: + # create test connection + connection: MoneroRpcConnection = MoneroRpcConnection(Utils.DAEMON_RPC_URI, Utils.DAEMON_RPC_USERNAME, Utils.DAEMON_RPC_PASSWORD) + + # test connection username property assign + try: + connection.username = "user" # type: ignore + except AttributeError as e: + err_msg: str = str(e) + assert "object has no setter" in err_msg, err_msg + + # test connection password property assign + try: + connection.password = "abc123" # type: ignore + except AttributeError as e: + err_msg: str = str(e) + assert "object has no setter" in err_msg, err_msg + + # set invalid username + try: + connection.set_credentials("", "abc123") + raise Exception("Should have thrown") + except Exception as e: + e_msg: str = str(e) + assert e_msg == "username cannot be empty because password is not empty", e_msg + + # set invalid password + try: + connection.set_credentials("user", "") + raise Exception("Should have thrown") + except Exception as e: + e_msg: str = str(e) + assert e_msg == "password cannot be empty because username is not empty", e_msg + + # test connection + assert connection.username == Utils.DAEMON_RPC_USERNAME + assert connection.password == Utils.DAEMON_RPC_PASSWORD + + connection.set_credentials("user", "abc123") + + # test connection + assert connection.username == "user" + assert connection.password == "abc123" + + assert connection.check_connection() + assert not connection.is_authenticated() + # TODO internal http client throwing "Network error" instaead of 201 http error + #assert connection.is_online() + #assert connection.is_connected() + assert not connection.is_online() + assert not connection.is_connected() + + # Can get and set arbitrary key/value attributes + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_set_attributes(self, node_connection: MoneroRpcConnection) -> None: + # set attributes + attrs: dict[str, str] = {} + for i in range(5): + key: str = f"attr{i}" + val: str = StringUtils.get_random_string() + attrs[key] = val + node_connection.set_attribute(key, val) + + # test attributes + for key in attrs: + val = attrs[key] + assert val == node_connection.get_attribute(key) + + # Can send json request + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_send_json_request(self, node_connection: MoneroRpcConnection) -> None: + result: object = node_connection.send_json_request("get_version") + assert result is not None + logger.debug(f"JSON-RPC response {result}") + + # test invalid json rpc method + try: + node_connection.send_json_request("invalid_method") + except MoneroRpcError as e: + if str(e) != "Method not found": + raise + assert e.code == -32601 + + # Can send binary request + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_send_binary_request(self, node_connection: MoneroRpcConnection) -> None: + parameters: dict[str, list[int]] = { "heights": list(range(100)) } + bin_result: str | None = node_connection.send_binary_request("get_blocks_by_height.bin", parameters) + assert bin_result is not None + result: str = MoneroUtils.binary_to_json(bin_result) + logger.debug(f"Binary response: {result}") + + # test invalid binary method + try: + node_connection.send_binary_request("invalid_method") + except MoneroRpcError as e: + assert e.code == 404 + + # Can send path request + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_send_path_request(self, node_connection: MoneroRpcConnection) -> None: + result: object = node_connection.send_path_request("get_height") + assert result is not None + logger.debug(f"Path response {result}") + + # test invalid path method + try: + node_connection.send_path_request("invalid_method") + except MoneroRpcError as e: + assert e.code == 404 + + #endregion diff --git a/tests/test_monero_wallet_full.py b/tests/test_monero_wallet_full.py index 0cde529..8fa22ec 100644 --- a/tests/test_monero_wallet_full.py +++ b/tests/test_monero_wallet_full.py @@ -122,7 +122,7 @@ def test_create_wallet_random_full(self, daemon: MoneroDaemonRpc) -> None: MoneroUtils.validate_mnemonic(wallet.get_seed()) MoneroUtils.validate_address(wallet.get_primary_address(), MoneroNetworkType.MAINNET) assert wallet.get_network_type() == MoneroNetworkType.MAINNET - AssertUtils.assert_connection_equals(wallet.get_daemon_connection(), MoneroRpcConnection(Utils.OFFLINE_SERVER_URI)) + AssertUtils.assert_equals(wallet.get_daemon_connection(), MoneroRpcConnection(Utils.OFFLINE_SERVER_URI)) assert wallet.is_connected_to_daemon() is False assert wallet.get_seed_language() == "English" assert wallet.get_path() == path @@ -158,7 +158,7 @@ def test_create_wallet_random_full(self, daemon: MoneroDaemonRpc) -> None: assert wallet.get_network_type() == MoneroNetworkType.TESTNET assert wallet.get_daemon_connection() is not None assert wallet.get_daemon_connection() != daemon.get_rpc_connection() - AssertUtils.assert_connection_equals(wallet.get_daemon_connection(), daemon.get_rpc_connection()) + AssertUtils.assert_equals(wallet.get_daemon_connection(), daemon.get_rpc_connection()) assert wallet.is_connected_to_daemon() assert wallet.get_seed_language() == "Spanish" assert path == wallet.get_path() @@ -185,7 +185,7 @@ def test_create_wallet_from_seed_full(self, daemon: MoneroDaemonRpc) -> None: assert wallet.get_seed() == Utils.SEED assert wallet.get_primary_address() == Utils.ADDRESS assert wallet.get_network_type() == Utils.NETWORK_TYPE - AssertUtils.assert_connection_equals(MoneroRpcConnection(Utils.OFFLINE_SERVER_URI), wallet.get_daemon_connection()) + AssertUtils.assert_equals(MoneroRpcConnection(Utils.OFFLINE_SERVER_URI), wallet.get_daemon_connection()) assert wallet.is_connected_to_daemon() is False assert wallet.get_seed_language() == "English" assert wallet.get_path() == path @@ -211,7 +211,7 @@ def test_create_wallet_from_seed_full(self, daemon: MoneroDaemonRpc) -> None: assert wallet.get_network_type() == Utils.NETWORK_TYPE assert wallet.get_daemon_connection() is not None assert wallet.get_daemon_connection() != daemon.get_rpc_connection() - AssertUtils.assert_connection_equals(wallet.get_daemon_connection(), daemon.get_rpc_connection()) + AssertUtils.assert_equals(wallet.get_daemon_connection(), daemon.get_rpc_connection()) assert wallet.is_connected_to_daemon() assert wallet.get_seed_language() == "English" assert wallet.get_path() == path @@ -233,7 +233,7 @@ def test_create_wallet_from_seed_full(self, daemon: MoneroDaemonRpc) -> None: assert wallet.get_seed() == Utils.SEED assert wallet.get_primary_address() == Utils.ADDRESS assert wallet.get_network_type() == Utils.NETWORK_TYPE - AssertUtils.assert_connection_equals(MoneroRpcConnection(Utils.OFFLINE_SERVER_URI), wallet.get_daemon_connection()) + AssertUtils.assert_equals(MoneroRpcConnection(Utils.OFFLINE_SERVER_URI), wallet.get_daemon_connection()) assert wallet.is_connected_to_daemon() is False assert wallet.get_seed_language() == "English" # TODO monero-project: why does height of new unsynced wallet start at 1? @@ -264,7 +264,7 @@ def test_create_wallet_from_seed_full(self, daemon: MoneroDaemonRpc) -> None: assert wallet.get_network_type() == Utils.NETWORK_TYPE assert wallet.get_daemon_connection() is not None assert wallet.get_daemon_connection() != daemon.get_rpc_connection() - AssertUtils.assert_connection_equals(wallet.get_daemon_connection(), daemon.get_rpc_connection()) + AssertUtils.assert_equals(wallet.get_daemon_connection(), daemon.get_rpc_connection()) assert wallet.is_connected_to_daemon() assert wallet.get_seed_language() == "English" assert wallet.get_path() == path @@ -311,7 +311,7 @@ def test_sync_random(self, daemon: MoneroDaemonRpc) -> None: restore_height: int = daemon.get_height() # test wallet's height before syncing - AssertUtils.assert_connection_equals(Utils.get_daemon_rpc_connection(), wallet.get_daemon_connection()) + AssertUtils.assert_equals(Utils.get_daemon_rpc_connection(), wallet.get_daemon_connection()) assert restore_height == wallet.get_daemon_height() assert wallet.is_connected_to_daemon() assert wallet.is_synced() is False @@ -482,7 +482,7 @@ def test_start_stop_syncing(self, daemon: MoneroDaemonRpc) -> None: wallet.set_restore_height(chain_height - 3) wallet.start_syncing() assert chain_height - 3 == wallet.get_restore_height() - AssertUtils.assert_connection_equals(daemon.get_rpc_connection(), wallet.get_daemon_connection()) + AssertUtils.assert_equals(daemon.get_rpc_connection(), wallet.get_daemon_connection()) wallet.stop_syncing() wallet.sync() wallet.stop_syncing() diff --git a/tests/utils/assert_utils.py b/tests/utils/assert_utils.py index c729318..196796d 100644 --- a/tests/utils/assert_utils.py +++ b/tests/utils/assert_utils.py @@ -4,8 +4,7 @@ from os import getenv from typing import Any, Optional from monero import ( - SerializableStruct, MoneroRpcConnection, - MoneroSubaddress + SerializableStruct, MoneroSubaddress ) logger: logging.Logger = logging.getLogger("AssertUtils") @@ -21,8 +20,6 @@ def assert_equals(cls, expr1: Any, expr2: Any, message: str = "assertion failed" 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}" @@ -33,18 +30,6 @@ def assert_list_equals(cls, expr1: list[Any], expr2: list[Any], message: str ="l elem2: Any = expr2[i] cls.assert_equals(elem1, elem2, message) - @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: diff --git a/tests/utils/daemon_utils.py b/tests/utils/daemon_utils.py index 5da634b..f9432d0 100644 --- a/tests/utils/daemon_utils.py +++ b/tests/utils/daemon_utils.py @@ -10,7 +10,8 @@ MoneroDaemonUpdateCheckResult, MoneroDaemonUpdateDownloadResult, MoneroNetworkType, MoneroRpcConnection, MoneroSubmitTxResult, MoneroKeyImageSpentStatus, MoneroDaemonRpc, MoneroTx, - MoneroBlock, MoneroOutputHistogramEntry, MoneroOutputDistributionEntry + MoneroBlock, MoneroOutputHistogramEntry, MoneroOutputDistributionEntry, + MoneroConnectionType, SerializableStruct ) from .gen_utils import GenUtils @@ -269,15 +270,51 @@ def test_tx_pool_stats(cls, stats: Optional[MoneroTxPoolStats]) -> None: #assert stats.histo is None @classmethod - def test_rpc_connection(cls, connection: Optional[MoneroRpcConnection], uri: Optional[str], connected: bool = True) -> None: + def test_rpc_connection(cls, connection: Optional[MoneroRpcConnection], uri: Optional[str], connected: bool, connection_type: Optional[MoneroConnectionType]) -> None: + """ + Test a monero rpc connection. + + :param MoneroRpcConnection | None connection: rpc connection to test. + :param str | None uri: rpc uri of the connection to test. + :param bool connected: checks if rpc is connected or not. + :param MoneroConnectionType | None connection_type: type of rpc connection to test. + :raises AssertionError: raises an error if rpc connection is not as expected. + """ + # check expected values from rpc connection assert connection is not None + assert isinstance(connection, SerializableStruct) + assert isinstance(connection, MoneroRpcConnection) assert uri is not None assert len(uri) > 0 assert connection.uri == uri - assert connection.check_connection() == connected + assert connection.check_connection() assert connection.is_connected() == connected assert connection.is_online() == connected + if connected: + assert connection.response_time is not None + assert connection.response_time > 0 + logger.debug(f"Rpc connection response time: {connection.response_time} ms") + else: + assert connection.response_time is None + + # test setting to readonly property + try: + connection.response_time = 0 # type: ignore + raise Exception("Should have failed") + except Exception as e: + e_msg: str = str(e) + assert e_msg != "Should have failed", e_msg + + # test connection type + if connection_type == MoneroConnectionType.I2P: + assert connection.is_i2p() + elif connection_type == MoneroConnectionType.TOR: + assert connection.is_onion() + elif connection_type is not None: + assert not connection.is_i2p() + assert not connection.is_onion() + @classmethod def test_block_template(cls, template: Optional[MoneroBlockTemplate]) -> None: assert template is not None diff --git a/tests/utils/integration_test_utils.py b/tests/utils/integration_test_utils.py index a2b9be0..949c8c4 100644 --- a/tests/utils/integration_test_utils.py +++ b/tests/utils/integration_test_utils.py @@ -17,6 +17,11 @@ class IntegrationTestUtils(ABC): __test__ = False + @classmethod + def setup_blockchain(cls) -> None: + """Setup blockchain for integration tests.""" + BlockchainUtils.setup_blockchain(TestUtils.NETWORK_TYPE) + @classmethod def setup(cls, wallet_type: WalletType) -> None: """ @@ -27,7 +32,7 @@ def setup(cls, wallet_type: WalletType) -> None: """ if wallet_type == WalletType.KEYS or wallet_type == WalletType.UNDEFINED: return - BlockchainUtils.setup_blockchain(TestUtils.NETWORK_TYPE) + cls.setup_blockchain() # get test wallet wallet: MoneroWallet type_str: str = "FULL" diff --git a/tests/utils/mining_utils.py b/tests/utils/mining_utils.py index 6912043..fcf2de8 100644 --- a/tests/utils/mining_utils.py +++ b/tests/utils/mining_utils.py @@ -20,7 +20,7 @@ def get_daemon(cls) -> MoneroDaemonRpc: Get internal mining daemon. """ if cls._DAEMON is None: - cls._DAEMON = MoneroDaemonRpc("127.0.0.1:18089") + cls._DAEMON = MoneroDaemonRpc("127.0.0.1:18089", Utils.DAEMON_RPC_USERNAME, Utils.DAEMON_RPC_PASSWORD) return cls._DAEMON diff --git a/tests/utils/string_utils.py b/tests/utils/string_utils.py index c8231e9..0ba4e96 100644 --- a/tests/utils/string_utils.py +++ b/tests/utils/string_utils.py @@ -3,20 +3,51 @@ class StringUtils(ABC): - """String utilities""" + """General string utilities""" @classmethod def get_percentage(cls, n: int, m: int, precision: int = 2) -> str: - """Get percentage in readable format""" + """ + Get percentage in readable format + + :param int n: steps completed. + :param int m: total steps. + :param int precision: percentage precision. + :returns str: percentage in readable format. + """ + # calculate percentage from steps 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""" + """ + Get percentage in readable format. + + :param float n: percentage value. + :param int precision: percentage precision. + :returns str: percentage in readable format. + """ + # set precision return f"{round(n, precision)}%" @classmethod def get_random_string(cls, n: int = 25) -> str: - """Generate random string""" + """ + Generate random string. + + :param int n: length of the random string to generate (default `25`). + :returns str: random string. + """ + # generate random string return token_hex(n) + + @classmethod + def is_none_or_empty(cls, str_value: str | None) -> bool: + """ + Checks if string is `None` or empty. + + :param str | None str_value: string value to check. + :returns bool: `True` if `str_value` is `None` or empty, `False` otherwise. + """ + return str_value is None or len(str_value) == 0 diff --git a/tests/utils/wallet_equality_utils.py b/tests/utils/wallet_equality_utils.py index a63b0bc..09e472b 100644 --- a/tests/utils/wallet_equality_utils.py +++ b/tests/utils/wallet_equality_utils.py @@ -80,7 +80,7 @@ def test_wallet_full_equality_on_chain(cls, wallet1: MoneroWalletFull, wallet2: wallet1_restore_height: int = wallet1.get_restore_height() wallet2_restore_height: int = wallet2.get_restore_height() assert wallet1_restore_height == wallet2_restore_height, f"{wallet1_restore_height} != {wallet2_restore_height}" - AssertUtils.assert_connection_equals(wallet1.get_daemon_connection(), wallet2.get_daemon_connection()) + AssertUtils.assert_equals(wallet1.get_daemon_connection(), wallet2.get_daemon_connection()) assert wallet1.get_seed_language() == wallet2.get_seed_language() # TODO more pybind specific extensions