From 19efe4c7498a772e4014eb139915feafe18bd216 Mon Sep 17 00:00:00 2001 From: everoddandeven Date: Sun, 22 Feb 2026 21:40:02 +0100 Subject: [PATCH] Implement wallet rpc get_txs, get_transfers, get_outputs * Bump ICU to 75.1.2 * Bump windows pybind to 12.0 * Update README --- .github/workflows/build-windows.yml | 12 +- .github/workflows/test.yml | 12 +- README.md | 8 +- src/cpp/wallet/py_monero_wallet_model.cpp | 240 +++++++++++-- src/cpp/wallet/py_monero_wallet_model.h | 43 ++- src/cpp/wallet/py_monero_wallet_rpc.cpp | 408 ++++++++++++++++++++++ src/cpp/wallet/py_monero_wallet_rpc.h | 10 + tests/test_monero_daemon_rpc.py | 2 +- tests/test_monero_wallet_common.py | 12 +- tests/test_monero_wallet_rpc.py | 22 +- tests/utils/test_utils.py | 4 +- 11 files changed, 700 insertions(+), 73 deletions(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 94086cf..406a598 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -48,11 +48,11 @@ jobs: base-devel wget - - name: Install ICU v75.1.1 + - name: Install ICU v75.1 shell: msys2 {0} run: | - wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-icu-75.1-1-any.pkg.tar.zst - pacman -U --noconfirm mingw-w64-x86_64-icu-75.1-1-any.pkg.tar.zst + wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-icu-75.1-2-any.pkg.tar.zst + pacman -U --noconfirm mingw-w64-x86_64-icu-75.1-2-any.pkg.tar.zst - name: Install boost v1.85.0 shell: msys2 {0} @@ -60,11 +60,11 @@ jobs: wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-boost-1.85.0-4-any.pkg.tar.zst pacman -U --noconfirm mingw-w64-x86_64-boost-1.85.0-4-any.pkg.tar.zst - - name: Install pybind11 v2.11.1 + - name: Install pybind11 v2.12.0 shell: msys2 {0} run: | - wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-pybind11-2.11.1-1-any.pkg.tar.zst - pacman -U --noconfirm mingw-w64-x86_64-pybind11-2.11.1-1-any.pkg.tar.zst + wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-pybind11-2.12.0-1-any.pkg.tar.zst + pacman -U --noconfirm mingw-w64-x86_64-pybind11-2.12.0-1-any.pkg.tar.zst - name: Build monero shell: msys2 {0} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a95039e..7090d1e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -164,11 +164,11 @@ jobs: base-devel wget - - name: Install ICU v75.1.1 + - name: Install ICU v75.1 shell: msys2 {0} run: | - wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-icu-75.1-1-any.pkg.tar.zst - pacman -U --noconfirm mingw-w64-x86_64-icu-75.1-1-any.pkg.tar.zst + wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-icu-75.1-2-any.pkg.tar.zst + pacman -U --noconfirm mingw-w64-x86_64-icu-75.1-2-any.pkg.tar.zst - name: Install boost v1.85.0 shell: msys2 {0} @@ -176,11 +176,11 @@ jobs: wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-boost-1.85.0-4-any.pkg.tar.zst pacman -U --noconfirm mingw-w64-x86_64-boost-1.85.0-4-any.pkg.tar.zst - - name: Install pybind11 v2.11.1 + - name: Install pybind11 v2.12.0 shell: msys2 {0} run: | - wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-pybind11-2.11.1-1-any.pkg.tar.zst - pacman -U --noconfirm mingw-w64-x86_64-pybind11-2.11.1-1-any.pkg.tar.zst + wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-pybind11-2.12.0-1-any.pkg.tar.zst + pacman -U --noconfirm mingw-w64-x86_64-pybind11-2.12.0-1-any.pkg.tar.zst - name: Clone monero-cpp (regtest) shell: msys2 {0} diff --git a/README.md b/README.md index d47dd06..6224178 100644 --- a/README.md +++ b/README.md @@ -160,12 +160,12 @@ wallet_full.close(True) ``` pacman -S mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake mingw-w64-x86_64-openssl mingw-w64-x86_64-zeromq mingw-w64-x86_64-libsodium mingw-w64-x86_64-hidapi mingw-w64-x86_64-unbound mingw-w64-x86_64-protobuf git mingw-w64-x86_64-libusb gettext base-devel - wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-icu-75.1-1-any.pkg.tar.zst - pacman -U mingw-w64-x86_64-icu-75.1-1-any.pkg.tar.zst + wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-icu-75.1-2-any.pkg.tar.zst + pacman -U mingw-w64-x86_64-icu-75.1-2-any.pkg.tar.zst wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-boost-1.85.0-4-any.pkg.tar.zst pacman -U mingw-w64-x86_64-boost-1.85.0-4-any.pkg.tar.zst - wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-pybind11-2.11.1-1-any.pkg.tar.zst - pacman -U mingw-w64-x86_64-pybind11-2.11.1-1-any.pkg.tar.zst + wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-pybind11-2.12.0-1-any.pkg.tar.zst + pacman -U mingw-w64-x86_64-pybind11-2.12.0-1-any.pkg.tar.zst ``` 5. Clone repo: `git clone --recurse-submodules https://github.com/everoddandeven/monero-python.git` diff --git a/src/cpp/wallet/py_monero_wallet_model.cpp b/src/cpp/wallet/py_monero_wallet_model.cpp index 0913e63..8fb25ee 100644 --- a/src/cpp/wallet/py_monero_wallet_model.cpp +++ b/src/cpp/wallet/py_monero_wallet_model.cpp @@ -71,20 +71,22 @@ bool PyOutputComparator::operator()(const monero::monero_output_wallet& o1, cons return o1.m_key_image.get()->m_hex.value() < o2.m_key_image.get()->m_hex.value(); } -void PyMoneroTxQuery::decontextualize(const std::shared_ptr &query) { +std::shared_ptr PyMoneroTxQuery::decontextualize(const std::shared_ptr &query) { query->m_is_incoming = boost::none; query->m_is_outgoing = boost::none; query->m_transfer_query = boost::none; query->m_input_query = boost::none; query->m_output_query = boost::none; + return query; } -void PyMoneroTxQuery::decontextualize(monero::monero_tx_query &query) { +monero::monero_tx_query PyMoneroTxQuery::decontextualize(monero::monero_tx_query &query) { query.m_is_incoming = boost::none; query.m_is_outgoing = boost::none; query.m_transfer_query = boost::none; query.m_input_query = boost::none; query.m_output_query = boost::none; + return query; } bool PyMoneroOutputQuery::is_contextual(const std::shared_ptr &query) { @@ -117,7 +119,6 @@ bool PyMoneroTransferQuery::is_contextual(const monero::monero_transfer_query &q bool PyMoneroTxWallet::decode_rpc_type(const std::string &rpc_type, const std::shared_ptr &tx) { bool is_outgoing = false; if (rpc_type == std::string("in")) { - is_outgoing = false; tx->m_is_confirmed = true; tx->m_in_tx_pool = false; tx->m_is_relayed = true; @@ -133,7 +134,6 @@ bool PyMoneroTxWallet::decode_rpc_type(const std::string &rpc_type, const std::s tx->m_is_failed = false; tx->m_is_miner_tx = false; } else if (rpc_type == std::string("pool")) { - is_outgoing = false; tx->m_is_confirmed = false; tx->m_in_tx_pool = true; tx->m_is_relayed = true; @@ -149,7 +149,6 @@ bool PyMoneroTxWallet::decode_rpc_type(const std::string &rpc_type, const std::s tx->m_is_failed = false; tx->m_is_miner_tx = false; } else if (rpc_type == std::string("block")) { - is_outgoing = false; tx->m_is_confirmed = true; tx->m_in_tx_pool = false; tx->m_is_relayed = true; @@ -223,45 +222,55 @@ void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tr is_outgoing = decode_rpc_type(it->second.data(), tx); key_found = true; } - else if (key == std::string("txid")) tx->m_hash = it->second.data(); - else if (key == std::string("tx_hash")) tx->m_hash = it->second.data(); + } + + for (auto it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("txid") || key == std::string("tx_hash")) tx->m_hash = it->second.data(); else if (key == std::string("fee")) tx->m_fee = it->second.get_value(); - else if (key == std::string("note")) tx->m_note = it->second.data(); - else if (key == std::string("tx_key")) tx->m_key = it->second.data(); + else if (key == std::string("note") && !it->second.data().empty()) tx->m_note = it->second.data(); + else if (key == std::string("tx_key") && !it->second.data().empty()) tx->m_key = it->second.data(); else if (key == std::string("tx_size")) tx->m_size = it->second.get_value(); else if (key == std::string("unlock_time")) tx->m_unlock_time = it->second.get_value(); else if (key == std::string("weight")) tx->m_weight = it->second.get_value(); else if (key == std::string("locked")) tx->m_is_locked = it->second.get_value(); - else if (key == std::string("tx_blob")) tx->m_full_hex = it->second.data(); - else if (key == std::string("tx_metadata")) tx->m_metadata = it->second.data(); + else if (key == std::string("tx_blob") && !it->second.data().empty()) tx->m_full_hex = it->second.data(); + else if (key == std::string("tx_metadata") && !it->second.data().empty()) tx->m_metadata = it->second.data(); else if (key == std::string("double_spend_seen")) tx->m_is_double_spend_seen = it->second.get_value(); else if (key == std::string("block_height") || key == std::string("height")) { - if (tx->m_is_confirmed) { + if (bool_equals_2(true, tx->m_is_confirmed)) { if (header == nullptr) header = std::make_shared(); header->m_height = it->second.get_value(); } } else if (key == std::string("timestamp")) { - if (tx->m_is_confirmed) { + if (bool_equals_2(true, tx->m_is_confirmed)) { if (header == nullptr) header = std::make_shared(); header->m_timestamp = it->second.get_value(); } } else if (key == std::string("confirmations")) tx->m_num_confirmations = it->second.get_value(); else if (key == std::string("suggested_confirmations_threshold")) { - if (is_outgoing && outgoing_transfer == nullptr) { - outgoing_transfer = std::make_shared(); + if (*is_outgoing) { + if (outgoing_transfer == nullptr) + outgoing_transfer = std::make_shared(); + outgoing_transfer->m_tx = tx; } - else if (!is_outgoing && incoming_transfer == nullptr) { - incoming_transfer = std::make_shared(); + else if (!*is_outgoing) { + if (incoming_transfer == nullptr) + incoming_transfer = std::make_shared(); incoming_transfer->m_tx = tx; incoming_transfer->m_num_suggested_confirmations = it->second.get_value(); } } else if (key == std::string("amount")) { - if (is_outgoing) { - if (outgoing_transfer == nullptr) outgoing_transfer = std::make_shared(); - incoming_transfer->m_amount = it->second.get_value(); + if (*is_outgoing) { + if (outgoing_transfer == nullptr) { + outgoing_transfer = std::make_shared(); + outgoing_transfer->m_tx = tx; + } + outgoing_transfer->m_amount = it->second.get_value(); } else { if (incoming_transfer == nullptr) incoming_transfer = std::make_shared(); @@ -270,7 +279,7 @@ void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tr } } else if (key == std::string("address")) { - if (!is_outgoing) { + if (!*is_outgoing) { if (incoming_transfer == nullptr) incoming_transfer = std::make_shared(); incoming_transfer->m_tx = tx; incoming_transfer->m_address = it->second.data(); @@ -283,8 +292,11 @@ void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tr } } else if (key == std::string("subaddr_indices")) { - if (is_outgoing) { - if (outgoing_transfer == nullptr) outgoing_transfer = std::make_shared(); + if (*is_outgoing) { + if (outgoing_transfer == nullptr) { + outgoing_transfer = std::make_shared(); + outgoing_transfer->m_tx = tx; + } } else { if (incoming_transfer == nullptr) incoming_transfer = std::make_shared(); @@ -302,12 +314,12 @@ void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tr std::string index_key = it3->first; if (index_key == std::string("major") && first_major) { - if (is_outgoing) outgoing_transfer->m_account_index = it3->second.get_value(); + if (*is_outgoing) outgoing_transfer->m_account_index = it3->second.get_value(); else incoming_transfer->m_account_index = it3->second.get_value(); first_major = false; } else if (index_key == std::string("minor")) { - if (is_outgoing) { + if (*is_outgoing) { outgoing_transfer->m_subaddress_indices.push_back(it3->second.get_value()); } else if (first_minor) { @@ -319,7 +331,7 @@ void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tr } } else if (key == std::string("destinations") || key == std::string("recipients")) { - if (!is_outgoing) throw std::runtime_error("Expected outgoing tx"); + if (!*is_outgoing) throw std::runtime_error("Expected outgoing transaction"); if (outgoing_transfer == nullptr) { outgoing_transfer = std::make_shared(); outgoing_transfer->m_tx = tx; @@ -374,7 +386,7 @@ void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tr } } else if (key == std::string("amounts_by_dest")) { - if (!is_outgoing) throw std::runtime_error("Expected outgoing transaction"); + if (!*is_outgoing) throw std::runtime_error("Expected outgoing transaction"); if (outgoing_transfer == nullptr) { outgoing_transfer = std::make_shared(); outgoing_transfer->m_tx = tx; @@ -408,12 +420,12 @@ void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tr if (!key_found && is_outgoing == boost::none) throw std::runtime_error("Must indicate if tx is outgoing (true) xor incoming (false) since unknown"); if (header != nullptr) { auto block = std::make_shared(); - block->copy(block, header); + header->copy(header, block); block->m_txs.push_back(tx); tx->m_block = block; } - if (is_outgoing && outgoing_transfer != nullptr) { + if (*is_outgoing && outgoing_transfer != nullptr) { if (tx->m_is_confirmed == boost::none) tx->m_is_confirmed = false; if (!outgoing_transfer->m_tx->m_is_confirmed) tx->m_num_confirmations = 0; tx->m_is_outgoing = true; @@ -423,7 +435,7 @@ void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tr } else tx->m_outgoing_transfer = outgoing_transfer; } - else if (is_outgoing != boost::none && is_outgoing == false && incoming_transfer != nullptr) { + else if (is_outgoing != boost::none && *is_outgoing == false && incoming_transfer != nullptr) { if (tx->m_is_confirmed == boost::none) tx->m_is_confirmed = false; if (!incoming_transfer->m_tx->m_is_confirmed) tx->m_num_confirmations = 0; tx->m_is_incoming = true; @@ -437,6 +449,11 @@ void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tr from_property_tree_with_transfer(node, tx, is_outgoing, config); } +void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tree::ptree& node, const std::shared_ptr& tx) { + boost::optional is_outgoing; + from_property_tree_with_transfer(node, tx, is_outgoing); +} + void PyMoneroTxWallet::from_property_tree_with_output(const boost::property_tree::ptree& node, const std::shared_ptr& tx) { tx->m_is_confirmed = true; tx->m_is_relayed = true; @@ -474,6 +491,137 @@ void PyMoneroTxWallet::from_property_tree_with_output(const boost::property_tree tx->m_outputs.push_back(output); } +void PyMoneroTxWallet::from_property_tree_with_output_and_merge(const boost::property_tree::ptree& node, std::map>& tx_map, std::map>& block_map) { + for(auto it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("transfers")) { + for(auto rpc_output_it = it->second.begin(); rpc_output_it != it->second.end(); ++rpc_output_it) { + auto tx = std::make_shared(); + from_property_tree_with_output(rpc_output_it->second, tx); + merge_tx(tx, tx_map, block_map); + } + } + } +} + +void PyMoneroTxWallet::from_property_tree_with_transfer_and_merge(const boost::property_tree::ptree& node, std::map>& tx_map, std::map>& block_map) { + for (auto it = node.begin(); it != node.end(); ++it) { + for (auto it2 = it->second.begin(); it2 != it->second.end(); ++it2) { + auto tx = std::make_shared(); + PyMoneroTxWallet::from_property_tree_with_transfer(it2->second, tx); + + if (tx->m_is_confirmed != boost::none && *tx->m_is_confirmed == true) { + if (tx->m_block == boost::none) throw std::runtime_error("Confirmed tx has no block"); + auto& block_txs = tx->m_block.get()->m_txs; + if (std::find(block_txs.begin(), block_txs.end(), tx) == block_txs.end()) { + throw std::runtime_error("Tx not found in its block"); + } + } + + // replace transfer amount with destination sum + // TODO monero-wallet-rpc: confirmed tx from/to same account has amount 0 but cached transfers + if (tx->m_outgoing_transfer != boost::none && bool_equals_2(true, tx->m_is_relayed) && !bool_equals_2(true, tx->m_is_failed) && + !tx->m_outgoing_transfer.get()->m_destinations.empty() && tx->m_outgoing_transfer.get()->m_amount.get() == 0) { + auto outgoing_transfer = tx->m_outgoing_transfer.get(); + uint64_t transfer_total = 0; + for(const auto& destination : outgoing_transfer->m_destinations) { + transfer_total += destination->m_amount.get(); + } + outgoing_transfer->m_amount = transfer_total; + } + + // merge tx + merge_tx(tx, tx_map, block_map); + } + } +} + +/** + * ---------------- DUPLICATED MONERO-CPP WALLET FULL CODE --------------------- + */ + +bool bool_equals_2(bool val, const boost::optional& opt_val) { + return opt_val == boost::none ? false : val == *opt_val; +} + +/** + * Returns true iff tx1's height is known to be less than tx2's height for sorting. + */ +bool tx_height_less_than(const std::shared_ptr& tx1, const std::shared_ptr& tx2) { + if (tx1->m_block != boost::none && tx2->m_block != boost::none) return tx1->get_height() < tx2->get_height(); + else if (tx1->m_block == boost::none) return false; + else return true; +} + +/** + * Returns true iff transfer1 is ordered before transfer2 by ascending account and subaddress indices. + */ +bool incoming_transfer_before(const std::shared_ptr& transfer1, const std::shared_ptr& transfer2) { + + // compare by height + if (tx_height_less_than(transfer1->m_tx, transfer2->m_tx)) return true; + + // compare by account and subaddress index + if (transfer1->m_account_index.get() < transfer2->m_account_index.get()) return true; + else if (transfer1->m_account_index.get() == transfer2->m_account_index.get()) return transfer1->m_subaddress_index.get() < transfer2->m_subaddress_index.get(); + else return false; +} + +/** + * Returns true iff wallet vout1 is ordered before vout2 by ascending account and subaddress indices then index. + */ +bool vout_before(const std::shared_ptr& o1, const std::shared_ptr& o2) { + if (o1 == o2) return false; // ignore equal references + std::shared_ptr ow1 = std::static_pointer_cast(o1); + std::shared_ptr ow2 = std::static_pointer_cast(o2); + + // compare by height + if (tx_height_less_than(ow1->m_tx, ow2->m_tx)) return true; + + // compare by account index, subaddress index, output index, then key image hex + if (ow1->m_account_index.get() < ow2->m_account_index.get()) return true; + if (ow1->m_account_index.get() == ow2->m_account_index.get()) { + if (ow1->m_subaddress_index.get() < ow2->m_subaddress_index.get()) return true; + if (ow1->m_subaddress_index.get() == ow2->m_subaddress_index.get()) { + if (ow1->m_index.get() < ow2->m_index.get()) return true; + if (ow1->m_index.get() == ow2->m_index.get()) throw std::runtime_error("Should never sort outputs with duplicate indices"); + } + } + return false; +} + +/** + * Merges a transaction into a unique set of transactions. + * + * @param tx is the transaction to merge into the existing txs + * @param tx_map maps tx hashes to txs + * @param block_map maps block heights to blocks + */ +void PyMoneroTxWallet::merge_tx(const std::shared_ptr& tx, std::map>& tx_map, std::map>& block_map) { + if (tx->m_hash == boost::none) throw std::runtime_error("Tx hash is not initialized"); + + // merge tx + std::map>::const_iterator tx_iter = tx_map.find(*tx->m_hash); + if (tx_iter == tx_map.end()) { + tx_map[*tx->m_hash] = tx; // cache new tx + } else { + std::shared_ptr& a_tx = tx_map[*tx->m_hash]; + a_tx->merge(a_tx, tx); // merge with existing tx + } + + // merge tx's block if confirmed + if (tx->get_height() != boost::none) { + std::map>::const_iterator block_iter = block_map.find(tx->get_height().get()); + if (block_iter == block_map.end()) { + block_map[tx->get_height().get()] = tx->m_block.get(); // cache new block + } else { + std::shared_ptr& a_block = block_map[tx->get_height().get()]; + a_block->merge(a_block, tx->m_block.get()); // merge with existing block + } + } +} + void PyMoneroTxSet::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& set) { for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { std::string key = it->first; @@ -1550,6 +1698,38 @@ rapidjson::Value PyMoneroTransferParams::to_rapidjson_val(rapidjson::Document::A return root; } +PyMoneroGetIncomingTransfersParams::PyMoneroGetIncomingTransfersParams(const std::string& transfer_type, bool verbose): + m_transfer_type(transfer_type), m_verbose(verbose) { +} + +rapidjson::Value PyMoneroGetIncomingTransfersParams::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + rapidjson::Value root(rapidjson::kObjectType); + rapidjson::Value value_num(rapidjson::kNumberType); + rapidjson::Value value_str(rapidjson::kStringType); + if (m_transfer_type != boost::none) monero_utils::add_json_member("transfer_type", m_transfer_type.get(), allocator, root, value_str); + if (m_verbose != boost::none) monero_utils::add_json_member("verbose", m_verbose.get(), allocator, root); + if (m_account_index != boost::none) monero_utils::add_json_member("account_index", m_account_index.get(), allocator, root, value_num); + if (!m_subaddr_indices.empty()) root.AddMember("subaddr_indices", monero_utils::to_rapidjson_val(allocator, m_subaddr_indices), allocator); + return root; +} + +rapidjson::Value PyMoneroGetTransfersParams::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + rapidjson::Value root(rapidjson::kObjectType); + rapidjson::Value value_num(rapidjson::kNumberType); + rapidjson::Value value_str(rapidjson::kStringType); + if (m_in != boost::none) monero_utils::add_json_member("in", m_in.get(), allocator, root); + if (m_out != boost::none) monero_utils::add_json_member("out", m_out.get(), allocator, root); + if (m_pool != boost::none) monero_utils::add_json_member("pool", m_pool.get(), allocator, root); + if (m_pending != boost::none) monero_utils::add_json_member("pending", m_pending.get(), allocator, root); + if (m_failed != boost::none) monero_utils::add_json_member("failed", m_failed.get(), allocator, root); + if (m_min_height != boost::none) monero_utils::add_json_member("min_height", m_min_height.get(), allocator, root); + if (m_max_height != boost::none) monero_utils::add_json_member("max_height", m_max_height.get(), allocator, root); + if (m_all_accounts != boost::none) monero_utils::add_json_member("all_accounts", m_all_accounts.get(), allocator, root); + if (m_account_index != boost::none) monero_utils::add_json_member("account_index", m_account_index.get(), allocator, root, value_num); + if (!m_subaddr_indices.empty()) root.AddMember("subaddr_indices", monero_utils::to_rapidjson_val(allocator, m_subaddr_indices), allocator); + return root; +} + void PyMoneroCheckReserve::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& check) { for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { std::string key = it->first; diff --git a/src/cpp/wallet/py_monero_wallet_model.h b/src/cpp/wallet/py_monero_wallet_model.h index 2bbfe9f..2612c6c 100644 --- a/src/cpp/wallet/py_monero_wallet_model.h +++ b/src/cpp/wallet/py_monero_wallet_model.h @@ -28,8 +28,8 @@ struct PyOutputComparator { class PyMoneroTxQuery : public monero::monero_tx_query { public: - static void decontextualize(const std::shared_ptr &query); - static void decontextualize(monero::monero_tx_query &query); + static std::shared_ptr decontextualize(const std::shared_ptr &query); + static monero::monero_tx_query decontextualize(monero::monero_tx_query &query); }; class PyMoneroOutputQuery : public monero::monero_output_query { @@ -82,7 +82,11 @@ class PyMoneroTxWallet : public monero::monero_tx_wallet { static void init_sent(const monero::monero_tx_config &config, std::shared_ptr &tx, bool copy_destinations); static void from_property_tree_with_transfer(const boost::property_tree::ptree& node, const std::shared_ptr& tx, boost::optional &is_outgoing, const monero_tx_config &config); static void from_property_tree_with_transfer(const boost::property_tree::ptree& node, const std::shared_ptr& tx, boost::optional &is_outgoing); + static void from_property_tree_with_transfer(const boost::property_tree::ptree& node, const std::shared_ptr& tx); + static void from_property_tree_with_transfer_and_merge(const boost::property_tree::ptree& node, std::map>& tx_map, std::map>& block_map); static void from_property_tree_with_output(const boost::property_tree::ptree& node, const std::shared_ptr& tx); + static void from_property_tree_with_output_and_merge(const boost::property_tree::ptree& node, std::map>& tx_map, std::map>& block_map); + static void merge_tx(const std::shared_ptr& tx, std::map>& tx_map, std::map>& block_map); }; class PyMoneroTxSet : public monero::monero_tx_set { @@ -966,6 +970,36 @@ class PyMoneroTransferParams : public PyMoneroJsonRequestParams { rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; +class PyMoneroGetTransfersParams : public PyMoneroJsonRequestParams { +public: + boost::optional m_in; + boost::optional m_out; + boost::optional m_pool; + boost::optional m_pending; + boost::optional m_failed; + boost::optional m_min_height; + boost::optional m_max_height; + boost::optional m_all_accounts; + boost::optional m_account_index; + std::vector m_subaddr_indices; + + PyMoneroGetTransfersParams() {} + bool filter_by_height() const { return m_min_height != boost::none || m_max_height != boost::none; } + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; +}; + +class PyMoneroGetIncomingTransfersParams : public PyMoneroJsonRequestParams { +public: + boost::optional m_transfer_type; + boost::optional m_verbose; + boost::optional m_account_index; + std::vector m_subaddr_indices; + + PyMoneroGetIncomingTransfersParams(const std::string& transfer_type, bool verbose = true); + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; +}; + class PyMoneroCheckReserve : public monero::monero_check_reserve { public: @@ -990,3 +1024,8 @@ class PyMoneroMessageSignatureResult : public monero::monero_message_signature_r static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr result); }; +// TODO expose bool_equals_2 from monero-cpp +bool bool_equals_2(bool val, const boost::optional& opt_val); +bool tx_height_less_than(const std::shared_ptr& tx1, const std::shared_ptr& tx2); +bool incoming_transfer_before(const std::shared_ptr& transfer1, const std::shared_ptr& transfer2); +bool vout_before(const std::shared_ptr& o1, const std::shared_ptr& o2); diff --git a/src/cpp/wallet/py_monero_wallet_rpc.cpp b/src/cpp/wallet/py_monero_wallet_rpc.cpp index 81138ee..f66b694 100644 --- a/src/cpp/wallet/py_monero_wallet_rpc.cpp +++ b/src/cpp/wallet/py_monero_wallet_rpc.cpp @@ -1241,6 +1241,14 @@ std::shared_ptr PyMoneroWalletRpc::check_reserve_proof(con return proof; } +std::string PyMoneroWalletRpc::get_tx_note(const std::string& tx_hash) const { + std::vector tx_hashes; + tx_hashes.push_back(tx_hash); + auto notes = get_tx_notes(tx_hashes); + if (notes.size() != 1) throw std::runtime_error("Expected one tx note"); + return notes[0]; +} + std::vector PyMoneroWalletRpc::get_tx_notes(const std::vector& tx_hashes) const { auto params = std::make_shared(tx_hashes); PyMoneroJsonRequest request("get_tx_notes", params); @@ -1264,6 +1272,15 @@ std::vector PyMoneroWalletRpc::get_tx_notes(const std::vector tx_hashes; + std::vector notes; + tx_hashes.push_back(tx_hash); + notes.push_back(note); + + set_tx_notes(tx_hashes, notes); +} + void PyMoneroWalletRpc::set_tx_notes(const std::vector& tx_hashes, const std::vector& notes) { auto params = std::make_shared(tx_hashes, notes); PyMoneroJsonRequest request("set_tx_notes", params); @@ -1731,3 +1748,394 @@ void PyMoneroWalletRpc::clear() { clear_address_cache(); m_path = ""; } + +std::vector> PyMoneroWalletRpc::get_txs() const { + return get_txs(monero_tx_query()); +} + +std::vector> PyMoneroWalletRpc::get_txs(const monero_tx_query& query) const { + MTRACE("get_txs(query)"); + + // copy query + std::shared_ptr query_sp = std::make_shared(query); // convert to shared pointer + std::shared_ptr _query = query_sp->copy(query_sp, std::make_shared()); // deep copy + + // temporarily disable transfer and output queries in order to collect all tx context + boost::optional> transfer_query = _query->m_transfer_query; + boost::optional> input_query = _query->m_input_query; + boost::optional> output_query = _query->m_output_query; + _query->m_transfer_query = boost::none; + _query->m_input_query = boost::none; + _query->m_output_query = boost::none; + + // fetch all transfers that meet tx query + std::shared_ptr temp_transfer_query = std::make_shared(); + temp_transfer_query->m_tx_query = PyMoneroTxQuery::decontextualize(_query->copy(_query, std::make_shared())); + temp_transfer_query->m_tx_query.get()->m_transfer_query = temp_transfer_query; + std::vector> transfers = get_transfers_aux(*temp_transfer_query); + monero_utils::free(temp_transfer_query->m_tx_query.get()); + + // collect unique txs from transfers while retaining order + std::vector> txs = std::vector>(); + std::unordered_set> txsSet; + for (const std::shared_ptr& transfer : transfers) { + if (txsSet.find(transfer->m_tx) == txsSet.end()) { + txs.push_back(transfer->m_tx); + txsSet.insert(transfer->m_tx); + } + } + + // cache types into maps for merging and lookup + std::map> tx_map; + std::map> block_map; + for (const std::shared_ptr& tx : txs) { + PyMoneroTxWallet::merge_tx(tx, tx_map, block_map); + } + + // fetch and merge outputs if requested + if ((_query->m_include_outputs != boost::none && *_query->m_include_outputs) || output_query != boost::none) { + std::shared_ptr temp_output_query = std::make_shared(); + temp_output_query->m_tx_query = PyMoneroTxQuery::decontextualize(_query->copy(_query, std::make_shared())); + temp_output_query->m_tx_query.get()->m_output_query = temp_output_query; + std::vector> outputs = get_outputs_aux(*temp_output_query); + monero_utils::free(temp_output_query->m_tx_query.get()); + + // merge output txs one time while retaining order + std::unordered_set> output_txs; + for (const std::shared_ptr& output : outputs) { + std::shared_ptr tx = std::static_pointer_cast(output->m_tx); + if (output_txs.find(tx) == output_txs.end()) { + PyMoneroTxWallet::merge_tx(tx, tx_map, block_map); + output_txs.insert(tx); + } + } + } + + // restore transfer and output queries + _query->m_transfer_query = transfer_query; + _query->m_input_query = input_query; + _query->m_output_query = output_query; + + // filter txs that don't meet transfer query + std::vector> queried_txs; + std::vector>::iterator tx_iter = txs.begin(); + while (tx_iter != txs.end()) { + std::shared_ptr tx = *tx_iter; + if (_query->meets_criteria(tx.get())) { + queried_txs.push_back(tx); + ++tx_iter; + } else { + tx_map.erase(tx->m_hash.get()); + tx_iter = txs.erase(tx_iter); + if (tx->m_block != boost::none) tx->m_block.get()->m_txs.erase(std::remove(tx->m_block.get()->m_txs.begin(), tx->m_block.get()->m_txs.end(), tx), tx->m_block.get()->m_txs.end()); // TODO, no way to use tx_iter? + } + } + txs = queried_txs; + + // special case: re-fetch txs if inconsistency caused by needing to make multiple wallet calls + // TODO monero-project: offer wallet.get_txs(...) + for (const std::shared_ptr& tx : txs) { + if ((*tx->m_is_confirmed && tx->m_block == boost::none) || (!*tx->m_is_confirmed && tx->m_block != boost::none)) { + std::cout << "WARNING: Inconsistency detected building txs from multiple wallet2 calls, re-fetching" << std::endl; + monero_utils::free(txs); + txs.clear(); + txs = get_txs(*_query); + monero_utils::free(_query); + return txs; + } + } + + // if tx hashes requested, order txs + if (!_query->m_hashes.empty()) { + txs.clear(); + for (const std::string& tx_hash : _query->m_hashes) { + std::map>::const_iterator tx_iter = tx_map.find(tx_hash); + if (tx_iter != tx_map.end()) txs.push_back(tx_iter->second); + } + } + + // free query and return + monero_utils::free(_query); + return txs; +} + +std::vector> PyMoneroWalletRpc::get_transfers(const monero_transfer_query& query) const { + // get transfers directly if query does not require tx context (e.g. other transfers, outputs) + if (!PyMoneroTransferQuery::is_contextual(query)) return get_transfers_aux(query); + + // otherwise get txs with full models to fulfill query + std::vector> transfers; + for (const std::shared_ptr& tx : get_txs(*(query.m_tx_query.get()))) { + for (const std::shared_ptr& transfer : tx->filter_transfers(query)) { // collect queried transfers, erase if excluded + transfers.push_back(transfer); + } + } + return transfers; +} + +std::vector> PyMoneroWalletRpc::get_outputs(const monero_output_query& query) const { + // get outputs directly if query does not require tx context (e.g. other outputs, transfers) + if (!PyMoneroOutputQuery::is_contextual(query)) return get_outputs_aux(query); + + // otherwise get txs with full models to fulfill query + std::vector> outputs; + for (const std::shared_ptr& tx : get_txs(*(query.m_tx_query.get()))) { + for (const std::shared_ptr& output : tx->filter_outputs_wallet(query)) { // collect queried outputs, erase if excluded + outputs.push_back(output); + } + } + return outputs; +} + +std::map> PyMoneroWalletRpc::get_account_indices(bool get_subaddr_indices) const { + std::map> indices; + for (const auto& account : monero::monero_wallet::get_accounts()) { + uint32_t account_idx = account.m_index.get(); + if (get_subaddr_indices) { + indices[account_idx] = get_subaddress_indices(account_idx); + } + else indices[account_idx] = std::vector(); + } + return indices; +} + +std::vector PyMoneroWalletRpc::get_subaddress_indices(uint32_t account_idx) const { + // fetch subaddresses + auto params = std::make_shared(account_idx); + PyMoneroJsonRequest request("get_address", params); + auto response = m_rpc->send_json_request(request); + if (response->m_result == boost::none) throw std::runtime_error("Invalid Monero JSONRPC response"); + auto node = response->m_result.get(); + std::vector subadress_indices; + // TODO refactory + for (auto it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + if (key == std::string("addresses")) { + auto node2 = it->second; + for (auto it2 = node2.begin(); it2 != node2.end(); ++it2) { + auto subaddress = std::make_shared(); + PyMoneroSubaddress::from_rpc_property_tree(it2->second, subaddress); + subadress_indices.push_back(subaddress->m_index.get()); + } + break; + } + } + return subadress_indices; +} + +std::vector> PyMoneroWalletRpc::get_transfers_aux(const monero_transfer_query& query) const { + MTRACE("PyMoneroWalletRpc::get_transfers(query)"); +// // log query +// if (query.m_tx_query != boost::none) { +// if ((*query.m_tx_query)->m_block == boost::none) std::cout << "Transfer query's tx query rooted at [tx]:" << (*query.m_tx_query)->serialize() << std::endl; +// else std::cout << "Transfer query's tx query rooted at [block]: " << (*(*query.m_tx_query)->m_block)->serialize() << std::endl; +// } else std::cout << "Transfer query: " << query.serialize() << std::endl; + + // copy and normalize query + std::shared_ptr _query; + if (query.m_tx_query == boost::none) { + std::shared_ptr query_ptr = std::make_shared(query); // convert to shared pointer for copy // TODO: does this copy unecessarily? copy constructor is not defined + _query = query_ptr->copy(query_ptr, std::make_shared()); + _query->m_tx_query = std::make_shared(); + _query->m_tx_query.get()->m_transfer_query = _query; + } else { + std::shared_ptr tx_query = query.m_tx_query.get()->copy(query.m_tx_query.get(), std::make_shared()); + _query = tx_query->m_transfer_query.get(); + } + std::shared_ptr tx_query = _query->m_tx_query.get(); + + boost::optional account_index = boost::none; + if (_query->m_account_index != boost::none) account_index = *_query->m_account_index; + std::set subaddress_indices; + for (int i = 0; i < _query->m_subaddress_indices.size(); i++) { + subaddress_indices.insert(_query->m_subaddress_indices[i]); + } + + // translate from monero_tx_query to in, out, pending, pool, failed terminology used by monero-wallet-rpc + bool can_be_confirmed = !bool_equals_2(false, tx_query->m_is_confirmed) && !bool_equals_2(true, tx_query->m_in_tx_pool) && !bool_equals_2(true, tx_query->m_is_failed) && !bool_equals_2(false, tx_query->m_is_relayed); + bool can_be_in_tx_pool = !bool_equals_2(true, tx_query->m_is_confirmed) && !bool_equals_2(false, tx_query->m_in_tx_pool) && !bool_equals_2(true, tx_query->m_is_failed) && tx_query->get_height() == boost::none && tx_query->m_min_height == boost::none && !bool_equals_2(false, tx_query->m_is_locked); + bool can_be_incoming = !bool_equals_2(false, _query->m_is_incoming) && !bool_equals_2(true, _query->is_outgoing()) && !bool_equals_2(true, _query->m_has_destinations); + bool can_be_outgoing = !bool_equals_2(false, _query->is_outgoing()) && !bool_equals_2(true, _query->m_is_incoming); + bool is_in = can_be_incoming && can_be_confirmed; + bool is_out = can_be_outgoing && can_be_confirmed; + bool is_pending = can_be_outgoing && can_be_in_tx_pool; + bool is_pool = can_be_incoming && can_be_in_tx_pool; + bool is_failed = !bool_equals_2(false, tx_query->m_is_failed) && !bool_equals_2(true, tx_query->m_is_confirmed) && !bool_equals_2(true, tx_query->m_in_tx_pool) && !bool_equals_2(false, tx_query->m_is_locked); + + // check if fetching pool txs contradicted by configuration + if (tx_query->m_in_tx_pool != boost::none && tx_query->m_in_tx_pool.get() && !can_be_in_tx_pool) { + monero_utils::free(tx_query); + throw std::runtime_error("Cannot fetch pool transactions because it contradicts configuration"); + } + + // cache unique txs and blocks + std::map> tx_map; + std::map> block_map; + + auto params = std::make_shared(); + params->m_in = is_in; + params->m_out = is_out; + params->m_pool = is_pool; + params->m_pending = is_pending; + params->m_failed = is_failed; + params->m_max_height = tx_query->m_max_height; + + if (tx_query->m_min_height != boost::none) { + uint64_t min_height = tx_query->m_min_height.get(); + // TODO monero-project: wallet2::get_payments() min_height is exclusive, so manually offset to match intended range (issues #5751, #5598) + if (min_height > 0) params->m_min_height = min_height - 1; + else params->m_min_height = min_height; + } + + if (_query->m_account_index == boost::none) { + if (_query->m_subaddress_index != boost::none) throw std::runtime_error("Filter specifies a subaddress index but not an account index"); + params->m_all_accounts = true; + } else { + params->m_account_index = _query->m_account_index; + + // set subaddress indices param + params->m_subaddr_indices = _query->m_subaddress_indices; + if (_query->m_subaddress_index != boost::none && std::find(_query->m_subaddress_indices.end(), _query->m_subaddress_indices.end(), _query->m_subaddress_index.get()) != _query->m_subaddress_indices.end()) { + params->m_subaddr_indices.push_back(_query->m_subaddress_index.get()); + } + } + + // build txs using `get_transfers` + PyMoneroJsonRequest request("get_transfers", params); + auto response = m_rpc->send_json_request(request); + if (response->m_result == boost::none) throw std::runtime_error("Invalid Monero JSONRPC response"); + auto node = response->m_result.get(); + + PyMoneroTxWallet::from_property_tree_with_transfer_and_merge(node, tx_map, block_map); + + // sort txs by block height + std::vector> txs; + for (std::map>::const_iterator tx_iter = tx_map.begin(); tx_iter != tx_map.end(); tx_iter++) { + txs.push_back(tx_iter->second); + } + sort(txs.begin(), txs.end(), tx_height_less_than); + + // filter transfers + std::vector> transfers; + for (const std::shared_ptr& tx : txs) { + + // tx is not incoming/outgoing unless already set + if (tx->m_is_incoming == boost::none) tx->m_is_incoming = false; + if (tx->m_is_outgoing == boost::none) tx->m_is_outgoing = false; + + // sort incoming transfers + sort(tx->m_incoming_transfers.begin(), tx->m_incoming_transfers.end(), incoming_transfer_before); + + // collect queried transfers, erase if excluded + for (const std::shared_ptr& transfer : tx->filter_transfers(*_query)) transfers.push_back(transfer); + + // remove excluded txs from block + if (tx->m_block != boost::none && tx->m_outgoing_transfer == boost::none && tx->m_incoming_transfers.empty()) { + tx->m_block.get()->m_txs.erase(std::remove(tx->m_block.get()->m_txs.begin(), tx->m_block.get()->m_txs.end(), tx), tx->m_block.get()->m_txs.end()); // TODO, no way to use const_iterator? + } + } + MTRACE("PyMoneroWalletRpc::get_transfers() returning " << transfers.size() << " transfers"); + + // free query and return transfers + monero_utils::free(tx_query); + return transfers; +} + +std::vector> PyMoneroWalletRpc::get_outputs_aux(const monero_output_query& query) const { + MTRACE("PyMoneroWalletRpc::get_outputs_aux(query)"); + +// // log query +// if (query.m_tx_query != boost::none) { +// if ((*query.m_tx_query)->m_block == boost::none) std::cout << "Output query's tx query rooted at [tx]:" << (*query.m_tx_query)->serialize() << std::endl; +// else std::cout << "Output query's tx query rooted at [block]: " << (*(*query.m_tx_query)->m_block)->serialize() << std::endl; +// } else std::cout << "Output query: " << query.serialize() << std::endl; + + // copy and normalize query + std::shared_ptr _query; + if (query.m_tx_query == boost::none) { + std::shared_ptr query_ptr = std::make_shared(query); // convert to shared pointer for copy + _query = query_ptr->copy(query_ptr, std::make_shared()); + } else { + std::shared_ptr tx_query = query.m_tx_query.get()->copy(query.m_tx_query.get(), std::make_shared()); + if (query.m_tx_query.get()->m_output_query != boost::none && query.m_tx_query.get()->m_output_query.get().get() == &query) { + _query = tx_query->m_output_query.get(); + } else { + if (query.m_tx_query.get()->m_output_query != boost::none) throw std::runtime_error("Output query's tx query must be a circular reference or null"); + std::shared_ptr query_ptr = std::make_shared(query); // convert query to shared pointer for copy + _query = query_ptr->copy(query_ptr, std::make_shared()); + _query->m_tx_query = tx_query; + } + } + if (_query->m_tx_query == boost::none) _query->m_tx_query = std::make_shared(); + std::shared_ptr tx_query = _query->m_tx_query.get(); + + // determine account and subaddress indices to be queried + std::map> indices; + if (_query->m_account_index != boost::none) { + std::vector subaddress_indices; + if (_query->m_subaddress_index != boost::none) { + subaddress_indices.push_back(_query->m_subaddress_index.get()); + } + for (const auto& subaddress_idx : _query->m_subaddress_indices) { + subaddress_indices.push_back(subaddress_idx); + } + indices[_query->m_account_index.get()] = subaddress_indices; + } + else { + if (_query->m_subaddress_index != boost::none) throw std::runtime_error("Request specifies a subaddress index but not an account index"); + if (_query->m_subaddress_indices.empty()) throw std::runtime_error("Request specifies subaddress indices but not an account index"); + // fetch all account indices without subaddresses + indices = get_account_indices(false); + } + + // cache unique txs and blocks + std::map> tx_map; + std::map> block_map; + + // collect txs with outputs for each indicated account using `incoming_transfers` rpc call + std::string transfer_type = "all"; + if (_query->m_is_spent != boost::none) { + if (_query->m_is_spent.value() == true) transfer_type = "unavailable"; + else transfer_type = "available"; + } + + auto params = std::make_shared(transfer_type); + + for(const auto& kv : indices) { + uint32_t account_idx = kv.first; + params->m_account_index = account_idx; + params->m_subaddr_indices = kv.second; + PyMoneroJsonRequest request("incoming_transfers", params); + auto response = m_rpc->send_json_request(request); + if (response->m_result == boost::none) throw std::runtime_error("Invalid Monero JSONRPC response"); + auto node = response->m_result.get(); + + // convert response to txs with outputs and merge + PyMoneroTxWallet::from_property_tree_with_output_and_merge(node, tx_map, block_map); + } + + // sort txs by block height + std::vector> txs ; + for (std::map>::const_iterator tx_iter = tx_map.begin(); tx_iter != tx_map.end(); tx_iter++) { + txs.push_back(tx_iter->second); + } + sort(txs.begin(), txs.end(), tx_height_less_than); + + // filter and return outputs + std::vector> outputs; + for (const std::shared_ptr& tx : txs) { + + // sort outputs + sort(tx->m_outputs.begin(), tx->m_outputs.end(), vout_before); + + // collect queried outputs, erase if excluded + for (const std::shared_ptr& output : tx->filter_outputs_wallet(*_query)) outputs.push_back(output); + + // remove txs without outputs + if (tx->m_outputs.empty() && tx->m_block != boost::none) tx->m_block.get()->m_txs.erase(std::remove(tx->m_block.get()->m_txs.begin(), tx->m_block.get()->m_txs.end(), tx), tx->m_block.get()->m_txs.end()); // TODO, no way to use const_iterator? + } + + // free query and return outputs + monero_utils::free(tx_query); + return outputs; +} \ No newline at end of file diff --git a/src/cpp/wallet/py_monero_wallet_rpc.h b/src/cpp/wallet/py_monero_wallet_rpc.h index 2c668cd..94951dc 100644 --- a/src/cpp/wallet/py_monero_wallet_rpc.h +++ b/src/cpp/wallet/py_monero_wallet_rpc.h @@ -111,6 +111,10 @@ class PyMoneroWalletRpc : public PyMoneroWallet { monero_subaddress get_subaddress(const uint32_t account_idx, const uint32_t subaddress_idx) const override; monero_subaddress create_subaddress(uint32_t account_idx, const std::string& label = "") override; void set_subaddress_label(uint32_t account_idx, uint32_t subaddress_idx, const std::string& label = "") override; + std::vector> get_txs() const override; + std::vector> get_txs(const monero_tx_query& query) const override; + std::vector> get_transfers(const monero_transfer_query& query) const override; + std::vector> get_outputs(const monero_output_query& query) const override; std::string export_outputs(bool all = false) const override; int import_outputs(const std::string& outputs_hex) override; std::vector> export_key_images(bool all = false) const override; @@ -139,7 +143,9 @@ class PyMoneroWalletRpc : public PyMoneroWallet { std::string get_reserve_proof_wallet(const std::string& message) const override; std::string get_reserve_proof_account(uint32_t account_idx, uint64_t amount, const std::string& message) const override; std::shared_ptr check_reserve_proof(const std::string& address, const std::string& message, const std::string& signature) const override; + std::string get_tx_note(const std::string& tx_hash) const override; std::vector get_tx_notes(const std::vector& tx_hashes) const override; + void set_tx_note(const std::string& tx_hashes, const std::string& notes) override; void set_tx_notes(const std::vector& tx_hashes, const std::vector& notes) override; std::vector get_address_book_entries(const std::vector& indices) const override; uint64_t add_address_book_entry(const std::string& address, const std::string& description) override; @@ -186,6 +192,10 @@ class PyMoneroWalletRpc : public PyMoneroWallet { PyMoneroWalletRpc* create_wallet_from_seed(const std::shared_ptr &conf); PyMoneroWalletRpc* create_wallet_from_keys(const std::shared_ptr &config); + std::map> get_account_indices(bool get_subaddress_indices) const; + std::vector get_subaddress_indices(uint32_t account_idx) const; + std::vector> get_outputs_aux(const monero_output_query& query) const; + std::vector> get_transfers_aux(const monero_transfer_query& query) const; std::string query_key(const std::string& key_type) const; std::vector> sweep_account(const monero_tx_config &conf); void clear_address_cache(); diff --git a/tests/test_monero_daemon_rpc.py b/tests/test_monero_daemon_rpc.py index babdbf3..d2803a0 100644 --- a/tests/test_monero_daemon_rpc.py +++ b/tests/test_monero_daemon_rpc.py @@ -373,7 +373,7 @@ def test_get_txs_by_hashes(self, daemon: MoneroDaemonRpc, wallet: MoneroWalletRp # Can get transaction pool statistics #@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - @pytest.mark.skip("TODO implement monero_wallet_rpc.get_txs()") + @pytest.mark.skip("TODO implement monero_wallet_rpc.create_txs()") def test_get_tx_pool_statistics(self, daemon: MoneroDaemonRpc, wallet: MoneroWalletRpc): wallet = wallet Utils.WALLET_TX_TRACKER.wait_for_txs_to_clear_pool([wallet]) diff --git a/tests/test_monero_wallet_common.py b/tests/test_monero_wallet_common.py index caf62fc..d83e3c4 100644 --- a/tests/test_monero_wallet_common.py +++ b/tests/test_monero_wallet_common.py @@ -55,6 +55,10 @@ def parse(cls, parser: ConfigParser) -> BaseTestMoneroWallet.Config: #region Private Methods + def _setup_blockchain(self) -> None: + BlockchainUtils.setup_blockchain(TestUtils.NETWORK_TYPE) + self.fund_test_wallet() + @classmethod def _get_test_daemon(cls) -> MoneroDaemonRpc: """ @@ -128,8 +132,9 @@ def fund_test_wallet(self) -> None: return wallet = self.get_test_wallet() - MiningUtils.fund_wallet(wallet, 1) - BlockchainUtils.wait_for_blocks(11) + tx = MiningUtils.fund_wallet(wallet, 1) + if tx is not None: + BlockchainUtils.wait_for_blocks(11) self._funded = True @classmethod @@ -165,8 +170,7 @@ def wallet(self) -> MoneroWallet: @pytest.fixture(scope="class", autouse=True) def before_all(self) -> None: """Executed once before all tests""" - BlockchainUtils.setup_blockchain(TestUtils.NETWORK_TYPE) - self.fund_test_wallet() + self._setup_blockchain() # Setup and teardown of each test @pytest.fixture(autouse=True) diff --git a/tests/test_monero_wallet_rpc.py b/tests/test_monero_wallet_rpc.py index 9a19c42..4e86009 100644 --- a/tests/test_monero_wallet_rpc.py +++ b/tests/test_monero_wallet_rpc.py @@ -52,8 +52,9 @@ def _get_seed_languages(self) -> list[str]: return self.get_test_wallet().get_seed_languages() @override + @pytest.fixture(scope="class", autouse=True) def before_all(self) -> None: - super().before_all() + self._setup_blockchain() # if full tests ran, wait for full wallet's pool txs to confirm if Utils.WALLET_FULL_TESTS_RUN: Utils.clear_wallet_full_txs_pool() @@ -126,7 +127,7 @@ def test_send_with_payment_id(self, wallet: MoneroWallet) -> None: def test_send_split(self, wallet: MoneroWallet) -> None: return super().test_send_split(wallet) - @pytest.mark.not_supported + @pytest.mark.skip(reason="TODO segmentation fault") @override def test_create_then_relay(self, wallet: MoneroWallet) -> None: return super().test_create_then_relay(wallet) @@ -136,11 +137,6 @@ def test_create_then_relay(self, wallet: MoneroWallet) -> None: def test_create_then_relay_split(self, wallet: MoneroWallet) -> None: return super().test_create_then_relay_split(wallet) - @pytest.mark.skip(reason="TODO implement get_txs") - @override - def test_get_txs_wallet(self, wallet: MoneroWallet) -> None: - return super().test_get_txs_wallet(wallet) - @pytest.mark.skip(reason="TODO monero-project") @override def test_get_public_view_key(self, wallet: MoneroWallet) -> None: @@ -156,21 +152,11 @@ def test_get_public_spend_key(self, wallet: MoneroWallet) -> None: def test_wallet_equality_ground_truth(self, wallet: MoneroWallet) -> None: return super().test_wallet_equality_ground_truth(wallet) - @pytest.mark.skip(reason="TODO") - @override - def test_set_tx_note(self, wallet: MoneroWallet) -> None: - return super().test_set_tx_note(wallet) - - @pytest.mark.skip(reason="TODO") + @pytest.mark.skip(reason="TODO Enable send tests") @override def test_set_tx_notes(self, wallet: MoneroWallet) -> None: return super().test_set_tx_notes(wallet) - @pytest.mark.skip(reason="TODO") - @override - def test_export_key_images(self, wallet: MoneroWallet) -> None: - return super().test_export_key_images(wallet) - @pytest.mark.skip(reason="TODO (monero-project): https://github.com/monero-project/monero/issues/5812") @override def test_import_key_images(self, wallet: MoneroWallet) -> None: diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index 1a0ecdb..bf208f6 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -7,8 +7,8 @@ from configparser import ConfigParser from monero import ( MoneroNetworkType, MoneroWalletFull, MoneroRpcConnection, - MoneroWalletConfig, MoneroDaemonRpc, MoneroWalletRpc, MoneroWalletKeys, - MoneroWallet, MoneroRpcError + MoneroWalletConfig, MoneroDaemonRpc, MoneroWalletRpc, + MoneroWallet, MoneroRpcError, MoneroWalletKeys ) from .wallet_sync_printer import WalletSyncPrinter