diff --git a/CMakeLists.txt b/CMakeLists.txt index b69db00c..9821e696 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -275,8 +275,10 @@ set( src/daemon/monero_daemon_model.cpp src/daemon/monero_daemon.cpp src/wallet/monero_wallet_model.cpp + src/wallet/monero_wallet_light_model.cpp src/wallet/monero_wallet_keys.cpp src/wallet/monero_wallet_full.cpp + src/wallet/monero_wallet_light.cpp ) if (BUILD_LIBRARY) @@ -357,9 +359,12 @@ endif() src/utils/monero_utils.h DESTINATION include/utils) INSTALL(FILES src/wallet/monero_wallet_full.h + src/wallet/monero_wallet_light.h src/wallet/monero_wallet.h src/wallet/monero_wallet_keys.h src/wallet/monero_wallet_model.h + src/wallet/monero_wallet_light_model.h + src/wallet/monero_wallet_utils.h DESTINATION include/wallet) INSTALL(TARGETS monero-cpp RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT Runtime diff --git a/src/daemon/monero_daemon_model.cpp b/src/daemon/monero_daemon_model.cpp index c5414b1f..0b2ec569 100644 --- a/src/daemon/monero_daemon_model.cpp +++ b/src/daemon/monero_daemon_model.cpp @@ -447,7 +447,7 @@ namespace monero { if (!src->m_output_indices.empty()) tgt->m_output_indices = std::vector(src->m_output_indices); tgt->m_metadata = src->m_metadata; tgt->m_common_tx_sets = src->m_common_tx_sets; - if (!src->m_extra.empty()) throw std::runtime_error("extra deep copy not implemented"); // TODO: implement extra + if (!src->m_extra.empty()) tgt->m_extra = std::vector(src->m_extra); tgt->m_rct_signatures = src->m_rct_signatures; tgt->m_rct_sig_prunable = src->m_rct_sig_prunable; tgt->m_is_kept_by_block = src->m_is_kept_by_block; diff --git a/src/utils/gen_utils.h b/src/utils/gen_utils.h index 35c7f7cc..42a5aa8f 100644 --- a/src/utils/gen_utils.h +++ b/src/utils/gen_utils.h @@ -58,6 +58,8 @@ #include #include #include +#include +#include #include #include #include "include_base_utils.h" @@ -80,6 +82,52 @@ namespace gen_utils return boost::uuids::to_string(uuid); } + static bool is_uint64_t(const std::string& str) { + try { + size_t sz; + std::stol(str, &sz); + return sz == str.size(); + } + catch (const std::invalid_argument&) { + // if no conversion could be performed. + return false; + } + catch (const std::out_of_range&) { + // if the converted value would fall out of the range of the result type. + return false; + } + } + + static uint64_t uint64_t_cast(const std::string& str) { + if (!is_uint64_t(str)) { + throw std::out_of_range("String provided is not a valid uint64_t"); + } + + uint64_t value; + + std::istringstream itr(str); + + itr >> value; + + return value; + } + + static uint64_t timestamp_to_epoch(const std::string& iso_timestamp) { + // ISO 8601 + std::string timestamp = boost::replace_all_copy(iso_timestamp, "T", " "); + boost::replace_all(timestamp, "Z", ""); + + boost::posix_time::ptime pt = boost::posix_time::time_from_string(timestamp); + boost::posix_time::ptime epoch(boost::gregorian::date(1970, 1, 1)); + boost::posix_time::time_duration diff = pt - epoch; + + return static_cast(diff.total_seconds()); + } + + static bool bool_equals(bool val, const boost::optional& opt_val) { + return opt_val == boost::none ? false : val == *opt_val; + } + /** * Wait for the given duration. * diff --git a/src/utils/monero_utils.cpp b/src/utils/monero_utils.cpp index 6f714266..24836221 100644 --- a/src/utils/monero_utils.cpp +++ b/src/utils/monero_utils.cpp @@ -58,10 +58,16 @@ #include "mnemonics/english.h" #include "string_tools.h" #include "byte_stream.h" +#include using namespace cryptonote; using namespace monero_utils; +static std::unordered_map starts_{}; + +static std::mutex mutex_; + void monero_utils::set_log_level(int level) { mlog_set_log_level(level); } @@ -70,6 +76,34 @@ void monero_utils::configure_logging(const std::string& path, bool console) { mlog_configure(path, console); } +void monero_utils::start_profile(const std::string& name) { + auto now = std::chrono::high_resolution_clock::now(); + std::lock_guard lock(mutex_); + starts_[name] = now; +} + +void monero_utils::end_profile(const std::string& name) { + auto now = std::chrono::high_resolution_clock::now(); + + std::chrono::high_resolution_clock::time_point start; + + { + std::lock_guard lock(mutex_); + + auto it = starts_.find(name); + if (it == starts_.end()) { + std::cerr << "Profiler: missing startProfile(\"" << name << "\")\n"; + return; + } + + start = it->second; + } + + auto duration = std::chrono::duration_cast(now - start); + + std::cout << "[PROFILE] " << name << " took " + << duration.count() << " ms\n"; +} // --------------------------- VALIDATION UTILS ------------------------------- monero_integrated_address monero_utils::get_integrated_address(monero_network_type network_type, const std::string& standard_address, const std::string& payment_id) { @@ -77,6 +111,7 @@ monero_integrated_address monero_utils::get_integrated_address(monero_network_ty // parse and validate address cryptonote::address_parse_info address_info; if (!get_account_address_from_str(address_info, static_cast(network_type), standard_address)) throw std::runtime_error("Invalid address"); + //if (address_info.is_subaddress) throw std::runtime_error("Subaddress shouldn't be used"); if (address_info.has_payment_id) throw std::runtime_error("The given address already has a payment id"); // randomly generate payment id if not given, else validate @@ -85,7 +120,7 @@ monero_integrated_address monero_utils::get_integrated_address(monero_network_ty payment_id_h8 = crypto::rand(); } else { cryptonote::blobdata payment_id_data; - if (!epee::string_tools::parse_hexstr_to_binbuff(payment_id, payment_id_data) || sizeof(crypto::hash8) != payment_id_data.size()) throw std::runtime_error("Invalid payment id"); + if (!epee::string_tools::parse_hexstr_to_binbuff(payment_id, payment_id_data) || sizeof(crypto::hash8) != payment_id_data.size()) throw std::runtime_error("Invalid payment ID: " + payment_id); payment_id_h8 = *reinterpret_cast(payment_id_data.data()); } @@ -145,6 +180,223 @@ void monero_utils::validate_private_spend_key(const std::string& private_spend_k } } +bool monero_utils::parse_long_payment_id(const std::string& payment_id_str, crypto::hash& payment_id) +{ + // monero-project logic based on wallet2::parse_short_payment_id() + cryptonote::blobdata payment_id_data; + if (!epee::string_tools::parse_hexstr_to_binbuff(payment_id_str, payment_id_data)) { + return false; + } + if (sizeof(crypto::hash) != payment_id_data.size()) { + return false; + } + + payment_id = *reinterpret_cast(payment_id_data.data()); + return true; +} + +bool monero_utils::parse_short_payment_id(const std::string& payment_id_str, crypto::hash8& payment_id) +{ + // monero-project logic based on wallet2::parse_short_payment_id() + cryptonote::blobdata payment_id_data; + if (!epee::string_tools::parse_hexstr_to_binbuff(payment_id_str, payment_id_data)) { + return false; + } + if (sizeof(crypto::hash8) != payment_id_data.size()) { + return false; + } + payment_id = *reinterpret_cast(payment_id_data.data()); + return true; +} + +void monero_utils::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 + } + } +} + +/** + * 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 + */ +std::shared_ptr monero_utils::decontextualize(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; +} + +bool monero_utils::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_input_query != boost::none) return true; // requires context of inputs + if (query.m_tx_query.get()->m_output_query != boost::none) return true; // requires context of outputs + return false; +} + +bool monero_utils::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; +} + +std::string monero_utils::make_uri(const std::string &address, cryptonote::network_type nettype, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, std::string &error) { + cryptonote::address_parse_info info; + if(!get_account_address_from_str(info, nettype, address)) + { + error = std::string("wrong address: ") + address; + return std::string(); + } + + // we want only one payment id + if (info.has_payment_id && !payment_id.empty()) + { + error = "A single payment id is allowed"; + return std::string(); + } + + if (!payment_id.empty()) + { + error = "Standalone payment id deprecated, use integrated address instead"; + return std::string(); + } + + std::string uri = "monero:" + address; + unsigned int n_fields = 0; + + if (!payment_id.empty()) + { + uri += (n_fields++ ? "&" : "?") + std::string("tx_payment_id=") + payment_id; + } + + if (amount > 0) + { + // URI encoded amount is in decimal units, not atomic units + uri += (n_fields++ ? "&" : "?") + std::string("tx_amount=") + cryptonote::print_money(amount); + } + + if (!recipient_name.empty()) + { + uri += (n_fields++ ? "&" : "?") + std::string("recipient_name=") + epee::net_utils::conver_to_url_format(recipient_name); + } + + if (!tx_description.empty()) + { + uri += (n_fields++ ? "&" : "?") + std::string("tx_description=") + epee::net_utils::conver_to_url_format(tx_description); + } + + return uri; +} + +bool monero_utils::parse_uri(const std::string &uri, std::string &address, cryptonote::network_type nettype, std::string &payment_id, uint64_t &amount, std::string &tx_description, std::string &recipient_name, std::vector &unknown_parameters, std::string &error) { + if (uri.substr(0, 7) != "monero:") + { + error = std::string("URI has wrong scheme (expected \"monero:\"): ") + uri; + return false; + } + + std::string remainder = uri.substr(7); + const char *ptr = strchr(remainder.c_str(), '?'); + address = ptr ? remainder.substr(0, ptr-remainder.c_str()) : remainder; + + cryptonote::address_parse_info info; + if(!get_account_address_from_str(info, nettype, address)) + { + error = std::string("URI has wrong address: ") + address; + return false; + } + if (!strchr(remainder.c_str(), '?')) + return true; + + std::vector arguments; + std::string body = remainder.substr(address.size() + 1); + if (body.empty()) + return true; + boost::split(arguments, body, boost::is_any_of("&")); + std::set have_arg; + for (const auto &arg: arguments) + { + std::vector kv; + boost::split(kv, arg, boost::is_any_of("=")); + if (kv.size() != 2) + { + error = std::string("URI has wrong parameter: ") + arg; + return false; + } + if (have_arg.find(kv[0]) != have_arg.end()) + { + error = std::string("URI has more than one instance of " + kv[0]); + return false; + } + have_arg.insert(kv[0]); + + if (kv[0] == "tx_amount") + { + amount = 0; + if (!cryptonote::parse_amount(amount, kv[1])) + { + error = std::string("URI has invalid amount: ") + kv[1]; + return false; + } + } + else if (kv[0] == "tx_payment_id") + { + if (info.has_payment_id) + { + error = "Separate payment id given with an integrated address"; + return false; + } + crypto::hash hash; + if (!monero_utils::parse_long_payment_id(kv[1], hash)) + { + error = "Invalid payment id: " + kv[1]; + return false; + } + payment_id = kv[1]; + } + else if (kv[0] == "recipient_name") + { + recipient_name = epee::net_utils::convert_from_url_format(kv[1]); + } + else if (kv[0] == "tx_description") + { + tx_description = epee::net_utils::convert_from_url_format(kv[1]); + } + else + { + unknown_parameters.push_back(arg); + } + } + return true; +} + + // -------------------------- BINARY SERIALIZATION ---------------------------- void monero_utils::json_to_binary(const std::string &json, std::string &bin) { @@ -378,3 +630,230 @@ std::shared_ptr monero_utils::cn_tx_to_tx(const cryptonote::transacti // rct::rctSig m_rct_signatures; // mutable size_t blob_size; } + +std::shared_ptr monero_utils::ptx_to_tx(const tools::wallet2::pending_tx &ptx, cryptonote::network_type nettype, monero_wallet* wallet) { + const auto &cn_tx = ptx.tx; + const auto &cd = ptx.construction_data; + std::shared_ptr tx = std::dynamic_pointer_cast(monero_utils::cn_tx_to_tx(cn_tx, true)); + tx->m_hash = epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(cn_tx)); + tx->m_relay = true; + tx->m_is_relayed = true; + tx->m_is_confirmed = false; + tx->m_in_tx_pool = true; + tx->m_is_miner_tx = false; + tx->m_is_locked = true; + tx->m_num_confirmations = 0; + tx->m_is_failed = false; + tx->m_ring_size = monero_utils::RING_SIZE; + tx->m_last_relayed_timestamp = static_cast(time(NULL)); + tx->m_is_double_spend_seen = false; + tx->m_prunable_hash = epee::string_tools::pod_to_hex(cn_tx.prunable_hash); + tx->m_is_outgoing = false; + tx->m_fee = ptx.fee; + tx->m_metadata = monero_utils::dump_ptx(ptx); + tx->m_weight = cryptonote::get_transaction_weight(cn_tx); + tx->m_change_amount = cd.change_dts.amount; + tx->m_change_address = cryptonote::get_account_address_as_str(nettype, cd.subaddr_account > 0, cd.change_dts.addr); + + uint32_t sender_account_idx = cd.subaddr_account; + size_t i = 0; + std::vector subaddresses_indices; + bool first = true; + for (const auto& in : tx->m_inputs) { + auto input = std::dynamic_pointer_cast(in); + uint32_t subaddress_idx = *next(cd.subaddr_indices.begin(), i); + input->m_account_index = sender_account_idx; + input->m_subaddress_index = subaddress_idx; + input->m_is_spent = true; + input->m_is_frozen = false; + + if (first) subaddresses_indices.push_back(subaddress_idx); + first = false; + i++; + } + + std::shared_ptr outgoing_transfer = std::make_shared(); + outgoing_transfer->m_tx = tx; + tx->m_outgoing_transfer = outgoing_transfer; + uint32_t subaddress_idx = 0; + outgoing_transfer->m_account_index = sender_account_idx; + outgoing_transfer->m_subaddress_indices = subaddresses_indices; + + std::shared_ptr change_output = nullptr; + std::vector> external_outputs; + + uint64_t out_amount = 0; + i = 0; + + std::map>> destination_index; + + for (const auto& out : tx->m_outputs) { + const auto &dest = ptx.dests[i]; + out->m_amount = dest.amount; + out->m_index = i; + auto output = std::dynamic_pointer_cast(out); + if (output == nullptr) { + i++; + continue; + } + + crypto::hash payment_id = crypto::null_hash; + std::string dest_address = dest.address(nettype, payment_id); + + try { + monero_subaddress subaddress = wallet->get_address_index(dest_address); + uint32_t receiver_account_idx = subaddress.m_account_index.get(); + uint32_t subaddress_idx = subaddress.m_index.get(); + output->m_account_index = receiver_account_idx; + output->m_subaddress_index = subaddress_idx; + output->m_is_spent = false; + output->m_is_frozen = false; + bool is_change = cd.change_dts.amount > 0 && dest.amount == cd.change_dts.amount && change_output == nullptr; + if (is_change) { + change_output = output; + } + if (!is_change) { + out_amount += output->m_amount.get(); + auto transfer = std::make_shared(); + transfer->m_tx = tx; + transfer->m_amount = output->m_amount; + transfer->m_address = dest_address; + transfer->m_account_index = receiver_account_idx; + transfer->m_subaddress_index = subaddress_idx; + transfer->m_num_suggested_confirmations = 10; + tx->m_incoming_transfers.push_back(transfer); + auto destination = std::make_shared(); + destination->m_amount = dest.amount; + destination->m_address = dest_address; + destination_index[receiver_account_idx][subaddress_idx] = destination; + } + } + catch (...) { + // external output + out_amount += output->m_amount.get(); + external_outputs.push_back(output); + } + i++; + } + + tx->m_is_incoming = !tx->m_incoming_transfers.empty(); + tx->m_is_outgoing = tx->m_outgoing_transfer != boost::none; + + if (change_output != nullptr) { + tx->m_outputs.erase( + std::remove(tx->m_outputs.begin(), tx->m_outputs.end(), change_output), + tx->m_outputs.end() + ); + } + + for(const auto& ext_out : external_outputs) { + tx->m_outputs.erase( + std::remove(tx->m_outputs.begin(), tx->m_outputs.end(), ext_out), + tx->m_outputs.end() + ); + + auto ext_output = std::make_shared(); + ext_output->m_stealth_public_key = ext_out->m_stealth_public_key; + ext_output->m_index = ext_out->m_index; + ext_output->m_amount = ext_out->m_amount; + tx->m_outputs.push_back(ext_output); + } + + outgoing_transfer->m_amount = out_amount; + + sort(tx->m_outputs.begin(), tx->m_outputs.end(), monero_utils::vout_before); + sort(tx->m_incoming_transfers.begin(), tx->m_incoming_transfers.end(), monero_utils::incoming_transfer_before); + + // order destinations + for(const auto &kv_index : destination_index) { + for(const auto &kv : kv_index.second) { + outgoing_transfer->m_destinations.push_back(kv.second); + } + } + + return tx; +} + +void monero_utils::add_pid_to_tx_extra(const boost::optional& payment_id_string, std::vector &extra) { + // Detect hash8 or hash32 char hex string as pid and configure 'extra' accordingly + bool r = false; + if (payment_id_string != boost::none && payment_id_string->size() > 0) { + crypto::hash payment_id; + r = monero_utils::parse_long_payment_id(*payment_id_string, payment_id); + if (r) { + std::string extra_nonce; + cryptonote::set_payment_id_to_tx_extra_nonce(extra_nonce, payment_id); + r = cryptonote::add_extra_nonce_to_tx_extra(extra, extra_nonce); + if (!r) { + throw std::runtime_error("Couldn't add pid nonce to tx extra"); + } + } else { + crypto::hash8 payment_id8; + r = monero_utils::parse_short_payment_id(*payment_id_string, payment_id8); + if (!r) { // a PID has been specified by the user but the last resort in validating it fails; error + throw std::runtime_error("Invalid pid"); + } + std::string extra_nonce; + cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, payment_id8); + r = cryptonote::add_extra_nonce_to_tx_extra(extra, extra_nonce); + if (!r) { + throw std::runtime_error("Couldn't add pid nonce to tx extra"); + } + } + } +} + +bool monero_utils::rct_hex_to_decrypted_mask(const std::string &rct_string, const crypto::secret_key &view_secret_key, const crypto::public_key& tx_pub_key, uint64_t internal_output_index, rct::key &decrypted_mask) { + // rct string is empty if output is non RCT + if (rct_string.empty()) { + return false; + } + // rct_string is a magic value if output is RCT and coinbase + if (rct_string == "coinbase") { + decrypted_mask = rct::identity(); + return true; + } + auto make_key_derivation = [&]() { + crypto::key_derivation derivation; + bool r = generate_key_derivation(tx_pub_key, view_secret_key, derivation); + if(!r) throw std::runtime_error("Failed to generate key derivation"); + crypto::secret_key scalar; + crypto::derivation_to_scalar(derivation, internal_output_index, scalar); + return rct::sk2rct(scalar); + }; + rct::key encrypted_mask; + // rct_string is a string with length 64+16 ( + ) if RCT version 2 + if (rct_string.size() < 64 * 2) { + decrypted_mask = rct::genCommitmentMask(make_key_derivation()); + return true; + } + // rct_string is a string with length 64+64+64 ( + + ) + std::string encrypted_mask_str = rct_string.substr(64,64); + if(!epee::string_tools::validate_hex(64, encrypted_mask_str)) throw std::runtime_error("Invalid rct mask: " + encrypted_mask_str); + epee::string_tools::hex_to_pod(encrypted_mask_str, encrypted_mask); + + if (encrypted_mask == rct::identity()) { + // backward compatibility; should no longer be needed after v11 mainnet fork + decrypted_mask = encrypted_mask; + return true; + } + + // Decrypt the mask + sc_sub(decrypted_mask.bytes, + encrypted_mask.bytes, + rct::hash_to_scalar(make_key_derivation()).bytes); + + return true; +} + +bool monero_utils::rct_hex_to_rct_commit(const std::string &rct_string, rct::key &rct_commit) { + // rct string is empty if output is non RCT + if (rct_string.empty()) { + return false; + } + // rct_string is a string with length 64+64+64 ( + + ) + std::string rct_commit_str = rct_string.substr(0,64); + if(!epee::string_tools::validate_hex(64, rct_commit_str)) throw std::runtime_error("Invalid rct commit hash: " + rct_commit_str); + epee::string_tools::hex_to_pod(rct_commit_str, rct_commit); + return true; +} diff --git a/src/utils/monero_utils.h b/src/utils/monero_utils.h index 2b13eb26..fb190216 100644 --- a/src/utils/monero_utils.h +++ b/src/utils/monero_utils.h @@ -56,7 +56,10 @@ #define monero_utils_h #include "wallet/monero_wallet_model.h" +#include "wallet/monero_wallet.h" #include "cryptonote_basic/cryptonote_basic.h" +#include "cryptonote_core/cryptonote_tx_utils.h" +#include "wallet/wallet2.h" #include "serialization/keyvalue_serialization.h" // TODO: consolidate with other binary deps? #include "storages/portable_storage.h" @@ -70,9 +73,13 @@ namespace monero_utils // ------------------------------ CONSTANTS --------------------------------- static const int RING_SIZE = 12; // network-enforced ring size + static const uint64_t TAIL_EMISSION_REWARD = 600000000000; // -------------------------------- UTILS ----------------------------------- + void start_profile(const std::string& name); + void end_profile(const std::string& name); + void set_log_level(int level); void configure_logging(const std::string& path, bool console); monero_integrated_address get_integrated_address(monero_network_type network_type, const std::string& standard_address, const std::string& payment_id); @@ -85,6 +92,24 @@ namespace monero_utils void json_to_binary(const std::string &json, std::string &bin); void binary_to_json(const std::string &bin, std::string &json); void binary_blocks_to_json(const std::string &bin, std::string &json); + bool parse_long_payment_id(const std::string& payment_id_str, crypto::hash& payment_id); + bool parse_short_payment_id(const std::string& payment_id_str, crypto::hash8& payment_id); + + std::shared_ptr decontextualize(std::shared_ptr query); + bool is_contextual(const monero_transfer_query& query); + bool is_contextual(const monero_output_query& query); + + std::string make_uri(const std::string &address, cryptonote::network_type nettype, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, std::string &error); + bool parse_uri(const std::string &uri, std::string &address, cryptonote::network_type nettype, std::string &payment_id, uint64_t &amount, std::string &tx_description, std::string &recipient_name, std::vector &unknown_parameters, std::string &error); + + /** + * 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 merge_tx(const std::shared_ptr& tx, std::map>& tx_map, std::map>& block_map); // ------------------------------ RAPIDJSON --------------------------------- @@ -161,13 +186,14 @@ namespace monero_utils */ std::shared_ptr cn_tx_to_tx(const cryptonote::transaction& cn_tx, bool init_as_tx_wallet = false); + std::shared_ptr ptx_to_tx(const tools::wallet2::pending_tx &ptx, cryptonote::network_type nettype, monero_wallet* wallet); + /** * Modified from core_rpc_server.cpp to return a std::string. * * TODO: remove this duplicate, use core_rpc_server instead */ - static std::string get_pruned_tx_json(cryptonote::transaction &tx) - { + static std::string get_pruned_tx_json(cryptonote::transaction &tx) { std::stringstream ss; json_archive ar(ss); bool r = tx.serialize_base(ar); @@ -175,6 +201,26 @@ namespace monero_utils return ss.str(); } + void add_pid_to_tx_extra(const boost::optional& payment_id_string, std::vector &extra); + + bool rct_hex_to_decrypted_mask(const std::string &rct_string, const crypto::secret_key &view_secret_key, const crypto::public_key& tx_pub_key, uint64_t internal_output_index, rct::key &decrypted_mask); + + bool rct_hex_to_rct_commit(const std::string &rct_string, rct::key &rct_commit); + + static std::string dump_ptx(const tools::wallet2::pending_tx &ptx) { + std::ostringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + try + { + ar << ptx; + } + catch (...) + { + return ""; + } + return epee::string_tools::buff_to_hex_nodelimer(oss.str()); + } + // ----------------------------- GATHER BLOCKS ------------------------------ static std::vector> get_blocks_from_txs(std::vector> txs) { @@ -231,6 +277,66 @@ namespace monero_utils return blocks; } + // compute m_num_suggested_confirmations TODO monero-project: this logic is based on wallet_rpc_server.cpp `set_confirmations` but it should be encapsulated in wallet2 + static void set_num_suggested_confirmations(std::shared_ptr& incoming_transfer, uint64_t blockchain_height, uint64_t block_reward, uint64_t unlock_time) { + if (block_reward == 0) incoming_transfer->m_num_suggested_confirmations = 0; + else incoming_transfer->m_num_suggested_confirmations = (incoming_transfer->m_amount.get() + block_reward - 1) / block_reward; + + if (unlock_time < CRYPTONOTE_MAX_BLOCK_NUMBER) { + if (unlock_time > blockchain_height) incoming_transfer->m_num_suggested_confirmations = std::max(incoming_transfer->m_num_suggested_confirmations.get(), unlock_time - blockchain_height); + } else { + const uint64_t now = time(NULL); + if (unlock_time > now) incoming_transfer->m_num_suggested_confirmations = std::max(incoming_transfer->m_num_suggested_confirmations.get(), (unlock_time - now + DIFFICULTY_TARGET_V2 - 1) / DIFFICULTY_TARGET_V2); + } + } + + /** + * Returns true iff tx1's height is known to be less than tx2's height for sorting. + */ + static 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. + */ + static 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. + */ + static 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::dynamic_pointer_cast(o1); + std::shared_ptr ow2 = std::dynamic_pointer_cast(o2); + if (ow1 == nullptr) return true; + if (ow1 == nullptr) return false; + // 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; + } + // ------------------------------ FREE MEMORY ------------------------------- static void free(std::shared_ptr block) { @@ -286,5 +392,30 @@ namespace monero_utils static void free(std::vector> outputs) { return monero_utils::free(monero_utils::get_blocks_from_outputs(outputs)); } + + + template + T pop_index(std::vector& vec, size_t idx) + { + CHECK_AND_ASSERT_MES(!vec.empty(), T(), "Vector must be non-empty"); + CHECK_AND_ASSERT_MES(idx < vec.size(), T(), "idx out of bounds"); + + T res = std::move(vec[idx]); + if (idx + 1 != vec.size()) { + vec[idx] = std::move(vec.back()); + } + vec.resize(vec.size() - 1); + + return res; + } + // + template + T pop_random_value(std::vector& vec) + { + CHECK_AND_ASSERT_MES(!vec.empty(), T(), "Vector must be non-empty"); + + size_t idx = crypto::rand() % vec.size(); + return pop_index (vec, idx); + } } #endif /* monero_utils_h */ diff --git a/src/wallet/monero_wallet.h b/src/wallet/monero_wallet.h index 9bcb36e0..780d40e7 100644 --- a/src/wallet/monero_wallet.h +++ b/src/wallet/monero_wallet.h @@ -112,9 +112,6 @@ namespace monero { virtual void on_output_spent(const monero_output_wallet& output) {}; }; - // forward declaration of internal wallet2 listener - struct wallet2_listener; - // ----------------------------- WALLET METHODS ----------------------------- /** diff --git a/src/wallet/monero_wallet_full.cpp b/src/wallet/monero_wallet_full.cpp index 12cd7c18..34123aa1 100644 --- a/src/wallet/monero_wallet_full.cpp +++ b/src/wallet/monero_wallet_full.cpp @@ -58,6 +58,7 @@ #include #endif +#include "utils/gen_utils.h" #include "utils/monero_utils.h" #include #include @@ -91,42 +92,6 @@ namespace monero { END_KV_SERIALIZE_MAP() }; - /** - * 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 - */ - std::shared_ptr decontextualize(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; - } - - 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_input_query != boost::none) return true; // requires context of inputs - 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; - } // compute m_num_confirmations TODO monero-project: this logic is based on wallet_rpc_server.cpp `set_confirmations` but it should be encapsulated in wallet2 void set_num_confirmations(std::shared_ptr& tx, uint64_t blockchain_height) { @@ -135,18 +100,6 @@ namespace monero { else tx->m_num_confirmations = blockchain_height - block->m_height.get(); } - // compute m_num_suggested_confirmations TODO monero-project: this logic is based on wallet_rpc_server.cpp `set_confirmations` but it should be encapsulated in wallet2 - void set_num_suggested_confirmations(std::shared_ptr& incoming_transfer, uint64_t blockchain_height, uint64_t block_reward, uint64_t unlock_time) { - if (block_reward == 0) incoming_transfer->m_num_suggested_confirmations = 0; - else incoming_transfer->m_num_suggested_confirmations = (incoming_transfer->m_amount.get() + block_reward - 1) / block_reward; - if (unlock_time < CRYPTONOTE_MAX_BLOCK_NUMBER) { - if (unlock_time > blockchain_height) incoming_transfer->m_num_suggested_confirmations = std::max(incoming_transfer->m_num_suggested_confirmations.get(), unlock_time - blockchain_height); - } else { - const uint64_t now = time(NULL); - if (unlock_time > now) incoming_transfer->m_num_suggested_confirmations = std::max(incoming_transfer->m_num_suggested_confirmations.get(), (unlock_time - now + DIFFICULTY_TARGET_V2 - 1) / DIFFICULTY_TARGET_V2); - } - } - std::shared_ptr build_tx_with_incoming_transfer(tools::wallet2& m_w2, uint64_t height, const crypto::hash &payment_id, const tools::wallet2::payment_details &pd) { // construct block @@ -185,7 +138,7 @@ namespace monero { incoming_transfer->m_account_index = pd.m_subaddr_index.major; incoming_transfer->m_subaddress_index = pd.m_subaddr_index.minor; incoming_transfer->m_address = m_w2.get_subaddress_as_str(pd.m_subaddr_index); - set_num_suggested_confirmations(incoming_transfer, height, m_w2.get_last_block_reward(), pd.m_unlock_time); + monero_utils::set_num_suggested_confirmations(incoming_transfer, height, m_w2.get_last_block_reward(), pd.m_unlock_time); // return pointer to new tx return tx; @@ -289,7 +242,7 @@ namespace monero { incoming_transfer->m_account_index = pd.m_subaddr_index.major; incoming_transfer->m_subaddress_index = pd.m_subaddr_index.minor; incoming_transfer->m_address = m_w2.get_subaddress_as_str(pd.m_subaddr_index); - set_num_suggested_confirmations(incoming_transfer, height, m_w2.get_last_block_reward(), pd.m_unlock_time); + monero_utils::set_num_suggested_confirmations(incoming_transfer, height, m_w2.get_last_block_reward(), pd.m_unlock_time); // return pointer to new tx return tx; @@ -392,82 +345,6 @@ namespace monero { return tx; } - /** - * 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 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 - } - } - } - - /** - * 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; - } std::string get_default_ringdb_path(cryptonote::network_type nettype) { @@ -697,358 +574,6 @@ namespace monero { return true; } - // ----------------------------- WALLET LISTENER ---------------------------- - - /** - * Listens to wallet2 notifications in order to notify external wallet listeners. - */ - struct wallet2_listener : public tools::i_wallet2_callback { - - public: - - /** - * Constructs the listener. - * - * @param wallet provides context to notify external listeners - * @param wallet2 provides source notifications which this listener propagates to external listeners - */ - wallet2_listener(monero_wallet_full& wallet, tools::wallet2& wallet2) : m_wallet(wallet), m_w2(wallet2) { - this->m_sync_start_height = boost::none; - this->m_sync_end_height = boost::none; - m_prev_balance = wallet.get_balance(); - m_prev_unlocked_balance = wallet.get_unlocked_balance(); - m_notification_pool = std::unique_ptr(tools::threadpool::getNewForUnitTests(1)); // TODO (monero-project): utility can be for general use - } - - ~wallet2_listener() { - MTRACE("~wallet2_listener()"); - m_w2.callback(nullptr); - m_notification_pool->recycle(); - } - - void update_listening() { - boost::lock_guard guarg(m_listener_mutex); - - // update callback - m_w2.callback(m_wallet.get_listeners().empty() ? nullptr : this); - - // if starting to listen, cache locked txs for later comparison - if (!m_wallet.get_listeners().empty() && m_w2.callback() == nullptr) { - tools::threadpool::waiter waiter(*m_notification_pool); - m_notification_pool->submit(&waiter, [this]() { - check_for_changed_unlocked_txs(); - }); - waiter.wait(); - } - } - - void on_sync_start(uint64_t start_height) { - tools::threadpool::waiter waiter(*m_notification_pool); - m_notification_pool->submit(&waiter, [this, start_height]() { - if (m_sync_start_height != boost::none || m_sync_end_height != boost::none) throw std::runtime_error("Sync start or end height should not already be allocated, is previous sync in progress?"); - m_sync_start_height = start_height; - m_sync_end_height = m_wallet.get_daemon_height(); - }); - waiter.wait(); // TODO: this processes notification on thread, process off thread - } - - void on_sync_end() { - tools::threadpool::waiter waiter(*m_notification_pool); - m_notification_pool->submit(&waiter, [this]() { - check_for_changed_balances(); - if (m_prev_locked_tx_hashes.size() > 0) check_for_changed_unlocked_txs(); - m_sync_start_height = boost::none; - m_sync_end_height = boost::none; - }); - m_notification_pool->recycle(); - waiter.wait(); - } - - void on_new_block(uint64_t height, const cryptonote::block& cn_block) override { - if (m_wallet.get_listeners().empty()) return; - - // ignore notifications before sync start height, irrelevant to clients - if (m_sync_start_height == boost::none || height < *m_sync_start_height) return; - - // queue notification processing off main thread - tools::threadpool::waiter waiter(*m_notification_pool); - m_notification_pool->submit(&waiter, [this, height]() { - - // notify listeners of new block - for (monero_wallet_listener* listener : m_wallet.get_listeners()) { - listener->on_new_block(height); - } - - // notify listeners of sync progress - if (height >= *m_sync_end_height) m_sync_end_height = height + 1; // increase end height if necessary - double percent_done = (double) (height - *m_sync_start_height + 1) / (double) (*m_sync_end_height - *m_sync_start_height); - std::string message = std::string("Synchronizing"); - for (monero_wallet_listener* listener : m_wallet.get_listeners()) { - listener->on_sync_progress(height, *m_sync_start_height, *m_sync_end_height, percent_done, message); - } - - // notify if balances change - bool balances_changed = check_for_changed_balances(); - - // notify when txs unlock after wallet is synced - if (balances_changed && m_wallet.is_synced()) check_for_changed_unlocked_txs(); - }); - waiter.wait(); - } - - void on_unconfirmed_money_received(uint64_t height, const crypto::hash &txid, const cryptonote::transaction& cn_tx, uint64_t amount, const cryptonote::subaddress_index& subaddr_index) override { - if (m_wallet.get_listeners().empty()) return; - - // queue notification processing off main thread - tools::threadpool::waiter waiter(*m_notification_pool); - m_notification_pool->submit(&waiter, [this, height, txid, cn_tx, amount, subaddr_index]() { - try { - - // create library tx - std::shared_ptr tx = std::static_pointer_cast(monero_utils::cn_tx_to_tx(cn_tx, true)); - tx->m_hash = epee::string_tools::pod_to_hex(txid); - tx->m_is_confirmed = false; - tx->m_is_locked = true; - std::shared_ptr output = std::make_shared(); - tx->m_outputs.push_back(output); - output->m_tx = tx; - output->m_amount = amount; - output->m_account_index = subaddr_index.major; - output->m_subaddress_index = subaddr_index.minor; - - // notify listeners of output - for (monero_wallet_listener* listener : m_wallet.get_listeners()) { - listener->on_output_received(*output); - } - - // notify if balances changed - check_for_changed_balances(); - - // watch for unlock - m_prev_locked_tx_hashes.insert(tx->m_hash.get()); - - // free memory - monero_utils::free(tx); - } catch (std::exception& e) { - std::cout << "Error processing unconfirmed output received: " << std::string(e.what()) << std::endl; - } - }); - waiter.wait(); - } - - void on_money_received(uint64_t height, const crypto::hash &txid, const cryptonote::transaction& cn_tx, uint64_t amount, uint64_t burnt, const cryptonote::subaddress_index& subaddr_index, bool is_change, uint64_t unlock_time) override { - if (m_wallet.get_listeners().empty()) return; - - // queue notification processing off main thread - tools::threadpool::waiter waiter(*m_notification_pool); - m_notification_pool->submit(&waiter, [this, height, txid, cn_tx, amount, burnt, subaddr_index, is_change, unlock_time]() { - try { - - // create native library tx - std::shared_ptr block = std::make_shared(); - block->m_height = height; - std::shared_ptr tx = std::static_pointer_cast(monero_utils::cn_tx_to_tx(cn_tx, true)); - block->m_txs.push_back(tx); - tx->m_block = block; - tx->m_hash = epee::string_tools::pod_to_hex(txid); - tx->m_is_confirmed = true; - tx->m_is_locked = true; - tx->m_unlock_time = unlock_time; - std::shared_ptr output = std::make_shared(); - tx->m_outputs.push_back(output); - output->m_tx = tx; - output->m_amount = amount - burnt; - output->m_account_index = subaddr_index.major; - output->m_subaddress_index = subaddr_index.minor; - - // notify listeners of output - for (monero_wallet_listener* listener : m_wallet.get_listeners()) { - listener->on_output_received(*output); - } - - // watch for unlock - m_prev_locked_tx_hashes.insert(tx->m_hash.get()); - - // free memory - monero_utils::free(block); - } catch (std::exception& e) { - std::cout << "Error processing confirmed output received: " << std::string(e.what()) << std::endl; - } - }); - waiter.wait(); - } - - void on_money_spent(uint64_t height, const crypto::hash &txid, const cryptonote::transaction& cn_tx_in, uint64_t amount, const cryptonote::transaction& cn_tx_out, const cryptonote::subaddress_index& subaddr_index) override { - if (m_wallet.get_listeners().empty()) return; - if (&cn_tx_in != &cn_tx_out) throw std::runtime_error("on_money_spent() in tx is different than out tx"); - - // queue notification processing off main thread - tools::threadpool::waiter waiter(*m_notification_pool); - m_notification_pool->submit(&waiter, [this, height, txid, cn_tx_in, amount, cn_tx_out, subaddr_index]() { - try { - - // create native library tx - std::shared_ptr block = std::make_shared(); - block->m_height = height; - std::shared_ptr tx = std::static_pointer_cast(monero_utils::cn_tx_to_tx(cn_tx_in, true)); - block->m_txs.push_back(tx); - tx->m_block = block; - tx->m_hash = epee::string_tools::pod_to_hex(txid); - tx->m_is_confirmed = true; - tx->m_is_locked = true; - std::shared_ptr output = std::make_shared(); - tx->m_inputs.push_back(output); - output->m_tx = tx; - output->m_amount = amount; - output->m_account_index = subaddr_index.major; - output->m_subaddress_index = subaddr_index.minor; - - // notify listeners of output - for (monero_wallet_listener* listener : m_wallet.get_listeners()) { - listener->on_output_spent(*output); - } - - // watch for unlock - m_prev_locked_tx_hashes.insert(tx->m_hash.get()); - - // free memory - monero_utils::free(block); - } catch (std::exception& e) { - std::cout << "Error processing confirmed output spent: " << std::string(e.what()) << std::endl; - } - }); - waiter.wait(); - } - - void on_spend_tx_hashes(const std::vector& tx_hashes) { - if (m_wallet.get_listeners().empty()) return; - monero_tx_query tx_query; - tx_query.m_hashes = tx_hashes; - tx_query.m_include_outputs = true; - tx_query.m_is_locked = true; - on_spend_txs(m_wallet.get_txs(tx_query)); - } - - void on_spend_txs(const std::vector>& txs) { - if (m_wallet.get_listeners().empty()) return; - tools::threadpool::waiter waiter(*m_notification_pool); - m_notification_pool->submit(&waiter, [this, txs]() { - check_for_changed_balances(); - for (const std::shared_ptr& tx : txs) notify_outputs(tx); - }); - waiter.wait(); - } - - private: - monero_wallet_full& m_wallet; // wallet to provide context for notifications - tools::wallet2& m_w2; // internal wallet implementation to listen to - boost::optional m_sync_start_height; - boost::optional m_sync_end_height; - boost::mutex m_listener_mutex; - uint64_t m_prev_balance; - uint64_t m_prev_unlocked_balance; - std::set m_prev_locked_tx_hashes; - std::unique_ptr m_notification_pool; // threadpool of size 1 to queue notifications for external announcement - - bool check_for_changed_balances() { - uint64_t balance = m_wallet.get_balance(); - uint64_t unlocked_balance = m_wallet.get_unlocked_balance(); - if (balance != m_prev_balance || unlocked_balance != m_prev_unlocked_balance) { - m_prev_balance = balance; - m_prev_unlocked_balance = unlocked_balance; - for (monero_wallet_listener* listener : m_wallet.get_listeners()) { - listener->on_balances_changed(balance, unlocked_balance); - } - return true; - } - return false; - } - - // TODO: this can probably be optimized using e.g. wallet2.get_num_rct_outputs() or wallet2.get_num_transfer_details(), or by retaining confirmed block height and only checking on or after unlock height, etc - void check_for_changed_unlocked_txs() { - - // get confirmed and locked txs - monero_tx_query query = monero_tx_query(); - query.m_is_locked = true; - query.m_is_confirmed = true; - query.m_min_height = m_wallet.get_height() - 70; // only monitor recent txs - std::vector> locked_txs = m_wallet.get_txs(query); - - // collect hashes of txs no longer locked - std::vector tx_hashes_no_longer_locked; - for (const std::string prev_locked_tx_hash : m_prev_locked_tx_hashes) { - bool found = false; - for (const std::shared_ptr& locked_tx : locked_txs) { - if (locked_tx->m_hash.get() == prev_locked_tx_hash) { - found = true; - break; - } - } - if (!found) tx_hashes_no_longer_locked.push_back(prev_locked_tx_hash); - } - - // fetch txs that are no longer locked - std::vector> txs_no_longer_locked; - if (!tx_hashes_no_longer_locked.empty()) { - query.m_hashes = tx_hashes_no_longer_locked; - query.m_is_locked = false; - query.m_include_outputs = true; - txs_no_longer_locked = m_wallet.get_txs(query); - } - - // notify listeners of newly unlocked inputs and outputs - for (const std::shared_ptr& unlocked_tx : txs_no_longer_locked) { - notify_outputs(unlocked_tx); - } - - // re-assign currently locked tx hashes // TODO: needs mutex for thread safety? - m_prev_locked_tx_hashes.clear(); - for (const std::shared_ptr& locked_tx : locked_txs) { - m_prev_locked_tx_hashes.insert(locked_tx->m_hash.get()); - } - - // free memory - monero_utils::free(locked_txs); - monero_utils::free(txs_no_longer_locked); - } - - void notify_outputs(const std::shared_ptr& tx) { - - // notify spent outputs - if (tx->m_outgoing_transfer != boost::none) { - - // build dummy input for notification // TODO: this provides one input with outgoing amount like monero-wallet-rpc client, use real inputs instead - std::shared_ptr input = std::make_shared(); - input->m_amount = tx->m_outgoing_transfer.get()->m_amount.get() + tx->m_fee.get(); - input->m_account_index = tx->m_outgoing_transfer.get()->m_account_index; - if (tx->m_outgoing_transfer.get()->m_subaddress_indices.size() == 1) input->m_subaddress_index = tx->m_outgoing_transfer.get()->m_subaddress_indices[0]; // initialize if transfer sourced from single subaddress - std::shared_ptr tx_notify = std::make_shared(); - input->m_tx = tx_notify; - tx_notify->m_inputs.push_back(input); - tx_notify->m_hash = tx->m_hash; - tx_notify->m_is_locked = tx->m_is_locked; - tx_notify->m_unlock_time = tx->m_unlock_time; - if (tx->m_block != boost::none) { - std::shared_ptr block_notify = std::make_shared(); - tx_notify->m_block = block_notify; - block_notify->m_height = tx->get_height(); - block_notify->m_txs.push_back(tx_notify); - } - - // notify listeners and free memory - for (monero_wallet_listener* listener : m_wallet.get_listeners()) listener->on_output_spent(*input); - monero_utils::free(tx_notify); - } - - // notify received outputs - if (!tx->m_incoming_transfers.empty()) { - for (const std::shared_ptr& output : tx->get_outputs_wallet()) { - for (monero_wallet_listener* listener : m_wallet.get_listeners()) listener->on_output_received(*output); - } - } - } - }; - // --------------------------- STATIC WALLET UTILS -------------------------- bool monero_wallet_full::wallet_exists(const std::string& path) { @@ -1753,7 +1278,7 @@ namespace monero { // fetch all transfers that meet tx query std::shared_ptr temp_transfer_query = std::make_shared(); - temp_transfer_query->m_tx_query = decontextualize(_query->copy(_query, std::make_shared())); + temp_transfer_query->m_tx_query = monero_utils::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()); @@ -1772,13 +1297,13 @@ namespace monero { std::map> tx_map; std::map> block_map; for (const std::shared_ptr& tx : txs) { - merge_tx(tx, tx_map, block_map); + monero_utils::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 = decontextualize(_query->copy(_query, std::make_shared())); + temp_output_query->m_tx_query = monero_utils::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()); @@ -1788,7 +1313,7 @@ namespace monero { 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()) { - merge_tx(tx, tx_map, block_map); + monero_utils::merge_tx(tx, tx_map, block_map); output_txs.insert(tx); } } @@ -1850,7 +1375,7 @@ namespace monero { // } else std::cout << "Transfer query: " << query.serialize() << std::endl; // get transfers directly if query does not require tx context (e.g. other transfers, outputs) - if (!is_contextual(query)) return get_transfers_aux(query); + if (!monero_utils::is_contextual(query)) return get_transfers_aux(query); // otherwise get txs with full models to fulfill query std::vector> transfers; @@ -1871,7 +1396,7 @@ namespace monero { // } else std::cout << "Output query: " << query.serialize() << std::endl; // get outputs directly if query does not require tx context (e.g. other outputs, transfers) - if (!is_contextual(query)) return get_outputs_aux(query); + if (!monero_utils::is_contextual(query)) return get_outputs_aux(query); // otherwise get txs with full models to fulfill query std::vector> outputs; @@ -3554,7 +3079,8 @@ namespace monero { #endif // initialize internal state - m_w2_listener = std::unique_ptr(new wallet2_listener(*this, *m_w2)); + m_w2->allow_mismatched_daemon_version(true); + m_w2_listener = std::unique_ptr(new monero_wallet_utils::wallet2_listener(*this, *m_w2)); if (get_daemon_connection() == boost::none) m_is_connected = false; m_is_synced = false; m_rescan_on_sync = false; @@ -3596,15 +3122,15 @@ namespace monero { } // 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 = !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) && tx_query->get_height() == boost::none && tx_query->m_min_height == boost::none && !bool_equals(false, tx_query->m_is_locked); - 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 can_be_confirmed = !gen_utils::bool_equals(false, tx_query->m_is_confirmed) && !gen_utils::bool_equals(true, tx_query->m_in_tx_pool) && !gen_utils::bool_equals(true, tx_query->m_is_failed) && !gen_utils::bool_equals(false, tx_query->m_is_relayed); + bool can_be_in_tx_pool = !gen_utils::bool_equals(true, tx_query->m_is_confirmed) && !gen_utils::bool_equals(false, tx_query->m_in_tx_pool) && !gen_utils::bool_equals(true, tx_query->m_is_failed) && tx_query->get_height() == boost::none && tx_query->m_min_height == boost::none && !gen_utils::bool_equals(false, tx_query->m_is_locked); + bool can_be_incoming = !gen_utils::bool_equals(false, _query->m_is_incoming) && !gen_utils::bool_equals(true, _query->is_outgoing()) && !gen_utils::bool_equals(true, _query->m_has_destinations); + bool can_be_outgoing = !gen_utils::bool_equals(false, _query->is_outgoing()) && !gen_utils::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) && !bool_equals(false, tx_query->m_is_locked); + bool is_failed = !gen_utils::bool_equals(false, tx_query->m_is_failed) && !gen_utils::bool_equals(true, tx_query->m_is_confirmed) && !gen_utils::bool_equals(true, tx_query->m_in_tx_pool) && !gen_utils::bool_equals(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) { @@ -3625,7 +3151,7 @@ namespace monero { if (!tx_query->m_hashes.empty() && std::find(tx_query->m_hashes.begin(), tx_query->m_hashes.end(), epee::string_tools::pod_to_hex(i->first)) == tx_query->m_hashes.end()) continue; // skip if hash filtered std::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 if failure filtered - merge_tx(tx, tx_map, block_map); + monero_utils::merge_tx(tx, tx_map, block_map); } } @@ -3642,7 +3168,7 @@ namespace monero { for (std::list>::const_iterator i = payments.begin(); i != payments.end(); ++i) { if (!tx_query->m_hashes.empty() && std::find(tx_query->m_hashes.begin(), tx_query->m_hashes.end(), epee::string_tools::pod_to_hex(i->second.m_pd.m_tx_hash)) == tx_query->m_hashes.end()) continue; // skip if hash filtered std::shared_ptr tx = build_tx_with_incoming_transfer_unconfirmed(*m_w2, height, i->first, i->second); - merge_tx(tx, tx_map, block_map); + monero_utils::merge_tx(tx, tx_map, block_map); } } @@ -3653,7 +3179,7 @@ namespace monero { for (std::list>::const_iterator i = payments.begin(); i != payments.end(); ++i) { if (!tx_query->m_hashes.empty() && std::find(tx_query->m_hashes.begin(), tx_query->m_hashes.end(), epee::string_tools::pod_to_hex(i->second.m_tx_hash)) == tx_query->m_hashes.end()) continue; // skip if hash filtered std::shared_ptr tx = build_tx_with_incoming_transfer(*m_w2, height, i->first, i->second); - merge_tx(tx, tx_map, block_map); + monero_utils::merge_tx(tx, tx_map, block_map); } } @@ -3664,7 +3190,7 @@ namespace monero { for (std::list>::const_iterator i = payments.begin(); i != payments.end(); ++i) { if (!tx_query->m_hashes.empty() && std::find(tx_query->m_hashes.begin(), tx_query->m_hashes.end(), epee::string_tools::pod_to_hex(i->first)) == tx_query->m_hashes.end()) continue; // skip if hash filtered std::shared_ptr tx = build_tx_with_outgoing_transfer(*m_w2, height, i->first, i->second); - merge_tx(tx, tx_map, block_map); + monero_utils::merge_tx(tx, tx_map, block_map); } } @@ -3673,7 +3199,7 @@ namespace monero { 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); + sort(txs.begin(), txs.end(), monero_utils::tx_height_less_than); // filter transfers std::vector> transfers; @@ -3684,7 +3210,7 @@ namespace monero { 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); + sort(tx->m_incoming_transfers.begin(), tx->m_incoming_transfers.end(), monero_utils::incoming_transfer_before); // collect queried transfers, erase if excluded for (const std::shared_ptr& transfer : tx->filter_transfers(*_query)) transfers.push_back(transfer); @@ -3739,7 +3265,7 @@ namespace monero { for (const auto& output_w2 : outputs_w2) { // TODO: skip tx building if m_w2 output excluded by indices, etc std::shared_ptr tx = build_tx_with_vout(*m_w2, output_w2); - merge_tx(tx, tx_map, block_map); + monero_utils::merge_tx(tx, tx_map, block_map); } // sort txs by block height @@ -3747,14 +3273,14 @@ namespace monero { 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); + sort(txs.begin(), txs.end(), monero_utils::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); + sort(tx->m_outputs.begin(), tx->m_outputs.end(), monero_utils::vout_before); // collect queried outputs, erase if excluded for (const std::shared_ptr& output : tx->filter_outputs_wallet(*_query)) outputs.push_back(output); diff --git a/src/wallet/monero_wallet_full.h b/src/wallet/monero_wallet_full.h index aa2b376c..9446326b 100644 --- a/src/wallet/monero_wallet_full.h +++ b/src/wallet/monero_wallet_full.h @@ -54,6 +54,7 @@ #include "monero_wallet.h" #include "wallet/wallet2.h" +#include "monero_wallet_utils.h" #include #include @@ -64,11 +65,6 @@ */ namespace monero { - // -------------------------------- LISTENERS ------------------------------- - - // forward declaration of internal wallet2 listener - struct wallet2_listener; - // --------------------------- STATIC WALLET UTILS -------------------------- /** @@ -262,8 +258,7 @@ namespace monero { // ---------------------------------- PRIVATE --------------------------------- private: - friend struct wallet2_listener; - std::unique_ptr m_w2_listener; // internal wallet implementation listener + std::unique_ptr m_w2_listener; // internal wallet implementation listener std::set m_listeners; // external wallet listeners static monero_wallet_full* create_wallet_from_seed(monero_wallet_config& config, std::unique_ptr http_client_factory); diff --git a/src/wallet/monero_wallet_keys.cpp b/src/wallet/monero_wallet_keys.cpp index 3d88ad51..f925fa6f 100644 --- a/src/wallet/monero_wallet_keys.cpp +++ b/src/wallet/monero_wallet_keys.cpp @@ -53,14 +53,17 @@ #include "monero_wallet_keys.h" #include "utils/monero_utils.h" +#include "monero_wallet_utils.h" #include #include #include "mnemonics/electrum-words.h" #include "mnemonics/english.h" +#include "common/base58.h" #include "cryptonote_basic/cryptonote_format_utils.h" #include "cryptonote_basic/cryptonote_basic_impl.h" #include "string_tools.h" #include "device/device.hpp" +#include "device/device_cold.hpp" using namespace epee; using namespace tools; @@ -71,6 +74,88 @@ using namespace crypto; */ namespace monero { + // ------------------------------- KEY IMAGE UTILS ------------------------------- + + std::shared_ptr monero_key_image_cache::get(const crypto::public_key& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index &received_subaddr) { + boost::lock_guard lock(m_mutex); + auto it_pubkey = m_cache.find(tx_public_key); + if (it_pubkey != m_cache.end()) { + auto it_out_index = it_pubkey->second.find(out_index); + if (it_out_index != it_pubkey->second.end()) { + auto it_subaddr = it_out_index->second.find(received_subaddr); + if (it_subaddr != it_out_index->second.end()) { + return std::get<0>(it_subaddr->second); + } + } + } + return nullptr; + } + + std::shared_ptr monero_key_image_cache::get(const std::string& tx_public_key, uint64_t out_index, uint32_t account_idx, uint32_t subaddress_idx) { + crypto::public_key _tx_public_key; + string_tools::hex_to_pod(tx_public_key, _tx_public_key); + return get(_tx_public_key, out_index, {account_idx, subaddress_idx}); + } + + void monero_key_image_cache::set(const std::shared_ptr& key_image, const crypto::public_key& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index &received_subaddr, bool request) { + boost::lock_guard lock(m_mutex); + m_cache[tx_public_key][out_index][received_subaddr] = std::make_pair(key_image, request); + } + + void monero_key_image_cache::set(const std::shared_ptr& key_image, const std::string& tx_public_key, uint64_t out_index, uint32_t account_idx, uint32_t subaddress_idx, bool request) { + crypto::public_key _tx_public_key; + string_tools::hex_to_pod(tx_public_key, _tx_public_key); + set(key_image, _tx_public_key, out_index, {account_idx, subaddress_idx}, request); + } + + bool monero_key_image_cache::request(const crypto::public_key& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index &received_subaddr) { + boost::lock_guard lock(m_mutex); + auto it_pubkey = m_cache.find(tx_public_key); + if (it_pubkey != m_cache.end()) { + auto it_out_index = it_pubkey->second.find(out_index); + if (it_out_index != it_pubkey->second.end()) { + auto it_subaddr = it_out_index->second.find(received_subaddr); + if (it_subaddr != it_out_index->second.end()) { + return std::get<1>(it_subaddr->second); + } + } + } + return false; + } + + bool monero_key_image_cache::request(const std::string& tx_public_key, uint64_t out_index, uint32_t account_idx, uint32_t subaddress_idx) { + crypto::public_key _tx_public_key; + string_tools::hex_to_pod(tx_public_key, _tx_public_key); + return request(_tx_public_key, out_index, {account_idx, subaddress_idx}); + } + + void monero_key_image_cache::set_request(const std::string& tx_public_key, uint64_t out_index, uint32_t account_idx, uint32_t subaddress_idx, bool request) { + auto key_image = get(tx_public_key, out_index, account_idx, subaddress_idx); + if (key_image == nullptr) throw std::runtime_error("Key image not found in cache"); + set(key_image, tx_public_key, out_index, account_idx, subaddress_idx, request); + } + + // Set up an address signature message hash + // Hash data: domain separator, spend public key, view public key, mode identifier, payload data + static crypto::hash get_message_hash(const std::string &data, const crypto::public_key &spend_key, const crypto::public_key &view_key, const uint8_t mode) + { + KECCAK_CTX ctx; + keccak_init(&ctx); + keccak_update(&ctx, (const uint8_t*)config::HASH_KEY_MESSAGE_SIGNING, sizeof(config::HASH_KEY_MESSAGE_SIGNING)); // includes NUL + keccak_update(&ctx, (const uint8_t*)&spend_key, sizeof(crypto::public_key)); + keccak_update(&ctx, (const uint8_t*)&view_key, sizeof(crypto::public_key)); + keccak_update(&ctx, (const uint8_t*)&mode, sizeof(uint8_t)); + char len_buf[(sizeof(size_t) * 8 + 6) / 7]; + char *ptr = len_buf; + tools::write_varint(ptr, data.size()); + CHECK_AND_ASSERT_THROW_MES(ptr > len_buf && ptr <= len_buf + sizeof(len_buf), "Length overflow"); + keccak_update(&ctx, (const uint8_t*)len_buf, ptr - len_buf); + keccak_update(&ctx, (const uint8_t*)data.data(), data.size()); + crypto::hash hash; + keccak_finish(&ctx, (uint8_t*)&hash); + return hash; + } + // ---------------------------- WALLET MANAGEMENT --------------------------- monero_wallet_keys* monero_wallet_keys::create_wallet_random(const monero_wallet_config& config) { @@ -118,13 +203,13 @@ namespace monero { // initialize wallet account monero_wallet_keys* wallet = new monero_wallet_keys(); wallet->m_account = cryptonote::account_base{}; - wallet->m_account.generate(spend_key_sk, true, false); + crypto::secret_key spend_key_value = wallet->m_account.generate(spend_key_sk, true, false); // initialize remaining wallet wallet->m_network_type = config.m_network_type.get(); wallet->m_language = language; epee::wipeable_string wipeable_mnemonic; - if (!crypto::ElectrumWords::bytes_to_words(spend_key_sk, wipeable_mnemonic, wallet->m_language)) { + if (!crypto::ElectrumWords::bytes_to_words(spend_key_value, wipeable_mnemonic, wallet->m_language)) { throw std::runtime_error("Failed to create mnemonic from private spend key for language: " + std::string(wallet->m_language)); } wallet->m_seed = std::string(wipeable_mnemonic.data(), wipeable_mnemonic.size()); @@ -223,6 +308,10 @@ namespace monero { // ----------------------------- WALLET METHODS ----------------------------- + bool monero_wallet_keys::key_on_device() const { + return m_account.get_device().get_type() != hw::device::device_type::SOFTWARE; + } + monero_wallet_keys::~monero_wallet_keys() { MTRACE("~monero_wallet_keys()"); close(); @@ -243,17 +332,57 @@ namespace monero { } monero_integrated_address monero_wallet_keys::get_integrated_address(const std::string& standard_address, const std::string& payment_id) const { - std::cout << "monero_wallet_keys::get_integrated_address()" << std::endl; - throw std::runtime_error("monero_wallet_keys::get_integrated_address() not implemented"); + MTRACE("get_integrated_address()"); + + // this logic is based on monero_wallet_full::get_integrated_address() + + // randomly generate payment id if not given, else validate + crypto::hash8 payment_id_h8; + if (payment_id.empty()) { + payment_id_h8 = crypto::rand(); + } else { + if (!monero_utils::parse_short_payment_id(payment_id, payment_id_h8)) throw std::runtime_error("Invalid payment ID: " + payment_id); + } + + // use primary address if standard address not given, else validate + if (standard_address.empty()) { + hw::device &hwdev = m_account.get_device(); + cryptonote::subaddress_index index{0, 0}; + cryptonote::account_public_address address = hwdev.get_subaddress(m_account.get_keys(), index); + return decode_integrated_address(cryptonote::get_account_integrated_address_as_str(get_nettype(), address, payment_id_h8)); + } else { + + // validate standard address + cryptonote::address_parse_info info; + if (!cryptonote::get_account_address_from_str(info, get_nettype(), standard_address)) throw std::runtime_error("Invalid address"); + if (info.is_subaddress) throw std::runtime_error("Subaddress shouldn't be used"); + if (info.has_payment_id) throw std::runtime_error("Already integrated address"); + if (payment_id.empty()) throw std::runtime_error("Payment ID shouldn't be left unspecified"); + + // create integrated address from given standard address + return decode_integrated_address(cryptonote::get_account_integrated_address_as_str(get_nettype(), info.address, payment_id_h8)); + } } monero_integrated_address monero_wallet_keys::decode_integrated_address(const std::string& integrated_address) const { - std::cout << "monero_wallet_keys::decode_integrated_address()" << std::endl; - throw std::runtime_error("monero_wallet_keys::decode_integrated_address() not implemented"); + MTRACE("monero_wallet_keys::decode_integrated_address()"); + // TODO this logic is based on monero_wallet_full::decode_integrated_address(), refactory code? + + cryptonote::address_parse_info info; + if (!cryptonote::get_account_address_from_str(info, get_nettype(), integrated_address)) throw std::runtime_error("Invalid address"); + if (!info.has_payment_id) throw std::runtime_error("Address is not an integrated address"); + + cryptonote::account_public_address address = info.address; + monero_integrated_address result; + result.m_integrated_address = integrated_address; + result.m_standard_address = cryptonote::get_account_address_as_str(get_nettype(), info.is_subaddress, address); + result.m_payment_id = string_tools::pod_to_hex(info.payment_id); + + return result; } monero_account monero_wallet_keys::get_account(uint32_t account_idx, bool include_subaddresses) const { - std::cout << "monero_wallet_keys::get_account()" << std::endl; + MTRACE("monero_wallet_keys::get_account()"); if (include_subaddresses) { std::string err = "monero_wallet_keys::get_account(account_idx, include_subaddresses) include_subaddresses must be false"; @@ -292,13 +421,220 @@ namespace monero { } std::string monero_wallet_keys::sign_message(const std::string& msg, monero_message_signature_type signature_type, uint32_t account_idx, uint32_t subaddress_idx) const { - std::cout << "monero_wallet_keys::sign_message()" << std::endl; - throw std::runtime_error("monero_wallet_keys::sign_message() not implemented"); + MTRACE("monero_wallet_keys::sign_message()"); + + cryptonote::subaddress_index index = {account_idx, subaddress_idx}; + + const cryptonote::account_keys &keys = m_account.get_keys(); + crypto::signature signature; + crypto::secret_key skey, m; + crypto::secret_key skey_spend, skey_view; + crypto::public_key pkey; + crypto::public_key pkey_spend, pkey_view; // to include both in hash + crypto::hash hash; + uint8_t mode; + + // Use the base address + if (index.is_zero()) + { + switch (signature_type) + { + case monero_message_signature_type::SIGN_WITH_SPEND_KEY: + skey = keys.m_spend_secret_key; + pkey = keys.m_account_address.m_spend_public_key; + mode = 0; + break; + case monero_message_signature_type::SIGN_WITH_VIEW_KEY: + skey = keys.m_view_secret_key; + pkey = keys.m_account_address.m_view_public_key; + mode = 1; + break; + default: throw std::runtime_error("Invalid signature type requested"); + } + hash = get_message_hash(msg,keys.m_account_address.m_spend_public_key,keys.m_account_address.m_view_public_key,mode); + } + // Use a subaddress + else + { + skey_spend = keys.m_spend_secret_key; + m = m_account.get_device().get_subaddress_secret_key(keys.m_view_secret_key, index); + sc_add((unsigned char*)&skey_spend, (unsigned char*)&m, (unsigned char*)&skey_spend); + secret_key_to_public_key(skey_spend,pkey_spend); + sc_mul((unsigned char*)&skey_view, (unsigned char*)&keys.m_view_secret_key, (unsigned char*)&skey_spend); + secret_key_to_public_key(skey_view,pkey_view); + switch (signature_type) + { + case monero_message_signature_type::SIGN_WITH_SPEND_KEY: + skey = skey_spend; + pkey = pkey_spend; + mode = 0; + break; + case monero_message_signature_type::SIGN_WITH_VIEW_KEY: + skey = skey_view; + pkey = pkey_view; + mode = 1; + break; + default: CHECK_AND_ASSERT_THROW_MES(false, "Invalid signature type requested"); + } + secret_key_to_public_key(skey, pkey); + hash = get_message_hash(msg,pkey_spend,pkey_view,mode); + } + crypto::generate_signature(hash, pkey, skey, signature); + return std::string("SigV2") + tools::base58::encode(std::string((const char *)&signature, sizeof(signature))); } monero_message_signature_result monero_wallet_keys::verify_message(const std::string& msg, const std::string& address, const std::string& signature) const { - std::cout << "monero_wallet_keys::verify_message()" << std::endl; - throw std::runtime_error("monero_wallet_keys::verify_message() not implemented"); + MTRACE("monero_wallet_keys::verify_message()"); + + // validate and parse address or url + cryptonote::address_parse_info info; + std::string err = "Invalid address"; + if (!get_account_address_from_str_or_url(info, get_nettype(), address, + [&err](const std::string &url, const std::vector &addresses, bool dnssec_valid)->std::string { + if (!dnssec_valid) { + err = std::string("Invalid DNSSEC for ") + url; + return {}; + } + if (addresses.empty()) { + err = std::string("No Monero address found at ") + url; + return {}; + } + return addresses[0]; + })) + { + throw std::runtime_error(err); + } + monero_message_signature_result result; + result.m_is_good = false; + result.m_is_old = false; + + static const size_t v1_header_len = strlen("SigV1"); + static const size_t v2_header_len = strlen("SigV2"); + const bool v1 = signature.size() >= v1_header_len && signature.substr(0, v1_header_len) == "SigV1"; + const bool v2 = signature.size() >= v2_header_len && signature.substr(0, v2_header_len) == "SigV2"; + if (!v1 && !v2) + { + std::cout << "Signature header check error" << std::endl; + return result; + } + crypto::hash hash; + if (v1) + { + crypto::cn_fast_hash(msg.data(), msg.size(), hash); + } + std::string decoded; + if (!tools::base58::decode(signature.substr(v1 ? v1_header_len : v2_header_len), decoded)) { + MWARNING("Signature decoding error"); + return result; + } + crypto::signature s; + if (sizeof(s) != decoded.size()) { + std::cout << "Signature decoding error" << std::endl; + return result; + } + memcpy(&s, decoded.data(), sizeof(s)); + + // Test each mode and return which mode, if either, succeeded + if (v2) + hash = get_message_hash(msg,info.address.m_spend_public_key,info.address.m_view_public_key,(uint8_t) 0); + if (crypto::check_signature(hash, info.address.m_spend_public_key, s)) + { + result.m_is_good = true; + result.m_signature_type = monero_message_signature_type::SIGN_WITH_SPEND_KEY; + result.m_is_old = !v2; + result.m_version = v1 ? 1u : 2u; + return result; + } + + if (v2) + hash = get_message_hash(msg,info.address.m_spend_public_key,info.address.m_view_public_key,(uint8_t) 1); + if (crypto::check_signature(hash, info.address.m_view_public_key, s)) + { + result.m_is_good = true; + result.m_signature_type = monero_message_signature_type::SIGN_WITH_VIEW_KEY; + result.m_is_old = !v2; + result.m_version = v1 ? 1u : 2u; + return result; + } + + // Both modes failed + return result; + } + + std::string monero_wallet_keys::get_payment_uri(const monero_tx_config& config) const { + MTRACE("get_payment_uri()"); + + // validate config + std::vector> destinations = config.get_normalized_destinations(); + if (destinations.size() != 1) throw std::runtime_error("Cannot make URI from supplied parameters: must provide exactly one destination to send funds"); + if (destinations.at(0)->m_address == boost::none) throw std::runtime_error("Cannot make URI from supplied parameters: must provide destination address"); + if (destinations.at(0)->m_amount == boost::none) throw std::runtime_error("Cannot make URI from supplied parameters: must provide destination amount"); + + // prepare wallet2 params + std::string address = destinations.at(0)->m_address.get(); + std::string payment_id = config.m_payment_id == boost::none ? "" : config.m_payment_id.get(); + uint64_t amount = destinations.at(0)->m_amount.get(); + std::string note = config.m_note == boost::none ? "" : config.m_note.get(); + std::string m_recipient_name = config.m_recipient_name == boost::none ? "" : config.m_recipient_name.get(); + + // make uri using wallet2 + std::string error; + std::string uri = monero_utils::make_uri(address, get_nettype(), payment_id, amount, note, m_recipient_name, error); + if (uri.empty()) throw std::runtime_error("Cannot make URI from supplied parameters: " + error); + return uri; + } + + std::shared_ptr monero_wallet_keys::parse_payment_uri(const std::string& uri) const { + MTRACE("parse_payment_uri(" << uri << ")"); + + // decode uri to parameters + std::string address; + std::string payment_id; + uint64_t amount = 0; + std::string note; + std::string m_recipient_name; + std::vector unknown_parameters; + std::string error; + if (!monero_utils::parse_uri(uri, address, get_nettype(), payment_id, amount, note, m_recipient_name, unknown_parameters, error)) { + throw std::runtime_error("Error parsing URI: " + error); + } + + // initialize config + std::shared_ptr config = std::make_shared(); + std::shared_ptr destination = std::make_shared(); + config->m_destinations.push_back(destination); + if (!address.empty()) destination->m_address = address; + destination->m_amount = amount; + if (!payment_id.empty()) config->m_payment_id = payment_id; + if (!note.empty()) config->m_note = note; + if (!m_recipient_name.empty()) config->m_recipient_name = m_recipient_name; + if (!unknown_parameters.empty()) MWARNING("WARNING in monero_wallet_full::parse_payment_uri: URI contains unknown parameters which are discarded"); // TODO: return unknown parameters? + return config; + } + + std::string monero_wallet_keys::get_tx_key(const std::string& tx_hash) const { + MTRACE("monero_wallet_light::get_tx_key()"); + + // validate and parse tx hash + crypto::hash _tx_hash; + if (!epee::string_tools::hex_to_pod(tx_hash, _tx_hash)) { + throw std::runtime_error("TX hash has invalid format"); + } + + // get tx key and additional keys + crypto::secret_key _tx_key; + std::vector additional_tx_keys; + if (!get_tx_key(_tx_hash, _tx_key, additional_tx_keys)) { + throw std::runtime_error("No tx secret key is stored for this tx"); + } + + // build and return tx key with additional keys + epee::wipeable_string s; + s += epee::to_hex::wipeable_string(_tx_key); + for (uint64_t i = 0; i < additional_tx_keys.size(); ++i) { + s += epee::to_hex::wipeable_string(additional_tx_keys[i]); + } + return std::string(s.data(), s.size()); } void monero_wallet_keys::close(bool save) { @@ -308,6 +644,516 @@ namespace monero { // ------------------------------- PRIVATE HELPERS ---------------------------- + /** + * Generates a key image for an output note (enote) in a simplified manner. + * This function already assumes that we checked that the onetime address was addressed to `received_subaddr`. + * + * @param ephem_pubkey is the tx main pubkey or an additional pubkey + * @param tx_output_index is the index of the enote in the local output set of the tx + * @param received_subaddr is the index of the recipient's subaddress + * @param ack recipient's account keys, including + * @param hwdev Hardware device used for cryptographic operations + */ + std::pair generate_ki(const crypto::public_key &ephem_pubkey, const size_t tx_output_index, const cryptonote::subaddress_index &received_subaddr, const cryptonote::account_keys &ack, hw::device &hwdev) { + // notation: + // - R: ephem_pubkey + // - a: ack.m_view_secret_key [private viewkey] + // - b: ack.m_spend_secret_key [private spendkey] + // - idx: tx_output_index + // - index_major: received_subaddr.major + // - index_minor: received_subaddr.minor + // - Hs() [hash-to-scalar] + // - Hp() [hash-to-point] + + // 1. Diffie-Helman derived secret D = a R + crypto::key_derivation recv_derivation; + CHECK_AND_ASSERT_THROW_MES(hwdev.generate_key_derivation(ephem_pubkey, ack.m_view_secret_key, recv_derivation), + "Failed to perform Diffie-Helman exchange against tx ephem pubkey"); + + // 2. Non-address-extended onetime key secret u = Hs(D || idx) + b + crypto::secret_key onetime_privkey_unextended; + hwdev.derive_secret_key(recv_derivation, tx_output_index, ack.m_spend_secret_key, onetime_privkey_unextended); + + // 3. Subaddress key extension s = Hs(a || index_major || index_minor) if is subaddress, else s = 0 + const crypto::secret_key subaddr_ext{received_subaddr.is_zero() ? + crypto::secret_key{} : hwdev.get_subaddress_secret_key(ack.m_view_secret_key, received_subaddr)}; + + // 4. Onetime address private key x = u + s + crypto::secret_key onetime_privkey; + hwdev.sc_secret_add(onetime_privkey, onetime_privkey_unextended, subaddr_ext); + + // 5. Onetime address K = x G + crypto::public_key onetime_pubkey; + CHECK_AND_ASSERT_THROW_MES(hwdev.secret_key_to_public_key(onetime_privkey, onetime_pubkey), + "Failed to make public key"); + + // 6. Key image I = x Hp(K) + crypto::key_image ki; + hwdev.generate_key_image(onetime_pubkey, onetime_privkey, ki); + + // sign the key image with the output secret key + crypto::signature signature; + std::vector key_ptrs; + key_ptrs.push_back(&ephem_pubkey); + + crypto::generate_ring_signature((const crypto::hash&)ki, ki, key_ptrs, onetime_privkey, 0, &signature); + + return std::make_pair(ki, signature); + } + + std::pair monero_wallet_keys::generate_key_image_for_enote(const crypto::public_key &ephem_pubkey, const size_t tx_output_index, const cryptonote::subaddress_index &received_subaddr) const { + if (is_view_only()) throw std::runtime_error("cannot generate key image: wallet is view only"); + return generate_ki(ephem_pubkey, tx_output_index, received_subaddr, m_account.get_keys(), m_account.get_device()); + } + + monero_key_image monero_wallet_keys::generate_key_image(const std::string &tx_public_key, uint64_t out_index, const cryptonote::subaddress_index &received_subaddr) const { + crypto::public_key tx_pub_key; + string_tools::hex_to_pod(tx_public_key, tx_pub_key); + + return generate_key_image(tx_pub_key, out_index, received_subaddr); + } + + monero_key_image monero_wallet_keys::generate_key_image(const crypto::public_key& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index &received_subaddr) const { + monero_key_image result; + + auto found = m_generated_key_images.get(tx_public_key, out_index, received_subaddr); + if (found != nullptr) return *found; + + std::pair key_image = generate_key_image_for_enote(tx_public_key, out_index, received_subaddr); + result.m_hex = string_tools::pod_to_hex(key_image.first); + result.m_signature = string_tools::pod_to_hex(key_image.second); + m_generated_key_images.set(std::make_shared(result), tx_public_key, out_index, received_subaddr); + + return result; + } + + bool monero_wallet_keys::key_image_is_ours(const crypto::key_image &key_image, const crypto::public_key& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index &received_subaddr) const { + std::string ki = string_tools::pod_to_hex(key_image); + auto found = m_generated_key_images.get(tx_public_key, out_index, received_subaddr); + + if (found != nullptr) { + return found->m_hex.get() == ki; + }; + + if (is_view_only()) return false; + + monero_key_image enote_key_image = generate_key_image(tx_public_key, out_index, received_subaddr); + std::string enote_ki = enote_key_image.m_hex.get(); + + if (ki == enote_ki) { + return true; + } + + return false; + } + + bool monero_wallet_keys::key_image_is_ours(const std::string &key_image, const std::string& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index &received_subaddr) const { + crypto::key_image ki; + crypto::public_key tx_pub_key; + + string_tools::hex_to_pod(key_image, ki); + string_tools::hex_to_pod(tx_public_key, tx_pub_key); + + return key_image_is_ours(ki, tx_pub_key, out_index, received_subaddr); + } + + std::string encrypt(const std::string &plaintext_str, const crypto::secret_key &skey, bool authenticated = true) { + const char *plaintext = plaintext_str.data(); + size_t len = plaintext_str.size(); + crypto::chacha_key key; + crypto::generate_chacha_key(&skey, sizeof(skey), key, 1); + std::string ciphertext; + crypto::chacha_iv iv = crypto::rand(); + ciphertext.resize(len + sizeof(iv) + (authenticated ? sizeof(crypto::signature) : 0)); + crypto::chacha20(plaintext, len, key, iv, &ciphertext[sizeof(iv)]); + memcpy(&ciphertext[0], &iv, sizeof(iv)); + if (authenticated) + { + crypto::hash hash; + crypto::cn_fast_hash(ciphertext.data(), ciphertext.size() - sizeof(crypto::signature), hash); + crypto::public_key pkey; + crypto::secret_key_to_public_key(skey, pkey); + crypto::signature &signature = *(crypto::signature*)&ciphertext[ciphertext.size() - sizeof(crypto::signature)]; + crypto::generate_signature(hash, pkey, skey, signature); + } + return ciphertext; + } + + std::string monero_wallet_keys::encrypt_with_private_view_key(const std::string &plaintext, bool authenticated) const { + return encrypt(plaintext, m_account.get_keys().m_view_secret_key, authenticated); + } + + template + T decrypt(const std::string &ciphertext, const crypto::secret_key &skey, bool authenticated = true) + { + const size_t prefix_size = sizeof(crypto::chacha_iv) + (authenticated ? sizeof(crypto::signature) : 0); + if(ciphertext.size() < prefix_size) throw std::runtime_error("Unexpected ciphertext size"); + uint64_t kdf_rounds = 1; + crypto::chacha_key key; + crypto::generate_chacha_key(&skey, sizeof(skey), key, kdf_rounds); + const crypto::chacha_iv &iv = *(const crypto::chacha_iv*)&ciphertext[0]; + if (authenticated) + { + crypto::hash hash; + crypto::cn_fast_hash(ciphertext.data(), ciphertext.size() - sizeof(crypto::signature), hash); + crypto::public_key pkey; + crypto::secret_key_to_public_key(skey, pkey); + const crypto::signature &signature = *(const crypto::signature*)&ciphertext[ciphertext.size() - sizeof(crypto::signature)]; + if(!crypto::check_signature(hash, pkey, signature)) throw std::runtime_error("Failed to authenticate ciphertext"); + } + std::unique_ptr buffer{new char[ciphertext.size() - prefix_size]}; + auto wiper = epee::misc_utils::create_scope_leave_handler([&]() { memwipe(buffer.get(), ciphertext.size() - prefix_size); }); + crypto::chacha20(ciphertext.data() + sizeof(iv), ciphertext.size() - prefix_size, key, iv, buffer.get()); + return T(buffer.get(), ciphertext.size() - prefix_size); + } + + std::string monero_wallet_keys::decrypt_with_private_view_key(const std::string &ciphertext, bool authenticated) const { + return decrypt(ciphertext, m_account.get_keys().m_view_secret_key, authenticated); + } + + std::vector monero_wallet_keys::parse_signed_tx(const std::string &signed_tx_st) const { + std::string s = signed_tx_st; + tools::wallet2::signed_tx_set signed_txs; + + const size_t magiclen = strlen(SIGNED_TX_PREFIX) - 1; + if (strncmp(s.c_str(), SIGNED_TX_PREFIX, magiclen)) + { + throw std::runtime_error("Bad magic from signed transaction"); + } + s = s.substr(magiclen); + const char version = s[0]; + s = s.substr(1); + if (version == '\003' || version == '\004') + { + throw std::runtime_error("Not loading deprecated format"); + } + else if (version == '\005') + { + try { s = decrypt_with_private_view_key(s); } + catch (const std::exception &e) { throw std::runtime_error(std::string("Failed to decrypt signed transaction: ") + e.what()); } + try + { + binary_archive ar{epee::strspan(s)}; + if (!::serialization::serialize(ar, signed_txs)) + { + throw std::runtime_error("Failed to deserialize signed transaction"); + } + } + catch (const std::exception &e) + { + throw std::runtime_error(std::string("Failed to decrypt signed transaction: ") + e.what()); + } + } + else + { + throw std::runtime_error("Unsupported version in signed transaction"); + } + + LOG_PRINT_L0("Loaded signed tx data from binary: " << signed_txs.ptx.size() << " transactions"); + for (auto &c_ptx: signed_txs.ptx) LOG_PRINT_L0(cryptonote::obj_to_json_str(c_ptx.tx)); + + return signed_txs.ptx; + } + + tools::wallet2::unsigned_tx_set monero_wallet_keys::parse_unsigned_tx(const std::string &unsigned_tx_st) const { + tools::wallet2::unsigned_tx_set exported_txs; + + std::string s = unsigned_tx_st; + const size_t magiclen = strlen(UNSIGNED_TX_PREFIX) - 1; + if (strncmp(s.c_str(), UNSIGNED_TX_PREFIX, magiclen)) + { + throw std::runtime_error("Bad magic from unsigned tx"); + } + s = s.substr(magiclen); + const char version = s[0]; + s = s.substr(1); + if (version == '\003' || version == '\004') + { + throw std::runtime_error("Not loading deprecated format"); + } + else if (version == '\005') + { + try { s = decrypt_with_private_view_key(s); } + catch(const std::exception &e) { + std::string msg = std::string("Failed to decrypt unsigned tx: ") + e.what(); + throw std::runtime_error(msg); + } + try + { + binary_archive ar{epee::strspan(s)}; + if (!::serialization::serialize(ar, exported_txs)) + { + throw std::runtime_error("Failed to parse data from unsigned tx"); + } + } + catch (...) + { + throw std::runtime_error("Failed to parse data from unsigned tx"); + } + } + else + { + throw std::runtime_error("Unsupported version in unsigned tx"); + } + + LOG_PRINT_L1("Loaded tx unsigned data from binary: " << exported_txs.txes.size() << " transactions"); + + return exported_txs; + } + + std::string monero_wallet_keys::dump_pending_tx(tools::wallet2::tx_construction_data &construction_data, const boost::optional& payment_id) const { + tools::wallet2::unsigned_tx_set txs; + if (payment_id != boost::none && !payment_id->empty()) { + crypto::hash8 _payment_id; + + if (!monero_utils::parse_short_payment_id(payment_id.get(), _payment_id)) throw std::runtime_error("invalid short payment id: " + payment_id.get()); + cryptonote::remove_field_from_tx_extra(construction_data.extra, typeid(cryptonote::tx_extra_nonce)); + std::string extra_nonce; + cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, _payment_id); + if(!cryptonote::add_extra_nonce_to_tx_extra(construction_data.extra, extra_nonce)) throw std::runtime_error("Failed to add decrypted payment id to tx extra"); + LOG_PRINT_L0("Successfully decrypted payment ID: " << payment_id.get()); + } + else { + LOG_PRINT_L0("Payment ID not set"); + } + + //txs.txes.push_back(get_construction_data_with_decrypted_short_payment_id(tx, m_account.get_device())); + txs.txes.push_back(construction_data); + + txs.new_transfers = export_outputs(false, 0); + // save as binary + std::ostringstream oss; + binary_archive ar(oss); + try + { + if (!::serialization::serialize(ar, txs)) + return std::string(); + } + catch (...) + { + return std::string(); + } + + LOG_PRINT_L0("Saving unsigned tx data: " << oss.str()); + + std::string ciphertext = encrypt_with_private_view_key(oss.str()); + return epee::string_tools::buff_to_hex_nodelimer(std::string(UNSIGNED_TX_PREFIX) + ciphertext); + } + + wallet2_exported_outputs monero_wallet_keys::export_outputs(bool all, uint32_t start, uint32_t count) const { + throw std::runtime_error("monero_wallet_keys::export_outputs(): not supported"); + } + + bool monero_wallet_keys::get_tx_key_cached(const crypto::hash &txid, crypto::secret_key &tx_key, std::vector &additional_tx_keys) const { + additional_tx_keys.clear(); + const std::unordered_map::const_iterator i = m_tx_keys.find(txid); + if (i == m_tx_keys.end()) + return false; + tx_key = i->second; + if (tx_key == crypto::null_skey) + return false; + const auto j = m_additional_tx_keys.find(txid); + if (j != m_additional_tx_keys.end()) + additional_tx_keys = j->second; + return true; + } + + bool monero_wallet_keys::get_tx_key(const crypto::hash &txid, crypto::secret_key &tx_key, std::vector &additional_tx_keys) const { + bool r = get_tx_key_cached(txid, tx_key, additional_tx_keys); + if (r) + { + MDEBUG("tx key cached for txid: " << txid); + return true; + } + + auto& hwdev = m_account.get_device(); + + // So far only Cold protocol devices are supported. + if (hwdev.device_protocol() != hw::device::PROTOCOL_COLD) + { + return false; + } + + auto dev_cold = dynamic_cast<::hw::device_cold*>(&hwdev); + CHECK_AND_ASSERT_THROW_MES(dev_cold, "Device does not implement cold signing interface"); + if (!dev_cold->is_get_tx_key_supported()) + { + MDEBUG("get_tx_key not supported by the device"); + return false; + } + + hw::device_cold::tx_key_data_t tx_key_data; + std::string tx_hash = epee::string_tools::pod_to_hex(txid); + tx_key_data.tx_prefix_hash = get_tx_prefix_hash(tx_hash); + + if (tx_key_data.tx_prefix_hash.empty()) return false; + + std::vector tx_keys; + dev_cold->get_tx_key(tx_keys, tx_key_data, m_account.get_keys().m_view_secret_key); + if (tx_keys.empty()) + { + MDEBUG("Empty tx keys for txid: " << txid); + return false; + } + + if (tx_keys[0] == crypto::null_skey) return false; + + tx_key = tx_keys[0]; + tx_keys.erase(tx_keys.begin()); + additional_tx_keys = tx_keys; + return true; + } + + // implementation based on monero-project wallet2::sign_tx() + std::string monero_wallet_keys::sign_tx(tools::wallet2::unsigned_tx_set &exported_txs, std::vector &txs, tools::wallet2::signed_tx_set &signed_txes, std::vector& signed_kis) { + const auto& subaddresses = m_subaddresses; + + // sign the transactions + for (size_t n = 0; n < exported_txs.txes.size(); ++n) + { + tools::wallet2::tx_construction_data &sd = exported_txs.txes[n]; + if(sd.sources.empty()) throw std::runtime_error("empty sources"); + if(sd.unlock_time) throw std::runtime_error("unlock time is non-zero"); + LOG_PRINT_L1(" " << (n+1) << ": " << sd.sources.size() << " inputs, ring size " << sd.sources[0].outputs.size()); + signed_txes.ptx.push_back(tools::wallet2::pending_tx()); + tools::wallet2::pending_tx &ptx = signed_txes.ptx.back(); + rct::RCTConfig rct_config = sd.rct_config; + crypto::secret_key tx_key; + std::vector additional_tx_keys; + + bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), subaddresses, sd.sources, sd.splitted_dsts, sd.change_dts.addr, sd.extra, ptx.tx, tx_key, additional_tx_keys, sd.use_rct, rct_config, sd.use_view_tags); + if(!r) throw std::runtime_error("tx not constructed"); + // we don't test tx size, because we don't know the current limit, due to not having a blockchain, + // and it's a bit pointless to fail there anyway, since it'd be a (good) guess only. We sign anyway, + // and if we really go over limit, the daemon will reject when it gets submitted. Chances are it's + // OK anyway since it was generated in the first place, and rerolling should be within a few bytes. + + // normally, the tx keys are saved in commit_tx, when the tx is actually sent to the daemon. + // we can't do that here since the tx will be sent from the compromised wallet, which we don't want + // to see that info, so we save it here + if (tx_key != crypto::null_skey) + { + const crypto::hash txid = get_transaction_hash(ptx.tx); + m_tx_keys[txid] = tx_key; + m_additional_tx_keys[txid] = additional_tx_keys; + } + + std::string key_images; + bool all_are_txin_to_key = std::all_of(ptx.tx.vin.begin(), ptx.tx.vin.end(), [&](const cryptonote::txin_v& s_e) -> bool + { + CHECKED_GET_SPECIFIC_VARIANT(s_e, const cryptonote::txin_to_key, in, false); + key_images += boost::to_string(in.k_image) + " "; + return true; + }); + if(!all_are_txin_to_key) throw std::runtime_error("unexpected txin type"); + + ptx.key_images = key_images; + ptx.fee = 0; + for (const auto &i: sd.sources) ptx.fee += i.amount; + for (const auto &i: sd.splitted_dsts) ptx.fee -= i.amount; + ptx.dust = 0; + ptx.dust_added_to_fee = false; + ptx.change_dts = sd.change_dts; + ptx.selected_transfers = sd.selected_transfers; + ptx.tx_key = rct::rct2sk(rct::identity()); // don't send it back to the untrusted view wallet + ptx.dests = sd.dests; + ptx.construction_data = sd; + + txs.push_back(ptx); + + // add tx keys only to ptx + txs.back().tx_key = tx_key; + txs.back().additional_tx_keys = additional_tx_keys; + } + + // add key image mapping for these txes + const auto &keys = m_account.get_keys(); + hw::device &hwdev = m_account.get_device(); + for (size_t n = 0; n < exported_txs.txes.size(); ++n) + { + const cryptonote::transaction &tx = signed_txes.ptx[n].tx; + + crypto::key_derivation derivation; + std::vector additional_derivations; + + // compute public keys from out secret keys + crypto::public_key tx_pub_key; + crypto::secret_key_to_public_key(txs[n].tx_key, tx_pub_key); + std::vector additional_tx_pub_keys; + for (const crypto::secret_key &skey: txs[n].additional_tx_keys) + { + additional_tx_pub_keys.resize(additional_tx_pub_keys.size() + 1); + crypto::secret_key_to_public_key(skey, additional_tx_pub_keys.back()); + } + + // compute derivations + hwdev.set_mode(hw::device::TRANSACTION_PARSE); + if (!hwdev.generate_key_derivation(tx_pub_key, keys.m_view_secret_key, derivation)) + { + MWARNING("Failed to generate key derivation from tx pubkey in " << cryptonote::get_transaction_hash(tx) << ", skipping"); + static_assert(sizeof(derivation) == sizeof(rct::key), "Mismatched sizes of key_derivation and rct::key"); + memcpy(&derivation, rct::identity().bytes, sizeof(derivation)); + } + for (size_t i = 0; i < additional_tx_pub_keys.size(); ++i) + { + additional_derivations.push_back({}); + if (!hwdev.generate_key_derivation(additional_tx_pub_keys[i], keys.m_view_secret_key, additional_derivations.back())) + { + MWARNING("Failed to generate key derivation from additional tx pubkey in " << cryptonote::get_transaction_hash(tx) << ", skipping"); + memcpy(&additional_derivations.back(), rct::identity().bytes, sizeof(crypto::key_derivation)); + } + } + + for (size_t i = 0; i < tx.vout.size(); ++i) + { + crypto::public_key output_public_key; + if (!get_output_public_key(tx.vout[i], output_public_key)) + continue; + + // if this output is back to this wallet, we can calculate its key image already + if (!is_out_to_acc_precomp(subaddresses, output_public_key, derivation, additional_derivations, i, hwdev, get_output_view_tag(tx.vout[i]))) + continue; + crypto::key_image ki; + cryptonote::keypair in_ephemeral; + if (cryptonote::generate_key_image_helper(keys, subaddresses, output_public_key, tx_pub_key, additional_tx_pub_keys, i, in_ephemeral, ki, hwdev)) + signed_txes.tx_key_images[output_public_key] = ki; + else + MERROR("Failed to calculate key image"); + } + } + + // add key images + signed_txes.key_images.resize(signed_kis.size()); + + for (size_t i = 0; i < signed_kis.size(); ++i) + { + std::string& signed_ki = signed_kis[i]; + crypto::key_image ski; + + if (signed_ki.empty()) + LOG_PRINT_L0("WARNING: key image not known in signing wallet at index " << i); + else epee::string_tools::hex_to_pod(signed_ki, ski); + + signed_txes.key_images[i] = ski; + } + + // save as binary + std::ostringstream oss; + binary_archive ar(oss); + try + { + if (!::serialization::serialize(ar, signed_txes)) + return std::string(); + } + catch(...) + { + return std::string(); + } + LOG_PRINT_L3("Saving signed tx data (with encryption): " << oss.str()); + std::string ciphertext = encrypt_with_private_view_key(oss.str()); + return std::string(SIGNED_TX_PREFIX) + ciphertext; + } + void monero_wallet_keys::init_common() { m_primary_address = m_account.get_public_address_str(static_cast(m_network_type)); const cryptonote::account_keys& keys = m_account.get_keys(); @@ -318,3 +1164,4 @@ namespace monero { if (m_prv_spend_key == "0000000000000000000000000000000000000000000000000000000000000000") m_prv_spend_key = ""; } } + diff --git a/src/wallet/monero_wallet_keys.h b/src/wallet/monero_wallet_keys.h index f4d3ae92..bbb28d93 100644 --- a/src/wallet/monero_wallet_keys.h +++ b/src/wallet/monero_wallet_keys.h @@ -54,6 +54,12 @@ #include "monero_wallet.h" #include "cryptonote_basic/account.h" +#include "wallet/wallet2.h" +#include +#include + +#define UNSIGNED_TX_PREFIX "Monero unsigned tx set\005" +#define SIGNED_TX_PREFIX "Monero signed tx set\005" using namespace monero; @@ -62,6 +68,25 @@ using namespace monero; */ namespace monero { + typedef std::tuple> wallet2_exported_outputs; + + class monero_key_image_cache { + public: + + std::shared_ptr get(const crypto::public_key& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index &received_subaddr); + std::shared_ptr get(const std::string& tx_public_key, uint64_t out_index, uint32_t account_idx = 0, uint32_t subaddress_idx = 0); + void set(const std::shared_ptr& key_image, const std::string& tx_public_key, uint64_t out_index, uint32_t account_idx = 0, uint32_t subaddress_idx = 0, bool requested = false); + void set(const std::shared_ptr& key_image, const crypto::public_key& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index &received_subaddr, bool requested = false); + bool request(const std::string& tx_public_key, uint64_t out_index, uint32_t account_idx, uint32_t subaddress_idx); + bool request(const crypto::public_key& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index &received_subaddr); + void set_request(const std::string& tx_public_key, uint64_t out_index, uint32_t account_idx = 0, uint32_t subaddress_idx = 0, bool request = true); + + private: + mutable boost::mutex m_mutex; + serializable_unordered_map, bool>>>> m_cache; + serializable_map m_frozen; + }; + /** * Implements a Monero wallet to provide basic key management. */ @@ -126,11 +151,14 @@ namespace monero { std::vector get_subaddresses(const uint32_t account_idx, const std::vector& subaddress_indices) const override; std::string sign_message(const std::string& msg, monero_message_signature_type signature_type, uint32_t account_idx = 0, uint32_t subaddress_idx = 0) const override; monero_message_signature_result verify_message(const std::string& msg, const std::string& address, const std::string& signature) const override; + std::string get_payment_uri(const monero_tx_config& config) const override; + std::shared_ptr parse_payment_uri(const std::string& uri) const override; + std::string get_tx_key(const std::string& tx_hash) const override; void close(bool save = false) override; // --------------------------------- PRIVATE -------------------------------- - private: + protected: bool m_is_view_only; monero_network_type m_network_type; cryptonote::account_base m_account; @@ -141,7 +169,32 @@ namespace monero { std::string m_pub_spend_key; std::string m_prv_spend_key; std::string m_primary_address; - - void init_common(); + mutable monero_key_image_cache m_generated_key_images; + serializable_unordered_map m_subaddresses; + serializable_unordered_map m_tx_keys; + serializable_unordered_map> m_additional_tx_keys; + + virtual void init_common(); + cryptonote::network_type get_nettype() const { return m_network_type == monero_network_type::TESTNET ? cryptonote::network_type::TESTNET : m_network_type == monero_network_type::STAGENET ? cryptonote::network_type::STAGENET : cryptonote::network_type::MAINNET; }; + bool key_on_device() const; + + monero_key_image generate_key_image(const std::string &tx_public_key, uint64_t out_index, const cryptonote::subaddress_index &received_subaddr) const; + monero_key_image generate_key_image(const crypto::public_key& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index &received_subaddr) const; + std::pair generate_key_image_for_enote(const crypto::public_key &ephem_pubkey, const size_t tx_output_index, const cryptonote::subaddress_index &received_subaddr) const; + bool key_image_is_ours(const crypto::key_image &key_image, const crypto::public_key& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index &received_subaddr) const; + bool key_image_is_ours(const std::string &key_image, const std::string& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index &received_subaddr) const; + + std::string encrypt_with_private_view_key(const std::string &plaintext, bool authenticated = true) const; + std::string decrypt_with_private_view_key(const std::string &ciphertext, bool authenticated = true) const; + + virtual wallet2_exported_outputs export_outputs(bool all, uint32_t start, uint32_t count = 0xffffffff) const; + + std::vector parse_signed_tx(const std::string &signed_tx_st) const; + tools::wallet2::unsigned_tx_set parse_unsigned_tx(const std::string &unsigned_tx_st) const; + std::string dump_pending_tx(tools::wallet2::tx_construction_data &construction_data, const boost::optional& payment_id) const; + std::string sign_tx(tools::wallet2::unsigned_tx_set &exported_txs, std::vector &txs, tools::wallet2::signed_tx_set &signed_txes, std::vector& signed_kis); + bool get_tx_key_cached(const crypto::hash &txid, crypto::secret_key &tx_key, std::vector &additional_tx_keys) const; + bool get_tx_key(const crypto::hash &txid, crypto::secret_key &tx_key, std::vector &additional_tx_keys) const; + virtual std::string get_tx_prefix_hash(const std::string& tx_hash) const { return std::string(""); }; }; } diff --git a/src/wallet/monero_wallet_light.cpp b/src/wallet/monero_wallet_light.cpp new file mode 100644 index 00000000..cef38683 --- /dev/null +++ b/src/wallet/monero_wallet_light.cpp @@ -0,0 +1,2823 @@ +#include "monero_wallet_light.h" +#include "utils/gen_utils.h" +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "cryptonote_core/cryptonote_tx_utils.h" +#include "mnemonics/electrum-words.h" +#include "mnemonics/english.h" +#include "common/threadpool.h" +#include "net/jsonrpc_structs.h" +#include "serialization/serialization.h" + +#define OUTPUT_EXPORT_FILE_MAGIC "Monero output export\004" +#define TAIL_EMISSION_HEIGHT 2641623 +#define APPROXIMATE_INPUT_BYTES 80 + +namespace monero { + + // ------------------------- INITIALIZE CONSTANTS --------------------------- + + static const int BULLETPROOF_VERSION = 4; // default bulletproof version + static const uint32_t RING_SIZE = 16; + static const uint32_t MIXIN_SIZE = RING_SIZE - 1; + static const uint32_t DEFAULT_FEE_PRIORITY = 1; + static const uint32_t DUST_THRESHOLD = 2000000000; + + // ----------------------- INTERNAL PRIVATE HELPERS ----------------------- + + uint64_t get_fee_multiplier(uint32_t priority) { + // v8 fee algorithm 3 + if (priority == 2) return 5; + if (priority == 3) return 25; + if (priority == 4) return 1000; + return 1; + } + + size_t estimate_rct_tx_size(int n_inputs, int mixin, int n_outputs, size_t extra_size) { + size_t size = 0; + // tx prefix first few bytes + size += 1 + 6; + + // vin + size += n_inputs * (1+6+(mixin+1)*2+32); + + // vout + size += n_outputs * (6+32); + + // extra + size += extra_size; + if (!extra_size && n_outputs <= 2) + size += 3 + sizeof(crypto::hash8); + + // rct signatures + size += 1; + + // rangeSigs + size_t log_padded_outputs = 0; + while ((1< 2) { + const uint64_t bp_base = 368; + size_t log_padded_outputs = 2; + while ((1< 0) return default_limit; + return CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 / 2 - CRYPTONOTE_COINBASE_BLOB_RESERVED_SIZE; // v8 + } + + void normalize_unconfirmed_tx(const std::shared_ptr &tx) { + tx->m_outputs.clear(); + + const auto &transfer = tx->m_outgoing_transfer.get(); + tx->m_change_address = boost::none; + tx->m_change_amount = boost::none; + + if (transfer->m_subaddress_indices.size() > 0) { + const auto &subaddress_idx = transfer->m_subaddress_indices[0]; + transfer->m_subaddress_indices.clear(); + transfer->m_subaddress_indices.push_back(subaddress_idx); // subaddress index is known iff 1 requested // TODO: get all known subaddress indices here + } + + for(const auto &input : tx->m_inputs) { + input->m_amount = boost::none; + } + } + + void validate_transfer(const std::vector &to_address_strings, const boost::optional& payment_id_string, cryptonote::network_type nettype, std::vector& infos, std::vector& extra) { + if (to_address_strings.empty()) throw std::runtime_error("No destinations for this transfer"); + crypto::hash8 integrated_payment_id = crypto::null_hash8; + std::string extra_nonce; + std::vector addr_infos(to_address_strings.size()); + size_t to_addr_idx = 0; + for (const auto& addr : to_address_strings) { + if (!cryptonote::get_account_address_from_str(addr_infos[to_addr_idx++], nettype, addr)) { + throw std::runtime_error("Invalid destination address"); + } + } + + bool payment_id_seen = payment_id_string != boost::none && !payment_id_string->empty(); + for (const auto& info : addr_infos) { + infos.push_back(info); + if (!info.has_payment_id) continue; + if (payment_id_seen || integrated_payment_id != crypto::null_hash8) { + throw std::runtime_error("A single payment id is allowed per transaction"); + } + integrated_payment_id = info.payment_id; + cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, integrated_payment_id); + if (!cryptonote::add_extra_nonce_to_tx_extra(extra, extra_nonce)) { + throw std::runtime_error("Something went wrong with integrated payment_id."); + } + } + + if (payment_id_seen) throw std::runtime_error("Standalone payment IDs are obsolete. Use subaddresses or integrated addresses instead"); + } + + void validate_cn_tx(const cryptonote::transaction &tx) { + if (get_tx_weight_limit() <= get_transaction_weight(tx)) throw std::runtime_error("transaction is too big"); + if(tx.rct_signatures.p.bulletproofs_plus.empty()) throw std::runtime_error("Expected tx to use bulletproofs"); + + auto tx_blob = t_serializable_object_to_blob(tx); + size_t tx_blob_size = tx_blob.size(); + if(tx_blob_size <= 0) throw std::runtime_error("Expected tx blob byte length > 0"); + } + + std::shared_ptr build_tx_with_vout(const monero_light_tx_store& tx_store, const monero_light_output_store& output_store, const monero_light_output& out, uint64_t current_height) { + + // construct block + std::shared_ptr block = std::make_shared(); + block->m_height = out.m_height; + + // construct tx + std::shared_ptr tx = std::make_shared(); + tx->m_block = block; + block->m_txs.push_back(tx); + tx->m_hash = out.m_tx_hash; + tx->m_is_confirmed = true; + tx->m_is_failed = false; + tx->m_is_relayed = true; + tx->m_in_tx_pool = false; + tx->m_relay = true; + tx->m_is_double_spend_seen = false; + tx->m_is_locked = tx_store.is_locked(out.m_tx_hash.get(), current_height); + + // construct output + std::shared_ptr output = std::make_shared(); + output->m_tx = tx; + tx->m_outputs.push_back(output); + output->m_amount = out.m_amount; + output->m_index = out.m_global_index; + output->m_account_index = out.m_recipient.m_maj_i; + output->m_subaddress_index = out.m_recipient.m_min_i; + output->m_is_spent = out.is_spent(); + output->m_is_frozen = false; + output->m_stealth_public_key = out.m_public_key; + if (out.key_image_is_known()) { + output->m_key_image = std::make_shared(); + output->m_key_image.get()->m_hex = out.m_key_image; + output->m_is_frozen = output_store.is_frozen(out); + } + + // return pointer to new tx + return tx; + } + + monero_light_get_random_outs_response get_random_outs(const std::unique_ptr& client, const std::vector &using_outs, boost::optional& prior_attempt) { + // request decoys for any newly selected inputs + std::vector decoy_requests; + if (prior_attempt != boost::none) { + for (size_t i = 0; i < using_outs.size(); ++i) { + // only need to request decoys for outs that were not already passed in + if (prior_attempt->find(*using_outs[i].m_public_key) == prior_attempt->end()) { + decoy_requests.push_back(using_outs[i]); + } + } + } else { + decoy_requests = using_outs; + } + + std::vector decoy_amounts; + for (auto &using_out : decoy_requests) { + if (using_out.is_rct()) { + decoy_amounts.push_back(0); + } else { + decoy_amounts.push_back(using_out.m_amount.get()); + MDEBUG("pushing decoy req amount: " << using_out.m_amount.get()); + } + } + + return client->get_random_outs(MIXIN_SIZE + 1, decoy_amounts); + } + + bool output_before(const monero_light_output& ow1, const monero_light_output& ow2) { + // compare by account index, subaddress index, output index, then global index + if (ow1.m_recipient.m_maj_i < ow2.m_recipient.m_maj_i) return true; + if (ow1.m_recipient.m_maj_i == ow2.m_recipient.m_maj_i) { + if (ow1.m_recipient.m_min_i < ow2.m_recipient.m_min_i) return true; + if (ow1.m_recipient.m_min_i == ow2.m_recipient.m_min_i) { + if (ow1.m_global_index.get() < ow2.m_global_index.get()) return true; + if (ow1.m_global_index.get() == ow2.m_global_index.get()) throw std::runtime_error("Should never sort outputs with duplicate indices"); + } + } + return false; + } + + tied_spendable_to_random_outs tie_unspent_to_mix_outs(const std::vector &using_outs, std::vector mix_outs_from_server, const boost::optional &prior_attempt_unspent_outs_to_mix_outs) { + // combine newly requested mix outs returned from the server, with the already known decoys from prior tx construction attempts, + // so that the same decoys will be re-used with the same outputs in all tx construction attempts. This ensures fee returned + // by calculate_fee() will be correct in the final tx, and also reduces number of needed trips to the server during tx construction. + monero_light_spendable_random_outputs prior_attempt_unspent_outs_to_mix_outs_new; + if (prior_attempt_unspent_outs_to_mix_outs) { + prior_attempt_unspent_outs_to_mix_outs_new = *prior_attempt_unspent_outs_to_mix_outs; + } + + std::vector mix_outs; + mix_outs.reserve(using_outs.size()); + + for (size_t i = 0; i < using_outs.size(); ++i) { + auto out = using_outs[i]; + + // if we don't already know of a particular out's mix outs (from a prior attempt), + // then tie out to a set of mix outs retrieved from the server + if (prior_attempt_unspent_outs_to_mix_outs_new.find(*out.m_public_key) == prior_attempt_unspent_outs_to_mix_outs_new.end()) { + for (size_t j = 0; j < mix_outs_from_server.size(); ++j) { + if ((out.m_rct != boost::none && mix_outs_from_server[j].m_amount.get() != 0) || + (out.m_rct == boost::none && mix_outs_from_server[j].m_amount.get() != out.m_amount.get())) { + continue; + } + + monero_light_random_outputs output_mix_outs = monero_utils::pop_index(mix_outs_from_server, j); + + // if we need to retry constructing tx, will remember to use same mix outs for this out on subsequent attempt(s) + prior_attempt_unspent_outs_to_mix_outs_new[*out.m_public_key] = output_mix_outs.m_outputs; + mix_outs.push_back(std::move(output_mix_outs)); + break; + } + } else { + monero_light_random_outputs output_mix_outs; + output_mix_outs.m_outputs = prior_attempt_unspent_outs_to_mix_outs_new[*out.m_public_key]; + output_mix_outs.m_amount = out.m_amount; + mix_outs.push_back(std::move(output_mix_outs)); + } + } + + // we expect to have a set of mix outs for every output in the tx + if (mix_outs.size() != using_outs.size()) { + throw std::runtime_error("not enough usable decoys found: " + std::to_string(mix_outs.size())); + } + + // we expect to use up all mix outs returned by the server + if (!mix_outs_from_server.empty()) { + throw std::runtime_error("too many decoy remaining"); + } + + tied_spendable_to_random_outs result; + result.m_mix_outs = std::move(mix_outs); + result.m_prior_attempt_unspent_outs_to_mix_outs_new = std::move(prior_attempt_unspent_outs_to_mix_outs_new); + + return result; + } + + monero_light_get_random_outs_params prepare_get_random_outs_params(const boost::optional& payment_id_string, const std::vector& sending_amounts, bool is_sweeping, uint32_t simple_priority, const std::vector &unspent_outs, uint64_t fee_per_b, uint64_t fee_quantization_mask, boost::optional prior_attempt_size_calcd_fee, boost::optional prior_attempt_unspent_outs_to_mix_outs = boost::none) { + monero_light_get_random_outs_params params; + + if (!is_sweeping) { + for (uint64_t sending_amount : sending_amounts) { + if (sending_amount == 0) { + throw std::runtime_error("entered amount is too low"); + } + } + } + + params.m_mixin = MIXIN_SIZE; + + std::vector extra; + monero_utils::add_pid_to_tx_extra(payment_id_string, extra); + + const uint64_t base_fee = fee_per_b; + const uint64_t fee_multiplier = get_fee_multiplier(simple_priority); + + uint64_t attempt_at_min_fee; + // use a minimum viable estimate_fee() with 1 input. It would be better to under-shoot this estimate, and then need to use a higher fee from calculate_fee() because the estimate is too low, + // versus the worse alternative of over-estimating here and getting stuck using too high of a fee that leads to fingerprinting + if (prior_attempt_size_calcd_fee == boost::none) + attempt_at_min_fee = estimate_fee(1, MIXIN_SIZE, 2, extra.size(), base_fee, fee_multiplier, fee_quantization_mask); + else + attempt_at_min_fee = *prior_attempt_size_calcd_fee; + + // fee may get changed as follows… + uint64_t sum_sending_amounts; + uint64_t potential_total; // aka balance_required + + if (is_sweeping) { + potential_total = sum_sending_amounts = UINT64_MAX; // balance required: all + } else { + sum_sending_amounts = 0; + for (uint64_t amount : sending_amounts) { + sum_sending_amounts += amount; + } + potential_total = sum_sending_amounts + attempt_at_min_fee; + } + // Gather outputs and amount to use for getting decoy outputs… + uint64_t using_outs_amount = 0; + std::vector remaining_outs = unspent_outs; // take copy so not to modify original + + // start by using all the passed in outs that were selected in a prior tx construction attempt + if (prior_attempt_unspent_outs_to_mix_outs != boost::none) { + for (size_t i = 0; i < remaining_outs.size(); ++i) { + monero_light_output &out = remaining_outs[i]; + + // search for out by public key to see if it should be re-used in an attempt + if (prior_attempt_unspent_outs_to_mix_outs->find(*out.m_public_key) != prior_attempt_unspent_outs_to_mix_outs->end()) { + using_outs_amount += out.m_amount.get(); + params.m_using_outs.push_back(std::move(monero_utils::pop_index(remaining_outs, i))); + } + } + } + + while (using_outs_amount < potential_total && remaining_outs.size() > 0) { + auto out = monero_utils::pop_random_value(remaining_outs); + if (out.m_amount.get() < DUST_THRESHOLD) { + if (!out.is_rct()) + continue; // unmixable (non-rct) dusty output + } + using_outs_amount += out.m_amount.get(); + params.m_using_outs.push_back(std::move(out)); + } + + //if (/*using_outs.size() > 1*/) { // FIXME? see original core js + uint64_t needed_fee = estimate_fee( + params.m_using_outs.size(), MIXIN_SIZE, sending_amounts.size(), extra.size(), + base_fee, fee_multiplier, fee_quantization_mask + ); + // if newNeededFee < neededFee, use neededFee instead (should only happen on the 2nd or later times through (due to estimated fee being too low)) + if (prior_attempt_size_calcd_fee != boost::none && needed_fee < attempt_at_min_fee) { + needed_fee = attempt_at_min_fee; + } + + // NOTE: needed_fee may get further modified below when !is_sweeping if using_outs_amount < total_incl_fees and gets finalized (for this function's scope) as using_fee + uint64_t total_wo_fee = is_sweeping + ? /*now that we know outsAmount>needed_fee*/(using_outs_amount - needed_fee) + : sum_sending_amounts; + params.m_final_total_wo_fee = total_wo_fee; + + uint64_t total_incl_fees; + if (is_sweeping) { + if (using_outs_amount < needed_fee) { // like checking if the result of the following total_wo_fee is < 0 + // sufficiently up-to-date (for this return case) required_balance and using_outs_amount (spendable balance) will have been stored for return by this point + throw std::runtime_error("need more money than found; sweeping, using_outs_amount: " + std::to_string(using_outs_amount) + ", needed_fee: " + std::to_string(needed_fee)); + } + total_incl_fees = using_outs_amount; + } else { + total_incl_fees = sum_sending_amounts + needed_fee; // because fee changed because using_outs.size() was updated + while (using_outs_amount < total_incl_fees && remaining_outs.size() > 0) { // add outputs 1 at a time till we either have them all or can meet the fee + { + auto out = monero_utils::pop_random_value(remaining_outs); + using_outs_amount += out.m_amount.get(); + params.m_using_outs.push_back(std::move(out)); + } + // Recalculate fee, total including fees + needed_fee = estimate_fee( + params.m_using_outs.size(), MIXIN_SIZE, sending_amounts.size(), extra.size(), + base_fee, fee_multiplier, fee_quantization_mask + ); + total_incl_fees = sum_sending_amounts + needed_fee; // because fee changed + } + } + params.m_using_fee = needed_fee; + + if (using_outs_amount < total_incl_fees) { + // sufficiently up-to-date (for this return case) required_balance and using_outs_amount (spendable balance) will have been stored for return by this point. + throw std::runtime_error("need more money than found; using_outs_amount: " + std::to_string(using_outs_amount) + ", total_incl_fees: " + std::to_string(total_incl_fees) + ", needed_fee: " + std::to_string(needed_fee)); + } + + // Change can now be calculated + uint64_t change_amount = 0; // to initialize + if (using_outs_amount > total_incl_fees) { + if(is_sweeping) throw std::runtime_error("Unexpected total_incl_fees > using_outs_amount while sweeping"); + change_amount = using_outs_amount - total_incl_fees; + } + params.m_change_amount = change_amount; + + // TODO create another tx if tx_estimated_weight >= TX_WEIGHT_TARGET(get_tx_weight_limit()) + + return params; + } + + tools::wallet2::pending_tx construct_tx( + cryptonote::network_type nettype, const serializable_unordered_map& subaddresses, + const cryptonote::account_keys& sender_account_keys, bool view_only, + const uint32_t subaddr_account_idx, const std::vector &to_address_strings, + const boost::optional& payment_id_string, const std::vector& sending_amounts, + std::vector& selected_transfers, + uint64_t change_amount, uint64_t fee_amount, const std::vector &outputs, + std::vector &mix_outs + ) { + std::vector extra; + std::vector to_addrs; + validate_transfer(to_address_strings, payment_id_string, nettype, to_addrs, extra); + // TODO: do we need to sort destinations by amount, here, according to 'decompose_destinations'? + if (mix_outs.size() != outputs.size()) { + throw std::runtime_error("wrong number of mix outs provided: " + std::to_string(mix_outs.size()) + ", outputs: " + std::to_string(outputs.size())); + } + for (size_t i = 0; i < mix_outs.size(); i++) { + if (mix_outs[i].m_outputs.size() < MIXIN_SIZE) { + throw std::runtime_error("not enough outputs for mixing"); + } + } + if (view_only) { + if (!sender_account_keys.get_device().verify_keys(sender_account_keys.m_view_secret_key, sender_account_keys.m_account_address.m_view_public_key)) { + throw std::runtime_error("Invalid view keys"); + } + } + else { + if (!sender_account_keys.get_device().verify_keys(sender_account_keys.m_spend_secret_key, sender_account_keys.m_account_address.m_spend_public_key) + || !sender_account_keys.get_device().verify_keys(sender_account_keys.m_view_secret_key, sender_account_keys.m_account_address.m_view_public_key)) { + throw std::runtime_error("Invalid secret keys"); + } + } + + uint64_t needed_money = fee_amount + change_amount; + for (uint64_t amount : sending_amounts) { + needed_money += amount; + if (needed_money < amount) throw std::runtime_error("transaction sum + fee exceeds " + cryptonote::print_money(std::numeric_limits::max())); + } + + uint64_t found_money = 0; + std::vector sources; + std::string spent_key_images; + LOG_PRINT_L2("preparing outputs"); + for (size_t out_index = 0; out_index < outputs.size(); out_index++) { + found_money += outputs[out_index].m_amount.get(); + if (found_money > UINT64_MAX) { + throw std::runtime_error("input amount overflow"); + } + auto src = cryptonote::tx_source_entry{}; + src.amount = outputs[out_index].m_amount.get(); + src.rct = outputs[out_index].is_rct(); + + typedef cryptonote::tx_source_entry::output_entry tx_output_entry; + if (mix_outs.size() != 0) { + // Sort fake outputs by global index + std::sort(mix_outs[out_index].m_outputs.begin(), mix_outs[out_index].m_outputs.end(), [] ( + monero_light_output const& a, + monero_light_output const& b + ) { + return a.m_global_index.get() < b.m_global_index.get(); + }); + for ( + size_t j = 0; + src.outputs.size() < MIXIN_SIZE && j < mix_outs[out_index].m_outputs.size(); + j++ + ) { + auto mix_out__output = mix_outs[out_index].m_outputs[j]; + if (mix_out__output.m_global_index == outputs[out_index].m_global_index) { + MDEBUG("got mixin the same as output, skipping"); + continue; + } + auto oe = tx_output_entry{}; + oe.first = mix_out__output.m_global_index.get(); + + crypto::public_key public_key = AUTO_VAL_INIT(public_key); + if(!epee::string_tools::hex_to_pod(*mix_out__output.m_public_key, public_key)) { + throw std::runtime_error("given an invalid public key"); + } + oe.second.dest = rct::pk2rct(public_key); + + if (mix_out__output.is_rct()) { + rct::key commit; + monero_utils::rct_hex_to_rct_commit(mix_out__output.m_rct.get(), commit); + oe.second.mask = commit; + } else { + if (outputs[out_index].is_rct()) { + throw std::runtime_error("mix RCT outs missing commit"); + } + oe.second.mask = rct::zeroCommit(src.amount); //create identity-masked commitment for non-rct mix input + } + src.outputs.push_back(oe); + } + } + + auto real_oe = tx_output_entry{}; + real_oe.first = outputs[out_index].m_global_index.get(); + + crypto::public_key public_key = AUTO_VAL_INIT(public_key); + if(!epee::string_tools::validate_hex(64, *outputs[out_index].m_public_key)) { + throw std::runtime_error("given an invalid public key"); + } + if (!epee::string_tools::hex_to_pod(*outputs[out_index].m_public_key, public_key)) { + throw std::runtime_error("given an invalid public key"); + } + real_oe.second.dest = rct::pk2rct(public_key); + + if (outputs[out_index].is_rct() && !outputs[out_index].is_mined()) { + rct::key commit; + monero_utils::rct_hex_to_rct_commit(outputs[out_index].m_rct.get(), commit); + real_oe.second.mask = commit; //add commitment for real input + } else { + real_oe.second.mask = rct::zeroCommit(src.amount/*aka outputs[out_index].amount*/); //create identity-masked commitment for non-rct input + } + + // Add real_oe to outputs + uint64_t real_output_index = src.outputs.size(); + for (size_t j = 0; j < src.outputs.size(); j++) { + if (real_oe.first < src.outputs[j].first) { + real_output_index = j; + break; + } + } + src.outputs.insert(src.outputs.begin() + real_output_index, real_oe); + crypto::public_key tx_pub_key = AUTO_VAL_INIT(tx_pub_key); + if(!epee::string_tools::validate_hex(64, *outputs[out_index].m_tx_pub_key)) { + throw std::runtime_error("given an invalid public key"); + } + + epee::string_tools::hex_to_pod(*outputs[out_index].m_tx_pub_key, tx_pub_key); + src.real_out_tx_key = tx_pub_key; + src.real_out_additional_tx_keys = cryptonote::get_additional_tx_pub_keys_from_extra(extra); + src.real_output = real_output_index; + uint64_t internal_output_index = *outputs[out_index].m_index; + src.real_output_in_tx_index = internal_output_index; + + src.rct = outputs[out_index].is_rct(); + if (src.rct) { + rct::key decrypted_mask; + bool r = monero_utils::rct_hex_to_decrypted_mask( + outputs[out_index].m_rct.get(), + sender_account_keys.m_view_secret_key, + tx_pub_key, + internal_output_index, + decrypted_mask + ); + if (!r) throw std::runtime_error("can't get decrypted mask from RCT hex"); + src.mask = decrypted_mask; + + rct::key calculated_commit = rct::commit(outputs[out_index].m_amount.get(), decrypted_mask); + rct::key parsed_commit; + monero_utils::rct_hex_to_rct_commit(outputs[out_index].m_rct.get(), parsed_commit); + if (!(real_oe.second.mask == calculated_commit)) { + throw std::runtime_error("rct commit hash mismatch"); + } + } else { + // in the original cn_utils impl this was left as null for generate_key_image_helper_rct to fill in with identity I + rct::identity(src.mask); + } + // not doing multisig here yet + src.multisig_kLRki = rct::multisig_kLRki({rct::zero(), rct::zero(), rct::zero(), rct::zero()}); + sources.push_back(src); + auto& key_image = outputs[out_index].m_key_image; + if (key_image != boost::none && !key_image->empty()) { + spent_key_images += key_image.get() + " "; + } + } + LOG_PRINT_L2("outputs prepared"); + // TODO: if this is a multisig wallet, create a list of multisig signers we can use + std::vector splitted_dsts; + if(to_addrs.size() != sending_amounts.size()) throw std::runtime_error("Amounts don't match destinations"); + for (size_t i = 0; i < to_addrs.size(); ++i) { + cryptonote::tx_destination_entry to_dst = AUTO_VAL_INIT(to_dst); + to_dst.addr = to_addrs[i].address; + to_dst.amount = sending_amounts[i]; + to_dst.is_subaddress = to_addrs[i].is_subaddress; + splitted_dsts.push_back(to_dst); + } + + cryptonote::tx_destination_entry change_dst = AUTO_VAL_INIT(change_dst); + change_dst.amount = change_amount; + if (change_dst.amount == 0) { + if (splitted_dsts.size() == 1) { + // If the change is 0, send it to a random address, to avoid confusing + // the sender with a 0 amount output. We send a 0 amount in order to avoid + // letting the destination be able to work out which of the inputs is the + // real one in our rings + LOG_PRINT_L2("generating dummy address for 0 change"); + cryptonote::account_base dummy; + dummy.generate(); + change_dst.addr = dummy.get_keys().m_account_address; + LOG_PRINT_L2("generated dummy address for 0 change"); + splitted_dsts.push_back(change_dst); + } + } else { + change_dst.addr = sender_account_keys.m_account_address; + splitted_dsts.push_back(change_dst); + } + + if (found_money > needed_money) { + if (change_dst.amount != fee_amount) { + throw std::runtime_error("result fee not equal to given"); + } + } + else if (found_money < needed_money) { + throw std::runtime_error("need more money than found; found_money: " + std::to_string(found_money) + ", needed_money: " + std::to_string(needed_money)); + } + + if (sources.empty()) throw std::runtime_error("sources is empty"); + + cryptonote::transaction tx; + crypto::secret_key tx_key; + std::vector additional_tx_keys; + + const rct::RCTConfig rct_config {rct::RangeProofPaddedBulletproof, BULLETPROOF_VERSION}; + LOG_PRINT_L2("constructing tx"); + bool r = cryptonote::construct_tx_and_get_tx_key( + sender_account_keys, subaddresses, + sources, splitted_dsts, change_dst.addr, extra, + tx, tx_key, additional_tx_keys, + true, rct_config, true); + + LOG_PRINT_L2("constructed tx, r=" << r); + if (!r) throw std::runtime_error("transaction was not constructed"); + validate_cn_tx(tx); + + tools::wallet2::pending_tx ptx; + ptx.key_images = spent_key_images; + ptx.dust = 0; + ptx.dust_added_to_fee = false; + ptx.tx = tx; + ptx.change_dts = change_dst; + ptx.tx_key = tx_key; + ptx.additional_tx_keys = additional_tx_keys; + ptx.fee = fee_amount; + ptx.dests = splitted_dsts; + ptx.selected_transfers = selected_transfers; + ptx.construction_data.sources = sources; + ptx.construction_data.change_dts = change_dst; + ptx.construction_data.splitted_dsts = splitted_dsts; + ptx.construction_data.selected_transfers = selected_transfers; + ptx.construction_data.extra = tx.extra; + ptx.construction_data.unlock_time = 0; + ptx.construction_data.use_rct = true; + ptx.construction_data.rct_config = rct_config; + ptx.construction_data.use_view_tags = true; + ptx.construction_data.dests = splitted_dsts; + // record which subaddress indices are being used as inputs + ptx.construction_data.subaddr_account = subaddr_account_idx; + ptx.construction_data.subaddr_indices.clear(); + for (const auto& selected_out : outputs) { + if (selected_out.m_recipient.m_maj_i != subaddr_account_idx) continue; + ptx.construction_data.subaddr_indices.insert(selected_out.m_recipient.m_min_i); + } + + LOG_PRINT_L2("transfer_selected_rct done"); + + return ptx; + } + + monero_wallet_light::monero_wallet_light(std::unique_ptr http_client_factory) { + m_client = std::make_unique(std::move(http_client_factory)); + } + + monero_wallet_light::~monero_wallet_light() { + MTRACE("~monero_wallet_light()"); + close(false); + } + + std::string monero_wallet_light::get_seed() const { + MTRACE("monero_wallet_light::get_seed()"); + if (is_view_only()) throw std::runtime_error("The wallet is watch-only. Cannot retrieve seed."); + return monero_wallet_keys::get_seed(); + } + + std::string monero_wallet_light::get_seed_language() const { + if (is_view_only()) throw std::runtime_error("The wallet is watch-only. Cannot retrieve seed language."); + return monero_wallet_keys::get_seed_language(); + } + + std::string monero_wallet_light::get_private_spend_key() const { + MTRACE("monero_wallet_light::get_private_spend_key()"); + if (is_view_only()) throw std::runtime_error("The wallet is watch-only. Cannot retrieve spend key."); + + std::string spend_key = epee::string_tools::pod_to_hex(unwrap(unwrap(m_account.get_keys().m_spend_secret_key))); + if (spend_key == "0000000000000000000000000000000000000000000000000000000000000000") spend_key = ""; + return spend_key; + } + + void monero_wallet_light::set_daemon_connection(const std::string& uri, const std::string& username, const std::string& password, const std::string& proxy_uri) { + m_client->set_connection(uri, username, password, proxy_uri); + if (is_connected_to_daemon()) { + try { login(); } + catch (...) { } + } + } + + void monero_wallet_light::set_daemon_connection(const boost::optional &connection) { + m_client->set_connection(connection); + if (is_connected_to_daemon()) { + try { login(); } + catch (...) { } + } + } + + boost::optional monero_wallet_light::get_daemon_connection() const { + return m_client->get_connection(); + } + + bool monero_wallet_light::is_connected_to_daemon() const { + m_is_connected = m_client->is_connected(); + return m_is_connected; + } + + uint64_t monero_wallet_light::get_daemon_height() const { + if (m_address_info.m_blockchain_height == boost::none) return 0; + uint64_t height = m_address_info.m_blockchain_height.get(); + return height == 0 ? 0 : height + 1; + } + + uint64_t monero_wallet_light::get_daemon_max_peer_height() const { + return get_daemon_height(); + } + + void monero_wallet_light::add_listener(monero_wallet_listener& listener) { + m_listeners.insert(&listener); + m_wallet_listener->update_listening(); + } + + void monero_wallet_light::remove_listener(monero_wallet_listener& listener) { + m_listeners.erase(&listener); + m_wallet_listener->update_listening(); + } + + std::set monero_wallet_light::get_listeners() { + return m_listeners; + } + + monero_sync_result monero_wallet_light::sync() { + MTRACE("sync()"); + if (!m_is_connected) throw std::runtime_error("Wallet is not connected to daemon"); + return lock_and_sync(); + } + + monero_sync_result monero_wallet_light::sync(monero_wallet_listener& listener) { + MTRACE("sync(listener)"); + if (!m_is_connected) throw std::runtime_error("Wallet is not connected to daemon"); + + // register listener + add_listener(listener); + + // sync wallet + monero_sync_result result = lock_and_sync(boost::none); + + // unregister listener + remove_listener(listener); + + // return sync result + return result; + } + + monero_sync_result monero_wallet_light::sync(uint64_t start_height) { + MTRACE("sync(" << start_height << ")"); + if (!m_is_connected) throw std::runtime_error("Wallet is not connected to daemon"); + return lock_and_sync(start_height); + } + + monero_sync_result monero_wallet_light::sync(uint64_t start_height, monero_wallet_listener& listener) { + MTRACE("sync(" << start_height << ", listener)"); + if (!m_is_connected) throw std::runtime_error("Wallet is not connected to daemon"); + + // wrap and register sync listener as wallet listener + add_listener(listener); + + // sync wallet + monero_sync_result result = lock_and_sync(start_height); + + // unregister sync listener + remove_listener(listener); + + // return sync result + return result; + } + + void monero_wallet_light::start_syncing(uint64_t sync_period_in_ms) { + if (!m_is_connected) throw std::runtime_error("Wallet is not connected to daemon"); + m_syncing_interval = sync_period_in_ms; + if (!m_syncing_enabled) { + m_syncing_enabled = true; + run_sync_loop(); // sync wallet on loop in background + } + } + + void monero_wallet_light::stop_syncing() { + m_syncing_enabled = false; + } + + void monero_wallet_light::scan_txs(const std::vector& tx_ids) { + sync(); + } + + void monero_wallet_light::rescan_spent() { + sync(); + } + + void monero_wallet_light::rescan_blockchain() { + MTRACE("rescan_blockchain()"); + if (!m_is_connected) throw std::runtime_error("Wallet is not connected to daemon"); + // m_rescan_on_sync = true; + lock_and_sync(); + } + + bool monero_wallet_light::is_daemon_synced() const { + if (!m_is_connected) throw std::runtime_error("Wallet is not connected to daemon"); + return true; + } + + bool monero_wallet_light::is_daemon_trusted() const { + return true; + } + + bool monero_wallet_light::is_synced() const { + if (!is_connected_to_daemon()) return false; + + if (m_address_info.m_blockchain_height.get() <= 1) { + return false; + } + + return m_address_info.m_scanned_block_height == m_address_info.m_blockchain_height.get(); + } + + monero_subaddress monero_wallet_light::get_address_index(const std::string& address) const { + MTRACE("get_address_index(" << address << ")"); + // validate address + cryptonote::address_parse_info info; + if (!get_account_address_from_str(info, get_nettype(), address)) { + throw std::runtime_error("Invalid address"); + } + + // get index of address in wallet + auto index = m_subaddresses.find(info.address.m_spend_public_key); + if (index == m_subaddresses.end()) throw std::runtime_error("Address doesn't belong to the wallet"); + + // return indices in subaddress + monero_subaddress subaddress; + cryptonote::subaddress_index cn_index = index->second; + subaddress.m_account_index = cn_index.major; + subaddress.m_index = cn_index.minor; + return subaddress; + } + + uint64_t monero_wallet_light::get_height() const { + if (m_address_info.m_scanned_block_height == boost::none) return 0; + uint64_t height = m_address_info.m_scanned_block_height.get(); + return height + 1; + } + + void monero_wallet_light::set_restore_height(uint64_t restore_height) { + auto response = m_client->import_request(get_primary_address(), get_private_view_key(), restore_height); + + if (response.m_import_fee != boost::none) { + throw std::runtime_error("Payment is required to rescan blockchain: address " + response.m_payment_address.get() + ", amount " + std::to_string(response.m_import_fee.get())); + } + } + + uint64_t monero_wallet_light::get_restore_height() const { + if (m_address_info.m_start_height == boost::none) return 0; + uint64_t height = m_address_info.m_start_height.get(); + return height == 0 ? 0 : height + 1; + } + + uint64_t monero_wallet_light::get_balance() const { + return m_output_store.get_balance(); + } + + uint64_t monero_wallet_light::get_balance(uint32_t account_index) const { + return m_output_store.get_balance(account_index); + } + + uint64_t monero_wallet_light::get_balance(uint32_t account_idx, uint32_t subaddress_idx) const { + return m_output_store.get_balance(account_idx, subaddress_idx); + } + + uint64_t monero_wallet_light::get_unlocked_balance() const { + return m_output_store.get_unlocked_balance(); + } + + uint64_t monero_wallet_light::get_unlocked_balance(uint32_t account_index) const { + return m_output_store.get_unlocked_balance(account_index); + } + + uint64_t monero_wallet_light::get_unlocked_balance(uint32_t account_idx, uint32_t subaddress_idx) const { + return m_output_store.get_unlocked_balance(account_idx, subaddress_idx); + } + + std::vector monero_wallet_light::get_accounts(bool include_subaddresses, const std::string& tag) const { + std::vector result; + bool default_found = false; + + if (m_subaddrs.m_all_subaddrs != boost::none) { + const auto& all_subaddrs = m_subaddrs.m_all_subaddrs.get(); + for (const auto& kv : all_subaddrs) { + if (kv.first == 0) default_found = true; + monero_account account = get_account(kv.first, include_subaddresses); + result.push_back(account); + } + } + + if (!default_found) { + monero_account primary_account = get_account(0, include_subaddresses); + result.push_back(primary_account); + } + + return result; + } + + monero_account monero_wallet_light::get_account(const uint32_t account_idx, bool include_subaddresses) const { + const auto& subaddrs = m_subaddrs.m_all_subaddrs; + + if (account_idx != 0 && (subaddrs == boost::none || subaddrs->empty())) throw std::runtime_error("Account out of bounds"); + + const auto& all_subaddrs = subaddrs.get(); + if (!all_subaddrs.is_upsert(account_idx)) throw std::runtime_error("account not upsert: " + std::to_string(account_idx)); + + monero_account account = monero_wallet_keys::get_account(account_idx, false); + + account.m_balance = get_balance(account_idx); + account.m_unlocked_balance = get_unlocked_balance(account_idx); + + try { + boost::optional label = get_subaddress_label(account_idx, 0); + if (label != boost::none && !label->empty()) account.m_tag = label; + } + catch (...) { + account.m_tag = boost::none; + } + + if (include_subaddresses) { + account.m_subaddresses = monero_wallet::get_subaddresses(account_idx); + } + + return account; + } + + monero_account monero_wallet_light::create_account(const std::string& label) { + uint32_t last_account_idx = 0; + if (m_subaddrs.m_all_subaddrs != boost::none) { + last_account_idx = m_subaddrs.m_all_subaddrs->get_last_account_index(); + } + + uint32_t account_idx = last_account_idx + 1; + monero_light_subaddrs subaddrs; + monero_light_index_range index_range(0, 0); + subaddrs[account_idx] = std::vector(); + subaddrs[account_idx].push_back(index_range); + upsert_subaddrs(subaddrs); + monero_account account = monero_wallet_keys::get_account(account_idx, false); + set_subaddress_label(account_idx, 0, label); + account.m_balance = 0; + account.m_unlocked_balance = 0; + if (label.empty()) account.m_tag = boost::none; + else account.m_tag = label; + return account; + } + + std::vector monero_wallet_light::get_subaddresses(const uint32_t account_idx, const std::vector& subaddress_indices) const { + std::vector subaddresses = get_subaddresses_aux(account_idx, subaddress_indices); + for(monero_subaddress& subaddress : subaddresses) { + init_subaddress(subaddress); + } + return subaddresses; + } + + monero_subaddress monero_wallet_light::create_subaddress(uint32_t account_idx, const std::string& label) { + bool account_found = false; + uint32_t last_subaddress_idx = 0; + + if (m_subaddrs.m_all_subaddrs != boost::none) { + account_found = m_subaddrs.m_all_subaddrs->contains(account_idx); + if (account_found) last_subaddress_idx = m_subaddrs.m_all_subaddrs->get_last_subaddress_index(account_idx); + } + + if (!account_found) throw std::runtime_error("create_subaddress(): account index out of bounds"); + + uint32_t subaddress_idx = last_subaddress_idx + 1; + + monero_light_subaddrs subaddrs; + monero_light_index_range index_range(last_subaddress_idx, subaddress_idx); + + subaddrs[account_idx] = std::vector(); + subaddrs[account_idx].push_back(index_range); + + upsert_subaddrs(subaddrs); + + monero_subaddress subaddress = get_subaddress(account_idx, subaddress_idx); + + set_subaddress_label(account_idx, subaddress_idx, label); + subaddress.m_label = label; + subaddress.m_balance = 0; + subaddress.m_unlocked_balance = 0; + subaddress.m_num_unspent_outputs = 0; + subaddress.m_is_used = false; + subaddress.m_num_blocks_to_unlock = 0; + + return subaddress; + } + + monero_subaddress monero_wallet_light::get_subaddress(const uint32_t account_idx, const uint32_t subaddress_idx) const { + std::vector indices; + indices.push_back(subaddress_idx); + std::vector subaddresses = monero_wallet_keys::get_subaddresses(account_idx, indices); + monero_subaddress& subaddress = subaddresses[0]; + init_subaddress(subaddress); + return subaddress; + } + + void monero_wallet_light::set_subaddress_label(uint32_t account_idx, uint32_t subaddress_idx, const std::string& label) { + m_subaddress_labels[account_idx][subaddress_idx] = label; + } + + std::vector monero_wallet_light::relay_txs(const std::vector& tx_metadatas) { + MTRACE("relay_txs()"); + + // relay each metadata as a tx + std::vector tx_hashes; + for (const auto& tx_metadata : tx_metadatas) { + + // parse tx metadata hex + cryptonote::blobdata blob; + if (!epee::string_tools::parse_hexstr_to_binbuff(tx_metadata, blob)) { + throw std::runtime_error("Failed to parse hex"); + } + + // deserialize tx + bool loaded = false; + tools::wallet2::pending_tx ptx; + try { + binary_archive ar{epee::strspan(blob)}; + if (::serialization::serialize(ar, ptx)) loaded = true; + } catch (...) {} + if (!loaded) { + try { + std::istringstream iss(blob); + boost::archive::portable_binary_iarchive ar(iss); + ar >> ptx; + loaded = true; + } catch (...) {} + } + if (!loaded) throw std::runtime_error("Failed to parse tx metadata"); + + // commit tx + try { + m_client->submit_raw_tx(epee::string_tools::buff_to_hex_nodelimer(tx_to_blob(ptx.tx))); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to commit tx"); + } + std::shared_ptr tx = monero_utils::ptx_to_tx(ptx, get_nettype(), this); + m_tx_store.set_unconfirmed(tx); + // collect resulting hash + std::string pending_tx_hash = epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx.tx)); + tx_hashes.push_back(pending_tx_hash); + } + + calculate_balance(); + + // notify listeners of spent funds + m_wallet_listener->on_spend_tx_hashes(tx_hashes); + + // return relayed tx hashes + return tx_hashes; + } + + monero_tx_set monero_wallet_light::describe_tx_set(const monero_tx_set& tx_set) { + + // get unsigned and multisig tx sets + std::string unsigned_tx_hex = tx_set.m_unsigned_tx_hex == boost::none ? "" : tx_set.m_unsigned_tx_hex.get(); + std::string multisig_tx_hex = tx_set.m_multisig_tx_hex == boost::none ? "" : tx_set.m_multisig_tx_hex.get(); + + // validate request + if (key_on_device()) throw std::runtime_error("command not supported by HW wallet"); + if (is_view_only()) throw std::runtime_error("command not supported by view-only wallet"); + if (unsigned_tx_hex.empty() && multisig_tx_hex.empty()) throw std::runtime_error("no txset provided"); + + std::vector tx_constructions; + if (!unsigned_tx_hex.empty()) { + try { + cryptonote::blobdata blob; + if (!epee::string_tools::parse_hexstr_to_binbuff(unsigned_tx_hex, blob)) throw std::runtime_error("Failed to parse hex."); + tools::wallet2::unsigned_tx_set exported_txs = parse_unsigned_tx(blob); + tx_constructions = exported_txs.txes; + } + catch (const std::exception &e) { + throw std::runtime_error("failed to parse unsigned transfers: " + std::string(e.what())); + } + } else if (!multisig_tx_hex.empty()) { + throw std::runtime_error("monero_wallet_light::describe_tx_set(): multisign not supported"); + } + + std::vector ptx; // TODO wallet_rpc_server: unused variable + try { + + // gather info for each tx + std::vector> txs; + std::unordered_map> dests; + int first_known_non_zero_change_index = -1; + for (int64_t n = 0; n < tx_constructions.size(); ++n) + { + // init tx + std::shared_ptr tx = std::make_shared(); + tx->m_is_outgoing = true; + tx->m_input_sum = 0; + tx->m_output_sum = 0; + tx->m_change_amount = 0; + tx->m_num_dummy_outputs = 0; + tx->m_ring_size = std::numeric_limits::max(); // smaller ring sizes will overwrite + + const tools::wallet2::tx_construction_data &cd = tx_constructions[n]; + std::vector tx_extra_fields; + bool has_encrypted_payment_id = false; + crypto::hash8 payment_id8 = crypto::null_hash8; + if (cryptonote::parse_tx_extra(cd.extra, tx_extra_fields)) + { + cryptonote::tx_extra_nonce extra_nonce; + if (find_tx_extra_field_by_type(tx_extra_fields, extra_nonce)) + { + crypto::hash payment_id; + if(cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id8)) + { + if (payment_id8 != crypto::null_hash8) + { + tx->m_payment_id = epee::string_tools::pod_to_hex(payment_id8); + has_encrypted_payment_id = true; + } + } + else if (cryptonote::get_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id)) + { + tx->m_payment_id = epee::string_tools::pod_to_hex(payment_id); + } + } + } + + for (uint64_t s = 0; s < cd.sources.size(); ++s) + { + tx->m_input_sum = tx->m_input_sum.get() + cd.sources[s].amount; + uint64_t ring_size = cd.sources[s].outputs.size(); + if (ring_size < tx->m_ring_size.get()) + tx->m_ring_size = ring_size; + } + for (uint64_t d = 0; d < cd.splitted_dsts.size(); ++d) + { + const cryptonote::tx_destination_entry &entry = cd.splitted_dsts[d]; + std::string address = cryptonote::get_account_address_as_str(get_nettype(), entry.is_subaddress, entry.addr); + if (has_encrypted_payment_id && !entry.is_subaddress && address != entry.original) + address = cryptonote::get_account_integrated_address_as_str(get_nettype(), entry.addr, payment_id8); + auto i = dests.find(entry.addr); + if (i == dests.end()) + dests.insert(std::make_pair(entry.addr, std::make_pair(address, entry.amount))); + else + i->second.second += entry.amount; + tx->m_output_sum = tx->m_output_sum.get() + entry.amount; + } + if (cd.change_dts.amount > 0) + { + auto it = dests.find(cd.change_dts.addr); + if (it == dests.end()) throw std::runtime_error("Claimed change does not go to a paid address"); + if (it->second.second < cd.change_dts.amount) throw std::runtime_error("Claimed change is larger than payment to the change address"); + if (cd.change_dts.amount > 0) + { + if (first_known_non_zero_change_index == -1) + first_known_non_zero_change_index = n; + const tools::wallet2::tx_construction_data &cdn = tx_constructions[first_known_non_zero_change_index]; + if (memcmp(&cd.change_dts.addr, &cdn.change_dts.addr, sizeof(cd.change_dts.addr))) throw std::runtime_error("Change goes to more than one address"); + } + tx->m_change_amount = tx->m_change_amount.get() + cd.change_dts.amount; + it->second.second -= cd.change_dts.amount; + if (it->second.second == 0) + dests.erase(cd.change_dts.addr); + } + + tx->m_outgoing_transfer = std::make_shared(); + uint64_t n_dummy_outputs = 0; + for (auto i = dests.begin(); i != dests.end(); ) + { + if (i->second.second > 0) + { + std::shared_ptr destination = std::make_shared(); + destination->m_address = i->second.first; + destination->m_amount = i->second.second; + tx->m_outgoing_transfer.get()->m_destinations.push_back(destination); + } + else + tx->m_num_dummy_outputs = tx->m_num_dummy_outputs.get() + 1; + ++i; + } + + if (tx->m_change_amount.get() > 0) + { + const tools::wallet2::tx_construction_data &cd0 = tx_constructions[0]; + tx->m_change_address = get_account_address_as_str(get_nettype(), cd0.subaddr_account > 0, cd0.change_dts.addr); + } + + tx->m_fee = tx->m_input_sum.get() - tx->m_output_sum.get(); + tx->m_unlock_time = cd.unlock_time; + tx->m_extra_hex = epee::to_hex::string({cd.extra.data(), cd.extra.size()}); + txs.push_back(tx); + } + + // build and return tx set + monero_tx_set tx_set; + tx_set.m_txs = txs; + return tx_set; + } + catch (const std::exception &e) + { + throw std::runtime_error("failed to parse unsigned transfers"); + } + } + + // implementation based on monero-project wallet_rpc_server.cpp::on_sign_transfer() + monero_tx_set monero_wallet_light::sign_txs(const std::string& unsigned_tx_hex) { + if (key_on_device()) throw std::runtime_error("command not supported by HW wallet"); + if (is_view_only()) throw std::runtime_error("command not supported by view-only wallet"); + + cryptonote::blobdata blob; + if (!epee::string_tools::parse_hexstr_to_binbuff(unsigned_tx_hex, blob)) throw std::runtime_error("Failed to parse hex."); + + tools::wallet2::unsigned_tx_set exported_txs = parse_unsigned_tx(blob); + + std::vector ptxs; + std::vector> txs; + try { + tools::wallet2::signed_tx_set signed_txs; + const auto& outputs = m_output_store.m_all; + std::vector signed_kis; + for(const auto& output : outputs) signed_kis.push_back(output.key_image_is_known() ? output.m_key_image.get() : ""); + std::string ciphertext = sign_tx(exported_txs, ptxs, signed_txs, signed_kis); + if (ciphertext.empty()) throw std::runtime_error("Failed to sign unsigned tx"); + + // init tx set + monero_tx_set tx_set; + tx_set.m_signed_tx_hex = epee::string_tools::buff_to_hex_nodelimer(ciphertext); + for (auto &ptx : ptxs) { + + // init tx + std::shared_ptr tx = std::make_shared(); + tx->m_is_outgoing = true; + tx->m_hash = epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx.tx)); + tx->m_key = epee::string_tools::pod_to_hex(unwrap(unwrap(ptx.tx_key))); + for (const crypto::secret_key& additional_tx_key : ptx.additional_tx_keys) { + tx->m_key = tx->m_key.get() += epee::string_tools::pod_to_hex(unwrap(unwrap(additional_tx_key))); + } + tx_set.m_txs.push_back(tx); + } + return tx_set; + } catch (const std::exception &e) { + throw std::runtime_error(std::string("Failed to sign unsigned tx: ") + e.what()); + } + } + + std::vector monero_wallet_light::submit_txs(const std::string& signed_tx_hex) { + MTRACE("monero_wallet_light::submit_txs()"); + if (key_on_device()) throw std::runtime_error("command not supported by HW wallet"); + + cryptonote::blobdata blob; + if (!epee::string_tools::parse_hexstr_to_binbuff(signed_tx_hex, blob)) throw std::runtime_error("Failed to parse hex."); + + std::vector ptx_vector; + try { + ptx_vector = parse_signed_tx(blob); + } catch (const std::exception &e) { + throw std::runtime_error(std::string("Failed to parse signed tx: ") + e.what()); + } + + try { + std::vector tx_hashes; + for (auto &ptx: ptx_vector) { + const auto res = m_client->submit_raw_tx(epee::string_tools::buff_to_hex_nodelimer(cryptonote::tx_to_blob(ptx.tx))); + if (res.m_status == boost::none || res.m_status.get() != std::string("OK")) throw std::runtime_error("Could not relay tx" + signed_tx_hex); + crypto::hash txid; + txid = cryptonote::get_transaction_hash(ptx.tx); + std::string pending_tx_hash = epee::string_tools::pod_to_hex(txid); + tx_hashes.push_back(pending_tx_hash); + std::shared_ptr tx = monero_utils::ptx_to_tx(ptx, get_nettype(), this); + m_tx_store.set_unconfirmed(tx); + } + + m_wallet_listener->on_spend_tx_hashes(tx_hashes); // notify listeners of spent funds + return tx_hashes; + } catch (const std::exception &e) { + throw std::runtime_error(std::string("Failed to submit signed tx: ") + e.what()); + } + } + + void monero_wallet_light::freeze_output(const std::string& key_image) { + m_output_store.freeze(key_image); + } + + void monero_wallet_light::thaw_output(const std::string& key_image) { + m_output_store.thaw(key_image); + } + + bool monero_wallet_light::is_output_frozen(const std::string& key_image) { + return m_output_store.is_frozen(key_image); + } + + monero_tx_priority monero_wallet_light::get_default_fee_priority() const { + return static_cast(DEFAULT_FEE_PRIORITY); + } + + std::vector> monero_wallet_light::create_txs(const monero_tx_config& config) { + MINFO("monero_wallet_light::create_txs()"); + if (is_multisig()) throw std::runtime_error("Multisig wallet not supported"); + if (!m_is_connected) throw std::runtime_error("Wallet is not connected to daemon"); + // validate config + if (config.m_sweep_each_subaddress != boost::none && config.m_sweep_each_subaddress.get() == true) throw std::runtime_error("Light wallet do not support sweep each subaddress individually"); + if (config.m_subtract_fee_from.size() > 0) throw std::runtime_error("Light wallet do not support subtracting fees from destinations"); + if (config.m_account_index == boost::none) throw std::runtime_error("Must specify account index to send from"); + + std::vector> result; + uint32_t subaddr_account_idx = config.m_account_index.get(); + uint64_t amount = 0; + std::vector sending_amounts; + std::vector dests; + std::string multisig_tx_hex; + std::string unsigned_tx_hex; + + for(const auto &dest : config.get_normalized_destinations()) { + const auto &dest_address = dest->m_address.get(); + if (!monero_utils::is_valid_address(dest_address, m_network_type)) throw std::runtime_error("Invalid destination address"); + dests.push_back(dest_address); + sending_amounts.push_back(*dest->m_amount); + amount += *dest->m_amount; + } + + boost::lock_guard guarg(m_sync_data_mutex); + + const auto unspent_outs = m_output_store.get_spendable(subaddr_account_idx, config.m_subaddress_indices, m_tx_store, get_height()); + uint64_t fee_per_b = m_unspent_outs.m_per_byte_fee.get(); + uint64_t fee_mask = m_unspent_outs.m_fee_mask.get(); + if (unspent_outs.empty()) throw std::runtime_error("not enough unlocked money"); + MINFO("monero_wallet_light::create_txs(): spendable outs: " << unspent_outs.size()); + + auto payment_id = config.m_payment_id; + bool is_sweeping = config.m_sweep_each_subaddress != boost::none ? *config.m_sweep_each_subaddress : false; + auto simple_priority = config.m_priority == boost::none ? 0 : config.m_priority.get(); + + m_prior_attempt_size_calcd_fee = boost::none; + m_prior_attempt_unspent_outs_to_mix_outs = boost::none; + m_construction_attempt = 0; + + const auto random_outs_params = prepare_get_random_outs_params(payment_id, sending_amounts, is_sweeping, simple_priority, unspent_outs, fee_per_b, fee_mask, m_prior_attempt_size_calcd_fee, m_prior_attempt_unspent_outs_to_mix_outs); + + if(random_outs_params.m_using_outs.size() == 0) throw std::runtime_error("Expected non-0 using_outs"); + + const auto random_outs_res = get_random_outs(std::move(m_client), random_outs_params.m_using_outs, m_prior_attempt_unspent_outs_to_mix_outs); + auto tied_outs = tie_unspent_to_mix_outs(random_outs_params.m_using_outs, random_outs_res.m_amount_outs, m_prior_attempt_unspent_outs_to_mix_outs); + auto selected_transfers = m_output_store.get_indexes(random_outs_params.m_using_outs); + + tools::wallet2::pending_tx ptx = construct_tx(get_nettype(), m_subaddresses, m_account.get_keys(), is_view_only(), subaddr_account_idx, dests, config.m_payment_id, sending_amounts, selected_transfers, random_outs_params.m_change_amount, random_outs_params.m_using_fee, random_outs_params.m_using_outs, tied_outs.m_mix_outs); + std::string full_hex = epee::string_tools::buff_to_hex_nodelimer(cryptonote::tx_to_blob(ptx.tx)); + + if (ptx.tx_key != crypto::null_skey) { + const crypto::hash txid = get_transaction_hash(ptx.tx); + m_tx_keys[txid] = ptx.tx_key; + m_additional_tx_keys[txid] = ptx.additional_tx_keys; + } + + std::shared_ptr tx = std::dynamic_pointer_cast(monero_utils::ptx_to_tx(ptx, get_nettype(), this)); + + bool relayed = false; + bool relay = config.m_relay == boost::none ? false : config.m_relay.get(); + if (relay) { + try { + auto submit_res = m_client->submit_raw_tx(full_hex); + + if (submit_res.m_status != boost::none && submit_res.m_status == std::string("OK")) { + MINFO("monero_wallet_light::create_txs(): relayed tx"); + relayed = true; + } + else MINFO("monero_wallet_light::create_txs(): tx not relayed"); + } + catch(...) { } + } + + tx->m_in_tx_pool = relayed; + tx->m_is_relayed = relayed; + tx->m_relay = relay; + tx->m_is_outgoing = true; + tx->m_is_failed = relay && !relayed; + tx->m_payment_id = config.m_payment_id; + tx->m_key = get_tx_key(tx->m_hash.get()); + tx->m_full_hex = full_hex; + + if (!relayed) { + tx->m_last_relayed_timestamp = boost::none; + tx->m_is_double_spend_seen = boost::none; + } + + if (is_view_only()) { + unsigned_tx_hex = dump_pending_tx(ptx.construction_data, config.m_payment_id); + if (unsigned_tx_hex.empty()) throw std::runtime_error("Failed to save unsigned tx set after creation"); + } + + std::shared_ptr unconfirmed_tx = std::make_shared(); + tx->copy(tx, unconfirmed_tx); + normalize_unconfirmed_tx(unconfirmed_tx); + result.push_back(unconfirmed_tx); + + MINFO("monero_wallet_light::create_txs(): created unconfirmed tx with " << tx->m_outputs.size() << " outputs and " << tx->m_inputs.size() << " inputs"); + + // build tx set + std::shared_ptr tx_set = std::make_shared(); + tx_set->m_txs = result; + for (int i = 0; i < result.size(); i++) result[i]->m_tx_set = tx_set; + if (!multisig_tx_hex.empty()) tx_set->m_multisig_tx_hex = multisig_tx_hex; + if (!unsigned_tx_hex.empty()) tx_set->m_unsigned_tx_hex = unsigned_tx_hex; + if (!is_view_only() && relayed) m_tx_store.set_unconfirmed(tx); + + calculate_balance(); + + if (relayed) m_wallet_listener->on_spend_txs(result); + + return result; + } + + std::vector> monero_wallet_light::get_txs() const { + return get_txs(monero_tx_query()); + } + + std::vector> monero_wallet_light::get_txs(const monero_tx_query& query) const { + MTRACE("monero_wallet_light::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 = monero_utils::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) { + monero_utils::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 = monero_utils::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()) { + monero_utils::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> monero_wallet_light::get_transfers(const monero_transfer_query& query) const { + // get transfers directly if query does not require tx context (e.g. other transfers, outputs) + if (!monero_utils::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> monero_wallet_light::get_outputs(const monero_output_query& query) const { + // get outputs directly if query does not require tx context (e.g. other outputs, transfers) + if (!monero_utils::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::string monero_wallet_light::export_outputs(bool all) const { + uint32_t start = 0; + uint32_t count = 0xffffffff; + std::stringstream oss; + binary_archive ar(oss); + + auto outputs = m_output_store.export_outputs(m_tx_store, m_generated_key_images, all, start, count); + if(!serialization::serialize(ar, outputs)) throw std::runtime_error("Failed to serialize output data"); + + std::string magic(OUTPUT_EXPORT_FILE_MAGIC, strlen(OUTPUT_EXPORT_FILE_MAGIC)); + const cryptonote::account_public_address &keys = m_account.get_keys().m_account_address; + std::string header; + header += std::string((const char *)&keys.m_spend_public_key, sizeof(crypto::public_key)); + header += std::string((const char *)&keys.m_view_public_key, sizeof(crypto::public_key)); + + std::string ciphertext = encrypt_with_private_view_key(header + oss.str()); + std::string outputs_str = magic + ciphertext; + return epee::string_tools::buff_to_hex_nodelimer(outputs_str); + } + + int monero_wallet_light::import_outputs(const std::string& outputs_hex) { + throw std::runtime_error("monero_wallet_light::import_outputs(): not supported"); + } + + std::vector> monero_wallet_light::export_key_images(bool all) const { + std::vector> key_images; + + const auto& outputs = m_output_store.m_all; + + size_t offset = 0; + if (!all) + { + while (offset < outputs.size() && !m_generated_key_images.request(outputs[offset].m_tx_pub_key.get(), outputs[offset].m_index.get(), outputs[offset].m_recipient.m_maj_i, outputs[offset].m_recipient.m_min_i)) + ++offset; + } + key_images.reserve(outputs.size() - offset); + + for(size_t n = offset; n < outputs.size(); ++n) { + const auto output = &outputs[n]; + std::shared_ptr key_image = std::make_shared(); + cryptonote::subaddress_index subaddr; + uint32_t account_idx = output->m_recipient.m_maj_i; + uint32_t subaddress_idx = output->m_recipient.m_min_i; + subaddr.major = account_idx; + subaddr.minor = subaddress_idx; + + auto cached_key_image = m_generated_key_images.get(output->m_tx_pub_key.get(), account_idx, subaddress_idx); + + if (cached_key_image != nullptr) { + key_image = cached_key_image; + } + else if (!is_view_only()) { + *key_image = generate_key_image(output->m_tx_pub_key.get(), output->m_index.get(), subaddr); + } + + key_images.push_back(key_image); + } + + return key_images; + } + + std::shared_ptr monero_wallet_light::import_key_images(const std::vector>& key_images) { + std::shared_ptr result = std::make_shared(); + result->m_height = 0; + result->m_spent_amount = 0; + result->m_unspent_amount = 0; + + if (key_images.empty()) { + return result; + } + + uint64_t spent_amount = 0; + uint64_t unspent_amount = 0; + + // validate key images + + std::vector> ski; + ski.resize(key_images.size()); + for (uint64_t n = 0; n < ski.size(); ++n) { + if (!epee::string_tools::hex_to_pod(key_images[n]->m_hex.get(), ski[n].first)) { + throw std::runtime_error("failed to parse key image"); + } + if (!epee::string_tools::hex_to_pod(key_images[n]->m_signature.get(), ski[n].second)) { + throw std::runtime_error("failed to parse signature"); + } + } + bool check_spent = is_connected_to_daemon(); + + auto& unspent_outs = m_output_store.m_all; + + if (key_images.size() > unspent_outs.size()) { + throw std::runtime_error("blockchain is out of date compared to the signed key images"); + } + + size_t key_images_size = key_images.size(); + + for (size_t i = 0; i < key_images_size; i++) { + auto& unspent_out = unspent_outs[i]; + uint64_t out_index = unspent_out.m_index.get(); + uint32_t account_idx = unspent_out.m_recipient.m_maj_i; + uint32_t subaddress_idx = unspent_out.m_recipient.m_min_i; + const std::string& tx_public_key = unspent_out.m_tx_pub_key.get(); + m_output_store.set_key_image(key_images[i]->m_hex.get(), i); + m_generated_key_images.set(key_images[i], tx_public_key, out_index, account_idx, subaddress_idx); + + if (!check_spent) continue; + if (m_tx_store.is_key_image_spent(key_images[i])) { + spent_amount += unspent_outs[i].m_amount.get(); + } + else { + unspent_amount += unspent_outs[i].m_amount.get(); + } + } + + result->m_height = unspent_outs[key_images_size - 1].m_height; + result->m_spent_amount = spent_amount; + result->m_unspent_amount = unspent_amount; + + if (spent_amount > 0 || unspent_amount > 0) { + process_outputs(); + m_output_store.set(m_tx_store, m_unspent_outs); + calculate_balance(); + } + return result; + } + + std::string monero_wallet_light::get_tx_note(const std::string& tx_hash) const { + MTRACE("monero_wallet_light::get_tx_note()"); + cryptonote::blobdata tx_blob; + if (!epee::string_tools::parse_hexstr_to_binbuff(tx_hash, tx_blob) || tx_blob.size() != sizeof(crypto::hash)) { + throw std::runtime_error("TX hash has invalid format"); + } + crypto::hash _tx_hash = *reinterpret_cast(tx_blob.data()); + std::unordered_map::const_iterator i = m_tx_notes.find(_tx_hash); + if (i == m_tx_notes.end()) return std::string(); + return i->second; + } + + std::vector monero_wallet_light::get_tx_notes(const std::vector& tx_hashes) const { + MTRACE("monero_wallet_light::get_tx_notes()"); + std::vector notes; + for (const auto& tx_hash : tx_hashes) notes.push_back(get_tx_note(tx_hash)); + return notes; + } + + void monero_wallet_light::set_tx_note(const std::string& tx_hash, const std::string& note) { + MTRACE("monero_wallet_light::set_tx_note()"); + cryptonote::blobdata tx_blob; + if (!epee::string_tools::parse_hexstr_to_binbuff(tx_hash, tx_blob) || tx_blob.size() != sizeof(crypto::hash)) { + throw std::runtime_error("TX hash has invalid format"); + } + crypto::hash _tx_hash = *reinterpret_cast(tx_blob.data()); + m_tx_notes[_tx_hash] = note; + } + + void monero_wallet_light::set_tx_notes(const std::vector& tx_hashes, const std::vector& notes) { + MTRACE("monero_wallet_light::set_tx_notes()"); + if (tx_hashes.size() != notes.size()) throw std::runtime_error("Different amount of txids and notes"); + for (int i = 0; i < tx_hashes.size(); i++) { + set_tx_note(tx_hashes[i], notes[i]); + } + } + + std::vector monero_wallet_light::get_address_book_entries(const std::vector& indices) const { + if (indices.empty()) return m_address_book; + std::vector result; + + for (uint64_t idx : indices) { + if (idx >= m_address_book.size()) throw std::runtime_error("Index out of range: " + std::to_string(idx)); + const auto &entry = m_address_book[idx]; + result.push_back(entry); + } + + return result; + } + + uint64_t monero_wallet_light::add_address_book_entry(const std::string& address, const std::string& description) { + MTRACE("monero_wallet_light::add_address_book_entry()"); + cryptonote::address_parse_info info; + epee::json_rpc::error er; + if(!get_account_address_from_str_or_url(info, get_nettype(), address, + [&er](const std::string &url, const std::vector &addresses, bool dnssec_valid)->std::string { + if (!dnssec_valid) throw std::runtime_error(std::string("Invalid DNSSEC for ") + url); + if (addresses.empty()) throw std::runtime_error(std::string("No Monero address found at ") + url); + return addresses[0]; + })) + { + throw std::runtime_error(std::string("Invalid address: ") + address); + } + + const auto old_size = m_address_book.size(); + + monero_address_book_entry entry(old_size, address, description); + m_address_book.push_back(entry); + + if (m_address_book.size() != old_size + 1) throw std::runtime_error("Failed to add address book entry"); + return m_address_book.size() - 1; + } + + void monero_wallet_light::edit_address_book_entry(uint64_t index, bool set_address, const std::string& address, bool set_description, const std::string& description) { + MTRACE("monero_wallet_light::edit_address_book_entry()"); + if (index >= m_address_book.size()) throw std::runtime_error("Index out of range: " + std::to_string(index)); + + monero_address_book_entry entry; + entry.m_index = index; + + cryptonote::address_parse_info info; + epee::json_rpc::error er; + if (set_address) { + er.message = ""; + if(!get_account_address_from_str_or_url(info, get_nettype(), address, + [&er](const std::string &url, const std::vector &addresses, bool dnssec_valid)->std::string { + if (!dnssec_valid) throw std::runtime_error(std::string("Invalid DNSSEC for ") + url); + if (addresses.empty()) throw std::runtime_error(std::string("No Monero address found at ") + url); + return addresses[0]; + })) + { + throw std::runtime_error("Invalid address: " + address); + } + + if (info.has_payment_id) { + entry.m_address = cryptonote::get_account_integrated_address_as_str(get_nettype(), info.address, info.payment_id); } + else entry.m_address = address; + } + + if (set_description) entry.m_description = description; + + m_address_book[index] = entry; + } + + void monero_wallet_light::delete_address_book_entry(uint64_t index) { + if (index >= m_address_book.size()) throw std::runtime_error("Index out of range: " + std::to_string(index)); + m_address_book.erase(m_address_book.begin()+index); + } + + void monero_wallet_light::set_attribute(const std::string &key, const std::string &value) { + m_attributes[key] = value; + } + + bool monero_wallet_light::get_attribute(const std::string &key, std::string &value) const { + std::unordered_map::const_iterator i = m_attributes.find(key); + if (i == m_attributes.end()) + return false; + value = i->second; + return true; + } + + uint64_t monero_wallet_light::wait_for_next_block() { + // use mutex and condition variable to wait for block + boost::mutex temp; + boost::condition_variable cv; + + // create listener which notifies condition variable when block is added + struct block_notifier : monero_wallet_listener { + boost::mutex* temp; + boost::condition_variable* cv; + uint64_t last_height; + block_notifier(boost::mutex* temp, boost::condition_variable* cv) { this->temp = temp; this->cv = cv; } + void on_new_block(uint64_t height) { + last_height = height; + cv->notify_one(); + } + } block_listener(&temp, &cv); + + // register the listener + add_listener(block_listener); + + // wait until condition variable is notified + boost::mutex::scoped_lock lock(temp); + cv.wait(lock); + + // unregister the listener + remove_listener(block_listener); + + // return last height + return block_listener.last_height; + } + + monero_multisig_info monero_wallet_light::get_multisig_info() const { + monero_multisig_info info; + info.m_is_multisig = false; + return info; + } + + void monero_wallet_light::close(bool save) { + MTRACE("monero_wallet_light::close()"); + if (save) throw std::runtime_error("MoneroWalletLight does not support saving"); + stop_syncing(); + if (m_sync_loop_running) { + m_sync_cv.notify_one(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); // TODO: in emscripten, m_sync_cv.notify_one() returns without waiting, so sleep; bug in emscripten upstream llvm? + m_syncing_thread.join(); + } + + m_account.deinit(); + m_wallet_listener.reset(); // wait for queued notifications + } + + // --------------------------- PRIVATE UTILS -------------------------- + + void monero_wallet_light::init_common() { + monero_wallet_keys::init_common(); + + m_is_synced = false; + m_rescan_on_sync = false; + m_syncing_enabled = false; + m_sync_loop_running = false; + + m_address_info.m_locked_funds= 0; + m_address_info.m_total_received= 0; + m_address_info.m_total_sent= 0; + m_address_info.m_scanned_height = 0; + m_address_info.m_scanned_block_height = 0; + m_address_info.m_start_height = 0; + m_address_info.m_transaction_height = 0; + m_address_info.m_blockchain_height = 0; + + m_address_txs.m_total_received= 0; + m_address_txs.m_scanned_height = 0; + m_address_txs.m_scanned_block_height = 0; + m_address_txs.m_start_height = 0; + m_address_txs.m_blockchain_height = 0; + + m_unspent_outs.m_per_byte_fee= 0; + m_unspent_outs.m_fee_mask= 0; + m_unspent_outs.m_amount= 0; + + monero_light_subaddrs subaddrs; + m_subaddrs.m_all_subaddrs = subaddrs; + process_subaddresses(); + set_subaddress_label(0, 0, "Primary account"); + m_wallet_listener = std::unique_ptr(new monero_wallet_utils::wallet2_listener(*this)); + } + + wallet2_exported_outputs monero_wallet_light::export_outputs(bool all, uint32_t start, uint32_t count) const { + return m_output_store.export_outputs(m_tx_store, m_generated_key_images, false, 0, count); + } + + cryptonote::subaddress_index get_transaction_sender(const monero_light_tx &tx) { + cryptonote::subaddress_index si = {0,0}; + + for (const auto &output : tx.m_spent_outputs) { + si.major = output.m_sender.m_maj_i; + si.minor = output.m_sender.m_min_i; + break; + } + + return si; + } + + std::vector> monero_wallet_light::get_transfers_aux(const monero_transfer_query& query) const { + monero_utils::start_profile("get_transfers_aux()"); + + // 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(); + + std::vector> transfers; + std::unordered_map> blocks; + + const uint64_t current_height = m_address_txs.m_blockchain_height.get() + 1; + const bool view_only = is_view_only(); + + for (const auto &tx : m_address_txs.m_transactions) { + uint64_t total_sent = tx.m_total_sent.get(); + uint64_t total_received = tx.m_total_received.get(); + uint64_t fee = tx.m_fee.get(); + bool is_incoming = total_received > 0; + bool is_outgoing = total_sent > 0; + bool is_change = is_incoming && is_outgoing; + + if (is_change && total_sent >= total_received) total_sent -= total_received; + else if (is_change) total_sent = 0; + + bool is_locked = tx.m_unlock_time.get() > current_height || current_height < (tx.m_height.get()) + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE; + bool is_confirmed = !tx.m_mempool.get(); + bool is_miner_tx = *tx.m_coinbase == true; + bool has_payment_id = tx.m_payment_id != boost::none && !tx.m_payment_id.get().empty() && tx.m_payment_id.get() != monero_tx::DEFAULT_PAYMENT_ID; + std::string payment_id = has_payment_id ? tx.m_payment_id.get() : ""; + uint64_t timestamp = tx.m_timestamp.get(); + uint64_t tx_height = is_confirmed ? *tx.m_height : 0; + uint64_t num_confirmations = is_confirmed ? current_height - tx_height : 0; + uint64_t change_amount = is_change ? total_received : 0; + uint64_t input_sum = 0; + uint64_t output_sum = 0; + std::string tx_hash = tx.m_hash.get(); + std::shared_ptr block = nullptr; + std::shared_ptr tx_wallet = std::make_shared(); + tx_wallet->m_is_incoming = is_incoming && !is_change; + tx_wallet->m_is_outgoing = is_outgoing; + tx_wallet->m_is_locked = is_locked; + tx_wallet->m_is_relayed = true; + tx_wallet->m_is_failed = false; + tx_wallet->m_is_double_spend_seen = false; + tx_wallet->m_is_confirmed = is_confirmed; + tx_wallet->m_is_miner_tx = is_miner_tx; + tx_wallet->m_unlock_time = *tx.m_unlock_time; + tx_wallet->m_in_tx_pool = !is_confirmed; + tx_wallet->m_relay = true; + tx_wallet->m_hash = tx_hash; + tx_wallet->m_num_confirmations = num_confirmations; + tx_wallet->m_fee = fee; + const auto sender = get_transaction_sender(tx); + + if (is_confirmed) { + auto it = blocks.find(tx_height); + if (it == blocks.end()) { + block = std::make_shared(); + block->m_height = tx_height; + block->m_timestamp = timestamp; + blocks[tx_height] = block; + } + else block = it->second; + + if (is_miner_tx) block->m_miner_tx = tx_wallet; + block->m_txs.push_back(tx_wallet); + tx_wallet->m_block = block; + } + else tx_wallet->m_received_timestamp = timestamp; + + if (is_incoming) { + for (const auto &out : m_output_store.get_by_tx_hash(tx_hash)) { + uint64_t out_amount = out.m_amount.get(); + uint32_t out_account_idx = out.m_recipient.m_maj_i; + uint32_t out_subaddress_idx = out.m_recipient.m_min_i; + + if (is_change && sender.major == out_account_idx) continue; + else if (is_change) { + tx_wallet->m_is_incoming = true; + total_sent += out_amount; + } + + std::shared_ptr incoming_transfer = std::make_shared(); + + const auto found = std::find_if(tx_wallet->m_incoming_transfers.begin(), tx_wallet->m_incoming_transfers.end(), [out_account_idx, out_subaddress_idx](const std::shared_ptr& transfer){ + return out_account_idx == transfer->m_account_index.get() && out_subaddress_idx == transfer->m_subaddress_index.get(); + }); + + if (found != tx_wallet->m_incoming_transfers.end()) { + (*found)->m_amount = (*found)->m_amount.get() + out_amount; + } + else { + incoming_transfer->m_tx = tx_wallet; + incoming_transfer->m_account_index = out_account_idx; + incoming_transfer->m_subaddress_index = out_subaddress_idx; + incoming_transfer->m_address = get_address(out_account_idx, out_subaddress_idx); + incoming_transfer->m_amount = out_amount; + + if (current_height >= TAIL_EMISSION_HEIGHT) { + uint64_t reward = m_tx_store.get_last_block_reward(); + monero_utils::set_num_suggested_confirmations(incoming_transfer, current_height, reward, *tx.m_unlock_time); + } else { + incoming_transfer->m_num_suggested_confirmations = 1; + } + + tx_wallet->m_incoming_transfers.push_back(incoming_transfer); + + std::shared_ptr output = std::make_shared(); + + if (out.m_key_image != boost::none) { + auto out_key_image = std::make_shared(); + out_key_image->m_hex = *out.m_key_image; + output->m_key_image = out_key_image; + } + + output->m_tx = tx_wallet; + output->m_account_index = out_account_idx; + output->m_subaddress_index = out_subaddress_idx; + output->m_amount = out_amount; + output->m_is_spent = out.is_spent(); + output->m_index = out.m_global_index.get(); + output->m_stealth_public_key = out.m_public_key; + + output_sum += out_amount; + } + } + + if (!is_outgoing && has_payment_id) tx_wallet->m_payment_id = payment_id; + } + + if (is_outgoing && !view_only) { + std::shared_ptr outgoing_transfer = std::make_shared(); + outgoing_transfer->m_tx = tx_wallet; + outgoing_transfer->m_amount = total_sent >= fee ? total_sent - fee : 0; + outgoing_transfer->m_account_index = sender.major; + + for (const auto& spent_output : tx.m_spent_outputs) { + uint32_t account_idx = spent_output.m_sender.m_maj_i; + uint32_t subaddress_idx = spent_output.m_sender.m_min_i; + uint64_t out_amount = spent_output.m_amount.get(); + + if (account_idx == sender.major && std::find_if(outgoing_transfer->m_subaddress_indices.begin(), outgoing_transfer->m_subaddress_indices.end(), [subaddress_idx](const uint32_t &idx) { return subaddress_idx == idx; }) == outgoing_transfer->m_subaddress_indices.end()) { + outgoing_transfer->m_addresses.push_back(get_address(account_idx, subaddress_idx)); + outgoing_transfer->m_subaddress_indices.push_back(subaddress_idx); + } + + std::shared_ptr output = std::make_shared(); + + if (spent_output.m_key_image != boost::none) { + auto out_key_image = std::make_shared(); + out_key_image->m_hex = spent_output.m_key_image; + output->m_key_image = out_key_image; + } + + output->m_account_index = account_idx; + output->m_subaddress_index = subaddress_idx; + output->m_amount = out_amount; + output->m_is_spent = true; + output->m_index = spent_output.m_out_index; + output->m_tx = tx_wallet; + + input_sum += out_amount; + } + + sort(outgoing_transfer->m_subaddress_indices.begin(), outgoing_transfer->m_subaddress_indices.end()); + + tx_wallet->m_outgoing_transfer = outgoing_transfer; + } + + sort(tx_wallet->m_incoming_transfers.begin(), tx_wallet->m_incoming_transfers.end(), monero_utils::incoming_transfer_before); + + if (is_confirmed && block != nullptr && !tx_query->meets_criteria(tx_wallet.get())) { + block->m_txs.erase(std::remove(block->m_txs.begin(), block->m_txs.end(), tx_wallet), block->m_txs.end()); + } + + for (const std::shared_ptr& transfer : tx_wallet->filter_transfers(*_query)) transfers.push_back(transfer); + } + + for (const auto &kv : m_tx_store.get_unconfirmed_txs()) { + auto txwallet = kv.second; + std::shared_ptr tx_wallet = std::make_shared(); + txwallet->copy(txwallet, tx_wallet); + tx_wallet->m_weight = boost::none; + tx_wallet->m_inputs.clear(); + tx_wallet->m_outputs.clear(); + tx_wallet->m_ring_size = boost::none; + tx_wallet->m_key = boost::none; + tx_wallet->m_full_hex = boost::none; + tx_wallet->m_metadata = boost::none; + tx_wallet->m_last_relayed_timestamp = boost::none; + for (const std::shared_ptr& transfer : tx_wallet->filter_transfers(*_query)) { + transfers.push_back(transfer); + } + } + + monero_utils::end_profile("get_transfers_aux()"); + return transfers; + } + + std::vector> monero_wallet_light::get_outputs_aux(const monero_output_query& query) const { + MTRACE("monero_wallet_light::get_outputs_aux(query)"); + + // 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(); + + // get light wallet data + std::vector outs; + + if (query.m_account_index != boost::none) { + if (query.m_subaddress_index == boost::none) { + outs = m_output_store.get(query.m_account_index.get()); + } + else { + outs = m_output_store.get(query.m_account_index.get(), query.m_subaddress_index.get()); + } + } else outs = m_output_store.m_all; + + std::vector> outputs; + const auto block_height = m_address_txs.m_blockchain_height.get(); + + // cache unique txs and blocks + std::map> tx_map; + std::map> block_map; + for (const auto &out : outs) { + // TODO: skip tx building if output excluded by indices, etc + std::shared_ptr tx = build_tx_with_vout(m_tx_store, m_output_store, out, block_height + 1); + monero_utils::merge_tx(tx, tx_map, block_map); + } + + 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(), monero_utils::tx_height_less_than); + + // filter and return outputs + for (const std::shared_ptr& tx : txs) { + + // sort outputs + sort(tx->m_outputs.begin(), tx->m_outputs.end(), monero_utils::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; + } + + std::vector monero_wallet_light::get_subaddresses_aux(const uint32_t account_idx, const std::vector& subaddress_indices) const { + // must provide subaddress indices + std::vector subaddress_idxs; + if (subaddress_indices.empty()) { + if (m_subaddrs.m_all_subaddrs != boost::none) + subaddress_idxs = m_subaddrs.m_all_subaddrs->get_subaddresses_indices(account_idx); + if (subaddress_idxs.empty()) subaddress_idxs.push_back(0); + } + else subaddress_idxs = subaddress_indices; + + if (subaddress_idxs.empty()) return std::vector(); + + // initialize subaddresses at indices + return monero_wallet_keys::get_subaddresses(account_idx, subaddress_idxs); + } + + uint64_t monero_wallet_light::get_subaddress_num_blocks_to_unlock(uint32_t account_idx, uint32_t subaddress_idx) const { + const auto& unspent_outs = m_output_store.get(account_idx, subaddress_idx); + return m_tx_store.calculate_num_blocks_to_unlock(unspent_outs, get_height()); + } + + bool monero_wallet_light::output_is_spent(monero_light_output &output) const { + const auto& key_images = output.m_spend_key_images; + const auto& rcpt = output.m_recipient; + cryptonote::subaddress_index received_subaddr; + + received_subaddr.major = rcpt.m_maj_i; + received_subaddr.minor = rcpt.m_min_i; + const std::string& tx_pub_key = output.m_tx_pub_key.get(); + uint64_t output_idx = output.m_index.get(); + + bool spent = false; + + for (auto& key_image : key_images) { + if (key_image_is_ours(key_image, tx_pub_key, output_idx, received_subaddr)) { + output.m_key_image = key_image; + spent = true; + break; + } + } + + bool checked_unconfirmed = false; + + if (!spent && !output.key_image_is_known()) { + try { + output.m_key_image = generate_key_image(tx_pub_key, output_idx, received_subaddr).m_hex; + // check key image is spent in unconfirmed transactions + spent = m_tx_store.is_key_image_spent(output.m_key_image.get()); + checked_unconfirmed = true; + } + catch (...) { + return false; + } + } + + if (!checked_unconfirmed && !spent && output.key_image_is_known()) { + // check key image is spent in unconfirmed transactions + spent = m_tx_store.is_key_image_spent(output.m_key_image.get()); + } + + return spent; + } + + bool monero_wallet_light::spend_is_real(monero_light_spend &spend) const { + if (spend.m_key_image == boost::none) return false; + std::string key_image = spend.m_key_image.get(); + cryptonote::subaddress_index received_subaddr = {spend.m_sender.m_maj_i,spend.m_sender.m_min_i}; + return key_image_is_ours(key_image, spend.m_tx_pub_key.get(), spend.m_out_index.get(), received_subaddr); + } + + void monero_wallet_light::init_subaddress(monero_subaddress& subaddress) const { + if (subaddress.m_account_index == boost::none) throw std::runtime_error("Cannot initialize subaddress: account index is none"); + if (subaddress.m_index == boost::none) throw std::runtime_error("Cannot initialize subaddress: subaddress index is none"); + uint32_t account_idx = subaddress.m_account_index.get(); + uint32_t subaddress_idx = subaddress.m_index.get(); + subaddress.m_label = get_subaddress_label(account_idx, subaddress_idx); + subaddress.m_balance = get_balance(account_idx, subaddress_idx); + subaddress.m_unlocked_balance = get_unlocked_balance(account_idx, subaddress_idx); + subaddress.m_num_unspent_outputs = m_output_store.get_num_unspent(account_idx, subaddress_idx); + subaddress.m_is_used = m_output_store.is_used(account_idx, subaddress_idx); + subaddress.m_num_blocks_to_unlock = get_subaddress_num_blocks_to_unlock(account_idx, subaddress_idx); + } + + void monero_wallet_light::calculate_balance() { + m_tx_store.set(m_address_txs, m_address_info); + m_output_store.calculate_balance(m_tx_store, get_height()); + } + + void monero_wallet_light::run_sync_loop() { + if (m_sync_loop_running) return; // only run one loop at a time + m_sync_loop_running = true; + + // start sync loop thread + // TODO: use global threadpool, background sync wasm wallet in c++ thread + m_syncing_thread = boost::thread([this]() { + + // sync while enabled + while (m_syncing_enabled) { + try { lock_and_sync(); } + catch (std::exception const& e) { MERROR("monero_wallet_light failed to background synchronize: " << e.what()); } + catch (...) { MERROR("monero_wallet_light failed to background synchronize: unknown error"); } + + // only wait if syncing still enabled + if (m_syncing_enabled) { + boost::mutex::scoped_lock lock(m_syncing_mutex); + boost::posix_time::milliseconds wait_for_ms(m_syncing_interval.load()); + m_sync_cv.timed_wait(lock, wait_for_ms); + } + } + + m_sync_loop_running = false; + }); + } + + monero_sync_result monero_wallet_light::lock_and_sync(boost::optional start_height) { + bool rescan = m_rescan_on_sync.exchange(false); + boost::lock_guard guarg(m_sync_mutex); // synchronize sync() and syncAsync() + monero_sync_result result; + result.m_num_blocks_fetched = 0; + result.m_received_money = false; + do { + // skip if daemon is not connected or synced + if (m_is_connected && is_daemon_synced()) { + + // rescan blockchain if requested + if (rescan) rescan_blockchain(); // infinite loop? + + // sync wallet + result = sync_aux(start_height); + } + } while (!rescan && (rescan = m_rescan_on_sync.exchange(false))); // repeat if not rescanned and rescan was requested + return result; + } + + monero_sync_result monero_wallet_light::sync_aux(boost::optional start_height) { + MTRACE("sync_aux()"); + monero_utils::start_profile("sync_aux"); + + monero_sync_result result; + result.m_num_blocks_fetched = 0; + result.m_received_money = false; + // attempt to refresh which may throw exception + try { + result = refresh(); + if (!m_is_synced) m_is_synced = true; + m_wallet_listener->update_listening(); // cannot unregister during sync which would segfault + } catch (std::exception& e) { + m_wallet_listener->on_sync_end(); // signal end of sync to reset listener's start and end heights + monero_utils::end_profile("sync_aux"); + throw; + } + + // notify listeners of sync end and check for updated funds + m_wallet_listener->on_sync_end(); + LOG_PRINT_L1("Light wallet refresh done, blocks received: " << result.m_num_blocks_fetched << ", balance (all accounts): " << cryptonote::print_money(get_balance()) << ", unlocked: " << cryptonote::print_money(get_unlocked_balance())); + monero_utils::end_profile("sync_aux"); + return result; + } + + monero_sync_result monero_wallet_light::refresh() { + const std::string& address = get_primary_address(); + const std::string& view_key = get_private_view_key(); + // determine sync start height + uint64_t last_height = get_height(); + + monero_sync_result result; + result.m_num_blocks_fetched = 0; + result.m_received_money = false; + const uint64_t old_outs_amount = m_unspent_outs.m_amount.get(); + + uint64_t blockchain_height = 1; + auto addr_info = m_client->get_address_info(address, view_key); + if (addr_info.m_blockchain_height != boost::none) blockchain_height = addr_info.m_blockchain_height.get() + 1; + if (addr_info.m_start_height != boost::none) { + uint64_t start_height = addr_info.m_start_height.get(); + if (last_height < start_height) last_height = start_height == 0 ? 0 : start_height + 1; + } + + // update address info height + m_address_info.m_blockchain_height = addr_info.m_blockchain_height; + m_address_info.m_start_height = addr_info.m_start_height; + m_address_info.m_scanned_height = addr_info.m_scanned_height; + m_address_info.m_scanned_block_height = addr_info.m_scanned_block_height; + m_address_info.m_transaction_height = addr_info.m_transaction_height; + // notify listeners of sync start + m_wallet_listener->on_sync_start(last_height); + + if (blockchain_height == last_height) { + return result; + } + + boost::unique_lock lock(m_sync_data_mutex); + + m_address_info = addr_info; + m_address_txs = m_client->get_address_txs(address, view_key); + m_unspent_outs = m_client->get_unspent_outs(address, view_key, 0, 0); + m_subaddrs = m_client->get_subaddrs(address, view_key); + process_subaddresses(); + process_txs(); + process_outputs(); + // initialize optimized data structures + m_output_store.set(m_tx_store, m_unspent_outs); + m_tx_store.set(m_address_txs, addr_info); + + const uint64_t new_outs_amount = m_unspent_outs.m_amount.get(); + + result.m_received_money = new_outs_amount > old_outs_amount; + + calculate_balance(); + + lock.unlock(); + + uint64_t current_height = get_height(); + uint64_t daemon_height = get_daemon_height(); + uint64_t restore_height = get_restore_height(); + + if (restore_height < current_height) { + if (last_height < restore_height) last_height = restore_height; + uint64_t blocks_fetched = current_height - last_height; + result.m_num_blocks_fetched = blocks_fetched; + + if (current_height > last_height) { + cryptonote::block dummy; + // notify blocks processed by lws + for(uint64_t block_height = last_height; block_height < current_height; block_height++) { + m_wallet_listener->on_new_block(block_height, dummy); + } + } + } + + return result; + } + + boost::optional monero_wallet_light::get_subaddress_label(uint32_t account_idx, uint32_t subaddress_idx) const { + auto subs = m_subaddress_labels.find(account_idx); + if (subs == m_subaddress_labels.end()) return boost::none; + auto sub = subs->second.find(subaddress_idx); + if (sub == subs->second.end() || sub->second.empty()) return boost::none; + return sub->second; + } + + // --------------------------- LWS UTILS -------------------------- + + void monero_wallet_light::process_txs() { + std::vector txs_to_remove; + size_t tx_idx = 0; + + for(auto &tx : m_address_txs.m_transactions) { + uint64_t tx_total_sent = tx.m_total_sent.get(); + uint64_t tx_total_received = tx.m_total_received.get(); + std::vector outs_to_remove; + size_t out_idx = 0; + + for (auto& spend : tx.m_spent_outputs) { + if(!spend_is_real(spend)) { + uint64_t spend_amount = spend.m_amount.get(); + if (spend_amount > tx_total_sent) throw std::runtime_error("tx total sent is negative: " + tx.m_hash.get()); + tx_total_sent -= spend_amount; + outs_to_remove.push_back(out_idx); + } + out_idx++; + } + + tx.m_total_sent = tx_total_sent; + tx.m_total_received = tx_total_received; + if (tx_total_received == 0 && tx_total_sent == 0) { + txs_to_remove.push_back(tx_idx); + } + else for (auto it = outs_to_remove.rbegin(); it != outs_to_remove.rend(); ++it) tx.m_spent_outputs.erase(tx.m_spent_outputs.begin() + *it); + + tx_idx++; + } + + for (auto it = txs_to_remove.rbegin(); it != txs_to_remove.rend(); ++it) m_address_txs.m_transactions.erase(m_address_txs.m_transactions.begin() + *it); + } + + void monero_wallet_light::process_outputs() { + auto result = m_unspent_outs; + uint64_t real_amount = m_unspent_outs.m_amount.get(); + + for (auto& output : m_unspent_outs.m_outputs) { + if (!output_is_spent(output)) continue; + real_amount -= output.m_amount.get(); + } + + sort(m_unspent_outs.m_outputs.begin(), m_unspent_outs.m_outputs.end(), output_before); + m_unspent_outs.m_amount = real_amount; + } + + void monero_wallet_light::process_subaddresses() { + const cryptonote::account_keys &account_keys = m_account.get_keys(); + hw::device &hwdev = m_account.get_device(); + m_subaddresses[account_keys.m_account_address.m_spend_public_key] = {0,0}; + if (m_subaddrs.m_all_subaddrs == boost::none) return; + const auto& all_subaddrs = m_subaddrs.m_all_subaddrs.get(); + for (const auto& kv : all_subaddrs) { + for (const auto& index_range : kv.second) { + for (uint32_t i = index_range.at(0); i <= index_range.at(1); i++) { + if (kv.first == 0 && i == 0) continue; + const auto& subaddress_spend_pub_key = hwdev.get_subaddress_spend_public_key(account_keys, {kv.first, i}); + if (m_subaddresses.find(subaddress_spend_pub_key) == m_subaddresses.end()) m_subaddresses[subaddress_spend_pub_key] = {kv.first, i}; + } + } + } + } + + void monero_wallet_light::upsert_subaddrs(const monero_light_subaddrs &subaddrs, bool get_all) { + const auto response = m_client->upsert_subaddrs(get_primary_address(), get_private_view_key(), subaddrs, get_all); + if (get_all) { + m_subaddrs.m_all_subaddrs = response.m_all_subaddrs; + process_subaddresses(); + } + } + + void monero_wallet_light::upsert_subaddrs(uint32_t account_idx, uint32_t subaddress_idx, bool get_all) { + if (account_idx == 0) throw std::runtime_error("subaddress major lookahead may not be zero"); + if (subaddress_idx == 0) throw std::runtime_error("subaddress minor lookahead may not be zero"); + + monero_light_subaddrs subaddrs; + monero_light_index_range index_range(0, subaddress_idx - 1); + + for(uint32_t i = 0; i < account_idx; i++) { + subaddrs[i] = std::vector(); + subaddrs[i].push_back(index_range); + } + + upsert_subaddrs(subaddrs, get_all); + } + + void monero_wallet_light::login(bool create_account, bool generated_locally) const { + m_client->login(get_primary_address(), get_private_view_key(), create_account, generated_locally); + } + + // --------------------------- STATIC WALLET UTILS -------------------------- + + bool monero_wallet_light::wallet_exists(const std::string& primary_address, const std::string& private_view_key, const std::string& server_uri, std::unique_ptr http_client_factory) { + MTRACE("monero_wallet_light::wallet_exists(" << primary_address << ")"); + + monero_light_client client(std::move(http_client_factory)); + client.set_connection(server_uri); + + try { + client.login(primary_address, private_view_key, false); + return true; + } + catch (const std::exception& ex) { + if (std::string("Unauthorized") == std::string(ex.what())) return true; + return false; + } + } + + bool monero_wallet_light::wallet_exists(const monero_wallet_config& config, const std::string& server_uri, std::unique_ptr http_client_factory) { + bool empty_seed = config.m_seed == boost::none || config.m_seed->empty(); + if (empty_seed) { + if (config.m_primary_address == boost::none || config.m_primary_address.get().empty()) throw std::runtime_error("must provide a valid primary address"); + if (config.m_private_view_key == boost::none || config.m_private_view_key.get().empty()) throw std::runtime_error("must provide a valid private view key"); + } + if (config.m_server == boost::none || config.m_server->m_uri == boost::none || config.m_server->m_uri->empty()) throw std::runtime_error("must provide a lws connection"); + + if (!empty_seed) { + monero_wallet_keys *wallet_keys = monero_wallet_keys::create_wallet_from_seed(config); + return wallet_exists(wallet_keys->get_primary_address(), wallet_keys->get_private_view_key(), *config.m_server->m_uri, std::move(http_client_factory)); + } + + return wallet_exists(config.m_primary_address.get(), config.m_private_view_key.get(), *config.m_server->m_uri, std::move(http_client_factory)); + } + + monero_wallet_light* monero_wallet_light::open_wallet(const monero_wallet_config& config, std::unique_ptr http_client_factory) { + monero_wallet_config _config = config.copy(); + if (config.m_seed != boost::none && !config.m_seed->empty()) { + return create_wallet_from_seed(_config, std::move(http_client_factory)); + } + + return create_wallet_from_keys(_config, std::move(http_client_factory)); + } + + monero_wallet_light* monero_wallet_light::create_wallet(const monero_wallet_config& config, std::unique_ptr http_client_factory) { + MTRACE("create_wallet(config)"); + + // validate and normalize config + monero_wallet_config config_normalized = config.copy(); + if (config.m_path == boost::none) config_normalized.m_path = std::string(""); + if (config.m_password == boost::none) config_normalized.m_password = std::string(""); + if (config.m_language == boost::none) config_normalized.m_language = std::string(""); + if (config.m_seed == boost::none) config_normalized.m_seed = std::string(""); + if (config.m_primary_address == boost::none) config_normalized.m_primary_address = std::string(""); + if (config.m_private_spend_key == boost::none) config_normalized.m_private_spend_key = std::string(""); + if (config.m_private_view_key == boost::none) config_normalized.m_private_view_key = std::string(""); + if (config.m_seed_offset == boost::none) config_normalized.m_seed_offset = std::string(""); + if (config.m_is_multisig == boost::none) config_normalized.m_is_multisig = false; + if (config.m_account_lookahead != boost::none && config.m_subaddress_lookahead == boost::none) throw std::runtime_error("No subaddress lookahead provided with account lookahead"); + if (config.m_account_lookahead == boost::none && config.m_subaddress_lookahead != boost::none) throw std::runtime_error("No account lookahead provided with subaddress lookahead"); + if (config_normalized.m_language.get().empty()) config_normalized.m_language = std::string("English"); + if (!monero_utils::is_valid_language(config_normalized.m_language.get())) throw std::runtime_error("Unknown language: " + config_normalized.m_language.get()); + if (config.m_network_type == boost::none) throw std::runtime_error("Must provide wallet network type"); + // create wallet + + if (!config_normalized.m_seed.get().empty()) { + if (config.m_server != boost::none && config.m_server->m_uri != boost::none) { + if (wallet_exists(config, config.m_server->m_uri.get(), std::move(http_client_factory))) { + throw std::runtime_error("Wallet already exists"); + } + } + return create_wallet_from_seed(config_normalized, std::move(http_client_factory)); + } else if (!config_normalized.m_primary_address.get().empty() || !config_normalized.m_private_spend_key.get().empty() || !config_normalized.m_private_view_key.get().empty()) { + if (config_normalized.m_server != boost::none && config_normalized.m_server->m_uri != boost::none && wallet_exists(config_normalized, config_normalized.m_server->m_uri.get(), std::move(http_client_factory))) { + throw std::runtime_error("Wallet already exists"); + } + + return create_wallet_from_keys(config_normalized, std::move(http_client_factory)); + } else { + return create_wallet_random(config_normalized, std::move(http_client_factory)); + } + } + + monero_wallet_light* monero_wallet_light::create_wallet_from_seed(monero_wallet_config& config, std::unique_ptr http_client_factory) { + MTRACE("monero_wallet_light::create_wallet_from_seed(...)"); + + // validate config + if (config.m_is_multisig != boost::none && config.m_is_multisig.get()) throw std::runtime_error("Restoring from multisig seed not supported"); + if (config.m_network_type == boost::none) throw std::runtime_error("Must provide wallet network type"); + if (config.m_seed == boost::none || config.m_seed.get().empty()) throw std::runtime_error("Must provide wallet seed"); + + // validate mnemonic and get recovery key and language + crypto::secret_key spend_key_sk; + std::string language = config.m_language != boost::none ? config.m_language.get() : ""; + bool is_valid = crypto::ElectrumWords::words_to_bytes(config.m_seed.get(), spend_key_sk, language); + if (!is_valid) throw std::runtime_error("Invalid mnemonic"); + if (language == crypto::ElectrumWords::old_language_name) language = Language::English().get_language_name(); + + // validate language + if (!crypto::ElectrumWords::is_valid_language(language)) throw std::runtime_error("Invalid language: " + language); + + // apply offset if given + bool offset_set = config.m_seed_offset != boost::none && !config.m_seed_offset.get().empty(); + if (offset_set) spend_key_sk = cryptonote::decrypt_key(spend_key_sk, config.m_seed_offset.get()); + + // initialize wallet account + monero_wallet_light* wallet = new monero_wallet_light(std::move(http_client_factory)); + wallet->m_account = cryptonote::account_base{}; + crypto::secret_key spend_key_val = wallet->m_account.generate(spend_key_sk, true, false); + + // initialize remaining wallet + wallet->m_network_type = config.m_network_type.get(); + wallet->m_language = language; + epee::wipeable_string wipeable_mnemonic; + if (!crypto::ElectrumWords::bytes_to_words(spend_key_val, wipeable_mnemonic, wallet->m_language)) { + throw std::runtime_error("Failed to create mnemonic from private spend key for language: " + std::string(wallet->m_language)); + } + wallet->m_seed = std::string(wipeable_mnemonic.data(), wipeable_mnemonic.size()); + if (offset_set && wallet->m_seed == config.m_seed) throw std::runtime_error("Expected different seed"); + wallet->init_common(); + wallet->m_is_view_only = false; + + wallet->set_daemon_connection(config.m_server); + bool is_connected = wallet->is_connected_to_daemon(); + + if (is_connected) { + if (config.m_account_lookahead != boost::none) { + wallet->upsert_subaddrs(config.m_account_lookahead.get(), config.m_subaddress_lookahead.get()); + } + + if (config.m_restore_height != boost::none) + { + wallet->set_restore_height(config.m_restore_height.get()); + } + + } else if (config.m_restore_height != boost::none) throw std::runtime_error("Cannote restore wallet from height: wallet is not connected to lws"); + + return wallet; + } + + monero_wallet_light* monero_wallet_light::create_wallet_from_keys(monero_wallet_config& config, std::unique_ptr http_client_factory) { + MTRACE("monero_wallet_light::create_wallet_from_keys(...)"); + + // validate and normalize config + monero_wallet_config config_normalized = config.copy(); + if (config.m_network_type == boost::none) throw std::runtime_error("Must provide wallet network type"); + if (config.m_language == boost::none || config_normalized.m_language.get().empty()) config_normalized.m_language = "English"; + if (config.m_private_spend_key == boost::none) config_normalized.m_private_spend_key = std::string(""); + if (config.m_private_view_key == boost::none) config_normalized.m_private_view_key = std::string(""); + if (!monero_utils::is_valid_language(config_normalized.m_language.get())) throw std::runtime_error("Unknown language: " + config_normalized.m_language.get()); + + // parse and validate private spend key + crypto::secret_key spend_key_sk; + bool has_spend_key = false; + if (!config_normalized.m_private_spend_key.get().empty()) { + cryptonote::blobdata spend_key_data; + if (!epee::string_tools::parse_hexstr_to_binbuff(config.m_private_spend_key.get(), spend_key_data) || spend_key_data.size() != sizeof(crypto::secret_key)) { + throw std::runtime_error("failed to parse secret spend key"); + } + has_spend_key = true; + spend_key_sk = *reinterpret_cast(spend_key_data.data()); + } + + // parse and validate private view key + bool has_view_key = true; + crypto::secret_key view_key_sk; + if (config_normalized.m_private_view_key.get().empty()) { + if (has_spend_key) has_view_key = false; + else throw std::runtime_error("Neither spend key nor view key supplied"); + } + if (has_view_key) { + cryptonote::blobdata view_key_data; + if (!epee::string_tools::parse_hexstr_to_binbuff(config_normalized.m_private_view_key.get(), view_key_data) || view_key_data.size() != sizeof(crypto::secret_key)) { + throw std::runtime_error("failed to parse secret view key"); + } + view_key_sk = *reinterpret_cast(view_key_data.data()); + } + + // parse and validate address + cryptonote::address_parse_info address_info; + if (config_normalized.m_primary_address == boost:: none || config_normalized.m_primary_address.get().empty()) { + if (has_view_key) throw std::runtime_error("must provide address if providing private view key"); + } else { + if (!get_account_address_from_str(address_info, static_cast(config_normalized.m_network_type.get()), config_normalized.m_primary_address.get())) throw std::runtime_error("failed to parse address"); + + // check the spend and view keys match the given address + crypto::public_key pkey; + if (has_spend_key) { + if (!crypto::secret_key_to_public_key(spend_key_sk, pkey)) throw std::runtime_error("failed to verify secret spend key"); + if (address_info.address.m_spend_public_key != pkey) throw std::runtime_error("spend key does not match address"); + } + if (has_view_key) { + if (!crypto::secret_key_to_public_key(view_key_sk, pkey)) throw std::runtime_error("failed to verify secret view key"); + if (address_info.address.m_view_public_key != pkey) throw std::runtime_error("view key does not match address"); + } + } + + // initialize wallet account + monero_wallet_light* wallet = new monero_wallet_light(std::move(http_client_factory)); + if (has_spend_key && has_view_key) { + wallet->m_account.create_from_keys(address_info.address, spend_key_sk, view_key_sk); + } else if (has_spend_key) { + wallet->m_account.generate(spend_key_sk, true, false); + } else { + wallet->m_account.create_from_viewkey(address_info.address, view_key_sk); + } + + // initialize remaining wallet + wallet->m_is_view_only = !has_spend_key; + wallet->m_network_type = config_normalized.m_network_type.get(); + if (!config_normalized.m_private_spend_key.get().empty()) { + wallet->m_language = config_normalized.m_language.get(); + epee::wipeable_string wipeable_mnemonic; + if (!crypto::ElectrumWords::bytes_to_words(spend_key_sk, wipeable_mnemonic, wallet->m_language)) { + throw std::runtime_error("Failed to create mnemonic from private spend key for language: " + std::string(wallet->m_language)); + } + wallet->m_seed = std::string(wipeable_mnemonic.data(), wipeable_mnemonic.size()); + } + wallet->init_common(); + wallet->set_daemon_connection(config.m_server); + + bool is_connected = wallet->is_connected_to_daemon(); + + if (is_connected) { + if (config.m_account_lookahead != boost::none) { + wallet->upsert_subaddrs(config.m_account_lookahead.get(), config.m_subaddress_lookahead.get()); + } + + if (config.m_restore_height != boost::none) + { + wallet->set_restore_height(config.m_restore_height.get()); + } + + } else if (config.m_restore_height != boost::none) throw std::runtime_error("Cannote restore wallet from height: wallet is not connected to lws"); + + return wallet; + } + + monero_wallet_light* monero_wallet_light::create_wallet_random(monero_wallet_config& config, std::unique_ptr http_client_factory) { + MTRACE("monero_wallet_light::create_wallet_random(...)"); + + // validate and normalize config + monero_wallet_config config_normalized = config.copy(); + if (config_normalized.m_network_type == boost::none) throw std::runtime_error("Must provide wallet network type"); + if (config_normalized.m_language == boost::none || config_normalized.m_language.get().empty()) config_normalized.m_language = "English"; + if (!monero_utils::is_valid_language(config_normalized.m_language.get())) throw std::runtime_error("Unknown language: " + config_normalized.m_language.get()); + + // initialize random wallet account + monero_wallet_light* wallet = new monero_wallet_light(std::move(http_client_factory)); + crypto::secret_key spend_key_sk = wallet->m_account.generate(); + + // initialize remaining wallet + wallet->m_network_type = config_normalized.m_network_type.get(); + wallet->m_language = config_normalized.m_language.get(); + epee::wipeable_string wipeable_mnemonic; + if (!crypto::ElectrumWords::bytes_to_words(spend_key_sk, wipeable_mnemonic, wallet->m_language)) { + throw std::runtime_error("Failed to create mnemonic from private spend key for language: " + std::string(wallet->m_language)); + } + wallet->m_seed = std::string(wipeable_mnemonic.data(), wipeable_mnemonic.size()); + wallet->init_common(); + wallet->m_is_view_only = false; + + wallet->set_daemon_connection(config.m_server); + + if (config.m_account_lookahead != boost::none && wallet->is_connected_to_daemon()) { + wallet->upsert_subaddrs(config.m_account_lookahead.get(), config.m_subaddress_lookahead.get()); + } + + return wallet; + } +} diff --git a/src/wallet/monero_wallet_light.h b/src/wallet/monero_wallet_light.h new file mode 100644 index 00000000..198cc624 --- /dev/null +++ b/src/wallet/monero_wallet_light.h @@ -0,0 +1,260 @@ +/** + * Copyright (c) woodser + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Parts of this file are originally copyright (c) 2014-2019, The Monero Project + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * All rights reserved. + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be + * used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers + */ + +#pragma once + +#include "monero_wallet_keys.h" +#include "monero_wallet_light_model.h" +#include "monero_wallet_utils.h" +#include "utils/monero_utils.h" +#include "cryptonote_basic/cryptonote_basic_impl.h" +#include +#include +#include + +/** + * Implements a monero_wallet.h light wallet. + */ +namespace monero { + + // --------------------------- STATIC WALLET UTILS -------------------------- + + // there is a light that never goes out + class monero_wallet_light : public monero_wallet_keys { + + public: + + /** + * Indicates if a wallet exists at the light wallet server. + * + * @param primary_address wallet standard address + * @param private_view_key wallet private view key + * @param server_uri light wallet server uri + * @param http_client_factory allows use of custom http clients + * @return true if a wallet exists at the light wallet server, false otherwise + */ + static bool wallet_exists(const std::string& primary_address, const std::string& private_view_key, const std::string& server_uri, std::unique_ptr http_client_factory = nullptr); + + /** + * Indicates if a wallet exists at the light wallet server. + * + * @param config wallet configuration + * @param server_uri light wallet server uri + * @param http_client_factory allows use of custom http clients + * @return true if a wallet exists at the light wallet server, false otherwise + */ + static bool wallet_exists(const monero_wallet_config& config, const std::string& server_uri, std::unique_ptr http_client_factory = nullptr); + + /** + * Open an existing wallet from a light wallet server. + * + * @param primary_address wallet standard address + * @param private_view_key wallet private view key + * @param server_uri light wallet server uri + * @param network_type is the wallet's network type + * @param http_client_factory allows use of custom http clients + * @return a pointer to the wallet instance + */ + static monero_wallet_light* open_wallet(const monero_wallet_config& config, std::unique_ptr http_client_factory = nullptr); + + /** + * Create a new wallet with the given configuration. + * + * @param config is the wallet configuration + * @param http_client_factory allows use of custom http clients + * @return a pointer to the wallet instance + */ + static monero_wallet_light* create_wallet(const monero_wallet_config& config, std::unique_ptr http_client_factory = nullptr); + + monero_wallet_light(std::unique_ptr http_client_factory = nullptr); + ~monero_wallet_light(); + + void set_daemon_connection(const std::string& uri, const std::string& username = "", const std::string& password = "", const std::string& proxy_uri = "") override; + void set_daemon_connection(const boost::optional& connection) override; + boost::optional get_daemon_connection() const override; + bool is_connected_to_daemon() const override; + bool is_daemon_synced() const override; + bool is_daemon_trusted() const override; + bool is_synced() const override; + std::string get_seed() const override; + std::string get_seed_language() const override; + std::string get_private_spend_key() const override; + monero_subaddress get_address_index(const std::string& address) const override; + uint64_t get_height() const override; + void set_restore_height(uint64_t restore_height) override; + uint64_t get_restore_height() const override; + uint64_t get_daemon_height() const override; + uint64_t get_daemon_max_peer_height() const override; + void add_listener(monero_wallet_listener& listener) override; + void remove_listener(monero_wallet_listener& listener) override; + std::set get_listeners() override; + monero_sync_result sync() override; + monero_sync_result sync(monero_wallet_listener& listener) override; + monero_sync_result sync(uint64_t start_height) override; + monero_sync_result sync(uint64_t start_height, monero_wallet_listener& listener) override; + void start_syncing(uint64_t sync_period_in_ms) override; + void stop_syncing() override; + void scan_txs(const std::vector& tx_ids) override; + void rescan_spent() override; + void rescan_blockchain() override; + uint64_t get_balance() const override; + uint64_t get_balance(uint32_t account_idx) const override; + uint64_t get_balance(uint32_t account_idx, uint32_t subaddress_idx) const override; + uint64_t get_unlocked_balance() const override; + uint64_t get_unlocked_balance(uint32_t account_idx) const override; + uint64_t get_unlocked_balance(uint32_t account_idx, uint32_t subaddress_idx) const override; + std::vector get_accounts(bool include_subaddresses, const std::string& tag) const override; + monero_account get_account(const uint32_t account_idx, bool include_subaddresses) const override; + monero_account create_account(const std::string& label = "") override; + monero_subaddress get_subaddress(const uint32_t account_idx, const uint32_t subaddress_idx) const override; + std::vector get_subaddresses(const uint32_t account_idx, const std::vector& subaddress_indices) 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 = true) const override; + std::shared_ptr import_key_images(const std::vector>& key_images) override; + void freeze_output(const std::string& key_image) override; + void thaw_output(const std::string& key_image) override; + bool is_output_frozen(const std::string& key_image) override; + monero_tx_priority get_default_fee_priority() const override; + std::vector> create_txs(const monero_tx_config& config) override; + std::vector relay_txs(const std::vector& tx_metadatas) override; + monero_tx_set describe_tx_set(const monero_tx_set& tx_set) override; + monero_tx_set sign_txs(const std::string& unsigned_tx_hex) override; + std::vector submit_txs(const std::string& signed_tx_hex) 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_hash, const std::string& note) 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; + void edit_address_book_entry(uint64_t index, bool set_address, const std::string& address, bool set_description, const std::string& description) override; + void delete_address_book_entry(uint64_t index) override; + bool get_attribute(const std::string& key, std::string& value) const override; + void set_attribute(const std::string& key, const std::string& val) override; + uint64_t wait_for_next_block() override; + bool is_multisig_import_needed() const override { return false; }; + monero_multisig_info get_multisig_info() const override; + void close(bool save) override; + + // ---------------------------------- PRIVATE --------------------------------- + + protected: + void init_common() override; + wallet2_exported_outputs export_outputs(bool all, uint32_t start, uint32_t count = 0xffffffff) const override; + std::string get_tx_prefix_hash(const std::string& tx_hash) const override { return m_output_store.get_tx_prefix_hash(tx_hash); }; + + std::unique_ptr m_wallet_listener; // internal wallet implementation listener + std::set m_listeners; // external wallet listeners + + static monero_wallet_light* create_wallet_from_seed(monero_wallet_config& config, std::unique_ptr http_client_factory); + static monero_wallet_light* create_wallet_from_keys(monero_wallet_config& config, std::unique_ptr http_client_factory); + static monero_wallet_light* create_wallet_random(monero_wallet_config& config, std::unique_ptr http_client_factory); + + void init_subaddress(monero_subaddress& subaddress) const; + boost::optional get_subaddress_label(uint32_t account_idx, uint32_t subaddress_idx) const; + uint64_t get_subaddress_num_blocks_to_unlock(uint32_t account_idx, uint32_t subaddress_idx) const; + std::vector get_subaddresses_aux(const uint32_t account_idx, const std::vector& subaddress_indices) const; + std::vector> get_transfers_aux(const monero_transfer_query& query) const; + std::vector> get_outputs_aux(const monero_output_query& query) const; + + // blockchain sync management + mutable std::atomic m_is_synced; // whether or not wallet is synced + mutable std::atomic m_is_connected; // cache connection status to avoid unecessary RPC calls + boost::condition_variable m_sync_cv; // to make sync threads woke + boost::recursive_mutex m_sync_mutex; // synchronize sync() and syncAsync() requests + std::atomic m_rescan_on_sync; // whether or not to rescan on sync + std::atomic m_syncing_enabled; // whether or not auto sync is enabled + std::atomic m_sync_loop_running; // whether or not the syncing thread is shut down + std::atomic m_syncing_interval; // auto sync loop interval in milliseconds + boost::thread m_syncing_thread; // thread for auto sync loop + boost::mutex m_syncing_mutex; // synchronize auto sync loop + void run_sync_loop(); // run the sync loop in a thread + monero_sync_result lock_and_sync(boost::optional start_height = boost::none); // internal function to synchronize request to sync and rescan + monero_sync_result sync_aux(boost::optional start_height = boost::none); // internal function to immediately block, sync, and report progress + + // wallet data + std::vector m_address_book; + serializable_unordered_map m_tx_notes; + serializable_unordered_map m_attributes; + serializable_unordered_map> m_subaddress_labels; + + mutable boost::recursive_mutex m_sync_data_mutex; + std::unique_ptr m_client; + monero_light_get_address_info_response m_address_info; + monero_light_get_address_txs_response m_address_txs; + monero_light_get_unspent_outs_response m_unspent_outs; + monero_light_get_subaddrs_response m_subaddrs; + monero_light_output_store m_output_store; + monero_light_tx_store m_tx_store; + boost::optional m_prior_attempt_size_calcd_fee; + boost::optional m_prior_attempt_unspent_outs_to_mix_outs; + size_t m_construction_attempt; + + bool output_is_spent(monero_light_output &output) const; + bool spend_is_real(monero_light_spend &spend) const; + void process_txs(); + void process_outputs(); + void process_subaddresses(); + void calculate_balance(); + void upsert_subaddrs(const monero_light_subaddrs &subaddrs, bool get_all = true); + void upsert_subaddrs(uint32_t account_idx, uint32_t subaddress_idx, bool get_all = true); + void login(bool create_account = true, bool generated_locally = true) const; + monero_sync_result refresh(); + }; + +} \ No newline at end of file diff --git a/src/wallet/monero_wallet_light_model.cpp b/src/wallet/monero_wallet_light_model.cpp new file mode 100644 index 00000000..f94287f3 --- /dev/null +++ b/src/wallet/monero_wallet_light_model.cpp @@ -0,0 +1,1800 @@ +/** + * Copyright (c) woodser + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Parts of this file are originally copyright (c) 2014-2019, The Monero Project + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * All rights reserved. + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be + * used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers + */ + +#include "monero_wallet_light_model.h" + +#include "utils/gen_utils.h" +#include "utils/monero_utils.h" +#include +#include "net/http.h" + +namespace monero { + + // ------------------------------- DESERIALIZE UTILS ------------------------------- + + std::shared_ptr monero_light_version::deserialize(const std::string& version_json) { + std::istringstream iss = version_json.empty() ? std::istringstream() : std::istringstream(version_json); + boost::property_tree::ptree node; + boost::property_tree::read_json(iss, node); + + std::shared_ptr version = std::make_shared(); + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("server_type")) version->m_server_type = it->second.data(); + else if (key == std::string("server_version")) version->m_server_version = it->second.data(); + else if (key == std::string("last_git_commit_hash")) version->m_last_git_commit_hash = it->second.data(); + else if (key == std::string("last_git_commit_date")) version->m_last_git_commit_date = it->second.data(); + else if (key == std::string("git_branch_name")) version->m_git_branch_name = it->second.data(); + else if (key == std::string("monero_version_full")) version->m_monero_version_full = it->second.data(); + else if (key == std::string("blockchain_height")) version->m_blockchain_height = it->second.get_value(); + else if (key == std::string("api")) version->m_api = it->second.get_value(); + else if (key == std::string("max_subaddresses")) version->m_max_subaddresses = it->second.get_value(); + else if (key == std::string("testnet")) version->m_testnet = it->second.get_value(); + else if (key == std::string("network")) { + std::string network_str = it->second.data(); + if (network_str == std::string("main")) version->m_network_type = monero_network_type::MAINNET; + else if (network_str == std::string("test")) version->m_network_type = monero_network_type::TESTNET; + else if (network_str == std::string("stage")) version->m_network_type = monero_network_type::STAGENET; + throw std::runtime_error("Cannot deserialize lws version: invalid network provided " + network_str); + } + } + + return version; + } + + std::shared_ptr monero_light_address_meta::deserialize(const std::string& config_json) { + // deserialize monero output json to property node + std::istringstream iss = config_json.empty() ? std::istringstream() : std::istringstream(config_json); + boost::property_tree::ptree node; + boost::property_tree::read_json(iss, node); + + // convert config property tree to monero_wallet_config + std::shared_ptr address_meta = std::make_shared(); + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("maj_i")) address_meta->m_maj_i = it->second.get_value(); + else if (key == std::string("min_i")) address_meta->m_min_i = it->second.get_value(); + } + + return address_meta; + } + + std::shared_ptr monero_light_output::deserialize(const std::string& config_json) { + // deserialize monero output json to property node + std::istringstream iss = config_json.empty() ? std::istringstream() : std::istringstream(config_json); + boost::property_tree::ptree node; + boost::property_tree::read_json(iss, node); + + // convert config property tree to monero_wallet_config + std::shared_ptr output = std::make_shared(); + std::shared_ptr recipient = std::make_shared(); + + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("tx_id")) output->m_tx_id = it->second.get_value(); + else if (key == std::string("amount")) output->m_amount = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("index")) output->m_index = it->second.get_value(); + else if (key == std::string("global_index")) output->m_global_index = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("rct")) output->m_rct = it->second.data(); + else if (key == std::string("tx_hash")) output->m_tx_hash = it->second.data(); + else if (key == std::string("tx_prefix_hash")) output->m_tx_prefix_hash = it->second.data(); + else if (key == std::string("public_key")) output->m_public_key = it->second.data(); + else if (key == std::string("tx_pub_key")) output->m_tx_pub_key = it->second.data(); + else if (key == std::string("spend_key_images")) for (boost::property_tree::ptree::const_iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) output->m_spend_key_images.push_back(it2->second.data()); + else if (key == std::string("timestamp")) output->m_timestamp = gen_utils::timestamp_to_epoch(it->second.data()); + else if (key == std::string("height")) output->m_height = it->second.get_value(); + else if (key == std::string("recipient")) { + monero_light_address_meta::from_property_tree(it->second, recipient); + } + } + + output->m_recipient = *recipient; + + return output; + } + + std::shared_ptr monero_light_spend::deserialize(const std::string& config_json) { + // deserialize monero output json to property node + std::istringstream iss = config_json.empty() ? std::istringstream() : std::istringstream(config_json); + boost::property_tree::ptree node; + boost::property_tree::read_json(iss, node); + + // convert config property tree to monero_wallet_config + std::shared_ptr spend = std::make_shared(); + std::shared_ptr sender = std::make_shared(); + + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("amount")) spend->m_amount = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("key_image")) spend->m_key_image = it->second.data(); + else if (key == std::string("tx_pub_key")) spend->m_tx_pub_key = it->second.data(); + else if (key == std::string("out_index")) spend->m_out_index = it->second.get_value(); + else if (key == std::string("mixin")) spend->m_mixin = it->second.get_value(); + else if (key == std::string("sender")) { + monero_light_address_meta::from_property_tree(it->second, sender); + } + } + + spend->m_sender = *sender; + + return spend; + } + + std::shared_ptr monero_light_tx::deserialize(const std::string& config_json) { + // deserialize monero output json to property node + std::istringstream iss = config_json.empty() ? std::istringstream() : std::istringstream(config_json); + boost::property_tree::ptree node; + boost::property_tree::read_json(iss, node); + + // convert config property tree to monero_wallet_config + std::shared_ptr transaction = std::make_shared(); + transaction->m_coinbase = false; + transaction->m_total_received = 0; + transaction->m_total_sent = 0; + + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("id")) transaction->m_id = it->second.get_value(); + else if (key == std::string("hash")) transaction->m_hash = it->second.data(); + else if (key == std::string("timestamp")) transaction->m_timestamp = gen_utils::timestamp_to_epoch(it->second.data()); + else if (key == std::string("total_received")) transaction->m_total_received = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("total_sent")) transaction->m_total_sent = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("fee")) transaction->m_fee = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("unlock_time")) transaction->m_unlock_time = it->second.get_value(); + else if (key == std::string("height")) transaction->m_height = it->second.get_value(); + else if (key == std::string("spent_outputs")) { + // deserialize monero_light_spend + boost::property_tree::ptree outs_node = it->second; + for (boost::property_tree::ptree::const_iterator it2 = outs_node.begin(); it2 != outs_node.end(); ++it2) { + std::shared_ptr out = std::make_shared(); + monero_light_spend::from_property_tree(it2->second, out); + transaction->m_spent_outputs.push_back(*out); + } + } + else if (key == std::string("payment_id")) transaction->m_payment_id = it->second.data(); + else if (key == std::string("coinbase")) transaction->m_coinbase = it->second.get_value(); + else if (key == std::string("mempool")) transaction->m_mempool = it->second.get_value(); + else if (key == std::string("mixin")) transaction->m_mixin = it->second.get_value(); + else if (key == std::string("recipient")) { + std::shared_ptr recipient = std::make_shared(); + monero_light_address_meta::from_property_tree(it->second, recipient); + transaction->m_recipient = *recipient; + } + } + + return transaction; + } + + std::shared_ptr monero_light_random_outputs::deserialize(const std::string& config_json) { + // deserialize monero output json to property node + std::istringstream iss = config_json.empty() ? std::istringstream() : std::istringstream(config_json); + boost::property_tree::ptree node; + boost::property_tree::read_json(iss, node); + + // convert config property tree to monero_wallet_config + std::shared_ptr random_outputs = std::make_shared(); + + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("amount")) random_outputs->m_amount = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("outputs")) { + boost::property_tree::ptree outs_node = it->second; + for (boost::property_tree::ptree::const_iterator it2 = outs_node.begin(); it2 != outs_node.end(); ++it2) { + std::shared_ptr out = std::make_shared(); + monero_light_output::from_property_tree(it2->second, out); + random_outputs->m_outputs.push_back(*out); + } + } + } + + return random_outputs; + } + + std::shared_ptr monero_light_get_address_info_response::deserialize(const std::string& config_json) { + // deserialize monero output json to property node + std::istringstream iss = config_json.empty() ? std::istringstream() : std::istringstream(config_json); + boost::property_tree::ptree node; + boost::property_tree::read_json(iss, node); + + // convert config property tree to monero_wallet_config + std::shared_ptr address_info = std::make_shared(); + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("locked_funds")) address_info->m_locked_funds = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("total_received")) address_info->m_total_received = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("total_sent")) address_info->m_total_sent = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("scanned_height")) address_info->m_scanned_height = it->second.get_value(); + else if (key == std::string("scanned_block_height")) address_info->m_scanned_block_height = it->second.get_value(); + else if (key == std::string("start_height")) address_info->m_start_height = it->second.get_value(); + else if (key == std::string("transaction_height")) address_info->m_transaction_height = it->second.get_value(); + else if (key == std::string("blockchain_height")) address_info->m_blockchain_height = it->second.get_value(); + else if (key == std::string("spent_outputs")) { + boost::property_tree::ptree spent_outputs_node = it->second; + for (boost::property_tree::ptree::const_iterator it2 = spent_outputs_node.begin(); it2 != spent_outputs_node.end(); ++it2) { + std::shared_ptr spent_output = std::make_shared(); + monero_light_spend::from_property_tree(it2->second, spent_output); + address_info->m_spent_outputs.push_back(*spent_output); + } + } + } + + return address_info; + } + + std::shared_ptr monero_light_get_address_txs_response::deserialize(const std::string& config_json) { + // deserialize monero output json to property node + std::istringstream iss = config_json.empty() ? std::istringstream() : std::istringstream(config_json); + boost::property_tree::ptree node; + boost::property_tree::read_json(iss, node); + + // convert config property tree to monero_wallet_config + std::shared_ptr address_txs = std::make_shared(); + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("total_received")) address_txs->m_total_received = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("scanned_height")) address_txs->m_scanned_height = it->second.get_value(); + else if (key == std::string("scanned_block_height")) address_txs->m_scanned_block_height = it->second.get_value(); + else if (key == std::string("start_height")) address_txs->m_start_height = it->second.get_value(); + else if (key == std::string("blockchain_height")) address_txs->m_blockchain_height = it->second.get_value(); + else if (key == std::string("transactions")) { + boost::property_tree::ptree transactions_node = it->second; + for (boost::property_tree::ptree::const_iterator it2 = transactions_node.begin(); it2 != transactions_node.end(); ++it2) { + std::shared_ptr transaction = std::make_shared(); + monero_light_tx::from_property_tree(it2->second, transaction); + address_txs->m_transactions.push_back(*transaction); + } + } + } + + return address_txs; + } + + std::shared_ptr monero_light_get_random_outs_response::deserialize(const std::string& config_json) { + // deserialize monero output json to property node + std::istringstream iss = config_json.empty() ? std::istringstream() : std::istringstream(config_json); + boost::property_tree::ptree node; + boost::property_tree::read_json(iss, node); + + // convert config property tree to monero_wallet_config + std::shared_ptr random_outs = std::make_shared(); + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("amount_outs")) { + boost::property_tree::ptree outs_node = it->second; + + for (boost::property_tree::ptree::const_iterator it2 = outs_node.begin(); it2 != outs_node.end(); ++it2) { + std::shared_ptr out = std::make_shared(); + monero_light_random_outputs::from_property_tree(it2->second, out); + random_outs->m_amount_outs.push_back(*out); + } + } + } + + return random_outs; + } + + std::shared_ptr monero_light_get_unspent_outs_response::deserialize(const std::string& config_json) { + // deserialize monero output json to property node + std::istringstream iss = config_json.empty() ? std::istringstream() : std::istringstream(config_json); + boost::property_tree::ptree node; + boost::property_tree::read_json(iss, node); + + // convert config property tree to monero_wallet_config + std::shared_ptr unspent_outs = std::make_shared(); + unspent_outs->m_outputs = std::vector(); + + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("per_byte_fee")) unspent_outs->m_per_byte_fee = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("fee_mask")) unspent_outs->m_fee_mask = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("amount")) unspent_outs->m_amount = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("outputs")) { + boost::property_tree::ptree outs_node = it->second; + for (boost::property_tree::ptree::const_iterator it2 = outs_node.begin(); it2 != outs_node.end(); ++it2) { + std::shared_ptr out = std::make_shared(); + monero_light_output::from_property_tree(it2->second, out); + unspent_outs->m_outputs.push_back(*out); + } + } + } + + return unspent_outs; + } + + std::shared_ptr monero_light_import_wallet_response::deserialize(const std::string& config_json) { + // deserialize monero output json to property node + std::istringstream iss = config_json.empty() ? std::istringstream() : std::istringstream(config_json); + boost::property_tree::ptree node; + boost::property_tree::read_json(iss, node); + + // convert config property tree to monero_wallet_config + std::shared_ptr import_request = std::make_shared(); + + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("payment_address")) import_request->m_payment_address = it->second.data(); + else if (key == std::string("payment_id")) import_request->m_payment_id = it->second.data(); + else if (key == std::string("import_fee")) import_request->m_import_fee = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("new_request")) import_request->m_new_request = it->second.get_value(); + else if (key == std::string("request_fulfilled")) import_request->m_request_fullfilled = it->second.get_value(); + else if (key == std::string("status")) import_request->m_status = it->second.data(); + } + + return import_request; + } + + std::shared_ptr monero_light_login_response::deserialize(const std::string& config_json) { + // deserialize monero output json to property node + std::istringstream iss = config_json.empty() ? std::istringstream() : std::istringstream(config_json); + boost::property_tree::ptree node; + boost::property_tree::read_json(iss, node); + + // convert config property tree to monero_wallet_config + std::shared_ptr login = std::make_shared(); + + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("new_address")) login->m_new_address = it->second.get_value(); + else if (key == std::string("generated_locally")) login->m_generated_locally = it->second.get_value(); + else if (key == std::string("start_height")) login->m_start_height = it->second.get_value(); + } + + return login; + } + + std::shared_ptr monero_light_submit_raw_tx_response::deserialize(const std::string& config_json) { + // deserialize monero output json to property node + std::istringstream iss = config_json.empty() ? std::istringstream() : std::istringstream(config_json); + boost::property_tree::ptree node; + boost::property_tree::read_json(iss, node); + + // convert config property tree to monero_wallet_config + std::shared_ptr tx = std::make_shared(); + + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("status")) tx->m_status = it->second.data(); + } + + return tx; + } + + std::shared_ptr monero_light_upsert_subaddrs_response::deserialize(const std::string& config_json) { + // deserialize monero output json to property node + std::istringstream iss = config_json.empty() ? std::istringstream() : std::istringstream(config_json); + boost::property_tree::ptree node; + boost::property_tree::read_json(iss, node); + + // convert config property tree to monero_wallet_config + std::shared_ptr response = std::make_shared(); + response->m_all_subaddrs = monero_light_subaddrs(); + + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("new_subaddrs")) { + std::shared_ptr new_subaddrs = std::make_shared(); + monero_light_subaddrs::from_property_tree(it->second, new_subaddrs); + response->m_new_subaddrs = *new_subaddrs; + } else if (key == std::string("all_subaddrs")) { + std::shared_ptr all_subaddrs = std::make_shared(); + monero_light_subaddrs::from_property_tree(it->second, all_subaddrs); + response->m_all_subaddrs = *all_subaddrs; + } + } + + return response; + } + + std::shared_ptr monero_light_get_subaddrs_response::deserialize(const std::string& config_json) { + // deserialize monero output json to property node + std::istringstream iss = config_json.empty() ? std::istringstream() : std::istringstream(config_json); + boost::property_tree::ptree node; + boost::property_tree::read_json(iss, node); + + // convert config property tree to monero_wallet_config + std::shared_ptr response = std::make_shared(); + response->m_all_subaddrs = monero_light_subaddrs(); + + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("all_subaddrs")) { + std::shared_ptr all_subaddrs = std::make_shared(); + monero_light_subaddrs::from_property_tree(it->second, all_subaddrs); + response->m_all_subaddrs = *all_subaddrs; + } + } + + return response; + } + + // ------------------------------- PROPERTY TREE UTILS ------------------------------- + + void monero_light_version::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& version) { + // convert config property tree to monero_light_version + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("server_type")) version->m_server_type = it->second.data(); + else if (key == std::string("server_version")) version->m_server_version = it->second.data(); + else if (key == std::string("last_git_commit_hash")) version->m_last_git_commit_hash = it->second.data(); + else if (key == std::string("last_git_commit_date")) version->m_last_git_commit_date = it->second.data(); + else if (key == std::string("git_branch_name")) version->m_git_branch_name = it->second.data(); + else if (key == std::string("monero_version_full")) version->m_monero_version_full = it->second.data(); + else if (key == std::string("blockchain_height")) version->m_blockchain_height = it->second.get_value(); + else if (key == std::string("api")) version->m_api = it->second.get_value(); + else if (key == std::string("max_subaddresses")) version->m_max_subaddresses = it->second.get_value(); + else if (key == std::string("testnet")) version->m_testnet = it->second.get_value(); + else if (key == std::string("network")) { + std::string network_str = it->second.data(); + if (network_str == std::string("mainnet") || network_str == "fakechain") version->m_network_type = monero_network_type::MAINNET; + else if (network_str == std::string("testnet")) version->m_network_type = monero_network_type::TESTNET; + else if (network_str == std::string("stagenet")) version->m_network_type = monero_network_type::STAGENET; + throw std::runtime_error("Cannot deserialize lws version: invalid network provided " + network_str); + } + } + } + + void monero_light_address_meta::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& address_meta) { + // convert config property tree to monero_light_address_meta + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("maj_i")) address_meta->m_maj_i = it->second.get_value(); + else if (key == std::string("min_i")) address_meta->m_min_i = it->second.get_value(); + } + } + + void monero_light_index_range::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& index_range) { + // convert config property tree to monero_wallet_config + int length = 0; + + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + uint32_t value = it->second.get_value(); + index_range->push_back(value); + + length++; + if (length > 2) throw std::runtime_error("Invalid index range length"); + //if (key == std::string("maj_i")) address_meta->m_maj_i = it->second.get_value(); + //else if (key == std::string("min_i")) address_meta->m_min_i = it->second.get_value(); + } + + if (length != 2) throw std::runtime_error("Invalid index range length"); + } + + void monero_light_subaddrs::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& subaddrs) { + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + boost::property_tree::ptree key_value_node = it->second; + boost::optional _key; + std::vector index_ranges; + + for (boost::property_tree::ptree::const_iterator it2 = key_value_node.begin(); it2 != key_value_node.end(); ++it2) { + std::string key = it2->first; + if (key == std::string("key")) _key = it2->second.get_value(); + else if (key == std::string("value")) { + for (boost::property_tree::ptree::const_iterator it3 = it2->second.begin(); it3 != it2->second.end(); ++it3) { + std::shared_ptr ir = std::make_shared(); + monero_light_index_range::from_property_tree(it3->second, ir); + index_ranges.push_back(*ir); + } + } + } + + if (_key == boost::none) throw std::runtime_error("Invalid subaddress"); + + subaddrs->emplace(_key.get(), index_ranges); + } + } + + void monero_light_output::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& output) { + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("tx_id")) output->m_tx_id = it->second.get_value(); + else if (key == std::string("amount")) output->m_amount = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("index")) output->m_index = it->second.get_value(); + else if (key == std::string("global_index")) output->m_global_index = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("rct")) output->m_rct = it->second.data(); + else if (key == std::string("tx_hash")) output->m_tx_hash = it->second.data(); + else if (key == std::string("tx_prefix_hash")) output->m_tx_prefix_hash = it->second.data(); + else if (key == std::string("public_key")) output->m_public_key = it->second.data(); + else if (key == std::string("tx_pub_key")) output->m_tx_pub_key = it->second.data(); + else if (key == std::string("spend_key_images")) { + for (boost::property_tree::ptree::const_iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) output->m_spend_key_images.push_back(it2->second.data()); + } + else if (key == std::string("timestamp")) output->m_timestamp = gen_utils::timestamp_to_epoch(it->second.data()); + else if (key == std::string("height")) output->m_height = it->second.get_value(); + else if (key == std::string("recipient")) { + std::shared_ptr recipient = std::make_shared(); + monero_light_address_meta::from_property_tree(it->second, recipient); + output->m_recipient = *recipient; + } + } + } + + void monero_light_spend::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& spend) { + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("amount")) spend->m_amount = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("key_image")) spend->m_key_image = it->second.data(); + else if (key == std::string("tx_pub_key")) spend->m_tx_pub_key = it->second.data(); + else if (key == std::string("out_index")) spend->m_out_index = it->second.get_value(); + else if (key == std::string("mixin")) spend->m_mixin = it->second.get_value(); + else if (key == std::string("sender")) { + std::shared_ptr sender = std::make_shared(); + monero_light_address_meta::from_property_tree(it->second, sender); + spend->m_sender = *sender; + } + } + } + + void monero_light_tx::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& transaction) { + std::shared_ptr recipient = std::make_shared(); + + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("id")) transaction->m_id = it->second.get_value(); + else if (key == std::string("hash")) transaction->m_hash = it->second.data(); + else if (key == std::string("timestamp")) transaction->m_timestamp = gen_utils::timestamp_to_epoch(it->second.data()); + else if (key == std::string("total_received")) transaction->m_total_received = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("total_sent")) transaction->m_total_sent = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("fee")) transaction->m_fee = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("unlock_time")) transaction->m_unlock_time = it->second.get_value(); + else if (key == std::string("height")) transaction->m_height = it->second.get_value(); + else if (key == std::string("spent_outputs")) { + // deserialize monero_light_spend + boost::property_tree::ptree outs = it->second; + for (boost::property_tree::ptree::const_iterator it2 = outs.begin(); it2 != outs.end(); ++it2) { + std::shared_ptr out = std::make_shared(); + monero_light_spend::from_property_tree(it2->second, out); + transaction->m_spent_outputs.push_back(*out); + } + } + else if (key == std::string("payment_id")) transaction->m_payment_id = it->second.data(); + else if (key == std::string("coinbase")) transaction->m_coinbase = it->second.get_value(); + else if (key == std::string("mempool")) transaction->m_mempool = it->second.get_value(); + else if (key == std::string("mixin")) transaction->m_mixin = it->second.get_value(); + else if (key == std::string("recipient")) { + monero_light_address_meta::from_property_tree(it->second, recipient); + } + } + + transaction->m_recipient = *recipient; + } + + void monero_light_random_outputs::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& random_outputs) { + // convert config property tree to monero_wallet_config + + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + + if (key == std::string("amount")) random_outputs->m_amount = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("outputs")) { + boost::property_tree::ptree outs_node = it->second; + for (boost::property_tree::ptree::const_iterator it2 = outs_node.begin(); it2 != outs_node.end(); ++it2) { + std::shared_ptr out = std::make_shared(); + monero_light_output::from_property_tree(it2->second, out); + random_outputs->m_outputs.push_back(*out); + } + } + } + } + + // ------------------------------- SERIALIZE UTILS ------------------------------- + + rapidjson::Value monero_light_subaddrs::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + rapidjson::Value root(rapidjson::kArrayType); + rapidjson::Value value_num(rapidjson::kNumberType); + rapidjson::Value value_arr(rapidjson::kArrayType); + + for(auto subaddr : *this) { + rapidjson::Value obj_value(rapidjson::kObjectType); + monero_utils::add_json_member("key", subaddr.first, allocator, obj_value, value_num); + std::vector index_ranges = subaddr.second; + //obj_value.AddMember("value", monero_utils::to_rapidjson_val(allocator, index_ranges), allocator); + rapidjson::Value obj_index_ranges(rapidjson::kArrayType); + + for (monero_light_index_range index_range : index_ranges) { + obj_index_ranges.PushBack(monero_utils::to_rapidjson_val(allocator, (std::vector)index_range), allocator); + } + + obj_value.AddMember("value", obj_index_ranges, allocator); + + root.PushBack(obj_value, allocator); + } + + return root; + } + + rapidjson::Value monero_light_wallet_request::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set string values + rapidjson::Value value_str(rapidjson::kStringType); + if (m_address != boost::none) monero_utils::add_json_member("address", m_address.get(), allocator, root, value_str); + if (m_view_key != boost::none) monero_utils::add_json_member("view_key", m_view_key.get(), allocator, root, value_str); + + // return root + return root; + } + + rapidjson::Value monero_light_get_random_outs_request::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set string values + rapidjson::Value value_str(rapidjson::kStringType); + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_count != boost::none) monero_utils::add_json_member("count", m_count.get(), allocator, root, value_num); + + std::vector amounts; + + for(const auto amount : m_amounts) { + amounts.push_back(std::to_string(amount)); + } + + // set sub-arrays + root.AddMember("amounts", monero_utils::to_rapidjson_val(allocator, amounts), allocator); + + // return root + return root; + } + + rapidjson::Value monero_light_import_wallet_request::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + + // create root + rapidjson::Value root = monero_light_wallet_request::to_rapidjson_val(allocator); + + // set number values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_from_height != boost::none) monero_utils::add_json_member("from_height", m_from_height.get(), allocator, root, value_num); + + // return root + return root; + } + + rapidjson::Value monero_light_get_unspent_outs_request::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + + // create root + rapidjson::Value root = monero_light_wallet_request::to_rapidjson_val(allocator); + + // set string values + rapidjson::Value value_str(rapidjson::kStringType); + rapidjson::Value value_num(rapidjson::kNumberType); + + if (m_amount != boost::none) monero_utils::add_json_member("amount", std::to_string(m_amount.get()), allocator, root, value_str); + if (m_mixin != boost::none) monero_utils::add_json_member("mixin", m_mixin.get(), allocator, root, value_num); + if (m_use_dust != boost::none) monero_utils::add_json_member("use_dust", m_use_dust.get(), allocator, root); + if (m_dust_threshold != boost::none) monero_utils::add_json_member("dust_threshold", std::to_string(m_dust_threshold.get()), allocator, root, value_str); + + // return root + return root; + } + + rapidjson::Value monero_light_login_request::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + + // create root + rapidjson::Value root = monero_light_wallet_request::to_rapidjson_val(allocator); + + if (m_create_account != boost::none) monero_utils::add_json_member("create_account", m_create_account.get(), allocator, root); + if (m_generated_locally != boost::none) monero_utils::add_json_member("generated_locally", m_generated_locally.get(), allocator, root); + + // return root + return root; + } + + rapidjson::Value monero_light_submit_raw_tx_request::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set string values + rapidjson::Value value_str(rapidjson::kStringType); + if (m_tx != boost::none) monero_utils::add_json_member("tx", m_tx.get(), allocator, root, value_str); + + // return root + return root; + } + + rapidjson::Value monero_light_upsert_subaddrs_request::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + + // create root + rapidjson::Value root = monero_light_wallet_request::to_rapidjson_val(allocator); + + if (m_subaddrs != boost::none) root.AddMember("subaddrs", m_subaddrs.get().to_rapidjson_val(allocator), allocator); + if (m_get_all != boost::none) monero_utils::add_json_member("get_all", m_get_all.get(), allocator, root); + + // return root + return root; + } + + // ------------------------------- COPY UTILS ------------------------------- + + std::shared_ptr monero_light_spend::copy(const std::shared_ptr& src, const std::shared_ptr& tgt) const { + if (this != src.get()) throw std::runtime_error("this spend != src"); + // copy wallet extensions + tgt->m_amount = src->m_amount; + tgt->m_key_image = src->m_key_image; + tgt->m_tx_pub_key = src->m_tx_pub_key; + tgt->m_out_index = src->m_out_index; + tgt->m_mixin = src->m_mixin; + tgt->m_sender = src->m_sender; + + return tgt; + } + + std::shared_ptr monero_light_tx::copy(const std::shared_ptr& src, const std::shared_ptr& tgt, bool exclude_spend) const { + if (this != src.get()) throw std::runtime_error("this light_tx != src"); + + // copy wallet extensions + tgt->m_id = src->m_id; + tgt->m_hash = src->m_hash; + tgt->m_timestamp = src->m_timestamp; + tgt->m_total_received = src->m_total_received; + tgt->m_total_sent = src->m_total_sent; + tgt->m_fee = src->m_fee; + tgt->m_unlock_time = src->m_unlock_time; + tgt->m_height = src->m_height; + tgt->m_payment_id = src->m_payment_id; + tgt->m_coinbase = src->m_coinbase; + tgt->m_mempool = src->m_mempool; + tgt->m_mixin = src->m_mixin; + tgt->m_recipient = src->m_recipient; + tgt->m_spent_outputs.clear(); + + if (exclude_spend) { + return tgt; + } + + if (!src->m_spent_outputs.empty()) { + for (const monero_light_spend& spent_output : src->m_spent_outputs) { + std::shared_ptr spent_output_ptr = std::make_shared(spent_output); + std::shared_ptr spent_output_copy = spent_output_ptr->copy(spent_output_ptr, std::make_shared()); + tgt->m_spent_outputs.push_back(*spent_output_copy); + } + } + + return tgt; + } + + // ------------------------------- LWS CLIENT ------------------------------- + + void monero_light_client::disconnect() { + if (m_http_client->is_connected()) { + boost::lock_guard lock(m_mutex); + m_http_client->disconnect(); + m_connected = false; + } + } + + monero_light_client::monero_light_client(std::unique_ptr http_client_factory) { + if (http_client_factory != nullptr) m_http_client = http_client_factory->create(); + else { + auto factory = new net::http::client_factory(); + m_http_client = factory->create(); + } + } + + monero_light_client::~monero_light_client() { + MTRACE("~monero_light_client()"); + disconnect(); + } + + void monero_light_client::set_connection(const boost::optional& connection) { + std::string uri; + std::string username; + std::string password; + std::string proxy; + + if (connection != boost::none) { + if (connection->m_uri != boost::none) uri = connection->m_uri.get(); + if (connection->m_proxy_uri != boost::none) proxy = connection->m_proxy_uri.get(); + if (connection->m_username != boost::none) username = connection->m_username.get(); + if (connection->m_password != boost::none) password = connection->m_password.get(); + } + + if (username.empty() && !password.empty()) throw std::runtime_error("username cannot be empty because password is not empty"); + if (!username.empty() && password.empty()) throw std::runtime_error("password cannot be empty because username is not empty"); + + boost::lock_guard lock(m_mutex); + disconnect(); + + if(!m_http_client->set_proxy(proxy)) { + throw std::runtime_error("failed to set proxy address"); + } + + epee::net_utils::http::login creds; + creds.username = username; + creds.password = password; + + if (!m_http_client->set_server(uri, creds)) { + throw std::runtime_error("Could not set monero-lws: " + uri); + } + + m_connected = false; + try { + if (m_http_client->connect(std::chrono::seconds(15))) { + get_version(); + m_connected = true; + } else if (!uri.empty()) MWARNING("Could not connect to monero-lws at " << uri); + } catch (const std::exception& ex) { MERROR("Could not connect to monero-lws at " << uri << ": " << ex.what()); } + + m_credentials = creds; + m_server = uri; + m_proxy = proxy; + } + + void monero_light_client::set_connection(const std::string& uri, const std::string& username, const std::string& password, const std::string& proxy) { + monero_rpc_connection connection; + connection.m_uri = uri; + connection.m_username = username; + connection.m_password = password; + connection.m_proxy_uri = proxy; + set_connection(connection); + } + + boost::optional monero_light_client::get_connection() const { + if (m_server.empty()) return boost::none; + + monero_rpc_connection connection; + connection.m_uri = m_server; + connection.m_proxy_uri = m_proxy; + if (!m_credentials.username.empty() && !m_credentials.password.empty()) { + connection.m_username = m_credentials.username; + epee::wipeable_string wipeablePassword = m_credentials.password; + connection.m_password = std::string(wipeablePassword.data(), wipeablePassword.size()); + } + + return connection; + } + + monero_light_get_address_info_response monero_light_client::get_address_info(const std::string &address, const std::string &view_key) const { + monero_light_wallet_request req; + monero_light_get_address_info_response res; + + req.m_address = address; + req.m_view_key = view_key; + + int response_code = invoke_post("/get_address_info", req, res); + if (response_code != 200) { + if (response_code == 403) throw std::runtime_error("Unauthorized"); + throw std::runtime_error("Could not get address info"); + } + + return res; + } + + monero_light_get_address_txs_response monero_light_client::get_address_txs(const std::string &address, const std::string &view_key) const { + monero_light_wallet_request req; + monero_light_get_address_txs_response res; + + req.m_address = address; + req.m_view_key = view_key; + + int response_code = invoke_post("/get_address_txs", req, res); + if (response_code != 200) { + if (response_code == 403) throw std::runtime_error("Unauthorized"); + throw std::runtime_error("Could not get address txs"); + } + + return res; + } + + monero_light_get_unspent_outs_response monero_light_client::get_unspent_outs(const std::string &address, const std::string &view_key, uint64_t amount, uint32_t mixin, bool use_dust, uint64_t dust_threshold) const { + monero_light_get_unspent_outs_request req; + monero_light_get_unspent_outs_response res; + + req.m_address = address; + req.m_view_key = view_key; + req.m_amount = amount; + req.m_mixin = mixin; + req.m_use_dust = use_dust; + req.m_dust_threshold = dust_threshold; + + int response_code = invoke_post("/get_unspent_outs", req, res); + if (response_code != 200) { + if (response_code == 403) throw std::runtime_error("Unauthorized"); + throw std::runtime_error("Could not get unspent outputs"); + } + + return res; + } + + monero_light_get_random_outs_response monero_light_client::get_random_outs(uint32_t count, const std::vector &amounts) const { + monero_light_get_random_outs_request req; + monero_light_get_random_outs_response res; + + req.m_count = count; + req.m_amounts = amounts; + + int response_code = invoke_post("/get_random_outs", req, res); + if (response_code != 200) { + throw std::runtime_error("Could not get random outputs"); + } + + return res; + } + + monero_light_get_subaddrs_response monero_light_client::get_subaddrs(const std::string &address, const std::string &view_key) const { + monero_light_wallet_request req; + monero_light_get_subaddrs_response res; + + req.m_address = address; + req.m_view_key = view_key; + + int response_code = invoke_post("/get_subaddrs", req, res); + if (response_code != 200) { + if (response_code == 403) throw std::runtime_error("Unauthorized"); + throw std::runtime_error("Could not get subaddresses"); + } + + return res; + } + + monero_light_upsert_subaddrs_response monero_light_client::upsert_subaddrs(const std::string &address, const std::string &view_key, const monero_light_subaddrs& subaddrs, bool get_all) const { + monero_light_upsert_subaddrs_request req; + monero_light_upsert_subaddrs_response res; + + req.m_address = address; + req.m_view_key = view_key; + req.m_subaddrs = subaddrs; + req.m_get_all = get_all; + + int response_code = invoke_post("/upsert_subaddrs", req, res); + if (response_code != 200) { + if (response_code == 409) throw std::runtime_error("Max subaddresses exceeded"); + if (response_code == 403) throw std::runtime_error("Unauthorized"); + throw std::runtime_error("Could not upsert subaddresses"); + } + + return res; + } + + monero_light_login_response monero_light_client::login(const std::string &address, const std::string &view_key, bool create_account, bool generated_locally) const { + monero_light_login_request req; + monero_light_login_response res; + + req.m_address = address; + req.m_view_key = view_key; + req.m_create_account = create_account; + req.m_generated_locally = generated_locally; + + int response_code = invoke_post("/login", req, res); + if (response_code != 200) { + if (response_code == 501) throw std::runtime_error("Account creation not allowed"); + if (response_code == 403) throw std::runtime_error("Unauthorized"); + throw std::runtime_error("Could not login on account: " + address); + } + + return res; + } + + monero_light_import_wallet_response monero_light_client::import_request(const std::string &address, const std::string &view_key, uint64_t from_height) const { + monero_light_import_wallet_request req; + monero_light_import_wallet_response res; + req.m_address = address; + req.m_view_key = view_key; + req.m_from_height = from_height; + + int response_code = invoke_post("/import_wallet_request", req, res); + if (response_code != 200) { + if (response_code == 403) throw std::runtime_error("Unauthorized"); + throw std::runtime_error("Could not import wallet"); + } + + return res; + } + + monero_light_submit_raw_tx_response monero_light_client::submit_raw_tx(const std::string& tx) const { + monero_light_submit_raw_tx_response res; + monero_light_submit_raw_tx_request req; + req.m_tx = tx; + + int response_code = invoke_post("/submit_raw_tx", req, res); + if (response_code != 200) { + throw std::runtime_error("Could not relay tx: " + tx); + } + + return res; + } + + monero_light_version monero_light_client::get_version() const { + monero_light_version res; + + int response_code = invoke_post("/get_version", monero_light_wallet_request{}, res); + if (response_code != 200) { + throw std::runtime_error("Could not get lws version"); + } + + return res; + } + + // ------------------------------- OUTPUT CONTAINER UTILS ------------------------------- + + std::vector monero_light_output_store::get_indexes(const std::vector &outputs) const { + std::vector indexes; + + for (const auto &output : outputs) { + std::string public_key = output.m_public_key.get(); + auto it = m_index.find(public_key); + + if (it == m_index.end()) throw std::runtime_error("output doesn't belong to the wallet"); + + indexes.push_back(it->second); + } + + return indexes; + } + + std::vector monero_light_output_store::get(uint32_t account_idx) const { + auto all = get_spent(account_idx); + auto unspent = get_unspent(account_idx); + all.insert(all.end(), unspent.begin(), unspent.end()); + return all; + } + + std::vector monero_light_output_store::get(uint32_t account_idx, uint32_t subaddress_idx) const { + auto all = get_spent(account_idx, subaddress_idx); + auto unspent = get_unspent(account_idx, subaddress_idx); + all.insert(all.end(), unspent.begin(), unspent.end()); + return all; + } + + std::vector monero_light_output_store::get_unspent(uint32_t account_idx, uint32_t subaddress_idx) const { + boost::lock_guard lock(m_mutex); + auto it1 = m_unspent.find(account_idx); + if (it1 == m_unspent.end()) { + // account not found + std::vector empty_result; + m_unspent[account_idx][subaddress_idx] = empty_result; + return empty_result; + } + else { + // account found + auto& subaddresses_map = it1->second; + auto it2 = subaddresses_map.find(subaddress_idx); + + if (it2 == subaddresses_map.end()) { + // subaddress not found + std::vector empty_result; + m_unspent[account_idx][subaddress_idx] = empty_result; + return empty_result; + } + + // subaddress found + return it2->second; + } + } + + std::vector monero_light_output_store::get_spent(uint32_t account_idx, uint32_t subaddress_idx) const { + boost::lock_guard lock(m_mutex); + auto it1 = m_spent.find(account_idx); + if (it1 == m_spent.end()) { + // account not found + std::vector empty_result; + m_spent[account_idx][subaddress_idx] = empty_result; + return empty_result; + } + else { + // account found + auto& subaddresses_map = it1->second; + auto it2 = subaddresses_map.find(subaddress_idx); + + if (it2 == subaddresses_map.end()) { + // subaddress not found + std::vector empty_result; + m_spent[account_idx][subaddress_idx] = empty_result; + return empty_result; + } + + // subaddress found + return it2->second; + } + } + + std::vector monero_light_output_store::get_spent(uint32_t account_idx) const { + boost::lock_guard lock(m_mutex); + auto it1 = m_spent.find(account_idx); + if (it1 == m_spent.end()) { + // account not found + std::vector empty_result; + return empty_result; + } + else { + // account found + std::vector result; + for (const auto &kv : it1->second) { + result.insert(result.end(), kv.second.begin(), kv.second.end()); + } + + return result; + } + } + + std::vector monero_light_output_store::get_unspent(uint32_t account_idx) const { + boost::lock_guard lock(m_mutex); + auto it1 = m_unspent.find(account_idx); + if (it1 == m_unspent.end()) { + // account not found + std::vector empty_result; + return empty_result; + } + + // account found + std::vector result; + for (const auto &kv : it1->second) { + result.insert(result.end(), kv.second.begin(), kv.second.end()); + } + + return result; + } + + std::vector monero_light_output_store::get_spendable(uint32_t account_idx, const std::vector &subaddresses_indices, const monero_light_tx_store& tx_store, uint64_t height) const { + boost::lock_guard lock(m_mutex); + auto it = m_unspent.find(account_idx); + if (it == m_unspent.end()) { + // account not found + std::vector empty_result; + return empty_result; + } + + std::vector spendable; + bool by_subaddress_idx = !subaddresses_indices.empty(); + for(const auto& kv : it->second) { + uint32_t subaddress_index = kv.first; + if (by_subaddress_idx) { + bool found = std::find(subaddresses_indices.begin(), subaddresses_indices.end(), subaddress_index) != subaddresses_indices.end(); + if (!found) continue; + } + + for (const auto& output : kv.second) { + if (is_frozen(output) || tx_store.is_locked(output, height)) continue; + spendable.push_back(output); + } + } + + return spendable; + } + + std::vector monero_light_output_store::get_by_tx_hash(const std::string& tx_hash, bool filter_spent) const { + boost::lock_guard lock(m_mutex); + auto it = m_tx_hash_index.find(tx_hash); + if (it == m_tx_hash_index.end()) return std::vector(); + if (!filter_spent) { + return it->second; + } + std::vector outputs; + for (const auto &output : it->second) { + if (!output.is_spent()) outputs.push_back(output); + } + + return outputs; + } + + std::string monero_light_output_store::get_tx_prefix_hash(const std::string& tx_hash) const { + boost::lock_guard lock(m_mutex); + auto outputs = get_by_tx_hash(tx_hash); + if (outputs.empty()) return std::string(""); + auto& output = outputs[0]; + return output.m_tx_prefix_hash.get(); + } + + void monero_light_output_store::set(const monero_light_tx_store& tx_store, const monero_light_get_unspent_outs_response& response) { + clear(); + if (response.m_outputs.empty()) return; + const std::vector& outputs = response.m_outputs; + std::vector spent; + std::vector unspent; + size_t index = 0; + boost::lock_guard lock(m_mutex); + + for (const auto &output : outputs) { + if (output.is_spent() || (output.key_image_is_known() && tx_store.is_key_image_in_pool(output.m_key_image.get()))) spent.push_back(output); + else unspent.push_back(output); + m_index[output.m_public_key.get()] = index; + + if (output.key_image_is_known()) { + std::string output_key_image = output.m_key_image.get(); + m_key_image_index[output_key_image] = index; + } + + std::string tx_hash = output.m_tx_hash.get(); + + auto tx_hash_it = m_tx_hash_index.find(tx_hash); + + if (tx_hash_it == m_tx_hash_index.end()) { + m_tx_hash_index[tx_hash] = std::vector(); + tx_hash_it = m_tx_hash_index.find(tx_hash); + } + + tx_hash_it->second.push_back(output); + index++; + } + + set(spent, unspent); + m_all = outputs; + } + + void monero_light_output_store::set(const std::vector& spent, const std::vector& unspent) { + set_spent(spent); + set_unspent(unspent); + } + + void monero_light_output_store::set_spent(const std::vector& outputs) { + boost::lock_guard lock(m_mutex); + for (const auto &output : outputs) { + const auto& address_meta = output.m_recipient; + uint32_t account_idx = address_meta.m_maj_i; + uint32_t subaddress_idx = address_meta.m_min_i; + + auto account_it = m_spent.find(account_idx); + if (account_it == m_spent.end()) { + m_spent[account_idx][subaddress_idx] = std::vector(); + account_it = m_spent.find(account_idx); + } + + auto subaddress_it = account_it->second.find(subaddress_idx); + if (subaddress_it == account_it->second.end()) { + m_spent[account_idx][subaddress_idx] = std::vector(); + subaddress_it = account_it->second.find(subaddress_idx); + } + + subaddress_it->second.push_back(output); + } + m_num_spent = outputs.size(); + } + + void monero_light_output_store::set_key_image_spent(const std::string& key_image, bool spent) { + m_key_image_status_index[key_image] = spent; + } + + void monero_light_output_store::set_unspent(const std::vector& outputs) { + boost::lock_guard lock(m_mutex); + for (const auto &output : outputs) { + const auto& address_meta = output.m_recipient; + uint32_t account_idx = address_meta.m_maj_i; + uint32_t subaddress_idx = address_meta.m_min_i; + + auto account_it = m_unspent.find(account_idx); + if (account_it == m_unspent.end()) { + m_unspent[account_idx][subaddress_idx] = std::vector(); + account_it = m_unspent.find(account_idx); + } + + auto subaddress_it = account_it->second.find(subaddress_idx); + if (subaddress_it == account_it->second.end()) { + m_unspent[account_idx][subaddress_idx] = std::vector(); + subaddress_it = account_it->second.find(subaddress_idx); + } + + subaddress_it->second.push_back(output); + } + m_num_unspent = outputs.size(); + } + + bool monero_light_output_store::is_used(uint32_t account_idx, uint32_t subaddress_idx) const { + auto outputs = get(account_idx, subaddress_idx); + return !outputs.empty(); + } + + uint64_t monero_light_output_store::get_num_unspent(uint32_t account_idx, uint32_t subaddress_idx) const { + auto unspent = get_unspent(account_idx, subaddress_idx); + return unspent.size(); + } + + void monero_light_output_store::clear_balance() { + m_account_balance.clear(); + m_account_unlocked_balance.clear(); + m_subaddress_balance.clear(); + m_subaddress_unlocked_balance.clear(); + m_balance = 0; + m_unlocked_balance = 0; + } + + void monero_light_output_store::clear() { + boost::lock_guard lock(m_mutex); + m_index.clear(); + m_key_image_index.clear(); + m_tx_hash_index.clear(); + m_unspent.clear(); + m_spent.clear(); + m_all.clear(); + clear_balance(); + } + + void monero_light_output_store::clear_frozen() { + boost::lock_guard lock(m_mutex); + m_frozen_key_image_index.clear(); + } + + void monero_light_output_store::calculate_balance(const monero_light_tx_store& tx_store, uint64_t current_height) { + clear_balance(); + for (const auto &kv : m_unspent) { + uint32_t account_idx = kv.first; + uint64_t account_balance = 0; + uint64_t account_unlocked_balance = 0; + + for (const auto &kv2 : kv.second) { + uint32_t subaddress_idx = kv2.first; + uint64_t subaddress_balance = 0; + uint64_t subaddress_unlocked_balance = 0; + + for(const auto &output : kv2.second) { + if (output.key_image_is_known() && tx_store.is_key_image_in_pool(output.m_key_image.get())) continue; + bool is_locked = tx_store.is_locked(output, current_height); + uint64_t amount = output.m_amount.get(); + subaddress_balance += amount; + if (!is_locked) subaddress_unlocked_balance += amount; + } + + account_balance += subaddress_balance; + account_unlocked_balance += subaddress_unlocked_balance; + + m_subaddress_balance[account_idx][subaddress_idx] = subaddress_balance; + m_subaddress_unlocked_balance[account_idx][subaddress_idx] = subaddress_unlocked_balance; + } + + m_balance += account_balance; + m_unlocked_balance += account_unlocked_balance; + + m_account_balance[account_idx] = account_balance; + m_account_unlocked_balance[account_idx] = account_unlocked_balance; + } + + // consider also unconfirmed txs + for (const auto &kv : tx_store.get_unconfirmed_txs()) { + const auto &tx = kv.second; + + if (tx->m_is_relayed != true || tx->m_is_failed == true) continue; + + uint64_t change_amount = 0; + + if (tx->m_change_amount != boost::none) change_amount = tx->m_change_amount.get(); + + m_balance += change_amount; + m_account_balance[0] += change_amount; + m_subaddress_balance[0][0] += change_amount; + + for (const std::shared_ptr &out : tx->m_outputs) { + std::shared_ptr output = std::dynamic_pointer_cast(out); + if (output == nullptr) { + continue; + } + + if (output->m_account_index == boost::none) throw std::runtime_error("output account index is none"); + if (output->m_subaddress_index == boost::none) throw std::runtime_error("output subaddress index is none"); + if (output->m_amount == boost::none) throw std::runtime_error("output amount is none"); + + uint32_t account_idx = output->m_account_index.get(); + uint32_t subaddress_idx = output->m_subaddress_index.get(); + uint64_t output_amount = output->m_amount.get(); + + auto account_it = m_account_balance.find(account_idx); + if (account_it == m_account_balance.end()) { + m_account_balance[account_idx] = output_amount; + m_account_unlocked_balance[account_idx] = 0; + m_subaddress_balance[account_idx][subaddress_idx] = output_amount; + } + else { + m_account_balance[account_idx] += output_amount; + + auto subaddr_it = m_subaddress_balance[account_idx].find(subaddress_idx); + if (subaddr_it == m_subaddress_balance[account_idx].end()) { + m_subaddress_balance[account_idx][subaddress_idx] = output_amount; + } + else m_subaddress_balance[account_idx][subaddress_idx] += output_amount; + } + m_balance += output_amount; + } + } + } + + uint64_t monero_light_output_store::get_balance(uint32_t account_idx) const { + auto it = m_account_balance.find(account_idx); + if (it == m_account_balance.end()) return 0; + return it->second; + } + + uint64_t monero_light_output_store::get_balance(uint32_t account_idx, uint32_t subaddress_idx) const { + auto it = m_subaddress_balance.find(account_idx); + if (it == m_subaddress_balance.end()) return 0; + auto it2 = it->second.find(subaddress_idx); + if (it2 == it->second.end()) return 0; + return it2->second; + } + + uint64_t monero_light_output_store::get_unlocked_balance(uint32_t account_idx) const { + auto it = m_account_unlocked_balance.find(account_idx); + if (it == m_account_unlocked_balance.end()) return 0; + return it->second; + } + + uint64_t monero_light_output_store::get_unlocked_balance(uint32_t account_idx, uint32_t subaddress_idx) const { + auto it = m_subaddress_unlocked_balance.find(account_idx); + if (it == m_subaddress_unlocked_balance.end()) return 0; + auto it2 = it->second.find(subaddress_idx); + if (it2 == it->second.end()) return 0; + return it2->second; + } + + void validate_key_image(const std::string& key_image) { + crypto::key_image ki; + if (!epee::string_tools::hex_to_pod(key_image, ki)) throw std::runtime_error("failed to parse key image: " + key_image); + } + + void monero_light_output_store::freeze(const std::string& key_image) { + if (key_image.empty()) throw std::runtime_error("Must specify key image to freeze"); + validate_key_image(key_image); + auto key_it = m_key_image_index.find(key_image); + if (key_it == m_key_image_index.end()) throw std::runtime_error("Key image not found"); + size_t index = key_it->second; + m_frozen_key_image_index[index] = true; + } + + void monero_light_output_store::thaw(const std::string& key_image) { + if (key_image.empty()) throw std::runtime_error("Must specify key image to thaw"); + validate_key_image(key_image); + auto key_it = m_key_image_index.find(key_image); + if (key_it == m_key_image_index.end()) throw std::runtime_error("Key image not found"); + size_t index = key_it->second; + m_frozen_key_image_index[index] = false; + } + + bool monero_light_output_store::is_frozen(const std::string& key_image) const { + validate_key_image(key_image); + auto key_it = m_key_image_index.find(key_image); + if (key_it == m_key_image_index.end()) throw std::runtime_error("Key image not found"); + size_t index = key_it->second; + auto frozen_it = m_frozen_key_image_index.find(index); + if (frozen_it == m_frozen_key_image_index.end()) return false; + return frozen_it->second; + } + + bool monero_light_output_store::is_frozen(const monero_light_output& output) const { + return is_frozen(output.m_key_image == boost::none ? "" : output.m_key_image.get()); + } + + void monero_light_output_store::set_key_image(const std::string& key_image, size_t index) { + boost::lock_guard lock(m_mutex); + m_key_image_index[key_image] = index; + } + + wallet2_exported_outputs monero_light_output_store::export_outputs(const monero_light_tx_store& tx_store, monero_key_image_cache& key_image_cache, bool all, uint32_t start, uint32_t count) const { + std::vector outs; + + // invalid cases + if(count == 0) throw std::runtime_error("Nothing requested"); + if(!all && start > 0) throw std::runtime_error("Incremental mode is incompatible with non-zero start"); + + // valid cases: + // all: all outputs, subject to start/count + // !all: incremental, subject to count + // for convenience, start/count are allowed to go past the valid range, then nothing is returned + const auto &unspent_outs = m_all; + + size_t offset = 0; + if (!all) + while (offset < unspent_outs.size() && (unspent_outs[offset].key_image_is_known() && !key_image_cache.request(unspent_outs[offset].m_tx_pub_key.get(), unspent_outs[offset].m_index.get(), unspent_outs[offset].m_recipient.m_maj_i, unspent_outs[offset].m_recipient.m_min_i))) + ++offset; + else + offset = start; + + outs.reserve(unspent_outs.size() - offset); + for (size_t n = offset; n < unspent_outs.size() && n - offset < count; ++n) + { + const auto &out = unspent_outs[n]; + uint64_t out_amount = out.m_amount.get(); + auto internal_output_index = out.m_index.get(); + std::string tx_hash = out.m_tx_hash.get(); + + uint64_t unlock_time = tx_store.get_unlock_time(tx_hash); + + tools::wallet2::exported_transfer_details etd; + + crypto::public_key public_key; + crypto::public_key tx_pub_key; + + epee::string_tools::hex_to_pod(out.m_public_key.get(), public_key); + epee::string_tools::hex_to_pod(out.m_tx_pub_key.get(), tx_pub_key); + + cryptonote::transaction_prefix tx_prefix; + + add_tx_pub_key_to_extra(tx_prefix, tx_pub_key); + + cryptonote::tx_out txout; + txout.target = cryptonote::txout_to_key(public_key); + txout.amount = out_amount; + tx_prefix.vout.resize(internal_output_index + 1); + tx_prefix.vout[internal_output_index] = txout; + tx_prefix.unlock_time = unlock_time; + + etd.m_pubkey = public_key; + etd.m_tx_pubkey = tx_pub_key; // pk_index? + etd.m_internal_output_index = internal_output_index; + etd.m_global_output_index = out.m_global_index.get(); + etd.m_flags.flags = 0; + etd.m_flags.m_spent = out.is_spent(); + etd.m_flags.m_frozen = false; + etd.m_flags.m_rct = out.is_rct(); + etd.m_flags.m_key_image_known = out.key_image_is_known(); + etd.m_flags.m_key_image_request = false; //td.m_key_image_request; + etd.m_flags.m_key_image_partial = false; + etd.m_amount = out_amount; + etd.m_additional_tx_keys = get_additional_tx_pub_keys_from_extra(tx_prefix); + etd.m_subaddr_index_major = out.m_recipient.m_maj_i; + etd.m_subaddr_index_minor = out.m_recipient.m_min_i; + + outs.push_back(etd); + } + + return std::make_tuple(offset, unspent_outs.size(), outs); + } + + // ------------------------------- TX CONTAINER UTILS ------------------------------- + + monero_light_tx monero_light_tx_store::get(const std::string& hash) const { + boost::lock_guard lock(m_mutex); + auto it = m_txs.find(hash); + if (it == m_txs.end()) throw std::runtime_error("tx not found in store"); + return it->second; + } + + monero_light_tx monero_light_tx_store::get(const monero_light_output& output) const { + return get(output.m_tx_hash.get()); + } + + uint64_t monero_light_tx_store::get_unlock_time(const std::string& hash) const { + const auto &tx = get(hash); + return tx.m_unlock_time.get(); + } + + void monero_light_tx_store::set(const monero_light_get_address_txs_response& response, const monero_light_get_address_info_response& addr_info_response) { + clear(); + set(response.m_transactions); + + for (const auto &spend : addr_info_response.m_spent_outputs) { + if (spend.m_key_image != boost::none) { + m_spent_key_images[spend.m_key_image.get()] = true; + } + } + } + + void monero_light_tx_store::set(const monero_light_tx& tx) { + boost::lock_guard lock(m_mutex); + m_txs[tx.m_hash.get()] = tx; + } + + void monero_light_tx_store::add_key_images_to_pool(const std::shared_ptr& tx) { + if (tx->m_is_relayed != true) { + return; + } + boost::lock_guard lock(m_mutex); + std::string tx_hash = tx->m_hash.get(); + m_pool_key_images.erase(tx_hash); + std::vector key_images; + + for(const auto &in : tx->m_inputs) { + std::shared_ptr input = std::static_pointer_cast(in); + + if (input == nullptr) { + throw std::runtime_error("Expected input monero_output_wallet"); + } + + if (input->m_key_image == boost::none || input->m_key_image.get()->m_hex == boost::none || input->m_key_image.get()->m_hex->empty()) throw std::runtime_error("Input key image is none"); + std::string key_image = input->m_key_image.get()->m_hex.get(); + key_images.push_back(key_image); + } + + if (key_images.size() > 0) m_pool_key_images[tx_hash] = key_images; + } + + void monero_light_tx_store::set_unconfirmed(const std::shared_ptr& tx) { + boost::lock_guard lock(m_mutex); + if (tx->m_hash == boost::none) throw std::runtime_error("Cannot set none unconfirmed tx hash"); + std::string tx_hash = tx->m_hash.get(); + if (tx_hash.empty()) throw std::runtime_error("Cannot set empty unconfirmed tx hash"); + m_unconfirmed_txs[tx_hash] = tx; + add_key_images_to_pool(tx); + } + + void monero_light_tx_store::remove_unconfirmed(const std::string& hash) { + boost::lock_guard lock(m_mutex); + m_unconfirmed_txs.erase(hash); + m_pool_key_images.erase(hash); + } + + void monero_light_tx_store::set_relayed(const std::string& hash) { + boost::lock_guard lock(m_mutex); + auto it = m_unconfirmed_txs.find(hash); + if (it == m_unconfirmed_txs.end()) { + return; + } + it->second->m_in_tx_pool = true; + it->second->m_is_locked = true; + it->second->m_is_relayed = true; + it->second->m_relay = true; + it->second->m_last_relayed_timestamp = static_cast(time(NULL)); + it->second->m_is_failed = false; + it->second->m_is_double_spend_seen = false; + add_key_images_to_pool(it->second); + } + + void monero_light_tx_store::set(const std::vector& txs, bool clear_txs) { + if (clear_txs) clear(); + boost::lock_guard lock(m_mutex); + for(const auto &tx : txs) { + std::string tx_hash = tx.m_hash.get(); + bool confirmed = !tx.m_mempool.get(); + bool is_miner_tx = tx.m_coinbase.get(); + m_txs[tx_hash] = tx; + if (confirmed) remove_unconfirmed(tx_hash); + if (is_miner_tx) { + uint64_t amount = tx.m_total_received.get(); + if (m_block_reward == 0 || amount < m_block_reward) m_block_reward = amount; + } + } + + if (m_block_reward == 0) m_block_reward = monero_utils::TAIL_EMISSION_REWARD; + } + + uint64_t monero_light_tx_store::calculate_num_blocks_to_unlock(const std::string& hash, uint64_t current_height) const { + monero_light_tx tx = get(hash); + uint64_t tx_height = tx.m_mempool.get() ? current_height : tx.m_height.get(); + uint64_t unlock_time = tx.m_unlock_time.get(); + uint64_t default_spendable_age = tx_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE; + uint64_t confirmations_needed = default_spendable_age > current_height ? default_spendable_age - current_height : 0; + uint64_t num_blocks_to_unlock = unlock_time <= current_height ? 0 : unlock_time - current_height; + return num_blocks_to_unlock > confirmations_needed ? num_blocks_to_unlock : confirmations_needed; + } + + uint64_t monero_light_tx_store::calculate_num_blocks_to_unlock(const std::vector& hashes, uint64_t current_height) const { + uint64_t num_blocks = 0; + for(const std::string& hash : hashes) { + uint64_t blocks = calculate_num_blocks_to_unlock(hash, current_height); + if (blocks > num_blocks) num_blocks = blocks; + } + return num_blocks; + } + + uint64_t monero_light_tx_store::calculate_num_blocks_to_unlock(const std::vector& outputs, uint64_t current_height) const { + std::vector hashes; + + for(const auto &output : outputs) { + if (output.m_tx_hash == boost::none) continue; + hashes.push_back(output.m_tx_hash.get()); + } + + return calculate_num_blocks_to_unlock(hashes, current_height); + } + + uint64_t monero_light_tx_store::calculate_num_blocks_to_unlock(const monero_light_output& output, uint64_t current_height) const { + return calculate_num_blocks_to_unlock(output.m_tx_hash.get(), current_height); + } + + bool monero_light_tx_store::is_locked(const std::string& hash, uint64_t current_height) const { + return calculate_num_blocks_to_unlock(hash, current_height) > 0; + } + + bool monero_light_tx_store::is_locked(const monero_light_output& output, uint64_t current_height) const { + return is_locked(output.m_tx_hash.get(), current_height); + } + + bool monero_light_tx_store::is_confirmed(const std::string& hash) const { + monero_light_tx tx = get(hash); + return tx.m_mempool == false; + } + + bool monero_light_tx_store::is_key_image_in_pool(const std::string& key_image) const { + boost::lock_guard lock(m_mutex); + for (const auto &kv : m_pool_key_images) { + for(const std::string &pool_key_image : kv.second) { + if (key_image == pool_key_image) return true; + } + } + return false; + } + + bool monero_light_tx_store::is_key_image_spent(const std::string& key_image) const { + if (is_key_image_in_pool(key_image)) return true; + auto it = m_spent_key_images.find(key_image); + if (it == m_spent_key_images.end()) return false; + return it->second; + } + + bool monero_light_tx_store::is_key_image_spent(const crypto::key_image& key_image) const { + std::string key_image_str = epee::string_tools::pod_to_hex(key_image); + return is_key_image_spent(key_image_str); + } + + bool monero_light_tx_store::is_key_image_spent(const std::shared_ptr& key_image) const { + if (key_image == nullptr) throw std::runtime_error("key image is null"); + return is_key_image_spent(*key_image); + } + + bool monero_light_tx_store::is_key_image_spent(const monero_key_image& key_image) const { + if (key_image.m_hex == boost::none) return false; + return is_key_image_spent(key_image.m_hex.get()); + } + + void monero_light_tx_store::clear_unconfirmed() { + boost::lock_guard lock(m_mutex); + m_unconfirmed_txs.clear(); + m_pool_key_images.clear(); + } + + void monero_light_tx_store::clear() { + boost::lock_guard lock(m_mutex); + m_txs.clear(); + m_spent_key_images.clear(); + m_block_reward = 0; + } + +} \ No newline at end of file diff --git a/src/wallet/monero_wallet_light_model.h b/src/wallet/monero_wallet_light_model.h new file mode 100644 index 00000000..97049eb5 --- /dev/null +++ b/src/wallet/monero_wallet_light_model.h @@ -0,0 +1,570 @@ +/** + * Copyright (c) woodser + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Parts of this file are originally copyright (c) 2014-2019, The Monero Project + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * All rights reserved. + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be + * used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers + */ + +#pragma once + +#include "wallet/monero_wallet_model.h" +#include "wallet/monero_wallet_keys.h" +#include "cryptonote_basic/cryptonote_basic.h" +#include + +using namespace monero; + +namespace monero { + + // ------------------------------- LIGHT WALLET DATA STRUCTURES ------------------------------- + + struct monero_light_version { + boost::optional m_server_type; + boost::optional m_server_version; + boost::optional m_last_git_commit_hash; + boost::optional m_last_git_commit_date; + boost::optional m_git_branch_name; + boost::optional m_monero_version_full; + boost::optional m_blockchain_height; + boost::optional m_api; + boost::optional m_max_subaddresses; + boost::optional m_network_type; + boost::optional m_testnet; + + static std::shared_ptr deserialize(const std::string& version_json); + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& version); + }; + + struct monero_light_address_meta { + uint32_t m_maj_i = 0; + uint32_t m_min_i = 0; + + static std::shared_ptr deserialize(const std::string& config_json); + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& address_meta); + }; + + struct monero_light_output { + boost::optional m_tx_id; + boost::optional m_amount; + boost::optional m_index; + boost::optional m_global_index; + boost::optional m_rct; + boost::optional m_tx_hash; + boost::optional m_tx_prefix_hash; + boost::optional m_public_key; + boost::optional m_tx_pub_key; + std::vector m_spend_key_images; + boost::optional m_timestamp; + boost::optional m_height; + monero_light_address_meta m_recipient; + + // custom members + boost::optional m_key_image; + boost::optional m_frozen; + + bool key_image_is_known() const { return m_key_image != boost::none && !m_key_image->empty(); }; + + bool is_rct() const { return m_rct != boost::none && !m_rct->empty(); }; + bool is_mined() const { return is_rct() && m_rct.get() == "coinbase"; }; + + bool is_spent() const { + if (!key_image_is_known() || m_spend_key_images.empty()) return false; + for(const auto& spend_key_image : m_spend_key_images) { + if (spend_key_image == m_key_image.get()) return true; + } + return false; + }; + + static std::shared_ptr deserialize(const std::string& config_json); + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& output); + }; + + struct monero_light_spend { + boost::optional m_amount; + boost::optional m_key_image; + boost::optional m_tx_pub_key; + boost::optional m_out_index; + boost::optional m_mixin; + monero_light_address_meta m_sender; + + static std::shared_ptr deserialize(const std::string& config_json); + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& spend); + std::shared_ptr copy(const std::shared_ptr& src, const std::shared_ptr& tgt) const; + }; + + struct monero_light_tx { + boost::optional m_id; + boost::optional m_hash; + boost::optional m_timestamp; + boost::optional m_total_received; + boost::optional m_total_sent; + boost::optional m_fee; + boost::optional m_unlock_time; + boost::optional m_height; + std::vector m_spent_outputs; + boost::optional m_payment_id; + boost::optional m_coinbase; + boost::optional m_mempool; + boost::optional m_mixin; + monero_light_address_meta m_recipient; + + static std::shared_ptr deserialize(const std::string& config_json); + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& transaction); + std::shared_ptr copy(const std::shared_ptr& src, const std::shared_ptr& tgt, bool exclude_spend = false) const; + }; + + struct monero_light_random_outputs { + boost::optional m_amount; + std::vector m_outputs; + + static std::shared_ptr deserialize(const std::string& config_json); + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& random_outputs); + }; + + typedef std::unordered_map> monero_light_spendable_random_outputs; + + struct tied_spendable_to_random_outs { + std::vector m_mix_outs; + monero_light_spendable_random_outputs m_prior_attempt_unspent_outs_to_mix_outs_new; + }; + + struct monero_light_get_random_outs_params { + uint32_t m_mixin; + std::vector m_using_outs; + uint64_t m_using_fee; + uint64_t m_final_total_wo_fee; + uint64_t m_change_amount; + }; + + // ------------------------------- REQUEST/RESPONSE DATA STRUCTURES ------------------------------- + + struct monero_light_wallet_request : public serializable_struct { + boost::optional m_address; + boost::optional m_view_key; + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const; + }; + + struct monero_light_get_address_info_response { + boost::optional m_locked_funds; + boost::optional m_total_received; + boost::optional m_total_sent; + boost::optional m_scanned_height; + boost::optional m_scanned_block_height; + boost::optional m_start_height; + boost::optional m_transaction_height; + boost::optional m_blockchain_height; + std::vector m_spent_outputs; + + static std::shared_ptr deserialize(const std::string& config_json); + }; + + struct monero_light_get_address_txs_response { + boost::optional m_total_received; + boost::optional m_scanned_height; + boost::optional m_scanned_block_height; + boost::optional m_start_height; + boost::optional m_blockchain_height; + std::vector m_transactions; + + static std::shared_ptr deserialize(const std::string& config_json); + }; + + struct monero_light_get_random_outs_request : public serializable_struct { + boost::optional m_count; + std::vector m_amounts; + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const; + }; + + struct monero_light_get_random_outs_response { + std::vector m_amount_outs; + + static std::shared_ptr deserialize(const std::string& config_json); + }; + + struct monero_light_get_unspent_outs_request : public monero_light_wallet_request { + boost::optional m_amount; + boost::optional m_mixin; + boost::optional m_use_dust; + boost::optional m_dust_threshold; + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const; + }; + + struct monero_light_get_unspent_outs_response { + boost::optional m_per_byte_fee; + boost::optional m_fee_mask; + boost::optional m_amount; + std::vector m_outputs; + + static std::shared_ptr deserialize(const std::string& config_json); + }; + + struct monero_light_import_wallet_request : public monero_light_wallet_request { + boost::optional m_from_height; + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const; + }; + + struct monero_light_import_wallet_response { + boost::optional m_payment_address; + boost::optional m_payment_id; + boost::optional m_import_fee; + boost::optional m_new_request; + boost::optional m_request_fullfilled; + boost::optional m_status; + + static std::shared_ptr deserialize(const std::string& config_json); + }; + + struct monero_light_login_request : public monero_light_wallet_request { + boost::optional m_create_account; + boost::optional m_generated_locally; + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const; + }; + + struct monero_light_login_response { + boost::optional m_new_address; + boost::optional m_generated_locally; + boost::optional m_start_height; + + static std::shared_ptr deserialize(const std::string& config_json); + }; + + struct monero_light_submit_raw_tx_request : public serializable_struct { + boost::optional m_tx; + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const; + }; + + struct monero_light_submit_raw_tx_response { + boost::optional m_status; + + static std::shared_ptr deserialize(const std::string& config_json); + }; + + class monero_light_index_range : public std::vector { + public: + monero_light_index_range() { + std::vector(); + }; + + monero_light_index_range(const uint32_t min_i, const uint32_t maj_i) { + push_back(min_i); + push_back(maj_i); + }; + + bool in_range(uint32_t subaddress_idx) const { + if (empty() || size() != 2) return false; + return at(0) <= subaddress_idx && subaddress_idx <= at(1); + }; + + std::vector to_subaddress_indices() const { + std::vector indices; + + if (size() != 2) { + return indices; + } + + uint32_t min_i = at(0); + uint32_t maj_i = at(1); + + for(uint32_t i = min_i; i <= maj_i; i++) { + indices.push_back(i); + } + + return indices; + } + + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& index_range); + }; + + class monero_light_subaddrs : public std::map>, public serializable_struct { + public: + + bool contains(const uint32_t account_index) const { + auto it = find(account_index); + return it != end(); + } + + bool contains(const uint32_t account_index, const uint32_t subaddress_index) const { + auto it = find(account_index); + if (it == end()) return false; + for(const auto& index_range : it->second) { + if (index_range.in_range(subaddress_index)) return true; + } + return false; + } + + bool is_upsert(const uint32_t account_idx) const { + if (account_idx == 0) return true; + auto it = find(account_idx); + return it != end(); + } + + std::vector get_subaddresses_indices(const uint32_t account_idx) const { + std::vector subaddress_idxs; + auto it = find(account_idx); + if (it != end()) { + for (const auto& index_range : it->second) { + const auto& idxs = index_range.to_subaddress_indices(); + subaddress_idxs.insert(subaddress_idxs.begin(), idxs.begin(), idxs.end()); + } + } + return subaddress_idxs; + } + + uint32_t get_last_account_index() const { + uint32_t last_account_idx = 0; + for(const auto &kv : *this) { + if (kv.first > last_account_idx) last_account_idx = kv.first; + } + return last_account_idx; + } + + uint32_t get_last_subaddress_index(const uint32_t account_idx) const { + uint32_t last_subaddress_idx = 0; + auto it = find(account_idx); + if (it == end()) throw std::runtime_error("account not found"); + for(const auto& index_range : it->second) { + last_subaddress_idx = index_range.at(1); + } + return last_subaddress_idx; + } + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const; + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& subaddrs); + }; + + struct monero_light_upsert_subaddrs_request : public monero_light_wallet_request { + boost::optional m_subaddrs; + boost::optional m_get_all; + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const; + }; + + struct monero_light_upsert_subaddrs_response { + boost::optional m_new_subaddrs; + boost::optional m_all_subaddrs; + + static std::shared_ptr deserialize(const std::string& config_json); + }; + + struct monero_light_get_subaddrs_response { + boost::optional m_all_subaddrs; + + static std::shared_ptr deserialize(const std::string& config_json); + }; + + // ------------------------------- UTILS ------------------------------- + + class monero_light_client { + public: + + monero_light_client(std::unique_ptr http_client_factory = nullptr); + ~monero_light_client(); + bool is_connected() const { return m_connected; }; + void disconnect(); + void set_connection(const boost::optional& connection); + void set_connection(const std::string& uri, const std::string& username = "", const std::string& password = "", const std::string& proxy = ""); + boost::optional get_connection() const; + + monero_light_get_address_info_response get_address_info(const std::string &address, const std::string &view_key) const; + monero_light_get_address_txs_response get_address_txs(const std::string &address, const std::string &view_key) const; + monero_light_get_unspent_outs_response get_unspent_outs(const std::string &address, const std::string &view_key, uint64_t amount, uint32_t mixin, bool use_dust = true, const uint64_t dust_threshold = 0) const; + monero_light_get_random_outs_response get_random_outs(uint32_t count, const std::vector &amounts) const; + monero_light_get_subaddrs_response get_subaddrs(const std::string &address, const std::string &view_key) const; + monero_light_upsert_subaddrs_response upsert_subaddrs(const std::string &address, const std::string &view_key, const monero_light_subaddrs &subaddrs, bool get_all = true) const; + monero_light_login_response login(const std::string &address, const std::string &view_key, bool create_account = true, bool generated_locally = true) const; + monero_light_import_wallet_response import_request(const std::string &address, const std::string &view_key, uint64_t from_height) const; + monero_light_submit_raw_tx_response submit_raw_tx(const std::string& tx) const; + monero_light_version get_version() const; + + protected: + mutable boost::recursive_mutex m_mutex; + std::string m_server; + std::string m_proxy; + epee::net_utils::http::login m_credentials; + std::unique_ptr m_http_client; + bool m_connected = false; + + template + inline int invoke_post(const boost::string_ref uri, const t_request& request, t_response& res, std::chrono::milliseconds timeout = std::chrono::seconds(15)) const { + if (!m_http_client) throw std::runtime_error("http client not set"); + + rapidjson::Document document(rapidjson::Type::kObjectType); + rapidjson::Value req = request.to_rapidjson_val(document.GetAllocator()); + rapidjson::StringBuffer sb; + rapidjson::Writer writer(sb); + req.Accept(writer); + std::string body = sb.GetString(); + + std::shared_ptr _res = std::make_shared(); + const epee::net_utils::http::http_response_info* response = _res.get(); + boost::lock_guard lock(m_mutex); + + if (!m_http_client->invoke_post(uri, body, timeout, &response)) { + throw std::runtime_error("Network error"); + } + + int status_code = response->m_response_code; + + if (status_code == 200) { + res = *t_response::deserialize(response->m_body); + } + + return status_code; + } + }; + + class monero_light_tx_store { + public: + monero_light_tx get(const std::string& hash) const; + monero_light_tx get(const monero_light_output& output) const; + uint64_t get_unlock_time(const std::string& hash) const; + void set(const monero_light_get_address_txs_response& response, const monero_light_get_address_info_response& addr_info_response); + void set(const monero_light_tx& tx); + void set_unconfirmed(const std::shared_ptr& tx); + void remove_unconfirmed(const std::string& hash); + void set_relayed(const std::string& hash); + serializable_unordered_map> get_unconfirmed_txs() const { return m_unconfirmed_txs; }; + bool is_key_image_in_pool(const std::string& key_image) const; + bool is_key_image_spent(const std::string& key_image) const; + bool is_key_image_spent(const crypto::key_image& key_image) const; + bool is_key_image_spent(const std::shared_ptr& key_image) const; + bool is_key_image_spent(const monero_key_image& key_image) const; + void set(const std::vector& txs, bool clear_txs = false); + uint64_t calculate_num_blocks_to_unlock(const monero_light_output& output, uint64_t current_height) const; + uint64_t calculate_num_blocks_to_unlock(const std::vector& outputs, uint64_t current_height) const; + uint64_t calculate_num_blocks_to_unlock(const std::vector& hashes, uint64_t current_height) const; + uint64_t calculate_num_blocks_to_unlock(const std::string& hash, uint64_t current_height) const; + bool is_locked(const std::string& hash, uint64_t current_height) const; + bool is_locked(const monero_light_output& output, uint64_t current_height) const; + bool is_confirmed(const std::string& hash) const; + void clear(); + void clear_unconfirmed(); + uint64_t get_last_block_reward() const { return m_block_reward > 1 ? m_block_reward - 2 : m_block_reward; } // TODO why wallet full gives to 2 piconero less ? + private: + mutable boost::recursive_mutex m_mutex; + serializable_unordered_map m_txs; + serializable_unordered_map> m_unconfirmed_txs; + serializable_unordered_map m_spent_key_images; + serializable_unordered_map> m_pool_key_images; + uint64_t m_block_reward = 0; + + void add_key_images_to_pool(const std::shared_ptr& tx); + }; + + class monero_light_output_store { + public: + std::vector m_all; + + std::vector get(uint32_t account_idx) const; + std::vector get(uint32_t account_idx, uint32_t subaddress_idx) const; + std::vector get_spent(uint32_t account_idx) const; + std::vector get_spent(uint32_t account_idx, uint32_t subaddress_idx) const; + std::vector get_unspent(uint32_t account_idx) const; + std::vector get_unspent(uint32_t account_idx, uint32_t subaddress_idx) const; + std::vector get_spendable(const uint32_t account_idx, const std::vector &subaddresses_indices, const monero_light_tx_store& tx_store, uint64_t height) const; + std::vector get_by_tx_hash(const std::string& tx_hash, bool filter_spent = false) const; + std::string get_tx_prefix_hash(const std::string& tx_hash) const; + void set(const monero_light_tx_store& tx_store, const monero_light_get_unspent_outs_response& response); + void set(const std::vector& spent, const std::vector& unspent); + void set_spent(const std::vector& outputs); + void set_unspent(const std::vector& outputs); + void set_key_image(const std::string& key_image, size_t index); + bool is_used(uint32_t account_idx, uint32_t subaddress_idx) const; + size_t get_num() const { return m_num_spent + m_num_unspent; } + size_t get_num_spent() const { return m_num_spent; } + size_t get_num_unspent() const { return m_num_unspent; } + uint64_t get_num_unspent(uint32_t account_idx, uint32_t subaddress_idx) const; + std::vector get_indexes(const std::vector& outputs) const; + void calculate_balance(const monero_light_tx_store& tx_store, uint64_t current_height); + uint64_t get_balance() const { return m_balance; }; + uint64_t get_balance(uint32_t account_idx) const; + uint64_t get_balance(uint32_t account_idx, uint32_t subaddress_idx) const; + uint64_t get_unlocked_balance() const { return m_unlocked_balance; }; + uint64_t get_unlocked_balance(uint32_t account_idx) const; + uint64_t get_unlocked_balance(uint32_t account_idx, uint32_t subaddress_idx) const; + bool is_frozen(const std::string& key_image) const; + bool is_frozen(const monero_light_output& output) const; + void freeze(const std::string& key_image); + void thaw(const std::string& key_image); + void clear(); + void clear_frozen(); + void set_key_image_spent(const std::string& key_image, bool spent = true); + bool is_key_image_spent(const std::string& key_image) const; + wallet2_exported_outputs export_outputs(const monero_light_tx_store& tx_store, monero_key_image_cache& key_image_cache, bool all, uint32_t start, uint32_t count = 0xffffffff) const; + + private: + mutable boost::recursive_mutex m_mutex; + // cache + mutable serializable_unordered_map m_index; + mutable serializable_unordered_map> m_tx_hash_index; + mutable serializable_unordered_map m_key_image_index; + mutable serializable_unordered_map m_key_image_status_index; + + mutable serializable_unordered_map m_frozen_key_image_index; + mutable serializable_unordered_map>> m_spent; + mutable serializable_unordered_map>> m_unspent; + size_t m_num_spent = 0; + size_t m_num_unspent = 0; + // balance info + uint64_t m_balance = 0; + uint64_t m_unlocked_balance = 0; + serializable_unordered_map m_account_balance; + serializable_unordered_map m_account_unlocked_balance; + serializable_unordered_map> m_subaddress_balance; + serializable_unordered_map> m_subaddress_unlocked_balance; + + void clear_balance(); + }; + +} \ No newline at end of file diff --git a/src/wallet/monero_wallet_utils.h b/src/wallet/monero_wallet_utils.h new file mode 100644 index 00000000..176b5692 --- /dev/null +++ b/src/wallet/monero_wallet_utils.h @@ -0,0 +1,430 @@ +/** + * Copyright (c) woodser + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Parts of this file are originally copyright (c) 2014-2019, The Monero Project + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * All rights reserved. + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be + * used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers + */ + +#pragma once + +#ifndef monero_wallet_utils_h +#define monero_wallet_utils_h + +#include "utils/monero_utils.h" +#include "wallet/monero_wallet_model.h" +#include "wallet/monero_wallet.h" +#include "cryptonote_basic/cryptonote_basic.h" +#include "cryptonote_core/cryptonote_tx_utils.h" +#include "wallet/wallet2.h" +#include "serialization/keyvalue_serialization.h" // TODO: consolidate with other binary deps? +#include "storages/portable_storage.h" +#include "common/threadpool.h" + +/** + * Collection of wallet utilities. + */ +namespace monero_wallet_utils +{ + + // ----------------------------- WALLET LISTENER ---------------------------- + + /** + * Listens to wallet2 notifications in order to notify external wallet listeners. + */ + struct wallet2_listener : public tools::i_wallet2_callback { + + public: + + /** + * Constructs the listener. + * + * @param wallet provides context to notify external listeners + * @param wallet2 provides source notifications which this listener propagates to external listeners + */ + wallet2_listener(monero_wallet& wallet, boost::optional wallet2 = boost::none) : m_wallet(wallet) { + this->m_sync_start_height = boost::none; + this->m_sync_end_height = boost::none; + if (wallet2 != boost::none) m_w2 = wallet2.get(); + m_prev_balance = wallet.get_balance(); + m_prev_unlocked_balance = wallet.get_unlocked_balance(); + m_notification_pool = std::unique_ptr(tools::threadpool::getNewForUnitTests(1)); // TODO (monero-project): utility can be for general use + } + + ~wallet2_listener() { + MTRACE("~wallet2_listener()"); + if (m_w2 != boost::none) + m_w2->callback(nullptr); + m_notification_pool->recycle(); + } + + void update_listening() { + boost::lock_guard guarg(m_listener_mutex); + + // update callback + if (m_w2 != boost::none) m_w2->callback(m_wallet.get_listeners().empty() ? nullptr : this); + + // if starting to listen, cache locked txs for later comparison + if (!m_wallet.get_listeners().empty()) { + if (m_w2 != boost::none && m_w2->callback() != nullptr) return; + tools::threadpool::waiter waiter(*m_notification_pool); + m_notification_pool->submit(&waiter, [this]() { + check_for_changed_unlocked_txs(); + }); + waiter.wait(); + } + } + + void on_sync_start(uint64_t start_height) { + tools::threadpool::waiter waiter(*m_notification_pool); + m_notification_pool->submit(&waiter, [this, start_height]() { + if (m_sync_start_height != boost::none || m_sync_end_height != boost::none) throw std::runtime_error("Sync start or end height should not already be allocated, is previous sync in progress?"); + m_sync_start_height = start_height; + m_sync_end_height = m_wallet.get_daemon_height(); + }); + waiter.wait(); // TODO: this processes notification on thread, process off thread + } + + void on_sync_end() { + tools::threadpool::waiter waiter(*m_notification_pool); + m_notification_pool->submit(&waiter, [this]() { + check_for_changed_balances(); + if (m_prev_locked_tx_hashes.size() > 0) check_for_changed_unlocked_txs(); + m_sync_start_height = boost::none; + m_sync_end_height = boost::none; + }); + m_notification_pool->recycle(); + waiter.wait(); + } + + void on_new_block(uint64_t height, const cryptonote::block& cn_block) override { + if (m_wallet.get_listeners().empty()) return; + + // ignore notifications before sync start height, irrelevant to clients + if (m_sync_start_height == boost::none || height < *m_sync_start_height) return; + + // queue notification processing off main thread + tools::threadpool::waiter waiter(*m_notification_pool); + m_notification_pool->submit(&waiter, [this, height]() { + + // notify listeners of new block + for (monero_wallet_listener* listener : m_wallet.get_listeners()) { + listener->on_new_block(height); + } + + // notify listeners of sync progress + if (height >= *m_sync_end_height) m_sync_end_height = height + 1; // increase end height if necessary + double percent_done = (double) (height - *m_sync_start_height + 1) / (double) (*m_sync_end_height - *m_sync_start_height); + std::string message = std::string("Synchronizing"); + for (monero_wallet_listener* listener : m_wallet.get_listeners()) { + listener->on_sync_progress(height, *m_sync_start_height, *m_sync_end_height, percent_done, message); + } + + // notify if balances change + bool balances_changed = check_for_changed_balances(); + + // notify when txs unlock after wallet is synced + if (balances_changed && m_wallet.is_synced()) check_for_changed_unlocked_txs(); + }); + waiter.wait(); + } + + void on_unconfirmed_money_received(uint64_t height, const crypto::hash &txid, const cryptonote::transaction& cn_tx, uint64_t amount, const cryptonote::subaddress_index& subaddr_index) override { + if (m_wallet.get_listeners().empty()) return; + + // queue notification processing off main thread + tools::threadpool::waiter waiter(*m_notification_pool); + m_notification_pool->submit(&waiter, [this, height, txid, cn_tx, amount, subaddr_index]() { + try { + + // create library tx + std::shared_ptr tx = std::static_pointer_cast(monero_utils::cn_tx_to_tx(cn_tx, true)); + tx->m_hash = epee::string_tools::pod_to_hex(txid); + tx->m_is_confirmed = false; + tx->m_is_locked = true; + std::shared_ptr output = std::make_shared(); + tx->m_outputs.push_back(output); + output->m_tx = tx; + output->m_amount = amount; + output->m_account_index = subaddr_index.major; + output->m_subaddress_index = subaddr_index.minor; + + // notify listeners of output + for (monero_wallet_listener* listener : m_wallet.get_listeners()) { + listener->on_output_received(*output); + } + + // notify if balances changed + check_for_changed_balances(); + + // watch for unlock + m_prev_locked_tx_hashes.insert(tx->m_hash.get()); + + // free memory + monero_utils::free(tx); + } catch (std::exception& e) { + std::cout << "Error processing unconfirmed output received: " << std::string(e.what()) << std::endl; + } + }); + waiter.wait(); + } + + void on_money_received(uint64_t height, const crypto::hash &txid, const cryptonote::transaction& cn_tx, uint64_t amount, uint64_t burnt, const cryptonote::subaddress_index& subaddr_index, bool is_change, uint64_t unlock_time) override { + if (m_wallet.get_listeners().empty()) return; + + // queue notification processing off main thread + tools::threadpool::waiter waiter(*m_notification_pool); + m_notification_pool->submit(&waiter, [this, height, txid, cn_tx, amount, burnt, subaddr_index, is_change, unlock_time]() { + try { + + // create native library tx + std::shared_ptr block = std::make_shared(); + block->m_height = height; + std::shared_ptr tx = std::static_pointer_cast(monero_utils::cn_tx_to_tx(cn_tx, true)); + block->m_txs.push_back(tx); + tx->m_block = block; + tx->m_hash = epee::string_tools::pod_to_hex(txid); + tx->m_is_confirmed = true; + tx->m_is_locked = true; + tx->m_unlock_time = unlock_time; + std::shared_ptr output = std::make_shared(); + tx->m_outputs.push_back(output); + output->m_tx = tx; + output->m_amount = amount - burnt; + output->m_account_index = subaddr_index.major; + output->m_subaddress_index = subaddr_index.minor; + + // notify listeners of output + for (monero_wallet_listener* listener : m_wallet.get_listeners()) { + listener->on_output_received(*output); + } + + // watch for unlock + m_prev_locked_tx_hashes.insert(tx->m_hash.get()); + + // free memory + monero_utils::free(block); + } catch (std::exception& e) { + std::cout << "Error processing confirmed output received: " << std::string(e.what()) << std::endl; + } + }); + waiter.wait(); + } + + void on_money_spent(uint64_t height, const crypto::hash &txid, const cryptonote::transaction& cn_tx_in, uint64_t amount, const cryptonote::transaction& cn_tx_out, const cryptonote::subaddress_index& subaddr_index) override { + if (m_wallet.get_listeners().empty()) return; + if (&cn_tx_in != &cn_tx_out) throw std::runtime_error("on_money_spent() in tx is different than out tx"); + + // queue notification processing off main thread + tools::threadpool::waiter waiter(*m_notification_pool); + m_notification_pool->submit(&waiter, [this, height, txid, cn_tx_in, amount, cn_tx_out, subaddr_index]() { + try { + + // create native library tx + std::shared_ptr block = std::make_shared(); + block->m_height = height; + std::shared_ptr tx = std::static_pointer_cast(monero_utils::cn_tx_to_tx(cn_tx_in, true)); + block->m_txs.push_back(tx); + tx->m_block = block; + tx->m_hash = epee::string_tools::pod_to_hex(txid); + tx->m_is_confirmed = true; + tx->m_is_locked = true; + std::shared_ptr output = std::make_shared(); + tx->m_inputs.push_back(output); + output->m_tx = tx; + output->m_amount = amount; + output->m_account_index = subaddr_index.major; + output->m_subaddress_index = subaddr_index.minor; + + // notify listeners of output + for (monero_wallet_listener* listener : m_wallet.get_listeners()) { + listener->on_output_spent(*output); + } + + // watch for unlock + m_prev_locked_tx_hashes.insert(tx->m_hash.get()); + + // free memory + monero_utils::free(block); + } catch (std::exception& e) { + std::cout << "Error processing confirmed output spent: " << std::string(e.what()) << std::endl; + } + }); + waiter.wait(); + } + + void on_spend_tx_hashes(const std::vector& tx_hashes) { + if (m_wallet.get_listeners().empty()) return; + monero_tx_query tx_query; + tx_query.m_hashes = tx_hashes; + tx_query.m_include_outputs = true; + tx_query.m_is_locked = true; + on_spend_txs(m_wallet.get_txs(tx_query)); + } + + void on_spend_txs(const std::vector>& txs) { + if (m_wallet.get_listeners().empty()) return; + tools::threadpool::waiter waiter(*m_notification_pool); + m_notification_pool->submit(&waiter, [this, txs]() { + check_for_changed_balances(); + for (const std::shared_ptr& tx : txs) notify_outputs(tx); + }); + waiter.wait(); + } + + protected: + monero_wallet& m_wallet; // wallet to provide context for notifications + boost::optional m_w2; // internal wallet implementation to listen to + boost::optional m_sync_start_height; + boost::optional m_sync_end_height; + boost::mutex m_listener_mutex; + uint64_t m_prev_balance; + uint64_t m_prev_unlocked_balance; + std::set m_prev_locked_tx_hashes; + std::unique_ptr m_notification_pool; // threadpool of size 1 to queue notifications for external announcement + + bool check_for_changed_balances() { + uint64_t balance = m_wallet.get_balance(); + uint64_t unlocked_balance = m_wallet.get_unlocked_balance(); + if (balance != m_prev_balance || unlocked_balance != m_prev_unlocked_balance) { + m_prev_balance = balance; + m_prev_unlocked_balance = unlocked_balance; + for (monero_wallet_listener* listener : m_wallet.get_listeners()) { + listener->on_balances_changed(balance, unlocked_balance); + } + return true; + } + return false; + } + + // TODO: this can probably be optimized using e.g. wallet2.get_num_rct_outputs() or wallet2.get_num_transfer_details(), or by retaining confirmed block height and only checking on or after unlock height, etc + void check_for_changed_unlocked_txs() { + + // get confirmed and locked txs + monero_tx_query query = monero_tx_query(); + query.m_is_locked = true; + query.m_is_confirmed = true; + query.m_min_height = m_wallet.get_height() - 70; // only monitor recent txs + std::vector> locked_txs = m_wallet.get_txs(query); + + // collect hashes of txs no longer locked + std::vector tx_hashes_no_longer_locked; + for (const std::string prev_locked_tx_hash : m_prev_locked_tx_hashes) { + bool found = false; + for (const std::shared_ptr& locked_tx : locked_txs) { + if (locked_tx->m_hash.get() == prev_locked_tx_hash) { + found = true; + break; + } + } + if (!found) tx_hashes_no_longer_locked.push_back(prev_locked_tx_hash); + } + + // fetch txs that are no longer locked + std::vector> txs_no_longer_locked; + if (!tx_hashes_no_longer_locked.empty()) { + query.m_hashes = tx_hashes_no_longer_locked; + query.m_is_locked = false; + query.m_include_outputs = true; + txs_no_longer_locked = m_wallet.get_txs(query); + } + + // notify listeners of newly unlocked inputs and outputs + for (const std::shared_ptr& unlocked_tx : txs_no_longer_locked) { + notify_outputs(unlocked_tx); + } + + // re-assign currently locked tx hashes // TODO: needs mutex for thread safety? + m_prev_locked_tx_hashes.clear(); + for (const std::shared_ptr& locked_tx : locked_txs) { + m_prev_locked_tx_hashes.insert(locked_tx->m_hash.get()); + } + + // free memory + monero_utils::free(locked_txs); + monero_utils::free(txs_no_longer_locked); + } + + void notify_outputs(const std::shared_ptr& tx) { + + // notify spent outputs + if (tx->m_outgoing_transfer != boost::none) { + + // build dummy input for notification // TODO: this provides one input with outgoing amount like monero-wallet-rpc client, use real inputs instead + std::shared_ptr input = std::make_shared(); + input->m_amount = tx->m_outgoing_transfer.get()->m_amount.get() + tx->m_fee.get(); + input->m_account_index = tx->m_outgoing_transfer.get()->m_account_index; + if (tx->m_outgoing_transfer.get()->m_subaddress_indices.size() == 1) input->m_subaddress_index = tx->m_outgoing_transfer.get()->m_subaddress_indices[0]; // initialize if transfer sourced from single subaddress + std::shared_ptr tx_notify = std::make_shared(); + input->m_tx = tx_notify; + tx_notify->m_inputs.push_back(input); + tx_notify->m_hash = tx->m_hash; + tx_notify->m_is_locked = tx->m_is_locked; + tx_notify->m_unlock_time = tx->m_unlock_time; + if (tx->m_block != boost::none) { + std::shared_ptr block_notify = std::make_shared(); + tx_notify->m_block = block_notify; + block_notify->m_height = tx->get_height(); + block_notify->m_txs.push_back(tx_notify); + } + + // notify listeners and free memory + for (monero_wallet_listener* listener : m_wallet.get_listeners()) listener->on_output_spent(*input); + monero_utils::free(tx_notify); + } + + // notify received outputs + if (!tx->m_incoming_transfers.empty()) { + for (const std::shared_ptr& output : tx->get_outputs_wallet()) { + for (monero_wallet_listener* listener : m_wallet.get_listeners()) listener->on_output_received(*output); + } + } + } + }; +} + +#endif /* monero_wallet_utils_h */