* txrecord refactor and introduction of txlist

* createtransaction rewrite (TxAssembler)
* added listwallettransactions rpc method
This commit is contained in:
David Burkett 2022-03-31 10:49:08 -04:00 committed by Loshan T
parent 768e3e8621
commit 30aa04815c
41 changed files with 2218 additions and 1582 deletions

View File

@ -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])

View File

@ -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 \

View File

@ -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 \

View File

@ -25,6 +25,7 @@
#include <wallet/load.h>
#include <wallet/reserve.h>
#include <wallet/rpcwallet.h>
#include <wallet/txlist.h>
#include <wallet/wallet.h>
#include <memory>
@ -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<WalletTxRecord> 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<WalletTx> getWalletTxs() override
std::vector<WalletTxRecord> getWalletTxs() override
{
LOCK(m_wallet->cs_wallet);
std::vector<WalletTx> 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);

View File

@ -13,6 +13,7 @@
#include <support/allocators/secure.h> // For SecureString
#include <util/message.h>
#include <util/ui_change_type.h>
#include <wallet/txrecord.h>
#include <functional>
#include <map>
@ -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<WalletTxRecord> getWalletTx(const uint256& txid) = 0;
//! Get list of all wallet transactions.
virtual std::vector<WalletTx> 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<WalletTxRecord> 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<CTxDestination, std::vector<std::tuple<OutputIndex, WalletTxOut>>>;
@ -430,6 +428,7 @@ struct WalletTxStatus
bool is_trusted;
bool is_abandoned;
bool is_coinbase;
bool is_hogex;
bool is_in_main_chain;
};

View File

@ -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:

View File

@ -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:

View File

@ -137,8 +137,8 @@ public:
GetHash(),
GetKernelOffset().ToHex(),
kernels_str,
GetInputCommits(),
GetOutputCommits()
GetInputs(),
GetOutputs()
);
}

View File

@ -1,36 +1,70 @@
#include <mweb/mweb_transact.h>
#include <key_io.h>
#include <policy/policy.h>
#include <mw/models/tx/PegOutCoin.h>
#include <mw/wallet/TxBuilder.h>
#include <util/translation.h>
using namespace MWEB;
TxType MWEB::GetTxType(const std::vector<CRecipient>& recipients, const std::vector<CInputCoin>& input_coins)
bool MWEB::ContainsPegIn(const TxType& mweb_type, const std::set<CInputCoin>& 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<CRecipient>& 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<CRecipient>& 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<CRecipient>& 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<CInputCoin>& selected_coins,
const std::vector<CRecipient>& 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<mw::Recipient> receivers;
std::vector<PegOutCoin> 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<CAmount> 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<mw::Coin> input_coins = GetInputCoins(selected_coins);
std::vector<mw::Coin> input_coins;
for (const auto& coin : selected_coins) {
if (coin.IsMWEB()) {
input_coins.push_back(coin.GetMWEBCoin());
}
}
std::vector<mw::Coin> 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<mw::Coin> Transact::GetInputCoins(const std::vector<CInputCoin>& inputs)
mw::Recipient Transact::BuildChangeRecipient()
{
std::vector<mw::Coin> 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<CInputCoin>& 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<CInputCoin>& 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<CRecipient>& 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};
}

View File

@ -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<CRecipient>& recipients, const std::vector<CInputCoin>& input_coins);
bool ContainsPegIn(const TxType& mweb_type, const std::set<CInputCoin>& input_coins);
bool IsChangeOnMWEB(const CWallet& wallet, const MWEB::TxType& mweb_type, const std::vector<CRecipient>& recipients, const CTxDestination& dest_change);
uint64_t CalcMWEBWeight(const MWEB::TxType& mweb_type, const bool change_on_mweb, const std::vector<CRecipient>& recipients);
int64_t CalcPegOutBytes(const TxType& mweb_type, const std::vector<CRecipient>& recipients);
class Transact
{
@ -28,15 +31,13 @@ public:
const std::vector<CRecipient>& 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<mw::Coin> GetInputCoins(const std::vector<CInputCoin>& inputs);
static CAmount GetMWEBInputAmount(const std::vector<CInputCoin>& inputs);
static CAmount GetLTCInputAmount(const std::vector<CInputCoin>& inputs);
static CAmount GetMWEBRecipientAmount(const std::vector<CRecipient>& recipients);
static bool UpdatePegInOutput(CMutableTransaction& transaction, const PegInCoin& pegin);
static mw::Recipient BuildChangeRecipient();
};
}

View File

@ -62,20 +62,14 @@ struct WalletTxInfo
// so we create an empty Transaction and store the spent hash here.
boost::optional<mw::Hash> 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<mw::Coin>(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<mw::Hash>(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<uint8_t>{stream.begin(), stream.end()});
}
const uint256& GetHash() const { return hash; }
const uint256& GetHash() const {
return hash;
}
};
} // namespace MWEB

View File

@ -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);
}
}

View File

@ -11,7 +11,6 @@
#include <qt/bitcoinunits.h>
#include <qt/guiutil.h>
#include <qt/paymentserver.h>
#include <qt/transactionrecord.h>
#include <consensus/consensus.h>
#include <interfaces/node.h>
@ -22,6 +21,7 @@
#include <util/system.h>
#include <validation.h>
#include <wallet/ismine.h>
#include <wallet/txrecord.h>
#include <stdint.h>
#include <string>
@ -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 += "<br><b>" + tr("Comment") + ":</b><br>" + GUIUtil::HtmlEscape(wtx.value_map["comment"], true) + "<br>";
strHTML += "<b>" + tr("Transaction ID") + ":</b> " + rec->getTxHash() + "<br>";
strHTML += "<b>" + tr("Transaction ID") + ":</b> " + QString::fromStdString(rec->GetTxHash().ToString()) + "<br>";
if (!wtx.tx->IsNull()) {
strHTML += "<b>" + tr("Transaction total size") + ":</b> " + QString::number(wtx.tx->GetTotalSize()) + " bytes<br>";
@ -115,7 +115,7 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall
if (wtx.tx->HasMWEBTx()) {
strHTML += "<b>" + tr("Transaction MWEB weight") + ":</b> " + QString::number(wtx.tx->mweb_tx.GetMWEBWeight()) + "<br>";
}
strHTML += "<b>" + tr("Output index") + ":</b> " + QString::number(rec->getOutputIndex()) + "<br>";
strHTML += "<b>" + tr("Component index") + ":</b> " + QString::fromStdString(rec->GetComponentIndex()) + "<br>";
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 = "<hr><br>" + tr("Debug information") + "<br><br>";
for (const CTxInput& txin : wtx.inputs) {
@ -346,15 +346,29 @@ QString TransactionDesc::toHTML_Debug(interfaces::Node& node, interfaces::Wallet
strHTML += "<b>" + tr("Credit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, wallet.getCredit(out.txout, ISMINE_ALL)) + "<br>";
strHTML += "<br><b>" + tr("Transaction") + ":</b><br>";
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 += "<br><b>" + tr("Inputs") + ":</b>";
strHTML += "<ul>";
for (const CTxInput& txin : wtx.inputs) {
strHTML += "<li>";
CTxOutput prevout;
if (node.getUnspentOutput(txin.GetIndex(), prevout)) {
strHTML += "<li>";
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")) + "</li>";
strHTML = strHTML + " IsWatchOnly=" + (wallet.txoutIsMine(prevout) & ISMINE_WATCH_ONLY ? tr("true") : tr("false")) + "</li>";
} else {
strHTML += "<li>";
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")) + "</li>";
strHTML = strHTML + " IsWatchOnly=" + (wallet.txinIsMine(txin) & ISMINE_WATCH_ONLY ? tr("true") : tr("false")) + "</li>";
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 += "</li>";
}
strHTML += "</ul>";
strHTML += "<br><b>" + tr("Outputs") + ":</b>";
strHTML += "<ul>";
for (const interfaces::WalletTxOut& txout : wtx.outputs) {
strHTML += "<li>";
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 += "</li>";
}
strHTML += "</ul>";
return strHTML;
}

View File

@ -8,7 +8,7 @@
#include <QObject>
#include <QString>
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

View File

@ -5,7 +5,7 @@
#include <qt/transactionfilterproxy.h>
#include <qt/transactiontablemodel.h>
#include <qt/transactionrecord.h>
#include <wallet/txrecord.h>
#include <cstdlib>
@ -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();

View File

@ -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 <qt/transactionrecord.h>
#include <chain.h>
#include <interfaces/wallet.h>
#include <key_io.h>
#include <wallet/ismine.h>
#include <stdint.h>
#include <QDateTime>
/* 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> TransactionRecord::decomposeTransaction(const interfaces::WalletTx& wtx)
{
QList<TransactionRecord> 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<std::string, std::string> 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;
}

View File

@ -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 <amount.h>
#include <uint256.h>
#include <QList>
#include <QString>
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<TransactionRecord> 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

View File

@ -11,7 +11,6 @@
#include <qt/optionsmodel.h>
#include <qt/platformstyle.h>
#include <qt/transactiondesc.h>
#include <qt/transactionrecord.h>
#include <qt/walletmodel.h>
#include <core_io.h>
@ -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<TransactionRecord> cachedWallet;
QList<WalletTxRecord> 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<TransactionRecord>::iterator lower = std::lower_bound(
QList<WalletTxRecord>::iterator lower = std::lower_bound(
cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan());
QList<TransactionRecord>::iterator upper = std::upper_bound(
QList<WalletTxRecord>::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<WalletTxRecord> 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<TransactionRecord> 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<TransactionRecord*>(index.internalPointer());
WalletTxRecord *rec = static_cast<WalletTxRecord*>(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<uint>(rec->time));
return QDateTime::fromTime_t(static_cast<uint>(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<uint>(rec->time));
QDateTime date = QDateTime::fromTime_t(static_cast<uint>(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)
{

View File

@ -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 */

View File

@ -13,7 +13,6 @@
#include <qt/platformstyle.h>
#include <qt/transactiondescdialog.h>
#include <qt/transactionfilterproxy.h>
#include <qt/transactionrecord.h>
#include <qt/transactiontablemodel.h>
#include <qt/walletmodel.h>
@ -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);

View File

@ -45,6 +45,15 @@ void WalletModelTransaction::reassignAmounts(interfaces::Wallet& wallet, int nCh
{
std::vector<CTxOutput> 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<SendCoinsRecipient>::iterator it = recipients.begin(); it != recipients.end(); ++it)
{

View File

@ -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

View File

@ -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<uint8_t>{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<uint8_t> 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<uint8_t> 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()

View File

@ -1,705 +0,0 @@
#include <wallet/createtransaction.h>
#include <consensus/validation.h>
#include <logging.h>
#include <mweb/mweb_transact.h>
#include <policy/policy.h>
#include <policy/fees.h>
#include <policy/rbf.h>
#include <txmempool.h>
#include <util/check.h>
#include <util/fees.h>
#include <util/rbf.h>
#include <util/system.h>
#include <util/translation.h>
#include <validation.h>
#include <wallet/fees.h>
#include <wallet/reserve.h>
#include <numeric>
#include <boost/optional.hpp>
static OutputType TransactionChangeType(const CWallet& wallet, const Optional<OutputType>& change_type, const std::vector<CRecipient>& 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<unsigned char> 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<CRecipient>& 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<CRecipient>& 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<CRecipient>& 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<CInputCoin>& 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<COutputCoin>& vAvailableCoins,
const CAmount& nTargetValue,
std::set<CInputCoin>& 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<COutputCoin>& vAvailableCoins,
const CAmount& nTargetValue,
std::set<CInputCoin>& setCoinsRet,
CAmount& nValueRet,
const CCoinControl& coin_control,
const std::vector<CRecipient>& 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<CRecipient>& 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<CInputCoin> setCoins;
LOCK(wallet.cs_wallet);
txNew.nLockTime = GetLocktimeForNewTransaction(wallet.chain(), wallet.GetLastBlockHash(), wallet.GetLastBlockHeight());
{
std::vector<COutputCoin> 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<CNoDestination>(&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<CInputCoin>(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<CTxOut>::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<COutPoint>(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<CTxOut>::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<CTxOut>::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<CInputCoin> 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<COutPoint>(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;
}

View File

@ -1,20 +0,0 @@
#pragma once
#include <wallet/wallet.h>
#include <wallet/coincontrol.h>
// Forward Declarations
struct bilingual_str;
struct FeeCalculation;
bool CreateTransactionEx(
CWallet& wallet,
const std::vector<CRecipient>& vecSend,
CTransactionRef& tx,
CAmount& nFeeRet,
int& nChangePosInOut,
bilingual_str& error,
const CCoinControl& coin_control,
FeeCalculation& fee_calc_out,
bool sign
);

View File

@ -34,6 +34,7 @@
#include <wallet/feebumper.h>
#include <wallet/load.h>
#include <wallet/rpcwallet.h>
#include <wallet/txlist.h>
#include <wallet/wallet.h>
#include <wallet/walletdb.h>
#include <wallet/walletutil.h>
@ -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<COutPoint>(s.index).n);
} else {
} else if (!boost::get<mw::Hash>(s.index).IsZero()) {
entry.pushKV("mweb_out", boost::get<mw::Hash>(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<std::vector<RPCResult>>(
{
{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<CWallet> 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<WalletTxRecord> 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, {} },

View File

@ -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;

662
src/wallet/txassembler.cpp Normal file
View File

@ -0,0 +1,662 @@
#include <wallet/txassembler.h>
#include <consensus/validation.h>
#include <policy/policy.h>
#include <util/check.h>
#include <util/fees.h>
#include <util/rbf.h>
#include <util/translation.h>
#include <wallet/fees.h>
#include <wallet/reserve.h>
Optional<AssembledTx> TxAssembler::AssembleTx(
const std::vector<CRecipient>& 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<CRecipient>& 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<CTxOut>::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<CTxOut>::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<CInputCoin> 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<CRecipient>& 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<CTxOut>::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<CInputCoin> 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<COutPoint>(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<CTxOut>::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<COutPoint>(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<CNoDestination>(&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<ReserveDestination>(&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<OutputType> 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<unsigned char> 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;
}

128
src/wallet/txassembler.h Normal file
View File

@ -0,0 +1,128 @@
#pragma once
#include <mweb/mweb_transact.h>
#include <policy/fees.h>
#include <script/standard.h>
#include <wallet/coincontrol.h>
#include <wallet/reserve.h>
#include <wallet/wallet.h>
#include <util/translation.h>
struct InProcessTx {
CMutableTransaction tx{};
std::vector<CRecipient> recipients;
CAmount recipient_amount;
std::unique_ptr<ReserveDestination> reserve_dest{nullptr};
DestinationAddr change_addr{};
int change_position{-1}; // MW: TODO - Change this to OutputIndex
int bytes{0};
CCoinControl coin_control;
CoinSelectionParams coin_selection_params{};
std::vector<COutputCoin> available_coins{};
std::set<CInputCoin> selected_coins{};
CAmount value_selected{0};
bool bnb_used{false};
FeeCalculation fee_calc{};
CAmount fee_needed{0};
CAmount total_fee{0};
unsigned int subtract_fee_from_amount{0};
// MWEB
uint64_t mweb_weight{0};
MWEB::TxType mweb_type{MWEB::TxType::LTC_TO_LTC};
bool change_on_mweb{false};
CAmount mweb_fee{0}; // Portion of the fee that will be paid by an MWEB kernel
};
class CreateTxError : public std::runtime_error
{
public:
CreateTxError(const bilingual_str& error)
: std::runtime_error(error.original), m_error(error) {}
~CreateTxError() = default;
const bilingual_str& GetError() const { return m_error; }
private:
bilingual_str m_error;
};
struct AssembledTx {
CTransactionRef tx;
CAmount fee;
FeeCalculation fee_calc; // MW: TODO - Really just needs FeeReason
int change_position;
};
class TxAssembler
{
CWallet& m_wallet;
public:
explicit TxAssembler(CWallet& wallet)
: m_wallet(wallet) {}
Optional<AssembledTx> AssembleTx(
const std::vector<CRecipient>& recipients,
const CCoinControl& coin_control,
const int nChangePosRequest,
const bool sign,
bilingual_str& errorOut
);
private:
AssembledTx CreateTransaction(
const std::vector<CRecipient>& recipients,
const CCoinControl& coin_control,
const int nChangePosRequest,
const bool sign
);
void CreateTransaction_Locked(
InProcessTx& new_tx,
const int nChangePosRequest,
const bool sign
);
void VerifyRecipients(const std::vector<CRecipient>& recipients);
// Adds the output to 'txNew' for each non-MWEB recipient.
// If the recipient is marked as 'fSubtractFeeFromAmount', 'nFeeRet' will be deducted from the amount.
//
// Returns false and populates the 'error' message if an error occurs.
// Currently, this will only occur when a recipient amount is considered dust.
void AddRecipientOutputs(InProcessTx& new_tx);
void AddChangeOutput(InProcessTx& new_tx, const CAmount& nChange) const;
void AddTxInputs(InProcessTx& new_tx) const;
uint32_t GetLocktimeForNewTransaction() const;
bool IsCurrentForAntiFeeSniping(interfaces::Chain& chain, const uint256& block_hash) const;
//
// Coin Selection
//
void InitCoinSelectionParams(InProcessTx& new_tx) const;
bool AttemptCoinSelection(
InProcessTx& new_tx,
const CAmount& nTargetValue
) const;
bool SelectCoins(
InProcessTx& new_tx,
const CAmount& nTargetValue,
CoinSelectionParams& coin_selection_params
) const;
// 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.
void ReduceFee(InProcessTx& new_tx) const;
int64_t CalculateMaximumTxSize(const InProcessTx& new_tx) const;
void UpdateChangeAddress(InProcessTx& new_tx) const;
OutputType GetChangeType(const InProcessTx& new_tx) const;
};

303
src/wallet/txlist.cpp Normal file
View File

@ -0,0 +1,303 @@
#include <wallet/txlist.h>
#include <wallet/wallet.h>
#include <key_io.h>
std::vector<WalletTxRecord> TxList::ListAll(const isminefilter& filter_ismine)
{
std::vector<WalletTxRecord> tx_records;
for (const auto& entry : m_wallet.mapWallet) {
List(tx_records, entry.second, filter_ismine);
}
return tx_records;
}
std::vector<WalletTxRecord> TxList::List(const CWalletTx& wtx, const isminefilter& filter_ismine, const boost::optional<int>& nMinDepth, const boost::optional<std::string>& filter_label)
{
std::vector<WalletTxRecord> tx_records;
List(tx_records, wtx, filter_ismine);
auto iter = tx_records.begin();
while (iter != tx_records.end()) {
if (iter->credit > 0) {
// Filter received transactions
if (nMinDepth && wtx.GetDepthInMainChain() < *nMinDepth) {
iter = tx_records.erase(iter);
continue;
}
if (filter_label) {
std::string label;
CTxDestination destination = DecodeDestination(iter->address);
if (IsValidDestination(destination)) {
const auto* address_book_entry = m_wallet.FindAddressBookEntry(destination);
if (address_book_entry) {
label = address_book_entry->GetLabel();
}
}
if (label != *filter_label) {
iter = tx_records.erase(iter);
continue;
}
}
} else {
// Filter sent transactions
if (filter_label) {
iter = tx_records.erase(iter);
continue;
}
}
iter++;
}
return tx_records;
}
void TxList::List(std::vector<WalletTxRecord>& tx_records, const CWalletTx& wtx, const isminefilter& filter_ismine)
{
CAmount nCredit = wtx.GetCredit(filter_ismine);
CAmount nDebit = wtx.GetDebit(filter_ismine);
CAmount nNet = nCredit - nDebit;
if (nNet > 0 || wtx.IsCoinBase() || wtx.IsHogEx()) {
// Credit
List_Credit(tx_records, wtx, filter_ismine);
} else if (IsAllFromMe(wtx)) {
if (IsAllToMe(wtx)) {
// Payment to Self
List_SelfSend(tx_records, wtx, filter_ismine);
} else {
// Debit
List_Debit(tx_records, wtx, filter_ismine);
}
} else {
// Mixed debit transaction, can't break down payees
WalletTxRecord tx_record(&m_wallet, &wtx);
tx_record.type = WalletTxRecord::Type::Other;
tx_record.debit = nNet;
tx_records.push_back(std::move(tx_record));
}
}
void TxList::List_Credit(std::vector<WalletTxRecord>& tx_records, const CWalletTx& wtx, const isminefilter& filter_ismine)
{
std::vector<CTxOutput> outputs = wtx.GetOutputs();
for (size_t i = 0; i < outputs.size(); i++) {
const CTxOutput& output = outputs[i];
if (!output.IsMWEB() && output.GetScriptPubKey().IsMWEBPegin()) {
continue;
}
isminetype ismine = m_wallet.IsMine(output);
if (ismine & filter_ismine) {
// Skip displaying hog-ex outputs when we have the MWEB transaction that contains the pegout.
// The original MWEB transaction will be displayed instead.
if (wtx.IsHogEx() && wtx.pegout_indices.size() > i) {
mw::Hash kernel_id = wtx.pegout_indices[i].first;
if (m_wallet.FindWalletTxByKernelId(kernel_id) != nullptr) {
continue;
}
}
WalletTxRecord sub(&m_wallet, &wtx, output.GetIndex());
sub.credit = m_wallet.GetValue(output);
sub.involvesWatchAddress = ismine & ISMINE_WATCH_ONLY;
if (IsAddressMine(output)) {
// Received by Litecoin Address
sub.type = WalletTxRecord::Type::RecvWithAddress;
sub.address = GetAddress(output).Encode();
} else {
// Received by IP connection (deprecated features), or a multisignature or other non-simple transaction
sub.type = WalletTxRecord::Type::RecvFromOther;
sub.address = wtx.mapValue.at("from");
}
if (wtx.IsCoinBase()) {
sub.type = WalletTxRecord::Type::Generated;
}
tx_records.push_back(sub);
}
}
// Include pegouts to addresses belonging to the wallet.
if (wtx.tx->HasMWEBTx()) {
for (const Kernel& kernel : wtx.tx->mweb_tx.m_transaction->GetKernels()) {
for (size_t i = 0; i < kernel.GetPegOuts().size(); i++) {
const PegOutCoin& pegout = kernel.GetPegOuts()[i];
if (!(m_wallet.IsMine(DestinationAddr{pegout.GetScriptPubKey()}) & filter_ismine)) {
continue;
}
WalletTxRecord tx_record(&m_wallet, &wtx, PegoutIndex{kernel.GetKernelID(), i});
tx_record.type = WalletTxRecord::Type::RecvWithAddress;
tx_record.credit = pegout.GetAmount();
tx_record.address = DestinationAddr(pegout.GetScriptPubKey()).Encode();
tx_records.push_back(std::move(tx_record));
}
}
}
}
void TxList::List_Debit(std::vector<WalletTxRecord>& tx_records, const CWalletTx& wtx, const isminefilter& filter_ismine)
{
CAmount nTxFee = wtx.GetFee(filter_ismine);
for (const CTxOutput& output : wtx.GetOutputs()) {
// If the output is a peg-in script, and we have the MWEB pegin tx, then ignore the peg-in script output.
// If it's a peg-in script, and we don't have the MWEB pegin tx, treat the output as a spend.
mw::Hash kernel_id;
if (!output.IsMWEB() && output.GetScriptPubKey().IsMWEBPegin(&kernel_id)) {
if (wtx.tx->HasMWEBTx() && wtx.tx->mweb_tx.GetKernelIDs().count(kernel_id) > 0) {
continue;
}
}
if (m_wallet.IsMine(output)) {
// Ignore parts sent to self, as this is usually the change
// from a transaction sent back to our own address.
continue;
}
WalletTxRecord tx_record(&m_wallet, &wtx, output.GetIndex());
tx_record.debit = -m_wallet.GetValue(output);
DestinationAddr address = GetAddress(output);
if (!address.IsEmpty()) {
// Sent to Litecoin Address
tx_record.type = WalletTxRecord::Type::SendToAddress;
tx_record.address = address.Encode();
} else {
// Sent to IP, or other non-address transaction like OP_EVAL
tx_record.type = WalletTxRecord::Type::SendToOther;
tx_record.address = wtx.mapValue.at("to");
}
/* Add fee to first output */
if (nTxFee > 0) {
tx_record.fee = -nTxFee;
nTxFee = 0;
}
tx_records.push_back(tx_record);
}
if (wtx.tx->HasMWEBTx()) {
for (const Kernel& kernel : wtx.tx->mweb_tx.m_transaction->GetKernels()) {
for (size_t i = 0; i < kernel.GetPegOuts().size(); i++) {
const PegOutCoin& pegout = kernel.GetPegOuts()[i];
if (m_wallet.IsMine(DestinationAddr{pegout.GetScriptPubKey()})) {
// Ignore parts sent to self, as this is usually the change
// from a transaction sent back to our own address.
continue;
}
WalletTxRecord tx_record(&m_wallet, &wtx, PegoutIndex{kernel.GetKernelID(), i});
tx_record.debit = -pegout.GetAmount();
tx_record.type = WalletTxRecord::Type::SendToAddress;
tx_record.address = DestinationAddr(pegout.GetScriptPubKey()).Encode();
/* Add fee to first output */
if (nTxFee > 0) {
tx_record.fee = -nTxFee;
nTxFee = 0;
}
tx_records.push_back(std::move(tx_record));
}
}
}
}
void TxList::List_SelfSend(std::vector<WalletTxRecord>& tx_records, const CWalletTx& wtx, const isminefilter& filter_ismine)
{
std::string address;
for (const CTxOutput& output : wtx.GetOutputs()) {
if (!output.IsMWEB() && output.GetScriptPubKey().IsMWEBPegin()) {
continue;
}
if (!address.empty()) address += ", ";
address += GetAddress(output).Encode();
}
for (const PegOutCoin& pegout : wtx.tx->mweb_tx.GetPegOuts()) {
if (!address.empty()) address += ", ";
address += DestinationAddr(pegout.GetScriptPubKey()).Encode();
}
CAmount nCredit = wtx.GetCredit(filter_ismine);
CAmount nDebit = wtx.GetDebit(filter_ismine);
CAmount nChange = wtx.GetChange();
WalletTxRecord tx_record(&m_wallet, &wtx);
tx_record.type = WalletTxRecord::SendToSelf;
tx_record.address = address;
tx_record.debit = -(nDebit - nChange);
tx_record.credit = nCredit - nChange;
tx_records.push_back(std::move(tx_record));
}
isminetype TxList::IsAddressMine(const CTxOutput& txout)
{
CTxDestination dest;
return m_wallet.ExtractOutputDestination(txout, dest) ? m_wallet.IsMine(dest) : ISMINE_NO;
}
DestinationAddr TxList::GetAddress(const CTxOutput& output)
{
if (!output.IsMWEB()) {
return output.GetTxOut().scriptPubKey;
}
mw::Coin coin;
if (m_wallet.GetCoin(output.ToMWEB(), coin)) {
StealthAddress addr;
if (m_wallet.GetMWWallet()->GetStealthAddress(coin, addr)) {
return addr;
}
}
return DestinationAddr{};
}
bool TxList::IsAllFromMe(const CWalletTx& wtx)
{
for (const CTxInput& input : wtx.GetInputs()) {
if (!m_wallet.IsMine(input)) {
return false;
}
}
return true;
}
bool TxList::IsAllToMe(const CWalletTx& wtx)
{
for (const CTxOutput& output : wtx.GetOutputs()) {
// If we don't have the MWEB peg-in tx, then we treat it as an output not belonging to ourselves.
mw::Hash kernel_id;
if (!output.IsMWEB() && output.GetScriptPubKey().IsMWEBPegin(&kernel_id)) {
if (wtx.tx->HasMWEBTx() && wtx.tx->mweb_tx.GetKernelIDs().count(kernel_id) > 0) {
return false;
}
}
if (!m_wallet.IsMine(output)) {
return false;
}
}
// Also check pegouts
for (const PegOutCoin& pegout : wtx.tx->mweb_tx.GetPegOuts()) {
if (!m_wallet.IsMine(DestinationAddr{pegout.GetScriptPubKey()})) {
return false;
}
}
return true;
}

40
src/wallet/txlist.h Normal file
View File

@ -0,0 +1,40 @@
#pragma once
#include <script/address.h>
#include <wallet/txrecord.h>
#include <wallet/ismine.h>
#include <boost/optional.hpp>
// Forward Declarations
class CTxOutput;
class CWallet;
class CWalletTx;
class TxList
{
const CWallet& m_wallet;
public:
TxList(const CWallet& wallet)
: m_wallet(wallet) {}
std::vector<WalletTxRecord> ListAll(const isminefilter& filter_ismine = ISMINE_ALL);
std::vector<WalletTxRecord> List(
const CWalletTx& wtx,
const isminefilter& filter_ismine,
const boost::optional<int>& nMinDepth,
const boost::optional<std::string>& filter_label
);
private:
void List(std::vector<WalletTxRecord>& tx_records, const CWalletTx& wtx, const isminefilter& filter_ismine);
void List_Credit(std::vector<WalletTxRecord>& tx_records, const CWalletTx& wtx, const isminefilter& filter_ismine);
void List_Debit(std::vector<WalletTxRecord>& tx_records, const CWalletTx& wtx, const isminefilter& filter_ismine);
void List_SelfSend(std::vector<WalletTxRecord>& tx_records, const CWalletTx& wtx, const isminefilter& filter_ismine);
isminetype IsAddressMine(const CTxOutput& txout);
DestinationAddr GetAddress(const CTxOutput& output);
bool IsAllFromMe(const CWalletTx& wtx);
bool IsAllToMe(const CWalletTx& wtx);
};

207
src/wallet/txrecord.cpp Normal file
View File

@ -0,0 +1,207 @@
#include <wallet/txrecord.h>
#include <wallet/wallet.h>
#include <wallet/txrecord.h>
#include <wallet/wallet.h>
#include <chain.h>
#include <core_io.h>
#include <interfaces/chain.h>
#include <key_io.h>
#include <util/check.h>
#include <chrono>
using namespace std::chrono;
bool WalletTxRecord::UpdateStatusIfNeeded(const uint256& block_hash)
{
assert(!block_hash.IsNull());
// Check if update is needed
if (status.m_cur_block_hash == block_hash && !status.needsUpdate) {
return false;
}
// Try locking the wallet
TRY_LOCK(m_pWallet->cs_wallet, locked_wallet);
if (!locked_wallet) {
return false;
}
const int last_block_height = m_pWallet->GetLastBlockHeight();
int64_t block_time = -1;
CHECK_NONFATAL(m_pWallet->chain().findBlock(m_pWallet->GetLastBlockHash(), interfaces::FoundBlock().time(block_time)));
// Sort order, unrecorded transactions sort to the top
// Sub-components sorted with standard outputs first, MWEB outputs second, then MWEB pegouts third.
std::string idx = strprintf("0%03d", 0);
if (index) {
if (index->type() == typeid(int)) {
idx = strprintf("0%03d", boost::get<int>(*index));
} else if (index->type() == typeid(mw::Hash)) {
idx = "1" + boost::get<mw::Hash>(*index).ToHex();
} else {
const PegoutIndex& pegout_idx = boost::get<PegoutIndex>(*index);
idx = "2" + pegout_idx.kernel_id.ToHex() + strprintf("%03d", pegout_idx.pos);
}
}
int block_height = m_wtx->m_confirm.block_height > 0 ? m_wtx->m_confirm.block_height : std::numeric_limits<int>::max();
int blocks_to_maturity = m_wtx->GetBlocksToMaturity();
status.sortKey = strprintf("%010d-%01d-%010u-%s",
block_height,
m_wtx->IsCoinBase() ? 1 : 0,
m_wtx->nTimeReceived,
idx);
status.countsForBalance = m_wtx->IsTrusted() && !(blocks_to_maturity > 0);
status.depth = m_wtx->GetDepthInMainChain();
status.m_cur_block_hash = block_hash;
if (m_wtx->IsInMainChain()) {
status.matures_in = blocks_to_maturity;
}
const int64_t time_since_epoch = (int64_t)duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
const bool up_to_date = (time_since_epoch - block_time < MAX_BLOCK_TIME_GAP);
if (up_to_date && !m_wtx->IsFinal()) {
if (m_wtx->tx->nLockTime < LOCKTIME_THRESHOLD) {
status.status = WalletTxStatus::OpenUntilBlock;
status.open_for = m_wtx->tx->nLockTime - last_block_height;
} else {
status.status = WalletTxStatus::OpenUntilDate;
status.open_for = m_wtx->tx->nLockTime;
}
}
// For generated transactions, determine maturity
else if (type == WalletTxRecord::Type::Generated || m_wtx->IsHogEx()) {
if (blocks_to_maturity > 0) {
status.status = WalletTxStatus::Immature;
if (!m_wtx->IsInMainChain()) {
status.status = WalletTxStatus::NotAccepted;
}
} else {
status.status = WalletTxStatus::Confirmed;
}
} else {
if (status.depth < 0) {
status.status = WalletTxStatus::Conflicted;
} else if (status.depth == 0) {
status.status = WalletTxStatus::Unconfirmed;
if (m_wtx->isAbandoned())
status.status = WalletTxStatus::Abandoned;
} else if (status.depth < RecommendedNumConfirmations) {
status.status = WalletTxStatus::Confirming;
} else {
status.status = WalletTxStatus::Confirmed;
}
}
status.needsUpdate = false;
return true;
}
const uint256& WalletTxRecord::GetTxHash() const
{
assert(m_wtx != nullptr);
return m_wtx->GetHash();
}
std::string WalletTxRecord::GetTxString() const
{
assert(m_wtx != nullptr);
if (m_wtx->IsPartialMWEB()) {
if (m_wtx->mweb_wtx_info->received_coin) {
const mw::Coin& received_coin = *m_wtx->mweb_wtx_info->received_coin;
return strprintf("MWEB Output(ID=%s, amount=%d)", received_coin.output_id.ToHex(), received_coin.amount);
} else if (m_wtx->mweb_wtx_info->spent_input) {
return strprintf("MWEB Input(ID=%s)\n", m_wtx->mweb_wtx_info->spent_input->ToHex());
}
}
return m_wtx->tx->ToString();
}
int64_t WalletTxRecord::GetTxTime() const
{
assert(m_wtx != nullptr);
return m_wtx->GetTxTime();
}
std::string WalletTxRecord::GetComponentIndex() const
{
std::string idx;
if (this->index) {
if (this->index->type() == typeid(int)) {
idx = std::to_string(boost::get<int>(*index));
} else if (index->type() == typeid(mw::Hash)) {
idx = "MWEB Output (" + boost::get<mw::Hash>(*index).ToHex() + ")";
} else {
const PegoutIndex& pegout_idx = boost::get<PegoutIndex>(*index);
idx = "Pegout (" + pegout_idx.kernel_id.ToHex() + ":" + std::to_string(pegout_idx.pos) + ")";
}
}
return idx;
}
std::string WalletTxRecord::GetType() const
{
switch (this->type) {
case Other: return "Other";
case Generated: return "Generated";
case SendToAddress: return "SendToAddress";
case SendToOther: return "SendToOther";
case RecvWithAddress: return "RecvWithAddress";
case RecvFromOther: return "RecvFromOther";
case SendToSelf: return "SendToSelf";
}
assert(false);
}
UniValue WalletTxRecord::ToUniValue() const
{
UniValue entry(UniValue::VOBJ);
entry.pushKV("type", GetType());
entry.pushKV("amount", ValueFromAmount(GetAmount()));
entry.pushKV("net", ValueFromAmount(GetNet()));
CTxDestination destination = DecodeDestination(this->address);
if (m_wtx->IsFromMe(ISMINE_WATCH_ONLY) || (IsValidDestination(destination) && (m_pWallet->IsMine(destination) & ISMINE_WATCH_ONLY))) {
entry.pushKV("involvesWatchonly", true);
}
if (IsValidDestination(destination)) {
const auto* address_book_entry = m_pWallet->FindAddressBookEntry(destination);
if (address_book_entry) {
entry.pushKV("label", address_book_entry->GetLabel());
}
}
if (!this->address.empty()) {
entry.pushKV("address", this->address);
}
if (this->index) {
if (this->index->type() == typeid(int)) {
entry.pushKV("vout", boost::get<int>(*this->index));
} else if (this->index->type() == typeid(mw::Hash)) {
entry.pushKV("mweb_out", boost::get<mw::Hash>(*this->index).ToHex());
} else {
const PegoutIndex& pegout_idx = boost::get<PegoutIndex>(*index);
entry.pushKV("pegout", pegout_idx.kernel_id.ToHex() + ":" + std::to_string(pegout_idx.pos));
}
}
if (this->debit > 0) {
entry.pushKV("fee", ValueFromAmount(-this->fee));
entry.pushKV("abandoned", m_wtx->isAbandoned());
}
return entry;
}

130
src/wallet/txrecord.h Normal file
View File

@ -0,0 +1,130 @@
#pragma once
#include <amount.h>
#include <primitives/transaction.h>
#include <univalue.h>
#include <boost/optional.hpp>
#include <boost/variant.hpp>
// Forward Declarations
class CWallet;
class CWalletTx;
struct PegoutIndex
{
// The ID of the kernel containing the pegout.
mw::Hash kernel_id;
// The position of the PegOutCoin within the kernel.
// Ex: If a kernel has 3 pegouts, the last one will have a pos of 2.
size_t pos;
};
struct WalletTxStatus
{
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;
int64_t depth;
int64_t 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;
};
class WalletTxRecord
{
public:
WalletTxRecord(const CWallet* pWallet, const CWalletTx* wtx, const OutputIndex& output_index)
: m_pWallet(pWallet), m_wtx(wtx)
{
if (output_index.type() == typeid(mw::Hash)) {
index = boost::make_optional(boost::get<mw::Hash>(output_index));
} else {
index = boost::make_optional((int)boost::get<COutPoint>(output_index).n);
}
}
WalletTxRecord(const CWallet* pWallet, const CWalletTx* wtx, const PegoutIndex& pegout_index)
: m_pWallet(pWallet), m_wtx(wtx), index(boost::make_optional(pegout_index)) {}
WalletTxRecord(const CWallet* pWallet, const CWalletTx* wtx)
: m_pWallet(pWallet), m_wtx(wtx), index(boost::none) {}
static const int RecommendedNumConfirmations = 6;
enum Type {
Other,
Generated,
SendToAddress,
SendToOther,
RecvWithAddress,
RecvFromOther,
SendToSelf,
};
Type type{Type::Other};
std::string address{};
CAmount debit{0};
CAmount credit{0};
CAmount fee{0};
bool involvesWatchAddress{false};
// Cached status attributes
WalletTxStatus status;
// Updates the transaction record's cached status attributes.
bool UpdateStatusIfNeeded(const uint256& block_hash);
const CWalletTx& GetWTX() const noexcept { return *m_wtx; }
const uint256& GetTxHash() const;
std::string GetTxString() const;
int64_t GetTxTime() const;
CAmount GetAmount() const noexcept { return credit > 0 ? credit : debit; }
//CAmount GetAmount() const noexcept { return credit + debit; }
CAmount GetNet() const noexcept { return credit + debit + fee; }
UniValue ToUniValue() const;
// Returns the formatted component index.
std::string GetComponentIndex() const;
private:
// Pointer to the CWallet instance
const CWallet* m_pWallet;
// The actual CWalletTx
const CWalletTx* m_wtx;
// The index of the transaction component this record is for.
boost::optional<boost::variant<int, mw::Hash, PegoutIndex>> index;
std::string GetType() const;
};

View File

@ -31,7 +31,7 @@
#include <util/string.h>
#include <util/translation.h>
#include <wallet/coincontrol.h>
#include <wallet/createtransaction.h>
#include <wallet/txassembler.h>
#include <wallet/fees.h>
#include <wallet/reserve.h>
@ -988,11 +988,22 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const boost::optional<MWEB::
bool CWallet::LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx)
{
const auto& ins = mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(hash), std::forward_as_tuple(this, nullptr));
CWalletTx& wtx = ins.first->second;
if (!fill_wtx(wtx, ins.second)) {
CWalletTx wtx_tmp(this, nullptr);
if (!fill_wtx(wtx_tmp, true)) {
return false;
}
uint256 wtx_hash = wtx_tmp.GetHash();
if (mapWallet.count(wtx_hash) > 0 && wtx_tmp.tx->IsNull()) {
LogPrintf("%s already exists\n", wtx_hash.ToString());
return true;
}
const auto& ins = mapWallet.emplace(wtx_hash, std::move(wtx_tmp));
assert(ins.second);
CWalletTx& wtx = ins.first->second;
// If wallet doesn't have a chain (e.g wallet-tool), don't bother to update txn.
if (HaveChain()) {
Optional<int> block_height = chain().getBlockHeight(wtx.m_confirm.hashBlock);
@ -1016,7 +1027,7 @@ bool CWallet::LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx
if (/* insertion took place */ ins.second) {
wtx.m_it_wtxOrdered = wtxOrdered.insert(std::make_pair(wtx.nOrderPos, &wtx));
}
AddToSpends(hash);
AddToSpends(wtx.GetHash());
AddMWEBOrigins(wtx);
for (const CTxInput& txin : wtx.GetInputs()) {
CWalletTx* prevtx = FindPrevTx(txin);
@ -1323,6 +1334,14 @@ void CWallet::blockConnected(const CBlock& block, int height)
WalletBatch(*database).WriteTx(*hogex_wtx);
}
for (const Kernel& kernel : block.mweb_block.m_block->GetKernels()) {
auto wtx = FindWalletTxByKernelId(kernel.GetKernelID());
if (wtx != nullptr) {
SyncTransaction(wtx->tx, wtx->mweb_wtx_info, {CWalletTx::Status::CONFIRMED, height, block_hash, wtx->m_confirm.nIndex});
transactionRemovedFromMempool(wtx->tx, MemPoolRemovalReason::BLOCK, 0 /* mempool_sequence */);
}
}
mw::Coin mweb_coin;
for (const Output& output : block.mweb_block.m_block->GetOutputs()) {
if (mweb_wallet->RewindOutput(output, mweb_coin)) {
@ -1926,6 +1945,35 @@ void CWalletTx::GetAmounts(std::list<COutputEntry>& listReceived,
if (fIsMine & filter)
listReceived.push_back(output);
}
// MWEB: Treat pegouts as outputs
for (const PegOutCoin& pegout : tx->mweb_tx.GetPegOuts()) {
DestinationAddr dest_addr{pegout.GetScriptPubKey()};
isminetype fIsMine = pwallet->IsMine(dest_addr);
// Only need to handle txouts if AT LEAST one of these is true:
// 1) they debit from us (sent)
// 2) the output is to us (received)
if (nDebit > 0) {
// Don't report 'change' txouts
if (pwallet->IsChange(dest_addr))
continue;
} else if (!(fIsMine & filter))
continue;
CTxDestination address;
dest_addr.ExtractDestination(address);
COutputEntry output = {address, pegout.GetAmount(), mw::Hash()};
// If we are debited by the transaction, add the output as a "sent" entry
if (nDebit > 0)
listSent.push_back(output);
// If we are receiving the output, add it as a "received" entry
if (fIsMine & filter)
listReceived.push_back(output);
}
}
/**
@ -2035,7 +2083,13 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc
}
if (!block.mweb_block.IsNull()) {
// MW: TODO - Pegouts?
for (const Kernel& kernel : block.mweb_block.m_block->GetKernels()) {
const CWalletTx* wtx = FindWalletTxByKernelId(kernel.GetKernelID());
if (wtx) {
SyncTransaction(wtx->tx, wtx->mweb_wtx_info, {CWalletTx::Status::CONFIRMED, block_height, block_hash, wtx->m_confirm.nIndex}, fUpdate);
}
}
mw::Coin mweb_coin;
for (const Output& output : block.mweb_block.m_block->GetOutputs()) {
if (mweb_wallet->RewindOutput(output, mweb_coin)) {
@ -2347,6 +2401,10 @@ bool CWallet::IsTrusted(const CWalletTx& wtx, std::set<uint256>& trusted_parents
int nDepth = wtx.GetDepthInMainChain();
if (nDepth >= 1) return true;
if (nDepth < 0) return false;
// If the HogEx is not in the main chain, then we should assume it has been replaced during a reorg.
if (wtx.IsHogEx()) return false;
// using wtx's cached debit
if (!m_spend_zero_conf_change || !wtx.IsFromMe(ISMINE_ALL)) return false;
@ -2598,7 +2656,7 @@ void CWallet::AvailableCoins(std::vector<COutputCoin>& vCoins, bool fOnlySafe, c
if (output.IsMWEB()) {
mw::Coin coin;
if (!GetCoin(output.ToMWEB(), coin)) {
if (!GetCoin(output.ToMWEB(), coin) || !coin.IsMine()) {
continue;
}
@ -3014,27 +3072,36 @@ bool CWallet::CreateTransaction(
bool sign)
{
int nChangePosIn = nChangePosInOut;
CTransactionRef tx2 = tx;
bool res = CreateTransactionEx(*this, vecSend, tx, nFeeRet, nChangePosInOut, error, coin_control, fee_calc_out, sign);
Optional<AssembledTx> tx1 = TxAssembler(*this).AssembleTx(vecSend, coin_control, nChangePosIn, sign, error);
if (tx1) {
tx = tx1->tx;
nFeeRet = tx1->fee;
nChangePosInOut = tx1->change_position;
fee_calc_out = tx1->fee_calc;
}
// try with avoidpartialspends unless it's enabled already
if (res && nFeeRet > 0 /* 0 means non-functional fee rate estimation */ && m_max_aps_fee > -1 && !coin_control.m_avoid_partial_spends) {
if (tx1 && tx1->fee > 0 /* 0 means non-functional fee rate estimation */ && m_max_aps_fee > -1 && !coin_control.m_avoid_partial_spends) {
CCoinControl tmp_cc = coin_control;
tmp_cc.m_avoid_partial_spends = true;
CAmount nFeeRet2;
int nChangePosInOut2 = nChangePosIn;
bilingual_str error2; // fired and forgotten; if an error occurs, we discard the results
if (CreateTransactionEx(*this, vecSend, tx2, nFeeRet2, nChangePosInOut2, error2, tmp_cc, fee_calc_out, sign)) {
Optional<AssembledTx> tx2 = TxAssembler(*this).AssembleTx(vecSend, tmp_cc, nChangePosIn, sign, error2);
if (tx2) {
// if fee of this alternative one is within the range of the max fee, we use this one
const bool use_aps = nFeeRet2 <= nFeeRet + m_max_aps_fee;
WalletLogPrintf("Fee non-grouped = %lld, grouped = %lld, using %s\n", nFeeRet, nFeeRet2, use_aps ? "grouped" : "non-grouped");
const bool use_aps = tx2->fee <= tx1->fee + m_max_aps_fee;
WalletLogPrintf("Fee non-grouped = %lld, grouped = %lld, using %s\n", tx1->fee, tx2->fee, use_aps ? "grouped" : "non-grouped");
if (use_aps) {
tx = tx2;
nFeeRet = nFeeRet2;
nChangePosInOut = nChangePosInOut2;
tx = tx2->tx;
nFeeRet = tx2->fee;
nChangePosInOut = tx2->change_position;
fee_calc_out = tx2->fee_calc;
}
}
}
return res;
return !!tx1;
}
void CWallet::CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::vector<std::pair<std::string, std::string>> orderForm)
@ -4071,6 +4138,11 @@ bool CWalletTx::IsImmature() const
return GetBlocksToMaturity() > 0;
}
bool CWalletTx::IsFinal() const
{
return pwallet->chain().checkFinalTx(*tx);
}
std::vector<OutputGroup> CWallet::GroupOutputs(const std::vector<COutputCoin>& outputs, bool single_coin, const size_t max_ancestors) const {
std::vector<OutputGroup> groups;
std::map<CTxDestination, OutputGroup> gmap;
@ -4517,6 +4589,20 @@ bool CWallet::ExtractDestinationScript(const CTxOutput& output, DestinationAddr&
return true;
}
const CWalletTx* CWallet::FindWalletTxByKernelId(const mw::Hash& kernel_id) const
{
LOCK(cs_wallet);
auto kernel_iter = mapKernelsMWEB.find(kernel_id);
if (kernel_iter != mapOutputsMWEB.end()) {
auto tx_iter = mapWallet.find(kernel_iter->second);
if (tx_iter != mapWallet.end()) {
return &tx_iter->second;
}
}
return nullptr;
}
const CWalletTx* CWallet::FindWalletTx(const OutputIndex& output) const
{
LOCK(cs_wallet);

View File

@ -349,6 +349,8 @@ public:
mweb_wtx_info = mweb_wtx_info_;
}
CWalletTx(CWalletTx&& wtx) = default;
void Init()
{
mapValue.clear();
@ -595,6 +597,7 @@ public:
bool IsCoinBase() const { return tx->IsCoinBase(); }
bool IsHogEx() const { return tx->IsHogEx(); }
bool IsImmature() const;
bool IsFinal() const;
// Disable copying of CWalletTx objects to prevent bugs where instances get
// copied in and out of the mapWallet map, and fields are updated in the
@ -1436,6 +1439,7 @@ public:
bool ExtractOutputDestination(const CTxOutput& output, CTxDestination& dest) const;
bool ExtractDestinationScript(const CTxOutput& output, DestinationAddr& dest) const;
const CWalletTx* FindWalletTxByKernelId(const mw::Hash& kernel_id) const;
const CWalletTx* FindWalletTx(const OutputIndex& output) const;
const CWalletTx* FindPrevTx(const CTxInput& input) const;
CWalletTx* FindPrevTx(const CTxInput& input);

View File

@ -262,6 +262,7 @@ public:
bool fIsEncrypted{false};
bool fAnyUnordered{false};
std::vector<uint256> vWalletUpgrade;
std::vector<uint256> vWalletRemove;
std::map<OutputType, uint256> m_active_external_spks;
std::map<OutputType, uint256> m_active_internal_spks;
std::map<uint256, DescriptorCache> m_descriptor_caches;
@ -304,8 +305,16 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
auto fill_wtx = [&](CWalletTx& wtx, bool new_tx) {
assert(new_tx);
ssValue >> wtx;
if (wtx.GetHash() != hash)
return false;
if (wtx.GetHash() != hash) {
// We previously calculated hash for mweb_wtx_info in an impractical way.
// We changed to just using the output ID as hash, so need to upgrade any existing txs.
if (wtx.mweb_wtx_info) {
wss.vWalletRemove.push_back(hash);
wss.vWalletUpgrade.push_back(wtx.GetHash());
} else {
return false;
}
}
// Undo serialize changes in 31600
if (31404 <= wtx.fTimeReceivedIsTxTime && wtx.fTimeReceivedIsTxTime <= 31703)
@ -833,6 +842,10 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
for (const uint256& hash : wss.vWalletUpgrade)
WriteTx(pwallet->mapWallet.at(hash));
for (const uint256& hash : wss.vWalletRemove) {
EraseTx(hash);
}
// Rewrite encrypted wallets of versions 0.4.0 and 0.5.0rc:
if (wss.fIsEncrypted && (last_client == 40000 || last_client == 50000))
return DBErrors::NEED_REWRITE;

View File

@ -111,7 +111,7 @@ class MiningTest(BitcoinTestFramework):
block.vtx = [coinbase_tx]
self.log.info("getblocktemplate: segwit rule must be set")
assert_raises_rpc_error(-8, "getblocktemplate must be called with the segwit rule set", node.getblocktemplate)
assert_raises_rpc_error(-8, "getblocktemplate must be called with the segwit & mweb rule sets", node.getblocktemplate)
self.log.info("getblocktemplate: Test valid block")
assert_template(node, block, None)

View File

@ -10,6 +10,7 @@ from test_framework.util import assert_equal
class MWEBBasicTest(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = True
self.extra_args = [['-whitelist=noban@127.0.0.1'],[]] # immediate tx relay
self.num_nodes = 2
def skip_test_if_missing_module(self):
@ -54,9 +55,7 @@ class MWEBBasicTest(BitcoinTestFramework):
self.log.info("Send MWEB coins to node 1")
addr1 = self.nodes[1].getnewaddress(address_type='mweb')
tx1_hash = self.nodes[0].sendtoaddress(addr1, 5)
tx1 = self.nodes[0].getmempoolentry(tx1_hash)
self.log.info("tx1: {}".format(tx1))
self.nodes[0].sendtoaddress(addr1, 5)
self.nodes[0].generate(1)
self.sync_all()

View File

@ -25,7 +25,10 @@ class MWEBWalletBasicTest(BitcoinTestFramework):
self.log.info("Setting up MWEB chain")
setup_mweb_chain(node0)
self.sync_all()
#
# Send to node1 mweb
#
self.log.info("Send to node1 mweb address")
n1_addr = node1.getnewaddress(address_type='mweb')
tx1_id = node0.sendtoaddress(n1_addr, 25)
@ -63,28 +66,40 @@ class MWEBWalletBasicTest(BitcoinTestFramework):
assert n0_tx1['amount'] == -25
assert n0_tx1['fee'] < 0 and n0_tx1['fee'] > -0.1
#
# Pegout to node2
#
self.log.info("Send (pegout) to node2 bech32 address")
n2_addr = node2.getnewaddress(address_type='bech32')
self.log.info("Address: {}".format(n2_addr))
tx2_id = node1.sendtoaddress(n2_addr, 15)
self.sync_mempools()
self.log.info("Verify node1's wallet lists the transaction as spent")
n1_tx2 = node1.gettransaction(txid=tx2_id)
self.log.info(n1_tx2)
assert_equal(n1_tx2['confirmations'], 0)
assert_equal(n1_tx2['amount'], -15)
assert n1_tx2['fee'] < 0 and n1_tx2['fee'] > -0.1
self.log.info("Verify node2's wallet receives the first pegout transaction")
n2_tx2 = node2.gettransaction(txid=tx2_id)
self.log.info(n2_tx2)
#n2_addr_coins = node2.listreceivedbyaddress(minconf=0, address_filter=n2_addr)
assert_equal(n2_tx2['amount'], 15)
assert_equal(n2_tx2['confirmations'], 0)
assert tx2_id in node1.getrawmempool()
self.log.info("Mine next block to make sure the transaction confirms successfully")
node0.generate(1)
self.sync_all()
assert tx2_id not in node1.getrawmempool()
self.log.info("Verify node2's wallet receives the first pegout transaction")
n2_addr_coins = node2.listreceivedbyaddress(minconf=0, address_filter=n2_addr)
assert_equal(len(n2_addr_coins), 1)
assert_equal(n2_addr_coins[0]['amount'], 15)
assert_equal(n2_addr_coins[0]['confirmations'], 1)
#
# Pegout to node2 using subtract fee from amount
#
self.log.info("Send (pegout) to node2 bech32 address")
n2_addr2 = node2.getnewaddress(address_type='bech32')
tx3_id = node1.sendtoaddress(address=n2_addr2, amount=5, subtractfeefromamount=True)
@ -96,25 +111,16 @@ class MWEBWalletBasicTest(BitcoinTestFramework):
assert n1_tx3['amount'] > -5 and n1_tx3['amount'] < -4.9
assert n1_tx3['fee'] < 0 and n1_tx3['fee'] > -0.1
assert tx2_id in node1.getrawmempool()
assert tx3_id in node1.getrawmempool()
self.log.info("Mine next block so node2 sees the transactions")
node0.generate(2)
self.log.info("Mine next block so node2 sees the transaction")
node0.generate(1)
self.sync_all()
assert tx2_id not in node1.getrawmempool()
assert tx3_id not in node1.getrawmempool()
self.log.info("Verify node2's wallet receives the first pegout transaction")
n2_addr_coins = node2.listreceivedbyaddress(minconf=0, address_filter=n2_addr)
assert_equal(len(n2_addr_coins), 1)
assert_equal(n2_addr_coins[0]['amount'], 15)
assert_equal(n2_addr_coins[0]['confirmations'], 1)
self.log.info("Verify node2's wallet receives the second pegout transaction")
n2_addr2_coins = node2.listreceivedbyaddress(minconf=0)
self.log.info(n2_addr2_coins)
n2_addr2_coins = node2.listreceivedbyaddress(minconf=0, address_filter=n2_addr2)
assert_equal(len(n2_addr2_coins), 1)
assert n2_addr2_coins[0]['amount'] < 5 and n2_addr2_coins[0]['amount'] > 4.9
assert_equal(n2_addr2_coins[0]['confirmations'], 1)
@ -127,6 +133,5 @@ class MWEBWalletBasicTest(BitcoinTestFramework):
# TODO: Conflicting txs
# TODO: Duplicate hash
if __name__ == '__main__':
MWEBWalletBasicTest().main()

View File

@ -252,6 +252,7 @@ BASE_SCRIPTS = [
'mweb_node_compatibility.py',
'mweb_wallet_address.py',
'mweb_wallet_basic.py',
'wallet_listwallettransactions.py',
'rpc_uptime.py',
'wallet_resendwallettransactions.py',
'wallet_resendwallettransactions.py --descriptors',

View File

@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""Test the listwallettransactions API for MWEB transactions."""
from decimal import Decimal
from io import BytesIO
from test_framework.messages import COIN, CTransaction
from test_framework.test_framework import BitcoinTestFramework
from test_framework.ltc_util import setup_mweb_chain
from test_framework.util import (
assert_array_result,
assert_equal,
hex_str_to_bytes,
)
def tx_from_hex(hexstring):
tx = CTransaction()
f = BytesIO(hex_str_to_bytes(hexstring))
tx.deserialize(f)
return tx
class ListWalletTransactionsTest(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = True
self.num_nodes = 3
self.extra_args = [['-whitelist=noban@127.0.0.1'],['-whitelist=noban@127.0.0.1'],[]] # immediate tx relay
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
def run_test(self):
node0 = self.nodes[0]
node1 = self.nodes[1]
node2 = self.nodes[2]
# Setup MWEB chain using node0 as miner
setup_mweb_chain(node0)
self.sync_all()
# Simple send, 0 to 1:
txid = node0.sendtoaddress(node1.getnewaddress(), 0.1)
self.sync_all()
assert_array_result(node0.listwallettransactions(txid),
{"txid": txid},
{"type": "SendToAddress", "amount": Decimal("-0.1"), "confirmations": 0})
assert_array_result(node1.listwallettransactions(),
{"txid": txid},
{"type": "RecvWithAddress", "amount": Decimal("0.1"), "confirmations": 0})
# mine a block, confirmations should change:
blockhash = node0.generate(1)[0]
blockheight = node0.getblockheader(blockhash)['height']
self.sync_all()
assert_array_result(node0.listwallettransactions(),
{"txid": txid},
{"type": "SendToAddress", "amount": Decimal("-0.1"), "confirmations": 1, "blockhash": blockhash, "blockheight": blockheight})
assert_array_result(node1.listwallettransactions(),
{"txid": txid},
{"type": "RecvWithAddress", "amount": Decimal("0.1"), "confirmations": 1, "blockhash": blockhash, "blockheight": blockheight})
# send-to-self:
txid = node0.sendtoaddress(node0.getnewaddress(), 0.2)
assert_array_result(node0.listwallettransactions(),
{"txid": txid, "type": "SendToSelf"},
{"amount": Decimal("0.2")})
# pegin to self
node0_mweb_addr = node0.getnewaddress(address_type='mweb')
txid = node0.sendtoaddress(node0_mweb_addr, 0.4)
assert_array_result(node0.listwallettransactions(),
{"txid": txid, "type": "SendToSelf"},
{"amount": Decimal("0.4")})
# mine a block, confirmations should change:
blockhash = node0.generate(1)[0]
blockheight = node0.getblockheader(blockhash)['height']
self.sync_all()
assert_array_result(node0.listwallettransactions(),
{"txid": txid, "type": "SendToSelf"},
{"amount": Decimal("0.4"), "confirmations": 1, "blockhash": blockhash, "blockheight": blockheight})
# pegin from 0 to 1
node1_mweb_addr = node1.getnewaddress(address_type='mweb')
txid = node0.sendtoaddress(node1_mweb_addr, 5)
self.sync_all()
assert_array_result(node0.listwallettransactions(txid),
{"txid": txid},
{"type": "SendToAddress", "amount": Decimal("-5.0"), "confirmations": 0, "address": node1_mweb_addr})
assert_array_result(node1.listwallettransactions(),
{"address": node1_mweb_addr},
{"type": "RecvWithAddress", "amount": Decimal("5.0"), "confirmations": 0})
# mine a block, confirmations should change:
blockhash = node0.generate(1)[0]
blockheight = node0.getblockheader(blockhash)['height']
self.sync_all()
assert_array_result(node0.listwallettransactions(txid),
{"txid": txid},
{"type": "SendToAddress", "amount": Decimal("-5.0"), "confirmations": 1, "blockheight": blockheight, "address": node1_mweb_addr})
assert_array_result(node1.listwallettransactions(),
{"address": node1_mweb_addr},
{"type": "RecvWithAddress", "amount": Decimal("5.0"), "confirmations": 1, "blockheight": blockheight})
# pegout from 1 to 0 (node0 online)
node0_addr = node0.getnewaddress()
txid = node1.sendtoaddress(node0_addr, 3)
self.sync_all()
assert_array_result(node1.listwallettransactions(),
{"address": node0_addr},
{"txid": txid, "type": "SendToAddress", "amount": Decimal("-3.0"), "confirmations": 0})
assert_array_result(node0.listwallettransactions(),
{"address": node0_addr},
{"txid": txid, "type": "RecvWithAddress", "amount": Decimal("3.0"), "confirmations": 0})
# mine a block, confirmations should change:
blockhash = node0.generate(1)[0]
blockheight = node0.getblockheader(blockhash)['height']
self.sync_all()
assert_array_result(node1.listwallettransactions(),
{"address": node0_addr},
{"txid": txid, "type": "SendToAddress", "amount": Decimal("-3.0"), "confirmations": 1, "blockheight": blockheight})
assert_array_result(node0.listwallettransactions(),
{"address": node0_addr},
{"type": "RecvWithAddress", "amount": Decimal("3.0"), "confirmations": 1, "blockheight": blockheight})
# pegout from 1 to 2 and mine a block while node2 is offline
# node2 won't see the original MWEB transaction; just the pegout kernel and hogex output.
node2_addr = node2.getnewaddress()
self.stop_node(2)
txid = node1.sendtoaddress(node2_addr, 1)
blockhash = node1.generate(1)[0]
blockheight = node1.getblockheader(blockhash)['height']
hogex_txid = node1.getblock(blockhash)['tx'][-1]
self.start_node(2)
self.connect_nodes(2, 0)
self.connect_nodes(2, 1)
self.sync_all()
assert_array_result(node1.listwallettransactions(),
{"address": node2_addr},
{"txid": txid, "type": "SendToAddress", "amount": Decimal("-1.0"), "confirmations": 1, "blockheight": blockheight})
assert_array_result(node2.listwallettransactions(),
{"address": node2_addr},
{"txid": hogex_txid, "type": "RecvWithAddress", "amount": Decimal("1.0"), "confirmations": 1, "blockheight": blockheight})
# TODO: Reorg and ensure hogex is marked as not accepted
if __name__ == '__main__':
ListWalletTransactionsTest().main()