diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index bd074e962..4fb5f1272 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -4625,6 +4625,447 @@ static uint32_t get_count_above(const std::vector &tr return count; } +bool wallet2::light_wallet_login(bool &new_address) +{ + MDEBUG("Light wallet login request"); + m_light_wallet_connected = false; + cryptonote::COMMAND_RPC_LOGIN::request request; + cryptonote::COMMAND_RPC_LOGIN::response response; + request.address = get_account().get_public_address_str(m_testnet); + request.view_key = string_tools::pod_to_hex(get_account().get_keys().m_view_secret_key); + // Always create account if it doesnt exist. + request.create_account = true; + m_daemon_rpc_mutex.lock(); + bool connected = epee::net_utils::invoke_http_json("/login", request, response, m_http_client, rpc_timeout, "POST"); + m_daemon_rpc_mutex.unlock(); + // MyMonero doesn't send any status message. OpenMonero does. + m_light_wallet_connected = connected && (response.status.empty() || response.status == "success"); + new_address = response.new_address; + MDEBUG("Status: " << response.status); + MDEBUG("Reason: " << response.reason); + MDEBUG("New wallet: " << response.new_address); + if(m_light_wallet_connected) + { + // Clear old data on successfull login. + // m_transfers.clear(); + // m_payments.clear(); + // m_unconfirmed_payments.clear(); + } + return m_light_wallet_connected; +} + +bool wallet2::light_wallet_import_wallet_request(cryptonote::COMMAND_RPC_IMPORT_WALLET_REQUEST::response &response) +{ + MDEBUG("Light wallet import wallet request"); + cryptonote::COMMAND_RPC_IMPORT_WALLET_REQUEST::request oreq; + oreq.address = get_account().get_public_address_str(m_testnet); + oreq.view_key = string_tools::pod_to_hex(get_account().get_keys().m_view_secret_key); + m_daemon_rpc_mutex.lock(); + bool r = epee::net_utils::invoke_http_json("/import_wallet_request", oreq, response, m_http_client, rpc_timeout, "POST"); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "import_wallet_request"); + + + return true; +} + +void wallet2::light_wallet_get_unspent_outs() +{ + MDEBUG("Getting unspent outs"); + + cryptonote::COMMAND_RPC_GET_UNSPENT_OUTS::request oreq; + cryptonote::COMMAND_RPC_GET_UNSPENT_OUTS::response ores; + + oreq.amount = "0"; + oreq.address = get_account().get_public_address_str(m_testnet); + oreq.view_key = string_tools::pod_to_hex(get_account().get_keys().m_view_secret_key); + // openMonero specific + oreq.dust_threshold = boost::lexical_cast(::config::DEFAULT_DUST_THRESHOLD); + // below are required by openMonero api - but are not used. + oreq.mixin = 0; + oreq.use_dust = true; + + + m_daemon_rpc_mutex.lock(); + bool r = epee::net_utils::invoke_http_json("/get_unspent_outs", oreq, ores, m_http_client, rpc_timeout, "POST"); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_unspent_outs"); + THROW_WALLET_EXCEPTION_IF(ores.status == "error", error::wallet_internal_error, ores.reason); + + m_light_wallet_per_kb_fee = ores.per_kb_fee; + + std::unordered_map transfers_txs; + for(const auto &t: m_transfers) + transfers_txs.emplace(t.m_txid,t.m_spent); + + MDEBUG("FOUND " << ores.outputs.size() <<" outputs"); + + // return if no outputs found + if(ores.outputs.empty()) + return; + + // Clear old outputs + m_transfers.clear(); + + for (const auto &o: ores.outputs) { + bool spent = false; + bool add_transfer = true; + crypto::key_image unspent_key_image; + crypto::public_key tx_public_key = AUTO_VAL_INIT(tx_public_key); + THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, o.tx_pub_key), error::wallet_internal_error, "Invalid tx_pub_key field"); + string_tools::hex_to_pod(o.tx_pub_key, tx_public_key); + + for (const std::string &ski: o.spend_key_images) { + spent = false; + + // Check if key image is ours + THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, ski), error::wallet_internal_error, "Invalid key image"); + string_tools::hex_to_pod(ski, unspent_key_image); + if(light_wallet_key_image_is_ours(unspent_key_image, tx_public_key, o.index)){ + MTRACE("Output " << o.public_key << " is spent. Key image: " << ski); + spent = true; + break; + } { + MTRACE("Unspent output found. " << o.public_key); + } + } + + // Check if tx already exists in m_transfers. + crypto::hash txid; + crypto::public_key tx_pub_key; + crypto::public_key public_key; + THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, o.tx_hash), error::wallet_internal_error, "Invalid tx_hash field"); + THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, o.public_key), error::wallet_internal_error, "Invalid public_key field"); + THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, o.tx_pub_key), error::wallet_internal_error, "Invalid tx_pub_key field"); + string_tools::hex_to_pod(o.tx_hash, txid); + string_tools::hex_to_pod(o.public_key, public_key); + string_tools::hex_to_pod(o.tx_pub_key, tx_pub_key); + + for(auto &t: m_transfers){ + if(t.get_public_key() == public_key) { + t.m_spent = spent; + add_transfer = false; + break; + } + } + + if(!add_transfer) + continue; + + m_transfers.push_back(boost::value_initialized()); + transfer_details& td = m_transfers.back(); + + td.m_block_height = o.height; + td.m_global_output_index = o.global_index; + td.m_txid = txid; + + // Add to extra + add_tx_pub_key_to_extra(td.m_tx, tx_pub_key); + + td.m_key_image = unspent_key_image; + td.m_key_image_known = !m_watch_only; + td.m_amount = o.amount; + td.m_pk_index = 0; + td.m_internal_output_index = o.index; + td.m_spent = spent; + + tx_out txout; + txout.target = txout_to_key(public_key); + txout.amount = td.m_amount; + + td.m_tx.vout.resize(td.m_internal_output_index + 1); + td.m_tx.vout[td.m_internal_output_index] = txout; + + // Add unlock time and coinbase bool got from get_address_txs api call + std::unordered_map::const_iterator found = m_light_wallet_address_txs.find(txid); + THROW_WALLET_EXCEPTION_IF(found == m_light_wallet_address_txs.end(), error::wallet_internal_error, "Lightwallet: tx not found in m_light_wallet_address_txs"); + bool miner_tx = found->second.m_coinbase; + td.m_tx.unlock_time = found->second.m_unlock_time; + + if (!o.rct.empty()) + { + // Coinbase tx's + if(miner_tx) + { + td.m_mask = rct::identity(); + } + else + { + // rct txs + // decrypt rct mask, calculate commit hash and compare against blockchain commit hash + rct::key rct_commit; + light_wallet_parse_rct_str(o.rct, tx_pub_key, td.m_internal_output_index, td.m_mask, rct_commit, true); + bool valid_commit = (rct_commit == rct::commit(td.amount(), td.m_mask)); + if(!valid_commit) + { + MDEBUG("output index: " << o.global_index); + MDEBUG("mask: " + string_tools::pod_to_hex(td.m_mask)); + MDEBUG("calculated commit: " + string_tools::pod_to_hex(rct::commit(td.amount(), td.m_mask))); + MDEBUG("expected commit: " + string_tools::pod_to_hex(rct_commit)); + MDEBUG("amount: " << td.amount()); + } + THROW_WALLET_EXCEPTION_IF(!valid_commit, error::wallet_internal_error, "Lightwallet: rct commit hash mismatch!"); + } + td.m_rct = true; + } + else + { + td.m_mask = rct::identity(); + td.m_rct = false; + } + if(!spent) + set_unspent(m_transfers.size()-1); + m_key_images[td.m_key_image] = m_transfers.size()-1; + m_pub_keys[td.get_public_key()] = m_transfers.size()-1; + } +} + +bool wallet2::light_wallet_get_address_info(cryptonote::COMMAND_RPC_GET_ADDRESS_INFO::response &response) +{ + MTRACE(__FUNCTION__); + + cryptonote::COMMAND_RPC_GET_ADDRESS_INFO::request request; + + request.address = get_account().get_public_address_str(m_testnet); + request.view_key = string_tools::pod_to_hex(get_account().get_keys().m_view_secret_key); + m_daemon_rpc_mutex.lock(); + bool r = epee::net_utils::invoke_http_json("/get_address_info", request, response, m_http_client, rpc_timeout, "POST"); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_address_info"); + // TODO: Validate result + return true; +} + +void wallet2::light_wallet_get_address_txs() +{ + MDEBUG("Refreshing light wallet"); + + cryptonote::COMMAND_RPC_GET_ADDRESS_TXS::request ireq; + cryptonote::COMMAND_RPC_GET_ADDRESS_TXS::response ires; + + ireq.address = get_account().get_public_address_str(m_testnet); + ireq.view_key = string_tools::pod_to_hex(get_account().get_keys().m_view_secret_key); + m_daemon_rpc_mutex.lock(); + bool r = epee::net_utils::invoke_http_json("/get_address_txs", ireq, ires, m_http_client, rpc_timeout, "POST"); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_address_txs"); + //OpenMonero sends status=success, Mymonero doesn't. + THROW_WALLET_EXCEPTION_IF((!ires.status.empty() && ires.status != "success"), error::no_connection_to_daemon, "get_address_txs"); + + + // Abort if no transactions + if(ires.transactions.empty()) + return; + + // Create searchable vectors + std::vector payments_txs; + for(const auto &p: m_payments) + payments_txs.push_back(p.second.m_tx_hash); + std::vector unconfirmed_payments_txs; + for(const auto &up: m_unconfirmed_payments) + unconfirmed_payments_txs.push_back(up.second.m_tx_hash); + + // for balance calculation + uint64_t wallet_total_sent = 0; + uint64_t wallet_total_unlocked_sent = 0; + // txs in pool + std::vector pool_txs; + + for (const auto &t: ires.transactions) { + const uint64_t total_received = t.total_received; + uint64_t total_sent = t.total_sent; + + // Check key images - subtract fake outputs from total_sent + for(const auto &so: t.spent_outputs) + { + crypto::public_key tx_public_key; + crypto::key_image key_image; + THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, so.tx_pub_key), error::wallet_internal_error, "Invalid tx_pub_key field"); + THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, so.key_image), error::wallet_internal_error, "Invalid key_image field"); + string_tools::hex_to_pod(so.tx_pub_key, tx_public_key); + string_tools::hex_to_pod(so.key_image, key_image); + + if(!light_wallet_key_image_is_ours(key_image, tx_public_key, so.out_index)) { + THROW_WALLET_EXCEPTION_IF(so.amount > t.total_sent, error::wallet_internal_error, "Lightwallet: total sent is negative!"); + total_sent -= so.amount; + } + } + + // Do not add tx if empty. + if(total_sent == 0 && total_received == 0) + continue; + + crypto::hash payment_id = null_hash; + crypto::hash tx_hash; + + THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, t.payment_id), error::wallet_internal_error, "Invalid payment_id field"); + THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, t.hash), error::wallet_internal_error, "Invalid hash field"); + string_tools::hex_to_pod(t.payment_id, payment_id); + string_tools::hex_to_pod(t.hash, tx_hash); + + // lightwallet specific info + bool incoming = (total_received > total_sent); + address_tx address_tx; + address_tx.m_tx_hash = tx_hash; + address_tx.m_incoming = incoming; + address_tx.m_amount = incoming ? total_received - total_sent : total_sent - total_received; + address_tx.m_block_height = t.height; + address_tx.m_unlock_time = t.unlock_time; + address_tx.m_timestamp = t.timestamp; + address_tx.m_coinbase = t.coinbase; + address_tx.m_mempool = t.mempool; + m_light_wallet_address_txs.emplace(tx_hash,address_tx); + + // populate data needed for history (m_payments, m_unconfirmed_payments, m_confirmed_txs) + // INCOMING transfers + if(total_received > total_sent) { + payment_details payment; + payment.m_tx_hash = tx_hash; + payment.m_amount = total_received - total_sent; + payment.m_block_height = t.height; + payment.m_unlock_time = t.unlock_time; + payment.m_timestamp = t.timestamp; + + if (t.mempool) { + if (std::find(unconfirmed_payments_txs.begin(), unconfirmed_payments_txs.end(), tx_hash) == unconfirmed_payments_txs.end()) { + pool_txs.push_back(tx_hash); + m_unconfirmed_payments.emplace(tx_hash, payment); + if (0 != m_callback) { + cryptonote::transaction dummy_tx; + m_callback->on_unconfirmed_money_received(t.height, payment.m_tx_hash, dummy_tx, payment.m_amount); + } + } + } else { + if (std::find(payments_txs.begin(), payments_txs.end(), tx_hash) == payments_txs.end()) { + m_payments.emplace(tx_hash, payment); + if (0 != m_callback) { + cryptonote::transaction dummy_tx; + m_callback->on_money_received(t.height, payment.m_tx_hash, dummy_tx, payment.m_amount); + } + } + } + // Outgoing transfers + } else { + uint64_t amount_sent = total_sent - total_received; + cryptonote::transaction dummy_tx; // not used by light wallet + // increase wallet total sent + wallet_total_sent += total_sent; + if (t.mempool) + { + // Handled by add_unconfirmed_tx in commit_tx + // If sent from another wallet instance we need to add it + if(m_unconfirmed_txs.find(tx_hash) == m_unconfirmed_txs.end()) + { + unconfirmed_transfer_details utd; + utd.m_amount_in = amount_sent; + utd.m_amount_out = amount_sent; + utd.m_change = 0; + utd.m_payment_id = payment_id; + utd.m_timestamp = t.timestamp; + utd.m_state = wallet2::unconfirmed_transfer_details::pending; + m_unconfirmed_txs.emplace(tx_hash,utd); + } + } + else + { + // Only add if new + auto confirmed_tx = m_confirmed_txs.find(tx_hash); + if(confirmed_tx == m_confirmed_txs.end()) { + // tx is added to m_unconfirmed_txs - move to confirmed + if(m_unconfirmed_txs.find(tx_hash) != m_unconfirmed_txs.end()) + { + process_unconfirmed(tx_hash, dummy_tx, t.height); + } + // Tx sent by another wallet instance + else + { + confirmed_transfer_details ctd; + ctd.m_amount_in = amount_sent; + ctd.m_amount_out = amount_sent; + ctd.m_change = 0; + ctd.m_payment_id = payment_id; + ctd.m_block_height = t.height; + ctd.m_timestamp = t.timestamp; + m_confirmed_txs.emplace(tx_hash,ctd); + } + if (0 != m_callback) + { + m_callback->on_money_spent(t.height, tx_hash, dummy_tx, amount_sent, dummy_tx); + } + } + // If not new - check the amount and update if necessary. + // when sending a tx to same wallet the receiving amount has to be credited + else + { + if(confirmed_tx->second.m_amount_in != amount_sent || confirmed_tx->second.m_amount_out != amount_sent) + { + MDEBUG("Adjusting amount sent/received for tx: <" + t.hash + ">. Is tx sent to own wallet? " << print_money(amount_sent) << " != " << print_money(confirmed_tx->second.m_amount_in)); + confirmed_tx->second.m_amount_in = amount_sent; + confirmed_tx->second.m_amount_out = amount_sent; + confirmed_tx->second.m_change = 0; + } + } + } + } + } + // TODO: purge old unconfirmed_txs + remove_obsolete_pool_txs(pool_txs); + + // Calculate wallet balance + m_light_wallet_balance = ires.total_received-wallet_total_sent; + // MyMonero doesnt send unlocked balance + if(ires.total_received_unlocked > 0) + m_light_wallet_unlocked_balance = ires.total_received_unlocked-wallet_total_sent; + else + m_light_wallet_unlocked_balance = m_light_wallet_balance; +} + +bool wallet2::light_wallet_parse_rct_str(const std::string& rct_string, const crypto::public_key& tx_pub_key, uint64_t internal_output_index, rct::key& decrypted_mask, rct::key& rct_commit, bool decrypt) const +{ + // rct string is empty if output is non RCT + if (rct_string.empty()) + return false; + // rct_string is a string with length 64+64+64 ( + + ) + rct::key encrypted_mask; + std::string rct_commit_str = rct_string.substr(0,64); + std::string encrypted_mask_str = rct_string.substr(64,64); + THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, rct_commit_str), error::wallet_internal_error, "Invalid rct commit hash: " + rct_commit_str); + THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, encrypted_mask_str), error::wallet_internal_error, "Invalid rct mask: " + encrypted_mask_str); + string_tools::hex_to_pod(rct_commit_str, rct_commit); + string_tools::hex_to_pod(encrypted_mask_str, encrypted_mask); + if (decrypt) { + // Decrypt the mask + crypto::key_derivation derivation; + generate_key_derivation(tx_pub_key, get_account().get_keys().m_view_secret_key, derivation); + crypto::secret_key scalar; + crypto::derivation_to_scalar(derivation, internal_output_index, scalar); + sc_sub(decrypted_mask.bytes,encrypted_mask.bytes,rct::hash_to_scalar(rct::sk2rct(scalar)).bytes); + } + return true; +} + +bool wallet2::light_wallet_key_image_is_ours(const crypto::key_image& key_image, const crypto::public_key& tx_public_key, uint64_t out_index) +{ + // Lookup key image from cache + std::map index_keyimage_map; + std::unordered_map >::const_iterator found_pub_key = m_key_image_cache.find(tx_public_key); + if(found_pub_key != m_key_image_cache.end()) { + // pub key found. key image for index cached? + index_keyimage_map = found_pub_key->second; + std::map::const_iterator index_found = index_keyimage_map.find(out_index); + if(index_found != index_keyimage_map.end()) + return key_image == index_found->second; + } + + // Not in cache - calculate key image + crypto::key_image calculated_key_image; + cryptonote::keypair in_ephemeral; + cryptonote::generate_key_image_helper(get_account().get_keys(), tx_public_key, out_index, in_ephemeral, calculated_key_image); + index_keyimage_map.emplace(out_index, calculated_key_image); + m_key_image_cache.emplace(tx_public_key, index_keyimage_map); + return key_image == calculated_key_image; +} + // Another implementation of transaction creation that is hopefully better // While there is anything left to pay, it goes through random outputs and tries // to fill the next destination/amount. If it fully fills it, it will use the diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 034dd693b..4fb8407d2 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -230,6 +230,13 @@ namespace tools cryptonote::subaddress_index m_subaddr_index; }; + struct address_tx : payment_details + { + bool m_coinbase; + bool m_mempool; + bool m_incoming; + }; + struct unconfirmed_transfer_details { cryptonote::transaction_prefix m_tx; @@ -423,6 +430,15 @@ namespace tools */ bool is_deterministic() const; bool get_seed(std::string& electrum_words, const std::string &passphrase = std::string()) const; + + /*! + * \brief Checks if light wallet. A light wallet sends view key to a server where the blockchain is scanned. + */ + bool light_wallet() const { return m_light_wallet; } + void set_light_wallet(bool light_wallet) { m_light_wallet = light_wallet; } + uint64_t get_light_wallet_scanned_block_height() const { return m_light_wallet_scanned_block_height; } + uint64_t get_light_wallet_blockchain_height() const { return m_light_wallet_blockchain_height; } + /*! * \brief Gets the seed language */ @@ -715,6 +731,24 @@ namespace tools uint64_t get_fee_multiplier(uint32_t priority, int fee_algorithm = -1); uint64_t get_per_kb_fee(); + // Light wallet specific functions + // fetch unspent outs from lw node and store in m_transfers + void light_wallet_get_unspent_outs(); + // fetch txs and store in m_payments + void light_wallet_get_address_txs(); + // get_address_info + bool light_wallet_get_address_info(cryptonote::COMMAND_RPC_GET_ADDRESS_INFO::response &response); + // Login. new_address is true if address hasn't been used on lw node before. + bool light_wallet_login(bool &new_address); + // Send an import request to lw node. returns info about import fee, address and payment_id + bool light_wallet_import_wallet_request(cryptonote::COMMAND_RPC_IMPORT_WALLET_REQUEST::response &response); + // get random outputs from light wallet server + void light_wallet_get_outs(std::vector> &outs, const std::list &selected_transfers, size_t fake_outputs_count); + // Parse rct string + bool light_wallet_parse_rct_str(const std::string& rct_string, const crypto::public_key& tx_pub_key, uint64_t internal_output_index, rct::key& decrypted_mask, rct::key& rct_commit, bool decrypt) const; + // check if key image is ours + bool light_wallet_key_image_is_ours(const crypto::key_image& key_image, const crypto::public_key& tx_public_key, uint64_t out_index); + private: /*! * \brief Stores wallet information to wallet file.