From 30aa04815c53c21a703d6713e514e2a47480fc57 Mon Sep 17 00:00:00 2001 From: David Burkett Date: Thu, 31 Mar 2022 10:49:08 -0400 Subject: [PATCH] * txrecord refactor and introduction of txlist * createtransaction rewrite (TxAssembler) * added listwallettransactions rpc method --- configure.ac | 2 +- src/Makefile.am | 6 +- src/Makefile.qt.include | 2 - src/interfaces/wallet.cpp | 39 +- src/interfaces/wallet.h | 15 +- src/libmw/include/mw/models/tx/Input.h | 2 + src/libmw/include/mw/models/tx/Output.h | 4 +- src/libmw/include/mw/models/tx/Transaction.h | 4 +- src/mweb/mweb_transact.cpp | 199 +++-- src/mweb/mweb_transact.h | 15 +- src/mweb/mweb_wallet.h | 20 +- src/qt/sendcoinsentry.cpp | 6 - src/qt/transactiondesc.cpp | 82 +- src/qt/transactiondesc.h | 8 +- src/qt/transactionfilterproxy.cpp | 4 +- src/qt/transactionrecord.cpp | 287 ------- src/qt/transactionrecord.h | 147 ---- src/qt/transactiontablemodel.cpp | 190 +++-- src/qt/transactiontablemodel.h | 24 +- src/qt/transactionview.cpp | 15 +- src/qt/walletmodeltransaction.cpp | 9 + src/rpc/mining.cpp | 11 +- src/test/validation_tests.cpp | 61 -- src/wallet/createtransaction.cpp | 705 ------------------ src/wallet/createtransaction.h | 20 - src/wallet/rpcwallet.cpp | 104 ++- src/wallet/test/wallet_tests.cpp | 5 +- src/wallet/txassembler.cpp | 662 ++++++++++++++++ src/wallet/txassembler.h | 128 ++++ src/wallet/txlist.cpp | 303 ++++++++ src/wallet/txlist.h | 40 + src/wallet/txrecord.cpp | 207 +++++ src/wallet/txrecord.h | 130 ++++ src/wallet/wallet.cpp | 124 ++- src/wallet/wallet.h | 4 + src/wallet/walletdb.cpp | 17 +- test/functional/mining_basic.py | 2 +- test/functional/mweb_basic.py | 5 +- test/functional/mweb_wallet_basic.py | 41 +- test/functional/test_runner.py | 1 + .../wallet_listwallettransactions.py | 150 ++++ 41 files changed, 2218 insertions(+), 1582 deletions(-) delete mode 100644 src/qt/transactionrecord.cpp delete mode 100644 src/qt/transactionrecord.h delete mode 100644 src/wallet/createtransaction.cpp delete mode 100644 src/wallet/createtransaction.h create mode 100644 src/wallet/txassembler.cpp create mode 100644 src/wallet/txassembler.h create mode 100644 src/wallet/txlist.cpp create mode 100644 src/wallet/txlist.h create mode 100644 src/wallet/txrecord.cpp create mode 100644 src/wallet/txrecord.h create mode 100644 test/functional/wallet_listwallettransactions.py diff --git a/configure.ac b/configure.ac index 97aadc28c..437134e4b 100644 --- a/configure.ac +++ b/configure.ac @@ -3,7 +3,7 @@ define(_CLIENT_VERSION_MAJOR, 0) define(_CLIENT_VERSION_MINOR, 21) define(_CLIENT_VERSION_REVISION, 2) define(_CLIENT_VERSION_BUILD, 0) -define(_CLIENT_VERSION_RC, 5) +define(_CLIENT_VERSION_RC, 6) define(_CLIENT_VERSION_IS_RELEASE, true) define(_COPYRIGHT_YEAR, 2022) define(_COPYRIGHT_HOLDERS,[The %s developers]) diff --git a/src/Makefile.am b/src/Makefile.am index d080fc111..6c9bfbf18 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -302,7 +302,6 @@ BITCOIN_CORE_H = \ wallet/coincontrol.h \ wallet/coinselection.h \ wallet/context.h \ - wallet/createtransaction.h \ wallet/crypter.h \ wallet/db.h \ wallet/feebumper.h \ @@ -314,6 +313,7 @@ BITCOIN_CORE_H = \ wallet/salvage.h \ wallet/scriptpubkeyman.h \ wallet/sqlite.h \ + wallet/txassembler.h \ wallet/wallet.h \ wallet/walletdb.h \ wallet/wallettool.h \ @@ -423,7 +423,6 @@ libbitcoin_wallet_a_SOURCES = \ wallet/bdb.cpp \ wallet/coincontrol.cpp \ wallet/context.cpp \ - wallet/createtransaction.cpp \ wallet/crypter.cpp \ wallet/db.cpp \ wallet/feebumper.cpp \ @@ -434,6 +433,9 @@ libbitcoin_wallet_a_SOURCES = \ wallet/rpcwallet.cpp \ wallet/salvage.cpp \ wallet/scriptpubkeyman.cpp \ + wallet/txassembler.cpp \ + wallet/txlist.cpp \ + wallet/txrecord.cpp \ wallet/wallet.cpp \ wallet/walletdb.cpp \ wallet/walletutil.cpp \ diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index c375649dd..64c4b58f2 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -153,7 +153,6 @@ BITCOIN_QT_H = \ qt/transactiondescdialog.h \ qt/transactionfilterproxy.h \ qt/transactionoverviewwidget.h \ - qt/transactionrecord.h \ qt/transactiontablemodel.h \ qt/transactionview.h \ qt/utilitydialog.h \ @@ -262,7 +261,6 @@ BITCOIN_QT_WALLET_CPP = \ qt/transactiondesc.cpp \ qt/transactiondescdialog.cpp \ qt/transactionfilterproxy.cpp \ - qt/transactionrecord.cpp \ qt/transactiontablemodel.cpp \ qt/transactionview.cpp \ qt/walletcontroller.cpp \ diff --git a/src/interfaces/wallet.cpp b/src/interfaces/wallet.cpp index 1eeaeb0d8..705137c68 100644 --- a/src/interfaces/wallet.cpp +++ b/src/interfaces/wallet.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -156,6 +157,7 @@ WalletTxStatus MakeWalletTxStatus(CWallet& wallet, const CWalletTx& wtx) result.is_trusted = wtx.IsTrusted(); result.is_abandoned = wtx.isAbandoned(); result.is_coinbase = wtx.IsCoinBase(); + result.is_hogex = wtx.IsHogEx(); result.is_in_main_chain = wtx.IsInMainChain(); return result; } @@ -375,43 +377,19 @@ public: } return {}; } - WalletTx getWalletTx(const uint256& txid) override + std::vector getWalletTx(const uint256& txid) override { LOCK(m_wallet->cs_wallet); auto mi = m_wallet->mapWallet.find(txid); if (mi != m_wallet->mapWallet.end()) { - return MakeWalletTx(*m_wallet, mi->second); + return TxList(*m_wallet).List(mi->second, ISMINE_ALL, boost::none, boost::none); } return {}; } - std::vector getWalletTxs() override + std::vector getWalletTxs() override { LOCK(m_wallet->cs_wallet); - std::vector result; - result.reserve(m_wallet->mapWallet.size()); - for (const auto& entry : m_wallet->mapWallet) { - result.emplace_back(MakeWalletTx(*m_wallet, entry.second)); - } - return result; - } - bool tryGetTxStatus(const uint256& txid, - interfaces::WalletTxStatus& tx_status, - int& num_blocks, - int64_t& block_time) override - { - TRY_LOCK(m_wallet->cs_wallet, locked_wallet); - if (!locked_wallet) { - return false; - } - auto mi = m_wallet->mapWallet.find(txid); - if (mi == m_wallet->mapWallet.end()) { - return false; - } - num_blocks = m_wallet->GetLastBlockHeight(); - block_time = -1; - CHECK_NONFATAL(m_wallet->chain().findBlock(m_wallet->GetLastBlockHash(), FoundBlock().time(block_time))); - tx_status = MakeWalletTxStatus(*m_wallet, mi->second); - return true; + return TxList(*m_wallet).ListAll(ISMINE_ALL); } WalletTx getWalletTxDetails(const uint256& txid, WalletTxStatus& tx_status, @@ -494,6 +472,11 @@ public: LOCK(m_wallet->cs_wallet); return m_wallet->GetCredit(txout, filter); } + bool isChange(const CTxOutput& txout) override + { + LOCK(m_wallet->cs_wallet); + return m_wallet->IsChange(txout); + } CoinsList listCoins() override { LOCK(m_wallet->cs_wallet); diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 8445bc395..71a8d52ee 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -13,6 +13,7 @@ #include // For SecureString #include #include +#include #include #include @@ -188,16 +189,10 @@ public: virtual CTransactionRef getTx(const uint256& txid) = 0; //! Get transaction information. - virtual WalletTx getWalletTx(const uint256& txid) = 0; + virtual std::vector getWalletTx(const uint256& txid) = 0; //! Get list of all wallet transactions. - virtual std::vector getWalletTxs() = 0; - - //! Try to get updated status for a particular transaction, if possible without blocking. - virtual bool tryGetTxStatus(const uint256& txid, - WalletTxStatus& tx_status, - int& num_blocks, - int64_t& block_time) = 0; + virtual std::vector getWalletTxs() = 0; //! Get transaction details. virtual WalletTx getWalletTxDetails(const uint256& txid, @@ -241,6 +236,9 @@ public: //! Return credit amount if transaction input belongs to wallet. virtual CAmount getCredit(const CTxOutput& txout, isminefilter filter) = 0; + //! Return true if the output is a change output. + virtual bool isChange(const CTxOutput& txout) = 0; + //! Return AvailableCoins + LockedCoins grouped by wallet address. //! (put change in one group with wallet address) using CoinsList = std::map>>; @@ -430,6 +428,7 @@ struct WalletTxStatus bool is_trusted; bool is_abandoned; bool is_coinbase; + bool is_hogex; bool is_in_main_chain; }; diff --git a/src/libmw/include/mw/models/tx/Input.h b/src/libmw/include/mw/models/tx/Input.h index 5c137bc82..b9fa9aa5f 100644 --- a/src/libmw/include/mw/models/tx/Input.h +++ b/src/libmw/include/mw/models/tx/Input.h @@ -17,6 +17,7 @@ class Input : public Traits::ICommitted, public Traits::IHashable, + public Traits::IPrintable, public Traits::ISerializable { enum FeatureBit { @@ -101,6 +102,7 @@ public: // // Traits // + std::string Format() const final { return "Input(ID=" + GetOutputID().Format() + ")"; } const mw::Hash& GetHash() const noexcept final { return m_hash; } private: diff --git a/src/libmw/include/mw/models/tx/Output.h b/src/libmw/include/mw/models/tx/Output.h index 44c417a27..8548e3d28 100644 --- a/src/libmw/include/mw/models/tx/Output.h +++ b/src/libmw/include/mw/models/tx/Output.h @@ -96,7 +96,8 @@ private: class Output : public Traits::ICommitted, public Traits::ISerializable, - public Traits::IHashable + public Traits::IHashable, + public Traits::IPrintable { public: // @@ -190,6 +191,7 @@ public: // // Traits // + std::string Format() const noexcept final { return "Output(ID=" + GetOutputID().Format() + ")"; } const mw::Hash& GetHash() const noexcept final { return m_hash; } private: diff --git a/src/libmw/include/mw/models/tx/Transaction.h b/src/libmw/include/mw/models/tx/Transaction.h index bbfba7695..2f19e5388 100644 --- a/src/libmw/include/mw/models/tx/Transaction.h +++ b/src/libmw/include/mw/models/tx/Transaction.h @@ -137,8 +137,8 @@ public: GetHash(), GetKernelOffset().ToHex(), kernels_str, - GetInputCommits(), - GetOutputCommits() + GetInputs(), + GetOutputs() ); } diff --git a/src/mweb/mweb_transact.cpp b/src/mweb/mweb_transact.cpp index 963c6213e..1e8c1d923 100644 --- a/src/mweb/mweb_transact.cpp +++ b/src/mweb/mweb_transact.cpp @@ -1,36 +1,70 @@ #include #include +#include #include #include +#include using namespace MWEB; -TxType MWEB::GetTxType(const std::vector& recipients, const std::vector& input_coins) +bool MWEB::ContainsPegIn(const TxType& mweb_type, const std::set& input_coins) { - assert(!recipients.empty()); - - static auto is_ltc = [](const CInputCoin& input) { return !input.IsMWEB(); }; - static auto is_mweb = [](const CInputCoin& input) { return input.IsMWEB(); }; - - if (recipients.front().IsMWEB()) { - // If any inputs are non-MWEB inputs, this is a peg-in transaction. - // Otherwise, it's a simple MWEB-to-MWEB transaction. - if (std::any_of(input_coins.cbegin(), input_coins.cend(), is_ltc)) { - return TxType::PEGIN; - } else { - return TxType::MWEB_TO_MWEB; - } - } else { - // If any inputs are MWEB inputs, this is a peg-out transaction. - // NOTE: This does not exclude the possibility that it's also pegging-in in addition to the pegout. - // Otherwise, if there are no MWEB inputs, it's a simple LTC-to-LTC transaction. - if (std::any_of(input_coins.cbegin(), input_coins.cend(), is_mweb)) { - return TxType::PEGOUT; - } else { - return TxType::LTC_TO_LTC; - } + if (mweb_type == MWEB::TxType::PEGIN) { + return true; } + + if (mweb_type == MWEB::TxType::PEGOUT) { + return std::any_of( + input_coins.cbegin(), input_coins.cend(), + [](const CInputCoin& coin) { return !coin.IsMWEB(); }); + } + + return false; +} + +bool MWEB::IsChangeOnMWEB(const CWallet& wallet, const MWEB::TxType& mweb_type, const std::vector& recipients, const CTxDestination& dest_change) +{ + if (mweb_type == MWEB::TxType::MWEB_TO_MWEB || mweb_type == MWEB::TxType::PEGOUT) { + return true; + } + + if (mweb_type == MWEB::TxType::PEGIN) { + return dest_change.type() == typeid(CNoDestination) || dest_change.type() == typeid(StealthAddress); + } + + return false; +} + +uint64_t MWEB::CalcMWEBWeight(const MWEB::TxType& mweb_type, const bool change_on_mweb, const std::vector& recipients) +{ + uint64_t mweb_weight = 0; + + if (mweb_type == MWEB::TxType::PEGIN || mweb_type == MWEB::TxType::MWEB_TO_MWEB) { + mweb_weight += Weight::STANDARD_OUTPUT_WEIGHT; // MW: FUTURE - Look at actual recipients list, but for now we only support 1 MWEB recipient. + } + + if (change_on_mweb) { + mweb_weight += Weight::STANDARD_OUTPUT_WEIGHT; + } + + if (mweb_type != MWEB::TxType::LTC_TO_LTC) { + CScript pegout_script = (mweb_type == MWEB::TxType::PEGOUT) ? recipients.front().GetScript() : CScript(); + mweb_weight += Weight::CalcKernelWeight(true, pegout_script); + } + + return mweb_weight; +} + +int64_t MWEB::CalcPegOutBytes(const TxType& mweb_type, const std::vector& recipients) +{ + if (mweb_type == MWEB::TxType::PEGOUT) { + CTxOut pegout_output(recipients.front().nAmount, recipients.front().receiver.GetScript()); + int64_t pegout_weight = (int64_t)::GetSerializeSize(pegout_output, PROTOCOL_VERSION) * WITNESS_SCALE_FACTOR; + return GetVirtualTransactionSize(pegout_weight, 0, 0); + } + + return 0; } bool Transact::CreateTx( @@ -38,22 +72,19 @@ bool Transact::CreateTx( CMutableTransaction& transaction, const std::vector& selected_coins, const std::vector& recipients, - const CAmount& ltc_fee, + const CAmount& total_fee, const CAmount& mweb_fee, - const bool include_mweb_change) + const CAmount& ltc_change, + const bool include_mweb_change, + bilingual_str& error) { - TxType type = MWEB::GetTxType(recipients, selected_coins); - if (type == TxType::LTC_TO_LTC) { - return true; - } - // Add recipients std::vector receivers; std::vector pegouts; for (const CRecipient& recipient : recipients) { CAmount recipient_amount = recipient.nAmount; if (recipient.fSubtractFeeFromAmount) { - recipient_amount -= ltc_fee; + recipient_amount -= total_fee; } if (recipient.IsMWEB()) { @@ -69,38 +100,56 @@ bool Transact::CreateTx( // Calculate pegin_amount boost::optional pegin_amount = boost::none; - CAmount ltc_input_amount = GetLTCInputAmount(selected_coins); + CAmount ltc_input_amount = std::accumulate( + selected_coins.cbegin(), selected_coins.cend(), CAmount(0), + [](CAmount amt, const CInputCoin& input) { return amt + (input.IsMWEB() ? 0 : input.GetAmount()); } + ); if (ltc_input_amount > 0) { - assert(ltc_fee < ltc_input_amount); - pegin_amount = (ltc_input_amount - ltc_fee + mweb_fee); // MW: TODO - There could also be LTC change + assert(total_fee < ltc_input_amount); + const CAmount ltc_fee = total_fee - mweb_fee; + pegin_amount = (ltc_input_amount - (ltc_fee + ltc_change)); } // Add Change if (include_mweb_change) { CAmount recipient_amount = std::accumulate( recipients.cbegin(), recipients.cend(), CAmount(0), - [ltc_fee](CAmount amount, const CRecipient& recipient) { - return amount + (recipient.nAmount - (recipient.fSubtractFeeFromAmount ? ltc_fee : 0)); + [total_fee](CAmount amount, const CRecipient& recipient) { + return amount + (recipient.nAmount - (recipient.fSubtractFeeFromAmount ? total_fee : 0)); } ); - CAmount change_amount = (pegin_amount.value_or(0) + GetMWEBInputAmount(selected_coins)) - (recipient_amount + mweb_fee); + CAmount mweb_input_amount = std::accumulate( + selected_coins.cbegin(), selected_coins.cend(), CAmount(0), + [](CAmount amt, const CInputCoin& input) { return amt + (input.IsMWEB() ? input.GetAmount() : 0); } + ); + + CAmount change_amount = (pegin_amount.value_or(0) + mweb_input_amount) - (recipient_amount + mweb_fee + ltc_change); if (change_amount < 0) { + error = _("MWEB change calculation failed"); return false; } + StealthAddress change_address; if (!mweb_wallet->GetStealthAddress(mw::CHANGE_INDEX, change_address)) { + error = _("Failed to retrieve change stealth address"); return false; } receivers.push_back(mw::Recipient{change_amount, change_address}); } - // Create transaction - std::vector input_coins = GetInputCoins(selected_coins); + std::vector input_coins; + for (const auto& coin : selected_coins) { + if (coin.IsMWEB()) { + input_coins.push_back(coin.GetMWEBCoin()); + } + } + std::vector output_coins; try { + // Create the MWEB transaction transaction.mweb_tx = TxBuilder::BuildTx( input_coins, receivers, @@ -109,7 +158,8 @@ bool Transact::CreateTx( mweb_fee, output_coins ); - } catch (std::exception&) { + } catch (std::exception& e) { + error = Untranslated(e.what()); return false; } @@ -120,54 +170,43 @@ bool Transact::CreateTx( // Update pegin output auto pegins = transaction.mweb_tx.GetPegIns(); if (!pegins.empty()) { - UpdatePegInOutput(transaction, pegins.front()); + for (size_t i = 0; i < transaction.vout.size(); i++) { + if (IsPegInOutput(CTransaction(transaction).GetOutput(i))) { + transaction.vout[i].nValue = pegins.front().GetAmount(); + transaction.vout[i].scriptPubKey = GetScriptForPegin(pegins.front().GetKernelID()); + break; + } + } } return true; } -std::vector Transact::GetInputCoins(const std::vector& inputs) +mw::Recipient Transact::BuildChangeRecipient() { - std::vector input_coins; - for (const auto& coin : inputs) { - if (coin.IsMWEB()) { - input_coins.push_back(coin.GetMWEBCoin()); - } - } - - return input_coins; -} - -CAmount Transact::GetMWEBInputAmount(const std::vector& inputs) -{ - return std::accumulate( - inputs.cbegin(), inputs.cend(), CAmount(0), - [](CAmount amt, const CInputCoin& input) { return amt + (input.IsMWEB() ? input.GetAmount() : 0); }); -} - -CAmount Transact::GetLTCInputAmount(const std::vector& inputs) -{ - return std::accumulate( - inputs.cbegin(), inputs.cend(), CAmount(0), - [](CAmount amt, const CInputCoin& input) { return amt + (input.IsMWEB() ? 0 : input.GetAmount()); }); -} - -CAmount Transact::GetMWEBRecipientAmount(const std::vector& recipients) -{ - return std::accumulate( + CAmount recipient_amount = std::accumulate( recipients.cbegin(), recipients.cend(), CAmount(0), - [](CAmount amt, const CRecipient& recipient) { return amt + (recipient.IsMWEB() ? recipient.nAmount : 0); }); -} - -bool Transact::UpdatePegInOutput(CMutableTransaction& transaction, const PegInCoin& pegin) -{ - for (size_t i = 0; i < transaction.vout.size(); i++) { - if (IsPegInOutput(CTransaction(transaction).GetOutput(i))) { - transaction.vout[i].nValue = pegin.GetAmount(); - transaction.vout[i].scriptPubKey = GetScriptForPegin(pegin.GetKernelID()); - return true; + [total_fee](CAmount amount, const CRecipient& recipient) { + return amount + (recipient.nAmount - (recipient.fSubtractFeeFromAmount ? total_fee : 0)); } + ); + + CAmount mweb_input_amount = std::accumulate( + selected_coins.cbegin(), selected_coins.cend(), CAmount(0), + [](CAmount amt, const CInputCoin& input) { return amt + (input.IsMWEB() ? input.GetAmount() : 0); } + ); + + CAmount change_amount = (pegin_amount.value_or(0) + mweb_input_amount) - (recipient_amount + mweb_fee + ltc_change); + if (change_amount < 0) { + error = _("MWEB change calculation failed"); + return false; } - return false; + StealthAddress change_address; + if (!mweb_wallet->GetStealthAddress(mw::CHANGE_INDEX, change_address)) { + error = _("Failed to retrieve change stealth address"); + return false; + } + + return mw::Recipient{change_amount, change_address}; } \ No newline at end of file diff --git a/src/mweb/mweb_transact.h b/src/mweb/mweb_transact.h index 4ee393742..903dd4301 100644 --- a/src/mweb/mweb_transact.h +++ b/src/mweb/mweb_transact.h @@ -16,7 +16,10 @@ enum class TxType { PEGOUT // NOTE: It's possible pegout transactions will also have pegins, but they will still be classified as PEGOUT }; -TxType GetTxType(const std::vector& recipients, const std::vector& input_coins); +bool ContainsPegIn(const TxType& mweb_type, const std::set& input_coins); +bool IsChangeOnMWEB(const CWallet& wallet, const MWEB::TxType& mweb_type, const std::vector& recipients, const CTxDestination& dest_change); +uint64_t CalcMWEBWeight(const MWEB::TxType& mweb_type, const bool change_on_mweb, const std::vector& recipients); +int64_t CalcPegOutBytes(const TxType& mweb_type, const std::vector& recipients); class Transact { @@ -28,15 +31,13 @@ public: const std::vector& recipients, const CAmount& ltc_fee, const CAmount& mweb_fee, - const bool include_mweb_change + const CAmount& ltc_change, + const bool include_mweb_change, + bilingual_str& error ); private: - static std::vector GetInputCoins(const std::vector& inputs); - static CAmount GetMWEBInputAmount(const std::vector& inputs); - static CAmount GetLTCInputAmount(const std::vector& inputs); - static CAmount GetMWEBRecipientAmount(const std::vector& recipients); - static bool UpdatePegInOutput(CMutableTransaction& transaction, const PegInCoin& pegin); + static mw::Recipient BuildChangeRecipient(); }; } \ No newline at end of file diff --git a/src/mweb/mweb_wallet.h b/src/mweb/mweb_wallet.h index 89eab7cd3..d82315643 100644 --- a/src/mweb/mweb_wallet.h +++ b/src/mweb/mweb_wallet.h @@ -62,20 +62,14 @@ struct WalletTxInfo // so we create an empty Transaction and store the spent hash here. boost::optional spent_input; - uint256 hash; + mutable uint256 hash; WalletTxInfo() : received_coin(boost::none), spent_input(boost::none), hash() { } WalletTxInfo(mw::Coin received) - : received_coin(std::move(received)), spent_input(boost::none) - { - hash = SerializeHash(*this); - } + : received_coin(std::move(received)), spent_input(boost::none), hash(received_coin->output_id.vec()) {} WalletTxInfo(mw::Hash spent) - : received_coin(boost::none), spent_input(std::move(spent)) - { - hash = SerializeHash(*this); - } + : received_coin(boost::none), spent_input(std::move(spent)), hash(spent_input->vec()) {} SERIALIZE_METHODS(WalletTxInfo, obj) { @@ -86,15 +80,15 @@ struct WalletTxInfo mw::Coin coin; SER_WRITE(obj, coin = *obj.received_coin); READWRITE(coin); + SER_READ(obj, obj.hash = uint256(coin.output_id.vec())); SER_READ(obj, obj.received_coin = boost::make_optional(std::move(coin))); } else { mw::Hash output_id; SER_WRITE(obj, output_id = *obj.spent_input); READWRITE(output_id); + SER_READ(obj, obj.hash = uint256(output_id.vec())); SER_READ(obj, obj.spent_input = boost::make_optional(std::move(output_id))); } - - SER_READ(obj, obj.hash = SerializeHash(obj)); } static WalletTxInfo FromHex(const std::string& str) @@ -114,7 +108,9 @@ struct WalletTxInfo return HexStr(std::vector{stream.begin(), stream.end()}); } - const uint256& GetHash() const { return hash; } + const uint256& GetHash() const { + return hash; + } }; } // namespace MWEB \ No newline at end of file diff --git a/src/qt/sendcoinsentry.cpp b/src/qt/sendcoinsentry.cpp index 955420fbf..3886c4926 100644 --- a/src/qt/sendcoinsentry.cpp +++ b/src/qt/sendcoinsentry.cpp @@ -252,14 +252,11 @@ void SendCoinsEntry::setPegInAddress(const std::string& address) setAddress(""); ui->payTo->setReadOnly(false); ui->payTo->setValidating(true); - ui->checkboxSubtractFeeFromAmount->setEnabled(true); } else { setAddress(QString::fromStdString("Peg-In: " + address)); ui->payTo->setReadOnly(true); ui->payTo->setValidating(false); ui->payTo->setCursorPosition(0); - ui->checkboxSubtractFeeFromAmount->setChecked(false); - ui->checkboxSubtractFeeFromAmount->setEnabled(false); } } @@ -272,14 +269,11 @@ void SendCoinsEntry::setPegOut(const bool pegout_set) setAddress(""); ui->payTo->setReadOnly(false); ui->payTo->setValidating(true); - ui->checkboxSubtractFeeFromAmount->setEnabled(true); } else { setAddress(QString::fromStdString("Peg-Out Address")); ui->payTo->setReadOnly(true); ui->payTo->setValidating(false); ui->payTo->setCursorPosition(0); - ui->checkboxSubtractFeeFromAmount->setChecked(false); - ui->checkboxSubtractFeeFromAmount->setEnabled(false); } } diff --git a/src/qt/transactiondesc.cpp b/src/qt/transactiondesc.cpp index 42ba80bad..7e74eab1d 100644 --- a/src/qt/transactiondesc.cpp +++ b/src/qt/transactiondesc.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include @@ -22,6 +21,7 @@ #include #include #include +#include #include #include @@ -77,13 +77,13 @@ bool GetPaymentRequestMerchant(const std::string& pr, QString& merchant) return false; } -QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wallet, TransactionRecord *rec, int unit) +QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wallet, WalletTxRecord *rec, int unit) { interfaces::WalletTxStatus status; interfaces::WalletOrderForm orderForm; bool inMempool; int numBlocks; - interfaces::WalletTx wtx = wallet.getWalletTxDetails(rec->hash, status, orderForm, inMempool, numBlocks); + interfaces::WalletTx wtx = wallet.getWalletTxDetails(rec->GetTxHash(), status, orderForm, inMempool, numBlocks); QString strHTML; @@ -105,7 +105,7 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall if (wtx.value_map.count("comment") && !wtx.value_map["comment"].empty()) strHTML += "
" + tr("Comment") + ":
" + GUIUtil::HtmlEscape(wtx.value_map["comment"], true) + "
"; - strHTML += "" + tr("Transaction ID") + ": " + rec->getTxHash() + "
"; + strHTML += "" + tr("Transaction ID") + ": " + QString::fromStdString(rec->GetTxHash().ToString()) + "
"; if (!wtx.tx->IsNull()) { strHTML += "" + tr("Transaction total size") + ": " + QString::number(wtx.tx->GetTotalSize()) + " bytes
"; @@ -115,7 +115,7 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall if (wtx.tx->HasMWEBTx()) { strHTML += "" + tr("Transaction MWEB weight") + ": " + QString::number(wtx.tx->mweb_tx.GetMWEBWeight()) + "
"; } - strHTML += "" + tr("Output index") + ": " + QString::number(rec->getOutputIndex()) + "
"; + strHTML += "" + tr("Component index") + ": " + QString::fromStdString(rec->GetComponentIndex()) + "
"; strHTML += toHTML_OrderForm(orderForm); @@ -137,7 +137,7 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall return strHTML; } -QString TransactionDesc::toHTML_Addresses(interfaces::Wallet& wallet, const interfaces::WalletTx& wtx, TransactionRecord* rec) +QString TransactionDesc::toHTML_Addresses(interfaces::Wallet& wallet, const interfaces::WalletTx& wtx, WalletTxRecord* rec) { QString strHTML; @@ -334,7 +334,7 @@ QString TransactionDesc::toHTML_OrderForm(const interfaces::WalletOrderForm& ord return strHTML; } -QString TransactionDesc::toHTML_Debug(interfaces::Node& node, interfaces::Wallet& wallet, const interfaces::WalletTx& wtx, TransactionRecord* rec, int unit) +QString TransactionDesc::toHTML_Debug(interfaces::Node& node, interfaces::Wallet& wallet, const interfaces::WalletTx& wtx, WalletTxRecord* rec, int unit) { QString strHTML = "

" + tr("Debug information") + "

"; for (const CTxInput& txin : wtx.inputs) { @@ -346,15 +346,29 @@ QString TransactionDesc::toHTML_Debug(interfaces::Node& node, interfaces::Wallet strHTML += "" + tr("Credit") + ": " + BitcoinUnits::formatHtmlWithUnit(unit, wallet.getCredit(out.txout, ISMINE_ALL)) + "
"; strHTML += "
" + tr("Transaction") + ":
"; - strHTML += GUIUtil::HtmlEscape(wtx.tx->ToString(), true); + strHTML += GUIUtil::HtmlEscape(rec->GetTxString(), true); + + auto get_prev_out = [&](const CTxInput& txin, CTxOutput& prevout) -> bool { + if (node.getUnspentOutput(txin.GetIndex(), prevout)) { + return true; + } + + if (txin.IsMWEB()) { + prevout = CTxOutput{txin.ToMWEB()}; + return true; + } + + return false; + }; strHTML += "
" + tr("Inputs") + ":"; strHTML += "
    "; for (const CTxInput& txin : wtx.inputs) { + strHTML += "
  • "; + CTxOutput prevout; - if (node.getUnspentOutput(txin.GetIndex(), prevout)) { - strHTML += "
  • "; + if (get_prev_out(txin, prevout)) { CTxDestination address; if (wallet.extractOutputDestination(prevout, address)) { std::string name; @@ -362,28 +376,40 @@ QString TransactionDesc::toHTML_Debug(interfaces::Node& node, interfaces::Wallet strHTML += GUIUtil::HtmlEscape(name) + " "; strHTML += QString::fromStdString(EncodeDestination(address)); } - strHTML = strHTML + " " + tr("Amount") + "=" + BitcoinUnits::formatHtmlWithUnit(unit, wallet.getValue(prevout)); - strHTML = strHTML + " IsMine=" + (wallet.txoutIsMine(prevout) & ISMINE_SPENDABLE ? tr("true") : tr("false")) + "
  • "; - strHTML = strHTML + " IsWatchOnly=" + (wallet.txoutIsMine(prevout) & ISMINE_WATCH_ONLY ? tr("true") : tr("false")) + ""; - } else { - strHTML += "
  • "; - if (txin.IsMWEB()) { - CTxOutput prevout(txin.ToMWEB()); - CTxDestination address; - if (wallet.extractOutputDestination(prevout, address)) { - std::string name; - if (wallet.getAddress(address, &name, /* is_mine= */ nullptr, /* purpose= */ nullptr) && !name.empty()) - strHTML += GUIUtil::HtmlEscape(name) + " "; - strHTML += QString::fromStdString(EncodeDestination(address)); - } - strHTML = strHTML + " " + tr("Amount") + "=" + BitcoinUnits::formatHtmlWithUnit(unit, wallet.getValue(prevout)); - } - strHTML = strHTML + " IsMine=" + (wallet.txinIsMine(txin) & ISMINE_SPENDABLE ? tr("true") : tr("false")) + "
  • "; - strHTML = strHTML + " IsWatchOnly=" + (wallet.txinIsMine(txin) & ISMINE_WATCH_ONLY ? tr("true") : tr("false")) + ""; + strHTML = strHTML + " " + tr("Amount") + "=" + BitcoinUnits::formatHtmlWithUnit(unit, wallet.getValue(prevout)); } + + strHTML = strHTML + " IsMine=" + (wallet.txinIsMine(txin) & ISMINE_SPENDABLE ? tr("true") : tr("false")); + strHTML = strHTML + " IsWatchOnly=" + (wallet.txinIsMine(txin) & ISMINE_WATCH_ONLY ? tr("true") : tr("false")); + strHTML += ""; } strHTML += "
"; + + + strHTML += "
" + tr("Outputs") + ":"; + strHTML += "
    "; + + for (const interfaces::WalletTxOut& txout : wtx.outputs) { + strHTML += "
  • "; + + CTxDestination address; + if (txout.address.ExtractDestination(address)) { + std::string name; + if (wallet.getAddress(address, &name, /* is_mine= */ nullptr, /* purpose= */ nullptr) && !name.empty()) + strHTML += GUIUtil::HtmlEscape(name) + " "; + strHTML += QString::fromStdString(EncodeDestination(address)); + } + + strHTML = strHTML + " " + tr("Amount") + "=" + BitcoinUnits::formatHtmlWithUnit(unit, wallet.getValue(txout.txout)); + + strHTML = strHTML + " IsMine=" + (wallet.txoutIsMine(txout.txout) & ISMINE_SPENDABLE ? tr("true") : tr("false")); + strHTML = strHTML + " IsWatchOnly=" + (wallet.txoutIsMine(txout.txout) & ISMINE_WATCH_ONLY ? tr("true") : tr("false")); + strHTML += "
  • "; + } + + strHTML += "
"; + return strHTML; } \ No newline at end of file diff --git a/src/qt/transactiondesc.h b/src/qt/transactiondesc.h index 8d8e4e9a8..e8a87dfce 100644 --- a/src/qt/transactiondesc.h +++ b/src/qt/transactiondesc.h @@ -8,7 +8,7 @@ #include #include -class TransactionRecord; +class WalletTxRecord; namespace interfaces { class Node; @@ -25,17 +25,17 @@ class TransactionDesc: public QObject Q_OBJECT public: - static QString toHTML(interfaces::Node& node, interfaces::Wallet& wallet, TransactionRecord *rec, int unit); + static QString toHTML(interfaces::Node& node, interfaces::Wallet& wallet, WalletTxRecord* rec, int unit); private: TransactionDesc() {} static QString FormatTxStatus(const interfaces::WalletTx& wtx, const interfaces::WalletTxStatus& status, bool inMempool, int numBlocks); - static QString toHTML_Addresses(interfaces::Wallet& wallet, const interfaces::WalletTx& wtx, TransactionRecord* rec); + static QString toHTML_Addresses(interfaces::Wallet& wallet, const interfaces::WalletTx& wtx, WalletTxRecord* rec); static QString toHTML_Amounts(interfaces::Wallet& wallet, const interfaces::WalletTx& wtx, const interfaces::WalletTxStatus& status, int unit); static QString toHTML_OrderForm(const interfaces::WalletOrderForm& orderForm); - static QString toHTML_Debug(interfaces::Node& node, interfaces::Wallet& wallet, const interfaces::WalletTx& wtx, TransactionRecord* rec, int unit); + static QString toHTML_Debug(interfaces::Node& node, interfaces::Wallet& wallet, const interfaces::WalletTx& wtx, WalletTxRecord* rec, int unit); }; #endif // BITCOIN_QT_TRANSACTIONDESC_H diff --git a/src/qt/transactionfilterproxy.cpp b/src/qt/transactionfilterproxy.cpp index a631f497a..5eed744b9 100644 --- a/src/qt/transactionfilterproxy.cpp +++ b/src/qt/transactionfilterproxy.cpp @@ -5,7 +5,7 @@ #include #include -#include +#include #include @@ -32,7 +32,7 @@ bool TransactionFilterProxy::filterAcceptsRow(int sourceRow, const QModelIndex & QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); int status = index.data(TransactionTableModel::StatusRole).toInt(); - if (!showInactive && status == TransactionStatus::Conflicted) + if (!showInactive && status == WalletTxStatus::Conflicted) return false; int type = index.data(TransactionTableModel::TypeRole).toInt(); diff --git a/src/qt/transactionrecord.cpp b/src/qt/transactionrecord.cpp deleted file mode 100644 index b3ca110e8..000000000 --- a/src/qt/transactionrecord.cpp +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright (c) 2011-2019 The Bitcoin Core developers -// Distributed under the MIT software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -#include - -#include -#include -#include -#include - -#include - -#include - -/* Return positive answer if transaction should be shown in list. - */ -bool TransactionRecord::showTransaction() -{ - // There are currently no cases where we hide transactions, but - // we may want to use this in the future for things like RBF. - return true; -} - -/* - * Decompose CWallet transaction to model transaction records. - */ -QList TransactionRecord::decomposeTransaction(const interfaces::WalletTx& wtx) -{ - QList parts; - int64_t nTime = wtx.time; - CAmount nCredit = wtx.credit; - CAmount nDebit = wtx.debit; - CAmount nNet = nCredit - nDebit; - uint256 hash = wtx.wtx_hash; - std::map mapValue = wtx.value_map; - - if (nNet > 0 || wtx.is_coinbase || wtx.is_hogex) - { - // - // Credit - // - for(unsigned int i = 0; i < wtx.outputs.size(); i++) - { - const interfaces::WalletTxOut& txout = wtx.outputs[i]; - isminetype mine = wtx.txout_is_mine[i]; - if(mine) - { - TransactionRecord sub(hash, nTime); - sub.idx = i; // vout index - sub.credit = txout.nValue; - sub.involvesWatchAddress = mine & ISMINE_WATCH_ONLY; - if (wtx.txout_address_is_mine[i]) - { - // Received by Bitcoin Address - sub.type = TransactionRecord::RecvWithAddress; - sub.address = txout.address.Encode(); - } - else - { - // Received by IP connection (deprecated features), or a multisignature or other non-simple transaction - sub.type = TransactionRecord::RecvFromOther; - sub.address = mapValue["from"]; - } - if (wtx.is_coinbase) - { - // Generated - sub.type = TransactionRecord::Generated; - } - - parts.append(sub); - } - } - } - else - { - bool involvesWatchAddress = false; - isminetype fAllFromMe = ISMINE_SPENDABLE; - for (const isminetype mine : wtx.txin_is_mine) - { - if(mine & ISMINE_WATCH_ONLY) involvesWatchAddress = true; - if(fAllFromMe > mine) fAllFromMe = mine; - } - - isminetype fAllToMe = ISMINE_SPENDABLE; - for (const isminetype mine : wtx.txout_is_mine) - { - if(mine & ISMINE_WATCH_ONLY) involvesWatchAddress = true; - if(fAllToMe > mine) fAllToMe = mine; - } - - // MWEB: Check pegouts for fAllToMe - for (const isminetype mine : wtx.pegout_is_mine) { - if (mine & ISMINE_WATCH_ONLY) involvesWatchAddress = true; - if (fAllToMe > mine) fAllToMe = mine; - } - - if (fAllFromMe && fAllToMe) - { - // Payment to self - std::string address; - for (auto it = wtx.outputs.begin(); it != wtx.outputs.end(); ++it) { - if (!address.empty()) address += ", "; - address += it->address.Encode(); - } - - for (auto it = wtx.pegouts.begin(); it != wtx.pegouts.end(); ++it) { - if (!address.empty()) address += ", "; - address += DestinationAddr(it->GetScriptPubKey()).Encode(); - } - - CAmount nChange = wtx.change; - parts.append(TransactionRecord(hash, nTime, TransactionRecord::SendToSelf, address, -(nDebit - nChange), nCredit - nChange)); - parts.last().involvesWatchAddress = involvesWatchAddress; // maybe pass to TransactionRecord as constructor argument - } - else if (fAllFromMe) - { - // - // Debit - // - CAmount nTxFee = wtx.fee; - - for (unsigned int nOut = 0; nOut < wtx.outputs.size(); nOut++) - { - const interfaces::WalletTxOut& txout = wtx.outputs[nOut]; - TransactionRecord sub(hash, nTime); - sub.idx = nOut; - sub.involvesWatchAddress = involvesWatchAddress; - - if(wtx.txout_is_mine[nOut]) - { - // Ignore parts sent to self, as this is usually the change - // from a transaction sent back to our own address. - continue; - } - - if (!txout.address.IsEmpty()) - { - // Sent to Bitcoin Address - sub.type = TransactionRecord::SendToAddress; - sub.address = txout.address.Encode(); - } - else - { - // Sent to IP, or other non-address transaction like OP_EVAL - sub.type = TransactionRecord::SendToOther; - sub.address = mapValue["to"]; - } - - CAmount nValue = txout.nValue; - /* Add fee to first output */ - if (nTxFee > 0) - { - nValue += nTxFee; - nTxFee = 0; - } - sub.debit = -nValue; - - parts.append(sub); - } - - for (unsigned int nPegout = 0; nPegout < wtx.pegouts.size(); nPegout++) { - const PegOutCoin& pegout = wtx.pegouts[nPegout]; - TransactionRecord sub(hash, nTime); - sub.idx = wtx.outputs.size() + nPegout; - sub.involvesWatchAddress = involvesWatchAddress; - - if (wtx.pegout_is_mine[nPegout]) { - // Ignore parts sent to self, as this is usually the change - // from a transaction sent back to our own address. - continue; - } - - // Sent to Bitcoin Address - sub.type = TransactionRecord::SendToAddress; - sub.address = DestinationAddr(pegout.GetScriptPubKey()).Encode(); - - CAmount nValue = pegout.GetAmount(); - /* Add fee to first output */ - if (nTxFee > 0) { - nValue += nTxFee; - nTxFee = 0; - } - sub.debit = -nValue; - - parts.append(sub); - } - } - else - { - // - // Mixed debit transaction, can't break down payees - // - parts.append(TransactionRecord(hash, nTime, TransactionRecord::Other, "", nNet, 0)); - parts.last().involvesWatchAddress = involvesWatchAddress; - } - } - - return parts; -} - -void TransactionRecord::updateStatus(const interfaces::WalletTxStatus& wtx, const uint256& block_hash, int numBlocks, int64_t block_time) -{ - // Determine transaction status - - // Sort order, unrecorded transactions sort to the top - status.sortKey = strprintf("%010d-%01d-%010u-%03d", - wtx.block_height, - wtx.is_coinbase ? 1 : 0, - wtx.time_received, - idx); - status.countsForBalance = wtx.is_trusted && !(wtx.blocks_to_maturity > 0); - status.depth = wtx.depth_in_main_chain; - status.m_cur_block_hash = block_hash; - - if (wtx.is_in_main_chain) { - status.matures_in = wtx.blocks_to_maturity; - } - - const bool up_to_date = ((int64_t)QDateTime::currentMSecsSinceEpoch() / 1000 - block_time < MAX_BLOCK_TIME_GAP); - if (up_to_date && !wtx.is_final) { - if (wtx.lock_time < LOCKTIME_THRESHOLD) { - status.status = TransactionStatus::OpenUntilBlock; - status.open_for = wtx.lock_time - numBlocks; - } - else - { - status.status = TransactionStatus::OpenUntilDate; - status.open_for = wtx.lock_time; - } - } - // For generated transactions, determine maturity - else if(type == TransactionRecord::Generated) - { - if (wtx.blocks_to_maturity > 0) - { - status.status = TransactionStatus::Immature; - - if (!wtx.is_in_main_chain) - { - status.status = TransactionStatus::NotAccepted; - } - } - else - { - status.status = TransactionStatus::Confirmed; - } - } - else - { - if (status.depth < 0) - { - status.status = TransactionStatus::Conflicted; - } - else if (status.depth == 0) - { - status.status = TransactionStatus::Unconfirmed; - if (wtx.is_abandoned) - status.status = TransactionStatus::Abandoned; - } - else if (status.depth < RecommendedNumConfirmations) - { - status.status = TransactionStatus::Confirming; - } - else - { - status.status = TransactionStatus::Confirmed; - } - } - status.needsUpdate = false; -} - -bool TransactionRecord::statusUpdateNeeded(const uint256& block_hash) const -{ - assert(!block_hash.IsNull()); - return status.m_cur_block_hash != block_hash || status.needsUpdate; -} - -QString TransactionRecord::getTxHash() const -{ - return QString::fromStdString(hash.ToString()); -} - -int TransactionRecord::getOutputIndex() const -{ - return idx; -} diff --git a/src/qt/transactionrecord.h b/src/qt/transactionrecord.h deleted file mode 100644 index a78d849e3..000000000 --- a/src/qt/transactionrecord.h +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) 2011-2018 The Bitcoin Core developers -// Distributed under the MIT software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -#ifndef BITCOIN_QT_TRANSACTIONRECORD_H -#define BITCOIN_QT_TRANSACTIONRECORD_H - -#include -#include - -#include -#include - -namespace interfaces { -class Node; -class Wallet; -struct WalletTx; -struct WalletTxStatus; -} - -/** UI model for transaction status. The transaction status is the part of a transaction that will change over time. - */ -class TransactionStatus -{ -public: - TransactionStatus() : countsForBalance(false), sortKey(""), - matures_in(0), status(Unconfirmed), depth(0), open_for(0) - { } - - enum Status { - Confirmed, /**< Have 6 or more confirmations (normal tx) or fully mature (mined tx) **/ - /// Normal (sent/received) transactions - OpenUntilDate, /**< Transaction not yet final, waiting for date */ - OpenUntilBlock, /**< Transaction not yet final, waiting for block */ - Unconfirmed, /**< Not yet mined into a block **/ - Confirming, /**< Confirmed, but waiting for the recommended number of confirmations **/ - Conflicted, /**< Conflicts with other transaction or mempool **/ - Abandoned, /**< Abandoned from the wallet **/ - /// Generated (mined) transactions - Immature, /**< Mined but waiting for maturity */ - NotAccepted /**< Mined but not accepted */ - }; - - /// Transaction counts towards available balance - bool countsForBalance; - /// Sorting key based on status - std::string sortKey; - - /** @name Generated (mined) transactions - @{*/ - int matures_in; - /**@}*/ - - /** @name Reported status - @{*/ - Status status; - qint64 depth; - qint64 open_for; /**< Timestamp if status==OpenUntilDate, otherwise number - of additional blocks that need to be mined before - finalization */ - /**@}*/ - - /** Current block hash (to know whether cached status is still valid) */ - uint256 m_cur_block_hash{}; - - bool needsUpdate; -}; - -/** UI model for a transaction. A core transaction can be represented by multiple UI transactions if it has - multiple outputs. - */ -class TransactionRecord -{ -public: - enum Type - { - Other, - Generated, - SendToAddress, - SendToOther, - RecvWithAddress, - RecvFromOther, - SendToSelf, - }; - - /** Number of confirmation recommended for accepting a transaction */ - static const int RecommendedNumConfirmations = 6; - - TransactionRecord(): - hash(), time(0), type(Other), address(""), debit(0), credit(0), idx(0) - { - } - - TransactionRecord(uint256 _hash, qint64 _time): - hash(_hash), time(_time), type(Other), address(""), debit(0), - credit(0), idx(0) - { - } - - TransactionRecord(uint256 _hash, qint64 _time, - Type _type, const std::string &_address, - const CAmount& _debit, const CAmount& _credit): - hash(_hash), time(_time), type(_type), address(_address), debit(_debit), credit(_credit), - idx(0) - { - } - - /** Decompose CWallet transaction to model transaction records. - */ - static bool showTransaction(); - static QList decomposeTransaction(const interfaces::WalletTx& wtx); - - /** @name Immutable transaction attributes - @{*/ - uint256 hash; - qint64 time; - Type type; - std::string address; - CAmount debit; - CAmount credit; - /**@}*/ - - /** Subtransaction index, for sort key */ - int idx; - - /** Status: can change with block chain update */ - TransactionStatus status; - - /** Whether the transaction was sent/received with a watch-only address */ - bool involvesWatchAddress; - - /** Return the unique identifier for this transaction (part) */ - QString getTxHash() const; - - /** Return the output index of the subtransaction */ - int getOutputIndex() const; - - /** Update status from core wallet tx. - */ - void updateStatus(const interfaces::WalletTxStatus& wtx, const uint256& block_hash, int numBlocks, int64_t block_time); - - /** Return whether a status update is needed. - */ - bool statusUpdateNeeded(const uint256& block_hash) const; -}; - -#endif // BITCOIN_QT_TRANSACTIONRECORD_H diff --git a/src/qt/transactiontablemodel.cpp b/src/qt/transactiontablemodel.cpp index 3148089b5..ddd9332aa 100644 --- a/src/qt/transactiontablemodel.cpp +++ b/src/qt/transactiontablemodel.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include @@ -40,17 +39,17 @@ static int column_alignments[] = { // Comparison operator for sort/binary search of model tx list struct TxLessThan { - bool operator()(const TransactionRecord &a, const TransactionRecord &b) const + bool operator()(const WalletTxRecord& a, const WalletTxRecord& b) const { - return a.hash < b.hash; + return a.GetTxHash() < b.GetTxHash(); } - bool operator()(const TransactionRecord &a, const uint256 &b) const + bool operator()(const WalletTxRecord& a, const uint256& b) const { - return a.hash < b; + return a.GetTxHash() < b; } - bool operator()(const uint256 &a, const TransactionRecord &b) const + bool operator()(const uint256& a, const WalletTxRecord& b) const { - return a < b.hash; + return a < b.GetTxHash(); } }; @@ -93,7 +92,7 @@ public: * As it is in the same order as the CWallet, by definition * this is sorted by sha256. */ - QList cachedWallet; + QList cachedWallet; bool fQueueNotifications = false; std::vector< TransactionNotification > vQueueNotifications; @@ -109,9 +108,7 @@ public: cachedWallet.clear(); { for (const auto& wtx : wallet.getWalletTxs()) { - if (TransactionRecord::showTransaction()) { - cachedWallet.append(TransactionRecord::decomposeTransaction(wtx)); - } + cachedWallet.append(wtx); } } } @@ -126,9 +123,9 @@ public: qDebug() << "TransactionTablePriv::updateWallet: " + QString::fromStdString(hash.ToString()) + " " + QString::number(status); // Find bounds of this transaction in model - QList::iterator lower = std::lower_bound( + QList::iterator lower = std::lower_bound( cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan()); - QList::iterator upper = std::upper_bound( + QList::iterator upper = std::upper_bound( cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan()); int lowerIndex = (lower - cachedWallet.begin()); int upperIndex = (upper - cachedWallet.begin()); @@ -157,20 +154,18 @@ public: if(showTransaction) { // Find transaction in wallet - interfaces::WalletTx wtx = wallet.getWalletTx(hash); - if(!wtx.tx) + std::vector toInsert = wallet.getWalletTx(hash); + if (toInsert.empty()) { qWarning() << "TransactionTablePriv::updateWallet: Warning: Got CT_NEW, but transaction is not in wallet"; break; } // Added -- insert at the right position - QList toInsert = - TransactionRecord::decomposeTransaction(wtx); - if(!toInsert.isEmpty()) /* only if something to insert */ + if(!toInsert.empty()) /* only if something to insert */ { parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex+toInsert.size()-1); int insert_idx = lowerIndex; - for (const TransactionRecord &rec : toInsert) + for (const WalletTxRecord& rec : toInsert) { cachedWallet.insert(insert_idx, rec); insert_idx += 1; @@ -194,7 +189,7 @@ public: // Miscellaneous updates -- nothing to do, status update will take care of this, and is only computed for // visible transactions. for (int i = lowerIndex; i < upperIndex; i++) { - TransactionRecord *rec = &cachedWallet[i]; + WalletTxRecord *rec = &cachedWallet[i]; rec->status.needsUpdate = true; } break; @@ -206,33 +201,30 @@ public: return cachedWallet.size(); } - TransactionRecord* index(interfaces::Wallet& wallet, const uint256& cur_block_hash, const int idx) + WalletTxRecord* index(interfaces::Wallet& wallet, const uint256& cur_block_hash, const int idx) { if (idx >= 0 && idx < cachedWallet.size()) { - TransactionRecord *rec = &cachedWallet[idx]; + WalletTxRecord *rec = &cachedWallet[idx]; // If a status update is needed (blocks came in since last check), // try to update the status of this transaction from the wallet. // Otherwise, simply re-use the cached status. - interfaces::WalletTxStatus wtx; - int numBlocks; - int64_t block_time; - if (!cur_block_hash.IsNull() && rec->statusUpdateNeeded(cur_block_hash) && wallet.tryGetTxStatus(rec->hash, wtx, numBlocks, block_time)) { - rec->updateStatus(wtx, cur_block_hash, numBlocks, block_time); + if (!cur_block_hash.IsNull()) { + rec->UpdateStatusIfNeeded(cur_block_hash); } return rec; } return nullptr; } - QString describe(interfaces::Node& node, interfaces::Wallet& wallet, TransactionRecord *rec, int unit) + QString describe(interfaces::Node& node, interfaces::Wallet& wallet, WalletTxRecord* rec, int unit) { return TransactionDesc::toHTML(node, wallet, rec, unit); } - QString getTxHex(interfaces::Wallet& wallet, TransactionRecord *rec) + QString getTxHex(interfaces::Wallet& wallet, WalletTxRecord *rec) { - auto tx = wallet.getTx(rec->hash); + auto tx = wallet.getTx(rec->GetTxHash()); if (tx) { std::string strHex = EncodeHexTx(*tx); return QString::fromStdString(strHex); @@ -299,37 +291,37 @@ int TransactionTableModel::columnCount(const QModelIndex &parent) const return columns.length(); } -QString TransactionTableModel::formatTxStatus(const TransactionRecord *wtx) const +QString TransactionTableModel::formatTxStatus(const WalletTxRecord* wtx) const { QString status; switch(wtx->status.status) { - case TransactionStatus::OpenUntilBlock: + case WalletTxStatus::OpenUntilBlock: status = tr("Open for %n more block(s)","",wtx->status.open_for); break; - case TransactionStatus::OpenUntilDate: + case WalletTxStatus::OpenUntilDate: status = tr("Open until %1").arg(GUIUtil::dateTimeStr(wtx->status.open_for)); break; - case TransactionStatus::Unconfirmed: + case WalletTxStatus::Unconfirmed: status = tr("Unconfirmed"); break; - case TransactionStatus::Abandoned: + case WalletTxStatus::Abandoned: status = tr("Abandoned"); break; - case TransactionStatus::Confirming: - status = tr("Confirming (%1 of %2 recommended confirmations)").arg(wtx->status.depth).arg(TransactionRecord::RecommendedNumConfirmations); + case WalletTxStatus::Confirming: + status = tr("Confirming (%1 of %2 recommended confirmations)").arg(wtx->status.depth).arg(WalletTxRecord::RecommendedNumConfirmations); break; - case TransactionStatus::Confirmed: + case WalletTxStatus::Confirmed: status = tr("Confirmed (%1 confirmations)").arg(wtx->status.depth); break; - case TransactionStatus::Conflicted: + case WalletTxStatus::Conflicted: status = tr("Conflicted"); break; - case TransactionStatus::Immature: + case WalletTxStatus::Immature: status = tr("Immature (%1 confirmations, will be available after %2)").arg(wtx->status.depth).arg(wtx->status.depth + wtx->status.matures_in); break; - case TransactionStatus::NotAccepted: + case WalletTxStatus::NotAccepted: status = tr("Generated but not accepted"); break; } @@ -337,11 +329,11 @@ QString TransactionTableModel::formatTxStatus(const TransactionRecord *wtx) cons return status; } -QString TransactionTableModel::formatTxDate(const TransactionRecord *wtx) const +QString TransactionTableModel::formatTxDate(const WalletTxRecord *wtx) const { - if(wtx->time) + if(wtx->GetTxTime()) { - return GUIUtil::dateTimeStr(wtx->time); + return GUIUtil::dateTimeStr(wtx->GetTxTime()); } return QString(); } @@ -364,44 +356,44 @@ QString TransactionTableModel::lookupAddress(const std::string &address, bool to return description; } -QString TransactionTableModel::formatTxType(const TransactionRecord *wtx) const +QString TransactionTableModel::formatTxType(const WalletTxRecord *wtx) const { switch(wtx->type) { - case TransactionRecord::RecvWithAddress: + case WalletTxRecord::RecvWithAddress: return tr("Received with"); - case TransactionRecord::RecvFromOther: + case WalletTxRecord::RecvFromOther: return tr("Received from"); - case TransactionRecord::SendToAddress: - case TransactionRecord::SendToOther: + case WalletTxRecord::SendToAddress: + case WalletTxRecord::SendToOther: return tr("Sent to"); - case TransactionRecord::SendToSelf: + case WalletTxRecord::SendToSelf: return tr("Payment to yourself"); - case TransactionRecord::Generated: + case WalletTxRecord::Generated: return tr("Mined"); default: return QString(); } } -QVariant TransactionTableModel::txAddressDecoration(const TransactionRecord *wtx) const +QVariant TransactionTableModel::txAddressDecoration(const WalletTxRecord *wtx) const { switch(wtx->type) { - case TransactionRecord::Generated: + case WalletTxRecord::Generated: return QIcon(":/icons/tx_mined"); - case TransactionRecord::RecvWithAddress: - case TransactionRecord::RecvFromOther: + case WalletTxRecord::RecvWithAddress: + case WalletTxRecord::RecvFromOther: return QIcon(":/icons/tx_input"); - case TransactionRecord::SendToAddress: - case TransactionRecord::SendToOther: + case WalletTxRecord::SendToAddress: + case WalletTxRecord::SendToOther: return QIcon(":/icons/tx_output"); default: return QIcon(":/icons/tx_inout"); } } -QString TransactionTableModel::formatTxToAddress(const TransactionRecord *wtx, bool tooltip) const +QString TransactionTableModel::formatTxToAddress(const WalletTxRecord *wtx, bool tooltip) const { QString watchAddress; if (tooltip) { @@ -411,35 +403,35 @@ QString TransactionTableModel::formatTxToAddress(const TransactionRecord *wtx, b switch(wtx->type) { - case TransactionRecord::RecvFromOther: + case WalletTxRecord::RecvFromOther: return QString::fromStdString(wtx->address) + watchAddress; - case TransactionRecord::RecvWithAddress: - case TransactionRecord::SendToAddress: - case TransactionRecord::Generated: + case WalletTxRecord::RecvWithAddress: + case WalletTxRecord::SendToAddress: + case WalletTxRecord::Generated: return lookupAddress(wtx->address, tooltip) + watchAddress; - case TransactionRecord::SendToOther: + case WalletTxRecord::SendToOther: return QString::fromStdString(wtx->address) + watchAddress; - case TransactionRecord::SendToSelf: + case WalletTxRecord::SendToSelf: return lookupAddress(wtx->address, tooltip) + watchAddress; default: return tr("(n/a)") + watchAddress; } } -QVariant TransactionTableModel::addressColor(const TransactionRecord *wtx) const +QVariant TransactionTableModel::addressColor(const WalletTxRecord *wtx) const { // Show addresses without label in a less visible color switch(wtx->type) { - case TransactionRecord::RecvWithAddress: - case TransactionRecord::SendToAddress: - case TransactionRecord::Generated: + case WalletTxRecord::RecvWithAddress: + case WalletTxRecord::SendToAddress: + case WalletTxRecord::Generated: { QString label = walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(wtx->address)); if(label.isEmpty()) return COLOR_BAREADDRESS; } break; - case TransactionRecord::SendToSelf: + case WalletTxRecord::SendToSelf: return COLOR_BAREADDRESS; default: break; @@ -447,9 +439,9 @@ QVariant TransactionTableModel::addressColor(const TransactionRecord *wtx) const return QVariant(); } -QString TransactionTableModel::formatTxAmount(const TransactionRecord *wtx, bool showUnconfirmed, BitcoinUnits::SeparatorStyle separators) const +QString TransactionTableModel::formatTxAmount(const WalletTxRecord *wtx, bool showUnconfirmed, BitcoinUnits::SeparatorStyle separators) const { - QString str = BitcoinUnits::format(walletModel->getOptionsModel()->getDisplayUnit(), wtx->credit + wtx->debit, false, separators); + QString str = BitcoinUnits::format(walletModel->getOptionsModel()->getDisplayUnit(), wtx->GetNet(), false, separators); if(showUnconfirmed) { if(!wtx->status.countsForBalance) @@ -460,18 +452,18 @@ QString TransactionTableModel::formatTxAmount(const TransactionRecord *wtx, bool return QString(str); } -QVariant TransactionTableModel::txStatusDecoration(const TransactionRecord *wtx) const +QVariant TransactionTableModel::txStatusDecoration(const WalletTxRecord *wtx) const { switch(wtx->status.status) { - case TransactionStatus::OpenUntilBlock: - case TransactionStatus::OpenUntilDate: + case WalletTxStatus::OpenUntilBlock: + case WalletTxStatus::OpenUntilDate: return COLOR_TX_STATUS_OPENUNTILDATE; - case TransactionStatus::Unconfirmed: + case WalletTxStatus::Unconfirmed: return QIcon(":/icons/transaction_0"); - case TransactionStatus::Abandoned: + case WalletTxStatus::Abandoned: return QIcon(":/icons/transaction_abandoned"); - case TransactionStatus::Confirming: + case WalletTxStatus::Confirming: switch(wtx->status.depth) { case 1: return QIcon(":/icons/transaction_1"); @@ -480,23 +472,23 @@ QVariant TransactionTableModel::txStatusDecoration(const TransactionRecord *wtx) case 4: return QIcon(":/icons/transaction_4"); default: return QIcon(":/icons/transaction_5"); }; - case TransactionStatus::Confirmed: + case WalletTxStatus::Confirmed: return QIcon(":/icons/transaction_confirmed"); - case TransactionStatus::Conflicted: + case WalletTxStatus::Conflicted: return QIcon(":/icons/transaction_conflicted"); - case TransactionStatus::Immature: { + case WalletTxStatus::Immature: { int total = wtx->status.depth + wtx->status.matures_in; int part = (wtx->status.depth * 4 / total) + 1; return QIcon(QString(":/icons/transaction_%1").arg(part)); } - case TransactionStatus::NotAccepted: + case WalletTxStatus::NotAccepted: return QIcon(":/icons/transaction_0"); default: return COLOR_BLACK; } } -QVariant TransactionTableModel::txWatchonlyDecoration(const TransactionRecord *wtx) const +QVariant TransactionTableModel::txWatchonlyDecoration(const WalletTxRecord *wtx) const { if (wtx->involvesWatchAddress) return QIcon(":/icons/eye"); @@ -504,11 +496,11 @@ QVariant TransactionTableModel::txWatchonlyDecoration(const TransactionRecord *w return QVariant(); } -QString TransactionTableModel::formatTooltip(const TransactionRecord *rec) const +QString TransactionTableModel::formatTooltip(const WalletTxRecord *rec) const { QString tooltip = formatTxStatus(rec) + QString("\n") + formatTxType(rec); - if(rec->type==TransactionRecord::RecvFromOther || rec->type==TransactionRecord::SendToOther || - rec->type==TransactionRecord::SendToAddress || rec->type==TransactionRecord::RecvWithAddress) + if(rec->type==WalletTxRecord::RecvFromOther || rec->type==WalletTxRecord::SendToOther || + rec->type==WalletTxRecord::SendToAddress || rec->type==WalletTxRecord::RecvWithAddress) { tooltip += QString(" ") + formatTxToAddress(rec, true); } @@ -519,7 +511,7 @@ QVariant TransactionTableModel::data(const QModelIndex &index, int role) const { if(!index.isValid()) return QVariant(); - TransactionRecord *rec = static_cast(index.internalPointer()); + WalletTxRecord *rec = static_cast(index.internalPointer()); switch(role) { @@ -559,7 +551,7 @@ QVariant TransactionTableModel::data(const QModelIndex &index, int role) const case Status: return QString::fromStdString(rec->status.sortKey); case Date: - return rec->time; + return qint64(rec->GetTxTime()); case Type: return formatTxType(rec); case Watchonly: @@ -567,7 +559,7 @@ QVariant TransactionTableModel::data(const QModelIndex &index, int role) const case ToAddress: return formatTxToAddress(rec, true); case Amount: - return qint64(rec->credit + rec->debit); + return qint64(rec->GetNet()); } break; case Qt::ToolTipRole: @@ -576,16 +568,16 @@ QVariant TransactionTableModel::data(const QModelIndex &index, int role) const return column_alignments[index.column()]; case Qt::ForegroundRole: // Use the "danger" color for abandoned transactions - if(rec->status.status == TransactionStatus::Abandoned) + if(rec->status.status == WalletTxStatus::Abandoned) { return COLOR_TX_STATUS_DANGER; } // Non-confirmed (but not immature) as transactions are grey - if(!rec->status.countsForBalance && rec->status.status != TransactionStatus::Immature) + if(!rec->status.countsForBalance && rec->status.status != WalletTxStatus::Immature) { return COLOR_UNCONFIRMED; } - if(index.column() == Amount && (rec->credit+rec->debit) < 0) + if (index.column() == Amount && rec->GetNet() < 0) { return COLOR_NEGATIVE; } @@ -597,7 +589,7 @@ QVariant TransactionTableModel::data(const QModelIndex &index, int role) const case TypeRole: return rec->type; case DateRole: - return QDateTime::fromTime_t(static_cast(rec->time)); + return QDateTime::fromTime_t(static_cast(rec->GetTxTime())); case WatchonlyRole: return rec->involvesWatchAddress; case WatchonlyDecorationRole: @@ -609,15 +601,15 @@ QVariant TransactionTableModel::data(const QModelIndex &index, int role) const case LabelRole: return walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(rec->address)); case AmountRole: - return qint64(rec->credit + rec->debit); + return qint64(rec->GetNet()); case TxHashRole: - return rec->getTxHash(); + return QString::fromStdString(rec->GetTxHash().ToString()); case TxHexRole: return priv->getTxHex(walletModel->wallet(), rec); case TxPlainTextRole: { QString details; - QDateTime date = QDateTime::fromTime_t(static_cast(rec->time)); + QDateTime date = QDateTime::fromTime_t(static_cast(rec->GetTxTime())); QString txLabel = walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(rec->address)); details.append(date.toString("M/d/yy HH:mm")); @@ -643,7 +635,7 @@ QVariant TransactionTableModel::data(const QModelIndex &index, int role) const return details; } case ConfirmedRole: - return rec->status.status == TransactionStatus::Status::Confirming || rec->status.status == TransactionStatus::Status::Confirmed; + return rec->status.status == WalletTxStatus::Status::Confirming || rec->status.status == WalletTxStatus::Status::Confirmed; case FormattedAmountRole: // Used for copy/export, so don't include separators return formatTxAmount(rec, false, BitcoinUnits::SeparatorStyle::NEVER); @@ -689,7 +681,7 @@ QVariant TransactionTableModel::headerData(int section, Qt::Orientation orientat QModelIndex TransactionTableModel::index(int row, int column, const QModelIndex &parent) const { Q_UNUSED(parent); - TransactionRecord* data = priv->index(walletModel->wallet(), walletModel->getLastBlockProcessed(), row); + WalletTxRecord* data = priv->index(walletModel->wallet(), walletModel->getLastBlockProcessed(), row); if(data) { return createIndex(row, column, data); @@ -708,9 +700,7 @@ void TransactionTablePriv::NotifyTransactionChanged(const uint256 &hash, ChangeT { // Find transaction in wallet // Determine whether to show transaction or not (determine this here so that no relocking is needed in GUI thread) - bool showTransaction = TransactionRecord::showTransaction(); - - TransactionNotification notification(hash, status, showTransaction); + TransactionNotification notification(hash, status, true); if (fQueueNotifications) { diff --git a/src/qt/transactiontablemodel.h b/src/qt/transactiontablemodel.h index 4b699d4d7..7f75a3af1 100644 --- a/src/qt/transactiontablemodel.h +++ b/src/qt/transactiontablemodel.h @@ -17,7 +17,7 @@ class Handler; } class PlatformStyle; -class TransactionRecord; +class WalletTxRecord; class TransactionTablePriv; class WalletModel; @@ -70,7 +70,7 @@ public: ConfirmedRole, /** Formatted amount, without brackets when unconfirmed */ FormattedAmountRole, - /** Transaction status (TransactionRecord::Status) */ + /** Transaction status (WalletTxRecord::Status) */ StatusRole, /** Unprocessed icon */ RawDecorationRole, @@ -96,16 +96,16 @@ private: void unsubscribeFromCoreSignals(); QString lookupAddress(const std::string &address, bool tooltip) const; - QVariant addressColor(const TransactionRecord *wtx) const; - QString formatTxStatus(const TransactionRecord *wtx) const; - QString formatTxDate(const TransactionRecord *wtx) const; - QString formatTxType(const TransactionRecord *wtx) const; - QString formatTxToAddress(const TransactionRecord *wtx, bool tooltip) const; - QString formatTxAmount(const TransactionRecord *wtx, bool showUnconfirmed=true, BitcoinUnits::SeparatorStyle separators=BitcoinUnits::SeparatorStyle::STANDARD) const; - QString formatTooltip(const TransactionRecord *rec) const; - QVariant txStatusDecoration(const TransactionRecord *wtx) const; - QVariant txWatchonlyDecoration(const TransactionRecord *wtx) const; - QVariant txAddressDecoration(const TransactionRecord *wtx) const; + QVariant addressColor(const WalletTxRecord *wtx) const; + QString formatTxStatus(const WalletTxRecord *wtx) const; + QString formatTxDate(const WalletTxRecord *wtx) const; + QString formatTxType(const WalletTxRecord *wtx) const; + QString formatTxToAddress(const WalletTxRecord *wtx, bool tooltip) const; + QString formatTxAmount(const WalletTxRecord *wtx, bool showUnconfirmed=true, BitcoinUnits::SeparatorStyle separators=BitcoinUnits::SeparatorStyle::STANDARD) const; + QString formatTooltip(const WalletTxRecord *rec) const; + QVariant txStatusDecoration(const WalletTxRecord *wtx) const; + QVariant txWatchonlyDecoration(const WalletTxRecord *wtx) const; + QVariant txAddressDecoration(const WalletTxRecord *wtx) const; public Q_SLOTS: /* New transaction, or transaction changed status */ diff --git a/src/qt/transactionview.cpp b/src/qt/transactionview.cpp index 54ecfc38e..a79e14f0a 100644 --- a/src/qt/transactionview.cpp +++ b/src/qt/transactionview.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include @@ -83,13 +82,13 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa } typeWidget->addItem(tr("All"), TransactionFilterProxy::ALL_TYPES); - typeWidget->addItem(tr("Received with"), TransactionFilterProxy::TYPE(TransactionRecord::RecvWithAddress) | - TransactionFilterProxy::TYPE(TransactionRecord::RecvFromOther)); - typeWidget->addItem(tr("Sent to"), TransactionFilterProxy::TYPE(TransactionRecord::SendToAddress) | - TransactionFilterProxy::TYPE(TransactionRecord::SendToOther)); - typeWidget->addItem(tr("To yourself"), TransactionFilterProxy::TYPE(TransactionRecord::SendToSelf)); - typeWidget->addItem(tr("Mined"), TransactionFilterProxy::TYPE(TransactionRecord::Generated)); - typeWidget->addItem(tr("Other"), TransactionFilterProxy::TYPE(TransactionRecord::Other)); + typeWidget->addItem(tr("Received with"), TransactionFilterProxy::TYPE(WalletTxRecord::RecvWithAddress) | + TransactionFilterProxy::TYPE(WalletTxRecord::RecvFromOther)); + typeWidget->addItem(tr("Sent to"), TransactionFilterProxy::TYPE(WalletTxRecord::SendToAddress) | + TransactionFilterProxy::TYPE(WalletTxRecord::SendToOther)); + typeWidget->addItem(tr("To yourself"), TransactionFilterProxy::TYPE(WalletTxRecord::SendToSelf)); + typeWidget->addItem(tr("Mined"), TransactionFilterProxy::TYPE(WalletTxRecord::Generated)); + typeWidget->addItem(tr("Other"), TransactionFilterProxy::TYPE(WalletTxRecord::Other)); hlayout->addWidget(typeWidget); diff --git a/src/qt/walletmodeltransaction.cpp b/src/qt/walletmodeltransaction.cpp index 7de1e7083..3608b84cd 100644 --- a/src/qt/walletmodeltransaction.cpp +++ b/src/qt/walletmodeltransaction.cpp @@ -45,6 +45,15 @@ void WalletModelTransaction::reassignAmounts(interfaces::Wallet& wallet, int nCh { std::vector outputs = wtx->GetOutputs(); + if (nChangePosRet == -1) { + for (size_t i = 0; i < outputs.size(); i++) { + if (wallet.isChange(outputs[i])) { + nChangePosRet = i; + break; + } + } + } + int i = 0; for (QList::iterator it = recipients.begin(); it != recipients.end(); ++it) { diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 41015685e..c78113c9c 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -726,14 +726,9 @@ static RPCHelpMan getblocktemplate() // TODO: Maybe recheck connections/IBD and (if something wrong) send an expires-immediately template to stop miners? } - // GBT must be called with 'segwit' set in the rules - if (setClientRules.count("segwit") != 1) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "getblocktemplate must be called with the segwit rule set (call with {\"rules\": [\"segwit\"]})"); - } - - // GBT must be called with 'mweb' set in the rules - if (setClientRules.count("mweb") != 1) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "getblocktemplate must be called with the segwit rule set (call with {\"rules\": [\"mweb\"]})"); + // GBT must be called with 'segwit' and 'mweb' sets in the rules + if (setClientRules.count("segwit") != 1 || setClientRules.count("mweb") != 1) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "getblocktemplate must be called with the segwit & mweb rule sets (call with {\"rules\": [\"mweb\", \"segwit\"]})"); } // Update block diff --git a/src/test/validation_tests.cpp b/src/test/validation_tests.cpp index dd419f3b4..1498e8bb0 100644 --- a/src/test/validation_tests.cpp +++ b/src/test/validation_tests.cpp @@ -58,65 +58,4 @@ BOOST_AUTO_TEST_CASE(subsidy_limit_test) BOOST_CHECK_EQUAL(nSum, CAmount{8399999990760000}); } -//BOOST_AUTO_TEST_CASE(signet_parse_tests) -//{ -// ArgsManager signet_argsman; -// signet_argsman.ForceSetArg("-signetchallenge", "51"); // set challenge to OP_TRUE -// const auto signet_params = CreateChainParams(signet_argsman, CBaseChainParams::SIGNET); -// CBlock block; -// BOOST_CHECK(signet_params->GetConsensus().signet_challenge == std::vector{OP_TRUE}); -// CScript challenge{OP_TRUE}; -// -// // empty block is invalid -// BOOST_CHECK(!SignetTxs::Create(block, challenge)); -// BOOST_CHECK(!CheckSignetBlockSolution(block, signet_params->GetConsensus())); -// -// // no witness commitment -// CMutableTransaction cb; -// cb.vout.emplace_back(0, CScript{}); -// block.vtx.push_back(MakeTransactionRef(cb)); -// block.vtx.push_back(MakeTransactionRef(cb)); // Add dummy tx to excercise merkle root code -// BOOST_CHECK(!SignetTxs::Create(block, challenge)); -// BOOST_CHECK(!CheckSignetBlockSolution(block, signet_params->GetConsensus())); -// -// // no header is treated valid -// std::vector witness_commitment_section_141{0xaa, 0x21, 0xa9, 0xed}; -// for (int i = 0; i < 32; ++i) { -// witness_commitment_section_141.push_back(0xff); -// } -// cb.vout.at(0).scriptPubKey = CScript{} << OP_RETURN << witness_commitment_section_141; -// block.vtx.at(0) = MakeTransactionRef(cb); -// BOOST_CHECK(SignetTxs::Create(block, challenge)); -// BOOST_CHECK(CheckSignetBlockSolution(block, signet_params->GetConsensus())); -// -// // no data after header, valid -// std::vector witness_commitment_section_325{0xec, 0xc7, 0xda, 0xa2}; -// cb.vout.at(0).scriptPubKey = CScript{} << OP_RETURN << witness_commitment_section_141 << witness_commitment_section_325; -// block.vtx.at(0) = MakeTransactionRef(cb); -// BOOST_CHECK(SignetTxs::Create(block, challenge)); -// BOOST_CHECK(CheckSignetBlockSolution(block, signet_params->GetConsensus())); -// -// // Premature end of data, invalid -// witness_commitment_section_325.push_back(0x01); -// witness_commitment_section_325.push_back(0x51); -// cb.vout.at(0).scriptPubKey = CScript{} << OP_RETURN << witness_commitment_section_141 << witness_commitment_section_325; -// block.vtx.at(0) = MakeTransactionRef(cb); -// BOOST_CHECK(!SignetTxs::Create(block, challenge)); -// BOOST_CHECK(!CheckSignetBlockSolution(block, signet_params->GetConsensus())); -// -// // has data, valid -// witness_commitment_section_325.push_back(0x00); -// cb.vout.at(0).scriptPubKey = CScript{} << OP_RETURN << witness_commitment_section_141 << witness_commitment_section_325; -// block.vtx.at(0) = MakeTransactionRef(cb); -// BOOST_CHECK(SignetTxs::Create(block, challenge)); -// BOOST_CHECK(CheckSignetBlockSolution(block, signet_params->GetConsensus())); -// -// // Extraneous data, invalid -// witness_commitment_section_325.push_back(0x00); -// cb.vout.at(0).scriptPubKey = CScript{} << OP_RETURN << witness_commitment_section_141 << witness_commitment_section_325; -// block.vtx.at(0) = MakeTransactionRef(cb); -// BOOST_CHECK(!SignetTxs::Create(block, challenge)); -// BOOST_CHECK(!CheckSignetBlockSolution(block, signet_params->GetConsensus())); -//} - BOOST_AUTO_TEST_SUITE_END() diff --git a/src/wallet/createtransaction.cpp b/src/wallet/createtransaction.cpp deleted file mode 100644 index 660465fa0..000000000 --- a/src/wallet/createtransaction.cpp +++ /dev/null @@ -1,705 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -static OutputType TransactionChangeType(const CWallet& wallet, const Optional& change_type, const std::vector& vecSend) -{ - // If -changetype is specified, always use that change type. - if (change_type) { - return *change_type; - } - - // if m_default_address_type is legacy, use legacy address as change (even - // if some of the outputs are P2WPKH or P2WSH). - if (wallet.m_default_address_type == OutputType::LEGACY) { - return OutputType::LEGACY; - } - - // if any destination is P2WPKH or P2WSH, use P2WPKH for the change - // output. - for (const auto& recipient : vecSend) { - // Check if any destination contains a witness program: - if (!recipient.IsMWEB()) { - int witnessversion = 0; - std::vector witnessprogram; - if (recipient.GetScript().IsWitnessProgram(witnessversion, witnessprogram)) { - return OutputType::BECH32; - } - } - } - - // else use m_default_address_type for change - return wallet.m_default_address_type; -} - -static bool IsCurrentForAntiFeeSniping(interfaces::Chain& chain, const uint256& block_hash) -{ - if (chain.isInitialBlockDownload()) { - return false; - } - constexpr int64_t MAX_ANTI_FEE_SNIPING_TIP_AGE = 8 * 60 * 60; // in seconds - int64_t block_time; - CHECK_NONFATAL(chain.findBlock(block_hash, interfaces::FoundBlock().time(block_time))); - if (block_time < (GetTime() - MAX_ANTI_FEE_SNIPING_TIP_AGE)) { - return false; - } - return true; -} -/** - * Return a height-based locktime for new transactions (uses the height of the - * current chain tip unless we are not synced with the current chain - */ -static uint32_t GetLocktimeForNewTransaction(interfaces::Chain& chain, const uint256& block_hash, int block_height) -{ - uint32_t locktime; - // Discourage fee sniping. - // - // For a large miner the value of the transactions in the best block and - // the mempool can exceed the cost of deliberately attempting to mine two - // blocks to orphan the current best block. By setting nLockTime such that - // only the next block can include the transaction, we discourage this - // practice as the height restricted and limited blocksize gives miners - // considering fee sniping fewer options for pulling off this attack. - // - // A simple way to think about this is from the wallet's point of view we - // always want the blockchain to move forward. By setting nLockTime this - // way we're basically making the statement that we only want this - // transaction to appear in the next block; we don't want to potentially - // encourage reorgs by allowing transactions to appear at lower heights - // than the next block in forks of the best chain. - // - // Of course, the subsidy is high enough, and transaction volume low - // enough, that fee sniping isn't a problem yet, but by implementing a fix - // now we ensure code won't be written that makes assumptions about - // nLockTime that preclude a fix later. - if (IsCurrentForAntiFeeSniping(chain, block_hash)) { - locktime = block_height; - - // Secondly occasionally randomly pick a nLockTime even further back, so - // that transactions that are delayed after signing for whatever reason, - // e.g. high-latency mix networks and some CoinJoin implementations, have - // better privacy. - if (GetRandInt(10) == 0) - locktime = std::max(0, (int)locktime - GetRandInt(100)); - } else { - // If our chain is lagging behind, we can't discourage fee sniping nor help - // the privacy of high-latency transactions. To avoid leaking a potentially - // unique "nLockTime fingerprint", set nLockTime to a constant. - locktime = 0; - } - assert(locktime < LOCKTIME_THRESHOLD); - return locktime; -} - -bool VerifyRecipients(const std::vector& vecSend, bilingual_str& error) -{ - if (vecSend.empty()) { - error = _("Transaction must have at least one recipient"); - return false; - } - - CAmount nValue = 0; - for (const auto& recipient : vecSend) { - if (recipient.IsMWEB() && vecSend.size() > 1) { - error = _("Only one MWEB recipient supported at this time"); - return false; - } - - if (nValue < 0 || recipient.nAmount < 0) { - error = _("Transaction amounts must not be negative"); - return false; - } - - nValue += recipient.nAmount; - } - - return true; -} - -bool AddRecipientOutputs( - CMutableTransaction& txNew, - const std::vector& vecSend, - CoinSelectionParams& coin_selection_params, - CAmount nFeeRet, - unsigned int nSubtractFeeFromAmount, - interfaces::Chain& chain, - bilingual_str& error) -{ - // vouts to the payees - if (!coin_selection_params.m_subtract_fee_outputs) { - coin_selection_params.tx_noinputs_size = 11; // Static vsize overhead + outputs vsize. 4 nVersion, 4 nLocktime, 1 input count, 1 output count, 1 witness overhead (dummy, flag, stack size) - } - - bool fFirst = true; - for (const auto& recipient : vecSend) { - if (recipient.IsMWEB()) { - continue; - } - - CTxOut txout(recipient.nAmount, recipient.GetScript()); - - if (recipient.fSubtractFeeFromAmount) { - assert(nSubtractFeeFromAmount != 0); - txout.nValue -= nFeeRet / nSubtractFeeFromAmount; // Subtract fee equally from each selected recipient - - if (fFirst) // first receiver pays the remainder not divisible by output count - { - fFirst = false; - txout.nValue -= nFeeRet % nSubtractFeeFromAmount; - } - } - // Include the fee cost for outputs. Note this is only used for BnB right now - coin_selection_params.tx_noinputs_size += ::GetSerializeSize(txout, PROTOCOL_VERSION); - - if (IsDust(txout, chain.relayDustFee())) { - if (recipient.fSubtractFeeFromAmount && nFeeRet > 0) { - if (txout.nValue < 0) - error = _("The transaction amount is too small to pay the fee"); - else - error = _("The transaction amount is too small to send after the fee has been deducted"); - } else - error = _("Transaction amount too small"); - return false; - } - txNew.vout.push_back(txout); - } - - return true; -} - -static bool IsChangeOnMWEB(const CWallet& wallet, const MWEB::TxType& mweb_type, const std::vector& vecSend, const CTxDestination& dest_change) -{ - if (mweb_type == MWEB::TxType::MWEB_TO_MWEB || mweb_type == MWEB::TxType::PEGOUT) { - return true; - } - - if (mweb_type == MWEB::TxType::PEGIN) { - // If you try pegging-in to only MWEB addresses belonging to others, - // you risk revealing the kernel blinding factor, allowing the receiver to malleate the kernel. - // To avoid this risk, include a change output so that kernel blind is not leaked. - bool pegin_change_required = true; - for (const CRecipient& recipient : vecSend) { - if (recipient.IsMWEB() && wallet.IsMine(recipient.receiver)) { - pegin_change_required = false; - break; - } - } - - return pegin_change_required - || dest_change.type() == typeid(CNoDestination) - || dest_change.type() == typeid(StealthAddress); - } - - return false; -} - -static bool ContainsPegIn(const MWEB::TxType& mweb_type, const std::set& setCoins) -{ - if (mweb_type == MWEB::TxType::PEGIN) { - return true; - } - - if (mweb_type == MWEB::TxType::PEGOUT) { - return std::any_of( - setCoins.cbegin(), setCoins.cend(), - [](const CInputCoin& coin) { return !coin.IsMWEB(); }); - } - - return false; -} - -static bool SelectCoinsEx( - CWallet& wallet, - const std::vector& vAvailableCoins, - const CAmount& nTargetValue, - std::set& setCoinsRet, - CAmount& nValueRet, - const CCoinControl& coin_control, - CoinSelectionParams& coin_selection_params, - bool& bnb_used) -{ - nValueRet = 0; - if (wallet.SelectCoins(vAvailableCoins, nTargetValue, setCoinsRet, nValueRet, coin_control, coin_selection_params, bnb_used)) { - return true; - } - - nValueRet = 0; - if (bnb_used) { - bnb_used = false; - coin_selection_params.use_bnb = false; - return wallet.SelectCoins(vAvailableCoins, nTargetValue, setCoinsRet, nValueRet, coin_control, coin_selection_params, bnb_used); - } - - return false; -} - -static bool AttemptCoinSelection( - CWallet& wallet, - const std::vector& vAvailableCoins, - const CAmount& nTargetValue, - std::set& setCoinsRet, - CAmount& nValueRet, - const CCoinControl& coin_control, - const std::vector& recipients, - CoinSelectionParams& coin_selection_params, - bool& bnb_used) -{ - static auto is_ltc = [](const CInputCoin& input) { return !input.IsMWEB(); }; - static auto is_mweb = [](const CInputCoin& input) { return input.IsMWEB(); }; - - if (recipients.front().IsMWEB()) { - // First try to construct an MWEB-to-MWEB transaction - CoinSelectionParams mweb_to_mweb = coin_selection_params; - mweb_to_mweb.input_preference = InputPreference::MWEB_ONLY; - mweb_to_mweb.mweb_change_output_weight = Weight::STANDARD_OUTPUT_WEIGHT; - mweb_to_mweb.mweb_nochange_weight = Weight::KERNEL_WITH_STEALTH_WEIGHT + (recipients.size() * Weight::STANDARD_OUTPUT_WEIGHT); - mweb_to_mweb.change_output_size = 0; - mweb_to_mweb.change_spend_size = 0; - mweb_to_mweb.tx_noinputs_size = 0; - bnb_used = false; - - if (SelectCoinsEx(wallet, vAvailableCoins, nTargetValue, setCoinsRet, nValueRet, coin_control, mweb_to_mweb, bnb_used)) { - return true; - } - - // If MWEB-to-MWEB fails, create a peg-in transaction - const bool change_on_mweb = IsChangeOnMWEB(wallet, MWEB::TxType::PEGIN, recipients, coin_control.destChange); - CoinSelectionParams params_pegin = coin_selection_params; - params_pegin.input_preference = InputPreference::ANY; - params_pegin.mweb_change_output_weight = change_on_mweb ? Weight::STANDARD_OUTPUT_WEIGHT : 0; - params_pegin.mweb_nochange_weight = Weight::KERNEL_WITH_STEALTH_WEIGHT + (recipients.size() * Weight::STANDARD_OUTPUT_WEIGHT); - params_pegin.change_output_size = change_on_mweb ? 0 : coin_selection_params.change_output_size; - params_pegin.change_spend_size = change_on_mweb ? 0 : coin_selection_params.change_spend_size; - bnb_used = false; - - if (SelectCoinsEx(wallet, vAvailableCoins, nTargetValue, setCoinsRet, nValueRet, coin_control, params_pegin, bnb_used)) { - return std::any_of(setCoinsRet.cbegin(), setCoinsRet.cend(), is_ltc); - } - } else { - // First try to construct a LTC-to-LTC transaction - CoinSelectionParams mweb_to_mweb = coin_selection_params; - mweb_to_mweb.input_preference = InputPreference::LTC_ONLY; - mweb_to_mweb.mweb_change_output_weight = 0; - mweb_to_mweb.mweb_nochange_weight = 0; - bnb_used = false; - - if (SelectCoinsEx(wallet, vAvailableCoins, nTargetValue, setCoinsRet, nValueRet, coin_control, mweb_to_mweb, bnb_used)) { - return true; - } - - // Only supports pegging-out to one address - if (recipients.size() > 1) { - return false; - } - - // If LTC-to-LTC fails, create a peg-out transaction - CoinSelectionParams params_pegout = coin_selection_params; - params_pegout.input_preference = InputPreference::ANY; - params_pegout.mweb_change_output_weight = Weight::STANDARD_OUTPUT_WEIGHT; - params_pegout.mweb_nochange_weight = Weight::CalcKernelWeight(true, recipients.front().GetScript()); - params_pegout.change_output_size = 0; - params_pegout.change_spend_size = 0; - bnb_used = false; - - if (SelectCoinsEx(wallet, vAvailableCoins, nTargetValue, setCoinsRet, nValueRet, coin_control, params_pegout, bnb_used)) { - return std::any_of(setCoinsRet.cbegin(), setCoinsRet.cend(), is_mweb); - } - } - - return false; -} - -bool CreateTransactionEx( - CWallet& wallet, - const std::vector& vecSend, - CTransactionRef& tx, - CAmount& nFeeRet, - int& nChangePosInOut, - bilingual_str& error, - const CCoinControl& coin_control, - FeeCalculation& fee_calc_out, - bool sign) -{ - if (!VerifyRecipients(vecSend, error)) { - return false; - } - - CAmount nValue = std::accumulate( - vecSend.cbegin(), vecSend.cend(), CAmount(0), - [](CAmount amt, const CRecipient& recipient) { return amt + recipient.nAmount; } - ); - - const OutputType change_type = TransactionChangeType(wallet, coin_control.m_change_type ? *coin_control.m_change_type : wallet.m_default_change_type, vecSend); - ReserveDestination reservedest(&wallet, change_type); - - unsigned int nSubtractFeeFromAmount = std::count_if(vecSend.cbegin(), vecSend.cend(), - [](const CRecipient& recipient) { return recipient.fSubtractFeeFromAmount; } - ); - - int nChangePosRequest = nChangePosInOut; - - CMutableTransaction txNew; - - FeeCalculation feeCalc; - CAmount nFeeNeeded; - - uint64_t mweb_weight = 0; - MWEB::TxType mweb_type = MWEB::TxType::LTC_TO_LTC; - bool change_on_mweb = false; - CAmount mweb_fee = 0; - - int nBytes; - { - std::set setCoins; - LOCK(wallet.cs_wallet); - txNew.nLockTime = GetLocktimeForNewTransaction(wallet.chain(), wallet.GetLastBlockHash(), wallet.GetLastBlockHeight()); - { - std::vector vAvailableCoins; - wallet.AvailableCoins(vAvailableCoins, true, &coin_control, 1, MAX_MONEY, MAX_MONEY, 0); - - // Create change script that will be used if we need change - // TODO: pass in scriptChange instead of reservekey so - // change transaction isn't always pay-to-bitcoin-address - DestinationAddr change_addr; - - // coin control: send change to custom address - if (!boost::get(&coin_control.destChange)) { - change_addr = DestinationAddr(coin_control.destChange); - } else { // no coin control: send change to newly generated address - // Note: We use a new key here to keep it from being obvious which side is the change. - // The drawback is that by not reusing a previous key, the change may be lost if a - // backup is restored, if the backup doesn't have the new private key for the change. - // If we reused the old key, it would be possible to add code to look for and - // rediscover unknown transactions that were written with keys of ours to recover - // post-backup change. - - - // Reserve a new key pair from key pool. If it fails, provide a dummy - // destination in case we don't need change. - CTxDestination dest; - if (!reservedest.GetReservedDestination(dest, true)) { - error = _("Transaction needs a change address, but we can't generate it. Please call keypoolrefill first."); - } - change_addr = GetScriptForDestination(dest); - // A valid destination implies a change script (and - // vice-versa). An empty change script will abort later, if the - // change keypool ran out, but change is required. - CHECK_NONFATAL(IsValidDestination(dest) != change_addr.IsEmpty()); - } - - CTxOut change_prototype_txout(0, change_addr.IsMWEB() ? CScript() : change_addr.GetScript()); - - CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy - coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout); - coin_selection_params.mweb_change_output_weight = Weight::STANDARD_OUTPUT_WEIGHT; - - // Set discard feerate - coin_selection_params.m_discard_feerate = GetDiscardRate(wallet); - - // Get the fee rate to use effective values in coin selection - coin_selection_params.m_effective_feerate = GetMinimumFeeRate(wallet, coin_control, &feeCalc); - // Do not, ever, assume that it's fine to change the fee rate if the user has explicitly - // provided one - if (coin_control.m_feerate && coin_selection_params.m_effective_feerate > *coin_control.m_feerate) { - error = strprintf(_("Fee rate (%s) is lower than the minimum fee rate setting (%s)"), coin_control.m_feerate->ToString(FeeEstimateMode::SAT_VB), coin_selection_params.m_effective_feerate.ToString(FeeEstimateMode::SAT_VB)); - return false; - } - if (feeCalc.reason == FeeReason::FALLBACK && !wallet.m_allow_fallback_fee) { - // eventually allow a fallback fee - error = _("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee."); - return false; - } - - // Get long term estimate - CCoinControl cc_temp; - cc_temp.m_confirm_target = wallet.chain().estimateMaxBlocks(); - coin_selection_params.m_long_term_feerate = GetMinimumFeeRate(wallet, cc_temp, nullptr); - - // BnB selector is the only selector used when this is true. - // That should only happen on the first pass through the loop. - coin_selection_params.use_bnb = true; - coin_selection_params.m_subtract_fee_outputs = nSubtractFeeFromAmount != 0; // If we are doing subtract fee from recipient, don't use effective values - - nFeeRet = 0; - bool pick_new_inputs = true; - CAmount nValueIn = 0; - - // Start with no fee and loop until there is enough fee - while (true) { - nChangePosInOut = nChangePosRequest; - txNew.vin.clear(); - txNew.vout.clear(); - mweb_weight = 0; - - CAmount nValueToSelect = nValue; - if (nSubtractFeeFromAmount == 0) - nValueToSelect += nFeeRet; - - if (!AddRecipientOutputs(txNew, vecSend, coin_selection_params, nFeeRet, nSubtractFeeFromAmount, wallet.chain(), error)) { - return false; - } - - // Choose coins to use - bool bnb_used = false; - if (pick_new_inputs) { - nValueIn = 0; - setCoins.clear(); - int change_spend_size = CalculateMaximumSignedInputSize(change_prototype_txout, &wallet); - // If the wallet doesn't know how to sign change output, assume p2sh-p2wpkh - // as lower-bound to allow BnB to do it's thing - if (change_spend_size == -1) { - coin_selection_params.change_spend_size = DUMMY_NESTED_P2WPKH_INPUT_SIZE; - } else { - coin_selection_params.change_spend_size = (size_t)change_spend_size; - } - if (!AttemptCoinSelection(wallet, vAvailableCoins, nValueToSelect, setCoins, nValueIn, coin_control, vecSend, coin_selection_params, bnb_used)) { - error = _("Insufficient funds"); - return false; - } - } - - mweb_type = MWEB::GetTxType(vecSend, std::vector(setCoins.begin(), setCoins.end())); - change_on_mweb = IsChangeOnMWEB(wallet, mweb_type, vecSend, coin_control.destChange); - - if (mweb_type != MWEB::TxType::LTC_TO_LTC) { - if (mweb_type == MWEB::TxType::PEGIN || mweb_type == MWEB::TxType::MWEB_TO_MWEB) { - mweb_weight += Weight::STANDARD_OUTPUT_WEIGHT; // MW: FUTURE - Look at actual recipients list, but for now we only support 1 MWEB recipient. - } - - if (change_on_mweb) { - mweb_weight += Weight::STANDARD_OUTPUT_WEIGHT; - } - - CScript pegout_script = (mweb_type == MWEB::TxType::PEGOUT) ? vecSend.front().GetScript() : CScript(); - mweb_weight += Weight::CalcKernelWeight(true, pegout_script); - - // We don't support multiple recipients for MWEB txs yet, - // so the only possible LTC outputs are pegins & change. - // Both of those are added after this, so clear outputs for now. - txNew.vout.clear(); - nChangePosInOut = -1; - } - - const CAmount nChange = nValueIn - nValueToSelect; - if (nChange > 0 && !change_on_mweb) { - // Fill a vout to ourself - CTxOut newTxOut(nChange, change_addr.IsMWEB() ? CScript() : change_addr.GetScript()); - - // Never create dust outputs; if we would, just - // add the dust to the fee. - // The nChange when BnB is used is always going to go to fees. - if (IsDust(newTxOut, coin_selection_params.m_discard_feerate) || bnb_used) { - nChangePosInOut = -1; - nFeeRet += nChange; - } else { - if (nChangePosInOut == -1) { - // Insert change txn at random position: - nChangePosInOut = GetRandInt(txNew.vout.size() + 1); - } else if ((unsigned int)nChangePosInOut > txNew.vout.size()) { - error = _("Change index out of range"); - return false; - } - - std::vector::iterator position = txNew.vout.begin() + nChangePosInOut; - txNew.vout.insert(position, newTxOut); - } - } else { - nChangePosInOut = -1; - } - - // Add Dummy peg-in script - if (ContainsPegIn(mweb_type, setCoins)) { - txNew.vout.push_back(CTxOut(MAX_MONEY, GetScriptForPegin(mw::Hash()))); - } - - // Dummy fill vin for maximum size estimation - // - for (const auto& coin : setCoins) { - if (!coin.IsMWEB()) { - txNew.vin.push_back(CTxIn(boost::get(coin.GetIndex()), CScript())); - } - } - - if (mweb_type != MWEB::TxType::MWEB_TO_MWEB) { - nBytes = CalculateMaximumSignedTxSize(CTransaction(txNew), &wallet, coin_control.fAllowWatchOnly); - } else { - nBytes = 0; - } - if (nBytes < 0) { - LogPrintf("Transaction failed to sign: %s", CTransaction(txNew).ToString().c_str()); - error = _("Signing transaction failed"); - return false; - } - - mweb_fee = GetMinimumFee(wallet, 0, mweb_weight, coin_control, &feeCalc); - if (mweb_type == MWEB::TxType::PEGOUT) { - CTxOut pegout_output(vecSend.front().nAmount, vecSend.front().receiver.GetScript()); - size_t pegout_weight = ::GetSerializeSize(pegout_output, PROTOCOL_VERSION) * WITNESS_SCALE_FACTOR; - size_t pegout_bytes = GetVirtualTransactionSize(pegout_weight, 0, 0); - - mweb_fee = GetMinimumFee(wallet, pegout_bytes, mweb_weight, coin_control, &feeCalc); - nBytes += pegout_bytes; - } - - nFeeNeeded = coin_selection_params.m_effective_feerate.GetTotalFee(nBytes, mweb_weight); - - if (nFeeRet >= nFeeNeeded) { - // Reduce fee to only the needed amount if possible. This - // prevents potential overpayment in fees if the coins - // selected to meet nFeeNeeded result in a transaction that - // requires less fee than the prior iteration. - - // If we have no change and a big enough excess fee, then - // try to construct transaction again only without picking - // new inputs. We now know we only need the smaller fee - // (because of reduced tx size) and so we should add a - // change output. Only try this once. - if (nChangePosInOut == -1 && nSubtractFeeFromAmount == 0 && pick_new_inputs && (mweb_type == MWEB::TxType::LTC_TO_LTC || mweb_type == MWEB::TxType::PEGIN)) { - unsigned int tx_size_with_change = change_on_mweb ? nBytes : nBytes + coin_selection_params.change_output_size + 2; // Add 2 as a buffer in case increasing # of outputs changes compact size - CAmount fee_needed_with_change = coin_selection_params.m_effective_feerate.GetTotalFee(tx_size_with_change, mweb_weight); - CAmount minimum_value_for_change = GetDustThreshold(change_prototype_txout, coin_selection_params.m_discard_feerate); - if (nFeeRet >= fee_needed_with_change + minimum_value_for_change) { - pick_new_inputs = false; - nFeeRet = fee_needed_with_change; - continue; - } - } - - // If we have change output already, just increase it - if (nFeeRet > nFeeNeeded && nSubtractFeeFromAmount == 0) { - CAmount extraFeePaid = nFeeRet - nFeeNeeded; - - if (nChangePosInOut != -1) { - std::vector::iterator change_position = txNew.vout.begin() + nChangePosInOut; - change_position->nValue += extraFeePaid; - nFeeRet -= extraFeePaid; - } else if (change_on_mweb) { - nFeeRet -= extraFeePaid; - } - } - - if (nFeeRet) - break; // Done, enough fee included. - } else if (!pick_new_inputs) { - // This shouldn't happen, we should have had enough excess - // fee to pay for the new output and still meet nFeeNeeded - // Or we should have just subtracted fee from recipients and - // nFeeNeeded should not have changed - error = _("Transaction fee and change calculation failed"); - return false; - } - - // Try to reduce change to include necessary fee - if (nChangePosInOut != -1 && nSubtractFeeFromAmount == 0) { - CAmount additionalFeeNeeded = nFeeNeeded - nFeeRet; - std::vector::iterator change_position = txNew.vout.begin() + nChangePosInOut; - // Only reduce change if remaining amount is still a large enough output. - if (change_position->nValue >= MIN_FINAL_CHANGE + additionalFeeNeeded) { - change_position->nValue -= additionalFeeNeeded; - nFeeRet += additionalFeeNeeded; - break; // Done, able to increase fee from change - } - } - - // If subtracting fee from recipients, we now know what fee we - // need to subtract, we have no reason to reselect inputs - if (nSubtractFeeFromAmount > 0) { - pick_new_inputs = false; - } - - // Include more fee and try again. - nFeeRet = nFeeNeeded; - coin_selection_params.use_bnb = false; - continue; - } - - // Give up if change keypool ran out and change is required - if (change_addr.IsEmpty() && nChangePosInOut != -1) { - return false; - } - } - - // Shuffle selected coins and fill in final vin - txNew.vin.clear(); - std::vector selected_coins(setCoins.begin(), setCoins.end()); - Shuffle(selected_coins.begin(), selected_coins.end(), FastRandomContext()); - - // Note how the sequence number is set to non-maxint so that - // the nLockTime set above actually works. - // - // BIP125 defines opt-in RBF as any nSequence < maxint-1, so - // we use the highest possible value in that range (maxint-2) - // to avoid conflicting with other possible uses of nSequence, - // and in the spirit of "smallest possible change from prior - // behavior." - const uint32_t nSequence = coin_control.m_signal_bip125_rbf.get_value_or(wallet.m_signal_rbf) ? MAX_BIP125_RBF_SEQUENCE : (CTxIn::SEQUENCE_FINAL - 1); - for (const auto& coin : selected_coins) { - if (!coin.IsMWEB()) { - txNew.vin.push_back(CTxIn(boost::get(coin.GetIndex()), CScript(), nSequence)); - } - } - - if (!MWEB::Transact::CreateTx(wallet.GetMWWallet(), txNew, selected_coins, vecSend, nFeeRet, mweb_fee, change_on_mweb)) { - error = _("Failed to create MWEB transaction"); - return false; - } - - if (sign && !wallet.SignTransaction(txNew)) { - error = _("Signing transaction failed"); - return false; - } - - // Return the constructed transaction data. - tx = MakeTransactionRef(std::move(txNew)); - - // Limit size - if (GetTransactionWeight(*tx) > MAX_STANDARD_TX_WEIGHT) { - error = _("Transaction too large"); - return false; - } - } - - if (nFeeRet > wallet.m_default_max_tx_fee) { - error = TransactionErrorString(TransactionError::MAX_FEE_EXCEEDED); - return false; - } - - if (gArgs.GetBoolArg("-walletrejectlongchains", DEFAULT_WALLET_REJECT_LONG_CHAINS)) { - // Lastly, ensure this tx will pass the mempool's chain limits - if (!wallet.chain().checkChainLimits(tx)) { - error = _("Transaction has too long of a mempool chain"); - return false; - } - } - - // Before we return success, we assume any change key will be used to prevent - // accidental re-use. - reservedest.KeepDestination(); - fee_calc_out = feeCalc; - - wallet.WalletLogPrintf("Fee Calculation: Fee:%d Bytes:%u Needed:%d Tgt:%d (requested %d) Reason:\"%s\" Decay %.5f: Estimation: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out) Fail: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out)\n", - nFeeRet, nBytes, nFeeNeeded, feeCalc.returnedTarget, feeCalc.desiredTarget, StringForFeeReason(feeCalc.reason), feeCalc.est.decay, - feeCalc.est.pass.start, feeCalc.est.pass.end, - (feeCalc.est.pass.totalConfirmed + feeCalc.est.pass.inMempool + feeCalc.est.pass.leftMempool) > 0.0 ? 100 * feeCalc.est.pass.withinTarget / (feeCalc.est.pass.totalConfirmed + feeCalc.est.pass.inMempool + feeCalc.est.pass.leftMempool) : 0.0, - feeCalc.est.pass.withinTarget, feeCalc.est.pass.totalConfirmed, feeCalc.est.pass.inMempool, feeCalc.est.pass.leftMempool, - feeCalc.est.fail.start, feeCalc.est.fail.end, - (feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) > 0.0 ? 100 * feeCalc.est.fail.withinTarget / (feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) : 0.0, - feeCalc.est.fail.withinTarget, feeCalc.est.fail.totalConfirmed, feeCalc.est.fail.inMempool, feeCalc.est.fail.leftMempool); - return true; -} \ No newline at end of file diff --git a/src/wallet/createtransaction.h b/src/wallet/createtransaction.h deleted file mode 100644 index 2d790c5a0..000000000 --- a/src/wallet/createtransaction.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include -#include - -// Forward Declarations -struct bilingual_str; -struct FeeCalculation; - -bool CreateTransactionEx( - CWallet& wallet, - const std::vector& vecSend, - CTransactionRef& tx, - CAmount& nFeeRet, - int& nChangePosInOut, - bilingual_str& error, - const CCoinControl& coin_control, - FeeCalculation& fee_calc_out, - bool sign -); \ No newline at end of file diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 1dc6e4536..283e988b2 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -401,10 +402,6 @@ void ParseRecipients(const UniValue& address_amounts, const UniValue& subtract_f } } - if (recipient_addr.IsMWEB() && !subtract_fee_outputs.empty()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Subtract fee from amount not yet supported for MWEB transactions."); - } - CRecipient recipient = {recipient_addr, amount, subtract_fee}; recipients.push_back(recipient); } @@ -1345,7 +1342,7 @@ static void ListTransactions(const CWallet* const pwallet, const CWalletTx& wtx, if (s.index.type() == typeid(COutPoint)) { entry.pushKV("vout", (int)boost::get(s.index).n); - } else { + } else if (!boost::get(s.index).IsZero()) { entry.pushKV("mweb_out", boost::get(s.index).ToHex()); } @@ -1374,14 +1371,16 @@ static void ListTransactions(const CWallet* const pwallet, const CWalletTx& wtx, entry.pushKV("involvesWatchonly", true); } MaybePushAddress(entry, r.destination); - if (wtx.IsCoinBase()) + if (wtx.IsCoinBase() || wtx.IsHogEx()) { if (wtx.GetDepthInMainChain() < 1) entry.pushKV("category", "orphan"); else if (wtx.IsImmature()) entry.pushKV("category", "immature"); - else + else if (wtx.IsCoinBase()) entry.pushKV("category", "generate"); + else + entry.pushKV("category", "hogex"); } else { @@ -1539,6 +1538,96 @@ static RPCHelpMan listtransactions() }; } +static RPCHelpMan listwallettransactions() +{ + return RPCHelpMan{"listwallettransactions", + "\nIf a label name is provided, this will return only incoming transactions paying to addresses with the specified label.\n" + "\nReturns the list of transactions as they would be displayed in the GUI.\n", + { + {"txid", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "The transaction id"}, + }, + RPCResult{ + RPCResult::Type::ARR, "", "", + { + {RPCResult::Type::OBJ, "", "", Cat(Cat>( + { + {RPCResult::Type::BOOL, "involvesWatchonly", "Only returns true if imported addresses were involved in transaction."}, + {RPCResult::Type::STR, "address", "The litecoin address of the transaction."}, + {RPCResult::Type::STR, "category", "The transaction category.\n" + "\"send\" Transactions sent.\n" + "\"receive\" Non-coinbase transactions received.\n" + "\"generate\" Coinbase transactions received with more than 100 confirmations.\n" + "\"immature\" Coinbase transactions received with 100 or fewer confirmations.\n" + "\"orphan\" Orphaned coinbase transactions received."}, + {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT + ". This is negative for the 'send' category, and is positive\n" + "for all other categories"}, + {RPCResult::Type::STR, "label", "A comment for the address/transaction, if any"}, + {RPCResult::Type::NUM, "vout", "the vout value"}, + {RPCResult::Type::STR_AMOUNT, "fee", "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the\n" + "'send' category of transactions."}, + }, + TransactionDescriptionString()), + { + {RPCResult::Type::BOOL, "abandoned", "'true' if the transaction has been abandoned (inputs are respendable). Only available for the \n" + "'send' category of transactions."}, + })}, + } + }, + RPCExamples{ + "\nList the wallet's transaction records\n" + + HelpExampleCli("listwallettransactions", "") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("listwallettransactions", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + if (!wallet) return NullUniValue; + const CWallet* const pwallet = wallet.get(); + + // Make sure the results are valid at least up to the most recent block + // the user could have gotten from another RPC command prior to now + pwallet->BlockUntilSyncedToCurrentChain(); + + UniValue ret(UniValue::VARR); + + { + LOCK(pwallet->cs_wallet); + + std::vector tx_records; + if (request.params[0].isNull()) { + tx_records = TxList(*pwallet).ListAll(ISMINE_ALL); + } else { + uint256 hash(ParseHashV(request.params[0], "txid")); + auto iter = pwallet->mapWallet.find(hash); + if (iter == pwallet->mapWallet.end()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id"); + } + + tx_records = TxList(*pwallet).List(iter->second, ISMINE_ALL, boost::none, boost::none); + } + + for (WalletTxRecord& tx_record : tx_records) { + tx_record.UpdateStatusIfNeeded(pwallet->GetLastBlockHash()); + } + + std::sort(tx_records.begin(), tx_records.end(), [](const WalletTxRecord& a, const WalletTxRecord& b) { + return a.status.sortKey > b.status.sortKey; + }); + + + for (WalletTxRecord& tx_record : tx_records) { + UniValue entry = tx_record.ToUniValue(); + WalletTxToJSON(pwallet->chain(), tx_record.GetWTX(), entry); + ret.push_back(entry); + } + } + + return ret; +}, + }; +} + static RPCHelpMan listsinceblock() { return RPCHelpMan{"listsinceblock", @@ -4638,6 +4727,7 @@ static const CRPCCommand commands[] = { "wallet", "listreceivedbylabel", &listreceivedbylabel, {"minconf","include_empty","include_watchonly"} }, { "wallet", "listsinceblock", &listsinceblock, {"blockhash","target_confirmations","include_watchonly","include_removed"} }, { "wallet", "listtransactions", &listtransactions, {"label|dummy","count","skip","include_watchonly"} }, + { "wallet", "listwallettransactions", &listwallettransactions, {"txid"} }, { "wallet", "listunspent", &listunspent, {"minconf","maxconf","addresses","include_unsafe","query_options"} }, { "wallet", "listwalletdir", &listwalletdir, {} }, { "wallet", "listwallets", &listwallets, {} }, diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 92d8006fc..af891f3ac 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -526,7 +526,10 @@ public: CCoinControl dummy; FeeCalculation fee_calc_out; { - BOOST_CHECK(wallet->CreateTransaction({recipient}, tx, fee, changePos, error, dummy, fee_calc_out)); + bool result = wallet->CreateTransaction({recipient}, tx, fee, changePos, error, dummy, fee_calc_out); + BOOST_CHECK(error.translated == ""); + BOOST_CHECK(result); + //BOOST_CHECK(wallet->CreateTransaction({recipient}, tx, fee, changePos, error, dummy, fee_calc_out)); } wallet->CommitTransaction(tx, {}, {}); CMutableTransaction blocktx; diff --git a/src/wallet/txassembler.cpp b/src/wallet/txassembler.cpp new file mode 100644 index 000000000..7759e7989 --- /dev/null +++ b/src/wallet/txassembler.cpp @@ -0,0 +1,662 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +Optional TxAssembler::AssembleTx( + const std::vector& recipients, + const CCoinControl& coin_control, + const int nChangePosRequest, + const bool sign, + bilingual_str& errorOut) +{ + try { + return CreateTransaction(recipients, coin_control, nChangePosRequest, sign); + } catch (const CreateTxError& e) { + m_wallet.WalletLogPrintf("%s\n", e.what()); + errorOut = e.GetError(); + } + + return nullopt; +} + +AssembledTx TxAssembler::CreateTransaction( + const std::vector& recipients, + const CCoinControl& coin_control, + const int nChangePosRequest, + const bool sign) +{ + VerifyRecipients(recipients); + + InProcessTx new_tx; + new_tx.recipients = recipients; + new_tx.recipient_amount = std::accumulate( + new_tx.recipients.cbegin(), new_tx.recipients.cend(), CAmount(0), + [](CAmount amt, const CRecipient& recipient) { return amt + recipient.nAmount; } + ); + new_tx.subtract_fee_from_amount = std::count_if(recipients.cbegin(), recipients.cend(), + [](const CRecipient& recipient) { return recipient.fSubtractFeeFromAmount; } + ); + new_tx.coin_control = coin_control; + + { + LOCK(m_wallet.cs_wallet); + CreateTransaction_Locked(new_tx, nChangePosRequest, sign); + } + + // Return the constructed transaction data. + CTransactionRef tx = MakeTransactionRef(std::move(new_tx.tx)); + + // Limit size + if (GetTransactionWeight(*tx) > MAX_STANDARD_TX_WEIGHT) { + throw CreateTxError(_("Transaction too large")); + } + + if (new_tx.total_fee > m_wallet.m_default_max_tx_fee) { + throw CreateTxError(TransactionErrorString(TransactionError::MAX_FEE_EXCEEDED)); + } + + if (gArgs.GetBoolArg("-walletrejectlongchains", DEFAULT_WALLET_REJECT_LONG_CHAINS)) { + // Lastly, ensure this tx will pass the mempool's chain limits + if (!m_wallet.chain().checkChainLimits(tx)) { + throw CreateTxError(_("Transaction has too long of a mempool chain")); + } + } + + // Before we return success, we assume any change key will be used to prevent + // accidental re-use. + if (new_tx.reserve_dest != nullptr) { + new_tx.reserve_dest->KeepDestination(); + } + + const FeeCalculation& feeCalc = new_tx.fee_calc; + m_wallet.WalletLogPrintf("Fee Calculation: Fee:%d Bytes:%u Needed:%d Tgt:%d (requested %d) Reason:\"%s\" Decay %.5f: Estimation: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out) Fail: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out)\n", + new_tx.total_fee, new_tx.bytes, new_tx.fee_needed, feeCalc.returnedTarget, feeCalc.desiredTarget, StringForFeeReason(feeCalc.reason), feeCalc.est.decay, + feeCalc.est.pass.start, feeCalc.est.pass.end, + (feeCalc.est.pass.totalConfirmed + feeCalc.est.pass.inMempool + feeCalc.est.pass.leftMempool) > 0.0 ? 100 * feeCalc.est.pass.withinTarget / (feeCalc.est.pass.totalConfirmed + feeCalc.est.pass.inMempool + feeCalc.est.pass.leftMempool) : 0.0, + feeCalc.est.pass.withinTarget, feeCalc.est.pass.totalConfirmed, feeCalc.est.pass.inMempool, feeCalc.est.pass.leftMempool, + feeCalc.est.fail.start, feeCalc.est.fail.end, + (feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) > 0.0 ? 100 * feeCalc.est.fail.withinTarget / (feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) : 0.0, + feeCalc.est.fail.withinTarget, feeCalc.est.fail.totalConfirmed, feeCalc.est.fail.inMempool, feeCalc.est.fail.leftMempool); + + return AssembledTx{tx, new_tx.total_fee, new_tx.fee_calc, new_tx.change_position}; +} + +void TxAssembler::CreateTransaction_Locked( + InProcessTx& new_tx, + const int nChangePosRequest, + const bool sign) +{ + AssertLockHeld(m_wallet.cs_wallet); + + new_tx.tx.nLockTime = GetLocktimeForNewTransaction(); + + m_wallet.AvailableCoins(new_tx.available_coins, true, &new_tx.coin_control, 1, MAX_MONEY, MAX_MONEY, 0); + UpdateChangeAddress(new_tx); + + InitCoinSelectionParams(new_tx); + + bool pick_new_inputs = true; + + // Starts with no fee and loops until there is enough fee + while (true) { + new_tx.change_position = nChangePosRequest; + new_tx.tx.vin.clear(); + new_tx.tx.vout.clear(); + + // Start by adding outputs for the recipients. + AddRecipientOutputs(new_tx); + + // Determine input amount needed. + CAmount amount_needed = new_tx.recipient_amount; + if (new_tx.subtract_fee_from_amount == 0) { + amount_needed += new_tx.total_fee; + } + + // Choose coins to use + if (pick_new_inputs) { + if (!AttemptCoinSelection(new_tx, amount_needed)) { + throw CreateTxError(_("Insufficient funds")); + } + + // Only use bnb on the first attempt. + new_tx.coin_selection_params.use_bnb = false; + } + + // We already created an output for each non-MWEB recipient, but for pegout transactions, + // the recipients are funded through the pegout kernel instead of traditional LTC outputs. + if (new_tx.mweb_type == MWEB::TxType::PEGOUT) { + new_tx.tx.vout.clear(); + } + + new_tx.change_on_mweb = MWEB::IsChangeOnMWEB(m_wallet, new_tx.mweb_type, new_tx.recipients, new_tx.coin_control.destChange); + new_tx.mweb_weight = MWEB::CalcMWEBWeight(new_tx.mweb_type, new_tx.change_on_mweb, new_tx.recipients); + + // Add peg-in script for 'value_selected', which is the most it could be with the chosen inputs. + // This ensures there will be enough fee included. + if (MWEB::ContainsPegIn(new_tx.mweb_type, new_tx.selected_coins)) { + std::vector::iterator position = new_tx.tx.vout.begin() + GetRandInt(new_tx.tx.vout.size() + 1); + new_tx.tx.vout.insert(position, CTxOut(new_tx.value_selected, GetScriptForPegin(mw::Hash()))); + } + + // Adds in a change output if there's enough leftover to create one + AddChangeOutput(new_tx, new_tx.value_selected - amount_needed); + + // Calculate transaction size and the total necessary fee amount (includes LTC and MWEB fees) + new_tx.bytes = CalculateMaximumTxSize(new_tx); + new_tx.fee_needed = new_tx.coin_selection_params.m_effective_feerate.GetTotalFee(new_tx.bytes, new_tx.mweb_weight); + + // Calculate the portion of the fee that should be paid on the MWEB side (using kernel fees) + size_t pegout_bytes = MWEB::CalcPegOutBytes(new_tx.mweb_type, new_tx.recipients); + new_tx.mweb_fee = new_tx.coin_selection_params.m_effective_feerate.GetTotalFee(pegout_bytes, new_tx.mweb_weight); + + if (new_tx.total_fee < new_tx.fee_needed && !pick_new_inputs) { + // This shouldn't happen, we should have had enough excess + // fee to pay for the new output and still meet nFeeNeeded + // Or we should have just subtracted fee from recipients and + // nFeeNeeded should not have changed + throw CreateTxError(_("Transaction fee and change calculation failed")); + } + + if (new_tx.total_fee >= new_tx.fee_needed) { + // Reduce fee to only the needed amount if possible. This + // prevents potential overpayment in fees if the coins + // selected to meet nFeeNeeded result in a transaction that + // requires less fee than the prior iteration. + ReduceFee(new_tx); + break; // Done, enough fee included. + } + + // Try to reduce change to include necessary fee + if (new_tx.change_position != -1 && new_tx.subtract_fee_from_amount == 0) { + CAmount additional_fee_needed = new_tx.fee_needed - new_tx.total_fee; + std::vector::iterator change_position = new_tx.tx.vout.begin() + new_tx.change_position; + + // Only reduce change if remaining amount is still a large enough output. + if (change_position->nValue >= MIN_FINAL_CHANGE + additional_fee_needed) { + change_position->nValue -= additional_fee_needed; + new_tx.total_fee += additional_fee_needed; + break; // Done, able to increase fee from change + } + } + + // If subtracting fee from recipients, we now know what fee we + // need to subtract, we have no reason to reselect inputs. + if (new_tx.subtract_fee_from_amount > 0) { + pick_new_inputs = false; + } + + // Include more fee and try again. + new_tx.total_fee = new_tx.fee_needed; + } + + // Give up if change keypool ran out and change is required + if (new_tx.change_addr.IsEmpty() && new_tx.change_position != -1) { + throw CreateTxError(_("Transaction needs a change address, but we can't generate it. Please call keypoolrefill first.")); + } + + AddTxInputs(new_tx); + + // Now build the MWEB side of the transaction + if (new_tx.mweb_type != MWEB::TxType::LTC_TO_LTC) { + // Lookup the change paid on the LTC side + CAmount ltc_change = 0; + if (new_tx.change_position != -1) { + assert(new_tx.tx.vout.size() > (size_t)new_tx.change_position); + ltc_change = new_tx.tx.vout[new_tx.change_position].nValue; + } + + std::vector selected_coins(new_tx.selected_coins.begin(), new_tx.selected_coins.end()); + bilingual_str error; + if (!MWEB::Transact::CreateTx(m_wallet.GetMWWallet(), new_tx.tx, selected_coins, new_tx.recipients, new_tx.total_fee, new_tx.mweb_fee, ltc_change, new_tx.change_on_mweb, error)) { + throw CreateTxError(error); + } + } + + if (sign && !m_wallet.SignTransaction(new_tx.tx)) { + throw CreateTxError(_("Signing transaction failed")); + } +} + +void TxAssembler::VerifyRecipients(const std::vector& recipients) +{ + if (recipients.empty()) { + throw CreateTxError(_("Transaction must have at least one recipient")); + } + + CAmount nValue = 0; + for (const auto& recipient : recipients) { + if (recipient.IsMWEB() && recipients.size() > 1) { + throw CreateTxError(_("Only one MWEB recipient supported at this time")); + } + + + nValue += recipient.nAmount; + if (nValue < 0 || recipient.nAmount < 0) { + throw CreateTxError(_("Transaction amounts must not be negative")); + } + } +} + +// Adds an output for each non-MWEB recipient. +// If the recipient is marked as 'fSubtractFeeFromAmount', the fee will be deducted from the amount. +// +// Throws a CreateTxError when a recipient amount is considered dust. +void TxAssembler::AddRecipientOutputs(InProcessTx& new_tx) +{ + // vouts to the payees + if (!new_tx.coin_selection_params.m_subtract_fee_outputs) { + new_tx.coin_selection_params.tx_noinputs_size = 11; // Static vsize overhead + outputs vsize. 4 nVersion, 4 nLocktime, 1 input count, 1 output count, 1 witness overhead (dummy, flag, stack size) + } + + bool fFirst = true; + for (const auto& recipient : new_tx.recipients) { + if (recipient.IsMWEB()) { + continue; + } + + CTxOut txout(recipient.nAmount, recipient.GetScript()); + + if (recipient.fSubtractFeeFromAmount) { + assert(new_tx.subtract_fee_from_amount != 0); + txout.nValue -= new_tx.total_fee / new_tx.subtract_fee_from_amount; // Subtract fee equally from each selected recipient + + if (fFirst) // first receiver pays the remainder not divisible by output count + { + fFirst = false; + txout.nValue -= new_tx.total_fee % new_tx.subtract_fee_from_amount; + } + } + // Include the fee cost for outputs. Note this is only used for BnB right now + new_tx.coin_selection_params.tx_noinputs_size += ::GetSerializeSize(txout, PROTOCOL_VERSION); + + if (IsDust(txout, m_wallet.chain().relayDustFee())) { + if (recipient.fSubtractFeeFromAmount && new_tx.total_fee > 0) { + if (txout.nValue < 0) { + throw CreateTxError(_("The transaction amount is too small to pay the fee")); + } else { + throw CreateTxError(_("The transaction amount is too small to send after the fee has been deducted")); + } + } + + throw CreateTxError(_("Transaction amount too small")); + } + new_tx.tx.vout.push_back(txout); + } +} + +void TxAssembler::AddChangeOutput(InProcessTx& new_tx, const CAmount& nChange) const +{ + if (nChange > 0 && !new_tx.change_on_mweb && !new_tx.change_addr.IsMWEB()) { + // Fill a vout to ourself + CTxOut newTxOut(nChange, new_tx.change_addr.GetScript()); + + // Never create dust outputs; if we would, just + // add the dust to the fee. + // The nChange when BnB is used is always going to go to fees. + if (IsDust(newTxOut, new_tx.coin_selection_params.m_discard_feerate) || new_tx.bnb_used) { + new_tx.change_position = -1; + new_tx.total_fee += nChange; + } else { + if (new_tx.change_position == -1) { + // Insert change txn at random position: + new_tx.change_position = GetRandInt(new_tx.tx.vout.size() + 1); + } else if ((unsigned int)new_tx.change_position > new_tx.tx.vout.size()) { + throw CreateTxError(_("Change index out of range")); + } + + std::vector::iterator position = new_tx.tx.vout.begin() + new_tx.change_position; + new_tx.tx.vout.insert(position, newTxOut); + } + } else { + new_tx.change_position = -1; + } +} + +void TxAssembler::AddTxInputs(InProcessTx& new_tx) const +{ + // Shuffle selected coins and fill in final vin + new_tx.tx.vin.clear(); + std::vector shuffled_coins(new_tx.selected_coins.begin(), new_tx.selected_coins.end()); + Shuffle(shuffled_coins.begin(), shuffled_coins.end(), FastRandomContext()); + + // Note how the sequence number is set to non-maxint so that + // the nLockTime set above actually works. + // + // BIP125 defines opt-in RBF as any nSequence < maxint-1, so + // we use the highest possible value in that range (maxint-2) + // to avoid conflicting with other possible uses of nSequence, + // and in the spirit of "smallest possible change from prior + // behavior." + const uint32_t nSequence = new_tx.coin_control.m_signal_bip125_rbf.get_value_or(m_wallet.m_signal_rbf) ? MAX_BIP125_RBF_SEQUENCE : (CTxIn::SEQUENCE_FINAL - 1); + for (const auto& coin : shuffled_coins) { + if (!coin.IsMWEB()) { + new_tx.tx.vin.push_back(CTxIn(boost::get(coin.GetIndex()), CScript(), nSequence)); + } + } +} + +void TxAssembler::InitCoinSelectionParams(InProcessTx& new_tx) const +{ + new_tx.coin_selection_params.change_output_size = new_tx.change_addr.IsMWEB() ? 0 : GetSerializeSize(CTxOut(0, new_tx.change_addr.GetScript())); + + // Set discard feerate + new_tx.coin_selection_params.m_discard_feerate = GetDiscardRate(m_wallet); + + // Get the fee rate to use effective values in coin selection + new_tx.coin_selection_params.m_effective_feerate = GetMinimumFeeRate(m_wallet, new_tx.coin_control, &new_tx.fee_calc); + + // Do not, ever, assume that it's fine to change the fee rate if the user has explicitly + // provided one + if (new_tx.coin_control.m_feerate && new_tx.coin_selection_params.m_effective_feerate > *new_tx.coin_control.m_feerate) { + throw CreateTxError(strprintf( + _("Fee rate (%s) is lower than the minimum fee rate setting (%s)"), + new_tx.coin_control.m_feerate->ToString(FeeEstimateMode::SAT_VB), + new_tx.coin_selection_params.m_effective_feerate.ToString(FeeEstimateMode::SAT_VB) + )); + } + + if (new_tx.fee_calc.reason == FeeReason::FALLBACK && !m_wallet.m_allow_fallback_fee) { + // eventually allow a fallback fee + throw CreateTxError(_("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee.")); + } + + // Get long term estimate + CCoinControl cc_temp; + cc_temp.m_confirm_target = m_wallet.chain().estimateMaxBlocks(); + new_tx.coin_selection_params.m_long_term_feerate = GetMinimumFeeRate(m_wallet, cc_temp, nullptr); + + // BnB selector is the only selector used when this is true. + // That should only happen on the first pass through the loop. + new_tx.coin_selection_params.use_bnb = true; + new_tx.coin_selection_params.m_subtract_fee_outputs = new_tx.subtract_fee_from_amount != 0; // If we are doing subtract fee from recipient, don't use effective values +} + +bool TxAssembler::AttemptCoinSelection(InProcessTx& new_tx, const CAmount& nTargetValue) const +{ + new_tx.value_selected = 0; + new_tx.selected_coins.clear(); + + CTxOut change_prototype_txout(0, new_tx.change_addr.IsMWEB() ? CScript() : new_tx.change_addr.GetScript()); + const int change_spend_size = CalculateMaximumSignedInputSize(change_prototype_txout, &m_wallet); + + // If the wallet doesn't know how to sign change output, assume p2sh-p2wpkh + // as lower-bound to allow BnB to do it's thing + if (change_spend_size == -1) { + new_tx.coin_selection_params.change_spend_size = DUMMY_NESTED_P2WPKH_INPUT_SIZE; + } else { + new_tx.coin_selection_params.change_spend_size = (size_t)change_spend_size; + } + + static auto is_ltc = [](const CInputCoin& input) { return !input.IsMWEB(); }; + static auto is_mweb = [](const CInputCoin& input) { return input.IsMWEB(); }; + + if (new_tx.recipients.front().IsMWEB()) { + // First try to construct an MWEB-to-MWEB transaction + CoinSelectionParams mweb_to_mweb = new_tx.coin_selection_params; + mweb_to_mweb.input_preference = InputPreference::MWEB_ONLY; + mweb_to_mweb.mweb_change_output_weight = Weight::STANDARD_OUTPUT_WEIGHT; + mweb_to_mweb.mweb_nochange_weight = Weight::KERNEL_WITH_STEALTH_WEIGHT + (new_tx.recipients.size() * Weight::STANDARD_OUTPUT_WEIGHT); + mweb_to_mweb.change_output_size = 0; + mweb_to_mweb.change_spend_size = 0; + mweb_to_mweb.tx_noinputs_size = 0; + + if (SelectCoins(new_tx, nTargetValue, mweb_to_mweb)) { + new_tx.mweb_type = MWEB::TxType::MWEB_TO_MWEB; + return true; + } + + // If MWEB-to-MWEB fails, create a peg-in transaction + const bool change_on_mweb = MWEB::IsChangeOnMWEB(m_wallet, MWEB::TxType::PEGIN, new_tx.recipients, new_tx.coin_control.destChange); + CoinSelectionParams params_pegin = new_tx.coin_selection_params; + params_pegin.input_preference = InputPreference::ANY; + params_pegin.mweb_change_output_weight = change_on_mweb ? Weight::STANDARD_OUTPUT_WEIGHT : 0; + params_pegin.mweb_nochange_weight = Weight::KERNEL_WITH_STEALTH_WEIGHT + (new_tx.recipients.size() * Weight::STANDARD_OUTPUT_WEIGHT); + params_pegin.change_output_size = change_on_mweb ? 0 : new_tx.coin_selection_params.change_output_size; + params_pegin.change_spend_size = change_on_mweb ? 0 : new_tx.coin_selection_params.change_spend_size; + + if (SelectCoins(new_tx, nTargetValue, params_pegin)) { + new_tx.mweb_type = MWEB::TxType::PEGIN; + return std::any_of(new_tx.selected_coins.cbegin(), new_tx.selected_coins.cend(), is_ltc); + } + } else { + // First try to construct a LTC-to-LTC transaction + CoinSelectionParams mweb_to_mweb = new_tx.coin_selection_params; + mweb_to_mweb.input_preference = InputPreference::LTC_ONLY; + mweb_to_mweb.mweb_change_output_weight = 0; + mweb_to_mweb.mweb_nochange_weight = 0; + + if (SelectCoins(new_tx, nTargetValue, mweb_to_mweb)) { + new_tx.mweb_type = MWEB::TxType::LTC_TO_LTC; + return true; + } + + // Only supports pegging-out to one address + if (new_tx.recipients.size() > 1) { + return false; + } + + // If LTC-to-LTC fails, create a peg-out transaction + CoinSelectionParams params_pegout = new_tx.coin_selection_params; + params_pegout.input_preference = InputPreference::ANY; + params_pegout.mweb_change_output_weight = Weight::STANDARD_OUTPUT_WEIGHT; + params_pegout.mweb_nochange_weight = Weight::CalcKernelWeight(true, new_tx.recipients.front().GetScript()); + params_pegout.change_output_size = 0; + params_pegout.change_spend_size = 0; + new_tx.tx.vout.clear(); + + if (SelectCoins(new_tx, nTargetValue, params_pegout)) { + new_tx.mweb_type = MWEB::TxType::PEGOUT; + return std::any_of(new_tx.selected_coins.cbegin(), new_tx.selected_coins.cend(), is_mweb); + } + } + + return false; +} + +bool TxAssembler::SelectCoins(InProcessTx& new_tx, const CAmount& nTargetValue, CoinSelectionParams& coin_selection_params) const +{ + new_tx.value_selected = 0; + if (m_wallet.SelectCoins(new_tx.available_coins, nTargetValue, new_tx.selected_coins, new_tx.value_selected, new_tx.coin_control, coin_selection_params, new_tx.bnb_used)) { + return true; + } + + new_tx.value_selected = 0; + if (new_tx.bnb_used) { + coin_selection_params.use_bnb = false; + return m_wallet.SelectCoins(new_tx.available_coins, nTargetValue, new_tx.selected_coins, new_tx.value_selected, new_tx.coin_control, coin_selection_params, new_tx.bnb_used); + } + + return false; +} + +// Return a height-based locktime for new transactions +// (uses the height of the current chain tip unless we are not synced with the current chain) +uint32_t TxAssembler::GetLocktimeForNewTransaction() const +{ + AssertLockHeld(m_wallet.cs_wallet); + + uint32_t locktime; + + // Discourage fee sniping. + // + // For a large miner the value of the transactions in the best block and + // the mempool can exceed the cost of deliberately attempting to mine two + // blocks to orphan the current best block. By setting nLockTime such that + // only the next block can include the transaction, we discourage this + // practice as the height restricted and limited blocksize gives miners + // considering fee sniping fewer options for pulling off this attack. + // + // A simple way to think about this is from the wallet's point of view we + // always want the blockchain to move forward. By setting nLockTime this + // way we're basically making the statement that we only want this + // transaction to appear in the next block; we don't want to potentially + // encourage reorgs by allowing transactions to appear at lower heights + // than the next block in forks of the best chain. + // + // Of course, the subsidy is high enough, and transaction volume low + // enough, that fee sniping isn't a problem yet, but by implementing a fix + // now we ensure code won't be written that makes assumptions about + // nLockTime that preclude a fix later. + if (IsCurrentForAntiFeeSniping(m_wallet.chain(), m_wallet.GetLastBlockHash())) { + locktime = m_wallet.GetLastBlockHeight(); + + // Secondly occasionally randomly pick a nLockTime even further back, so + // that transactions that are delayed after signing for whatever reason, + // e.g. high-latency mix networks and some CoinJoin implementations, have + // better privacy. + if (GetRandInt(10) == 0) + locktime = std::max(0, (int)locktime - GetRandInt(100)); + } else { + // If our chain is lagging behind, we can't discourage fee sniping nor help + // the privacy of high-latency transactions. To avoid leaking a potentially + // unique "nLockTime fingerprint", set nLockTime to a constant. + locktime = 0; + } + assert(locktime < LOCKTIME_THRESHOLD); + return locktime; +} + +bool TxAssembler::IsCurrentForAntiFeeSniping(interfaces::Chain& chain, const uint256& block_hash) const +{ + if (chain.isInitialBlockDownload()) { + return false; + } + + constexpr int64_t MAX_ANTI_FEE_SNIPING_TIP_AGE = 8 * 60 * 60; // in seconds + int64_t block_time; + CHECK_NONFATAL(chain.findBlock(block_hash, interfaces::FoundBlock().time(block_time))); + + if (block_time < (GetTime() - MAX_ANTI_FEE_SNIPING_TIP_AGE)) { + return false; + } + + return true; +} + +void TxAssembler::ReduceFee(InProcessTx& new_tx) const +{ + // If we have no change and a big enough excess fee, then try to construct transaction again, only without picking new inputs. + // We now know we only need the smaller fee (because of reduced tx size) and so we should add a change output. + if (new_tx.change_position == -1 && new_tx.subtract_fee_from_amount == 0 && !new_tx.change_on_mweb && !new_tx.change_addr.IsMWEB()) { + unsigned int output_buffer = 2; // Add 2 as a buffer in case increasing # of outputs changes compact size + unsigned int tx_size_with_change = new_tx.change_on_mweb ? new_tx.bytes : new_tx.bytes + new_tx.coin_selection_params.change_output_size + output_buffer; + CAmount fee_needed_with_change = new_tx.coin_selection_params.m_effective_feerate.GetTotalFee(tx_size_with_change, new_tx.mweb_weight); + + CTxOut change_prototype_txout(0, new_tx.change_addr.GetScript()); + CAmount minimum_value_for_change = GetDustThreshold(change_prototype_txout, new_tx.coin_selection_params.m_discard_feerate); + + if (new_tx.total_fee >= fee_needed_with_change + minimum_value_for_change) { + const CAmount nChange = new_tx.total_fee - fee_needed_with_change; + new_tx.fee_needed = fee_needed_with_change; + new_tx.total_fee = fee_needed_with_change; + AddChangeOutput(new_tx, nChange); + new_tx.bytes = CalculateMaximumTxSize(new_tx); + } + } + + // If we have a change output already, just increase it + if (new_tx.total_fee > new_tx.fee_needed && new_tx.subtract_fee_from_amount == 0) { + CAmount extra_fee_paid = new_tx.total_fee - new_tx.fee_needed; + if (new_tx.change_position != -1) { + std::vector::iterator change_position = new_tx.tx.vout.begin() + new_tx.change_position; + change_position->nValue += extra_fee_paid; + new_tx.total_fee -= extra_fee_paid; + } else if (new_tx.change_on_mweb) { + new_tx.total_fee -= extra_fee_paid; + } + } +} + +int64_t TxAssembler::CalculateMaximumTxSize(const InProcessTx& new_tx) const +{ + CMutableTransaction tmp_tx(new_tx.tx); + + // Dummy fill vin for maximum size estimation + for (const auto& coin : new_tx.selected_coins) { + if (!coin.IsMWEB()) { + tmp_tx.vin.push_back(CTxIn(boost::get(coin.GetIndex()), CScript())); + } + } + + int64_t bytes = 0; + if (new_tx.mweb_type != MWEB::TxType::MWEB_TO_MWEB) { + bytes = CalculateMaximumSignedTxSize(CTransaction(tmp_tx), &m_wallet, new_tx.coin_control.fAllowWatchOnly); + if (bytes < 0) { + throw CreateTxError(_("Signing transaction failed")); + } + } + + // Include HogEx input bytes for any pegins + if (MWEB::ContainsPegIn(new_tx.mweb_type, new_tx.selected_coins)) { + bytes += ::GetSerializeSize(CTxIn(), PROTOCOL_VERSION); + } + + // Include pegout bytes + bytes += MWEB::CalcPegOutBytes(new_tx.mweb_type, new_tx.recipients); + + return bytes; +} + +void TxAssembler::UpdateChangeAddress(InProcessTx& new_tx) const +{ + // coin control: send change to custom address + if (!boost::get(&new_tx.coin_control.destChange)) { + new_tx.change_addr = DestinationAddr(new_tx.coin_control.destChange); + } else { // no coin control: send change to newly generated address + // Note: We use a new key here to keep it from being obvious which side is the change. + // The drawback is that by not reusing a previous key, the change may be lost if a + // backup is restored, if the backup doesn't have the new private key for the change. + // If we reused the old key, it would be possible to add code to look for and + // rediscover unknown transactions that were written with keys of ours to recover + // post-backup change. + + new_tx.reserve_dest = std::make_unique(&m_wallet, GetChangeType(new_tx)); + + // Reserve a new key pair from key pool. If it fails, provide a dummy + // destination in case we don't need change. + CTxDestination dest; + new_tx.reserve_dest->GetReservedDestination(dest, true); + new_tx.change_addr = GetScriptForDestination(dest); + // A valid destination implies a change script (and + // vice-versa). An empty change script will abort later, if the + // change keypool ran out, but change is required. + CHECK_NONFATAL(IsValidDestination(dest) != new_tx.change_addr.IsEmpty()); + } +} + +OutputType TxAssembler::GetChangeType(const InProcessTx& new_tx) const +{ + Optional change_type = new_tx.coin_control.m_change_type ? *new_tx.coin_control.m_change_type : m_wallet.m_default_change_type; + + // If -changetype is specified, always use that change type. + if (change_type) { + return *change_type; + } + + // if m_default_address_type is legacy, use legacy address as change + // (even if some of the outputs are P2WPKH or P2WSH). + if (m_wallet.m_default_address_type == OutputType::LEGACY) { + return OutputType::LEGACY; + } + + // If any destination is P2WPKH or P2WSH, use P2WPKH for the change output. + for (const auto& recipient : new_tx.recipients) { + if (!recipient.IsMWEB()) { + int witnessversion = 0; + std::vector witnessprogram; + if (recipient.GetScript().IsWitnessProgram(witnessversion, witnessprogram)) { + return OutputType::BECH32; + } + } + } + + // else use m_default_address_type for change + return m_wallet.m_default_address_type; +} \ No newline at end of file diff --git a/src/wallet/txassembler.h b/src/wallet/txassembler.h new file mode 100644 index 000000000..b34130b95 --- /dev/null +++ b/src/wallet/txassembler.h @@ -0,0 +1,128 @@ +#pragma once + +#include +#include +#include