diff options
author | Riccardo Spagni <ric@spagni.net> | 2017-10-15 18:25:08 +0200 |
---|---|---|
committer | Riccardo Spagni <ric@spagni.net> | 2017-10-15 18:25:08 +0200 |
commit | 960886aa04f0b50265884c0d4338031a93b1bc5d (patch) | |
tree | b74d770e86309091072c4ca66a6dbef7fff5597a /src/wallet/wallet2.cpp | |
parent | Merge pull request #2555 (diff) | |
parent | wallet2+API: use separate callbacks for lightwallets (diff) | |
download | monero-960886aa04f0b50265884c0d4338031a93b1bc5d.tar.xz |
Merge pull request #2109
97c2e449 wallet2+API: use separate callbacks for lightwallets (Jaquee)
d9261867 walletAPI: correct confirmations in txHistory for unsynced wallets (Jaquee)
9442b043 walletAPI: lightwallet exceptions (Jaquee)
fc922934 walletAPI: add lightwallet login() and importWalletRequest() (Jaquee)
79207743 walletAPI: init() lightwallet and SSL support (Jaquee)
dde5a1fc walletAPI: add tx unlock_time (Jaquee)
bba5cbed wallet2: remove obsolete get_num_rct_outputs() call from create_transactions_2 (Jaquee)
7a482f30 wallet2: create_transactions_2 lightwallet support (Jaquee)
ce61b818 wallet2: get_outs lightwallet support (Jaquee)
1197cb71 wallet2: commit_tx() lightwallet support (Jaquee)
43b57804 wallet2: refactor is_tx_spendtime_unlocked() (Jaquee)
32e2b003 wallet2: add lightwallet exceptions to common functions (Jaquee)
2e692fc0 wallet2: refresh() lightwallet support (Jaquee)
f44d156c my/openmonero API functions (Jaquee)
288d3c75 wallet2: add remove_obsolete_pool_txs() (Jaquee)
2c6aad7e wallet2: add on_pool_tx_removed callback (Jaquee)
ff7c30aa wallet2: light wallet member variables (Jaquee)
e2a276cb wallet2: add ssl option to init() (Jaquee)
a13540be add string_tools::validate_hex() (Jaquee)
fd773d88 refactor cryptonote_basic::add_tx_pub_key_to_extra (Jaquee)
8bfa6c2d lightwallet rpc server commands (Jaquee)
76961ddc Serializer: string to integer conversion for MyMonero compatibility (Jaquee)
1cf940f2 epee http_client SSL support (Jaquee)
eec10137 CMakeLists.txt - Add openssl include dir (Jaquee)
Diffstat (limited to 'src/wallet/wallet2.cpp')
-rw-r--r-- | src/wallet/wallet2.cpp | 754 |
1 files changed, 699 insertions, 55 deletions
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 024b7a6ff..fdb3bf976 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -525,7 +525,7 @@ std::unique_ptr<wallet2> wallet2::make_dummy(const boost::program_options::varia } //---------------------------------------------------------------------------------------------------- -bool wallet2::init(std::string daemon_address, boost::optional<epee::net_utils::http::login> daemon_login, uint64_t upper_transaction_size_limit) +bool wallet2::init(std::string daemon_address, boost::optional<epee::net_utils::http::login> daemon_login, uint64_t upper_transaction_size_limit, bool ssl) { m_checkpoints.init_default_checkpoints(m_testnet); if(m_http_client.is_connected()) @@ -534,7 +534,10 @@ bool wallet2::init(std::string daemon_address, boost::optional<epee::net_utils:: m_upper_transaction_size_limit = upper_transaction_size_limit; m_daemon_address = std::move(daemon_address); m_daemon_login = std::move(daemon_login); - return m_http_client.set_server(get_daemon_address(), get_daemon_login()); + // When switching from light wallet to full wallet, we need to reset the height we got from lw node. + if(m_light_wallet) + m_local_bc_height = m_blockchain.size(); + return m_http_client.set_server(get_daemon_address(), get_daemon_login(), ssl); } //---------------------------------------------------------------------------------------------------- bool wallet2::is_deterministic() const @@ -1513,6 +1516,34 @@ void wallet2::pull_next_blocks(uint64_t start_height, uint64_t &blocks_start_hei error = true; } } + +void wallet2::remove_obsolete_pool_txs(const std::vector<crypto::hash> &tx_hashes) +{ + // remove pool txes to us that aren't in the pool anymore + std::unordered_map<crypto::hash, wallet2::payment_details>::iterator uit = m_unconfirmed_payments.begin(); + while (uit != m_unconfirmed_payments.end()) + { + const crypto::hash &txid = uit->second.m_tx_hash; + bool found = false; + for (const auto &it2: tx_hashes) + { + if (it2 == txid) + { + found = true; + break; + } + } + auto pit = uit++; + if (!found) + { + MDEBUG("Removing " << txid << " from unconfirmed payments, not found in pool"); + m_unconfirmed_payments.erase(pit); + if (0 != m_callback) + m_callback->on_pool_tx_removed(txid); + } + } +} + //---------------------------------------------------------------------------------------------------- void wallet2::update_pool_state(bool refreshed) { @@ -1590,28 +1621,8 @@ void wallet2::update_pool_state(bool refreshed) // the in transfers list instead (or nowhere if it just // disappeared without being mined) if (refreshed) - { - std::unordered_map<crypto::hash, wallet2::payment_details>::iterator uit = m_unconfirmed_payments.begin(); - while (uit != m_unconfirmed_payments.end()) - { - const crypto::hash &txid = uit->second.m_tx_hash; - bool found = false; - for (const auto &it2: res.tx_hashes) - { - if (it2 == txid) - { - found = true; - break; - } - } - auto pit = uit++; - if (!found) - { - MDEBUG("Removing " << txid << " from unconfirmed payments, not found in pool"); - m_unconfirmed_payments.erase(pit); - } - } - } + remove_obsolete_pool_txs(res.tx_hashes); + MDEBUG("update_pool_state done second loop"); // gather txids of new pool txes to us @@ -1828,6 +1839,39 @@ bool wallet2::delete_address_book_row(std::size_t row_id) { //---------------------------------------------------------------------------------------------------- void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& received_money) { + if(m_light_wallet) { + + // MyMonero get_address_info needs to be called occasionally to trigger wallet sync. + // This call is not really needed for other purposes and can be removed if mymonero changes their backend. + cryptonote::COMMAND_RPC_GET_ADDRESS_INFO::response res; + + // Get basic info + if(light_wallet_get_address_info(res)) { + // Last stored block height + uint64_t prev_height = m_light_wallet_blockchain_height; + // Update lw heights + m_light_wallet_scanned_block_height = res.scanned_block_height; + m_light_wallet_blockchain_height = res.blockchain_height; + m_local_bc_height = res.blockchain_height; + // If new height - call new_block callback + if(m_light_wallet_blockchain_height != prev_height) + { + MDEBUG("new block since last time!"); + m_callback->on_lw_new_block(m_light_wallet_blockchain_height - 1); + } + m_light_wallet_connected = true; + MDEBUG("lw scanned block height: " << m_light_wallet_scanned_block_height); + MDEBUG("lw blockchain height: " << m_light_wallet_blockchain_height); + MDEBUG(m_light_wallet_blockchain_height-m_light_wallet_scanned_block_height << " blocks behind"); + // TODO: add wallet created block info + + light_wallet_get_address_txs(); + } else + m_light_wallet_connected = false; + + // Lighwallet refresh done + return; + } received_money = false; blocks_fetched = 0; uint64_t added_blocks = 0; @@ -2603,6 +2647,12 @@ bool wallet2::check_connection(uint32_t *version, uint32_t timeout) boost::lock_guard<boost::mutex> lock(m_daemon_rpc_mutex); + // TODO: Add light wallet version check. + if(m_light_wallet) { + version = 0; + return m_light_wallet_connected; + } + if(!m_http_client.is_connected()) { m_node_rpc_proxy.invalidate(); @@ -2896,6 +2946,8 @@ void wallet2::store_to(const std::string &path, const std::string &password) uint64_t wallet2::balance(uint32_t index_major) const { uint64_t amount = 0; + if(m_light_wallet) + return m_light_wallet_unlocked_balance; for (const auto& i : balance_per_subaddress(index_major)) amount += i.second; return amount; @@ -2904,6 +2956,8 @@ uint64_t wallet2::balance(uint32_t index_major) const uint64_t wallet2::unlocked_balance(uint32_t index_major) const { uint64_t amount = 0; + if(m_light_wallet) + return m_light_wallet_balance; for (const auto& i : unlocked_balance_per_subaddress(index_major)) amount += i.second; return amount; @@ -3105,10 +3159,15 @@ void wallet2::rescan_blockchain(bool refresh) //---------------------------------------------------------------------------------------------------- bool wallet2::is_transfer_unlocked(const transfer_details& td) const { - if(!is_tx_spendtime_unlocked(td.m_tx.unlock_time, td.m_block_height)) + return is_transfer_unlocked(td.m_tx.unlock_time, td.m_block_height); +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::is_transfer_unlocked(uint64_t unlock_time, uint64_t block_height) const +{ + if(!is_tx_spendtime_unlocked(unlock_time, block_height)) return false; - if(td.m_block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE > m_blockchain.size()) + if(block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE > m_local_bc_height) return false; return true; @@ -3119,7 +3178,7 @@ bool wallet2::is_tx_spendtime_unlocked(uint64_t unlock_time, uint64_t block_heig if(unlock_time < CRYPTONOTE_MAX_BLOCK_NUMBER) { //interpret as block index - if(m_blockchain.size()-1 + CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_BLOCKS >= unlock_time) + if(m_local_bc_height-1 + CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_BLOCKS >= unlock_time) return true; else return false; @@ -3424,25 +3483,42 @@ crypto::hash8 wallet2::get_short_payment_id(const pending_tx &ptx) const void wallet2::commit_tx(pending_tx& ptx) { using namespace cryptonote; - crypto::hash txid; - - COMMAND_RPC_SEND_RAW_TX::request req; - req.tx_as_hex = epee::string_tools::buff_to_hex_nodelimer(tx_to_blob(ptx.tx)); - req.do_not_relay = false; - COMMAND_RPC_SEND_RAW_TX::response daemon_send_resp; - m_daemon_rpc_mutex.lock(); - bool r = epee::net_utils::invoke_http_json("/sendrawtransaction", req, daemon_send_resp, m_http_client, rpc_timeout); - m_daemon_rpc_mutex.unlock(); - THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "sendrawtransaction"); - THROW_WALLET_EXCEPTION_IF(daemon_send_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "sendrawtransaction"); - THROW_WALLET_EXCEPTION_IF(daemon_send_resp.status != CORE_RPC_STATUS_OK, error::tx_rejected, ptx.tx, daemon_send_resp.status, daemon_send_resp.reason); - - // sanity checks - for (size_t idx: ptx.selected_transfers) + + if(m_light_wallet) { - THROW_WALLET_EXCEPTION_IF(idx >= m_transfers.size(), error::wallet_internal_error, - "Bad output index in selected transfers: " + boost::lexical_cast<std::string>(idx)); + cryptonote::COMMAND_RPC_SUBMIT_RAW_TX::request oreq; + cryptonote::COMMAND_RPC_SUBMIT_RAW_TX::response ores; + 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); + oreq.tx = epee::string_tools::buff_to_hex_nodelimer(tx_to_blob(ptx.tx)); + m_daemon_rpc_mutex.lock(); + bool r = epee::net_utils::invoke_http_json("/submit_raw_tx", oreq, ores, m_http_client, rpc_timeout, "POST"); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "submit_raw_tx"); + // MyMonero and OpenMonero use different status strings + THROW_WALLET_EXCEPTION_IF(ores.status != "OK" && ores.status != "success" , error::tx_rejected, ptx.tx, ores.status, ores.error); } + else + { + // Normal submit + COMMAND_RPC_SEND_RAW_TX::request req; + req.tx_as_hex = epee::string_tools::buff_to_hex_nodelimer(tx_to_blob(ptx.tx)); + req.do_not_relay = false; + COMMAND_RPC_SEND_RAW_TX::response daemon_send_resp; + m_daemon_rpc_mutex.lock(); + bool r = epee::net_utils::invoke_http_json("/sendrawtransaction", req, daemon_send_resp, m_http_client, rpc_timeout); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "sendrawtransaction"); + THROW_WALLET_EXCEPTION_IF(daemon_send_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "sendrawtransaction"); + THROW_WALLET_EXCEPTION_IF(daemon_send_resp.status != CORE_RPC_STATUS_OK, error::tx_rejected, ptx.tx, daemon_send_resp.status, daemon_send_resp.reason); + // sanity checks + for (size_t idx: ptx.selected_transfers) + { + THROW_WALLET_EXCEPTION_IF(idx >= m_transfers.size(), error::wallet_internal_error, + "Bad output index in selected transfers: " + boost::lexical_cast<std::string>(idx)); + } + } + crypto::hash txid; txid = get_transaction_hash(ptx.tx); crypto::hash payment_id = crypto::null_hash; @@ -3864,6 +3940,8 @@ uint64_t wallet2::get_dynamic_per_kb_fee_estimate() //---------------------------------------------------------------------------------------------------- uint64_t wallet2::get_per_kb_fee() { + if(m_light_wallet) + return m_light_wallet_per_kb_fee; bool use_dyn_fee = use_fork_rules(HF_VERSION_DYNAMIC_FEE, -720 * 1); if (!use_dyn_fee) return FEE_PER_KB; @@ -3989,10 +4067,134 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions(std::vector<crypto } } + +bool wallet2::tx_add_fake_output(std::vector<std::vector<tools::wallet2::get_outs_entry>> &outs, uint64_t global_index, const crypto::public_key& tx_public_key, const rct::key& mask, uint64_t real_index, bool unlocked) const +{ + if (!unlocked) // don't add locked outs + return false; + if (global_index == real_index) // don't re-add real one + return false; + auto item = std::make_tuple(global_index, tx_public_key, mask); + if (std::find(outs.back().begin(), outs.back().end(), item) != outs.back().end()) // don't add duplicates + return false; + outs.back().push_back(item); + return true; +} + +void wallet2::light_wallet_get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> &outs, const std::list<size_t> &selected_transfers, size_t fake_outputs_count) { + + MDEBUG("LIGHTWALLET - Getting random outs"); + + cryptonote::COMMAND_RPC_GET_RANDOM_OUTS::request oreq; + cryptonote::COMMAND_RPC_GET_RANDOM_OUTS::response ores; + + size_t light_wallet_requested_outputs_count = (size_t)((fake_outputs_count + 1) * 1.5 + 1); + + // Amounts to ask for + // MyMonero api handle amounts and fees as strings + for(size_t idx: selected_transfers) { + const uint64_t ask_amount = m_transfers[idx].is_rct() ? 0 : m_transfers[idx].amount(); + std::ostringstream amount_ss; + amount_ss << ask_amount; + oreq.amounts.push_back(amount_ss.str()); + } + + oreq.count = light_wallet_requested_outputs_count; + m_daemon_rpc_mutex.lock(); + bool r = epee::net_utils::invoke_http_json("/get_random_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_random_outs"); + THROW_WALLET_EXCEPTION_IF(ores.amount_outs.empty() , error::wallet_internal_error, "No outputs recieved from light wallet node. Error: " + ores.Error); + + // Check if we got enough outputs for each amount + for(auto& out: ores.amount_outs) { + const uint64_t out_amount = boost::lexical_cast<uint64_t>(out.amount); + THROW_WALLET_EXCEPTION_IF(out.outputs.size() < light_wallet_requested_outputs_count , error::wallet_internal_error, "Not enough outputs for amount: " + boost::lexical_cast<std::string>(out.amount)); + MDEBUG(out.outputs.size() << " outputs for amount "+ boost::lexical_cast<std::string>(out.amount) + " received from light wallet node"); + } + + MDEBUG("selected transfers size: " << selected_transfers.size()); + + for(size_t idx: selected_transfers) + { + // Create new index + outs.push_back(std::vector<get_outs_entry>()); + outs.back().reserve(fake_outputs_count + 1); + + // add real output first + const transfer_details &td = m_transfers[idx]; + const uint64_t amount = td.is_rct() ? 0 : td.amount(); + outs.back().push_back(std::make_tuple(td.m_global_output_index, td.get_public_key(), rct::commit(td.amount(), td.m_mask))); + MDEBUG("added real output " << string_tools::pod_to_hex(td.get_public_key())); + + // Even if the lightwallet server returns random outputs, we pick them randomly. + std::vector<size_t> order; + order.resize(light_wallet_requested_outputs_count); + for (size_t n = 0; n < order.size(); ++n) + order[n] = n; + std::shuffle(order.begin(), order.end(), std::default_random_engine(crypto::rand<unsigned>())); + + + LOG_PRINT_L2("Looking for " << (fake_outputs_count+1) << " outputs with amounts " << print_money(td.is_rct() ? 0 : td.amount())); + MDEBUG("OUTS SIZE: " << outs.back().size()); + for (size_t o = 0; o < light_wallet_requested_outputs_count && outs.back().size() < fake_outputs_count + 1; ++o) + { + // Random pick + size_t i = order[o]; + + // Find which random output key to use + bool found_amount = false; + size_t amount_key; + for(amount_key = 0; amount_key < ores.amount_outs.size(); ++amount_key) + { + if(boost::lexical_cast<uint64_t>(ores.amount_outs[amount_key].amount) == amount) { + found_amount = true; + break; + } + } + THROW_WALLET_EXCEPTION_IF(!found_amount , error::wallet_internal_error, "Outputs for amount " + boost::lexical_cast<std::string>(ores.amount_outs[amount_key].amount) + " not found" ); + + LOG_PRINT_L2("Index " << i << "/" << light_wallet_requested_outputs_count << ": idx " << ores.amount_outs[amount_key].outputs[i].global_index << " (real " << td.m_global_output_index << "), unlocked " << "(always in light)" << ", key " << ores.amount_outs[0].outputs[i].public_key); + + // Convert light wallet string data to proper data structures + crypto::public_key tx_public_key; + rct::key mask = AUTO_VAL_INIT(mask); // decrypted mask - not used here + rct::key rct_commit = AUTO_VAL_INIT(rct_commit); + THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, ores.amount_outs[amount_key].outputs[i].public_key), error::wallet_internal_error, "Invalid public_key"); + string_tools::hex_to_pod(ores.amount_outs[amount_key].outputs[i].public_key, tx_public_key); + const uint64_t global_index = ores.amount_outs[amount_key].outputs[i].global_index; + if(!light_wallet_parse_rct_str(ores.amount_outs[amount_key].outputs[i].rct, tx_public_key, 0, mask, rct_commit, false)) + rct_commit = rct::zeroCommit(td.amount()); + + if (tx_add_fake_output(outs, global_index, tx_public_key, rct_commit, td.m_global_output_index, true)) { + MDEBUG("added fake output " << ores.amount_outs[amount_key].outputs[i].public_key); + MDEBUG("index " << global_index); + } + } + + THROW_WALLET_EXCEPTION_IF(outs.back().size() < fake_outputs_count + 1 , error::wallet_internal_error, "Not enough fake outputs found" ); + + // Real output is the first. Shuffle outputs + MTRACE(outs.back().size() << " outputs added. Sorting outputs by index:"); + std::sort(outs.back().begin(), outs.back().end(), [](const get_outs_entry &a, const get_outs_entry &b) { return std::get<0>(a) < std::get<0>(b); }); + + // Print output order + for(auto added_out: outs.back()) + MTRACE(std::get<0>(added_out)); + + } +} + void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> &outs, const std::list<size_t> &selected_transfers, size_t fake_outputs_count) { LOG_PRINT_L2("fake_outputs_count: " << fake_outputs_count); outs.clear(); + + if(m_light_wallet && fake_outputs_count > 0) { + light_wallet_get_outs(outs, selected_transfers, fake_outputs_count); + return; + } + if (fake_outputs_count > 0) { // get histogram for the amounts we need @@ -4189,14 +4391,7 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> { size_t i = base + order[o]; LOG_PRINT_L2("Index " << i << "/" << requested_outputs_count << ": idx " << req.outputs[i].index << " (real " << td.m_global_output_index << "), unlocked " << daemon_resp.outs[i].unlocked << ", key " << daemon_resp.outs[i].key); - if (req.outputs[i].index == td.m_global_output_index) // don't re-add real one - continue; - if (!daemon_resp.outs[i].unlocked) // don't add locked outs - continue; - auto item = std::make_tuple(req.outputs[i].index, daemon_resp.outs[i].key, daemon_resp.outs[i].mask); - if (std::find(outs.back().begin(), outs.back().end(), item) != outs.back().end()) // don't add duplicates - continue; - outs.back().push_back(item); + tx_add_fake_output(outs, req.outputs[i].index, daemon_resp.outs[i].key, daemon_resp.outs[i].mask, td.m_global_output_index, daemon_resp.outs[i].unlocked); } if (outs.back().size() < fake_outputs_count + 1) { @@ -4218,7 +4413,7 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> const transfer_details &td = m_transfers[idx]; std::vector<get_outs_entry> v; const rct::key mask = td.is_rct() ? rct::commit(td.amount(), td.m_mask) : rct::zeroCommit(td.amount()); - v.push_back(std::make_tuple(td.m_global_output_index, boost::get<txout_to_key>(td.m_tx.vout[td.m_internal_output_index].target).key, mask)); + v.push_back(std::make_tuple(td.m_global_output_index, td.get_public_key(), mask)); outs.push_back(v); } } @@ -4433,6 +4628,9 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry src.rct = td.is_rct(); //paste mixin transaction + THROW_WALLET_EXCEPTION_IF(outs.size() < out_index + 1 , error::wallet_internal_error, "outs.size() < out_index + 1"); + THROW_WALLET_EXCEPTION_IF(outs[out_index].size() < fake_outputs_count , error::wallet_internal_error, "fake_outputs_count > random outputs found"); + typedef cryptonote::tx_source_entry::output_entry tx_output_entry; for (size_t n = 0; n < fake_outputs_count + 1; ++n) { @@ -4454,7 +4652,7 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry tx_output_entry real_oe; real_oe.first = td.m_global_output_index; - real_oe.second.dest = rct::pk2rct(boost::get<txout_to_key>(td.m_tx.vout[td.m_internal_output_index].target).key); + real_oe.second.dest = rct::pk2rct(td.get_public_key()); real_oe.second.mask = rct::commit(td.amount(), td.m_mask); *it_to_replace = real_oe; src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_tx, td.m_pk_index); @@ -4701,6 +4899,445 @@ static uint32_t get_count_above(const std::vector<wallet2::transfer_details> &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<std::string>(::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<crypto::hash,bool> 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>()); + 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<crypto::hash,address_tx>::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<crypto::hash> payments_txs; + for(const auto &p: m_payments) + payments_txs.push_back(p.second.m_tx_hash); + std::vector<crypto::hash> 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<crypto::hash> 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) { + m_callback->on_lw_unconfirmed_money_received(t.height, payment.m_tx_hash, 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) { + m_callback->on_lw_money_received(t.height, payment.m_tx_hash, 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_lw_money_spent(t.height, tx_hash, amount_sent); + } + } + // 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 commit> + <encrypted mask> + <rct amount>) + 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<uint64_t, crypto::key_image> index_keyimage_map; + std::unordered_map<crypto::public_key, std::map<uint64_t, crypto::key_image> >::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<uint64_t,crypto::key_image>::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 @@ -4718,6 +5355,10 @@ static uint32_t get_count_above(const std::vector<wallet2::transfer_details> &tr // usable balance. std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, bool trusted_daemon) { + if(m_light_wallet) { + // Populate m_transfers + light_wallet_get_unspent_outs(); + } std::vector<std::pair<uint32_t, std::vector<size_t>>> unused_transfers_indices_per_subaddr; std::vector<std::pair<uint32_t, std::vector<size_t>>> unused_dust_indices_per_subaddr; uint64_t needed_money; @@ -4880,7 +5521,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp std::vector<size_t> preferred_inputs; uint64_t rct_outs_needed = 2 * (fake_outs_count + 1); rct_outs_needed += 100; // some fudge factor since we don't know how many are locked - if (use_rct && get_num_rct_outputs() >= rct_outs_needed) + if (use_rct) { // this is used to build a tx that's 1 or 2 inputs, and 2 outputs, which // will get us a known fee. @@ -5334,6 +5975,9 @@ void wallet2::get_hard_fork_info(uint8_t version, uint64_t &earliest_height) //---------------------------------------------------------------------------------------------------- bool wallet2::use_fork_rules(uint8_t version, int64_t early_blocks) { + // TODO: How to get fork rule info from light wallet node? + if(m_light_wallet) + return true; uint64_t height, earliest_height; boost::optional<std::string> result = m_node_rpc_proxy.get_height(height); throw_on_rpc_response_error(result, "get_info"); |