From d7a4af0fad2fbf0af1f1499a8215f6902c07489e Mon Sep 17 00:00:00 2001 From: everoddandeven Date: Sat, 25 Apr 2026 03:37:36 +0200 Subject: [PATCH] Daemon and wallet model maintenance * Implement serializable_struct for extended daemon and wallet model * Use listener reference in monero_daemon_rpc * Refactory PyMoneroUtils::make_uri, PyMoneroUtils::get_payment_uri * Refactory test code --- src/cpp/common/py_monero_common.cpp | 7 +- src/cpp/daemon/py_monero_daemon.h | 6 +- src/cpp/daemon/py_monero_daemon_model.cpp | 385 +++++++++++++++++- src/cpp/daemon/py_monero_daemon_model.h | 72 ++-- src/cpp/daemon/py_monero_daemon_rpc.cpp | 19 +- src/cpp/daemon/py_monero_daemon_rpc.h | 8 +- src/cpp/py_monero.cpp | 48 +-- src/cpp/utils/py_monero_utils.cpp | 43 +- src/cpp/utils/py_monero_utils.h | 4 +- src/cpp/wallet/py_monero_wallet_model.cpp | 21 +- src/cpp/wallet/py_monero_wallet_model.h | 4 +- src/cpp/wallet/py_monero_wallet_rpc.cpp | 1 - src/cpp/wallet/py_monero_wallet_rpc.h | 1 - src/python/monero_alt_chain.pyi | 5 +- src/python/monero_ban.pyi | 5 +- src/python/monero_block_template.pyi | 5 +- src/python/monero_connection_span.pyi | 5 +- .../monero_daemon_update_check_result.pyi | 5 +- src/python/monero_decoded_address.pyi | 3 +- src/python/monero_destination.pyi | 3 +- src/python/monero_fee_estimate.pyi | 5 +- src/python/monero_miner_tx_sum.pyi | 5 +- src/python/monero_mining_status.pyi | 5 +- src/python/monero_multisig_info.pyi | 5 +- src/python/monero_multisig_init_result.pyi | 5 +- src/python/monero_multisig_sign_result.pyi | 5 +- .../monero_output_distribution_entry.pyi | 5 +- src/python/monero_output_histogram_entry.pyi | 5 +- src/python/monero_peer.pyi | 3 +- src/python/monero_prune_result.pyi | 5 +- src/python/monero_utils.pyi | 3 +- src/python/monero_wallet_config.pyi | 3 +- tests/test_monero_common.py | 27 +- tests/test_monero_daemon_rpc.py | 25 +- tests/test_monero_rpc_connection.py | 4 +- tests/test_monero_utils.py | 39 +- tests/test_monero_wallet_common.py | 2 +- tests/test_monero_wallet_full.py | 15 +- tests/test_monero_wallet_model.py | 46 ++- tests/test_monero_wallet_rpc.py | 8 + tests/utils/block_utils.py | 54 ++- tests/utils/daemon_utils.py | 271 ++++-------- tests/utils/docker_wallet_rpc_manager.py | 10 +- tests/utils/output_utils.py | 33 +- tests/utils/sync_seed_tester.py | 58 +-- tests/utils/test_utils.py | 46 +-- tests/utils/tx_tester.py | 234 +++++++++++ tests/utils/tx_utils.py | 198 +-------- tests/utils/wallet_error_utils.py | 18 +- tests/utils/wallet_test_utils.py | 48 ++- tests/utils/wallet_utils.py | 13 +- 51 files changed, 1174 insertions(+), 679 deletions(-) create mode 100644 tests/utils/tx_tester.py diff --git a/src/cpp/common/py_monero_common.cpp b/src/cpp/common/py_monero_common.cpp index b2c918e..565829d 100644 --- a/src/cpp/common/py_monero_common.cpp +++ b/src/cpp/common/py_monero_common.cpp @@ -240,17 +240,14 @@ rapidjson::Value monero_request_params::to_rapidjson_val(rapidjson::Document::Al // --------------------------- MONERO RPC REQUEST --------------------------- -monero_rpc_request::monero_rpc_request(const std::string& method, const boost::optional& params, bool json_rpc): m_params(std::make_shared(params)) { - m_method = method; +monero_rpc_request::monero_rpc_request(const std::string& method, const boost::optional& params, bool json_rpc): m_method(method), m_params(std::make_shared(params)) { if (json_rpc) { m_id = "0"; m_version = "2.0"; } } -monero_rpc_request::monero_rpc_request(const std::string& method, const std::shared_ptr& params, bool json_rpc): - m_params(params) { - m_method = method; +monero_rpc_request::monero_rpc_request(const std::string& method, const std::shared_ptr& params, bool json_rpc): m_method(method), m_params(params) { if (params == nullptr) m_params = std::make_shared(); if (json_rpc) { m_id = "0"; diff --git a/src/cpp/daemon/py_monero_daemon.h b/src/cpp/daemon/py_monero_daemon.h index 870756e..a4048fa 100644 --- a/src/cpp/daemon/py_monero_daemon.h +++ b/src/cpp/daemon/py_monero_daemon.h @@ -79,9 +79,9 @@ class monero_daemon { */ virtual ~monero_daemon() {} monero_daemon() { } - virtual void add_listener(const std::shared_ptr &listener) { throw std::runtime_error("monero_daemon: not supported"); } - virtual void remove_listener(const std::shared_ptr &listener) { throw std::runtime_error("monero_daemon: not supported"); } - virtual std::vector> get_listeners() { throw std::runtime_error("monero_daemon: not supported"); } + virtual void add_listener(monero_daemon_listener &listener) { throw std::runtime_error("monero_daemon: not supported"); } + virtual void remove_listener(monero_daemon_listener &listener) { throw std::runtime_error("monero_daemon: not supported"); } + virtual std::set get_listeners() { throw std::runtime_error("monero_daemon: not supported"); } virtual void remove_listeners() { throw std::runtime_error("monero_daemon::remove_listeners(): not supported"); }; virtual monero::monero_version get_version() { throw std::runtime_error("monero_daemon: not supported"); } virtual bool is_trusted() { 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 a2e4d01..9427a76 100644 --- a/src/cpp/daemon/py_monero_daemon_model.cpp +++ b/src/cpp/daemon/py_monero_daemon_model.cpp @@ -56,6 +56,17 @@ // --------------------------- Custom Data Model --------------------------- +// Move to monero::monero_utils +rapidjson::Value to_rapidjson_vector_int_val(rapidjson::Document::AllocatorType& allocator, const std::vector& nums) { + rapidjson::Value value_arr(rapidjson::kArrayType); + rapidjson::Value value_num(rapidjson::kNumberType); + for (const auto& num : nums) { + value_num.SetInt(num); + value_arr.PushBack(value_num, allocator); + } + return value_arr; +} + void PyMoneroBlockHeader::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& header) { for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { std::string key = it->first; @@ -503,6 +514,27 @@ void monero_alt_chain::from_property_tree(const boost::property_tree::ptree& nod } } +rapidjson::Value monero_alt_chain::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set string values + rapidjson::Value value_str(rapidjson::kStringType); + if (m_main_chain_parent_block_hash != boost::none) monero_utils::add_json_member("mainChainParentBlockHash", m_main_chain_parent_block_hash.get(), allocator, root, value_str); + + // set number values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_difficulty != boost::none) monero_utils::add_json_member("difficulty", m_difficulty.get(), allocator, root, value_num); + if (m_height != boost::none) monero_utils::add_json_member("height", m_height.get(), allocator, root, value_num); + if (m_length != boost::none) monero_utils::add_json_member("length", m_length.get(), allocator, root, value_num); + + // set sub-arrays + if (!m_block_hashes.empty()) root.AddMember("blockHashes", monero_utils::to_rapidjson_val(allocator, m_block_hashes), allocator); + + // return root + return root; +} + // --------------------------- MONERO BAN --------------------------- void monero_ban::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& ban) { @@ -559,6 +591,21 @@ void monero_prune_result::from_property_tree(const boost::property_tree::ptree& } } +rapidjson::Value monero_prune_result::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set number values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_pruning_seed != boost::none) monero_utils::add_json_member("pruningSeed", m_pruning_seed.get(), allocator, root, value_num); + + // set bool values + if (m_is_pruned != boost::none) monero_utils::add_json_member("isPruned", m_is_pruned.get(), allocator, root); + + // return root + return root; +} + // --------------------------- MONERO MINING STATUS --------------------------- void monero_mining_status::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& status) { @@ -577,7 +624,28 @@ void monero_mining_status::from_property_tree(const boost::property_tree::ptree& } } -// --------------------------- MINER TX SUM --------------------------- +rapidjson::Value monero_mining_status::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set string values + rapidjson::Value value_str(rapidjson::kStringType); + if (m_address != boost::none) monero_utils::add_json_member("address", m_address.get(), allocator, root, value_str); + + // set number values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_speed != boost::none) monero_utils::add_json_member("speed", m_speed.get(), allocator, root, value_num); + if (m_num_threads != boost::none) monero_utils::add_json_member("numThreads", m_num_threads.get(), allocator, root, value_num); + + // set bool values + if (m_is_active != boost::none) monero_utils::add_json_member("isActive", m_is_active.get(), allocator, root); + if (m_is_background != boost::none) monero_utils::add_json_member("isBackground", m_is_background.get(), allocator, root); + + // return root + return root; +} + +// --------------------------- MONERO MINER TX SUM --------------------------- void monero_miner_tx_sum::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& sum) { for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { @@ -587,6 +655,19 @@ void monero_miner_tx_sum::from_property_tree(const boost::property_tree::ptree& } } +rapidjson::Value monero_miner_tx_sum::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set number values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_emission_sum != boost::none) monero_utils::add_json_member("emissionSum", m_emission_sum.get(), allocator, root, value_num); + if (m_fee_sum != boost::none) monero_utils::add_json_member("feeSum", m_fee_sum.get(), allocator, root, value_num); + + // return root + return root; +} + // --------------------------- MONERO BLOCK TEMPLATE --------------------------- void monero_block_template::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& tmplt) { @@ -605,6 +686,30 @@ void monero_block_template::from_property_tree(const boost::property_tree::ptree } } +rapidjson::Value monero_block_template::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set string values + rapidjson::Value value_str(rapidjson::kStringType); + if (m_block_template_blob != boost::none) monero_utils::add_json_member("blockTemplateBlob", m_block_template_blob.get(), allocator, root, value_str); + if (m_block_hashing_blob != boost::none) monero_utils::add_json_member("blockHashingBlob", m_block_hashing_blob.get(), allocator, root, value_str); + if (m_prev_hash != boost::none) monero_utils::add_json_member("prevHash", m_prev_hash.get(), allocator, root, value_str); + if (m_seed_hash != boost::none) monero_utils::add_json_member("seedHash", m_seed_hash.get(), allocator, root, value_str); + if (m_next_seed_hash != boost::none) monero_utils::add_json_member("nextSeedHash", m_next_seed_hash.get(), allocator, root, value_str); + + // set number values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_difficulty != boost::none) monero_utils::add_json_member("difficulty", m_difficulty.get(), allocator, root, value_num); + if (m_expected_reward != boost::none) monero_utils::add_json_member("expectedReward", m_expected_reward.get(), allocator, root, value_num); + if (m_height != boost::none) monero_utils::add_json_member("height", m_height.get(), allocator, root, value_num); + if (m_reserved_offset != boost::none) monero_utils::add_json_member("reservedOffset", m_reserved_offset.get(), allocator, root, value_num); + if (m_seed_height != boost::none) monero_utils::add_json_member("seedHeight", m_seed_height.get(), allocator, root, value_num); + + // return root + return root; +} + // --------------------------- MONERO CONNECTION SPAN --------------------------- void monero_connection_span::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& span) { @@ -620,6 +725,27 @@ void monero_connection_span::from_property_tree(const boost::property_tree::ptre } } +rapidjson::Value monero_connection_span::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set string values + rapidjson::Value value_str(rapidjson::kStringType); + if (m_connection_id != boost::none) monero_utils::add_json_member("connectionId", m_connection_id.get(), allocator, root, value_str); + if (m_remote_address != boost::none) monero_utils::add_json_member("remoteAddress", m_remote_address.get(), allocator, root, value_str); + + // set number values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_num_blocks != boost::none) monero_utils::add_json_member("numBlocks", m_num_blocks.get(), allocator, root, value_num); + if (m_rate != boost::none) monero_utils::add_json_member("rate", m_rate.get(), allocator, root, value_num); + if (m_speed != boost::none) monero_utils::add_json_member("speed", m_speed.get(), allocator, root, value_num); + if (m_size != boost::none) monero_utils::add_json_member("size", m_size.get(), allocator, root, value_num); + if (m_start_height != boost::none) monero_utils::add_json_member("startHeight", m_start_height.get(), allocator, root, value_num); + + // return root + return root; +} + // --------------------------- MONERO PEER --------------------------- void monero_peer::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& peer) { @@ -687,6 +813,47 @@ void monero_peer::from_property_tree(const boost::property_tree::ptree& node, st } } +rapidjson::Value monero_peer::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set string values + rapidjson::Value value_str(rapidjson::kStringType); + if (m_id != boost::none) monero_utils::add_json_member("id", m_id.get(), allocator, root, value_str); + if (m_address != boost::none) monero_utils::add_json_member("address", m_address.get(), allocator, root, value_str); + if (m_host != boost::none) monero_utils::add_json_member("host", m_host.get(), allocator, root, value_str); + if (m_hash != boost::none) monero_utils::add_json_member("hash", m_hash.get(), allocator, root, value_str); + if (m_state != boost::none) monero_utils::add_json_member("state", m_state.get(), allocator, root, value_str); + + // set number values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_port != boost::none) monero_utils::add_json_member("port", m_port.get(), allocator, root, value_num); + if (m_rpc_port != boost::none) monero_utils::add_json_member("rpcPort", m_rpc_port.get(), allocator, root, value_num); + if (m_last_seen_timestamp != boost::none) monero_utils::add_json_member("lastSeenTimestamp", m_last_seen_timestamp.get(), allocator, root, value_num); + if (m_pruning_seed != boost::none) monero_utils::add_json_member("pruningSeed", m_pruning_seed.get(), allocator, root, value_num); + if (m_rpc_credits_per_hash != boost::none) monero_utils::add_json_member("rpcCreditsPerHash", m_rpc_credits_per_hash.get(), allocator, root, value_num); + if (m_avg_download != boost::none) monero_utils::add_json_member("avgDownload", m_avg_download.get(), allocator, root, value_num); + if (m_avg_upload != boost::none) monero_utils::add_json_member("avgUpload", m_avg_upload.get(), allocator, root, value_num); + if (m_current_download != boost::none) monero_utils::add_json_member("currentDownload", m_current_download.get(), allocator, root, value_num); + if (m_current_upload != boost::none) monero_utils::add_json_member("currentUpload", m_current_upload.get(), allocator, root, value_num); + if (m_height != boost::none) monero_utils::add_json_member("height", m_height.get(), allocator, root, value_num); + if (m_live_time != boost::none) monero_utils::add_json_member("liveTime", m_live_time.get(), allocator, root, value_num); + if (m_num_receives != boost::none) monero_utils::add_json_member("numReceives", m_num_receives.get(), allocator, root, value_num); + if (m_num_sends != boost::none) monero_utils::add_json_member("numSends", m_num_sends.get(), allocator, root, value_num); + if (m_receive_idle_time != boost::none) monero_utils::add_json_member("receiveIdleTime", m_receive_idle_time.get(), allocator, root, value_num); + if (m_send_idle_time != boost::none) monero_utils::add_json_member("sendIdleTime", m_send_idle_time.get(), allocator, root, value_num); + if (m_num_support_flags != boost::none) monero_utils::add_json_member("numSupportFlags", m_num_support_flags.get(), allocator, root, value_num); + + // set bool values + if (m_is_online != boost::none) monero_utils::add_json_member("isOnline", m_is_online.get(), allocator, root); + if (m_is_incoming != boost::none) monero_utils::add_json_member("isIncoming", m_is_incoming.get(), allocator, root); + if (m_is_local_ip != boost::none) monero_utils::add_json_member("isLocalIp", m_is_local_ip.get(), allocator, root); + if (m_is_local_host != boost::none) monero_utils::add_json_member("isLocalHost", m_is_local_host.get(), allocator, root); + + // return root + return root; +} + // --------------------------- MONERO SUBMIT TX RESULT --------------------------- void monero_submit_tx_result::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& result) { @@ -710,6 +877,33 @@ void monero_submit_tx_result::from_property_tree(const boost::property_tree::ptr } } +rapidjson::Value monero_submit_tx_result::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root = monero_rpc_payment_info::to_rapidjson_val(allocator); + + // set string values + rapidjson::Value value_str(rapidjson::kStringType); + if (m_reason != boost::none) monero_utils::add_json_member("reason", m_reason.get(), allocator, root, value_str); + + // set bool values + if (m_has_invalid_input != boost::none) monero_utils::add_json_member("hasInvalidInput", m_has_invalid_input.get(), allocator, root); + if (m_has_invalid_output != boost::none) monero_utils::add_json_member("hasInvalidOutput", m_has_invalid_output.get(), allocator, root); + if (m_has_too_few_outputs != boost::none) monero_utils::add_json_member("hasTooFewOutputs", m_has_too_few_outputs.get(), allocator, root); + if (m_is_good != boost::none) monero_utils::add_json_member("isGood", m_is_good.get(), allocator, root); + if (m_is_relayed != boost::none) monero_utils::add_json_member("isRelayed", m_is_relayed.get(), allocator, root); + if (m_is_double_spend != boost::none) monero_utils::add_json_member("isDoubleSpend", m_is_double_spend.get(), allocator, root); + if (m_is_fee_too_low != boost::none) monero_utils::add_json_member("isFeeTooLow", m_is_fee_too_low.get(), allocator, root); + if (m_is_mixin_too_low != boost::none) monero_utils::add_json_member("isMixinTooLow", m_is_mixin_too_low.get(), allocator, root); + if (m_is_overspend != boost::none) monero_utils::add_json_member("isOverspend", m_is_overspend.get(), allocator, root); + if (m_is_too_big != boost::none) monero_utils::add_json_member("isTooBig", m_is_too_big.get(), allocator, root); + if (m_is_tx_extra_too_big != boost::none) monero_utils::add_json_member("isTxExtraTooBig", m_is_tx_extra_too_big.get(), allocator, root); + if (m_is_nonzero_unlock_time != boost::none) monero_utils::add_json_member("isNonZeroUnlockTime", m_is_nonzero_unlock_time.get(), allocator, root); + if (m_sanity_check_failed != boost::none) monero_utils::add_json_member("sanityCheckFailed", m_sanity_check_failed.get(), allocator, root); + + // return root + return root; +} + // --------------------------- MONERO OUTPUT DISTRIBUTION ENTRY --------------------------- void monero_output_distribution_entry::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& entry) { @@ -727,6 +921,23 @@ void monero_output_distribution_entry::from_property_tree(const boost::property_ } } +rapidjson::Value monero_output_distribution_entry::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set number values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_amount != boost::none) monero_utils::add_json_member("amount", m_amount.get(), allocator, root, value_num); + if (m_base != boost::none) monero_utils::add_json_member("base", m_base.get(), allocator, root, value_num); + if (m_start_height != boost::none) monero_utils::add_json_member("startHeight", m_start_height.get(), allocator, root, value_num); + + // set sub-arrays + if (!m_distribution.empty()) root.AddMember("distribution", to_rapidjson_vector_int_val(allocator, m_distribution), allocator); + + // return root + return root; +} + // --------------------------- MONERO OUTPUT HISTOGRAM ENTRY --------------------------- void monero_output_histogram_entry::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& entry) { @@ -755,6 +966,21 @@ void monero_output_histogram_entry::from_property_tree(const boost::property_tre } } +rapidjson::Value monero_output_histogram_entry::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set number values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_amount != boost::none) monero_utils::add_json_member("amount", m_amount.get(), allocator, root, value_num); + if (m_num_instances != boost::none) monero_utils::add_json_member("numInstances", m_num_instances.get(), allocator, root, value_num); + if (m_unlocked_instances != boost::none) monero_utils::add_json_member("unlockedInstances", m_unlocked_instances.get(), allocator, root, value_num); + if (m_recent_instances != boost::none) monero_utils::add_json_member("recentInstances", m_recent_instances.get(), allocator, root, value_num); + + // return root + return root; +} + // --------------------------- MONERO TX POOL STATS --------------------------- void monero_tx_pool_stats::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& stats) { @@ -809,17 +1035,16 @@ rapidjson::Value monero_tx_pool_stats::to_rapidjson_val(rapidjson::Document::All rapidjson::Value value_num(rapidjson::kNumberType); if (m_num_txs != boost::none) monero_utils::add_json_member("numTxs", m_num_txs.get(), allocator, root, value_num); if (m_num_not_relayed != boost::none) monero_utils::add_json_member("numNotRelayed", m_num_not_relayed.get(), allocator, root, value_num); - - if (m_num_failing != boost::none) monero_utils::add_json_member("numTxs", m_num_failing.get(), allocator, root, value_num); - if (m_num_double_spends != boost::none) monero_utils::add_json_member("numTxs", m_num_double_spends.get(), allocator, root, value_num); - if (m_num10m != boost::none) monero_utils::add_json_member("numTxs", m_num10m.get(), allocator, root, value_num); - if (m_fee_total != boost::none) monero_utils::add_json_member("numTxs", m_fee_total.get(), allocator, root, value_num); - if (m_bytes_max != boost::none) monero_utils::add_json_member("numTxs", m_bytes_max.get(), allocator, root, value_num); - if (m_bytes_med != boost::none) monero_utils::add_json_member("numTxs", m_bytes_med.get(), allocator, root, value_num); - if (m_bytes_min != boost::none) monero_utils::add_json_member("numTxs", m_bytes_min.get(), allocator, root, value_num); - if (m_bytes_total != boost::none) monero_utils::add_json_member("numTxs", m_bytes_total.get(), allocator, root, value_num); - if (m_histo98pc != boost::none) monero_utils::add_json_member("numTxs", m_histo98pc.get(), allocator, root, value_num); - if (m_oldest_timestamp != boost::none) monero_utils::add_json_member("numTxs", m_oldest_timestamp.get(), allocator, root, value_num); + if (m_num_failing != boost::none) monero_utils::add_json_member("numFailing", m_num_failing.get(), allocator, root, value_num); + if (m_num_double_spends != boost::none) monero_utils::add_json_member("numDoubleSpends", m_num_double_spends.get(), allocator, root, value_num); + if (m_num10m != boost::none) monero_utils::add_json_member("num10m", m_num10m.get(), allocator, root, value_num); + if (m_fee_total != boost::none) monero_utils::add_json_member("feeTotal", m_fee_total.get(), allocator, root, value_num); + if (m_bytes_max != boost::none) monero_utils::add_json_member("bytesMax", m_bytes_max.get(), allocator, root, value_num); + if (m_bytes_med != boost::none) monero_utils::add_json_member("bytesMed", m_bytes_med.get(), allocator, root, value_num); + if (m_bytes_min != boost::none) monero_utils::add_json_member("bytesMin", m_bytes_min.get(), allocator, root, value_num); + if (m_bytes_total != boost::none) monero_utils::add_json_member("bytesTotal", m_bytes_total.get(), allocator, root, value_num); + if (m_histo98pc != boost::none) monero_utils::add_json_member("histo98pc", m_histo98pc.get(), allocator, root, value_num); + if (m_oldest_timestamp != boost::none) monero_utils::add_json_member("oldestTimestamp", m_oldest_timestamp.get(), allocator, root, value_num); // set object values rapidjson::Value histo(rapidjson::kObjectType); @@ -847,6 +1072,24 @@ void monero_daemon_update_check_result::from_property_tree(const boost::property } } +rapidjson::Value monero_daemon_update_check_result::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set string values + rapidjson::Value value_str(rapidjson::kStringType); + if (m_version != boost::none) monero_utils::add_json_member("version", m_version.get(), allocator, root, value_str); + if (m_hash != boost::none) monero_utils::add_json_member("hash", m_hash.get(), allocator, root, value_str); + if (m_auto_uri != boost::none) monero_utils::add_json_member("autoUri", m_auto_uri.get(), allocator, root, value_str); + if (m_user_uri != boost::none) monero_utils::add_json_member("userUri", m_user_uri.get(), allocator, root, value_str); + + // set bool values + if (m_is_update_available != boost::none) monero_utils::add_json_member("isUpdateAvailable", m_is_update_available.get(), allocator, root); + + // return root + return root; +} + // --------------------------- MONERO DAEMON UPDATE DOWNLOAD RESULT --------------------------- void monero_daemon_update_download_result::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& check) { @@ -858,6 +1101,18 @@ void monero_daemon_update_download_result::from_property_tree(const boost::prope } } +rapidjson::Value monero_daemon_update_download_result::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root = monero_daemon_update_check_result::to_rapidjson_val(allocator); + + // set string values + rapidjson::Value value_str(rapidjson::kStringType); + if (m_download_path != boost::none) monero_utils::add_json_member("downloadPath", m_download_path.get(), allocator, root, value_str); + + // return root + return root; +} + // --------------------------- MONERO FEE ESTIMATE --------------------------- void monero_fee_estimate::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& estimate) { @@ -875,6 +1130,22 @@ void monero_fee_estimate::from_property_tree(const boost::property_tree::ptree& } } +rapidjson::Value monero_fee_estimate::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set number values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_fee != boost::none) monero_utils::add_json_member("fee", m_fee.get(), allocator, root, value_num); + if (m_quantization_mask != boost::none) monero_utils::add_json_member("quantizationMask", m_quantization_mask.get(), allocator, root, value_num); + + // set sub-arrays + if (!m_fees.empty()) root.AddMember("fees", monero_utils::to_rapidjson_val(allocator, m_fees), allocator); + + // return root + return root; +} + // --------------------------- MONERO DAEMON INFO --------------------------- void monero_daemon_info::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& info) { @@ -921,6 +1192,53 @@ void monero_daemon_info::from_property_tree(const boost::property_tree::ptree& n } } +rapidjson::Value monero_daemon_info::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set string values + rapidjson::Value value_str(rapidjson::kStringType); + if (m_version != boost::none) monero_utils::add_json_member("version", m_version.get(), allocator, root, value_str); + if (m_bootstrap_daemon_address != boost::none) monero_utils::add_json_member("bootstrapDaemonAddress", m_bootstrap_daemon_address.get(), allocator, root, value_str); + + // set number values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_num_alt_blocks != boost::none) monero_utils::add_json_member("numAltBlocks", m_num_alt_blocks.get(), allocator, root, value_num); + if (m_block_size_limit != boost::none) monero_utils::add_json_member("blockSizeLimit", m_block_size_limit.get(), allocator, root, value_num); + if (m_block_size_median != boost::none) monero_utils::add_json_member("blockSizeMedian", m_block_size_median.get(), allocator, root, value_num); + if (m_block_weight_limit != boost::none) monero_utils::add_json_member("blockWeightLimit", m_block_weight_limit.get(), allocator, root, value_num); + if (m_block_weight_median != boost::none) monero_utils::add_json_member("blockWeightMedian", m_block_weight_median.get(), allocator, root, value_num); + if (m_difficulty != boost::none) monero_utils::add_json_member("difficulty", m_difficulty.get(), allocator, root, value_num); + if (m_cumulative_difficulty != boost::none) monero_utils::add_json_member("cumulativeDifficulty", m_cumulative_difficulty.get(), allocator, root, value_num); + if (m_free_space != boost::none) monero_utils::add_json_member("freeSpace", m_free_space.get(), allocator, root, value_num); + if (m_num_offline_peers != boost::none) monero_utils::add_json_member("numOfflinePeers", m_num_offline_peers.get(), allocator, root, value_num); + if (m_num_online_peers != boost::none) monero_utils::add_json_member("numOnlinePeers", m_num_online_peers.get(), allocator, root, value_num); + if (m_height != boost::none) monero_utils::add_json_member("height", m_height.get(), allocator, root, value_num); + if (m_height_without_bootstrap != boost::none) monero_utils::add_json_member("heightWithoutBootstrap", m_height_without_bootstrap.get(), allocator, root, value_num); + if (m_network_type != boost::none) monero_utils::add_json_member("networkType", (uint8_t)m_network_type.get(), allocator, root, value_num); + if (m_num_incoming_connections != boost::none) monero_utils::add_json_member("numIncomingConnections", m_num_incoming_connections.get(), allocator, root, value_num); + if (m_num_outgoing_connections != boost::none) monero_utils::add_json_member("numOutgoingConnections", m_num_outgoing_connections.get(), allocator, root, value_num); + if (m_num_rpc_connections != boost::none) monero_utils::add_json_member("numRpcConnections", m_num_rpc_connections.get(), allocator, root, value_num); + if (m_start_timestamp != boost::none) monero_utils::add_json_member("startTimestamp", m_start_timestamp.get(), allocator, root, value_num); + if (m_adjusted_timestamp != boost::none) monero_utils::add_json_member("adjustedTimestamp", m_adjusted_timestamp.get(), allocator, root, value_num); + if (m_target != boost::none) monero_utils::add_json_member("target", m_target.get(), allocator, root, value_num); + if (m_target_height != boost::none) monero_utils::add_json_member("targetHeight", m_target_height.get(), allocator, root, value_num); + if (m_num_txs != boost::none) monero_utils::add_json_member("numTxs", m_num_txs.get(), allocator, root, value_num); + if (m_num_txs_pool != boost::none) monero_utils::add_json_member("numTxsPool", m_num_txs_pool.get(), allocator, root, value_num); + if (m_database_size != boost::none) monero_utils::add_json_member("databaseSize", m_database_size.get(), allocator, root, value_num); + + // set bool values + if (m_is_offline != boost::none) monero_utils::add_json_member("isOffline", m_is_offline.get(), allocator, root); + if (m_was_bootstrap_ever_used != boost::none) monero_utils::add_json_member("wasBootstrapEverUsed", m_was_bootstrap_ever_used.get(), allocator, root); + if (m_update_available != boost::none) monero_utils::add_json_member("updateAvailable", m_update_available.get(), allocator, root); + if (m_is_busy_syncing != boost::none) monero_utils::add_json_member("isBusySyncing", m_is_busy_syncing.get(), allocator, root); + if (m_is_synchronized != boost::none) monero_utils::add_json_member("isSynchronized", m_is_synchronized.get(), allocator, root); + if (m_is_restricted != boost::none) monero_utils::add_json_member("isRestricted", m_is_restricted.get(), allocator, root); + + // return root + return root; +} + // --------------------------- MONERO DAEMON SYNC INFO --------------------------- void monero_daemon_sync_info::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& info) { @@ -935,6 +1253,28 @@ void monero_daemon_sync_info::from_property_tree(const boost::property_tree::ptr } } +rapidjson::Value monero_daemon_sync_info::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set string values + rapidjson::Value value_str(rapidjson::kStringType); + if (m_overview != boost::none) monero_utils::add_json_member("overview", m_overview.get(), allocator, root, value_str); + + // set number values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_height != boost::none) monero_utils::add_json_member("height", m_height.get(), allocator, root, value_num); + if (m_target_height != boost::none) monero_utils::add_json_member("targetHeight", m_target_height.get(), allocator, root, value_num); + if (m_next_needed_pruning_seed != boost::none) monero_utils::add_json_member("nextNeededPruningSeed", m_next_needed_pruning_seed.get(), allocator, root, value_num); + + // set sub-arrays + if (!m_peers.empty()) root.AddMember("peers", monero_utils::to_rapidjson_val(allocator, m_peers), allocator); + if (!m_spans.empty()) root.AddMember("spans", monero_utils::to_rapidjson_val(allocator, m_spans), allocator); + + // return root + return root; +} + // --------------------------- MONERO HARD FORK INFO --------------------------- void monero_hard_fork_info::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& info) { @@ -952,3 +1292,24 @@ void monero_hard_fork_info::from_property_tree(const boost::property_tree::ptree else if (key == std::string("voting")) info->m_voting = it->second.get_value(); } } + +rapidjson::Value monero_hard_fork_info::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set number values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_earliest_height != boost::none) monero_utils::add_json_member("earliestHeight", m_earliest_height.get(), allocator, root, value_num); + if (m_state != boost::none) monero_utils::add_json_member("state", m_state.get(), allocator, root, value_num); + if (m_threshold != boost::none) monero_utils::add_json_member("threshold", m_threshold.get(), allocator, root, value_num); + if (m_version != boost::none) monero_utils::add_json_member("version", m_version.get(), allocator, root, value_num); + if (m_num_votes != boost::none) monero_utils::add_json_member("numVotes", m_num_votes.get(), allocator, root, value_num); + if (m_window != boost::none) monero_utils::add_json_member("window", m_window.get(), allocator, root, value_num); + if (m_voting != boost::none) monero_utils::add_json_member("voting", m_voting.get(), allocator, root, value_num); + + // set bool values + if (m_is_enabled != boost::none) monero_utils::add_json_member("isEnabled", m_is_enabled.get(), allocator, root); + + // return root + return root; +} diff --git a/src/cpp/daemon/py_monero_daemon_model.h b/src/cpp/daemon/py_monero_daemon_model.h index 3180a76..2e77f46 100644 --- a/src/cpp/daemon/py_monero_daemon_model.h +++ b/src/cpp/daemon/py_monero_daemon_model.h @@ -108,7 +108,7 @@ struct monero_rpc_payment_info : public monero::serializable_struct { static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& rpc_payment_info); }; -struct monero_alt_chain { +struct monero_alt_chain : public monero::serializable_struct { public: std::vector m_block_hashes; boost::optional m_difficulty; @@ -116,6 +116,7 @@ struct monero_alt_chain { boost::optional m_length; boost::optional m_main_chain_parent_block_hash; + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& alt_chain); }; @@ -131,15 +132,16 @@ struct monero_ban : public monero::serializable_struct { static void from_property_tree(const boost::property_tree::ptree& node, std::vector>& bans); }; -struct monero_prune_result { +struct monero_prune_result : public monero::serializable_struct { public: boost::optional m_is_pruned; boost::optional m_pruning_seed; static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& result); + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; -struct monero_mining_status { +struct monero_mining_status : public monero::serializable_struct { public: boost::optional m_is_active; boost::optional m_is_background; @@ -148,46 +150,50 @@ struct monero_mining_status { boost::optional m_num_threads; static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& status); + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; -struct monero_miner_tx_sum { +struct monero_miner_tx_sum : public monero::serializable_struct { public: boost::optional m_emission_sum; boost::optional m_fee_sum; static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& sum); + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; -struct monero_block_template { +struct monero_block_template : public monero::serializable_struct { public: boost::optional m_block_template_blob; boost::optional m_block_hashing_blob; + boost::optional m_prev_hash; + boost::optional m_seed_hash; + boost::optional m_next_seed_hash; boost::optional m_difficulty; boost::optional m_expected_reward; boost::optional m_height; - boost::optional m_prev_hash; boost::optional m_reserved_offset; boost::optional m_seed_height; - boost::optional m_seed_hash; - boost::optional m_next_seed_hash; static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& tmplt); + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; -struct monero_connection_span { +struct monero_connection_span : public monero::serializable_struct { public: boost::optional m_connection_id; - boost::optional m_num_blocks; boost::optional m_remote_address; + boost::optional m_num_blocks; boost::optional m_rate; boost::optional m_speed; boost::optional m_size; boost::optional m_start_height; static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& span); + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; -struct monero_peer { +struct monero_peer : public monero::serializable_struct { public: boost::optional m_id; boost::optional m_address; @@ -218,35 +224,35 @@ struct monero_peer { static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& peer); static void from_property_tree(const boost::property_tree::ptree& node, std::vector>& peers); + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; struct monero_submit_tx_result : public monero_rpc_payment_info { public: + boost::optional m_has_invalid_input; + boost::optional m_has_invalid_output; + boost::optional m_has_too_few_outputs; boost::optional m_is_good; boost::optional m_is_relayed; boost::optional m_is_double_spend; boost::optional m_is_fee_too_low; boost::optional m_is_mixin_too_low; - boost::optional m_has_invalid_input; - boost::optional m_has_invalid_output; - boost::optional m_has_too_few_outputs; boost::optional m_is_overspend; boost::optional m_is_too_big; boost::optional m_sanity_check_failed; - boost::optional m_reason; - boost::optional m_credits; - boost::optional m_top_block_hash; boost::optional m_is_tx_extra_too_big; boost::optional m_is_nonzero_unlock_time; + boost::optional m_reason; static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& result); + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; struct monero_tx_backlog_entry { // TODO }; -struct monero_output_distribution_entry { +struct monero_output_distribution_entry : public monero::serializable_struct { public: boost::optional m_amount; boost::optional m_base; @@ -254,9 +260,10 @@ struct monero_output_distribution_entry { boost::optional m_start_height; static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& entry); + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; -struct monero_output_histogram_entry { +struct monero_output_histogram_entry : public monero::serializable_struct { public: boost::optional m_amount; boost::optional m_num_instances; @@ -265,6 +272,7 @@ struct monero_output_histogram_entry { 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; }; struct monero_tx_pool_stats : public monero::serializable_struct { @@ -287,7 +295,7 @@ struct monero_tx_pool_stats : public monero::serializable_struct { static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& stats); }; -struct monero_daemon_update_check_result { +struct monero_daemon_update_check_result : public monero::serializable_struct { public: boost::optional m_is_update_available; boost::optional m_version; @@ -296,6 +304,7 @@ struct monero_daemon_update_check_result { boost::optional m_user_uri; static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& check); + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; struct monero_daemon_update_download_result : public monero_daemon_update_check_result { @@ -303,15 +312,17 @@ struct monero_daemon_update_download_result : public monero_daemon_update_check_ boost::optional m_download_path; static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& check); + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; -struct monero_fee_estimate { +struct monero_fee_estimate : public monero::serializable_struct { public: + boost::optional m_quantization_mask; boost::optional m_fee; std::vector m_fees; - boost::optional m_quantization_mask; static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& estimate); + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; struct monero_daemon_info : public monero_rpc_payment_info { @@ -339,46 +350,43 @@ struct monero_daemon_info : public monero_rpc_payment_info { boost::optional m_adjusted_timestamp; boost::optional m_target; boost::optional m_target_height; - boost::optional m_top_block_hash; boost::optional m_num_txs; boost::optional m_num_txs_pool; boost::optional m_was_bootstrap_ever_used; boost::optional m_database_size; boost::optional m_update_available; - boost::optional m_credits; boost::optional m_is_busy_syncing; boost::optional m_is_synchronized; boost::optional m_is_restricted; static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& info); + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; struct monero_daemon_sync_info : public monero_rpc_payment_info { public: + boost::optional m_overview; boost::optional m_height; - std::vector m_peers; - std::vector m_spans; boost::optional m_target_height; boost::optional m_next_needed_pruning_seed; - boost::optional m_overview; - boost::optional m_credits; - boost::optional m_top_block_hash; + std::vector m_peers; + std::vector m_spans; static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& info); + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; struct monero_hard_fork_info : public monero_rpc_payment_info { public: - boost::optional m_earliest_height; boost::optional m_is_enabled; + boost::optional m_earliest_height; boost::optional m_state; boost::optional m_threshold; boost::optional m_version; boost::optional m_num_votes; boost::optional m_window; boost::optional m_voting; - boost::optional m_credits; - boost::optional m_top_block_hash; static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& info); + 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 5222fb5..0a18c5b 100644 --- a/src/cpp/daemon/py_monero_daemon_rpc.cpp +++ b/src/cpp/daemon/py_monero_daemon_rpc.cpp @@ -87,11 +87,10 @@ class monero_daemon_poller: public thread_poller { std::shared_ptr m_last_header; void announce_block_header(const std::shared_ptr& header) { - const auto& listeners = m_daemon->get_listeners(); - for (const auto& listener : listeners) { + auto listeners = m_daemon->get_listeners(); + for (auto& listener : listeners) { try { listener->on_block_header(header); - } catch (const std::exception& e) { MERROR("Error calling listener on new block header: " << e.what()); } @@ -128,20 +127,20 @@ monero_daemon_rpc::monero_daemon_rpc(const std::string& uri, const std::string& if (!uri.empty()) m_rpc->check_connection(); } -std::vector> monero_daemon_rpc::get_listeners() { +std::set monero_daemon_rpc::get_listeners() { boost::lock_guard lock(m_listeners_mutex); return m_listeners; } -void monero_daemon_rpc::add_listener(const std::shared_ptr &listener) { +void monero_daemon_rpc::add_listener(monero_daemon_listener &listener) { boost::lock_guard lock(m_listeners_mutex); - m_listeners.push_back(listener); + m_listeners.insert(&listener); refresh_listening(); } -void monero_daemon_rpc::remove_listener(const std::shared_ptr &listener) { +void monero_daemon_rpc::remove_listener(monero_daemon_listener &listener) { boost::lock_guard lock(m_listeners_mutex); - m_listeners.erase(std::remove_if(m_listeners.begin(), m_listeners.end(), [&listener](std::shared_ptr iter){ return iter == listener; }), m_listeners.end()); + m_listeners.erase(&listener); refresh_listening(); } @@ -752,7 +751,7 @@ std::shared_ptr monero_daemon_rpc::wait_for_next_bl bool ready = false; // create listener which notifies condition variable when block is added - auto block_listener = std::make_shared(&temp, &cv, &ready); + block_notifier block_listener(&temp, &cv, &ready); // register the listener add_listener(block_listener); @@ -765,7 +764,7 @@ std::shared_ptr monero_daemon_rpc::wait_for_next_bl remove_listener(block_listener); // return last height - return block_listener->m_last_header; + return block_listener.m_last_header; } std::shared_ptr monero_daemon_rpc::get_bandwidth_limits() { diff --git a/src/cpp/daemon/py_monero_daemon_rpc.h b/src/cpp/daemon/py_monero_daemon_rpc.h index 085b575..bbce1ac 100644 --- a/src/cpp/daemon/py_monero_daemon_rpc.h +++ b/src/cpp/daemon/py_monero_daemon_rpc.h @@ -73,9 +73,9 @@ class monero_daemon_rpc : public monero_daemon { /** * Supported daemon methods. */ - std::vector> get_listeners() override; - void add_listener(const std::shared_ptr &listener) override; - void remove_listener(const std::shared_ptr &listener) override; + std::set get_listeners() override; + void add_listener(monero_daemon_listener &listener) override; + void remove_listener(monero_daemon_listener &listener) override; void remove_listeners() override; std::shared_ptr get_rpc_connection() const; bool is_connected(); @@ -142,7 +142,7 @@ class monero_daemon_rpc : public monero_daemon { private: friend class monero_daemon_poller; mutable boost::recursive_mutex m_listeners_mutex; - std::vector> m_listeners; + std::set m_listeners; std::shared_ptr m_rpc; std::shared_ptr m_poller; std::unordered_map> m_cached_headers; diff --git a/src/cpp/py_monero.cpp b/src/cpp/py_monero.cpp index 68a1b57..4d2698c 100644 --- a/src/cpp/py_monero.cpp +++ b/src/cpp/py_monero.cpp @@ -96,7 +96,7 @@ PYBIND11_MODULE(monero, m) { auto py_monero_rpc_payment_info =py::class_>(m, "MoneroRpcPaymentInfo"); auto py_monero_rpc_connection = py::class_>(m, "MoneroRpcConnection"); - auto py_monero_ssl_options = py::class_(m, "SslOptions"); + auto py_monero_ssl_options = py::class_>(m, "SslOptions"); auto py_monero_version = py::class_>(m, "MoneroVersion"); auto py_monero_block_header = py::class_>(m, "MoneroBlockHeader"); auto py_monero_block = py::class_>(m, "MoneroBlock"); @@ -108,7 +108,7 @@ PYBIND11_MODULE(monero, m) { auto py_monero_sync_result = py::class_>(m, "MoneroSyncResult"); auto py_monero_account = py::class_>(m, "MoneroAccount"); auto py_monero_account_tag = py::class_>(m, "MoneroAccountTag"); - auto py_monero_destination = py::class_>(m, "MoneroDestination"); + auto py_monero_destination = py::class_>(m, "MoneroDestination"); auto py_monero_transfer = py::class_>(m, "MoneroTransfer"); auto py_monero_incoming_transfer = py::class_>(m, "MoneroIncomingTransfer"); auto py_monero_outgoing_transfer = py::class_>(m, "MoneroOutgoingTransfer"); @@ -119,17 +119,17 @@ PYBIND11_MODULE(monero, m) { auto py_monero_tx_query = py::class_>(m, "MoneroTxQuery"); auto py_monero_tx_set = py::class_>(m, "MoneroTxSet"); auto py_monero_integrated_address = py::class_>(m, "MoneroIntegratedAddress"); - auto py_monero_decoded_address = py::class_>(m, "MoneroDecodedAddress"); + auto py_monero_decoded_address = py::class_>(m, "MoneroDecodedAddress"); auto py_monero_tx_config = py::class_>(m, "MoneroTxConfig"); auto py_monero_key_image_import_result = py::class_>(m, "MoneroKeyImageImportResult"); auto py_monero_message_signature_result = py::class_>(m, "MoneroMessageSignatureResult"); auto py_monero_check = py::class_>(m, "MoneroCheck"); auto py_monero_check_tx = py::class_>(m, "MoneroCheckTx"); auto py_monero_check_reserve = py::class_>(m, "MoneroCheckReserve"); - auto py_monero_multisig_info = py::class_>(m, "MoneroMultisigInfo"); - auto py_monero_multisig_init_result = py::class_>(m, "MoneroMultisigInitResult"); - auto py_monero_multisig_sign_result = py::class_>(m, "MoneroMultisigSignResult"); - auto py_monero_address_book_entry = py::class_>(m, "MoneroAddressBookEntry"); + auto py_monero_multisig_info = py::class_>(m, "MoneroMultisigInfo"); + auto py_monero_multisig_init_result = py::class_>(m, "MoneroMultisigInitResult"); + auto py_monero_multisig_sign_result = py::class_>(m, "MoneroMultisigSignResult"); + auto py_monero_address_book_entry = py::class_>(m, "MoneroAddressBookEntry"); auto py_monero_wallet_listener = py::class_>(m, "MoneroWalletListener"); auto py_monero_daemon_listener = py::class_>(m, "MoneroDaemonListener"); auto py_monero_daemon = py::class_>(m, "MoneroDaemon"); @@ -330,7 +330,7 @@ PYBIND11_MODULE(monero, m) { .def_readwrite("ssl_allow_any_cert", &ssl_options::m_ssl_allow_any_cert); // monero_fee_estimate - py::class_>(m, "MoneroFeeEstimate") + py::class_>(m, "MoneroFeeEstimate") .def(py::init<>()) .def_readwrite("fee", &monero_fee_estimate::m_fee) .def_readwrite("fees", &monero_fee_estimate::m_fees) @@ -475,7 +475,7 @@ PYBIND11_MODULE(monero, m) { }, py::arg("other")); // monero_block_template - py::class_>(m, "MoneroBlockTemplate") + py::class_>(m, "MoneroBlockTemplate") .def(py::init<>()) .def_readwrite("block_template_blob", &monero_block_template::m_block_template_blob) .def_readwrite("block_hashing_blob", &monero_block_template::m_block_hashing_blob) @@ -489,7 +489,7 @@ PYBIND11_MODULE(monero, m) { .def_readwrite("next_seed_hash", &monero_block_template::m_next_seed_hash); // monero_connection_span - py::class_>(m, "MoneroConnectionSpan") + py::class_>(m, "MoneroConnectionSpan") .def(py::init<>()) .def_readwrite("connection_id", &monero_connection_span::m_connection_id) .def_readwrite("num_blocks", &monero_connection_span::m_num_blocks) @@ -500,7 +500,7 @@ PYBIND11_MODULE(monero, m) { .def_readwrite("start_height", &monero_connection_span::m_start_height); // monero_peer - py::class_>(m, "MoneroPeer") + py::class_>(m, "MoneroPeer") .def(py::init<>()) .def_readwrite("id", &monero_peer::m_id) .def_readwrite("address", &monero_peer::m_address) @@ -530,7 +530,7 @@ PYBIND11_MODULE(monero, m) { .def_readwrite("connection_type", &monero_peer::m_connection_type); // monero_alt_chain - py::class_>(m, "MoneroAltChain") + py::class_>(m, "MoneroAltChain") .def(py::init<>()) .def_readwrite("block_hashes", &monero_alt_chain::m_block_hashes) .def_readwrite("difficulty", &monero_alt_chain::m_difficulty) @@ -539,7 +539,7 @@ PYBIND11_MODULE(monero, m) { .def_readwrite("main_chain_parent_block_hash", &monero_alt_chain::m_main_chain_parent_block_hash); // monero_ban - py::class_>(m, "MoneroBan") + py::class_>(m, "MoneroBan") .def(py::init<>()) .def_readwrite("host", &monero_ban::m_host) .def_readwrite("ip", &monero_ban::m_ip) @@ -547,7 +547,7 @@ PYBIND11_MODULE(monero, m) { .def_readwrite("seconds", &monero_ban::m_seconds); // monero_output_distribution_entry - py::class_>(m, "MoneroOutputDistributionEntry") + py::class_>(m, "MoneroOutputDistributionEntry") .def(py::init<>()) .def_readwrite("amount", &monero_output_distribution_entry::m_amount) .def_readwrite("base", &monero_output_distribution_entry::m_base) @@ -555,7 +555,7 @@ PYBIND11_MODULE(monero, m) { .def_readwrite("start_height", &monero_output_distribution_entry::m_start_height); // monero_output_histogram_entry - py::class_>(m, "MoneroOutputHistogramEntry") + py::class_>(m, "MoneroOutputHistogramEntry") .def(py::init<>()) .def_readwrite("amount", &monero_output_histogram_entry::m_amount) .def_readwrite("num_instances", &monero_output_histogram_entry::m_num_instances) @@ -575,7 +575,7 @@ PYBIND11_MODULE(monero, m) { .def_readwrite("voting", &monero_hard_fork_info::m_voting); // monero_prune_result - py::class_>(m, "MoneroPruneResult") + py::class_>(m, "MoneroPruneResult") .def(py::init<>()) .def_readwrite("is_pruned", &monero_prune_result::m_is_pruned) .def_readwrite("pruning_seed", &monero_prune_result::m_pruning_seed); @@ -626,7 +626,7 @@ PYBIND11_MODULE(monero, m) { .def_readwrite("is_restricted", &monero_daemon_info::m_is_restricted); // monero_daemon_update_check_result - py::class_>(m, "MoneroDaemonUpdateCheckResult") + py::class_>(m, "MoneroDaemonUpdateCheckResult") .def(py::init<>()) .def_readwrite("is_update_available", &monero_daemon_update_check_result::m_is_update_available) .def_readwrite("version", &monero_daemon_update_check_result::m_version) @@ -675,7 +675,7 @@ PYBIND11_MODULE(monero, m) { .def_readwrite("histo", &monero_tx_pool_stats::m_histo, py::return_value_policy::reference_internal); // monero_mining_status - py::class_>(m, "MoneroMiningStatus") + py::class_>(m, "MoneroMiningStatus") .def(py::init<>()) .def_readwrite("is_active", &monero_mining_status::m_is_active) .def_readwrite("is_background", &monero_mining_status::m_is_background) @@ -684,7 +684,7 @@ PYBIND11_MODULE(monero, m) { .def_readwrite("num_threads", &monero_mining_status::m_num_threads); // monero_miner_tx_sum - py::class_>(m, "MoneroMinerTxSum") + py::class_>(m, "MoneroMinerTxSum") .def(py::init<>()) .def_readwrite("emission_sum", &monero_miner_tx_sum::m_emission_sum) .def_readwrite("fee_sum", &monero_miner_tx_sum::m_fee_sum); @@ -1262,10 +1262,10 @@ PYBIND11_MODULE(monero, m) { // monero_daemon py_monero_daemon .def(py::init<>()) - .def("add_listener", [](monero_daemon& self, const std::shared_ptr& listener) { + .def("add_listener", [](monero_daemon& self, monero_daemon_listener& listener) { MONERO_CATCH_AND_RETHROW(self.add_listener(listener)); }, py::arg("listener"), py::call_guard()) - .def("remove_listener", [](monero_daemon& self, const std::shared_ptr& listener) { + .def("remove_listener", [](monero_daemon& self, monero_daemon_listener& listener) { MONERO_CATCH_AND_RETHROW(self.remove_listener(listener)); }, py::arg("listener"), py::call_guard()) .def("get_listeners", [](monero_daemon& self) { @@ -2128,9 +2128,9 @@ PYBIND11_MODULE(monero, m) { .def_static("get_blocks_from_outputs", [](const std::vector>& outputs) { MONERO_CATCH_AND_RETHROW(PyMoneroUtils::get_blocks_from_outputs(outputs)); }, py::arg("outputs")) - .def_static("get_payment_uri", [](const monero::monero_tx_config &config) { - MONERO_CATCH_AND_RETHROW(PyMoneroUtils::get_payment_uri(config)); - }, py::arg("config")) + .def_static("get_payment_uri", [](const monero::monero_tx_config &config, monero_network_type network_type) { + MONERO_CATCH_AND_RETHROW(PyMoneroUtils::get_payment_uri(config, network_type)); + }, py::arg("config"), py::arg("network_type") = monero::monero_network_type::MAINNET) .def_static("xmr_to_atomic_units", [](double amount_xmr) { MONERO_CATCH_AND_RETHROW(PyMoneroUtils::xmr_to_atomic_units(amount_xmr)); }, py::arg("amount_xmr")) diff --git a/src/cpp/utils/py_monero_utils.cpp b/src/cpp/utils/py_monero_utils.cpp index c9933f2..9ccc015 100644 --- a/src/cpp/utils/py_monero_utils.cpp +++ b/src/cpp/utils/py_monero_utils.cpp @@ -245,7 +245,7 @@ std::vector> PyMoneroUtils::get_blocks_from_output return monero_utils::get_blocks_from_outputs(outputs); } -std::string PyMoneroUtils::get_payment_uri(const monero_tx_config& config) { +std::string PyMoneroUtils::get_payment_uri(const monero_tx_config& config, monero_network_type network_type) { // validate config std::vector> destinations = config.get_normalized_destinations(); if (destinations.size() != 1) throw std::runtime_error("Cannot make URI from supplied parameters: must provide exactly one destination to send funds"); @@ -260,7 +260,7 @@ std::string PyMoneroUtils::get_payment_uri(const monero_tx_config& config) { std::string m_recipient_name = config.m_recipient_name == boost::none ? "" : config.m_recipient_name.get(); // make uri - std::string uri = make_uri(address, payment_id, amount, note, m_recipient_name); + std::string uri = make_uri(address, payment_id, amount, note, m_recipient_name, network_type); if (uri.empty()) throw std::runtime_error("Cannot make URI from supplied parameters"); return uri; } @@ -280,52 +280,27 @@ bool PyMoneroUtils::is_hex_64(const std::string& value) { return std::regex_match(value, hexRegex); } -std::string PyMoneroUtils::make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name) { +std::string PyMoneroUtils::make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, monero_network_type network_type) { cryptonote::address_parse_info info; - if(!get_account_address_from_str(info, cryptonote::MAINNET, address)) - { - if(!get_account_address_from_str(info, cryptonote::TESTNET, address)) - { - if(!get_account_address_from_str(info, cryptonote::STAGENET, address)) - { - throw std::runtime_error(std::string("wrong address: ") + address); - } - } + if(!get_account_address_from_str(info, static_cast(network_type), address)) { + throw std::runtime_error(std::string("Invalid address: ") + address); } - - // we want only one payment id - if (info.has_payment_id && !payment_id.empty()) - { - throw std::runtime_error("A single payment id is allowed"); - } - - if (!payment_id.empty()) - { + if (!payment_id.empty()) { throw std::runtime_error("Standalone payment id deprecated, use integrated address instead"); } std::string uri = "monero:" + address; unsigned int n_fields = 0; - if (!payment_id.empty()) - { - uri += (n_fields++ ? "&" : "?") + std::string("tx_payment_id=") + payment_id; - } - - if (amount > 0) - { + if (amount > 0) { // URI encoded amount is in decimal units, not atomic units uri += (n_fields++ ? "&" : "?") + std::string("tx_amount=") + cryptonote::print_money(amount); } - - if (!recipient_name.empty()) - { + if (!recipient_name.empty()) { uri += (n_fields++ ? "&" : "?") + std::string("recipient_name=") + epee::net_utils::conver_to_url_format(recipient_name); } - - if (!tx_description.empty()) - { + if (!tx_description.empty()) { uri += (n_fields++ ? "&" : "?") + std::string("tx_description=") + epee::net_utils::conver_to_url_format(tx_description); } diff --git a/src/cpp/utils/py_monero_utils.h b/src/cpp/utils/py_monero_utils.h index 43652b5..5160d8b 100644 --- a/src/cpp/utils/py_monero_utils.h +++ b/src/cpp/utils/py_monero_utils.h @@ -94,7 +94,7 @@ class PyMoneroUtils { static std::vector> get_blocks_from_txs(std::vector> txs); static std::vector> get_blocks_from_transfers(std::vector> transfers); static std::vector> get_blocks_from_outputs(std::vector> outputs); - static std::string get_payment_uri(const monero_tx_config& config); + static std::string get_payment_uri(const monero_tx_config& config, monero_network_type network_type); static uint64_t xmr_to_atomic_units(double amount_xmr); static double atomic_units_to_xmr(uint64_t amount_atomic_units); @@ -105,5 +105,5 @@ class PyMoneroUtils { private: static bool is_hex_64(const std::string& value); - static std::string make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name); + static std::string make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, monero::monero_network_type network_type); }; diff --git a/src/cpp/wallet/py_monero_wallet_model.cpp b/src/cpp/wallet/py_monero_wallet_model.cpp index b580285..24fa3d3 100644 --- a/src/cpp/wallet/py_monero_wallet_model.cpp +++ b/src/cpp/wallet/py_monero_wallet_model.cpp @@ -248,7 +248,7 @@ void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tr outgoing_transfer = std::make_shared(); outgoing_transfer->m_tx = tx; } - else if (!*is_outgoing) { + else { if (incoming_transfer == nullptr) incoming_transfer = std::make_shared(); incoming_transfer->m_tx = tx; @@ -1108,6 +1108,23 @@ monero_decoded_address::monero_decoded_address(const std::string& address, moner m_network_type(network_type) { } +rapidjson::Value monero_decoded_address::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set string values + rapidjson::Value value_str(rapidjson::kStringType); + monero_utils::add_json_member("address", m_address, allocator, root, value_str); + + // set number values + rapidjson::Value value_num(rapidjson::kNumberType); + monero_utils::add_json_member("addressType", (uint8_t)m_address_type, allocator, root, value_num); + monero_utils::add_json_member("networkType", (uint8_t)m_network_type, allocator, root, value_num); + + // return root + return root; +} + // --------------------------- MONERO ACCOUNT TAG --------------------------- void monero_account_tag::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& account_tag) { @@ -1147,8 +1164,6 @@ rapidjson::Value monero_account_tag::to_rapidjson_val(rapidjson::Document::Alloc return root; } - - /** * ---------------- DUPLICATED MONERO-CPP WALLET FULL CODE --------------------- */ diff --git a/src/cpp/wallet/py_monero_wallet_model.h b/src/cpp/wallet/py_monero_wallet_model.h index 8fe903e..150389f 100644 --- a/src/cpp/wallet/py_monero_wallet_model.h +++ b/src/cpp/wallet/py_monero_wallet_model.h @@ -214,13 +214,15 @@ struct monero_output_comparator { bool operator()(const monero::monero_output_wallet& o1, const monero::monero_output_wallet& o2) const; }; -struct monero_decoded_address { +struct monero_decoded_address : public monero::serializable_struct { public: std::string m_address; monero_address_type m_address_type; monero::monero_network_type m_network_type; monero_decoded_address(const std::string& address, monero_address_type address_type, monero::monero_network_type network_type); + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; struct monero_account_tag : public monero::serializable_struct { diff --git a/src/cpp/wallet/py_monero_wallet_rpc.cpp b/src/cpp/wallet/py_monero_wallet_rpc.cpp index 32c44ba..a8ecb1b 100644 --- a/src/cpp/wallet/py_monero_wallet_rpc.cpp +++ b/src/cpp/wallet/py_monero_wallet_rpc.cpp @@ -492,7 +492,6 @@ std::string monero_wallet_rpc::get_address(const uint32_t account_idx, const uin std::vector empty_indices; get_subaddresses(account_idx, empty_indices, true); auto it3 = m_address_cache.find(account_idx); - if (it3 == m_address_cache.end()) throw std::runtime_error("Could not find account address at index (" + std::to_string(account_idx) + ", " + std::to_string(subaddress_idx) + ")" ); auto it4 = it3->second.find(subaddress_idx); if (it4 == it3->second.end()) return std::string(""); return it4->second; diff --git a/src/cpp/wallet/py_monero_wallet_rpc.h b/src/cpp/wallet/py_monero_wallet_rpc.h index 89a0063..136914b 100644 --- a/src/cpp/wallet/py_monero_wallet_rpc.h +++ b/src/cpp/wallet/py_monero_wallet_rpc.h @@ -235,7 +235,6 @@ class monero_wallet_rpc : public PyMoneroWallet { private: friend class monero_wallet_poller; - inline static const uint64_t DEFAULT_SYNC_PERIOD_IN_MS = 20000; boost::optional m_sync_period_in_ms; std::string m_path = ""; std::shared_ptr m_rpc; diff --git a/src/python/monero_alt_chain.pyi b/src/python/monero_alt_chain.pyi index 0373b9b..ae7c7b5 100644 --- a/src/python/monero_alt_chain.pyi +++ b/src/python/monero_alt_chain.pyi @@ -1,4 +1,7 @@ -class MoneroAltChain: +from .serializable_struct import SerializableStruct + + +class MoneroAltChain(SerializableStruct): """Models an alternative chain seen by the node.""" block_hashes: list[str] diff --git a/src/python/monero_ban.pyi b/src/python/monero_ban.pyi index 42c9529..31d0f71 100644 --- a/src/python/monero_ban.pyi +++ b/src/python/monero_ban.pyi @@ -1,4 +1,7 @@ -class MoneroBan: +from .serializable_struct import SerializableStruct + + +class MoneroBan(SerializableStruct): """Model a Monero banhammer.""" host: str | None diff --git a/src/python/monero_block_template.pyi b/src/python/monero_block_template.pyi index 3b4a59e..1fddd7d 100644 --- a/src/python/monero_block_template.pyi +++ b/src/python/monero_block_template.pyi @@ -1,4 +1,7 @@ -class MoneroBlockTemplate: +from .serializable_struct import SerializableStruct + + +class MoneroBlockTemplate(SerializableStruct): """Monero block template to mine.""" block_hashing_blob: str | None diff --git a/src/python/monero_connection_span.pyi b/src/python/monero_connection_span.pyi index f305cd2..8943e87 100644 --- a/src/python/monero_connection_span.pyi +++ b/src/python/monero_connection_span.pyi @@ -1,4 +1,7 @@ -class MoneroConnectionSpan: +from .serializable_struct import SerializableStruct + + +class MoneroConnectionSpan(SerializableStruct): """Monero daemon connection span.""" connection_id: str | None diff --git a/src/python/monero_daemon_update_check_result.pyi b/src/python/monero_daemon_update_check_result.pyi index 5980a94..b32ddcb 100644 --- a/src/python/monero_daemon_update_check_result.pyi +++ b/src/python/monero_daemon_update_check_result.pyi @@ -1,4 +1,7 @@ -class MoneroDaemonUpdateCheckResult: +from .serializable_struct import SerializableStruct + + +class MoneroDaemonUpdateCheckResult(SerializableStruct): """Models the result of checking for a daemon update.""" auto_uri: str | None diff --git a/src/python/monero_decoded_address.pyi b/src/python/monero_decoded_address.pyi index 4ad12fa..fb7f436 100644 --- a/src/python/monero_decoded_address.pyi +++ b/src/python/monero_decoded_address.pyi @@ -1,8 +1,9 @@ +from .serializable_struct import SerializableStruct from .monero_address_type import MoneroAddressType from .monero_network_type import MoneroNetworkType -class MoneroDecodedAddress: +class MoneroDecodedAddress(SerializableStruct): """Maintains metadata for a decoded address.""" address: str diff --git a/src/python/monero_destination.pyi b/src/python/monero_destination.pyi index 9d1f2c2..cc34598 100644 --- a/src/python/monero_destination.pyi +++ b/src/python/monero_destination.pyi @@ -1,7 +1,8 @@ import typing +from .serializable_struct import SerializableStruct -class MoneroDestination: +class MoneroDestination(SerializableStruct): """Models an outgoing transfer destination.""" address: str | None diff --git a/src/python/monero_fee_estimate.pyi b/src/python/monero_fee_estimate.pyi index a66f20d..c7d1d5e 100644 --- a/src/python/monero_fee_estimate.pyi +++ b/src/python/monero_fee_estimate.pyi @@ -1,4 +1,7 @@ -class MoneroFeeEstimate: +from .serializable_struct import SerializableStruct + + +class MoneroFeeEstimate(SerializableStruct): """Models a Monero fee estimate.""" fee: int | None diff --git a/src/python/monero_miner_tx_sum.pyi b/src/python/monero_miner_tx_sum.pyi index 24e6df5..83cae3e 100644 --- a/src/python/monero_miner_tx_sum.pyi +++ b/src/python/monero_miner_tx_sum.pyi @@ -1,4 +1,7 @@ -class MoneroMinerTxSum: +from .serializable_struct import SerializableStruct + + +class MoneroMinerTxSum(SerializableStruct): """Model for the sum of miner emissions and fees.""" emission_sum: int | None diff --git a/src/python/monero_mining_status.pyi b/src/python/monero_mining_status.pyi index 2229e7e..ee17844 100644 --- a/src/python/monero_mining_status.pyi +++ b/src/python/monero_mining_status.pyi @@ -1,4 +1,7 @@ -class MoneroMiningStatus: +from .serializable_struct import SerializableStruct + + +class MoneroMiningStatus(SerializableStruct): """Models a Monero daemon mining status.""" address: str | None diff --git a/src/python/monero_multisig_info.pyi b/src/python/monero_multisig_info.pyi index 3eb82ec..230ebfe 100644 --- a/src/python/monero_multisig_info.pyi +++ b/src/python/monero_multisig_info.pyi @@ -1,4 +1,7 @@ -class MoneroMultisigInfo: +from .serializable_struct import SerializableStruct + + +class MoneroMultisigInfo(SerializableStruct): """Models information about a multisig wallet.""" is_multisig: bool diff --git a/src/python/monero_multisig_init_result.pyi b/src/python/monero_multisig_init_result.pyi index 8e769b9..14dd245 100644 --- a/src/python/monero_multisig_init_result.pyi +++ b/src/python/monero_multisig_init_result.pyi @@ -1,4 +1,7 @@ -class MoneroMultisigInitResult: +from .serializable_struct import SerializableStruct + + +class MoneroMultisigInitResult(SerializableStruct): """ Models the result of initializing a multisig wallet which results in the multisig wallet's address xor another multisig hex to share with diff --git a/src/python/monero_multisig_sign_result.pyi b/src/python/monero_multisig_sign_result.pyi index d25a95c..6c219ea 100644 --- a/src/python/monero_multisig_sign_result.pyi +++ b/src/python/monero_multisig_sign_result.pyi @@ -1,4 +1,7 @@ -class MoneroMultisigSignResult: +from .serializable_struct import SerializableStruct + + +class MoneroMultisigSignResult(SerializableStruct): """Models the result of signing multisig tx hex.""" signed_multisig_tx_hex: str | None diff --git a/src/python/monero_output_distribution_entry.pyi b/src/python/monero_output_distribution_entry.pyi index 60fda5b..a745489 100644 --- a/src/python/monero_output_distribution_entry.pyi +++ b/src/python/monero_output_distribution_entry.pyi @@ -1,4 +1,7 @@ -class MoneroOutputDistributionEntry: +from .serializable_struct import SerializableStruct + + +class MoneroOutputDistributionEntry(SerializableStruct): """Models a Monero output distribution entry.""" amount: int | None diff --git a/src/python/monero_output_histogram_entry.pyi b/src/python/monero_output_histogram_entry.pyi index 064b4ce..211b435 100644 --- a/src/python/monero_output_histogram_entry.pyi +++ b/src/python/monero_output_histogram_entry.pyi @@ -1,4 +1,7 @@ -class MoneroOutputHistogramEntry: +from .serializable_struct import SerializableStruct + + +class MoneroOutputHistogramEntry(SerializableStruct): """Models a Monero output histogram entry.""" amount: int | None diff --git a/src/python/monero_peer.pyi b/src/python/monero_peer.pyi index 0167f2e..70e2d4d 100644 --- a/src/python/monero_peer.pyi +++ b/src/python/monero_peer.pyi @@ -1,7 +1,8 @@ +from .serializable_struct import SerializableStruct from .monero_connection_type import MoneroConnectionType -class MoneroPeer: +class MoneroPeer(SerializableStruct): """Models a peer to the daemon.""" address: str | None diff --git a/src/python/monero_prune_result.pyi b/src/python/monero_prune_result.pyi index 3ac7900..93cd4cd 100644 --- a/src/python/monero_prune_result.pyi +++ b/src/python/monero_prune_result.pyi @@ -1,4 +1,7 @@ -class MoneroPruneResult: +from .serializable_struct import SerializableStruct + + +class MoneroPruneResult(SerializableStruct): """Models the result of pruning the blockchain.""" is_pruned: bool | None diff --git a/src/python/monero_utils.pyi b/src/python/monero_utils.pyi index ba5c690..37ce8da 100644 --- a/src/python/monero_utils.pyi +++ b/src/python/monero_utils.pyi @@ -128,11 +128,12 @@ class MoneroUtils: ... @staticmethod - def get_payment_uri(config: MoneroTxConfig) -> str: + def get_payment_uri(config: MoneroTxConfig, network_type: MoneroNetworkType = MoneroNetworkType.MAINNET) -> str: """ Creates a payment URI from a tx configuration. :param MoneroTxConfig config: specifies configuration for a payment URI. + :param MoneroNetworkType network_type: address network type (optional). :returns str: the payment URI. """ ... diff --git a/src/python/monero_wallet_config.pyi b/src/python/monero_wallet_config.pyi index f4ddfb4..176a3b5 100644 --- a/src/python/monero_wallet_config.pyi +++ b/src/python/monero_wallet_config.pyi @@ -1,10 +1,11 @@ import typing +from .serializable_struct import SerializableStruct from .monero_network_type import MoneroNetworkType from .monero_rpc_connection import MoneroRpcConnection -class MoneroWalletConfig: +class MoneroWalletConfig(SerializableStruct): """Configures a wallet to create.""" account_lookahead: int | None diff --git a/tests/test_monero_common.py b/tests/test_monero_common.py index a736bca..066451f 100644 --- a/tests/test_monero_common.py +++ b/tests/test_monero_common.py @@ -1,8 +1,11 @@ import pytest import logging +from json import loads + from monero import ( - MoneroError, MoneroRpcError, SerializableStruct + SerializableStruct, SslOptions, + MoneroError, MoneroRpcError ) from utils import BaseTestClass @@ -32,5 +35,23 @@ def test_monero_error(self) -> None: # test serializable struct @pytest.mark.xfail(raises=TypeError, reason="Serializable struct is an abstract class") def test_serializable_struct(self) -> None: - ser_struct: SerializableStruct = SerializableStruct() - ser_struct.serialize() + SerializableStruct() + + def test_ssl_options(self) -> None: + ssl_options: SslOptions = SslOptions() + ssl_options.ssl_allow_any_cert = True + ssl_options.ssl_allowed_fingerprints = ["fingerprint1", "fingerprint2"] + ssl_options.ssl_ca_file = "ca_file" + ssl_options.ssl_certificate_path = "certificate_path" + ssl_options.ssl_private_key_path = "private_key_path" + logger.info(f"Testing ssl options: {ssl_options.serialize()}") + obj: dict[str, str] = loads(ssl_options.serialize()) + assert obj['sslAllowAnyCert'] == ssl_options.ssl_allow_any_cert + assert obj['sslCaFile'] == ssl_options.ssl_ca_file + assert obj['sslCertificatePath'] == ssl_options.ssl_certificate_path + assert obj['sslPrivateKeyPath'] == ssl_options.ssl_private_key_path + + allowed_fingerprints: list[str] = obj['sslAllowedFingerprints'] # type: ignore + + for i, allowed_fingerprint in enumerate(allowed_fingerprints): + assert allowed_fingerprint == ssl_options.ssl_allowed_fingerprints[i] diff --git a/tests/test_monero_daemon_rpc.py b/tests/test_monero_daemon_rpc.py index 986cac7..7f717e3 100644 --- a/tests/test_monero_daemon_rpc.py +++ b/tests/test_monero_daemon_rpc.py @@ -18,7 +18,7 @@ from utils import ( TestUtils as Utils, TestContext, BinaryBlockContext, - AssertUtils, TxUtils, + AssertUtils, TxUtils, OutputUtils, BlockUtils, GenUtils, DaemonUtils, WalletType, IntegrationTestUtils, @@ -89,6 +89,7 @@ def test_offline_daemon(self) -> None: @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_version(self, daemon: MoneroDaemonRpc) -> None: version: MoneroVersion = daemon.get_version() + logger.debug(f"Testing monero version: {version.serialize()}") assert version.number is not None assert version.number > 0 assert version.is_release is not None @@ -96,7 +97,8 @@ def test_get_version(self, daemon: MoneroDaemonRpc) -> None: # Can indicate if it's trusted @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_is_trusted(self, daemon: MoneroDaemonRpc) -> None: - daemon.is_trusted() + is_trusted: bool = daemon.is_trusted() + logger.debug(f"Trusted daemon: {is_trusted}") # Can get the blockchain height @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -117,7 +119,7 @@ def test_get_block_id_by_height(self, daemon: MoneroDaemonRpc) -> None: @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_block_template(self, daemon: MoneroDaemonRpc) -> None: template: MoneroBlockTemplate = daemon.get_block_template(Utils.ADDRESS, 2) - DaemonUtils.test_block_template(template) + BlockUtils.test_block_template(template) # Can get the last block's header @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -505,6 +507,7 @@ def test_get_miner_tx_sum(self, daemon: MoneroDaemonRpc) -> None: @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_fee_estimate(self, daemon: MoneroDaemonRpc) -> None: fee_estimate = daemon.get_fee_estimate() + logger.debug(f"Testing fee estimate: {fee_estimate.serialize()}") GenUtils.test_unsigned_big_integer(fee_estimate.fee, True) assert len(fee_estimate.fees) == 4, "Exptected 4 fees" for fee in fee_estimate.fees: @@ -567,7 +570,6 @@ def test_get_tx_pool_statistics(self, daemon: MoneroDaemonRpc, wallet: MoneroWal # get tx pool stats stats: MoneroTxPoolStats = daemon.get_tx_pool_stats() - logger.debug(f"Testing tx pool stats: {stats.serialize()}") assert stats.num_txs is not None assert stats.num_txs > i - 1 DaemonUtils.test_tx_pool_stats(stats) @@ -728,7 +730,7 @@ def test_get_output_histogram_binary(self, daemon: MoneroDaemonRpc) -> None: entries: list[MoneroOutputHistogramEntry] = daemon.get_output_histogram([], None, None, None, None) assert len(entries) > 0 for entry in entries: - DaemonUtils.test_output_histogram_entry(entry) + OutputUtils.test_output_histogram_entry(entry) # Can get an output distribution (binary) @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -737,7 +739,7 @@ def test_get_output_distribution_binary(self, daemon: MoneroDaemonRpc) -> None: amounts: list[int] = [0, 1, 10, 100, 1000, 10000, 100000, 1000000] entries: list[MoneroOutputDistributionEntry] = daemon.get_output_distribution(amounts) for entry in entries: - DaemonUtils.test_output_distribution_entry(entry) + OutputUtils.test_output_distribution_entry(entry) # Can get general information @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -762,7 +764,7 @@ def test_get_hard_fork_information(self, daemon: MoneroDaemonRpc) -> None: def test_get_alternative_chains(self, daemon: MoneroDaemonRpc) -> None: alt_chains: list[MoneroAltChain] = daemon.get_alt_chains() for alt_chain in alt_chains: - DaemonUtils.test_alt_chain(alt_chain) + BlockUtils.test_alt_chain(alt_chain) # Can get alternative block hashes @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -857,7 +859,7 @@ def test_block_listener(self, daemon: MoneroDaemonRpc, wallet: MoneroWalletRpc) # start mining if possible to help push the network along address: str = wallet.get_primary_address() try: - daemon.start_mining(address, 8, False, True) + daemon.start_mining(address, 1, False, True) except Exception as e: logger.warning(f"[!]: {str(e)}") @@ -946,7 +948,7 @@ def test_mining(self, daemon: MoneroDaemonRpc, wallet: MoneroWalletRpc) -> None: address: str = wallet.get_primary_address() # start mining - daemon.start_mining(address, 2, False, True) + daemon.start_mining(address, 1, False, True) # stop mining daemon.stop_mining() @@ -963,6 +965,7 @@ def test_get_mining_status(self, daemon: MoneroDaemonRpc, wallet: MoneroWalletRp # test status without mining status: MoneroMiningStatus = daemon.get_mining_status() + logger.debug(f"Testing mining status: {status.serialize()}") assert status.is_active is False assert status.address is None, f"Mining address is not None: {status.address}" assert 0 == status.speed @@ -971,10 +974,11 @@ def test_get_mining_status(self, daemon: MoneroDaemonRpc, wallet: MoneroWalletRp # test status with mining address: str = wallet.get_primary_address() - thread_count: int = 3 + thread_count: int = 1 is_background: bool = False daemon.start_mining(address, thread_count, is_background, True) status = daemon.get_mining_status() + logger.debug(f"Testing mining status: {status.serialize()}") assert status.speed is not None assert status.is_active is True assert address == status.address @@ -1010,6 +1014,7 @@ def test_submit_mined_block(self, daemon: MoneroDaemonRpc) -> None: @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_prune_blockchain(self, daemon: MoneroDaemonRpc) -> None: result: MoneroPruneResult = daemon.prune_blockchain(True) + logger.debug(f"Testing prune result: {result.serialize()}") if result.is_pruned: assert result.pruning_seed is not None diff --git a/tests/test_monero_rpc_connection.py b/tests/test_monero_rpc_connection.py index 263a5ed..803f769 100644 --- a/tests/test_monero_rpc_connection.py +++ b/tests/test_monero_rpc_connection.py @@ -1,9 +1,7 @@ import pytest import logging -from monero import ( - MoneroRpcConnection, MoneroConnectionType, MoneroRpcError -) +from monero import MoneroRpcConnection, MoneroConnectionType, MoneroRpcError from utils import TestUtils as Utils, RpcConnectionUtils, StringUtils, BaseTestClass logger: logging.Logger = logging.getLogger("TestMoneroRpcConnection") diff --git a/tests/test_monero_utils.py b/tests/test_monero_utils.py index eb24b68..2ba7af5 100644 --- a/tests/test_monero_utils.py +++ b/tests/test_monero_utils.py @@ -6,9 +6,9 @@ from typing import Any from configparser import ConfigParser from monero import ( - MoneroNetworkType, MoneroIntegratedAddress, MoneroUtils, MoneroTxConfig + MoneroNetworkType, MoneroIntegratedAddress, MoneroUtils, MoneroTxConfig, ) -from utils import AddressBook, KeysBook, WalletUtils, BaseTestClass +from utils import AddressBook, KeysBook, WalletUtils, BaseTestClass, WalletErrorUtils logger: logging.Logger = logging.getLogger("TestMoneroUtils") @@ -39,8 +39,7 @@ def parse(cls, parser: ConfigParser) -> TestMoneroUtils.Config: """ config = cls() # check section - if not parser.has_section("serialization"): - raise Exception("Cannot find section [serialization] in test_monero_utils.ini") + assert parser.has_section("serialization"), "Section [serialization] not found in test config" # load address books config.mainnet = AddressBook.parse(parser, "mainnet") config.testnet = AddressBook.parse(parser, "testnet") @@ -233,6 +232,7 @@ def test_key_validation(self, config: TestMoneroUtils.Config) -> None: # test private view key validation assert MoneroUtils.is_valid_private_view_key(config.keys.private_view_key) + MoneroUtils.validate_private_view_key(config.keys.private_view_key) WalletUtils.test_invalid_private_view_key("") WalletUtils.test_invalid_private_view_key(None) WalletUtils.test_invalid_private_view_key(config.keys.invalid_private_view_key) @@ -251,6 +251,7 @@ def test_key_validation(self, config: TestMoneroUtils.Config) -> None: # test public spend key validation assert MoneroUtils.is_valid_public_spend_key(config.keys.public_spend_key) + MoneroUtils.validate_public_spend_key(config.keys.public_spend_key) WalletUtils.test_invalid_public_spend_key("") WalletUtils.test_invalid_public_spend_key(None) WalletUtils.test_invalid_public_spend_key(config.keys.invalid_public_spend_key) @@ -328,19 +329,37 @@ def test_atomic_unit_conversion(self) -> None: # Can get payment uri def test_get_payment_uri(self, config: TestMoneroUtils.Config) -> None: address = config.mainnet.primary_address_1 - tx_config = MoneroTxConfig() - tx_config.address = address - tx_config.amount = 250000000000 - tx_config.recipient_name = "John Doe" - tx_config.note = "My transfer to wallet" + tx_config: MoneroTxConfig = WalletUtils.build_payment_uri_config(address) payment_uri = MoneroUtils.get_payment_uri(tx_config) + logger.debug(f"Testing payment uri: {payment_uri}") query = "tx_amount=0.250000000000&recipient_name=John%20Doe&tx_description=My%20transfer%20to%20wallet" assert payment_uri == f"monero:{address}?{query}" + # Test invalid payment uri address network type + def test_payment_uri_invalid_network_type(self, config: TestMoneroUtils.Config) -> None: + address = config.testnet.primary_address_1 + tx_config: MoneroTxConfig = WalletUtils.build_payment_uri_config(address) + try: + MoneroUtils.get_payment_uri(tx_config) + raise Exception("Should have failed") + except Exception as e: + WalletErrorUtils.test_invalid_address_error(e, address) + + # Test deprecated standalone payment id + def test_payment_uri_deprecated_payment_uri(self, config: TestMoneroUtils.Config) -> None: + address = config.testnet.primary_address_1 + tx_config: MoneroTxConfig = WalletUtils.build_payment_uri_config(address) + tx_config.payment_id = "03284e41c342f03603284e41c342f03603284e41c342f03603284e41c342f036" + try: + MoneroUtils.get_payment_uri(tx_config, MoneroNetworkType.TESTNET) + raise Exception("Should have failed") + except Exception as e: + WalletErrorUtils.test_deprecated_payment_id_error(e) + # Can get version def test_get_version(self) -> None: version = MoneroUtils.get_version() - assert version is not None, "Version is None" + logger.debug(f"Testing monero-python version: {version}") assert version != "", "Version is empty" # Can get ring size diff --git a/tests/test_monero_wallet_common.py b/tests/test_monero_wallet_common.py index 0ba986d..753e308 100644 --- a/tests/test_monero_wallet_common.py +++ b/tests/test_monero_wallet_common.py @@ -3274,7 +3274,7 @@ def test_mining(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: status = daemon.get_mining_status() if status.is_active: wallet.stop_mining() - wallet.start_mining(2, False, True) + wallet.start_mining(1, False, True) wallet.stop_mining() # Can change the wallet password diff --git a/tests/test_monero_wallet_full.py b/tests/test_monero_wallet_full.py index 1a8169a..c3cf678 100644 --- a/tests/test_monero_wallet_full.py +++ b/tests/test_monero_wallet_full.py @@ -66,8 +66,7 @@ def _create_wallet(self, config: Optional[MoneroWalletConfig], start_syncing: bo # create wallet wallet = MoneroWalletFull.create_wallet(config) if not random: - restore_height: int = 0 if config.restore_height is None else config.restore_height - assert restore_height == wallet.get_restore_height() + assert config.restore_height == wallet.get_restore_height() if start_syncing is not False and wallet.is_connected_to_daemon(): wallet.start_syncing(Utils.SYNC_PERIOD_IN_MS) return wallet @@ -469,10 +468,9 @@ def test_sync_wallet_from_keys(self, daemon: MoneroDaemonRpc, wallet: MoneroWall @pytest.mark.skipif(Utils.LITE_MODE, reason="LITE_MODE enabled") def test_start_stop_syncing(self, daemon: MoneroDaemonRpc) -> None: # test unconnected wallet - path: str = Utils.get_random_wallet_path() config: MoneroWalletConfig = MoneroWalletConfig() config.server = MoneroRpcConnection(Utils.OFFLINE_SERVER_URI) - config.path = path + config.path = Utils.get_random_wallet_path() wallet: MoneroWalletFull = self._create_wallet(config) try: assert len(wallet.get_seed()) > 0 @@ -480,15 +478,13 @@ def test_start_stop_syncing(self, daemon: MoneroDaemonRpc) -> None: assert wallet.get_balance() == 0 wallet.start_syncing() except Exception as e: - e_msg: str = str(e) - assert e_msg == "Wallet is not connected to daemon", e_msg + WalletErrorUtils.test_wallet_is_not_connected_error(e) finally: wallet.close() # test connecting wallet - path = Utils.get_random_wallet_path() config = MoneroWalletConfig() - config.path = path + config.path = Utils.get_random_wallet_path() config.server = MoneroRpcConnection(Utils.OFFLINE_SERVER_URI) wallet = self._create_wallet(config) try: @@ -511,9 +507,8 @@ def test_start_stop_syncing(self, daemon: MoneroDaemonRpc) -> None: # test that sync starts automatically restore_height: int = daemon.get_height() - 100 - path = Utils.get_random_wallet_path() config = MoneroWalletConfig() - config.path = path + config.path = Utils.get_random_wallet_path() config.seed = Utils.SEED config.restore_height = restore_height wallet = self._create_wallet(config, False) diff --git a/tests/test_monero_wallet_model.py b/tests/test_monero_wallet_model.py index de3824b..5c64b4f 100644 --- a/tests/test_monero_wallet_model.py +++ b/tests/test_monero_wallet_model.py @@ -1,8 +1,12 @@ import pytest import logging -from monero import MoneroTxQuery, MoneroTransferQuery, MoneroOutputQuery -from utils import BaseTestClass +from monero import ( + MoneroTxQuery, MoneroTransferQuery, MoneroOutputQuery, + MoneroWalletConfig, MoneroDestination, MoneroUtils, + MoneroTxConfig +) +from utils import BaseTestClass, TestUtils, AssertUtils logger: logging.Logger = logging.getLogger("TestMoneroWalletModel") @@ -159,4 +163,42 @@ def test_tx_query(self) -> None: assert tx_query.input_query != input_query assert input_query.tx_query is None + def test_destination(self) -> None: + amount: int = MoneroUtils.xmr_to_atomic_units(1) + dest: MoneroDestination = MoneroDestination(TestUtils.ADDRESS, amount) + logger.debug(f"Testing destination: {dest.serialize()}") + copy: MoneroDestination = dest.copy() + AssertUtils.assert_equals(dest, copy) + + @pytest.mark.xfail(raises=AssertionError, reason="TODO fix monero-cpp monero_rpc_connection default empty values") + def test_wallet_config(self) -> None: + config: MoneroWalletConfig = TestUtils.get_wallet_full_config(TestUtils.get_daemon_rpc_connection()) + logger.debug(f"Testing wallet config: {config.serialize()}") + copy: MoneroWalletConfig = config.copy() + AssertUtils.assert_equals(config, copy) + config_str: str = config.serialize() + deserialized_config: MoneroWalletConfig = MoneroWalletConfig.deserialize(config_str) + logger.debug(f"Deserialized config: {deserialized_config.serialize()}") + AssertUtils.assert_equals(config, deserialized_config) + + def test_tx_config(self) -> None: + config: MoneroTxConfig = MoneroTxConfig() + config.set_address(TestUtils.ADDRESS) + config.amount = MoneroUtils.xmr_to_atomic_units(0.5) + config.account_index = 0 + config.subaddress_indices = [i for i in range(10)] + config.below_amount = MoneroUtils.xmr_to_atomic_units(0.1) + config.can_split = True + config.fee = MoneroUtils.xmr_to_atomic_units(0.00075) + config.sweep_each_subaddress = False + + copy: MoneroTxConfig = config.copy() + AssertUtils.assert_equals(config, copy) + + config_str: str = config.serialize() + logger.debug(f"Serialized tx config: {config_str}") + + deserialized_config: MoneroTxConfig = MoneroTxConfig.deserialize(config_str) + AssertUtils.assert_equals(config, deserialized_config) + #endregion diff --git a/tests/test_monero_wallet_rpc.py b/tests/test_monero_wallet_rpc.py index 1240eda..1313418 100644 --- a/tests/test_monero_wallet_rpc.py +++ b/tests/test_monero_wallet_rpc.py @@ -82,6 +82,14 @@ def get_daemon_rpc_uri(self) -> str: #region Tests + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_offline_wallet(self) -> None: + offline_wallet: MoneroWalletRpc = MoneroWalletRpc(Utils.OFFLINE_SERVER_URI, Utils.WALLET_RPC_USERNAME, Utils.WALLET_PASSWORD) + try: + offline_wallet.is_view_only() + except Exception as e: + WalletErrorUtils.test_wallet_is_not_connected_error(e) + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_sync_progress(self, wallet: MoneroWalletRpc) -> None: listener: WalletNotificationCollector = WalletNotificationCollector() diff --git a/tests/utils/block_utils.py b/tests/utils/block_utils.py index 4e9a4ed..19418f2 100644 --- a/tests/utils/block_utils.py +++ b/tests/utils/block_utils.py @@ -3,9 +3,12 @@ from typing import Optional from abc import ABC from monero import ( - MoneroBlockHeader, MoneroBlock, MoneroDaemonRpc + MoneroAltChain, + MoneroBlockHeader, MoneroBlock, + MoneroBlockTemplate, MoneroDaemonRpc ) +from .gen_utils import GenUtils from .context import TestContext, BinaryBlockContext from .tx_utils import TxUtils @@ -16,14 +19,15 @@ class BlockUtils(ABC): """Block test utilities.""" @classmethod - def test_block_header(cls, header: Optional[MoneroBlockHeader], is_full: Optional[bool]) -> None: + def test_block_header(cls, header: MoneroBlockHeader, is_full: Optional[bool], debug: bool = True) -> None: """Test a block header. :param MoneroBlockHeader header: header to test. :param bool | None is_full: check full header. """ + if debug: + logger.debug(f"Testing block header: {header.serialize()}") # test base fields - assert header is not None assert header.height is not None assert header.height >= 0 assert header.major_version is not None @@ -54,6 +58,7 @@ def test_full_header(cls, header: MoneroBlockHeader, is_full: Optional[bool]) -> :param MoneroBlockHeader header: header to test full details. :param bool | None is_full: indicates if `header`'s full details should be defined. """ + logger.debug(f"Testing full header: {header.serialize()}") if is_full: # check full block assert header.size is not None @@ -88,18 +93,18 @@ def test_full_header(cls, header: MoneroBlockHeader, is_full: Optional[bool]) -> assert header.weight is None @classmethod - def test_block(cls, block: Optional[MoneroBlock], ctx: TestContext) -> None: + def test_block(cls, block: MoneroBlock, ctx: TestContext) -> None: """Test a block :param MoneroBlock | None block: block to test. :param TestContext ctx: test context. """ # test required fields - assert block is not None, "Expected MoneroBlock, got None" + logger.debug(f"Testing block: {block.serialize()}") assert block.miner_tx is not None, "Expected block miner tx" # TODO: miner tx doesn't have as much stuff, can't call TxUtils.test_tx? TxUtils.test_miner_tx(block.miner_tx) - cls.test_block_header(block, ctx.header_is_full) + cls.test_block_header(block, ctx.header_is_full, False) if ctx.has_hex: assert block.hex is not None @@ -172,3 +177,40 @@ def is_tx_in_block(cls, tx_hash: str | None, block: MoneroBlock) -> bool: return True return False + + @classmethod + def test_block_template(cls, template: MoneroBlockTemplate) -> None: + """Test a mining block template. + + :param MoneroBlockTemplate template: mining block template to test. + """ + logger.debug(f"Testing block template: {template.serialize()}") + assert template.block_template_blob is not None + assert template.block_hashing_blob is not None + assert template.difficulty is not None + assert template.expected_reward is not None + assert template.height is not None + assert template.prev_hash is not None + assert template.reserved_offset is not None + assert template.seed_height is not None + assert template.seed_height is not None + assert template.seed_height >= 0 + assert template.seed_hash is not None + assert len(template.seed_hash) > 0 + # next seed hash can be null or initialized TODO: test circumstances for each + + @classmethod + def test_alt_chain(cls, alt_chain: MoneroAltChain) -> None: + """Test daemon alternative chain info. + + :param MoneroAltChain alt_chain: alternative chain info to test. + """ + logger.debug(f"Testing alternative chain: {alt_chain.serialize()}") + assert len(alt_chain.block_hashes) > 0 + GenUtils.test_unsigned_big_integer(alt_chain.difficulty, True) + assert alt_chain.height is not None + assert alt_chain.length is not None + assert alt_chain.main_chain_parent_block_hash is not None + assert alt_chain.height > 0 + assert alt_chain.length > 0 + assert 64 == len(alt_chain.main_chain_parent_block_hash) diff --git a/tests/utils/daemon_utils.py b/tests/utils/daemon_utils.py index dcab507..655b4f0 100644 --- a/tests/utils/daemon_utils.py +++ b/tests/utils/daemon_utils.py @@ -3,13 +3,11 @@ from abc import ABC from monero import ( MoneroDaemon, MoneroPeer, MoneroDaemonInfo, MoneroDaemonSyncInfo, - MoneroConnectionSpan, MoneroHardForkInfo, - MoneroAltChain, MoneroBan, MoneroMinerTxSum, - MoneroTxPoolStats, MoneroBlockTemplate, + MoneroConnectionSpan, MoneroHardForkInfo, MoneroBlock, + MoneroBan, MoneroMinerTxSum, MoneroTx, MoneroTxPoolStats, MoneroDaemonUpdateCheckResult, MoneroDaemonUpdateDownloadResult, MoneroNetworkType, MoneroSubmitTxResult, - MoneroKeyImageSpentStatus, MoneroDaemonRpc, MoneroTx, - MoneroBlock, MoneroOutputHistogramEntry, MoneroOutputDistributionEntry + MoneroKeyImageSpentStatus, MoneroDaemonRpc, ) from .gen_utils import GenUtils @@ -20,22 +18,6 @@ class DaemonUtils(ABC): """Daemon test utilities.""" - @classmethod - def network_type_to_str(cls, nettype: MoneroNetworkType) -> str: - """Convert network type enum to string. - - :param MoneroNetworkType nettype: network type to convert to string. - :returns str: network type in string format. - """ - if nettype == MoneroNetworkType.MAINNET: - return "mainnet" - elif nettype == MoneroNetworkType.TESTNET: - return "testnet" - elif nettype == MoneroNetworkType.STAGENET: - return "stagenet" - - raise TypeError(f"Invalid network type provided: {str(nettype)}") - @classmethod def is_regtest(cls, network_type_str: str | None) -> bool: """Check if network type string indicates `regtest` network. @@ -68,14 +50,15 @@ def parse_network_type(cls, nettype: str) -> MoneroNetworkType: # region Test Utils @classmethod - def test_known_peer(cls, peer: MoneroPeer | None, from_connection: bool) -> None: + def test_known_peer(cls, peer: MoneroPeer, from_connection: bool, debug: bool = True) -> None: """Test known daemon peer. - :param MoneroPeer | None peer: daemon peer to test. + :param MoneroPeer peer: daemon peer to test. :param bool from_connection: indicates if `peer` is obtained from daemon connections. """ + if debug: + logger.debug(f"Testing known peer: {peer.serialize()}") # common peer validation - assert peer is not None, "Peer is null" assert peer.id is not None assert peer.host is not None assert peer.port is not None @@ -97,13 +80,13 @@ def test_known_peer(cls, peer: MoneroPeer | None, from_connection: bool) -> None assert peer.last_seen_timestamp >= 0, f"Last seen timestamp is invalid: {peer.last_seen_timestamp}" @classmethod - def test_peer(cls, peer: MoneroPeer | None) -> None: + def test_peer(cls, peer: MoneroPeer) -> None: """Test daemon connection peer. - :param MoneroPeer | None peer: peer connection to test. + :param MoneroPeer peer: peer connection to test. """ - assert peer is not None - cls.test_known_peer(peer, True) + logger.debug(f"Testing peer: {peer.serialize()}") + cls.test_known_peer(peer, True, False) assert peer.hash is not None assert peer.avg_download is not None assert peer.avg_upload is not None @@ -140,95 +123,81 @@ def test_info(cls, info: MoneroDaemonInfo) -> None: :param MoneroDaemonInfo info: daemon info to test. """ - assert info.num_alt_blocks is not None - assert info.block_size_limit is not None - assert info.block_size_median is not None - assert info.num_offline_peers is not None - assert info.num_online_peers is not None - assert info.height is not None - assert info.height_without_bootstrap is not None - assert info.num_incoming_connections is not None - assert info.num_outgoing_connections is not None - assert info.num_rpc_connections is not None - assert info.start_timestamp is not None - assert info.adjusted_timestamp is not None - assert info.target is not None - assert info.target_height is not None - assert info.num_txs is not None - assert info.num_txs_pool is not None - assert info.block_weight_limit is not None - assert info.block_weight_median is not None - assert info.database_size is not None + logger.debug(f"Testing daemon info: {info.serialize()}") assert info.version is not None - assert info.num_alt_blocks >= 0 - assert info.block_size_limit > 0 - assert info.block_size_median > 0 + assert info.num_alt_blocks is not None and info.num_alt_blocks >= 0 + assert info.block_size_limit is not None and info.block_size_limit > 0 + assert info.block_size_median is not None and info.block_size_median > 0 assert info.bootstrap_daemon_address is None or len(info.bootstrap_daemon_address) > 0 GenUtils.test_unsigned_big_integer(info.cumulative_difficulty) GenUtils.test_unsigned_big_integer(info.free_space) - assert info.num_offline_peers >= 0 - assert info.num_online_peers >= 0 - assert info.height >= 0 - assert info.height_without_bootstrap > 0 - assert info.num_incoming_connections >= 0 + assert info.num_offline_peers is not None and info.num_offline_peers >= 0 + assert info.num_online_peers is not None and info.num_online_peers >= 0 + assert info.height is not None and info.height >= 0 + assert info.height_without_bootstrap is not None and info.height_without_bootstrap > 0 + assert info.num_incoming_connections is not None and info.num_incoming_connections >= 0 + assert info.num_outgoing_connections is not None and info.num_outgoing_connections >= 0 assert info.network_type is not None assert info.is_offline is not None - assert info.num_outgoing_connections >= 0 - assert info.num_rpc_connections >= 0 - assert info.start_timestamp > 0 - assert info.adjusted_timestamp > 0 - assert info.target > 0 - assert info.target_height >= 0 - assert info.num_txs >= 0 - assert info.num_txs_pool >= 0 + assert info.num_rpc_connections is not None and info.num_rpc_connections >= 0 + assert info.start_timestamp is not None and info.start_timestamp > 0 + assert info.adjusted_timestamp is not None and info.adjusted_timestamp > 0 + assert info.target is not None and info.target > 0 + assert info.target_height is not None and info.target_height >= 0 + assert info.num_txs is not None and info.num_txs >= 0 + assert info.num_txs_pool is not None and info.num_txs_pool >= 0 assert info.was_bootstrap_ever_used is not None - assert info.block_weight_limit > 0 - assert info.block_weight_median > 0 - assert info.database_size > 0 + assert info.block_weight_limit is not None and info.block_weight_limit > 0 + assert info.block_weight_median is not None and info.block_weight_median > 0 + assert info.database_size is not None and info.database_size > 0 assert info.update_available is not None - GenUtils.test_unsigned_big_integer(info.credits, False) # 0 credits + # 0 credits + GenUtils.test_unsigned_big_integer(info.credits, False) assert info.top_block_hash is not None assert len(info.top_block_hash) > 0 assert info.is_busy_syncing is not None assert info.is_synchronized is not None @classmethod - def test_sync_info(cls, sync_info: MoneroDaemonSyncInfo | None) -> None: + def test_connection_span(cls, span: MoneroConnectionSpan) -> None: + """Test daemon connection span. + + :param MoneroConnectionSpan span: daemon connection span to test. + """ + logger.debug(f"Testing connection span: {span.serialize()}") + raise NotImplementedError("DaemonUtils.test_connection_span(): not implemented") + + @classmethod + def test_sync_info(cls, sync_info: MoneroDaemonSyncInfo) -> None: """Test daemon synchronization info. - :param MoneroDaemonSyncInfo | None sync_info: daemon sync info to test. + :param MoneroDaemonSyncInfo sync_info: daemon sync info to test. """ - assert sync_info is not None - assert sync_info.height is not None - assert sync_info.height >= 0 + logger.debug(f"Testing daemon sync info: {sync_info.serialize()}") + assert sync_info.height is not None and sync_info.height >= 0 + # test peers for connection in sync_info.peers: cls.test_peer(connection) + # test connection spans for span in sync_info.spans: cls.test_connection_span(span) assert sync_info.next_needed_pruning_seed is not None assert sync_info.next_needed_pruning_seed >= 0 assert sync_info.overview is None - GenUtils.test_unsigned_big_integer(sync_info.credits, False) # 0 credits + # 0 credits + GenUtils.test_unsigned_big_integer(sync_info.credits, False) assert sync_info.top_block_hash is None - @classmethod - def test_connection_span(cls, span: MoneroConnectionSpan | None) -> None: - """Test daemon connection span. - - :param MoneroConnectionSpan | None span: daemon connection span to test. - """ - assert span is not None - raise NotImplementedError("DaemonUtils.test_connection_span(): not implemented") - @classmethod def test_hard_fork_info(cls, hard_fork_info: MoneroHardForkInfo) -> None: """Test daemon hard fork information. :param MoneroHardForkInfo hard_fork_info: daemon hard fork information to test. """ + logger.debug(f"Testing hard fork info: {hard_fork_info.serialize()}") assert hard_fork_info.earliest_height is not None assert hard_fork_info.is_enabled is not None assert hard_fork_info.state is not None @@ -237,78 +206,54 @@ def test_hard_fork_info(cls, hard_fork_info: MoneroHardForkInfo) -> None: assert hard_fork_info.num_votes is not None assert hard_fork_info.voting is not None assert hard_fork_info.window is not None - GenUtils.test_unsigned_big_integer(hard_fork_info.credits, False) # 0 credits + # 0 credits + GenUtils.test_unsigned_big_integer(hard_fork_info.credits, False) assert hard_fork_info.top_block_hash is None @classmethod - def test_alt_chain(cls, alt_chain: MoneroAltChain | None) -> None: - """Test daemon alternative chain info. - - :param MoneroAltChain | None alt_chain: alternative chain info to test. - """ - assert alt_chain is not None - assert len(alt_chain.block_hashes) > 0 - GenUtils.test_unsigned_big_integer(alt_chain.difficulty, True) - assert alt_chain.height is not None - assert alt_chain.length is not None - assert alt_chain.main_chain_parent_block_hash is not None - assert alt_chain.height > 0 - assert alt_chain.length > 0 - assert 64 == len(alt_chain.main_chain_parent_block_hash) - - @classmethod - def test_ban(cls, ban: MoneroBan | None) -> None: + def test_ban(cls, ban: MoneroBan) -> None: """Test daemon ban. - :param MoneroBan | None ban: daemon ban to test. + :param MoneroBan ban: daemon ban to test. """ - assert ban is not None + logger.debug(f"Testing ban: {ban.serialize()}") assert ban.host is not None assert ban.ip is not None assert ban.seconds is not None @classmethod - def test_miner_tx_sum(cls, tx_sum: MoneroMinerTxSum | None) -> None: + def test_miner_tx_sum(cls, tx_sum: MoneroMinerTxSum) -> None: """Test miner tx sum result. - :param MoneroMinerTxSum | None tx_sum: miner tx sum to test. + :param MoneroMinerTxSum tx_sum: miner tx sum to test. """ - assert tx_sum is not None + logger.debug(f"Testing tx sum: {tx_sum.serialize()}") 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: MoneroTxPoolStats | None) -> None: + def test_tx_pool_stats(cls, stats: MoneroTxPoolStats) -> None: """Test daemon tx pool statistics. - :param MoneroTxPoolStats | None stats: daemon tx pool statistics to test. + :param MoneroTxPoolStats stats: daemon tx pool statistics to test. """ + logger.debug(f"Testing tx pool stats: {stats.serialize()}") assert stats is not None assert stats.num_txs is not None assert stats.num_txs >= 0 if stats.num_txs > 0: # TODO test stats.histo - assert stats.bytes_max is not None - assert stats.bytes_med is not None - assert stats.bytes_min is not None - assert stats.bytes_total is not None - assert stats.oldest_timestamp is not None - assert stats.num10m is not None - assert stats.num_double_spends is not None - assert stats.num_failing is not None - assert stats.num_not_relayed is not None - - assert stats.bytes_max > 0 - assert stats.bytes_med > 0 - assert stats.bytes_min > 0 - assert stats.bytes_total > 0 + assert stats.bytes_max is not None and stats.bytes_max > 0 + assert stats.bytes_med is not None and stats.bytes_med > 0 + assert stats.bytes_min is not None and stats.bytes_min > 0 + assert stats.bytes_total is not None and stats.bytes_total > 0 + assert stats.oldest_timestamp is not None and stats.oldest_timestamp > 0 assert stats.histo98pc is None or stats.histo98pc > 0 - assert stats.oldest_timestamp > 0 - assert stats.num10m >= 0 - assert stats.num_double_spends >= 0 - assert stats.num_failing >= 0 - assert stats.num_not_relayed >= 0 + assert stats.num10m is not None and stats.num10m >= 0 + assert stats.num_double_spends is not None and stats.num_double_spends >= 0 + assert stats.num_failing is not None and stats.num_failing >= 0 + assert stats.num_not_relayed is not None and stats.num_not_relayed >= 0 else: assert stats.bytes_max is None @@ -324,33 +269,13 @@ def test_tx_pool_stats(cls, stats: MoneroTxPoolStats | None) -> None: assert len(stats.histo.values()) == 0 @classmethod - def test_block_template(cls, template: MoneroBlockTemplate | None) -> None: - """Test a mining block template. - - :param MoneroBlockTemplate | None template: mining block template to test. - """ - assert template is not None - assert template.block_template_blob is not None - assert template.block_hashing_blob is not None - assert template.difficulty is not None - assert template.expected_reward is not None - assert template.height is not None - assert template.prev_hash is not None - assert template.reserved_offset is not None - assert template.seed_height is not None - assert template.seed_height is not None - assert template.seed_height >= 0 - assert template.seed_hash is not None - assert len(template.seed_hash) > 0 - # next seed hash can be null or initialized TODO: test circumstances for each - - @classmethod - def test_update_check_result(cls, result: MoneroDaemonUpdateCheckResult | None) -> None: + def test_update_check_result(cls, result: MoneroDaemonUpdateCheckResult, debug: bool = True) -> None: """Test daemon update check result. - :param MoneroDaemonUpdateCheckResult | None result: daemon update check result to test. + :param MoneroDaemonUpdateCheckResult result: daemon update check result to test. """ - assert result is not None + if debug: + logger.debug(f"Testing update check result: {result.serialize()}") assert isinstance(result, MoneroDaemonUpdateCheckResult) assert result.is_update_available is not None if result.is_update_available: @@ -378,9 +303,10 @@ def test_update_download_result(cls, result: MoneroDaemonUpdateDownloadResult, p """Test daemon update download result. :param MoneroDaemonUpdateDownloadResult result: daemon update download result to test. - :param str | None: expected download path in result. + :param str | None path: expected download path in result. """ - cls.test_update_check_result(result) + logger.debug(f"Testing update download result: {result.serialize()}") + cls.test_update_check_result(result, False) if result.is_update_available: if result.download_path is None: # TODO monero-project daemon returning empty status string on download update error @@ -412,12 +338,12 @@ def test_submit_tx_result_common(cls, result: MoneroSubmitTxResult) -> None: assert result.reason is None or len(result.reason) > 0 @classmethod - def test_submit_tx_result_good(cls, result: MoneroSubmitTxResult | None) -> None: + def test_submit_tx_result_good(cls, result: MoneroSubmitTxResult) -> None: """Test succesfull daemon submit tx result. - :param MoneroSubmitTxResult | None result: daemon submit tx result to test. + :param MoneroSubmitTxResult result: daemon submit tx result to test. """ - assert result is not None + logger.debug(f"Testing valid submit tx result: {result.serialize()}") cls.test_submit_tx_result_common(result) # test good tx submission assert result.is_double_spend is False, "tx submission is double spend." @@ -437,12 +363,12 @@ def test_submit_tx_result_good(cls, result: MoneroSubmitTxResult | None) -> None assert result.is_nonzero_unlock_time is False, "tx has non-zero unlock time." @classmethod - def test_submit_tx_result_double_spend(cls, result: MoneroSubmitTxResult | None) -> None: + def test_submit_tx_result_double_spend(cls, result: MoneroSubmitTxResult) -> None: """Test double spend daemon submit tx result. - :param MoneroSubmitTxResult | None result: daemon submit tx result to test. + :param MoneroSubmitTxResult result: daemon submit tx result to test. """ - assert result is not None + logger.debug(f"Testing double spend submit tx result: {result.serialize()}") cls.test_submit_tx_result_common(result) assert result.is_good is False assert result.is_double_spend is True @@ -474,35 +400,6 @@ def test_spent_statuses(cls, daemon: MoneroDaemonRpc, key_images: list[str], exp for status in statuses: assert status == expected_status - @classmethod - def test_output_distribution_entry(cls, entry: MoneroOutputDistributionEntry | None) -> None: - """Test daemon output distribution entry. - - :param MoneroOutputDistributionEntry | None entry: daemon output distribution entry to test. - """ - assert entry is not None - GenUtils.test_unsigned_big_integer(entry.amount) - assert entry.base is not None - assert entry.base >= 0 - assert len(entry.distribution) > 0 - assert entry.start_height is not None - assert entry.start_height >= 0 - - @classmethod - def test_output_histogram_entry(cls, entry: MoneroOutputHistogramEntry | None) -> None: - """Test daemon output histogram entry. - - :param MoneroOutputHistogramEntry | None entry: daemon output histogram entry to test. - """ - assert entry is not None - GenUtils.test_unsigned_big_integer(entry.amount) - assert entry.num_instances is not None - assert entry.num_instances >= 0 - assert entry.unlocked_instances is not None - assert entry.unlocked_instances >= 0 - assert entry.recent_instances is not None - assert entry.recent_instances >= 0 - @classmethod def get_confirmed_txs(cls, daemon: MoneroDaemonRpc, num_txs: int) -> list[MoneroTx]: """ diff --git a/tests/utils/docker_wallet_rpc_manager.py b/tests/utils/docker_wallet_rpc_manager.py index 6a7d94d..afef702 100644 --- a/tests/utils/docker_wallet_rpc_manager.py +++ b/tests/utils/docker_wallet_rpc_manager.py @@ -228,13 +228,6 @@ def open_wallet(self, c: MoneroWalletConfig | None, in_container: bool) -> Moner """ return self.setup_wallet(c, False, in_container) - def get_wallets(self) -> list[MoneroWalletRpc]: - """Get all active wallet rpc instances. - - :returns list[MoneroWalletRpc]: rpc wallet instances. - """ - return list(self._wallets.values()) - def is_docker_instance(self, wallet: MoneroWallet) -> bool: """Check if wallet is a managed docker instance. @@ -277,6 +270,7 @@ def clear(self, save: bool = False) -> None: """ for wallet in self._wallets.values(): if not wallet.is_closed(): + rpc_connection = wallet.get_rpc_connection() try: wallet.close(save) except Exception as e: @@ -284,6 +278,6 @@ def clear(self, save: bool = False) -> None: if "No wallet file" != e_str: raise - logger.debug("Free wallet rpc instance") + logger.debug(f"Closed docker wallet rpc: {rpc_connection.uri if rpc_connection is not None else 'None'}") self._wallets.clear() diff --git a/tests/utils/output_utils.py b/tests/utils/output_utils.py index 21a911d..9220c6f 100644 --- a/tests/utils/output_utils.py +++ b/tests/utils/output_utils.py @@ -5,7 +5,8 @@ from monero import ( MoneroWallet, MoneroOutputQuery, - MoneroOutput, MoneroKeyImage, MoneroOutputWallet + MoneroOutput, MoneroKeyImage, MoneroOutputWallet, + MoneroOutputDistributionEntry, MoneroOutputHistogramEntry ) from .gen_utils import GenUtils @@ -25,11 +26,41 @@ def test_key_image(cls, image: Optional[MoneroKeyImage], context: Optional[TestC :param TestContext context: test context (default `None`). """ assert image is not None + logger.debug(f"Testing key image: {image.serialize()}") assert image.hex is not None assert len(image.hex) > 0 if image.signature is not None: assert len(image.signature) > 0 + @classmethod + def test_output_distribution_entry(cls, entry: MoneroOutputDistributionEntry) -> None: + """Test daemon output distribution entry. + + :param MoneroOutputDistributionEntry entry: daemon output distribution entry to test. + """ + logger.debug(f"Testing output distribution entry: {entry.serialize()}") + GenUtils.test_unsigned_big_integer(entry.amount) + assert entry.base is not None + assert entry.base >= 0 + assert len(entry.distribution) > 0 + assert entry.start_height is not None + assert entry.start_height >= 0 + + @classmethod + def test_output_histogram_entry(cls, entry: MoneroOutputHistogramEntry) -> None: + """Test daemon output histogram entry. + + :param MoneroOutputHistogramEntry entry: daemon output histogram entry to test. + """ + logger.debug(f"Testing output histogram entry: {entry.serialize()}") + GenUtils.test_unsigned_big_integer(entry.amount) + assert entry.num_instances is not None + assert entry.num_instances >= 0 + assert entry.unlocked_instances is not None + assert entry.unlocked_instances >= 0 + assert entry.recent_instances is not None + assert entry.recent_instances >= 0 + @classmethod def test_output(cls, output: Optional[MoneroOutput], context: Optional[TestContext] = None) -> None: """Test monero output. diff --git a/tests/utils/sync_seed_tester.py b/tests/utils/sync_seed_tester.py index 6571fe2..853d837 100644 --- a/tests/utils/sync_seed_tester.py +++ b/tests/utils/sync_seed_tester.py @@ -37,15 +37,16 @@ class SyncSeedTester: create_wallet: Callable[[MoneroWalletConfig, bool], MoneroWalletFull] """Create wallet function.""" - def __init__(self, - daemon: MoneroDaemonRpc, - wallet: MoneroWalletFull, - create_wallet: Callable[[MoneroWalletConfig, bool], MoneroWalletFull], - start_height: Optional[int], - restore_height: Optional[int], - skip_gt_comparison: bool = False, - test_post_sync_notifications: bool = False - ) -> None: + def __init__( + self, + daemon: MoneroDaemonRpc, + wallet: MoneroWalletFull, + create_wallet: Callable[[MoneroWalletConfig, bool], MoneroWalletFull], + start_height: Optional[int], + restore_height: Optional[int], + skip_gt_comparison: bool = False, + test_post_sync_notifications: bool = False + ) -> None: """Initialize a new sync seed tester. :param MoneroDaemonRpc daemon: daemon test instance. @@ -64,6 +65,26 @@ def __init__(self, self.skip_gt_comparison = skip_gt_comparison self.test_post_sync_notifications = test_post_sync_notifications + def test_post_sync(self, wallet: MoneroWalletFull, wallet_sync_tester: WalletSyncTester) -> None: + # start automatic syncing + wallet.start_syncing(TestUtils.SYNC_PERIOD_IN_MS) + + # attempt to start mining to push the network along + MiningUtils.try_start_mining() + + try: + logger.info("Waiting for next block to test post sync notifications") + self.daemon.wait_for_next_block_header() + + # ensure wallet has time to detect new block + sleep((TestUtils.SYNC_PERIOD_IN_MS / 1000) + 3) + + # test that wallet listener's onSyncProgress() and onNewBlock() were invoked after previous completion + assert wallet_sync_tester.on_sync_progress_after_done + assert wallet_sync_tester.on_new_block_after_done + finally: + MiningUtils.try_stop_mining() + def test_notifications(self, wallet: MoneroWalletFull, start_height_expected: int, end_height_expected: int) -> None: """Test wallet notifications. @@ -127,24 +148,7 @@ def test_notifications(self, wallet: MoneroWalletFull, start_height_expected: in # if testing post-sync notifications, wait for a block to be added to the chain # then test that sync arg listener was not invoked and registered wallet listener was invoked if self.test_post_sync_notifications: - # start automatic syncing - wallet.start_syncing(TestUtils.SYNC_PERIOD_IN_MS) - - # attempt to start mining to push the network along - MiningUtils.try_start_mining() - - try: - logger.info("Waiting for next block to test post sync notifications") - self.daemon.wait_for_next_block_header() - - # ensure wallet has time to detect new block - sleep((TestUtils.SYNC_PERIOD_IN_MS / 1000) + 3) - - # test that wallet listener's onSyncProgress() and onNewBlock() were invoked after previous completion - assert wallet_sync_tester.on_sync_progress_after_done - assert wallet_sync_tester.on_new_block_after_done - finally: - MiningUtils.try_stop_mining() + self.test_post_sync(wallet, wallet_sync_tester) def test(self) -> None: """Run sync seed test.""" diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index 1e6513f..56a403e 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -3,8 +3,7 @@ from time import time from typing import Optional from abc import ABC -from os.path import exists as path_exists -from os import makedirs, getenv +from os import getenv from configparser import ConfigParser from monero import ( MoneroNetworkType, MoneroWalletFull, MoneroRpcConnection, @@ -251,32 +250,11 @@ def load(cls) -> None: ) cls.RPC_WALLET_MANAGER.set_connection_credentials(cls.WALLET_RPC_USERNAME, cls.WALLET_RPC_PASSWORD) - @classmethod - def get_network_type(cls) -> str: - """Get test network type. - - :returns str: network type string. - """ - return DaemonUtils.network_type_to_str(cls.NETWORK_TYPE) - @classmethod def initialize_test_wallet_dir(cls) -> None: """Initialize test wallets directory.""" GenUtils.create_dir_if_not_exists(cls.TEST_WALLETS_DIR) - @classmethod - def check_test_wallets_dir_exists(cls) -> bool: - """Checks if tests wallets directory exists. - - :returns bool: `True` if test wallet directory already exists, `False` other. - """ - return path_exists(cls.TEST_WALLETS_DIR) - - @classmethod - def create_test_wallets_dir(cls) -> None: - """Create test wallets directory.""" - makedirs(cls.TEST_WALLETS_DIR) - @classmethod def get_random_wallet_path(cls) -> str: """Get random test wallet path. @@ -581,9 +559,6 @@ def create_wallet_ground_truth( :param int | None restore_height: wallet restore height. :returns MoneroWalletFull: ground-truth full wallet. """ - # create directory for test wallets if it doesn't exist - if not cls.check_test_wallets_dir_exists(): - cls.create_test_wallets_dir() # create ground truth wallet daemon_connection = MoneroRpcConnection(cls.DAEMON_RPC_URI, cls.DAEMON_RPC_USERNAME, cls.DAEMON_RPC_PASSWORD) @@ -615,25 +590,6 @@ def clear_wallet_full_txs_pool(cls) -> None: cls.WALLET_TX_TRACKER.wait_for_txs_to_clear_pool(wallet_full) wallet_full.close(True) - @classmethod - def dispose(cls) -> None: - """Dispose wallet resources.""" - # dispose mining wallet - if cls._WALLET_MINING is not None: - cls._WALLET_MINING.close(True) - - # dispose full wallet - if cls._WALLET_FULL is not None: - cls._WALLET_FULL.close(True) - - # dispose rpc wallet - if cls._WALLET_RPC is not None: - cls._WALLET_RPC.close(True) - - # dispose rpc wallet 2 - if cls._WALLET_RPC_2 is not None: - cls._WALLET_RPC_2.close(True) - # load configuration TestUtils.load() diff --git a/tests/utils/tx_tester.py b/tests/utils/tx_tester.py new file mode 100644 index 0000000..4ddfc56 --- /dev/null +++ b/tests/utils/tx_tester.py @@ -0,0 +1,234 @@ + +from monero import MoneroTx + +from .context import TestContext +from .gen_utils import GenUtils +from .output_utils import OutputUtils + + +class TxTester: + """Test a `MoneroTx`.""" + + tx: MoneroTx + """Transaction to test.""" + + ctx: TestContext + """Transaction test context.""" + + def __init__(self, tx: MoneroTx, ctx: TestContext) -> None: + """Initialize a new transaction tester. + + :param MoneroTx tx: transaction to test. + :param TestContext ctx: test context. + """ + # check inputs + assert ctx.is_pruned is not None + assert ctx.is_confirmed is not None + assert ctx.from_get_tx_pool is not None + self.tx = tx + self.ctx = ctx + + def _test_standard(self) -> None: + """Test standard tx data model accross all txs.""" + + # standard across all txs + assert self.tx.hash is not None + assert len(self.tx.hash) == 64 + if self.tx.is_relayed is None: + assert self.tx.in_tx_pool is True + else: + assert self.tx.is_relayed is not None + assert self.tx.is_confirmed is not None + assert self.tx.in_tx_pool is not None + assert self.tx.is_miner_tx is not None + assert self.tx.is_double_spend_seen is not None + assert self.tx.version is not None + assert self.tx.version >= 0 + assert self.tx.unlock_time is not None + assert self.tx.unlock_time >= 0 + assert self.tx.extra is not None + assert len(self.tx.extra) > 0 + GenUtils.test_unsigned_big_integer(self.tx.fee, True) + + # test presence of output indices + # TODO change this over to outputs only + if self.tx.is_miner_tx is True: + # TODO how to get output indices for miner transactions? + assert len(self.tx.output_indices) == 0 + assert self.tx.fee == 0 + assert len(self.tx.inputs) == 0 + assert len(self.tx.signatures) == 0 + if self.tx.in_tx_pool or self.ctx.from_get_tx_pool or self.ctx.has_output_indices is False: + assert len(self.tx.output_indices) == 0 + else: + assert len(self.tx.output_indices) > 0 + + def _test_confirmed(self) -> None: + """Test transaction confirmation.""" + # test confirmed ctx + if self.ctx.is_confirmed is True: + assert self.tx.is_confirmed is True + elif self.ctx.is_confirmed is False: + assert self.tx.is_confirmed is False + + # test confirmed + if self.tx.is_confirmed is True: + block = self.tx.block + assert block is not None + assert self.tx in block.txs + assert block.height is not None + assert block.height > 0 + assert block.timestamp is not None + assert block.timestamp > 0 + assert self.tx.relay is True + assert self.tx.is_relayed is True + assert self.tx.is_failed is False + assert self.tx.in_tx_pool is False + assert self.tx.is_double_spend_seen is False + if self.ctx.from_binary_block is True: + assert self.tx.num_confirmations is None + else: + assert self.tx.num_confirmations is not None + assert self.tx.num_confirmations > 0 + else: + assert self.tx.block is None, f"Expected block tx to be null: {self.tx.block.serialize()}" + assert self.tx.num_confirmations == 0 + + def _test_in_tx_pool(self) -> None: + """Test transaction pool details.""" + # test in tx pool + if self.tx.in_tx_pool: + assert self.tx.is_confirmed is False + assert self.tx.is_double_spend_seen is False + assert self.tx.last_failed_height is None + assert self.tx.last_failed_hash is None + assert self.tx.received_timestamp is not None + assert self.tx.received_timestamp > 0 + if self.ctx.from_get_tx_pool: + assert self.tx.size is not None + assert self.tx.size > 0 + assert self.tx.weight is not None + assert self.tx.weight > 0 + assert self.tx.is_kept_by_block is not None + assert self.tx.max_used_block_height is not None + assert self.tx.max_used_block_height >= 0 + assert self.tx.max_used_block_hash is not None + + assert self.tx.last_failed_height is None + assert self.tx.last_failed_hash is None + else: + assert self.tx.last_relayed_timestamp is None + + def _test_failed(self) -> None: + # test failed + # TODO what else to test associated with failed + if self.tx.is_failed: + assert self.tx.received_timestamp is not None + assert self.tx.received_timestamp > 0 + else: + if self.tx.is_relayed is None: + assert self.tx.relay is None + elif self.tx.is_relayed: + assert self.tx.is_double_spend_seen is False + else: + assert self.tx.is_relayed is False + if self.ctx.from_get_tx_pool: + assert self.tx.relay is False + assert self.tx.is_double_spend_seen is not None + + assert self.tx.last_failed_height is None + assert self.tx.last_failed_hash is None + + # received time only for tx pool or failed txs + if self.tx.received_timestamp is not None: + assert self.tx.in_tx_pool or self.tx.is_failed + + def _test_inputs_and_outputs(self) -> None: + """Test transaction's inputs and outputs.""" + + # test inputs and outputs + if not self.tx.is_miner_tx: + assert len(self.tx.inputs) > 0 + + for tx_input in self.tx.inputs: + assert self.tx == tx_input.tx + OutputUtils.test_input(tx_input, self.ctx) + + assert len(self.tx.outputs) > 0 + for output in self.tx.outputs: + assert self.tx == output.tx + OutputUtils.test_output(output, self.ctx) + + def _test_full_tx(self) -> None: + """Test non-pruned transaction""" + assert self.tx.version is not None + assert self.tx.version >= 0 + assert self.tx.unlock_time is not None + assert self.tx.unlock_time >= 0 + assert self.tx.extra is not None + assert len(self.tx.extra) > 0 + + if self.ctx.from_binary_block is True: + # TODO: get_blocks_by_height() has inconsistent client-side pruning + assert self.tx.full_hex is None + assert self.tx.rct_sig_prunable is None + else: + assert self.tx.full_hex is not None + assert len(self.tx.full_hex) > 0 + # TODO define and test this + #assert self.tx.rct_sig_prunable is not None + + assert self.tx.is_double_spend_seen is False + if self.tx.is_confirmed: + assert self.tx.last_relayed_timestamp is None + assert self.tx.received_timestamp is None + else: + if self.tx.is_relayed: + assert self.tx.last_relayed_timestamp is not None + assert self.tx.last_relayed_timestamp > 0 + else: + assert self.tx.last_relayed_timestamp is None + + assert self.tx.received_timestamp is not None + assert self.tx.received_timestamp > 0 + + def _test_pruning(self) -> None: + """Test transaction pruning.""" + # test pruned vs not pruned + # tx might be pruned regardless of configuration + is_pruned: bool = self.tx.pruned_hex is not None + if self.ctx.is_pruned: + assert is_pruned + if self.ctx.from_get_tx_pool or self.ctx.from_binary_block: + assert self.tx.prunable_hash is None + else: + assert self.tx.prunable_hash is not None + + if is_pruned: + assert self.tx.rct_sig_prunable is None + assert self.tx.size is None + assert self.tx.last_relayed_timestamp is None + assert self.tx.received_timestamp is None + # TODO getting full hex in regtest regardless configuration + # assert self.tx.full_hex is None, f"Expected None got: {tx.full_hex}" + assert self.tx.pruned_hex is not None + else: + self._test_full_tx() + + def run(self) -> None: + """Run MoneroTx test.""" + ... + + self._test_standard() + self._test_confirmed() + self._test_in_tx_pool() + self._test_failed() + self._test_inputs_and_outputs() + self._test_pruning() + + # TODO test failed tx + + # TODO implement extra copy + # test deep copy + #if ctx.do_not_test_copy is not True: + # cls.test_tx_copy(tx, ctx) diff --git a/tests/utils/tx_utils.py b/tests/utils/tx_utils.py index 200a4ee..8020f1c 100644 --- a/tests/utils/tx_utils.py +++ b/tests/utils/tx_utils.py @@ -4,10 +4,9 @@ from typing import Optional from monero import MoneroTx -from .gen_utils import GenUtils from .context import TestContext from .assert_utils import AssertUtils -from .output_utils import OutputUtils +from .tx_tester import TxTester logger: logging.Logger = logging.getLogger("TxUtils") @@ -33,7 +32,7 @@ def test_tx_copy(cls, tx: Optional[MoneroTx], context: Optional[TestContext]) -> block_copy = tx.block.copy() block_copy.txs = [copy] - AssertUtils.assert_equals(str(tx), str(copy)) + AssertUtils.assert_equals(tx, copy) assert copy != tx # test different input references @@ -60,198 +59,15 @@ def test_tx_copy(cls, tx: Optional[MoneroTx], context: Optional[TestContext]) -> assert str(tx) == str(merged) @classmethod - def test_tx(cls, tx: Optional[MoneroTx], ctx: Optional[TestContext]) -> None: + def test_tx(cls, tx: MoneroTx | None, ctx: TestContext) -> None: """Test monero tx. :param MoneroTx | None tx: transaction to test. - :param TestContext | None ctx: test context. + :param TestContext ctx: test context. """ - # check inputs - assert tx is not None - assert ctx is not None - assert ctx.is_pruned is not None - assert ctx.is_confirmed is not None - assert ctx.from_get_tx_pool is not None - - # standard across all txs - assert tx.hash is not None - assert len(tx.hash) == 64 - if tx.is_relayed is None: - assert tx.in_tx_pool is True - else: - assert tx.is_relayed is not None - assert tx.is_confirmed is not None - assert tx.in_tx_pool is not None - assert tx.is_miner_tx is not None - assert tx.is_double_spend_seen is not None - assert tx.version is not None - assert tx.version >= 0 - assert tx.unlock_time is not None - assert tx.unlock_time >= 0 - assert tx.extra is not None - assert len(tx.extra) > 0 - GenUtils.test_unsigned_big_integer(tx.fee, True) - - # test presence of output indices - # TODO change this over to outputs only - if tx.is_miner_tx is True: - # TODO how to get output indices for miner transactions? - assert len(tx.output_indices) == 0 - if tx.in_tx_pool or ctx.from_get_tx_pool or ctx.has_output_indices is False: - assert len(tx.output_indices) == 0 - else: - assert len(tx.output_indices) > 0 - - # test confirmed ctx - if ctx.is_confirmed is True: - assert tx.is_confirmed is True - elif ctx.is_confirmed is False: - assert tx.is_confirmed is False - - # test confirmed - if tx.is_confirmed is True: - block = tx.block - assert block is not None - assert tx in block.txs - assert block.height is not None - assert block.height > 0 - assert block.timestamp is not None - assert block.timestamp > 0 - assert tx.relay is True - assert tx.is_relayed is True - assert tx.is_failed is False - assert tx.in_tx_pool is False - assert tx.is_double_spend_seen is False - if ctx.from_binary_block is True: - assert tx.num_confirmations is None - else: - assert tx.num_confirmations is not None - assert tx.num_confirmations > 0 - else: - assert tx.block is None, f"Expected block tx to be null: {tx.block.serialize()}" - assert tx.num_confirmations == 0 - - # test in tx pool - if tx.in_tx_pool: - assert tx.is_confirmed is False - assert tx.is_double_spend_seen is False - assert tx.last_failed_height is None - assert tx.last_failed_hash is None - assert tx.received_timestamp is not None - assert tx.received_timestamp > 0 - if ctx.from_get_tx_pool: - assert tx.size is not None - assert tx.size > 0 - assert tx.weight is not None - assert tx.weight > 0 - assert tx.is_kept_by_block is not None - assert tx.max_used_block_height is not None - assert tx.max_used_block_height >= 0 - assert tx.max_used_block_hash is not None - - assert tx.last_failed_height is None - assert tx.last_failed_hash is None - else: - assert tx.last_relayed_timestamp is None - - # test miner tx - if tx.is_miner_tx: - assert tx.fee == 0 - assert len(tx.inputs) == 0 - assert len(tx.signatures) == 0 - - # test failed - # TODO what else to test associated with failed - if tx.is_failed: - assert tx.received_timestamp is not None - assert tx.received_timestamp > 0 - else: - if tx.is_relayed is None: - assert tx.relay is None - elif tx.is_relayed: - assert tx.is_double_spend_seen is False - else: - assert tx.is_relayed is False - if ctx.from_get_tx_pool: - assert tx.relay is False - assert tx.is_double_spend_seen is not None - - assert tx.last_failed_height is None - assert tx.last_failed_hash is None - - # received time only for tx pool or failed txs - if tx.received_timestamp is not None: - assert tx.in_tx_pool or tx.is_failed - - # test inputs and outputs - if not tx.is_miner_tx: - assert len(tx.inputs) > 0 - - for tx_input in tx.inputs: - assert tx == tx_input.tx - OutputUtils.test_input(tx_input, ctx) - - assert len(tx.outputs) > 0 - for output in tx.outputs: - assert tx == output.tx - OutputUtils.test_output(output, ctx) - - # test pruned vs not pruned - # tx might be pruned regardless of configuration - is_pruned: bool = tx.pruned_hex is not None - if ctx.is_pruned: - assert is_pruned - if ctx.from_get_tx_pool or ctx.from_binary_block: - assert tx.prunable_hash is None - else: - assert tx.prunable_hash is not None - - if is_pruned: - assert tx.rct_sig_prunable is None - assert tx.size is None - assert tx.last_relayed_timestamp is None - assert tx.received_timestamp is None - # TODO getting full hex in regtest regardless configuration - # assert tx.full_hex is None, f"Expected None got: {tx.full_hex}" - assert tx.pruned_hex is not None - else: - assert tx.version is not None - assert tx.version >= 0 - assert tx.unlock_time is not None - assert tx.unlock_time >= 0 - assert tx.extra is not None - assert len(tx.extra) > 0 - - if ctx.from_binary_block is True: - # TODO: get_blocks_by_height() has inconsistent client-side pruning - assert tx.full_hex is None - assert tx.rct_sig_prunable is None - else: - assert tx.full_hex is not None - assert len(tx.full_hex) > 0 - # TODO define and test this - #assert tx.rct_sig_prunable is not None - - assert tx.is_double_spend_seen is False - if tx.is_confirmed: - assert tx.last_relayed_timestamp is None - assert tx.received_timestamp is None - else: - if tx.is_relayed: - assert tx.last_relayed_timestamp is not None - assert tx.last_relayed_timestamp > 0 - else: - assert tx.last_relayed_timestamp is None - - assert tx.received_timestamp is not None - assert tx.received_timestamp > 0 - - # TODO test failed tx - - # TODO implement extra copy - # test deep copy - #if ctx.do_not_test_copy is not True: - # cls.test_tx_copy(tx, ctx) + assert tx is not None, "No tx provided" + tester: TxTester = TxTester(tx, ctx) + tester.run() @classmethod def test_miner_tx(cls, miner_tx: Optional[MoneroTx]) -> None: diff --git a/tests/utils/wallet_error_utils.py b/tests/utils/wallet_error_utils.py index 59c632b..ee095d4 100644 --- a/tests/utils/wallet_error_utils.py +++ b/tests/utils/wallet_error_utils.py @@ -10,13 +10,16 @@ class WalletErrorUtils(ABC): """Wallet is closed error message.""" @classmethod - def test_invalid_address_error(cls, ex: Exception) -> None: + def test_invalid_address_error(cls, ex: Exception, address: str | None = None) -> None: """Test exception is invalid address. :param Exception ex: exception to test. """ msg: str = str(ex) - assert msg == "Invalid address", msg + err_msg: str = "Invalid address" + if address is not None: + err_msg = f"{err_msg}: {address}" + assert msg == err_msg, msg @classmethod def test_invalid_tx_hash_error(cls, ex: Exception) -> None: @@ -80,3 +83,14 @@ def test_wallet_is_closed_error(cls, error: Exception) -> None: """ err_msg: str = str(error) assert err_msg == cls.WALLET_IS_CLOSED_ERROR, err_msg + + @classmethod + def test_wallet_is_not_connected_error(cls, error: Exception) -> None: + err_msg: str = str(error) + # TODO normalize Network error message? + assert err_msg == "Wallet is not connected to daemon" or err_msg == "Network error", err_msg + + @classmethod + def test_deprecated_payment_id_error(cls, error: Exception) -> None: + err_msg: str = str(error) + assert err_msg == "Standalone payment id deprecated, use integrated address instead", err_msg diff --git a/tests/utils/wallet_test_utils.py b/tests/utils/wallet_test_utils.py index 5626d48..222c9d6 100644 --- a/tests/utils/wallet_test_utils.py +++ b/tests/utils/wallet_test_utils.py @@ -126,6 +126,32 @@ def is_wallet_funded(cls, wallet: MoneroWallet, xmr_amount_per_address: float, n return subaddresses_found >= required_subaddresses + @classmethod + def build_tx_config(cls, wallet: MoneroWallet, num_accounts: int, num_subaddresses: int, amount_per_address: int, supports_get_accounts: bool) -> MoneroTxConfig: + tx_config: MoneroTxConfig = MoneroTxConfig() + tx_config.account_index = 0 + tx_config.relay = True + tx_config.can_split = True + + while supports_get_accounts and len(wallet.get_accounts()) < num_accounts: + wallet.create_account() + + for account_idx in range(num_accounts): + account: MoneroAccount = wallet.get_account(account_idx) + num_subaddr: int = len(account.subaddresses) + + while num_subaddr < num_subaddresses: + wallet.create_subaddress(account_idx) + num_subaddr += 1 + + addresses: list[MoneroSubaddress] = wallet.get_subaddresses(account_idx, list(range(num_subaddresses + 1))) + for address in addresses: + assert address.address is not None + dest = MoneroDestination(address.address, amount_per_address) + tx_config.destinations.append(dest) + + return tx_config + @classmethod def fund_wallet(cls, wallet: MoneroWallet, xmr_amount_per_address: float = 10, num_accounts: int = 3, num_subaddresses: int = 5) -> list[MoneroTxWallet]: """Fund a wallet with mined coins. @@ -147,29 +173,9 @@ def fund_wallet(cls, wallet: MoneroWallet, xmr_amount_per_address: float = 10, n amount_required_str: str = f"{MoneroUtils.atomic_units_to_xmr(amount_required)} XMR" logger.debug(f"Funding wallet {primary_addr} with {amount_required_str}...") - - tx_config: MoneroTxConfig = MoneroTxConfig() - tx_config.account_index = 0 - tx_config.relay = True - tx_config.can_split = True - supports_get_accounts: bool = isinstance(wallet, MoneroWalletRpc) or isinstance(wallet, MoneroWalletFull) - while supports_get_accounts and len(wallet.get_accounts()) < num_accounts: - wallet.create_account() - - for account_idx in range(num_accounts): - account: MoneroAccount = wallet.get_account(account_idx) - num_subaddr: int = len(account.subaddresses) - - while num_subaddr < num_subaddresses: - wallet.create_subaddress(account_idx) - num_subaddr += 1 - addresses: list[MoneroSubaddress] = wallet.get_subaddresses(account_idx, list(range(num_subaddresses + 1))) - for address in addresses: - assert address.address is not None - dest = MoneroDestination(address.address, amount_per_address) - tx_config.destinations.append(dest) + tx_config: MoneroTxConfig = cls.build_tx_config(wallet, num_accounts, num_subaddresses, amount_per_address, supports_get_accounts) mining_wallet: MoneroWalletFull = TestUtils.get_mining_wallet() wallet_balance: int = mining_wallet.get_balance() diff --git a/tests/utils/wallet_utils.py b/tests/utils/wallet_utils.py index 851248f..e86524a 100644 --- a/tests/utils/wallet_utils.py +++ b/tests/utils/wallet_utils.py @@ -6,7 +6,8 @@ from monero import ( MoneroNetworkType, MoneroUtils, MoneroAccount, MoneroSubaddress, MoneroAddressBookEntry, - MoneroMessageSignatureResult, MoneroWallet + MoneroMessageSignatureResult, MoneroWallet, + MoneroTxConfig ) from .gen_utils import GenUtils @@ -233,4 +234,14 @@ def test_wallet_keys(cls, address: str, view_key: str, spend_key: str, w: Monero MoneroUtils.validate_mnemonic(w.get_seed()) assert MoneroWallet.DEFAULT_LANGUAGE == w.get_seed_language() + @classmethod + def build_payment_uri_config(cls, address: str) -> MoneroTxConfig: + tx_config = MoneroTxConfig() + tx_config.address = address + tx_config.amount = 250000000000 + tx_config.recipient_name = "John Doe" + tx_config.note = "My transfer to wallet" + + return tx_config + #endregion