diff --git a/CMakeLists.txt b/CMakeLists.txt index a2467156..816ed591 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -330,6 +330,7 @@ set( src/common/monero_rpc_connection.cpp src/utils/gen_utils.cpp src/utils/monero_utils.cpp + src/utils/monero_wallet_utils.cpp src/daemon/monero_daemon_model.cpp src/daemon/monero_daemon.cpp src/daemon/monero_daemon_rpc_model.cpp @@ -337,8 +338,10 @@ set( src/wallet/monero_wallet_model.cpp src/wallet/monero_wallet_keys.cpp src/wallet/monero_wallet_rpc_model.cpp + src/wallet/monero_wallet_light_model.cpp src/wallet/monero_wallet_rpc.cpp src/wallet/monero_wallet_full.cpp + src/wallet/monero_wallet_light.cpp ) if (BUILD_LIBRARY) @@ -446,13 +449,16 @@ endif() DESTINATION include/daemon) INSTALL(FILES src/utils/gen_utils.h src/utils/monero_utils.h + src/utils/monero_wallet_utils.h DESTINATION include/utils) INSTALL(FILES src/wallet/monero_wallet_full.h src/wallet/monero_wallet_rpc.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_rpc_model.h + src/wallet/monero_wallet_light_model.h DESTINATION include/wallet) INSTALL(TARGETS monero-cpp monero-cpp-static RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT Runtime diff --git a/src/common/monero_error.h b/src/common/monero_error.h index c6ada469..2a3fda38 100644 --- a/src/common/monero_error.h +++ b/src/common/monero_error.h @@ -81,6 +81,14 @@ namespace monero { } }; + /** + * Exception when a derived one-time output key does not match the output's claimed public key. + */ + class monero_output_ownership_error : public monero_error { + public: + monero_output_ownership_error() { message = "Derived one-time output key does not match the output's public key"; } + }; + /** * Exception when interacting with the Monero daemon or wallet RPC API. */ diff --git a/src/utils/gen_utils.h b/src/utils/gen_utils.h index 5062e503..8b148c46 100644 --- a/src/utils/gen_utils.h +++ b/src/utils/gen_utils.h @@ -67,8 +67,13 @@ #include #include #include +#include +#include +#include +#include #include "include_base_utils.h" #include "common/util.h" +#include "crypto/crypto.h" /** * Collection of generic utilities. @@ -87,6 +92,39 @@ namespace gen_utils return boost::uuids::to_string(uuid); } + static bool is_uint64_t(const std::string& str) { + if (str.empty() || !std::all_of(str.begin(), str.end(), [](unsigned char c) { return std::isdigit(c) != 0; })) return false; + errno = 0; + char* end = nullptr; + std::strtoull(str.c_str(), &end, 10); + if (errno == ERANGE) return false; + return end == str.c_str() + str.size(); + } + + 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"); + return static_cast(std::strtoull(str.c_str(), nullptr, 10)); + } + + // based on Howard Hinnant's days_from_civil algorithm (http://howardhinnant.github.io/date_algorithms.html) + static uint64_t timestamp_to_epoch(const std::string& iso_timestamp) { + int year, month, day, hour, minute, second; + if (std::sscanf(iso_timestamp.c_str(), "%d-%d-%d%*[T ]%d:%d:%d", &year, &month, &day, &hour, &minute, &second) != 6) { + throw std::runtime_error("Invalid ISO 8601 timestamp: " + iso_timestamp); + } + + int64_t y = year - (month <= 2 ? 1 : 0); + int64_t era = (y >= 0 ? y : y - 399) / 400; + uint64_t yoe = static_cast(y - era * 400); + uint64_t doy = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1; + uint64_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + int64_t days = era * 146097 + static_cast(doe) - 719468; + + int64_t seconds = days * 86400 + hour * 3600 + minute * 60 + second; + if (seconds < 0) throw std::out_of_range("Timestamp is before the Unix epoch"); + return static_cast(seconds); + } + /** * Wait for the given duration. * @@ -179,6 +217,26 @@ namespace gen_utils throw std::runtime_error("Cannot reconcile vectors" + (!err_msg.empty() ? std::string(". ") + err_msg : std::string(""))); } + 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); + } + // ------------------------- THREAD POLLER ---------------------------- // WARNING: Only destroy a thread_poller from a thread other than its own pool thread. diff --git a/src/utils/monero_utils.cpp b/src/utils/monero_utils.cpp index 17743a56..a800dffa 100644 --- a/src/utils/monero_utils.cpp +++ b/src/utils/monero_utils.cpp @@ -601,10 +601,16 @@ bool monero_utils::vout_before(const std::shared_ptr& o1, const s 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()) { + static const uint32_t EXTERNAL_SENTINEL = std::numeric_limits::max(); + uint32_t account1 = ow1->m_account_index.value_or(EXTERNAL_SENTINEL); + uint32_t account2 = ow2->m_account_index.value_or(EXTERNAL_SENTINEL); + + if (account1 < account2) return true; + if (account1 == account2) { + uint32_t subaddress1 = ow1->m_subaddress_index.value_or(EXTERNAL_SENTINEL); + uint32_t subaddress2 = ow2->m_subaddress_index.value_or(EXTERNAL_SENTINEL); + if (subaddress1 < subaddress2) return true; + if (subaddress1 == subaddress2) { 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"); } @@ -791,3 +797,75 @@ bool monero_utils::parse_payment_id_short(const std::string& payment_id_str, cry payment_id = *reinterpret_cast(payment_id_data.data()); return true; } + +std::shared_ptr monero_utils::generate_key_image(const crypto::public_key &ephem_pubkey, const size_t tx_output_index, const cryptonote::subaddress_index &received_subaddr, const cryptonote::account_base& account, const boost::optional& expected_output_pubkey) { + // - 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] + + const cryptonote::account_keys &ack = account.get_keys(); + hw::device &hwdev = account.get_device(); + + // 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"); + + if (expected_output_pubkey != boost::none && onetime_pubkey != *expected_output_pubkey) throw monero_output_ownership_error(); + + // 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(&onetime_pubkey); + + crypto::generate_ring_signature((const crypto::hash&)ki, ki, key_ptrs, onetime_privkey, 0, &signature); + + std::shared_ptr key_image = std::make_shared(); + key_image->m_hex = epee::string_tools::pod_to_hex(ki); + key_image->m_signature = epee::string_tools::pod_to_hex(signature); + return key_image; +} + +void monero_utils::verify_output_ownership(const crypto::public_key &ephem_pubkey, const size_t tx_output_index, const cryptonote::subaddress_index &received_subaddr, const cryptonote::account_base& account, const crypto::public_key &expected_output_pubkey) { + const cryptonote::account_keys &ack = account.get_keys(); + hw::device &hwdev = account.get_device(); + + 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"); + + crypto::secret_key onetime_privkey_unextended; + hwdev.derive_secret_key(recv_derivation, tx_output_index, ack.m_spend_secret_key, onetime_privkey_unextended); + + 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)}; + + crypto::secret_key onetime_privkey; + hwdev.sc_secret_add(onetime_privkey, onetime_privkey_unextended, subaddr_ext); + + 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"); + + if (onetime_pubkey != expected_output_pubkey) throw monero_output_ownership_error(); +} diff --git a/src/utils/monero_utils.h b/src/utils/monero_utils.h index 407d3e56..61533f1e 100644 --- a/src/utils/monero_utils.h +++ b/src/utils/monero_utils.h @@ -55,8 +55,10 @@ #ifndef monero_utils_h #define monero_utils_h +#include "common/monero_error.h" #include "wallet/monero_wallet_model.h" #include "cryptonote_basic/cryptonote_basic.h" +#include "cryptonote_core/cryptonote_tx_utils.h" #include "serialization/keyvalue_serialization.h" // TODO: consolidate with other binary deps? #include "storages/portable_storage.h" @@ -219,15 +221,39 @@ namespace monero_utils bool tx_height_less_than(const std::shared_ptr& tx1, const std::shared_ptr& tx2); /** - * Returns true iff transfer1 is ordered before transfer2 by ascending account and subaddress indices. - */ + * 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); /** - * Returns true iff wallet vout1 is ordered before vout2 by ascending account and subaddress indices then index. - */ + * 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); + /** + * Generates a key image for an output note (enote) in a simplified manner. + * + * @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 account recipient's account + * @param expected_output_pubkey is the output's actual public key, to verify against the derived one-time key when supplied (boost::none skips the check for callers with no output public key to check against) + * @return the generated key image + */ + std::shared_ptr generate_key_image(const crypto::public_key &ephem_pubkey, const size_t tx_output_index, const cryptonote::subaddress_index &received_subaddr, const cryptonote::account_base& account, const boost::optional& expected_output_pubkey); + + /** + * Derives the one-time output public key and throws if it does not match expected_output_pubkey, + * without generating or signing a key image. + * + * @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 account recipient's account + * @param expected_output_pubkey is the output's actual public key, to verify against the derived one-time key + */ + void verify_output_ownership(const crypto::public_key &ephem_pubkey, const size_t tx_output_index, const cryptonote::subaddress_index &received_subaddr, const cryptonote::account_base& account, const crypto::public_key &expected_output_pubkey); + // ----------------------------- GATHER BLOCKS ------------------------------ static std::vector> get_blocks_from_txs(std::vector> txs) { @@ -286,6 +312,19 @@ 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); + } + } + // ------------------------------ FREE MEMORY ------------------------------- static void free(std::shared_ptr block) { diff --git a/src/utils/monero_wallet_utils.cpp b/src/utils/monero_wallet_utils.cpp new file mode 100644 index 00000000..635383f5 --- /dev/null +++ b/src/utils/monero_wallet_utils.cpp @@ -0,0 +1,659 @@ +/** + * Copyright (c) everoddandeven + * + * 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 + * Parts of this file are originally copyright (c) 2014-2019, MyMonero.com + * + * 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_utils.h" +#include "rpc/core_rpc_server_commands_defs.h" +#include "storages/portable_storage_template_helper.h" +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "mnemonics/electrum-words.h" +#include "mnemonics/english.h" +#include "string_tools.h" +#include "byte_stream.h" +#include "gen_utils.h" + +#define UNSIGNED_TX_PREFIX "Monero unsigned tx set\005" +#define SIGNED_TX_PREFIX "Monero signed tx set\005" + +std::shared_ptr monero_wallet_utils::ptx_to_tx(const tools::wallet2::pending_tx &ptx, cryptonote::network_type nettype, monero_wallet* wallet, std::string* out_change_pubkey) { + if (out_change_pubkey != nullptr) *out_change_pubkey = ""; + 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; + try { tx->m_prunable_hash = epee::string_tools::pod_to_hex(cryptonote::get_transaction_prunable_hash(cn_tx)); } + catch (...) { tx->m_prunable_hash = boost::none; } + tx->m_is_outgoing = false; + tx->m_fee = ptx.fee; + + // dump wallet2 pending tx + try { + std::ostringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + ar << ptx; + tx->m_metadata = epee::string_tools::buff_to_hex_nodelimer(oss.str()); + } catch (...) { + tx->m_metadata = ""; + } + + 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; + // cd.subaddr_indices is the deduplicated set of subaddresses used, not one entry per input, and + // construct_tx_and_get_tx_key() reorders sources/tx.vin by key image, so position doesn't match + // selection order either. Approximate with the smallest used index; callers with a real per-input + // lookup (monero_wallet_light's create_txs()/sweep_account()) overwrite m_subaddress_index after. + size_t i = 0; + for (const auto& in : tx->m_inputs) { + auto input = std::dynamic_pointer_cast(in); + uint32_t subaddress_idx = cd.subaddr_indices.empty() ? 0 : *next(cd.subaddr_indices.begin(), std::min(i, cd.subaddr_indices.size() - 1)); + input->m_account_index = sender_account_idx; + input->m_subaddress_index = subaddress_idx; + input->m_is_spent = true; + input->m_is_frozen = false; + i++; + } + + std::shared_ptr outgoing_transfer = std::make_shared(); + outgoing_transfer->m_tx = tx; + tx->m_outgoing_transfer = outgoing_transfer; + outgoing_transfer->m_account_index = sender_account_idx; + outgoing_transfer->m_subaddress_indices = std::vector(cd.subaddr_indices.begin(), cd.subaddr_indices.end()); + + std::shared_ptr change_output = nullptr; + std::vector> external_outputs; + + uint64_t out_amount = 0; + i = 0; + + std::map>>> destination_index; + std::vector> external_destinations; + + for (const auto& out : tx->m_outputs) { + auto output = std::dynamic_pointer_cast(out); + if (output == nullptr) { + i++; + continue; + } + + if (i >= cd.splitted_dsts.size()) { + out_amount += output->m_amount.get(); + external_outputs.push_back(output); + i++; + continue; + } + + const auto &dest = cd.splitted_dsts[i]; + out->m_amount = dest.amount; + out->m_index = i; + + 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.addr == cd.change_dts.addr && 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].push_back(destination); + } + } + catch (...) { + // external output + out_amount += output->m_amount.get(); + external_outputs.push_back(output); + if (dest.amount > 0) { + auto destination = std::make_shared(); + destination->m_amount = dest.amount; + destination->m_address = dest_address; + external_destinations.push_back(destination); + } + } + i++; + } + + tx->m_is_incoming = !tx->m_incoming_transfers.empty(); + tx->m_is_outgoing = tx->m_outgoing_transfer != nullptr; + + if (change_output != nullptr) { + if (out_change_pubkey != nullptr) *out_change_pubkey = change_output->m_stealth_public_key.value_or(""); + 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_tx = tx; // required by monero_utils::vout_before()/tx_height_less_than(), which assume every tx output has its tx set + 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) { + for (const auto& destination : kv.second) outgoing_transfer->m_destinations.push_back(destination); + } + } + for (const auto& destination : external_destinations) { + outgoing_transfer->m_destinations.push_back(destination); + } + + return tx; +} + +tools::wallet2::signed_tx_set monero_wallet_utils::parse_signed_tx(const std::string &signed_tx_st, const crypto::secret_key &view_secret_key) { + 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 { + // decrypt with private view key + s = monero_wallet_utils::decrypt(s, view_secret_key); + } + 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_L2(cryptonote::obj_to_json_str(c_ptx.tx)); + + return signed_txs; +} + +tools::wallet2::unsigned_tx_set monero_wallet_utils::parse_unsigned_tx(const std::string &unsigned_tx_st, const crypto::secret_key &view_secret_key) { + 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 { + // decrypt with private view key + s = monero_wallet_utils::decrypt(s, view_secret_key); + } + catch(const std::exception &e) { + throw std::runtime_error(std::string("Failed to decrypt unsigned tx: ") + e.what()); + } + 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_utils::dump_unsigned_tx(std::vector& construction_data, const boost::optional& payment_id, const wallet2_exported_outputs& outputs, const crypto::secret_key &view_secret_key) { + tools::wallet2::unsigned_tx_set txs; + if (payment_id == boost::none || payment_id->empty()) LOG_PRINT_L0("Payment ID not set"); + + txs.txes = construction_data; + txs.new_transfers = outputs; + + // 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_L2("Saving unsigned tx data (" << static_cast(oss.tellp()) << " bytes)"); + + // encrypt with private view key + std::string ciphertext = monero_wallet_utils::encrypt(oss.str(), view_secret_key); + return epee::string_tools::buff_to_hex_nodelimer(std::string(UNSIGNED_TX_PREFIX) + ciphertext); +} + +tools::wallet2::tx_construction_data monero_wallet_utils::get_construction_data_with_decrypted_short_payment_id(const tools::wallet2::pending_tx &ptx, hw::device &hwdev) { + tools::wallet2::tx_construction_data construction_data = ptx.construction_data; + + std::vector tx_extra_fields; + cryptonote::parse_tx_extra(ptx.tx.extra, tx_extra_fields); // ok if partially parsed + cryptonote::tx_extra_nonce extra_nonce; + if (cryptonote::find_tx_extra_field_by_type(tx_extra_fields, extra_nonce)) { + crypto::hash8 payment_id = crypto::null_hash8; + if (cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id)) { + const crypto::public_key view_key_pub = cryptonote::get_destination_view_key_pub(construction_data.splitted_dsts, construction_data.change_dts.addr); + if (view_key_pub == crypto::null_pkey) { + MWARNING("Encrypted payment id found, but no unique destination public key, cannot decrypt"); + } + else if (hwdev.decrypt_payment_id(payment_id, view_key_pub, ptx.tx_key)) { + // remove encrypted + cryptonote::remove_field_from_tx_extra(construction_data.extra, typeid(cryptonote::tx_extra_nonce)); + // add decrypted + std::string decrypted_extra_nonce; + cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(decrypted_extra_nonce, payment_id); + if (!cryptonote::add_extra_nonce_to_tx_extra(construction_data.extra, decrypted_extra_nonce)) throw std::runtime_error("Failed to add decrypted payment id to tx extra"); + LOG_PRINT_L1("Decrypted payment ID: " << payment_id); + } + } + } + + return construction_data; +} + +std::string monero_wallet_utils::sign_tx(tools::wallet2::unsigned_tx_set &exported_txs, std::vector &txs, tools::wallet2::signed_tx_set &signed_txes, std::vector& signed_kis, const cryptonote::account_base& account, const serializable_unordered_map& 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(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 + + 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 = account.get_keys(); + hw::device &hwdev = 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()); + + // encrypt with private view key + std::string ciphertext = monero_wallet_utils::encrypt(oss.str(), keys.m_view_secret_key); + return std::string(SIGNED_TX_PREFIX) + ciphertext; +} + +uint64_t monero_wallet_utils::estimate_fee(int n_inputs, int mixin, int n_outputs, size_t extra_size, uint64_t base_fee, uint64_t fee_multiplier, uint64_t fee_quantization_mask) { + const size_t estimated_tx_weight = estimate_tx_weight(n_inputs, mixin, n_outputs, extra_size); + return calculate_fee_from_weight(base_fee, estimated_tx_weight, fee_multiplier, fee_quantization_mask); +} + +uint64_t monero_wallet_utils::get_fee_multiplier(uint32_t priority) { + // v8 enforced fee algorithm 3 + if (priority == 2) return 5; + if (priority == 3) return 25; + if (priority == 4) return 1000; + return 1; +} + +size_t monero_wallet_utils::estimate_rct_tx_size(int n_inputs, int mixin, int n_outputs, size_t extra_size) { + size_t size = 1 + 6; // tx prefix first few bytes + size += n_inputs * (1+6+(mixin+1)*2+32); // vin + size += n_outputs * (6+32); // vuout + size += extra_size; // extra + size += 1; // rct signatures + + size_t log_padded_outputs = 0; // rangeSigs + while ((1< 2) { + const uint64_t bp_base = (32 * (6 + 7 * 2)) / 2; // notional size of a 2-output bulletproof+ proof, normalized to 1 proof + 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 monero_wallet_utils::validate_cn_tx(const cryptonote::transaction &tx) { + if (get_tx_weight_limit() <= cryptonote::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 = cryptonote::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"); +} + +void monero_wallet_utils::add_pid_to_tx_extra(const boost::optional& payment_id_string, std::vector &extra) { + if (payment_id_string == boost::none || payment_id_string->size() == 0) return; + + // detect hash8 or hash32 char hex string as pid and configure 'extra' accordingly + crypto::hash payment_id; + if (monero_utils::parse_payment_id_long(*payment_id_string, payment_id)) { + std::string extra_nonce; + cryptonote::set_payment_id_to_tx_extra_nonce(extra_nonce, payment_id); + if (!cryptonote::add_extra_nonce_to_tx_extra(extra, extra_nonce)) throw std::runtime_error("Couldn't add pid nonce to tx extra"); + } else { + crypto::hash8 payment_id8; + // a PID has been specified by the user but the last resort in validating it fails; error + if (!monero_utils::parse_payment_id_short(*payment_id_string, payment_id8)) throw std::runtime_error("Invalid pid"); + std::string extra_nonce; + cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, payment_id8); + if (!cryptonote::add_extra_nonce_to_tx_extra(extra, extra_nonce)) throw std::runtime_error("Couldn't add pid nonce to tx extra"); + } +} + +bool monero_wallet_utils::rct_hex_to_decrypted_mask(const std::string &rct_str, 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_str.empty()) return false; + + // rct_str is a magic value if output is RCT and coinbase + if (rct_str == "coinbase") { + decrypted_mask = rct::identity(); + return true; + } + + auto make_key_derivation = [&]() { + crypto::key_derivation derivation; + if(!generate_key_derivation(tx_pub_key, view_secret_key, derivation)) 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_str is a string with length 64+16 ( + ) if RCT version 2 + if (rct_str.size() < 64 * 2) { + decrypted_mask = rct::genCommitmentMask(make_key_derivation()); + return true; + } + + // rct_str is a string with length 64+64+64 ( + + ) + std::string encrypted_mask_str = rct_str.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_wallet_utils::rct_hex_to_rct_commit(const std::string &rct_str, rct::key &rct_commit) { + // rct string is empty if output is non RCT + if (rct_str.empty()) return false; + + // rct_str is a string with length 64+64+64 ( + + ) + std::string rct_commit_str = rct_str.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; +} + +bool monero_wallet_utils::is_rct_hex_unblinded_coinbase(const std::string &rct_str) { + if (rct_str == "coinbase") return true; + if (rct_str.size() < 64 * 3) return false; + + std::string commit_str = rct_str.substr(0, 64); + std::string mask_str = rct_str.substr(64, 64); + if (!epee::string_tools::validate_hex(64, commit_str) || !epee::string_tools::validate_hex(64, mask_str)) return false; + + rct::key commit; + rct::key mask; + epee::string_tools::hex_to_pod(commit_str, commit); + epee::string_tools::hex_to_pod(mask_str, mask); + return commit == rct::zero() && mask == rct::identity(); +} + +void monero_wallet_utils::normalize_unconfirmed_tx(const std::shared_ptr &tx) { + tx->m_outputs.clear(); + tx->m_incoming_transfers.clear(); + tx->m_is_incoming = boost::none; + + tx->m_change_address = boost::none; + tx->m_change_amount = boost::none; + + for(const auto &input : tx->m_inputs) { + input->m_amount = boost::none; + } +} + +std::string monero_wallet_utils::encrypt(const std::string &plaintext_str, const crypto::secret_key &skey, bool authenticated) { + 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; +} diff --git a/src/utils/monero_wallet_utils.h b/src/utils/monero_wallet_utils.h new file mode 100644 index 00000000..34f54a73 --- /dev/null +++ b/src/utils/monero_wallet_utils.h @@ -0,0 +1,344 @@ +/** + * Copyright (c) everoddandeven + * + * 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 + * Parts of this file are originally copyright (c) 2014-2019, MyMonero.com + * + * 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 "monero_utils.h" +#include "wallet/wallet2.h" +#include "wallet/monero_wallet.h" + +/** + * Collection of utilities for the Monero wallet. + */ +namespace monero_wallet_utils +{ + + // ------------------------------ CONSTANTS --------------------------------- + + static const uint64_t TAIL_EMISSION_REWARD = 600000000000; + + // -------------------------------- UTILS ----------------------------------- + + typedef std::tuple> wallet2_exported_outputs; + + /** + * Convert a wallet2::pending_tx to a transaction in this library's + * native model. + * + * @param cn_tx is the wallet2 pending transaction to convert + * @param nettype cryptonote's network type + * @param monero_wallet wallet that created the pending transaction + * @param out_change_pubkey if non-null, receives the change output's stealth public key (hex), or an + * empty string if there was no change output. The change output is otherwise stripped out of + * the returned tx's outputs, so this is the only way to identify it unambiguously afterward + * (its amount alone isn't a safe identifier: another output could coincidentally match it). + * @return a wallet transaction in this library's native model + */ + std::shared_ptr ptx_to_tx(const tools::wallet2::pending_tx &ptx, cryptonote::network_type nettype, monero_wallet* wallet, std::string* out_change_pubkey = nullptr); + + /** + * Parse signed tx hex to wallet2's internal data model signed_tx_set. + * + * Based on wallet2::parse_tx_from_str(). + * + * @param unsigned_tx_st unsigned tx hex + * @param view_secret_key private view key + * @return signed tx set from wallet2's internal data model + */ + tools::wallet2::signed_tx_set parse_signed_tx(const std::string &signed_tx_st, const crypto::secret_key &view_secret_key); + + /** + * Parse unsigned tx hex to wallet2's internal data model unsigned_tx_set. + * + * Based on wallet2::parse_unsigned_tx_from_str(). + * + * @param unsigned_tx_st unsigned tx hex + * @param view_secret_key private view key + * @return unsigned tx set from wallet2's internal data model + */ + tools::wallet2::unsigned_tx_set parse_unsigned_tx(const std::string &unsigned_tx_st, const crypto::secret_key &view_secret_key); + + /** + * Dump wallet2's tx construction data. + * + * Based on wallet2::dump_tx_to_str(). + * + * @param construction_data + * @param paymend_id + * @param outputs + * @param view_secret_key + * @return unsigned tx hex + */ + std::string dump_unsigned_tx(std::vector& construction_data, const boost::optional& payment_id, const wallet2_exported_outputs& outputs, const crypto::secret_key &view_secret_key); + + /** + * Returns a copy of ptx's construction data with its encrypted short (integrated address) + * payment id nonce decrypted back to plaintext, if it carries one. + * + * Based on wallet2.cpp's get_construction_data_with_decrypted_short_payment_id(). + * + * @param ptx is the pending tx whose construction data should be exported + * @param hwdev is the account's device abstraction, used to decrypt the payment id + * @return a copy of ptx.construction_data with its payment id nonce decrypted, if it had one + */ + tools::wallet2::tx_construction_data get_construction_data_with_decrypted_short_payment_id(const tools::wallet2::pending_tx &ptx, hw::device &hwdev); + + /** + * Signs wallet2's unsigned tx set with wallet account. + * + * Based on wallet2::sign_tx(). + * + * @param exported_txs + * @param txs + * @param signed_txs + * @param signed_kis + * @param account + * @param subaddresses + * @return signed tx hex (ciphertext) + */ + 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, const cryptonote::account_base& account, const serializable_unordered_map& subaddresses); + + /** + * Estimate the network fee for a transaction with the given shape (v8 fork rule). + * + * Based on monero-project's wallet2::estimate_fee(). + * + * @param n_inputs is the number of inputs the tx will spend + * @param mixin is the ring size minus one (number of decoys per input) + * @param n_outputs is the number of outputs the tx will create + * @param extra_size is the size in bytes of the tx's "extra" field (e.g. tx pub key, payment id) + * @param base_fee is the daemon's per-byte base fee + * @param fee_multiplier scales the fee according to priority, see get_fee_multiplier() + * @param fee_quantization_mask rounds the fee up to a multiple of this mask, see calculate_fee_from_weight() + * @return the estimated fee in atomic units + */ + uint64_t estimate_fee(int n_inputs, int mixin, int n_outputs, size_t extra_size, uint64_t base_fee, uint64_t fee_multiplier, uint64_t fee_quantization_mask); + + /** + * Estimate the serialized size in bytes of a RingCT transaction with the given shape (v8 fork rule). + * + * Based on monero-project's wallet2.cpp estimate_rct_tx_size(). + * + * @param n_inputs is the number of inputs the tx will spend + * @param mixin is the ring size minus one (number of decoys per input) + * @param n_outputs is the number of outputs the tx will create + * @param extra_size is the size in bytes of the tx's "extra" field + * @return the estimated tx size in bytes + */ + size_t estimate_rct_tx_size(int n_inputs, int mixin, int n_outputs, size_t extra_size); + + /** + * Compute a tx fee from its weight, quantized so the fee doesn't reveal the tx's exact weight (v8 fork rule). + * + * Based on monero-project's wallet2.cpp calculate_fee_from_weight(). + * + * @param base_fee is the daemon's per-byte base fee + * @param weight is the tx weight in bytes, see estimate_tx_weight() + * @param fee_multiplier scales the fee according to priority, see get_fee_multiplier() + * @param fee_quantization_mask rounds the fee up to a multiple of this mask + * @return the quantized fee in atomic units + */ + uint64_t calculate_fee_from_weight(uint64_t base_fee, uint64_t weight, uint64_t fee_multiplier, uint64_t fee_quantization_mask); + + /** + * Estimate the weight of a RingCT transaction with the given shape (v8 fork rule). + * + * Based on monero-project's wallet2.cpp estimate_tx_weight() v8 enforced. + * + * @param n_inputs is the number of inputs the tx will spend + * @param mixin is the ring size minus one (number of decoys per input) + * @param n_outputs is the number of outputs the tx will create + * @param extra_size is the size in bytes of the tx's "extra" field + * @return the estimated tx weight in bytes + */ + uint64_t estimate_tx_weight(int n_inputs, int mixin, int n_outputs, size_t extra_size); + + /** + * Get the maximum tx weight allowed to be relayed by the network (v8 fork rule). + * + * Based on monero-project's wallet2::get_upper_transaction_weight_limit(). + * + * @param default_limit overrides the computed limit when non-zero (default 0, i.e. not overridden) + * @return the tx weight limit in bytes + */ + uint64_t get_tx_weight_limit(uint64_t default_limit = 0); + + /** + * Get the fee multiplier applied to the base fee for a given tx priority (fee algorithm 3). + * + * Based on monero-project's wallet2::get_fee_multiplier(). + * + * @param priority is the tx priority: 1 (or 0, which defaults to 1) is normal, 2 is elevated, 3 is priority, 4 is flash + * @return the fee multiplier for the given priority + */ + uint64_t get_fee_multiplier(uint32_t priority); + + /** + * Validates a cryptonote::transaction. + * + * @param cn_tx is the transaction to validate + */ + void validate_cn_tx(const cryptonote::transaction &cn_tx); + + /** + * Encode a payment id into a tx's "extra" field. + * + * Based on mymonero-core-cpp's monero_transfer_utils.cpp + * internal helper _add_pid_to_tx_extra(). + * + * @param payment_id_string is the payment id to encode, as a 16 or 64 char hex string; a none or empty value is a no-op + * @param extra is the tx's extra field to append the encoded payment id nonce to + * @throws std::runtime_error if payment_id_string is neither a valid long nor short payment id, or if it could not be added to extra + */ + void add_pid_to_tx_extra(const boost::optional& payment_id_string, std::vector &extra); + + /** + * Decrypt an output's hex-econded RingCT commitment mask. + * + * Based on mymonero-core-cpp's monero_transfer_utils.cpp + * internal helper _rct_hex_to_decrypted_mask(). + * + * @param rct_str is the output's hex-encoded rct field + * @param view_secret_key is the wallet's private view key, used to derive the shared secret with the tx + * @param tx_pub_key is the transaction's public key + * @param internal_output_index is the output's index within the transaction, used as the derivation index + * @param decrypted_mask is set to the output's decrypted commitment mask + * @return true if a mask was resolved (including the non-RCT and coinbase cases), false if rct_str is empty + * @throws std::runtime_error if rct_str carries a malformed encrypted mask, or if the key derivation fails + */ + bool rct_hex_to_decrypted_mask(const std::string &rct_str, const crypto::secret_key &view_secret_key, const crypto::public_key& tx_pub_key, uint64_t internal_output_index, rct::key &decrypted_mask); + + /** + * Parse a hex-encoded RingCT commitment. + * + * Based on mymonero-core-cpp's monero_transfer_utils.cpp + * internal helper _rct_hex_to_rct_commit(). + * + * @param rct_str is the output's hex-encoded rct field + * @param rct_commit is set to the output's parsed commitment + * @return true if a commitment was parsed, false otherwise + * @throws std::runtime_error if the commitment substring is not valid hex + */ + bool rct_hex_to_rct_commit(const std::string &rct_str, rct::key &rct_commit); + + /** + * Indicates if a hex-encoded rct field describes a coinbase output, + * validating its commitment via rct::zeroCommit() rather than trusted. + * + * 1. "coinbase" (mymonero/openmonero-style). + * 2. "", where the commitment is left zeroed + * and the mask is the identity element. + * + * @param rct_str is the output's hex-encoded rct field + * @return true if rct_str describes an unblinded coinbase output under either convention + */ + bool is_rct_hex_unblinded_coinbase(const std::string &rct_str); + + /** + * Normalize an unconfirmed transaction by clearing its outputs and incoming transfers. + * + * @param tx is the unconfirmed transaction to normalize + */ + void normalize_unconfirmed_tx(const std::shared_ptr& tx); + + /** + * Encrypt a string with chacha20, optionally signing the result so tampering can be detected on decryption. + * + * Based on monero-project's wallet2::encrypt(). + * + * @param plaintext_str is the data to encrypt + * @param skey is the secret key used to derive the chacha20 key and, if authenticated, to sign the ciphertext + * @param authenticated specifies if a signature is appended to the ciphertext to allow monero_wallet_utils::decrypt() to verify its integrity (default true) + * @return the ciphertext, prefixed with a random chacha20 IV and, if authenticated, suffixed with a signature + */ + std::string encrypt(const std::string &plaintext_str, const crypto::secret_key &skey, bool authenticated = true); + + /** + * Decrypt a string previously encrypted with monero_wallet_utils::encrypt(). + * + * Based on wallet2::decrypt(). + * + * @tparam T is the type to return the decrypted data as (e.g. std::string), constructed from a (const char*, size_t) buffer + * @param ciphertext is the encrypted data to decrypt, as produced by monero_wallet_utils::encrypt() + * @param skey is the secret key used to derive the chacha20 key and, if authenticated, to verify the ciphertext's signature + * @param authenticated specifies if the ciphertext carries a signature that must be verified before decrypting (default true); must match the value used to encrypt + * @return T the decrypted data + * @throws std::runtime_error if the ciphertext is smaller than the expected prefix, or if authenticated and its signature fails to verify + */ + 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); + } +} + +#endif diff --git a/src/wallet/monero_wallet_full.cpp b/src/wallet/monero_wallet_full.cpp index bc651307..e72aaaf4 100644 --- a/src/wallet/monero_wallet_full.cpp +++ b/src/wallet/monero_wallet_full.cpp @@ -101,18 +101,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 @@ -151,7 +139,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; @@ -255,7 +243,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; diff --git a/src/wallet/monero_wallet_keys.cpp b/src/wallet/monero_wallet_keys.cpp index 0e0895e5..f9730774 100644 --- a/src/wallet/monero_wallet_keys.cpp +++ b/src/wallet/monero_wallet_keys.cpp @@ -105,6 +105,55 @@ namespace monero { return hash; } + // ------------------------------- MONERO KEY IMAGE CACHE ------------------------------- + + 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; + if (!string_tools::hex_to_pod(tx_public_key, _tx_public_key)) throw std::runtime_error("failed to parse tx public key"); + cryptonote::subaddress_index received_subaddr{account_idx, subaddress_idx}; + + 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; + } + + 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; + if (!string_tools::hex_to_pod(tx_public_key, _tx_public_key)) throw std::runtime_error("failed to parse tx public key"); + cryptonote::subaddress_index received_subaddr{account_idx, subaddress_idx}; + + boost::lock_guard lock(m_mutex); + m_cache[_tx_public_key][out_index][received_subaddr] = std::make_pair(key_image, request); + } + + 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; + if (!string_tools::hex_to_pod(tx_public_key, _tx_public_key)) throw std::runtime_error("failed to parse tx public key"); + cryptonote::subaddress_index received_subaddr{account_idx, subaddress_idx}; + + 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; + } + // ---------------------------- WALLET MANAGEMENT --------------------------- monero_wallet_keys* monero_wallet_keys::create_wallet_random(const monero_wallet_config& config) { @@ -568,6 +617,46 @@ namespace monero { // ------------------------------- PRIVATE HELPERS ---------------------------- + std::shared_ptr monero_wallet_keys::generate_key_image(const std::string& tx_public_key, uint64_t out_index, uint32_t account_idx, uint32_t subaddress_idx, const boost::optional& expected_output_public_key) const { + crypto::public_key tx_pub_key; + if (!string_tools::hex_to_pod(tx_public_key, tx_pub_key)) throw std::runtime_error("failed to parse tx public key"); + boost::optional output_pub_key = boost::none; + if (expected_output_public_key != boost::none) { + crypto::public_key parsed; + if (!string_tools::hex_to_pod(*expected_output_public_key, parsed)) throw std::runtime_error("failed to parse output public key"); + output_pub_key = parsed; + } + cryptonote::subaddress_index received_subaddr{account_idx, subaddress_idx}; + + auto found = m_key_image_cache->get(tx_public_key, out_index, account_idx, subaddress_idx); + if (found != nullptr) { + if (output_pub_key != boost::none && !is_view_only()) monero_utils::verify_output_ownership(tx_pub_key, out_index, received_subaddr, m_account, *output_pub_key); + return found; + } + + if (is_view_only()) throw std::runtime_error("Cannot generate key image: wallet is view only"); + std::shared_ptr key_image = monero_utils::generate_key_image(tx_pub_key, out_index, received_subaddr, m_account, output_pub_key); + m_key_image_cache->set(key_image, tx_public_key, out_index, account_idx, subaddress_idx); + return key_image; + } + + bool monero_wallet_keys::is_key_image_ours(const std::string &key_image_hex, const std::string& tx_public_key, uint64_t out_index, uint32_t account_idx, uint32_t subaddress_idx, const boost::optional& expected_output_public_key) const { + std::shared_ptr cached_key_image = m_key_image_cache->get(tx_public_key, out_index, account_idx, subaddress_idx); + if (cached_key_image != nullptr) { + if (expected_output_public_key != boost::none && !is_view_only()) { + crypto::public_key tx_pub_key; + if (!string_tools::hex_to_pod(tx_public_key, tx_pub_key)) throw std::runtime_error("failed to parse tx public key"); + crypto::public_key output_pub_key; + if (!string_tools::hex_to_pod(*expected_output_public_key, output_pub_key)) throw std::runtime_error("failed to parse output public key"); + monero_utils::verify_output_ownership(tx_pub_key, out_index, cryptonote::subaddress_index{account_idx, subaddress_idx}, m_account, output_pub_key); + } + return cached_key_image->m_hex.get() == key_image_hex; + } + if (is_view_only()) return false; + std::shared_ptr key_image = generate_key_image(tx_public_key, out_index, account_idx, subaddress_idx, expected_output_public_key); + return key_image_hex == key_image->m_hex.get(); + } + 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(); @@ -575,6 +664,7 @@ namespace monero { m_prv_view_key = epee::string_tools::pod_to_hex(unwrap(unwrap(keys.m_view_secret_key))); m_pub_spend_key = epee::string_tools::pod_to_hex(keys.m_account_address.m_spend_public_key); m_prv_spend_key = epee::string_tools::pod_to_hex(unwrap(unwrap(keys.m_spend_secret_key))); + m_key_image_cache = std::make_shared(); if (m_prv_spend_key == "0000000000000000000000000000000000000000000000000000000000000000") m_prv_spend_key = ""; m_is_closed = false; } diff --git a/src/wallet/monero_wallet_keys.h b/src/wallet/monero_wallet_keys.h index 3d8d71d7..be0aa665 100644 --- a/src/wallet/monero_wallet_keys.h +++ b/src/wallet/monero_wallet_keys.h @@ -54,7 +54,9 @@ #include "monero_wallet.h" #include "cryptonote_basic/account.h" +#include "cryptonote_basic/subaddress_index.h" #include +#include using namespace monero; @@ -63,6 +65,18 @@ using namespace monero; */ namespace monero { + class monero_key_image_cache { + public: + + 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); + bool request(const std::string& tx_public_key, uint64_t out_index, uint32_t account_idx, uint32_t subaddress_idx); + + private: + mutable boost::mutex m_mutex; + serializable_unordered_map, bool>>>> m_cache; + }; + /** * Implements a Monero wallet to provide basic key management. */ @@ -134,7 +148,7 @@ namespace monero { // --------------------------------- PRIVATE -------------------------------- - private: + protected: bool m_is_view_only = false; monero_network_type m_network_type; cryptonote::account_base m_account; @@ -146,8 +160,11 @@ namespace monero { std::string m_prv_spend_key; std::string m_primary_address; std::atomic m_is_closed{false}; + std::shared_ptr m_key_image_cache; - void init_common(); + virtual void init_common(); void assert_not_closed() const; + std::shared_ptr generate_key_image(const std::string& tx_public_key, uint64_t out_index, uint32_t account_idx, uint32_t subaddress_idx, const boost::optional& expected_output_public_key) const; + bool is_key_image_ours(const std::string &key_image_hex, const std::string& tx_public_key, uint64_t out_index, uint32_t account_idx, uint32_t subaddress_idx, const boost::optional& expected_output_public_key) const; }; } diff --git a/src/wallet/monero_wallet_light.cpp b/src/wallet/monero_wallet_light.cpp new file mode 100644 index 00000000..10dce4c3 --- /dev/null +++ b/src/wallet/monero_wallet_light.cpp @@ -0,0 +1,4008 @@ +/** + * Copyright (c) everoddandeven + * + * 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 + * Parts of this file are originally copyright (c) 2014-2019, MyMonero.com + * + * 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 std::list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this std::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.h" +#include "monero_wallet_light_model.h" +#include "utils/gen_utils.h" +#include "utils/monero_wallet_utils.h" +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "cryptonote_core/cryptonote_tx_utils.h" +#include "ringct/rctSigs.h" +#include "mnemonics/electrum-words.h" +#include "mnemonics/english.h" +#include "common/threadpool.h" +#include "net/jsonrpc_structs.h" +#include "serialization/serialization.h" +#include "common/monero_error.h" +#include "device/device.hpp" +#include "device/device_cold.hpp" +#include +#include + +#define OUTPUT_EXPORT_FILE_MAGIC "Monero output export\004" +#define APPROXIMATE_INPUT_BYTES 80 + +namespace monero { + + // ------------------------- INITIALIZE CONSTANTS --------------------------- + + static const int BULLETPROOF_VERSION = 4; // default bulletproof version + static const uint8_t DEFAULT_FEE_PRIORITY = 1; + static const uint32_t MIXIN_SIZE = 15; + static const uint64_t DUST_THRESHOLD = 2000000000; + static const size_t MAX_TX_INPUTS = 150; + + // --------------------------- LWS CLIENT -------------------------- + + class lws_client { + public: + lws_client(const std::shared_ptr& rpc, const std::string& primary_address, const std::string& private_view_key): m_rpc(rpc), m_primary_address(primary_address), m_prv_view_key(private_view_key) { } + + std::shared_ptr get_rpc_connection() const { return m_rpc; } + + std::shared_ptr get_daemon_status() const { + auto result = m_rpc->send_path_request("daemon_status"); + auto response = std::make_shared(); + monero_daemon_status::from_property_tree(result, response); + return response; + } + + std::shared_ptr login(bool create_account = true, bool generated_locally = true) const { + auto params = std::make_shared(m_primary_address, m_prv_view_key, create_account, generated_locally); + auto result = m_rpc->send_path_request("login", params); + auto response = std::make_shared(); + monero_login_response::from_property_tree(result, response); + return response; + } + + std::shared_ptr get_address_info() const { + auto params = std::make_shared(m_primary_address, m_prv_view_key); + auto result = m_rpc->send_path_request("get_address_info", params); + auto response = std::make_shared(); + monero_get_address_info_response::from_property_tree(result, response); + return response; + } + + std::shared_ptr get_address_txs() const { + auto params = std::make_shared(m_primary_address, m_prv_view_key); + auto result = m_rpc->send_path_request("get_address_txs", params); + auto response = std::make_shared(); + monero_get_address_txs_response::from_property_tree(result, response); + return response; + } + + std::shared_ptr get_unspent_outs(uint64_t amount, uint32_t mixin, bool use_dust = true, uint64_t dust_threshold = 0) const { + auto params = std::make_shared(m_primary_address, m_prv_view_key, amount, mixin, use_dust, dust_threshold); + auto result = m_rpc->send_path_request("get_unspent_outs", params); + auto response = std::make_shared(); + monero_get_unspent_outs_response::from_property_tree(result, response); + return response; + } + + std::shared_ptr get_random_outs(const std::vector& amounts, uint32_t count) const { + auto params = std::make_shared(count, amounts); + auto result = m_rpc->send_path_request("get_random_outs", params); + auto response = std::make_shared(); + monero_get_random_outs_response::from_property_tree(result, response); + return response; + } + + std::shared_ptr get_subaddrs() const { + auto params = std::make_shared(m_primary_address, m_prv_view_key); + auto result = m_rpc->send_path_request("get_subaddrs", params); + auto response = std::make_shared(); + monero_subaddrs_response::from_property_tree(result, response); + return response; + } + + std::shared_ptr upsert_subaddrs(const monero_subaddrs& subaddrs, bool get_all = true) const { + auto params = std::make_shared(m_primary_address, m_prv_view_key, subaddrs, get_all); + auto result = m_rpc->send_path_request("upsert_subaddrs", params); + auto response = std::make_shared(); + monero_subaddrs_response::from_property_tree(result, response); + return response; + } + + std::shared_ptr import_request(uint64_t from_height) const { + auto params = std::make_shared(m_primary_address, m_prv_view_key, from_height); + auto result = m_rpc->send_path_request("import_wallet_request", params); + auto response = std::make_shared(); + monero_import_wallet_response::from_property_tree(result, response); + return response; + } + + std::shared_ptr submit_raw_tx(const std::string& tx) const { + auto params = std::make_shared(tx); + auto result = m_rpc->send_path_request("submit_raw_tx", params); + auto response = std::make_shared(); + monero_submit_raw_tx_response::from_property_tree(result, response); + return response; + } + + bool is_connected() const { + try { + auto response = get_daemon_status(); + return response->m_state != boost::none && response->m_state.get() != std::string("unavailable"); + } catch (...) { + return false; + } + } + + private: + std::shared_ptr m_rpc; + std::string m_primary_address; + std::string m_prv_view_key; + }; + + // ----------------------- INTERNAL PRIVATE HELPERS ----------------------- + + bool output_before(const std::shared_ptr& ow1, const std::shared_ptr& 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()) { + if (ow1 == ow2) return false; + return ow1->m_public_key.get() < ow2->m_public_key.get(); + } + } + } + return false; + } + + void normalize_subaddress_indices(const std::shared_ptr& tx, const tools::wallet2::pending_tx& ptx, const std::vector>& outs) { + const std::set used_indexes(ptx.selected_transfers.begin(), ptx.selected_transfers.end()); + std::unordered_map global_idx_to_subaddr; + for (const auto& out : outs) { + if (out->m_cache_index != boost::none && used_indexes.count(*out->m_cache_index)) { + global_idx_to_subaddr[out->m_global_index.get()] = out->m_recipient->m_min_i; + } + } + + const auto& sources = ptx.construction_data.sources; + for (size_t i = 0; i < sources.size() && i < tx->m_inputs.size(); i++) { + const auto& src = sources[i]; + if (src.real_output >= src.outputs.size()) continue; + const auto it = global_idx_to_subaddr.find(src.outputs[src.real_output].first); + if (it == global_idx_to_subaddr.end()) continue; + auto input = std::dynamic_pointer_cast(tx->m_inputs[i]); + if (input) input->m_subaddress_index = it->second; + } + } + + /** + * Builds a tools::wallet2::pending_tx for a light wallet transfer. + * + * Implementation based on mymonero-core-cpp's monero_transfer_utils::create_transaction(). + */ + class tx_builder { + public: + tx_builder(cryptonote::network_type nettype, const cryptonote::account_keys& sender_account_keys, bool view_only, lws_client& client, const std::shared_ptr& cache): m_nettype(nettype), m_sender_account_keys(sender_account_keys), m_view_only(view_only), m_client(client), m_cache(cache) {} + + tools::wallet2::pending_tx build(const uint32_t subaddr_account_idx, const std::vector &to_address_strings, 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_mask, cryptonote::blobdata& tx_blob, const std::set& subtract_fee_from = {}) const { + MTRACE("tx_builder::build()"); + if (payment_id_string != boost::none && !payment_id_string->empty()) throw std::runtime_error("Standalone payment IDs are obsolete. Use subaddresses or integrated addresses instead"); + boost::optional prior_fee_attempt; + boost::optional prior_tie_attempt; + size_t construction_attempt = 0; + // fee_per_b is already the per-priority rate from get_base_fee(); nothing left to scale + const uint64_t fee_multiplier = 1; + + // requests decoys and builds the tx, reconstructing with a corrected fee (reusing already-tied decoys) + // if the actual serialized weight needs more fee than was assumed when selecting inputs. + // implementation based mymonero-core-cpp's monero_send_routine.cpp _reenterable_construct_and_send_tx retry loop. + tools::wallet2::pending_tx ptx; + while (true) { + MTRACE("tx_builder::build(): attempt " << construction_attempt + 1); + const auto output_selection = select_outputs(payment_id_string, sending_amounts, is_sweeping, simple_priority, unspent_outs, fee_per_b, fee_mask, prior_fee_attempt, subtract_fee_from, prior_tie_attempt); + if (output_selection.m_selected_outs.size() == 0) throw std::runtime_error("No output to select"); + + const auto decoys = fetch_decoys(output_selection.m_selected_outs, prior_tie_attempt); + auto tied_outs = monero_outputs_decoys_tie::tie(output_selection.m_selected_outs, decoys, prior_tie_attempt); + std::vector selected_transfers = output_selection.get_output_indexes(); + // re-derived every attempt since output_selection.m_fee can change between retries + const std::vector actual_sending_amounts = is_sweeping ? std::vector{output_selection.m_amount} : get_adjusted_amounts(sending_amounts, subtract_fee_from, output_selection.m_fee); + + ptx = build_pending_tx(subaddr_account_idx, to_address_strings, payment_id_string, actual_sending_amounts, selected_transfers, output_selection.m_change_amount, output_selection.m_fee, output_selection.m_selected_outs, tied_outs.m_decoys); + tx_blob = cryptonote::tx_to_blob(ptx.tx); + uint64_t weight = cryptonote::get_transaction_weight(ptx.tx, tx_blob.size()); + uint64_t fee_actually_needed = monero_wallet_utils::calculate_fee_from_weight(fee_per_b, weight, fee_multiplier, fee_mask); + + if (fee_actually_needed <= output_selection.m_fee) break; + if (++construction_attempt > 15) throw std::runtime_error("Unable to construct a transaction with sufficient fee for unknown reason."); + + prior_fee_attempt = fee_actually_needed; + prior_tie_attempt = tied_outs.m_tie_attempt; + } + + return ptx; + } + + // implementation based on monero-project's wallet2::get_tx_proof() + uint64_t compute_amount_received(const cryptonote::transaction& tx, const crypto::secret_key& tx_key, const std::vector& additional_tx_keys, const cryptonote::account_public_address& address) const { + hw::device& hwdev = m_sender_account_keys.get_device(); + const bool is_out = m_cache->m_subaddresses.find(address.m_spend_public_key) == m_cache->m_subaddresses.end(); + + std::vector shared_secret; + rct::key aP; + if (is_out) { + shared_secret.resize(1 + additional_tx_keys.size()); + hwdev.scalarmultKey(aP, rct::pk2rct(address.m_view_public_key), rct::sk2rct(tx_key)); + shared_secret[0] = rct::rct2pk(aP); + for (size_t i = 1; i < shared_secret.size(); ++i) { + hwdev.scalarmultKey(aP, rct::pk2rct(address.m_view_public_key), rct::sk2rct(additional_tx_keys[i - 1])); + shared_secret[i] = rct::rct2pk(aP); + } + } else { + crypto::public_key tx_pub_key = cryptonote::get_tx_pub_key_from_extra(tx); + if (tx_pub_key == crypto::null_pkey) throw std::runtime_error("Tx pubkey was not found"); + std::vector additional_tx_pub_keys = cryptonote::get_additional_tx_pub_keys_from_extra(tx); + shared_secret.resize(1 + additional_tx_pub_keys.size()); + const crypto::secret_key& a = m_sender_account_keys.m_view_secret_key; + hwdev.scalarmultKey(aP, rct::pk2rct(tx_pub_key), rct::sk2rct(a)); + shared_secret[0] = rct::rct2pk(aP); + for (size_t i = 1; i < shared_secret.size(); ++i) { + hwdev.scalarmultKey(aP, rct::pk2rct(additional_tx_pub_keys[i - 1]), rct::sk2rct(a)); + shared_secret[i] = rct::rct2pk(aP); + } + } + return received_from_shared_secrets(tx, shared_secret, address); + } + + // implementation based on monero-project's wallet2::check_tx_key() + uint64_t compute_amount_received_from_key(const cryptonote::transaction& tx, const crypto::secret_key& tx_key, const std::vector& additional_tx_keys, const cryptonote::account_public_address& address) const { + hw::device& hwdev = m_sender_account_keys.get_device(); + std::vector shared_secret(1 + additional_tx_keys.size()); + rct::key aP; + hwdev.scalarmultKey(aP, rct::pk2rct(address.m_view_public_key), rct::sk2rct(tx_key)); + shared_secret[0] = rct::rct2pk(aP); + for (size_t i = 1; i < shared_secret.size(); ++i) { + hwdev.scalarmultKey(aP, rct::pk2rct(address.m_view_public_key), rct::sk2rct(additional_tx_keys[i - 1])); + shared_secret[i] = rct::rct2pk(aP); + } + return received_from_shared_secrets(tx, shared_secret, address); + } + + private: + cryptonote::network_type m_nettype; + const cryptonote::account_keys& m_sender_account_keys; + bool m_view_only; + lws_client& m_client; + std::shared_ptr m_cache; + + uint64_t received_from_shared_secrets(const cryptonote::transaction& tx, const std::vector& shared_secret, const cryptonote::account_public_address& address) const { + hw::device& hwdev = m_sender_account_keys.get_device(); + const size_t num_sigs = shared_secret.size(); + + crypto::key_derivation derivation; + if (!crypto::generate_key_derivation(shared_secret[0], rct::rct2sk(rct::I), derivation)) throw std::runtime_error("Failed to generate key derivation"); + std::vector additional_derivations(num_sigs - 1); + for (size_t i = 1; i < num_sigs; ++i) { + if (!crypto::generate_key_derivation(shared_secret[i], rct::rct2sk(rct::I), additional_derivations[i - 1])) throw std::runtime_error("Failed to generate key derivation"); + } + + uint64_t received = 0; + for (size_t n = 0; n < tx.vout.size(); ++n) { + crypto::public_key output_public_key; + if (!cryptonote::get_output_public_key(tx.vout[n], output_public_key)) continue; + + crypto::key_derivation found_derivation; + if (is_out_to_acc(address, output_public_key, derivation, additional_derivations, n, cryptonote::get_output_view_tag(tx.vout[n]), found_derivation)) { + uint64_t amount; + if (tx.version == 1 || tx.rct_signatures.type == rct::RCTTypeNull) amount = tx.vout[n].amount; + else { + // mirrors wallet2.cpp's file-local decodeRct(): derive the per-output scalar first, then + // decode via decodeRctSimple/decodeRct depending on rct type + crypto::secret_key scalar1; + hwdev.derivation_to_scalar(found_derivation, n, scalar1); + rct::key mask; + switch (tx.rct_signatures.type) { + case rct::RCTTypeSimple: + case rct::RCTTypeBulletproof: + case rct::RCTTypeBulletproof2: + case rct::RCTTypeCLSAG: + case rct::RCTTypeBulletproofPlus: + amount = rct::decodeRctSimple(tx.rct_signatures, rct::sk2rct(scalar1), n, mask, hwdev); + break; + case rct::RCTTypeFull: + amount = rct::decodeRct(tx.rct_signatures, rct::sk2rct(scalar1), n, mask, hwdev); + break; + default: + amount = 0; + break; + } + } + received += amount; + } + } + return received; + } + + // validates sender keys + void validate_keys() const { + if (m_view_only) { + if (!m_sender_account_keys.get_device().verify_keys(m_sender_account_keys.m_view_secret_key, m_sender_account_keys.m_account_address.m_view_public_key)) { + throw std::runtime_error("Invalid view keys"); + } + } + else { + if (!m_sender_account_keys.get_device().verify_keys(m_sender_account_keys.m_spend_secret_key, m_sender_account_keys.m_account_address.m_spend_public_key) + || !m_sender_account_keys.get_device().verify_keys(m_sender_account_keys.m_view_secret_key, m_sender_account_keys.m_account_address.m_view_public_key)) { + throw std::runtime_error("Invalid secret keys"); + } + } + } + + // validates transfer inputs + static 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"); + } + + // returns destination amounts adjusted for given fee if subtract_fee_from is enabled + // implementation based on wallet2's internal TX::get_adjusted_dsts() + static std::vector get_adjusted_amounts(const std::vector& sending_amounts, const std::set& subtract_fee_from, uint64_t needed_fee) { + // subtract_fee_from is not enabled or no more remaning needed_fee for this tx + if (subtract_fee_from.empty() || needed_fee == 0) return sending_amounts; + + uint64_t subtractable_total = 0; + for (uint32_t idx : subtract_fee_from) { + if (idx >= sending_amounts.size()) throw std::runtime_error("Invalid destination index to subtract fee from: " + std::to_string(idx)); + subtractable_total += sending_amounts[idx]; + } + if (subtractable_total < needed_fee) throw std::runtime_error("Destinations selected to subtract fee from are too small to cover the fee"); + + std::vector result = sending_amounts; + uint64_t remaining = needed_fee; + auto it = subtract_fee_from.cbegin(); + uint64_t amount_to_subtract = 0; + while (remaining) { + // set the amount to subtract iterating at the beginning of the list so equal amounts are + // subtracted throughout the list of destinations. We use max(x, 1) so that we we still step + // forwards even when the amount remaining is less than the number of subtractable indices + if (it == subtract_fee_from.cbegin()) amount_to_subtract = std::max(remaining / subtract_fee_from.size(), 1); + + uint64_t& amount = result[*it]; + if (amount <= amount_to_subtract) throw std::runtime_error("Subtracting fee from destination would leave it with a zero or negative amount"); + remaining -= amount_to_subtract; + amount -= amount_to_subtract; + ++it; + + // wrap around to the first subtractable index once we hit the end of the list + if (it == subtract_fee_from.cend()) it = subtract_fee_from.cbegin(); + } + + return result; + } + + // select outputs from get_random_outs request + // implementation based on mymonero-core-cpp's monero_transfer_utils::send_step1__prepare_params_for_get_decoys() + static monero_output_selection select_outputs(const boost::optional& payment_id, 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_fee_attempt, const std::set& subtract_fee_from, boost::optional prior_tie_attempt = boost::none) { + // validate sending amounts + if (!is_sweeping) { + for (uint64_t sending_amount : sending_amounts) { + if (sending_amount == 0) throw std::runtime_error("entered amount is too low"); + } + } + + monero_output_selection params; + params.m_mixin = MIXIN_SIZE; + + std::vector extra; + monero_wallet_utils::add_pid_to_tx_extra(payment_id, extra); + + const uint64_t base_fee = fee_per_b; + // fee_per_b is already resolved for this priority tier (see get_base_fee()); no scaling left + const uint64_t fee_multiplier = 1; + const size_t expected_n_outputs = (is_sweeping ? 1 : sending_amounts.size()) + 1; + + 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_fee_attempt == boost::none) attempt_at_min_fee = monero_wallet_utils::estimate_fee(1, MIXIN_SIZE, expected_n_outputs, extra.size(), base_fee, fee_multiplier, fee_quantization_mask); + else attempt_at_min_fee = *prior_fee_attempt; + + // 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; + // based on wallet2::create_transactions_2(): total_needed_money = needed_money + (subtract_fee_from_outputs.size() ? 0 : min_fee)) + potential_total = sum_sending_amounts + (subtract_fee_from.empty() ? attempt_at_min_fee : 0); + } + + // Gather outputs and amount to use for getting decoy outputs + uint64_t selected_outs_amount = 0; + // take copy so not to modify original + std::vector> remaining_outs = unspent_outs; + + // start by using all the passed in outs that were selected in a prior tx construction attempt + if (prior_tie_attempt != boost::none) { + for (size_t i = 0; i < remaining_outs.size(); ) { + auto &out = remaining_outs[i]; + // search for out by public key to see if it should be re-used in an attempt + if (prior_tie_attempt->find(out->m_public_key.get()) != prior_tie_attempt->end()) { + selected_outs_amount += out->m_amount.get(); + // pop_index swaps the last element into index i, so re-check the same index rather than advancing + params.m_selected_outs.push_back(gen_utils::pop_index(remaining_outs, i)); + } else { + ++i; + } + } + } + + while (selected_outs_amount < potential_total && remaining_outs.size() > 0) { + if (params.m_selected_outs.size() >= MAX_TX_INPUTS) { + if (is_sweeping) break; // the sweep loop makes another tx from what's left + throw std::runtime_error("Too many inputs needed for a single transaction (max " + std::to_string(MAX_TX_INPUTS) + "): consolidate the wallet's outputs first."); + } + + if (is_sweeping && !params.m_selected_outs.empty()) { + const uint64_t estimated_weight = monero_wallet_utils::estimate_tx_weight(boost::numeric_cast(params.m_selected_outs.size() + 1), MIXIN_SIZE, expected_n_outputs, extra.size()); + // stop pulling in more outputs once the tx would exceed a safe weight margin + // based on wallet2.cpp's TX_WEIGHT_TARGET + if (estimated_weight >= monero_wallet_utils::get_tx_weight_limit() * 2 / 3) break; + } + + auto out = gen_utils::pop_random_value(remaining_outs); + if (out->m_amount.get() < DUST_THRESHOLD && !out->is_rct()) { + // unmixable (non-rct) dusty output + continue; + } + selected_outs_amount += out->m_amount.get(); + params.m_selected_outs.push_back(std::move(out)); + } + + //if (/*selected_outs.size() > 1*/) FIXME? see original mymonero core js + uint64_t needed_fee = monero_wallet_utils::estimate_fee(params.m_selected_outs.size(), MIXIN_SIZE, expected_n_outputs, 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_fee_attempt != 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 selected_outs_amount < total_incl_fees and gets finalized (for this function's scope) as fee + uint64_t total_wo_fee = is_sweeping ? /*now that we know outsAmount>needed_fee*/(selected_outs_amount - needed_fee) : sum_sending_amounts; + params.m_amount = total_wo_fee; + + uint64_t total_incl_fees; + if (is_sweeping) { + if (selected_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 selected_outs_amount (spendable balance) will have been stored for return by this point + throw std::runtime_error("need more money than found; sweeping, selected_outs_amount: " + std::to_string(selected_outs_amount) + ", needed_fee: " + std::to_string(needed_fee)); + } + + total_incl_fees = selected_outs_amount; + } else { + // because fee changed because selected_outs.size() was updated + total_incl_fees = sum_sending_amounts + (subtract_fee_from.empty() ? needed_fee : 0); + while (selected_outs_amount < total_incl_fees && remaining_outs.size() > 0) { + if (params.m_selected_outs.size() >= MAX_TX_INPUTS) { + throw std::runtime_error("Too many inputs needed for a single transaction (max " + std::to_string(MAX_TX_INPUTS) + "): consolidate the wallet's outputs first."); + } + // add outputs 1 at a time till we either have them all or can meet the fee + { + auto out = gen_utils::pop_random_value(remaining_outs); + if (out->m_amount.get() < DUST_THRESHOLD && !out->is_rct()) continue; // unmixable pre-RingCT dust + selected_outs_amount += out->m_amount.get(); + params.m_selected_outs.push_back(std::move(out)); + } + + { + // based on wallet2::create_transactions_2() + const uint64_t estimated_weight = monero_wallet_utils::estimate_tx_weight(boost::numeric_cast(params.m_selected_outs.size()), MIXIN_SIZE, expected_n_outputs, extra.size()); + if (estimated_weight >= monero_wallet_utils::get_tx_weight_limit() * 2 / 3) { + throw std::runtime_error("Too many small outputs are needed to cover this amount in a single transaction: consolidate the wallet's outputs first."); + } + } + + // recalculate fee, total including fees + needed_fee = monero_wallet_utils::estimate_fee(params.m_selected_outs.size(), MIXIN_SIZE, expected_n_outputs, extra.size(), base_fee, fee_multiplier, fee_quantization_mask); + // because fee changed + total_incl_fees = sum_sending_amounts + (subtract_fee_from.empty() ? needed_fee : 0); + } + } + + params.m_fee = needed_fee; + + if (selected_outs_amount < total_incl_fees) { + // sufficiently up-to-date (for this return case) required_balance and selected_outs_amount (spendable balance) will have been stored for return by this point. + throw std::runtime_error("need more money than found; selected_outs_amount: " + std::to_string(selected_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 (selected_outs_amount > total_incl_fees) { + if (is_sweeping) throw std::runtime_error("Unexpected total_incl_fees > selected_outs_amount while sweeping"); + change_amount = selected_outs_amount - total_incl_fees; + } + + params.m_change_amount = change_amount; + return params; + } + + // get random outputs + std::vector> fetch_decoys(const std::vector> &selected_outs, const boost::optional& prior_attempt) const { + // request decoys for any newly selected inputs + std::vector> decoy_requests; + if (prior_attempt != boost::none) { + for (size_t i = 0; i < selected_outs.size(); ++i) { + // only need to request decoys for outs that were not already passed in + if (prior_attempt->find(*selected_outs[i]->m_public_key) == prior_attempt->end()) { + decoy_requests.push_back(selected_outs[i]); + } + } + } else decoy_requests = selected_outs; + + std::vector decoy_amounts; + for (auto &using_out : decoy_requests) { + if (using_out->is_rct()) decoy_amounts.push_back(0); + else { + MDEBUG("pushing decoy request amount: " << using_out->m_amount.get()); + decoy_amounts.push_back(using_out->m_amount.get()); + } + } + + return m_client.get_random_outs(decoy_amounts, MIXIN_SIZE + 1)->m_amount_outs; + } + + // resolves each spendable output (plus its decoys) to a cryptonote::tx_source_entry, and tallies found_money / the spent key images along the way + std::vector prepare_sources(const std::vector> &outputs, std::vector> &mix_outs, const std::vector& extra, uint64_t& found_money, std::string& spent_key_images) const { + std::vector sources; + LOG_PRINT_L2("preparing outputs"); + for (size_t out_index = 0; out_index < outputs.size(); out_index++) { + const uint64_t amount = outputs[out_index]->m_amount.get(); + found_money += amount; + if (found_money < amount) 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.at(out_index)->m_outputs.begin(), mix_outs.at(out_index)->m_outputs.end(), [] ( + std::shared_ptr const& a, + std::shared_ptr const& b + ) { return a->m_global_index.get() < b->m_global_index.get(); }); + + std::vector> candidates; + candidates.reserve(mix_outs[out_index]->m_outputs.size()); + for (const auto& mix_out : mix_outs[out_index]->m_outputs) { + if (mix_out->m_global_index == outputs[out_index]->m_global_index) { + MDEBUG("got mixin the same as output, skipping"); + continue; + } + candidates.push_back(mix_out); + } + + if (candidates.size() > MIXIN_SIZE) candidates.erase(candidates.begin() + crypto::rand_idx(candidates.size())); + + for (size_t j = 0; j < candidates.size() && src.outputs.size() < MIXIN_SIZE; j++) { + const auto& mix_out__output = candidates[j]; + 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_wallet_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"); + // create identity-masked commitment for non-rct mix input + oe.second.mask = rct::zeroCommit(src.amount); + } + + 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_coinbase()) { + rct::key commit; + monero_wallet_utils::rct_hex_to_rct_commit(outputs[out_index]->m_rct.get(), commit); + // add commitment for real input + real_oe.second.mask = commit; + } else { + // create identity-masked commitment for non-rct input + real_oe.second.mask = rct::zeroCommit(src.amount/*aka outputs[out_index].amount*/); + } + + // 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); + + if (src.outputs.size() != MIXIN_SIZE + 1) throw std::runtime_error("not enough distinct outputs for mixing"); + for (size_t j = 1; j < src.outputs.size(); ++j) { + if (src.outputs[j].first == src.outputs[j - 1].first) throw std::runtime_error("duplicate mixin output from light wallet server"); + } + + 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_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_wallet_utils::rct_hex_to_decrypted_mask(outputs[out_index]->m_rct.get(), m_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); + 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"); + return sources; + } + + // builds the destination list (recipients + change), matching wallet2's decompose logic for a 0 change amount + std::vector prepare_destinations(const std::vector& to_addrs, const std::vector& to_address_strings, const std::vector& sending_amounts, uint64_t change_amount, cryptonote::tx_destination_entry& change_dst, uint32_t subaddr_account_idx) const { + // 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; + to_dst.is_integrated = to_addrs[i].has_payment_id; + to_dst.original = to_address_strings[i]; + splitted_dsts.push_back(to_dst); + } + + change_dst = cryptonote::tx_destination_entry{}; + 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 = m_sender_account_keys.get_device().get_subaddress(m_sender_account_keys, {subaddr_account_idx, 0}); + change_dst.is_subaddress = subaddr_account_idx != 0; // matches wallet2's own convention + splitted_dsts.push_back(change_dst); + } + + return splitted_dsts; + } + + // calls into cryptonote to sign the sources/destinations into an actual transaction, and packs the result into a pending_tx + tools::wallet2::pending_tx construct_pending_tx(std::vector& sources, std::vector& splitted_dsts, const std::vector& dsts, cryptonote::tx_destination_entry& change_dst, std::vector& extra, uint64_t fee_amount, std::vector& selected_transfers, uint32_t subaddr_account_idx, const std::vector> &outputs, const std::string& spent_key_images) const { + cryptonote::transaction tx; + crypto::secret_key tx_key; + std::vector additional_tx_keys; + + // build a subaddress map scoped to just this tx (change address plus each spent output's subaddress) + hw::device& hwdev = m_sender_account_keys.get_device(); + serializable_unordered_map tx_subaddresses; + const auto add_subaddress = [&](uint32_t major, uint32_t minor) { + const crypto::public_key spend_public_key = (major == 0 && minor == 0) + ? m_sender_account_keys.m_account_address.m_spend_public_key + : hwdev.get_subaddress_spend_public_key(m_sender_account_keys, {major, minor}); + tx_subaddresses[spend_public_key] = {major, minor}; + }; + add_subaddress(subaddr_account_idx, 0); // change always returns here, even if no selected output uses this index + + std::set subaddr_indices; + for (const auto& selected_out : outputs) { + if (selected_out->m_recipient->m_maj_i != subaddr_account_idx) continue; + const uint32_t minor = selected_out->m_recipient->m_min_i; + if (subaddr_indices.insert(minor).second) add_subaddress(subaddr_account_idx, minor); // derive once per distinct minor index + } + + const rct::RCTConfig rct_config {rct::RangeProofPaddedBulletproof, BULLETPROOF_VERSION}; + LOG_PRINT_L2("constructing tx"); + bool r = cryptonote::construct_tx_and_get_tx_key(m_sender_account_keys, tx_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"); + monero_wallet_utils::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 = 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 = dsts; + // record which subaddress indices are being used as inputs + ptx.construction_data.subaddr_account = subaddr_account_idx; + ptx.construction_data.subaddr_indices = std::move(subaddr_indices); + + LOG_PRINT_L2("transfer_selected_rct done"); + return ptx; + } + + tools::wallet2::pending_tx build_pending_tx(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) const { + std::vector extra; + std::vector to_addrs; + validate_transfer(to_address_strings, payment_id_string, m_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"); + } + + validate_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::string spent_key_images; + std::vector sources = prepare_sources(outputs, mix_outs, extra, found_money, spent_key_images); + + cryptonote::tx_destination_entry change_dst = AUTO_VAL_INIT(change_dst); + std::vector splitted_dsts = prepare_destinations(to_addrs, to_address_strings, sending_amounts, change_amount, change_dst, subaddr_account_idx); + + std::vector dsts; + dsts.reserve(to_addrs.size()); + for (size_t i = 0; i < to_addrs.size(); ++i) { + cryptonote::tx_destination_entry dst = AUTO_VAL_INIT(dst); + dst.addr = to_addrs[i].address; + dst.amount = sending_amounts[i]; + dst.is_subaddress = to_addrs[i].is_subaddress; + dsts.push_back(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"); + + tools::wallet2::pending_tx ptx = construct_pending_tx(sources, splitted_dsts, dsts, change_dst, extra, fee_amount, selected_transfers, subaddr_account_idx, outputs, spent_key_images); + sanity_check(ptx, to_addrs, sending_amounts); + return ptx; + } + + // checks whether an output at output_index was sent to address. + // implementation based on monero-project's wallet2::is_out_to_acc(). + static bool is_out_to_acc(const cryptonote::account_public_address& address, const crypto::public_key& out_key, const crypto::key_derivation& derivation, const std::vector& additional_derivations, const size_t output_index, const boost::optional& view_tag_opt, crypto::key_derivation& found_derivation) { + crypto::public_key derived_out_key; + bool found = false; + + if (cryptonote::out_can_be_to_acc(view_tag_opt, derivation, output_index)) { + if (!crypto::derive_public_key(derivation, output_index, address.m_spend_public_key, derived_out_key)) throw std::runtime_error("Failed to derive public key"); + if (out_key == derived_out_key) { + found = true; + found_derivation = derivation; + } + } + + if (!found && !additional_derivations.empty()) { + const crypto::key_derivation& additional_derivation = additional_derivations[output_index]; + if (cryptonote::out_can_be_to_acc(view_tag_opt, additional_derivation, output_index)) { + if (!crypto::derive_public_key(additional_derivation, output_index, address.m_spend_public_key, derived_out_key)) throw std::runtime_error("Failed to derive public key"); + if (out_key == derived_out_key) { + found = true; + found_derivation = additional_derivation; + } + } + } + + return found; + } + + // catches integrated address built from a subaddress (see https://github.com/monero-project/monero/issues/8380). + // implementation based on wallet2::sanity_check(). + void sanity_check(const tools::wallet2::pending_tx& ptx, const std::vector& to_addrs, const std::vector& sending_amounts) const { + 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) { + uint64_t received = compute_amount_received(ptx.tx, ptx.tx_key, ptx.additional_tx_keys, to_addrs[i].address); + if (received < sending_amounts[i]) { + throw std::runtime_error("Total received by " + cryptonote::get_account_address_as_str(m_nettype, to_addrs[i].is_subaddress, to_addrs[i].address) + + ": " + cryptonote::print_money(received) + ", expected " + cryptonote::print_money(sending_amounts[i])); + } + } + } + }; + + struct built_tx { + tools::wallet2::pending_tx ptx; + std::shared_ptr tx; + std::string full_hex; + std::string change_pubkey; + }; + + // ----------------------------- WALLET LISTENER ---------------------------- + + /** + * Notifies external wallet listeners of monero_wallet_light activity. + */ + struct wallet_light_listener { + + public: + + /** + * Constructs the listener. + * + * @param wallet provides context to notify external listeners + */ + wallet_light_listener(monero_wallet_light& wallet) : m_wallet(wallet) { + 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(2)); + } + + ~wallet_light_listener() { + MTRACE("~wallet_light_listener()"); + m_notification_pool.reset(); + } + + void flush_pending_notifications() { + boost::lock_guard dispatch_lock(m_dispatch_mutex); + } + + bool is_dispatching_on_calling_thread() const { + if (!m_dispatch_mutex.try_lock()) return false; + bool result = m_dispatch_depth > 0 && m_dispatch_thread_id == boost::this_thread::get_id(); + m_dispatch_mutex.unlock(); + return result; + } + + void on_sync_start(uint64_t start_height) { + uint64_t sync_end_height = m_wallet.get_daemon_height(); + tools::threadpool::waiter waiter(*m_notification_pool); + m_notification_pool->submit(&waiter, [this, start_height, sync_end_height]() { + boost::lock_guard dispatch_lock(m_dispatch_mutex); + dispatch_thread_marker thread_marker(*this); + boost::lock_guard lock(m_state_mutex); + 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 = sync_end_height; + }); + if (!waiter.wait()) throw std::runtime_error("Failed to start sync notification, is previous sync in progress?"); + } + + void on_sync_end() { + tools::threadpool::waiter waiter(*m_notification_pool); + m_notification_pool->submit(&waiter, [this]() { + boost::lock_guard dispatch_lock(m_dispatch_mutex); + dispatch_thread_marker thread_marker(*this); + auto reset_heights = [this]() { + boost::lock_guard lock(m_state_mutex); + m_sync_start_height = boost::none; + m_sync_end_height = boost::none; + }; + try { + check_for_changed_balances(); + check_for_changed_txs(); + } catch (...) { + reset_heights(); + throw; + } + reset_heights(); + }); + waiter.wait(); + } + + void on_new_block(uint64_t height) { + if (m_wallet.get_listeners().empty()) return; + + // ignore notifications before sync start height, irrelevant to clients + { + boost::lock_guard lock(m_state_mutex); + 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]() { + boost::lock_guard dispatch_lock(m_dispatch_mutex); + dispatch_thread_marker thread_marker(*this); + + // notify listeners of new block + for (monero_wallet_listener* listener : m_wallet.get_listeners()) { + listener->on_new_block(height); + } + + // notify listeners of sync progress + uint64_t sync_start_height, sync_end_height; + { + boost::lock_guard lock(m_state_mutex); + if (height >= *m_sync_end_height) m_sync_end_height = height + 1; // increase end height if necessary + sync_start_height = *m_sync_start_height; + sync_end_height = *m_sync_end_height; + } + double percent_done = (double) (height - sync_start_height + 1) / (double) (sync_end_height - sync_start_height); + std::string message = std::string("Synchronizing"); + for (monero_wallet_listener* listener : m_wallet.get_listeners()) { + listener->on_sync_progress(height, sync_start_height, sync_end_height, percent_done, message); + } + }); + 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]() { + boost::lock_guard dispatch_lock(m_dispatch_mutex); + dispatch_thread_marker thread_marker(*this); + check_for_changed_balances(); + for (const std::shared_ptr& tx : txs) { + notify_outputs(tx); + + // seed tracking state so the next on_sync_end() diff doesn't re-notify this tx as "new" + if (tx->m_hash != boost::none) { + boost::lock_guard lock(m_state_mutex); + m_prev_known_tx_hashes.insert(tx->m_hash.get()); + if (tx->m_is_locked.value_or(true)) m_prev_locked_tx_hashes.insert(tx->m_hash.get()); + } + } + }); + waiter.wait(); + } + + private: + monero_wallet_light& m_wallet; // wallet to provide context for notifications + boost::optional m_sync_start_height; + boost::optional m_sync_end_height; + uint64_t m_prev_balance; + uint64_t m_prev_unlocked_balance; + std::set m_prev_known_tx_hashes; // txs seen as of the last diff, to detect newly-appeared ones + std::set m_prev_locked_tx_hashes; // locked txs seen as of the last diff, to detect newly-unlocked ones + std::set m_prev_confirmed_tx_hashes; // confirmed txs seen as of the last diff, to detect newly-confirmed ones + std::unique_ptr m_notification_pool; // threadpool of size 2 (1 real worker) to queue notifications for external announcement + boost::mutex m_state_mutex; + mutable boost::recursive_mutex m_dispatch_mutex; // held for the duration of each dispatch's get_listeners()+iterate, see flush_pending_notifications() + size_t m_dispatch_depth = 0; // > 0 while a dispatch is executing, guarded by m_dispatch_mutex; see is_dispatching_on_calling_thread() + boost::thread::id m_dispatch_thread_id; // which thread is currently dispatching, valid only while m_dispatch_depth > 0 + + // RAII: records which thread is executing a dispatch and for how many nested levels + struct dispatch_thread_marker { + wallet_light_listener& self; + dispatch_thread_marker(wallet_light_listener& self) : self(self) { + if (self.m_dispatch_depth++ == 0) self.m_dispatch_thread_id = boost::this_thread::get_id(); + } + ~dispatch_thread_marker() { --self.m_dispatch_depth; } + }; + + bool check_for_changed_balances() { + uint64_t balance = m_wallet.get_balance(); + uint64_t unlocked_balance = m_wallet.get_unlocked_balance(); + bool changed; + { + boost::lock_guard lock(m_state_mutex); + changed = balance != m_prev_balance || unlocked_balance != m_prev_unlocked_balance; + if (changed) { + m_prev_balance = balance; + m_prev_unlocked_balance = unlocked_balance; + } + } + if (changed) { + for (monero_wallet_listener* listener : m_wallet.get_listeners()) { + listener->on_balances_changed(balance, unlocked_balance); + } + } + return changed; + } + + void check_for_changed_txs() { + if (m_wallet.get_listeners().empty()) return; + + // get recent txs to check for new or newly-unlocked activity + monero_tx_query query = monero_tx_query(); + query.m_include_outputs = true; + uint64_t height = m_wallet.get_height(); + uint64_t min_height = height >= 70 ? height - 70 : 0; // 70-block window is enough for the unlock diff + { + boost::lock_guard lock(m_state_mutex); + if (m_sync_start_height != boost::none && *m_sync_start_height < min_height) min_height = *m_sync_start_height; + } + query.m_min_height = min_height; + std::vector> recent_txs = m_wallet.get_txs(query); + + std::set current_tx_hashes; + std::set current_locked_tx_hashes; + std::set current_confirmed_tx_hashes; + std::vector> txs_to_notify; + + { + boost::lock_guard lock(m_state_mutex); + for (const std::shared_ptr& tx : recent_txs) { + const std::string& hash = tx->m_hash.get(); + bool locked = tx->m_is_locked.value_or(false); + bool confirmed = tx->m_is_confirmed.value_or(false); + current_tx_hashes.insert(hash); + if (locked) current_locked_tx_hashes.insert(hash); + if (confirmed) current_confirmed_tx_hashes.insert(hash); + + bool is_new = m_prev_known_tx_hashes.find(hash) == m_prev_known_tx_hashes.end(); + bool newly_unlocked = !locked && m_prev_locked_tx_hashes.find(hash) != m_prev_locked_tx_hashes.end(); + bool newly_confirmed = confirmed && m_prev_confirmed_tx_hashes.find(hash) == m_prev_confirmed_tx_hashes.end(); + if (is_new || newly_unlocked || newly_confirmed) txs_to_notify.push_back(tx); + } + + m_prev_known_tx_hashes = current_tx_hashes; + m_prev_locked_tx_hashes = current_locked_tx_hashes; + m_prev_confirmed_tx_hashes = current_confirmed_tx_hashes; + } + + for (const std::shared_ptr& tx : txs_to_notify) notify_outputs(tx); + + // free memory + monero_utils::free(recent_txs); + } + + void notify_outputs(const std::shared_ptr& tx) { + + // notify spent outputs + if (tx->m_outgoing_transfer != nullptr) { + + // 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->m_amount.get() + tx->m_fee.get(); + input->m_account_index = tx->m_outgoing_transfer->m_account_index; + if (tx->m_outgoing_transfer->m_subaddress_indices.size() == 1) input->m_subaddress_index = tx->m_outgoing_transfer->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 != nullptr) { + 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_light::wallet_exists(const std::string& primary_address, const std::string& private_view_key, const std::shared_ptr& rpc) { + MTRACE("monero_wallet_light::wallet_exists(" << primary_address << ")"); + try { + lws_client client(rpc, primary_address, private_view_key); + client.login(false, false); + return true; + } + catch (const monero_rpc_error& ex) { + if (ex.code == 403) return false; + throw; + } + } + + bool monero_wallet_light::wallet_exists(const monero_wallet_config& config, const std::shared_ptr& rpc) { + MTRACE("monero_wallet_light::wallet_exists(config)"); + if (rpc == nullptr || rpc->m_uri == boost::none) throw std::runtime_error("Cannot check if wallet exists without a valid RPC connection"); + + std::string seed = config.m_seed != boost::none ? config.m_seed.get() : ""; + std::string primary_address = config.m_primary_address != boost::none ? config.m_primary_address.get() : ""; + std::string private_view_key = config.m_private_view_key != boost::none ? config.m_private_view_key.get() : ""; + + if (seed.empty() && primary_address.empty() && private_view_key.empty()) return false; + if (!seed.empty() && (primary_address.empty() || private_view_key.empty())) { + // derive primary address and private view key from seed + monero_wallet_keys* wallet_keys = monero_wallet_keys::create_wallet_from_seed(config); + primary_address = wallet_keys->get_primary_address(); + private_view_key = wallet_keys->get_private_view_key(); + delete wallet_keys; + } else if ((!primary_address.empty() && private_view_key.empty()) || (primary_address.empty() && !private_view_key.empty())) { + throw std::runtime_error("Must provide both primary address and private view key to check if wallet exists"); + } + return wallet_exists(primary_address, private_view_key, rpc); + } + + monero_wallet_light* monero_wallet_light::open_wallet(const monero_wallet_config& config, const std::shared_ptr& rpc) { + monero_wallet_config _config = config.copy(); + if (config.m_seed != boost::none && !config.m_seed->empty()) return create_wallet_from_seed(_config, rpc); + return create_wallet_from_keys(_config, rpc); + } + + monero_wallet_light* monero_wallet_light::create_wallet(const monero_wallet_config& config, const std::shared_ptr& rpc) { + MTRACE("monero_wallet_light::create_wallet(config)"); + + // validate and normalize config + monero_wallet_config config_normalized = config.copy(); + if (config.m_server != nullptr) throw std::runtime_error("Cannot provide server config for light wallet"); + 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 (rpc != nullptr && rpc->m_uri != boost::none && wallet_exists(config, rpc)) throw std::runtime_error("Wallet already exists"); + return create_wallet_from_seed(config_normalized, rpc); + } + 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 (rpc != nullptr && rpc->m_uri != boost::none && wallet_exists(config_normalized, rpc)) throw std::runtime_error("Wallet already exists"); + return create_wallet_from_keys(config_normalized, rpc); + } else { + return create_wallet_random(config_normalized, rpc); + } + } + + monero_wallet_light* monero_wallet_light::create_wallet_from_seed(monero_wallet_config& config, const std::shared_ptr& rpc) { + 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"); + 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"); + + // 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 + std::unique_ptr wallet_guard(new monero_wallet_light(rpc)); + monero_wallet_light* wallet = wallet_guard.get(); + 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; + + if (wallet->is_connected_to_daemon()) { + auto login_response = wallet->m_client->login(true, true); + // seed m_start_height from login(), in case it's set, so get_restore_height() works before the first refresh() + if (login_response->m_start_height != boost::none) wallet->m_cache->set_start_height(login_response->m_start_height.get()); + if (config.m_account_lookahead != boost::none) { + const uint32_t account_lookahead = config.m_account_lookahead.get(); + wallet->upsert_subaddrs(account_lookahead == 0 ? 0 : account_lookahead - 1, config.m_subaddress_lookahead.get()); + } + try { + if (config.m_restore_height != boost::none) { + wallet->set_restore_height(config.m_restore_height.get()); + } else if (login_response->m_new_address.value_or(false)) { + wallet->set_restore_height(0); + } + } catch (const monero_rpc_error& e) { + if (e.code == 403) throw monero_rpc_error(403, "Wallet requires lws administrator approval. Reopen after approval with an explicit restore height."); + throw; + } + } + else if (config.m_restore_height != boost::none) throw std::runtime_error("Cannot restore wallet from height: wallet is not connected to lws"); + + return wallet_guard.release(); + } + + monero_wallet_light* monero_wallet_light::create_wallet_from_keys(monero_wallet_config& config, const std::shared_ptr& rpc) { + 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()); + 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"); + + // 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 + std::unique_ptr wallet_guard(new monero_wallet_light(rpc)); + monero_wallet_light* wallet = wallet_guard.get(); + 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(); + if (wallet->is_connected_to_daemon()) { + auto login_response = wallet->m_client->login(true, false); + // seed m_start_height from login(), in case it's set, so get_restore_height() works before the first refresh() + if (login_response->m_start_height != boost::none) wallet->m_cache->set_start_height(login_response->m_start_height.get()); + if (config.m_account_lookahead != boost::none) { + const uint32_t account_lookahead = config.m_account_lookahead.get(); + wallet->upsert_subaddrs(account_lookahead == 0 ? 0 : account_lookahead - 1, config.m_subaddress_lookahead.get()); + } + try { + if (config.m_restore_height != boost::none) { + wallet->set_restore_height(config.m_restore_height.get()); + } else if (login_response->m_new_address.value_or(false)) { + wallet->set_restore_height(0); + } + } catch (const monero_rpc_error& e) { + if (e.code == 403) throw monero_rpc_error(403, "Wallet requires lws administrator approval. Reopen after approval with an explicit restore height."); + throw; + } + } + else if (config.m_restore_height != boost::none) throw std::runtime_error("Cannot restore wallet from height: wallet is not connected to lws"); + + return wallet_guard.release(); + } + + monero_wallet_light* monero_wallet_light::create_wallet_random(monero_wallet_config& config, const std::shared_ptr& rpc) { + 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()); + 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"); + + // initialize random wallet account + std::unique_ptr wallet_guard(new monero_wallet_light(rpc)); + monero_wallet_light* wallet = wallet_guard.get(); + 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; + + if (wallet->is_connected_to_daemon()) { + auto login_response = wallet->m_client->login(true, true); + // seed m_start_height from login(), in case it's set, so get_restore_height() works before the first refresh() + if (login_response->m_start_height != boost::none) wallet->m_cache->set_start_height(login_response->m_start_height.get()); + if (config.m_account_lookahead != boost::none) { + const uint32_t account_lookahead = config.m_account_lookahead.get(); + wallet->upsert_subaddrs(account_lookahead == 0 ? 0 : account_lookahead - 1, config.m_subaddress_lookahead.get()); + } + } + + return wallet_guard.release(); + } + + // ----------------------------- WALLET METHODS ----------------------------- + + monero_wallet_light::~monero_wallet_light() { + MTRACE("~monero_wallet_light()"); + close(false); + } + + monero_wallet_light::monero_wallet_light(const std::shared_ptr& rpc_connection): m_rpc(rpc_connection) { + if (rpc_connection == nullptr) throw monero_error("Connection cannot be null"); + if (!rpc_connection->is_online().value_or(false) && rpc_connection->m_uri != boost::none) rpc_connection->check_connection(); + } + + monero_wallet_light::monero_wallet_light(const std::string& uri, const std::string& username, const std::string& password, const std::string& proxy_uri, const std::string& zmq_uri, const boost::optional& timeout): m_rpc(std::make_shared(uri, username, password, proxy_uri, zmq_uri, 0, timeout)) { + if (m_rpc->m_uri != boost::none) m_rpc->check_connection(); + } + + std::shared_ptr monero_wallet_light::get_rpc_connection() const { + assert_not_closed(); + return m_rpc; + } + + bool monero_wallet_light::is_connected_to_daemon() const { + assert_not_closed(); + m_is_connected = m_client->is_connected(); + return m_is_connected; + } + + uint64_t monero_wallet_light::get_daemon_height() const { + assert_not_closed(); + if (!m_is_connected) throw std::runtime_error("Wallet is not connected to daemon"); + auto status = m_client->get_daemon_status(); + if (status->m_height == boost::none) throw std::runtime_error("Failed to get daemon height"); + return status->m_height.get(); + } + + uint64_t monero_wallet_light::get_daemon_max_peer_height() const { + assert_not_closed(); + if (!m_is_connected) throw std::runtime_error("Wallet is not connected to daemon"); + auto status = m_client->get_daemon_status(); + if (status->m_target_height == boost::none) throw std::runtime_error("Failed to get daemon max peer height"); + uint64_t result = status->m_target_height.get(); + if (result == 0) { + // target height can be 0 when daemon is synced + if (status->m_height == boost::none) throw std::runtime_error("Failed to get daemon max peer height"); + result = status->m_height.get(); + } + return result; + } + + void monero_wallet_light::add_listener(monero_wallet_listener& listener) { + assert_not_closed(); + boost::lock_guard lock(m_listeners_mutex); + m_listeners.insert(&listener); + } + + void monero_wallet_light::remove_listener(monero_wallet_listener& listener) { + assert_not_closed(); + { + boost::lock_guard lock(m_listeners_mutex); + m_listeners.erase(&listener); + } + if (m_wallet_listener != nullptr) m_wallet_listener->flush_pending_notifications(); + } + + std::set monero_wallet_light::get_listeners() { + assert_not_closed(); + boost::lock_guard lock(m_listeners_mutex); + return m_listeners; + } + + monero_sync_result monero_wallet_light::sync() { + MTRACE("monero_wallet_light::sync()"); + assert_not_closed(); + return lock_and_sync(); + } + + monero_sync_result monero_wallet_light::sync(monero_wallet_listener& listener) { + MTRACE("monero_wallet_light::sync(listener)"); + assert_not_closed(); + + // register listener + add_listener(listener); + + // sync wallet + monero_sync_result result; + try { result = lock_and_sync(boost::none); } + catch (...) { remove_listener(listener); throw; } + + // unregister listener + remove_listener(listener); + + // return sync result + return result; + } + + monero_sync_result monero_wallet_light::sync(uint64_t start_height) { + MTRACE("monero_wallet_light::sync(" << start_height << ")"); + assert_not_closed(); + return lock_and_sync(start_height); + } + + monero_sync_result monero_wallet_light::sync(uint64_t start_height, monero_wallet_listener& listener) { + MTRACE("monero_wallet_light::sync(" << start_height << ", listener)"); + assert_not_closed(); + + // wrap and register sync listener as wallet listener + add_listener(listener); + + // sync wallet + monero_sync_result result; + try { result = lock_and_sync(start_height); } + catch (...) { remove_listener(listener); throw; } + + // unregister sync listener + remove_listener(listener); + + // return sync result + return result; + } + + void monero_wallet_light::start_syncing(uint64_t sync_period_in_ms) { + assert_not_closed(); + m_syncing_interval = sync_period_in_ms; + if (!m_syncing_enabled) { + m_syncing_enabled = true; + run_sync_loop(); // sync wallet on loop in background + } + } + + // TODO implement also in monero_wallet_full + void monero_wallet_light::stop_syncing() { + assert_not_closed(); + boost::mutex::scoped_lock lock(m_syncing_mutex); + m_syncing_enabled = false; + m_sync_cv.notify_one(); + } + + void monero_wallet_light::scan_txs(const std::vector& tx_ids) { + assert_not_closed(); + sync(); + } + + bool monero_wallet_light::is_daemon_synced() const { + assert_not_closed(); + if (!m_is_connected) throw std::runtime_error("Wallet is not connected to daemon"); + auto status = m_client->get_daemon_status(); + if (status->m_state != boost::none && status->m_state.get() == std::string("synchronizing")) return false; + const uint64_t height = status->m_height.value_or(0); + const uint64_t target = status->m_target_height.value_or(0); + return target == 0 || height >= target; + } + + bool monero_wallet_light::is_daemon_trusted() const { + assert_not_closed(); + return true; + } + + bool monero_wallet_light::is_synced() const { + assert_not_closed(); + if (!is_connected_to_daemon()) return false; + if (m_cache->get_blockchain_height() <= 1) return false; + return m_cache->get_scanned_block_height() == m_cache->get_blockchain_height(); + } + + monero_subaddress monero_wallet_light::get_address_index(const std::string& address) const { + MTRACE("monero_wallet_light::get_address_index(" << address << ")"); + assert_not_closed(); + // validate address + cryptonote::address_parse_info info; + if (!get_account_address_from_str(info, static_cast(m_network_type), address)) { + throw std::runtime_error("Invalid address"); + } + + sync_op_lock op_lock(*this); // do not read m_cache->m_subaddresses while the sync thread is upserting into it + // get index of address in wallet + auto index = m_cache->m_subaddresses.find(info.address.m_spend_public_key); + if (index == m_cache->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 { + assert_not_closed(); + return m_cache->get_scanned_block_height() + 1; + } + + void monero_wallet_light::set_restore_height(uint64_t restore_height) { + assert_not_closed(); + uint64_t from_height = restore_height == 0 ? 0 : restore_height - 1; + auto response = m_client->import_request(from_height); + if (response->m_import_fee != boost::none && response->m_import_fee.get() > 0) { + throw std::runtime_error("Payment is required to rescan blockchain: address " + response->m_payment_address.value_or("unknown") + ", amount " + std::to_string(response->m_import_fee.get())); + } + + if (response->m_request_fullfilled == boost::none || !response->m_request_fullfilled.get()) { + throw std::runtime_error("Restore height request is pending lws administrator approval; the requested history is not yet being scanned"); + } + m_cache->set_start_height(from_height); + } + + uint64_t monero_wallet_light::get_restore_height() const { + assert_not_closed(); + uint64_t height = m_cache->get_start_height(); + // m_start_height is only known after refresh() processes a get_address_info response, and + // login()'s response doesn't carry it on monero-lws; fetch it directly the first time instead + // of reporting an unset cache as height 0 + if (height == 0 && !m_start_height_resolved && m_is_connected) { + try { + auto addr_info = m_client->get_address_info(); + if (addr_info->m_start_height != boost::none) { + height = addr_info->m_start_height.get(); + m_cache->set_start_height(height); + m_start_height_resolved = true; + } + } catch (const std::exception&) { + // fall through with height == 0 (unknown, or genuinely genesis) + } + } + return height == 0 ? 0 : height + 1; + } + + uint64_t monero_wallet_light::get_balance() const { + assert_not_closed(); + return m_cache->get_balance(); + } + + uint64_t monero_wallet_light::get_balance(uint32_t account_index) const { + assert_not_closed(); + return m_cache->get_balance(account_index); + } + + uint64_t monero_wallet_light::get_balance(uint32_t account_idx, uint32_t subaddress_idx) const { + assert_not_closed(); + return m_cache->get_balance(account_idx, subaddress_idx); + } + + uint64_t monero_wallet_light::get_unlocked_balance() const { + assert_not_closed(); + return m_cache->get_unlocked_balance(); + } + + uint64_t monero_wallet_light::get_unlocked_balance(uint32_t account_index) const { + assert_not_closed(); + return m_cache->get_unlocked_balance(account_index); + } + + uint64_t monero_wallet_light::get_unlocked_balance(uint32_t account_idx, uint32_t subaddress_idx) const { + assert_not_closed(); + return m_cache->get_unlocked_balance(account_idx, subaddress_idx); + } + + std::vector monero_wallet_light::get_accounts(bool include_subaddresses, const std::string& tag) const { + assert_not_closed(); + sync_op_lock op_lock(*this); // do not read m_cache->m_subaddrs while the sync thread is reassigning it + std::vector result; + bool default_found = false; + + if (m_cache->m_subaddrs->m_all_subaddrs != nullptr) { + for (const auto& kv : *m_cache->m_subaddrs->m_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 { + assert_not_closed(); + sync_op_lock op_lock(*this); // do not read m_cache->m_subaddrs while the sync thread is reassigning it + if (account_idx != 0 && (m_cache->m_subaddrs->m_all_subaddrs == nullptr || m_cache->m_subaddrs->m_all_subaddrs->empty())) throw std::runtime_error("Account out of bounds"); + if (m_cache->m_subaddrs->m_all_subaddrs != nullptr && !m_cache->m_subaddrs->m_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); + 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) { + assert_not_closed(); + if (!label.empty()) throw monero_error("monero_wallet_light doesn't support creating account with label"); + sync_op_lock op_lock(*this); // do not refresh while modifying accounts + + // a wallet opened but not yet synced has empty cached ranges + m_cache->m_subaddrs = m_client->get_subaddrs(); + process_subaddresses(); + + uint32_t last_account_idx = 0; + if (m_cache->m_subaddrs->m_all_subaddrs != nullptr) { + last_account_idx = m_cache->m_subaddrs->m_all_subaddrs->get_last_account_index(); + } + + uint32_t account_idx = last_account_idx + 1; + if (account_idx >= monero_subaddrs::MAX_ACCOUNTS) throw std::runtime_error("Cannot create account: maximum account count reached"); + upsert_subaddrs(account_idx, 0); + monero_account account = monero_wallet_keys::get_account(account_idx, false); + account.m_balance = 0; + account.m_unlocked_balance = 0; + return account; + } + + std::vector monero_wallet_light::get_subaddresses(const uint32_t account_idx, const std::vector& subaddress_indices) const { + assert_not_closed(); + sync_op_lock op_lock(*this); + std::vector subaddresses = get_subaddresses_aux(account_idx, subaddress_indices); + for(monero_subaddress& subaddress : subaddresses) m_cache->init_subaddress(subaddress); + return subaddresses; + } + + monero_subaddress monero_wallet_light::create_subaddress(uint32_t account_idx, const std::string& label) { + assert_not_closed(); + if (!label.empty()) throw monero_error("monero_wallet_light doesn't support creating subaddress with label"); + sync_op_lock op_lock(*this); // do not refresh while modifying subaddresses + + // a wallet opened but not yet synced has empty cached ranges + m_cache->m_subaddrs = m_client->get_subaddrs(); + process_subaddresses(); + + bool account_found = false; + uint32_t last_subaddress_idx = 0; + + if (m_cache->m_subaddrs->m_all_subaddrs != nullptr) { + account_found = m_cache->m_subaddrs->m_all_subaddrs->is_upsert(account_idx); + if (m_cache->m_subaddrs->m_all_subaddrs->contains(account_idx)) last_subaddress_idx = m_cache->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_subaddrs subaddrs; + subaddrs[account_idx] = std::vector>(); + subaddrs[account_idx].push_back(std::make_shared(0, subaddress_idx)); + auto response = m_client->upsert_subaddrs(subaddrs, true); + m_cache->m_subaddrs->m_all_subaddrs = response->m_all_subaddrs; + process_subaddresses(); + + monero_subaddress subaddress = get_subaddress(account_idx, subaddress_idx); + 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 { + assert_not_closed(); + sync_op_lock op_lock(*this); + std::vector indices; + indices.push_back(subaddress_idx); + std::vector subaddresses = monero_wallet_keys::get_subaddresses(account_idx, indices); + monero_subaddress& subaddress = subaddresses[0]; + m_cache->init_subaddress(subaddress); + return subaddress; + } + + std::vector monero_wallet_light::relay_txs(const std::vector& tx_metadatas) { + MTRACE("monero_wallet_light::relay_txs()"); + assert_not_closed(); + + // relay each metadata as a tx + std::vector tx_hashes; + { + sync_op_lock op_lock(*this); // do not refresh while relaying txs + try { + 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 + std::string full_hex = epee::string_tools::buff_to_hex_nodelimer(tx_to_blob(ptx.tx)); + std::shared_ptr submit_res; + try { submit_res = m_client->submit_raw_tx(full_hex); } + catch (const std::exception& e) { throw std::runtime_error(std::string("Failed to commit tx: ") + e.what()); } + if (submit_res == nullptr || submit_res->m_status == boost::none || submit_res->m_status.get() != std::string("OK")) { + throw std::runtime_error("Failed to commit tx" + (submit_res != nullptr && submit_res->m_status != boost::none ? (": " + submit_res->m_status.get()) : std::string())); + } + if (ptx.tx_key != crypto::null_skey) { + const crypto::hash txid = get_transaction_hash(ptx.tx); + m_cache->m_tx_keys[txid] = ptx.tx_key; + m_cache->m_additional_tx_keys[txid] = ptx.additional_tx_keys; + } + + std::string change_pubkey; + std::shared_ptr tx = monero_wallet_utils::ptx_to_tx(ptx, static_cast(m_network_type), this, &change_pubkey); + tx->m_full_hex = full_hex; + m_cache->add_unconfirmed_tx(tx, change_pubkey); + // 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); + } + } catch (...) { + if (!tx_hashes.empty()) m_cache->calculate_balance(); + throw; + } + + if (!tx_metadatas.empty()) m_cache->calculate_balance(); + } + + // notify listeners of spent funds + if (m_wallet_listener != nullptr) 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) { + assert_not_closed(); + + // 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 (m_account.get_device().get_type() != hw::device::device_type::SOFTWARE) 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 = monero_wallet_utils::parse_unsigned_tx(blob, m_account.get_keys().m_view_secret_key); + 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) { + dests.clear(); + + // 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(static_cast(m_network_type), entry.is_subaddress, entry.addr); + if (has_encrypted_payment_id && !entry.is_subaddress && address != entry.original) + address = cryptonote::get_account_integrated_address_as_str(static_cast(m_network_type), 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(); + 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(static_cast(m_network_type), 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's wallet_rpc_server.cpp::on_sign_transfer() + monero_tx_set monero_wallet_light::sign_txs(const std::string& unsigned_tx_hex) { + assert_not_closed(); + if (m_account.get_device().get_type() != hw::device::device_type::SOFTWARE) 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"); + sync_op_lock op_lock(*this); + + 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 = monero_wallet_utils::parse_unsigned_tx(blob, m_account.get_keys().m_view_secret_key); + + std::vector ptxs; + std::vector> txs; + try { + tools::wallet2::signed_tx_set signed_txs; + std::vector signed_kis; + serializable_unordered_map signer_subaddresses = m_cache->m_subaddresses; + hw::device& hwdev = m_account.get_device(); + for (const auto& sd : exported_txs.txes) { + cryptonote::subaddress_index idx{sd.subaddr_account, 0}; + const crypto::public_key spend_pub_key = sd.subaddr_account == 0 + ? m_account.get_keys().m_account_address.m_spend_public_key + : hwdev.get_subaddress_spend_public_key(m_account.get_keys(), idx); + signer_subaddresses[spend_pub_key] = idx; + + for (uint32_t minor : sd.subaddr_indices) { + if (minor == 0) continue; // (account,0) already added above + cryptonote::subaddress_index src_idx{sd.subaddr_account, minor}; + signer_subaddresses[hwdev.get_subaddress_spend_public_key(m_account.get_keys(), src_idx)] = src_idx; + } + } + + // TODO monero-project add offset field for wallet2::signed_tx_set ? + const auto& new_transfers = std::get<2>(exported_txs.new_transfers); + if (!new_transfers.empty()) { + uint64_t new_transfers_offset = std::get<0>(exported_txs.new_transfers); + if (new_transfers_offset != 0) throw std::runtime_error("Cannot sign an incrementally-exported unsigned tx set (offset " + std::to_string(new_transfers_offset) + ")."); + + signed_kis.reserve(new_transfers.size()); + for (const auto& etd : new_transfers) { + cryptonote::subaddress_index idx{etd.m_subaddr_index_major, etd.m_subaddr_index_minor}; + if (!idx.is_zero()) { + const crypto::public_key spend_pub_key = hwdev.get_subaddress_spend_public_key(m_account.get_keys(), idx); + signer_subaddresses[spend_pub_key] = idx; + } + + crypto::key_image ki; + cryptonote::keypair in_ephemeral; + bool found = cryptonote::generate_key_image_helper(m_account.get_keys(), signer_subaddresses, etd.m_pubkey, etd.m_tx_pubkey, etd.m_additional_tx_keys, etd.m_internal_output_index, in_ephemeral, ki, hwdev); + if (!found) throw std::runtime_error("Failed to derive key image for an exported output - it doesn't match this account's keys"); + signed_kis.push_back(epee::string_tools::pod_to_hex(ki)); + } + } + + std::string ciphertext = monero_wallet_utils::sign_tx(exported_txs, ptxs, signed_txs, signed_kis, m_account, signer_subaddresses); + 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) { + if (ptx.tx_key != crypto::null_skey) { + const crypto::hash txid = cryptonote::get_transaction_hash(ptx.tx); + m_cache->m_tx_keys[txid] = ptx.tx_key; + m_cache->m_additional_tx_keys[txid] = ptx.additional_tx_keys; + } + + // 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()"); + assert_not_closed(); + if (m_account.get_device().get_type() != hw::device::device_type::SOFTWARE) 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."); + + tools::wallet2::signed_tx_set signed_txs; + try { signed_txs = monero_wallet_utils::parse_signed_tx(blob, m_account.get_keys().m_view_secret_key); } + catch (const std::exception &e) { throw std::runtime_error(std::string("Failed to parse signed tx: ") + e.what()); } + + try { + std::vector tx_hashes; + { + sync_op_lock op_lock(*this); // do not refresh while relaying txs + + bool key_images_changed = false; + std::unordered_map identity_to_index; + identity_to_index.reserve(m_cache->m_outputs.size()); + for (size_t j = 0; j < m_cache->m_outputs.size(); ++j) { + const auto& out = m_cache->m_outputs[j]; + if (out->m_tx_pub_key == boost::none || out->m_index == boost::none) continue; + identity_to_index[out->m_tx_pub_key.get() + ":" + std::to_string(out->m_index.get())] = j; + } + + hw::device& hwdev = m_account.get_device(); + try { + for (auto &ptx: signed_txs.ptx) { + const std::string full_hex = epee::string_tools::buff_to_hex_nodelimer(cryptonote::tx_to_blob(ptx.tx)); + const auto res = m_client->submit_raw_tx(full_hex); + 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); + + // mark this tx's real spent outputs now that it has actually relayed + if (ptx.construction_data.sources.size() == ptx.tx.vin.size()) { + for (size_t j = 0; j < ptx.tx.vin.size(); ++j) { + const cryptonote::txin_to_key* in = boost::get(&ptx.tx.vin[j]); + if (in == nullptr) continue; + const auto& src = ptx.construction_data.sources[j]; + std::string identity = epee::string_tools::pod_to_hex(src.real_out_tx_key) + ":" + std::to_string(src.real_output_in_tx_index); + auto pos_it = identity_to_index.find(identity); + if (pos_it == identity_to_index.end()) continue; // output no longer tracked (e.g. pruned) - nothing to attach the key image to + auto& unspent_out = m_cache->m_outputs[pos_it->second]; + std::string key_image_hex = epee::string_tools::pod_to_hex(in->k_image); + 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(); + unspent_out->m_key_image = key_image_hex; + m_cache->set_key_image(key_image_hex, pos_it->second); + + if (m_key_image_cache->get(tx_public_key, out_index, account_idx, subaddress_idx) == nullptr) { + auto key_image = std::make_shared(); + key_image->m_hex = key_image_hex; + m_key_image_cache->set(key_image, tx_public_key, out_index, account_idx, subaddress_idx); + } + key_images_changed = true; + } + } else MWARNING("submit_txs(): signed tx " << pending_tx_hash << " construction data does not match its own input count; relaying it without updating cached key images for its inputs"); + + std::string change_pubkey; + std::shared_ptr tx = monero_wallet_utils::ptx_to_tx(ptx, static_cast(m_network_type), this, &change_pubkey); + tx->m_full_hex = full_hex; + m_cache->add_unconfirmed_tx(tx, change_pubkey); + + if (!signed_txs.tx_key_images.empty()) { + crypto::public_key tx_pub_key = cryptonote::get_tx_pub_key_from_extra(ptx.tx); + crypto::key_derivation derivation; + if (tx_pub_key != crypto::null_pkey && hwdev.generate_key_derivation(tx_pub_key, m_account.get_keys().m_view_secret_key, derivation)) { + std::vector additional_tx_pub_keys = cryptonote::get_additional_tx_pub_keys_from_extra(ptx.tx.extra); + std::vector additional_derivations(additional_tx_pub_keys.size()); + for (size_t i = 0; i < additional_tx_pub_keys.size(); ++i) { + if (!hwdev.generate_key_derivation(additional_tx_pub_keys[i], m_account.get_keys().m_view_secret_key, additional_derivations[i])) { + additional_derivations[i] = crypto::key_derivation{}; + } + } + + for (size_t vout_idx = 0; vout_idx < ptx.tx.vout.size(); ++vout_idx) { + crypto::public_key out_key; + if (!cryptonote::get_output_public_key(ptx.tx.vout[vout_idx], out_key)) continue; + auto tx_ki_it = signed_txs.tx_key_images.find(out_key); + if (tx_ki_it == signed_txs.tx_key_images.end()) continue; + + auto subaddr_recv_info = cryptonote::is_out_to_acc_precomp(m_cache->m_subaddresses, out_key, derivation, additional_derivations, vout_idx, hwdev, cryptonote::get_output_view_tag(ptx.tx.vout[vout_idx])); + if (!subaddr_recv_info) continue; + + auto key_image = std::make_shared(); + key_image->m_hex = epee::string_tools::pod_to_hex(tx_ki_it->second); + m_key_image_cache->set(key_image, epee::string_tools::pod_to_hex(tx_pub_key), vout_idx, subaddr_recv_info->index.major, subaddr_recv_info->index.minor); + } + } + } + } + } catch (...) { + if (key_images_changed) { + uint64_t gross_amount = 0; + for (const auto& out : m_cache->m_outputs) gross_amount += out->m_amount.get(); + uint64_t real_amount = process_outputs(m_cache->m_outputs, gross_amount); + m_cache->resort_outputs_by_chain_order(); + m_cache->reindex_outputs(real_amount); + invalidate_sync(); + } + m_cache->calculate_balance(); + throw; + } + + if (key_images_changed) { + uint64_t gross_amount = 0; + for (const auto& out : m_cache->m_outputs) gross_amount += out->m_amount.get(); + uint64_t real_amount = process_outputs(m_cache->m_outputs, gross_amount); + m_cache->resort_outputs_by_chain_order(); + m_cache->reindex_outputs(real_amount); + invalidate_sync(); + } + m_cache->calculate_balance(); + } + + if (m_wallet_listener != nullptr) 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()); + } + } + + // implementation based on monero-project's wallet2::get_tx_key() + std::string monero_wallet_light::get_tx_key(const std::string& tx_hash) const { + MTRACE("monero_wallet_light::get_tx_key()"); + assert_not_closed(); + sync_op_lock op_lock(*this); + + // validate and parse tx hash + crypto::hash txid; + if (!epee::string_tools::hex_to_pod(tx_hash, txid)) throw std::runtime_error("TX hash has invalid format"); + + crypto::secret_key tx_key; + std::vector additional_tx_keys; + + // check tx keys stored locally from when this wallet created the tx + const auto tx_key_it = m_cache->m_tx_keys.find(txid); + bool found = tx_key_it != m_cache->m_tx_keys.end() && tx_key_it->second != crypto::null_skey; + if (found) { + tx_key = tx_key_it->second; + const auto additional_it = m_cache->m_additional_tx_keys.find(txid); + if (additional_it != m_cache->m_additional_tx_keys.end()) additional_tx_keys = additional_it->second; + } else { + // fall back to a cold signing device, so far only the cold protocol is supported + auto& hwdev = m_account.get_device(); + if (hwdev.device_protocol() != hw::device::PROTOCOL_COLD) throw std::runtime_error("No tx secret key is stored for this tx"); + + 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()) throw std::runtime_error("No tx secret key is stored for this tx"); + + hw::device_cold::tx_key_data_t tx_key_data; + tx_key_data.tx_prefix_hash = m_cache->get_tx_prefix_hash(tx_hash); + if (tx_key_data.tx_prefix_hash.empty()) throw std::runtime_error("No tx secret key is stored for this tx"); + + 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() || tx_keys[0] == crypto::null_skey) throw std::runtime_error("No tx secret key is stored for this tx"); + + tx_key = tx_keys[0]; + tx_keys.erase(tx_keys.begin()); + additional_tx_keys = tx_keys; + } + + // 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()); + } + + // implementation based on monero-project's wallet2::check_tx_key() + std::shared_ptr monero_wallet_light::check_tx_key(const std::string& tx_hash, const std::string& tx_key, const std::string& address) const { + MTRACE("monero_wallet_light::check_tx_key()"); + assert_not_closed(); + sync_op_lock op_lock(*this); + + // 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"); + + // validate and parse tx key + epee::wipeable_string tx_key_str = tx_key; + if (tx_key_str.size() < 64 || tx_key_str.size() % 64) throw std::runtime_error("Tx key has invalid format"); + const char *data = tx_key_str.data(); + crypto::secret_key _tx_key; + if (!epee::wipeable_string(data, 64).hex_to_pod(unwrap(unwrap(_tx_key)))) throw std::runtime_error("Tx key has invalid format"); + + // get additional keys + uint64_t offset = 64; + std::vector additional_tx_keys; + while (offset < tx_key_str.size()) { + additional_tx_keys.resize(additional_tx_keys.size() + 1); + if (!epee::wipeable_string(data + offset, 64).hex_to_pod(unwrap(unwrap(additional_tx_keys.back())))) throw std::runtime_error("Tx key has invalid format"); + offset += 64; + } + + // validate and parse address + cryptonote::address_parse_info info; + if (!cryptonote::get_account_address_from_str(info, static_cast(m_network_type), address)) throw std::runtime_error("Invalid address"); + + // this wallet only has raw tx bytes cached for txs it itself built and relayed + std::shared_ptr cached_tx = m_cache->get_self_constructed_tx(tx_hash); + if (cached_tx == nullptr || cached_tx->m_full_hex == boost::none || cached_tx->m_full_hex->empty()) { + throw std::runtime_error("No tx secret key is stored for this tx"); + } + + cryptonote::blobdata tx_blob; + if (!epee::string_tools::parse_hexstr_to_binbuff(cached_tx->m_full_hex.get(), tx_blob)) throw std::runtime_error("Failed to parse cached tx"); + cryptonote::transaction tx; + if (!cryptonote::parse_and_validate_tx_from_blob(tx_blob, tx)) throw std::runtime_error("Failed to parse cached tx"); + if (!additional_tx_keys.empty() && additional_tx_keys.size() != tx.vout.size()) throw std::runtime_error("Tx key has invalid format"); + + tx_builder tx_builder(static_cast(m_network_type), m_account.get_keys(), is_view_only(), *m_client, m_cache); + uint64_t received_amount = tx_builder.compute_amount_received_from_key(tx, _tx_key, additional_tx_keys, info.address); + + bool failed = cached_tx->m_is_failed != boost::none && cached_tx->m_is_failed.get(); + bool in_tx_pool = cached_tx->m_in_tx_pool != boost::none && cached_tx->m_in_tx_pool.get(); + uint64_t num_confirmations = 0; + if (!failed && !in_tx_pool) { + uint64_t tx_height = cached_tx->get_height().value_or(0); + uint64_t chain_height = get_daemon_height(); + if (chain_height > tx_height) num_confirmations = chain_height - tx_height; + } + + std::shared_ptr check_tx = std::make_shared(); + check_tx->m_is_good = !failed; // a tx expired or reorged out is not proof of payment + check_tx->m_received_amount = received_amount; + check_tx->m_in_tx_pool = in_tx_pool; + check_tx->m_num_confirmations = num_confirmations; + return check_tx; + } + + void monero_wallet_light::freeze_output(const std::string& key_image) { + assert_not_closed(); + sync_op_lock op_lock(*this); // do not refresh while modifying outputs + m_cache->set_key_image_frozen(key_image, true); + m_cache->calculate_balance(); + } + + void monero_wallet_light::thaw_output(const std::string& key_image) { + assert_not_closed(); + sync_op_lock op_lock(*this); // do not refresh while modifying outputs + m_cache->set_key_image_frozen(key_image, false); + m_cache->calculate_balance(); + } + + bool monero_wallet_light::is_output_frozen(const std::string& key_image) { + assert_not_closed(); + sync_op_lock op_lock(*this); // do not refresh while reading outputs + return m_cache->is_key_image_frozen(key_image); + } + + monero_tx_priority monero_wallet_light::get_default_fee_priority() const { + assert_not_closed(); + return static_cast(DEFAULT_FEE_PRIORITY); + } + + std::vector> monero_wallet_light::create_txs(const monero_tx_config& config) { + MINFO("monero_wallet_light::create_txs()"); + assert_not_closed(); + 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_account_index == boost::none) throw std::runtime_error("Must specify account index to send from"); + + std::vector> result; + bool any_relayed = false; + uint32_t subaddr_account_idx = config.m_account_index.get(); + 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()) { + if (dest == nullptr) throw std::runtime_error("Destination is not defined"); + if (dest->m_amount == boost::none) throw std::runtime_error("Destination amount not defined"); + if (dest->m_address == boost::none) throw std::runtime_error("Destination address not defined"); + 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.get()); + } + + const std::set subtract_fee_from(config.m_subtract_fee_from.begin(), config.m_subtract_fee_from.end()); + for (uint32_t idx : subtract_fee_from) { + if (idx >= sending_amounts.size()) throw std::runtime_error("Invalid destination index to subtract fee from: " + std::to_string(idx)); + } + + const size_t max_destinations_per_tx = BULLETPROOF_MAX_OUTPUTS - 1; + if (dests.size() > max_destinations_per_tx && !config.m_can_split.value_or(false)) { + throw std::runtime_error("Too many destinations for a single transaction (max " + std::to_string(max_destinations_per_tx) + "); enable can_split to send in multiple transactions"); + } + + { + sync_op_lock op_lock(*this); // do not refresh while creating txs + + auto unspent_outs = m_cache->get_spendable(subaddr_account_idx, config.m_subaddress_indices); + auto simple_priority = config.m_priority == boost::none ? 0 : config.m_priority.get(); + uint64_t fee_per_b = m_cache->get_base_fee(simple_priority); + uint64_t fee_mask = m_cache->get_fee_mask(); + if (unspent_outs.empty()) throw std::runtime_error("not enough unlocked money"); + + bool relay = config.m_relay == boost::none ? false : config.m_relay.get(); + if (relay && is_view_only()) throw std::runtime_error("Cannot relay unsigned tx: wallet is view-only"); + + tx_builder tx_builder(static_cast(m_network_type), m_account.get_keys(), is_view_only(), *m_client, m_cache); + + std::vector unsigned_construction_data; + std::vector built_txs; + + // one iteration per chunk of up to max_destinations_per_tx destinations + for (size_t chunk_start = 0; chunk_start < dests.size(); chunk_start += max_destinations_per_tx) { + const size_t chunk_end = std::min(chunk_start + max_destinations_per_tx, dests.size()); + const std::vector chunk_dests(dests.begin() + chunk_start, dests.begin() + chunk_end); + const std::vector chunk_amounts(sending_amounts.begin() + chunk_start, sending_amounts.begin() + chunk_end); + + // remap subtract_fee_from indices from global destination indices to this chunk's local ones + std::set chunk_subtract_fee_from; + for (uint32_t idx : subtract_fee_from) { + if (idx >= chunk_start && idx < chunk_end) chunk_subtract_fee_from.insert(boost::numeric_cast(idx - chunk_start)); + } + + if (!subtract_fee_from.empty() && chunk_subtract_fee_from.empty()) { + throw std::runtime_error("subtract_fee_from indices are not represented in every chunk of this split transaction (destinations " + std::to_string(chunk_start) + "-" + std::to_string(chunk_end - 1) + " have none): include at least one subtract_fee_from index per " + std::to_string(max_destinations_per_tx) + "-destination chunk"); + } + + if (unspent_outs.empty()) throw std::runtime_error("not enough unlocked money"); + + built_tx built; + try { + cryptonote::blobdata tx_blob; + built.ptx = tx_builder.build(subaddr_account_idx, chunk_dests, config.m_payment_id, chunk_amounts, false, simple_priority, unspent_outs, fee_per_b, fee_mask, tx_blob, chunk_subtract_fee_from); + built.full_hex = epee::string_tools::buff_to_hex_nodelimer(tx_blob); + + if (built.ptx.tx_key != crypto::null_skey) { + const crypto::hash txid = get_transaction_hash(built.ptx.tx); + m_cache->m_tx_keys[txid] = built.ptx.tx_key; + m_cache->m_additional_tx_keys[txid] = built.ptx.additional_tx_keys; + } + + built.tx = std::dynamic_pointer_cast(monero_wallet_utils::ptx_to_tx(built.ptx, static_cast(m_network_type), this, &built.change_pubkey)); + normalize_subaddress_indices(built.tx, built.ptx, unspent_outs); // unspent_outs is pruned for the next chunk below + + // exclude the outputs this tx just used from the pool available to the next chunk, so two + // txs in the same batch never try to spend the same output + if (chunk_end < dests.size()) { + const std::set used_indexes(built.ptx.selected_transfers.begin(), built.ptx.selected_transfers.end()); + std::vector> remaining_outs; + remaining_outs.reserve(unspent_outs.size() > used_indexes.size() ? unspent_outs.size() - used_indexes.size() : 0); + for (const auto& out : unspent_outs) { + if (out->m_cache_index != boost::none && used_indexes.count(*out->m_cache_index)) continue; + remaining_outs.push_back(out); + } + unspent_outs = std::move(remaining_outs); + } + } catch (...) { + if (built.tx != nullptr) monero_utils::free(built.tx); + for (auto& prior : built_txs) if (prior.tx != nullptr) monero_utils::free(prior.tx); + throw; + } + + built_txs.push_back(std::move(built)); + } + + try { + for (auto& built : built_txs) { + tools::wallet2::pending_tx& ptx = built.ptx; + std::shared_ptr& tx = built.tx; + const std::string& full_hex = built.full_hex; + const std::string& change_pubkey = built.change_pubkey; + + bool relayed = false; + if (relay) { + auto submit_res = m_client->submit_raw_tx(full_hex); + if (submit_res->m_status == boost::none || submit_res->m_status.get() != std::string("OK")) { + throw std::runtime_error("Failed to relay tx" + (submit_res->m_status != boost::none ? (": " + submit_res->m_status.get()) : std::string())); + } + MINFO("monero_wallet_light::create_txs(): relayed tx"); + relayed = true; + } + + tx->m_in_tx_pool = relayed; + tx->m_is_relayed = relayed; + tx->m_relay = relay; + tx->m_is_outgoing = true; + tx->m_is_failed = false; + 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_construction_data.push_back(monero_wallet_utils::get_construction_data_with_decrypted_short_payment_id(ptx, m_account.get_device())); + + std::shared_ptr unconfirmed_tx = std::make_shared(); + tx->copy(tx, unconfirmed_tx); + monero_wallet_utils::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"); + + if (!is_view_only() && relayed) m_cache->add_unconfirmed_tx(tx, change_pubkey); + else monero_utils::free(tx); + tx = nullptr; // mark handled so the catch below doesn't touch it again + if (relayed) any_relayed = true; + } + } catch (...) { + if (any_relayed) m_cache->calculate_balance(); + for (auto& built : built_txs) if (built.tx != nullptr) monero_utils::free(built.tx); + throw; + } + + if (is_view_only()) { + unsigned_tx_hex = monero_wallet_utils::dump_unsigned_tx(unsigned_construction_data, config.m_payment_id, m_cache->export_outputs(true, 0), m_account.get_keys().m_view_secret_key); + if (unsigned_tx_hex.empty()) throw std::runtime_error("Failed to save unsigned tx set after creation"); + } + + // 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; + + m_cache->calculate_balance(); + } + + if (any_relayed && m_wallet_listener != nullptr) m_wallet_listener->on_spend_txs(result); + + return result; + } + + std::vector> monero_wallet_light::sweep_account(const monero_tx_config& config) { + MINFO("monero_wallet_light::sweep_account()"); + assert_not_closed(); + 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"); + + std::vector> destinations = config.get_normalized_destinations(); + if (config.m_account_index == boost::none) throw std::runtime_error("Must specify account index to sweep from"); + if (destinations.size() != 1 || destinations[0]->m_address == boost::none || destinations[0]->m_address.get().empty()) throw std::runtime_error("Must provide exactly one destination address to sweep to"); + if (destinations[0]->m_amount != boost::none) throw std::runtime_error("Cannot specify destination amount to sweep"); + if (config.m_key_image != boost::none) throw std::runtime_error("Cannot define key image in sweep_account(); use sweep_output() to sweep an output by its key image"); + if (config.m_sweep_each_subaddress != boost::none && config.m_sweep_each_subaddress.get() == true) throw std::runtime_error("Cannot sweep each subaddress individually with sweep_account"); + if (config.m_subtract_fee_from.size() > 0) throw std::runtime_error("Sweep transactions do not support subtracting fees from destinations"); + + const std::string& dest_address = destinations[0]->m_address.get(); + if (!monero_utils::is_valid_address(dest_address, m_network_type)) throw std::runtime_error("Invalid destination address"); + + std::vector> result; + uint32_t subaddr_account_idx = config.m_account_index.get(); + + sync_op_lock op_lock(*this); // do not refresh while creating txs + + auto unspent_outs = m_cache->get_spendable(subaddr_account_idx, config.m_subaddress_indices); + if (unspent_outs.empty()) throw std::runtime_error("not enough unlocked money"); + + std::vector> mixable_outs; + for (const auto& out : unspent_outs) { + if (out->m_amount.get() < DUST_THRESHOLD && !out->is_rct()) continue; + mixable_outs.push_back(out); + } + unspent_outs = std::move(mixable_outs); + if (unspent_outs.empty()) throw std::runtime_error("not enough unlocked money"); + + // mirrors wallet2::create_transactions_all()'s "below" filter + if (config.m_below_amount != boost::none && config.m_below_amount.get() > 0) { + std::vector> below_amount_outs; + for (const auto& out : unspent_outs) { + if (out->m_amount.get() < config.m_below_amount.get()) below_amount_outs.push_back(out); + } + unspent_outs = std::move(below_amount_outs); + if (unspent_outs.empty()) throw std::runtime_error("not enough unlocked money"); + } + + auto simple_priority = config.m_priority == boost::none ? 0 : config.m_priority.get(); + uint64_t fee_per_b = m_cache->get_base_fee(simple_priority); + uint64_t fee_mask = m_cache->get_fee_mask(); + bool relay = config.m_relay == boost::none ? false : config.m_relay.get(); + if (relay && is_view_only()) throw std::runtime_error("Cannot relay unsigned tx: wallet is view-only"); + + tx_builder tx_builder(static_cast(m_network_type), m_account.get_keys(), is_view_only(), *m_client, m_cache); + std::vector unsigned_construction_data; + std::vector built_txs; + + while (!unspent_outs.empty()) { + built_tx built; + try { + cryptonote::blobdata tx_blob; + built.ptx = tx_builder.build(subaddr_account_idx, {dest_address}, config.m_payment_id, {}, true, simple_priority, unspent_outs, fee_per_b, fee_mask, tx_blob); + built.full_hex = epee::string_tools::buff_to_hex_nodelimer(tx_blob); + + if (built.ptx.tx_key != crypto::null_skey) { + const crypto::hash txid = get_transaction_hash(built.ptx.tx); + m_cache->m_tx_keys[txid] = built.ptx.tx_key; + m_cache->m_additional_tx_keys[txid] = built.ptx.additional_tx_keys; + } + + built.tx = std::dynamic_pointer_cast(monero_wallet_utils::ptx_to_tx(built.ptx, static_cast(m_network_type), this, &built.change_pubkey)); + normalize_subaddress_indices(built.tx, built.ptx, unspent_outs); // unspent_outs is pruned for the next iteration below + + // exclude the outputs this tx just used from the pool for the next iteration + { + const std::set used_indexes(built.ptx.selected_transfers.begin(), built.ptx.selected_transfers.end()); + std::vector> remaining_outs; + remaining_outs.reserve(unspent_outs.size() > used_indexes.size() ? unspent_outs.size() - used_indexes.size() : 0); + for (const auto& out : unspent_outs) { + if (out->m_cache_index != boost::none && used_indexes.count(*out->m_cache_index)) continue; + remaining_outs.push_back(out); + } + unspent_outs = std::move(remaining_outs); + } + } catch (...) { + if (built.tx != nullptr) monero_utils::free(built.tx); + for (auto& prior : built_txs) if (prior.tx != nullptr) monero_utils::free(prior.tx); + throw; + } + + built_txs.push_back(std::move(built)); + } + + bool any_relayed = false; + try { + for (auto& built : built_txs) { + tools::wallet2::pending_tx& ptx = built.ptx; + std::shared_ptr& tx = built.tx; + const std::string& full_hex = built.full_hex; + const std::string& change_pubkey = built.change_pubkey; + + bool relayed = false; + if (relay) { + auto submit_res = m_client->submit_raw_tx(full_hex); + if (submit_res->m_status == boost::none || submit_res->m_status.get() != std::string("OK")) { + throw std::runtime_error("Failed to relay tx" + (submit_res->m_status != boost::none ? (": " + submit_res->m_status.get()) : std::string())); + } + MINFO("monero_wallet_light::sweep_account(): relayed tx"); + relayed = true; + } + + tx->m_in_tx_pool = relayed; + tx->m_is_relayed = relayed; + tx->m_relay = relay; + tx->m_is_outgoing = true; + tx->m_is_failed = false; + 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_construction_data.push_back(monero_wallet_utils::get_construction_data_with_decrypted_short_payment_id(ptx, m_account.get_device())); + + std::shared_ptr unconfirmed_tx = std::make_shared(); + tx->copy(tx, unconfirmed_tx); + monero_wallet_utils::normalize_unconfirmed_tx(unconfirmed_tx); + result.push_back(unconfirmed_tx); + + if (!is_view_only() && relayed) m_cache->add_unconfirmed_tx(tx, change_pubkey); + else monero_utils::free(tx); + tx = nullptr; // mark handled so the catch below doesn't touch it again + if (relayed) any_relayed = true; + } + } catch (...) { + if (any_relayed) m_cache->calculate_balance(); + for (auto& built : built_txs) if (built.tx != nullptr) monero_utils::free(built.tx); + throw; + } + + std::string unsigned_tx_hex; + if (is_view_only()) { + unsigned_tx_hex = monero_wallet_utils::dump_unsigned_tx(unsigned_construction_data, config.m_payment_id, m_cache->export_outputs(true, 0), m_account.get_keys().m_view_secret_key); + if (unsigned_tx_hex.empty()) throw std::runtime_error("Failed to save unsigned tx set after creation"); + } + + std::shared_ptr tx_set = std::make_shared(); + tx_set->m_txs = result; + for (size_t i = 0; i < result.size(); i++) result[i]->m_tx_set = tx_set; + if (!unsigned_tx_hex.empty()) tx_set->m_unsigned_tx_hex = unsigned_tx_hex; + + m_cache->calculate_balance(); + + return result; + } + + std::vector> monero_wallet_light::sweep_unlocked(const monero_tx_config& config) { + MINFO("monero_wallet_light::sweep_unlocked()"); + assert_not_closed(); + + // validate config + std::vector> destinations = config.get_normalized_destinations(); + if (destinations.size() != 1) throw std::runtime_error("Must specify exactly one destination to sweep to"); + if (destinations[0]->m_address == boost::none) throw std::runtime_error("Must specify destination address to sweep to"); + if (destinations[0]->m_amount != boost::none) throw std::runtime_error("Cannot specify amount to sweep"); + if (config.m_account_index == boost::none && config.m_subaddress_indices.size() != 0) throw std::runtime_error("Must specify account index if subaddress indices are specified"); + + // determine account and subaddress indices to sweep; default to all with unlocked balance if not specified + std::map> indices; + if (config.m_account_index != boost::none) { + if (config.m_subaddress_indices.size() != 0) { + indices[config.m_account_index.get()] = config.m_subaddress_indices; + } else { + std::vector subaddress_indices; + for (const monero_subaddress& subaddress : get_subaddresses(config.m_account_index.get(), std::vector())) { + if (subaddress.m_unlocked_balance.get() > 0) subaddress_indices.push_back(subaddress.m_index.get()); + } + indices[config.m_account_index.get()] = subaddress_indices; + } + } else { + std::vector accounts = get_accounts(true, std::string("")); + for (const monero_account& account : accounts) { + if (account.m_unlocked_balance.get() > 0) { + std::vector subaddress_indices; + for (const monero_subaddress& subaddress : account.m_subaddresses) { + if (subaddress.m_unlocked_balance.get() > 0) subaddress_indices.push_back(subaddress.m_index.get()); + } + indices[account.m_index.get()] = subaddress_indices; + } + } + } + + // sweep from each account and collect resulting txs + std::vector> txs; + { + sync_op_lock op_lock(*this); + for (std::pair> subaddress_indices_pair : indices) { + monero_tx_config copy = config.copy(); + copy.m_account_index = subaddress_indices_pair.first; + copy.m_sweep_each_subaddress = false; + copy.m_subaddress_indices = subaddress_indices_pair.second; + std::vector> account_txs = sweep_account(copy); + txs.insert(std::end(txs), std::begin(account_txs), std::end(account_txs)); + } + } + + // notify listeners of spent funds + if (config.m_relay != boost::none && config.m_relay.get() && m_wallet_listener != nullptr) m_wallet_listener->on_spend_txs(txs); + return txs; + } + + std::shared_ptr monero_wallet_light::sweep_output(const monero_tx_config& config) { + MINFO("monero_wallet_light::sweep_output()"); + assert_not_closed(); + 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 + std::vector> destinations = config.get_normalized_destinations(); + if (config.m_key_image == boost::none || config.m_key_image.get().empty()) throw std::runtime_error("Must provide key image of output to sweep"); + if (destinations.size() != 1 || destinations[0]->m_address == boost::none || destinations[0]->m_address.get().empty()) throw std::runtime_error("Must provide exactly one destination address to sweep output to"); + if (destinations[0]->m_amount != boost::none) throw std::runtime_error("Cannot specify amount to sweep"); + if (config.m_subtract_fee_from.size() > 0) throw std::runtime_error("Sweep transactions do not support subtracting fees from destinations"); + + const std::string& dest_address = destinations[0]->m_address.get(); + if (!monero_utils::is_valid_address(dest_address, m_network_type)) throw std::runtime_error("Invalid destination address"); + if (config.m_relay.value_or(false) && is_view_only()) throw std::runtime_error("Cannot relay unsigned tx: wallet is view-only"); + + std::shared_ptr unconfirmed_tx; + std::vector> result; + bool relayed = false; + + { + sync_op_lock op_lock(*this); // do not refresh while creating txs + + // locate the requested output and confirm it's actually spendable (unspent, unfrozen, unlocked) + std::shared_ptr output = m_cache->get_output(config.m_key_image.get()); + uint32_t subaddr_account_idx = output->m_recipient->m_maj_i; + uint32_t subaddress_idx = output->m_recipient->m_min_i; + const auto candidates = m_cache->get_spendable(subaddr_account_idx, std::vector{subaddress_idx}); + if (std::find(candidates.begin(), candidates.end(), output) == candidates.end()) throw std::runtime_error("Output is unspendable (already spent, frozen, or still locked)"); + + auto simple_priority = config.m_priority == boost::none ? 0 : config.m_priority.get(); + uint64_t fee_per_b = m_cache->get_base_fee(simple_priority); + uint64_t fee_mask = m_cache->get_fee_mask(); + std::vector> unspent_outs{output}; + + tx_builder tx_builder(static_cast(m_network_type), m_account.get_keys(), is_view_only(), *m_client, m_cache); + + cryptonote::blobdata tx_blob; + tools::wallet2::pending_tx ptx = tx_builder.build(subaddr_account_idx, {dest_address}, config.m_payment_id, {}, true, simple_priority, unspent_outs, fee_per_b, fee_mask, tx_blob); + std::string full_hex = epee::string_tools::buff_to_hex_nodelimer(tx_blob); + if (ptx.selected_transfers.size() > 1) throw std::runtime_error("The transaction uses multiple inputs, which is not supposed to happen"); + + if (ptx.tx_key != crypto::null_skey) { + const crypto::hash txid = get_transaction_hash(ptx.tx); + m_cache->m_tx_keys[txid] = ptx.tx_key; + m_cache->m_additional_tx_keys[txid] = ptx.additional_tx_keys; + } + + std::string change_pubkey; + std::shared_ptr tx = std::dynamic_pointer_cast(monero_wallet_utils::ptx_to_tx(ptx, static_cast(m_network_type), this, &change_pubkey)); + + bool relay = config.m_relay == boost::none ? false : config.m_relay.get(); + if (relay) { + auto submit_res = m_client->submit_raw_tx(full_hex); + if (submit_res->m_status == boost::none || submit_res->m_status.get() != std::string("OK")) { + throw std::runtime_error("Failed to relay tx" + (submit_res->m_status != boost::none ? (": " + submit_res->m_status.get()) : std::string())); + } + MINFO("monero_wallet_light::sweep_output(): relayed tx"); + relayed = true; + } + + tx->m_in_tx_pool = relayed; + tx->m_is_relayed = relayed; + tx->m_relay = relay; + tx->m_is_outgoing = true; + tx->m_is_failed = false; + 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; + } + + std::string unsigned_tx_hex; + if (is_view_only()) { + std::vector unsigned_construction_data{monero_wallet_utils::get_construction_data_with_decrypted_short_payment_id(ptx, m_account.get_device())}; + unsigned_tx_hex = monero_wallet_utils::dump_unsigned_tx(unsigned_construction_data, config.m_payment_id, m_cache->export_outputs(true, 0), m_account.get_keys().m_view_secret_key); + if (unsigned_tx_hex.empty()) throw std::runtime_error("Failed to save unsigned tx set after creation"); + } + + unconfirmed_tx = std::make_shared(); + tx->copy(tx, unconfirmed_tx); + monero_wallet_utils::normalize_unconfirmed_tx(unconfirmed_tx); + + result = {unconfirmed_tx}; + std::shared_ptr tx_set = std::make_shared(); + tx_set->m_txs = result; + unconfirmed_tx->m_tx_set = tx_set; + if (!unsigned_tx_hex.empty()) tx_set->m_unsigned_tx_hex = unsigned_tx_hex; + if (!is_view_only() && relayed) m_cache->add_unconfirmed_tx(tx, change_pubkey); + else monero_utils::free(tx); + + m_cache->calculate_balance(); + } + + if (relayed && m_wallet_listener != nullptr) m_wallet_listener->on_spend_txs(result); + + return unconfirmed_tx; + } + + std::vector> monero_wallet_light::get_txs() const { + assert_not_closed(); + 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)"); + assert_not_closed(); + + // 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 + std::shared_ptr transfer_query = _query->m_transfer_query; + std::shared_ptr input_query = _query->m_input_query; + std::shared_ptr output_query = _query->m_output_query; + _query->m_transfer_query = nullptr; + _query->m_input_query = nullptr; + _query->m_output_query = nullptr; + + // fetch all transfers that meet tx query + std::shared_ptr temp_transfer_query = std::make_shared(); + temp_transfer_query->m_tx_query = monero_tx_query::decontextualize(_query->copy(_query, std::make_shared())); + temp_transfer_query->m_tx_query->m_transfer_query = temp_transfer_query; + std::vector> transfers = get_transfers_aux(*temp_transfer_query); + monero_utils::free(temp_transfer_query->m_tx_query); + + // 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 != nullptr) { + std::shared_ptr temp_output_query = std::make_shared(); + temp_output_query->m_tx_query = monero_tx_query::decontextualize(_query->copy(_query, std::make_shared())); + temp_output_query->m_tx_query->m_output_query = temp_output_query; + std::vector> outputs = get_outputs_aux(*temp_output_query); + monero_utils::free(temp_output_query->m_tx_query); + + // 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 != nullptr) 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 + for (const std::shared_ptr& tx : txs) { + if ((*tx->m_is_confirmed && tx->m_block == nullptr) || (!*tx->m_is_confirmed && tx->m_block != nullptr)) { + MWARNING("Inconsistency detected building txs from multiple light wallet calls, re-fetching"); + 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 { + assert_not_closed(); + // get transfers directly if query does not require tx context (e.g. other transfers, outputs) + if (!monero_transfer_query::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)) { + 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 { + assert_not_closed(); + // get outputs directly if query does not require tx context (e.g. other outputs, transfers) + if (!monero_output_query::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)) { + 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 { + assert_not_closed(); + sync_op_lock op_lock(*this); // do not refresh while exporting outputs + uint32_t start = 0; + uint32_t count = 0xffffffff; + std::stringstream oss; + binary_archive ar(oss); + + auto outputs = m_cache->export_outputs(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)); + // encrypt with private view key + std::string ciphertext = monero_wallet_utils::encrypt(header + oss.str(), m_account.get_keys().m_view_secret_key); + std::string outputs_str = magic + ciphertext; + return epee::string_tools::buff_to_hex_nodelimer(outputs_str); + } + + std::shared_ptr monero_wallet_light::export_key_images(bool all) const { + assert_not_closed(); + sync_op_lock op_lock(*this); // do not refresh while exporting key images + std::shared_ptr result = std::make_shared(); + + const auto& outputs = m_cache->m_outputs; + + size_t offset = 0; + if (!all) { + while (offset < outputs.size() && !m_key_image_cache->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; + } + result->m_offset = offset; + + result->m_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(); + uint32_t account_idx = output->m_recipient->m_maj_i; + uint32_t subaddress_idx = output->m_recipient->m_min_i; + + auto cached_key_image = m_key_image_cache->get(output->m_tx_pub_key.get(), output->m_index.get(), account_idx, subaddress_idx); + const bool cached_is_exportable = cached_key_image != nullptr && cached_key_image->m_hex != boost::none && cached_key_image->m_signature != boost::none; + if (cached_is_exportable) { + if (!is_view_only()) { + crypto::public_key tx_pub_key; + if (!epee::string_tools::hex_to_pod(output->m_tx_pub_key.get(), tx_pub_key)) throw std::runtime_error("Failed to parse output tx public key at index " + std::to_string(n)); + crypto::public_key output_pub_key; + if (!epee::string_tools::hex_to_pod(output->m_public_key.get(), output_pub_key)) throw std::runtime_error("Failed to parse output public key at index " + std::to_string(n)); + monero_utils::verify_output_ownership(tx_pub_key, output->m_index.get(), cryptonote::subaddress_index{account_idx, subaddress_idx}, m_account, output_pub_key); + } + key_image = cached_key_image; + } + else if (!is_view_only()) { + crypto::public_key tx_pub_key; + if (!epee::string_tools::hex_to_pod(output->m_tx_pub_key.get(), tx_pub_key)) throw std::runtime_error("Failed to parse output tx public key at index " + std::to_string(n)); + crypto::public_key output_pub_key; + if (!epee::string_tools::hex_to_pod(output->m_public_key.get(), output_pub_key)) throw std::runtime_error("Failed to parse output public key at index " + std::to_string(n)); + key_image = monero_utils::generate_key_image(tx_pub_key, output->m_index.get(), cryptonote::subaddress_index{account_idx, subaddress_idx}, m_account, boost::optional(output_pub_key)); + m_key_image_cache->set(key_image, output->m_tx_pub_key.get(), output->m_index.get(), account_idx, subaddress_idx); + } + else if (cached_key_image != nullptr) { + throw std::runtime_error("Key image for output at index " + std::to_string(n) + " has no signature and a view-only wallet cannot sign it"); + } + else { + throw std::runtime_error("Key image unknown for output at index " + std::to_string(n) + " on a view-only wallet; import key images from a spend-capable wallet first"); + } + result->m_key_images.push_back(key_image); + } + + return result; + } + + // implementation based on monero-project's wallet2::import_key_images() + std::shared_ptr monero_wallet_light::import_key_images(const std::vector>& key_images, uint64_t offset) { + MTRACE("monero_wallet_light::import_key_images()"); + assert_not_closed(); + sync_op_lock op_lock(*this); // do not refresh while importing key images + auto& unspent_outs = m_cache->m_outputs; + if (offset > unspent_outs.size()) throw monero_error("Offset larger than known outputs"); + if (key_images.size() > unspent_outs.size() - offset) throw monero_error("The blockchain is out of date compared to the signed 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 (size_t n = 0; n < ski.size(); ++n) { + if (key_images[n] == nullptr || key_images[n]->m_hex == boost::none) { + throw std::runtime_error("key image entry " + std::to_string(n) + " is missing its value"); + } + if (key_images[n]->m_signature == boost::none) { + throw std::runtime_error("key image entry " + std::to_string(n) + " has no signature: export it from a spend-capable wallet"); + } + 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"); + } + } + + // verify each key image is signed by the output it claims to spend, as done in wallet2::import_key_images() + auto verify_at = [&](size_t ski_idx, size_t out_pos) -> bool { + const auto& unspent_out = unspent_outs[out_pos + offset]; + crypto::public_key pkey; + if (!epee::string_tools::hex_to_pod(unspent_out->m_public_key.get(), pkey)) throw std::runtime_error("failed to parse output public key"); + std::vector pkeys; + pkeys.push_back(&pkey); + return crypto::check_ring_signature((const crypto::hash&)ski[ski_idx].first, ski[ski_idx].first, pkeys, &ski[ski_idx].second); + }; + for (size_t n = 0; n < ski.size(); ++n) { + if (!(rct::scalarmultKey(rct::ki2rct(ski[n].first), rct::curveOrder()) == rct::identity())) { + throw std::runtime_error("key image out of validity domain: input " + std::to_string(n + offset) + "/" + std::to_string(ski.size())); + } + } + + std::vector assignment(ski.size()); + for (size_t n = 0; n < ski.size(); ++n) assignment[n] = n; + { + size_t group_start = 0; + while (group_start < ski.size()) { + size_t group_end = group_start + 1; + uint64_t group_height = unspent_outs[group_start + offset]->m_height.value_or(0); + while (group_end < ski.size() && unspent_outs[group_end + offset]->m_height.value_or(0) == group_height) ++group_end; + + if (group_end - group_start == 1) { + if (!verify_at(group_start, group_start)) throw std::runtime_error("signature check failed: input " + std::to_string(group_start + offset) + "/" + std::to_string(ski.size())); + } else { + // lws exposes only height, not a tx's position within its block + size_t group_size = group_end - group_start; + std::vector> verifies(group_size, std::vector(group_size, false)); + for (size_t k = 0; k < group_size; ++k) { + for (size_t m = 0; m < group_size; ++m) verifies[k][m] = verify_at(group_start + k, group_start + m); + } + + std::vector local_assignment(group_size, SIZE_MAX); + std::vector candidate_used(group_size, false); + for (size_t k = 0; k < group_size; ++k) { + size_t match = SIZE_MAX; + size_t match_count = 0; + for (size_t m = 0; m < group_size; ++m) { + if (verifies[k][m]) { match = m; ++match_count; } + } + if (match_count == 0) throw std::runtime_error("signature check failed: inputs " + std::to_string(group_start + offset) + "-" + std::to_string(group_end - 1 + offset) + "/" + std::to_string(ski.size())); + if (match_count > 1 || candidate_used[match]) throw std::runtime_error("signature check ambiguous: inputs " + std::to_string(group_start + offset) + "-" + std::to_string(group_end - 1 + offset) + "/" + std::to_string(ski.size())); + candidate_used[match] = true; + local_assignment[k] = match; + } + for (size_t k = 0; k < group_size; ++k) assignment[group_start + k] = group_start + local_assignment[k]; + } + + group_start = group_end; + } + } + + bool check_spent = is_connected_to_daemon(); + size_t key_images_size = key_images.size(); + const auto pool_key_images = check_spent ? m_cache->get_pool_key_images() : std::unordered_set(); + + for (size_t i = 0; i < key_images_size; ++i) { + auto& unspent_out = unspent_outs[assignment[i] + offset]; + 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(); + unspent_out->m_key_image = key_images[i]->m_hex; + m_cache->set_key_image(key_images[i]->m_hex.get(), assignment[i] + offset); + m_key_image_cache->set(key_images[i], tx_public_key, out_index, account_idx, subaddress_idx); + + if (!check_spent) continue; + if (m_cache->is_key_image_spent(key_images[i], pool_key_images)) spent_amount += unspent_out->m_amount.get(); + else unspent_amount += unspent_out->m_amount.get(); + } + + result->m_height = unspent_outs[key_images_size - 1 + offset]->m_height; + result->m_spent_amount = spent_amount; + result->m_unspent_amount = unspent_amount; + + uint64_t gross_amount = 0; + for (const auto& out : m_cache->m_outputs) gross_amount += out->m_amount.get(); + uint64_t real_amount = process_outputs(m_cache->m_outputs, gross_amount); + m_cache->resort_outputs_by_chain_order(); + m_cache->reindex_outputs(real_amount); + m_cache->calculate_balance(); + invalidate_sync(); + return result; + } + + uint64_t monero_wallet_light::wait_for_next_block() { + assert_not_closed(); + // 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 = 0; + bool notified = false; + block_notifier(boost::mutex* temp, boost::condition_variable* cv) { this->temp = temp; this->cv = cv; } + void on_new_block(uint64_t height) { + boost::mutex::scoped_lock lock(*temp); + last_height = height; + notified = true; + lock.unlock(); + 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, [&block_listener]() { return block_listener.notified; }); + lock.unlock(); + // unregister the listener + remove_listener(block_listener); + + // return last height + return block_listener.last_height; + } + + bool monero_wallet_light::is_multisig_import_needed() const { + assert_not_closed(); + return false; + } + + monero_multisig_info monero_wallet_light::get_multisig_info() const { + assert_not_closed(); + monero_multisig_info info; + info.m_is_multisig = false; + info.m_is_ready = false; + info.m_threshold = 0; + info.m_num_participants = 0; + return info; + } + + void monero_wallet_light::close(bool save) { + MTRACE("monero_wallet_light::close()"); + if (save) throw std::runtime_error("monero_wallet_light does not support saving"); + if (m_is_closed) return; // closing a closed wallet has not effect + if (m_wallet_listener != nullptr && m_wallet_listener->is_dispatching_on_calling_thread()) { + throw std::runtime_error("Cannot close the wallet from within a listener callback: call close() from a different thread instead"); + } + 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? + if (m_syncing_thread.joinable() && m_syncing_thread.get_id() != boost::this_thread::get_id()) m_syncing_thread.join(); + } + + boost::lock_guard sync_guard(m_sync_mutex); + + m_account.deinit(); + // TODO port to monero_wallet_full + m_wallet_listener.reset(); // wait for queued notifications + + boost::lock_guard sync_data_guard(m_sync_data_mutex); + m_is_connected = false; + m_is_closed = true; + } + + // --------------------------- PRIVATE UTILS -------------------------- + + monero_wallet_light::sync_op_lock::sync_op_lock(const monero_wallet_light& wallet) : m_wallet(wallet) { + wallet.m_num_sync_pauses++; // pause background sync + wallet.m_sync_data_mutex.lock(); + } + + monero_wallet_light::sync_op_lock::~sync_op_lock() { + m_wallet.m_sync_data_mutex.unlock(); + m_wallet.m_num_sync_pauses--; // resume background sync + } + + void monero_wallet_light::init_common() { + monero_wallet_keys::init_common(); + m_cache = std::make_shared(m_key_image_cache); + m_client.reset(new lws_client(m_rpc, get_primary_address(), get_private_view_key())); + m_rescan_on_sync = false; + m_syncing_enabled = false; + m_sync_loop_running = false; + m_num_sync_pauses = 0; + m_start_height_resolved = false; + invalidate_sync(); + process_subaddresses(); + m_wallet_listener = std::unique_ptr(new wallet_light_listener(*this)); + } + + cryptonote::subaddress_index get_transaction_sender(const std::shared_ptr& 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 { + sync_op_lock op_lock(*this); // do not read the cache while the sync thread is replacing it + + // copy and normalize query + std::shared_ptr _query; + if (query.m_tx_query == nullptr) { + 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->m_transfer_query = _query; + } else { + std::shared_ptr tx_query = query.m_tx_query->copy(query.m_tx_query, std::make_shared()); + _query = tx_query->m_transfer_query; + } + std::shared_ptr tx_query = _query->m_tx_query; + + std::vector> transfers; + std::unordered_map> blocks; + + const uint64_t current_height = get_height(); + std::unordered_set known_hashes; + + for (const auto &tx : m_cache->get_txs()) { + 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(); + + if (fee == 0) { + std::shared_ptr self_tx = m_cache->get_self_constructed_tx(tx->m_hash.get()); + if (self_tx != nullptr && self_tx->m_fee != boost::none && *self_tx->m_fee > 0) fee = *self_tx->m_fee; + } + + 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_confirmed = !tx->m_mempool.get(); + uint64_t tx_height = is_confirmed ? *tx->m_height : 0; + std::string tx_hash = tx->m_hash.get(); + bool is_locked = m_cache->get_num_blocks_to_unlock(tx_hash) > 0; + 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 = is_confirmed ? tx->m_timestamp.get() : 0; + uint64_t num_confirmations = is_confirmed ? current_height - tx_height : 0; + known_hashes.insert(tx_hash); + boost::optional known_change_pubkey = m_cache->get_change_pubkey(tx_hash); + 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_cache->get_tx_outputs(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; + + bool is_specific_change_output = false; + bool hide_from_incoming = false; + if (is_change) { + hide_from_incoming = sender.major == out_account_idx; + if (hide_from_incoming) { + if (known_change_pubkey != boost::none && !known_change_pubkey->empty()) { + is_specific_change_output = out->m_public_key.value_or("") == *known_change_pubkey; + } else { + // fall back to assuming any output returning to the + // sender's own account is the automatic change + is_specific_change_output = true; + } + } + } + + if (hide_from_incoming) { + if (!is_specific_change_output) total_sent += out_amount; + 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; + + uint64_t reward = m_cache->get_last_block_reward(); + monero_utils::set_num_suggested_confirmations(incoming_transfer, current_height, reward, *tx->m_unlock_time); + + 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; + } + } + } + + if (has_payment_id) tx_wallet->m_payment_id = payment_id; + + if (is_outgoing) { + 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; + outgoing_transfer->m_destinations = m_cache->get_tx_destinations(tx_hash); + + if (!outgoing_transfer->m_destinations.empty()) { + uint64_t amount = 0; + for (const std::shared_ptr& destination : outgoing_transfer->m_destinations) amount += *destination->m_amount; + outgoing_transfer->m_amount = amount; + } + + 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; + // TODO append inputs (tests want inputs to be empty for now) + } + + 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); + + for (const std::shared_ptr& transfer : tx_wallet->filter_transfers(*_query)) transfers.push_back(transfer); + + if (block != nullptr && tx_wallet->m_outgoing_transfer == nullptr && tx_wallet->m_incoming_transfers.empty()) { + block->m_txs.erase(std::remove(block->m_txs.begin(), block->m_txs.end(), tx_wallet), block->m_txs.end()); + if (block->m_miner_tx == tx_wallet) block->m_miner_tx = nullptr; + tx_wallet->m_block = nullptr; + } + } + + m_cache->for_each_unconfirmed_tx([&](const std::string& hash, const std::shared_ptr& txwallet) { + if (known_hashes.find(hash) != known_hashes.end()) return; + 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::free(tx_query); + return transfers; + } + + std::vector> monero_wallet_light::get_outputs_aux(const monero_output_query& query) const { + MTRACE("monero_wallet_light::get_outputs_aux(query)"); + sync_op_lock op_lock(*this); // do not read the cache while the sync thread is replacing it + + // copy and normalize query + std::shared_ptr _query; + if (query.m_tx_query == nullptr) { + 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->copy(query.m_tx_query, std::make_shared()); + if (query.m_tx_query->m_output_query != nullptr && query.m_tx_query->m_output_query.get() == &query) { + _query = tx_query->m_output_query; + } else { + if (query.m_tx_query->m_output_query != nullptr) 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 == nullptr) _query->m_tx_query = std::make_shared(); + std::shared_ptr tx_query = _query->m_tx_query; + + // get light wallet data + std::vector> outs; + + if (query.m_account_index != boost::none) { + if (query.m_subaddress_index == boost::none) { + outs = m_cache->get_outputs(query.m_account_index.get()); + } + else { + outs = m_cache->get_outputs(query.m_account_index.get(), query.m_subaddress_index.get()); + } + } else outs = m_cache->m_outputs; + + std::vector> outputs; + + // cache unique txs and blocks + std::map> tx_map; + std::map> block_map; + const auto pool_key_images = m_cache->get_pool_key_images(); + for (const auto &out : outs) { + // TODO: skip tx building if output excluded by indices, etc + std::shared_ptr tx = m_cache->init_tx_with_output(out, pool_key_images); + 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 != nullptr) 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 { + sync_op_lock op_lock(*this); // do not read m_cache->m_subaddrs while the sync thread is reassigning it + // must provide subaddress indices + std::vector subaddress_idxs; + if (subaddress_indices.empty()) { + if (m_cache->m_subaddrs->m_all_subaddrs != nullptr) + subaddress_idxs = m_cache->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); + } + + bool monero_wallet_light::is_output_spent(const std::shared_ptr &output, const std::unordered_set& pool_key_images) const { + uint32_t account_idx = output->m_recipient->m_maj_i; + uint32_t subaddress_idx = output->m_recipient->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; + + try { + for (auto& key_image : output->m_spend_key_images) { + if (is_key_image_ours(key_image, tx_pub_key, output_idx, account_idx, subaddress_idx, output->m_public_key.get())) { + output->m_key_image = key_image; + spent = true; + break; + } + } + } + catch (const monero_output_ownership_error& e) { + MERROR("Output ownership check failed for tx_pub_key=" << tx_pub_key << " index=" << output_idx << ": " << e.what()); + output->m_frozen = true; + return false; + } + + bool checked_unconfirmed = false; + + if (!spent && !output->is_key_image_known()) { + if (is_view_only() && m_key_image_cache->get(tx_pub_key, output_idx, account_idx, subaddress_idx) == nullptr) { + m_key_image_cache->set(nullptr, tx_pub_key, output_idx, account_idx, subaddress_idx, true); + return false; + } + try { + output->m_key_image = generate_key_image(tx_pub_key, output_idx, account_idx, subaddress_idx, boost::optional(output->m_public_key.get()))->m_hex; + // check key image is spent in unconfirmed transactions + spent = m_cache->is_key_image_spent(output->m_key_image.get(), pool_key_images); + checked_unconfirmed = true; + } + catch (const monero_output_ownership_error& e) { + MERROR("Output ownership check failed for tx_pub_key=" << tx_pub_key << " index=" << output_idx << ": " << e.what()); + output->m_frozen = true; + return false; + } + catch (...) { + if (is_view_only()) m_key_image_cache->set(nullptr, tx_pub_key, output_idx, account_idx, subaddress_idx, true); + return false; + } + } + + if (!checked_unconfirmed && !spent && output->is_key_image_known()) { + // check key image is spent in unconfirmed transactions + spent = m_cache->is_key_image_spent(output->m_key_image.get(), pool_key_images); + } + + return spent; + } + + bool monero_wallet_light::is_spend_real(const std::shared_ptr& spend) const { + if (spend->m_key_image == boost::none) return false; + std::string key_image = spend->m_key_image.get(); + return is_key_image_ours(key_image, spend->m_tx_pub_key.get(), spend->m_out_index.get(), spend->m_sender->m_maj_i, spend->m_sender->m_min_i, boost::none); + } + + // TODO monero_wallet_full::run_sync_loop() + void monero_wallet_light::run_sync_loop() { + boost::mutex::scoped_lock lock(m_syncing_mutex); + 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 and not paused by a wallet operation + while (m_syncing_enabled) { + if (m_num_sync_pauses == 0) { + try { lock_and_sync(boost::none, true /* from_background_loop */); } + catch (std::exception const& e) { MERROR("monero_wallet_light failed to background synchronize: " << e.what()); } + catch (...) { MERROR("monero_wallet_light failed to background synchronize"); } + } + + // 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, [this]() { return !m_syncing_enabled.load(); }); + } + } + + boost::mutex::scoped_lock exit_lock(m_syncing_mutex); + m_sync_loop_running = false; + }); + } + + monero_sync_result monero_wallet_light::lock_and_sync(boost::optional start_height, bool from_background_loop) { + 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 { + bool daemon_ready = is_connected_to_daemon(); + if (!daemon_ready && !from_background_loop) throw std::runtime_error("Wallet is not connected to daemon"); + try { daemon_ready = daemon_ready && is_daemon_synced(); } + catch (const std::exception& e) { MWARNING("Failed to check if daemon is synced on sync start: " << e.what()); } + if (daemon_ready) { + + // rescan blockchain if requested + if (rescan) rescan_blockchain(); // infinite loop? + + // sync wallet + result = sync_aux(start_height, from_background_loop); + } + } while (!rescan && (rescan = m_rescan_on_sync.exchange(false))); // repeat if not rescanned and rescan was requested + return result; + } + + void monero_wallet_light::invalidate_sync() { + m_last_synced_total_received = ~static_cast(0); + m_last_synced_total_sent = ~static_cast(0); + } + + monero_sync_result monero_wallet_light::sync_aux(boost::optional start_height, bool from_background_loop) { + MTRACE("monero_wallet_light::sync_aux()"); + + if (start_height != boost::none && *start_height < get_restore_height()) set_restore_height(*start_height); + + monero_sync_result result; + result.m_num_blocks_fetched = 0; + result.m_received_money = false; + // attempt to refresh which may throw exception + try { + // determine sync start height + uint64_t last_height = get_height(); + + auto addr_info = m_client->get_address_info(); + uint64_t new_height = addr_info->m_scanned_block_height.value_or(0) + 1; + if (addr_info->m_start_height != boost::none) { + uint64_t lws_start_height = addr_info->m_start_height.get(); + if (last_height < lws_start_height) last_height = lws_start_height == 0 ? 0 : lws_start_height + 1; + } + + const uint64_t total_received = addr_info->m_total_received.value_or(0); + const uint64_t total_sent = addr_info->m_total_sent.value_or(0); + const bool account_state_unchanged = total_received == m_last_synced_total_received && total_sent == m_last_synced_total_sent; + + if (from_background_loop && new_height == last_height && account_state_unchanged) { + m_cache->set_sync_status(*addr_info); + return result; + } + + m_wallet_listener->on_sync_start(last_height); + + boost::unique_lock lock(m_sync_data_mutex); + + std::unordered_set known_tx_hashes; + for (const auto& known_tx : m_cache->get_txs()) if (known_tx->m_hash != boost::none) known_tx_hashes.insert(known_tx->m_hash.get()); + + monero_get_unspent_outs_response unspent_outs = *m_client->get_unspent_outs(0, 0); + monero_get_address_txs_response address_txs = *m_client->get_address_txs(); + m_cache->m_subaddrs = m_client->get_subaddrs(); + process_subaddresses(); + process_txs(address_txs, unspent_outs); + unspent_outs.m_amount = process_outputs(unspent_outs.m_outputs, unspent_outs.m_amount.value_or(0)); + + for (const auto& tx : address_txs.m_transactions) { + const std::string& hash = tx->m_hash.value_or(""); + if (tx->m_total_received.value_or(0) > 0 && known_tx_hashes.count(hash) == 0 && m_cache->get_self_constructed_tx(hash) == nullptr) { + result.m_received_money = true; + break; + } + } + + m_cache->refresh(unspent_outs, address_txs, *addr_info); + m_last_synced_total_received = total_received; + m_last_synced_total_sent = total_sent; + + lock.unlock(); + + uint64_t current_height = get_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 ? current_height - last_height : 0; + result.m_num_blocks_fetched = blocks_fetched; + + if (current_height > last_height && !get_listeners().empty()) { + // 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); + } + } + } + } catch (std::exception& e) { + m_wallet_listener->on_sync_end(); // signal end of sync to reset listener's start and end heights + 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())); + return result; + } + + void monero_wallet_light::process_txs(monero_get_address_txs_response& address_txs, const monero_get_unspent_outs_response& unspent_outs) { + std::unordered_set owned_output_tx_hashes; + for (const auto& out : unspent_outs.m_outputs) { + if (out->m_tx_hash != boost::none) owned_output_tx_hashes.insert(out->m_tx_hash.get()); + } + + std::vector txs_to_remove; + size_t tx_idx = 0; + + for(auto &tx : 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 (!is_spend_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 && owned_output_tx_hashes.count(tx->m_hash.get()) == 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) address_txs.m_transactions.erase(address_txs.m_transactions.begin() + *it); + } + + uint64_t monero_wallet_light::process_outputs(std::vector>& outputs, uint64_t total_amount) { + const auto pool_key_images = m_cache->get_pool_key_images(); + for (auto& output : outputs) { + if (!is_output_spent(output, pool_key_images)) continue; + uint64_t amount = output->m_amount.get(); + total_amount = total_amount > amount ? total_amount - amount : 0; + } + + sort(outputs.begin(), outputs.end(), output_before); + return total_amount; + } + + std::vector> merge_processed_subaddr_range(std::vector>& processed_ranges, uint32_t start, uint32_t end) { + std::vector> uncovered; + uint64_t cursor = start; + for (const auto& r : processed_ranges) { + if (r.second < cursor) continue; + if (r.first > end) break; + if (r.first > cursor) uncovered.emplace_back(static_cast(cursor), r.first - 1); + cursor = static_cast(r.second) + 1; + if (cursor > end) break; + } + if (cursor <= end) uncovered.emplace_back(static_cast(cursor), end); + + // fold [start, end] into processed_ranges and re-coalesce so it stays sorted and disjoint + processed_ranges.emplace_back(start, end); + std::sort(processed_ranges.begin(), processed_ranges.end()); + std::vector> merged; + for (const auto& r : processed_ranges) { + if (!merged.empty() && static_cast(r.first) <= static_cast(merged.back().second) + 1) merged.back().second = std::max(merged.back().second, r.second); + else merged.push_back(r); + } + processed_ranges = std::move(merged); + + return uncovered; + } + + void monero_wallet_light::process_subaddresses() { + const cryptonote::account_keys &account_keys = m_account.get_keys(); + hw::device &hwdev = m_account.get_device(); + m_cache->m_subaddresses[account_keys.m_account_address.m_spend_public_key] = {0,0}; + if (m_cache->m_subaddrs->m_all_subaddrs == nullptr) return; + for (const auto& kv : *m_cache->m_subaddrs->m_all_subaddrs) { + const uint32_t account_idx = kv.first; + std::vector>& processed_ranges = m_cache->m_processed_subaddr_ranges[account_idx]; + + for (const auto& index_range : kv.second) { + const uint32_t range_start = index_range->at(0); + const uint32_t range_end = index_range->at(1); + for (const auto& uncovered : merge_processed_subaddr_range(processed_ranges, range_start, range_end)) { + for (uint64_t i = uncovered.first; i <= uncovered.second; i++) { + if (account_idx == 0 && i == 0) continue; + const uint32_t minor = static_cast(i); + const auto& subaddress_spend_pub_key = hwdev.get_subaddress_spend_public_key(account_keys, {account_idx, minor}); + m_cache->m_subaddresses[subaddress_spend_pub_key] = {account_idx, minor}; + } + } + } + } + } + + void monero_wallet_light::upsert_subaddrs(uint32_t account_idx, uint32_t subaddress_idx, bool get_all) { + monero_subaddrs subaddrs; + auto index_range = std::make_shared(0, subaddress_idx == 0 ? 0 : subaddress_idx - 1); + + for (uint64_t i = 0; i <= static_cast(account_idx); i++) { + uint32_t idx = static_cast(i); + subaddrs[idx] = std::vector>(); + subaddrs[idx].push_back(index_range); + } + + auto response = m_client->upsert_subaddrs(subaddrs, get_all); + + if (get_all) { + m_cache->m_subaddrs->m_all_subaddrs = response->m_all_subaddrs; + process_subaddresses(); + } + } + +} diff --git a/src/wallet/monero_wallet_light.h b/src/wallet/monero_wallet_light.h new file mode 100644 index 00000000..65fa17bd --- /dev/null +++ b/src/wallet/monero_wallet_light.h @@ -0,0 +1,261 @@ +/** + * Copyright (c) everoddandeven + * + * 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 "utils/monero_utils.h" +#include "cryptonote_basic/cryptonote_basic_impl.h" +#include +#include +#include + +/** + * Implements a monero_wallet.h by wrapping vtnerd's monero-lws. + */ +namespace monero { + + // forward declaration of internal lws client + class lws_client; + + // forward declaration of internal wallet listener + struct wallet_light_listener; + + // forward declarations of monero_wallet_light_model.h types + class monero_wallet_light_cache; + struct monero_output_light; + struct monero_spend; + struct monero_get_address_txs_response; + struct monero_get_unspent_outs_response; + + // there is a light that never goes out + class monero_wallet_light : public monero_wallet_keys { + + public: + + // --------------------------- STATIC WALLET UTILS -------------------------- + + /** + * 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 rpc is the rpc connection to lws + * @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::shared_ptr& rpc); + + /** + * Indicates if a wallet exists at the light wallet server. + * + * @param config wallet configuration + * @param rpc is the rpc connection to lws + * @return true if a wallet exists at the light wallet server, false otherwise + */ + static bool wallet_exists(const monero_wallet_config& config, const std::shared_ptr& rpc); + + /** + * 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 rpc is the rpc connection to lws for the wallet to use + * @return a pointer to the wallet instance + */ + static monero_wallet_light* open_wallet(const monero_wallet_config& config, const std::shared_ptr& rpc); + + /** + * Create a new wallet with the given configuration. + * + * @param config is the wallet configuration + * @param rpc is the rpc connection to lws for the wallet to use + * @return a pointer to the wallet instance + */ + static monero_wallet_light* create_wallet(const monero_wallet_config& config, const std::shared_ptr& rpc); + + /** + * Destruct the wallet. + */ + ~monero_wallet_light(); + + /** + * Get the wallet's RPC connection. + * + * @return the wallet's rpc connection + */ + std::shared_ptr get_rpc_connection() const; + + /** + * Supported wallet methods. + */ + bool is_connected_to_daemon() const override; + bool is_daemon_synced() const override; + bool is_daemon_trusted() const override; + bool is_synced() 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; + 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; + 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; + std::shared_ptr export_key_images(bool all = false) const override; + std::shared_ptr import_key_images(const std::vector>& key_images, uint64_t offset = 0) 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> sweep_unlocked(const monero_tx_config& config) override; + std::shared_ptr sweep_output(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_key(const std::string& tx_hash) const override; + std::shared_ptr check_tx_key(const std::string& tx_hash, const std::string& tx_key, const std::string& address) const override; + uint64_t wait_for_next_block() override; + bool is_multisig_import_needed() const override; + monero_multisig_info get_multisig_info() const override; + void close(bool save) override; + + // ---------------------------------- PRIVATE --------------------------------- + + private: + monero_wallet_light(const std::shared_ptr& rpc_connection); + monero_wallet_light(const std::string& uri = "", const std::string& username = "", const std::string& password = "", const std::string& proxy_uri = "", const std::string& zmq_uri = "", const boost::optional& timeout = boost::none); + + std::shared_ptr m_rpc; + std::unique_ptr m_client; + void init_common() override; + + friend struct wallet_light_listener; + std::unique_ptr m_wallet_listener; // internal wallet implementation listener + std::set m_listeners; // external wallet listeners + mutable boost::mutex m_listeners_mutex; // synchronize access to m_listeners + + static monero_wallet_light* create_wallet_from_seed(monero_wallet_config& config, const std::shared_ptr& rpc); + static monero_wallet_light* create_wallet_from_keys(monero_wallet_config& config, const std::shared_ptr& rpc); + static monero_wallet_light* create_wallet_random(monero_wallet_config& config, const std::shared_ptr& rpc); + + 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; + std::vector> sweep_account(const monero_tx_config& config); + + struct sync_op_lock { + sync_op_lock(const monero_wallet_light& wallet); + sync_op_lock(const sync_op_lock&) = delete; + ~sync_op_lock(); + const monero_wallet_light& m_wallet; + }; + + // blockchain sync management + mutable std::atomic m_is_connected{false}; // cache connection status to avoid unecessary RPC calls + mutable std::atomic m_start_height_resolved{false}; // cache whether m_start_height has been resolved 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 + mutable std::atomic m_num_sync_pauses{0};// number of operations pausing background sync + std::atomic m_rescan_on_sync{false}; // whether or not to rescan on sync + std::atomic m_syncing_enabled{false}; // whether or not auto sync is enabled + std::atomic m_sync_loop_running{false}; // whether or not the syncing thread is shut down + std::atomic m_syncing_interval{0}; // 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, bool from_background_loop = false); // internal function to synchronize request to sync and rescan + monero_sync_result sync_aux(boost::optional start_height = boost::none, bool from_background_loop = false); // internal function to immediately block, sync, and report progress + std::atomic m_last_synced_total_received{0}; + std::atomic m_last_synced_total_sent{0}; + void invalidate_sync(); + + // wallet data + mutable boost::recursive_mutex m_sync_data_mutex; + std::shared_ptr m_cache; + + bool is_output_spent(const std::shared_ptr &output, const std::unordered_set& pool_key_images) const; + bool is_spend_real(const std::shared_ptr& spend) const; + void process_txs(monero_get_address_txs_response& address_txs, const monero_get_unspent_outs_response& unspent_outs); + uint64_t process_outputs(std::vector>& outputs, uint64_t total_amount); + void process_subaddresses(); + void upsert_subaddrs(uint32_t account_idx, uint32_t subaddress_idx, bool get_all = true); + }; + +} \ 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..0468ecbf --- /dev/null +++ b/src/wallet/monero_wallet_light_model.cpp @@ -0,0 +1,1479 @@ +/** + * Copyright (c) everoddaneven + * + * 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 + * Parts of this file are originally copyright (c) 2014-2019, MyMonero.com + * + * 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 "utils/monero_wallet_utils.h" + +namespace monero { + + // --------------------------- MONERO DAEMON STATUS --------------------------- + + void monero_daemon_status::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& status) { + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + if (key == std::string("outgoing_connections_count")) status->m_outgoing_connections_count = it->second.get_value(); + else if (key == std::string("incoming_connections_count")) status->m_incoming_connections_count = it->second.get_value(); + else if (key == std::string("height")) status->m_height = it->second.get_value(); + else if (key == std::string("target_height")) status->m_target_height = it->second.get_value(); + else if (key == std::string("state")) status->m_state = it->second.data(); + else if (key == std::string("network")) { + std::string network_str = it->second.data(); + if (network_str == std::string("main") || network_str == std::string("fake")) status->m_network_type = monero_network_type::MAINNET; + else if (network_str == std::string("test")) status->m_network_type = monero_network_type::TESTNET; + else if (network_str == std::string("stage")) status->m_network_type = monero_network_type::STAGENET; + else throw std::runtime_error("Cannot deserialize lws status: invalid network provided " + network_str); + } + } + } + + // --------------------------- MONERO ADDRESS META --------------------------- + + void monero_address_meta::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& 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 = boost::numeric_cast(gen_utils::uint64_t_cast(it->second.data())); + else if (key == std::string("min_i")) address_meta->m_min_i = boost::numeric_cast(gen_utils::uint64_t_cast(it->second.data())); + } + } + + // --------------------------- MONERO OUTPUT LIGHT --------------------------- + + bool monero_output_light::is_key_image_known() const { + return m_key_image != boost::none && !m_key_image->empty(); + } + + bool monero_output_light::is_rct() const { + return m_rct != boost::none && !m_rct->empty(); + } + + bool monero_output_light::is_coinbase() const { + return is_rct() && monero_wallet_utils::is_rct_hex_unblinded_coinbase(m_rct.get()); + } + + bool monero_output_light::is_spent() const { + if (!is_key_image_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; + } + + void monero_output_light::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")) { + uint64_t index = it->second.get_value(); + if (index > 0xffff) throw std::runtime_error("Output index from server is out of bounds"); + output->m_index = index; + } + 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_address_meta::from_property_tree(it->second, recipient); + output->m_recipient = recipient; + } + } + } + + // --------------------------- MONERO SPEND --------------------------- + + void monero_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_address_meta::from_property_tree(it->second, sender); + spend->m_sender = sender; + } + } + } + + // --------------------------- MONERO TX LIGHT --------------------------- + + void monero_tx_light::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& transaction) { + 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")) { + for (boost::property_tree::ptree::const_iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) { + std::shared_ptr out = std::make_shared(); + monero_spend::from_property_tree(it2->second, out); + if (out->m_sender == nullptr || out->m_amount == boost::none || out->m_tx_pub_key == boost::none || out->m_out_index == boost::none) { + throw std::runtime_error("Light wallet server response is missing required spend fields"); + } + 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")) { + auto recipient = std::make_shared(); + monero_address_meta::from_property_tree(it->second, recipient); + transaction->m_recipient = recipient; + } + } + } + + // --------------------------- MONERO RANDOM OUTPUTS --------------------------- + + void monero_random_outputs::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& random_outputs) { + 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")) { + for (boost::property_tree::ptree::const_iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) { + std::shared_ptr out = std::make_shared(); + monero_output_light::from_property_tree(it2->second, out); + if (out->m_public_key == boost::none || out->m_global_index == boost::none) { + throw std::runtime_error("Light wallet server response is missing required decoy output fields"); + } + random_outputs->m_outputs.push_back(out); + } + } + } + } + + // --------------------------- MONERO INDEX RANGE --------------------------- + + monero_index_range::monero_index_range(const uint32_t min_i, const uint32_t maj_i) { + push_back(min_i); + push_back(maj_i); + } + + std::vector monero_index_range::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 (uint64_t i = min_i; i <= maj_i; i++) indices.push_back(static_cast(i)); + return indices; + } + + void monero_index_range::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& index_range) { + 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 (length != 2) throw std::runtime_error("Invalid index range length"); + + static const uint64_t MAX_INDEX_RANGE_SPAN = 1000000; + uint32_t start = index_range->at(0); + uint32_t end = index_range->at(1); + if (start > end) throw std::runtime_error("Invalid index range from server: start > end"); + if (static_cast(end) - start > MAX_INDEX_RANGE_SPAN) throw std::runtime_error("Index range from server is implausibly large"); + } + + // --------------------------- MONERO SUBADDRS --------------------------- + + std::vector monero_subaddrs::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.end(), idxs.begin(), idxs.end()); + } + } + return subaddress_idxs; + } + + uint32_t monero_subaddrs::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 monero_subaddrs::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) { + if (index_range->at(1) > last_subaddress_idx) last_subaddress_idx = index_range->at(1); + } + return last_subaddress_idx; + } + + void monero_subaddrs::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& subaddrs) { + uint64_t account_count = 0; + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + if (++account_count > MAX_ACCOUNTS) throw std::runtime_error("Light wallet server reported an implausible number of accounts"); + boost::optional _key; + std::vector> index_ranges; + for (boost::property_tree::ptree::const_iterator it2 = it->second.begin(); it2 != it->second.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_index_range::from_property_tree(it3->second, ir); + index_ranges.push_back(ir); + } + } + } + + if (_key == boost::none) throw std::runtime_error("Cannot deserialize subaddress: key 'key' not found."); + if (_key.get() >= MAX_ACCOUNTS) throw std::runtime_error("Light wallet server reported an implausible account index " + std::to_string(_key.get())); + + static const uint64_t MAX_ACCOUNT_INDEX_RANGE_TOTAL_SPAN = 1000000; + uint64_t total_span = 0; + for (const auto& index_range : index_ranges) { + if (index_range->size() != 2) continue; // already rejected above + total_span += static_cast(index_range->at(1)) - index_range->at(0) + 1; + if (total_span > MAX_ACCOUNT_INDEX_RANGE_TOTAL_SPAN) throw std::runtime_error("Subaddress ranges from server are implausibly large in aggregate for account " + std::to_string(_key.get())); + } + + subaddrs->emplace(_key.get(), index_ranges); + } + } + + rapidjson::Value monero_subaddrs::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + + // create root + rapidjson::Value root(rapidjson::kArrayType); + + // set sub-objects + rapidjson::Value value_num(rapidjson::kNumberType); + + for(const auto& subaddr : *this) { + rapidjson::Value obj_value(rapidjson::kObjectType); + monero_utils::add_json_member("key", subaddr.first, allocator, obj_value, value_num); + const auto& index_ranges = subaddr.second; + rapidjson::Value obj_index_ranges(rapidjson::kArrayType); + for (const auto& 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; + } + + // --------------------------- MONERO OUTPUTS DECOYS TIE --------------------------- + + // 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. + // implementation based on mymonero-core-cpp's monero_transfer_utils::pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts() + monero_outputs_decoys_tie monero_outputs_decoys_tie::tie(const std::vector>& outputs, std::vector> decoys, const boost::optional& prior_tie_attempt) { + monero_output_map tie_attempt; + if (prior_tie_attempt != boost::none) tie_attempt = *prior_tie_attempt; + + std::vector> mix_outs; + mix_outs.reserve(outputs.size()); + + for (size_t i = 0; i < outputs.size(); ++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 + auto& out = outputs[i]; + if (tie_attempt.find(out->m_public_key.get()) == tie_attempt.end()) { + for (size_t j = 0; j < decoys.size(); ++j) { + if ((out->is_rct() && decoys[j]->m_amount.get() != 0) || + (!out->is_rct() && decoys[j]->m_amount.get() != out->m_amount.get())) { + continue; + } + + // if we need to retry constructing tx, will remember to use same mix outs for this out on subsequent attempt(s) + std::shared_ptr decoy_outputs = gen_utils::pop_index(decoys, j); + tie_attempt[*out->m_public_key] = decoy_outputs->m_outputs; + mix_outs.push_back(decoy_outputs); + break; + } + } else { + std::shared_ptr decoy_outputs = std::make_shared(); + decoy_outputs->m_outputs = tie_attempt[*out->m_public_key]; + decoy_outputs->m_amount = out->m_amount; + mix_outs.push_back(decoy_outputs); + } + } + + // we expect to have a set of mix outs for every output in the tx + if (mix_outs.size() != outputs.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 (!decoys.empty()) throw std::runtime_error("too many decoy remaining"); + + monero_outputs_decoys_tie result; + result.m_decoys = mix_outs; + result.m_tie_attempt = std::move(tie_attempt); + return result; + } + + // --------------------------- MONERO OUTPUT SELECTION --------------------------- + + std::vector monero_output_selection::get_output_indexes() const { + std::vector indexes; + indexes.reserve(m_selected_outs.size()); + for (const auto &output : m_selected_outs) { + if (output->m_cache_index == boost::none) throw std::runtime_error("output doesn't belong to the wallet"); + indexes.push_back(output->m_cache_index.get()); + } + return indexes; + } + + // --------------------------- MONERO WALLET PARAMS --------------------------- + + rapidjson::Value monero_wallet_params::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; + } + + // --------------------------- MONERO GET RANDOM OUTS PARAMS --------------------------- + + rapidjson::Value monero_get_random_outs_params::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + + // create root + rapidjson::Value root(rapidjson::kObjectType); + + // set num values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_count != boost::none) monero_utils::add_json_member("count", m_count.get(), allocator, root, value_num); + + // convert amounts to strings + 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; + } + + // --------------------------- MONERO IMPORT WALLET PARAMS --------------------------- + + rapidjson::Value monero_import_wallet_params::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + + // create root + rapidjson::Value root = monero_wallet_params::to_rapidjson_val(allocator); + + // set num values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_from_height != boost::none) monero_utils::add_json_member("from_height", m_from_height.get(), allocator, root, value_num); + + // return root + return root; + } + + // --------------------------- MONERO GET UNSPENT OUTS PARAMS --------------------------- + + rapidjson::Value monero_get_unspent_outs_params::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + + // create root + rapidjson::Value root = monero_wallet_params::to_rapidjson_val(allocator); + + // set string values + rapidjson::Value value_str(rapidjson::kStringType); + if (m_amount != boost::none) monero_utils::add_json_member("amount", std::to_string(m_amount.get()), allocator, root, value_str); + if (m_dust_threshold != boost::none) monero_utils::add_json_member("dust_threshold", std::to_string(m_dust_threshold.get()), allocator, root, value_str); + + // set num values + rapidjson::Value value_num(rapidjson::kNumberType); + if (m_mixin != boost::none) monero_utils::add_json_member("mixin", m_mixin.get(), allocator, root, value_num); + + // set bool values + if (m_use_dust != boost::none) monero_utils::add_json_member("use_dust", m_use_dust.get(), allocator, root); + + // return root + return root; + } + + // --------------------------- MONERO LOGIN PARAMS --------------------------- + + rapidjson::Value monero_login_params::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + + // create root + rapidjson::Value root = monero_wallet_params::to_rapidjson_val(allocator); + + // set bool values + 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; + } + + // --------------------------- MONERO SUBMIT RAW TX PARAMS --------------------------- + + rapidjson::Value monero_submit_raw_tx_params::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; + } + + // --------------------------- MONERO UPSERT SUBADDRS PARAMS --------------------------- + + rapidjson::Value monero_upsert_subaddrs_params::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + + // create root + rapidjson::Value root = monero_wallet_params::to_rapidjson_val(allocator); + + // set bool values + if (m_get_all != boost::none) monero_utils::add_json_member("get_all", m_get_all.get(), allocator, root); + + // set sub-objects + if (m_subaddrs != boost::none) root.AddMember("subaddrs", m_subaddrs->to_rapidjson_val(allocator), allocator); + + // return root + return root; + } + + // --------------------------- MONERO GET ADDRESS INFO RESPONSE --------------------------- + + void monero_get_address_info_response::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& response) { + 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")) response->m_locked_funds = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("total_received")) response->m_total_received = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("total_sent")) response->m_total_sent = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("scanned_height")) response->m_scanned_height = it->second.get_value(); + else if (key == std::string("scanned_block_height")) response->m_scanned_block_height = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("start_height")) response->m_start_height = it->second.get_value(); + else if (key == std::string("transaction_height")) response->m_transaction_height = it->second.get_value(); + else if (key == std::string("blockchain_height")) response->m_blockchain_height = it->second.get_value(); + else if (key == std::string("spent_outputs")) { + for (boost::property_tree::ptree::const_iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) { + std::shared_ptr spent_output = std::make_shared(); + monero_spend::from_property_tree(it2->second, spent_output); + if (spent_output->m_sender == nullptr) throw std::runtime_error("Light wallet server response is missing spend sender info"); + response->m_spent_outputs.push_back(spent_output); + } + } + } + } + + // --------------------------- MONERO GET ADDRESS TXS RESPONSE --------------------------- + + void monero_get_address_txs_response::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& response) { + 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")) response->m_total_received = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("scanned_height")) response->m_scanned_height = it->second.get_value(); + else if (key == std::string("scanned_block_height")) response->m_scanned_block_height = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("start_height")) response->m_start_height = it->second.get_value(); + else if (key == std::string("blockchain_height")) response->m_blockchain_height = it->second.get_value(); + else if (key == std::string("transactions")) { + for (boost::property_tree::ptree::const_iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) { + std::shared_ptr transaction = std::make_shared(); + monero_tx_light::from_property_tree(it2->second, transaction); + if (transaction->m_hash == boost::none || transaction->m_total_received == boost::none + || transaction->m_total_sent == boost::none || transaction->m_mempool == boost::none + || transaction->m_coinbase == boost::none || transaction->m_fee == boost::none + || transaction->m_unlock_time == boost::none) { + throw std::runtime_error("Light wallet server response is missing required transaction fields"); + } + response->m_transactions.push_back(transaction); + } + } + } + } + + // --------------------------- MONERO GET RANDOM OUTS RESPONSE --------------------------- + + void monero_get_random_outs_response::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& response) { + 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")) { + for (boost::property_tree::ptree::const_iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) { + std::shared_ptr out = std::make_shared(); + monero_random_outputs::from_property_tree(it2->second, out); + response->m_amount_outs.push_back(out); + } + } + } + } + + // --------------------------- MONERO GET UNSPENT OUTS RESPONSE --------------------------- + + void monero_get_unspent_outs_response::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& response) { + 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")) response->m_per_byte_fee = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("fee_mask")) response->m_fee_mask = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("amount")) response->m_amount = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("fees")) { + for (boost::property_tree::ptree::const_iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) { + response->m_fees.push_back(gen_utils::uint64_t_cast(it2->second.data())); + } + } + else if (key == std::string("outputs")) { + for (boost::property_tree::ptree::const_iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) { + std::shared_ptr out = std::make_shared(); + monero_output_light::from_property_tree(it2->second, out); + if (out->m_recipient == nullptr) throw std::runtime_error("Light wallet server response is missing output recipient info"); + if (out->m_tx_pub_key == boost::none || out->m_public_key == boost::none || out->m_global_index == boost::none + || out->m_tx_hash == boost::none || out->m_amount == boost::none || out->m_index == boost::none) { + throw std::runtime_error("Light wallet server response is missing required output fields"); + } + response->m_outputs.push_back(out); + } + } + } + } + + // --------------------------- MONERO IMPORT WALLET RESPONSE --------------------------- + + void monero_import_wallet_response::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& response) { + 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")) response->m_payment_address = it->second.data(); + else if (key == std::string("payment_id")) response->m_payment_id = it->second.data(); + else if (key == std::string("import_fee")) response->m_import_fee = gen_utils::uint64_t_cast(it->second.data()); + else if (key == std::string("new_request")) response->m_new_request = it->second.get_value(); + else if (key == std::string("request_fulfilled")) response->m_request_fullfilled = it->second.get_value(); + else if (key == std::string("status")) response->m_status = it->second.data(); + } + } + + // --------------------------- MONERO LOGIN RESPONSE --------------------------- + + void monero_login_response::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& response) { + 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")) response->m_new_address = it->second.get_value(); + else if (key == std::string("generated_locally")) response->m_generated_locally = it->second.get_value(); + else if (key == std::string("start_height")) response->m_start_height = it->second.get_value(); + } + } + + // --------------------------- MONERO SUBMIT RAW TX RESPONSE --------------------------- + + void monero_submit_raw_tx_response::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& response) { + for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { + std::string key = it->first; + if (key == std::string("status")) response->m_status = it->second.data(); + } + } + + // --------------------------- MONERO SUBADDRS RESPONSE --------------------------- + + void monero_subaddrs_response::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& response) { + 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_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_subaddrs::from_property_tree(it->second, all_subaddrs); + response->m_all_subaddrs = all_subaddrs; + } + } + } + + // --------------------------- MONERO LIGHT WALLET CACHE --------------------------- + + namespace { + // flattens every subaddress bucket of a single account into one vector + std::vector> flatten_outputs(const serializable_unordered_map>>& subaddress_buckets) { + std::vector> result; + for (const auto &kv : subaddress_buckets) result.insert(result.end(), kv.second.begin(), kv.second.end()); + return result; + } + + uint64_t find_or_zero(const serializable_unordered_map& m, uint32_t key) { + auto it = m.find(key); + return it == m.end() ? 0 : it->second; + } + + uint64_t find_or_zero(const serializable_unordered_map>& m, uint32_t account_idx, uint32_t subaddress_idx) { + auto it = m.find(account_idx); + if (it == m.end()) return 0; + auto it2 = it->second.find(subaddress_idx); + return it2 == it->second.end() ? 0 : it2->second; + } + } + + monero_wallet_light_cache::monero_wallet_light_cache(const std::shared_ptr& key_image_cache) : m_key_image_cache(key_image_cache), m_subaddrs(std::make_shared()) { + m_subaddrs->m_all_subaddrs = std::make_shared(); + } + + monero_wallet_light_cache::~monero_wallet_light_cache() { + MTRACE("monero_wallet_light_cache::~monero_wallet_light_cache()"); + for (auto& kv : m_self_constructed_txs) monero_utils::free(kv.second); + } + + std::vector> monero_wallet_light_cache::get_outputs(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_wallet_light_cache::get_outputs(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_wallet_light_cache::get_unspent(uint32_t account_idx, uint32_t subaddress_idx) const { + boost::lock_guard lock(m_mutex); + auto account_it = m_unspent.find(account_idx); + if (account_it == m_unspent.end()) return {}; + auto subaddr_it = account_it->second.find(subaddress_idx); + return subaddr_it == account_it->second.end() ? std::vector>() : subaddr_it->second; + + } + + std::vector> monero_wallet_light_cache::get_spent(uint32_t account_idx, uint32_t subaddress_idx) const { + boost::lock_guard lock(m_mutex); + auto account_it = m_spent.find(account_idx); + if (account_it == m_spent.end()) return {}; + auto subaddr_it = account_it->second.find(subaddress_idx); + return subaddr_it == account_it->second.end() ? std::vector>() : subaddr_it->second; + } + + std::vector> monero_wallet_light_cache::get_spent(uint32_t account_idx) const { + boost::lock_guard lock(m_mutex); + auto it = m_spent.find(account_idx); + return it == m_spent.end() ? std::vector>() : flatten_outputs(it->second); + } + + std::vector> monero_wallet_light_cache::get_unspent(uint32_t account_idx) const { + boost::lock_guard lock(m_mutex); + auto it = m_unspent.find(account_idx); + return it == m_unspent.end() ? std::vector>() : flatten_outputs(it->second); + } + + std::vector> monero_wallet_light_cache::get_spendable(uint32_t account_idx, const std::vector &subaddresses_indices) 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(); + const auto pool_key_images = get_pool_key_images(); + 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 (output->m_frozen.value_or(false) || get_num_blocks_to_unlock(output->m_tx_hash.get()) > 0) continue; + if (output->is_key_image_known() && pool_key_images.count(output->m_key_image.get())) continue; + spendable.push_back(output); + } + } + + return spendable; + } + + std::vector> monero_wallet_light_cache::get_tx_outputs(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_wallet_light_cache::get_tx_prefix_hash(const std::string& tx_hash) const { + boost::lock_guard lock(m_mutex); + auto outputs = get_tx_outputs(tx_hash); + if (outputs.empty()) return std::string(""); + auto& output = outputs[0]; + return output->m_tx_prefix_hash.get(); + } + + void monero_wallet_light_cache::resort_outputs_by_chain_order() { + boost::lock_guard lock(m_mutex); + std::sort(m_outputs.begin(), m_outputs.end(), [](const std::shared_ptr& a, const std::shared_ptr& b) { + uint64_t a_h = a->m_height.value_or(0), b_h = b->m_height.value_or(0); + if (a_h != b_h) return a_h < b_h; + const std::string& a_tx_hash = a->m_tx_hash.value_or(std::string()); + const std::string& b_tx_hash = b->m_tx_hash.value_or(std::string()); + if (a_tx_hash != b_tx_hash) return a_tx_hash < b_tx_hash; + return a->m_index.value_or(0) < b->m_index.value_or(0); + }); + } + + static const uint64_t MAX_SANE_FEE_MASK = 1000000; // 100x today's real value + static const uint64_t MAX_SANE_PER_BYTE_FEE = 100000000; + + void monero_wallet_light_cache::set_outputs(monero_get_unspent_outs_response& response) { + boost::lock_guard lock(m_mutex); + + std::unordered_set frozen_key_images; + for (const auto& output : m_outputs) { + if (output->m_frozen.value_or(false) && output->is_key_image_known()) frozen_key_images.insert(output->m_key_image.get()); + } + + m_per_byte_fee = response.m_per_byte_fee.value_or(0); + m_fees = std::move(response.m_fees); + m_fee_mask = std::min(response.m_fee_mask.value_or(0), MAX_SANE_FEE_MASK); + m_amount = response.m_amount.value_or(0); + m_outputs = std::move(response.m_outputs); + resort_outputs_by_chain_order(); + + if (!frozen_key_images.empty()) { + for (auto& output : m_outputs) { + if (output->is_key_image_known() && frozen_key_images.count(output->m_key_image.get())) output->m_frozen = true; + } + } + + reindex(); + } + + uint64_t monero_wallet_light_cache::get_per_byte_fee() const { + boost::lock_guard lock(m_mutex); + return m_per_byte_fee; + } + + uint64_t monero_wallet_light_cache::get_base_fee(uint32_t priority) const { + boost::lock_guard lock(m_mutex); + uint64_t fee_per_byte; + if (!m_fees.empty()) { + // mirrors wallet2::get_base_fee()'s 2021-scaling path: clamp to [1,4] and index directly. + // These per-tier fees come straight from the daemon and aren't simple multiples of each other. + const uint32_t clamped = priority == 0 ? 1 : std::min(priority, 4); + const uint32_t idx = clamped - 1; + fee_per_byte = idx < m_fees.size() ? m_fees[idx] : m_per_byte_fee * monero_wallet_utils::get_fee_multiplier(priority); + } else { + // legacy fallback for servers that only ever returned a flat per_byte_fee + fee_per_byte = m_per_byte_fee * monero_wallet_utils::get_fee_multiplier(priority); + } + // check the fully resolved rate + if (fee_per_byte > MAX_SANE_PER_BYTE_FEE) { + throw std::runtime_error("Light wallet server reported an implausible per-byte fee (" + std::to_string(fee_per_byte) + " piconero/byte)"); + } + return fee_per_byte; + } + + uint64_t monero_wallet_light_cache::get_fee_mask() const { + boost::lock_guard lock(m_mutex); + return m_fee_mask == 0 ? 1 : m_fee_mask; + } + + uint64_t monero_wallet_light_cache::get_amount() const { + boost::lock_guard lock(m_mutex); + return m_amount; + } + + // re-derives spent/unspent buckets and indices from the current m_outputs (e.g. after outputs already + // in the cache had key images assigned in place, as import_key_images does), without a fresh response + void monero_wallet_light_cache::reindex_outputs(uint64_t amount) { + boost::lock_guard lock(m_mutex); + m_amount = amount; + reindex(); + } + + // (re)builds m_key_image_index/m_tx_hash_index/m_spent/m_unspent, and each output's m_cache_index, + // from the current m_outputs + void monero_wallet_light_cache::reindex() { + { + boost::lock_guard lock(m_mutex); + m_key_image_index.clear(); + m_tx_hash_index.clear(); + m_unspent.clear(); + m_spent.clear(); + clear_balance(); + } + if (m_outputs.empty()) return; + size_t index = 0; + boost::lock_guard lock(m_mutex); + const auto pool_key_images = get_pool_key_images(); + + for (const auto &output : m_outputs) { + output->m_cache_index = index; + + if (output->is_spent() || (output->is_key_image_known() && pool_key_images.count(output->m_key_image.get()))) { + m_spent[output->m_recipient->m_maj_i][output->m_recipient->m_min_i].push_back(output); + } else { + m_unspent[output->m_recipient->m_maj_i][output->m_recipient->m_min_i].push_back(output); + } + + if (output->is_key_image_known()) { + std::string output_key_image = output->m_key_image.get(); + m_key_image_index[output_key_image] = index; + } + + m_tx_hash_index[output->m_tx_hash.get()].push_back(output); + index++; + } + } + + bool monero_wallet_light_cache::is_subaddress_used(uint32_t account_idx, uint32_t subaddress_idx) const { + boost::lock_guard lock(m_mutex); + auto unspent_account_it = m_unspent.find(account_idx); + if (unspent_account_it != m_unspent.end()) { + auto subaddr_it = unspent_account_it->second.find(subaddress_idx); + if (subaddr_it != unspent_account_it->second.end() && !subaddr_it->second.empty()) return true; + } + auto spent_account_it = m_spent.find(account_idx); + if (spent_account_it != m_spent.end()) { + auto subaddr_it = spent_account_it->second.find(subaddress_idx); + if (subaddr_it != spent_account_it->second.end() && !subaddr_it->second.empty()) return true; + } + return false; + } + + uint64_t monero_wallet_light_cache::get_num_unspent(uint32_t account_idx, uint32_t subaddress_idx) const { + boost::lock_guard lock(m_mutex); + auto account_it = m_unspent.find(account_idx); + if (account_it == m_unspent.end()) return 0; + auto subaddr_it = account_it->second.find(subaddress_idx); + if (subaddr_it == account_it->second.end()) return 0; + const auto pool_key_images = get_pool_key_images(); + uint64_t count = 0; + for (const auto& output : subaddr_it->second) { + if (output->is_key_image_known() && pool_key_images.count(output->m_key_image.get())) continue; + count++; + } + return count; + } + + void monero_wallet_light_cache::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_wallet_light_cache::calculate_balance() { + boost::lock_guard lock(m_mutex); + clear_balance(); + const auto pool_key_images = get_pool_key_images(); + 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->is_key_image_known() && pool_key_images.count(output->m_key_image.get())) continue; + if (output->m_frozen.value_or(false)) continue; + bool locked = get_num_blocks_to_unlock(output->m_tx_hash.get()) > 0; + uint64_t amount = output->m_amount.get(); + subaddress_balance += amount; + if (!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_each_unconfirmed_tx([this](const std::string& hash, const std::shared_ptr& tx) { + if (tx->m_is_relayed != true || tx->m_is_failed == true) return; + + uint64_t change_amount = 0; + if (tx->m_change_amount != boost::none) change_amount = tx->m_change_amount.get(); + + uint32_t change_account_idx = 0; + if (tx->m_outgoing_transfer != nullptr && tx->m_outgoing_transfer->m_account_index != boost::none) { + change_account_idx = tx->m_outgoing_transfer->m_account_index.get(); + } + + m_balance += change_amount; + m_account_balance[change_account_idx] += change_amount; + m_subaddress_balance[change_account_idx][0] += change_amount; + + for (const std::shared_ptr &out : tx->m_outputs) { + std::shared_ptr output = std::dynamic_pointer_cast(out); + if (output == nullptr || output->m_account_index == boost::none || output->m_subaddress_index == boost::none) continue; + 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_wallet_light_cache::get_balance() const { + boost::lock_guard lock(m_mutex); + return m_balance; + } + + uint64_t monero_wallet_light_cache::get_balance(uint32_t account_idx) const { + boost::lock_guard lock(m_mutex); + return find_or_zero(m_account_balance, account_idx); + } + + uint64_t monero_wallet_light_cache::get_balance(uint32_t account_idx, uint32_t subaddress_idx) const { + boost::lock_guard lock(m_mutex); + return find_or_zero(m_subaddress_balance, account_idx, subaddress_idx); + } + + uint64_t monero_wallet_light_cache::get_unlocked_balance() const { + boost::lock_guard lock(m_mutex); + return m_unlocked_balance; + } + + uint64_t monero_wallet_light_cache::get_unlocked_balance(uint32_t account_idx) const { + boost::lock_guard lock(m_mutex); + return find_or_zero(m_account_unlocked_balance, account_idx); + } + + uint64_t monero_wallet_light_cache::get_unlocked_balance(uint32_t account_idx, uint32_t subaddress_idx) const { + boost::lock_guard lock(m_mutex); + return find_or_zero(m_subaddress_unlocked_balance, account_idx, subaddress_idx); + } + + 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"); + } + + void monero_wallet_light_cache::set_key_image_frozen(const std::string& key_image, bool frozen) { + if (key_image.empty()) throw std::runtime_error(std::string("Must specify key image to ") + (frozen ? "freeze" : "thaw")); + validate_key_image(key_image); + auto it = m_key_image_index.find(key_image); + if (it == m_key_image_index.end()) throw std::runtime_error("Key image not found"); + m_outputs[it->second]->m_frozen = frozen; + } + + bool monero_wallet_light_cache::is_key_image_frozen(const std::string& key_image) const { + validate_key_image(key_image); + auto it = m_key_image_index.find(key_image); + if (it == m_key_image_index.end()) throw std::runtime_error("Key image not found"); + return m_outputs[it->second]->m_frozen.value_or(false); + } + + std::shared_ptr monero_wallet_light_cache::get_output(const std::string& key_image) const { + validate_key_image(key_image); + auto it = m_key_image_index.find(key_image); + if (it == m_key_image_index.end()) throw std::runtime_error("Key image not found"); + return m_outputs[it->second]; + } + + void monero_wallet_light_cache::set_key_image(const std::string& key_image, size_t index) { + boost::lock_guard lock(m_mutex); + m_key_image_index[key_image] = index; + } + + // implementation based on monero-project's wallet2::export_outputs() + monero_wallet_utils::wallet2_exported_outputs monero_wallet_light_cache::export_outputs(bool all, uint32_t start, uint32_t count) const { + std::vector exported_transfers; + + // 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_outputs; + + size_t offset = 0; + if (!all) { + while (offset < unspent_outs.size() && (unspent_outs[offset]->is_key_image_known() && !m_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; + + exported_transfers.reserve(offset <= unspent_outs.size() ? unspent_outs.size() - offset : 0); + 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 = get_tx(tx_hash)->m_unlock_time.get(); + + crypto::public_key public_key; + crypto::public_key tx_pub_key; + if (!epee::string_tools::hex_to_pod(out->m_public_key.get(), public_key)) throw std::runtime_error("failed to parse output public key"); + if (!epee::string_tools::hex_to_pod(out->m_tx_pub_key.get(), tx_pub_key)) throw std::runtime_error("failed to parse tx public 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; + + tools::wallet2::exported_transfer_details etd; + 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 = out->m_frozen.value_or(false); + etd.m_flags.m_rct = out->is_rct(); + etd.m_flags.m_key_image_known = out->is_key_image_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; + + exported_transfers.push_back(etd); + } + + return std::make_tuple(offset, unspent_outs.size(), exported_transfers); + } + + std::vector> monero_wallet_light_cache::get_txs() const { + boost::lock_guard lock(m_mutex); + return m_tx_list; + } + + std::shared_ptr monero_wallet_light_cache::get_tx(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; + } + + void monero_wallet_light_cache::set_txs(monero_get_address_txs_response& response, const monero_get_address_info_response& addr_info_response) { + boost::lock_guard lock(m_mutex); + m_txs.clear(); + m_spent_key_images.clear(); + m_block_reward = 0; + m_tx_list.clear(); + std::unordered_map tx_list_index; + + for(auto &tx : response.m_transactions) { + std::string tx_hash = tx->m_hash.get(); + bool confirmed = !tx->m_mempool.get(); + if (tx->m_coinbase.get()) { + uint64_t amount = tx->m_total_received.get(); + if (m_block_reward == 0 || amount < m_block_reward) m_block_reward = amount; + } + + auto tx_list_it = tx_list_index.find(tx_hash); + if (tx_list_it == tx_list_index.end()) { + tx_list_index[tx_hash] = m_tx_list.size(); + m_tx_list.push_back(tx); + } else { + m_tx_list[tx_list_it->second] = tx; + } + if (confirmed) { + auto self_constructed_it = m_self_constructed_txs.find(tx_hash); + if (self_constructed_it != m_self_constructed_txs.end() && self_constructed_it->second != nullptr) { + const auto& self_tx = self_constructed_it->second; + self_tx->m_is_confirmed = true; + self_tx->m_in_tx_pool = false; + self_tx->m_is_failed = false; + if (tx->m_height != boost::none) { + if (self_tx->m_block == nullptr) { + auto block = std::make_shared(); + self_tx->m_block = block; + block->m_txs.push_back(self_tx); + } + self_tx->m_block->m_height = tx->m_height; + } + } + } + m_txs[tx_hash] = std::move(tx); + } + + const uint64_t now = static_cast(time(NULL)); + + for (auto& kv : m_self_constructed_txs) { + const auto& self_tx = kv.second; + if (self_tx == nullptr || self_tx->m_is_confirmed != true) continue; + if (tx_list_index.find(kv.first) != tx_list_index.end()) continue; + self_tx->m_is_confirmed = false; + self_tx->m_in_tx_pool = true; + self_tx->m_is_failed = false; + self_tx->m_block = nullptr; + m_self_constructed_tx_relay_times[kv.first] = now; + } + for (auto& kv : m_self_constructed_txs) { + const auto& self_tx = kv.second; + if (self_tx == nullptr || self_tx->m_is_confirmed == true || self_tx->m_is_failed == true) continue; + if (self_tx->m_is_relayed != true) continue; + auto relay_time_it = m_self_constructed_tx_relay_times.find(kv.first); + if (relay_time_it == m_self_constructed_tx_relay_times.end()) continue; // no relay time on record (e.g. cache predates this fix) - can't judge, leave it alone + if (now < relay_time_it->second + CRYPTONOTE_MEMPOOL_TX_LIVETIME) continue; + self_tx->m_is_failed = true; + self_tx->m_in_tx_pool = false; + } + + if (m_block_reward == 0) m_block_reward = monero_wallet_utils::TAIL_EMISSION_REWARD; + + 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_wallet_light_cache::refresh(monero_get_unspent_outs_response& unspent_outs, monero_get_address_txs_response& address_txs, const monero_get_address_info_response& address_info) { + boost::lock_guard lock(m_mutex); + set_txs(address_txs, address_info); + set_outputs(unspent_outs); + set_sync_status(address_info); + calculate_balance(); + } + + uint64_t monero_wallet_light_cache::get_blockchain_height() const { + boost::lock_guard lock(m_mutex); + return m_blockchain_height; + } + + uint64_t monero_wallet_light_cache::get_last_block_reward() const { + boost::lock_guard lock(m_mutex); + return m_block_reward > 1 ? m_block_reward - 2 : m_block_reward; // TODO why wallet full gives to 2 piconero less ? + } + + uint64_t monero_wallet_light_cache::get_scanned_block_height() const { + boost::lock_guard lock(m_mutex); + return m_scanned_block_height; + } + + uint64_t monero_wallet_light_cache::get_start_height() const { + boost::lock_guard lock(m_mutex); + return m_start_height; + } + + void monero_wallet_light_cache::set_start_height(uint64_t height) { + boost::lock_guard lock(m_mutex); + m_start_height = height; + } + + void monero_wallet_light_cache::set_sync_status(const monero_get_address_info_response& address_info) { + boost::lock_guard lock(m_mutex); + m_blockchain_height = address_info.m_blockchain_height.value_or(0); + m_scanned_block_height = address_info.m_scanned_block_height.value_or(0); + m_start_height = address_info.m_start_height.value_or(0); + } + + boost::optional monero_wallet_light_cache::get_change_pubkey(const std::string& tx_hash) const { + auto it = m_self_constructed_txs.find(tx_hash); + if (it == m_self_constructed_txs.end() || it->second == nullptr) return boost::none; + for (const auto& out : it->second->m_outputs) { + auto change_out = std::dynamic_pointer_cast(out); + if (change_out != nullptr && change_out->m_is_change.value_or(false)) return change_out->m_stealth_public_key.value_or(""); + } + return std::string(""); + } + + std::vector> monero_wallet_light_cache::get_tx_destinations(const std::string& tx_hash) const { + auto it = m_self_constructed_txs.find(tx_hash); + if (it == m_self_constructed_txs.end() || it->second == nullptr || it->second->m_outgoing_transfer == nullptr) return std::vector>(); + std::vector> destinations; + for (const auto& destination : it->second->m_outgoing_transfer->m_destinations) { + destinations.push_back(destination->copy(destination, std::make_shared())); + } + return destinations; + } + + std::shared_ptr monero_wallet_light_cache::get_self_constructed_tx(const std::string& tx_hash) const { + auto it = m_self_constructed_txs.find(tx_hash); + if (it == m_self_constructed_txs.end()) return nullptr; + return it->second; + } + + void monero_wallet_light_cache::add_unconfirmed_tx(const std::shared_ptr& tx, const std::string& change_pubkey) { + 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"); + if (!change_pubkey.empty()) { + auto change_out = std::make_shared(); + change_out->m_tx = tx; + change_out->m_is_change = true; + change_out->m_stealth_public_key = change_pubkey; + tx->m_outputs.push_back(change_out); + } + m_self_constructed_txs[tx_hash] = tx; + if (tx->m_is_relayed.value_or(false)) m_self_constructed_tx_relay_times[tx_hash] = static_cast(time(NULL)); + } + + void monero_wallet_light_cache::for_each_unconfirmed_tx(const std::function&)>& visitor) const { + boost::lock_guard lock(m_mutex); + for (const auto& kv : m_self_constructed_txs) { + if (kv.second != nullptr && !kv.second->m_is_confirmed.value_or(false)) visitor(kv.first, kv.second); + } + } + + uint64_t monero_wallet_light_cache::get_num_blocks_to_unlock(const std::string& tx_hash) const { + boost::lock_guard lock(m_mutex); + auto tx_it = m_txs.find(tx_hash); + if (tx_it == m_txs.end()) return CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE; + const auto& tx = tx_it->second; + uint64_t current_height = get_scanned_block_height() + 1; + 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; + if (unlock_time < CRYPTONOTE_MAX_BLOCK_NUMBER) { + // unlock_time is a block height + num_blocks_to_unlock = unlock_time <= current_height ? 0 : unlock_time - current_height; + } else { + // unlock_time is a unix timestamp; approximate remaining blocks assuming DIFFICULTY_TARGET_V2 seconds/block + uint64_t now = static_cast(time(NULL)); + num_blocks_to_unlock = unlock_time <= now ? 0 : (unlock_time - now) / DIFFICULTY_TARGET_V2; + } + + return num_blocks_to_unlock > confirmations_needed ? num_blocks_to_unlock : confirmations_needed; + } + + uint64_t monero_wallet_light_cache::get_num_blocks_to_unlock(const std::vector>& outputs) const { + uint64_t num_blocks = 0; + for(const auto &output : outputs) { + if (output->m_tx_hash == boost::none) continue; + uint64_t blocks = get_num_blocks_to_unlock(output->m_tx_hash.get()); + if (blocks > num_blocks) num_blocks = blocks; + } + return num_blocks; + } + + uint64_t monero_wallet_light_cache::get_num_blocks_to_unlock(uint32_t account_idx, uint32_t subaddress_idx) const { + boost::lock_guard lock(m_mutex); + uint64_t num_blocks = 0; + auto spent_account_it = m_spent.find(account_idx); + if (spent_account_it != m_spent.end()) { + auto subaddr_it = spent_account_it->second.find(subaddress_idx); + if (subaddr_it != spent_account_it->second.end()) num_blocks = std::max(num_blocks, get_num_blocks_to_unlock(subaddr_it->second)); + } + auto unspent_account_it = m_unspent.find(account_idx); + if (unspent_account_it != m_unspent.end()) { + auto subaddr_it = unspent_account_it->second.find(subaddress_idx); + if (subaddr_it != unspent_account_it->second.end()) num_blocks = std::max(num_blocks, get_num_blocks_to_unlock(subaddr_it->second)); + } + return num_blocks; + } + + bool monero_wallet_light_cache::is_key_image_in_pool(const std::string& key_image) const { + boost::lock_guard lock(m_mutex); + for (const auto &kv : m_self_constructed_txs) { + const auto& self_tx = kv.second; + if (self_tx == nullptr || self_tx->m_is_relayed != true || self_tx->m_is_failed == true) continue; + for (const auto &in : self_tx->m_inputs) { + std::shared_ptr input = std::static_pointer_cast(in); + if (input == nullptr || input->m_key_image == nullptr || input->m_key_image->m_hex == boost::none) continue; + if (input->m_key_image->m_hex.get() == key_image) return true; + } + } + return false; + } + + std::unordered_set monero_wallet_light_cache::get_pool_key_images() const { + boost::lock_guard lock(m_mutex); + std::unordered_set images; + for (const auto &kv : m_self_constructed_txs) { + const auto& self_tx = kv.second; + if (self_tx == nullptr || self_tx->m_is_relayed != true || self_tx->m_is_failed == true) continue; + for (const auto &in : self_tx->m_inputs) { + std::shared_ptr input = std::static_pointer_cast(in); + if (input == nullptr || input->m_key_image == nullptr || input->m_key_image->m_hex == boost::none) continue; + images.insert(input->m_key_image->m_hex.get()); + } + } + return images; + } + + bool monero_wallet_light_cache::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_wallet_light_cache::is_key_image_spent(const std::shared_ptr& key_image) const { + if (key_image == nullptr) throw std::runtime_error("key image is null"); + if (key_image->m_hex == boost::none) return false; + return is_key_image_spent(key_image->m_hex.get()); + } + + bool monero_wallet_light_cache::is_key_image_spent(const std::string& key_image, const std::unordered_set& pool_key_images) const { + if (pool_key_images.count(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_wallet_light_cache::is_key_image_spent(const std::shared_ptr& key_image, const std::unordered_set& pool_key_images) const { + if (key_image == nullptr) throw std::runtime_error("key image is null"); + if (key_image->m_hex == boost::none) return false; + return is_key_image_spent(key_image->m_hex.get(), pool_key_images); + } + + void monero_wallet_light_cache::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_balance = get_balance(account_idx, subaddress_idx); + subaddress.m_unlocked_balance = get_unlocked_balance(account_idx, subaddress_idx); + subaddress.m_num_unspent_outputs = get_num_unspent(account_idx, subaddress_idx); + subaddress.m_is_used = is_subaddress_used(account_idx, subaddress_idx); + subaddress.m_num_blocks_to_unlock = get_num_blocks_to_unlock(account_idx, subaddress_idx); + } + + std::shared_ptr monero_wallet_light_cache::init_tx_with_output(const std::shared_ptr& out, const std::unordered_set& pool_key_images) const { + // 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 = get_num_blocks_to_unlock(out->m_tx_hash.get()) > 0; + + // 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->is_key_image_known()) { + output->m_key_image = std::make_shared(); + output->m_key_image.get()->m_hex = out->m_key_image; + output->m_is_frozen = out->m_frozen.value_or(false); + if (!*output->m_is_spent) output->m_is_spent = is_key_image_spent(out->m_key_image.get(), pool_key_images); + } + + // return pointer to new tx + return tx; + } + + +} \ 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..efba665a --- /dev/null +++ b/src/wallet/monero_wallet_light_model.h @@ -0,0 +1,466 @@ +/** + * Copyright (c) everoddaneven + * + * 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 "utils/monero_wallet_utils.h" +#include +#include +#include + +/** + * Internal data model for monero_wallet_light. + */ +namespace monero { + + // ------------------------------- LWS DATA MODEL ------------------------------- + + struct monero_daemon_status { + boost::optional m_state; + boost::optional m_outgoing_connections_count; + boost::optional m_incoming_connections_count; + boost::optional m_height; + boost::optional m_target_height; + boost::optional m_network_type; + + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& version); + }; + + struct monero_address_meta { + uint32_t m_maj_i = 0; + uint32_t m_min_i = 0; + + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& address_meta); + }; + + struct monero_output_light { + 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; + boost::optional m_key_image; + boost::optional m_tx_id; + boost::optional m_amount; + boost::optional m_index; + boost::optional m_global_index; + boost::optional m_timestamp; + boost::optional m_height; + boost::optional m_cache_index; + boost::optional m_frozen; + std::shared_ptr m_recipient; + std::vector m_spend_key_images; + + bool is_key_image_known() const; + bool is_rct() const; + bool is_coinbase() const; + bool is_spent() const; + + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& output); + }; + + struct monero_spend { + boost::optional m_key_image; + boost::optional m_tx_pub_key; + boost::optional m_amount; + boost::optional m_out_index; + boost::optional m_mixin; + std::shared_ptr m_sender; + + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& spend); + }; + + struct monero_tx_light { + boost::optional m_hash; + boost::optional m_payment_id; + boost::optional m_id; + 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; + boost::optional m_mixin; + boost::optional m_coinbase; + boost::optional m_mempool; + std::shared_ptr m_recipient; + std::vector> m_spent_outputs; + + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& transaction); + }; + + struct monero_random_outputs { + boost::optional m_amount; + std::vector> m_outputs; + + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& random_outputs); + }; + + class monero_index_range : public std::vector { + public: + monero_index_range() = default; + monero_index_range(const uint32_t min_i, const uint32_t maj_i); + + std::vector to_subaddress_indices() const; + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& index_range); + }; + + class monero_subaddrs : public std::map>>, public serializable_struct { + public: + static const uint64_t MAX_ACCOUNTS = 10000; + + bool contains(const uint32_t account_idx) const { return find(account_idx) != end(); } + bool is_upsert(const uint32_t account_idx) const { return account_idx == 0 || contains(account_idx); } + uint32_t get_last_account_index() const; + uint32_t get_last_subaddress_index(const uint32_t account_idx) const; + std::vector get_subaddresses_indices(const uint32_t account_idx) const; + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& subaddrs); + }; + + typedef std::unordered_map>> monero_output_map; + + struct monero_outputs_decoys_tie { + std::vector> m_decoys; + monero_output_map m_tie_attempt; + + static monero_outputs_decoys_tie tie(const std::vector>& outputs, std::vector> decoys, const boost::optional& prior_tie_attempt); + }; + + struct monero_output_selection { + uint32_t m_mixin; + uint64_t m_fee; + uint64_t m_amount; + uint64_t m_change_amount; + std::vector> m_selected_outs; + + std::vector get_output_indexes() const; + }; + + // ------------------------------ RPC Params --------------------------------- + + struct monero_wallet_params : public serializable_struct { + boost::optional m_address; + boost::optional m_view_key; + + monero_wallet_params(const std::string& address, const std::string& view_key): m_address(address), m_view_key(view_key) {} + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; + }; + + struct monero_get_random_outs_params : public serializable_struct { + boost::optional m_count; + std::vector m_amounts; + + monero_get_random_outs_params(uint32_t count, const std::vector& amounts): m_count(count), m_amounts(amounts) {} + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; + }; + + struct monero_get_unspent_outs_params : public monero_wallet_params { + boost::optional m_amount; + boost::optional m_dust_threshold; + boost::optional m_mixin; + boost::optional m_use_dust; + + monero_get_unspent_outs_params(const std::string& address, const std::string& view_key, uint64_t amount, uint32_t mixin, bool use_dust, uint64_t dust_threshold): monero_wallet_params(address, view_key), m_amount(amount), m_mixin(mixin), m_use_dust(use_dust), m_dust_threshold(dust_threshold) {} + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; + }; + + struct monero_import_wallet_params : public monero_wallet_params { + boost::optional m_from_height; + + monero_import_wallet_params(const std::string& address, const std::string& view_key, uint64_t from_height): monero_wallet_params(address, view_key), m_from_height(from_height) {} + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; + }; + + struct monero_login_params : public monero_wallet_params { + boost::optional m_create_account; + boost::optional m_generated_locally; + + monero_login_params(const std::string& address, const std::string& view_key, bool create_account, bool generated_locally): monero_wallet_params(address, view_key), m_create_account(create_account), m_generated_locally(generated_locally) {} + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; + }; + + struct monero_submit_raw_tx_params : public serializable_struct { + boost::optional m_tx; + + monero_submit_raw_tx_params(const std::string& tx): m_tx(tx) {} + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; + }; + + struct monero_upsert_subaddrs_params : public monero_wallet_params { + boost::optional m_get_all; + boost::optional m_subaddrs; + + monero_upsert_subaddrs_params(const std::string& address, const std::string& view_key, const monero_subaddrs& subaddrs, bool get_all): monero_wallet_params(address, view_key), m_subaddrs(subaddrs), m_get_all(get_all) {} + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; + }; + + // ------------------------------ RPC Response --------------------------------- + + struct monero_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 void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& response); + }; + + struct monero_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 void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& response); + }; + + struct monero_get_random_outs_response { + std::vector> m_amount_outs; + + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& response); + }; + + struct monero_get_unspent_outs_response { + boost::optional m_per_byte_fee; + boost::optional m_fee_mask; + boost::optional m_amount; + std::vector> m_outputs; + std::vector m_fees; + + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& response); + }; + + struct monero_import_wallet_response { + boost::optional m_payment_address; + boost::optional m_payment_id; + boost::optional m_status; + boost::optional m_import_fee; + boost::optional m_new_request; + boost::optional m_request_fullfilled; + + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& response); + }; + + struct monero_login_response { + boost::optional m_start_height; + boost::optional m_new_address; + boost::optional m_generated_locally; + + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& response); + }; + + struct monero_submit_raw_tx_response { + boost::optional m_status; + + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& response); + }; + + struct monero_subaddrs_response { + std::shared_ptr m_new_subaddrs; + std::shared_ptr m_all_subaddrs; + + static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& response); + }; + + // ------------------------------- MONERO WALLET CACHE ------------------------------- + + struct monero_output_wallet_light : public monero_output_wallet { + boost::optional m_is_change; + }; + + class monero_wallet_light_cache { + public: + std::vector> m_outputs; + std::shared_ptr m_subaddrs; + serializable_unordered_map m_subaddresses; + serializable_unordered_map>> m_processed_subaddr_ranges; + serializable_unordered_map m_tx_keys; + serializable_unordered_map> m_additional_tx_keys; + + monero_wallet_light_cache(const std::shared_ptr& key_image_cache); + ~monero_wallet_light_cache(); + + // sync info + uint64_t get_blockchain_height() const; + uint64_t get_last_block_reward() const; + uint64_t get_scanned_block_height() const; + uint64_t get_start_height() const; + void set_start_height(uint64_t height); + void set_sync_status(const monero_get_address_info_response& address_info); + + // balance info + uint64_t get_balance() const; + 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; + uint64_t get_unlocked_balance(uint32_t account_idx) const; + uint64_t get_unlocked_balance(uint32_t account_idx, uint32_t subaddress_idx) const; + void calculate_balance(); + + // transactions + std::vector> get_txs() const; + void add_unconfirmed_tx(const std::shared_ptr& tx, const std::string& change_pubkey = ""); + void for_each_unconfirmed_tx(const std::function&)>& visitor) const; + std::string get_tx_prefix_hash(const std::string& tx_hash) const; + void refresh(monero_get_unspent_outs_response& unspent_outs, monero_get_address_txs_response& address_txs, const monero_get_address_info_response& address_info); + std::vector> get_tx_destinations(const std::string& tx_hash) const; + std::shared_ptr get_self_constructed_tx(const std::string& tx_hash) const; + uint64_t get_num_blocks_to_unlock(const std::string& tx_hash) const; + std::shared_ptr init_tx_with_output(const std::shared_ptr& out, const std::unordered_set& pool_key_images) const; + + // key images + bool is_key_image_spent(const std::string& key_image) const; + bool is_key_image_spent(const std::shared_ptr& key_image) const; + // bulk variants for callers checking many key images in a loop (e.g. import_key_images()): + // pass a pool_key_images set from get_pool_key_images() instead of re-scanning per call + bool is_key_image_spent(const std::string& key_image, const std::unordered_set& pool_key_images) const; + bool is_key_image_spent(const std::shared_ptr& key_image, const std::unordered_set& pool_key_images) const; + // key images of every unconfirmed, relayed, self-constructed tx's inputs; computed once for + // callers that otherwise re-scan via is_key_image_in_pool() once per output in a loop + std::unordered_set get_pool_key_images() const; + bool is_key_image_frozen(const std::string& key_image) const; + void set_key_image_frozen(const std::string& key_image, bool frozen); + void set_key_image(const std::string& key_image, size_t index); + + // outputs + std::shared_ptr get_output(const std::string& key_image) const; + std::vector> get_outputs(uint32_t account_idx) const; + std::vector> get_outputs(uint32_t account_idx, uint32_t subaddress_idx) const; + std::vector> get_spendable(const uint32_t account_idx, const std::vector &subaddresses_indices) const; + std::vector> get_tx_outputs(const std::string& tx_hash, bool filter_spent = false) const; + monero_wallet_utils::wallet2_exported_outputs export_outputs(bool all, uint32_t start, uint32_t count = 0xffffffff) const; + boost::optional get_change_pubkey(const std::string& tx_hash) const; + uint64_t get_num_blocks_to_unlock(const std::vector>& outputs) const; + void reindex_outputs(uint64_t amount); + void resort_outputs_by_chain_order(); + uint64_t get_per_byte_fee() const; + uint64_t get_base_fee(uint32_t priority) const; + uint64_t get_fee_mask() const; + uint64_t get_amount() const; + + // subaddresses + bool is_subaddress_used(uint32_t account_idx, uint32_t subaddress_idx) const; + uint64_t get_num_unspent(uint32_t account_idx, uint32_t subaddress_idx) const; + uint64_t get_num_blocks_to_unlock(uint32_t account_idx, uint32_t subaddress_idx) const; + void init_subaddress(monero_subaddress& subaddress) const; + + private: + mutable boost::recursive_mutex m_mutex; + std::shared_ptr m_key_image_cache; + + // transactions + serializable_unordered_map> m_txs; + std::vector> m_tx_list; + serializable_unordered_map> m_self_constructed_txs; + serializable_unordered_map m_self_constructed_tx_relay_times; // unix seconds + serializable_unordered_map m_spent_key_images; + + std::shared_ptr get_tx(const std::string& hash) const; + void set_txs(monero_get_address_txs_response& response, const monero_get_address_info_response& addr_info_response); + + // blockchain + uint64_t m_block_reward = 0; + uint64_t m_blockchain_height = 0; + uint64_t m_scanned_block_height = 0; + uint64_t m_start_height = 0; + + // key images + bool is_key_image_in_pool(const std::string& key_image) const; + + // outputs + uint64_t m_per_byte_fee = 0; + uint64_t m_fee_mask = 0; + uint64_t m_amount = 0; + std::vector m_fees; + mutable serializable_unordered_map>> m_tx_hash_index; + mutable serializable_unordered_map m_key_image_index; + mutable serializable_unordered_map>>> m_spent; + mutable serializable_unordered_map>>> m_unspent; + + void reindex(); + void set_outputs(monero_get_unspent_outs_response& response); + 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; + + // 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