// Copyright (c) 2011-2014 The Cryptonote developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "WalletTransactionSender.h" #include "WalletUtils.h" namespace { uint64_t countNeededMoney(uint64_t fee, const std::vector& transfers) { uint64_t needed_money = fee; for (auto& transfer: transfers) { CryptoNote::throwIf(transfer.amount == 0, cryptonote::error::ZERO_DESTINATION); CryptoNote::throwIf(transfer.amount < 0, cryptonote::error::WRONG_AMOUNT); needed_money += transfer.amount; CryptoNote::throwIf(static_cast(needed_money) < transfer.amount, cryptonote::error::SUM_OVERFLOW); } return needed_money; } void createChangeDestinations(const cryptonote::account_public_address& address, uint64_t neededMoney, uint64_t foundMoney, cryptonote::tx_destination_entry& changeDts) { if (neededMoney < foundMoney) { changeDts.addr = address; changeDts.amount = foundMoney - neededMoney; } } void constructTx(const cryptonote::account_keys keys, const std::vector& sources, const std::vector& splittedDests, const std::string& extra, uint64_t unlockTimestamp, uint64_t sizeLimit, cryptonote::transaction& tx) { std::vector extraVec; extraVec.reserve(extra.size()); std::for_each(extra.begin(), extra.end(), [&extraVec] (const char el) { extraVec.push_back(el);}); bool r = cryptonote::construct_tx(keys, sources, splittedDests, extraVec, tx, unlockTimestamp); CryptoNote::throwIf(!r, cryptonote::error::INTERNAL_WALLET_ERROR); CryptoNote::throwIf(cryptonote::get_object_blobsize(tx) >= sizeLimit, cryptonote::error::TRANSACTION_SIZE_TOO_BIG); } void fillTransactionHash(const cryptonote::transaction& tx, CryptoNote::TransactionHash& hash) { crypto::hash h = cryptonote::get_transaction_hash(tx); memcpy(hash.data(), reinterpret_cast(&h), hash.size()); } } //namespace namespace CryptoNote { WalletTransactionSender::WalletTransactionSender(WalletUserTransactionsCache& transactionsCache, WalletTxSendingState& sendingTxsStates, WalletTransferDetails& transferDetails, WalletUnconfirmedTransactions& unconfirmedTransactions): m_transactionsCache(transactionsCache), m_sendingTxsStates(sendingTxsStates), m_transferDetails(transferDetails), m_unconfirmedTransactions(unconfirmedTransactions), m_isInitialized(false), m_isStoping(false) { m_upperTransactionSizeLimit = CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE*2 - CRYPTONOTE_COINBASE_BLOB_RESERVED_SIZE; } void WalletTransactionSender::init(cryptonote::account_keys keys) { if (m_isInitialized) { throw std::system_error(make_error_code(cryptonote::error::NOT_INITIALIZED)); } m_keys = keys; m_isInitialized = true; } void WalletTransactionSender::stop() { m_isStoping = true; } std::shared_ptr WalletTransactionSender::makeSendRequest(TransactionId& transactionId, std::deque >& events, const std::vector& transfers, uint64_t fee, const std::string& extra, uint64_t mixIn, uint64_t unlockTimestamp) { if (!m_isInitialized) throw std::system_error(make_error_code(cryptonote::error::NOT_INITIALIZED)); using namespace cryptonote; std::shared_ptr context = std::make_shared(); throwIf(transfers.empty(), cryptonote::error::ZERO_DESTINATION); TransferId firstTransferId = m_transactionsCache.insertTransfers(transfers); uint64_t neededMoney = countNeededMoney(fee, transfers); Transaction transaction; transaction.firstTransferId = firstTransferId; transaction.transferCount = transfers.size(); transaction.totalAmount = neededMoney; transaction.fee = fee; transaction.isCoinbase = false; transaction.timestamp = 0; transaction.extra = extra; transaction.blockHeight = UNCONFIRMED_TRANSACTION_HEIGHT; TransactionId txId = m_transactionsCache.insertTransaction(std::move(transaction)); transactionId = txId; m_sendingTxsStates.sending(txId); context->transactionId = txId; context->unlockTimestamp = unlockTimestamp; context->mixIn = mixIn; context->foundMoney = m_transferDetails.selectTransfersToSend(neededMoney, 0 == mixIn, context->dustPolicy.dustThreshold, context->selectedTransfers); throwIf(context->foundMoney < neededMoney, cryptonote::error::WRONG_AMOUNT); if(context->mixIn) { std::shared_ptr request = makeGetRandomOutsRequest(context); return request; } return doSendTransaction(context, events); } std::shared_ptr WalletTransactionSender::makeGetRandomOutsRequest(std::shared_ptr context) { uint64_t outsCount = context->mixIn + 1;// add one to make possible (if need) to skip real output key std::vector amounts; for (auto idx: context->selectedTransfers) { const TransferDetails& td = m_transferDetails.getTransferDetails(idx); throwIf(td.tx.vout.size() <= td.internalOutputIndex, cryptonote::error::INTERNAL_WALLET_ERROR); amounts.push_back(td.amount()); } return std::make_shared(amounts, outsCount, context, std::bind(&WalletTransactionSender::sendTransactionRandomOutsByAmount, this, context, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); } void WalletTransactionSender::sendTransactionRandomOutsByAmount(std::shared_ptr context, std::deque >& events, boost::optional >& nextRequest, std::error_code ec) { if (m_isStoping) { events.push_back(std::make_shared(context->transactionId, make_error_code(cryptonote::error::TX_CANCELLED))); return; } if (ec) { events.push_back(std::make_shared(context->transactionId, ec)); return; } auto scanty_it = std::find_if(context->outs.begin(), context->outs.end(), [&] (cryptonote::COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::outs_for_amount& out) {return out.outs.size() < context->mixIn;}); if (scanty_it != context->outs.end()) { events.push_back(std::make_shared(context->transactionId, make_error_code(cryptonote::error::MIXIN_COUNT_TOO_BIG))); return; } std::shared_ptr req = doSendTransaction(context, events); if (req) nextRequest = req; } std::shared_ptr WalletTransactionSender::doSendTransaction(std::shared_ptr context, std::deque >& events) { if (m_isStoping) { events.push_back(std::make_shared(context->transactionId, make_error_code(cryptonote::error::TX_CANCELLED))); return std::shared_ptr(); } try { Transaction& transaction = m_transactionsCache.getTransaction(context->transactionId); std::vector sources; prepareInputs(context->selectedTransfers, context->outs, sources, context->mixIn); cryptonote::tx_destination_entry changeDts = AUTO_VAL_INIT(changeDts); createChangeDestinations(m_keys.m_account_address, transaction.totalAmount, context->foundMoney, changeDts); std::vector splittedDests; splitDestinations(transaction.firstTransferId, transaction.transferCount, changeDts, context->dustPolicy, splittedDests); cryptonote::transaction tx; constructTx(m_keys, sources, splittedDests, transaction.extra, context->unlockTimestamp, m_upperTransactionSizeLimit, tx); fillTransactionHash(tx, transaction.hash); m_unconfirmedTransactions.add(tx, context->transactionId, changeDts.amount); notifyBalanceChanged(events); return std::make_shared(tx, std::bind(&WalletTransactionSender::relayTransactionCallback, this, context->transactionId, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); } catch(std::system_error& ec) { events.push_back(std::make_shared(context->transactionId, ec.code())); } catch(std::exception&) { events.push_back(std::make_shared(context->transactionId, make_error_code(cryptonote::error::INTERNAL_WALLET_ERROR))); } return std::shared_ptr(); } void WalletTransactionSender::relayTransactionCallback(TransactionId txId, std::deque >& events, boost::optional >& nextRequest, std::error_code ec) { if (m_isStoping) return; if (ec) { m_sendingTxsStates.error(txId); } else { m_sendingTxsStates.sent(txId); } events.push_back(std::make_shared(txId, ec)); } void WalletTransactionSender::splitDestinations(TransferId firstTransferId, size_t transfersCount, const cryptonote::tx_destination_entry& changeDts, const TxDustPolicy& dustPolicy, std::vector& splittedDests) { uint64_t dust = 0; digitSplitStrategy(firstTransferId, transfersCount, changeDts, dustPolicy.dustThreshold, splittedDests, dust); throwIf(dustPolicy.dustThreshold < dust, cryptonote::error::INTERNAL_WALLET_ERROR); if (0 != dust && !dustPolicy.addToFee) { splittedDests.push_back(cryptonote::tx_destination_entry(dust, dustPolicy.addrForDust)); } } void WalletTransactionSender::digitSplitStrategy(TransferId firstTransferId, size_t transfersCount, const cryptonote::tx_destination_entry& change_dst, uint64_t dust_threshold, std::vector& splitted_dsts, uint64_t& dust) { splitted_dsts.clear(); dust = 0; for (TransferId idx = firstTransferId; idx < firstTransferId + transfersCount; ++idx) { Transfer& de = m_transactionsCache.getTransfer(idx); cryptonote::account_public_address addr; if (!cryptonote::get_account_address_from_str(addr, de.address)) { throw std::system_error(make_error_code(cryptonote::error::BAD_ADDRESS)); } cryptonote::decompose_amount_into_digits(de.amount, dust_threshold, [&](uint64_t chunk) { splitted_dsts.push_back(cryptonote::tx_destination_entry(chunk, addr)); }, [&](uint64_t a_dust) { splitted_dsts.push_back(cryptonote::tx_destination_entry(a_dust, addr)); } ); } cryptonote::decompose_amount_into_digits(change_dst.amount, dust_threshold, [&](uint64_t chunk) { splitted_dsts.push_back(cryptonote::tx_destination_entry(chunk, change_dst.addr)); }, [&](uint64_t a_dust) { dust = a_dust; } ); } void WalletTransactionSender::prepareInputs(const std::list& selectedTransfers, std::vector& outs, std::vector& sources, uint64_t mixIn) { size_t i = 0; for (size_t idx: selectedTransfers) { sources.resize(sources.size()+1); cryptonote::tx_source_entry& src = sources.back(); TransferDetails& td = m_transferDetails.getTransferDetails(idx); src.amount = td.amount(); //paste mixin transaction if(outs.size()) { outs[i].outs.sort([](const cryptonote::COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::out_entry& a, const cryptonote::COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::out_entry& b){return a.global_amount_index < b.global_amount_index;}); for (auto& daemon_oe: outs[i].outs) { if(td.globalOutputIndex == daemon_oe.global_amount_index) continue; cryptonote::tx_source_entry::output_entry oe; oe.first = daemon_oe.global_amount_index; oe.second = daemon_oe.out_key; src.outputs.push_back(oe); if(src.outputs.size() >= mixIn) break; } } //paste real transaction to the random index auto it_to_insert = std::find_if(src.outputs.begin(), src.outputs.end(), [&](const cryptonote::tx_source_entry::output_entry& a) { return a.first >= td.globalOutputIndex; }); cryptonote::tx_source_entry::output_entry real_oe; real_oe.first = td.globalOutputIndex; real_oe.second = boost::get(td.tx.vout[td.internalOutputIndex].target).key; auto interted_it = src.outputs.insert(it_to_insert, real_oe); src.real_out_tx_key = get_tx_pub_key_from_extra(td.tx); src.real_output = interted_it - src.outputs.begin(); src.real_output_in_tx_index = td.internalOutputIndex; ++i; } } void WalletTransactionSender::notifyBalanceChanged(std::deque >& events) { uint64_t actualBalance = m_transferDetails.countActualBalance(); uint64_t pendingBalance = m_unconfirmedTransactions.countPendingBalance(); pendingBalance += m_transferDetails.countPendingBalance(); events.push_back(std::make_shared(actualBalance)); events.push_back(std::make_shared(pendingBalance)); } } /* namespace CryptoNote */