diff --git a/src/wallet/monero_wallet_core.cpp b/src/wallet/monero_wallet_core.cpp index 652ac00..1835c98 100644 --- a/src/wallet/monero_wallet_core.cpp +++ b/src/wallet/monero_wallet_core.cpp @@ -82,6 +82,37 @@ namespace monero { // ----------------------- INTERNAL PRIVATE HELPERS ----------------------- + /** + * Remove query criteria which require looking up other transfers/outputs to + * fulfill query. + * + * @param query the query to decontextualize + * @return a reference to the query for convenience + */ + shared_ptr decontextualize(shared_ptr query) { + query->m_is_incoming = boost::none; + query->m_is_outgoing = boost::none; + query->m_transfer_query = boost::none; + query->m_output_query = boost::none; + return query; + } + + bool is_contextual(const monero_transfer_query& query) { + if (query.m_tx_query == boost::none) return false; + if (query.m_tx_query.get()->m_is_incoming != boost::none) return true; // requires context of all transfers + if (query.m_tx_query.get()->m_is_outgoing != boost::none) return true; + if (query.m_tx_query.get()->m_output_query != boost::none) return true; // requires context of outputs + return false; + } + + bool is_contextual(const monero_output_query& query) { + if (query.m_tx_query == boost::none) return false; + if (query.m_tx_query.get()->m_is_incoming != boost::none) return true; // requires context of all transfers + if (query.m_tx_query.get()->m_is_outgoing != boost::none) return true; + if (query.m_tx_query.get()->m_transfer_query != boost::none) return true; // requires context of transfers + return false; + } + bool bool_equals(bool val, const boost::optional& opt_val) { return opt_val == boost::none ? false : val == *opt_val; } @@ -1389,26 +1420,25 @@ namespace monero { vector> monero_wallet_core::get_txs(const monero_tx_query& query, vector& missing_tx_hashes) const { MTRACE("get_txs(query)"); - // copy and normalize tx query - shared_ptr query_ptr = make_shared(query); // convert to shared pointer for copy - shared_ptr _query = query_ptr->copy(query_ptr, make_shared()); // deep copy - if (_query->m_transfer_query == boost::none) _query->m_transfer_query = make_shared(); - if (_query->m_output_query == boost::none) _query->m_output_query = make_shared(); + // copy query + shared_ptr query_sp = make_shared(query); // convert to shared pointer + shared_ptr _query = query_sp->copy(query_sp, make_shared()); // deep copy // // log query // if (_query->m_block != boost::none) cout << "Tx query's rooted at [block]: " << _query->m_block.get()->serialize() << endl; // else cout << "Tx _query: " << _query->serialize() << endl; - // temporarily disable transfer and output queries in order to collect all tx information - shared_ptr transfer_query = _query->m_transfer_query.get(); - shared_ptr output_query = _query->m_output_query.get(); + // temporarily disable transfer and output queries in order to collect all tx context + boost::optional> transfer_query = _query->m_transfer_query; + boost::optional> output_query = _query->m_output_query; _query->m_transfer_query = boost::none; _query->m_output_query = boost::none; // fetch all transfers that meet tx query - monero_transfer_query temp_transfer_query; - temp_transfer_query.m_tx_query = make_shared(*_query); - vector> transfers = get_transfers(temp_transfer_query); + shared_ptr temp_transfer_query = make_shared(); + temp_transfer_query->m_tx_query = decontextualize(_query->copy(_query, make_shared())); + temp_transfer_query->m_tx_query.get()->m_transfer_query = temp_transfer_query; + vector> transfers = get_transfers_aux(*temp_transfer_query); // collect unique txs from transfers while retaining order vector> txs = vector>(); @@ -1428,12 +1458,11 @@ namespace monero { } // fetch and merge outputs if requested - monero_output_query temp_output_query; - temp_output_query.m_tx_query = make_shared(*_query); - if ((_query->m_include_outputs != boost::none && *_query->m_include_outputs) || !output_query->is_default()) { - - // fetch outputs - vector> outputs = get_outputs(temp_output_query); + if ((_query->m_include_outputs != boost::none && *_query->m_include_outputs) || output_query != boost::none) { + shared_ptr temp_output_query = make_shared(); + temp_output_query->m_tx_query = decontextualize(_query->copy(_query, make_shared())); + temp_output_query->m_tx_query.get()->m_output_query = temp_output_query; + vector> outputs = get_outputs_aux(*temp_output_query); // merge output txs one time while retaining order unordered_set> output_txs; @@ -1485,227 +1514,43 @@ namespace monero { } vector> monero_wallet_core::get_transfers(const monero_transfer_query& query) const { - MTRACE("monero_wallet_core::get_transfers(query)"); // // log query // if (query.m_tx_query != boost::none) { // if ((*query.m_tx_query)->m_block == boost::none) cout << "Transfer query's tx query rooted at [tx]:" << (*query.m_tx_query)->serialize() << endl; // else cout << "Transfer query's tx query rooted at [block]: " << (*(*query.m_tx_query)->m_block)->serialize() << endl; -// } +// } else cout << "Transfer query: " << query.serialize() << endl; - // copy and normalize query - shared_ptr _query; - if (query.m_tx_query == boost::none) { - shared_ptr query_ptr = 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, make_shared()); - } else { - shared_ptr tx_query = query.m_tx_query.get()->copy(query.m_tx_query.get(), make_shared()); - if (query.m_tx_query.get()->m_transfer_query != boost::none && query.m_tx_query.get()->m_transfer_query.get().get() == &query) { - _query = tx_query->m_transfer_query.get(); - } else { - if (query.m_tx_query.get()->m_transfer_query != boost::none) throw runtime_error("Transfer query's tx query must be a circular reference or null"); - shared_ptr query_ptr = make_shared(query); // convert query to shared pointer for copy - _query = query_ptr->copy(query_ptr, make_shared()); - _query->m_tx_query = tx_query; - } - } - if (_query->m_tx_query == boost::none) _query->m_tx_query = make_shared(); - shared_ptr tx_query = _query->m_tx_query.get(); - tx_query->m_transfer_query = boost::none; // break circular link for meets_criteria() + // get transfers directly if query does not require tx context (e.g. other transfers, outputs) + if (!is_contextual(query)) return get_transfers_aux(query); - // build parameters for m_w2->get_payments() - uint64_t min_height = tx_query->m_min_height == boost::none ? 0 : *tx_query->m_min_height; - uint64_t max_height = tx_query->m_max_height == boost::none ? CRYPTONOTE_MAX_BLOCK_NUMBER : min((uint64_t) CRYPTONOTE_MAX_BLOCK_NUMBER, *tx_query->m_max_height); - if (min_height > 0) min_height--; // TODO monero core: wallet2::get_payments() m_min_height is exclusive, so manually offset to match intended range (issues 5751, #5598) - 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]); - } - - // check if pool txs explicitly requested without daemon connection - if (tx_query->m_in_tx_pool != boost::none && tx_query->m_in_tx_pool.get() && !is_connected()) { - throw runtime_error("Cannot fetch pool transactions because wallet has no daemon connection"); - } - - // translate from monero_tx_query to in, out, pending, pool, failed terminology used by monero-wallet-rpc - bool can_be_confirmed = !bool_equals(false, tx_query->m_is_confirmed) && !bool_equals(true, tx_query->m_in_tx_pool) && !bool_equals(true, tx_query->m_is_failed) && !bool_equals(false, tx_query->m_is_relayed); - bool can_be_in_tx_pool = is_connected() && !bool_equals(true, tx_query->m_is_confirmed) && !bool_equals(false, tx_query->m_in_tx_pool) && !bool_equals(true, tx_query->m_is_failed) && !bool_equals(false, tx_query->m_is_relayed) && tx_query->get_height() == boost::none && tx_query->m_min_height == boost::none; - bool can_be_incoming = !bool_equals(false, _query->m_is_incoming) && !bool_equals(true, _query->is_outgoing()) && !bool_equals(true, _query->m_has_destinations); - bool can_be_outgoing = !bool_equals(false, _query->is_outgoing()) && !bool_equals(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(false, tx_query->m_is_failed) && !bool_equals(true, tx_query->m_is_confirmed) && !bool_equals(true, tx_query->m_in_tx_pool); - - // cache unique txs and blocks - uint64_t height = get_height(); - map> tx_map; - map> block_map; - - // get confirmed incoming transfers - if (is_in) { - std::list> payments; - m_w2->get_payments(payments, min_height, max_height, account_index, subaddress_indices); - for (std::list>::const_iterator i = payments.begin(); i != payments.end(); ++i) { - shared_ptr tx = build_tx_with_incoming_transfer(*m_w2, height, i->first, i->second); - merge_tx(tx, tx_map, block_map, false); - } - } - - // get confirmed outgoing transfers - if (is_out) { - std::list> payments; - m_w2->get_payments_out(payments, min_height, max_height, account_index, subaddress_indices); - for (std::list>::const_iterator i = payments.begin(); i != payments.end(); ++i) { - shared_ptr tx = build_tx_with_outgoing_transfer(*m_w2, height, i->first, i->second); - merge_tx(tx, tx_map, block_map, false); - } - } - - // get unconfirmed or failed outgoing transfers - if (is_pending || is_failed) { - std::list> upayments; - m_w2->get_unconfirmed_payments_out(upayments, account_index, subaddress_indices); - for (std::list>::const_iterator i = upayments.begin(); i != upayments.end(); ++i) { - shared_ptr tx = build_tx_with_outgoing_transfer_unconfirmed(*m_w2, i->first, i->second); - if (tx_query->m_is_failed != boost::none && tx_query->m_is_failed.get() != tx->m_is_failed.get()) continue; // skip merging if tx excluded - merge_tx(tx, tx_map, block_map, false); - } - } - - // get unconfirmed incoming transfers - if (is_pool) { - - // update pool state TODO monero-core: this should be encapsulated in wallet when unconfirmed transfers queried - std::vector> process_txs; - m_w2->update_pool_state(process_txs); - if (!process_txs.empty()) m_w2->process_pool_state(process_txs); - - std::list> payments; - m_w2->get_unconfirmed_payments(payments, account_index, subaddress_indices); - for (std::list>::const_iterator i = payments.begin(); i != payments.end(); ++i) { - shared_ptr tx = build_tx_with_incoming_transfer_unconfirmed(*m_w2, i->first, i->second); - merge_tx(tx, tx_map, block_map, false); - } - } - - // sort txs by block height - vector> txs ; - for (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 transfers + // otherwise get txs with full models to fulfill query vector> transfers; - for (const 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 transfers - sort(tx->m_incoming_transfers.begin(), tx->m_incoming_transfers.end(), incoming_transfer_before); - - // collect outgoing transfer, erase if excluded TODO: js does not erase excluded data, port to js - if (tx->m_outgoing_transfer != boost::none && _query->meets_criteria(tx->m_outgoing_transfer.get().get())) transfers.push_back(tx->m_outgoing_transfer.get()); - else tx->m_outgoing_transfer = boost::none; - - // collect incoming transfers, erase if excluded - vector>::iterator iter = tx->m_incoming_transfers.begin(); - while (iter != tx->m_incoming_transfers.end()) { - if (_query->meets_criteria((*iter).get())) { - transfers.push_back(*iter); - iter++; - } else { - iter = tx->m_incoming_transfers.erase(iter); - } - } - - // 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? + for (const shared_ptr& tx : get_txs(*(query.m_tx_query.get()))) { + for (const shared_ptr& transfer : tx->filter_transfers(query)) { // collect queried transfers, erase if excluded + transfers.push_back(transfer); } } - MTRACE("monero_wallet_core.cpp get_transfers() returning " << transfers.size() << " transfers"); - return transfers; } vector> monero_wallet_core::get_outputs(const monero_output_query& query) const { - MTRACE("monero_wallet_core::get_outputs(query)"); // // log query -// MTRACE("Output query: " << query.serialize()); // if (query.m_tx_query != boost::none) { -// if ((*query.m_tx_query)->m_block == boost::none) MTRACE("Output query's tx query rooted at [tx]:" << (*query.m_tx_query)->serialize()); -// else MTRACE("Output query's tx query rooted at [block]: " << (*(*query.m_tx_query)->m_block)->serialize()); -// } +// if ((*query.m_tx_query)->m_block == boost::none) cout << "Output query's tx query rooted at [tx]:" << (*query.m_tx_query)->serialize() << endl; +// else cout << "Output query's tx query rooted at [block]: " << (*(*query.m_tx_query)->m_block)->serialize() << endl; +// } else cout << "Output query: " << query.serialize() << endl; - // copy and normalize query - shared_ptr _query; - if (query.m_tx_query == boost::none) { - shared_ptr query_ptr = make_shared(query); // convert to shared pointer for copy - _query = query_ptr->copy(query_ptr, make_shared()); - } else { - shared_ptr tx_query = query.m_tx_query.get()->copy(query.m_tx_query.get(), 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 runtime_error("Output query's tx query must be a circular reference or null"); - shared_ptr query_ptr = make_shared(query); // convert query to shared pointer for copy - _query = query_ptr->copy(query_ptr, make_shared()); - _query->m_tx_query = tx_query; - } - } - if (_query->m_tx_query == boost::none) _query->m_tx_query = make_shared(); - shared_ptr tx_query = _query->m_tx_query.get(); - tx_query->m_output_query = boost::none; // break circular link for meets_criteria() + // get outputs directly if query does not require tx context (e.g. other outputs, transfers) + if (!is_contextual(query)) return get_outputs_aux(query); - // get output data from wallet2 - tools::wallet2::transfer_container outputs_w2; - m_w2->get_transfers(outputs_w2); - - // cache unique txs and blocks - map> tx_map; - map> block_map; - for (const auto& output_w2 : outputs_w2) { - // TODO: skip tx building if m_w2 output excluded by indices, etc - shared_ptr tx = build_tx_with_vout(*m_w2, output_w2); - merge_tx(tx, tx_map, block_map, false); - } - - // sort txs by block height - vector> txs ; - for (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 + // otherwise get txs with full models to fulfill query vector> outputs; - for (const shared_ptr& tx : txs) { - - // sort outputs - sort(tx->m_outputs.begin(), tx->m_outputs.end(), vout_before); - - // collect queried outputs, remove excluded outputs - vector>::iterator voutIter = tx->m_outputs.begin(); - while (voutIter != tx->m_outputs.end()) { - shared_ptr vout_wallet = static_pointer_cast(*voutIter); - if (_query->meets_criteria(vout_wallet.get())) { - outputs.push_back(vout_wallet); - voutIter++; - } else { - voutIter = tx->m_outputs.erase(voutIter); // remove excluded outputs - } + for (const shared_ptr& tx : get_txs(*(query.m_tx_query.get()))) { + for (const shared_ptr& output : tx->filter_outputs_wallet(query)) { // collect queried outputs, erase if excluded + 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? } return outputs; } @@ -3203,6 +3048,201 @@ namespace monero { #endif } + vector> monero_wallet_core::get_transfers_aux(const monero_transfer_query& query) const { + MTRACE("monero_wallet_core::get_transfers(query)"); + +// // log query +// if (query.m_tx_query != boost::none) { +// if ((*query.m_tx_query)->m_block == boost::none) cout << "Transfer query's tx query rooted at [tx]:" << (*query.m_tx_query)->serialize() << endl; +// else cout << "Transfer query's tx query rooted at [block]: " << (*(*query.m_tx_query)->m_block)->serialize() << endl; +// } else cout << "Transfer query: " << query.serialize() << endl; + + // copy and normalize query + shared_ptr _query; + if (query.m_tx_query == boost::none) { + shared_ptr query_ptr = 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, make_shared()); + _query->m_tx_query = make_shared(); + _query->m_tx_query.get()->m_transfer_query = _query; + } else { + shared_ptr tx_query = query.m_tx_query.get()->copy(query.m_tx_query.get(), make_shared()); + _query = tx_query->m_transfer_query.get(); + } + shared_ptr tx_query = _query->m_tx_query.get(); + + // build parameters for m_w2->get_payments() + uint64_t min_height = tx_query->m_min_height == boost::none ? 0 : *tx_query->m_min_height; + uint64_t max_height = tx_query->m_max_height == boost::none ? CRYPTONOTE_MAX_BLOCK_NUMBER : min((uint64_t) CRYPTONOTE_MAX_BLOCK_NUMBER, *tx_query->m_max_height); + if (min_height > 0) min_height--; // TODO monero core: wallet2::get_payments() m_min_height is exclusive, so manually offset to match intended range (issues 5751, #5598) + 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]); + } + + // check if pool txs explicitly requested without daemon connection + if (tx_query->m_in_tx_pool != boost::none && tx_query->m_in_tx_pool.get() && !is_connected()) { + throw runtime_error("Cannot fetch pool transactions because wallet has no daemon connection"); + } + + // translate from monero_tx_query to in, out, pending, pool, failed terminology used by monero-wallet-rpc + bool can_be_confirmed = !bool_equals(false, tx_query->m_is_confirmed) && !bool_equals(true, tx_query->m_in_tx_pool) && !bool_equals(true, tx_query->m_is_failed) && !bool_equals(false, tx_query->m_is_relayed); + bool can_be_in_tx_pool = is_connected() && !bool_equals(true, tx_query->m_is_confirmed) && !bool_equals(false, tx_query->m_in_tx_pool) && !bool_equals(true, tx_query->m_is_failed) && !bool_equals(false, tx_query->m_is_relayed) && tx_query->get_height() == boost::none && tx_query->m_min_height == boost::none; + bool can_be_incoming = !bool_equals(false, _query->m_is_incoming) && !bool_equals(true, _query->is_outgoing()) && !bool_equals(true, _query->m_has_destinations); + bool can_be_outgoing = !bool_equals(false, _query->is_outgoing()) && !bool_equals(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(false, tx_query->m_is_failed) && !bool_equals(true, tx_query->m_is_confirmed) && !bool_equals(true, tx_query->m_in_tx_pool); + + // cache unique txs and blocks + uint64_t height = get_height(); + map> tx_map; + map> block_map; + + // get confirmed incoming transfers + if (is_in) { + std::list> payments; + m_w2->get_payments(payments, min_height, max_height, account_index, subaddress_indices); + for (std::list>::const_iterator i = payments.begin(); i != payments.end(); ++i) { + shared_ptr tx = build_tx_with_incoming_transfer(*m_w2, height, i->first, i->second); + merge_tx(tx, tx_map, block_map, false); + } + } + + // get confirmed outgoing transfers + if (is_out) { + std::list> payments; + m_w2->get_payments_out(payments, min_height, max_height, account_index, subaddress_indices); + for (std::list>::const_iterator i = payments.begin(); i != payments.end(); ++i) { + shared_ptr tx = build_tx_with_outgoing_transfer(*m_w2, height, i->first, i->second); + merge_tx(tx, tx_map, block_map, false); + } + } + + // get unconfirmed or failed outgoing transfers + if (is_pending || is_failed) { + std::list> upayments; + m_w2->get_unconfirmed_payments_out(upayments, account_index, subaddress_indices); + for (std::list>::const_iterator i = upayments.begin(); i != upayments.end(); ++i) { + shared_ptr tx = build_tx_with_outgoing_transfer_unconfirmed(*m_w2, i->first, i->second); + if (tx_query->m_is_failed != boost::none && tx_query->m_is_failed.get() != tx->m_is_failed.get()) continue; // skip merging if tx excluded + merge_tx(tx, tx_map, block_map, false); + } + } + + // get unconfirmed incoming transfers + if (is_pool) { + + // update pool state TODO monero-core: this should be encapsulated in wallet when unconfirmed transfers queried + std::vector> process_txs; + m_w2->update_pool_state(process_txs); + if (!process_txs.empty()) m_w2->process_pool_state(process_txs); + + std::list> payments; + m_w2->get_unconfirmed_payments(payments, account_index, subaddress_indices); + for (std::list>::const_iterator i = payments.begin(); i != payments.end(); ++i) { + shared_ptr tx = build_tx_with_incoming_transfer_unconfirmed(*m_w2, i->first, i->second); + merge_tx(tx, tx_map, block_map, false); + } + } + + // sort txs by block height + vector> txs ; + for (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 transfers + vector> transfers; + for (const 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 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("monero_wallet_core.cpp get_transfers() returning " << transfers.size() << " transfers"); + + return transfers; + } + + vector> monero_wallet_core::get_outputs_aux(const monero_output_query& query) const { + MTRACE("monero_wallet_core::get_outputs_aux(query)"); + +// // log query +// if (query.m_tx_query != boost::none) { +// if ((*query.m_tx_query)->m_block == boost::none) cout << "Output query's tx query rooted at [tx]:" << (*query.m_tx_query)->serialize() << endl; +// else cout << "Output query's tx query rooted at [block]: " << (*(*query.m_tx_query)->m_block)->serialize() << endl; +// } else cout << "Output query: " << query.serialize() << endl; + + // copy and normalize query + shared_ptr _query; + if (query.m_tx_query == boost::none) { + shared_ptr query_ptr = make_shared(query); // convert to shared pointer for copy + _query = query_ptr->copy(query_ptr, make_shared()); + } else { + shared_ptr tx_query = query.m_tx_query.get()->copy(query.m_tx_query.get(), 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 runtime_error("Output query's tx query must be a circular reference or null"); + shared_ptr query_ptr = make_shared(query); // convert query to shared pointer for copy + _query = query_ptr->copy(query_ptr, make_shared()); + _query->m_tx_query = tx_query; + } + } + if (_query->m_tx_query == boost::none) _query->m_tx_query = make_shared(); + + // get output data from wallet2 + tools::wallet2::transfer_container outputs_w2; + m_w2->get_transfers(outputs_w2); + + // cache unique txs and blocks + map> tx_map; + map> block_map; + for (const auto& output_w2 : outputs_w2) { + // TODO: skip tx building if m_w2 output excluded by indices, etc + shared_ptr tx = build_tx_with_vout(*m_w2, output_w2); + merge_tx(tx, tx_map, block_map, false); + } + + // sort txs by block height + vector> txs ; + for (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 + vector> outputs; + for (const 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 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? + } + return outputs; + } + // private helper to initialize subaddresses using transfer details vector monero_wallet_core::get_subaddresses_aux(const uint32_t account_idx, const vector& subaddress_indices, const vector& transfers) const { vector subaddresses; diff --git a/src/wallet/monero_wallet_core.h b/src/wallet/monero_wallet_core.h index ae816af..b43f242 100644 --- a/src/wallet/monero_wallet_core.h +++ b/src/wallet/monero_wallet_core.h @@ -292,6 +292,8 @@ namespace monero { void init_common(); vector get_subaddresses_aux(uint32_t account_idx, const vector& subaddress_indices, const vector& transfers) const; + vector> get_transfers_aux(const monero_transfer_query& query) const; + vector> get_outputs_aux(const monero_output_query& query) const; vector> sweep_account(const monero_tx_config& config); // sweeps unlocked funds within an account; private helper to sweep_unlocked() // blockchain sync management diff --git a/src/wallet/monero_wallet_model.cpp b/src/wallet/monero_wallet_model.cpp index ef723b4..8ba0edd 100644 --- a/src/wallet/monero_wallet_model.cpp +++ b/src/wallet/monero_wallet_model.cpp @@ -85,6 +85,7 @@ namespace monero { shared_ptr tx_query = make_shared(); monero_tx_query::from_property_tree(it2->second, tx_query); block->m_txs.push_back(tx_query); + tx_query->m_block = block; } } } @@ -227,9 +228,9 @@ namespace monero { if (!src->m_incoming_transfers.empty()) { tgt->m_incoming_transfers = vector>(); for (const shared_ptr& transfer : src->m_incoming_transfers) { - shared_ptr transferCopy = transfer->copy(transfer, make_shared()); - transferCopy->m_tx = tgt; - tgt->m_incoming_transfers.push_back(transferCopy); + shared_ptr transfer_copy = transfer->copy(transfer, make_shared()); + transfer_copy->m_tx = tgt; + tgt->m_incoming_transfers.push_back(transfer_copy); } } if (src->m_outgoing_transfer != boost::none) { @@ -288,6 +289,67 @@ namespace monero { m_extra_hex = gen_utils::reconcile(m_extra_hex, other->m_extra_hex); } + vector> monero_tx_wallet::get_transfers() const { + monero_transfer_query query; + return get_transfers(query); + } + + vector> monero_tx_wallet::get_transfers(const monero_transfer_query& query) const { + vector> transfers; + if (m_outgoing_transfer != boost::none && query.meets_criteria(m_outgoing_transfer.get().get())) transfers.push_back(m_outgoing_transfer.get()); + for (const shared_ptr& transfer : m_incoming_transfers) if (query.meets_criteria(transfer.get())) transfers.push_back(transfer); + return transfers; + } + + vector> monero_tx_wallet::filter_transfers(const monero_transfer_query& query) { + + // collect outgoing transfer, erase if excluded + vector> transfers; + if (m_outgoing_transfer != boost::none && query.meets_criteria(m_outgoing_transfer.get().get())) transfers.push_back(m_outgoing_transfer.get()); + else m_outgoing_transfer = boost::none; + + // collect incoming transfers, erase if excluded + vector>::iterator iter = m_incoming_transfers.begin(); + while (iter != m_incoming_transfers.end()) { + if (query.meets_criteria((*iter).get())) { + transfers.push_back(*iter); + iter++; + } else { + iter = m_incoming_transfers.erase(iter); + } + } + return transfers; + } + + vector> monero_tx_wallet::get_outputs_wallet() const { + monero_output_query query; + return get_outputs_wallet(query); + } + + vector> monero_tx_wallet::get_outputs_wallet(const monero_output_query& query) const { + vector> outputs; + for (const shared_ptr& output : m_outputs) { + shared_ptr output_wallet = dynamic_pointer_cast(output); + if (query.meets_criteria(output_wallet.get())) outputs.push_back(output_wallet); + } + return outputs; + } + + vector> monero_tx_wallet::filter_outputs_wallet(const monero_output_query& query) { + vector> outputs; + vector>::iterator iter = m_outputs.begin(); + while (iter != m_outputs.end()) { + shared_ptr output_wallet = dynamic_pointer_cast(*iter); + if (query.meets_criteria(output_wallet.get())) { + outputs.push_back(output_wallet); + iter++; + } else { + iter = m_outputs.erase(iter); + } + } + return outputs; + } + // -------------------------- MONERO TX QUERY ----------------------------- rapidjson::Value monero_tx_query::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { @@ -336,10 +398,12 @@ namespace monero { else if (key == string("transferQuery")) { tx_query->m_transfer_query = make_shared(); monero_transfer_query::from_property_tree(it->second, tx_query->m_transfer_query.get()); + tx_query->m_transfer_query.get()->m_tx_query = tx_query; } else if (key == string("outputQuery")) { tx_query->m_output_query = make_shared(); monero_output_query::from_property_tree(it->second, tx_query->m_output_query.get()); + tx_query->m_output_query.get()->m_tx_query = tx_query; } } } @@ -386,13 +450,19 @@ namespace monero { tgt->m_min_height = src->m_min_height; tgt->m_max_height = src->m_max_height; tgt->m_include_outputs = src->m_include_outputs; - if (src->m_transfer_query != boost::none) tgt->m_transfer_query = src->m_transfer_query.get()->copy(src->m_transfer_query.get(), make_shared()); - if (src->m_output_query != boost::none) tgt->m_output_query = src->m_output_query.get()->copy(src->m_output_query.get(), make_shared()); + if (src->m_transfer_query != boost::none) { + tgt->m_transfer_query = src->m_transfer_query.get()->copy(src->m_transfer_query.get(), make_shared()); + tgt->m_transfer_query.get()->m_tx_query = tgt; + } + if (src->m_output_query != boost::none) { + tgt->m_output_query = src->m_output_query.get()->copy(src->m_output_query.get(), make_shared()); + tgt->m_output_query.get()->m_tx_query = tgt; + } return tgt; }; - bool monero_tx_query::meets_criteria(monero_tx_wallet* tx) const { - if (tx == nullptr) return false; + bool monero_tx_query::meets_criteria(monero_tx_wallet* tx, bool query_children) const { + if (tx == nullptr) throw runtime_error("nullptr given to monero_tx_query::meets_criteria()"); // filter on tx if (m_hash != boost::none && m_hash != tx->m_hash) return false; @@ -404,35 +474,6 @@ namespace monero { if (m_is_miner_tx != boost::none && m_is_miner_tx != tx->m_is_miner_tx) return false; if (m_is_locked != boost::none && m_is_locked != tx->m_is_locked) return false; - // at least one transfer must meet transfer query if defined - if (m_transfer_query != boost::none) { - bool matchFound = false; - if (tx->m_outgoing_transfer != boost::none && m_transfer_query.get()->meets_criteria(tx->m_outgoing_transfer.get().get())) matchFound = true; - else if (!tx->m_incoming_transfers.empty()) { - for (const shared_ptr& incoming_transfer : tx->m_incoming_transfers) { - if (m_transfer_query.get()->meets_criteria(incoming_transfer.get())) { - matchFound = true; - break; - } - } - } - if (!matchFound) return false; - } - - // at least one output must meet output query if defined - if (m_output_query != boost::none && !m_output_query.get()->is_default()) { - if (tx->m_outputs.empty()) return false; - bool matchFound = false; - for (const shared_ptr& output : tx->m_outputs) { - shared_ptr vout_wallet = static_pointer_cast(output); - if (m_output_query.get()->meets_criteria(vout_wallet.get())) { - matchFound = true; - break; - } - } - if (!matchFound) return false; - } - // filter on having a payment id if (m_has_payment_id != boost::none) { if (*m_has_payment_id && tx->m_payment_id == boost::none) return false; @@ -453,6 +494,38 @@ namespace monero { if (m_min_height != boost::none && (txHeight == boost::none || *txHeight < *m_min_height)) return false; if (m_max_height != boost::none && (txHeight == boost::none || *txHeight > *m_max_height)) return false; + // done if not querying transfers or outputs + if (!query_children) return true; + + // at least one transfer must meet transfer query if defined + if (m_transfer_query != boost::none) { + bool match_found = false; + if (tx->m_outgoing_transfer != boost::none && m_transfer_query.get()->meets_criteria(tx->m_outgoing_transfer.get().get())) match_found = true; + else if (!tx->m_incoming_transfers.empty()) { + for (const shared_ptr& incoming_transfer : tx->m_incoming_transfers) { + if (m_transfer_query.get()->meets_criteria(incoming_transfer.get(), false)) { + match_found = true; + break; + } + } + } + if (!match_found) return false; + } + + // at least one output must meet output query if defined + if (m_output_query != boost::none) { + if (tx->m_outputs.empty()) return false; + bool match_found = false; + for (const shared_ptr& output : tx->m_outputs) { + shared_ptr vout_wallet = static_pointer_cast(output); + if (m_output_query.get()->meets_criteria(vout_wallet.get(), false)) { + match_found = true; + break; + } + } + if (!match_found) return false; + } + // transaction meets query criteria return true; } @@ -678,7 +751,7 @@ namespace monero { m_destinations = gen_utils::reconcile(m_destinations, other->m_destinations); } - // ----------------------- MONERO TRANSFER REQUEST -------------------------- + // ----------------------- MONERO TRANSFER QUERY -------------------------- rapidjson::Value monero_transfer_query::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { @@ -746,9 +819,8 @@ namespace monero { // get / create transfer query shared_ptr transfer_query = tx_query->m_transfer_query == boost::none ? make_shared() : *tx_query->m_transfer_query; - // transfer query references tx query but not the other way around to avoid circular loop // TODO: could add check within meetsCriterias() + // set transfer query's tx query transfer_query->m_tx_query = tx_query; - tx_query->m_transfer_query = boost::none; // return deserialized query return transfer_query; @@ -781,13 +853,12 @@ namespace monero { return tgt; }; - bool monero_transfer_query::meets_criteria(monero_transfer* transfer) const { - if (transfer == nullptr) throw runtime_error("transfer is null"); - if (m_tx_query != boost::none && (*m_tx_query)->m_transfer_query != boost::none) throw runtime_error("Transfer query's tx query cannot have a circular transfer query"); // TODO: could auto detect and handle this. port to java/js + bool monero_transfer_query::meets_criteria(monero_transfer* transfer, bool query_parent) const { + if (transfer == nullptr) throw runtime_error("nullptr given to monero_transfer_query::meets_criteria()"); // filter on common fields if (is_incoming() != boost::none && *is_incoming() != *transfer->is_incoming()) return false; - if (is_outgoing() != boost::none && is_outgoing() != transfer->is_outgoing()) return false; + if (is_outgoing() != boost::none && *is_outgoing() != *transfer->is_outgoing()) return false; if (m_amount != boost::none && *m_amount != *transfer->m_amount) return false; if (m_account_index != boost::none && *m_account_index != *transfer->m_account_index) return false; @@ -849,7 +920,7 @@ namespace monero { if (inTransfer == nullptr && out_transfer == nullptr) throw runtime_error("Transfer must be monero_incoming_transfer or monero_outgoing_transfer"); // filter with tx query - if (m_tx_query != boost::none && !(*m_tx_query)->meets_criteria(transfer->m_tx.get())) return false; + if (query_parent && m_tx_query != boost::none && !(*m_tx_query)->meets_criteria(transfer->m_tx.get(), false)) return false; return true; } @@ -925,7 +996,7 @@ namespace monero { m_is_frozen = gen_utils::reconcile(m_is_frozen, other->m_is_frozen); } - // ------------------------ MONERO OUTPUT REQUEST --------------------------- + // ------------------------ MONERO OUTPUT QUERY --------------------------- rapidjson::Value monero_output_query::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { @@ -976,20 +1047,13 @@ namespace monero { // get / create output query shared_ptr output_query = tx_query->m_output_query == boost::none ? make_shared() : *tx_query->m_output_query; - // output query references tx query but not the other way around to avoid circular loop // TODO: could add check within meetsCriterias(), TODO: move this temp hack to the caller + // set output query's tx query output_query->m_tx_query = tx_query; - tx_query->m_output_query = boost::none; // return deserialized query return output_query; } - // initialize static empty output for is_default() check - const unique_ptr monero_output_query::M_EMPTY_OUTPUT = unique_ptr(new monero_output_wallet()); - bool monero_output_query::is_default() const { - return meets_criteria(monero_output_query::M_EMPTY_OUTPUT.get()); - } - shared_ptr monero_output_query::copy(const shared_ptr& src, const shared_ptr& tgt) const { return monero_output_query::copy(static_pointer_cast(src), static_pointer_cast(tgt)); }; @@ -1009,10 +1073,12 @@ namespace monero { if (!src->m_subaddress_indices.empty()) tgt->m_subaddress_indices = vector(src->m_subaddress_indices); tgt->m_min_amount = src->m_min_amount; tgt->m_max_amount = src->m_max_amount; + tgt->m_tx_query = src->m_tx_query; return tgt; }; - bool monero_output_query::meets_criteria(monero_output_wallet* output) const { + bool monero_output_query::meets_criteria(monero_output_wallet* output, bool query_parent) const { + if (output == nullptr) throw runtime_error("nullptr given to monero_output_query::meets_criteria()"); // filter on output if (m_account_index != boost::none && (output->m_account_index == boost::none || *m_account_index != *output->m_account_index)) return false; @@ -1033,7 +1099,7 @@ namespace monero { if (m_max_amount != boost::none && (output->m_amount == boost::none || output->m_amount.get() > m_max_amount.get())) return false; // filter with tx query - if (m_tx_query != boost::none && !(*m_tx_query)->meets_criteria(static_pointer_cast(output->m_tx).get())) return false; + if (query_parent && m_tx_query != boost::none && !(*m_tx_query)->meets_criteria(static_pointer_cast(output->m_tx).get(), false)) return false; // output meets query return true; @@ -1128,7 +1194,7 @@ namespace monero { else if (priority_num == 1) config->m_priority = monero_tx_priority::UNIMPORTANT; else if (priority_num == 2) config->m_priority = monero_tx_priority::NORMAL; else if (priority_num == 3) config->m_priority = monero_tx_priority::ELEVATED; - else throw new runtime_error("Invalid priority number: " + std::to_string(priority_num)); + else throw runtime_error("Invalid priority number: " + std::to_string(priority_num)); } else if (key == string("ringSize")) config->m_ring_size = it->second.get_value(); else if (key == string("fee")) config->m_fee = it->second.get_value(); diff --git a/src/wallet/monero_wallet_model.h b/src/wallet/monero_wallet_model.h index 08d253b..900e3ac 100644 --- a/src/wallet/monero_wallet_model.h +++ b/src/wallet/monero_wallet_model.h @@ -125,6 +125,8 @@ namespace monero { /** * Models a base transfer of funds to or from the wallet. + * + * TODO: m_is_incoming for api consistency */ struct monero_transfer : serializable_struct { shared_ptr m_tx; @@ -195,7 +197,7 @@ namespace monero { shared_ptr copy(const shared_ptr& src, const shared_ptr& tgt) const; shared_ptr copy(const shared_ptr& src, const shared_ptr& tgt) const; boost::optional is_incoming() const; - bool meets_criteria(monero_transfer* transfer) const; + bool meets_criteria(monero_transfer* transfer, bool query_parent = true) const; }; /** @@ -231,14 +233,10 @@ namespace monero { rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const; static void from_property_tree(const boost::property_tree::ptree& node, const shared_ptr& output_query); static shared_ptr deserialize_from_block(const string& output_query_json); - bool is_default() const; shared_ptr copy(const shared_ptr& src, const shared_ptr& tgt) const; shared_ptr copy(const shared_ptr& src, const shared_ptr& tgt) const; // TODO: necessary to override all super classes? shared_ptr copy(const shared_ptr& src, const shared_ptr& tgt) const; - bool meets_criteria(monero_output_wallet* output) const; - - private: - static const unique_ptr M_EMPTY_OUTPUT; + bool meets_criteria(monero_output_wallet* output, bool query_parent = true) const; }; /** @@ -265,6 +263,12 @@ namespace monero { shared_ptr copy(const shared_ptr& src, const shared_ptr& tgt) const; void merge(const shared_ptr& self, const shared_ptr& other); void merge(const shared_ptr& self, const shared_ptr& other); + vector> get_transfers() const; + vector> get_transfers(const monero_transfer_query& query) const; + vector> filter_transfers(const monero_transfer_query& query); + vector> get_outputs_wallet() const; + vector> get_outputs_wallet(const monero_output_query& query) const; + vector> filter_outputs_wallet(const monero_output_query& query); }; /** @@ -291,7 +295,7 @@ namespace monero { shared_ptr copy(const shared_ptr& src, const shared_ptr& tgt) const; shared_ptr copy(const shared_ptr& src, const shared_ptr& tgt) const; // TODO: necessary to override all super classes? shared_ptr copy(const shared_ptr& src, const shared_ptr& tgt) const; - bool meets_criteria(monero_tx_wallet* tx) const; + bool meets_criteria(monero_tx_wallet* tx, bool query_children = true) const; }; /** diff --git a/test/sample_code.cpp b/test/sample_code.cpp index 2accc41..e60a18a 100644 --- a/test/sample_code.cpp +++ b/test/sample_code.cpp @@ -55,9 +55,10 @@ int main(int argc, const char* argv[]) { monero_tx_query tx_query; tx_query.m_hash = "314a0f1375db31cea4dac4e0a51514a6282b43792269b3660166d4d2b46437ca"; shared_ptr tx = wallet_restored->get_txs(tx_query)[0]; - for (const shared_ptr in_transfer : tx->m_incoming_transfers) { - uint64_t in_amount = in_transfer->m_amount.get(); - int account_index = in_transfer->m_account_index.get(); + for (const shared_ptr transfer : tx->get_transfers()) { + bool is_incoming = transfer->is_incoming().get(); + uint64_t in_amount = transfer->m_amount.get(); + int account_index = transfer->m_account_index.get(); } // query incoming transfers to account 1