diff --git a/src/cpp/daemon/py_monero_daemon.h b/src/cpp/daemon/py_monero_daemon.h index a4048fa..6f97ffe 100644 --- a/src/cpp/daemon/py_monero_daemon.h +++ b/src/cpp/daemon/py_monero_daemon.h @@ -150,8 +150,7 @@ class monero_daemon { virtual std::vector get_key_image_spent_statuses(const std::vector& key_images) { throw std::runtime_error("monero_daemon: not supported"); } virtual std::vector> get_outputs(const std::vector& outputs) { throw std::runtime_error("monero_daemon: not supported"); } virtual std::vector> get_output_histogram(const std::vector& amounts, const boost::optional& min_count, const boost::optional& max_count, const boost::optional& is_unlocked, const boost::optional& recent_cutoff) { throw std::runtime_error("monero_daemon: not supported"); } - virtual std::vector> get_output_distribution(const std::vector& amounts) { throw std::runtime_error("monero_daemon: not supported"); } - virtual std::vector> get_output_distribution(const std::vector& amounts, bool is_cumulative, uint64_t start_height, uint64_t end_height) { throw std::runtime_error("monero_daemon: not supported"); } + virtual std::vector> get_output_distribution(const std::vector& amounts, const boost::optional& is_cumulative = boost::none, const boost::optional& start_height = boost::none, const boost::optional& end_height = boost::none) { throw std::runtime_error("monero_daemon: not supported"); } virtual std::shared_ptr get_info() { throw std::runtime_error("monero_daemon: not supported"); } virtual std::shared_ptr get_sync_info() { throw std::runtime_error("monero_daemon: not supported"); } virtual std::shared_ptr get_hard_fork_info() { throw std::runtime_error("monero_daemon: not supported"); } diff --git a/src/cpp/daemon/py_monero_daemon_model.cpp b/src/cpp/daemon/py_monero_daemon_model.cpp index 9427a76..3687a96 100644 --- a/src/cpp/daemon/py_monero_daemon_model.cpp +++ b/src/cpp/daemon/py_monero_daemon_model.cpp @@ -921,6 +921,21 @@ void monero_output_distribution_entry::from_property_tree(const boost::property_ } } +void monero_output_distribution_entry::from_property_tree(const boost::property_tree::ptree& node, std::vector>& entries) { + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("distributions")) { + auto node2 = it->second; + for(boost::property_tree::ptree::const_iterator it2 = node2.begin(); it2 != node2.end(); ++it2) { + auto entry = std::make_shared(); + from_property_tree(it2->second, entry); + entries.push_back(entry); + } + } + } +} + rapidjson::Value monero_output_distribution_entry::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { // create root rapidjson::Value root(rapidjson::kObjectType); diff --git a/src/cpp/daemon/py_monero_daemon_model.h b/src/cpp/daemon/py_monero_daemon_model.h index 2e77f46..23000fa 100644 --- a/src/cpp/daemon/py_monero_daemon_model.h +++ b/src/cpp/daemon/py_monero_daemon_model.h @@ -260,6 +260,7 @@ struct monero_output_distribution_entry : public monero::serializable_struct { boost::optional m_start_height; static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& entry); + static void from_property_tree(const boost::property_tree::ptree& node, std::vector>& entries); rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; diff --git a/src/cpp/daemon/py_monero_daemon_rpc.cpp b/src/cpp/daemon/py_monero_daemon_rpc.cpp index 0a18c5b..0a700c5 100644 --- a/src/cpp/daemon/py_monero_daemon_rpc.cpp +++ b/src/cpp/daemon/py_monero_daemon_rpc.cpp @@ -517,7 +517,6 @@ std::vector> monero_daemon_rpc::get_outpu std::vector> monero_daemon_rpc::get_output_histogram(const std::vector& amounts, const boost::optional& min_count, const boost::optional& max_count, const boost::optional& is_unlocked, const boost::optional& recent_cutoff) { MTRACE("monero_daemon_rpc::get_output_histogram()"); - auto params = std::make_shared(amounts, min_count, max_count, is_unlocked, recent_cutoff); auto res = m_rpc->send_json_request("get_output_histogram", params); check_response_status(res); @@ -526,6 +525,16 @@ std::vector> monero_daemon_rpc::g return entries; } +std::vector> monero_daemon_rpc::get_output_distribution(const std::vector& amounts, const boost::optional& is_cumulative, const boost::optional& start_height, const boost::optional& end_height) { + MTRACE("monero_daemon_rpc::get_output_distribution()"); + auto params = std::make_shared(amounts, is_cumulative, start_height, end_height); + auto res = m_rpc->send_json_request("get_output_distribution", params); + check_response_status(res); + std::vector> entries; + monero_output_distribution_entry::from_property_tree(res, entries); + return entries; +} + std::shared_ptr monero_daemon_rpc::get_info() { auto res = m_rpc->send_json_request("get_info"); check_response_status(res); diff --git a/src/cpp/daemon/py_monero_daemon_rpc.h b/src/cpp/daemon/py_monero_daemon_rpc.h index bbce1ac..95a0701 100644 --- a/src/cpp/daemon/py_monero_daemon_rpc.h +++ b/src/cpp/daemon/py_monero_daemon_rpc.h @@ -110,6 +110,7 @@ class monero_daemon_rpc : public monero_daemon { std::vector get_key_image_spent_statuses(const std::vector& key_images) override; std::vector> get_outputs(const std::vector& outputs) override; std::vector> get_output_histogram(const std::vector& amounts, const boost::optional& min_count, const boost::optional& max_count, const boost::optional& is_unlocked, const boost::optional& recent_cutoff) override; + std::vector> get_output_distribution(const std::vector& amounts, const boost::optional& is_cumulative = boost::none, const boost::optional& start_height = boost::none, const boost::optional& end_height = boost::none) override; std::shared_ptr get_info() override; std::shared_ptr get_sync_info() override; std::shared_ptr get_hard_fork_info() override; diff --git a/src/cpp/daemon/py_monero_daemon_rpc_model.cpp b/src/cpp/daemon/py_monero_daemon_rpc_model.cpp index 96eace0..021b6cd 100644 --- a/src/cpp/daemon/py_monero_daemon_rpc_model.cpp +++ b/src/cpp/daemon/py_monero_daemon_rpc_model.cpp @@ -299,6 +299,29 @@ rapidjson::Value monero_get_output_histogram_params::to_rapidjson_val(rapidjson: return root; } +// --------------------------- MONERO GET OUTPUT DISTRIBUTION PARAMS --------------------------- + +rapidjson::Value monero_get_output_distribution_params::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set num values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_from_height != boost::none) monero_utils::add_json_member("from_height", m_from_height.get(), allocator, root, value_num); + if (m_to_height != boost::none) monero_utils::add_json_member("to_height", m_to_height.get(), allocator, root, value_num); + + // set bool values + if (m_cumulative != boost::none) monero_utils::add_json_member("cumulative", m_cumulative.get(), allocator, root); + if (m_binary != boost::none) monero_utils::add_json_member("binary", m_binary.get(), allocator, root); + + // set sub-array values + if (!m_amounts.empty()) root.AddMember("amounts", monero_utils::to_rapidjson_val(allocator, m_amounts), allocator); + + // return root + return root; +} + + // --------------------------- MONERO GET BLOCK RESULT --------------------------- void monero_get_block_result::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& result) { diff --git a/src/cpp/daemon/py_monero_daemon_rpc_model.h b/src/cpp/daemon/py_monero_daemon_rpc_model.h index 4f99282..fefc503 100644 --- a/src/cpp/daemon/py_monero_daemon_rpc_model.h +++ b/src/cpp/daemon/py_monero_daemon_rpc_model.h @@ -208,6 +208,19 @@ struct monero_get_output_histogram_params : public monero::serializable_struct { rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; +struct monero_get_output_distribution_params : public monero::serializable_struct { +public: + std::vector m_amounts; + boost::optional m_cumulative; + boost::optional m_binary; + boost::optional m_from_height; + boost::optional m_to_height; + + monero_get_output_distribution_params(const std::vector& amounts, const boost::optional& cumulative, const boost::optional& from_height, const boost::optional& to_height) : m_amounts(amounts), m_cumulative(cumulative), m_from_height(from_height), m_to_height(to_height), m_binary(false) { } + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; +}; + // ------------------------------ JSON-RPC Response --------------------------------- struct monero_get_block_result { diff --git a/src/cpp/py_monero.cpp b/src/cpp/py_monero.cpp index 4d2698c..a3d4646 100644 --- a/src/cpp/py_monero.cpp +++ b/src/cpp/py_monero.cpp @@ -1379,12 +1379,9 @@ PYBIND11_MODULE(monero, m) { .def("get_output_histogram", [](monero_daemon& self, const std::vector& amounts, const boost::optional& min_count, const boost::optional& max_count, const boost::optional& is_unlocked, const boost::optional& recent_cutoff) { MONERO_CATCH_AND_RETHROW(self.get_output_histogram(amounts, min_count, max_count, is_unlocked, recent_cutoff)); }, py::arg("amounts"), py::arg("min_count"), py::arg("max_count"), py::arg("is_unlocked"), py::arg("recent_cutoff"), py::call_guard()) - .def("get_output_distribution", [](monero_daemon& self, const std::vector& amounts) { - MONERO_CATCH_AND_RETHROW(self.get_output_distribution(amounts)); - }, py::arg("amounts"), py::call_guard()) - .def("get_output_distribution", [](monero_daemon& self, const std::vector& amounts, bool is_cumulative, uint64_t start_height, uint64_t end_height) { + .def("get_output_distribution", [](monero_daemon& self, const std::vector& amounts, const boost::optional& is_cumulative, const boost::optional& start_height, const boost::optional& end_height) { MONERO_CATCH_AND_RETHROW(self.get_output_distribution(amounts, is_cumulative, start_height, end_height)); - }, py::arg("amounts"), py::arg("is_cumulative"), py::arg("start_height"), py::arg("end_height"), py::call_guard()) + }, py::arg("amounts"), py::arg("is_cumulative") = py::none(), py::arg("start_height") = py::none(), py::arg("end_height") = py::none(), py::call_guard()) .def("get_info", [](monero_daemon& self) { MONERO_CATCH_AND_RETHROW(self.get_info()); }, py::call_guard()) @@ -2144,18 +2141,16 @@ 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)); + }, py::arg("bin")) + .def_static("binary_blocks_to_json", [](const py::bytes &bin) { + std::string b{bin}; + MONERO_CATCH_AND_RETHROW(PyMoneroUtils::binary_blocks_to_json(b)); }, py::arg("bin")); py_tx_height_comparator diff --git a/src/cpp/utils/py_monero_utils.cpp b/src/cpp/utils/py_monero_utils.cpp index 9ccc015..b79e054 100644 --- a/src/cpp/utils/py_monero_utils.cpp +++ b/src/cpp/utils/py_monero_utils.cpp @@ -182,8 +182,10 @@ std::string PyMoneroUtils::binary_to_json(const std::string &bin) { return json; } -void PyMoneroUtils::binary_blocks_to_json(const std::string &bin, std::string &json) { +std::string PyMoneroUtils::binary_blocks_to_json(const std::string &bin) { + std::string json; monero_utils::binary_blocks_to_json(bin, json); + return json; } void PyMoneroUtils::binary_blocks_to_property_tree(const std::string &bin, boost::property_tree::ptree &node) { diff --git a/src/cpp/utils/py_monero_utils.h b/src/cpp/utils/py_monero_utils.h index 5160d8b..873d485 100644 --- a/src/cpp/utils/py_monero_utils.h +++ b/src/cpp/utils/py_monero_utils.h @@ -88,7 +88,7 @@ class PyMoneroUtils { static std::string dict_to_binary(const py::dict &dictionary); static py::dict binary_to_dict(const std::string& bin); static std::string binary_to_json(const std::string &bin); - static void binary_blocks_to_json(const std::string &bin, std::string &json); + static std::string binary_blocks_to_json(const std::string &bin); static void binary_blocks_to_property_tree(const std::string &bin, boost::property_tree::ptree &node); static bool is_valid_language(const std::string& language); static std::vector> get_blocks_from_txs(std::vector> txs); diff --git a/src/cpp/wallet/py_monero_wallet.cpp b/src/cpp/wallet/py_monero_wallet.cpp index c7fd402..58c736a 100644 --- a/src/cpp/wallet/py_monero_wallet.cpp +++ b/src/cpp/wallet/py_monero_wallet.cpp @@ -64,16 +64,6 @@ void PyMoneroWallet::announce_new_block(uint64_t height) { } } -void PyMoneroWallet::announce_sync_progress(uint64_t height, uint64_t start_height, uint64_t end_height, float percent_done, const std::string &message) { - for (const auto &listener : m_listeners) { - try { - listener->on_sync_progress(height, start_height, end_height, percent_done, message); - } catch (const std::exception &e) { - MERROR(e.what()); - } - } -} - void PyMoneroWallet::announce_balances_changed(uint64_t balance, uint64_t unlocked_balance) { for (const auto &listener : m_listeners) { try { diff --git a/src/cpp/wallet/py_monero_wallet.h b/src/cpp/wallet/py_monero_wallet.h index 95a28f0..cf3baa6 100644 --- a/src/cpp/wallet/py_monero_wallet.h +++ b/src/cpp/wallet/py_monero_wallet.h @@ -602,7 +602,6 @@ class PyMoneroWallet : public monero::monero_wallet { } virtual void announce_new_block(uint64_t height); - virtual void announce_sync_progress(uint64_t height, uint64_t start_height, uint64_t end_height, float percent_done, const std::string &message); virtual void announce_balances_changed(uint64_t balance, uint64_t unlocked_balance); virtual void announce_output_spent(const std::shared_ptr &output); virtual void announce_output_received(const std::shared_ptr &output); diff --git a/src/python/monero_daemon.pyi b/src/python/monero_daemon.pyi index 9b8385f..8d677b2 100644 --- a/src/python/monero_daemon.pyi +++ b/src/python/monero_daemon.pyi @@ -318,25 +318,14 @@ class MoneroDaemon: """ ... - @typing.overload - def get_output_distribution(self, amounts: list[int]) -> list[MoneroOutputDistributionEntry]: - """ - Creates an output distribution. - - :param list[int] amounts: are amounts of outputs to make the distribution with. - :returns list[MoneroOutputDistributionEntry]: output distribution entries meeting the parameters. - """ - ... - - @typing.overload - def get_output_distribution(self, amounts: list[int], is_cumulative: bool, start_height: int, end_height: int) -> list[MoneroOutputDistributionEntry]: + def get_output_distribution(self, amounts: list[int], is_cumulative: bool | None = None, start_height: int | None = None, end_height: int | None = None) -> list[MoneroOutputDistributionEntry]: """ Creates an output distribution. :param list[int] amounts: are amounts of outputs to make the distribution with. - :param bool is_cumulative: specifies if the results should be cumulative. - :param int start_height: is the start height lower bound inclusive (optional). - :param int end_height: is the end height upper bound inclusive (optional). + :param bool | None is_cumulative: specifies if the results should be cumulative (optional). + :param int | None start_height: is the start height lower bound inclusive (optional). + :param int | None end_height: is the end height upper bound inclusive (optional). :returns list[MoneroOutputDistributionEntry]: output distribution entries meeting the parameters. """ ... diff --git a/src/python/monero_utils.pyi b/src/python/monero_utils.pyi index 37ce8da..2eb35e0 100644 --- a/src/python/monero_utils.pyi +++ b/src/python/monero_utils.pyi @@ -1,4 +1,4 @@ -from typing import Any, overload +from typing import Any from .monero_output_wallet import MoneroOutputWallet from .monero_block import MoneroBlock from .monero_transfer import MoneroTransfer @@ -22,7 +22,6 @@ class MoneroUtils: ... @staticmethod - @overload def binary_to_dict(bin: bytes) -> dict[Any, Any]: """ Deserialize a dictionary from binary format. @@ -33,18 +32,6 @@ 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. - :returns dict: Deserialized dictionary. - """ - ... - - @staticmethod - @overload def binary_to_json(bin: bytes) -> str: """ Deserialize a JSON string from binary format. @@ -55,13 +42,12 @@ class MoneroUtils: ... @staticmethod - @overload - def binary_to_json(bin: str) -> str: + def binary_blocks_to_json(bin: bytes) -> str: """ - Deserialize a JSON string from binary format. + Deserialize blocks JSON string from binary format. - :param str bin: JSON string in binary format. - :returns str: The deserialized JSON string. + :param bytes bin: blocks JSON string in binary format. + :returns str: The deserialized blocks in JSON string format. """ ... diff --git a/tests/test_monero_daemon_rpc.py b/tests/test_monero_daemon_rpc.py index 7f717e3..45fd150 100644 --- a/tests/test_monero_daemon_rpc.py +++ b/tests/test_monero_daemon_rpc.py @@ -732,11 +732,10 @@ def test_get_output_histogram_binary(self, daemon: MoneroDaemonRpc) -> None: for entry in entries: OutputUtils.test_output_histogram_entry(entry) - # Can get an output distribution (binary) + # Can get an output distribution @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - @pytest.mark.xfail(reason="TODO not implemented") - def test_get_output_distribution_binary(self, daemon: MoneroDaemonRpc) -> None: - amounts: list[int] = [0, 1, 10, 100, 1000, 10000, 100000, 1000000] + def test_get_output_distribution(self, daemon: MoneroDaemonRpc) -> None: + amounts: list[int] = [0] entries: list[MoneroOutputDistributionEntry] = daemon.get_output_distribution(amounts) for entry in entries: OutputUtils.test_output_distribution_entry(entry) diff --git a/tests/test_monero_rpc_connection.py b/tests/test_monero_rpc_connection.py index 803f769..6f595b5 100644 --- a/tests/test_monero_rpc_connection.py +++ b/tests/test_monero_rpc_connection.py @@ -1,8 +1,11 @@ import pytest import logging -from monero import MoneroRpcConnection, MoneroConnectionType, MoneroRpcError -from utils import TestUtils as Utils, RpcConnectionUtils, StringUtils, BaseTestClass +from monero import MoneroRpcConnection, MoneroConnectionType, MoneroRpcError, MoneroUtils +from utils import ( + TestUtils as Utils, RpcConnectionUtils, + StringUtils, BaseTestClass +) logger: logging.Logger = logging.getLogger("TestMoneroRpcConnection") @@ -218,6 +221,8 @@ def test_send_binary_request(self, node_connection: MoneroRpcConnection) -> None bin_result: bytes | None = node_connection.send_binary_request("get_blocks_by_height.bin", parameters) assert bin_result is not None logger.debug(f"Binary response: {bin_result}") + json_result: str = MoneroUtils.binary_blocks_to_json(bin_result) + logger.debug(f"Deserialized binary response: {StringUtils.prettify(json_result)}") # test invalid binary method try: diff --git a/tests/test_monero_utils.py b/tests/test_monero_utils.py index 2ba7af5..c5c5bf2 100644 --- a/tests/test_monero_utils.py +++ b/tests/test_monero_utils.py @@ -239,6 +239,7 @@ def test_key_validation(self, config: TestMoneroUtils.Config) -> None: # test public view key validation assert MoneroUtils.is_valid_public_view_key(config.keys.public_view_key) + MoneroUtils.validate_public_view_key(config.keys.public_view_key) WalletUtils.test_invalid_public_view_key("") WalletUtils.test_invalid_public_view_key(None) WalletUtils.test_invalid_public_view_key(config.keys.invalid_public_view_key) diff --git a/tests/utils/string_utils.py b/tests/utils/string_utils.py index 2980119..cc599b4 100644 --- a/tests/utils/string_utils.py +++ b/tests/utils/string_utils.py @@ -1,4 +1,6 @@ from abc import ABC +from typing import Any +from json import loads, dumps from secrets import token_hex @@ -38,3 +40,8 @@ def get_random_string(cls, n: int = 25) -> str: """ # generate random string return token_hex(n) + + @classmethod + def prettify(cls, json_str: str) -> str: + parsed_obj: Any = loads(json_str) + return dumps(parsed_obj, indent=1)