* txrecord refactor and introduction of txlist
* createtransaction rewrite (TxAssembler) * added listwallettransactions rpc method
This commit is contained in:
parent
768e3e8621
commit
30aa04815c
@ -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])
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -137,8 +137,8 @@ public:
|
||||
GetHash(),
|
||||
GetKernelOffset().ToHex(),
|
||||
kernels_str,
|
||||
GetInputCommits(),
|
||||
GetOutputCommits()
|
||||
GetInputs(),
|
||||
GetOutputs()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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};
|
||||
}
|
||||
@ -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();
|
||||
};
|
||||
|
||||
}
|
||||
@ -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
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
@ -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, {} },
|
||||
|
||||
@ -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
662
src/wallet/txassembler.cpp
Normal 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
128
src/wallet/txassembler.h
Normal 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
303
src/wallet/txlist.cpp
Normal 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
40
src/wallet/txlist.h
Normal 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
207
src/wallet/txrecord.cpp
Normal 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
130
src/wallet/txrecord.h
Normal 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;
|
||||
};
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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',
|
||||
|
||||
150
test/functional/wallet_listwallettransactions.py
Normal file
150
test/functional/wallet_listwallettransactions.py
Normal 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()
|
||||
Loading…
x
Reference in New Issue
Block a user