diff options
Diffstat (limited to 'src/wallet')
-rw-r--r-- | src/wallet/api/CMakeLists.txt | 3 | ||||
-rw-r--r-- | src/wallet/api/wallet.cpp | 17 | ||||
-rw-r--r-- | src/wallet/api/wallet.h | 3 | ||||
-rw-r--r-- | src/wallet/api/wallet2_api.h | 3 | ||||
-rw-r--r-- | src/wallet/wallet2.cpp | 165 | ||||
-rw-r--r-- | src/wallet/wallet_rpc_server.cpp | 41 | ||||
-rw-r--r-- | src/wallet/wallet_rpc_server_commands_defs.h | 23 |
7 files changed, 197 insertions, 58 deletions
diff --git a/src/wallet/api/CMakeLists.txt b/src/wallet/api/CMakeLists.txt index 30eb4ce03..655cdfefd 100644 --- a/src/wallet/api/CMakeLists.txt +++ b/src/wallet/api/CMakeLists.txt @@ -71,10 +71,13 @@ target_link_libraries(wallet_api mnemonics ${LMDB_LIBRARY} ${Boost_CHRONO_LIBRARY} + ${Boost_LOCALE_LIBRARY} + ${ICU_LIBRARIES} ${Boost_SERIALIZATION_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} ${Boost_SYSTEM_LIBRARY} ${Boost_THREAD_LIBRARY} + ${CMAKE_THREAD_LIBS_INIT} ${Boost_REGEX_LIBRARY} PRIVATE ${EXTRA_LIBRARIES}) diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp index 2b0349501..0afbda705 100644 --- a/src/wallet/api/wallet.cpp +++ b/src/wallet/api/wallet.cpp @@ -836,6 +836,11 @@ bool WalletImpl::setPassword(const std::string &password) return status() == Status_Ok; } +const std::string& WalletImpl::getPassword() const +{ + return m_password; +} + bool WalletImpl::setDevicePin(const std::string &pin) { clearStatus(); @@ -1175,7 +1180,7 @@ bool WalletImpl::exportKeyImages(const string &filename, bool all) try { - if (!m_wallet->export_key_images(filename), all) + if (!m_wallet->export_key_images(filename, all)) { setStatusError(tr("failed to save file ") + filename); return false; @@ -2360,6 +2365,11 @@ bool WalletImpl::parse_uri(const std::string &uri, std::string &address, std::st return m_wallet->parse_uri(uri, address, payment_id, amount, tx_description, recipient_name, unknown_parameters, error); } +std::string WalletImpl::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) const +{ + return m_wallet->make_uri(address, payment_id, amount, tx_description, recipient_name, error); +} + std::string WalletImpl::getDefaultDataDir() const { return tools::get_default_data_dir(); @@ -2387,6 +2397,11 @@ void WalletImpl::setOffline(bool offline) m_wallet->set_offline(offline); } +bool WalletImpl::isOffline() const +{ + return m_wallet->is_offline(); +} + void WalletImpl::hardForkInfo(uint8_t &version, uint64_t &earliest_height) const { m_wallet->get_hard_fork_info(version, earliest_height); diff --git a/src/wallet/api/wallet.h b/src/wallet/api/wallet.h index d0900daa2..67fc2c08a 100644 --- a/src/wallet/api/wallet.h +++ b/src/wallet/api/wallet.h @@ -89,6 +89,7 @@ public: std::string errorString() const override; void statusWithErrorString(int& status, std::string& errorString) const override; bool setPassword(const std::string &password) override; + const std::string& getPassword() const override; bool setDevicePin(const std::string &password) override; bool setDevicePassphrase(const std::string &password) override; std::string address(uint32_t accountIndex = 0, uint32_t addressIndex = 0) const override; @@ -185,6 +186,7 @@ public: virtual std::string getCacheAttribute(const std::string &key) const override; virtual void setOffline(bool offline) override; + virtual bool isOffline() const override; virtual bool setUserNote(const std::string &txid, const std::string ¬e) override; virtual std::string getUserNote(const std::string &txid) const override; @@ -203,6 +205,7 @@ public: virtual void startRefresh() override; virtual void pauseRefresh() override; 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) override; + virtual 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) const override; virtual std::string getDefaultDataDir() const override; virtual bool lightWalletLogin(bool &isNewWallet) const override; virtual bool lightWalletImportWalletRequest(std::string &payment_id, uint64_t &fee, bool &new_request, bool &request_fulfilled, std::string &payment_address, std::string &status) override; diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h index 99a4de731..f9c421a93 100644 --- a/src/wallet/api/wallet2_api.h +++ b/src/wallet/api/wallet2_api.h @@ -456,6 +456,7 @@ struct Wallet //! returns both error and error string atomically. suggested to use in instead of status() and errorString() virtual void statusWithErrorString(int& status, std::string& errorString) const = 0; virtual bool setPassword(const std::string &password) = 0; + virtual const std::string& getPassword() const = 0; virtual bool setDevicePin(const std::string &pin) { (void)pin; return false; }; virtual bool setDevicePassphrase(const std::string &passphrase) { (void)passphrase; return false; }; virtual std::string address(uint32_t accountIndex = 0, uint32_t addressIndex = 0) const = 0; @@ -1019,6 +1020,7 @@ struct Wallet virtual bool verifyMessageWithPublicKey(const std::string &message, const std::string &publicKey, 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; + virtual 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) const = 0; virtual std::string getDefaultDataDir() const = 0; @@ -1033,6 +1035,7 @@ struct Wallet * \param offline - true/false */ virtual void setOffline(bool offline) = 0; + virtual bool isOffline() const = 0; //! blackballs a set of outputs virtual bool blackballOutputs(const std::vector<std::string> &outputs, bool add) = 0; diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 05fe0a1ad..cc0dff999 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -144,6 +144,9 @@ using namespace cryptonote; #define IGNORE_LONG_PAYMENT_ID_FROM_BLOCK_VERSION 12 +#define DEFAULT_UNLOCK_TIME (CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE * DIFFICULTY_TARGET_V2) +#define RECENT_SPEND_WINDOW (50 * DIFFICULTY_TARGET_V2) + static const std::string MULTISIG_SIGNATURE_MAGIC = "SigMultisigPkV1"; static const std::string MULTISIG_EXTRA_INFO_MAGIC = "MultisigxV1"; @@ -1019,7 +1022,13 @@ gamma_picker::gamma_picker(const std::vector<uint64_t> &rct_offsets, double shap end = rct_offsets.data() + rct_offsets.size() - CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE; num_rct_outputs = *(end - 1); THROW_WALLET_EXCEPTION_IF(num_rct_outputs == 0, error::wallet_internal_error, "No rct outputs"); + THROW_WALLET_EXCEPTION_IF(outputs_to_consider == 0, error::wallet_internal_error, "No rct outputs to consider"); average_output_time = DIFFICULTY_TARGET_V2 * blocks_to_consider / outputs_to_consider; // this assumes constant target over the whole rct range + if (average_output_time == 0) { + // TODO: apply this to all cases; do so alongside a hard fork, where all clients will update at the same time, preventing anonymity puddle formation + average_output_time = DIFFICULTY_TARGET_V2 * blocks_to_consider / static_cast<double>(outputs_to_consider); + } + THROW_WALLET_EXCEPTION_IF(average_output_time == 0, error::wallet_internal_error, "Average seconds per output cannot be 0."); }; gamma_picker::gamma_picker(const std::vector<uint64_t> &rct_offsets): gamma_picker(rct_offsets, GAMMA_SHAPE, GAMMA_SCALE) {} @@ -1028,6 +1037,34 @@ uint64_t gamma_picker::pick() { double x = gamma(engine); x = exp(x); + + if (x > DEFAULT_UNLOCK_TIME) + { + // We are trying to select an output from the chain that appeared 'x' seconds before the + // current chain tip, where 'x' is selected from the gamma distribution recommended in Miller et al. + // (https://arxiv.org/pdf/1704.04299/). + // Our method is to get the average time delta between outputs in the recent past, estimate the number of + // outputs 'n' that would have appeared between 'chain_tip - x' and 'chain_tip', select the real output at + // 'current_num_outputs - n', then randomly select an output from the block where that output appears. + // Source code to paper: https://github.com/maltemoeser/moneropaper + // + // Due to the 'default spendable age' mechanic in Monero, 'current_num_outputs' only contains + // currently *unlocked* outputs, which means the earliest output that can be selected is not at the chain tip! + // Therefore, we must offset 'x' so it matches up with the timing of the outputs being considered. We do + // this by saying if 'x` equals the expected age of the first unlocked output (compared to the current + // chain tip - i.e. DEFAULT_UNLOCK_TIME), then select the first unlocked output. + x -= DEFAULT_UNLOCK_TIME; + } + else + { + // If the spent time suggested by the gamma is less than the unlock time, that means the gamma is suggesting an output + // that is no longer feasible to be spent (possible since the gamma was constructed when consensus rules did not enforce the + // lock time). The assumption made in this code is that an output expected spent quicker than the unlock time would likely + // be spent within RECENT_SPEND_WINDOW after allowed. So it returns an output that falls between 0 and the RECENT_SPEND_WINDOW. + // The RECENT_SPEND_WINDOW was determined with empirical analysis of observed data. + x = crypto::rand_idx(static_cast<uint64_t>(RECENT_SPEND_WINDOW)); + } + uint64_t output_index = x / average_output_time; if (output_index >= num_rct_outputs) return std::numeric_limits<uint64_t>::max(); // bad pick @@ -1213,6 +1250,7 @@ wallet2::wallet2(network_type nettype, uint64_t kdf_rounds, bool unattended, std wallet2::~wallet2() { + deinit(); } bool wallet2::has_testnet_option(const boost::program_options::variables_map& vm) @@ -1320,6 +1358,9 @@ bool wallet2::set_daemon(std::string daemon_address, boost::optional<epee::net_u m_trusted_daemon = trusted_daemon; if (changed) { + if (!m_persistent_rpc_client_id) { + set_rpc_client_secret_key(rct::rct2sk(rct::skGen())); + } m_rpc_payment_state.expected_spent = 0; m_rpc_payment_state.discrepancy = 0; m_node_rpc_proxy.invalidate(); @@ -1897,7 +1938,7 @@ void wallet2::cache_tx_data(const cryptonote::transaction& tx, const crypto::has const bool is_miner = tx.vin.size() == 1 && tx.vin[0].type() == typeid(cryptonote::txin_gen); if (!is_miner || m_refresh_type != RefreshType::RefreshNoCoinbase) { - const size_t rec_size = is_miner && m_refresh_type == RefreshType::RefreshOptimizeCoinbase ? 1 : tx.vout.size(); + const size_t rec_size = (is_miner && m_refresh_type == RefreshType::RefreshOptimizeCoinbase && tx.version < 2) ? 1 : tx.vout.size(); if (!tx.vout.empty()) { // if tx.vout is not empty, we loop through all tx pubkeys @@ -2046,7 +2087,7 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote { // assume coinbase isn't for us } - else if (miner_tx && m_refresh_type == RefreshOptimizeCoinbase) + else if (miner_tx && m_refresh_type == RefreshOptimizeCoinbase && tx.version < 2) { check_acc_out_precomp_once(tx.vout[0], derivation, additional_derivations, 0, is_out_data_ptr, tx_scan_info[0], output_found[0]); THROW_WALLET_EXCEPTION_IF(tx_scan_info[0].error, error::acc_outs_lookup_error, tx, tx_pub_key, m_account.get_keys()); @@ -2773,9 +2814,8 @@ void wallet2::process_parsed_blocks(uint64_t start_height, const std::vector<cry { if (tx_cache_data[i].empty()) continue; - tpool.submit(&waiter, [&hwdev, &gender, &tx_cache_data, i]() { + tpool.submit(&waiter, [&gender, &tx_cache_data, i]() { auto &slot = tx_cache_data[i]; - boost::unique_lock<hw::device> hwdev_lock(hwdev); for (auto &iod: slot.primary) gender(iod); for (auto &iod: slot.additional) @@ -2818,8 +2858,9 @@ void wallet2::process_parsed_blocks(uint64_t start_height, const std::vector<cry if (m_refresh_type != RefreshType::RefreshNoCoinbase) { THROW_WALLET_EXCEPTION_IF(txidx >= tx_cache_data.size(), error::wallet_internal_error, "txidx out of range"); - const size_t n_vouts = m_refresh_type == RefreshType::RefreshOptimizeCoinbase ? 1 : parsed_blocks[i].block.miner_tx.vout.size(); - tpool.submit(&waiter, [&, i, n_vouts, txidx](){ geniod(parsed_blocks[i].block.miner_tx, n_vouts, txidx); }, true); + const cryptonote::transaction& tx = parsed_blocks[i].block.miner_tx; + const size_t n_vouts = (m_refresh_type == RefreshType::RefreshOptimizeCoinbase && tx.version < 2) ? 1 : tx.vout.size(); + tpool.submit(&waiter, [&, i, n_vouts, txidx](){ geniod(tx, n_vouts, txidx); }, true); } ++txidx; for (size_t j = 0; j < parsed_blocks[i].txes.size(); ++j) @@ -3491,14 +3532,6 @@ void wallet2::refresh(bool trusted_daemon, uint64_t start_height, uint64_t & blo blocks_fetched += added_blocks; } THROW_WALLET_EXCEPTION_IF(!waiter.wait(), error::wallet_internal_error, "Exception in thread pool"); - if(!first && blocks_start_height == next_blocks_start_height) - { - m_node_rpc_proxy.set_height(m_blockchain.size()); - refreshed = true; - break; - } - - first = false; // handle error from async fetching thread if (error) @@ -3509,6 +3542,15 @@ void wallet2::refresh(bool trusted_daemon, uint64_t start_height, uint64_t & blo throw std::runtime_error("proxy exception in refresh thread"); } + if(!first && blocks_start_height == next_blocks_start_height) + { + m_node_rpc_proxy.set_height(m_blockchain.size()); + refreshed = true; + break; + } + + first = false; + if (!next_blocks.empty()) { const uint64_t expected_start_height = std::max(static_cast<uint64_t>(m_blockchain.size()), uint64_t(1)) - 1; @@ -3745,9 +3787,11 @@ void wallet2::detach_blockchain(uint64_t height, std::map<std::pair<uint64_t, ui //---------------------------------------------------------------------------------------------------- bool wallet2::deinit() { - m_is_initialized=false; - unlock_keys_file(); - m_account.deinit(); + if(m_is_initialized) { + m_is_initialized = false; + unlock_keys_file(); + m_account.deinit(); + } return true; } //---------------------------------------------------------------------------------------------------- @@ -4422,7 +4466,7 @@ bool wallet2::load_keys_buf(const std::string& keys_buf, const epee::wipeable_st account_public_address device_account_public_address; THROW_WALLET_EXCEPTION_IF(!hwdev.get_public_address(device_account_public_address), error::wallet_internal_error, "Cannot get a device address"); - THROW_WALLET_EXCEPTION_IF(device_account_public_address != m_account.get_keys().m_account_address, error::wallet_internal_error, "Device wallet does not match wallet address. " + THROW_WALLET_EXCEPTION_IF(device_account_public_address != m_account.get_keys().m_account_address, error::wallet_internal_error, "Device wallet does not match wallet address. If the device uses the passphrase feature, please check whether the passphrase was entered correctly (it may have been misspelled - different passphrases generate different wallets, passphrase is case-sensitive). " "Device address: " + cryptonote::get_account_address_as_str(m_nettype, false, device_account_public_address) + ", wallet address: " + m_account.get_public_address_str(m_nettype)); LOG_PRINT_L0("Device inited..."); @@ -8587,18 +8631,30 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> } // get the keys for those - req.get_txid = false; - + // the response can get large and end up rejected by the anti DoS limits, so chunk it if needed + size_t offset = 0; + while (offset < req.outputs.size()) { + static const size_t chunk_size = 1000; + COMMAND_RPC_GET_OUTPUTS_BIN::request chunk_req = AUTO_VAL_INIT(chunk_req); + COMMAND_RPC_GET_OUTPUTS_BIN::response chunk_daemon_resp = AUTO_VAL_INIT(chunk_daemon_resp); + chunk_req.get_txid = false; + for (size_t i = 0; i < std::min<size_t>(req.outputs.size() - offset, chunk_size); ++i) + chunk_req.outputs.push_back(req.outputs[offset + i]); + const boost::lock_guard<boost::recursive_mutex> lock{m_daemon_rpc_mutex}; uint64_t pre_call_credits = m_rpc_payment_state.credits; - req.client = get_client_signature(); - bool r = epee::net_utils::invoke_http_bin("/get_outs.bin", req, daemon_resp, *m_http_client, rpc_timeout); - THROW_ON_RPC_RESPONSE_ERROR(r, {}, daemon_resp, "get_outs.bin", error::get_outs_error, get_rpc_status(daemon_resp.status)); - THROW_WALLET_EXCEPTION_IF(daemon_resp.outs.size() != req.outputs.size(), error::wallet_internal_error, + chunk_req.client = get_client_signature(); + bool r = epee::net_utils::invoke_http_bin("/get_outs.bin", chunk_req, chunk_daemon_resp, *m_http_client, rpc_timeout); + THROW_ON_RPC_RESPONSE_ERROR(r, {}, chunk_daemon_resp, "get_outs.bin", error::get_outs_error, get_rpc_status(chunk_daemon_resp.status)); + THROW_WALLET_EXCEPTION_IF(chunk_daemon_resp.outs.size() != chunk_req.outputs.size(), error::wallet_internal_error, "daemon returned wrong response for get_outs.bin, wrong amounts count = " + - std::to_string(daemon_resp.outs.size()) + ", expected " + std::to_string(req.outputs.size())); - check_rpc_cost("/get_outs.bin", daemon_resp.credits, pre_call_credits, daemon_resp.outs.size() * COST_PER_OUT); + std::to_string(chunk_daemon_resp.outs.size()) + ", expected " + std::to_string(chunk_req.outputs.size())); + check_rpc_cost("/get_outs.bin", chunk_daemon_resp.credits, pre_call_credits, chunk_daemon_resp.outs.size() * COST_PER_OUT); + + offset += chunk_size; + for (size_t i = 0; i < chunk_daemon_resp.outs.size(); ++i) + daemon_resp.outs.push_back(std::move(chunk_daemon_resp.outs[i])); } std::unordered_map<uint64_t, uint64_t> scanty_outs; @@ -8641,7 +8697,8 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> 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; + if (daemon_resp.outs[i].unlocked) + real_out_found = true; } THROW_WALLET_EXCEPTION_IF(!real_out_found, error::wallet_internal_error, "Daemon response did not include the requested real output"); @@ -10204,6 +10261,38 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp const size_t num_outputs = get_num_outputs(tx.dsts, m_transfers, tx.selected_transfers); needed_fee = estimate_fee(use_per_byte_fee, use_rct ,tx.selected_transfers.size(), fake_outs_count, num_outputs, extra.size(), bulletproof, clsag, base_fee, fee_multiplier, fee_quantization_mask); + auto try_carving_from_partial_payment = [&](uint64_t needed_fee, uint64_t available_for_fee) + { + // The check against original_output_index is to ensure the last entry in tx.dsts is really + // a partial payment. Otherwise multiple requested outputs to the same address could + // fool this logic into thinking there is a partial payment. + if (needed_fee > available_for_fee && !dsts.empty() && dsts[0].amount > 0 && tx.dsts.size() > original_output_index) + { + // we don't have enough for the fee, but we've only partially paid the current address, + // so we can take the fee from the paid amount, since we'll have to make another tx anyway + LOG_PRINT_L2("Attempting to carve tx fee " << print_money(needed_fee) << " from partial payment (first pass)"); + std::vector<cryptonote::tx_destination_entry>::iterator i; + i = std::find_if(tx.dsts.begin(), tx.dsts.end(), + [&](const cryptonote::tx_destination_entry &d) { return !memcmp (&d.addr, &dsts[0].addr, sizeof(dsts[0].addr)); }); + THROW_WALLET_EXCEPTION_IF(i == tx.dsts.end(), error::wallet_internal_error, "paid address not found in outputs"); + if (i->amount > needed_fee) + { + uint64_t new_paid_amount = i->amount /*+ test_ptx.fee*/ - needed_fee; + LOG_PRINT_L2("Adjusting amount paid to " << get_account_address_as_str(m_nettype, i->is_subaddress, i->addr) << " from " << + print_money(i->amount) << " to " << print_money(new_paid_amount) << " to accommodate " << + print_money(needed_fee) << " fee"); + dsts[0].amount += i->amount - new_paid_amount; + i->amount = new_paid_amount; + test_ptx.fee = needed_fee; + available_for_fee = needed_fee; + } + } + return available_for_fee; + }; + + // Try to carve the estimated fee from the partial payment (if there is one) + available_for_fee = try_carving_from_partial_payment(needed_fee, available_for_fee); + uint64_t inputs = 0, outputs = needed_fee; for (size_t idx: tx.selected_transfers) inputs += m_transfers[idx].amount(); for (const auto &o: tx.dsts) outputs += o.amount; @@ -10229,26 +10318,8 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp LOG_PRINT_L2("Made a " << get_weight_string(test_ptx.tx, txBlob.size()) << " tx, with " << print_money(available_for_fee) << " available for fee (" << print_money(needed_fee) << " needed)"); - if (needed_fee > available_for_fee && !dsts.empty() && dsts[0].amount > 0) - { - // we don't have enough for the fee, but we've only partially paid the current address, - // so we can take the fee from the paid amount, since we'll have to make another tx anyway - std::vector<cryptonote::tx_destination_entry>::iterator i; - i = std::find_if(tx.dsts.begin(), tx.dsts.end(), - [&](const cryptonote::tx_destination_entry &d) { return !memcmp (&d.addr, &dsts[0].addr, sizeof(dsts[0].addr)); }); - THROW_WALLET_EXCEPTION_IF(i == tx.dsts.end(), error::wallet_internal_error, "paid address not found in outputs"); - if (i->amount > needed_fee) - { - uint64_t new_paid_amount = i->amount /*+ test_ptx.fee*/ - needed_fee; - LOG_PRINT_L2("Adjusting amount paid to " << get_account_address_as_str(m_nettype, i->is_subaddress, i->addr) << " from " << - print_money(i->amount) << " to " << print_money(new_paid_amount) << " to accommodate " << - print_money(needed_fee) << " fee"); - dsts[0].amount += i->amount - new_paid_amount; - i->amount = new_paid_amount; - test_ptx.fee = needed_fee; - available_for_fee = needed_fee; - } - } + // Try to carve the fee from the partial payment again after updating from estimate to actual + available_for_fee = try_carving_from_partial_payment(needed_fee, available_for_fee); if (needed_fee > available_for_fee) { diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index b72817ba0..e1a06886b 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -1292,13 +1292,20 @@ namespace tools try { // gather info to ask the user - std::unordered_map<cryptonote::account_public_address, std::pair<std::string, uint64_t>> dests; + std::unordered_map<cryptonote::account_public_address, std::pair<std::string, uint64_t>> tx_dests; + std::unordered_map<cryptonote::account_public_address, std::pair<std::string, uint64_t>> all_dests; int first_known_non_zero_change_index = -1; + res.summary.amount_in = 0; + res.summary.amount_out = 0; + res.summary.change_amount = 0; + res.summary.fee = 0; for (size_t n = 0; n < tx_constructions.size(); ++n) { const tools::wallet2::tx_construction_data &cd = tx_constructions[n]; res.desc.push_back({0, 0, std::numeric_limits<uint32_t>::max(), 0, {}, "", 0, "", 0, 0, ""}); wallet_rpc::COMMAND_RPC_DESCRIBE_TRANSFER::transfer_description &desc = res.desc.back(); + // Clear the recipients collection ready for this loop iteration + tx_dests.clear(); std::vector<cryptonote::tx_extra_field> tx_extra_fields; bool has_encrypted_payment_id = false; @@ -1337,17 +1344,17 @@ namespace tools std::string address = cryptonote::get_account_address_as_str(m_wallet->nettype(), entry.is_subaddress, entry.addr); if (has_encrypted_payment_id && !entry.is_subaddress && address != entry.original) address = cryptonote::get_account_integrated_address_as_str(m_wallet->nettype(), entry.addr, payment_id8); - auto i = dests.find(entry.addr); - if (i == dests.end()) - dests.insert(std::make_pair(entry.addr, std::make_pair(address, entry.amount))); + auto i = tx_dests.find(entry.addr); + if (i == tx_dests.end()) + tx_dests.insert(std::make_pair(entry.addr, std::make_pair(address, entry.amount))); else i->second.second += entry.amount; desc.amount_out += entry.amount; } if (cd.change_dts.amount > 0) { - auto it = dests.find(cd.change_dts.addr); - if (it == dests.end()) + auto it = tx_dests.find(cd.change_dts.addr); + if (it == tx_dests.end()) { er.code = WALLET_RPC_ERROR_CODE_BAD_UNSIGNED_TX_DATA; er.message = "Claimed change does not go to a paid address"; @@ -1374,29 +1381,45 @@ namespace tools desc.change_amount += cd.change_dts.amount; it->second.second -= cd.change_dts.amount; if (it->second.second == 0) - dests.erase(cd.change_dts.addr); + tx_dests.erase(cd.change_dts.addr); } - for (auto i = dests.begin(); i != dests.end(); ) + for (auto i = tx_dests.begin(); i != tx_dests.end(); ++i) { if (i->second.second > 0) { desc.recipients.push_back({i->second.first, i->second.second}); + auto it_in_all = all_dests.find(i->first); + if (it_in_all == all_dests.end()) + all_dests.insert(std::make_pair(i->first, i->second)); + else + it_in_all->second.second += i->second.second; } else ++desc.dummy_outputs; - ++i; } if (desc.change_amount > 0) { const tools::wallet2::tx_construction_data &cd0 = tx_constructions[0]; desc.change_address = get_account_address_as_str(m_wallet->nettype(), cd0.subaddr_account > 0, cd0.change_dts.addr); + res.summary.change_address = desc.change_address; } desc.fee = desc.amount_in - desc.amount_out; desc.unlock_time = cd.unlock_time; desc.extra = epee::to_hex::string({cd.extra.data(), cd.extra.size()}); + + // Update summary items + res.summary.amount_in += desc.amount_in; + res.summary.amount_out += desc.amount_out; + res.summary.change_amount += desc.change_amount; + res.summary.fee += desc.fee; + } + // Populate the summary recipients list + for (auto i = all_dests.begin(); i != all_dests.end(); ++i) + { + res.summary.recipients.push_back({i->second.first, i->second.second}); } } catch (const std::exception &e) diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h index 6640441ed..248d31aa4 100644 --- a/src/wallet/wallet_rpc_server_commands_defs.h +++ b/src/wallet/wallet_rpc_server_commands_defs.h @@ -47,7 +47,7 @@ // advance which version they will stop working with // Don't go over 32767 for any of these #define WALLET_RPC_VERSION_MAJOR 1 -#define WALLET_RPC_VERSION_MINOR 22 +#define WALLET_RPC_VERSION_MINOR 23 #define MAKE_WALLET_RPC_VERSION(major,minor) (((major)<<16)|(minor)) #define WALLET_RPC_VERSION MAKE_WALLET_RPC_VERSION(WALLET_RPC_VERSION_MAJOR, WALLET_RPC_VERSION_MINOR) namespace tools @@ -701,6 +701,25 @@ namespace wallet_rpc END_KV_SERIALIZE_MAP() }; + struct txset_summary + { + uint64_t amount_in; + uint64_t amount_out; + std::list<recipient> recipients; + uint64_t change_amount; + std::string change_address; + uint64_t fee; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(amount_in) + KV_SERIALIZE(amount_out) + KV_SERIALIZE(recipients) + KV_SERIALIZE(change_amount) + KV_SERIALIZE(change_address) + KV_SERIALIZE(fee) + END_KV_SERIALIZE_MAP() + }; + struct request_t { std::string unsigned_txset; @@ -716,8 +735,10 @@ namespace wallet_rpc struct response_t { std::list<transfer_description> desc; + struct txset_summary summary; BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(summary) KV_SERIALIZE(desc) END_KV_SERIALIZE_MAP() }; |