diff options
Diffstat (limited to 'src/wallet')
28 files changed, 6605 insertions, 1177 deletions
diff --git a/src/wallet/CMakeLists.txt b/src/wallet/CMakeLists.txt index 506eaef85..2e7610b64 100644 --- a/src/wallet/CMakeLists.txt +++ b/src/wallet/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2014-2016, The Monero Project +# Copyright (c) 2014-2017, The Monero Project # # All rights reserved. # @@ -32,13 +32,16 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(wallet_sources wallet2.cpp - wallet_rpc_server.cpp + wallet_args.cpp + node_rpc_proxy.cpp api/wallet.cpp api/wallet_manager.cpp api/transaction_info.cpp api/transaction_history.cpp api/pending_transaction.cpp - api/utils.cpp) + api/utils.cpp + api/address_book.cpp + api/unsigned_transaction.cpp) set(wallet_api_headers wallet2_api.h) @@ -46,25 +49,30 @@ set(wallet_api_headers set(wallet_private_headers wallet2.h + wallet_args.h wallet_errors.h wallet_rpc_server.h wallet_rpc_server_commands_defs.h wallet_rpc_server_error_codes.h + node_rpc_proxy.h api/wallet.h api/wallet_manager.h api/transaction_info.h api/transaction_history.h api/pending_transaction.h - api/common_defines.h) + api/common_defines.h + api/address_book.h + api/unsigned_transaction.h) -bitmonero_private_headers(wallet +monero_private_headers(wallet ${wallet_private_headers}) -bitmonero_add_library(wallet +monero_add_library(wallet ${wallet_sources} ${wallet_api_headers} ${wallet_private_headers}) target_link_libraries(wallet PUBLIC + common cryptonote_core mnemonics p2p @@ -76,17 +84,61 @@ target_link_libraries(wallet ${Boost_REGEX_LIBRARY} PRIVATE ${EXTRA_LIBRARIES}) +add_dependencies(wallet version) + +if (NOT BUILD_GUI_DEPS) + set(wallet_rpc_sources + wallet_rpc_server.cpp) + + set(wallet_rpc_headers) + + set(wallet_rpc_private_headers + wallet_rpc_server.h) + + monero_private_headers(wallet_rpc_server + ${wallet_rpc_private_headers}) + monero_add_executable(wallet_rpc_server + ${wallet_rpc_sources} + ${wallet_rpc_headers} + ${wallet_rpc_private_headers}) + + target_link_libraries(wallet_rpc_server + PRIVATE + wallet + epee + rpc + cryptonote_core + crypto + common + ${Boost_CHRONO_LIBRARY} + ${Boost_PROGRAM_OPTIONS_LIBRARY} + ${Boost_FILESYSTEM_LIBRARY} + ${Boost_THREAD_LIBRARY} + ${CMAKE_THREAD_LIBS_INIT} + ${EXTRA_LIBRARIES}) + add_dependencies(wallet_rpc_server version) + set_property(TARGET wallet_rpc_server + PROPERTY + OUTPUT_NAME "monero-wallet-rpc") + install(TARGETS wallet_rpc_server DESTINATION bin) +endif() + # build and install libwallet_merged only if we building for GUI if (BUILD_GUI_DEPS) - set(libs_to_merge wallet cryptonote_core mnemonics common crypto ringct) + set(libs_to_merge wallet cryptonote_core cryptonote_basic mnemonics common crypto ringct) foreach(lib ${libs_to_merge}) list(APPEND objlibs $<TARGET_OBJECTS:obj_${lib}>) # matches naming convention in src/CMakeLists.txt endforeach() add_library(wallet_merged STATIC ${objlibs}) + if(IOS) + set(lib_folder lib-${ARCH}) + else() + set(lib_folder lib) + endif() install(TARGETS wallet_merged - ARCHIVE DESTINATION lib) + ARCHIVE DESTINATION ${lib_folder}) install(FILES ${wallet_api_headers} DESTINATION include/wallet) diff --git a/src/wallet/api/address_book.cpp b/src/wallet/api/address_book.cpp new file mode 100644 index 000000000..28f835ebd --- /dev/null +++ b/src/wallet/api/address_book.cpp @@ -0,0 +1,183 @@ +// Copyright (c) 2014-2017, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 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 "address_book.h" +#include "wallet.h" +#include "crypto/hash.h" +#include "wallet/wallet2.h" +#include "common_defines.h" + +#include <vector> + +namespace Monero { + +AddressBook::~AddressBook() {} + +AddressBookImpl::AddressBookImpl(WalletImpl *wallet) + : m_wallet(wallet) {} + +bool AddressBookImpl::addRow(const std::string &dst_addr , const std::string &payment_id_str, const std::string &description) +{ + clearStatus(); + + cryptonote::account_public_address addr; + bool has_short_pid; + crypto::hash8 payment_id_short; + if(!cryptonote::get_account_integrated_address_from_str(addr, has_short_pid, payment_id_short, m_wallet->m_wallet->testnet(), dst_addr)) { + m_errorString = tr("Invalid destination address"); + m_errorCode = Invalid_Address; + return false; + } + + crypto::hash payment_id = cryptonote::null_hash; + bool has_long_pid = (payment_id_str.empty())? false : tools::wallet2::parse_long_payment_id(payment_id_str, payment_id); + + // Short payment id provided + if(payment_id_str.length() == 16) { + m_errorString = tr("Invalid payment ID. Short payment ID should only be used in an integrated address"); + m_errorCode = Invalid_Payment_Id; + return false; + } + + // long payment id provided but not valid + if(!payment_id_str.empty() && !has_long_pid) { + m_errorString = tr("Invalid payment ID"); + m_errorCode = Invalid_Payment_Id; + return false; + } + + // integrated + long payment id provided + if(has_long_pid && has_short_pid) { + m_errorString = tr("Integrated address and long payment id can't be used at the same time"); + m_errorCode = Invalid_Payment_Id; + return false; + } + + // Pad short pid with zeros + if (has_short_pid) + { + memcpy(payment_id.data, payment_id_short.data, 8); + } + + bool r = m_wallet->m_wallet->add_address_book_row(addr,payment_id,description); + if (r) + refresh(); + else + m_errorCode = General_Error; + return r; +} + +void AddressBookImpl::refresh() +{ + LOG_PRINT_L2("Refreshing addressbook"); + + clearRows(); + + // Fetch from Wallet2 and create vector of AddressBookRow objects + std::vector<tools::wallet2::address_book_row> rows = m_wallet->m_wallet->get_address_book(); + for (size_t i = 0; i < rows.size(); ++i) { + tools::wallet2::address_book_row * row = &rows.at(i); + + std::string payment_id = (row->m_payment_id == cryptonote::null_hash)? "" : epee::string_tools::pod_to_hex(row->m_payment_id); + std::string address = cryptonote::get_account_address_as_str(m_wallet->m_wallet->testnet(),row->m_address); + // convert the zero padded short payment id to integrated address + if (payment_id.length() > 16 && payment_id.substr(16).find_first_not_of('0') == std::string::npos) { + payment_id = payment_id.substr(0,16); + crypto::hash8 payment_id_short; + if(tools::wallet2::parse_short_payment_id(payment_id, payment_id_short)) { + address = cryptonote::get_account_integrated_address_as_str(m_wallet->m_wallet->testnet(), row->m_address, payment_id_short); + // Don't show payment id when integrated address is used + payment_id = ""; + } + } + AddressBookRow * abr = new AddressBookRow(i, address, payment_id, row->m_description); + m_rows.push_back(abr); + } + +} + +bool AddressBookImpl::deleteRow(std::size_t rowId) +{ + LOG_PRINT_L2("Deleting address book row " << rowId); + bool r = m_wallet->m_wallet->delete_address_book_row(rowId); + if (r) + refresh(); + return r; +} + +int AddressBookImpl::lookupPaymentID(const std::string &payment_id) const +{ + // turn short ones into long ones for comparison + const std::string long_payment_id = payment_id + std::string(64 - payment_id.size(), '0'); + + int idx = -1; + for (const auto &row: m_rows) { + ++idx; + // this does short/short and long/long + if (payment_id == row->getPaymentId()) + return idx; + // short/long + if (long_payment_id == row->getPaymentId()) + return idx; + // one case left: payment_id was long, row's is short + const std::string long_row_payment_id = row->getPaymentId() + std::string(64 - row->getPaymentId().size(), '0'); + if (payment_id == long_row_payment_id) + return idx; + } + return -1; +} + +void AddressBookImpl::clearRows() { + for (auto r : m_rows) { + delete r; + } + m_rows.clear(); +} + +void AddressBookImpl::clearStatus(){ + m_errorString = ""; + m_errorCode = 0; +} + +std::vector<AddressBookRow*> AddressBookImpl::getAll() const +{ + return m_rows; +} + + +AddressBookImpl::~AddressBookImpl() +{ + clearRows(); +} + +} // namespace + +namespace Bitmonero = Monero; diff --git a/src/wallet/api/address_book.h b/src/wallet/api/address_book.h new file mode 100644 index 000000000..25f59128b --- /dev/null +++ b/src/wallet/api/address_book.h @@ -0,0 +1,70 @@ +// Copyright (c) 2014-2017, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 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 "wallet/wallet2_api.h" +#include "wallet/wallet2.h" + +namespace Monero { + +class WalletImpl; + +class AddressBookImpl : public AddressBook +{ +public: + AddressBookImpl(WalletImpl * wallet); + ~AddressBookImpl(); + + // Fetches addresses from Wallet2 + void refresh(); + std::vector<AddressBookRow*> getAll() const; + bool addRow(const std::string &dst_addr , const std::string &payment_id, const std::string &description); + bool deleteRow(std::size_t rowId); + + // Error codes. See AddressBook:ErrorCode enum in wallet2_api.h + std::string errorString() const {return m_errorString;} + int errorCode() const {return m_errorCode;} + + int lookupPaymentID(const std::string &payment_id) const; + +private: + void clearRows(); + void clearStatus(); + +private: + WalletImpl *m_wallet; + std::vector<AddressBookRow*> m_rows; + std::string m_errorString; + int m_errorCode; +}; + +} + +namespace Bitmonero = Monero; + diff --git a/src/wallet/api/pending_transaction.cpp b/src/wallet/api/pending_transaction.cpp index 26ce9fc7e..9798d66c6 100644 --- a/src/wallet/api/pending_transaction.cpp +++ b/src/wallet/api/pending_transaction.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -32,9 +32,8 @@ #include "wallet.h" #include "common_defines.h" -#include "cryptonote_core/cryptonote_format_utils.h" -#include "cryptonote_core/cryptonote_basic_impl.h" -#include "cryptonote_core/cryptonote_format_utils.h" +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "cryptonote_basic/cryptonote_basic_impl.h" #include <memory> #include <vector> @@ -43,7 +42,7 @@ using namespace std; -namespace Bitmonero { +namespace Monero { PendingTransaction::~PendingTransaction() {} @@ -51,7 +50,7 @@ PendingTransaction::~PendingTransaction() {} PendingTransactionImpl::PendingTransactionImpl(WalletImpl &wallet) : m_wallet(wallet) { - + m_status = Status_Ok; } PendingTransactionImpl::~PendingTransactionImpl() @@ -69,19 +68,47 @@ string PendingTransactionImpl::errorString() const return m_errorString; } -bool PendingTransactionImpl::commit() +std::vector<std::string> PendingTransactionImpl::txid() const +{ + std::vector<std::string> txid; + for (const auto &pt: m_pending_tx) + txid.push_back(epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(pt.tx))); + return txid; +} + +bool PendingTransactionImpl::commit(const std::string &filename, bool overwrite) { - LOG_PRINT_L0("m_pending_tx size: " << m_pending_tx.size()); - assert(m_pending_tx.size() == 1); + LOG_PRINT_L3("m_pending_tx size: " << m_pending_tx.size()); + try { + // Save tx to file + if (!filename.empty()) { + boost::system::error_code ignore; + bool tx_file_exists = boost::filesystem::exists(filename, ignore); + if(tx_file_exists && !overwrite){ + m_errorString = string(tr("Attempting to save transaction to file, but specified file(s) exist. Exiting to not risk overwriting. File:")) + filename; + m_status = Status_Error; + LOG_ERROR(m_errorString); + return false; + } + bool r = m_wallet.m_wallet->save_tx(m_pending_tx, filename); + if (!r) { + m_errorString = tr("Failed to write transaction(s) to file"); + m_status = Status_Error; + } else { + m_status = Status_Ok; + } + } + // Commit tx + else { while (!m_pending_tx.empty()) { auto & ptx = m_pending_tx.back(); m_wallet.m_wallet->commit_tx(ptx); - // success_msg_writer(true) << tr("Money successfully sent, transaction ") << get_transaction_hash(ptx.tx); // if no exception, remove element from vector m_pending_tx.pop_back(); } // TODO: extract method; + } } catch (const tools::error::daemon_busy&) { // TODO: make it translatable with "tr"? m_errorString = tr("daemon is busy. Please try again later."); @@ -92,8 +119,12 @@ bool PendingTransactionImpl::commit() } catch (const tools::error::tx_rejected& e) { std::ostringstream writer(m_errorString); writer << (boost::format(tr("transaction %s was rejected by daemon with status: ")) % get_transaction_hash(e.tx())) << e.status(); + std::string reason = e.reason(); m_status = Status_Error; - } catch (std::exception &e) { + m_errorString = writer.str(); + if (!reason.empty()) + m_errorString += string(tr(". Reason: ")) + reason; + } catch (const std::exception &e) { m_errorString = string(tr("Unknown exception: ")) + e.what(); m_status = Status_Error; } catch (...) { @@ -128,11 +159,18 @@ uint64_t PendingTransactionImpl::dust() const uint64_t PendingTransactionImpl::fee() const { uint64_t result = 0; - for (const auto ptx : m_pending_tx) { + for (const auto &ptx : m_pending_tx) { result += ptx.fee; } return result; } +uint64_t PendingTransactionImpl::txCount() const +{ + return m_pending_tx.size(); } +} + +namespace Bitmonero = Monero; + diff --git a/src/wallet/api/pending_transaction.h b/src/wallet/api/pending_transaction.h index 8e09bec91..0c3e95a85 100644 --- a/src/wallet/api/pending_transaction.h +++ b/src/wallet/api/pending_transaction.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -35,7 +35,7 @@ #include <vector> -namespace Bitmonero { +namespace Monero { class WalletImpl; class PendingTransactionImpl : public PendingTransaction @@ -45,10 +45,12 @@ public: ~PendingTransactionImpl(); int status() const; std::string errorString() const; - bool commit(); + bool commit(const std::string &filename = "", bool overwrite = false); uint64_t amount() const; uint64_t dust() const; uint64_t fee() const; + std::vector<std::string> txid() const; + uint64_t txCount() const; // TODO: continue with interface; private: @@ -62,3 +64,5 @@ private: } + +namespace Bitmonero = Monero; diff --git a/src/wallet/api/transaction_history.cpp b/src/wallet/api/transaction_history.cpp index 95aafb04f..85f2b05ce 100644 --- a/src/wallet/api/transaction_history.cpp +++ b/src/wallet/api/transaction_history.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -42,7 +42,7 @@ using namespace epee; -namespace Bitmonero { +namespace Monero { TransactionHistory::~TransactionHistory() {} @@ -55,37 +55,60 @@ TransactionHistoryImpl::TransactionHistoryImpl(WalletImpl *wallet) TransactionHistoryImpl::~TransactionHistoryImpl() { - + for (auto t : m_history) + delete t; } int TransactionHistoryImpl::count() const { - return m_history.size(); + boost::shared_lock<boost::shared_mutex> lock(m_historyMutex); + int result = m_history.size(); + return result; +} + +TransactionInfo *TransactionHistoryImpl::transaction(int index) const +{ + boost::shared_lock<boost::shared_mutex> lock(m_historyMutex); + // sanity check + if (index < 0) + return nullptr; + unsigned index_ = static_cast<unsigned>(index); + return index_ < m_history.size() ? m_history[index_] : nullptr; } TransactionInfo *TransactionHistoryImpl::transaction(const std::string &id) const { - return nullptr; + boost::shared_lock<boost::shared_mutex> lock(m_historyMutex); + auto itr = std::find_if(m_history.begin(), m_history.end(), + [&](const TransactionInfo * ti) { + return ti->hash() == id; + }); + return itr != m_history.end() ? *itr : nullptr; } std::vector<TransactionInfo *> TransactionHistoryImpl::getAll() const { + boost::shared_lock<boost::shared_mutex> lock(m_historyMutex); return m_history; } void TransactionHistoryImpl::refresh() { + // multithreaded access: + // boost::lock_guard<boost::mutex> guarg(m_historyMutex); + // for "write" access, locking exclusively + boost::unique_lock<boost::shared_mutex> lock(m_historyMutex); + // TODO: configurable values; uint64_t min_height = 0; uint64_t max_height = (uint64_t)-1; + uint64_t wallet_height = m_wallet->blockChainHeight(); // delete old transactions; for (auto t : m_history) delete t; m_history.clear(); - - // transactions are stored in wallet2: // - confirmed_transfer_details - out transfers // - unconfirmed_transfer_details - pending out transfers @@ -101,15 +124,14 @@ void TransactionHistoryImpl::refresh() std::string payment_id = string_tools::pod_to_hex(i->first); if (payment_id.substr(16).find_first_not_of('0') == std::string::npos) payment_id = payment_id.substr(0,16); - // TODO TransactionInfoImpl * ti = new TransactionInfoImpl(); ti->m_paymentid = payment_id; ti->m_amount = pd.m_amount; ti->m_direction = TransactionInfo::Direction_In; ti->m_hash = string_tools::pod_to_hex(pd.m_tx_hash); ti->m_blockheight = pd.m_block_height; - // TODO: - // ti->m_timestamp = pd.m_timestamp; + ti->m_timestamp = pd.m_timestamp; + ti->m_confirmations = wallet_height - pd.m_block_height; m_history.push_back(ti); /* output.insert(std::make_pair(pd.m_block_height, std::make_pair(true, (boost::format("%20.20s %s %s %s") @@ -135,8 +157,8 @@ void TransactionHistoryImpl::refresh() const crypto::hash &hash = i->first; const tools::wallet2::confirmed_transfer_details &pd = i->second; - uint64_t fee = pd.m_amount_in - pd.m_amount_out; uint64_t change = pd.m_change == (uint64_t)-1 ? 0 : pd.m_change; // change may not be known + uint64_t fee = pd.m_amount_in - pd.m_amount_out; std::string payment_id = string_tools::pod_to_hex(i->second.m_payment_id); @@ -151,6 +173,8 @@ void TransactionHistoryImpl::refresh() ti->m_direction = TransactionInfo::Direction_Out; ti->m_hash = string_tools::pod_to_hex(hash); ti->m_blockheight = pd.m_block_height; + ti->m_timestamp = pd.m_timestamp; + ti->m_confirmations = wallet_height - pd.m_block_height; // single output transaction might contain multiple transfers for (const auto &d: pd.m_dests) { @@ -160,9 +184,9 @@ void TransactionHistoryImpl::refresh() } // unconfirmed output transactions - std::list<std::pair<crypto::hash, tools::wallet2::unconfirmed_transfer_details>> upayments; - m_wallet->m_wallet->get_unconfirmed_payments_out(upayments); - for (std::list<std::pair<crypto::hash, tools::wallet2::unconfirmed_transfer_details>>::const_iterator i = upayments.begin(); i != upayments.end(); ++i) { + std::list<std::pair<crypto::hash, tools::wallet2::unconfirmed_transfer_details>> upayments_out; + m_wallet->m_wallet->get_unconfirmed_payments_out(upayments_out); + for (std::list<std::pair<crypto::hash, tools::wallet2::unconfirmed_transfer_details>>::const_iterator i = upayments_out.begin(); i != upayments_out.end(); ++i) { const tools::wallet2::unconfirmed_transfer_details &pd = i->second; const crypto::hash &hash = i->first; uint64_t amount = pd.m_amount_in; @@ -180,14 +204,36 @@ void TransactionHistoryImpl::refresh() ti->m_failed = is_failed; ti->m_pending = true; ti->m_hash = string_tools::pod_to_hex(hash); + ti->m_timestamp = pd.m_timestamp; + ti->m_confirmations = 0; m_history.push_back(ti); } - + + + // unconfirmed payments (tx pool) + std::list<std::pair<crypto::hash, tools::wallet2::payment_details>> upayments; + m_wallet->m_wallet->get_unconfirmed_payments(upayments); + for (std::list<std::pair<crypto::hash, tools::wallet2::payment_details>>::const_iterator i = upayments.begin(); i != upayments.end(); ++i) { + const tools::wallet2::payment_details &pd = i->second; + std::string payment_id = string_tools::pod_to_hex(i->first); + if (payment_id.substr(16).find_first_not_of('0') == std::string::npos) + payment_id = payment_id.substr(0,16); + TransactionInfoImpl * ti = new TransactionInfoImpl(); + ti->m_paymentid = payment_id; + ti->m_amount = pd.m_amount; + ti->m_direction = TransactionInfo::Direction_In; + ti->m_hash = string_tools::pod_to_hex(pd.m_tx_hash); + ti->m_blockheight = pd.m_block_height; + ti->m_pending = true; + ti->m_timestamp = pd.m_timestamp; + ti->m_confirmations = 0; + m_history.push_back(ti); + + LOG_PRINT_L1(__FUNCTION__ << ": Unconfirmed payment found " << pd.m_amount); + } + } -TransactionInfo *TransactionHistoryImpl::transaction(int index) const -{ - return nullptr; -} +} // namespace -} +namespace Bitmonero = Monero; diff --git a/src/wallet/api/transaction_history.h b/src/wallet/api/transaction_history.h index 171fd2210..4987bdab2 100644 --- a/src/wallet/api/transaction_history.h +++ b/src/wallet/api/transaction_history.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -29,10 +29,10 @@ // Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers #include "wallet/wallet2_api.h" +#include <boost/thread/shared_mutex.hpp> -namespace Bitmonero { +namespace Monero { -class TransactionInfo; class WalletImpl; class TransactionHistoryImpl : public TransactionHistory @@ -51,7 +51,10 @@ private: // TransactionHistory is responsible of memory management std::vector<TransactionInfo*> m_history; WalletImpl *m_wallet; + mutable boost::shared_mutex m_historyMutex; }; } +namespace Bitmonero = Monero; + diff --git a/src/wallet/api/transaction_info.cpp b/src/wallet/api/transaction_info.cpp index f25c53a90..79a8fe9b5 100644 --- a/src/wallet/api/transaction_info.cpp +++ b/src/wallet/api/transaction_info.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -33,7 +33,7 @@ using namespace std; -namespace Bitmonero { +namespace Monero { TransactionInfo::~TransactionInfo() {} @@ -49,6 +49,7 @@ TransactionInfoImpl::TransactionInfoImpl() , m_fee(0) , m_blockheight(0) , m_timestamp(0) + , m_confirmations(0) { } @@ -109,4 +110,11 @@ const std::vector<TransactionInfo::Transfer> &TransactionInfoImpl::transfers() c return m_transfers; } +uint64_t TransactionInfoImpl::confirmations() const +{ + return m_confirmations; +} + } // namespace + +namespace Bitmonero = Monero; diff --git a/src/wallet/api/transaction_info.h b/src/wallet/api/transaction_info.h index 82ab2cc6b..16fa5da7a 100644 --- a/src/wallet/api/transaction_info.h +++ b/src/wallet/api/transaction_info.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -32,7 +32,7 @@ #include <string> #include <ctime> -namespace Bitmonero { +namespace Monero { class TransactionHistoryImpl; @@ -55,6 +55,7 @@ public: virtual std::time_t timestamp() const; virtual std::string paymentId() const; virtual const std::vector<Transfer> &transfers() const; + virtual uint64_t confirmations() const; private: int m_direction; @@ -67,9 +68,12 @@ private: std::time_t m_timestamp; std::string m_paymentid; std::vector<Transfer> m_transfers; + uint64_t m_confirmations; friend class TransactionHistoryImpl; }; } // namespace + +namespace Bitmonero = Monero; diff --git a/src/wallet/api/unsigned_transaction.cpp b/src/wallet/api/unsigned_transaction.cpp new file mode 100644 index 000000000..1d9ef5d7c --- /dev/null +++ b/src/wallet/api/unsigned_transaction.cpp @@ -0,0 +1,284 @@ +// Copyright (c) 2014-2017, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 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 "unsigned_transaction.h" +#include "wallet.h" +#include "common_defines.h" + +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "cryptonote_basic/cryptonote_basic_impl.h" + +#include <memory> +#include <vector> +#include <sstream> +#include <boost/format.hpp> + +using namespace std; + +namespace Monero { + +UnsignedTransaction::~UnsignedTransaction() {} + + +UnsignedTransactionImpl::UnsignedTransactionImpl(WalletImpl &wallet) + : m_wallet(wallet) +{ + m_status = Status_Ok; +} + +UnsignedTransactionImpl::~UnsignedTransactionImpl() +{ + LOG_PRINT_L3("Unsigned tx deleted"); +} + +int UnsignedTransactionImpl::status() const +{ + return m_status; +} + +string UnsignedTransactionImpl::errorString() const +{ + return m_errorString; +} + +bool UnsignedTransactionImpl::sign(const std::string &signedFileName) +{ + if(m_wallet.watchOnly()) + { + m_errorString = tr("This is a watch only wallet"); + m_status = Status_Error; + return false; + } + std::vector<tools::wallet2::pending_tx> ptx; + try + { + bool r = m_wallet.m_wallet->sign_tx(m_unsigned_tx_set, signedFileName, ptx); + if (!r) + { + m_errorString = tr("Failed to sign transaction"); + m_status = Status_Error; + return false; + } + } + catch (const std::exception &e) + { + m_errorString = string(tr("Failed to sign transaction")) + e.what(); + m_status = Status_Error; + return false; + } + return true; +} + +//---------------------------------------------------------------------------------------------------- +bool UnsignedTransactionImpl::checkLoadedTx(const std::function<size_t()> get_num_txes, const std::function<const tools::wallet2::tx_construction_data&(size_t)> &get_tx, const std::string &extra_message) +{ + // gather info to ask the user + uint64_t amount = 0, amount_to_dests = 0, change = 0; + size_t min_mixin = ~0; + std::unordered_map<std::string, uint64_t> dests; + const std::string wallet_address = m_wallet.m_wallet->get_account().get_public_address_str(m_wallet.m_wallet->testnet()); + int first_known_non_zero_change_index = -1; + for (size_t n = 0; n < get_num_txes(); ++n) + { + const tools::wallet2::tx_construction_data &cd = get_tx(n); + for (size_t s = 0; s < cd.sources.size(); ++s) + { + amount += cd.sources[s].amount; + size_t mixin = cd.sources[s].outputs.size() - 1; + if (mixin < min_mixin) + min_mixin = mixin; + } + for (size_t d = 0; d < cd.splitted_dsts.size(); ++d) + { + const cryptonote::tx_destination_entry &entry = cd.splitted_dsts[d]; + std::string address = get_account_address_as_str(m_wallet.m_wallet->testnet(), entry.addr); + std::unordered_map<std::string,uint64_t>::iterator i = dests.find(address); + if (i == dests.end()) + dests.insert(std::make_pair(address, entry.amount)); + else + i->second += entry.amount; + amount_to_dests += entry.amount; + } + if (cd.change_dts.amount > 0) + { + std::unordered_map<std::string, uint64_t>::iterator it = dests.find(get_account_address_as_str(m_wallet.m_wallet->testnet(), cd.change_dts.addr)); + if (it == dests.end()) + { + m_status = Status_Error; + m_errorString = tr("Claimed change does not go to a paid address"); + return false; + } + if (it->second < cd.change_dts.amount) + { + m_status = Status_Error; + m_errorString = tr("Claimed change is larger than payment to the change address"); + return false; + } + if (cd.change_dts.amount > 0) + { + if (first_known_non_zero_change_index == -1) + first_known_non_zero_change_index = n; + if (memcmp(&cd.change_dts.addr, &get_tx(first_known_non_zero_change_index).change_dts.addr, sizeof(cd.change_dts.addr))) + { + m_status = Status_Error; + m_errorString = tr("Change goes to more than one address"); + return false; + } + } + change += cd.change_dts.amount; + it->second -= cd.change_dts.amount; + if (it->second == 0) + dests.erase(get_account_address_as_str(m_wallet.m_wallet->testnet(), cd.change_dts.addr)); + } + } + std::string dest_string; + for (std::unordered_map<std::string, uint64_t>::const_iterator i = dests.begin(); i != dests.end(); ) + { + dest_string += (boost::format(tr("sending %s to %s")) % cryptonote::print_money(i->second) % i->first).str(); + ++i; + if (i != dests.end()) + dest_string += ", "; + } + if (dest_string.empty()) + dest_string = tr("with no destinations"); + + std::string change_string; + if (change > 0) + { + std::string address = get_account_address_as_str(m_wallet.m_wallet->testnet(), get_tx(0).change_dts.addr); + change_string += (boost::format(tr("%s change to %s")) % cryptonote::print_money(change) % address).str(); + } + else + change_string += tr("no change"); + uint64_t fee = amount - amount_to_dests; + m_confirmationMessage = (boost::format(tr("Loaded %lu transactions, for %s, fee %s, %s, %s, with min mixin %lu. %s")) % (unsigned long)get_num_txes() % cryptonote::print_money(amount) % cryptonote::print_money(fee) % dest_string % change_string % (unsigned long)min_mixin % extra_message).str(); + return true; +} + +std::vector<uint64_t> UnsignedTransactionImpl::amount() const +{ + std::vector<uint64_t> result; + for (const auto &utx : m_unsigned_tx_set.txes) { + for (const auto &unsigned_dest : utx.dests) { + result.push_back(unsigned_dest.amount); + } + } + return result; +} + +std::vector<uint64_t> UnsignedTransactionImpl::fee() const +{ + std::vector<uint64_t> result; + for (const auto &utx : m_unsigned_tx_set.txes) { + uint64_t fee = 0; + for (const auto &i: utx.sources) fee += i.amount; + for (const auto &i: utx.splitted_dsts) fee -= i.amount; + result.push_back(fee); + } + return result; +} + +std::vector<uint64_t> UnsignedTransactionImpl::mixin() const +{ + std::vector<uint64_t> result; + for (const auto &utx: m_unsigned_tx_set.txes) { + size_t min_mixin = ~0; + // TODO: Is this loop needed or is sources[0] ? + for (size_t s = 0; s < utx.sources.size(); ++s) { + size_t mixin = utx.sources[s].outputs.size() - 1; + if (mixin < min_mixin) + min_mixin = mixin; + } + result.push_back(min_mixin); + } + return result; +} + +uint64_t UnsignedTransactionImpl::txCount() const +{ + return m_unsigned_tx_set.txes.size(); +} + +std::vector<std::string> UnsignedTransactionImpl::paymentId() const +{ + std::vector<string> result; + for (const auto &utx: m_unsigned_tx_set.txes) { + crypto::hash payment_id = cryptonote::null_hash; + cryptonote::tx_extra_nonce extra_nonce; + std::vector<cryptonote::tx_extra_field> tx_extra_fields; + cryptonote::parse_tx_extra(utx.extra, tx_extra_fields); + if (cryptonote::find_tx_extra_field_by_type(tx_extra_fields, extra_nonce)) + { + crypto::hash8 payment_id8 = cryptonote::null_hash8; + if(cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id8)) + { + // We can't decrypt short pid without recipient key. + memcpy(payment_id.data, payment_id8.data, 8); + } + else if (!cryptonote::get_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id)) + { + payment_id = cryptonote::null_hash; + } + } + if(payment_id != cryptonote::null_hash) + result.push_back(epee::string_tools::pod_to_hex(payment_id)); + else + result.push_back(""); + } + return result; +} + +std::vector<std::string> UnsignedTransactionImpl::recipientAddress() const +{ + // TODO: return integrated address if short payment ID exists + std::vector<string> result; + for (const auto &utx: m_unsigned_tx_set.txes) { + result.push_back(cryptonote::get_account_address_as_str(m_wallet.m_wallet->testnet(), utx.dests[0].addr)); + } + return result; +} + +uint64_t UnsignedTransactionImpl::minMixinCount() const +{ + uint64_t min_mixin = ~0; + for (const auto &utx: m_unsigned_tx_set.txes) { + for (size_t s = 0; s < utx.sources.size(); ++s) { + size_t mixin = utx.sources[s].outputs.size() - 1; + if (mixin < min_mixin) + min_mixin = mixin; + } + } + return min_mixin; +} + +} // namespace + +namespace Bitmonero = Monero; + diff --git a/src/wallet/api/unsigned_transaction.h b/src/wallet/api/unsigned_transaction.h new file mode 100644 index 000000000..9c442f503 --- /dev/null +++ b/src/wallet/api/unsigned_transaction.h @@ -0,0 +1,76 @@ +// Copyright (c) 2014-2017, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 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 "wallet/wallet2_api.h" +#include "wallet/wallet2.h" + +#include <string> +#include <vector> + + +namespace Monero { + +class WalletImpl; +class UnsignedTransactionImpl : public UnsignedTransaction +{ +public: + UnsignedTransactionImpl(WalletImpl &wallet); + ~UnsignedTransactionImpl(); + int status() const; + std::string errorString() const; + std::vector<uint64_t> amount() const; + std::vector<uint64_t> dust() const; + std::vector<uint64_t> fee() const; + std::vector<uint64_t> mixin() const; + std::vector<std::string> paymentId() const; + std::vector<std::string> recipientAddress() const; + uint64_t txCount() const; + // sign txs and save to file + bool sign(const std::string &signedFileName); + std::string confirmationMessage() const {return m_confirmationMessage;} + uint64_t minMixinCount() const; + +private: + // Callback function to check all loaded tx's and generate confirmationMessage + bool checkLoadedTx(const std::function<size_t()> get_num_txes, const std::function<const tools::wallet2::tx_construction_data&(size_t)> &get_tx, const std::string &extra_message); + + friend class WalletImpl; + WalletImpl &m_wallet; + + int m_status; + std::string m_errorString; + tools::wallet2::unsigned_tx_set m_unsigned_tx_set; + std::string m_confirmationMessage; +}; + + +} + +namespace Bitmonero = Monero; diff --git a/src/wallet/api/utils.cpp b/src/wallet/api/utils.cpp index aa85323f0..a9646e038 100644 --- a/src/wallet/api/utils.cpp +++ b/src/wallet/api/utils.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -31,49 +31,26 @@ #include "include_base_utils.h" // LOG_PRINT_x -#include "net/http_client.h" // epee::net_utils::... -#include <boost/asio.hpp> +#include "common/util.h" using namespace std; -namespace Bitmonero { +namespace Monero { namespace Utils { - -// copy-pasted from simplewallet. - bool isAddressLocal(const std::string &address) -{ - // extract host - epee::net_utils::http::url_content u_c; - if (!epee::net_utils::parse_url(address, u_c)) - { - LOG_PRINT_L1("Failed to determine whether daemon is local, assuming not"); - return false; - } - if (u_c.host.empty()) - { - LOG_PRINT_L1("Failed to determine whether daemon is local, assuming not"); +{ + try { + return tools::is_local_address(address); + } catch (const std::exception &e) { + MERROR("error: " << e.what()); return false; } - - // resolve to IP - boost::asio::io_service io_service; - boost::asio::ip::tcp::resolver resolver(io_service); - boost::asio::ip::tcp::resolver::query query(u_c.host, ""); - boost::asio::ip::tcp::resolver::iterator i = resolver.resolve(query); - while (i != boost::asio::ip::tcp::resolver::iterator()) - { - const boost::asio::ip::tcp::endpoint &ep = *i; - if (ep.address().is_loopback()) - return true; - ++i; - } - - return false; } } } // namespace + +namespace Bitmonero = Monero; diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp index eefb49e95..21760ac49 100644 --- a/src/wallet/api/wallet.cpp +++ b/src/wallet/api/wallet.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -31,17 +31,23 @@ #include "wallet.h" #include "pending_transaction.h" +#include "unsigned_transaction.h" #include "transaction_history.h" +#include "address_book.h" #include "common_defines.h" #include "mnemonics/electrum-words.h" #include <boost/format.hpp> #include <sstream> +#include <unordered_map> using namespace std; using namespace cryptonote; -namespace Bitmonero { +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "WalletAPI" + +namespace Monero { namespace { // copy-pasted from simplewallet @@ -49,13 +55,18 @@ namespace { static const int DEFAULT_REFRESH_INTERVAL_MILLIS = 1000 * 10; // limit maximum refresh interval as one minute static const int MAX_REFRESH_INTERVAL_MILLIS = 1000 * 60 * 1; + // Default refresh interval when connected to remote node + static const int DEFAULT_REMOTE_NODE_REFRESH_INTERVAL_MILLIS = 1000 * 10; + // Connection timeout 30 sec + static const int DEFAULT_CONNECTION_TIMEOUT_MILLIS = 1000 * 30; } struct Wallet2CallbackImpl : public tools::i_wallet2_callback { - Wallet2CallbackImpl() + Wallet2CallbackImpl(WalletImpl * wallet) : m_listener(nullptr) + , m_wallet(wallet) { } @@ -77,48 +88,69 @@ struct Wallet2CallbackImpl : public tools::i_wallet2_callback virtual void on_new_block(uint64_t height, const cryptonote::block& block) { - LOG_PRINT_L3(__FUNCTION__ << ": new block. height: " << height); - - if (m_listener) { - m_listener->newBlock(height); - // m_listener->updated(); + // Don't flood the GUI with signals. On fast refresh - send signal every 1000th block + // get_refresh_from_block_height() returns the blockheight from when the wallet was + // created or the restore height specified when wallet was recovered + if(height >= m_wallet->m_wallet->get_refresh_from_block_height() || height % 1000 == 0) { + // LOG_PRINT_L3(__FUNCTION__ << ": new block. height: " << height); + if (m_listener) { + m_listener->newBlock(height); + } } } - virtual void on_money_received(uint64_t height, const cryptonote::transaction& tx, uint64_t amount) + virtual void on_money_received(uint64_t height, const crypto::hash &txid, uint64_t amount) { - std::string tx_hash = epee::string_tools::pod_to_hex(get_transaction_hash(tx)); + std::string tx_hash = epee::string_tools::pod_to_hex(txid); LOG_PRINT_L3(__FUNCTION__ << ": money received. height: " << height << ", tx: " << tx_hash << ", amount: " << print_money(amount)); - if (m_listener) { + // do not signal on received tx if wallet is not syncronized completely + if (m_listener && m_wallet->synchronized()) { m_listener->moneyReceived(tx_hash, amount); m_listener->updated(); } } - virtual void on_money_spent(uint64_t height, const cryptonote::transaction& in_tx, uint64_t amount, + virtual void on_unconfirmed_money_received(uint64_t height, const crypto::hash &txid, uint64_t amount) + { + + std::string tx_hash = epee::string_tools::pod_to_hex(txid); + + LOG_PRINT_L3(__FUNCTION__ << ": unconfirmed money received. height: " << height + << ", tx: " << tx_hash + << ", amount: " << print_money(amount)); + // do not signal on received tx if wallet is not syncronized completely + if (m_listener && m_wallet->synchronized()) { + m_listener->unconfirmedMoneyReceived(tx_hash, amount); + m_listener->updated(); + } + } + + virtual void on_money_spent(uint64_t height, const crypto::hash &txid, uint64_t amount, const cryptonote::transaction& spend_tx) { // TODO; - std::string tx_hash = epee::string_tools::pod_to_hex(get_transaction_hash(spend_tx)); + std::string tx_hash = epee::string_tools::pod_to_hex(txid); LOG_PRINT_L3(__FUNCTION__ << ": money spent. height: " << height << ", tx: " << tx_hash << ", amount: " << print_money(amount)); - if (m_listener) { + // do not signal on sent tx if wallet is not syncronized completely + if (m_listener && m_wallet->synchronized()) { m_listener->moneySpent(tx_hash, amount); m_listener->updated(); } } - virtual void on_skip_transaction(uint64_t height, const cryptonote::transaction& tx) + virtual void on_skip_transaction(uint64_t height, const crypto::hash &txid) { // TODO; } WalletListener * m_listener; + WalletImpl * m_wallet; }; Wallet::~Wallet() {} @@ -154,8 +186,71 @@ std::string Wallet::genPaymentId() bool Wallet::paymentIdValid(const string &paiment_id) { - crypto::hash8 pid; - return tools::wallet2::parse_short_payment_id(paiment_id, pid); + crypto::hash8 pid8; + if (tools::wallet2::parse_short_payment_id(paiment_id, pid8)) + return true; + crypto::hash pid; + if (tools::wallet2::parse_long_payment_id(paiment_id, pid)) + return true; + return false; +} + +bool Wallet::addressValid(const std::string &str, bool testnet) +{ + bool has_payment_id; + cryptonote::account_public_address address; + crypto::hash8 pid; + return get_account_integrated_address_from_str(address, has_payment_id, pid, testnet, str); +} + +bool Wallet::keyValid(const std::string &secret_key_string, const std::string &address_string, bool isViewKey, bool testnet, std::string &error) +{ + bool has_payment_id; + cryptonote::account_public_address address; + crypto::hash8 pid; + if(!get_account_integrated_address_from_str(address, has_payment_id, pid, testnet, address_string)) { + error = tr("Failed to parse address"); + return false; + } + + cryptonote::blobdata key_data; + if(!epee::string_tools::parse_hexstr_to_binbuff(secret_key_string, key_data) || key_data.size() != sizeof(crypto::secret_key)) + { + error = tr("Failed to parse key"); + return false; + } + crypto::secret_key key = *reinterpret_cast<const crypto::secret_key*>(key_data.data()); + + // check the key match the given address + crypto::public_key pkey; + if (!crypto::secret_key_to_public_key(key, pkey)) { + error = tr("failed to verify key"); + return false; + } + bool matchAddress = false; + if(isViewKey) + matchAddress = address.m_view_public_key == pkey; + else + matchAddress = address.m_spend_public_key == pkey; + + if(!matchAddress) { + error = tr("key does not match address"); + return false; + } + + return true; +} + +std::string Wallet::paymentIdFromAddress(const std::string &str, bool testnet) +{ + bool has_payment_id; + cryptonote::account_public_address address; + crypto::hash8 pid; + if (!get_account_integrated_address_from_str(address, has_payment_id, pid, testnet, str)) + return ""; + if (!has_payment_id) + return ""; + return epee::string_tools::pod_to_hex(pid); } uint64_t Wallet::maximumAllowedAmount() @@ -163,18 +258,34 @@ uint64_t Wallet::maximumAllowedAmount() return std::numeric_limits<uint64_t>::max(); } +void Wallet::init(const char *argv0, const char *default_log_base_name) { + epee::string_tools::set_module_name_and_folder(argv0); + mlog_configure(mlog_get_default_log_path(default_log_base_name), true); +} + +void Wallet::debug(const std::string &str) { + MDEBUG(str); +} ///////////////////////// WalletImpl implementation //////////////////////// WalletImpl::WalletImpl(bool testnet) - :m_wallet(nullptr), m_status(Wallet::Status_Ok), m_trustedDaemon(false), - m_wallet2Callback(nullptr) + :m_wallet(nullptr) + , m_status(Wallet::Status_Ok) + , m_trustedDaemon(false) + , m_wallet2Callback(nullptr) + , m_recoveringFromSeed(false) + , m_synchronized(false) + , m_rebuildWalletCache(false) + , m_is_connected(false) { m_wallet = new tools::wallet2(testnet); m_history = new TransactionHistoryImpl(this); - m_wallet2Callback = new Wallet2CallbackImpl; + m_wallet2Callback = new Wallet2CallbackImpl(this); m_wallet->callback(m_wallet2Callback); m_refreshThreadDone = false; m_refreshEnabled = false; + m_addressBook = new AddressBookImpl(this); + m_refreshIntervalMillis = DEFAULT_REFRESH_INTERVAL_MILLIS; @@ -186,21 +297,29 @@ WalletImpl::WalletImpl(bool testnet) WalletImpl::~WalletImpl() { + + LOG_PRINT_L1(__FUNCTION__); + // Pause refresh thread - prevents refresh from starting again + pauseRefresh(); + // Close wallet - stores cache and stops ongoing refresh operation + close(); + // Stop refresh thread stopRefresh(); + delete m_wallet2Callback; delete m_history; + delete m_addressBook; delete m_wallet; - delete m_wallet2Callback; + LOG_PRINT_L1(__FUNCTION__ << " finished"); } bool WalletImpl::create(const std::string &path, const std::string &password, const std::string &language) { clearStatus(); - + m_recoveringFromSeed = false; bool keys_file_exists; bool wallet_file_exists; tools::wallet2::wallet_exists(path, keys_file_exists, wallet_file_exists); - // TODO: figure out how to setup logger; LOG_PRINT_L3("wallet_path: " << path << ""); LOG_PRINT_L3("keys_file_exists: " << std::boolalpha << keys_file_exists << std::noboolalpha << " wallet_file_exists: " << std::boolalpha << wallet_file_exists << std::noboolalpha); @@ -210,7 +329,7 @@ bool WalletImpl::create(const std::string &path, const std::string &password, co if (keys_file_exists || wallet_file_exists) { m_errorString = "attempting to generate or restore wallet, but specified file(s) exist. Exiting to not risk overwriting."; LOG_ERROR(m_errorString); - m_status = Status_Error; + m_status = Status_Critical; return false; } // TODO: validate language @@ -222,25 +341,166 @@ bool WalletImpl::create(const std::string &path, const std::string &password, co m_status = Status_Ok; } catch (const std::exception &e) { LOG_ERROR("Error creating wallet: " << e.what()); + m_status = Status_Critical; + m_errorString = e.what(); + return false; + } + + return true; +} + +bool WalletImpl::createWatchOnly(const std::string &path, const std::string &password, const std::string &language) const +{ + clearStatus(); + std::unique_ptr<tools::wallet2> view_wallet(new tools::wallet2(m_wallet->testnet())); + + // Store same refresh height as original wallet + view_wallet->set_refresh_from_block_height(m_wallet->get_refresh_from_block_height()); + + bool keys_file_exists; + bool wallet_file_exists; + tools::wallet2::wallet_exists(path, keys_file_exists, wallet_file_exists); + LOG_PRINT_L3("wallet_path: " << path << ""); + LOG_PRINT_L3("keys_file_exists: " << std::boolalpha << keys_file_exists << std::noboolalpha + << " wallet_file_exists: " << std::boolalpha << wallet_file_exists << std::noboolalpha); + + // add logic to error out if new wallet requested but named wallet file exists + if (keys_file_exists || wallet_file_exists) { + m_errorString = "attempting to generate view only wallet, but specified file(s) exist. Exiting to not risk overwriting."; + LOG_ERROR(m_errorString); + m_status = Status_Error; + return false; + } + // TODO: validate language + view_wallet->set_seed_language(language); + + const crypto::secret_key viewkey = m_wallet->get_account().get_keys().m_view_secret_key; + const cryptonote::account_public_address address = m_wallet->get_account().get_keys().m_account_address; + + try { + view_wallet->generate(path, password, address, viewkey); + m_status = Status_Ok; + } catch (const std::exception &e) { + LOG_ERROR("Error creating view only wallet: " << e.what()); m_status = Status_Error; m_errorString = e.what(); return false; } + return true; +} + +bool WalletImpl::recoverFromKeys(const std::string &path, + const std::string &language, + const std::string &address_string, + const std::string &viewkey_string, + const std::string &spendkey_string) +{ + cryptonote::account_public_address address; + bool has_payment_id; + crypto::hash8 new_payment_id; + if(!get_account_integrated_address_from_str(address, has_payment_id, new_payment_id, m_wallet->testnet(), address_string)) + { + m_errorString = tr("failed to parse address"); + m_status = Status_Error; + return false; + } + + // parse optional spend key + crypto::secret_key spendkey; + bool has_spendkey = false; + if (!spendkey_string.empty()) { + cryptonote::blobdata spendkey_data; + if(!epee::string_tools::parse_hexstr_to_binbuff(spendkey_string, spendkey_data) || spendkey_data.size() != sizeof(crypto::secret_key)) + { + m_errorString = tr("failed to parse secret spend key"); + m_status = Status_Error; + return false; + } + has_spendkey = true; + spendkey = *reinterpret_cast<const crypto::secret_key*>(spendkey_data.data()); + } + + // parse view secret key + if (viewkey_string.empty()) { + m_errorString = tr("No view key supplied, cancelled"); + m_status = Status_Error; + return false; + } + cryptonote::blobdata viewkey_data; + if(!epee::string_tools::parse_hexstr_to_binbuff(viewkey_string, viewkey_data) || viewkey_data.size() != sizeof(crypto::secret_key)) + { + m_errorString = tr("failed to parse secret view key"); + m_status = Status_Error; + return false; + } + crypto::secret_key viewkey = *reinterpret_cast<const crypto::secret_key*>(viewkey_data.data()); + + // check the spend and view keys match the given address + crypto::public_key pkey; + if(has_spendkey) { + if (!crypto::secret_key_to_public_key(spendkey, pkey)) { + m_errorString = tr("failed to verify secret spend key"); + m_status = Status_Error; + return false; + } + if (address.m_spend_public_key != pkey) { + m_errorString = tr("spend key does not match address"); + m_status = Status_Error; + return false; + } + } + if (!crypto::secret_key_to_public_key(viewkey, pkey)) { + m_errorString = tr("failed to verify secret view key"); + m_status = Status_Error; + return false; + } + if (address.m_view_public_key != pkey) { + m_errorString = tr("view key does not match address"); + m_status = Status_Error; + return false; + } + try + { + if (has_spendkey) { + m_wallet->generate(path, "", address, spendkey, viewkey); + LOG_PRINT_L1("Generated new wallet from keys"); + } + else { + m_wallet->generate(path, "", address, viewkey); + LOG_PRINT_L1("Generated new view only wallet from keys"); + } + + } + catch (const std::exception& e) { + m_errorString = string(tr("failed to generate new wallet: ")) + e.what(); + m_status = Status_Error; + return false; + } return true; } + bool WalletImpl::open(const std::string &path, const std::string &password) { clearStatus(); + m_recoveringFromSeed = false; try { // TODO: handle "deprecated" + // Check if wallet cache exists + bool keys_file_exists; + bool wallet_file_exists; + tools::wallet2::wallet_exists(path, keys_file_exists, wallet_file_exists); + if(!wallet_file_exists){ + // Rebuilding wallet cache, using refresh height from .keys file + m_rebuildWalletCache = true; + } m_wallet->load(path, password); m_password = password; } catch (const std::exception &e) { LOG_ERROR("Error opening wallet: " << e.what()); - m_status = Status_Error; + m_status = Status_Critical; m_errorString = e.what(); } return m_status == Status_Ok; @@ -257,6 +517,7 @@ bool WalletImpl::recover(const std::string &path, const std::string &seed) return false; } + m_recoveringFromSeed = true; crypto::secret_key recovery_key; std::string old_language; if (!crypto::ElectrumWords::words_to_bytes(seed, recovery_key, old_language)) { @@ -268,9 +529,9 @@ bool WalletImpl::recover(const std::string &path, const std::string &seed) try { m_wallet->set_seed_language(old_language); m_wallet->generate(path, "", recovery_key, true, false); - // TODO: wallet->init(daemon_address); + } catch (const std::exception &e) { - m_status = Status_Error; + m_status = Status_Critical; m_errorString = e.what(); } return m_status == Status_Ok; @@ -280,19 +541,22 @@ bool WalletImpl::close() { bool result = false; - LOG_PRINT_L3("closing wallet..."); + LOG_PRINT_L1("closing wallet..."); try { - // do not store wallet with invalid status - if (status() == Status_Ok) + // Do not store wallet with invalid status + // Status Critical refers to errors on opening or creating wallets. + if (status() != Status_Critical) m_wallet->store(); - LOG_PRINT_L3("wallet::store done"); - LOG_PRINT_L3("Calling wallet::stop..."); + else + LOG_ERROR("Status_Critical - not storing wallet"); + LOG_PRINT_L1("wallet::store done"); + LOG_PRINT_L1("Calling wallet::stop..."); m_wallet->stop(); - LOG_PRINT_L3("wallet::stop done"); + LOG_PRINT_L1("wallet::stop done"); result = true; clearStatus(); } catch (const std::exception &e) { - m_status = Status_Error; + m_status = Status_Critical; m_errorString = e.what(); LOG_ERROR("Error closing wallet: " << e.what()); } @@ -349,11 +613,21 @@ std::string WalletImpl::integratedAddress(const std::string &payment_id) const { crypto::hash8 pid; if (!tools::wallet2::parse_short_payment_id(payment_id, pid)) { - pid = crypto::rand<crypto::hash8>(); + return ""; } return m_wallet->get_account().get_public_integrated_address_str(pid, m_wallet->testnet()); } +std::string WalletImpl::privateViewKey() const +{ + return epee::string_tools::pod_to_hex(m_wallet->get_account().get_keys().m_view_secret_key); +} + +std::string WalletImpl::path() const +{ + return m_wallet->path(); +} + bool WalletImpl::store(const std::string &path) { clearStatus(); @@ -382,32 +656,23 @@ string WalletImpl::keysFilename() const return m_wallet->get_keys_file(); } -bool WalletImpl::init(const std::string &daemon_address, uint64_t upper_transaction_size_limit) +bool WalletImpl::init(const std::string &daemon_address, uint64_t upper_transaction_size_limit, const std::string &daemon_username, const std::string &daemon_password) { clearStatus(); - - m_wallet->init(daemon_address, upper_transaction_size_limit); - if (Utils::isAddressLocal(daemon_address)) { - this->setTrustedDaemon(true); - } - bool result = this->refresh(); - // enabling background refresh thread - startRefresh(); - return result; - + if(daemon_username != "") + m_daemon_login.emplace(daemon_username, daemon_password); + return doInit(daemon_address, upper_transaction_size_limit); } -void WalletImpl::initAsync(const string &daemon_address, uint64_t upper_transaction_size_limit) +void WalletImpl::setRefreshFromBlockHeight(uint64_t refresh_from_block_height) { - clearStatus(); - m_wallet->init(daemon_address, upper_transaction_size_limit); - if (Utils::isAddressLocal(daemon_address)) { - this->setTrustedDaemon(true); - } - startRefresh(); + m_wallet->set_refresh_from_block_height(refresh_from_block_height); } - +void WalletImpl::setRecoveringFromSeed(bool recoveringFromSeed) +{ + m_recoveringFromSeed = recoveringFromSeed; +} uint64_t WalletImpl::balance() const { @@ -423,9 +688,14 @@ uint64_t WalletImpl::blockChainHeight() const { return m_wallet->get_blockchain_current_height(); } - +uint64_t WalletImpl::approximateBlockChainHeight() const +{ + return m_wallet->get_approximate_blockchain_height(); +} uint64_t WalletImpl::daemonBlockChainHeight() const { + if (!m_is_connected) + return 0; std::string err; uint64_t result = m_wallet->get_daemon_blockchain_height(err); if (!err.empty()) { @@ -441,6 +711,41 @@ uint64_t WalletImpl::daemonBlockChainHeight() const return result; } +uint64_t WalletImpl::daemonBlockChainTargetHeight() const +{ + if (!m_is_connected) + return 0; + std::string err; + uint64_t result = m_wallet->get_daemon_blockchain_target_height(err); + if (!err.empty()) { + LOG_ERROR(__FUNCTION__ << ": " << err); + result = 0; + m_errorString = err; + m_status = Status_Error; + + } else { + m_status = Status_Ok; + m_errorString = ""; + } + // Target height can be 0 when daemon is synced. Use blockchain height instead. + if(result == 0) + result = daemonBlockChainHeight(); + return result; +} + +bool WalletImpl::daemonSynced() const +{ + if(connected() == Wallet::ConnectionStatus_Disconnected) + return false; + uint64_t blockChainHeight = daemonBlockChainHeight(); + return (blockChainHeight >= daemonBlockChainTargetHeight() && blockChainHeight > 1); +} + +bool WalletImpl::synchronized() const +{ + return m_synchronized; +} + bool WalletImpl::refresh() { clearStatus(); @@ -450,7 +755,7 @@ bool WalletImpl::refresh() void WalletImpl::refreshAsync() { - LOG_PRINT_L3(__FUNCTION__ << ": Refreshing asyncronously.."); + LOG_PRINT_L3(__FUNCTION__ << ": Refreshing asynchronously.."); clearStatus(); m_refreshCV.notify_one(); } @@ -471,6 +776,93 @@ int WalletImpl::autoRefreshInterval() const return m_refreshIntervalMillis; } +UnsignedTransaction *WalletImpl::loadUnsignedTx(const std::string &unsigned_filename) { + clearStatus(); + UnsignedTransactionImpl * transaction = new UnsignedTransactionImpl(*this); + if (!m_wallet->load_unsigned_tx(unsigned_filename, transaction->m_unsigned_tx_set)){ + m_errorString = tr("Failed to load unsigned transactions"); + m_status = Status_Error; + } + + // Check tx data and construct confirmation message + std::string extra_message; + if (!transaction->m_unsigned_tx_set.transfers.empty()) + extra_message = (boost::format("%u outputs to import. ") % (unsigned)transaction->m_unsigned_tx_set.transfers.size()).str(); + transaction->checkLoadedTx([&transaction](){return transaction->m_unsigned_tx_set.txes.size();}, [&transaction](size_t n)->const tools::wallet2::tx_construction_data&{return transaction->m_unsigned_tx_set.txes[n];}, extra_message); + m_status = transaction->status(); + m_errorString = transaction->errorString(); + + return transaction; +} + +bool WalletImpl::submitTransaction(const string &fileName) { + clearStatus(); + std::unique_ptr<PendingTransactionImpl> transaction(new PendingTransactionImpl(*this)); + + bool r = m_wallet->load_tx(fileName, transaction->m_pending_tx); + if (!r) { + m_errorString = tr("Failed to load transaction from file"); + m_status = Status_Ok; + return false; + } + + if(!transaction->commit()) { + m_errorString = transaction->m_errorString; + m_status = Status_Error; + return false; + } + + return true; +} + +bool WalletImpl::exportKeyImages(const string &filename) +{ + if (m_wallet->watch_only()) + { + m_errorString = tr("Wallet is view only"); + m_status = Status_Error; + return false; + } + + try + { + if (!m_wallet->export_key_images(filename)) + { + m_errorString = tr("failed to save file ") + filename; + m_status = Status_Error; + return false; + } + } + catch (const std::exception &e) + { + LOG_ERROR("Error exporting key images: " << e.what()); + m_errorString = e.what(); + m_status = Status_Error; + return false; + } + return true; +} + +bool WalletImpl::importKeyImages(const string &filename) +{ + try + { + uint64_t spent = 0, unspent = 0; + uint64_t height = m_wallet->import_key_images(filename, spent, unspent); + LOG_PRINT_L2("Signed key images imported to height " << height << ", " + << print_money(spent) << " spent, " << print_money(unspent) << " unspent"); + } + catch (const std::exception &e) + { + LOG_ERROR("Error exporting key images: " << e.what()); + m_errorString = string(tr("Failed to import key images: ")) + e.what(); + m_status = Status_Error; + return false; + } + + return true; +} + // TODO: // 1 - properly handle payment id (add another menthod with explicit 'payment_id' param) // 2 - check / design how "Transaction" can be single interface @@ -481,13 +873,15 @@ int WalletImpl::autoRefreshInterval() const // - unconfirmed_transfer_details; // - confirmed_transfer_details) -PendingTransaction *WalletImpl::createTransaction(const string &dst_addr, const string &payment_id, uint64_t amount, uint32_t mixin_count, +PendingTransaction *WalletImpl::createTransaction(const string &dst_addr, const string &payment_id, optional<uint64_t> amount, uint32_t mixin_count, PendingTransaction::Priority priority) { clearStatus(); - vector<cryptonote::tx_destination_entry> dsts; - cryptonote::tx_destination_entry de; + // Pause refresh thread while creating transaction + pauseRefresh(); + + cryptonote::account_public_address addr; // indicates if dst_addr is integrated address (address + payment_id) bool has_payment_id; @@ -500,7 +894,7 @@ PendingTransaction *WalletImpl::createTransaction(const string &dst_addr, const PendingTransactionImpl * transaction = new PendingTransactionImpl(*this); do { - if(!cryptonote::get_account_integrated_address_from_str(de.addr, has_payment_id, payment_id_short, m_wallet->testnet(), dst_addr)) { + if(!cryptonote::get_account_integrated_address_from_str(addr, has_payment_id, payment_id_short, m_wallet->testnet(), dst_addr)) { // TODO: copy-paste 'if treating as an address fails, try as url' from simplewallet.cpp:1982 m_status = Status_Error; m_errorString = "Invalid destination address"; @@ -533,15 +927,129 @@ PendingTransaction *WalletImpl::createTransaction(const string &dst_addr, const break; } } + else if (has_payment_id) { + std::string extra_nonce; + set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, payment_id_short); + bool r = add_extra_nonce_to_tx_extra(extra, extra_nonce); + if (!r) { + m_status = Status_Error; + m_errorString = tr("Failed to add short payment id: ") + epee::string_tools::pod_to_hex(payment_id_short); + break; + } + } + - de.amount = amount; - dsts.push_back(de); //std::vector<tools::wallet2::pending_tx> ptx_vector; try { - transaction->m_pending_tx = m_wallet->create_transactions_2(dsts, fake_outs_count, 0 /* unlock_time */, - static_cast<uint32_t>(priority), - extra, m_trustedDaemon); + if (amount) { + vector<cryptonote::tx_destination_entry> dsts; + cryptonote::tx_destination_entry de; + de.addr = addr; + de.amount = *amount; + dsts.push_back(de); + transaction->m_pending_tx = m_wallet->create_transactions_2(dsts, fake_outs_count, 0 /* unlock_time */, + static_cast<uint32_t>(priority), + extra, m_trustedDaemon); + } else { + transaction->m_pending_tx = m_wallet->create_transactions_all(0, addr, fake_outs_count, 0 /* unlock_time */, + static_cast<uint32_t>(priority), + extra, m_trustedDaemon); + } + + } catch (const tools::error::daemon_busy&) { + // TODO: make it translatable with "tr"? + m_errorString = tr("daemon is busy. Please try again later."); + m_status = Status_Error; + } catch (const tools::error::no_connection_to_daemon&) { + m_errorString = tr("no connection to daemon. Please make sure daemon is running."); + m_status = Status_Error; + } catch (const tools::error::wallet_rpc_error& e) { + m_errorString = tr("RPC error: ") + e.to_string(); + m_status = Status_Error; + } catch (const tools::error::get_random_outs_error &e) { + m_errorString = (boost::format(tr("failed to get random outputs to mix: %s")) % e.what()).str(); + m_status = Status_Error; + + } catch (const tools::error::not_enough_money& e) { + m_status = Status_Error; + std::ostringstream writer; + + writer << boost::format(tr("not enough money to transfer, available only %s, sent amount %s")) % + print_money(e.available()) % + print_money(e.tx_amount()); + m_errorString = writer.str(); + + } catch (const tools::error::tx_not_possible& e) { + m_status = Status_Error; + std::ostringstream writer; + + writer << boost::format(tr("not enough money to transfer, available only %s, transaction amount %s = %s + %s (fee)")) % + print_money(e.available()) % + print_money(e.tx_amount() + e.fee()) % + print_money(e.tx_amount()) % + print_money(e.fee()); + m_errorString = writer.str(); + + } catch (const tools::error::not_enough_outs_to_mix& e) { + std::ostringstream writer; + writer << tr("not enough outputs for specified mixin_count") << " = " << e.mixin_count() << ":"; + for (const std::pair<uint64_t, uint64_t> outs_for_amount : e.scanty_outs()) { + writer << "\n" << tr("output amount") << " = " << print_money(outs_for_amount.first) << ", " << tr("found outputs to mix") << " = " << outs_for_amount.second; + } + m_errorString = writer.str(); + m_status = Status_Error; + } catch (const tools::error::tx_not_constructed&) { + m_errorString = tr("transaction was not constructed"); + m_status = Status_Error; + } catch (const tools::error::tx_rejected& e) { + std::ostringstream writer; + writer << (boost::format(tr("transaction %s was rejected by daemon with status: ")) % get_transaction_hash(e.tx())) << e.status(); + m_errorString = writer.str(); + m_status = Status_Error; + } catch (const tools::error::tx_sum_overflow& e) { + m_errorString = e.what(); + m_status = Status_Error; + } catch (const tools::error::zero_destination&) { + m_errorString = tr("one of destinations is zero"); + m_status = Status_Error; + } catch (const tools::error::tx_too_big& e) { + m_errorString = tr("failed to find a suitable way to split transactions"); + m_status = Status_Error; + } catch (const tools::error::transfer_error& e) { + m_errorString = string(tr("unknown transfer error: ")) + e.what(); + m_status = Status_Error; + } catch (const tools::error::wallet_internal_error& e) { + m_errorString = string(tr("internal error: ")) + e.what(); + m_status = Status_Error; + } catch (const std::exception& e) { + m_errorString = string(tr("unexpected error: ")) + e.what(); + m_status = Status_Error; + } catch (...) { + m_errorString = tr("unknown error"); + m_status = Status_Error; + } + } while (false); + + transaction->m_status = m_status; + transaction->m_errorString = m_errorString; + // Resume refresh thread + startRefresh(); + return transaction; +} + +PendingTransaction *WalletImpl::createSweepUnmixableTransaction() + +{ + clearStatus(); + vector<cryptonote::tx_destination_entry> dsts; + cryptonote::tx_destination_entry de; + + PendingTransactionImpl * transaction = new PendingTransactionImpl(*this); + + do { + try { + transaction->m_pending_tx = m_wallet->create_unmixable_sweep_transactions(m_trustedDaemon); } catch (const tools::error::daemon_busy&) { // TODO: make it translatable with "tr"? @@ -561,6 +1069,15 @@ PendingTransaction *WalletImpl::createTransaction(const string &dst_addr, const m_status = Status_Error; std::ostringstream writer; + writer << boost::format(tr("not enough money to transfer, available only %s, sent amount %s")) % + print_money(e.available()) % + print_money(e.tx_amount()); + m_errorString = writer.str(); + + } catch (const tools::error::tx_not_possible& e) { + m_status = Status_Error; + std::ostringstream writer; + writer << boost::format(tr("not enough money to transfer, available only %s, transaction amount %s = %s + %s (fee)")) % print_money(e.available()) % print_money(e.tx_amount() + e.fee()) % @@ -623,6 +1140,11 @@ TransactionHistory *WalletImpl::history() const return m_history; } +AddressBook *WalletImpl::addressBook() const +{ + return m_addressBook; +} + void WalletImpl::setListener(WalletListener *l) { // TODO thread synchronization; @@ -639,10 +1161,67 @@ void WalletImpl::setDefaultMixin(uint32_t arg) m_wallet->default_mixin(arg); } +bool WalletImpl::setUserNote(const std::string &txid, const std::string ¬e) +{ + cryptonote::blobdata txid_data; + if(!epee::string_tools::parse_hexstr_to_binbuff(txid, txid_data) || txid_data.size() != sizeof(crypto::hash)) + return false; + const crypto::hash htxid = *reinterpret_cast<const crypto::hash*>(txid_data.data()); + + m_wallet->set_tx_note(htxid, note); + return true; +} + +std::string WalletImpl::getUserNote(const std::string &txid) const +{ + cryptonote::blobdata txid_data; + if(!epee::string_tools::parse_hexstr_to_binbuff(txid, txid_data) || txid_data.size() != sizeof(crypto::hash)) + return ""; + const crypto::hash htxid = *reinterpret_cast<const crypto::hash*>(txid_data.data()); + + return m_wallet->get_tx_note(htxid); +} + +std::string WalletImpl::getTxKey(const std::string &txid) const +{ + cryptonote::blobdata txid_data; + if(!epee::string_tools::parse_hexstr_to_binbuff(txid, txid_data) || txid_data.size() != sizeof(crypto::hash)) + { + return ""; + } + const crypto::hash htxid = *reinterpret_cast<const crypto::hash*>(txid_data.data()); + + crypto::secret_key tx_key; + if (m_wallet->get_tx_key(htxid, tx_key)) + { + return epee::string_tools::pod_to_hex(tx_key); + } + else + { + return ""; + } +} + +std::string WalletImpl::signMessage(const std::string &message) +{ + return m_wallet->sign(message); +} + +bool WalletImpl::verifySignedMessage(const std::string &message, const std::string &address, const std::string &signature) const +{ + cryptonote::account_public_address addr; + bool has_payment_id; + crypto::hash8 payment_id; + + if (!cryptonote::get_account_integrated_address_from_str(addr, has_payment_id, payment_id, m_wallet->testnet(), address)) + return false; + + return m_wallet->verify(message, addr, signature); +} bool WalletImpl::connectToDaemon() { - bool result = m_wallet->check_connection(); + bool result = m_wallet->check_connection(NULL, DEFAULT_CONNECTION_TIMEOUT_MILLIS); m_status = result ? Status_Ok : Status_Error; if (!result) { m_errorString = "Error connecting to daemon at " + m_wallet->get_daemon_address(); @@ -652,9 +1231,15 @@ bool WalletImpl::connectToDaemon() return result; } -bool WalletImpl::connected() const +Wallet::ConnectionStatus WalletImpl::connected() const { - return m_wallet->check_connection(); + uint32_t version = 0; + m_is_connected = m_wallet->check_connection(&version, DEFAULT_CONNECTION_TIMEOUT_MILLIS); + if (!m_is_connected) + return Wallet::ConnectionStatus_Disconnected; + if ((version >> 16) != CORE_RPC_VERSION_MAJOR) + return Wallet::ConnectionStatus_WrongVersion; + return Wallet::ConnectionStatus_Connected; } void WalletImpl::setTrustedDaemon(bool arg) @@ -667,7 +1252,12 @@ bool WalletImpl::trustedDaemon() const return m_trustedDaemon; } -void WalletImpl::clearStatus() +bool WalletImpl::watchOnly() const +{ + return m_wallet->watch_only(); +} + +void WalletImpl::clearStatus() const { m_status = Status_Ok; m_errorString.clear(); @@ -695,7 +1285,7 @@ void WalletImpl::refreshThreadFunc() LOG_PRINT_L3(__FUNCTION__ << ": refresh lock acquired..."); LOG_PRINT_L3(__FUNCTION__ << ": m_refreshEnabled: " << m_refreshEnabled); LOG_PRINT_L3(__FUNCTION__ << ": m_status: " << m_status); - if (m_refreshEnabled /*&& m_status == Status_Ok*/) { + if (m_refreshEnabled) { LOG_PRINT_L3(__FUNCTION__ << ": refreshing..."); doRefresh(); } @@ -708,7 +1298,22 @@ void WalletImpl::doRefresh() // synchronizing async and sync refresh calls boost::lock_guard<boost::mutex> guarg(m_refreshMutex2); try { - m_wallet->refresh(); + // Syncing daemon and refreshing wallet simultaneously is very resource intensive. + // Disable refresh if wallet is disconnected or daemon isn't synced. + if (daemonSynced()) { + m_wallet->refresh(); + if (!m_synchronized) { + m_synchronized = true; + } + // assuming if we have empty history, it wasn't initialized yet + // for futher history changes client need to update history in + // "on_money_received" and "on_money_sent" callbacks + if (m_history->count() == 0) { + m_history->refresh(); + } + } else { + LOG_PRINT_L3(__FUNCTION__ << ": skipping refresh - daemon is not synced"); + } } catch (const std::exception &e) { m_status = Status_Error; m_errorString = e.what(); @@ -722,6 +1327,7 @@ void WalletImpl::doRefresh() void WalletImpl::startRefresh() { if (!m_refreshEnabled) { + LOG_PRINT_L2(__FUNCTION__ << ": refresh started/resumed..."); m_refreshEnabled = true; m_refreshCV.notify_one(); } @@ -741,6 +1347,7 @@ void WalletImpl::stopRefresh() void WalletImpl::pauseRefresh() { + LOG_PRINT_L2(__FUNCTION__ << ": refresh paused..."); // TODO synchronize access if (!m_refreshThreadDone) { m_refreshEnabled = false; @@ -748,4 +1355,76 @@ void WalletImpl::pauseRefresh() } +bool WalletImpl::isNewWallet() const +{ + // in case wallet created without daemon connection, closed and opened again, + // it's the same case as if it created from scratch, i.e. we need "fast sync" + // with the daemon (pull hashes instead of pull blocks). + // If wallet cache is rebuilt, creation height stored in .keys is used. + // Watch only wallet is a copy of an existing wallet. + return !(blockChainHeight() > 1 || m_recoveringFromSeed || m_rebuildWalletCache) && !watchOnly(); +} + +bool WalletImpl::doInit(const string &daemon_address, uint64_t upper_transaction_size_limit) +{ + if (!m_wallet->init(daemon_address, m_daemon_login, upper_transaction_size_limit)) + return false; + + // in case new wallet, this will force fast-refresh (pulling hashes instead of blocks) + // If daemon isn't synced a calculated block height will be used instead + if (isNewWallet() && daemonSynced()) { + LOG_PRINT_L2(__FUNCTION__ << ":New Wallet - fast refresh until " << daemonBlockChainHeight()); + m_wallet->set_refresh_from_block_height(daemonBlockChainHeight()); + } + + if (m_rebuildWalletCache) + LOG_PRINT_L2(__FUNCTION__ << ": Rebuilding wallet cache, fast refresh until block " << m_wallet->get_refresh_from_block_height()); + + if (Utils::isAddressLocal(daemon_address)) { + this->setTrustedDaemon(true); + m_refreshIntervalMillis = DEFAULT_REFRESH_INTERVAL_MILLIS; + } else { + this->setTrustedDaemon(false); + m_refreshIntervalMillis = DEFAULT_REMOTE_NODE_REFRESH_INTERVAL_MILLIS; + } + return true; +} + +bool WalletImpl::parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &amount, std::string &tx_description, std::string &recipient_name, std::vector<std::string> &unknown_parameters, std::string &error) +{ + return m_wallet->parse_uri(uri, address, payment_id, amount, tx_description, recipient_name, unknown_parameters, error); +} + +bool WalletImpl::rescanSpent() +{ + clearStatus(); + if (!trustedDaemon()) { + m_status = Status_Error; + m_errorString = tr("Rescan spent can only be used with a trusted daemon"); + return false; + } + try { + m_wallet->rescan_spent(); + } catch (const std::exception &e) { + LOG_ERROR(__FUNCTION__ << " error: " << e.what()); + m_status = Status_Error; + m_errorString = e.what(); + return false; + } + return true; +} + + +void WalletImpl::hardForkInfo(uint8_t &version, uint64_t &earliest_height) const +{ + m_wallet->get_hard_fork_info(version, earliest_height); +} + +bool WalletImpl::useForkRules(uint8_t version, int64_t early_blocks) const +{ + return m_wallet->use_fork_rules(version,early_blocks); +} + } // namespace + +namespace Bitmonero = Monero; diff --git a/src/wallet/api/wallet.h b/src/wallet/api/wallet.h index d97a8f3b3..c376dd6c1 100644 --- a/src/wallet/api/wallet.h +++ b/src/wallet/api/wallet.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -40,9 +40,11 @@ #include <boost/thread/condition_variable.hpp> -namespace Bitmonero { +namespace Monero { class TransactionHistoryImpl; class PendingTransactionImpl; +class UnsignedTransactionImpl; +class AddressBookImpl; struct Wallet2CallbackImpl; class WalletImpl : public Wallet @@ -52,8 +54,15 @@ public: ~WalletImpl(); bool create(const std::string &path, const std::string &password, const std::string &language); + bool createWatchOnly(const std::string &path, const std::string &password, + const std::string &language) const; bool open(const std::string &path, const std::string &password); bool recover(const std::string &path, const std::string &seed); + bool recoverFromKeys(const std::string &path, + const std::string &language, + const std::string &address_string, + const std::string &viewkey_string, + const std::string &spendkey_string = ""); bool close(); std::string seed() const; std::string getSeedLanguage() const; @@ -64,47 +73,74 @@ public: bool setPassword(const std::string &password); std::string address() const; std::string integratedAddress(const std::string &payment_id) const; + std::string privateViewKey() const; + std::string path() const; bool store(const std::string &path); std::string filename() const; std::string keysFilename() const; - bool init(const std::string &daemon_address, uint64_t upper_transaction_size_limit); - void initAsync(const std::string &daemon_address, uint64_t upper_transaction_size_limit); + bool init(const std::string &daemon_address, uint64_t upper_transaction_size_limit = 0, const std::string &daemon_username = "", const std::string &daemon_password = ""); bool connectToDaemon(); - bool connected() const; + ConnectionStatus connected() const; void setTrustedDaemon(bool arg); bool trustedDaemon() const; uint64_t balance() const; uint64_t unlockedBalance() const; uint64_t blockChainHeight() const; + uint64_t approximateBlockChainHeight() const; uint64_t daemonBlockChainHeight() const; + uint64_t daemonBlockChainTargetHeight() const; + bool synchronized() const; bool refresh(); void refreshAsync(); void setAutoRefreshInterval(int millis); int autoRefreshInterval() const; - - + void setRefreshFromBlockHeight(uint64_t refresh_from_block_height); + void setRecoveringFromSeed(bool recoveringFromSeed); + bool watchOnly() const; + bool rescanSpent(); + bool testnet() const {return m_wallet->testnet();} + void hardForkInfo(uint8_t &version, uint64_t &earliest_height) const; + bool useForkRules(uint8_t version, int64_t early_blocks) const; PendingTransaction * createTransaction(const std::string &dst_addr, const std::string &payment_id, - uint64_t amount, uint32_t mixin_count, + optional<uint64_t> amount, uint32_t mixin_count, PendingTransaction::Priority priority = PendingTransaction::Priority_Low); + virtual PendingTransaction * createSweepUnmixableTransaction(); + bool submitTransaction(const std::string &fileName); + virtual UnsignedTransaction * loadUnsignedTx(const std::string &unsigned_filename); + bool exportKeyImages(const std::string &filename); + bool importKeyImages(const std::string &filename); virtual void disposeTransaction(PendingTransaction * t); virtual TransactionHistory * history() const; + virtual AddressBook * addressBook() const; virtual void setListener(WalletListener * l); virtual uint32_t defaultMixin() const; virtual void setDefaultMixin(uint32_t arg); + virtual bool setUserNote(const std::string &txid, const std::string ¬e); + virtual std::string getUserNote(const std::string &txid) const; + virtual std::string getTxKey(const std::string &txid) const; + virtual std::string signMessage(const std::string &message); + virtual bool verifySignedMessage(const std::string &message, const std::string &address, const std::string &signature) const; + virtual void startRefresh(); + virtual void pauseRefresh(); + virtual bool parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &amount, std::string &tx_description, std::string &recipient_name, std::vector<std::string> &unknown_parameters, std::string &error); private: - void clearStatus(); + void clearStatus() const; void refreshThreadFunc(); void doRefresh(); - void startRefresh(); + bool daemonSynced() const; void stopRefresh(); - void pauseRefresh(); + bool isNewWallet() const; + bool doInit(const std::string &daemon_address, uint64_t upper_transaction_size_limit); private: friend class PendingTransactionImpl; + friend class UnsignedTransactionImpl; friend class TransactionHistoryImpl; + friend struct Wallet2CallbackImpl; + friend class AddressBookImpl; tools::wallet2 * m_wallet; mutable std::atomic<int> m_status; @@ -114,6 +150,7 @@ private: bool m_trustedDaemon; WalletListener * m_walletListener; Wallet2CallbackImpl * m_wallet2Callback; + AddressBookImpl * m_addressBook; // multi-threaded refresh stuff std::atomic<bool> m_refreshEnabled; @@ -126,11 +163,21 @@ private: boost::mutex m_refreshMutex2; boost::condition_variable m_refreshCV; boost::thread m_refreshThread; - + // flag indicating wallet is recovering from seed + // so it shouldn't be considered as new and pull blocks (slow-refresh) + // instead of pulling hashes (fast-refresh) + std::atomic<bool> m_recoveringFromSeed; + std::atomic<bool> m_synchronized; + std::atomic<bool> m_rebuildWalletCache; + // cache connection status to avoid unnecessary RPC calls + mutable std::atomic<bool> m_is_connected; + boost::optional<epee::net_utils::http::login> m_daemon_login{}; }; } // namespace +namespace Bitmonero = Monero; + #endif diff --git a/src/wallet/api/wallet_manager.cpp b/src/wallet/api/wallet_manager.cpp index ca83806ff..b2f947972 100644 --- a/src/wallet/api/wallet_manager.cpp +++ b/src/wallet/api/wallet_manager.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -31,16 +31,33 @@ #include "wallet_manager.h" #include "wallet.h" +#include "common_defines.h" +#include "common/dns_utils.h" +#include "common/util.h" +#include "common/updates.h" +#include "version.h" +#include "net/http_client.h" #include <boost/filesystem.hpp> #include <boost/regex.hpp> +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "WalletAPI" namespace epee { unsigned int g_test_dbg_lock_sleep = 0; } -namespace Bitmonero { +namespace { + template<typename Request, typename Response> + bool connect_and_invoke(const std::string& address, const std::string& path, const Request& request, Response& response) + { + epee::net_utils::http::http_simple_client client{}; + return client.set_server(address, boost::none) && epee::net_utils::invoke_http_json(path, request, response, client); + } +} + +namespace Monero { Wallet *WalletManagerImpl::createWallet(const std::string &path, const std::string &password, const std::string &language, bool testnet) @@ -54,16 +71,37 @@ Wallet *WalletManagerImpl::openWallet(const std::string &path, const std::string { WalletImpl * wallet = new WalletImpl(testnet); wallet->open(path, password); + //Refresh addressBook + wallet->addressBook()->refresh(); return wallet; } -Wallet *WalletManagerImpl::recoveryWallet(const std::string &path, const std::string &memo, bool testnet) +Wallet *WalletManagerImpl::recoveryWallet(const std::string &path, const std::string &memo, bool testnet, uint64_t restoreHeight) { WalletImpl * wallet = new WalletImpl(testnet); + if(restoreHeight > 0){ + wallet->setRefreshFromBlockHeight(restoreHeight); + } wallet->recover(path, memo); return wallet; } +Wallet *WalletManagerImpl::createWalletFromKeys(const std::string &path, + const std::string &language, + bool testnet, + uint64_t restoreHeight, + const std::string &addressString, + const std::string &viewKeyString, + const std::string &spendKeyString) +{ + WalletImpl * wallet = new WalletImpl(testnet); + if(restoreHeight > 0){ + wallet->setRefreshFromBlockHeight(restoreHeight); + } + wallet->recoverFromKeys(path, language, addressString, viewKeyString, spendKeyString); + return wallet; +} + bool WalletManagerImpl::closeWallet(Wallet *wallet) { WalletImpl * wallet_ = dynamic_cast<WalletImpl*>(wallet); @@ -78,6 +116,12 @@ bool WalletManagerImpl::closeWallet(Wallet *wallet) bool WalletManagerImpl::walletExists(const std::string &path) { + bool keys_file_exists; + bool wallet_file_exists; + tools::wallet2::wallet_exists(path, keys_file_exists, wallet_file_exists); + if(keys_file_exists){ + return true; + } return false; } @@ -85,10 +129,13 @@ bool WalletManagerImpl::walletExists(const std::string &path) std::vector<std::string> WalletManagerImpl::findWallets(const std::string &path) { std::vector<std::string> result; + boost::filesystem::path work_dir(path); + // return empty result if path doesn't exist + if(!boost::filesystem::is_directory(path)){ + return result; + } const boost::regex wallet_rx("(.*)\\.(keys)$"); // searching for <wallet_name>.keys files boost::filesystem::recursive_directory_iterator end_itr; // Default ctor yields past-the-end - boost::filesystem::path work_dir(path); - for (boost::filesystem::recursive_directory_iterator itr(path); itr != end_itr; ++itr) { // Skip if not a file if (!boost::filesystem::is_regular_file(itr->status())) @@ -116,11 +163,295 @@ std::string WalletManagerImpl::errorString() const return m_errorString; } -void WalletManagerImpl::setDaemonHost(const std::string &hostname) +void WalletManagerImpl::setDaemonAddress(const std::string &address) +{ + m_daemonAddress = address; +} + +bool WalletManagerImpl::connected(uint32_t *version) const +{ + epee::json_rpc::request<cryptonote::COMMAND_RPC_GET_VERSION::request> req_t = AUTO_VAL_INIT(req_t); + epee::json_rpc::response<cryptonote::COMMAND_RPC_GET_VERSION::response, std::string> resp_t = AUTO_VAL_INIT(resp_t); + req_t.jsonrpc = "2.0"; + req_t.id = epee::serialization::storage_entry(0); + req_t.method = "get_version"; + if (!connect_and_invoke(m_daemonAddress, "/json_rpc", req_t, resp_t)) + return false; + + if (version) + *version = resp_t.result.version; + return true; +} + +bool WalletManagerImpl::checkPayment(const std::string &address_text, const std::string &txid_text, const std::string &txkey_text, const std::string &daemon_address, uint64_t &received, uint64_t &height, std::string &error) const +{ + error = ""; + cryptonote::blobdata txid_data; + if(!epee::string_tools::parse_hexstr_to_binbuff(txid_text, txid_data) || txid_data.size() != sizeof(crypto::hash)) + { + error = tr("failed to parse txid"); + return false; + } + crypto::hash txid = *reinterpret_cast<const crypto::hash*>(txid_data.data()); + + if (txkey_text.size() < 64 || txkey_text.size() % 64) + { + error = tr("failed to parse tx key"); + return false; + } + crypto::secret_key tx_key; + cryptonote::blobdata tx_key_data; + if(!epee::string_tools::parse_hexstr_to_binbuff(txkey_text, tx_key_data) || tx_key_data.size() != sizeof(crypto::hash)) + { + error = tr("failed to parse tx key"); + return false; + } + tx_key = *reinterpret_cast<const crypto::secret_key*>(tx_key_data.data()); + + bool testnet = address_text[0] != '4'; + cryptonote::account_public_address address; + bool has_payment_id; + crypto::hash8 payment_id; + if(!cryptonote::get_account_integrated_address_from_str(address, has_payment_id, payment_id, testnet, address_text)) + { + error = tr("failed to parse address"); + return false; + } + + cryptonote::COMMAND_RPC_GET_TRANSACTIONS::request req; + cryptonote::COMMAND_RPC_GET_TRANSACTIONS::response res; + req.txs_hashes.push_back(epee::string_tools::pod_to_hex(txid)); + if (!connect_and_invoke(m_daemonAddress, "/gettransactions", req, res) || + (res.txs.size() != 1 && res.txs_as_hex.size() != 1)) + { + error = tr("failed to get transaction from daemon"); + return false; + } + cryptonote::blobdata tx_data; + bool ok; + if (res.txs.size() == 1) + ok = epee::string_tools::parse_hexstr_to_binbuff(res.txs.front().as_hex, tx_data); + else + ok = epee::string_tools::parse_hexstr_to_binbuff(res.txs_as_hex.front(), tx_data); + if (!ok) + { + error = tr("failed to parse transaction from daemon"); + return false; + } + crypto::hash tx_hash, tx_prefix_hash; + cryptonote::transaction tx; + if (!cryptonote::parse_and_validate_tx_from_blob(tx_data, tx, tx_hash, tx_prefix_hash)) + { + error = tr("failed to validate transaction from daemon"); + return false; + } + if (tx_hash != txid) + { + error = tr("failed to get the right transaction from daemon"); + return false; + } + + crypto::key_derivation derivation; + if (!crypto::generate_key_derivation(address.m_view_public_key, tx_key, derivation)) + { + error = tr("failed to generate key derivation from supplied parameters"); + return false; + } + + received = 0; + try { + for (size_t n = 0; n < tx.vout.size(); ++n) + { + if (typeid(cryptonote::txout_to_key) != tx.vout[n].target.type()) + continue; + const cryptonote::txout_to_key tx_out_to_key = boost::get<cryptonote::txout_to_key>(tx.vout[n].target); + crypto::public_key pubkey; + derive_public_key(derivation, n, address.m_spend_public_key, pubkey); + if (pubkey == tx_out_to_key.key) + { + uint64_t amount; + if (tx.version == 1) + { + amount = tx.vout[n].amount; + } + else + { + try + { + rct::key Ctmp; + //rct::key amount_key = rct::hash_to_scalar(rct::scalarmultKey(rct::pk2rct(address.m_view_public_key), rct::sk2rct(tx_key))); + crypto::key_derivation derivation; + bool r = crypto::generate_key_derivation(address.m_view_public_key, tx_key, derivation); + if (!r) + { + LOG_ERROR("Failed to generate key derivation to decode rct output " << n); + amount = 0; + } + else + { + crypto::secret_key scalar1; + crypto::derivation_to_scalar(derivation, n, scalar1); + rct::ecdhTuple ecdh_info = tx.rct_signatures.ecdhInfo[n]; + rct::ecdhDecode(ecdh_info, rct::sk2rct(scalar1)); + rct::key C = tx.rct_signatures.outPk[n].mask; + rct::addKeys2(Ctmp, ecdh_info.mask, ecdh_info.amount, rct::H); + if (rct::equalKeys(C, Ctmp)) + amount = rct::h2d(ecdh_info.amount); + else + amount = 0; + } + } + catch (...) { amount = 0; } + } + received += amount; + } + } + } + catch(const std::exception &e) + { + LOG_ERROR("error: " << e.what()); + error = std::string(tr("error: ")) + e.what(); + return false; + } + + if (received > 0) + { + LOG_PRINT_L1(get_account_address_as_str(testnet, address) << " " << tr("received") << " " << cryptonote::print_money(received) << " " << tr("in txid") << " " << txid); + } + else + { + LOG_PRINT_L1(get_account_address_as_str(testnet, address) << " " << tr("received nothing in txid") << " " << txid); + } + if (res.txs.front().in_pool) + { + height = 0; + } + else + { + height = res.txs.front().block_height; + } + + return true; +} + +uint64_t WalletManagerImpl::blockchainHeight() const +{ + cryptonote::COMMAND_RPC_GET_INFO::request ireq; + cryptonote::COMMAND_RPC_GET_INFO::response ires; + + if (!connect_and_invoke(m_daemonAddress, "/getinfo", ireq, ires)) + return 0; + return ires.height; +} + +uint64_t WalletManagerImpl::blockchainTargetHeight() const +{ + cryptonote::COMMAND_RPC_GET_INFO::request ireq; + cryptonote::COMMAND_RPC_GET_INFO::response ires; + + if (!connect_and_invoke(m_daemonAddress, "/getinfo", ireq, ires)) + return 0; + return ires.target_height >= ires.height ? ires.target_height : ires.height; +} + +uint64_t WalletManagerImpl::networkDifficulty() const +{ + cryptonote::COMMAND_RPC_GET_INFO::request ireq; + cryptonote::COMMAND_RPC_GET_INFO::response ires; + + if (!connect_and_invoke(m_daemonAddress, "/getinfo", ireq, ires)) + return 0; + return ires.difficulty; +} + +double WalletManagerImpl::miningHashRate() const +{ + cryptonote::COMMAND_RPC_MINING_STATUS::request mreq; + cryptonote::COMMAND_RPC_MINING_STATUS::response mres; + + epee::net_utils::http::http_simple_client http_client; + if (!connect_and_invoke(m_daemonAddress, "/mining_status", mreq, mres)) + return 0.0; + if (!mres.active) + return 0.0; + return mres.speed; +} + +uint64_t WalletManagerImpl::blockTarget() const +{ + cryptonote::COMMAND_RPC_GET_INFO::request ireq; + cryptonote::COMMAND_RPC_GET_INFO::response ires; + + if (!connect_and_invoke(m_daemonAddress, "/getinfo", ireq, ires)) + return 0; + return ires.target; +} + +bool WalletManagerImpl::isMining() const +{ + cryptonote::COMMAND_RPC_MINING_STATUS::request mreq; + cryptonote::COMMAND_RPC_MINING_STATUS::response mres; + + if (!connect_and_invoke(m_daemonAddress, "/mining_status", mreq, mres)) + return false; + return mres.active; +} + +bool WalletManagerImpl::startMining(const std::string &address, uint32_t threads, bool background_mining, bool ignore_battery) { + cryptonote::COMMAND_RPC_START_MINING::request mreq; + cryptonote::COMMAND_RPC_START_MINING::response mres; + + mreq.miner_address = address; + mreq.threads_count = threads; + mreq.ignore_battery = ignore_battery; + mreq.do_background_mining = background_mining; + + if (!connect_and_invoke(m_daemonAddress, "/start_mining", mreq, mres)) + return false; + return mres.status == CORE_RPC_STATUS_OK; +} + +bool WalletManagerImpl::stopMining() +{ + cryptonote::COMMAND_RPC_STOP_MINING::request mreq; + cryptonote::COMMAND_RPC_STOP_MINING::response mres; + + if (!connect_and_invoke(m_daemonAddress, "/stop_mining", mreq, mres)) + return false; + return mres.status == CORE_RPC_STATUS_OK; +} +std::string WalletManagerImpl::resolveOpenAlias(const std::string &address, bool &dnssec_valid) const +{ + std::vector<std::string> addresses = tools::dns_utils::addresses_from_url(address, dnssec_valid); + if (addresses.empty()) + return ""; + return addresses.front(); } +std::tuple<bool, std::string, std::string, std::string, std::string> WalletManager::checkUpdates(const std::string &software, const std::string &subdir) +{ +#ifdef BUILD_TAG + static const char buildtag[] = BOOST_PP_STRINGIZE(BUILD_TAG); +#else + static const char buildtag[] = "source"; +#endif + + std::string version, hash; + MDEBUG("Checking for a new " << software << " version for " << buildtag); + if (!tools::check_updates(software, buildtag, version, hash)) + return std::make_tuple(false, "", "", "", ""); + + if (tools::vercmp(version.c_str(), MONERO_VERSION) > 0) + { + std::string user_url = tools::get_update_url(software, subdir, buildtag, version, true); + std::string auto_url = tools::get_update_url(software, subdir, buildtag, version, false); + MGINFO("Version " << version << " of " << software << " for " << buildtag << " is available: " << user_url << ", SHA256 hash " << hash); + return std::make_tuple(true, version, hash, user_url, auto_url); + } + return std::make_tuple(false, "", "", "", ""); +} ///////////////////// WalletManagerFactory implementation ////////////////////// @@ -130,7 +461,6 @@ WalletManager *WalletManagerFactory::getWalletManager() static WalletManagerImpl * g_walletManager = nullptr; if (!g_walletManager) { - epee::log_space::log_singletone::add_logger(LOGGER_CONSOLE, NULL, NULL, LOG_LEVEL_MAX); g_walletManager = new WalletManagerImpl(); } @@ -139,9 +469,16 @@ WalletManager *WalletManagerFactory::getWalletManager() void WalletManagerFactory::setLogLevel(int level) { - epee::log_space::log_singletone::get_set_log_detalisation_level(true, level); + mlog_set_log_level(level); +} + +void WalletManagerFactory::setLogCategories(const std::string &categories) +{ + mlog_set_log(categories.c_str()); } } + +namespace Bitmonero = Monero; diff --git a/src/wallet/api/wallet_manager.h b/src/wallet/api/wallet_manager.h index 7585c1af7..033e8108f 100644 --- a/src/wallet/api/wallet_manager.h +++ b/src/wallet/api/wallet_manager.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -32,7 +32,7 @@ #include "wallet/wallet2_api.h" #include <string> -namespace Bitmonero { +namespace Monero { class WalletManagerImpl : public WalletManager { @@ -40,17 +40,38 @@ public: Wallet * createWallet(const std::string &path, const std::string &password, const std::string &language, bool testnet); Wallet * openWallet(const std::string &path, const std::string &password, bool testnet); - virtual Wallet * recoveryWallet(const std::string &path, const std::string &memo, bool testnet); + virtual Wallet * recoveryWallet(const std::string &path, const std::string &memo, bool testnet, uint64_t restoreHeight); + virtual Wallet * createWalletFromKeys(const std::string &path, + const std::string &language, + bool testnet, + uint64_t restoreHeight, + const std::string &addressString, + const std::string &viewKeyString, + const std::string &spendKeyString = ""); virtual bool closeWallet(Wallet *wallet); bool walletExists(const std::string &path); std::vector<std::string> findWallets(const std::string &path); std::string errorString() const; - void setDaemonHost(const std::string &hostname); + void setDaemonAddress(const std::string &address); + bool connected(uint32_t *version = NULL) const; + bool checkPayment(const std::string &address, const std::string &txid, const std::string &txkey, const std::string &daemon_address, uint64_t &received, uint64_t &height, std::string &error) const; + uint64_t blockchainHeight() const; + uint64_t blockchainTargetHeight() const; + uint64_t networkDifficulty() const; + double miningHashRate() const; + uint64_t blockTarget() const; + bool isMining() const; + bool startMining(const std::string &address, uint32_t threads = 1, bool background_mining = false, bool ignore_battery = true); + bool stopMining(); + std::string resolveOpenAlias(const std::string &address, bool &dnssec_valid) const; private: WalletManagerImpl() {} friend struct WalletManagerFactory; + std::string m_daemonAddress; std::string m_errorString; }; } // namespace + +namespace Bitmonero = Monero; diff --git a/src/wallet/node_rpc_proxy.cpp b/src/wallet/node_rpc_proxy.cpp new file mode 100644 index 000000000..03e1bbd98 --- /dev/null +++ b/src/wallet/node_rpc_proxy.cpp @@ -0,0 +1,169 @@ +// Copyright (c) 2017, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 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. + +#include "node_rpc_proxy.h" +#include "rpc/core_rpc_server_commands_defs.h" +#include "common/json_util.h" +#include "storages/http_abstract_invoke.h" + +using namespace epee; + +namespace tools +{ + +static const std::chrono::seconds rpc_timeout = std::chrono::minutes(3) + std::chrono::seconds(30); + +NodeRPCProxy::NodeRPCProxy(epee::net_utils::http::http_simple_client &http_client, boost::mutex &mutex) + : m_http_client(http_client) + , m_daemon_rpc_mutex(mutex) + , m_height(0) + , m_height_time(0) + , m_earliest_height() + , m_dynamic_per_kb_fee_estimate(0) + , m_dynamic_per_kb_fee_estimate_cached_height(0) + , m_dynamic_per_kb_fee_estimate_grace_blocks(0) + , m_rpc_version(0) +{} + +void NodeRPCProxy::invalidate() +{ + m_height = 0; + m_height_time = 0; + for (size_t n = 0; n < 256; ++n) + m_earliest_height[n] = 0; + m_dynamic_per_kb_fee_estimate = 0; + m_dynamic_per_kb_fee_estimate_cached_height = 0; + m_dynamic_per_kb_fee_estimate_grace_blocks = 0; + m_rpc_version = 0; +} + +boost::optional<std::string> NodeRPCProxy::get_rpc_version(uint32_t &rpc_version) +{ + const time_t now = time(NULL); + if (m_rpc_version == 0) + { + epee::json_rpc::request<cryptonote::COMMAND_RPC_GET_VERSION::request> req_t = AUTO_VAL_INIT(req_t); + epee::json_rpc::response<cryptonote::COMMAND_RPC_GET_VERSION::response, std::string> resp_t = AUTO_VAL_INIT(resp_t); + req_t.jsonrpc = "2.0"; + req_t.id = epee::serialization::storage_entry(0); + req_t.method = "get_version"; + m_daemon_rpc_mutex.lock(); + bool r = net_utils::invoke_http_json("/json_rpc", req_t, resp_t, m_http_client, rpc_timeout); + m_daemon_rpc_mutex.unlock(); + CHECK_AND_ASSERT_MES(r, std::string(), "Failed to connect to daemon"); + CHECK_AND_ASSERT_MES(resp_t.result.status != CORE_RPC_STATUS_BUSY, resp_t.result.status, "Failed to connect to daemon"); + CHECK_AND_ASSERT_MES(resp_t.result.status == CORE_RPC_STATUS_OK, resp_t.result.status, "Failed to get daemon RPC version"); + m_rpc_version = resp_t.result.version; + } + rpc_version = m_rpc_version; + return boost::optional<std::string>(); +} + +boost::optional<std::string> NodeRPCProxy::get_height(uint64_t &height) +{ + const time_t now = time(NULL); + if (m_height == 0 || now >= m_height_time + 30) // re-cache every 30 seconds + { + cryptonote::COMMAND_RPC_GET_HEIGHT::request req = AUTO_VAL_INIT(req); + cryptonote::COMMAND_RPC_GET_HEIGHT::response res = AUTO_VAL_INIT(res); + + m_daemon_rpc_mutex.lock(); + bool r = net_utils::invoke_http_json("/getheight", req, res, m_http_client, rpc_timeout); + m_daemon_rpc_mutex.unlock(); + CHECK_AND_ASSERT_MES(r, std::string(), "Failed to connect to daemon"); + CHECK_AND_ASSERT_MES(res.status != CORE_RPC_STATUS_BUSY, res.status, "Failed to connect to daemon"); + CHECK_AND_ASSERT_MES(res.status == CORE_RPC_STATUS_OK, res.status, "Failed to get current blockchain height"); + m_height = res.height; + m_height_time = now; + } + height = m_height; + return boost::optional<std::string>(); +} + +void NodeRPCProxy::set_height(uint64_t h) +{ + m_height = h; +} + +boost::optional<std::string> NodeRPCProxy::get_earliest_height(uint8_t version, uint64_t &earliest_height) +{ + if (m_earliest_height[version] == 0) + { + epee::json_rpc::request<cryptonote::COMMAND_RPC_HARD_FORK_INFO::request> req_t = AUTO_VAL_INIT(req_t); + epee::json_rpc::response<cryptonote::COMMAND_RPC_HARD_FORK_INFO::response, std::string> resp_t = AUTO_VAL_INIT(resp_t); + + m_daemon_rpc_mutex.lock(); + req_t.jsonrpc = "2.0"; + req_t.id = epee::serialization::storage_entry(0); + req_t.method = "hard_fork_info"; + req_t.params.version = version; + bool r = net_utils::invoke_http_json("/json_rpc", req_t, resp_t, m_http_client, rpc_timeout); + m_daemon_rpc_mutex.unlock(); + CHECK_AND_ASSERT_MES(r, std::string(), "Failed to connect to daemon"); + CHECK_AND_ASSERT_MES(resp_t.result.status != CORE_RPC_STATUS_BUSY, resp_t.result.status, "Failed to connect to daemon"); + CHECK_AND_ASSERT_MES(resp_t.result.status == CORE_RPC_STATUS_OK, resp_t.result.status, "Failed to get hard fork status"); + m_earliest_height[version] = resp_t.result.earliest_height; + } + + earliest_height = m_earliest_height[version]; + return boost::optional<std::string>(); +} + +boost::optional<std::string> NodeRPCProxy::get_dynamic_per_kb_fee_estimate(uint64_t grace_blocks, uint64_t &fee) +{ + uint64_t height; + + boost::optional<std::string> result = get_height(height); + if (result) + return result; + + if (m_dynamic_per_kb_fee_estimate_cached_height != height || m_dynamic_per_kb_fee_estimate_grace_blocks != grace_blocks) + { + epee::json_rpc::request<cryptonote::COMMAND_RPC_GET_PER_KB_FEE_ESTIMATE::request> req_t = AUTO_VAL_INIT(req_t); + epee::json_rpc::response<cryptonote::COMMAND_RPC_GET_PER_KB_FEE_ESTIMATE::response, std::string> resp_t = AUTO_VAL_INIT(resp_t); + + m_daemon_rpc_mutex.lock(); + req_t.jsonrpc = "2.0"; + req_t.id = epee::serialization::storage_entry(0); + req_t.method = "get_fee_estimate"; + req_t.params.grace_blocks = grace_blocks; + bool r = net_utils::invoke_http_json("/json_rpc", req_t, resp_t, m_http_client, rpc_timeout); + m_daemon_rpc_mutex.unlock(); + CHECK_AND_ASSERT_MES(r, std::string(), "Failed to connect to daemon"); + CHECK_AND_ASSERT_MES(resp_t.result.status != CORE_RPC_STATUS_BUSY, resp_t.result.status, "Failed to connect to daemon"); + CHECK_AND_ASSERT_MES(resp_t.result.status == CORE_RPC_STATUS_OK, resp_t.result.status, "Failed to get fee estimate"); + m_dynamic_per_kb_fee_estimate = resp_t.result.fee; + m_dynamic_per_kb_fee_estimate_cached_height = height; + m_dynamic_per_kb_fee_estimate_grace_blocks = grace_blocks; + } + + fee = m_dynamic_per_kb_fee_estimate; + return boost::optional<std::string>(); +} + +} diff --git a/src/wallet/node_rpc_proxy.h b/src/wallet/node_rpc_proxy.h new file mode 100644 index 000000000..02d1d8d93 --- /dev/null +++ b/src/wallet/node_rpc_proxy.h @@ -0,0 +1,65 @@ +// Copyright (c) 2017, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 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. + +#pragma once + +#include <string> +#include <boost/thread/mutex.hpp> +#include "include_base_utils.h" +#include "net/http_client.h" + +namespace tools +{ + +class NodeRPCProxy +{ +public: + NodeRPCProxy(epee::net_utils::http::http_simple_client &http_client, boost::mutex &mutex); + + void invalidate(); + + boost::optional<std::string> get_rpc_version(uint32_t &version); + boost::optional<std::string> get_height(uint64_t &height); + void set_height(uint64_t h); + boost::optional<std::string> get_earliest_height(uint8_t version, uint64_t &earliest_height); + boost::optional<std::string> get_dynamic_per_kb_fee_estimate(uint64_t grace_blocks, uint64_t &fee); + +private: + epee::net_utils::http::http_simple_client &m_http_client; + boost::mutex &m_daemon_rpc_mutex; + + uint64_t m_height; + time_t m_height_time; + uint64_t m_earliest_height[256]; + uint64_t m_dynamic_per_kb_fee_estimate; + uint64_t m_dynamic_per_kb_fee_estimate_cached_height; + uint64_t m_dynamic_per_kb_fee_estimate_grace_blocks; + uint32_t m_rpc_version; +}; + +} diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index ed4ab93de..9069789ca 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -30,32 +30,34 @@ #include <random> #include <tuple> -#include <boost/archive/binary_oarchive.hpp> -#include <boost/archive/binary_iarchive.hpp> - +#include <boost/format.hpp> +#include <boost/optional/optional.hpp> #include <boost/utility/value_init.hpp> #include "include_base_utils.h" using namespace epee; #include "cryptonote_config.h" #include "wallet2.h" -#include "cryptonote_core/cryptonote_format_utils.h" +#include "wallet2_api.h" +#include "cryptonote_basic/cryptonote_format_utils.h" #include "rpc/core_rpc_server_commands_defs.h" #include "misc_language.h" -#include "cryptonote_core/cryptonote_basic_impl.h" +#include "cryptonote_basic/cryptonote_basic_impl.h" #include "common/boost_serialization_helper.h" +#include "common/command_line.h" #include "profile_tools.h" #include "crypto/crypto.h" #include "serialization/binary_utils.h" #include "cryptonote_protocol/blobdatatype.h" #include "mnemonics/electrum-words.h" -#include "common/dns_utils.h" +#include "common/i18n.h" #include "common/util.h" #include "rapidjson/document.h" #include "rapidjson/writer.h" #include "rapidjson/stringbuffer.h" #include "common/json_util.h" #include "common/base58.h" +#include "common/scoped_message_writer.h" #include "ringct/rctSigs.h" extern "C" @@ -65,6 +67,9 @@ extern "C" } using namespace cryptonote; +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "wallet.wallet2" + // used to choose when to stop adding outputs to a tx #define APPROXIMATE_INPUT_BYTES 80 @@ -74,6 +79,16 @@ using namespace cryptonote; // arbitrary, used to generate different hashes from the same input #define CHACHA8_KEY_TAIL 0x8c +#define UNSIGNED_TX_PREFIX "Monero unsigned tx set\003" +#define SIGNED_TX_PREFIX "Monero signed tx set\003" + +#define RECENT_OUTPUT_RATIO (0.5) // 50% of outputs are from the recent zone +#define RECENT_OUTPUT_ZONE ((time_t)(1.8 * 86400)) // last 1.8 day makes up the recent zone (taken from monerolink.pdf, Miller et al) + +#define FEE_ESTIMATE_GRACE_BLOCKS 10 // estimate fee valid for that many blocks + +#define SECOND_OUTPUT_RELATEDNESS_THRESHOLD 0.0f + #define KILL_IOSERVICE() \ do { \ work.reset(); \ @@ -82,8 +97,22 @@ using namespace cryptonote; ioservice.stop(); \ } while(0) +#define KEY_IMAGE_EXPORT_FILE_MAGIC "Monero key image export\002" + namespace { +// Create on-demand to prevent static initialization order fiasco issues. +struct options { + const command_line::arg_descriptor<std::string> daemon_address = {"daemon-address", tools::wallet2::tr("Use daemon instance at <host>:<port>"), ""}; + const command_line::arg_descriptor<std::string> daemon_host = {"daemon-host", tools::wallet2::tr("Use daemon instance at host <arg> instead of localhost"), ""}; + const command_line::arg_descriptor<std::string> password = {"password", tools::wallet2::tr("Wallet password (escape/quote as needed)"), "", true}; + const command_line::arg_descriptor<std::string> password_file = {"password-file", tools::wallet2::tr("Wallet password file"), "", true}; + const command_line::arg_descriptor<int> daemon_port = {"daemon-port", tools::wallet2::tr("Use daemon instance at port <arg> instead of 18081"), 0}; + const command_line::arg_descriptor<std::string> daemon_login = {"daemon-login", tools::wallet2::tr("Specify username[:password] for daemon RPC client"), "", true}; + const command_line::arg_descriptor<bool> testnet = {"testnet", tools::wallet2::tr("For testnet. Daemon must also be launched with --testnet flag"), false}; + const command_line::arg_descriptor<bool> restricted = {"restricted-rpc", tools::wallet2::tr("Restricts to view-only commands"), false}; +}; + void do_prepare_file_names(const std::string& file_path, std::string& keys_file, std::string& wallet_file) { keys_file = file_path; @@ -109,6 +138,294 @@ uint64_t calculate_fee(uint64_t fee_per_kb, const cryptonote::blobdata &blob, ui return calculate_fee(fee_per_kb, blob.size(), fee_multiplier); } +std::unique_ptr<tools::wallet2> make_basic(const boost::program_options::variables_map& vm, const options& opts) +{ + const bool testnet = command_line::get_arg(vm, opts.testnet); + const bool restricted = command_line::get_arg(vm, opts.restricted); + + auto daemon_address = command_line::get_arg(vm, opts.daemon_address); + auto daemon_host = command_line::get_arg(vm, opts.daemon_host); + auto daemon_port = command_line::get_arg(vm, opts.daemon_port); + + if (!daemon_address.empty() && !daemon_host.empty() && 0 != daemon_port) + { + tools::fail_msg_writer() << tools::wallet2::tr("can't specify daemon host or port more than once"); + return nullptr; + } + + boost::optional<epee::net_utils::http::login> login{}; + if (command_line::has_arg(vm, opts.daemon_login)) + { + auto parsed = tools::login::parse( + command_line::get_arg(vm, opts.daemon_login), false, "Daemon client password" + ); + if (!parsed) + return nullptr; + + login.emplace(std::move(parsed->username), std::move(parsed->password).password()); + } + + if (daemon_host.empty()) + daemon_host = "localhost"; + + if (!daemon_port) + { + daemon_port = testnet ? config::testnet::RPC_DEFAULT_PORT : config::RPC_DEFAULT_PORT; + } + + if (daemon_address.empty()) + daemon_address = std::string("http://") + daemon_host + ":" + std::to_string(daemon_port); + + std::unique_ptr<tools::wallet2> wallet(new tools::wallet2(testnet, restricted)); + wallet->init(std::move(daemon_address), std::move(login)); + return wallet; +} + +boost::optional<tools::password_container> get_password(const boost::program_options::variables_map& vm, const options& opts, const bool verify) +{ + if (command_line::has_arg(vm, opts.password) && command_line::has_arg(vm, opts.password_file)) + { + tools::fail_msg_writer() << tools::wallet2::tr("can't specify more than one of --password and --password-file"); + return boost::none; + } + + if (command_line::has_arg(vm, opts.password)) + { + return tools::password_container{command_line::get_arg(vm, opts.password)}; + } + + if (command_line::has_arg(vm, opts.password_file)) + { + std::string password; + bool r = epee::file_io_utils::load_file_to_string(command_line::get_arg(vm, opts.password_file), + password); + if (!r) + { + tools::fail_msg_writer() << tools::wallet2::tr("the password file specified could not be read"); + return boost::none; + } + + // Remove line breaks the user might have inserted + boost::trim_right_if(password, boost::is_any_of("\r\n")); + return {tools::password_container{std::move(password)}}; + } + + return tools::wallet2::password_prompt(verify); +} + +std::unique_ptr<tools::wallet2> generate_from_json(const std::string& json_file, const boost::program_options::variables_map& vm, const options& opts) +{ + const bool testnet = command_line::get_arg(vm, opts.testnet); + + /* GET_FIELD_FROM_JSON_RETURN_ON_ERROR Is a generic macro that can return + false. Gcc will coerce this into unique_ptr(nullptr), but clang correctly + fails. This large wrapper is for the use of that macro */ + std::unique_ptr<tools::wallet2> wallet; + const auto do_generate = [&]() -> bool { + std::string buf; + if (!epee::file_io_utils::load_file_to_string(json_file, buf)) { + tools::fail_msg_writer() << tools::wallet2::tr("Failed to load file ") << json_file; + return false; + } + + rapidjson::Document json; + if (json.Parse(buf.c_str()).HasParseError()) { + tools::fail_msg_writer() << tools::wallet2::tr("Failed to parse JSON"); + return false; + } + + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, version, unsigned, Uint, true, 0); + const int current_version = 1; + if (field_version > current_version) { + tools::fail_msg_writer() << boost::format(tools::wallet2::tr("Version %u too new, we can only grok up to %u")) % field_version % current_version; + return false; + } + + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, filename, std::string, String, true, std::string()); + + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, scan_from_height, uint64_t, Uint64, false, 0); + const bool recover = field_scan_from_height_found; + + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, password, std::string, String, false, std::string()); + + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, viewkey, std::string, String, false, std::string()); + crypto::secret_key viewkey; + if (field_viewkey_found) + { + cryptonote::blobdata viewkey_data; + if(!epee::string_tools::parse_hexstr_to_binbuff(field_viewkey, viewkey_data) || viewkey_data.size() != sizeof(crypto::secret_key)) + { + tools::fail_msg_writer() << tools::wallet2::tr("failed to parse view key secret key"); + return false; + } + viewkey = *reinterpret_cast<const crypto::secret_key*>(viewkey_data.data()); + crypto::public_key pkey; + if (!crypto::secret_key_to_public_key(viewkey, pkey)) { + tools::fail_msg_writer() << tools::wallet2::tr("failed to verify view key secret key"); + return false; + } + } + + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, spendkey, std::string, String, false, std::string()); + crypto::secret_key spendkey; + if (field_spendkey_found) + { + cryptonote::blobdata spendkey_data; + if(!epee::string_tools::parse_hexstr_to_binbuff(field_spendkey, spendkey_data) || spendkey_data.size() != sizeof(crypto::secret_key)) + { + tools::fail_msg_writer() << tools::wallet2::tr("failed to parse spend key secret key"); + return false; + } + spendkey = *reinterpret_cast<const crypto::secret_key*>(spendkey_data.data()); + crypto::public_key pkey; + if (!crypto::secret_key_to_public_key(spendkey, pkey)) { + tools::fail_msg_writer() << tools::wallet2::tr("failed to verify spend key secret key"); + return false; + } + } + + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, seed, std::string, String, false, std::string()); + std::string old_language; + crypto::secret_key recovery_key; + bool restore_deterministic_wallet = false; + if (field_seed_found) + { + if (!crypto::ElectrumWords::words_to_bytes(field_seed, recovery_key, old_language)) + { + tools::fail_msg_writer() << tools::wallet2::tr("Electrum-style word list failed verification"); + return false; + } + restore_deterministic_wallet = true; + } + + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, address, std::string, String, false, std::string()); + + // compatibility checks + if (!field_seed_found && !field_viewkey_found) + { + tools::fail_msg_writer() << tools::wallet2::tr("At least one of Electrum-style word list and private view key must be specified"); + return false; + } + if (field_seed_found && (field_viewkey_found || field_spendkey_found)) + { + tools::fail_msg_writer() << tools::wallet2::tr("Both Electrum-style word list and private key(s) specified"); + return false; + } + + // if an address was given, we check keys against it, and deduce the spend + // public key if it was not given + if (field_address_found) + { + cryptonote::account_public_address address; + bool has_payment_id; + crypto::hash8 new_payment_id; + if(!get_account_integrated_address_from_str(address, has_payment_id, new_payment_id, testnet, field_address)) + { + tools::fail_msg_writer() << tools::wallet2::tr("invalid address"); + return false; + } + if (field_viewkey_found) + { + crypto::public_key pkey; + if (!crypto::secret_key_to_public_key(viewkey, pkey)) { + tools::fail_msg_writer() << tools::wallet2::tr("failed to verify view key secret key"); + return false; + } + if (address.m_view_public_key != pkey) { + tools::fail_msg_writer() << tools::wallet2::tr("view key does not match standard address"); + return false; + } + } + if (field_spendkey_found) + { + crypto::public_key pkey; + if (!crypto::secret_key_to_public_key(spendkey, pkey)) { + tools::fail_msg_writer() << tools::wallet2::tr("failed to verify spend key secret key"); + return false; + } + if (address.m_spend_public_key != pkey) { + tools::fail_msg_writer() << tools::wallet2::tr("spend key does not match standard address"); + return false; + } + } + } + + const bool deprecated_wallet = restore_deterministic_wallet && ((old_language == crypto::ElectrumWords::old_language_name) || + crypto::ElectrumWords::get_is_old_style_seed(field_seed)); + if (deprecated_wallet) { + tools::fail_msg_writer() << tools::wallet2::tr("Cannot create deprecated wallets from JSON"); + return false; + } + + wallet.reset(make_basic(vm, opts).release()); + wallet->set_refresh_from_block_height(field_scan_from_height); + + try + { + if (!field_seed.empty()) + { + wallet->generate(field_filename, field_password, recovery_key, recover, false); + } + else + { + cryptonote::account_public_address address; + if (!crypto::secret_key_to_public_key(viewkey, address.m_view_public_key)) { + tools::fail_msg_writer() << tools::wallet2::tr("failed to verify view key secret key"); + return false; + } + + if (field_spendkey.empty()) + { + // if we have an addres but no spend key, we can deduce the spend public key + // from the address + if (field_address_found) + { + cryptonote::account_public_address address2; + bool has_payment_id; + crypto::hash8 new_payment_id; + get_account_integrated_address_from_str(address2, has_payment_id, new_payment_id, testnet, field_address); + address.m_spend_public_key = address2.m_spend_public_key; + } + wallet->generate(field_filename, field_password, address, viewkey); + } + else + { + if (!crypto::secret_key_to_public_key(spendkey, address.m_spend_public_key)) { + tools::fail_msg_writer() << tools::wallet2::tr("failed to verify spend key secret key"); + return false; + } + wallet->generate(field_filename, field_password, address, spendkey, viewkey); + } + } + } + catch (const std::exception& e) + { + tools::fail_msg_writer() << tools::wallet2::tr("failed to generate new wallet: ") << e.what(); + return false; + } + return true; + }; + + if (do_generate()) + { + return wallet; + } + return nullptr; +} + +static void throw_on_rpc_response_error(const boost::optional<std::string> &status, const char *method) +{ + // no error + if (!status) + return; + + // empty string -> not connection + THROW_WALLET_EXCEPTION_IF(status->empty(), tools::error::no_connection_to_daemon, method); + + THROW_WALLET_EXCEPTION_IF(*status == CORE_RPC_STATUS_BUSY, tools::error::daemon_busy, method); + THROW_WALLET_EXCEPTION_IF(*status != CORE_RPC_STATUS_OK, tools::error::wallet_generic_rpc_error, method, *status); +} + } //namespace namespace tools @@ -116,11 +433,88 @@ namespace tools // for now, limit to 30 attempts. TODO: discuss a good number to limit to. const size_t MAX_SPLIT_ATTEMPTS = 30; +constexpr const std::chrono::seconds wallet2::rpc_timeout; +const char* wallet2::tr(const char* str) { return i18n_translate(str, "tools::wallet2"); } + +bool wallet2::has_testnet_option(const boost::program_options::variables_map& vm) +{ + return command_line::get_arg(vm, options().testnet); +} + +void wallet2::init_options(boost::program_options::options_description& desc_params) +{ + const options opts{}; + command_line::add_arg(desc_params, opts.daemon_address); + command_line::add_arg(desc_params, opts.daemon_host); + command_line::add_arg(desc_params, opts.password); + command_line::add_arg(desc_params, opts.password_file); + command_line::add_arg(desc_params, opts.daemon_port); + command_line::add_arg(desc_params, opts.daemon_login); + command_line::add_arg(desc_params, opts.testnet); + command_line::add_arg(desc_params, opts.restricted); +} + +boost::optional<password_container> wallet2::password_prompt(const bool new_password) +{ + auto pwd_container = tools::password_container::prompt( + new_password, (new_password ? tr("Enter new wallet password") : tr("Wallet password")) + ); + if (!pwd_container) + { + tools::fail_msg_writer() << tr("failed to read wallet password"); + } + return pwd_container; +} + +std::unique_ptr<wallet2> wallet2::make_from_json(const boost::program_options::variables_map& vm, const std::string& json_file) +{ + const options opts{}; + return generate_from_json(json_file, vm, opts); +} + +std::pair<std::unique_ptr<wallet2>, password_container> wallet2::make_from_file( + const boost::program_options::variables_map& vm, const std::string& wallet_file) +{ + const options opts{}; + auto pwd = get_password(vm, opts, false); + if (!pwd) + { + return {nullptr, password_container{}}; + } + auto wallet = make_basic(vm, opts); + if (wallet) + { + wallet->load(wallet_file, pwd->password()); + } + return {std::move(wallet), std::move(*pwd)}; +} + +std::pair<std::unique_ptr<wallet2>, password_container> wallet2::make_new(const boost::program_options::variables_map& vm) +{ + const options opts{}; + auto pwd = get_password(vm, opts, true); + if (!pwd) + { + return {nullptr, password_container{}}; + } + return {make_basic(vm, opts), std::move(*pwd)}; +} + +std::unique_ptr<wallet2> wallet2::make_dummy(const boost::program_options::variables_map& vm) +{ + const options opts{}; + return make_basic(vm, opts); +} + //---------------------------------------------------------------------------------------------------- -void wallet2::init(const std::string& daemon_address, 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) { + if(m_http_client.is_connected()) + m_http_client.disconnect(); m_upper_transaction_size_limit = upper_transaction_size_limit; - m_daemon_address = daemon_address; + 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()); } //---------------------------------------------------------------------------------------------------- bool wallet2::is_deterministic() const @@ -173,21 +567,23 @@ bool wallet2::is_deprecated() const return is_old_file_format; } //---------------------------------------------------------------------------------------------------- -void wallet2::set_spent(transfer_details &td, uint64_t height) +void wallet2::set_spent(size_t idx, uint64_t height) { + transfer_details &td = m_transfers[idx]; LOG_PRINT_L2("Setting SPENT at " << height << ": ki " << td.m_key_image << ", amount " << print_money(td.m_amount)); td.m_spent = true; td.m_spent_height = height; } //---------------------------------------------------------------------------------------------------- -void wallet2::set_unspent(transfer_details &td) +void wallet2::set_unspent(size_t idx) { + transfer_details &td = m_transfers[idx]; LOG_PRINT_L2("Setting UNSPENT: ki " << td.m_key_image << ", amount " << print_money(td.m_amount)); td.m_spent = false; td.m_spent_height = 0; } //---------------------------------------------------------------------------------------------------- -void wallet2::check_acc_out(const account_keys &acc, const tx_out &o, const crypto::public_key &tx_pub_key, size_t i, bool &received, uint64_t &money_transfered, bool &error) const +void wallet2::check_acc_out_precomp(const crypto::public_key &spend_public_key, const tx_out &o, const crypto::key_derivation &derivation, size_t i, bool &received, uint64_t &money_transfered, bool &error) const { if (o.target.type() != typeid(txout_to_key)) { @@ -195,7 +591,7 @@ void wallet2::check_acc_out(const account_keys &acc, const tx_out &o, const cryp LOG_ERROR("wrong type id in transaction out"); return; } - received = is_out_to_acc(acc, boost::get<txout_to_key>(o.target), tx_pub_key, i); + received = is_out_to_acc_precomp(spend_public_key, boost::get<txout_to_key>(o.target), derivation, i); if(received) { money_transfered = o.amount; // may be 0 for ringct outputs @@ -207,7 +603,7 @@ void wallet2::check_acc_out(const account_keys &acc, const tx_out &o, const cryp error = false; } //---------------------------------------------------------------------------------------------------- -static uint64_t decodeRct(const rct::rctSig & rv, const crypto::public_key pub, const crypto::secret_key &sec, unsigned int i, rct::key & mask) +static uint64_t decodeRct(const rct::rctSig & rv, const crypto::public_key &pub, const crypto::secret_key &sec, unsigned int i, rct::key & mask) { crypto::key_derivation derivation; bool r = crypto::generate_key_derivation(pub, sec, derivation); @@ -238,28 +634,20 @@ static uint64_t decodeRct(const rct::rctSig & rv, const crypto::public_key pub, } } //---------------------------------------------------------------------------------------------------- -void wallet2::process_new_transaction(const cryptonote::transaction& tx, const std::vector<uint64_t> &o_indices, uint64_t height, uint64_t ts, bool miner_tx, bool pool) +bool wallet2::wallet_generate_key_image_helper(const cryptonote::account_keys& ack, const crypto::public_key& tx_public_key, size_t real_output_index, cryptonote::keypair& in_ephemeral, crypto::key_image& ki) { - class lazy_txid_getter - { - const cryptonote::transaction &tx; - crypto::hash lazy_txid; - bool computed; - public: - lazy_txid_getter(const transaction &tx): tx(tx), computed(false) {} - const crypto::hash &operator()() - { - if (!computed) - { - lazy_txid = cryptonote::get_transaction_hash(tx); - computed = true; - } - return lazy_txid; - } - } txid(tx); + if (!cryptonote::generate_key_image_helper(ack, tx_public_key, real_output_index, in_ephemeral, ki)) + return false; + return true; +} +//---------------------------------------------------------------------------------------------------- +void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote::transaction& tx, const std::vector<uint64_t> &o_indices, uint64_t height, uint64_t ts, bool miner_tx, bool pool) +{ + // In this function, tx (probably) only contains the base information + // (that is, the prunable stuff may or may not be included) if (!miner_tx) - process_unconfirmed(tx, height); + process_unconfirmed(txid, tx, height); std::vector<size_t> outs; uint64_t tx_money_got_in_outs = 0; crypto::public_key tx_pub_key = null_pkey; @@ -268,18 +656,23 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s if(!parse_tx_extra(tx.extra, tx_extra_fields)) { // Extra may only be partially parsed, it's OK if tx_extra_fields contains public key - LOG_PRINT_L0("Transaction extra has unsupported format: " << txid()); + LOG_PRINT_L0("Transaction extra has unsupported format: " << txid); } // Don't try to extract tx public key if tx has no ouputs - if (!tx.vout.empty()) + size_t pk_index = 0; + while (!tx.vout.empty()) { + // if tx.vout is not empty, we loop through all tx pubkeys + tx_extra_pub_key pub_key_field; - if(!find_tx_extra_field_by_type(tx_extra_fields, pub_key_field)) + if(!find_tx_extra_field_by_type(tx_extra_fields, pub_key_field, pk_index++)) { - LOG_PRINT_L0("Public key wasn't found in the transaction extra. Skipping transaction " << txid()); + if (pk_index > 1) + break; + LOG_PRINT_L0("Public key wasn't found in the transaction extra. Skipping transaction " << txid); if(0 != m_callback) - m_callback->on_skip_transaction(height, tx); + m_callback->on_skip_transaction(height, txid, tx); return; } @@ -291,6 +684,9 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s std::deque<uint64_t> amount(tx.vout.size()); std::deque<rct::key> mask(tx.vout.size()); int threads = tools::get_max_concurrency(); + const cryptonote::account_keys& keys = m_account.get_keys(); + crypto::key_derivation derivation; + generate_key_derivation(tx_pub_key, keys.m_view_secret_key, derivation); if (miner_tx && m_refresh_type == RefreshNoCoinbase) { // assume coinbase isn't for us @@ -299,7 +695,7 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s { uint64_t money_transfered = 0; bool error = false, received = false; - check_acc_out(m_account.get_keys(), tx.vout[0], tx_pub_key, 0, received, money_transfered, error); + check_acc_out_precomp(keys.m_account_address.m_spend_public_key, tx.vout[0], derivation, 0, received, money_transfered, error); if (error) { r = false; @@ -309,14 +705,13 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s // this assumes that the miner tx pays a single address if (received) { - cryptonote::generate_key_image_helper(m_account.get_keys(), tx_pub_key, 0, in_ephemeral[0], ki[0]); + wallet_generate_key_image_helper(keys, tx_pub_key, 0, in_ephemeral[0], ki[0]); THROW_WALLET_EXCEPTION_IF(in_ephemeral[0].pub != boost::get<cryptonote::txout_to_key>(tx.vout[0].target).key, error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key"); outs.push_back(0); if (money_transfered == 0) { - const cryptonote::account_keys& keys = m_account.get_keys(); money_transfered = tools::decodeRct(tx.rct_signatures, pub_key_field.pub_key, keys.m_view_secret_key, 0, mask[0]); } amount[0] = money_transfered; @@ -332,14 +727,13 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s threadpool.create_thread(boost::bind(&boost::asio::io_service::run, &ioservice)); } - const account_keys &keys = m_account.get_keys(); std::vector<uint64_t> money_transfered(tx.vout.size()); std::deque<bool> error(tx.vout.size()); std::deque<bool> received(tx.vout.size()); // the first one was already checked for (size_t i = 1; i < tx.vout.size(); ++i) { - ioservice.dispatch(boost::bind(&wallet2::check_acc_out, this, std::cref(keys), std::cref(tx.vout[i]), std::cref(tx_pub_key), i, + ioservice.dispatch(boost::bind(&wallet2::check_acc_out_precomp, this, std::cref(keys.m_account_address.m_spend_public_key), std::cref(tx.vout[i]), std::cref(derivation), i, std::ref(received[i]), std::ref(money_transfered[i]), std::ref(error[i]))); } KILL_IOSERVICE(); @@ -352,14 +746,13 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s } if (received[i]) { - cryptonote::generate_key_image_helper(m_account.get_keys(), tx_pub_key, i, in_ephemeral[i], ki[i]); + wallet_generate_key_image_helper(keys, tx_pub_key, i, in_ephemeral[i], ki[i]); THROW_WALLET_EXCEPTION_IF(in_ephemeral[i].pub != boost::get<cryptonote::txout_to_key>(tx.vout[i].target).key, error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key"); outs.push_back(i); if (money_transfered[i] == 0) { - const cryptonote::account_keys& keys = m_account.get_keys(); money_transfered[i] = tools::decodeRct(tx.rct_signatures, pub_key_field.pub_key, keys.m_view_secret_key, i, mask[i]); } tx_money_got_in_outs += money_transfered[i]; @@ -380,13 +773,12 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s threadpool.create_thread(boost::bind(&boost::asio::io_service::run, &ioservice)); } - const account_keys &keys = m_account.get_keys(); std::vector<uint64_t> money_transfered(tx.vout.size()); std::deque<bool> error(tx.vout.size()); std::deque<bool> received(tx.vout.size()); for (size_t i = 0; i < tx.vout.size(); ++i) { - ioservice.dispatch(boost::bind(&wallet2::check_acc_out, this, std::cref(keys), std::cref(tx.vout[i]), std::cref(tx_pub_key), i, + ioservice.dispatch(boost::bind(&wallet2::check_acc_out_precomp, this, std::cref(keys.m_account_address.m_spend_public_key), std::cref(tx.vout[i]), std::cref(derivation), i, std::ref(received[i]), std::ref(money_transfered[i]), std::ref(error[i]))); } KILL_IOSERVICE(); @@ -400,14 +792,13 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s } if (received[i]) { - cryptonote::generate_key_image_helper(m_account.get_keys(), tx_pub_key, i, in_ephemeral[i], ki[i]); + wallet_generate_key_image_helper(keys, tx_pub_key, i, in_ephemeral[i], ki[i]); THROW_WALLET_EXCEPTION_IF(in_ephemeral[i].pub != boost::get<cryptonote::txout_to_key>(tx.vout[i].target).key, error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key"); outs.push_back(i); if (money_transfered[i] == 0) { - const cryptonote::account_keys& keys = m_account.get_keys(); money_transfered[i] = tools::decodeRct(tx.rct_signatures, pub_key_field.pub_key, keys.m_view_secret_key, i, mask[i]); } tx_money_got_in_outs += money_transfered[i]; @@ -422,7 +813,7 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s { uint64_t money_transfered = 0; bool error = false, received = false; - check_acc_out(m_account.get_keys(), tx.vout[i], tx_pub_key, i, received, money_transfered, error); + check_acc_out_precomp(keys.m_account_address.m_spend_public_key, tx.vout[i], derivation, i, received, money_transfered, error); if (error) { r = false; @@ -432,14 +823,13 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s { if (received) { - cryptonote::generate_key_image_helper(m_account.get_keys(), tx_pub_key, i, in_ephemeral[i], ki[i]); + wallet_generate_key_image_helper(keys, tx_pub_key, i, in_ephemeral[i], ki[i]); THROW_WALLET_EXCEPTION_IF(in_ephemeral[i].pub != boost::get<cryptonote::txout_to_key>(tx.vout[i].target).key, error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key"); outs.push_back(i); if (money_transfered == 0) { - const cryptonote::account_keys& keys = m_account.get_keys(); money_transfered = tools::decodeRct(tx.rct_signatures, pub_key_field.pub_key, keys.m_view_secret_key, i, mask[i]); } amount[i] = money_transfered; @@ -462,17 +852,17 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s " not match with daemon response size=" + std::to_string(o_indices.size())); } - BOOST_FOREACH(size_t o, outs) + for(size_t o: outs) { THROW_WALLET_EXCEPTION_IF(tx.vout.size() <= o, error::wallet_internal_error, "wrong out in transaction: internal index=" + std::to_string(o) + ", total_outs=" + std::to_string(tx.vout.size())); - auto kit = m_key_images.find(ki[o]); - THROW_WALLET_EXCEPTION_IF(kit != m_key_images.end() && kit->second >= m_transfers.size(), - error::wallet_internal_error, std::string("Unexpected transfer index from key image: ") - + "got " + (kit == m_key_images.end() ? "<none>" : boost::lexical_cast<std::string>(kit->second)) + auto kit = m_pub_keys.find(in_ephemeral[o].pub); + THROW_WALLET_EXCEPTION_IF(kit != m_pub_keys.end() && kit->second >= m_transfers.size(), + error::wallet_internal_error, std::string("Unexpected transfer index from public key: ") + + "got " + (kit == m_pub_keys.end() ? "<none>" : boost::lexical_cast<std::string>(kit->second)) + ", m_transfers.size() is " + boost::lexical_cast<std::string>(m_transfers.size())); - if (kit == m_key_images.end()) + if (kit == m_pub_keys.end()) { if (!pool) { @@ -482,9 +872,11 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s td.m_internal_output_index = o; td.m_global_output_index = o_indices[o]; td.m_tx = (const cryptonote::transaction_prefix&)tx; - td.m_txid = txid(); + td.m_txid = txid; td.m_key_image = ki[o]; + td.m_key_image_known = !m_watch_only; td.m_amount = tx.vout[o].amount; + td.m_pk_index = pk_index - 1; if (td.m_amount == 0) { td.m_mask = mask[o]; @@ -501,23 +893,24 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s td.m_mask = rct::identity(); td.m_rct = false; } - set_unspent(td); + set_unspent(m_transfers.size()-1); m_key_images[td.m_key_image] = m_transfers.size()-1; - LOG_PRINT_L0("Received money: " << print_money(td.amount()) << ", with tx: " << txid()); + m_pub_keys[in_ephemeral[o].pub] = m_transfers.size()-1; + LOG_PRINT_L0("Received money: " << print_money(td.amount()) << ", with tx: " << txid); if (0 != m_callback) - m_callback->on_money_received(height, tx, td.m_amount); + m_callback->on_money_received(height, txid, tx, td.m_amount); } } else if (m_transfers[kit->second].m_spent || m_transfers[kit->second].amount() >= tx.vout[o].amount) { - LOG_ERROR("key image " << epee::string_tools::pod_to_hex(ki) + LOG_ERROR("Public key " << epee::string_tools::pod_to_hex(kit->first) << " from received " << print_money(tx.vout[o].amount) << " output already exists with " << (m_transfers[kit->second].m_spent ? "spent" : "unspent") << " " << print_money(m_transfers[kit->second].amount()) << ", received output ignored"); } else { - LOG_ERROR("key image " << epee::string_tools::pod_to_hex(ki) + LOG_ERROR("Public key " << epee::string_tools::pod_to_hex(kit->first) << " from received " << print_money(tx.vout[o].amount) << " output already exists with " << print_money(m_transfers[kit->second].amount()) << ", replacing with new output"); // The new larger output replaced a previous smaller one @@ -530,8 +923,9 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s td.m_internal_output_index = o; td.m_global_output_index = o_indices[o]; td.m_tx = (const cryptonote::transaction_prefix&)tx; - td.m_txid = txid(); + td.m_txid = txid; td.m_amount = tx.vout[o].amount; + td.m_pk_index = pk_index - 1; if (td.m_amount == 0) { td.m_mask = mask[o]; @@ -548,12 +942,12 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s td.m_mask = rct::identity(); td.m_rct = false; } - THROW_WALLET_EXCEPTION_IF(td.m_key_image != ki[o], error::wallet_internal_error, "Inconsistent key images"); + THROW_WALLET_EXCEPTION_IF(td.get_public_key() != in_ephemeral[o].pub, error::wallet_internal_error, "Inconsistent public keys"); THROW_WALLET_EXCEPTION_IF(td.m_spent, error::wallet_internal_error, "Inconsistent spent status"); - LOG_PRINT_L0("Received money: " << print_money(td.amount()) << ", with tx: " << txid()); + LOG_PRINT_L0("Received money: " << print_money(td.amount()) << ", with tx: " << txid); if (0 != m_callback) - m_callback->on_money_received(height, tx, td.m_amount); + m_callback->on_money_received(height, txid, tx, td.m_amount); } } } @@ -562,7 +956,7 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s uint64_t tx_money_spent_in_ins = 0; // check all outputs for spending (compare key images) - BOOST_FOREACH(auto& in, tx.vin) + for(auto& in: tx.vin) { if(in.type() != typeid(cryptonote::txin_to_key)) continue; @@ -578,17 +972,20 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s std::string(", expected ") + print_money(td.amount())); } amount = td.amount(); - LOG_PRINT_L0("Spent money: " << print_money(amount) << ", with tx: " << txid()); tx_money_spent_in_ins += amount; - set_spent(td, height); - if (0 != m_callback) - m_callback->on_money_spent(height, tx, amount, tx); + if (!pool) + { + LOG_PRINT_L0("Spent money: " << print_money(amount) << ", with tx: " << txid); + set_spent(it->second, height); + if (0 != m_callback) + m_callback->on_money_spent(height, txid, tx, amount, tx); + } } } if (tx_money_spent_in_ins > 0) { - process_outgoing(tx, height, ts, tx_money_spent_in_ins, tx_money_got_in_outs); + process_outgoing(txid, tx, height, ts, tx_money_spent_in_ins, tx_money_got_in_outs); } uint64_t received = (tx_money_spent_in_ins < tx_money_got_in_outs) ? tx_money_got_in_outs - tx_money_spent_in_ins : 0; @@ -634,25 +1031,27 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s } payment_details payment; - payment.m_tx_hash = txid(); + payment.m_tx_hash = txid; payment.m_amount = received; payment.m_block_height = height; payment.m_unlock_time = tx.unlock_time; payment.m_timestamp = ts; - if (pool) + if (pool) { m_unconfirmed_payments.emplace(payment_id, payment); + if (0 != m_callback) + m_callback->on_unconfirmed_money_received(height, txid, tx, payment.m_amount); + } else m_payments.emplace(payment_id, payment); LOG_PRINT_L2("Payment found in " << (pool ? "pool" : "block") << ": " << payment_id << " / " << payment.m_tx_hash << " / " << payment.m_amount); } } //---------------------------------------------------------------------------------------------------- -void wallet2::process_unconfirmed(const cryptonote::transaction& tx, uint64_t height) +void wallet2::process_unconfirmed(const crypto::hash &txid, const cryptonote::transaction& tx, uint64_t height) { if (m_unconfirmed_txs.empty()) return; - crypto::hash txid = get_transaction_hash(tx); auto unconf_it = m_unconfirmed_txs.find(txid); if(unconf_it != m_unconfirmed_txs.end()) { if (store_tx_info()) { @@ -668,9 +1067,8 @@ void wallet2::process_unconfirmed(const cryptonote::transaction& tx, uint64_t he } } //---------------------------------------------------------------------------------------------------- -void wallet2::process_outgoing(const cryptonote::transaction &tx, uint64_t height, uint64_t ts, uint64_t spent, uint64_t received) +void wallet2::process_outgoing(const crypto::hash &txid, const cryptonote::transaction &tx, uint64_t height, uint64_t ts, uint64_t spent, uint64_t received) { - crypto::hash txid = get_transaction_hash(tx); std::pair<std::unordered_map<crypto::hash, confirmed_transfer_details>::iterator, bool> entry = m_confirmed_txs.insert(std::make_pair(txid, confirmed_transfer_details())); // fill with the info we know, some info might already be there if (entry.second) @@ -684,6 +1082,17 @@ void wallet2::process_outgoing(const cryptonote::transaction &tx, uint64_t heigh else entry.first->second.m_amount_out = spent - tx.rct_signatures.txnFee; entry.first->second.m_change = received; + + std::vector<tx_extra_field> tx_extra_fields; + if(parse_tx_extra(tx.extra, tx_extra_fields)) + { + tx_extra_nonce extra_nonce; + if (find_tx_extra_field_by_type(tx_extra_fields, extra_nonce)) + { + // we do not care about failure here + get_payment_id_from_tx_extra_nonce(extra_nonce.nonce, entry.first->second.m_payment_id); + } + } } entry.first->second.m_block_height = height; entry.first->second.m_timestamp = ts; @@ -702,16 +1111,19 @@ void wallet2::process_new_blockchain_entry(const cryptonote::block& b, const cry if(b.timestamp + 60*60*24 > m_account.get_createtime() && height >= m_refresh_from_block_height) { TIME_MEASURE_START(miner_tx_handle_time); - process_new_transaction(b.miner_tx, o_indices.indices[txidx++].indices, height, b.timestamp, true, false); + process_new_transaction(get_transaction_hash(b.miner_tx), b.miner_tx, o_indices.indices[txidx++].indices, height, b.timestamp, true, false); TIME_MEASURE_FINISH(miner_tx_handle_time); TIME_MEASURE_START(txs_handle_time); - BOOST_FOREACH(auto& txblob, bche.txs) + THROW_WALLET_EXCEPTION_IF(bche.txs.size() != b.tx_hashes.size(), error::wallet_internal_error, "Wrong amount of transactions for block"); + size_t idx = 0; + for (const auto& txblob: bche.txs) { cryptonote::transaction tx; - bool r = parse_and_validate_tx_from_blob(txblob, tx); + bool r = parse_and_validate_tx_base_from_blob(txblob, tx); THROW_WALLET_EXCEPTION_IF(!r, error::tx_parse_error, txblob); - process_new_transaction(tx, o_indices.indices[txidx++].indices, height, b.timestamp, false, false); + process_new_transaction(b.tx_hashes[idx], tx, o_indices.indices[txidx++].indices, height, b.timestamp, false, false); + ++idx; } TIME_MEASURE_FINISH(txs_handle_time); LOG_PRINT_L2("Processed block: " << bl_id << ", height " << height << ", " << miner_tx_handle_time + txs_handle_time << "(" << miner_tx_handle_time << "/" << txs_handle_time <<")ms"); @@ -767,9 +1179,37 @@ void wallet2::pull_blocks(uint64_t start_height, uint64_t &blocks_start_height, cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::response res = AUTO_VAL_INIT(res); req.block_ids = short_chain_history; + uint32_t rpc_version; + boost::optional<std::string> result = m_node_rpc_proxy.get_rpc_version(rpc_version); + // no error + if (!!result) + { + // empty string -> not connection + THROW_WALLET_EXCEPTION_IF(result->empty(), tools::error::no_connection_to_daemon, "getversion"); + THROW_WALLET_EXCEPTION_IF(*result == CORE_RPC_STATUS_BUSY, tools::error::daemon_busy, "getversion"); + if (*result != CORE_RPC_STATUS_OK) + { + MDEBUG("Cannot determined daemon RPC version, not asking for pruned blocks"); + req.prune = false; // old daemon + } + } + else + { + if (rpc_version >= MAKE_CORE_RPC_VERSION(1, 7)) + { + MDEBUG("Daemon is recent enough, asking for pruned blocks"); + req.prune = true; + } + else + { + MDEBUG("Daemon is too old, not asking for pruned blocks"); + req.prune = false; + } + } + req.start_height = start_height; m_daemon_rpc_mutex.lock(); - bool r = net_utils::invoke_http_bin_remote_command2(m_daemon_address + "/getblocks.bin", req, res, m_http_client, WALLET_RCP_CONNECTION_TIMEOUT); + bool r = net_utils::invoke_http_bin("/getblocks.bin", req, res, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "getblocks.bin"); THROW_WALLET_EXCEPTION_IF(res.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "getblocks.bin"); @@ -791,7 +1231,7 @@ void wallet2::pull_hashes(uint64_t start_height, uint64_t &blocks_start_height, req.start_height = start_height; m_daemon_rpc_mutex.lock(); - bool r = net_utils::invoke_http_bin_remote_command2(m_daemon_address + "/gethashes.bin", req, res, m_http_client, WALLET_RCP_CONNECTION_TIMEOUT); + bool r = net_utils::invoke_http_bin("/gethashes.bin", req, res, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "gethashes.bin"); THROW_WALLET_EXCEPTION_IF(res.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "gethashes.bin"); @@ -875,7 +1315,7 @@ void wallet2::process_blocks(uint64_t start_height, const std::list<cryptonote:: } else { - BOOST_FOREACH(auto& bl_entry, blocks) + for(auto& bl_entry: blocks) { cryptonote::block bl; bool r = cryptonote::parse_and_validate_block_from_blob(bl_entry.block, bl); @@ -949,25 +1389,28 @@ void wallet2::pull_next_blocks(uint64_t start_height, uint64_t &blocks_start_hei //---------------------------------------------------------------------------------------------------- void wallet2::update_pool_state() { + MDEBUG("update_pool_state start"); + // get the pool state - cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL::request req; - cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL::response res; + cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL_HASHES::request req; + cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL_HASHES::response res; m_daemon_rpc_mutex.lock(); - bool r = epee::net_utils::invoke_http_json_remote_command2(m_daemon_address + "/get_transaction_pool", req, res, m_http_client, 200000); + bool r = epee::net_utils::invoke_http_json("/get_transaction_pool_hashes.bin", req, res, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); - THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_transaction_pool"); - THROW_WALLET_EXCEPTION_IF(res.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_transaction_pool"); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_transaction_pool_hashes.bin"); + THROW_WALLET_EXCEPTION_IF(res.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_transaction_pool_hashes.bin"); THROW_WALLET_EXCEPTION_IF(res.status != CORE_RPC_STATUS_OK, error::get_tx_pool_error); + MDEBUG("update_pool_state got pool"); // remove any pending tx that's not in the pool std::unordered_map<crypto::hash, wallet2::unconfirmed_transfer_details>::iterator it = m_unconfirmed_txs.begin(); while (it != m_unconfirmed_txs.end()) { - const std::string txid = epee::string_tools::pod_to_hex(it->first); + const crypto::hash &txid = it->first; bool found = false; - for (auto it2: res.transactions) + for (const auto &it2: res.tx_hashes) { - if (it2.id_hash == txid) + if (it2 == txid) { found = true; break; @@ -997,12 +1440,13 @@ void wallet2::update_pool_state() if (pit->second.m_tx.vin[vini].type() == typeid(txin_to_key)) { txin_to_key &tx_in_to_key = boost::get<txin_to_key>(pit->second.m_tx.vin[vini]); - for (auto &td: m_transfers) + for (size_t i = 0; i < m_transfers.size(); ++i) { + const transfer_details &td = m_transfers[i]; if (td.m_key_image == tx_in_to_key.k_image) { LOG_PRINT_L1("Resetting spent status for output " << vini << ": " << td.m_key_image); - set_unspent(td); + set_unspent(i); break; } } @@ -1011,16 +1455,17 @@ void wallet2::update_pool_state() } } } + MDEBUG("update_pool_state done first loop"); // 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 std::string txid = string_tools::pod_to_hex(uit->first); + const crypto::hash &txid = uit->second.m_tx_hash; bool found = false; - for (auto it2: res.transactions) + for (const auto &it2: res.tx_hashes) { - if (it2.id_hash == txid) + if (it2 == txid) { found = true; break; @@ -1029,102 +1474,122 @@ void wallet2::update_pool_state() auto pit = uit++; if (!found) { + MDEBUG("Removing " << txid << " from unconfirmed payments, not found in pool"); m_unconfirmed_payments.erase(pit); } } + MDEBUG("update_pool_state done second loop"); - // add new pool txes to us - for (auto it: res.transactions) + // gather txids of new pool txes to us + std::vector<crypto::hash> txids; + for (const auto &txid: res.tx_hashes) { - cryptonote::blobdata txid_data; - if(epee::string_tools::parse_hexstr_to_binbuff(it.id_hash, txid_data)) + if (m_scanned_pool_txs[0].find(txid) != m_scanned_pool_txs[0].end() || m_scanned_pool_txs[1].find(txid) != m_scanned_pool_txs[1].end()) + { + LOG_PRINT_L2("Already seen " << txid << ", skipped"); + continue; + } + if (m_unconfirmed_payments.find(txid) == m_unconfirmed_payments.end()) { - const crypto::hash txid = *reinterpret_cast<const crypto::hash*>(txid_data.data()); - if (m_unconfirmed_payments.find(txid) == m_unconfirmed_payments.end()) + LOG_PRINT_L1("Found new pool tx: " << txid); + bool found = false; + for (const auto &i: m_unconfirmed_txs) { - LOG_PRINT_L1("Found new pool tx: " << txid); - bool found = false; - for (const auto &i: m_unconfirmed_txs) + if (i.first == txid) { - if (i.first == txid) - { - found = true; - break; - } + found = true; + break; } - if (!found) + } + if (!found) + { + // not one of those we sent ourselves + txids.push_back(txid); + } + else + { + LOG_PRINT_L1("We sent that one"); + } + } + else + { + LOG_PRINT_L1("Already saw that one, it's for us"); + } + } + + // get those txes + if (!txids.empty()) + { + cryptonote::COMMAND_RPC_GET_TRANSACTIONS::request req; + cryptonote::COMMAND_RPC_GET_TRANSACTIONS::response res; + for (const auto &txid: txids) + req.txs_hashes.push_back(epee::string_tools::pod_to_hex(txid)); + MDEBUG("asking for " << txids.size() << " transactions"); + req.decode_as_json = false; + m_daemon_rpc_mutex.lock(); + bool r = epee::net_utils::invoke_http_json("/gettransactions", req, res, m_http_client, rpc_timeout); + m_daemon_rpc_mutex.unlock(); + MDEBUG("Got " << r << " and " << res.status); + if (r && res.status == CORE_RPC_STATUS_OK) + { + if (res.txs.size() == txids.size()) + { + size_t n = 0; + for (const auto &txid: txids) { - // not one of those we sent ourselves - cryptonote::COMMAND_RPC_GET_TRANSACTIONS::request req; - cryptonote::COMMAND_RPC_GET_TRANSACTIONS::response res; - req.txs_hashes.push_back(it.id_hash); - req.decode_as_json = false; - m_daemon_rpc_mutex.lock(); - bool r = epee::net_utils::invoke_http_json_remote_command2(m_daemon_address + "/gettransactions", req, res, m_http_client, 200000); - m_daemon_rpc_mutex.unlock(); - if (r && res.status == CORE_RPC_STATUS_OK) + // might have just been put in a block + if (res.txs[n].in_pool) { - if (res.txs.size() == 1) + cryptonote::transaction tx; + cryptonote::blobdata bd; + crypto::hash tx_hash, tx_prefix_hash; + if (epee::string_tools::parse_hexstr_to_binbuff(res.txs[n].as_hex, bd)) { - // might have just been put in a block - if (res.txs[0].in_pool) + if (cryptonote::parse_and_validate_tx_from_blob(bd, tx, tx_hash, tx_prefix_hash)) { - cryptonote::transaction tx; - cryptonote::blobdata bd; - crypto::hash tx_hash, tx_prefix_hash; - if (epee::string_tools::parse_hexstr_to_binbuff(res.txs[0].as_hex, bd)) + if (tx_hash == txid) { - if (cryptonote::parse_and_validate_tx_from_blob(bd, tx, tx_hash, tx_prefix_hash)) + process_new_transaction(txid, tx, std::vector<uint64_t>(), 0, time(NULL), false, true); + m_scanned_pool_txs[0].insert(txid); + if (m_scanned_pool_txs[0].size() > 5000) { - if (tx_hash == txid) - { - process_new_transaction(tx, std::vector<uint64_t>(), 0, time(NULL), false, true); - } - else - { - LOG_PRINT_L0("Mismatched txids when processing unconfimed txes from pool"); - } - } - else - { - LOG_PRINT_L0("failed to validate transaction from daemon"); + std::swap(m_scanned_pool_txs[0], m_scanned_pool_txs[1]); + m_scanned_pool_txs[0].clear(); } } else { - LOG_PRINT_L0("Failed to parse tx " << txid); + LOG_PRINT_L0("Mismatched txids when processing unconfimed txes from pool"); } } else { - LOG_PRINT_L1("Tx " << txid << " was in pool, but is no more"); + LOG_PRINT_L0("failed to validate transaction from daemon"); } } else { - LOG_PRINT_L0("Expected 1 tx, got " << res.txs.size()); + LOG_PRINT_L0("Failed to parse tx " << txid); } } else { - LOG_PRINT_L0("Error calling gettransactions daemon RPC: r " << r << ", status " << res.status); + LOG_PRINT_L1("Tx " << txid << " was in pool, but is no more"); } - } - else - { - LOG_PRINT_L1("We sent that one"); + ++n; } } else { - LOG_PRINT_L1("Already saw that one"); + LOG_PRINT_L0("Expected " << txids.size() << " tx(es), got " << res.txs.size()); } } else { - LOG_PRINT_L0("Failed to parse txid"); + LOG_PRINT_L0("Error calling gettransactions daemon RPC: r " << r << ", status " << res.status); } } + MDEBUG("update_pool_state end"); } //---------------------------------------------------------------------------------------------------- void wallet2::fast_refresh(uint64_t stop_height, uint64_t &blocks_start_height, std::list<crypto::hash> &short_chain_history) @@ -1155,7 +1620,7 @@ void wallet2::fast_refresh(uint64_t stop_height, uint64_t &blocks_start_height, } } current_index = blocks_start_height; - BOOST_FOREACH(auto& bl_id, hashes) + for(auto& bl_id: hashes) { if(current_index >= m_blockchain.size()) { @@ -1182,6 +1647,30 @@ void wallet2::fast_refresh(uint64_t stop_height, uint64_t &blocks_start_height, } } + +bool wallet2::add_address_book_row(const cryptonote::account_public_address &address, const crypto::hash &payment_id, const std::string &description) +{ + wallet2::address_book_row a; + a.m_address = address; + a.m_payment_id = payment_id; + a.m_description = description; + + auto old_size = m_address_book.size(); + m_address_book.push_back(a); + if(m_address_book.size() == old_size+1) + return true; + return false; +} + +bool wallet2::delete_address_book_row(std::size_t row_id) { + if(m_address_book.size() <= row_id) + return false; + + m_address_book.erase(m_address_book.begin()+row_id); + + return true; +} + //---------------------------------------------------------------------------------------------------- void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& received_money) { @@ -1211,6 +1700,9 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re // and then fall through to regular refresh processing } + // If stop() is called during fast refresh we don't need to continue + if(!m_run.load(std::memory_order_relaxed)) + return; pull_blocks(start_height, blocks_start_height, short_chain_history, blocks, o_indices); // always reset start_height to 0 to force short_chain_ history to be used on // subsequent pulls in this refresh. @@ -1230,8 +1722,11 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re process_blocks(blocks_start_height, blocks, o_indices, added_blocks); blocks_fetched += added_blocks; pull_thread.join(); - if(!added_blocks) + if(blocks_start_height == next_blocks_start_height) + { + m_node_rpc_proxy.set_height(m_blockchain.size()); break; + } // switch to the new blocks from the daemon blocks_start_height = next_blocks_start_height; @@ -1266,7 +1761,9 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re try { - update_pool_state(); + // If stop() is called we don't need to check pending transactions + if(m_run.load(std::memory_order_relaxed)) + update_pool_state(); } catch (...) { @@ -1301,7 +1798,7 @@ void wallet2::detach_blockchain(uint64_t height) if (td.m_spent && td.m_spent_height >= height) { LOG_PRINT_L1("Resetting spent status for output " << i << ": " << td.m_key_image); - set_unspent(td); + set_unspent(i); } } @@ -1313,7 +1810,13 @@ void wallet2::detach_blockchain(uint64_t height) auto it_ki = m_key_images.find(m_transfers[i].m_key_image); THROW_WALLET_EXCEPTION_IF(it_ki == m_key_images.end(), error::wallet_internal_error, "key image not found"); m_key_images.erase(it_ki); - ++transfers_detached; + } + + for(size_t i = i_start; i!= m_transfers.size();i++) + { + auto it_pk = m_pub_keys.find(m_transfers[i].get_public_key()); + THROW_WALLET_EXCEPTION_IF(it_pk == m_pub_keys.end(), error::wallet_internal_error, "public key not found"); + m_pub_keys.erase(it_pk); } m_transfers.erase(it, m_transfers.end()); @@ -1350,6 +1853,7 @@ bool wallet2::clear() m_blockchain.clear(); m_transfers.clear(); m_key_images.clear(); + m_pub_keys.clear(); m_unconfirmed_txs.clear(); m_payments.clear(); m_tx_keys.clear(); @@ -1395,6 +1899,9 @@ bool wallet2::store_keys(const std::string& keys_file_name, const std::string& p value2.SetInt(m_always_confirm_transfers ? 1 :0); json.AddMember("always_confirm_transfers", value2, json.GetAllocator()); + value2.SetInt(m_print_ring_members ? 1 :0); + json.AddMember("print_ring_members", value2, json.GetAllocator()); + value2.SetInt(m_store_tx_info ? 1 :0); json.AddMember("store_tx_info", value2, json.GetAllocator()); @@ -1413,6 +1920,24 @@ bool wallet2::store_keys(const std::string& keys_file_name, const std::string& p value2.SetUint64(m_refresh_from_block_height); json.AddMember("refresh_height", value2, json.GetAllocator()); + value2.SetInt(m_confirm_missing_payment_id ? 1 :0); + json.AddMember("confirm_missing_payment_id", value2, json.GetAllocator()); + + value2.SetInt(m_ask_password ? 1 :0); + json.AddMember("ask_password", value2, json.GetAllocator()); + + value2.SetUint(m_min_output_count); + json.AddMember("min_output_count", value2, json.GetAllocator()); + + value2.SetUint64(m_min_output_value); + json.AddMember("min_output_value", value2, json.GetAllocator()); + + value2.SetInt(cryptonote::get_default_decimal_point()); + json.AddMember("default_decimal_point", value2, json.GetAllocator()); + + value2.SetInt(m_merge_destinations ? 1 :0); + json.AddMember("merge_destinations", value2, json.GetAllocator()); + // Serialize the JSON object rapidjson::StringBuffer buffer; rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); @@ -1474,10 +1999,16 @@ bool wallet2::load_keys(const std::string& keys_file_name, const std::string& pa is_old_file_format = true; m_watch_only = false; m_always_confirm_transfers = false; + m_print_ring_members = false; m_default_mixin = 0; m_default_priority = 0; m_auto_refresh = true; m_refresh_type = RefreshType::RefreshDefault; + m_confirm_missing_payment_id = true; + m_ask_password = true; + m_min_output_count = 0; + m_min_output_value = 0; + m_merge_destinations = false; } else { @@ -1501,8 +2032,10 @@ bool wallet2::load_keys(const std::string& keys_file_name, const std::string& pa } GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, watch_only, int, Int, false, false); m_watch_only = field_watch_only; - GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, always_confirm_transfers, int, Int, false, false); - m_always_confirm_transfers = field_always_confirm_transfers_found && field_always_confirm_transfers; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, always_confirm_transfers, int, Int, false, true); + m_always_confirm_transfers = field_always_confirm_transfers; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, print_ring_members, int, Int, false, true); + m_print_ring_members = field_print_ring_members; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, store_tx_keys, int, Int, false, true); GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, store_tx_info, int, Int, false, true); m_store_tx_info = ((field_store_tx_keys != 0) || (field_store_tx_info != 0)); @@ -1533,8 +2066,19 @@ bool wallet2::load_keys(const std::string& keys_file_name, const std::string& pa LOG_PRINT_L0("Unknown refresh-type value (" << field_refresh_type << "), using default"); } GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, refresh_height, uint64_t, Uint64, false, 0); - if (field_refresh_height_found) - m_refresh_from_block_height = field_refresh_height; + m_refresh_from_block_height = field_refresh_height; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, confirm_missing_payment_id, int, Int, false, true); + m_confirm_missing_payment_id = field_confirm_missing_payment_id; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, ask_password, int, Int, false, true); + m_ask_password = field_ask_password; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, default_decimal_point, int, Int, false, CRYPTONOTE_DISPLAY_DECIMAL_POINT); + cryptonote::set_default_decimal_point(field_default_decimal_point); + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, min_output_count, uint32_t, Uint, false, 0); + m_min_output_count = field_min_output_count; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, min_output_value, uint64_t, Uint64, false, 0); + m_min_output_value = field_min_output_value; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, merge_destinations, int, Int, false, false); + m_merge_destinations = field_merge_destinations; } const cryptonote::account_keys& keys = m_account.get_keys(); @@ -1590,7 +2134,8 @@ bool wallet2::verify_password(const std::string& password) const const cryptonote::account_keys& keys = account_data_check.get_keys(); r = r && verify_keys(keys.m_view_secret_key, keys.m_account_address.m_view_public_key); - r = r && verify_keys(keys.m_spend_secret_key, keys.m_account_address.m_spend_public_key); + if(!m_watch_only) + r = r && verify_keys(keys.m_spend_secret_key, keys.m_account_address.m_spend_public_key); return r; } @@ -1618,11 +2163,24 @@ crypto::secret_key wallet2::generate(const std::string& wallet_, const std::stri m_account_public_address = m_account.get_keys().m_account_address; m_watch_only = false; + if(m_refresh_from_block_height == 0 && !recover){ + // Wallets created offline don't know blockchain height. + // Set blockchain height calculated from current date/time + // -1 month for fluctuations in block time and machine date/time setup. + // avg seconds per block + const int seconds_per_block = DIFFICULTY_TARGET_V2; + // ~num blocks per month + const uint64_t blocks_per_month = 60*60*24*30/seconds_per_block; + uint64_t approx_blockchain_height = get_approximate_blockchain_height(); + if(approx_blockchain_height > 0) { + m_refresh_from_block_height = approx_blockchain_height - blocks_per_month; + } + } bool r = store_keys(m_keys_file, password, false); THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); - if(!r) LOG_PRINT_RED_L0("String with address text not saved"); + if(!r) MERROR("String with address text not saved"); cryptonote::block b; generate_genesis(b); @@ -1657,7 +2215,7 @@ void wallet2::generate(const std::string& wallet_, const std::string& password, THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); - if(!r) LOG_PRINT_RED_L0("String with address text not saved"); + if(!r) MERROR("String with address text not saved"); cryptonote::block b; generate_genesis(b); @@ -1692,7 +2250,7 @@ void wallet2::generate(const std::string& wallet_, const std::string& password, THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); - if(!r) LOG_PRINT_RED_L0("String with address text not saved"); + if(!r) MERROR("String with address text not saved"); cryptonote::block b; generate_genesis(b); @@ -1711,7 +2269,7 @@ void wallet2::rewrite(const std::string& wallet_name, const std::string& passwor prepare_file_names(wallet_name); boost::system::error_code ignored_ec; THROW_WALLET_EXCEPTION_IF(!boost::filesystem::exists(m_keys_file, ignored_ec), error::file_not_found, m_keys_file); - bool r = store_keys(m_keys_file, password, false); + bool r = store_keys(m_keys_file, password, m_watch_only); THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); } /*! @@ -1785,42 +2343,49 @@ bool wallet2::parse_payment_id(const std::string& payment_id_str, crypto::hash& return false; } //---------------------------------------------------------------------------------------------------- +void wallet2::set_default_decimal_point(unsigned int decimal_point) +{ + cryptonote::set_default_decimal_point(decimal_point); +} +//---------------------------------------------------------------------------------------------------- +unsigned int wallet2::get_default_decimal_point() const +{ + return cryptonote::get_default_decimal_point(); +} +//---------------------------------------------------------------------------------------------------- bool wallet2::prepare_file_names(const std::string& file_path) { do_prepare_file_names(file_path, m_keys_file, m_wallet_file); return true; } //---------------------------------------------------------------------------------------------------- -bool wallet2::check_connection(bool *same_version) +bool wallet2::check_connection(uint32_t *version, uint32_t timeout) { boost::lock_guard<boost::mutex> lock(m_daemon_rpc_mutex); if(!m_http_client.is_connected()) { - net_utils::http::url_content u; - net_utils::parse_url(m_daemon_address, u); - - if(!u.port) - { - u.port = m_testnet ? config::testnet::RPC_DEFAULT_PORT : config::RPC_DEFAULT_PORT; - } - - if (!m_http_client.connect(u.host, std::to_string(u.port), WALLET_RCP_CONNECTION_TIMEOUT)) + m_node_rpc_proxy.invalidate(); + if (!m_http_client.connect(std::chrono::milliseconds(timeout))) return false; } - if (same_version) + if (version) { epee::json_rpc::request<cryptonote::COMMAND_RPC_GET_VERSION::request> req_t = AUTO_VAL_INIT(req_t); epee::json_rpc::response<cryptonote::COMMAND_RPC_GET_VERSION::response, std::string> resp_t = AUTO_VAL_INIT(resp_t); req_t.jsonrpc = "2.0"; req_t.id = epee::serialization::storage_entry(0); req_t.method = "get_version"; - bool r = net_utils::invoke_http_json_remote_command2(m_daemon_address + "/json_rpc", req_t, resp_t, m_http_client); - if (!r || resp_t.result.status != CORE_RPC_STATUS_OK) - *same_version = false; + bool r = net_utils::invoke_http_json("/json_rpc", req_t, resp_t, m_http_client); + if(!r) { + *version = 0; + return false; + } + if (resp_t.result.status != CORE_RPC_STATUS_OK) + *version = 0; else - *same_version = resp_t.result.version == CORE_RPC_VERSION; + *version = resp_t.result.version; } return true; @@ -1884,16 +2449,38 @@ void wallet2::load(const std::string& wallet_, const std::string& password) std::stringstream iss; iss << cache_data; - boost::archive::binary_iarchive ar(iss); - ar >> *this; + try { + boost::archive::portable_binary_iarchive ar(iss); + ar >> *this; + } + catch (...) + { + LOG_PRINT_L0("Failed to open portable binary, trying unportable"); + boost::filesystem::copy_file(m_wallet_file, m_wallet_file + ".unportable", boost::filesystem::copy_option::overwrite_if_exists); + iss.str(""); + iss << cache_data; + boost::archive::binary_iarchive ar(iss); + ar >> *this; + } } catch (...) { LOG_PRINT_L1("Failed to load encrypted cache, trying unencrypted"); std::stringstream iss; iss << buf; - boost::archive::binary_iarchive ar(iss); - ar >> *this; + try { + boost::archive::portable_binary_iarchive ar(iss); + ar >> *this; + } + catch (...) + { + LOG_PRINT_L0("Failed to open portable binary, trying unportable"); + boost::filesystem::copy_file(m_wallet_file, m_wallet_file + ".unportable", boost::filesystem::copy_option::overwrite_if_exists); + iss.str(""); + iss << buf; + boost::archive::binary_iarchive ar(iss); + ar >> *this; + } } THROW_WALLET_EXCEPTION_IF( m_account_public_address.m_spend_public_key != m_account.get_keys().m_account_address.m_spend_public_key || @@ -1918,11 +2505,16 @@ void wallet2::load(const std::string& wallet_, const std::string& password) } //---------------------------------------------------------------------------------------------------- void wallet2::check_genesis(const crypto::hash& genesis_hash) const { - std::string what("Genesis block missmatch. You probably use wallet without testnet flag with blockchain from test network or vice versa"); + std::string what("Genesis block mismatch. You probably use wallet without testnet flag with blockchain from test network or vice versa"); THROW_WALLET_EXCEPTION_IF(genesis_hash != m_blockchain[0], error::wallet_internal_error, what); } //---------------------------------------------------------------------------------------------------- +std::string wallet2::path() const +{ + return m_wallet_file; +} +//---------------------------------------------------------------------------------------------------- void wallet2::store() { store_to("", ""); @@ -1962,7 +2554,7 @@ void wallet2::store_to(const std::string &path, const std::string &password) } // preparing wallet data std::stringstream oss; - boost::archive::binary_oarchive ar(oss); + boost::archive::portable_binary_oarchive ar(oss); ar << *this; wallet2::cache_file_data cache_file_data = boost::value_initialized<wallet2::cache_file_data>(); @@ -2022,7 +2614,7 @@ void wallet2::store_to(const std::string &path, const std::string &password) uint64_t wallet2::unlocked_balance() const { uint64_t amount = 0; - BOOST_FOREACH(const transfer_details& td, m_transfers) + for(const transfer_details& td: m_transfers) if(!td.m_spent && is_transfer_unlocked(td)) amount += td.amount(); @@ -2032,12 +2624,12 @@ uint64_t wallet2::unlocked_balance() const uint64_t wallet2::balance() const { uint64_t amount = 0; - BOOST_FOREACH(auto& td, m_transfers) + for(auto& td: m_transfers) if(!td.m_spent) amount += td.amount(); - BOOST_FOREACH(auto& utx, m_unconfirmed_txs) + for(auto& utx: m_unconfirmed_txs) if (utx.second.m_state != wallet2::unconfirmed_transfer_details::failed) amount+= utx.second.m_change; @@ -2097,44 +2689,50 @@ void wallet2::get_unconfirmed_payments(std::list<std::pair<crypto::hash,wallet2: //---------------------------------------------------------------------------------------------------- void wallet2::rescan_spent() { - std::vector<std::string> key_images; - - // make a list of key images for all our outputs - for (size_t i = 0; i < m_transfers.size(); ++i) + // This is RPC call that can take a long time if there are many outputs, + // so we call it several times, in stripes, so we don't time out spuriously + std::vector<int> spent_status; + spent_status.reserve(m_transfers.size()); + const size_t chunk_size = 1000; + for (size_t start_offset = 0; start_offset < m_transfers.size(); start_offset += chunk_size) { - const transfer_details& td = m_transfers[i]; - key_images.push_back(string_tools::pod_to_hex(td.m_key_image)); + const size_t n_outputs = std::min<size_t>(chunk_size, m_transfers.size() - start_offset); + MDEBUG("Calling is_key_image_spent on " << start_offset << " - " << (start_offset + n_outputs - 1) << ", out of " << m_transfers.size()); + COMMAND_RPC_IS_KEY_IMAGE_SPENT::request req = AUTO_VAL_INIT(req); + COMMAND_RPC_IS_KEY_IMAGE_SPENT::response daemon_resp = AUTO_VAL_INIT(daemon_resp); + for (size_t n = start_offset; n < start_offset + n_outputs; ++n) + req.key_images.push_back(string_tools::pod_to_hex(m_transfers[n].m_key_image)); + m_daemon_rpc_mutex.lock(); + bool r = epee::net_utils::invoke_http_json("/is_key_image_spent", req, daemon_resp, m_http_client, rpc_timeout); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "is_key_image_spent"); + THROW_WALLET_EXCEPTION_IF(daemon_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "is_key_image_spent"); + THROW_WALLET_EXCEPTION_IF(daemon_resp.status != CORE_RPC_STATUS_OK, error::is_key_image_spent_error, daemon_resp.status); + THROW_WALLET_EXCEPTION_IF(daemon_resp.spent_status.size() != n_outputs, error::wallet_internal_error, + "daemon returned wrong response for is_key_image_spent, wrong amounts count = " + + std::to_string(daemon_resp.spent_status.size()) + ", expected " + std::to_string(n_outputs)); + std::copy(daemon_resp.spent_status.begin(), daemon_resp.spent_status.end(), std::back_inserter(spent_status)); } - COMMAND_RPC_IS_KEY_IMAGE_SPENT::request req = AUTO_VAL_INIT(req); - COMMAND_RPC_IS_KEY_IMAGE_SPENT::response daemon_resp = AUTO_VAL_INIT(daemon_resp); - req.key_images = key_images; - m_daemon_rpc_mutex.lock(); - bool r = epee::net_utils::invoke_http_json_remote_command2(m_daemon_address + "/is_key_image_spent", req, daemon_resp, m_http_client, 200000); - m_daemon_rpc_mutex.unlock(); - THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "is_key_image_spent"); - THROW_WALLET_EXCEPTION_IF(daemon_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "is_key_image_spent"); - THROW_WALLET_EXCEPTION_IF(daemon_resp.status != CORE_RPC_STATUS_OK, error::is_key_image_spent_error, daemon_resp.status); - THROW_WALLET_EXCEPTION_IF(daemon_resp.spent_status.size() != key_images.size(), error::wallet_internal_error, - "daemon returned wrong response for is_key_image_spent, wrong amounts count = " + - std::to_string(daemon_resp.spent_status.size()) + ", expected " + std::to_string(key_images.size())); - // update spent status for (size_t i = 0; i < m_transfers.size(); ++i) { transfer_details& td = m_transfers[i]; - if (td.m_spent != (daemon_resp.spent_status[i] != COMMAND_RPC_IS_KEY_IMAGE_SPENT::UNSPENT)) + // a view wallet may not know about key images + if (!td.m_key_image_known) + continue; + if (td.m_spent != (spent_status[i] != COMMAND_RPC_IS_KEY_IMAGE_SPENT::UNSPENT)) { if (td.m_spent) { LOG_PRINT_L0("Marking output " << i << "(" << td.m_key_image << ") as unspent, it was marked as spent"); - set_unspent(td); + set_unspent(i); td.m_spent_height = 0; } else { LOG_PRINT_L0("Marking output " << i << "(" << td.m_key_image << ") as spent, it was marked as unspent"); - set_spent(td, td.m_spent_height); + set_spent(i, td.m_spent_height); // unknown height, if this gets reorged, it might still be missed } } @@ -2227,6 +2825,19 @@ namespace vec.pop_back(); return res; } + + template<typename T> + void pop_if_present(std::vector<T>& vec, T e) + { + for (size_t i = 0; i < vec.size(); ++i) + { + if (e == vec[i]) + { + pop_index (vec, i); + return; + } + } + } } //---------------------------------------------------------------------------------------------------- // This returns a handwavy estimation of how much two outputs are related @@ -2260,7 +2871,7 @@ float wallet2::get_output_relatedness(const transfer_details &td0, const transfe return 0.0f; } //---------------------------------------------------------------------------------------------------- -size_t wallet2::pop_best_value_from(const transfer_container &transfers, std::vector<size_t> &unused_indices, const std::list<transfer_container::iterator>& selected_transfers) const +size_t wallet2::pop_best_value_from(const transfer_container &transfers, std::vector<size_t> &unused_indices, const std::list<size_t>& selected_transfers, bool smallest) const { std::vector<size_t> candidates; float best_relatedness = 1.0f; @@ -2268,9 +2879,9 @@ size_t wallet2::pop_best_value_from(const transfer_container &transfers, std::ve { const transfer_details &candidate = transfers[unused_indices[n]]; float relatedness = 0.0f; - for (const auto &i: selected_transfers) + for (std::list<size_t>::const_iterator i = selected_transfers.begin(); i != selected_transfers.end(); ++i) { - float r = get_output_relatedness(candidate, *i); + float r = get_output_relatedness(candidate, transfers[*i]); if (r > relatedness) { relatedness = r; @@ -2288,20 +2899,37 @@ size_t wallet2::pop_best_value_from(const transfer_container &transfers, std::ve if (relatedness == best_relatedness) candidates.push_back(n); } - size_t idx = crypto::rand<size_t>() % candidates.size(); + + // we have all the least related outputs in candidates, so we can pick either + // the smallest, or a random one, depending on request + size_t idx; + if (smallest) + { + idx = 0; + for (size_t n = 0; n < candidates.size(); ++n) + { + const transfer_details &td = transfers[unused_indices[candidates[n]]]; + if (td.amount() < transfers[unused_indices[candidates[idx]]].amount()) + idx = n; + } + } + else + { + idx = crypto::rand<size_t>() % candidates.size(); + } return pop_index (unused_indices, candidates[idx]); } //---------------------------------------------------------------------------------------------------- -size_t wallet2::pop_best_value(std::vector<size_t> &unused_indices, const std::list<transfer_container::iterator>& selected_transfers) const +size_t wallet2::pop_best_value(std::vector<size_t> &unused_indices, const std::list<size_t>& selected_transfers, bool smallest) const { - return pop_best_value_from(m_transfers, unused_indices, selected_transfers); + return pop_best_value_from(m_transfers, unused_indices, selected_transfers, smallest); } //---------------------------------------------------------------------------------------------------- // Select random input sources for transaction. // returns: // direct return: amount of money found // modified reference: selected_transfers, a list of iterators/indices of input sources -uint64_t wallet2::select_transfers(uint64_t needed_money, std::vector<size_t> unused_transfers_indices, std::list<transfer_container::iterator>& selected_transfers, bool trusted_daemon) +uint64_t wallet2::select_transfers(uint64_t needed_money, std::vector<size_t> unused_transfers_indices, std::list<size_t>& selected_transfers, bool trusted_daemon) { uint64_t found_money = 0; while (found_money < needed_money && !unused_transfers_indices.empty()) @@ -2309,7 +2937,7 @@ uint64_t wallet2::select_transfers(uint64_t needed_money, std::vector<size_t> un size_t idx = pop_best_value(unused_transfers_indices, selected_transfers); transfer_container::iterator it = m_transfers.begin() + idx; - selected_transfers.push_back(it); + selected_transfers.push_back(idx); found_money += it->amount(); } @@ -2323,6 +2951,7 @@ void wallet2::add_unconfirmed_tx(const cryptonote::transaction& tx, uint64_t amo utd.m_amount_out = 0; for (const auto &d: dests) utd.m_amount_out += d.amount; + utd.m_amount_out += change_amount; // dests does not contain change utd.m_change = change_amount; utd.m_sent_time = time(NULL); utd.m_tx = (const cryptonote::transaction_prefix&)tx; @@ -2395,74 +3024,7 @@ std::vector<std::vector<cryptonote::tx_destination_entry>> split_amounts( return retVal; } } // anonymous namespace - -/** - * @brief gets a monero address from the TXT record of a DNS entry - * - * gets the monero address from the TXT record of the DNS entry associated - * with <url>. If this lookup fails, or the TXT record does not contain an - * XMR address in the correct format, returns an empty string. <dnssec_valid> - * will be set true or false according to whether or not the DNS query passes - * DNSSEC validation. - * - * @param url the url to look up - * @param dnssec_valid return-by-reference for DNSSEC status of query - * - * @return a monero address (as a string) or an empty string - */ -std::vector<std::string> wallet2::addresses_from_url(const std::string& url, bool& dnssec_valid) -{ - std::vector<std::string> addresses; - // get txt records - bool dnssec_available, dnssec_isvalid; - std::string oa_addr = tools::DNSResolver::instance().get_dns_format_from_oa_address(url); - auto records = tools::DNSResolver::instance().get_txt_record(oa_addr, dnssec_available, dnssec_isvalid); - - // TODO: update this to allow for conveying that dnssec was not available - if (dnssec_available && dnssec_isvalid) - { - dnssec_valid = true; - } - else dnssec_valid = false; - - // for each txt record, try to find a monero address in it. - for (auto& rec : records) - { - std::string addr = address_from_txt_record(rec); - if (addr.size()) - { - addresses.push_back(addr); - } - } - - return addresses; -} - //---------------------------------------------------------------------------------------------------- -// TODO: parse the string in a less stupid way, probably with regex -std::string wallet2::address_from_txt_record(const std::string& s) -{ - // make sure the txt record has "oa1:xmr" and find it - auto pos = s.find("oa1:xmr"); - - // search from there to find "recipient_address=" - pos = s.find("recipient_address=", pos); - - pos += 18; // move past "recipient_address=" - - // find the next semicolon - auto pos2 = s.find(";", pos); - if (pos2 != std::string::npos) - { - // length of address == 95, we can at least validate that much here - if (pos2 - pos == 95) - { - return s.substr(pos, 95); - } - } - return std::string(); -} - crypto::hash wallet2::get_payment_id(const pending_tx &ptx) const { std::vector<tx_extra_field> tx_extra_fields; @@ -2487,6 +3049,24 @@ crypto::hash wallet2::get_payment_id(const pending_tx &ptx) const } return payment_id; } + +crypto::hash8 wallet2::get_short_payment_id(const pending_tx &ptx) const +{ + crypto::hash8 payment_id8 = null_hash8; + std::vector<tx_extra_field> tx_extra_fields; + if(!parse_tx_extra(ptx.tx.extra, tx_extra_fields)) + return payment_id8; + cryptonote::tx_extra_nonce extra_nonce; + if (find_tx_extra_field_by_type(tx_extra_fields, extra_nonce)) + { + if(get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id8)) + { + decrypt_payment_id(payment_id8, ptx.dests[0].addr.m_view_public_key, ptx.tx_key); + } + } + return payment_id8; +} + //---------------------------------------------------------------------------------------------------- // take a pending tx and actually send it to the daemon void wallet2::commit_tx(pending_tx& ptx) @@ -2499,12 +3079,19 @@ void wallet2::commit_tx(pending_tx& ptx) 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_remote_command2(m_daemon_address + "/sendrawtransaction", req, daemon_send_resp, m_http_client, 200000); + 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)); + } + txid = get_transaction_hash(ptx.tx); crypto::hash payment_id = cryptonote::null_hash; std::vector<cryptonote::tx_destination_entry> dests; @@ -2513,8 +3100,8 @@ void wallet2::commit_tx(pending_tx& ptx) { payment_id = get_payment_id(ptx); dests = ptx.dests; - BOOST_FOREACH(transfer_container::iterator it, ptx.selected_transfers) - amount_in += it->amount(); + for(size_t idx: ptx.selected_transfers) + amount_in += m_transfers[idx].amount(); } add_unconfirmed_tx(ptx.tx, amount_in, dests, payment_id, ptx.change_dts.amount); if (store_tx_info()) @@ -2524,13 +3111,13 @@ void wallet2::commit_tx(pending_tx& ptx) LOG_PRINT_L2("transaction " << txid << " generated ok and sent to daemon, key_images: [" << ptx.key_images << "]"); - BOOST_FOREACH(transfer_container::iterator it, ptx.selected_transfers) + for(size_t idx: ptx.selected_transfers) { - set_spent(*it, 0); + set_spent(idx, 0); } //fee includes dust if dust policy specified it. - LOG_PRINT_L0("Transaction successfully sent. <" << txid << ">" << ENDL + LOG_PRINT_L1("Transaction successfully sent. <" << txid << ">" << ENDL << "Commission: " << print_money(ptx.fee) << " (dust sent to dust addr: " << print_money((ptx.dust_added_to_fee ? 0 : ptx.dust)) << ")" << ENDL << "Balance: " << print_money(balance()) << ENDL << "Unlocked: " << print_money(unlocked_balance()) << ENDL @@ -2544,26 +3131,310 @@ void wallet2::commit_tx(std::vector<pending_tx>& ptx_vector) commit_tx(ptx); } } +//---------------------------------------------------------------------------------------------------- +bool wallet2::save_tx(const std::vector<pending_tx>& ptx_vector, const std::string &filename) +{ + LOG_PRINT_L0("saving " << ptx_vector.size() << " transactions"); + unsigned_tx_set txs; + for (auto &tx: ptx_vector) + { + tx_construction_data construction_data = tx.construction_data; + // Short payment id is encrypted with tx_key. + // Since sign_tx() generates new tx_keys and encrypts the payment id, we need to save the decrypted payment ID + // Get decrypted payment id from pending_tx + crypto::hash8 payment_id = get_short_payment_id(tx); + if (payment_id != null_hash8) + { + // Remove encrypted + remove_field_from_tx_extra(construction_data.extra, typeid(cryptonote::tx_extra_nonce)); + // Add decrypted + std::string extra_nonce; + set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, payment_id); + if (!add_extra_nonce_to_tx_extra(construction_data.extra, extra_nonce)) + { + LOG_ERROR("Failed to add decrypted payment id to tx extra"); + return false; + } + LOG_PRINT_L1("Decrypted payment ID: " << payment_id); + } + // Save tx construction_data to unsigned_tx_set + txs.txes.push_back(construction_data); + } + + txs.transfers = m_transfers; + // save as binary + std::ostringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + try + { + ar << txs; + } + catch (...) + { + return false; + } + LOG_PRINT_L2("Saving unsigned tx data: " << oss.str()); + return epee::file_io_utils::save_string_to_file(filename, std::string(UNSIGNED_TX_PREFIX) + oss.str()); +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::load_unsigned_tx(const std::string &unsigned_filename, unsigned_tx_set &exported_txs) +{ + std::string s; + boost::system::error_code errcode; -uint64_t wallet2::get_fee_multiplier(uint32_t priority, bool use_new_fee) const + if (!boost::filesystem::exists(unsigned_filename, errcode)) + { + LOG_PRINT_L0("File " << unsigned_filename << " does not exist: " << errcode); + return false; + } + if (!epee::file_io_utils::load_file_to_string(unsigned_filename.c_str(), s)) + { + LOG_PRINT_L0("Failed to load from " << unsigned_filename); + return false; + } + const size_t magiclen = strlen(UNSIGNED_TX_PREFIX); + if (strncmp(s.c_str(), UNSIGNED_TX_PREFIX, magiclen)) + { + LOG_PRINT_L0("Bad magic from " << unsigned_filename); + return false; + } + s = s.substr(magiclen); + try + { + std::istringstream iss(s); + boost::archive::portable_binary_iarchive ar(iss); + ar >> exported_txs; + } + catch (...) + { + LOG_PRINT_L0("Failed to parse data from " << unsigned_filename); + return false; + } + LOG_PRINT_L1("Loaded tx unsigned data from binary: " << exported_txs.txes.size() << " transactions"); + + return true; +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::sign_tx(const std::string &unsigned_filename, const std::string &signed_filename, std::vector<wallet2::pending_tx> &txs, std::function<bool(const unsigned_tx_set&)> accept_func) +{ + unsigned_tx_set exported_txs; + if(!load_unsigned_tx(unsigned_filename, exported_txs)) + return false; + + if (accept_func && !accept_func(exported_txs)) + { + LOG_PRINT_L1("Transactions rejected by callback"); + return false; + } + return sign_tx(exported_txs, signed_filename, txs); +} + +//---------------------------------------------------------------------------------------------------- +bool wallet2::sign_tx(unsigned_tx_set &exported_txs, const std::string &signed_filename, std::vector<wallet2::pending_tx> &txs) +{ + import_outputs(exported_txs.transfers); + + // sign the transactions + signed_tx_set signed_txes; + for (size_t n = 0; n < exported_txs.txes.size(); ++n) + { + const tools::wallet2::tx_construction_data &sd = exported_txs.txes[n]; + LOG_PRINT_L1(" " << (n+1) << ": " << sd.sources.size() << " inputs, mixin " << (sd.sources[0].outputs.size()-1)); + signed_txes.ptx.push_back(pending_tx()); + tools::wallet2::pending_tx &ptx = signed_txes.ptx.back(); + crypto::secret_key tx_key; + bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), sd.sources, sd.splitted_dsts, sd.extra, ptx.tx, sd.unlock_time, tx_key, sd.use_rct); + THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sd.sources, sd.splitted_dsts, sd.unlock_time, m_testnet); + // we don't test tx size, because we don't know the current limit, due to not having a blockchain, + // and it's a bit pointless to fail there anyway, since it'd be a (good) guess only. We sign anyway, + // and if we really go over limit, the daemon will reject when it gets submitted. Chances are it's + // OK anyway since it was generated in the first place, and rerolling should be within a few bytes. + + // normally, the tx keys are saved in commit_tx, when the tx is actually sent to the daemon. + // we can't do that here since the tx will be sent from the compromised wallet, which we don't want + // to see that info, so we save it here + if (store_tx_info()) + { + const crypto::hash txid = get_transaction_hash(ptx.tx); + m_tx_keys.insert(std::make_pair(txid, tx_key)); + } + + std::string key_images; + bool all_are_txin_to_key = std::all_of(ptx.tx.vin.begin(), ptx.tx.vin.end(), [&](const txin_v& s_e) -> bool + { + CHECKED_GET_SPECIFIC_VARIANT(s_e, const txin_to_key, in, false); + key_images += boost::to_string(in.k_image) + " "; + return true; + }); + THROW_WALLET_EXCEPTION_IF(!all_are_txin_to_key, error::unexpected_txin_type, ptx.tx); + + 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 key images + signed_txes.key_images.resize(m_transfers.size()); + for (size_t i = 0; i < m_transfers.size(); ++i) + { + if (!m_transfers[i].m_key_image_known) + LOG_PRINT_L0("WARNING: key image not known in signing wallet at index " << i); + signed_txes.key_images[i] = m_transfers[i].m_key_image; + } + + // save as binary + std::ostringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + try + { + ar << signed_txes; + } + catch(...) + { + return false; + } + LOG_PRINT_L3("Saving signed tx data: " << oss.str()); + return epee::file_io_utils::save_string_to_file(signed_filename, std::string(SIGNED_TX_PREFIX) + oss.str()); +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::load_tx(const std::string &signed_filename, std::vector<tools::wallet2::pending_tx> &ptx, std::function<bool(const signed_tx_set&)> accept_func) +{ + std::string s; + boost::system::error_code errcode; + signed_tx_set signed_txs; + + if (!boost::filesystem::exists(signed_filename, errcode)) + { + LOG_PRINT_L0("File " << signed_filename << " does not exist: " << errcode); + return false; + } + + if (!epee::file_io_utils::load_file_to_string(signed_filename.c_str(), s)) + { + LOG_PRINT_L0("Failed to load from " << signed_filename); + return false; + } + const size_t magiclen = strlen(SIGNED_TX_PREFIX); + if (strncmp(s.c_str(), SIGNED_TX_PREFIX, magiclen)) + { + LOG_PRINT_L0("Bad magic from " << signed_filename); + return false; + } + s = s.substr(magiclen); + try + { + std::istringstream iss(s); + boost::archive::portable_binary_iarchive ar(iss); + ar >> signed_txs; + } + catch (...) + { + LOG_PRINT_L0("Failed to parse data from " << signed_filename); + return false; + } + LOG_PRINT_L0("Loaded signed tx data from binary: " << signed_txs.ptx.size() << " transactions"); + for (auto &ptx: signed_txs.ptx) LOG_PRINT_L0(cryptonote::obj_to_json_str(ptx.tx)); + + if (accept_func && !accept_func(signed_txs)) + { + LOG_PRINT_L1("Transactions rejected by callback"); + return false; + } + + // import key images + if (signed_txs.key_images.size() > m_transfers.size()) + { + LOG_PRINT_L1("More key images returned that we know outputs for"); + return false; + } + for (size_t i = 0; i < signed_txs.key_images.size(); ++i) + { + transfer_details &td = m_transfers[i]; + if (td.m_key_image_known && td.m_key_image != signed_txs.key_images[i]) + LOG_PRINT_L0("WARNING: imported key image differs from previously known key image at index " << i << ": trusting imported one"); + td.m_key_image = signed_txs.key_images[i]; + m_key_images[m_transfers[i].m_key_image] = i; + td.m_key_image_known = true; + m_pub_keys[m_transfers[i].get_public_key()] = i; + } + + ptx = signed_txs.ptx; + + return true; +} +//---------------------------------------------------------------------------------------------------- +uint64_t wallet2::get_fee_multiplier(uint32_t priority, int fee_algorithm) const { static const uint64_t old_multipliers[3] = {1, 2, 3}; static const uint64_t new_multipliers[3] = {1, 20, 166}; + static const uint64_t newer_multipliers[4] = {1, 4, 20, 166}; - // 0 -> default (here, x1) + // 0 -> default (here, x1 till fee algorithm 2, x4 from it) if (priority == 0) priority = m_default_priority; if (priority == 0) - priority = 1; + { + if (fee_algorithm >= 2) + priority = 2; + else + priority = 1; + } - // 1 to 3 are allowed as priorities - if (priority >= 1 && priority <= 3) - return (use_new_fee ? new_multipliers : old_multipliers)[priority-1]; + // 1 to 3/4 are allowed as priorities + uint32_t max_priority = (fee_algorithm >= 2) ? 4 : 3; + if (priority >= 1 && priority <= max_priority) + { + switch (fee_algorithm) + { + case 0: return old_multipliers[priority-1]; + case 1: return new_multipliers[priority-1]; + case 2: return newer_multipliers[priority-1]; + default: THROW_WALLET_EXCEPTION_IF (true, error::invalid_priority); + } + } THROW_WALLET_EXCEPTION_IF (false, error::invalid_priority); return 1; } +//---------------------------------------------------------------------------------------------------- +uint64_t wallet2::get_dynamic_per_kb_fee_estimate() +{ + uint64_t fee; + boost::optional<std::string> result = m_node_rpc_proxy.get_dynamic_per_kb_fee_estimate(FEE_ESTIMATE_GRACE_BLOCKS, fee); + if (!result) + return fee; + LOG_PRINT_L1("Failed to query per kB fee, using " << print_money(FEE_PER_KB)); + return FEE_PER_KB; +} +//---------------------------------------------------------------------------------------------------- +uint64_t wallet2::get_per_kb_fee() +{ + bool use_dyn_fee = use_fork_rules(HF_VERSION_DYNAMIC_FEE, -720 * 1); + if (!use_dyn_fee) + return FEE_PER_KB; + return get_dynamic_per_kb_fee_estimate(); +} +//---------------------------------------------------------------------------------------------------- +int wallet2::get_fee_algorithm() +{ + // changes at v3 and v5 + if (use_fork_rules(5, 0)) + return 2; + if (use_fork_rules(3, -720 * 14)) + return 1; + return 0; +} //---------------------------------------------------------------------------------------------------- // separated the call(s) to wallet2::transfer into their own function // @@ -2571,11 +3442,10 @@ uint64_t wallet2::get_fee_multiplier(uint32_t priority, bool use_new_fee) const // transactions will be required std::vector<wallet2::pending_tx> wallet2::create_transactions(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, bool trusted_daemon) { - const std::vector<size_t> unused_transfers_indices = select_available_outputs_from_histogram(fake_outs_count + 1, true, true, trusted_daemon); + const std::vector<size_t> unused_transfers_indices = select_available_outputs_from_histogram(fake_outs_count + 1, true, true, true, trusted_daemon); - const bool use_new_fee = use_fork_rules(3, -720 * 14); - const uint64_t fee_per_kb = use_new_fee ? FEE_PER_KB : FEE_PER_KB_OLD; - const uint64_t fee_multiplier = get_fee_multiplier(priority, use_new_fee); + const uint64_t fee_per_kb = get_per_kb_fee(); + const uint64_t fee_multiplier = get_fee_multiplier(priority, get_fee_algorithm()); // failsafe split attempt counter size_t attempt_count = 0; @@ -2613,9 +3483,9 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions(std::vector<crypto ptx_vector.push_back(ptx); // mark transfers to be used as "spent" - BOOST_FOREACH(transfer_container::iterator it, ptx.selected_transfers) + for(size_t idx: ptx.selected_transfers) { - set_spent(*it, 0); + set_spent(idx, 0); } } @@ -2625,9 +3495,9 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions(std::vector<crypto for (auto & ptx : ptx_vector) { // mark transfers to be used as not spent - BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) + for(size_t idx2: ptx.selected_transfers) { - set_unspent(*it2); + set_unspent(idx2); } } @@ -2644,9 +3514,9 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions(std::vector<crypto for (auto & ptx : ptx_vector) { // mark transfers to be used as not spent - BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) + for(size_t idx2: ptx.selected_transfers) { - set_unspent(*it2); + set_unspent(idx2); } } @@ -2663,9 +3533,9 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions(std::vector<crypto for (auto & ptx : ptx_vector) { // mark transfers to be used as not spent - BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) + for(size_t idx2: ptx.selected_transfers) { - set_unspent(*it2); + set_unspent(idx2); } } @@ -2674,8 +3544,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions(std::vector<crypto } } -template<typename entry> -void wallet2::get_outs(std::vector<std::vector<entry>> &outs, const std::list<transfer_container::iterator> &selected_transfers, size_t fake_outputs_count) +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(); @@ -2688,13 +3557,14 @@ void wallet2::get_outs(std::vector<std::vector<entry>> &outs, const std::list<tr req_t.jsonrpc = "2.0"; req_t.id = epee::serialization::storage_entry(0); req_t.method = "get_output_histogram"; - for(auto it: selected_transfers) - req_t.params.amounts.push_back(it->is_rct() ? 0 : it->amount()); + for(size_t idx: selected_transfers) + req_t.params.amounts.push_back(m_transfers[idx].is_rct() ? 0 : m_transfers[idx].amount()); std::sort(req_t.params.amounts.begin(), req_t.params.amounts.end()); auto end = std::unique(req_t.params.amounts.begin(), req_t.params.amounts.end()); req_t.params.amounts.resize(std::distance(req_t.params.amounts.begin(), end)); req_t.params.unlocked = true; - bool r = net_utils::invoke_http_json_remote_command2(m_daemon_address + "/json_rpc", req_t, resp_t, m_http_client); + req_t.params.recent_cutoff = time(NULL) - RECENT_OUTPUT_ZONE; + bool r = net_utils::invoke_http_json("/json_rpc", req_t, resp_t, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "transfer_selected"); THROW_WALLET_EXCEPTION_IF(resp_t.result.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_output_histogram"); @@ -2705,31 +3575,49 @@ void wallet2::get_outs(std::vector<std::vector<entry>> &outs, const std::list<tr LOG_PRINT_L2("base_requested_outputs_count: " << base_requested_outputs_count); // generate output indices to request - COMMAND_RPC_GET_OUTPUTS::request req = AUTO_VAL_INIT(req); - COMMAND_RPC_GET_OUTPUTS::response daemon_resp = AUTO_VAL_INIT(daemon_resp); + COMMAND_RPC_GET_OUTPUTS_BIN::request req = AUTO_VAL_INIT(req); + COMMAND_RPC_GET_OUTPUTS_BIN::response daemon_resp = AUTO_VAL_INIT(daemon_resp); - for(transfer_container::iterator it: selected_transfers) + size_t num_selected_transfers = 0; + for(size_t idx: selected_transfers) { - const uint64_t amount = it->is_rct() ? 0 : it->amount(); + ++num_selected_transfers; + const transfer_details &td = m_transfers[idx]; + const uint64_t amount = td.is_rct() ? 0 : td.amount(); std::unordered_set<uint64_t> seen_indices; // request more for rct in base recent (locked) coinbases are picked, since they're locked for longer - size_t requested_outputs_count = base_requested_outputs_count + (it->is_rct() ? CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW - CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE : 0); + size_t requested_outputs_count = base_requested_outputs_count + (td.is_rct() ? CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW - CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE : 0); size_t start = req.outputs.size(); // if there are just enough outputs to mix with, use all of them. // Eventually this should become impossible. - uint64_t num_outs = 0; + uint64_t num_outs = 0, num_recent_outs = 0; for (auto he: resp_t.result.histogram) { if (he.amount == amount) { - num_outs = he.instances; + LOG_PRINT_L2("Found " << print_money(amount) << ": " << he.total_instances << " total, " + << he.unlocked_instances << " unlocked, " << he.recent_instances << " recent"); + num_outs = he.unlocked_instances; + num_recent_outs = he.recent_instances; break; } } - LOG_PRINT_L1("" << num_outs << " outputs of size " << print_money(amount)); + LOG_PRINT_L1("" << num_outs << " unlocked outputs of size " << print_money(amount)); THROW_WALLET_EXCEPTION_IF(num_outs == 0, error::wallet_internal_error, - "histogram reports no outputs for " + boost::lexical_cast<std::string>(amount) + ", not even ours"); + "histogram reports no unlocked outputs for " + boost::lexical_cast<std::string>(amount) + ", not even ours"); + THROW_WALLET_EXCEPTION_IF(num_recent_outs > num_outs, error::wallet_internal_error, + "histogram reports more recent outs than outs for " + boost::lexical_cast<std::string>(amount)); + + // X% of those outs are to be taken from recent outputs + size_t recent_outputs_count = requested_outputs_count * RECENT_OUTPUT_RATIO; + if (recent_outputs_count == 0) + recent_outputs_count = 1; // ensure we have at least one, if possible + if (recent_outputs_count > num_recent_outs) + recent_outputs_count = num_recent_outs; + if (td.m_global_output_index >= num_outs - num_recent_outs && recent_outputs_count > 0) + --recent_outputs_count; // if the real out is recent, pick one less recent fake out + LOG_PRINT_L1("Using " << recent_outputs_count << " recent outputs"); if (num_outs <= requested_outputs_count) { @@ -2745,8 +3633,9 @@ void wallet2::get_outs(std::vector<std::vector<entry>> &outs, const std::list<tr { // start with real one uint64_t num_found = 1; - seen_indices.emplace(it->m_global_output_index); - req.outputs.push_back({amount, it->m_global_output_index}); + seen_indices.emplace(td.m_global_output_index); + req.outputs.push_back({amount, td.m_global_output_index}); + LOG_PRINT_L1("Selecting real output: " << td.m_global_output_index << " for " << print_money(amount)); // while we still need more mixins while (num_found < requested_outputs_count) @@ -2759,13 +3648,29 @@ void wallet2::get_outs(std::vector<std::vector<entry>> &outs, const std::list<tr // return to the top of the loop and try again, otherwise add it to the // list of output indices we've seen. - // triangular distribution over [a,b) with a=0, mode c=b=up_index_limit - uint64_t r = crypto::rand<uint64_t>() % ((uint64_t)1 << 53); - double frac = std::sqrt((double)r / ((uint64_t)1 << 53)); - uint64_t i = (uint64_t)(frac*num_outs); - // just in case rounding up to 1 occurs after sqrt - if (i == num_outs) - --i; + uint64_t i; + if (num_found - 1 < recent_outputs_count) // -1 to account for the real one we seeded with + { + // triangular distribution over [a,b) with a=0, mode c=b=up_index_limit + uint64_t r = crypto::rand<uint64_t>() % ((uint64_t)1 << 53); + double frac = std::sqrt((double)r / ((uint64_t)1 << 53)); + i = (uint64_t)(frac*num_recent_outs) + num_outs - num_recent_outs; + // just in case rounding up to 1 occurs after calc + if (i == num_outs) + --i; + LOG_PRINT_L2("picking " << i << " as recent"); + } + else + { + // triangular distribution over [a,b) with a=0, mode c=b=up_index_limit + uint64_t r = crypto::rand<uint64_t>() % ((uint64_t)1 << 53); + double frac = std::sqrt((double)r / ((uint64_t)1 << 53)); + i = (uint64_t)(frac*num_outs); + // just in case rounding up to 1 occurs after calc + if (i == num_outs) + --i; + LOG_PRINT_L2("picking " << i << " as triangular"); + } if (seen_indices.count(i)) continue; @@ -2778,7 +3683,7 @@ void wallet2::get_outs(std::vector<std::vector<entry>> &outs, const std::list<tr // sort the subsection, to ensure the daemon doesn't know wich output is ours std::sort(req.outputs.begin() + start, req.outputs.end(), - [](const COMMAND_RPC_GET_OUTPUTS::out &a, const COMMAND_RPC_GET_OUTPUTS::out &b) { return a.index < b.index; }); + [](const get_outputs_out &a, const get_outputs_out &b) { return a.index < b.index; }); } for (auto i: req.outputs) @@ -2786,7 +3691,7 @@ void wallet2::get_outs(std::vector<std::vector<entry>> &outs, const std::list<tr // get the keys for those m_daemon_rpc_mutex.lock(); - r = epee::net_utils::invoke_http_bin_remote_command2(m_daemon_address + "/get_outs.bin", req, daemon_resp, m_http_client, 200000); + r = epee::net_utils::invoke_http_bin("/get_outs.bin", req, daemon_resp, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_outs.bin"); THROW_WALLET_EXCEPTION_IF(daemon_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_outs.bin"); @@ -2797,16 +3702,34 @@ void wallet2::get_outs(std::vector<std::vector<entry>> &outs, const std::list<tr std::unordered_map<uint64_t, uint64_t> scanty_outs; size_t base = 0; - outs.reserve(selected_transfers.size()); - for(transfer_container::iterator it: selected_transfers) + outs.reserve(num_selected_transfers); + for(size_t idx: selected_transfers) { - size_t requested_outputs_count = base_requested_outputs_count + (it->is_rct() ? CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW - CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE : 0); - outs.push_back(std::vector<entry>()); + const transfer_details &td = m_transfers[idx]; + size_t requested_outputs_count = base_requested_outputs_count + (td.is_rct() ? CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW - CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE : 0); + outs.push_back(std::vector<get_outs_entry>()); outs.back().reserve(fake_outputs_count + 1); - const rct::key mask = it->is_rct() ? rct::commit(it->amount(), it->m_mask) : rct::zeroCommit(it->amount()); + const rct::key mask = td.is_rct() ? rct::commit(td.amount(), td.m_mask) : rct::zeroCommit(td.amount()); + + // make sure the real outputs we asked for are really included, along + // with the correct key and mask: this guards against an active attack + // where the node sends dummy data for all outputs, and we then send + // the real one, which the node can then tell from the fake outputs, + // as it has different data than the dummy data it had sent earlier + bool real_out_found = false; + for (size_t n = 0; n < requested_outputs_count; ++n) + { + size_t i = base + n; + if (req.outputs[i].index == td.m_global_output_index) + if (daemon_resp.outs[i].key == boost::get<txout_to_key>(td.m_tx.vout[td.m_internal_output_index].target).key) + if (daemon_resp.outs[i].mask == mask) + real_out_found = true; + } + THROW_WALLET_EXCEPTION_IF(!real_out_found, error::wallet_internal_error, + "Daemon response did not include the requested real output"); // pick real out first (it will be sorted when done) - outs.back().push_back(std::make_tuple(it->m_global_output_index, boost::get<txout_to_key>(it->m_tx.vout[it->m_internal_output_index].target).key, mask)); + outs.back().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)); // then pick others in random order till we reach the required number // since we use an equiprobable pick here, we don't upset the triangular distribution @@ -2816,12 +3739,12 @@ void wallet2::get_outs(std::vector<std::vector<entry>> &outs, const std::list<tr 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 of size " << print_money(it->is_rct() ? 0 : it->amount())); + LOG_PRINT_L2("Looking for " << (fake_outputs_count+1) << " outputs of size " << print_money(td.is_rct() ? 0 : td.amount())); for (size_t o = 0; o < requested_outputs_count && outs.back().size() < fake_outputs_count + 1; ++o) { size_t i = base + order[o]; - LOG_PRINT_L2("Index " << i << "/" << requested_outputs_count << ": idx " << req.outputs[i].index << " (real " << it->m_global_output_index << "), unlocked " << daemon_resp.outs[i].unlocked << ", key " << daemon_resp.outs[i].key); - if (req.outputs[i].index == it->m_global_output_index) // don't re-add real one + 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; @@ -2832,12 +3755,12 @@ void wallet2::get_outs(std::vector<std::vector<entry>> &outs, const std::list<tr } if (outs.back().size() < fake_outputs_count + 1) { - scanty_outs[it->is_rct() ? 0 : it->amount()] = outs.back().size(); + scanty_outs[td.is_rct() ? 0 : td.amount()] = outs.back().size(); } else { // sort the subsection, so any spares are reset in order - std::sort(outs.back().begin(), outs.back().end(), [](const entry &a, const entry &b) { return std::get<0>(a) < std::get<0>(b); }); + 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); }); } base += requested_outputs_count; } @@ -2845,18 +3768,20 @@ void wallet2::get_outs(std::vector<std::vector<entry>> &outs, const std::list<tr } else { - for (transfer_container::iterator it: selected_transfers) + for (size_t idx: selected_transfers) { - std::vector<entry> v; - const rct::key mask = it->is_rct() ? rct::commit(it->amount(), it->m_mask) : rct::zeroCommit(it->amount()); - v.push_back(std::make_tuple(it->m_global_output_index, boost::get<txout_to_key>(it->m_tx.vout[it->m_internal_output_index].target).key, mask)); + 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)); outs.push_back(v); } } } template<typename T> -void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_entry>& dsts, const std::list<transfer_container::iterator> selected_transfers, size_t fake_outputs_count, +void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_entry>& dsts, const std::list<size_t> selected_transfers, size_t fake_outputs_count, + std::vector<std::vector<tools::wallet2::get_outs_entry>> &outs, uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& extra, T destination_split_strategy, const tx_dust_policy& dust_policy, cryptonote::transaction& tx, pending_tx &ptx) { using namespace cryptonote; @@ -2869,7 +3794,7 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent // calculate total amount being sent to all destinations // throw if total amount overflows uint64_t - BOOST_FOREACH(auto& dt, dsts) + for(auto& dt: dsts) { THROW_WALLET_EXCEPTION_IF(0 == dt.amount, error::zero_destination); needed_money += dt.amount; @@ -2878,27 +3803,27 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent } uint64_t found_money = 0; - BOOST_FOREACH(auto it, selected_transfers) + for(size_t idx: selected_transfers) { - found_money += it->amount(); + found_money += m_transfers[idx].amount(); } LOG_PRINT_L2("wanted " << print_money(needed_money) << ", found " << print_money(found_money) << ", fee " << print_money(fee)); THROW_WALLET_EXCEPTION_IF(found_money < needed_money, error::not_enough_money, found_money, needed_money - fee, fee); - typedef std::tuple<uint64_t, crypto::public_key, rct::key> entry; - std::vector<std::vector<entry>> outs; - get_outs(outs, selected_transfers, fake_outputs_count); // may throw + if (outs.empty()) + get_outs(outs, selected_transfers, fake_outputs_count); // may throw //prepare inputs + LOG_PRINT_L2("preparing outputs"); typedef cryptonote::tx_source_entry::output_entry tx_output_entry; size_t i = 0, out_index = 0; std::vector<cryptonote::tx_source_entry> sources; - BOOST_FOREACH(transfer_container::iterator it, selected_transfers) + for(size_t idx: selected_transfers) { sources.resize(sources.size()+1); cryptonote::tx_source_entry& src = sources.back(); - transfer_details& td = *it; + const transfer_details& td = m_transfers[idx]; src.amount = td.amount(); src.rct = td.is_rct(); //paste keys (fake and real) @@ -2927,12 +3852,13 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent 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.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); + src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_tx, td.m_pk_index); src.real_output = it_to_replace - src.outputs.begin(); src.real_output_in_tx_index = td.m_internal_output_index; detail::print_source_entry(src); ++out_index; } + LOG_PRINT_L2("outputs prepared"); cryptonote::tx_destination_entry change_dts = AUTO_VAL_INIT(change_dts); if (needed_money < found_money) @@ -2944,18 +3870,20 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent std::vector<cryptonote::tx_destination_entry> splitted_dsts, dust_dsts; uint64_t dust = 0; destination_split_strategy(dsts, change_dts, dust_policy.dust_threshold, splitted_dsts, dust_dsts); - BOOST_FOREACH(auto& d, dust_dsts) { + for(auto& d: dust_dsts) { THROW_WALLET_EXCEPTION_IF(dust_policy.dust_threshold < d.amount, error::wallet_internal_error, "invalid dust value: dust = " + std::to_string(d.amount) + ", dust_threshold = " + std::to_string(dust_policy.dust_threshold)); } - BOOST_FOREACH(auto& d, dust_dsts) { + for(auto& d: dust_dsts) { if (!dust_policy.add_to_fee) splitted_dsts.push_back(cryptonote::tx_destination_entry(d.amount, dust_policy.addr_for_dust)); dust += d.amount; } crypto::secret_key tx_key; + LOG_PRINT_L2("constructing tx"); bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), sources, splitted_dsts, extra, tx, unlock_time, tx_key); + LOG_PRINT_L2("constructed tx, r="<<r); THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sources, splitted_dsts, unlock_time, m_testnet); THROW_WALLET_EXCEPTION_IF(upper_transaction_size_limit <= get_object_blobsize(tx), error::tx_too_big, tx, upper_transaction_size_limit); @@ -2983,9 +3911,19 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent ptx.selected_transfers = selected_transfers; ptx.tx_key = tx_key; ptx.dests = dsts; + ptx.construction_data.sources = sources; + ptx.construction_data.change_dts = change_dts; + 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 = unlock_time; + ptx.construction_data.use_rct = false; + ptx.construction_data.dests = dsts; + LOG_PRINT_L2("transfer_selected done"); } -void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry> dsts, const std::list<transfer_container::iterator> selected_transfers, size_t fake_outputs_count, +void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry> dsts, const std::list<size_t> selected_transfers, size_t fake_outputs_count, + std::vector<std::vector<tools::wallet2::get_outs_entry>> &outs, uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& extra, cryptonote::transaction& tx, pending_tx &ptx) { using namespace cryptonote; @@ -2994,11 +3932,14 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry uint64_t upper_transaction_size_limit = get_upper_tranaction_size_limit(); uint64_t needed_money = fee; - LOG_PRINT_L2("transfer: starting with fee " << print_money (needed_money)); + LOG_PRINT_L2("transfer_selected_rct: starting with fee " << print_money (needed_money)); + LOG_PRINT_L0("selected transfers: "); + for (auto t: selected_transfers) + LOG_PRINT_L2(" " << t); // calculate total amount being sent to all destinations // throw if total amount overflows uint64_t - BOOST_FOREACH(auto& dt, dsts) + for(auto& dt: dsts) { THROW_WALLET_EXCEPTION_IF(0 == dt.amount, error::zero_destination); needed_money += dt.amount; @@ -3007,26 +3948,26 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry } uint64_t found_money = 0; - BOOST_FOREACH(auto it, selected_transfers) + for(size_t idx: selected_transfers) { - found_money += it->amount(); + found_money += m_transfers[idx].amount(); } LOG_PRINT_L2("wanted " << print_money(needed_money) << ", found " << print_money(found_money) << ", fee " << print_money(fee)); THROW_WALLET_EXCEPTION_IF(found_money < needed_money, error::not_enough_money, found_money, needed_money - fee, fee); - typedef std::tuple<uint64_t, crypto::public_key, rct::key> entry; - std::vector<std::vector<entry>> outs; - get_outs(outs, selected_transfers, fake_outputs_count); // may throw + if (outs.empty()) + get_outs(outs, selected_transfers, fake_outputs_count); // may throw //prepare inputs + LOG_PRINT_L2("preparing outputs"); size_t i = 0, out_index = 0; std::vector<cryptonote::tx_source_entry> sources; - BOOST_FOREACH(transfer_container::iterator it, selected_transfers) + for(size_t idx: selected_transfers) { sources.resize(sources.size()+1); cryptonote::tx_source_entry& src = sources.back(); - transfer_details& td = *it; + const transfer_details& td = m_transfers[idx]; src.amount = td.amount(); src.rct = td.is_rct(); //paste mixin transaction @@ -3055,29 +3996,45 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry 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.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); + src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_tx, td.m_pk_index); src.real_output = it_to_replace - src.outputs.begin(); src.real_output_in_tx_index = td.m_internal_output_index; src.mask = td.m_mask; detail::print_source_entry(src); ++out_index; } + LOG_PRINT_L2("outputs prepared"); // we still keep a copy, since we want to keep dsts free of change for user feedback purposes std::vector<cryptonote::tx_destination_entry> splitted_dsts = dsts; cryptonote::tx_destination_entry change_dts = AUTO_VAL_INIT(change_dts); - if (needed_money < found_money) + change_dts.amount = found_money - needed_money; + if (change_dts.amount == 0) + { + // 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_dts.addr = dummy.get_keys().m_account_address; + LOG_PRINT_L2("generated dummy address for 0 change"); + } + else { change_dts.addr = m_account.get_keys().m_account_address; - change_dts.amount = found_money - needed_money; - splitted_dsts.push_back(change_dts); } + splitted_dsts.push_back(change_dts); crypto::secret_key tx_key; + LOG_PRINT_L2("constructing tx"); bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), sources, splitted_dsts, extra, tx, unlock_time, tx_key, true); + LOG_PRINT_L2("constructed tx, r="<<r); THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sources, dsts, unlock_time, m_testnet); THROW_WALLET_EXCEPTION_IF(upper_transaction_size_limit <= get_object_blobsize(tx), error::tx_too_big, tx, upper_transaction_size_limit); + LOG_PRINT_L2("gathering key images"); std::string key_images; bool all_are_txin_to_key = std::all_of(tx.vin.begin(), tx.vin.end(), [&](const txin_v& s_e) -> bool { @@ -3086,6 +4043,7 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry return true; }); THROW_WALLET_EXCEPTION_IF(!all_are_txin_to_key, error::unexpected_txin_type, tx); + LOG_PRINT_L2("gathered key images"); ptx.key_images = key_images; ptx.fee = fee; @@ -3096,6 +4054,15 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry ptx.selected_transfers = selected_transfers; ptx.tx_key = tx_key; ptx.dests = dsts; + ptx.construction_data.sources = sources; + ptx.construction_data.change_dts = change_dts; + 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 = unlock_time; + ptx.construction_data.use_rct = true; + ptx.construction_data.dests = dsts; + LOG_PRINT_L2("transfer_selected_rct done"); } static size_t estimate_rct_tx_size(int n_inputs, int mixin, int n_outputs) @@ -3143,12 +4110,20 @@ static size_t estimate_rct_tx_size(int n_inputs, int mixin, int n_outputs) return size; } -std::vector<size_t> wallet2::pick_prefered_rct_inputs(uint64_t needed_money) const +static size_t estimate_tx_size(bool use_rct, int n_inputs, int mixin, int n_outputs) +{ + if (use_rct) + return estimate_rct_tx_size(n_inputs, mixin, n_outputs + 1); + else + return n_inputs * (mixin+1) * APPROXIMATE_INPUT_BYTES; +} + +std::vector<size_t> wallet2::pick_preferred_rct_inputs(uint64_t needed_money) const { std::vector<size_t> picks; float current_output_relatdness = 1.0f; - LOG_PRINT_L2("pick_prefered_rct_inputs: needed_money " << print_money(needed_money)); + LOG_PRINT_L2("pick_preferred_rct_inputs: needed_money " << print_money(needed_money)); // try to find a rct input of enough size for (size_t i = 0; i < m_transfers.size(); ++i) @@ -3203,6 +4178,60 @@ std::vector<size_t> wallet2::pick_prefered_rct_inputs(uint64_t needed_money) con return picks; } +bool wallet2::should_pick_a_second_output(bool use_rct, size_t n_transfers, const std::vector<size_t> &unused_transfers_indices, const std::vector<size_t> &unused_dust_indices) const +{ + if (!use_rct) + return false; + if (n_transfers > 1) + return false; + if (unused_dust_indices.empty() && unused_transfers_indices.empty()) + return false; + // we want at least one free rct output to avoid a corner case where + // we'd choose a non rct output which doesn't have enough "siblings" + // value-wise on the chain, and thus can't be mixed + bool found = false; + for (auto i: unused_dust_indices) + { + if (m_transfers[i].is_rct()) + { + found = true; + break; + } + } + if (!found) for (auto i: unused_transfers_indices) + { + if (m_transfers[i].is_rct()) + { + found = true; + break; + } + } + if (!found) + return false; + return true; +} + +std::vector<size_t> wallet2::get_only_rct(const std::vector<size_t> &unused_dust_indices, const std::vector<size_t> &unused_transfers_indices) const +{ + std::vector<size_t> indices; + for (size_t n: unused_dust_indices) + if (m_transfers[n].is_rct()) + indices.push_back(n); + for (size_t n: unused_transfers_indices) + if (m_transfers[n].is_rct()) + indices.push_back(n); + return indices; +} + +static uint32_t get_count_above(const std::vector<wallet2::transfer_details> &transfers, const std::vector<size_t> &indices, uint64_t threshold) +{ + uint32_t count = 0; + for (size_t idx: indices) + if (transfers[idx].amount() >= threshold) + ++count; + return count; +} + // 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 @@ -3225,19 +4254,29 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp uint64_t needed_money; uint64_t accumulated_fee, accumulated_outputs, accumulated_change; struct TX { - std::list<transfer_container::iterator> selected_transfers; + std::list<size_t> selected_transfers; std::vector<cryptonote::tx_destination_entry> dsts; cryptonote::transaction tx; pending_tx ptx; size_t bytes; - void add(const account_public_address &addr, uint64_t amount) { - std::vector<cryptonote::tx_destination_entry>::iterator i; - i = std::find_if(dsts.begin(), dsts.end(), [&](const cryptonote::tx_destination_entry &d) { return !memcmp (&d.addr, &addr, sizeof(addr)); }); - if (i == dsts.end()) - dsts.push_back(tx_destination_entry(amount,addr)); - else + void add(const account_public_address &addr, uint64_t amount, unsigned int original_output_index, bool merge_destinations) { + if (merge_destinations) + { + std::vector<cryptonote::tx_destination_entry>::iterator i; + i = std::find_if(dsts.begin(), dsts.end(), [&](const cryptonote::tx_destination_entry &d) { return !memcmp (&d.addr, &addr, sizeof(addr)); }); + if (i == dsts.end()) + dsts.push_back(tx_destination_entry(0,addr)); i->amount += amount; + } + else + { + THROW_WALLET_EXCEPTION_IF(original_output_index > dsts.size(), error::wallet_internal_error, "original_output_index too large"); + if (original_output_index == dsts.size()) + dsts.push_back(tx_destination_entry(0,addr)); + THROW_WALLET_EXCEPTION_IF(memcmp(&dsts[original_output_index].addr, &addr, sizeof(addr)), error::wallet_internal_error, "Mismatched destination address"); + dsts[original_output_index].amount += amount; + } } }; std::vector<TX> txes; @@ -3246,9 +4285,8 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp uint64_t upper_transaction_size_limit = get_upper_tranaction_size_limit(); const bool use_rct = use_fork_rules(4, 0); - const bool use_new_fee = use_fork_rules(3, -720 * 14); - const uint64_t fee_per_kb = use_new_fee ? FEE_PER_KB : FEE_PER_KB_OLD; - const uint64_t fee_multiplier = get_fee_multiplier(priority, use_new_fee); + const uint64_t fee_per_kb = get_per_kb_fee(); + const uint64_t fee_multiplier = get_fee_multiplier(priority, get_fee_algorithm()); // throw if attempting a transaction with no destinations THROW_WALLET_EXCEPTION_IF(dsts.empty(), error::zero_destination); @@ -3256,7 +4294,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp // calculate total amount being sent to all destinations // throw if total amount overflows uint64_t needed_money = 0; - BOOST_FOREACH(auto& dt, dsts) + for(auto& dt: dsts) { THROW_WALLET_EXCEPTION_IF(0 == dt.amount, error::zero_destination); needed_money += dt.amount; @@ -3281,6 +4319,12 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp } LOG_PRINT_L2("Starting with " << unused_transfers_indices.size() << " non-dust outputs and " << unused_dust_indices.size() << " dust outputs"); + // early out if we know we can't make it anyway + // we could also check for being within FEE_PER_KB, but if the fee calculation + // ever changes, this might be missed, so let this go through + THROW_WALLET_EXCEPTION_IF(needed_money > unlocked_balance(), error::not_enough_money, + unlocked_balance(), needed_money, 0); + if (unused_dust_indices.empty() && unused_transfers_indices.empty()) return std::vector<wallet2::pending_tx>(); @@ -3291,12 +4335,14 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp accumulated_change = 0; adding_fee = false; needed_fee = 0; + std::vector<std::vector<tools::wallet2::get_outs_entry>> outs; // for rct, since we don't see the amounts, we will try to make all transactions // look the same, with 1 or 2 inputs, and 2 outputs. One input is preferable, as // this prevents linking to another by provenance analysis, but two is ok if we // try to pick outputs not from the same block. We will get two outputs, one for // the destination, and one for change. + LOG_PRINT_L2("checking preferred"); std::vector<size_t> prefered_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 @@ -3305,37 +4351,84 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp // this is used to build a tx that's 1 or 2 inputs, and 2 outputs, which // will get us a known fee. uint64_t estimated_fee = calculate_fee(fee_per_kb, estimate_rct_tx_size(2, fake_outs_count + 1, 2), fee_multiplier); - prefered_inputs = pick_prefered_rct_inputs(needed_money + estimated_fee); + prefered_inputs = pick_preferred_rct_inputs(needed_money + estimated_fee); if (!prefered_inputs.empty()) { string s; - for (auto i: prefered_inputs) s += print_money(m_transfers[i].amount()) + " "; + for (auto i: prefered_inputs) s += boost::lexical_cast<std::string>(i) + "(" + print_money(m_transfers[i].amount()) + ") "; LOG_PRINT_L1("Found prefered rct inputs for rct tx: " << s); } } - - // while we have something to send - while ((!dsts.empty() && dsts[0].amount > 0) || adding_fee) { + LOG_PRINT_L2("done checking preferred"); + + // while: + // - we have something to send + // - or we need to gather more fee + // - or we have just one input in that tx, which is rct (to try and make all/most rct txes 2/2) + unsigned int original_output_index = 0; + while ((!dsts.empty() && dsts[0].amount > 0) || adding_fee || should_pick_a_second_output(use_rct, txes.back().selected_transfers.size(), unused_transfers_indices, unused_dust_indices)) { TX &tx = txes.back(); + LOG_PRINT_L2("Start of loop with " << unused_transfers_indices.size() << " " << unused_dust_indices.size()); + LOG_PRINT_L2("unused_transfers_indices:"); + for (auto t: unused_transfers_indices) + LOG_PRINT_L2(" " << t); + LOG_PRINT_L2("unused_dust_indices:"); + for (auto t: unused_dust_indices) + LOG_PRINT_L2(" " << t); + LOG_PRINT_L2("dsts size " << dsts.size() << ", first " << (dsts.empty() ? -1 : dsts[0].amount)); + LOG_PRINT_L2("adding_fee " << adding_fee << ", use_rct " << use_rct); + // if we need to spend money and don't have any left, we fail if (unused_dust_indices.empty() && unused_transfers_indices.empty()) { LOG_PRINT_L2("No more outputs to choose from"); - THROW_WALLET_EXCEPTION_IF(1, error::not_enough_money, unlocked_balance(), needed_money, accumulated_fee + needed_fee); + THROW_WALLET_EXCEPTION_IF(1, error::tx_not_possible, unlocked_balance(), needed_money, accumulated_fee + needed_fee); } // get a random unspent output and use it to pay part (or all) of the current destination (and maybe next one, etc) // This could be more clever, but maybe at the cost of making probabilistic inferences easier - size_t idx = !prefered_inputs.empty() ? pop_back(prefered_inputs) : !unused_transfers_indices.empty() ? pop_best_value(unused_transfers_indices, tx.selected_transfers) : pop_best_value(unused_dust_indices, tx.selected_transfers); + size_t idx; + if ((dsts.empty() || dsts[0].amount == 0) && !adding_fee) { + // the "make rct txes 2/2" case - we pick a small value output to "clean up" the wallet too + std::vector<size_t> indices = get_only_rct(unused_dust_indices, unused_transfers_indices); + idx = pop_best_value(indices, tx.selected_transfers, true); + + // we might not want to add it if it's a large output and we don't have many left + if (m_transfers[idx].amount() >= m_min_output_value) { + if (get_count_above(m_transfers, unused_transfers_indices, m_min_output_value) < m_min_output_count) { + LOG_PRINT_L2("Second output was not strictly needed, and we're running out of outputs above " << print_money(m_min_output_value) << ", not adding"); + break; + } + } + + // since we're trying to add a second output which is not strictly needed, + // we only add it if it's unrelated enough to the first one + float relatedness = get_output_relatedness(m_transfers[idx], m_transfers[tx.selected_transfers.front()]); + if (relatedness > SECOND_OUTPUT_RELATEDNESS_THRESHOLD) + { + LOG_PRINT_L2("Second output was not strictly needed, and relatedness " << relatedness << ", not adding"); + break; + } + pop_if_present(unused_transfers_indices, idx); + pop_if_present(unused_dust_indices, idx); + } else if (!prefered_inputs.empty()) { + idx = pop_back(prefered_inputs); + pop_if_present(unused_transfers_indices, idx); + pop_if_present(unused_dust_indices, idx); + } else + idx = pop_best_value(unused_transfers_indices.empty() ? unused_dust_indices : unused_transfers_indices, tx.selected_transfers); const transfer_details &td = m_transfers[idx]; - LOG_PRINT_L2("Picking output " << idx << ", amount " << print_money(td.amount())); + LOG_PRINT_L2("Picking output " << idx << ", amount " << print_money(td.amount()) << ", ki " << td.m_key_image); // add this output to the list to spend - tx.selected_transfers.push_back(m_transfers.begin() + idx); + tx.selected_transfers.push_back(idx); uint64_t available_amount = td.amount(); accumulated_outputs += available_amount; + // clear any fake outs we'd already gathered, since we'll need a new set + outs.clear(); + if (adding_fee) { LOG_PRINT_L2("We need more fee, adding it to fee"); @@ -3343,22 +4436,23 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp } else { - while (!dsts.empty() && dsts[0].amount <= available_amount) + while (!dsts.empty() && dsts[0].amount <= available_amount && estimate_tx_size(use_rct, tx.selected_transfers.size(), fake_outs_count, tx.dsts.size()) < TX_SIZE_TARGET(upper_transaction_size_limit)) { // we can fully pay that destination LOG_PRINT_L2("We can fully pay " << get_account_address_as_str(m_testnet, dsts[0].addr) << " for " << print_money(dsts[0].amount)); - tx.add(dsts[0].addr, dsts[0].amount); + tx.add(dsts[0].addr, dsts[0].amount, original_output_index, m_merge_destinations); available_amount -= dsts[0].amount; dsts[0].amount = 0; pop_index(dsts, 0); + ++original_output_index; } - if (available_amount > 0 && !dsts.empty()) { + if (available_amount > 0 && !dsts.empty() && estimate_tx_size(use_rct, tx.selected_transfers.size(), fake_outs_count, tx.dsts.size()) < TX_SIZE_TARGET(upper_transaction_size_limit)) { // we can partially fill that destination LOG_PRINT_L2("We can partially pay " << get_account_address_as_str(m_testnet, dsts[0].addr) << " for " << print_money(available_amount) << "/" << print_money(dsts[0].amount)); - tx.add(dsts[0].addr, available_amount); + tx.add(dsts[0].addr, available_amount, original_output_index, m_merge_destinations); dsts[0].amount -= available_amount; available_amount = 0; } @@ -3375,11 +4469,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp } else { - size_t estimated_rct_tx_size; - if (use_rct) - estimated_rct_tx_size = estimate_rct_tx_size(tx.selected_transfers.size(), fake_outs_count, tx.dsts.size() + 1); - else - estimated_rct_tx_size = tx.selected_transfers.size() * (fake_outs_count+1) * APPROXIMATE_INPUT_BYTES; + const size_t estimated_rct_tx_size = estimate_tx_size(use_rct, tx.selected_transfers.size(), fake_outs_count, tx.dsts.size()); try_tx = dsts.empty() || (estimated_rct_tx_size >= TX_SIZE_TARGET(upper_transaction_size_limit)); } @@ -3392,15 +4482,15 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp LOG_PRINT_L2("Trying to create a tx now, with " << tx.dsts.size() << " destinations and " << tx.selected_transfers.size() << " outputs"); if (use_rct) - transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, unlock_time, needed_fee, extra, + transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, test_tx, test_ptx); else - transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, unlock_time, needed_fee, extra, + transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx); auto txBlob = t_serializable_object_to_blob(test_ptx.tx); needed_fee = calculate_fee(fee_per_kb, txBlob, fee_multiplier); available_for_fee = test_ptx.fee + test_ptx.change_dts.amount + (!test_ptx.dust_added_to_fee ? test_ptx.dust : 0); - LOG_PRINT_L2("Made a " << txBlob.size() << " kB tx, with " << print_money(available_for_fee) << " available for fee (" << + LOG_PRINT_L2("Made a " << ((txBlob.size() + 1023) / 1024) << " kB tx, with " << print_money(available_for_fee) << " available for fee (" << print_money(needed_fee) << " needed)"); if (needed_fee > available_for_fee && dsts[0].amount > 0) @@ -3433,13 +4523,19 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp else { LOG_PRINT_L2("We made a tx, adjusting fee and saving it"); - if (use_rct) - transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, unlock_time, needed_fee, extra, - test_tx, test_ptx); - else - transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, unlock_time, needed_fee, extra, - detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx); - txBlob = t_serializable_object_to_blob(test_ptx.tx); + do { + if (use_rct) + transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, + test_tx, test_ptx); + else + transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, + detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx); + txBlob = t_serializable_object_to_blob(test_ptx.tx); + needed_fee = calculate_fee(fee_per_kb, txBlob, fee_multiplier); + LOG_PRINT_L2("Made an attempt at a final " << ((txBlob.size() + 1023)/1024) << " kB tx, with " << print_money(test_ptx.fee) << + " fee and " << print_money(test_ptx.change_dts.amount) << " change"); + } while (needed_fee > test_ptx.fee); + LOG_PRINT_L2("Made a final " << ((txBlob.size() + 1023)/1024) << " kB tx, with " << print_money(test_ptx.fee) << " fee and " << print_money(test_ptx.change_dts.amount) << " change"); @@ -3461,7 +4557,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp if (adding_fee) { LOG_PRINT_L1("We ran out of outputs while trying to gather final fee"); - THROW_WALLET_EXCEPTION_IF(1, error::not_enough_money, unlocked_balance(), needed_money, accumulated_fee + needed_fee); + THROW_WALLET_EXCEPTION_IF(1, error::tx_not_possible, unlocked_balance(), needed_money, accumulated_fee + needed_fee); } LOG_PRINT_L1("Done creating " << txes.size() << " transactions, " << print_money(accumulated_fee) << @@ -3472,8 +4568,8 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp { TX &tx = *i; uint64_t tx_money = 0; - for (std::list<transfer_container::iterator>::const_iterator mi = tx.selected_transfers.begin(); mi != tx.selected_transfers.end(); ++mi) - tx_money += (*mi)->amount(); + for (size_t idx: tx.selected_transfers) + tx_money += m_transfers[idx].amount(); LOG_PRINT_L1(" Transaction " << (1+std::distance(txes.begin(), i)) << "/" << txes.size() << ": " << (tx.bytes+1023)/1024 << " kB, sending " << print_money(tx_money) << " in " << tx.selected_transfers.size() << " outputs to " << tx.dsts.size() << " destination(s), including " << @@ -3485,13 +4581,36 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp return ptx_vector; } -std::vector<wallet2::pending_tx> wallet2::create_transactions_all(const cryptonote::account_public_address &address, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t> extra, bool trusted_daemon) +std::vector<wallet2::pending_tx> wallet2::create_transactions_all(uint64_t below, const cryptonote::account_public_address &address, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t> extra, bool trusted_daemon) { std::vector<size_t> unused_transfers_indices; std::vector<size_t> unused_dust_indices; + const bool use_rct = use_fork_rules(4, 0); + + // gather all our dust and non dust outputs + for (size_t i = 0; i < m_transfers.size(); ++i) + { + const transfer_details& td = m_transfers[i]; + if (!td.m_spent && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td)) + { + if (below == 0 || td.amount() < below) + { + if (td.is_rct() || is_valid_decomposed_amount(td.amount())) + unused_transfers_indices.push_back(i); + else + unused_dust_indices.push_back(i); + } + } + } + + return create_transactions_from(address, unused_transfers_indices, unused_dust_indices, fake_outs_count, unlock_time, priority, extra, trusted_daemon); +} + +std::vector<wallet2::pending_tx> wallet2::create_transactions_from(const cryptonote::account_public_address &address, std::vector<size_t> unused_transfers_indices, std::vector<size_t> unused_dust_indices, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t> extra, bool trusted_daemon) +{ uint64_t accumulated_fee, accumulated_outputs, accumulated_change; struct TX { - std::list<transfer_container::iterator> selected_transfers; + std::list<size_t> selected_transfers; std::vector<cryptonote::tx_destination_entry> dsts; cryptonote::transaction tx; pending_tx ptx; @@ -3500,24 +4619,12 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_all(const cryptono std::vector<TX> txes; uint64_t needed_fee, available_for_fee = 0; uint64_t upper_transaction_size_limit = get_upper_tranaction_size_limit(); - const bool use_rct = use_fork_rules(4, 0); + std::vector<std::vector<get_outs_entry>> outs; - const bool use_new_fee = use_fork_rules(3, -720 * 14); - const uint64_t fee_per_kb = use_new_fee ? FEE_PER_KB : FEE_PER_KB_OLD; - const uint64_t fee_multiplier = get_fee_multiplier(priority, use_new_fee); + const bool use_rct = fake_outs_count > 0 && use_fork_rules(4, 0); + const uint64_t fee_per_kb = get_per_kb_fee(); + const uint64_t fee_multiplier = get_fee_multiplier(priority, get_fee_algorithm()); - // gather all our dust and non dust outputs - for (size_t i = 0; i < m_transfers.size(); ++i) - { - const transfer_details& td = m_transfers[i]; - if (!td.m_spent && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td)) - { - if (td.is_rct() || is_valid_decomposed_amount(td.amount())) - unused_transfers_indices.push_back(i); - else - unused_dust_indices.push_back(i); - } - } LOG_PRINT_L2("Starting with " << unused_transfers_indices.size() << " non-dust outputs and " << unused_dust_indices.size() << " dust outputs"); if (unused_dust_indices.empty() && unused_transfers_indices.empty()) @@ -3543,18 +4650,17 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_all(const cryptono LOG_PRINT_L2("Picking output " << idx << ", amount " << print_money(td.amount())); // add this output to the list to spend - tx.selected_transfers.push_back(m_transfers.begin() + idx); + tx.selected_transfers.push_back(idx); uint64_t available_amount = td.amount(); accumulated_outputs += available_amount; + // clear any fake outs we'd already gathered, since we'll need a new set + outs.clear(); + // here, check if we need to sent tx and start a new one LOG_PRINT_L2("Considering whether to create a tx now, " << tx.selected_transfers.size() << " inputs, tx limit " << upper_transaction_size_limit); - size_t estimated_rct_tx_size; - if (use_rct) - estimated_rct_tx_size = estimate_rct_tx_size(tx.selected_transfers.size(), fake_outs_count, tx.dsts.size() + 1); - else - estimated_rct_tx_size = tx.selected_transfers.size() * (fake_outs_count+1) * APPROXIMATE_INPUT_BYTES; + const size_t estimated_rct_tx_size = estimate_tx_size(use_rct, tx.selected_transfers.size(), fake_outs_count, tx.dsts.size() + 1); bool try_tx = (unused_dust_indices.empty() && unused_transfers_indices.empty()) || ( estimated_rct_tx_size >= TX_SIZE_TARGET(upper_transaction_size_limit)); if (try_tx) { @@ -3568,15 +4674,15 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_all(const cryptono LOG_PRINT_L2("Trying to create a tx now, with " << tx.dsts.size() << " destinations and " << tx.selected_transfers.size() << " outputs"); if (use_rct) - transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, unlock_time, needed_fee, extra, + transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, test_tx, test_ptx); else - transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, unlock_time, needed_fee, extra, + transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx); auto txBlob = t_serializable_object_to_blob(test_ptx.tx); needed_fee = calculate_fee(fee_per_kb, txBlob, fee_multiplier); available_for_fee = test_ptx.fee + test_ptx.dests[0].amount + test_ptx.change_dts.amount; - LOG_PRINT_L2("Made a " << txBlob.size() << " kB tx, with " << print_money(available_for_fee) << " available for fee (" << + LOG_PRINT_L2("Made a " << ((txBlob.size() + 1023) / 1024) << " kB tx, with " << print_money(available_for_fee) << " available for fee (" << print_money(needed_fee) << " needed)"); THROW_WALLET_EXCEPTION_IF(needed_fee > available_for_fee, error::wallet_internal_error, "Transaction cannot pay for itself"); @@ -3585,10 +4691,10 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_all(const cryptono LOG_PRINT_L2("We made a tx, adjusting fee and saving it"); tx.dsts[0].amount = available_for_fee - needed_fee; if (use_rct) - transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, unlock_time, needed_fee, extra, + transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, test_tx, test_ptx); else - transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, unlock_time, needed_fee, extra, + transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx); txBlob = t_serializable_object_to_blob(test_ptx.tx); needed_fee = calculate_fee(fee_per_kb, txBlob, fee_multiplier); @@ -3620,8 +4726,8 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_all(const cryptono { TX &tx = *i; uint64_t tx_money = 0; - for (std::list<transfer_container::iterator>::const_iterator mi = tx.selected_transfers.begin(); mi != tx.selected_transfers.end(); ++mi) - tx_money += (*mi)->amount(); + for (size_t idx: tx.selected_transfers) + tx_money += m_transfers[idx].amount(); LOG_PRINT_L1(" Transaction " << (1+std::distance(txes.begin(), i)) << "/" << txes.size() << ": " << (tx.bytes+1023)/1024 << " kB, sending " << print_money(tx_money) << " in " << tx.selected_transfers.size() << " outputs to " << tx.dsts.size() << " destination(s), including " << @@ -3647,151 +4753,22 @@ uint64_t wallet2::unlocked_dust_balance(const tx_dust_policy &dust_policy) const } return money; } - -template<typename T> -void wallet2::transfer_from(const std::vector<size_t> &outs, size_t num_outputs, uint64_t unlock_time, uint64_t needed_fee, T destination_split_strategy, const tx_dust_policy& dust_policy, const std::vector<uint8_t> &extra, cryptonote::transaction& tx, pending_tx &ptx) -{ - using namespace cryptonote; - - uint64_t upper_transaction_size_limit = get_upper_tranaction_size_limit(); - - // select all dust inputs for transaction - // throw if there are none - uint64_t money = 0; - std::list<transfer_container::iterator> selected_transfers; -#if 1 - for (size_t n = 0; n < outs.size(); ++n) - { - const transfer_details& td = m_transfers[outs[n]]; - if (!td.m_spent) - { - selected_transfers.push_back (m_transfers.begin() + outs[n]); - money += td.amount(); - if (selected_transfers.size() >= num_outputs) - break; - } - } -#else - for (transfer_container::iterator i = m_transfers.begin(); i != m_transfers.end(); ++i) - { - const transfer_details& td = *i; - if (!td.m_spent && (td.amount() < dust_policy.dust_threshold || !is_valid_decomposed_amount(td.amount())) && is_transfer_unlocked(td)) - { - selected_transfers.push_back (i); - money += td.amount(); - if (selected_transfers.size() >= num_outputs) - break; - } - } -#endif - - // we don't allow no output to self, easier, but one may want to burn the dust if = fee - THROW_WALLET_EXCEPTION_IF(money <= needed_fee, error::not_enough_money, money, needed_fee, needed_fee); - - typedef cryptonote::tx_source_entry::output_entry tx_output_entry; - - //prepare inputs - size_t i = 0; - std::vector<cryptonote::tx_source_entry> sources; - BOOST_FOREACH(transfer_container::iterator it, selected_transfers) - { - sources.resize(sources.size()+1); - cryptonote::tx_source_entry& src = sources.back(); - transfer_details& td = *it; - src.amount = td.amount(); - src.rct = td.is_rct(); - - //paste real transaction to the random index - auto it_to_insert = std::find_if(src.outputs.begin(), src.outputs.end(), [&](const tx_output_entry& a) - { - return a.first >= td.m_global_output_index; - }); - 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.mask = rct::commit(td.amount(), td.m_mask); - auto interted_it = src.outputs.insert(it_to_insert, real_oe); - src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_tx); - src.real_output = interted_it - src.outputs.begin(); - src.real_output_in_tx_index = td.m_internal_output_index; - detail::print_source_entry(src); - ++i; - } - - cryptonote::tx_destination_entry change_dts = AUTO_VAL_INIT(change_dts); - - std::vector<cryptonote::tx_destination_entry> dsts; - uint64_t money_back = money - needed_fee; - if (dust_policy.dust_threshold > 0) - money_back = money_back - money_back % dust_policy.dust_threshold; - dsts.push_back(cryptonote::tx_destination_entry(money_back, m_account_public_address)); - std::vector<cryptonote::tx_destination_entry> splitted_dsts, dust; - destination_split_strategy(dsts, change_dts, dust_policy.dust_threshold, splitted_dsts, dust); - BOOST_FOREACH(auto& d, dust) { - THROW_WALLET_EXCEPTION_IF(dust_policy.dust_threshold < d.amount, error::wallet_internal_error, "invalid dust value: dust = " + - std::to_string(d.amount) + ", dust_threshold = " + std::to_string(dust_policy.dust_threshold)); - } - - crypto::secret_key tx_key; - bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), sources, splitted_dsts, extra, tx, unlock_time, tx_key); - THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sources, splitted_dsts, unlock_time, m_testnet); - THROW_WALLET_EXCEPTION_IF(upper_transaction_size_limit <= get_object_blobsize(tx), error::tx_too_big, tx, upper_transaction_size_limit); - - std::string key_images; - bool all_are_txin_to_key = std::all_of(tx.vin.begin(), tx.vin.end(), [&](const txin_v& s_e) -> bool - { - CHECKED_GET_SPECIFIC_VARIANT(s_e, const txin_to_key, in, false); - key_images += boost::to_string(in.k_image) + " "; - return true; - }); - THROW_WALLET_EXCEPTION_IF(!all_are_txin_to_key, error::unexpected_txin_type, tx); - - ptx.key_images = key_images; - ptx.fee = money - money_back; - ptx.dust = 0; - ptx.tx = tx; - ptx.change_dts = change_dts; - ptx.selected_transfers = selected_transfers; - ptx.tx_key = tx_key; - ptx.dests = dsts; -} - //---------------------------------------------------------------------------------------------------- void wallet2::get_hard_fork_info(uint8_t version, uint64_t &earliest_height) { - epee::json_rpc::request<cryptonote::COMMAND_RPC_HARD_FORK_INFO::request> req_t = AUTO_VAL_INIT(req_t); - epee::json_rpc::response<cryptonote::COMMAND_RPC_HARD_FORK_INFO::response, std::string> resp_t = AUTO_VAL_INIT(resp_t); - - m_daemon_rpc_mutex.lock(); - req_t.jsonrpc = "2.0"; - req_t.id = epee::serialization::storage_entry(0); - req_t.method = "hard_fork_info"; - req_t.params.version = version; - bool r = net_utils::invoke_http_json_remote_command2(m_daemon_address + "/json_rpc", req_t, resp_t, m_http_client); - m_daemon_rpc_mutex.unlock(); - CHECK_AND_ASSERT_THROW_MES(r, "Failed to connect to daemon"); - CHECK_AND_ASSERT_THROW_MES(resp_t.result.status != CORE_RPC_STATUS_BUSY, "Failed to connect to daemon"); - CHECK_AND_ASSERT_THROW_MES(resp_t.result.status == CORE_RPC_STATUS_OK, "Failed to get hard fork status"); - - earliest_height = resp_t.result.earliest_height; + boost::optional<std::string> result = m_node_rpc_proxy.get_earliest_height(version, earliest_height); + throw_on_rpc_response_error(result, "get_hard_fork_info"); } //---------------------------------------------------------------------------------------------------- bool wallet2::use_fork_rules(uint8_t version, int64_t early_blocks) { - cryptonote::COMMAND_RPC_GET_HEIGHT::request req = AUTO_VAL_INIT(req); - cryptonote::COMMAND_RPC_GET_HEIGHT::response res = AUTO_VAL_INIT(res); + 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"); + result = m_node_rpc_proxy.get_earliest_height(version, earliest_height); + throw_on_rpc_response_error(result, "get_hard_fork_info"); - m_daemon_rpc_mutex.lock(); - bool r = net_utils::invoke_http_json_remote_command2(m_daemon_address + "/getheight", req, res, m_http_client); - m_daemon_rpc_mutex.unlock(); - CHECK_AND_ASSERT_MES(r, false, "Failed to connect to daemon"); - CHECK_AND_ASSERT_MES(res.status != CORE_RPC_STATUS_BUSY, false, "Failed to connect to daemon"); - CHECK_AND_ASSERT_MES(res.status == CORE_RPC_STATUS_OK, false, "Failed to get current blockchain height"); - - uint64_t earliest_height; - get_hard_fork_info(version, earliest_height); // can throw - - bool close_enough = res.height >= earliest_height - early_blocks; // start using the rules that many blocks beforehand + bool close_enough = height >= earliest_height - early_blocks; // start using the rules that many blocks beforehand if (close_enough) LOG_PRINT_L2("Using v" << (unsigned)version << " rules"); else @@ -3803,7 +4780,7 @@ uint64_t wallet2::get_upper_tranaction_size_limit() { if (m_upper_transaction_size_limit > 0) return m_upper_transaction_size_limit; - uint64_t full_reward_zone = use_fork_rules(2, 10) ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V2 : CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V1; + uint64_t full_reward_zone = use_fork_rules(5, 10) ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 : use_fork_rules(2, 10) ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V2 : CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V1; return ((full_reward_zone * 125) / 100) - CRYPTONOTE_COINBASE_BLOB_RESERVED_SIZE; } //---------------------------------------------------------------------------------------------------- @@ -3829,7 +4806,7 @@ std::vector<uint64_t> wallet2::get_unspent_amounts_vector() for (const auto &td: m_transfers) { if (!td.m_spent) - set.insert(td.amount()); + set.insert(td.is_rct() ? 0 : td.amount()); } std::vector<uint64_t> vector; vector.reserve(set.size()); @@ -3840,7 +4817,7 @@ std::vector<uint64_t> wallet2::get_unspent_amounts_vector() return vector; } //---------------------------------------------------------------------------------------------------- -std::vector<size_t> wallet2::select_available_outputs_from_histogram(uint64_t count, bool atleast, bool unlocked, bool trusted_daemon) +std::vector<size_t> wallet2::select_available_outputs_from_histogram(uint64_t count, bool atleast, bool unlocked, bool allow_rct, bool trusted_daemon) { epee::json_rpc::request<cryptonote::COMMAND_RPC_GET_OUTPUT_HISTOGRAM::request> req_t = AUTO_VAL_INIT(req_t); epee::json_rpc::response<cryptonote::COMMAND_RPC_GET_OUTPUT_HISTOGRAM::response, std::string> resp_t = AUTO_VAL_INIT(resp_t); @@ -3853,9 +4830,9 @@ std::vector<size_t> wallet2::select_available_outputs_from_histogram(uint64_t co req_t.params.min_count = count; req_t.params.max_count = 0; req_t.params.unlocked = unlocked; - bool r = net_utils::invoke_http_json_remote_command2(m_daemon_address + "/json_rpc", req_t, resp_t, m_http_client); + bool r = net_utils::invoke_http_json("/json_rpc", req_t, resp_t, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); - THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "select_available_unmixable_outputs"); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "select_available_outputs_from_histogram"); THROW_WALLET_EXCEPTION_IF(resp_t.result.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_output_histogram"); THROW_WALLET_EXCEPTION_IF(resp_t.result.status != CORE_RPC_STATUS_OK, error::get_histogram_error, resp_t.result.status); @@ -3865,10 +4842,10 @@ std::vector<size_t> wallet2::select_available_outputs_from_histogram(uint64_t co mixable.insert(i.amount); } - return select_available_outputs([mixable, atleast](const transfer_details &td) { - if (td.is_rct()) + return select_available_outputs([mixable, atleast, allow_rct](const transfer_details &td) { + if (!allow_rct && td.is_rct()) return false; - const uint64_t amount = td.amount(); + const uint64_t amount = td.is_rct() ? 0 : td.amount(); if (atleast) { if (mixable.find(amount) != mixable.end()) return true; @@ -3892,7 +4869,7 @@ uint64_t wallet2::get_num_rct_outputs() req_t.params.amounts.push_back(0); req_t.params.min_count = 0; req_t.params.max_count = 0; - bool r = net_utils::invoke_http_json_remote_command2(m_daemon_address + "/json_rpc", req_t, resp_t, m_http_client); + bool r = net_utils::invoke_http_json("/json_rpc", req_t, resp_t, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_num_rct_outputs"); THROW_WALLET_EXCEPTION_IF(resp_t.result.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_output_histogram"); @@ -3900,19 +4877,27 @@ uint64_t wallet2::get_num_rct_outputs() THROW_WALLET_EXCEPTION_IF(resp_t.result.histogram.size() != 1, error::get_histogram_error, "Expected exactly one response"); THROW_WALLET_EXCEPTION_IF(resp_t.result.histogram[0].amount != 0, error::get_histogram_error, "Expected 0 amount"); - return resp_t.result.histogram[0].instances; + return resp_t.result.histogram[0].total_instances; +} +//---------------------------------------------------------------------------------------------------- +const wallet2::transfer_details &wallet2::get_transfer_details(size_t idx) const +{ + THROW_WALLET_EXCEPTION_IF(idx >= m_transfers.size(), error::wallet_internal_error, "Bad transfer index"); + return m_transfers[idx]; } //---------------------------------------------------------------------------------------------------- std::vector<size_t> wallet2::select_available_unmixable_outputs(bool trusted_daemon) { // request all outputs with less than 3 instances - return select_available_outputs_from_histogram(3, false, true, trusted_daemon); + const size_t min_mixin = use_fork_rules(5, 10) ? 4 : 2; // v5 increases min mixin from 2 to 4 + return select_available_outputs_from_histogram(min_mixin + 1, false, true, false, trusted_daemon); } //---------------------------------------------------------------------------------------------------- std::vector<size_t> wallet2::select_available_mixable_outputs(bool trusted_daemon) { // request all outputs with at least 3 instances, so we can use mixin 2 with - return select_available_outputs_from_histogram(3, true, true, trusted_daemon); + const size_t min_mixin = use_fork_rules(5, 10) ? 4 : 2; // v5 increases min mixin from 2 to 4 + return select_available_outputs_from_histogram(min_mixin + 1, true, true, true, trusted_daemon); } //---------------------------------------------------------------------------------------------------- std::vector<wallet2::pending_tx> wallet2::create_unmixable_sweep_transactions(bool trusted_daemon) @@ -3921,8 +4906,7 @@ std::vector<wallet2::pending_tx> wallet2::create_unmixable_sweep_transactions(bo const bool hf1_rules = use_fork_rules(2, 10); // first hard fork has version 2 tx_dust_policy dust_policy(hf1_rules ? 0 : ::config::DEFAULT_DUST_THRESHOLD); - const bool use_new_fee = use_fork_rules(3, -720 * 14); - const uint64_t fee_per_kb = use_new_fee ? FEE_PER_KB : FEE_PER_KB_OLD; + const uint64_t fee_per_kb = get_per_kb_fee(); // may throw std::vector<size_t> unmixable_outputs = select_available_unmixable_outputs(trusted_daemon); @@ -3933,100 +4917,17 @@ std::vector<wallet2::pending_tx> wallet2::create_unmixable_sweep_transactions(bo return std::vector<wallet2::pending_tx>(); } - // failsafe split attempt counter - size_t attempt_count = 0; - - for(attempt_count = 1; ;attempt_count++) + // split in "dust" and "non dust" to make it easier to select outputs + std::vector<size_t> unmixable_transfer_outputs, unmixable_dust_outputs; + for (auto n: unmixable_outputs) { - size_t num_tx = 0.5 + pow(1.7,attempt_count-1); - size_t num_outputs_per_tx = (num_dust_outputs + num_tx - 1) / num_tx; - - std::vector<pending_tx> ptx_vector; - try - { - // for each new tx - for (size_t i=0; i<num_tx;++i) - { - cryptonote::transaction tx; - pending_tx ptx; - std::vector<uint8_t> extra; - - // loop until fee is met without increasing tx size to next KB boundary. - uint64_t needed_fee = 0; - do - { - transfer_from(unmixable_outputs, num_outputs_per_tx, (uint64_t)0 /* unlock_time */, 0, detail::digit_split_strategy, dust_policy, extra, tx, ptx); - auto txBlob = t_serializable_object_to_blob(ptx.tx); - needed_fee = calculate_fee(fee_per_kb, txBlob, 1); - - // reroll the tx with the actual amount minus the fee - // if there's not enough for the fee, it'll throw - transfer_from(unmixable_outputs, num_outputs_per_tx, (uint64_t)0 /* unlock_time */, needed_fee, detail::digit_split_strategy, dust_policy, extra, tx, ptx); - txBlob = t_serializable_object_to_blob(ptx.tx); - needed_fee = calculate_fee(fee_per_kb, txBlob, 1); - } while (ptx.fee < needed_fee); - - ptx_vector.push_back(ptx); - - // mark transfers to be used as "spent" - BOOST_FOREACH(transfer_container::iterator it, ptx.selected_transfers) - { - set_spent(*it, 0); - } - } - - // if we made it this far, we've selected our transactions. committing them will mark them spent, - // so this is a failsafe in case they don't go through - // unmark pending tx transfers as spent - for (auto & ptx : ptx_vector) - { - // mark transfers to be used as not spent - BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) - { - set_unspent(*it2); - } - } - - // if we made it this far, we're OK to actually send the transactions - return ptx_vector; - - } - // only catch this here, other exceptions need to pass through to the calling function - catch (const tools::error::tx_too_big& e) - { - - // unmark pending tx transfers as spent - for (auto & ptx : ptx_vector) - { - // mark transfers to be used as not spent - BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) - { - set_unspent(*it2); - } - } - - if (attempt_count >= MAX_SPLIT_ATTEMPTS) - { - throw; - } - } - catch (...) - { - // in case of some other exception, make sure any tx in queue are marked unspent again - - // unmark pending tx transfers as spent - for (auto & ptx : ptx_vector) - { - // mark transfers to be used as not spent - BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) - { - set_unspent(*it2); - } - } - - throw; - } + if (m_transfers[n].amount() < fee_per_kb) + unmixable_dust_outputs.push_back(n); + else + unmixable_transfer_outputs.push_back(n); } + + return create_transactions_from(m_account_public_address, unmixable_transfer_outputs, unmixable_dust_outputs, 0 /*fake_outs_count */, 0 /* unlock_time */, 1 /*priority */, std::vector<uint8_t>(), trusted_daemon); } bool wallet2::get_tx_key(const crypto::hash &txid, crypto::secret_key &tx_key) const @@ -4055,23 +4956,38 @@ std::string wallet2::get_daemon_address() const uint64_t wallet2::get_daemon_blockchain_height(string &err) { - // XXX: DRY violation. copy-pasted from simplewallet.cpp:get_daemon_blockchain_height() - // consider to move it from simplewallet to wallet2 ? - COMMAND_RPC_GET_HEIGHT::request req; - COMMAND_RPC_GET_HEIGHT::response res = boost::value_initialized<COMMAND_RPC_GET_HEIGHT::response>(); + uint64_t height; + + boost::optional<std::string> result = m_node_rpc_proxy.get_height(height); + if (result) + { + err = *result; + return 0; + } + + err = ""; + return height; +} + +uint64_t wallet2::get_daemon_blockchain_target_height(string &err) +{ + epee::json_rpc::request<cryptonote::COMMAND_RPC_GET_INFO::request> req_t = AUTO_VAL_INIT(req_t); + epee::json_rpc::response<cryptonote::COMMAND_RPC_GET_INFO::response, std::string> resp_t = AUTO_VAL_INIT(resp_t); m_daemon_rpc_mutex.lock(); - bool ok = net_utils::invoke_http_json_remote_command2(m_daemon_address + "/getheight", req, res, m_http_client); + req_t.jsonrpc = "2.0"; + req_t.id = epee::serialization::storage_entry(0); + req_t.method = "get_info"; + bool ok = net_utils::invoke_http_json("/json_rpc", req_t, resp_t, m_http_client); m_daemon_rpc_mutex.unlock(); - // XXX: DRY violation. copy-pasted from simplewallet.cpp:interpret_rpc_response() if (ok) { - if (res.status == CORE_RPC_STATUS_BUSY) + if (resp_t.result.status == CORE_RPC_STATUS_BUSY) { err = "daemon is busy. Please try again later."; } - else if (res.status != CORE_RPC_STATUS_OK) + else if (resp_t.result.status != CORE_RPC_STATUS_OK) { - err = res.status; + err = resp_t.result.status; } else // success, cleaning up error message { @@ -4082,7 +4998,22 @@ uint64_t wallet2::get_daemon_blockchain_height(string &err) { err = "possibly lost connection to daemon"; } - return res.height; + return resp_t.result.target_height; +} + +uint64_t wallet2::get_approximate_blockchain_height() const +{ + if (m_testnet) return 0; + // time of v2 fork + const time_t fork_time = 1458748658; + // v2 fork block + const uint64_t fork_block = 1009827; + // avg seconds per block + const int seconds_per_block = DIFFICULTY_TARGET_V2; + // Calculated blockchain height + uint64_t approx_blockchain_height = fork_block + (time(NULL) - fork_time)/seconds_per_block; + LOG_PRINT_L2("Calculated blockchain height: " << approx_blockchain_height); + return approx_blockchain_height; } void wallet2::set_tx_note(const crypto::hash &txid, const std::string ¬e) @@ -4131,6 +5062,74 @@ bool wallet2::verify(const std::string &data, const cryptonote::account_public_a return crypto::check_signature(hash, address.m_spend_public_key, s); } //---------------------------------------------------------------------------------------------------- +crypto::public_key wallet2::get_tx_pub_key_from_received_outs(const tools::wallet2::transfer_details &td) const +{ + std::vector<tx_extra_field> tx_extra_fields; + if(!parse_tx_extra(td.m_tx.extra, tx_extra_fields)) + { + // Extra may only be partially parsed, it's OK if tx_extra_fields contains public key + } + + // Due to a previous bug, there might be more than one tx pubkey in extra, one being + // the result of a previously discarded signature. + // For speed, since scanning for outputs is a slow process, we check whether extra + // contains more than one pubkey. If not, the first one is returned. If yes, they're + // checked for whether they yield at least one output + tx_extra_pub_key pub_key_field; + THROW_WALLET_EXCEPTION_IF(!find_tx_extra_field_by_type(tx_extra_fields, pub_key_field, 0), error::wallet_internal_error, + "Public key wasn't found in the transaction extra"); + const crypto::public_key tx_pub_key = pub_key_field.pub_key; + bool two_found = find_tx_extra_field_by_type(tx_extra_fields, pub_key_field, 1); + if (!two_found) { + // easy case, just one found + return tx_pub_key; + } + + // more than one, loop and search + const cryptonote::account_keys& keys = m_account.get_keys(); + size_t pk_index = 0; + while (find_tx_extra_field_by_type(tx_extra_fields, pub_key_field, pk_index++)) { + const crypto::public_key tx_pub_key = pub_key_field.pub_key; + crypto::key_derivation derivation; + generate_key_derivation(tx_pub_key, keys.m_view_secret_key, derivation); + + for (size_t i = 0; i < td.m_tx.vout.size(); ++i) + { + uint64_t money_transfered = 0; + bool error = false, received = false; + check_acc_out_precomp(keys.m_account_address.m_spend_public_key, td.m_tx.vout[i], derivation, i, received, money_transfered, error); + if (!error && received) + return tx_pub_key; + } + } + + // we found no key yielding an output + THROW_WALLET_EXCEPTION_IF(true, error::wallet_internal_error, + "Public key yielding at least one output wasn't found in the transaction extra"); + return cryptonote::null_pkey; +} + +bool wallet2::export_key_images(const std::string filename) +{ + std::vector<std::pair<crypto::key_image, crypto::signature>> ski = export_key_images(); + std::string magic(KEY_IMAGE_EXPORT_FILE_MAGIC, strlen(KEY_IMAGE_EXPORT_FILE_MAGIC)); + const cryptonote::account_public_address &keys = get_account().get_keys().m_account_address; + + std::string data; + data += std::string((const char *)&keys.m_spend_public_key, sizeof(crypto::public_key)); + data += std::string((const char *)&keys.m_view_public_key, sizeof(crypto::public_key)); + for (const auto &i: ski) + { + data += std::string((const char *)&i.first, sizeof(crypto::key_image)); + data += std::string((const char *)&i.second, sizeof(crypto::signature)); + } + + // encrypt data, keep magic plaintext + std::string ciphertext = encrypt_with_view_secret_key(data); + return epee::file_io_utils::save_string_to_file(filename, magic + ciphertext); +} + +//---------------------------------------------------------------------------------------------------- std::vector<std::pair<crypto::key_image, crypto::signature>> wallet2::export_key_images() const { std::vector<std::pair<crypto::key_image, crypto::signature>> ski; @@ -4156,16 +5155,15 @@ std::vector<std::pair<crypto::key_image, crypto::signature>> wallet2::export_key { // Extra may only be partially parsed, it's OK if tx_extra_fields contains public key } - tx_extra_pub_key pub_key_field; - THROW_WALLET_EXCEPTION_IF(!find_tx_extra_field_by_type(tx_extra_fields, pub_key_field), error::wallet_internal_error, - "Public key wasn't found in the transaction extra"); - crypto::public_key tx_pub_key = pub_key_field.pub_key; + + crypto::public_key tx_pub_key = get_tx_pub_key_from_received_outs(td); // generate ephemeral secret key crypto::key_image ki; cryptonote::keypair in_ephemeral; cryptonote::generate_key_image_helper(m_account.get_keys(), tx_pub_key, td.m_internal_output_index, in_ephemeral, ki); - THROW_WALLET_EXCEPTION_IF(ki != td.m_key_image, + + THROW_WALLET_EXCEPTION_IF(td.m_key_image_known && ki != td.m_key_image, error::wallet_internal_error, "key_image generated not matched with cached key image"); THROW_WALLET_EXCEPTION_IF(in_ephemeral.pub != pkey, error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key"); @@ -4181,6 +5179,70 @@ std::vector<std::pair<crypto::key_image, crypto::signature>> wallet2::export_key } return ski; } + +uint64_t wallet2::import_key_images(const std::string &filename, uint64_t &spent, uint64_t &unspent) +{ + std::string data; + bool r = epee::file_io_utils::load_file_to_string(filename, data); + + if (!r) + { + fail_msg_writer() << tr("failed to read file ") << filename; + return 0; + } + const size_t magiclen = strlen(KEY_IMAGE_EXPORT_FILE_MAGIC); + if (data.size() < magiclen || memcmp(data.data(), KEY_IMAGE_EXPORT_FILE_MAGIC, magiclen)) + { + fail_msg_writer() << "Bad key image export file magic in " << filename; + return 0; + } + + try + { + data = decrypt_with_view_secret_key(std::string(data, magiclen)); + } + catch (const std::exception &e) + { + fail_msg_writer() << "Failed to decrypt " << filename << ": " << e.what(); + return 0; + } + + const size_t headerlen = 2 * sizeof(crypto::public_key); + if (data.size() < headerlen) + { + fail_msg_writer() << "Bad data size from file " << filename; + return 0; + } + const crypto::public_key &public_spend_key = *(const crypto::public_key*)&data[0]; + const crypto::public_key &public_view_key = *(const crypto::public_key*)&data[sizeof(crypto::public_key)]; + const cryptonote::account_public_address &keys = get_account().get_keys().m_account_address; + if (public_spend_key != keys.m_spend_public_key || public_view_key != keys.m_view_public_key) + { + fail_msg_writer() << "Key images from " << filename << " are for a different account"; + return 0; + } + + const size_t record_size = sizeof(crypto::key_image) + sizeof(crypto::signature); + if ((data.size() - headerlen) % record_size) + { + fail_msg_writer() << "Bad data size from file " << filename; + return 0; + } + size_t nki = (data.size() - headerlen) / record_size; + + std::vector<std::pair<crypto::key_image, crypto::signature>> ski; + ski.reserve(nki); + for (size_t n = 0; n < nki; ++n) + { + crypto::key_image key_image = *reinterpret_cast<const crypto::key_image*>(&data[headerlen + n * record_size]); + crypto::signature signature = *reinterpret_cast<const crypto::signature*>(&data[headerlen + n * record_size + sizeof(crypto::key_image)]); + + ski.push_back(std::make_pair(key_image, signature)); + } + + return import_key_images(ski, spent, unspent); +} + //---------------------------------------------------------------------------------------------------- uint64_t wallet2::import_key_images(const std::vector<std::pair<crypto::key_image, crypto::signature>> &signed_key_images, uint64_t &spent, uint64_t &unspent) { @@ -4221,10 +5283,14 @@ uint64_t wallet2::import_key_images(const std::vector<std::pair<crypto::key_imag } for (size_t n = 0; n < signed_key_images.size(); ++n) + { m_transfers[n].m_key_image = signed_key_images[n].first; + m_key_images[m_transfers[n].m_key_image] = n; + m_transfers[n].m_key_image_known = true; + } m_daemon_rpc_mutex.lock(); - bool r = epee::net_utils::invoke_http_json_remote_command2(m_daemon_address + "/is_key_image_spent", req, daemon_resp, m_http_client, 200000); + bool r = epee::net_utils::invoke_http_json("/is_key_image_spent", req, daemon_resp, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "is_key_image_spent"); THROW_WALLET_EXCEPTION_IF(daemon_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "is_key_image_spent"); @@ -4252,6 +5318,336 @@ uint64_t wallet2::import_key_images(const std::vector<std::pair<crypto::key_imag return m_transfers[signed_key_images.size() - 1].m_block_height; } //---------------------------------------------------------------------------------------------------- +std::vector<tools::wallet2::transfer_details> wallet2::export_outputs() const +{ + std::vector<tools::wallet2::transfer_details> outs; + + outs.reserve(m_transfers.size()); + for (size_t n = 0; n < m_transfers.size(); ++n) + { + const transfer_details &td = m_transfers[n]; + + outs.push_back(td); + } + + return outs; +} +//---------------------------------------------------------------------------------------------------- +size_t wallet2::import_outputs(const std::vector<tools::wallet2::transfer_details> &outputs) +{ + m_transfers.clear(); + m_transfers.reserve(outputs.size()); + for (size_t i = 0; i < outputs.size(); ++i) + { + transfer_details td = outputs[i]; + + // the hot wallet wouldn't have known about key images (except if we already exported them) + cryptonote::keypair in_ephemeral; + std::vector<tx_extra_field> tx_extra_fields; + tx_extra_pub_key pub_key_field; + + THROW_WALLET_EXCEPTION_IF(td.m_tx.vout.empty(), error::wallet_internal_error, "tx with no outputs at index " + boost::lexical_cast<std::string>(i)); + THROW_WALLET_EXCEPTION_IF(!parse_tx_extra(td.m_tx.extra, tx_extra_fields), error::wallet_internal_error, + "Transaction extra has unsupported format at index " + boost::lexical_cast<std::string>(i)); + crypto::public_key tx_pub_key = get_tx_pub_key_from_received_outs(td); + + cryptonote::generate_key_image_helper(m_account.get_keys(), tx_pub_key, td.m_internal_output_index, in_ephemeral, td.m_key_image); + td.m_key_image_known = true; + THROW_WALLET_EXCEPTION_IF(in_ephemeral.pub != boost::get<cryptonote::txout_to_key>(td.m_tx.vout[td.m_internal_output_index].target).key, + error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key at index " + boost::lexical_cast<std::string>(i)); + + m_key_images[td.m_key_image] = m_transfers.size(); + m_pub_keys[td.get_public_key()] = m_transfers.size(); + m_transfers.push_back(td); + } + + return m_transfers.size(); +} +//---------------------------------------------------------------------------------------------------- +std::string wallet2::encrypt(const std::string &plaintext, const crypto::secret_key &skey, bool authenticated) const +{ + crypto::chacha8_key key; + crypto::generate_chacha8_key(&skey, sizeof(skey), key); + std::string ciphertext; + crypto::chacha8_iv iv = crypto::rand<crypto::chacha8_iv>(); + ciphertext.resize(plaintext.size() + sizeof(iv) + (authenticated ? sizeof(crypto::signature) : 0)); + crypto::chacha8(plaintext.data(), plaintext.size(), 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(signature), hash); + crypto::public_key pkey; + crypto::secret_key_to_public_key(skey, pkey); + crypto::signature &signature = *(crypto::signature*)&ciphertext[ciphertext.size() - sizeof(crypto::signature)]; + crypto::generate_signature(hash, pkey, skey, signature); + } + return ciphertext; +} +//---------------------------------------------------------------------------------------------------- +std::string wallet2::encrypt_with_view_secret_key(const std::string &plaintext, bool authenticated) const +{ + return encrypt(plaintext, get_account().get_keys().m_view_secret_key, authenticated); +} +//---------------------------------------------------------------------------------------------------- +std::string wallet2::decrypt(const std::string &ciphertext, const crypto::secret_key &skey, bool authenticated) const +{ + const size_t prefix_size = sizeof(chacha8_iv) + (authenticated ? sizeof(crypto::signature) : 0); + THROW_WALLET_EXCEPTION_IF(ciphertext.size() < prefix_size, + error::wallet_internal_error, "Unexpected ciphertext size"); + + crypto::chacha8_key key; + crypto::generate_chacha8_key(&skey, sizeof(skey), key); + const crypto::chacha8_iv &iv = *(const crypto::chacha8_iv*)&ciphertext[0]; + std::string plaintext; + plaintext.resize(ciphertext.size() - prefix_size); + if (authenticated) + { + crypto::hash hash; + crypto::cn_fast_hash(ciphertext.data(), ciphertext.size() - sizeof(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)]; + THROW_WALLET_EXCEPTION_IF(!crypto::check_signature(hash, pkey, signature), + error::wallet_internal_error, "Failed to authenticate criphertext"); + } + crypto::chacha8(ciphertext.data() + sizeof(iv), ciphertext.size() - prefix_size, key, iv, &plaintext[0]); + return plaintext; +} +//---------------------------------------------------------------------------------------------------- +std::string wallet2::decrypt_with_view_secret_key(const std::string &ciphertext, bool authenticated) const +{ + return decrypt(ciphertext, get_account().get_keys().m_view_secret_key, authenticated); +} +//---------------------------------------------------------------------------------------------------- +std::string wallet2::make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, std::string &error) +{ + cryptonote::account_public_address tmp_address; + bool has_payment_id; + crypto::hash8 new_payment_id; + if(!get_account_integrated_address_from_str(tmp_address, has_payment_id, new_payment_id, testnet(), address)) + { + error = std::string("wrong address: ") + address; + return std::string(); + } + + // we want only one payment id + if (has_payment_id && !payment_id.empty()) + { + error = "A single payment id is allowed"; + return std::string(); + } + + if (!payment_id.empty()) + { + crypto::hash pid32; + crypto::hash8 pid8; + if (!wallet2::parse_long_payment_id(payment_id, pid32) && !wallet2::parse_short_payment_id(payment_id, pid8)) + { + error = "Invalid payment id"; + return std::string(); + } + } + + std::string uri = "monero:" + address; + unsigned int n_fields = 0; + + if (!payment_id.empty()) + { + uri += (n_fields++ ? "&" : "?") + std::string("tx_payment_id=") + payment_id; + } + + if (amount > 0) + { + // URI encoded amount is in decimal units, not atomic units + uri += (n_fields++ ? "&" : "?") + std::string("tx_amount=") + cryptonote::print_money(amount); + } + + if (!recipient_name.empty()) + { + uri += (n_fields++ ? "&" : "?") + std::string("recipient_name=") + epee::net_utils::conver_to_url_format(recipient_name); + } + + if (!tx_description.empty()) + { + uri += (n_fields++ ? "&" : "?") + std::string("tx_description=") + epee::net_utils::conver_to_url_format(tx_description); + } + + return uri; +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &amount, std::string &tx_description, std::string &recipient_name, std::vector<std::string> &unknown_parameters, std::string &error) +{ + if (uri.substr(0, 7) != "monero:") + { + error = std::string("URI has wrong scheme (expected \"monero:\"): ") + uri; + return false; + } + + std::string remainder = uri.substr(7); + const char *ptr = strchr(remainder.c_str(), '?'); + address = ptr ? remainder.substr(0, ptr-remainder.c_str()) : remainder; + + cryptonote::account_public_address addr; + bool has_payment_id; + crypto::hash8 new_payment_id; + if(!get_account_integrated_address_from_str(addr, has_payment_id, new_payment_id, testnet(), address)) + { + error = std::string("URI has wrong address: ") + address; + return false; + } + if (!strchr(remainder.c_str(), '?')) + return true; + + std::vector<std::string> arguments; + std::string body = remainder.substr(address.size() + 1); + if (body.empty()) + return true; + boost::split(arguments, body, boost::is_any_of("&")); + std::set<std::string> have_arg; + for (const auto &arg: arguments) + { + std::vector<std::string> kv; + boost::split(kv, arg, boost::is_any_of("=")); + if (kv.size() != 2) + { + error = std::string("URI has wrong parameter: ") + arg; + return false; + } + if (have_arg.find(kv[0]) != have_arg.end()) + { + error = std::string("URI has more than one instance of " + kv[0]); + return false; + } + have_arg.insert(kv[0]); + + if (kv[0] == "tx_amount") + { + amount = 0; + if (!cryptonote::parse_amount(amount, kv[1])) + { + error = std::string("URI has invalid amount: ") + kv[1]; + return false; + } + } + else if (kv[0] == "tx_payment_id") + { + if (has_payment_id) + { + error = "Separate payment id given with an integrated address"; + return false; + } + crypto::hash hash; + crypto::hash8 hash8; + if (!wallet2::parse_long_payment_id(kv[1], hash) && !wallet2::parse_short_payment_id(kv[1], hash8)) + { + error = "Invalid payment id: " + kv[1]; + return false; + } + payment_id = kv[1]; + } + else if (kv[0] == "recipient_name") + { + recipient_name = epee::net_utils::convert_from_url_format(kv[1]); + } + else if (kv[0] == "tx_description") + { + tx_description = epee::net_utils::convert_from_url_format(kv[1]); + } + else + { + unknown_parameters.push_back(arg); + } + } + return true; +} +//---------------------------------------------------------------------------------------------------- +uint64_t wallet2::get_blockchain_height_by_date(uint16_t year, uint8_t month, uint8_t day) +{ + uint32_t version; + if (!check_connection(&version)) + { + throw std::runtime_error("failed to connect to daemon: " + get_daemon_address()); + } + if (version < MAKE_CORE_RPC_VERSION(1, 6)) + { + throw std::runtime_error("this function requires RPC version 1.6 or higher"); + } + std::tm date = { 0, 0, 0, 0, 0, 0, 0, 0 }; + date.tm_year = year - 1900; + date.tm_mon = month - 1; + date.tm_mday = day; + if (date.tm_mon < 0 || 11 < date.tm_mon || date.tm_mday < 1 || 31 < date.tm_mday) + { + throw std::runtime_error("month or day out of range"); + } + uint64_t timestamp_target = std::mktime(&date); + std::string err; + uint64_t height_min = 0; + uint64_t height_max = get_daemon_blockchain_height(err) - 1; + if (!err.empty()) + { + throw std::runtime_error("failed to get blockchain height"); + } + while (true) + { + COMMAND_RPC_GET_BLOCKS_BY_HEIGHT::request req; + COMMAND_RPC_GET_BLOCKS_BY_HEIGHT::response res; + uint64_t height_mid = (height_min + height_max) / 2; + req.heights = + { + height_min, + height_mid, + height_max + }; + bool r = net_utils::invoke_http_bin("/getblocks_by_height.bin", req, res, m_http_client, rpc_timeout); + if (!r || res.status != CORE_RPC_STATUS_OK) + { + std::ostringstream oss; + oss << "failed to get blocks by heights: "; + for (auto height : req.heights) + oss << height << ' '; + oss << endl << "reason: "; + if (!r) + oss << "possibly lost connection to daemon"; + else if (res.status == CORE_RPC_STATUS_BUSY) + oss << "daemon is busy"; + else + oss << res.status; + throw std::runtime_error(oss.str()); + } + cryptonote::block blk_min, blk_mid, blk_max; + if (!parse_and_validate_block_from_blob(res.blocks[0].block, blk_min)) throw std::runtime_error("failed to parse blob at height " + std::to_string(height_min)); + if (!parse_and_validate_block_from_blob(res.blocks[1].block, blk_mid)) throw std::runtime_error("failed to parse blob at height " + std::to_string(height_mid)); + if (!parse_and_validate_block_from_blob(res.blocks[2].block, blk_max)) throw std::runtime_error("failed to parse blob at height " + std::to_string(height_max)); + uint64_t timestamp_min = blk_min.timestamp; + uint64_t timestamp_mid = blk_mid.timestamp; + uint64_t timestamp_max = blk_max.timestamp; + if (!(timestamp_min <= timestamp_mid && timestamp_mid <= timestamp_max)) + { + // the timestamps are not in the chronological order. + // assuming they're sufficiently close to each other, simply return the smallest height + return std::min({height_min, height_mid, height_max}); + } + if (timestamp_target > timestamp_max) + { + throw std::runtime_error("specified date is in the future"); + } + if (timestamp_target <= timestamp_min + 2 * 24 * 60 * 60) // two days of "buffer" period + { + return height_min; + } + if (timestamp_target <= timestamp_mid) + height_max = height_mid; + else + height_min = height_mid; + if (height_max - height_min <= 2 * 24 * 30) // don't divide the height range finer than two days + { + return height_min; + } + } +} +//---------------------------------------------------------------------------------------------------- void wallet2::generate_genesis(cryptonote::block& b) { if (m_testnet) { diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index dd7cd19dc..03c6a431b 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -31,19 +31,22 @@ #pragma once #include <memory> -#include <boost/archive/binary_iarchive.hpp> + +#include <boost/program_options/options_description.hpp> +#include <boost/program_options/variables_map.hpp> #include <boost/serialization/list.hpp> #include <boost/serialization/vector.hpp> #include <atomic> #include "include_base_utils.h" -#include "cryptonote_core/account.h" -#include "cryptonote_core/account_boost_serialization.h" -#include "cryptonote_core/cryptonote_basic_impl.h" +#include "cryptonote_basic/account.h" +#include "cryptonote_basic/account_boost_serialization.h" +#include "cryptonote_basic/cryptonote_basic_impl.h" #include "net/http_client.h" #include "storages/http_abstract_invoke.h" #include "rpc/core_rpc_server_commands_defs.h" -#include "cryptonote_core/cryptonote_format_utils.h" +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "cryptonote_core/cryptonote_tx_utils.h" #include "common/unordered_containers_boost_serialization.h" #include "crypto/chacha8.h" #include "crypto/hash.h" @@ -51,9 +54,15 @@ #include "ringct/rctOps.h" #include "wallet_errors.h" +#include "common/password.h" +#include "node_rpc_proxy.h" #include <iostream> -#define WALLET_RCP_CONNECTION_TIMEOUT 200000 + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "wallet.wallet2" + +class Serialization_portability_wallet_Test; namespace tools { @@ -61,9 +70,10 @@ namespace tools { public: virtual void on_new_block(uint64_t height, const cryptonote::block& block) {} - virtual void on_money_received(uint64_t height, const cryptonote::transaction& tx, uint64_t amount) {} - virtual void on_money_spent(uint64_t height, const cryptonote::transaction& in_tx, uint64_t amount, const cryptonote::transaction& spend_tx) {} - virtual void on_skip_transaction(uint64_t height, const cryptonote::transaction& tx) {} + virtual void on_money_received(uint64_t height, const crypto::hash &txid, const cryptonote::transaction& tx, uint64_t amount) {} + virtual void on_unconfirmed_money_received(uint64_t height, const crypto::hash &txid, const cryptonote::transaction& tx, uint64_t amount) {} + virtual void on_money_spent(uint64_t height, const crypto::hash &txid, const cryptonote::transaction& in_tx, uint64_t amount, const cryptonote::transaction& spend_tx) {} + virtual void on_skip_transaction(uint64_t height, const crypto::hash &txid, const cryptonote::transaction& tx) {} virtual ~i_wallet2_callback() {} }; @@ -83,7 +93,10 @@ namespace tools class wallet2 { + friend class ::Serialization_portability_wallet_Test; public: + static constexpr const std::chrono::seconds rpc_timeout = std::chrono::minutes(3) + std::chrono::seconds(30); + enum RefreshType { RefreshFull, RefreshOptimizeCoinbase, @@ -92,10 +105,32 @@ namespace tools }; private: - wallet2(const wallet2&) : m_run(true), m_callback(0), m_testnet(false), m_always_confirm_transfers (false), m_store_tx_info(true), m_default_mixin(0), m_default_priority(0), m_refresh_type(RefreshOptimizeCoinbase), m_auto_refresh(true), m_refresh_from_block_height(0) {} + wallet2(const wallet2&) : m_run(true), m_callback(0), m_testnet(false), m_always_confirm_transfers(true), m_print_ring_members(false), m_store_tx_info(true), m_default_mixin(0), m_default_priority(0), m_refresh_type(RefreshOptimizeCoinbase), m_auto_refresh(true), m_refresh_from_block_height(0), m_confirm_missing_payment_id(true), m_ask_password(true), m_min_output_count(0), m_min_output_value(0), m_merge_destinations(false), m_node_rpc_proxy(m_http_client, m_daemon_rpc_mutex) {} public: - wallet2(bool testnet = false, bool restricted = false) : m_run(true), m_callback(0), m_testnet(testnet), m_restricted(restricted), is_old_file_format(false), m_store_tx_info(true), m_default_mixin(0), m_default_priority(0), m_refresh_type(RefreshOptimizeCoinbase), m_auto_refresh(true), m_refresh_from_block_height(0) {} + static const char* tr(const char* str); + + static bool has_testnet_option(const boost::program_options::variables_map& vm); + static void init_options(boost::program_options::options_description& desc_params); + + //! \return Password retrieved from prompt. Logs error on failure. + static boost::optional<password_container> password_prompt(const bool new_password); + + //! Uses stdin and stdout. Returns a wallet2 if no errors. + static std::unique_ptr<wallet2> make_from_json(const boost::program_options::variables_map& vm, const std::string& json_file); + + //! Uses stdin and stdout. Returns a wallet2 and password for `wallet_file` if no errors. + static std::pair<std::unique_ptr<wallet2>, password_container> + make_from_file(const boost::program_options::variables_map& vm, const std::string& wallet_file); + + //! Uses stdin and stdout. Returns a wallet2 and password for wallet with no file if no errors. + static std::pair<std::unique_ptr<wallet2>, password_container> make_new(const boost::program_options::variables_map& vm); + + //! Just parses variables. + static std::unique_ptr<wallet2> make_dummy(const boost::program_options::variables_map& vm); + + wallet2(bool testnet = false, bool restricted = false) : m_run(true), m_callback(0), m_testnet(testnet), m_always_confirm_transfers(true), m_print_ring_members(false), m_store_tx_info(true), m_default_mixin(0), m_default_priority(0), m_refresh_type(RefreshOptimizeCoinbase), m_auto_refresh(true), m_refresh_from_block_height(0), m_confirm_missing_payment_id(true), m_ask_password(true), m_min_output_count(0), m_min_output_value(0), m_merge_destinations(false), m_restricted(restricted), is_old_file_format(false), m_node_rpc_proxy(m_http_client, m_daemon_rpc_mutex) {} + struct transfer_details { uint64_t m_block_height; @@ -109,9 +144,28 @@ namespace tools rct::key m_mask; uint64_t m_amount; bool m_rct; + bool m_key_image_known; + size_t m_pk_index; bool is_rct() const { return m_rct; } uint64_t amount() const { return m_amount; } + const crypto::public_key &get_public_key() const { return boost::get<const cryptonote::txout_to_key>(m_tx.vout[m_internal_output_index].target).key; } + + BEGIN_SERIALIZE_OBJECT() + FIELD(m_block_height) + FIELD(m_tx) + FIELD(m_txid) + FIELD(m_internal_output_index) + FIELD(m_global_output_index) + FIELD(m_spent) + FIELD(m_spent_height) + FIELD(m_key_image) + FIELD(m_mask) + FIELD(m_amount) + FIELD(m_rct) + FIELD(m_key_image_known) + FIELD(m_pk_index) + END_SERIALIZE() }; struct payment_details @@ -151,19 +205,50 @@ namespace tools m_amount_in(utd.m_amount_in), m_amount_out(utd.m_amount_out), m_change(utd.m_change), m_block_height(height), m_dests(utd.m_dests), m_payment_id(utd.m_payment_id), m_timestamp(utd.m_timestamp) {} }; + struct tx_construction_data + { + std::vector<cryptonote::tx_source_entry> sources; + cryptonote::tx_destination_entry change_dts; + std::vector<cryptonote::tx_destination_entry> splitted_dsts; // split, includes change + std::list<size_t> selected_transfers; + std::vector<uint8_t> extra; + uint64_t unlock_time; + bool use_rct; + std::vector<cryptonote::tx_destination_entry> dests; // original setup, does not include change + }; + typedef std::vector<transfer_details> transfer_container; typedef std::unordered_multimap<crypto::hash, payment_details> payment_container; + // The convention for destinations is: + // dests does not include change + // splitted_dsts (in construction_data) does struct pending_tx { cryptonote::transaction tx; uint64_t dust, fee; bool dust_added_to_fee; cryptonote::tx_destination_entry change_dts; - std::list<transfer_container::iterator> selected_transfers; + std::list<size_t> selected_transfers; std::string key_images; crypto::secret_key tx_key; std::vector<cryptonote::tx_destination_entry> dests; + + tx_construction_data construction_data; + }; + + // The term "Unsigned tx" is not really a tx since it's not signed yet. + // It doesnt have tx hash, key and the integrated address is not separated into addr + payment id. + struct unsigned_tx_set + { + std::vector<tx_construction_data> txes; + wallet2::transfer_container transfers; + }; + + struct signed_tx_set + { + std::vector<pending_tx> ptx; + std::vector<crypto::key_image> key_images; }; struct keys_file_data @@ -187,6 +272,16 @@ namespace tools FIELD(cache_data) END_SERIALIZE() }; + + // GUI Address book + struct address_book_row + { + cryptonote::account_public_address m_address; + crypto::hash m_payment_id; + std::string m_description; + }; + + typedef std::tuple<uint64_t, crypto::public_key, rct::key> get_outs_entry; /*! * \brief Generates a wallet or restores one. @@ -235,6 +330,8 @@ namespace tools */ void store_to(const std::string &path, const std::string &password); + std::string path() const; + /*! * \brief verifies given password is correct for default wallet keys file */ @@ -243,14 +340,16 @@ namespace tools const cryptonote::account_base& get_account()const{return m_account;} void set_refresh_from_block_height(uint64_t height) {m_refresh_from_block_height = height;} + uint64_t get_refresh_from_block_height() const {return m_refresh_from_block_height;} // upper_transaction_size_limit as defined below is set to // approximately 125% of the fixed minimum allowable penalty // free block size. TODO: fix this so that it actually takes // into account the current median block size rather than // the minimum block size. - void init(const std::string& daemon_address = "http://localhost:8080", uint64_t upper_transaction_size_limit = 0); bool deinit(); + bool init(std::string daemon_address = "http://localhost:8080", + boost::optional<epee::net_utils::http::login> daemon_login = boost::none, uint64_t upper_transaction_size_limit = 0); void stop() { m_run.store(false, std::memory_order_relaxed); } @@ -296,20 +395,29 @@ namespace tools void transfer(const std::vector<cryptonote::tx_destination_entry>& dsts, const size_t fake_outputs_count, const std::vector<size_t> &unused_transfers_indices, uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& extra, bool trusted_daemon); void transfer(const std::vector<cryptonote::tx_destination_entry>& dsts, const size_t fake_outputs_count, const std::vector<size_t> &unused_transfers_indices, uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& extra, cryptonote::transaction& tx, pending_tx& ptx, bool trusted_daemon); template<typename T> - void transfer_from(const std::vector<size_t> &outs, size_t num_outputs, uint64_t unlock_time, uint64_t needed_fee, T destination_split_strategy, const tx_dust_policy& dust_policy, const std::vector<uint8_t>& extra, cryptonote::transaction& tx, pending_tx &ptx); - template<typename T> - void transfer_selected(const std::vector<cryptonote::tx_destination_entry>& dsts, const std::list<transfer_container::iterator> selected_transfers, size_t fake_outputs_count, + void transfer_selected(const std::vector<cryptonote::tx_destination_entry>& dsts, const std::list<size_t> selected_transfers, size_t fake_outputs_count, + std::vector<std::vector<tools::wallet2::get_outs_entry>> &outs, uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& extra, T destination_split_strategy, const tx_dust_policy& dust_policy, cryptonote::transaction& tx, pending_tx &ptx); - void transfer_selected_rct(std::vector<cryptonote::tx_destination_entry> dsts, const std::list<transfer_container::iterator> selected_transfers, size_t fake_outputs_count, + void transfer_selected_rct(std::vector<cryptonote::tx_destination_entry> dsts, const std::list<size_t> selected_transfers, size_t fake_outputs_count, + std::vector<std::vector<tools::wallet2::get_outs_entry>> &outs, uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& extra, cryptonote::transaction& tx, pending_tx &ptx); void commit_tx(pending_tx& ptx_vector); void commit_tx(std::vector<pending_tx>& ptx_vector); + bool save_tx(const std::vector<pending_tx>& ptx_vector, const std::string &filename); + // load unsigned tx from file and sign it. Takes confirmation callback as argument. Used by the cli wallet + bool sign_tx(const std::string &unsigned_filename, const std::string &signed_filename, std::vector<wallet2::pending_tx> &ptx, std::function<bool(const unsigned_tx_set&)> accept_func = NULL); + // sign unsigned tx. Takes unsigned_tx_set as argument. Used by GUI + bool sign_tx(unsigned_tx_set &exported_txs, const std::string &signed_filename, std::vector<wallet2::pending_tx> &ptx); + // load unsigned_tx_set from file. + bool load_unsigned_tx(const std::string &unsigned_filename, unsigned_tx_set &exported_txs); + bool load_tx(const std::string &signed_filename, std::vector<tools::wallet2::pending_tx> &ptx, std::function<bool(const signed_tx_set&)> accept_func = NULL); std::vector<pending_tx> create_transactions(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, bool trusted_daemon); std::vector<wallet2::pending_tx> 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, bool trusted_daemon); - std::vector<wallet2::pending_tx> create_transactions_all(const cryptonote::account_public_address &address, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t> extra, bool trusted_daemon); + std::vector<wallet2::pending_tx> create_transactions_all(uint64_t below, const cryptonote::account_public_address &address, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t> extra, bool trusted_daemon); + std::vector<wallet2::pending_tx> create_transactions_from(const cryptonote::account_public_address &address, std::vector<size_t> unused_transfers_indices, std::vector<size_t> unused_dust_indices, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t> extra, bool trusted_daemon); std::vector<pending_tx> create_unmixable_sweep_transactions(bool trusted_daemon); - bool check_connection(bool *same_version = NULL); + bool check_connection(uint32_t *version = NULL, uint32_t timeout = 200000); void get_transfers(wallet2::transfer_container& incoming_transfers) const; void get_payments(const crypto::hash& payment_id, std::list<wallet2::payment_details>& payments, uint64_t min_height = 0) const; void get_payments(std::list<std::pair<crypto::hash,wallet2::payment_details>>& payments, uint64_t min_height, uint64_t max_height = (uint64_t)-1) const; @@ -352,9 +460,39 @@ namespace tools a & m_tx_notes; if(ver < 13) return; - a & m_unconfirmed_payments; + if (ver < 17) + { + // we're loading an old version, where m_unconfirmed_payments was a std::map + std::unordered_map<crypto::hash, payment_details> m; + a & m; + for (std::unordered_map<crypto::hash, payment_details>::const_iterator i = m.begin(); i != m.end(); ++i) + m_unconfirmed_payments.insert(*i); + } if(ver < 14) return; + if(ver < 15) + { + // we're loading an older wallet without a pubkey map, rebuild it + for (size_t i = 0; i < m_transfers.size(); ++i) + { + const transfer_details &td = m_transfers[i]; + const cryptonote::tx_out &out = td.m_tx.vout[td.m_internal_output_index]; + const cryptonote::txout_to_key &o = boost::get<const cryptonote::txout_to_key>(out.target); + m_pub_keys.emplace(o.key, i); + } + return; + } + a & m_pub_keys; + if(ver < 16) + return; + a & m_address_book; + if(ver < 17) + return; + a & m_unconfirmed_payments; + if(ver < 18) + return; + a & m_scanned_pool_txs[0]; + a & m_scanned_pool_txs[1]; } /*! @@ -370,17 +508,14 @@ namespace tools * \return Whether path is valid format */ static bool wallet_valid_path_format(const std::string& file_path); - static bool parse_long_payment_id(const std::string& payment_id_str, crypto::hash& payment_id); static bool parse_short_payment_id(const std::string& payment_id_str, crypto::hash8& payment_id); static bool parse_payment_id(const std::string& payment_id_str, crypto::hash& payment_id); - static std::vector<std::string> addresses_from_url(const std::string& url, bool& dnssec_valid); - - static std::string address_from_txt_record(const std::string& s); - bool always_confirm_transfers() const { return m_always_confirm_transfers; } void always_confirm_transfers(bool always) { m_always_confirm_transfers = always; } + bool print_ring_members() const { return m_print_ring_members; } + void print_ring_members(bool value) { m_print_ring_members = value; } bool store_tx_info() const { return m_store_tx_info; } void store_tx_info(bool store) { m_store_tx_info = store; } uint32_t default_mixin() const { return m_default_mixin; } @@ -389,26 +524,52 @@ namespace tools void set_default_priority(uint32_t p) { m_default_priority = p; } bool auto_refresh() const { return m_auto_refresh; } void auto_refresh(bool r) { m_auto_refresh = r; } + bool confirm_missing_payment_id() const { return m_confirm_missing_payment_id; } + void confirm_missing_payment_id(bool always) { m_confirm_missing_payment_id = always; } + bool ask_password() const { return m_ask_password; } + void ask_password(bool always) { m_ask_password = always; } + void set_default_decimal_point(unsigned int decimal_point); + unsigned int get_default_decimal_point() const; + void set_min_output_count(uint32_t count) { m_min_output_count = count; } + uint32_t get_min_output_count() const { return m_min_output_count; } + void set_min_output_value(uint64_t value) { m_min_output_value = value; } + uint64_t get_min_output_value() const { return m_min_output_value; } + void merge_destinations(bool merge) { m_merge_destinations = merge; } + bool merge_destinations() const { return m_merge_destinations; } bool get_tx_key(const crypto::hash &txid, crypto::secret_key &tx_key) const; + /*! + * \brief GUI Address book get/store + */ + std::vector<address_book_row> get_address_book() const { return m_address_book; } + bool add_address_book_row(const cryptonote::account_public_address &address, const crypto::hash &payment_id, const std::string &description); + bool delete_address_book_row(std::size_t row_id); + uint64_t get_num_rct_outputs(); + const transfer_details &get_transfer_details(size_t idx) const; void get_hard_fork_info(uint8_t version, uint64_t &earliest_height); bool use_fork_rules(uint8_t version, int64_t early_blocks = 0); + int get_fee_algorithm(); std::string get_wallet_file() const; std::string get_keys_file() const; std::string get_daemon_address() const; + const boost::optional<epee::net_utils::http::login>& get_daemon_login() const { return m_daemon_login; } uint64_t get_daemon_blockchain_height(std::string& err); - - std::vector<size_t> select_available_outputs_from_histogram(uint64_t count, bool atleast, bool unlocked, bool trusted_daemon); + uint64_t get_daemon_blockchain_target_height(std::string& err); + /*! + * \brief Calculates the approximate blockchain height from current date/time. + */ + uint64_t get_approximate_blockchain_height() const; + std::vector<size_t> select_available_outputs_from_histogram(uint64_t count, bool atleast, bool unlocked, bool allow_rct, bool trusted_daemon); std::vector<size_t> select_available_outputs(const std::function<bool(const transfer_details &td)> &f); std::vector<size_t> select_available_unmixable_outputs(bool trusted_daemon); std::vector<size_t> select_available_mixable_outputs(bool trusted_daemon); - size_t pop_best_value_from(const transfer_container &transfers, std::vector<size_t> &unused_dust_indices, const std::list<transfer_container::iterator>& selected_transfers) const; - size_t pop_best_value(std::vector<size_t> &unused_dust_indices, const std::list<transfer_container::iterator>& selected_transfers) const; + size_t pop_best_value_from(const transfer_container &transfers, std::vector<size_t> &unused_dust_indices, const std::list<size_t>& selected_transfers, bool smallest = false) const; + size_t pop_best_value(std::vector<size_t> &unused_dust_indices, const std::list<size_t>& selected_transfers, bool smallest = false) const; void set_tx_note(const crypto::hash &txid, const std::string ¬e); std::string get_tx_note(const crypto::hash &txid) const; @@ -416,10 +577,26 @@ namespace tools std::string sign(const std::string &data) const; bool verify(const std::string &data, const cryptonote::account_public_address &address, const std::string &signature) const; + std::vector<tools::wallet2::transfer_details> export_outputs() const; + size_t import_outputs(const std::vector<tools::wallet2::transfer_details> &outputs); + + bool export_key_images(const std::string filename); std::vector<std::pair<crypto::key_image, crypto::signature>> export_key_images() const; uint64_t import_key_images(const std::vector<std::pair<crypto::key_image, crypto::signature>> &signed_key_images, uint64_t &spent, uint64_t &unspent); + uint64_t import_key_images(const std::string &filename, uint64_t &spent, uint64_t &unspent); void update_pool_state(); + + std::string encrypt(const std::string &plaintext, const crypto::secret_key &skey, bool authenticated = true) const; + std::string encrypt_with_view_secret_key(const std::string &plaintext, bool authenticated = true) const; + std::string decrypt(const std::string &ciphertext, const crypto::secret_key &skey, bool authenticated = true) const; + std::string decrypt_with_view_secret_key(const std::string &ciphertext, bool authenticated = true) const; + + std::string make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, std::string &error); + bool parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &amount, std::string &tx_description, std::string &recipient_name, std::vector<std::string> &unknown_parameters, std::string &error); + + uint64_t get_blockchain_height_by_date(uint16_t year, uint8_t month, uint8_t day); // 1<=month<=12, 1<=day<=31 + private: /*! * \brief Stores wallet information to wallet file. @@ -435,7 +612,7 @@ namespace tools * \param password Password of wallet file */ bool load_keys(const std::string& keys_file_name, const std::string& password); - void process_new_transaction(const cryptonote::transaction& tx, const std::vector<uint64_t> &o_indices, uint64_t height, uint64_t ts, bool miner_tx, bool pool); + void process_new_transaction(const crypto::hash &txid, const cryptonote::transaction& tx, const std::vector<uint64_t> &o_indices, uint64_t height, uint64_t ts, bool miner_tx, bool pool); void process_new_blockchain_entry(const cryptonote::block& b, const cryptonote::block_complete_entry& bche, const crypto::hash& bl_id, uint64_t height, const cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices &o_indices); void detach_blockchain(uint64_t height); void get_short_chain_history(std::list<crypto::hash>& ids) const; @@ -446,28 +623,35 @@ namespace tools void fast_refresh(uint64_t stop_height, uint64_t &blocks_start_height, std::list<crypto::hash> &short_chain_history); void pull_next_blocks(uint64_t start_height, uint64_t &blocks_start_height, std::list<crypto::hash> &short_chain_history, const std::list<cryptonote::block_complete_entry> &prev_blocks, std::list<cryptonote::block_complete_entry> &blocks, std::vector<cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices> &o_indices, bool &error); void process_blocks(uint64_t start_height, const std::list<cryptonote::block_complete_entry> &blocks, const std::vector<cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices> &o_indices, uint64_t& blocks_added); - uint64_t select_transfers(uint64_t needed_money, std::vector<size_t> unused_transfers_indices, std::list<transfer_container::iterator>& selected_transfers, bool trusted_daemon); + uint64_t select_transfers(uint64_t needed_money, std::vector<size_t> unused_transfers_indices, std::list<size_t>& selected_transfers, bool trusted_daemon); bool prepare_file_names(const std::string& file_path); - void process_unconfirmed(const cryptonote::transaction& tx, uint64_t height); - void process_outgoing(const cryptonote::transaction& tx, uint64_t height, uint64_t ts, uint64_t spent, uint64_t received); + void process_unconfirmed(const crypto::hash &txid, const cryptonote::transaction& tx, uint64_t height); + void process_outgoing(const crypto::hash &txid, const cryptonote::transaction& tx, uint64_t height, uint64_t ts, uint64_t spent, uint64_t received); void add_unconfirmed_tx(const cryptonote::transaction& tx, uint64_t amount_in, const std::vector<cryptonote::tx_destination_entry> &dests, const crypto::hash &payment_id, uint64_t change_amount); void generate_genesis(cryptonote::block& b); void check_genesis(const crypto::hash& genesis_hash) const; //throws bool generate_chacha8_key_from_secret_keys(crypto::chacha8_key &key) const; crypto::hash get_payment_id(const pending_tx &ptx) const; - void check_acc_out(const cryptonote::account_keys &acc, const cryptonote::tx_out &o, const crypto::public_key &tx_pub_key, size_t i, bool &received, uint64_t &money_transfered, bool &error) const; + crypto::hash8 get_short_payment_id(const pending_tx &ptx) const; + void check_acc_out_precomp(const crypto::public_key &spend_public_key, const cryptonote::tx_out &o, const crypto::key_derivation &derivation, size_t i, bool &received, uint64_t &money_transfered, bool &error) const; void parse_block_round(const cryptonote::blobdata &blob, cryptonote::block &bl, crypto::hash &bl_id, bool &error) const; uint64_t get_upper_tranaction_size_limit(); std::vector<uint64_t> get_unspent_amounts_vector(); - uint64_t get_fee_multiplier(uint32_t priority, bool use_new_fee) const; + uint64_t get_fee_multiplier(uint32_t priority, int fee_algorithm) const; + uint64_t get_dynamic_per_kb_fee_estimate(); + uint64_t get_per_kb_fee(); float get_output_relatedness(const transfer_details &td0, const transfer_details &td1) const; - std::vector<size_t> pick_prefered_rct_inputs(uint64_t needed_money) const; - void set_spent(transfer_details &td, uint64_t height); - void set_unspent(transfer_details &td); - template<typename entry> - void get_outs(std::vector<std::vector<entry>> &outs, const std::list<transfer_container::iterator> &selected_transfers, size_t fake_outputs_count); + std::vector<size_t> pick_preferred_rct_inputs(uint64_t needed_money) const; + void set_spent(size_t idx, uint64_t height); + void set_unspent(size_t idx); + void get_outs(std::vector<std::vector<get_outs_entry>> &outs, const std::list<size_t> &selected_transfers, size_t fake_outputs_count); + bool wallet_generate_key_image_helper(const cryptonote::account_keys& ack, const crypto::public_key& tx_public_key, size_t real_output_index, cryptonote::keypair& in_ephemeral, crypto::key_image& ki); + crypto::public_key get_tx_pub_key_from_received_outs(const tools::wallet2::transfer_details &td) const; + bool should_pick_a_second_output(bool use_rct, size_t n_transfers, const std::vector<size_t> &unused_transfers_indices, const std::vector<size_t> &unused_dust_indices) const; + std::vector<size_t> get_only_rct(const std::vector<size_t> &unused_dust_indices, const std::vector<size_t> &unused_transfers_indices) const; cryptonote::account_base m_account; + boost::optional<epee::net_utils::http::login> m_daemon_login; std::string m_daemon_address; std::string m_wallet_file; std::string m_keys_file; @@ -476,14 +660,16 @@ namespace tools std::atomic<uint64_t> m_local_bc_height; //temporary workaround std::unordered_map<crypto::hash, unconfirmed_transfer_details> m_unconfirmed_txs; std::unordered_map<crypto::hash, confirmed_transfer_details> m_confirmed_txs; - std::unordered_map<crypto::hash, payment_details> m_unconfirmed_payments; + std::unordered_multimap<crypto::hash, payment_details> m_unconfirmed_payments; std::unordered_map<crypto::hash, crypto::secret_key> m_tx_keys; transfer_container m_transfers; payment_container m_payments; std::unordered_map<crypto::key_image, size_t> m_key_images; + std::unordered_map<crypto::public_key, size_t> m_pub_keys; cryptonote::account_public_address m_account_public_address; std::unordered_map<crypto::hash, std::string> m_tx_notes; + std::vector<tools::wallet2::address_book_row> m_address_book; uint64_t m_upper_transaction_size_limit; //TODO: auto-calc this value or request from daemon, now use some fixed value std::atomic<bool> m_run; @@ -497,30 +683,43 @@ namespace tools bool is_old_file_format; /*!< Whether the wallet file is of an old file format */ bool m_watch_only; /*!< no spend key */ bool m_always_confirm_transfers; + bool m_print_ring_members; bool m_store_tx_info; /*!< request txkey to be returned in RPC, and store in the wallet cache file */ uint32_t m_default_mixin; uint32_t m_default_priority; RefreshType m_refresh_type; bool m_auto_refresh; uint64_t m_refresh_from_block_height; + bool m_confirm_missing_payment_id; + bool m_ask_password; + uint32_t m_min_output_count; + uint64_t m_min_output_value; + bool m_merge_destinations; + NodeRPCProxy m_node_rpc_proxy; + std::unordered_set<crypto::hash> m_scanned_pool_txs[2]; }; } -BOOST_CLASS_VERSION(tools::wallet2, 14) -BOOST_CLASS_VERSION(tools::wallet2::transfer_details, 4) +BOOST_CLASS_VERSION(tools::wallet2, 18) +BOOST_CLASS_VERSION(tools::wallet2::transfer_details, 7) BOOST_CLASS_VERSION(tools::wallet2::payment_details, 1) -BOOST_CLASS_VERSION(tools::wallet2::unconfirmed_transfer_details, 5) -BOOST_CLASS_VERSION(tools::wallet2::confirmed_transfer_details, 2) +BOOST_CLASS_VERSION(tools::wallet2::unconfirmed_transfer_details, 6) +BOOST_CLASS_VERSION(tools::wallet2::confirmed_transfer_details, 3) +BOOST_CLASS_VERSION(tools::wallet2::address_book_row, 16) +BOOST_CLASS_VERSION(tools::wallet2::unsigned_tx_set, 0) +BOOST_CLASS_VERSION(tools::wallet2::signed_tx_set, 0) +BOOST_CLASS_VERSION(tools::wallet2::tx_construction_data, 0) +BOOST_CLASS_VERSION(tools::wallet2::pending_tx, 0) namespace boost { namespace serialization { template <class Archive> - inline void initialize_transfer_details(Archive &a, tools::wallet2::transfer_details &x, const boost::serialization::version_type ver) + inline typename std::enable_if<!Archive::is_loading::value, void>::type initialize_transfer_details(Archive &a, tools::wallet2::transfer_details &x, const boost::serialization::version_type ver) { } - template<> - inline void initialize_transfer_details(boost::archive::binary_iarchive &a, tools::wallet2::transfer_details &x, const boost::serialization::version_type ver) + template <class Archive> + inline typename std::enable_if<Archive::is_loading::value, void>::type initialize_transfer_details(Archive &a, tools::wallet2::transfer_details &x, const boost::serialization::version_type ver) { if (ver < 1) { @@ -535,6 +734,14 @@ namespace boost { x.m_rct = x.m_tx.vout[x.m_internal_output_index].amount == 0; } + if (ver < 6) + { + x.m_key_image_known = true; + } + if (ver < 7) + { + x.m_pk_index = 0; + } } template <class Archive> @@ -582,6 +789,26 @@ namespace boost return; } a & x.m_rct; + if (ver < 5) + { + initialize_transfer_details(a, x, ver); + return; + } + if (ver < 6) + { + // v5 did not properly initialize + uint8_t u; + a & u; + x.m_key_image_known = true; + return; + } + a & x.m_key_image_known; + if (ver < 7) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_pk_index; } template <class Archive> @@ -613,6 +840,14 @@ namespace boost return; a & x.m_amount_in; a & x.m_amount_out; + if (ver < 6) + { + // v<6 may not have change accumulated in m_amount_out, which is a pain, + // as it's readily understood to be sum of outputs. + // We convert it to include change from v6 + if (!typename Archive::is_saving() && x.m_change != (uint64_t)-1) + x.m_amount_out += x.m_change; + } } template <class Archive> @@ -629,6 +864,20 @@ namespace boost if (ver < 2) return; a & x.m_timestamp; + if (ver < 3) + { + // v<3 may not have change accumulated in m_amount_out, which is a pain, + // as it's readily understood to be sum of outputs. Whether it got added + // or not depends on whether it came from a unconfirmed_transfer_details + // (not included) or not (included). We can't reliably tell here, so we + // check whether either yields a "negative" fee, or use the other if so. + // We convert it to include change from v3 + if (!typename Archive::is_saving() && x.m_change != (uint64_t)-1) + { + if (x.m_amount_in > (x.m_amount_out + x.m_change)) + x.m_amount_out += x.m_change; + } + } } template <class Archive> @@ -649,6 +898,56 @@ namespace boost a & x.amount; a & x.addr; } + + template <class Archive> + inline void serialize(Archive& a, tools::wallet2::address_book_row& x, const boost::serialization::version_type ver) + { + a & x.m_address; + a & x.m_payment_id; + a & x.m_description; + } + + template <class Archive> + inline void serialize(Archive &a, tools::wallet2::unsigned_tx_set &x, const boost::serialization::version_type ver) + { + a & x.txes; + a & x.transfers; + } + + template <class Archive> + inline void serialize(Archive &a, tools::wallet2::signed_tx_set &x, const boost::serialization::version_type ver) + { + a & x.ptx; + a & x.key_images; + } + + template <class Archive> + inline void serialize(Archive &a, tools::wallet2::tx_construction_data &x, const boost::serialization::version_type ver) + { + a & x.sources; + a & x.change_dts; + a & x.splitted_dsts; + a & x.selected_transfers; + a & x.extra; + a & x.unlock_time; + a & x.use_rct; + a & x.dests; + } + + template <class Archive> + inline void serialize(Archive &a, tools::wallet2::pending_tx &x, const boost::serialization::version_type ver) + { + a & x.tx; + a & x.dust; + a & x.fee; + a & x.dust_added_to_fee; + a & x.change_dts; + a & x.selected_transfers; + a & x.key_images; + a & x.tx_key; + a & x.dests; + a & x.construction_data; + } } } @@ -665,7 +964,7 @@ namespace tools splitted_dsts.clear(); dust_dsts.clear(); - BOOST_FOREACH(auto& de, dsts) + for(auto& de: dsts) { cryptonote::decompose_amount_into_digits(de.amount, 0, [&](uint64_t chunk) { splitted_dsts.push_back(cryptonote::tx_destination_entry(chunk, de.addr)); }, @@ -728,7 +1027,7 @@ namespace tools // calculate total amount being sent to all destinations // throw if total amount overflows uint64_t - BOOST_FOREACH(auto& dt, dsts) + for(auto& dt: dsts) { THROW_WALLET_EXCEPTION_IF(0 == dt.amount, error::zero_destination); needed_money += dt.amount; @@ -737,7 +1036,7 @@ namespace tools // randomly select inputs for transaction // throw if requested send amount is greater than amount available to send - std::list<transfer_container::iterator> selected_transfers; + std::list<size_t> selected_transfers; uint64_t found_money = select_transfers(needed_money, unused_transfers_indices, selected_transfers, trusted_daemon); THROW_WALLET_EXCEPTION_IF(found_money < needed_money, error::not_enough_money, found_money, needed_money - fee, fee); @@ -749,8 +1048,9 @@ namespace tools { COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::request req = AUTO_VAL_INIT(req); req.outs_count = fake_outputs_count + 1;// add one to make possible (if need) to skip real output key - BOOST_FOREACH(transfer_container::iterator it, selected_transfers) + for(size_t idx: selected_transfers) { + const transfer_container::const_iterator it = m_transfers.begin() + idx; THROW_WALLET_EXCEPTION_IF(it->m_tx.vout.size() <= it->m_internal_output_index, error::wallet_internal_error, "m_internal_output_index = " + std::to_string(it->m_internal_output_index) + " is greater or equal to outputs count = " + std::to_string(it->m_tx.vout.size())); @@ -758,7 +1058,7 @@ namespace tools } m_daemon_rpc_mutex.lock(); - bool r = epee::net_utils::invoke_http_bin_remote_command2(m_daemon_address + "/getrandom_outs.bin", req, daemon_resp, m_http_client, 200000); + bool r = epee::net_utils::invoke_http_bin("/getrandom_outs.bin", req, daemon_resp, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "getrandom_outs.bin"); THROW_WALLET_EXCEPTION_IF(daemon_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "getrandom_outs.bin"); @@ -768,7 +1068,7 @@ namespace tools std::to_string(daemon_resp.outs.size()) + ", expected " + std::to_string(selected_transfers.size())); std::unordered_map<uint64_t, uint64_t> scanty_outs; - BOOST_FOREACH(COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::outs_for_amount& amount_outs, daemon_resp.outs) + for(COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::outs_for_amount& amount_outs: daemon_resp.outs) { if (amount_outs.outs.size() < fake_outputs_count) { @@ -781,18 +1081,18 @@ namespace tools //prepare inputs size_t i = 0; std::vector<cryptonote::tx_source_entry> sources; - BOOST_FOREACH(transfer_container::iterator it, selected_transfers) + for(size_t idx: selected_transfers) { sources.resize(sources.size()+1); cryptonote::tx_source_entry& src = sources.back(); - transfer_details& td = *it; + const transfer_details& td = m_transfers[idx]; src.amount = td.amount(); src.rct = false; //paste mixin transaction if(daemon_resp.outs.size()) { daemon_resp.outs[i].outs.sort([](const out_entry& a, const out_entry& b){return a.global_amount_index < b.global_amount_index;}); - BOOST_FOREACH(out_entry& daemon_oe, daemon_resp.outs[i].outs) + for(out_entry& daemon_oe: daemon_resp.outs[i].outs) { if(td.m_global_output_index == daemon_oe.global_amount_index) continue; @@ -834,11 +1134,11 @@ namespace tools std::vector<cryptonote::tx_destination_entry> splitted_dsts, dust_dsts; uint64_t dust = 0; destination_split_strategy(dsts, change_dts, dust_policy.dust_threshold, splitted_dsts, dust_dsts); - BOOST_FOREACH(auto& d, dust_dsts) { + for(auto& d: dust_dsts) { THROW_WALLET_EXCEPTION_IF(dust_policy.dust_threshold < d.amount, error::wallet_internal_error, "invalid dust value: dust = " + std::to_string(d.amount) + ", dust_threshold = " + std::to_string(dust_policy.dust_threshold)); } - BOOST_FOREACH(auto& d, dust_dsts) { + for(auto& d: dust_dsts) { if (!dust_policy.add_to_fee) splitted_dsts.push_back(cryptonote::tx_destination_entry(d.amount, dust_policy.addr_for_dust)); dust += d.amount; @@ -872,6 +1172,14 @@ namespace tools ptx.selected_transfers = selected_transfers; ptx.tx_key = tx_key; ptx.dests = dsts; + ptx.construction_data.sources = sources; + ptx.construction_data.change_dts = change_dts; + 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 = unlock_time; + ptx.construction_data.use_rct = false; + ptx.construction_data.dests = dsts; } diff --git a/src/wallet/wallet2_api.h b/src/wallet/wallet2_api.h index 08e2ae16b..17d0caf7d 100644 --- a/src/wallet/wallet2_api.h +++ b/src/wallet/wallet2_api.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -34,13 +34,28 @@ #include <string> #include <vector> #include <ctime> +#include <iostream> // Public interface for libwallet library -namespace Bitmonero { +namespace Monero { namespace Utils { bool isAddressLocal(const std::string &hostaddr); } + + template<typename T> + class optional { + public: + optional(): set(false) {} + optional(const T &t): t(t), set(true) {} + const T &operator*() const { return t; } + T &operator*() { return t; } + operator bool() const { return set; } + private: + T t; + bool set; + }; + /** * @brief Transaction-like interface for sending money */ @@ -48,7 +63,8 @@ struct PendingTransaction { enum Status { Status_Ok, - Status_Error + Status_Error, + Status_Critical }; enum Priority { @@ -61,10 +77,59 @@ struct PendingTransaction virtual ~PendingTransaction() = 0; virtual int status() const = 0; virtual std::string errorString() const = 0; - virtual bool commit() = 0; + // commit transaction or save to file if filename is provided. + virtual bool commit(const std::string &filename = "", bool overwrite = false) = 0; virtual uint64_t amount() const = 0; virtual uint64_t dust() const = 0; virtual uint64_t fee() const = 0; + virtual std::vector<std::string> txid() const = 0; + /*! + * \brief txCount - number of transactions current transaction will be splitted to + * \return + */ + virtual uint64_t txCount() const = 0; +}; + +/** + * @brief Transaction-like interface for sending money + */ +struct UnsignedTransaction +{ + enum Status { + Status_Ok, + Status_Error, + Status_Critical + }; + + enum Priority { + Priority_Low = 1, + Priority_Medium = 2, + Priority_High = 3, + Priority_Last + }; + + virtual ~UnsignedTransaction() = 0; + virtual int status() const = 0; + virtual std::string errorString() const = 0; + virtual std::vector<uint64_t> amount() const = 0; + virtual std::vector<uint64_t> fee() const = 0; + virtual std::vector<uint64_t> mixin() const = 0; + // returns a string with information about all transactions. + virtual std::string confirmationMessage() const = 0; + virtual std::vector<std::string> paymentId() const = 0; + virtual std::vector<std::string> recipientAddress() const = 0; + virtual uint64_t minMixinCount() const = 0; + /*! + * \brief txCount - number of transactions current transaction will be splitted to + * \return + */ + virtual uint64_t txCount() const = 0; + /*! + * @brief sign - Sign txs and saves to file + * @param signedFileName + * return - true on success + */ + virtual bool sign(const std::string &signedFileName) = 0; }; /** @@ -90,6 +155,7 @@ struct TransactionInfo virtual uint64_t amount() const = 0; virtual uint64_t fee() const = 0; virtual uint64_t blockHeight() const = 0; + virtual uint64_t confirmations() const = 0; //! transaction_id virtual std::string hash() const = 0; virtual std::time_t timestamp() const = 0; @@ -110,6 +176,51 @@ struct TransactionHistory virtual void refresh() = 0; }; +/** + * @brief AddressBookRow - provides functions to manage address book + */ +struct AddressBookRow { +public: + AddressBookRow(std::size_t _rowId, const std::string &_address, const std::string &_paymentId, const std::string &_description): + m_rowId(_rowId), + m_address(_address), + m_paymentId(_paymentId), + m_description(_description) {} + +private: + std::size_t m_rowId; + std::string m_address; + std::string m_paymentId; + std::string m_description; +public: + std::string extra; + std::string getAddress() const {return m_address;} + std::string getDescription() const {return m_description;} + std::string getPaymentId() const {return m_paymentId;} + std::size_t getRowId() const {return m_rowId;} +}; + +/** + * @brief The AddressBook - interface for +Book + */ +struct AddressBook +{ + enum ErrorCode { + Status_Ok, + General_Error, + Invalid_Address, + Invalid_Payment_Id + }; + virtual ~AddressBook() = 0; + virtual std::vector<AddressBookRow*> getAll() const = 0; + virtual bool addRow(const std::string &dst_addr , const std::string &payment_id, const std::string &description) = 0; + virtual bool deleteRow(std::size_t rowId) = 0; + virtual void refresh() = 0; + virtual std::string errorString() const = 0; + virtual int errorCode() const = 0; + virtual int lookupPaymentID(const std::string &payment_id) const = 0; +}; struct WalletListener { @@ -127,6 +238,13 @@ struct WalletListener * @param amount - amount */ virtual void moneyReceived(const std::string &txId, uint64_t amount) = 0; + + /** + * @brief unconfirmedMoneyReceived - called when payment arrived in tx pool + * @param txId - transaction id + * @param amount - amount + */ + virtual void unconfirmedMoneyReceived(const std::string &txId, uint64_t amount) = 0; /** * @brief newBlock - called when new block received @@ -156,7 +274,14 @@ struct Wallet enum Status { Status_Ok, - Status_Error + Status_Error, + Status_Critical + }; + + enum ConnectionStatus { + ConnectionStatus_Disconnected, + ConnectionStatus_Connected, + ConnectionStatus_WrongVersion }; virtual ~Wallet() = 0; @@ -169,7 +294,12 @@ struct Wallet virtual std::string errorString() const = 0; virtual bool setPassword(const std::string &password) = 0; virtual std::string address() const = 0; - + virtual std::string path() const = 0; + virtual bool testnet() const = 0; + //! returns current hard fork info + virtual void hardForkInfo(uint8_t &version, uint64_t &earliest_height) const = 0; + //! check if hard fork rules should be used + virtual bool useForkRules(uint8_t version, int64_t early_blocks) const = 0; /*! * \brief integratedAddress - returns integrated address for current wallet address and given payment_id. * if passed "payment_id" param is an empty string or not-valid payment id string @@ -181,6 +311,12 @@ struct Wallet */ virtual std::string integratedAddress(const std::string &payment_id) const = 0; + /*! + * \brief privateViewKey - returns private view key + * \return - private view key + */ + virtual std::string privateViewKey() const = 0; + /*! * \brief store - stores wallet to file. * \param path - main filename to store wallet to. additionally stores address file and keys file. @@ -199,25 +335,38 @@ struct Wallet */ virtual std::string keysFilename() const = 0; /*! - * \brief init - initializes wallet with daemon connection params. implicitly connects to the daemon - * and refreshes the wallet. "refreshed" callback will be invoked. if daemon_address is - * local address, "trusted daemon" will be set to true forcibly - * - * \param daemon_address - daemon address in "hostname:port" format - * \param upper_transaction_size_limit - * \return - true if initialized and refreshed successfully - */ - virtual bool init(const std::string &daemon_address, uint64_t upper_transaction_size_limit) = 0; - - /*! - * \brief init - initalizes wallet asynchronously. logic is the same as "init" but returns immediately. - * "refreshed" callback will be invoked. + * \brief init - initializes wallet with daemon connection params. + * if daemon_address is local address, "trusted daemon" will be set to true forcibly + * startRefresh() should be called when wallet is initialized. * * \param daemon_address - daemon address in "hostname:port" format * \param upper_transaction_size_limit - * \return - true if initialized and refreshed successfully + * \return - true on success */ - virtual void initAsync(const std::string &daemon_address, uint64_t upper_transaction_size_limit) = 0; + virtual bool init(const std::string &daemon_address, uint64_t upper_transaction_size_limit, const std::string &daemon_username = "", const std::string &daemon_password = "") = 0; + + /*! + * \brief createWatchOnly - Creates a watch only wallet + * \param path - where to store the wallet + * \param password + * \param language + * \return - true if created successfully + */ + virtual bool createWatchOnly(const std::string &path, const std::string &password, const std::string &language) const = 0; + + /*! + * \brief setRefreshFromBlockHeight - start refresh from block height on recover + * + * \param refresh_from_block_height - blockchain start height + */ + virtual void setRefreshFromBlockHeight(uint64_t refresh_from_block_height) = 0; + + /*! + * \brief setRecoveringFromSeed - set state recover form seed + * + * \param recoveringFromSeed - true/false + */ + virtual void setRecoveringFromSeed(bool recoveringFromSeed) = 0; /** * @brief connectToDaemon - connects to the daemon. TODO: check if it can be removed @@ -229,12 +378,18 @@ struct Wallet * @brief connected - checks if the wallet connected to the daemon * @return - true if connected */ - virtual bool connected() const = 0; + virtual ConnectionStatus connected() const = 0; virtual void setTrustedDaemon(bool arg) = 0; virtual bool trustedDaemon() const = 0; virtual uint64_t balance() const = 0; virtual uint64_t unlockedBalance() const = 0; + /** + * @brief watchOnly - checks if wallet is watch only + * @return - true if watch only + */ + virtual bool watchOnly() const = 0; + /** * @brief blockChainHeight - returns current blockchain height * @return @@ -242,19 +397,52 @@ struct Wallet virtual uint64_t blockChainHeight() const = 0; /** + * @brief approximateBlockChainHeight - returns approximate blockchain height calculated from date/time + * @return + */ + virtual uint64_t approximateBlockChainHeight() const = 0; + + /** * @brief daemonBlockChainHeight - returns daemon blockchain height * @return 0 - in case error communicating with the daemon. * status() will return Status_Error and errorString() will return verbose error description */ virtual uint64_t daemonBlockChainHeight() const = 0; + /** + * @brief daemonBlockChainTargetHeight - returns daemon blockchain target height + * @return 0 - in case error communicating with the daemon. + * status() will return Status_Error and errorString() will return verbose error description + */ + virtual uint64_t daemonBlockChainTargetHeight() const = 0; + + /** + * @brief synchronized - checks if wallet was ever synchronized + * @return + */ + virtual bool synchronized() const = 0; static std::string displayAmount(uint64_t amount); static uint64_t amountFromString(const std::string &amount); static uint64_t amountFromDouble(double amount); static std::string genPaymentId(); static bool paymentIdValid(const std::string &paiment_id); + static bool addressValid(const std::string &str, bool testnet); + static bool keyValid(const std::string &secret_key_string, const std::string &address_string, bool isViewKey, bool testnet, std::string &error); + static std::string paymentIdFromAddress(const std::string &str, bool testnet); static uint64_t maximumAllowedAmount(); + // Easylogger wrapper + static void init(const char *argv0, const char *default_log_base_name); + static void debug(const std::string &str); + + /** + * @brief StartRefresh - Start/resume refresh thread (refresh every 10 seconds) + */ + virtual void startRefresh() = 0; + /** + * @brief pauseRefresh - pause refresh thread + */ + virtual void pauseRefresh() = 0; /** * @brief refresh - refreshes the wallet, updating transactions from daemon @@ -292,15 +480,54 @@ struct Wallet */ virtual PendingTransaction * createTransaction(const std::string &dst_addr, const std::string &payment_id, - uint64_t amount, uint32_t mixin_count, + optional<uint64_t> amount, uint32_t mixin_count, PendingTransaction::Priority = PendingTransaction::Priority_Low) = 0; /*! + * \brief createSweepUnmixableTransaction creates transaction with unmixable outputs. + * \return PendingTransaction object. caller is responsible to check PendingTransaction::status() + * after object returned + */ + + virtual PendingTransaction * createSweepUnmixableTransaction() = 0; + + /*! + * \brief loadUnsignedTx - creates transaction from unsigned tx file + * \return - UnsignedTransaction object. caller is responsible to check UnsignedTransaction::status() + * after object returned + */ + virtual UnsignedTransaction * loadUnsignedTx(const std::string &unsigned_filename) = 0; + + /*! + * \brief submitTransaction - submits transaction in signed tx file + * \return - true on success + */ + virtual bool submitTransaction(const std::string &fileName) = 0; + + + /*! * \brief disposeTransaction - destroys transaction object * \param t - pointer to the "PendingTransaction" object. Pointer is not valid after function returned; */ virtual void disposeTransaction(PendingTransaction * t) = 0; + + /*! + * \brief exportKeyImages - exports key images to file + * \param filename + * \return - true on success + */ + virtual bool exportKeyImages(const std::string &filename) = 0; + + /*! + * \brief importKeyImages - imports key images from file + * \param filename + * \return - true on success + */ + virtual bool importKeyImages(const std::string &filename) = 0; + + virtual TransactionHistory * history() const = 0; + virtual AddressBook * addressBook() const = 0; virtual void setListener(WalletListener *) = 0; /*! * \brief defaultMixin - returns number of mixins used in transactions @@ -312,6 +539,43 @@ struct Wallet * \param arg */ virtual void setDefaultMixin(uint32_t arg) = 0; + + /*! + * \brief setUserNote - attach an arbitrary string note to a txid + * \param txid - the transaction id to attach the note to + * \param note - the note + * \return true if succesful, false otherwise + */ + virtual bool setUserNote(const std::string &txid, const std::string ¬e) = 0; + /*! + * \brief getUserNote - return an arbitrary string note attached to a txid + * \param txid - the transaction id to attach the note to + * \return the attached note, or empty string if there is none + */ + virtual std::string getUserNote(const std::string &txid) const = 0; + virtual std::string getTxKey(const std::string &txid) const = 0; + + /* + * \brief signMessage - sign a message with the spend private key + * \param message - the message to sign (arbitrary byte data) + * \return the signature + */ + virtual std::string signMessage(const std::string &message) = 0; + /*! + * \brief verifySignedMessage - verify a signature matches a given message + * \param message - the message (arbitrary byte data) + * \param address - the address the signature claims to be made with + * \param signature - the signature + * \return true if the signature verified, false otherwise + */ + virtual bool verifySignedMessage(const std::string &message, const std::string &addres, const std::string &signature) const = 0; + + virtual bool parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &amount, std::string &tx_description, std::string &recipient_name, std::vector<std::string> &unknown_parameters, std::string &error) = 0; + /* + * \brief rescanSpent - Rescan spent outputs - Can only be used with trusted daemon + * \return true on success + */ + virtual bool rescanSpent() = 0; }; /** @@ -341,9 +605,30 @@ struct WalletManager * \brief recovers existing wallet using memo (electrum seed) * \param path Name of wallet file to be created * \param memo memo (25 words electrum seed) + * \param testnet testnet + * \param restoreHeight restore from start height * \return Wallet instance (Wallet::status() needs to be called to check if recovered successfully) */ - virtual Wallet * recoveryWallet(const std::string &path, const std::string &memo, bool testnet = false) = 0; + virtual Wallet * recoveryWallet(const std::string &path, const std::string &memo, bool testnet = false, uint64_t restoreHeight = 0) = 0; + + /*! + * \brief recovers existing wallet using keys. Creates a view only wallet if spend key is omitted + * \param path Name of wallet file to be created + * \param language language + * \param testnet testnet + * \param restoreHeight restore from start height + * \param addressString public address + * \param viewKeyString view key + * \param spendKeyString spend key (optional) + * \return Wallet instance (Wallet::status() needs to be called to check if recovered successfully) + */ + virtual Wallet * createWalletFromKeys(const std::string &path, + const std::string &language, + bool testnet, + uint64_t restoreHeight, + const std::string &addressString, + const std::string &viewKeyString, + const std::string &spendKeyString = "") = 0; /*! * \brief Closes wallet. In case operation succeded, wallet object deleted. in case operation failed, wallet object not deleted @@ -370,9 +655,57 @@ struct WalletManager */ virtual std::vector<std::string> findWallets(const std::string &path) = 0; + /*! + * \brief checkPayment - checks a payment was made using a txkey + * \param address - the address the payment was sent to + * \param txid - the transaction id for that payment + * \param txkey - the transaction's secret key + * \param daemon_address - the address (host and port) to the daemon to request transaction data + * \param received - if succesful, will hold the amount of monero received + * \param height - if succesful, will hold the height of the transaction (0 if only in the pool) + * \param error - if unsuccesful, will hold an error string with more information about the error + * \return - true is succesful, false otherwise + */ + virtual bool checkPayment(const std::string &address, const std::string &txid, const std::string &txkey, const std::string &daemon_address, uint64_t &received, uint64_t &height, std::string &error) const = 0; + //! returns verbose error string regarding last error; virtual std::string errorString() const = 0; + //! set the daemon address (hostname and port) + virtual void setDaemonAddress(const std::string &address) = 0; + + //! returns whether the daemon can be reached, and its version number + virtual bool connected(uint32_t *version = NULL) const = 0; + + //! returns current blockchain height + virtual uint64_t blockchainHeight() const = 0; + + //! returns current blockchain target height + virtual uint64_t blockchainTargetHeight() const = 0; + + //! returns current network difficulty + virtual uint64_t networkDifficulty() const = 0; + + //! returns current mining hash rate (0 if not mining) + virtual double miningHashRate() const = 0; + + //! returns current block target + virtual uint64_t blockTarget() const = 0; + + //! returns true iff mining + virtual bool isMining() const = 0; + + //! starts mining with the set number of threads + virtual bool startMining(const std::string &address, uint32_t threads = 1, bool background_mining = false, bool ignore_battery = true) = 0; + + //! stops mining + virtual bool stopMining() = 0; + + //! resolves an OpenAlias address to a monero address + virtual std::string resolveOpenAlias(const std::string &address, bool &dnssec_valid) const = 0; + + //! checks for an update and returns version, hash and url + static std::tuple<bool, std::string, std::string, std::string, std::string> checkUpdates(const std::string &software, const std::string &subdir); }; @@ -392,8 +725,11 @@ struct WalletManagerFactory static WalletManager * getWalletManager(); static void setLogLevel(int level); + static void setLogCategories(const std::string &categories); }; } +namespace Bitmonero = Monero; + diff --git a/src/wallet/wallet_args.cpp b/src/wallet/wallet_args.cpp new file mode 100644 index 000000000..1508f3791 --- /dev/null +++ b/src/wallet/wallet_args.cpp @@ -0,0 +1,177 @@ +// Copyright (c) 2014-2017, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 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. +#include "wallet/wallet_args.h" + +#include <boost/filesystem/path.hpp> +#include <boost/format.hpp> +#include "common/i18n.h" +#include "common/scoped_message_writer.h" +#include "common/util.h" +#include "misc_log_ex.h" +#include "string_tools.h" +#include "version.h" + +#if defined(WIN32) +#include <crtdbg.h> +#endif + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "wallet.wallet2" + +// workaround for a suspected bug in pthread/kernel on MacOS X +#ifdef __APPLE__ +#define DEFAULT_MAX_CONCURRENCY 1 +#else +#define DEFAULT_MAX_CONCURRENCY 0 +#endif + + +namespace wallet_args +{ + // Create on-demand to prevent static initialization order fiasco issues. + command_line::arg_descriptor<std::string> arg_generate_from_json() + { + return {"generate-from-json", wallet_args::tr("Generate wallet from JSON format file"), ""}; + } + command_line::arg_descriptor<std::string> arg_wallet_file() + { + return {"wallet-file", wallet_args::tr("Use wallet <arg>"), ""}; + } + + const char* tr(const char* str) + { + return i18n_translate(str, "wallet_args"); + } + + boost::optional<boost::program_options::variables_map> main( + int argc, char** argv, + const char* const usage, + boost::program_options::options_description desc_params, + const boost::program_options::positional_options_description& positional_options, + const char *default_log_name, + bool log_to_console) + + { + namespace bf = boost::filesystem; + namespace po = boost::program_options; +#ifdef WIN32 + _CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF ); +#endif + + const command_line::arg_descriptor<std::string> arg_log_level = {"log-level", "0-4 or categories", ""}; + const command_line::arg_descriptor<uint32_t> arg_max_concurrency = {"max-concurrency", wallet_args::tr("Max number of threads to use for a parallel job"), DEFAULT_MAX_CONCURRENCY}; + const command_line::arg_descriptor<std::string> arg_log_file = {"log-file", wallet_args::tr("Specify log file"), ""}; + const command_line::arg_descriptor<std::string> arg_config_file = {"config-file", wallet_args::tr("Config file"), "", true}; + + + std::string lang = i18n_get_language(); + tools::sanitize_locale(); + tools::set_strict_default_file_permissions(true); + + epee::string_tools::set_module_name_and_folder(argv[0]); + + po::options_description desc_general(wallet_args::tr("General options")); + command_line::add_arg(desc_general, command_line::arg_help); + command_line::add_arg(desc_general, command_line::arg_version); + + command_line::add_arg(desc_params, arg_log_file, ""); + command_line::add_arg(desc_params, arg_log_level); + command_line::add_arg(desc_params, arg_max_concurrency); + command_line::add_arg(desc_params, arg_config_file); + + i18n_set_language("translations", "monero", lang); + + po::options_description desc_all; + desc_all.add(desc_general).add(desc_params); + po::variables_map vm; + bool r = command_line::handle_error_helper(desc_all, [&]() + { + auto parser = po::command_line_parser(argc, argv).options(desc_all).positional(positional_options); + po::store(parser.run(), vm); + + if(command_line::has_arg(vm, arg_config_file)) + { + std::string config = command_line::get_arg(vm, arg_config_file); + bf::path config_path(config); + boost::system::error_code ec; + if (bf::exists(config_path, ec)) + { + po::store(po::parse_config_file<char>(config_path.string<std::string>().c_str(), desc_params), vm); + } + else + { + tools::fail_msg_writer() << wallet_args::tr("Can't find config file ") << config; + return false; + } + } + + po::notify(vm); + return true; + }); + if (!r) + return boost::none; + + std::string log_path; + if (!vm["log-file"].defaulted()) + log_path = command_line::get_arg(vm, arg_log_file); + else + log_path = mlog_get_default_log_path(default_log_name); + mlog_configure(log_path, log_to_console); + if (!vm["log-level"].defaulted()) + { + mlog_set_log(command_line::get_arg(vm, arg_log_level).c_str()); + } + + if (command_line::get_arg(vm, command_line::arg_help)) + { + tools::msg_writer() << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")"; + tools::msg_writer() << wallet_args::tr("Usage:") << ' ' << usage; + tools::msg_writer() << desc_all; + return boost::none; + } + else if (command_line::get_arg(vm, command_line::arg_version)) + { + tools::msg_writer() << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")"; + return boost::none; + } + + if(command_line::has_arg(vm, arg_max_concurrency)) + tools::set_max_concurrency(command_line::get_arg(vm, arg_max_concurrency)); + + tools::scoped_message_writer(epee::console_color_white, true) << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")"; + + if (!vm["log-level"].defaulted()) + MINFO("Setting log level = " << command_line::get_arg(vm, arg_log_level)); + else + MINFO("Setting log levels = " << getenv("MONERO_LOGS")); + MINFO(wallet_args::tr("Logging to: ") << log_path); + tools::scoped_message_writer(epee::console_color_white, true) << boost::format(wallet_args::tr("Logging to %s")) % log_path; + + return {std::move(vm)}; + } +} diff --git a/src/wallet/wallet_args.h b/src/wallet/wallet_args.h new file mode 100644 index 000000000..cf23ffded --- /dev/null +++ b/src/wallet/wallet_args.h @@ -0,0 +1,54 @@ +// Copyright (c) 2014-2017, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 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. +#include <boost/optional/optional.hpp> +#include <boost/program_options/options_description.hpp> +#include <boost/program_options/positional_options.hpp> +#include <boost/program_options/variables_map.hpp> + +#include "common/command_line.h" + +namespace wallet_args +{ + command_line::arg_descriptor<std::string> arg_generate_from_json(); + command_line::arg_descriptor<std::string> arg_wallet_file(); + + const char* tr(const char* str); + + /*! Processes command line arguments (`argc` and `argv`) using `desc_params` + and `positional_options`, while adding parameters for log files and + concurrency. Log file and concurrency arguments are handled, along with basic + global init for the wallet process. + + \return The list of parsed options, iff there are no errors.*/ + boost::optional<boost::program_options::variables_map> main( + int argc, char** argv, + const char* const usage, + boost::program_options::options_description desc_params, + const boost::program_options::positional_options_description& positional_options, + const char *default_log_name, bool log_to_console = false); +} diff --git a/src/wallet/wallet_errors.h b/src/wallet/wallet_errors.h index c5590d79c..3e3578149 100644 --- a/src/wallet/wallet_errors.h +++ b/src/wallet/wallet_errors.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -35,7 +35,7 @@ #include <string> #include <vector> -#include "cryptonote_core/cryptonote_format_utils.h" +#include "cryptonote_basic/cryptonote_format_utils.h" #include "rpc/core_rpc_server_commands_defs.h" #include "include_base_utils.h" @@ -68,6 +68,7 @@ namespace tools // transfer_error * // get_random_outs_general_error // not_enough_money + // tx_not_possible // not_enough_outs_to_mix // tx_not_constructed // tx_rejected @@ -351,6 +352,32 @@ namespace tools : transfer_error(std::move(loc), "not enough money") , m_available(availbable) , m_tx_amount(tx_amount) + { + } + + uint64_t available() const { return m_available; } + uint64_t tx_amount() const { return m_tx_amount; } + + std::string to_string() const + { + std::ostringstream ss; + ss << transfer_error::to_string() << + ", available = " << cryptonote::print_money(m_available) << + ", tx_amount = " << cryptonote::print_money(m_tx_amount); + return ss.str(); + } + + private: + uint64_t m_available; + uint64_t m_tx_amount; + }; + //---------------------------------------------------------------------------------------------------- + struct tx_not_possible : public transfer_error + { + explicit tx_not_possible(std::string&& loc, uint64_t availbable, uint64_t tx_amount, uint64_t fee) + : transfer_error(std::move(loc), "tx not possible") + , m_available(availbable) + , m_tx_amount(tx_amount) , m_fee(fee) { } @@ -599,6 +626,18 @@ namespace tools std::string m_request; }; //---------------------------------------------------------------------------------------------------- + struct wallet_generic_rpc_error : public wallet_rpc_error + { + explicit wallet_generic_rpc_error(std::string&& loc, const std::string& request, const std::string& status) + : wallet_rpc_error(std::move(loc), std::string("error in ") + request + " RPC: " + status, request), + m_status(status) + { + } + const std::string& status() const { return m_status; } + private: + const std::string m_status; + }; + //---------------------------------------------------------------------------------------------------- struct daemon_busy : public wallet_rpc_error { explicit daemon_busy(std::string&& loc, const std::string& request) diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index faa40e166..f2b3dcaf5 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -27,42 +27,74 @@ // 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 <boost/asio/ip/address.hpp> +#include <boost/filesystem/operations.hpp> +#include <cstdint> #include "include_base_utils.h" using namespace epee; #include "wallet_rpc_server.h" +#include "wallet/wallet_args.h" #include "common/command_line.h" -#include "cryptonote_core/cryptonote_format_utils.h" -#include "cryptonote_core/account.h" +#include "common/i18n.h" +#include "common/util.h" +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "cryptonote_basic/account.h" #include "wallet_rpc_server_commands_defs.h" #include "misc_language.h" +#include "string_coding.h" #include "string_tools.h" #include "crypto/hash.h" +#include "mnemonics/electrum-words.h" +#include "rpc/rpc_args.h" +#include "rpc/core_rpc_server_commands_defs.h" + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "wallet.rpc" + +namespace +{ + const command_line::arg_descriptor<std::string, true> arg_rpc_bind_port = {"rpc-bind-port", "Sets bind port for server"}; + const command_line::arg_descriptor<bool> arg_disable_rpc_login = {"disable-rpc-login", "Disable HTTP authentication for RPC connections served by this process"}; + const command_line::arg_descriptor<bool> arg_trusted_daemon = {"trusted-daemon", "Enable commands which rely on a trusted daemon", false}; + const command_line::arg_descriptor<std::string> arg_wallet_dir = {"wallet-dir", "Directory for newly created wallets"}; + + constexpr const char default_rpc_username[] = "monero"; +} namespace tools { - //----------------------------------------------------------------------------------- - const command_line::arg_descriptor<std::string> wallet_rpc_server::arg_rpc_bind_port = {"rpc-bind-port", "Starts wallet as rpc server for wallet operations, sets bind port for server", "", true}; - const command_line::arg_descriptor<std::string> wallet_rpc_server::arg_rpc_bind_ip = {"rpc-bind-ip", "Specify ip to bind rpc server", "127.0.0.1"}; - const command_line::arg_descriptor<std::string> wallet_rpc_server::arg_user_agent = {"user-agent", "Restrict RPC to clients using this user agent", ""}; + const char* wallet_rpc_server::tr(const char* str) + { + return i18n_translate(str, "tools::wallet_rpc_server"); + } - void wallet_rpc_server::init_options(boost::program_options::options_description& desc) + //------------------------------------------------------------------------------------------------------------------------------ + wallet_rpc_server::wallet_rpc_server():m_wallet(NULL), rpc_login_filename(), m_stop(false), m_trusted_daemon(false) + { + } + //------------------------------------------------------------------------------------------------------------------------------ + wallet_rpc_server::~wallet_rpc_server() { - command_line::add_arg(desc, arg_rpc_bind_ip); - command_line::add_arg(desc, arg_rpc_bind_port); - command_line::add_arg(desc, arg_user_agent); + try + { + boost::system::error_code ec{}; + boost::filesystem::remove(rpc_login_filename, ec); + } + catch (...) {} } //------------------------------------------------------------------------------------------------------------------------------ - wallet_rpc_server::wallet_rpc_server(wallet2& w):m_wallet(w) - {} + void wallet_rpc_server::set_wallet(wallet2 *cr) + { + m_wallet = cr; + } //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::run() { m_stop = false; m_net_server.add_idle_handler([this](){ try { - m_wallet.refresh(); + if (m_wallet) m_wallet->refresh(); } catch (const std::exception& ex) { LOG_ERROR("Exception at while refreshing, what=" << ex.what()); } @@ -81,30 +113,196 @@ namespace tools return epee::http_server_impl_base<wallet_rpc_server, connection_context>::run(1, true); } //------------------------------------------------------------------------------------------------------------------------------ - bool wallet_rpc_server::handle_command_line(const boost::program_options::variables_map& vm) + void wallet_rpc_server::stop() { - m_bind_ip = command_line::get_arg(vm, arg_rpc_bind_ip); - m_port = command_line::get_arg(vm, arg_rpc_bind_port); - m_user_agent = command_line::get_arg(vm, arg_user_agent); - return true; + if (m_wallet) + { + m_wallet->store(); + delete m_wallet; + m_wallet = NULL; + } } //------------------------------------------------------------------------------------------------------------------------------ - bool wallet_rpc_server::init(const boost::program_options::variables_map& vm) + bool wallet_rpc_server::init(const boost::program_options::variables_map *vm) { + auto rpc_config = cryptonote::rpc_args::process(*vm); + if (!rpc_config) + return false; + + m_vm = vm; + tools::wallet2 *walvars; + std::unique_ptr<tools::wallet2> tmpwal; + + if (m_wallet) + walvars = m_wallet; + else + { + tmpwal = tools::wallet2::make_dummy(*m_vm); + walvars = tmpwal.get(); + } + boost::optional<epee::net_utils::http::login> http_login{}; + std::string bind_port = command_line::get_arg(*m_vm, arg_rpc_bind_port); + const bool disable_auth = command_line::get_arg(*m_vm, arg_disable_rpc_login); + m_trusted_daemon = command_line::get_arg(*m_vm, arg_trusted_daemon); + if (!command_line::has_arg(*m_vm, arg_trusted_daemon)) + { + if (tools::is_local_address(walvars->get_daemon_address())) + { + MINFO(tr("Daemon is local, assuming trusted")); + m_trusted_daemon = true; + } + } + if (command_line::has_arg(*m_vm, arg_wallet_dir)) + { + m_wallet_dir = command_line::get_arg(*m_vm, arg_wallet_dir); +#ifdef _WIN32 +#define MKDIR(path, mode) mkdir(path) +#else +#define MKDIR(path, mode) mkdir(path, mode) +#endif + MKDIR(m_wallet_dir.c_str(), 0700); + } + + if (disable_auth) + { + if (rpc_config->login) + { + const cryptonote::rpc_args::descriptors arg{}; + LOG_ERROR(tr("Cannot specify --") << arg_disable_rpc_login.name << tr(" and --") << arg.rpc_login.name); + return false; + } + } + else // auth enabled + { + if (!rpc_config->login) + { + std::array<std::uint8_t, 16> rand_128bit{{}}; + crypto::rand(rand_128bit.size(), rand_128bit.data()); + http_login.emplace( + default_rpc_username, + string_encoding::base64_encode(rand_128bit.data(), rand_128bit.size()) + ); + } + else + { + http_login.emplace( + std::move(rpc_config->login->username), std::move(rpc_config->login->password).password() + ); + } + assert(bool(http_login)); + + std::string temp = "monero-wallet-rpc." + bind_port + ".login"; + const auto cookie = tools::create_private_file(temp); + if (!cookie) + { + LOG_ERROR(tr("Failed to create file ") << temp << tr(". Check permissions or remove file")); + return false; + } + rpc_login_filename.swap(temp); // nothrow guarantee destructor cleanup + temp = rpc_login_filename; + std::fputs(http_login->username.c_str(), cookie.get()); + std::fputc(':', cookie.get()); + std::fputs(http_login->password.c_str(), cookie.get()); + std::fflush(cookie.get()); + if (std::ferror(cookie.get())) + { + LOG_ERROR(tr("Error writing to file ") << temp); + return false; + } + LOG_PRINT_L0(tr("RPC username/password is stored in file ") << temp); + } // end auth enabled + + m_http_client.set_server(walvars->get_daemon_address(), walvars->get_daemon_login()); + m_net_server.set_threads_prefix("RPC"); - bool r = handle_command_line(vm); - CHECK_AND_ASSERT_MES(r, false, "Failed to process command line in core_rpc_server"); - return epee::http_server_impl_base<wallet_rpc_server, connection_context>::init(m_port, m_bind_ip, m_user_agent); + return epee::http_server_impl_base<wallet_rpc_server, connection_context>::init( + std::move(bind_port), std::move(rpc_config->bind_ip), std::move(http_login) + ); + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::not_open(epee::json_rpc::error& er) + { + er.code = WALLET_RPC_ERROR_CODE_NOT_OPEN; + er.message = "No wallet file"; + return false; + } + //------------------------------------------------------------------------------------------------------------------------------ + void wallet_rpc_server::fill_transfer_entry(tools::wallet_rpc::transfer_entry &entry, const crypto::hash &txid, const crypto::hash &payment_id, const tools::wallet2::payment_details &pd) + { + entry.txid = string_tools::pod_to_hex(pd.m_tx_hash); + entry.payment_id = string_tools::pod_to_hex(payment_id); + if (entry.payment_id.substr(16).find_first_not_of('0') == std::string::npos) + entry.payment_id = entry.payment_id.substr(0,16); + entry.height = pd.m_block_height; + entry.timestamp = pd.m_timestamp; + entry.amount = pd.m_amount; + entry.fee = 0; // TODO + entry.note = m_wallet->get_tx_note(pd.m_tx_hash); + entry.type = "in"; + } + //------------------------------------------------------------------------------------------------------------------------------ + void wallet_rpc_server::fill_transfer_entry(tools::wallet_rpc::transfer_entry &entry, const crypto::hash &txid, const tools::wallet2::confirmed_transfer_details &pd) + { + entry.txid = string_tools::pod_to_hex(txid); + entry.payment_id = string_tools::pod_to_hex(pd.m_payment_id); + if (entry.payment_id.substr(16).find_first_not_of('0') == std::string::npos) + entry.payment_id = entry.payment_id.substr(0,16); + entry.height = pd.m_block_height; + entry.timestamp = pd.m_timestamp; + entry.fee = pd.m_amount_in - pd.m_amount_out; + uint64_t change = pd.m_change == (uint64_t)-1 ? 0 : pd.m_change; // change may not be known + entry.amount = pd.m_amount_in - change - entry.fee; + entry.note = m_wallet->get_tx_note(txid); + + for (const auto &d: pd.m_dests) { + entry.destinations.push_back(wallet_rpc::transfer_destination()); + wallet_rpc::transfer_destination &td = entry.destinations.back(); + td.amount = d.amount; + td.address = get_account_address_as_str(m_wallet->testnet(), d.addr); + } + + entry.type = "out"; + } + //------------------------------------------------------------------------------------------------------------------------------ + void wallet_rpc_server::fill_transfer_entry(tools::wallet_rpc::transfer_entry &entry, const crypto::hash &txid, const tools::wallet2::unconfirmed_transfer_details &pd) + { + bool is_failed = pd.m_state == tools::wallet2::unconfirmed_transfer_details::failed; + entry.txid = string_tools::pod_to_hex(txid); + entry.payment_id = string_tools::pod_to_hex(pd.m_payment_id); + entry.payment_id = string_tools::pod_to_hex(pd.m_payment_id); + if (entry.payment_id.substr(16).find_first_not_of('0') == std::string::npos) + entry.payment_id = entry.payment_id.substr(0,16); + entry.height = 0; + entry.timestamp = pd.m_timestamp; + entry.fee = pd.m_amount_in - pd.m_amount_out; + entry.amount = pd.m_amount_in - pd.m_change - entry.fee; + entry.note = m_wallet->get_tx_note(txid); + entry.type = is_failed ? "failed" : "pending"; + } + //------------------------------------------------------------------------------------------------------------------------------ + void wallet_rpc_server::fill_transfer_entry(tools::wallet_rpc::transfer_entry &entry, const crypto::hash &payment_id, const tools::wallet2::payment_details &pd) + { + entry.txid = string_tools::pod_to_hex(pd.m_tx_hash); + entry.payment_id = string_tools::pod_to_hex(payment_id); + if (entry.payment_id.substr(16).find_first_not_of('0') == std::string::npos) + entry.payment_id = entry.payment_id.substr(0,16); + entry.height = 0; + entry.timestamp = pd.m_timestamp; + entry.amount = pd.m_amount; + entry.fee = 0; // TODO + entry.note = m_wallet->get_tx_note(pd.m_tx_hash); + entry.type = "pool"; } //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_getbalance(const wallet_rpc::COMMAND_RPC_GET_BALANCE::request& req, wallet_rpc::COMMAND_RPC_GET_BALANCE::response& res, epee::json_rpc::error& er) { + if (!m_wallet) return not_open(er); try { - res.balance = m_wallet.balance(); - res.unlocked_balance = m_wallet.unlocked_balance(); + res.balance = m_wallet->balance(); + res.unlocked_balance = m_wallet->unlocked_balance(); } - catch (std::exception& e) + catch (const std::exception& e) { er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; er.message = e.what(); @@ -115,11 +313,12 @@ namespace tools //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_getaddress(const wallet_rpc::COMMAND_RPC_GET_ADDRESS::request& req, wallet_rpc::COMMAND_RPC_GET_ADDRESS::response& res, epee::json_rpc::error& er) { + if (!m_wallet) return not_open(er); try { - res.address = m_wallet.get_account().get_public_address_str(m_wallet.testnet()); + res.address = m_wallet->get_account().get_public_address_str(m_wallet->testnet()); } - catch (std::exception& e) + catch (const std::exception& e) { er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; er.message = e.what(); @@ -130,11 +329,12 @@ namespace tools //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_getheight(const wallet_rpc::COMMAND_RPC_GET_HEIGHT::request& req, wallet_rpc::COMMAND_RPC_GET_HEIGHT::response& res, epee::json_rpc::error& er) { + if (!m_wallet) return not_open(er); try { - res.height = m_wallet.get_blockchain_current_height(); + res.height = m_wallet->get_blockchain_current_height(); } - catch (std::exception& e) + catch (const std::exception& e) { er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; er.message = e.what(); @@ -152,7 +352,7 @@ namespace tools cryptonote::tx_destination_entry de; bool has_payment_id; crypto::hash8 new_payment_id; - if(!get_account_integrated_address_from_str(de.addr, has_payment_id, new_payment_id, m_wallet.testnet(), it->address)) + if(!get_account_address_from_str_or_url(de.addr, has_payment_id, new_payment_id, m_wallet->testnet(), it->address, false)) { er.code = WALLET_RPC_ERROR_CODE_WRONG_ADDRESS; er.message = std::string("WALLET_RPC_ERROR_CODE_WRONG_ADDRESS: ") + it->address; @@ -195,7 +395,7 @@ namespace tools cryptonote::set_payment_id_to_tx_extra_nonce(extra_nonce, long_payment_id); } /* or short payment ID */ - else if (!wallet2::parse_short_payment_id(payment_id_str, short_payment_id)) { + else if (wallet2::parse_short_payment_id(payment_id_str, short_payment_id)) { cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, short_payment_id); } else { @@ -222,7 +422,9 @@ namespace tools std::vector<cryptonote::tx_destination_entry> dsts; std::vector<uint8_t> extra; - if (m_wallet.restricted()) + LOG_PRINT_L3("on_transfer_split starts"); + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) { er.code = WALLET_RPC_ERROR_CODE_DENIED; er.message = "Command unavailable in restricted mode."; @@ -238,11 +440,11 @@ namespace tools try { uint64_t mixin = req.mixin; - if (mixin < 2 && m_wallet.use_fork_rules(2, 10)) { + if (mixin < 2 && m_wallet->use_fork_rules(2, 10)) { LOG_PRINT_L1("Requested mixin " << req.mixin << " too low for hard fork 2, using 2"); mixin = 2; } - std::vector<wallet2::pending_tx> ptx_vector = m_wallet.create_transactions_2(dsts, mixin, req.unlock_time, req.priority, extra, req.trusted_daemon); + std::vector<wallet2::pending_tx> ptx_vector = m_wallet->create_transactions_2(dsts, mixin, req.unlock_time, req.priority, extra, m_trusted_daemon); // reject proposed transactions if there are more than one. see on_transfer_split below. if (ptx_vector.size() != 1) @@ -252,7 +454,7 @@ namespace tools return false; } - m_wallet.commit_tx(ptx_vector); + m_wallet->commit_tx(ptx_vector); // populate response with tx hash res.tx_hash = epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx_vector.back().tx)); @@ -260,6 +462,7 @@ namespace tools { res.tx_key = epee::string_tools::pod_to_hex(ptx_vector.back().tx_key); } + res.fee = ptx_vector.back().fee; return true; } catch (const tools::error::daemon_busy& e) @@ -289,7 +492,8 @@ namespace tools std::vector<cryptonote::tx_destination_entry> dsts; std::vector<uint8_t> extra; - if (m_wallet.restricted()) + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) { er.code = WALLET_RPC_ERROR_CODE_DENIED; er.message = "Command unavailable in restricted mode."; @@ -305,14 +509,18 @@ namespace tools try { uint64_t mixin = req.mixin; - if (mixin < 2 && m_wallet.use_fork_rules(2, 10)) { + if (mixin < 2 && m_wallet->use_fork_rules(2, 10)) { LOG_PRINT_L1("Requested mixin " << req.mixin << " too low for hard fork 2, using 2"); mixin = 2; } std::vector<wallet2::pending_tx> ptx_vector; - ptx_vector = m_wallet.create_transactions_2(dsts, mixin, req.unlock_time, req.priority, extra, req.trusted_daemon); + LOG_PRINT_L2("on_transfer_split calling create_transactions_2"); + ptx_vector = m_wallet->create_transactions_2(dsts, mixin, req.unlock_time, req.priority, extra, m_trusted_daemon); + LOG_PRINT_L2("on_transfer_split called create_transactions_2"); - m_wallet.commit_tx(ptx_vector); + LOG_PRINT_L2("on_transfer_split calling commit_txyy"); + m_wallet->commit_tx(ptx_vector); + LOG_PRINT_L2("on_transfer_split called commit_txyy"); // populate response with tx hashes for (auto & ptx : ptx_vector) @@ -322,6 +530,7 @@ namespace tools { res.tx_key_list.push_back(epee::string_tools::pod_to_hex(ptx.tx_key)); } + res.fee_list.push_back(ptx.fee); } return true; @@ -349,7 +558,8 @@ namespace tools //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_sweep_dust(const wallet_rpc::COMMAND_RPC_SWEEP_DUST::request& req, wallet_rpc::COMMAND_RPC_SWEEP_DUST::response& res, epee::json_rpc::error& er) { - if (m_wallet.restricted()) + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) { er.code = WALLET_RPC_ERROR_CODE_DENIED; er.message = "Command unavailable in restricted mode."; @@ -358,9 +568,9 @@ namespace tools try { - std::vector<wallet2::pending_tx> ptx_vector = m_wallet.create_unmixable_sweep_transactions(req.trusted_daemon); + std::vector<wallet2::pending_tx> ptx_vector = m_wallet->create_unmixable_sweep_transactions(m_trusted_daemon); - m_wallet.commit_tx(ptx_vector); + m_wallet->commit_tx(ptx_vector); // populate response with tx hashes for (auto & ptx : ptx_vector) @@ -370,6 +580,7 @@ namespace tools { res.tx_key_list.push_back(epee::string_tools::pod_to_hex(ptx.tx_key)); } + res.fee_list.push_back(ptx.fee); } return true; @@ -400,7 +611,8 @@ namespace tools std::vector<cryptonote::tx_destination_entry> dsts; std::vector<uint8_t> extra; - if (m_wallet.restricted()) + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) { er.code = WALLET_RPC_ERROR_CODE_DENIED; er.message = "Command unavailable in restricted mode."; @@ -419,9 +631,9 @@ namespace tools try { - std::vector<wallet2::pending_tx> ptx_vector = m_wallet.create_transactions_all(dsts[0].addr, req.mixin, req.unlock_time, req.priority, extra, req.trusted_daemon); + std::vector<wallet2::pending_tx> ptx_vector = m_wallet->create_transactions_all(req.below_amount, dsts[0].addr, req.mixin, req.unlock_time, req.priority, extra, m_trusted_daemon); - m_wallet.commit_tx(ptx_vector); + m_wallet->commit_tx(ptx_vector); // populate response with tx hashes for (auto & ptx : ptx_vector) @@ -431,6 +643,7 @@ namespace tools { res.tx_key_list.push_back(epee::string_tools::pod_to_hex(ptx.tx_key)); } + res.fee_list.push_back(ptx.fee); } return true; @@ -458,6 +671,7 @@ namespace tools //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_make_integrated_address(const wallet_rpc::COMMAND_RPC_MAKE_INTEGRATED_ADDRESS::request& req, wallet_rpc::COMMAND_RPC_MAKE_INTEGRATED_ADDRESS::response& res, epee::json_rpc::error& er) { + if (!m_wallet) return not_open(er); try { crypto::hash8 payment_id; @@ -475,11 +689,11 @@ namespace tools } } - res.integrated_address = m_wallet.get_account().get_public_integrated_address_str(payment_id, m_wallet.testnet()); + res.integrated_address = m_wallet->get_account().get_public_integrated_address_str(payment_id, m_wallet->testnet()); res.payment_id = epee::string_tools::pod_to_hex(payment_id); return true; } - catch (std::exception &e) + catch (const std::exception &e) { er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; er.message = e.what(); @@ -490,13 +704,14 @@ namespace tools //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_split_integrated_address(const wallet_rpc::COMMAND_RPC_SPLIT_INTEGRATED_ADDRESS::request& req, wallet_rpc::COMMAND_RPC_SPLIT_INTEGRATED_ADDRESS::response& res, epee::json_rpc::error& er) { + if (!m_wallet) return not_open(er); try { cryptonote::account_public_address address; crypto::hash8 payment_id; bool has_payment_id; - if(!get_account_integrated_address_from_str(address, has_payment_id, payment_id, m_wallet.testnet(), req.integrated_address)) + if(!get_account_integrated_address_from_str(address, has_payment_id, payment_id, m_wallet->testnet(), req.integrated_address)) { er.code = WALLET_RPC_ERROR_CODE_WRONG_ADDRESS; er.message = "Invalid address"; @@ -508,11 +723,11 @@ namespace tools er.message = "Address is not an integrated address"; return false; } - res.standard_address = get_account_address_as_str(m_wallet.testnet(),address); + res.standard_address = get_account_address_as_str(m_wallet->testnet(),address); res.payment_id = epee::string_tools::pod_to_hex(payment_id); return true; } - catch (std::exception &e) + catch (const std::exception &e) { er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; er.message = e.what(); @@ -523,7 +738,8 @@ namespace tools //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_store(const wallet_rpc::COMMAND_RPC_STORE::request& req, wallet_rpc::COMMAND_RPC_STORE::response& res, epee::json_rpc::error& er) { - if (m_wallet.restricted()) + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) { er.code = WALLET_RPC_ERROR_CODE_DENIED; er.message = "Command unavailable in restricted mode."; @@ -532,9 +748,9 @@ namespace tools try { - m_wallet.store(); + m_wallet->store(); } - catch (std::exception& e) + catch (const std::exception& e) { er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; er.message = e.what(); @@ -545,13 +761,14 @@ namespace tools //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_get_payments(const wallet_rpc::COMMAND_RPC_GET_PAYMENTS::request& req, wallet_rpc::COMMAND_RPC_GET_PAYMENTS::response& res, epee::json_rpc::error& er) { + if (!m_wallet) return not_open(er); crypto::hash payment_id; crypto::hash8 payment_id8; cryptonote::blobdata payment_id_blob; if(!epee::string_tools::parse_hexstr_to_binbuff(req.payment_id, payment_id_blob)) { er.code = WALLET_RPC_ERROR_CODE_WRONG_PAYMENT_ID; - er.message = "Payment ID has invald format"; + er.message = "Payment ID has invalid format"; return false; } @@ -574,7 +791,7 @@ namespace tools res.payments.clear(); std::list<wallet2::payment_details> payment_list; - m_wallet.get_payments(payment_id, payment_list); + m_wallet->get_payments(payment_id, payment_list); for (auto & payment : payment_list) { wallet_rpc::payment_details rpc_payment; @@ -592,12 +809,13 @@ namespace tools bool wallet_rpc_server::on_get_bulk_payments(const wallet_rpc::COMMAND_RPC_GET_BULK_PAYMENTS::request& req, wallet_rpc::COMMAND_RPC_GET_BULK_PAYMENTS::response& res, epee::json_rpc::error& er) { res.payments.clear(); + if (!m_wallet) return not_open(er); /* If the payment ID list is empty, we get payments to any payment ID (or lack thereof) */ if (req.payment_ids.empty()) { std::list<std::pair<crypto::hash,wallet2::payment_details>> payment_list; - m_wallet.get_payments(payment_list, req.min_block_height); + m_wallet->get_payments(payment_list, req.min_block_height); for (auto & payment : payment_list) { @@ -646,7 +864,7 @@ namespace tools } std::list<wallet2::payment_details> payment_list; - m_wallet.get_payments(payment_id, payment_list, req.min_block_height); + m_wallet->get_payments(payment_id, payment_list, req.min_block_height); for (auto & payment : payment_list) { @@ -665,6 +883,7 @@ namespace tools //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_incoming_transfers(const wallet_rpc::COMMAND_RPC_INCOMING_TRANSFERS::request& req, wallet_rpc::COMMAND_RPC_INCOMING_TRANSFERS::response& res, epee::json_rpc::error& er) { + if (!m_wallet) return not_open(er); if(req.transfer_type.compare("all") != 0 && req.transfer_type.compare("available") != 0 && req.transfer_type.compare("unavailable") != 0) { er.code = WALLET_RPC_ERROR_CODE_TRANSFER_TYPE; @@ -686,7 +905,7 @@ namespace tools } wallet2::transfer_container transfers; - m_wallet.get_transfers(transfers); + m_wallet->get_transfers(transfers); bool transfers_found = false; for (const auto& td : transfers) @@ -713,7 +932,8 @@ namespace tools //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_query_key(const wallet_rpc::COMMAND_RPC_QUERY_KEY::request& req, wallet_rpc::COMMAND_RPC_QUERY_KEY::response& res, epee::json_rpc::error& er) { - if (m_wallet.restricted()) + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) { er.code = WALLET_RPC_ERROR_CODE_DENIED; er.message = "Command unavailable in restricted mode."; @@ -722,7 +942,7 @@ namespace tools if (req.key_type.compare("mnemonic") == 0) { - if (!m_wallet.get_seed(res.key)) + if (!m_wallet->get_seed(res.key)) { er.message = "The wallet is non-deterministic. Cannot display seed."; return false; @@ -730,7 +950,7 @@ namespace tools } else if(req.key_type.compare("view_key") == 0) { - res.key = string_tools::pod_to_hex(m_wallet.get_account().get_keys().m_view_secret_key); + res.key = string_tools::pod_to_hex(m_wallet->get_account().get_keys().m_view_secret_key); } else { @@ -743,7 +963,8 @@ namespace tools //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_rescan_blockchain(const wallet_rpc::COMMAND_RPC_RESCAN_BLOCKCHAIN::request& req, wallet_rpc::COMMAND_RPC_RESCAN_BLOCKCHAIN::response& res, epee::json_rpc::error& er) { - if (m_wallet.restricted()) + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) { er.code = WALLET_RPC_ERROR_CODE_DENIED; er.message = "Command unavailable in restricted mode."; @@ -752,9 +973,9 @@ namespace tools try { - m_wallet.rescan_blockchain(); + m_wallet->rescan_blockchain(); } - catch (std::exception& e) + catch (const std::exception& e) { er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; er.message = e.what(); @@ -765,20 +986,22 @@ namespace tools //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_sign(const wallet_rpc::COMMAND_RPC_SIGN::request& req, wallet_rpc::COMMAND_RPC_SIGN::response& res, epee::json_rpc::error& er) { - if (m_wallet.restricted()) + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) { er.code = WALLET_RPC_ERROR_CODE_DENIED; er.message = "Command unavailable in restricted mode."; return false; } - res.signature = m_wallet.sign(req.data); + res.signature = m_wallet->sign(req.data); return true; } //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_verify(const wallet_rpc::COMMAND_RPC_VERIFY::request& req, wallet_rpc::COMMAND_RPC_VERIFY::response& res, epee::json_rpc::error& er) { - if (m_wallet.restricted()) + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) { er.code = WALLET_RPC_ERROR_CODE_DENIED; er.message = "Command unavailable in restricted mode."; @@ -788,20 +1011,21 @@ namespace tools cryptonote::account_public_address address; bool has_payment_id; crypto::hash8 payment_id; - if(!get_account_integrated_address_from_str(address, has_payment_id, payment_id, m_wallet.testnet(), req.address)) + if(!get_account_address_from_str_or_url(address, has_payment_id, payment_id, m_wallet->testnet(), req.address, false)) { er.code = WALLET_RPC_ERROR_CODE_WRONG_ADDRESS; er.message = ""; return false; } - res.good = m_wallet.verify(req.data, address, req.signature); + res.good = m_wallet->verify(req.data, address, req.signature); return true; } //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_stop_wallet(const wallet_rpc::COMMAND_RPC_STOP_WALLET::request& req, wallet_rpc::COMMAND_RPC_STOP_WALLET::response& res, epee::json_rpc::error& er) { - if (m_wallet.restricted()) + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) { er.code = WALLET_RPC_ERROR_CODE_DENIED; er.message = "Command unavailable in restricted mode."; @@ -810,10 +1034,10 @@ namespace tools try { - m_wallet.store(); + m_wallet->store(); m_stop.store(true, std::memory_order_relaxed); } - catch (std::exception& e) + catch (const std::exception& e) { er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; er.message = e.what(); @@ -824,6 +1048,14 @@ namespace tools //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_set_tx_notes(const wallet_rpc::COMMAND_RPC_SET_TX_NOTES::request& req, wallet_rpc::COMMAND_RPC_SET_TX_NOTES::response& res, epee::json_rpc::error& er) { + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) + { + er.code = WALLET_RPC_ERROR_CODE_DENIED; + er.message = "Command unavailable in restricted mode."; + return false; + } + if (req.txids.size() != req.notes.size()) { er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; @@ -836,7 +1068,7 @@ namespace tools while (i != req.txids.end()) { cryptonote::blobdata txid_blob; - if(!epee::string_tools::parse_hexstr_to_binbuff(*i++, txid_blob)) + if(!epee::string_tools::parse_hexstr_to_binbuff(*i++, txid_blob) || txid_blob.size() != sizeof(crypto::hash)) { er.code = WALLET_RPC_ERROR_CODE_WRONG_TXID; er.message = "TX ID has invalid format"; @@ -851,7 +1083,7 @@ namespace tools std::list<std::string>::const_iterator in = req.notes.begin(); while (il != txids.end()) { - m_wallet.set_tx_note(*il++, *in++); + m_wallet->set_tx_note(*il++, *in++); } return true; @@ -860,13 +1092,14 @@ namespace tools bool wallet_rpc_server::on_get_tx_notes(const wallet_rpc::COMMAND_RPC_GET_TX_NOTES::request& req, wallet_rpc::COMMAND_RPC_GET_TX_NOTES::response& res, epee::json_rpc::error& er) { res.notes.clear(); + if (!m_wallet) return not_open(er); std::list<crypto::hash> txids; std::list<std::string>::const_iterator i = req.txids.begin(); while (i != req.txids.end()) { cryptonote::blobdata txid_blob; - if(!epee::string_tools::parse_hexstr_to_binbuff(*i++, txid_blob)) + if(!epee::string_tools::parse_hexstr_to_binbuff(*i++, txid_blob) || txid_blob.size() != sizeof(crypto::hash)) { er.code = WALLET_RPC_ERROR_CODE_WRONG_TXID; er.message = "TX ID has invalid format"; @@ -880,14 +1113,15 @@ namespace tools std::list<crypto::hash>::const_iterator il = txids.begin(); while (il != txids.end()) { - res.notes.push_back(m_wallet.get_tx_note(*il++)); + res.notes.push_back(m_wallet->get_tx_note(*il++)); } return true; } //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_get_transfers(const wallet_rpc::COMMAND_RPC_GET_TRANSFERS::request& req, wallet_rpc::COMMAND_RPC_GET_TRANSFERS::response& res, epee::json_rpc::error& er) { - if (m_wallet.restricted()) + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) { er.code = WALLET_RPC_ERROR_CODE_DENIED; er.message = "Command unavailable in restricted mode."; @@ -904,109 +1138,135 @@ namespace tools if (req.in) { std::list<std::pair<crypto::hash, tools::wallet2::payment_details>> payments; - m_wallet.get_payments(payments, min_height, max_height); + m_wallet->get_payments(payments, min_height, max_height); for (std::list<std::pair<crypto::hash, tools::wallet2::payment_details>>::const_iterator i = payments.begin(); i != payments.end(); ++i) { - res.in.push_back(wallet_rpc::COMMAND_RPC_GET_TRANSFERS::entry()); - wallet_rpc::COMMAND_RPC_GET_TRANSFERS::entry &entry = res.in.back(); - const tools::wallet2::payment_details &pd = i->second; - - entry.txid = string_tools::pod_to_hex(pd.m_tx_hash); - entry.payment_id = string_tools::pod_to_hex(i->first); - if (entry.payment_id.substr(16).find_first_not_of('0') == std::string::npos) - entry.payment_id = entry.payment_id.substr(0,16); - entry.height = pd.m_block_height; - entry.timestamp = pd.m_timestamp; - entry.amount = pd.m_amount; - entry.fee = 0; // TODO - entry.note = m_wallet.get_tx_note(pd.m_tx_hash); + res.in.push_back(wallet_rpc::transfer_entry()); + fill_transfer_entry(res.in.back(), i->second.m_tx_hash, i->first, i->second); } } if (req.out) { std::list<std::pair<crypto::hash, tools::wallet2::confirmed_transfer_details>> payments; - m_wallet.get_payments_out(payments, min_height, max_height); + m_wallet->get_payments_out(payments, min_height, max_height); for (std::list<std::pair<crypto::hash, tools::wallet2::confirmed_transfer_details>>::const_iterator i = payments.begin(); i != payments.end(); ++i) { - res.out.push_back(wallet_rpc::COMMAND_RPC_GET_TRANSFERS::entry()); - wallet_rpc::COMMAND_RPC_GET_TRANSFERS::entry &entry = res.out.back(); - const tools::wallet2::confirmed_transfer_details &pd = i->second; - - entry.txid = string_tools::pod_to_hex(i->first); - entry.payment_id = string_tools::pod_to_hex(i->second.m_payment_id); - if (entry.payment_id.substr(16).find_first_not_of('0') == std::string::npos) - entry.payment_id = entry.payment_id.substr(0,16); - entry.height = pd.m_block_height; - entry.timestamp = pd.m_timestamp; - entry.fee = pd.m_amount_in - pd.m_amount_out; - uint64_t change = pd.m_change == (uint64_t)-1 ? 0 : pd.m_change; // change may not be known - entry.amount = pd.m_amount_in - change - entry.fee; - entry.note = m_wallet.get_tx_note(i->first); - - for (const auto &d: pd.m_dests) { - entry.destinations.push_back(wallet_rpc::transfer_destination()); - wallet_rpc::transfer_destination &td = entry.destinations.back(); - td.amount = d.amount; - td.address = get_account_address_as_str(m_wallet.testnet(), d.addr); - } + res.out.push_back(wallet_rpc::transfer_entry()); + fill_transfer_entry(res.out.back(), i->first, i->second); } } if (req.pending || req.failed) { std::list<std::pair<crypto::hash, tools::wallet2::unconfirmed_transfer_details>> upayments; - m_wallet.get_unconfirmed_payments_out(upayments); + m_wallet->get_unconfirmed_payments_out(upayments); for (std::list<std::pair<crypto::hash, tools::wallet2::unconfirmed_transfer_details>>::const_iterator i = upayments.begin(); i != upayments.end(); ++i) { const tools::wallet2::unconfirmed_transfer_details &pd = i->second; bool is_failed = pd.m_state == tools::wallet2::unconfirmed_transfer_details::failed; if (!((req.failed && is_failed) || (!is_failed && req.pending))) continue; - std::list<wallet_rpc::COMMAND_RPC_GET_TRANSFERS::entry> &entries = is_failed ? res.failed : res.pending; - entries.push_back(wallet_rpc::COMMAND_RPC_GET_TRANSFERS::entry()); - wallet_rpc::COMMAND_RPC_GET_TRANSFERS::entry &entry = entries.back(); - - entry.txid = string_tools::pod_to_hex(i->first); - entry.payment_id = string_tools::pod_to_hex(i->second.m_payment_id); - entry.payment_id = string_tools::pod_to_hex(i->second.m_payment_id); - if (entry.payment_id.substr(16).find_first_not_of('0') == std::string::npos) - entry.payment_id = entry.payment_id.substr(0,16); - entry.height = 0; - entry.timestamp = pd.m_timestamp; - entry.fee = pd.m_amount_in - pd.m_amount_out; - entry.amount = pd.m_amount_in - pd.m_change - entry.fee; - entry.note = m_wallet.get_tx_note(i->first); + std::list<wallet_rpc::transfer_entry> &entries = is_failed ? res.failed : res.pending; + entries.push_back(wallet_rpc::transfer_entry()); + fill_transfer_entry(entries.back(), i->first, i->second); } } if (req.pool) { - m_wallet.update_pool_state(); + m_wallet->update_pool_state(); std::list<std::pair<crypto::hash, tools::wallet2::payment_details>> payments; - m_wallet.get_unconfirmed_payments(payments); + m_wallet->get_unconfirmed_payments(payments); for (std::list<std::pair<crypto::hash, tools::wallet2::payment_details>>::const_iterator i = payments.begin(); i != payments.end(); ++i) { - res.pool.push_back(wallet_rpc::COMMAND_RPC_GET_TRANSFERS::entry()); - wallet_rpc::COMMAND_RPC_GET_TRANSFERS::entry &entry = res.pool.back(); - const tools::wallet2::payment_details &pd = i->second; - - entry.txid = string_tools::pod_to_hex(pd.m_tx_hash); - entry.payment_id = string_tools::pod_to_hex(i->first); - if (entry.payment_id.substr(16).find_first_not_of('0') == std::string::npos) - entry.payment_id = entry.payment_id.substr(0,16); - entry.height = 0; - entry.timestamp = pd.m_timestamp; - entry.amount = pd.m_amount; - entry.fee = 0; // TODO - entry.note = m_wallet.get_tx_note(pd.m_tx_hash); + res.pool.push_back(wallet_rpc::transfer_entry()); + fill_transfer_entry(res.pool.back(), i->first, i->second); } } return true; } //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_get_transfer_by_txid(const wallet_rpc::COMMAND_RPC_GET_TRANSFER_BY_TXID::request& req, wallet_rpc::COMMAND_RPC_GET_TRANSFER_BY_TXID::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) + { + er.code = WALLET_RPC_ERROR_CODE_DENIED; + er.message = "Command unavailable in restricted mode."; + return false; + } + + crypto::hash txid; + cryptonote::blobdata txid_blob; + if(!epee::string_tools::parse_hexstr_to_binbuff(req.txid, txid_blob)) + { + er.code = WALLET_RPC_ERROR_CODE_WRONG_TXID; + er.message = "Transaction ID has invalid format"; + return false; + } + + if(sizeof(txid) == txid_blob.size()) + { + txid = *reinterpret_cast<const crypto::hash*>(txid_blob.data()); + } + else + { + er.code = WALLET_RPC_ERROR_CODE_WRONG_TXID; + er.message = "Transaction ID has invalid size: " + req.txid; + return false; + } + + std::list<std::pair<crypto::hash, tools::wallet2::payment_details>> payments; + m_wallet->get_payments(payments, 0); + for (std::list<std::pair<crypto::hash, tools::wallet2::payment_details>>::const_iterator i = payments.begin(); i != payments.end(); ++i) { + if (i->second.m_tx_hash == txid) + { + fill_transfer_entry(res.transfer, i->second.m_tx_hash, i->first, i->second); + return true; + } + } + + std::list<std::pair<crypto::hash, tools::wallet2::confirmed_transfer_details>> payments_out; + m_wallet->get_payments_out(payments_out, 0); + for (std::list<std::pair<crypto::hash, tools::wallet2::confirmed_transfer_details>>::const_iterator i = payments_out.begin(); i != payments_out.end(); ++i) { + if (i->first == txid) + { + fill_transfer_entry(res.transfer, i->first, i->second); + return true; + } + } + + std::list<std::pair<crypto::hash, tools::wallet2::unconfirmed_transfer_details>> upayments; + m_wallet->get_unconfirmed_payments_out(upayments); + for (std::list<std::pair<crypto::hash, tools::wallet2::unconfirmed_transfer_details>>::const_iterator i = upayments.begin(); i != upayments.end(); ++i) { + if (i->first == txid) + { + fill_transfer_entry(res.transfer, i->first, i->second); + return true; + } + } + + m_wallet->update_pool_state(); + + std::list<std::pair<crypto::hash, tools::wallet2::payment_details>> pool_payments; + m_wallet->get_unconfirmed_payments(pool_payments); + for (std::list<std::pair<crypto::hash, tools::wallet2::payment_details>>::const_iterator i = pool_payments.begin(); i != pool_payments.end(); ++i) { + if (i->second.m_tx_hash == txid) + { + fill_transfer_entry(res.transfer, i->first, i->second); + return true; + } + } + + er.code = WALLET_RPC_ERROR_CODE_WRONG_TXID; + er.message = "Transaction not found."; + return false; + } + //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_export_key_images(const wallet_rpc::COMMAND_RPC_EXPORT_KEY_IMAGES::request& req, wallet_rpc::COMMAND_RPC_EXPORT_KEY_IMAGES::response& res, epee::json_rpc::error& er) { + if (!m_wallet) return not_open(er); try { - std::vector<std::pair<crypto::key_image, crypto::signature>> ski = m_wallet.export_key_images(); + std::vector<std::pair<crypto::key_image, crypto::signature>> ski = m_wallet->export_key_images(); res.signed_key_images.resize(ski.size()); for (size_t n = 0; n < ski.size(); ++n) { @@ -1027,6 +1287,14 @@ namespace tools //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_import_key_images(const wallet_rpc::COMMAND_RPC_IMPORT_KEY_IMAGES::request& req, wallet_rpc::COMMAND_RPC_IMPORT_KEY_IMAGES::response& res, epee::json_rpc::error& er) { + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) + { + er.code = WALLET_RPC_ERROR_CODE_DENIED; + er.message = "Command unavailable in restricted mode."; + return false; + } + try { std::vector<std::pair<crypto::key_image, crypto::signature>> ski; @@ -1035,7 +1303,7 @@ namespace tools { cryptonote::blobdata bd; - if(!epee::string_tools::parse_hexstr_to_binbuff(req.signed_key_images[n].key_image, bd)) + if(!epee::string_tools::parse_hexstr_to_binbuff(req.signed_key_images[n].key_image, bd) || bd.size() != sizeof(crypto::key_image)) { er.code = WALLET_RPC_ERROR_CODE_WRONG_KEY_IMAGE; er.message = "failed to parse key image"; @@ -1043,7 +1311,7 @@ namespace tools } ski[n].first = *reinterpret_cast<const crypto::key_image*>(bd.data()); - if(!epee::string_tools::parse_hexstr_to_binbuff(req.signed_key_images[n].signature, bd)) + if(!epee::string_tools::parse_hexstr_to_binbuff(req.signed_key_images[n].signature, bd) || bd.size() != sizeof(crypto::signature)) { er.code = WALLET_RPC_ERROR_CODE_WRONG_SIGNATURE; er.message = "failed to parse signature"; @@ -1052,7 +1320,7 @@ namespace tools ski[n].second = *reinterpret_cast<const crypto::signature*>(bd.data()); } uint64_t spent = 0, unspent = 0; - uint64_t height = m_wallet.import_key_images(ski, spent, unspent); + uint64_t height = m_wallet->import_key_images(ski, spent, unspent); res.spent = spent; res.unspent = unspent; res.height = height; @@ -1068,5 +1336,482 @@ namespace tools return true; } //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_make_uri(const wallet_rpc::COMMAND_RPC_MAKE_URI::request& req, wallet_rpc::COMMAND_RPC_MAKE_URI::response& res, epee::json_rpc::error& er) + { + std::string error; + std::string uri = m_wallet->make_uri(req.address, req.payment_id, req.amount, req.tx_description, req.recipient_name, error); + if (uri.empty()) + { + er.code = WALLET_RPC_ERROR_CODE_WRONG_URI; + er.message = std::string("Cannot make URI from supplied parameters: ") + error; + return false; + } + + res.uri = uri; + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_parse_uri(const wallet_rpc::COMMAND_RPC_PARSE_URI::request& req, wallet_rpc::COMMAND_RPC_PARSE_URI::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + std::string error; + if (!m_wallet->parse_uri(req.uri, res.uri.address, res.uri.payment_id, res.uri.amount, res.uri.tx_description, res.uri.recipient_name, res.unknown_parameters, error)) + { + er.code = WALLET_RPC_ERROR_CODE_WRONG_URI; + er.message = "Error parsing URI: " + error; + return false; + } + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_get_address_book(const wallet_rpc::COMMAND_RPC_GET_ADDRESS_BOOK_ENTRY::request& req, wallet_rpc::COMMAND_RPC_GET_ADDRESS_BOOK_ENTRY::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + const auto ab = m_wallet->get_address_book(); + if (req.entries.empty()) + { + uint64_t idx = 0; + for (const auto &entry: ab) + res.entries.push_back(wallet_rpc::COMMAND_RPC_GET_ADDRESS_BOOK_ENTRY::entry{idx++, get_account_address_as_str(m_wallet->testnet(), entry.m_address), epee::string_tools::pod_to_hex(entry.m_payment_id), entry.m_description}); + } + else + { + for (uint64_t idx: req.entries) + { + if (idx >= ab.size()) + { + er.code = WALLET_RPC_ERROR_CODE_WRONG_INDEX; + er.message = "Index out of range: " + std::to_string(idx); + return false; + } + const auto &entry = ab[idx]; + res.entries.push_back(wallet_rpc::COMMAND_RPC_GET_ADDRESS_BOOK_ENTRY::entry{idx, get_account_address_as_str(m_wallet->testnet(), entry.m_address), epee::string_tools::pod_to_hex(entry.m_payment_id), entry.m_description}); + } + } + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_add_address_book(const wallet_rpc::COMMAND_RPC_ADD_ADDRESS_BOOK_ENTRY::request& req, wallet_rpc::COMMAND_RPC_ADD_ADDRESS_BOOK_ENTRY::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) + { + er.code = WALLET_RPC_ERROR_CODE_DENIED; + er.message = "Command unavailable in restricted mode."; + return false; + } + + cryptonote::account_public_address address; + bool has_payment_id; + crypto::hash8 payment_id8; + crypto::hash payment_id = cryptonote::null_hash; + if(!get_account_address_from_str_or_url(address, has_payment_id, payment_id8, m_wallet->testnet(), req.address, false)) + { + er.code = WALLET_RPC_ERROR_CODE_WRONG_ADDRESS; + er.message = std::string("WALLET_RPC_ERROR_CODE_WRONG_ADDRESS: ") + req.address; + return false; + } + if (has_payment_id) + { + memcpy(payment_id.data, payment_id8.data, 8); + memset(payment_id.data + 8, 0, 24); + } + if (!req.payment_id.empty()) + { + if (has_payment_id) + { + er.code = WALLET_RPC_ERROR_CODE_WRONG_PAYMENT_ID; + er.message = "Separate payment ID given with integrated address"; + return false; + } + + crypto::hash long_payment_id; + crypto::hash8 short_payment_id; + + if (!wallet2::parse_long_payment_id(req.payment_id, payment_id)) + { + if (!wallet2::parse_short_payment_id(req.payment_id, payment_id8)) + { + er.code = WALLET_RPC_ERROR_CODE_WRONG_PAYMENT_ID; + er.message = "Payment id has invalid format: \"" + req.payment_id + "\", expected 16 or 64 character string"; + return false; + } + else + { + memcpy(payment_id.data, payment_id8.data, 8); + memset(payment_id.data + 8, 0, 24); + } + } + } + if (!m_wallet->add_address_book_row(address, payment_id, req.description)) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Failed to add address book entry"; + return false; + } + res.index = m_wallet->get_address_book().size(); + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_delete_address_book(const wallet_rpc::COMMAND_RPC_DELETE_ADDRESS_BOOK_ENTRY::request& req, wallet_rpc::COMMAND_RPC_DELETE_ADDRESS_BOOK_ENTRY::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) + { + er.code = WALLET_RPC_ERROR_CODE_DENIED; + er.message = "Command unavailable in restricted mode."; + return false; + } + + const auto ab = m_wallet->get_address_book(); + if (req.index >= ab.size()) + { + er.code = WALLET_RPC_ERROR_CODE_WRONG_INDEX; + er.message = "Index out of range: " + std::to_string(req.index); + return false; + } + if (!m_wallet->delete_address_book_row(req.index)) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Failed to delete address book entry"; + return false; + } + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_rescan_spent(const wallet_rpc::COMMAND_RPC_RESCAN_SPENT::request& req, wallet_rpc::COMMAND_RPC_RESCAN_SPENT::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) + { + er.code = WALLET_RPC_ERROR_CODE_DENIED; + er.message = "Command unavailable in restricted mode."; + return false; + } + try + { + m_wallet->rescan_spent(); + return true; + } + catch (const std::exception &e) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = e.what(); + return false; + } + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_start_mining(const wallet_rpc::COMMAND_RPC_START_MINING::request& req, wallet_rpc::COMMAND_RPC_START_MINING::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + if (!m_trusted_daemon) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "This command requires a trusted daemon."; + return false; + } + + size_t max_mining_threads_count = (std::max)(tools::get_max_concurrency(), static_cast<unsigned>(2)); + if (req.threads_count < 1 || max_mining_threads_count < req.threads_count) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "The specified number of threads is inappropriate."; + return false; + } + + cryptonote::COMMAND_RPC_START_MINING::request daemon_req = AUTO_VAL_INIT(daemon_req); + daemon_req.miner_address = m_wallet->get_account().get_public_address_str(m_wallet->testnet()); + daemon_req.threads_count = req.threads_count; + daemon_req.do_background_mining = req.do_background_mining; + daemon_req.ignore_battery = req.ignore_battery; + + cryptonote::COMMAND_RPC_START_MINING::response daemon_res; + bool r = net_utils::invoke_http_json("/start_mining", daemon_req, daemon_res, m_http_client); + if (!r || daemon_res.status != CORE_RPC_STATUS_OK) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Couldn't start mining due to unknown error."; + return false; + } + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_stop_mining(const wallet_rpc::COMMAND_RPC_STOP_MINING::request& req, wallet_rpc::COMMAND_RPC_STOP_MINING::response& res, epee::json_rpc::error& er) + { + cryptonote::COMMAND_RPC_STOP_MINING::request daemon_req; + cryptonote::COMMAND_RPC_STOP_MINING::response daemon_res; + bool r = net_utils::invoke_http_json("/stop_mining", daemon_req, daemon_res, m_http_client); + if (!r || daemon_res.status != CORE_RPC_STATUS_OK) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Couldn't stop mining due to unknown error."; + return false; + } + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_get_languages(const wallet_rpc::COMMAND_RPC_GET_LANGUAGES::request& req, wallet_rpc::COMMAND_RPC_GET_LANGUAGES::response& res, epee::json_rpc::error& er) + { + crypto::ElectrumWords::get_language_list(res.languages); + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_create_wallet(const wallet_rpc::COMMAND_RPC_CREATE_WALLET::request& req, wallet_rpc::COMMAND_RPC_CREATE_WALLET::response& res, epee::json_rpc::error& er) + { + if (m_wallet_dir.empty()) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "No wallet dir configured"; + return false; + } + + namespace po = boost::program_options; + po::variables_map vm2; + const char *ptr = strchr(req.filename.c_str(), '/'); +#ifdef _WIN32 + if (!ptr) + ptr = strchr(req.filename.c_str(), '\\'); + if (!ptr) + ptr = strchr(req.filename.c_str(), ':'); +#endif + if (ptr) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Invalid filename"; + return false; + } + std::string wallet_file = m_wallet_dir + "/" + req.filename; + { + std::vector<std::string> languages; + crypto::ElectrumWords::get_language_list(languages); + std::vector<std::string>::iterator it; + std::string wallet_file; + char *ptr; + + it = std::find(languages.begin(), languages.end(), req.language); + if (it == languages.end()) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Unknown language"; + return false; + } + } + { + po::options_description desc("dummy"); + const command_line::arg_descriptor<std::string, true> arg_password = {"password", "password"}; + const char *argv[4]; + int argc = 3; + argv[0] = "wallet-rpc"; + argv[1] = "--password"; + argv[2] = req.password.c_str(); + argv[3] = NULL; + vm2 = *m_vm; + command_line::add_arg(desc, arg_password); + po::store(po::parse_command_line(argc, argv, desc), vm2); + } + std::unique_ptr<tools::wallet2> wal = tools::wallet2::make_new(vm2).first; + if (!wal) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Failed to create wallet"; + return false; + } + wal->set_seed_language(req.language); + cryptonote::COMMAND_RPC_GET_HEIGHT::request hreq; + cryptonote::COMMAND_RPC_GET_HEIGHT::response hres; + hres.height = 0; + bool r = net_utils::invoke_http_json("/getheight", hreq, hres, m_http_client); + wal->set_refresh_from_block_height(hres.height); + crypto::secret_key dummy_key; + try { + wal->generate(wallet_file, req.password, dummy_key, false, false); + } + catch (const std::exception& e) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Failed to generate wallet"; + return false; + } + if (m_wallet) + delete m_wallet; + m_wallet = wal.release(); + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_open_wallet(const wallet_rpc::COMMAND_RPC_OPEN_WALLET::request& req, wallet_rpc::COMMAND_RPC_OPEN_WALLET::response& res, epee::json_rpc::error& er) + { + if (m_wallet_dir.empty()) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "No wallet dir configured"; + return false; + } + + namespace po = boost::program_options; + po::variables_map vm2; + const char *ptr = strchr(req.filename.c_str(), '/'); +#ifdef _WIN32 + if (!ptr) + ptr = strchr(req.filename.c_str(), '\\'); + if (!ptr) + ptr = strchr(req.filename.c_str(), ':'); +#endif + if (ptr) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Invalid filename"; + return false; + } + std::string wallet_file = m_wallet_dir + "/" + req.filename; + { + po::options_description desc("dummy"); + const command_line::arg_descriptor<std::string, true> arg_password = {"password", "password"}; + const char *argv[4]; + int argc = 3; + argv[0] = "wallet-rpc"; + argv[1] = "--password"; + argv[2] = req.password.c_str(); + argv[3] = NULL; + vm2 = *m_vm; + command_line::add_arg(desc, arg_password); + po::store(po::parse_command_line(argc, argv, desc), vm2); + } + std::unique_ptr<tools::wallet2> wal; + try { + wal = tools::wallet2::make_from_file(vm2, wallet_file).first; + } + catch (const std::exception& e) + { + wal = nullptr; + } + if (!wal) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Failed to open wallet"; + return false; + } + if (m_wallet) + delete m_wallet; + m_wallet = wal.release(); + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ } +int main(int argc, char** argv) { + namespace po = boost::program_options; + + const auto arg_wallet_file = wallet_args::arg_wallet_file(); + const auto arg_from_json = wallet_args::arg_generate_from_json(); + + po::options_description desc_params(wallet_args::tr("Wallet options")); + tools::wallet2::init_options(desc_params); + command_line::add_arg(desc_params, arg_rpc_bind_port); + command_line::add_arg(desc_params, arg_disable_rpc_login); + command_line::add_arg(desc_params, arg_trusted_daemon); + cryptonote::rpc_args::init_options(desc_params); + command_line::add_arg(desc_params, arg_wallet_file); + command_line::add_arg(desc_params, arg_from_json); + command_line::add_arg(desc_params, arg_wallet_dir); + + + const auto vm = wallet_args::main( + argc, argv, + "monero-wallet-rpc [--wallet-file=<file>|--generate-from-json=<file>|--wallet-dir=<directory>] [--rpc-bind-port=<port>]", + desc_params, + po::positional_options_description(), + "monero-wallet-rpc.log", + true + ); + if (!vm) + { + return 1; + } + + std::unique_ptr<tools::wallet2> wal; + try + { + const auto wallet_file = command_line::get_arg(*vm, arg_wallet_file); + const auto from_json = command_line::get_arg(*vm, arg_from_json); + const auto wallet_dir = command_line::get_arg(*vm, arg_wallet_dir); + + if(!wallet_file.empty() && !from_json.empty()) + { + LOG_ERROR(tools::wallet_rpc_server::tr("Can't specify more than one of --wallet-file and --generate-from-json")); + return 1; + } + + if (!wallet_dir.empty()) + { + wal = NULL; + goto just_dir; + } + + if (wallet_file.empty() && from_json.empty()) + { + LOG_ERROR(tools::wallet_rpc_server::tr("Must specify --wallet-file or --generate-from-json or --wallet-dir")); + return 1; + } + + LOG_PRINT_L0(tools::wallet_rpc_server::tr("Loading wallet...")); + if(!wallet_file.empty()) + { + wal = tools::wallet2::make_from_file(*vm, wallet_file).first; + } + else + { + wal = tools::wallet2::make_from_json(*vm, from_json); + } + if (!wal) + { + return 1; + } + + bool quit = false; + tools::signal_handler::install([&wal, &quit](int) { + assert(wal); + quit = true; + wal->stop(); + }); + + wal->refresh(); + // if we ^C during potentially length load/refresh, there's no server loop yet + if (quit) + { + MINFO(tools::wallet_rpc_server::tr("Storing wallet...")); + wal->store(); + MINFO(tools::wallet_rpc_server::tr("Stored ok")); + return 1; + } + MINFO(tools::wallet_rpc_server::tr("Loaded ok")); + } + catch (const std::exception& e) + { + LOG_ERROR(tools::wallet_rpc_server::tr("Wallet initialization failed: ") << e.what()); + return 1; + } +just_dir: + tools::wallet_rpc_server wrpc; + if (wal) wrpc.set_wallet(wal.release()); + bool r = wrpc.init(&(vm.get())); + CHECK_AND_ASSERT_MES(r, 1, tools::wallet_rpc_server::tr("Failed to initialize wallet rpc server")); + tools::signal_handler::install([&wrpc, &wal](int) { + wrpc.send_stop_signal(); + }); + LOG_PRINT_L0(tools::wallet_rpc_server::tr("Starting wallet rpc server")); + wrpc.run(); + LOG_PRINT_L0(tools::wallet_rpc_server::tr("Stopped wallet rpc server")); + try + { + LOG_PRINT_L0(tools::wallet_rpc_server::tr("Storing wallet...")); + wrpc.stop(); + LOG_PRINT_L0(tools::wallet_rpc_server::tr("Stored ok")); + } + catch (const std::exception& e) + { + LOG_ERROR(tools::wallet_rpc_server::tr("Failed to store wallet: ") << e.what()); + return 1; + } + return 0; +} diff --git a/src/wallet/wallet_rpc_server.h b/src/wallet/wallet_rpc_server.h index b3e95c18a..230dcee5b 100644 --- a/src/wallet/wallet_rpc_server.h +++ b/src/wallet/wallet_rpc_server.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -32,10 +32,14 @@ #include <boost/program_options/options_description.hpp> #include <boost/program_options/variables_map.hpp> +#include <string> #include "net/http_server_impl_base.h" #include "wallet_rpc_server_commands_defs.h" #include "wallet2.h" -#include "common/command_line.h" + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "wallet.rpc" + namespace tools { /************************************************************************/ @@ -46,16 +50,16 @@ namespace tools public: typedef epee::net_utils::connection_context_base connection_context; - wallet_rpc_server(wallet2& cr); - - const static command_line::arg_descriptor<std::string> arg_rpc_bind_port; - const static command_line::arg_descriptor<std::string> arg_rpc_bind_ip; - const static command_line::arg_descriptor<std::string> arg_user_agent; + static const char* tr(const char* str); + wallet_rpc_server(); + ~wallet_rpc_server(); - static void init_options(boost::program_options::options_description& desc); - bool init(const boost::program_options::variables_map& vm); + bool init(const boost::program_options::variables_map *vm); bool run(); + void stop(); + void set_wallet(wallet2 *cr); + private: CHAIN_HTTP_TO_MAP2(connection_context); //forward http requests to uri map @@ -81,10 +85,22 @@ namespace tools MAP_JON_RPC_WE("set_tx_notes", on_set_tx_notes, wallet_rpc::COMMAND_RPC_SET_TX_NOTES) MAP_JON_RPC_WE("get_tx_notes", on_get_tx_notes, wallet_rpc::COMMAND_RPC_GET_TX_NOTES) MAP_JON_RPC_WE("get_transfers", on_get_transfers, wallet_rpc::COMMAND_RPC_GET_TRANSFERS) + MAP_JON_RPC_WE("get_transfer_by_txid", on_get_transfer_by_txid, wallet_rpc::COMMAND_RPC_GET_TRANSFER_BY_TXID) MAP_JON_RPC_WE("sign", on_sign, wallet_rpc::COMMAND_RPC_SIGN) MAP_JON_RPC_WE("verify", on_verify, wallet_rpc::COMMAND_RPC_VERIFY) MAP_JON_RPC_WE("export_key_images", on_export_key_images, wallet_rpc::COMMAND_RPC_EXPORT_KEY_IMAGES) MAP_JON_RPC_WE("import_key_images", on_import_key_images, wallet_rpc::COMMAND_RPC_IMPORT_KEY_IMAGES) + MAP_JON_RPC_WE("make_uri", on_make_uri, wallet_rpc::COMMAND_RPC_MAKE_URI) + MAP_JON_RPC_WE("parse_uri", on_parse_uri, wallet_rpc::COMMAND_RPC_PARSE_URI) + MAP_JON_RPC_WE("get_address_book", on_get_address_book, wallet_rpc::COMMAND_RPC_GET_ADDRESS_BOOK_ENTRY) + MAP_JON_RPC_WE("add_address_book", on_add_address_book, wallet_rpc::COMMAND_RPC_ADD_ADDRESS_BOOK_ENTRY) + MAP_JON_RPC_WE("delete_address_book",on_delete_address_book,wallet_rpc::COMMAND_RPC_DELETE_ADDRESS_BOOK_ENTRY) + MAP_JON_RPC_WE("rescan_spent", on_rescan_spent, wallet_rpc::COMMAND_RPC_RESCAN_SPENT) + MAP_JON_RPC_WE("start_mining", on_start_mining, wallet_rpc::COMMAND_RPC_START_MINING) + MAP_JON_RPC_WE("stop_mining", on_stop_mining, wallet_rpc::COMMAND_RPC_STOP_MINING) + MAP_JON_RPC_WE("get_languages", on_get_languages, wallet_rpc::COMMAND_RPC_GET_LANGUAGES) + MAP_JON_RPC_WE("create_wallet", on_create_wallet, wallet_rpc::COMMAND_RPC_CREATE_WALLET) + MAP_JON_RPC_WE("open_wallet", on_open_wallet, wallet_rpc::COMMAND_RPC_OPEN_WALLET) END_JSON_RPC_MAP() END_URI_MAP2() @@ -108,20 +124,39 @@ namespace tools bool on_set_tx_notes(const wallet_rpc::COMMAND_RPC_SET_TX_NOTES::request& req, wallet_rpc::COMMAND_RPC_SET_TX_NOTES::response& res, epee::json_rpc::error& er); bool on_get_tx_notes(const wallet_rpc::COMMAND_RPC_GET_TX_NOTES::request& req, wallet_rpc::COMMAND_RPC_GET_TX_NOTES::response& res, epee::json_rpc::error& er); bool on_get_transfers(const wallet_rpc::COMMAND_RPC_GET_TRANSFERS::request& req, wallet_rpc::COMMAND_RPC_GET_TRANSFERS::response& res, epee::json_rpc::error& er); + bool on_get_transfer_by_txid(const wallet_rpc::COMMAND_RPC_GET_TRANSFER_BY_TXID::request& req, wallet_rpc::COMMAND_RPC_GET_TRANSFER_BY_TXID::response& res, epee::json_rpc::error& er); bool on_sign(const wallet_rpc::COMMAND_RPC_SIGN::request& req, wallet_rpc::COMMAND_RPC_SIGN::response& res, epee::json_rpc::error& er); bool on_verify(const wallet_rpc::COMMAND_RPC_VERIFY::request& req, wallet_rpc::COMMAND_RPC_VERIFY::response& res, epee::json_rpc::error& er); bool on_export_key_images(const wallet_rpc::COMMAND_RPC_EXPORT_KEY_IMAGES::request& req, wallet_rpc::COMMAND_RPC_EXPORT_KEY_IMAGES::response& res, epee::json_rpc::error& er); bool on_import_key_images(const wallet_rpc::COMMAND_RPC_IMPORT_KEY_IMAGES::request& req, wallet_rpc::COMMAND_RPC_IMPORT_KEY_IMAGES::response& res, epee::json_rpc::error& er); - - bool handle_command_line(const boost::program_options::variables_map& vm); + bool on_make_uri(const wallet_rpc::COMMAND_RPC_MAKE_URI::request& req, wallet_rpc::COMMAND_RPC_MAKE_URI::response& res, epee::json_rpc::error& er); + bool on_parse_uri(const wallet_rpc::COMMAND_RPC_PARSE_URI::request& req, wallet_rpc::COMMAND_RPC_PARSE_URI::response& res, epee::json_rpc::error& er); + bool on_get_address_book(const wallet_rpc::COMMAND_RPC_GET_ADDRESS_BOOK_ENTRY::request& req, wallet_rpc::COMMAND_RPC_GET_ADDRESS_BOOK_ENTRY::response& res, epee::json_rpc::error& er); + bool on_add_address_book(const wallet_rpc::COMMAND_RPC_ADD_ADDRESS_BOOK_ENTRY::request& req, wallet_rpc::COMMAND_RPC_ADD_ADDRESS_BOOK_ENTRY::response& res, epee::json_rpc::error& er); + bool on_delete_address_book(const wallet_rpc::COMMAND_RPC_DELETE_ADDRESS_BOOK_ENTRY::request& req, wallet_rpc::COMMAND_RPC_DELETE_ADDRESS_BOOK_ENTRY::response& res, epee::json_rpc::error& er); + bool on_rescan_spent(const wallet_rpc::COMMAND_RPC_RESCAN_SPENT::request& req, wallet_rpc::COMMAND_RPC_RESCAN_SPENT::response& res, epee::json_rpc::error& er); + bool on_start_mining(const wallet_rpc::COMMAND_RPC_START_MINING::request& req, wallet_rpc::COMMAND_RPC_START_MINING::response& res, epee::json_rpc::error& er); + bool on_stop_mining(const wallet_rpc::COMMAND_RPC_STOP_MINING::request& req, wallet_rpc::COMMAND_RPC_STOP_MINING::response& res, epee::json_rpc::error& er); + bool on_get_languages(const wallet_rpc::COMMAND_RPC_GET_LANGUAGES::request& req, wallet_rpc::COMMAND_RPC_GET_LANGUAGES::response& res, epee::json_rpc::error& er); + bool on_create_wallet(const wallet_rpc::COMMAND_RPC_CREATE_WALLET::request& req, wallet_rpc::COMMAND_RPC_CREATE_WALLET::response& res, epee::json_rpc::error& er); + bool on_open_wallet(const wallet_rpc::COMMAND_RPC_OPEN_WALLET::request& req, wallet_rpc::COMMAND_RPC_OPEN_WALLET::response& res, epee::json_rpc::error& er); //json rpc v2 bool on_query_key(const wallet_rpc::COMMAND_RPC_QUERY_KEY::request& req, wallet_rpc::COMMAND_RPC_QUERY_KEY::response& res, epee::json_rpc::error& er); - wallet2& m_wallet; - std::string m_port; - std::string m_bind_ip; - std::string m_user_agent; + // helpers + void fill_transfer_entry(tools::wallet_rpc::transfer_entry &entry, const crypto::hash &txid, const crypto::hash &payment_id, const tools::wallet2::payment_details &pd); + void fill_transfer_entry(tools::wallet_rpc::transfer_entry &entry, const crypto::hash &txid, const tools::wallet2::confirmed_transfer_details &pd); + void fill_transfer_entry(tools::wallet_rpc::transfer_entry &entry, const crypto::hash &txid, const tools::wallet2::unconfirmed_transfer_details &pd); + void fill_transfer_entry(tools::wallet_rpc::transfer_entry &entry, const crypto::hash &payment_id, const tools::wallet2::payment_details &pd); + bool not_open(epee::json_rpc::error& er); + + wallet2 *m_wallet; + std::string m_wallet_dir; + std::string rpc_login_filename; std::atomic<bool> m_stop; + bool m_trusted_daemon; + epee::net_utils::http::http_simple_client m_http_client; + const boost::program_options::variables_map *m_vm; }; } diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h index cde9863a2..3c10dc41f 100644 --- a/src/wallet/wallet_rpc_server_commands_defs.h +++ b/src/wallet/wallet_rpc_server_commands_defs.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -30,9 +30,13 @@ #pragma once #include "cryptonote_protocol/cryptonote_protocol_defs.h" -#include "cryptonote_core/cryptonote_basic.h" +#include "cryptonote_basic/cryptonote_basic.h" #include "crypto/hash.h" #include "wallet_rpc_server_error_codes.h" + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "wallet.rpc" + namespace tools { namespace wallet_rpc @@ -115,7 +119,6 @@ namespace wallet_rpc uint64_t unlock_time; std::string payment_id; bool get_tx_key; - bool trusted_daemon; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(destinations) @@ -124,7 +127,6 @@ namespace wallet_rpc KV_SERIALIZE(unlock_time) KV_SERIALIZE(payment_id) KV_SERIALIZE(get_tx_key) - KV_SERIALIZE(trusted_daemon) END_KV_SERIALIZE_MAP() }; @@ -133,11 +135,13 @@ namespace wallet_rpc std::string tx_hash; std::string tx_key; std::list<std::string> amount_keys; + uint64_t fee; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(tx_hash) KV_SERIALIZE(tx_key) KV_SERIALIZE(amount_keys) + KV_SERIALIZE(fee) END_KV_SERIALIZE_MAP() }; }; @@ -152,7 +156,6 @@ namespace wallet_rpc uint64_t unlock_time; std::string payment_id; bool get_tx_keys; - bool trusted_daemon; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(destinations) @@ -161,7 +164,6 @@ namespace wallet_rpc KV_SERIALIZE(unlock_time) KV_SERIALIZE(payment_id) KV_SERIALIZE(get_tx_keys) - KV_SERIALIZE(trusted_daemon) END_KV_SERIALIZE_MAP() }; @@ -178,10 +180,12 @@ namespace wallet_rpc { std::list<std::string> tx_hash_list; std::list<std::string> tx_key_list; + std::list<uint64_t> fee_list; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(tx_hash_list) KV_SERIALIZE(tx_key_list) + KV_SERIALIZE(fee_list) END_KV_SERIALIZE_MAP() }; }; @@ -191,11 +195,9 @@ namespace wallet_rpc struct request { bool get_tx_keys; - bool trusted_daemon; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(get_tx_keys) - KV_SERIALIZE(trusted_daemon) END_KV_SERIALIZE_MAP() }; @@ -212,10 +214,12 @@ namespace wallet_rpc { std::list<std::string> tx_hash_list; std::list<std::string> tx_key_list; + std::list<uint64_t> fee_list; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(tx_hash_list) KV_SERIALIZE(tx_key_list) + KV_SERIALIZE(fee_list) END_KV_SERIALIZE_MAP() }; }; @@ -230,7 +234,7 @@ namespace wallet_rpc uint64_t unlock_time; std::string payment_id; bool get_tx_keys; - bool trusted_daemon; + uint64_t below_amount; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(address) @@ -239,7 +243,7 @@ namespace wallet_rpc KV_SERIALIZE(unlock_time) KV_SERIALIZE(payment_id) KV_SERIALIZE(get_tx_keys) - KV_SERIALIZE(trusted_daemon) + KV_SERIALIZE(below_amount) END_KV_SERIALIZE_MAP() }; @@ -256,10 +260,12 @@ namespace wallet_rpc { std::list<std::string> tx_hash_list; std::list<std::string> tx_key_list; + std::list<uint64_t> fee_list; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(tx_hash_list) KV_SERIALIZE(tx_key_list) + KV_SERIALIZE(fee_list) END_KV_SERIALIZE_MAP() }; }; @@ -517,6 +523,31 @@ namespace wallet_rpc }; }; + struct transfer_entry + { + std::string txid; + std::string payment_id; + uint64_t height; + uint64_t timestamp; + uint64_t amount; + uint64_t fee; + std::string note; + std::list<transfer_destination> destinations; + std::string type; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(txid); + KV_SERIALIZE(payment_id); + KV_SERIALIZE(height); + KV_SERIALIZE(timestamp); + KV_SERIALIZE(amount); + KV_SERIALIZE(fee); + KV_SERIALIZE(note); + KV_SERIALIZE(destinations); + KV_SERIALIZE(type); + END_KV_SERIALIZE_MAP() + }; + struct COMMAND_RPC_GET_TRANSFERS { struct request @@ -543,43 +574,41 @@ namespace wallet_rpc END_KV_SERIALIZE_MAP() }; - struct entry + struct response + { + std::list<transfer_entry> in; + std::list<transfer_entry> out; + std::list<transfer_entry> pending; + std::list<transfer_entry> failed; + std::list<transfer_entry> pool; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(in); + KV_SERIALIZE(out); + KV_SERIALIZE(pending); + KV_SERIALIZE(failed); + KV_SERIALIZE(pool); + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_GET_TRANSFER_BY_TXID + { + struct request { std::string txid; - std::string payment_id; - uint64_t height; - uint64_t timestamp; - uint64_t amount; - uint64_t fee; - std::string note; - std::list<transfer_destination> destinations; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(txid); - KV_SERIALIZE(payment_id); - KV_SERIALIZE(height); - KV_SERIALIZE(timestamp); - KV_SERIALIZE(amount); - KV_SERIALIZE(fee); - KV_SERIALIZE(note); - KV_SERIALIZE(destinations); END_KV_SERIALIZE_MAP() }; struct response { - std::list<entry> in; - std::list<entry> out; - std::list<entry> pending; - std::list<entry> failed; - std::list<entry> pool; + transfer_entry transfer; BEGIN_KV_SERIALIZE_MAP() - KV_SERIALIZE(in); - KV_SERIALIZE(out); - KV_SERIALIZE(pending); - KV_SERIALIZE(failed); - KV_SERIALIZE(pool); + KV_SERIALIZE(transfer); END_KV_SERIALIZE_MAP() }; }; @@ -695,5 +724,248 @@ namespace wallet_rpc }; }; + struct uri_spec + { + std::string address; + std::string payment_id; + uint64_t amount; + std::string tx_description; + std::string recipient_name; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(address); + KV_SERIALIZE(payment_id); + KV_SERIALIZE(amount); + KV_SERIALIZE(tx_description); + KV_SERIALIZE(recipient_name); + END_KV_SERIALIZE_MAP() + }; + + struct COMMAND_RPC_MAKE_URI + { + struct request: public uri_spec + { + }; + + struct response + { + std::string uri; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(uri) + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_PARSE_URI + { + struct request + { + std::string uri; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(uri) + END_KV_SERIALIZE_MAP() + }; + + struct response + { + uri_spec uri; + std::vector<std::string> unknown_parameters; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(uri); + KV_SERIALIZE(unknown_parameters); + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_ADD_ADDRESS_BOOK_ENTRY + { + struct request + { + std::string address; + std::string payment_id; + std::string description; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(address) + KV_SERIALIZE(payment_id) + KV_SERIALIZE(description) + END_KV_SERIALIZE_MAP() + }; + + struct response + { + uint64_t index; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(index); + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_GET_ADDRESS_BOOK_ENTRY + { + struct request + { + std::list<uint64_t> entries; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(entries) + END_KV_SERIALIZE_MAP() + }; + + struct entry + { + uint64_t index; + std::string address; + std::string payment_id; + std::string description; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(index) + KV_SERIALIZE(address) + KV_SERIALIZE(payment_id) + KV_SERIALIZE(description) + END_KV_SERIALIZE_MAP() + }; + + struct response + { + std::vector<entry> entries; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(entries) + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_DELETE_ADDRESS_BOOK_ENTRY + { + struct request + { + uint64_t index; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(index); + END_KV_SERIALIZE_MAP() + }; + + struct response + { + BEGIN_KV_SERIALIZE_MAP() + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_RESCAN_SPENT + { + struct request + { + BEGIN_KV_SERIALIZE_MAP() + END_KV_SERIALIZE_MAP() + }; + + struct response + { + BEGIN_KV_SERIALIZE_MAP() + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_START_MINING + { + struct request + { + uint64_t threads_count; + bool do_background_mining; + bool ignore_battery; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(threads_count) + KV_SERIALIZE(do_background_mining) + KV_SERIALIZE(ignore_battery) + END_KV_SERIALIZE_MAP() + }; + + struct response + { + BEGIN_KV_SERIALIZE_MAP() + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_STOP_MINING + { + struct request + { + BEGIN_KV_SERIALIZE_MAP() + END_KV_SERIALIZE_MAP() + }; + + struct response + { + BEGIN_KV_SERIALIZE_MAP() + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_GET_LANGUAGES + { + struct request + { + BEGIN_KV_SERIALIZE_MAP() + END_KV_SERIALIZE_MAP() + }; + struct response + { + std::vector<std::string> languages; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(languages) + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_CREATE_WALLET + { + struct request + { + std::string filename; + std::string password; + std::string language; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(filename) + KV_SERIALIZE(password) + KV_SERIALIZE(language) + END_KV_SERIALIZE_MAP() + }; + struct response + { + BEGIN_KV_SERIALIZE_MAP() + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_OPEN_WALLET + { + struct request + { + std::string filename; + std::string password; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(filename) + KV_SERIALIZE(password) + END_KV_SERIALIZE_MAP() + }; + struct response + { + BEGIN_KV_SERIALIZE_MAP() + END_KV_SERIALIZE_MAP() + }; + }; } } diff --git a/src/wallet/wallet_rpc_server_error_codes.h b/src/wallet/wallet_rpc_server_error_codes.h index 4617a1449..3c79c0ac3 100644 --- a/src/wallet/wallet_rpc_server_error_codes.h +++ b/src/wallet/wallet_rpc_server_error_codes.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016, The Monero Project +// Copyright (c) 2014-2017, The Monero Project // // All rights reserved. // @@ -41,3 +41,6 @@ #define WALLET_RPC_ERROR_CODE_WRONG_TXID -8 #define WALLET_RPC_ERROR_CODE_WRONG_SIGNATURE -9 #define WALLET_RPC_ERROR_CODE_WRONG_KEY_IMAGE -10 +#define WALLET_RPC_ERROR_CODE_WRONG_URI -11 +#define WALLET_RPC_ERROR_CODE_WRONG_INDEX -12 +#define WALLET_RPC_ERROR_CODE_NOT_OPEN -13 |