wallet: Make fee settings non-static members
This commit is contained in:
parent
d5b2e98250
commit
fac0db0ff8
16 changed files with 186 additions and 187 deletions
|
@ -191,20 +191,6 @@ class NodeImpl : public Node
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bool getNetworkActive() override { return g_connman && g_connman->GetNetworkActive(); }
|
bool getNetworkActive() override { return g_connman && g_connman->GetNetworkActive(); }
|
||||||
unsigned int getTxConfirmTarget() override { CHECK_WALLET(return ::nTxConfirmTarget); }
|
|
||||||
CAmount getRequiredFee(unsigned int tx_bytes) override { CHECK_WALLET(return GetRequiredFee(tx_bytes)); }
|
|
||||||
CAmount getMinimumFee(unsigned int tx_bytes,
|
|
||||||
const CCoinControl& coin_control,
|
|
||||||
int* returned_target,
|
|
||||||
FeeReason* reason) override
|
|
||||||
{
|
|
||||||
FeeCalculation fee_calc;
|
|
||||||
CAmount result;
|
|
||||||
CHECK_WALLET(result = GetMinimumFee(tx_bytes, coin_control, ::mempool, ::feeEstimator, &fee_calc));
|
|
||||||
if (returned_target) *returned_target = fee_calc.returnedTarget;
|
|
||||||
if (reason) *reason = fee_calc.reason;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
CAmount getMaxTxFee() override { return ::maxTxFee; }
|
CAmount getMaxTxFee() override { return ::maxTxFee; }
|
||||||
CFeeRate estimateSmartFee(int num_blocks, bool conservative, int* returned_target = nullptr) override
|
CFeeRate estimateSmartFee(int num_blocks, bool conservative, int* returned_target = nullptr) override
|
||||||
{
|
{
|
||||||
|
|
|
@ -26,11 +26,9 @@ class Coin;
|
||||||
class RPCTimerInterface;
|
class RPCTimerInterface;
|
||||||
class UniValue;
|
class UniValue;
|
||||||
class proxyType;
|
class proxyType;
|
||||||
enum class FeeReason;
|
|
||||||
struct CNodeStateStats;
|
struct CNodeStateStats;
|
||||||
|
|
||||||
namespace interfaces {
|
namespace interfaces {
|
||||||
|
|
||||||
class Handler;
|
class Handler;
|
||||||
class Wallet;
|
class Wallet;
|
||||||
|
|
||||||
|
@ -152,18 +150,6 @@ public:
|
||||||
//! Get network active.
|
//! Get network active.
|
||||||
virtual bool getNetworkActive() = 0;
|
virtual bool getNetworkActive() = 0;
|
||||||
|
|
||||||
//! Get tx confirm target.
|
|
||||||
virtual unsigned int getTxConfirmTarget() = 0;
|
|
||||||
|
|
||||||
//! Get required fee.
|
|
||||||
virtual CAmount getRequiredFee(unsigned int tx_bytes) = 0;
|
|
||||||
|
|
||||||
//! Get minimum fee.
|
|
||||||
virtual CAmount getMinimumFee(unsigned int tx_bytes,
|
|
||||||
const CCoinControl& coin_control,
|
|
||||||
int* returned_target,
|
|
||||||
FeeReason* reason) = 0;
|
|
||||||
|
|
||||||
//! Get max tx fee.
|
//! Get max tx fee.
|
||||||
virtual CAmount getMaxTxFee() = 0;
|
virtual CAmount getMaxTxFee() = 0;
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
#include <consensus/validation.h>
|
#include <consensus/validation.h>
|
||||||
#include <interfaces/handler.h>
|
#include <interfaces/handler.h>
|
||||||
#include <net.h>
|
#include <net.h>
|
||||||
|
#include <policy/feerate.h>
|
||||||
|
#include <policy/fees.h>
|
||||||
#include <policy/policy.h>
|
#include <policy/policy.h>
|
||||||
#include <primitives/transaction.h>
|
#include <primitives/transaction.h>
|
||||||
#include <script/ismine.h>
|
#include <script/ismine.h>
|
||||||
|
@ -20,6 +22,7 @@
|
||||||
#include <uint256.h>
|
#include <uint256.h>
|
||||||
#include <validation.h>
|
#include <validation.h>
|
||||||
#include <wallet/feebumper.h>
|
#include <wallet/feebumper.h>
|
||||||
|
#include <wallet/fees.h>
|
||||||
#include <wallet/wallet.h>
|
#include <wallet/wallet.h>
|
||||||
|
|
||||||
namespace interfaces {
|
namespace interfaces {
|
||||||
|
@ -403,6 +406,20 @@ public:
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
CAmount getRequiredFee(unsigned int tx_bytes) override { return GetRequiredFee(m_wallet, tx_bytes); }
|
||||||
|
CAmount getMinimumFee(unsigned int tx_bytes,
|
||||||
|
const CCoinControl& coin_control,
|
||||||
|
int* returned_target,
|
||||||
|
FeeReason* reason) override
|
||||||
|
{
|
||||||
|
FeeCalculation fee_calc;
|
||||||
|
CAmount result;
|
||||||
|
result = GetMinimumFee(m_wallet, tx_bytes, coin_control, ::mempool, ::feeEstimator, &fee_calc);
|
||||||
|
if (returned_target) *returned_target = fee_calc.returnedTarget;
|
||||||
|
if (reason) *reason = fee_calc.reason;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
unsigned int getConfirmTarget() override { return m_wallet.m_confirm_target; }
|
||||||
bool hdEnabled() override { return m_wallet.IsHDEnabled(); }
|
bool hdEnabled() override { return m_wallet.IsHDEnabled(); }
|
||||||
OutputType getDefaultAddressType() override { return m_wallet.m_default_address_type; }
|
OutputType getDefaultAddressType() override { return m_wallet.m_default_address_type; }
|
||||||
OutputType getDefaultChangeType() override { return m_wallet.m_default_change_type; }
|
OutputType getDefaultChangeType() override { return m_wallet.m_default_change_type; }
|
||||||
|
|
|
@ -22,8 +22,10 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
class CCoinControl;
|
class CCoinControl;
|
||||||
|
class CFeeRate;
|
||||||
class CKey;
|
class CKey;
|
||||||
class CWallet;
|
class CWallet;
|
||||||
|
enum class FeeReason;
|
||||||
enum class OutputType;
|
enum class OutputType;
|
||||||
struct CRecipient;
|
struct CRecipient;
|
||||||
|
|
||||||
|
@ -218,6 +220,18 @@ public:
|
||||||
//! Return wallet transaction output information.
|
//! Return wallet transaction output information.
|
||||||
virtual std::vector<WalletTxOut> getCoins(const std::vector<COutPoint>& outputs) = 0;
|
virtual std::vector<WalletTxOut> getCoins(const std::vector<COutPoint>& outputs) = 0;
|
||||||
|
|
||||||
|
//! Get required fee.
|
||||||
|
virtual CAmount getRequiredFee(unsigned int tx_bytes) = 0;
|
||||||
|
|
||||||
|
//! Get minimum fee.
|
||||||
|
virtual CAmount getMinimumFee(unsigned int tx_bytes,
|
||||||
|
const CCoinControl& coin_control,
|
||||||
|
int* returned_target,
|
||||||
|
FeeReason* reason) = 0;
|
||||||
|
|
||||||
|
//! Get tx confirm target.
|
||||||
|
virtual unsigned int getConfirmTarget() = 0;
|
||||||
|
|
||||||
// Return whether HD enabled.
|
// Return whether HD enabled.
|
||||||
virtual bool hdEnabled() = 0;
|
virtual bool hdEnabled() = 0;
|
||||||
|
|
||||||
|
|
|
@ -509,7 +509,7 @@ void CoinControlDialog::updateLabels(WalletModel *model, QDialog* dialog)
|
||||||
nBytes -= 34;
|
nBytes -= 34;
|
||||||
|
|
||||||
// Fee
|
// Fee
|
||||||
nPayFee = model->node().getMinimumFee(nBytes, *coinControl(), nullptr /* returned_target */, nullptr /* reason */);
|
nPayFee = model->wallet().getMinimumFee(nBytes, *coinControl(), nullptr /* returned_target */, nullptr /* reason */);
|
||||||
|
|
||||||
if (nPayAmount > 0)
|
if (nPayAmount > 0)
|
||||||
{
|
{
|
||||||
|
|
|
@ -114,7 +114,7 @@ SendCoinsDialog::SendCoinsDialog(const PlatformStyle *_platformStyle, QWidget *p
|
||||||
if (!settings.contains("nSmartFeeSliderPosition"))
|
if (!settings.contains("nSmartFeeSliderPosition"))
|
||||||
settings.setValue("nSmartFeeSliderPosition", 0);
|
settings.setValue("nSmartFeeSliderPosition", 0);
|
||||||
if (!settings.contains("nTransactionFee"))
|
if (!settings.contains("nTransactionFee"))
|
||||||
settings.setValue("nTransactionFee", (qint64)DEFAULT_TRANSACTION_FEE);
|
settings.setValue("nTransactionFee", (qint64)DEFAULT_PAY_TX_FEE);
|
||||||
if (!settings.contains("fPayOnlyMinFee"))
|
if (!settings.contains("fPayOnlyMinFee"))
|
||||||
settings.setValue("fPayOnlyMinFee", false);
|
settings.setValue("fPayOnlyMinFee", false);
|
||||||
ui->groupFee->setId(ui->radioSmartFee, 0);
|
ui->groupFee->setId(ui->radioSmartFee, 0);
|
||||||
|
@ -175,7 +175,7 @@ void SendCoinsDialog::setModel(WalletModel *_model)
|
||||||
connect(ui->checkBoxMinimumFee, SIGNAL(stateChanged(int)), this, SLOT(coinControlUpdateLabels()));
|
connect(ui->checkBoxMinimumFee, SIGNAL(stateChanged(int)), this, SLOT(coinControlUpdateLabels()));
|
||||||
connect(ui->optInRBF, SIGNAL(stateChanged(int)), this, SLOT(updateSmartFeeLabel()));
|
connect(ui->optInRBF, SIGNAL(stateChanged(int)), this, SLOT(updateSmartFeeLabel()));
|
||||||
connect(ui->optInRBF, SIGNAL(stateChanged(int)), this, SLOT(coinControlUpdateLabels()));
|
connect(ui->optInRBF, SIGNAL(stateChanged(int)), this, SLOT(coinControlUpdateLabels()));
|
||||||
ui->customFee->setSingleStep(model->node().getRequiredFee(1000));
|
ui->customFee->setSingleStep(model->wallet().getRequiredFee(1000));
|
||||||
updateFeeSectionControls();
|
updateFeeSectionControls();
|
||||||
updateMinFeeLabel();
|
updateMinFeeLabel();
|
||||||
updateSmartFeeLabel();
|
updateSmartFeeLabel();
|
||||||
|
@ -193,7 +193,7 @@ void SendCoinsDialog::setModel(WalletModel *_model)
|
||||||
settings.remove("nSmartFeeSliderPosition");
|
settings.remove("nSmartFeeSliderPosition");
|
||||||
}
|
}
|
||||||
if (settings.value("nConfTarget").toInt() == 0)
|
if (settings.value("nConfTarget").toInt() == 0)
|
||||||
ui->confTargetSelector->setCurrentIndex(getIndexForConfTarget(model->node().getTxConfirmTarget()));
|
ui->confTargetSelector->setCurrentIndex(getIndexForConfTarget(model->wallet().getConfirmTarget()));
|
||||||
else
|
else
|
||||||
ui->confTargetSelector->setCurrentIndex(getIndexForConfTarget(settings.value("nConfTarget").toInt()));
|
ui->confTargetSelector->setCurrentIndex(getIndexForConfTarget(settings.value("nConfTarget").toInt()));
|
||||||
}
|
}
|
||||||
|
@ -629,7 +629,7 @@ void SendCoinsDialog::useAvailableBalance(SendCoinsEntry* entry)
|
||||||
|
|
||||||
void SendCoinsDialog::setMinimumFee()
|
void SendCoinsDialog::setMinimumFee()
|
||||||
{
|
{
|
||||||
ui->customFee->setValue(model->node().getRequiredFee(1000));
|
ui->customFee->setValue(model->wallet().getRequiredFee(1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
void SendCoinsDialog::updateFeeSectionControls()
|
void SendCoinsDialog::updateFeeSectionControls()
|
||||||
|
@ -661,7 +661,7 @@ void SendCoinsDialog::updateMinFeeLabel()
|
||||||
{
|
{
|
||||||
if (model && model->getOptionsModel())
|
if (model && model->getOptionsModel())
|
||||||
ui->checkBoxMinimumFee->setText(tr("Pay only the required fee of %1").arg(
|
ui->checkBoxMinimumFee->setText(tr("Pay only the required fee of %1").arg(
|
||||||
BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), model->node().getRequiredFee(1000)) + "/kB")
|
BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), model->wallet().getRequiredFee(1000)) + "/kB")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -675,7 +675,7 @@ void SendCoinsDialog::updateCoinControlState(CCoinControl& ctrl)
|
||||||
// Avoid using global defaults when sending money from the GUI
|
// Avoid using global defaults when sending money from the GUI
|
||||||
// Either custom fee will be used or if not selected, the confirmation target from dropdown box
|
// Either custom fee will be used or if not selected, the confirmation target from dropdown box
|
||||||
ctrl.m_confirm_target = getConfTargetForIndex(ui->confTargetSelector->currentIndex());
|
ctrl.m_confirm_target = getConfTargetForIndex(ui->confTargetSelector->currentIndex());
|
||||||
ctrl.signalRbf = ui->optInRBF->isChecked();
|
ctrl.m_signal_bip125_rbf = ui->optInRBF->isChecked();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SendCoinsDialog::updateSmartFeeLabel()
|
void SendCoinsDialog::updateSmartFeeLabel()
|
||||||
|
@ -687,7 +687,7 @@ void SendCoinsDialog::updateSmartFeeLabel()
|
||||||
coin_control.m_feerate.reset(); // Explicitly use only fee estimation rate for smart fee labels
|
coin_control.m_feerate.reset(); // Explicitly use only fee estimation rate for smart fee labels
|
||||||
int returned_target;
|
int returned_target;
|
||||||
FeeReason reason;
|
FeeReason reason;
|
||||||
CFeeRate feeRate = CFeeRate(model->node().getMinimumFee(1000, coin_control, &returned_target, &reason));
|
CFeeRate feeRate = CFeeRate(model->wallet().getMinimumFee(1000, coin_control, &returned_target, &reason));
|
||||||
|
|
||||||
ui->labelSmartFee->setText(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), feeRate.GetFeePerK()) + "/kB");
|
ui->labelSmartFee->setText(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), feeRate.GetFeePerK()) + "/kB");
|
||||||
|
|
||||||
|
|
|
@ -486,7 +486,7 @@ bool WalletModel::saveReceiveRequest(const std::string &sAddress, const int64_t
|
||||||
bool WalletModel::bumpFee(uint256 hash)
|
bool WalletModel::bumpFee(uint256 hash)
|
||||||
{
|
{
|
||||||
CCoinControl coin_control;
|
CCoinControl coin_control;
|
||||||
coin_control.signalRbf = true;
|
coin_control.m_signal_bip125_rbf = true;
|
||||||
std::vector<std::string> errors;
|
std::vector<std::string> errors;
|
||||||
CAmount old_fee;
|
CAmount old_fee;
|
||||||
CAmount new_fee;
|
CAmount new_fee;
|
||||||
|
|
|
@ -26,12 +26,12 @@ public:
|
||||||
bool fAllowWatchOnly;
|
bool fAllowWatchOnly;
|
||||||
//! Override automatic min/max checks on fee, m_feerate must be set if true
|
//! Override automatic min/max checks on fee, m_feerate must be set if true
|
||||||
bool fOverrideFeeRate;
|
bool fOverrideFeeRate;
|
||||||
//! Override the default payTxFee if set
|
//! Override the wallet's m_pay_tx_fee if set
|
||||||
boost::optional<CFeeRate> m_feerate;
|
boost::optional<CFeeRate> m_feerate;
|
||||||
//! Override the default confirmation target if set
|
//! Override the default confirmation target if set
|
||||||
boost::optional<unsigned int> m_confirm_target;
|
boost::optional<unsigned int> m_confirm_target;
|
||||||
//! Signal BIP-125 replace by fee.
|
//! Override the wallet's m_signal_rbf if set
|
||||||
bool signalRbf;
|
boost::optional<bool> m_signal_bip125_rbf;
|
||||||
//! Fee estimation mode to control arguments to estimateSmartFee
|
//! Fee estimation mode to control arguments to estimateSmartFee
|
||||||
FeeEstimateMode m_fee_mode;
|
FeeEstimateMode m_fee_mode;
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ public:
|
||||||
m_feerate.reset();
|
m_feerate.reset();
|
||||||
fOverrideFeeRate = false;
|
fOverrideFeeRate = false;
|
||||||
m_confirm_target.reset();
|
m_confirm_target.reset();
|
||||||
signalRbf = fWalletRbf;
|
m_signal_bip125_rbf.reset();
|
||||||
m_fee_mode = FeeEstimateMode::UNSET;
|
m_fee_mode = FeeEstimateMode::UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -134,7 +134,7 @@ Result CreateTransaction(const CWallet* wallet, const uint256& txid, const CCoin
|
||||||
FormatMoney(minTotalFee), FormatMoney(nOldFeeRate.GetFee(maxNewTxSize)), FormatMoney(::incrementalRelayFee.GetFee(maxNewTxSize))));
|
FormatMoney(minTotalFee), FormatMoney(nOldFeeRate.GetFee(maxNewTxSize)), FormatMoney(::incrementalRelayFee.GetFee(maxNewTxSize))));
|
||||||
return Result::INVALID_PARAMETER;
|
return Result::INVALID_PARAMETER;
|
||||||
}
|
}
|
||||||
CAmount requiredFee = GetRequiredFee(maxNewTxSize);
|
CAmount requiredFee = GetRequiredFee(*wallet, maxNewTxSize);
|
||||||
if (total_fee < requiredFee) {
|
if (total_fee < requiredFee) {
|
||||||
errors.push_back(strprintf("Insufficient totalFee (cannot be less than required fee %s)",
|
errors.push_back(strprintf("Insufficient totalFee (cannot be less than required fee %s)",
|
||||||
FormatMoney(requiredFee)));
|
FormatMoney(requiredFee)));
|
||||||
|
@ -143,7 +143,7 @@ Result CreateTransaction(const CWallet* wallet, const uint256& txid, const CCoin
|
||||||
new_fee = total_fee;
|
new_fee = total_fee;
|
||||||
nNewFeeRate = CFeeRate(total_fee, maxNewTxSize);
|
nNewFeeRate = CFeeRate(total_fee, maxNewTxSize);
|
||||||
} else {
|
} else {
|
||||||
new_fee = GetMinimumFee(maxNewTxSize, coin_control, mempool, ::feeEstimator, nullptr /* FeeCalculation */);
|
new_fee = GetMinimumFee(*wallet, maxNewTxSize, coin_control, mempool, ::feeEstimator, nullptr /* FeeCalculation */);
|
||||||
nNewFeeRate = CFeeRate(new_fee, maxNewTxSize);
|
nNewFeeRate = CFeeRate(new_fee, maxNewTxSize);
|
||||||
|
|
||||||
// New fee rate must be at least old rate + minimum incremental relay rate
|
// New fee rate must be at least old rate + minimum incremental relay rate
|
||||||
|
@ -194,14 +194,14 @@ Result CreateTransaction(const CWallet* wallet, const uint256& txid, const CCoin
|
||||||
|
|
||||||
// If the output would become dust, discard it (converting the dust to fee)
|
// If the output would become dust, discard it (converting the dust to fee)
|
||||||
poutput->nValue -= nDelta;
|
poutput->nValue -= nDelta;
|
||||||
if (poutput->nValue <= GetDustThreshold(*poutput, GetDiscardRate(::feeEstimator))) {
|
if (poutput->nValue <= GetDustThreshold(*poutput, GetDiscardRate(*wallet, ::feeEstimator))) {
|
||||||
LogPrint(BCLog::RPC, "Bumping fee and discarding dust output\n");
|
LogPrint(BCLog::RPC, "Bumping fee and discarding dust output\n");
|
||||||
new_fee += poutput->nValue;
|
new_fee += poutput->nValue;
|
||||||
mtx.vout.erase(mtx.vout.begin() + nOutput);
|
mtx.vout.erase(mtx.vout.begin() + nOutput);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark new tx not replaceable, if requested.
|
// Mark new tx not replaceable, if requested.
|
||||||
if (!coin_control.signalRbf) {
|
if (!coin_control.m_signal_bip125_rbf.get_value_or(wallet->m_signal_rbf)) {
|
||||||
for (auto& input : mtx.vin) {
|
for (auto& input : mtx.vin) {
|
||||||
if (input.nSequence < 0xfffffffe) input.nSequence = 0xfffffffe;
|
if (input.nSequence < 0xfffffffe) input.nSequence = 0xfffffffe;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,15 +13,15 @@
|
||||||
#include <wallet/wallet.h>
|
#include <wallet/wallet.h>
|
||||||
|
|
||||||
|
|
||||||
CAmount GetRequiredFee(unsigned int nTxBytes)
|
CAmount GetRequiredFee(const CWallet& wallet, unsigned int nTxBytes)
|
||||||
{
|
{
|
||||||
return std::max(CWallet::minTxFee.GetFee(nTxBytes), ::minRelayTxFee.GetFee(nTxBytes));
|
return GetRequiredFeeRate(wallet).GetFee(nTxBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
CAmount GetMinimumFee(unsigned int nTxBytes, const CCoinControl& coin_control, const CTxMemPool& pool, const CBlockPolicyEstimator& estimator, FeeCalculation *feeCalc)
|
CAmount GetMinimumFee(const CWallet& wallet, unsigned int nTxBytes, const CCoinControl& coin_control, const CTxMemPool& pool, const CBlockPolicyEstimator& estimator, FeeCalculation* feeCalc)
|
||||||
{
|
{
|
||||||
CAmount fee_needed = GetMinimumFeeRate(coin_control, pool, estimator, feeCalc).GetFee(nTxBytes);
|
CAmount fee_needed = GetMinimumFeeRate(wallet, coin_control, pool, estimator, feeCalc).GetFee(nTxBytes);
|
||||||
// Always obey the maximum
|
// Always obey the maximum
|
||||||
if (fee_needed > maxTxFee) {
|
if (fee_needed > maxTxFee) {
|
||||||
fee_needed = maxTxFee;
|
fee_needed = maxTxFee;
|
||||||
|
@ -30,48 +30,48 @@ CAmount GetMinimumFee(unsigned int nTxBytes, const CCoinControl& coin_control, c
|
||||||
return fee_needed;
|
return fee_needed;
|
||||||
}
|
}
|
||||||
|
|
||||||
CFeeRate GetRequiredFeeRate()
|
CFeeRate GetRequiredFeeRate(const CWallet& wallet)
|
||||||
{
|
{
|
||||||
return std::max(CWallet::minTxFee, ::minRelayTxFee);
|
return std::max(wallet.m_min_fee, ::minRelayTxFee);
|
||||||
}
|
}
|
||||||
|
|
||||||
CFeeRate GetMinimumFeeRate(const CCoinControl& coin_control, const CTxMemPool& pool, const CBlockPolicyEstimator& estimator, FeeCalculation *feeCalc)
|
CFeeRate GetMinimumFeeRate(const CWallet& wallet, const CCoinControl& coin_control, const CTxMemPool& pool, const CBlockPolicyEstimator& estimator, FeeCalculation* feeCalc)
|
||||||
{
|
{
|
||||||
/* User control of how to calculate fee uses the following parameter precedence:
|
/* User control of how to calculate fee uses the following parameter precedence:
|
||||||
1. coin_control.m_feerate
|
1. coin_control.m_feerate
|
||||||
2. coin_control.m_confirm_target
|
2. coin_control.m_confirm_target
|
||||||
3. payTxFee (user-set global variable)
|
3. m_pay_tx_fee (user-set member variable of wallet)
|
||||||
4. nTxConfirmTarget (user-set global variable)
|
4. m_confirm_target (user-set member variable of wallet)
|
||||||
The first parameter that is set is used.
|
The first parameter that is set is used.
|
||||||
*/
|
*/
|
||||||
CFeeRate feerate_needed ;
|
CFeeRate feerate_needed;
|
||||||
if (coin_control.m_feerate) { // 1.
|
if (coin_control.m_feerate) { // 1.
|
||||||
feerate_needed = *(coin_control.m_feerate);
|
feerate_needed = *(coin_control.m_feerate);
|
||||||
if (feeCalc) feeCalc->reason = FeeReason::PAYTXFEE;
|
if (feeCalc) feeCalc->reason = FeeReason::PAYTXFEE;
|
||||||
// Allow to override automatic min/max check over coin control instance
|
// Allow to override automatic min/max check over coin control instance
|
||||||
if (coin_control.fOverrideFeeRate) return feerate_needed;
|
if (coin_control.fOverrideFeeRate) return feerate_needed;
|
||||||
}
|
}
|
||||||
else if (!coin_control.m_confirm_target && ::payTxFee != CFeeRate(0)) { // 3. TODO: remove magic value of 0 for global payTxFee
|
else if (!coin_control.m_confirm_target && wallet.m_pay_tx_fee != CFeeRate(0)) { // 3. TODO: remove magic value of 0 for wallet member m_pay_tx_fee
|
||||||
feerate_needed = ::payTxFee;
|
feerate_needed = wallet.m_pay_tx_fee;
|
||||||
if (feeCalc) feeCalc->reason = FeeReason::PAYTXFEE;
|
if (feeCalc) feeCalc->reason = FeeReason::PAYTXFEE;
|
||||||
}
|
}
|
||||||
else { // 2. or 4.
|
else { // 2. or 4.
|
||||||
// We will use smart fee estimation
|
// We will use smart fee estimation
|
||||||
unsigned int target = coin_control.m_confirm_target ? *coin_control.m_confirm_target : ::nTxConfirmTarget;
|
unsigned int target = coin_control.m_confirm_target ? *coin_control.m_confirm_target : wallet.m_confirm_target;
|
||||||
// By default estimates are economical iff we are signaling opt-in-RBF
|
// By default estimates are economical iff we are signaling opt-in-RBF
|
||||||
bool conservative_estimate = !coin_control.signalRbf;
|
bool conservative_estimate = !coin_control.m_signal_bip125_rbf.get_value_or(wallet.m_signal_rbf);
|
||||||
// Allow to override the default fee estimate mode over the CoinControl instance
|
// Allow to override the default fee estimate mode over the CoinControl instance
|
||||||
if (coin_control.m_fee_mode == FeeEstimateMode::CONSERVATIVE) conservative_estimate = true;
|
if (coin_control.m_fee_mode == FeeEstimateMode::CONSERVATIVE) conservative_estimate = true;
|
||||||
else if (coin_control.m_fee_mode == FeeEstimateMode::ECONOMICAL) conservative_estimate = false;
|
else if (coin_control.m_fee_mode == FeeEstimateMode::ECONOMICAL) conservative_estimate = false;
|
||||||
|
|
||||||
feerate_needed = estimator.estimateSmartFee(target, feeCalc, conservative_estimate);
|
feerate_needed = estimator.estimateSmartFee(target, feeCalc, conservative_estimate);
|
||||||
if (feerate_needed == CFeeRate(0)) {
|
if (feerate_needed == CFeeRate(0)) {
|
||||||
// if we don't have enough data for estimateSmartFee, then use fallbackFee
|
// if we don't have enough data for estimateSmartFee, then use fallback fee
|
||||||
feerate_needed = CWallet::fallbackFee;
|
feerate_needed = wallet.m_fallback_fee;
|
||||||
if (feeCalc) feeCalc->reason = FeeReason::FALLBACK;
|
if (feeCalc) feeCalc->reason = FeeReason::FALLBACK;
|
||||||
|
|
||||||
// directly return if fallback fee is disabled (feerate 0 == disabled)
|
// directly return if fallback fee is disabled (feerate 0 == disabled)
|
||||||
if (CWallet::fallbackFee == CFeeRate(0)) return feerate_needed;
|
if (wallet.m_fallback_fee == CFeeRate(0)) return feerate_needed;
|
||||||
}
|
}
|
||||||
// Obey mempool min fee when using smart fee estimation
|
// Obey mempool min fee when using smart fee estimation
|
||||||
CFeeRate min_mempool_feerate = pool.GetMinFee(gArgs.GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000);
|
CFeeRate min_mempool_feerate = pool.GetMinFee(gArgs.GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000);
|
||||||
|
@ -81,8 +81,8 @@ CFeeRate GetMinimumFeeRate(const CCoinControl& coin_control, const CTxMemPool& p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// prevent user from paying a fee below minRelayTxFee or minTxFee
|
// prevent user from paying a fee below the required fee rate
|
||||||
CFeeRate required_feerate = GetRequiredFeeRate();
|
CFeeRate required_feerate = GetRequiredFeeRate(wallet);
|
||||||
if (required_feerate > feerate_needed) {
|
if (required_feerate > feerate_needed) {
|
||||||
feerate_needed = required_feerate;
|
feerate_needed = required_feerate;
|
||||||
if (feeCalc) feeCalc->reason = FeeReason::REQUIRED;
|
if (feeCalc) feeCalc->reason = FeeReason::REQUIRED;
|
||||||
|
@ -90,12 +90,12 @@ CFeeRate GetMinimumFeeRate(const CCoinControl& coin_control, const CTxMemPool& p
|
||||||
return feerate_needed;
|
return feerate_needed;
|
||||||
}
|
}
|
||||||
|
|
||||||
CFeeRate GetDiscardRate(const CBlockPolicyEstimator& estimator)
|
CFeeRate GetDiscardRate(const CWallet& wallet, const CBlockPolicyEstimator& estimator)
|
||||||
{
|
{
|
||||||
unsigned int highest_target = estimator.HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE);
|
unsigned int highest_target = estimator.HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE);
|
||||||
CFeeRate discard_rate = estimator.estimateSmartFee(highest_target, nullptr /* FeeCalculation */, false /* conservative */);
|
CFeeRate discard_rate = estimator.estimateSmartFee(highest_target, nullptr /* FeeCalculation */, false /* conservative */);
|
||||||
// Don't let discard_rate be greater than longest possible fee estimate if we get a valid fee estimate
|
// Don't let discard_rate be greater than longest possible fee estimate if we get a valid fee estimate
|
||||||
discard_rate = (discard_rate == CFeeRate(0)) ? CWallet::m_discard_rate : std::min(discard_rate, CWallet::m_discard_rate);
|
discard_rate = (discard_rate == CFeeRate(0)) ? wallet.m_discard_rate : std::min(discard_rate, wallet.m_discard_rate);
|
||||||
// Discard rate must be at least dustRelayFee
|
// Discard rate must be at least dustRelayFee
|
||||||
discard_rate = std::max(discard_rate, ::dustRelayFee);
|
discard_rate = std::max(discard_rate, ::dustRelayFee);
|
||||||
return discard_rate;
|
return discard_rate;
|
||||||
|
|
|
@ -12,35 +12,36 @@ class CBlockPolicyEstimator;
|
||||||
class CCoinControl;
|
class CCoinControl;
|
||||||
class CFeeRate;
|
class CFeeRate;
|
||||||
class CTxMemPool;
|
class CTxMemPool;
|
||||||
|
class CWallet;
|
||||||
struct FeeCalculation;
|
struct FeeCalculation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the minimum required fee taking into account the
|
* Return the minimum required absolute fee for this size
|
||||||
* floating relay fee and user set minimum transaction fee
|
* based on the required fee rate
|
||||||
*/
|
*/
|
||||||
CAmount GetRequiredFee(unsigned int nTxBytes);
|
CAmount GetRequiredFee(const CWallet& wallet, unsigned int nTxBytes);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Estimate the minimum fee considering user set parameters
|
* Estimate the minimum fee considering user set parameters
|
||||||
* and the required fee
|
* and the required fee
|
||||||
*/
|
*/
|
||||||
CAmount GetMinimumFee(unsigned int nTxBytes, const CCoinControl& coin_control, const CTxMemPool& pool, const CBlockPolicyEstimator& estimator, FeeCalculation *feeCalc);
|
CAmount GetMinimumFee(const CWallet& wallet, unsigned int nTxBytes, const CCoinControl& coin_control, const CTxMemPool& pool, const CBlockPolicyEstimator& estimator, FeeCalculation* feeCalc);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the minimum required feerate taking into account the
|
* Return the minimum required feerate taking into account the
|
||||||
* floating relay feerate and user set minimum transaction feerate
|
* minimum relay feerate and user set minimum transaction feerate
|
||||||
*/
|
*/
|
||||||
CFeeRate GetRequiredFeeRate();
|
CFeeRate GetRequiredFeeRate(const CWallet& wallet);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Estimate the minimum fee rate considering user set parameters
|
* Estimate the minimum fee rate considering user set parameters
|
||||||
* and the required fee
|
* and the required fee
|
||||||
*/
|
*/
|
||||||
CFeeRate GetMinimumFeeRate(const CCoinControl& coin_control, const CTxMemPool& pool, const CBlockPolicyEstimator& estimator, FeeCalculation *feeCalc);
|
CFeeRate GetMinimumFeeRate(const CWallet& wallet, const CCoinControl& coin_control, const CTxMemPool& pool, const CBlockPolicyEstimator& estimator, FeeCalculation* feeCalc);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the maximum feerate for discarding change.
|
* Return the maximum feerate for discarding change.
|
||||||
*/
|
*/
|
||||||
CFeeRate GetDiscardRate(const CBlockPolicyEstimator& estimator);
|
CFeeRate GetDiscardRate(const CWallet& wallet, const CBlockPolicyEstimator& estimator);
|
||||||
|
|
||||||
#endif // BITCOIN_WALLET_FEES_H
|
#endif // BITCOIN_WALLET_FEES_H
|
||||||
|
|
|
@ -64,7 +64,7 @@ std::string WalletInit::GetHelpString(bool showDebug) const
|
||||||
strUsage += HelpMessageOpt("-mintxfee=<amt>", strprintf(_("Fees (in %s/kB) smaller than this are considered zero fee for transaction creation (default: %s)"),
|
strUsage += HelpMessageOpt("-mintxfee=<amt>", strprintf(_("Fees (in %s/kB) smaller than this are considered zero fee for transaction creation (default: %s)"),
|
||||||
CURRENCY_UNIT, FormatMoney(DEFAULT_TRANSACTION_MINFEE)));
|
CURRENCY_UNIT, FormatMoney(DEFAULT_TRANSACTION_MINFEE)));
|
||||||
strUsage += HelpMessageOpt("-paytxfee=<amt>", strprintf(_("Fee (in %s/kB) to add to transactions you send (default: %s)"),
|
strUsage += HelpMessageOpt("-paytxfee=<amt>", strprintf(_("Fee (in %s/kB) to add to transactions you send (default: %s)"),
|
||||||
CURRENCY_UNIT, FormatMoney(payTxFee.GetFeePerK())));
|
CURRENCY_UNIT, FormatMoney(CFeeRate{DEFAULT_PAY_TX_FEE}.GetFeePerK())));
|
||||||
strUsage += HelpMessageOpt("-rescan", _("Rescan the block chain for missing wallet transactions on startup"));
|
strUsage += HelpMessageOpt("-rescan", _("Rescan the block chain for missing wallet transactions on startup"));
|
||||||
strUsage += HelpMessageOpt("-salvagewallet", _("Attempt to recover private keys from a corrupt wallet on startup"));
|
strUsage += HelpMessageOpt("-salvagewallet", _("Attempt to recover private keys from a corrupt wallet on startup"));
|
||||||
strUsage += HelpMessageOpt("-spendzeroconfchange", strprintf(_("Spend unconfirmed change when sending transactions (default: %u)"), DEFAULT_SPEND_ZEROCONF_CHANGE));
|
strUsage += HelpMessageOpt("-spendzeroconfchange", strprintf(_("Spend unconfirmed change when sending transactions (default: %u)"), DEFAULT_SPEND_ZEROCONF_CHANGE));
|
||||||
|
@ -149,55 +149,6 @@ bool WalletInit::ParameterInteraction() const
|
||||||
InitWarning(AmountHighWarn("-minrelaytxfee") + " " +
|
InitWarning(AmountHighWarn("-minrelaytxfee") + " " +
|
||||||
_("The wallet will avoid paying less than the minimum relay fee."));
|
_("The wallet will avoid paying less than the minimum relay fee."));
|
||||||
|
|
||||||
if (gArgs.IsArgSet("-mintxfee"))
|
|
||||||
{
|
|
||||||
CAmount n = 0;
|
|
||||||
if (!ParseMoney(gArgs.GetArg("-mintxfee", ""), n) || 0 == n)
|
|
||||||
return InitError(AmountErrMsg("mintxfee", gArgs.GetArg("-mintxfee", "")));
|
|
||||||
if (n > HIGH_TX_FEE_PER_KB)
|
|
||||||
InitWarning(AmountHighWarn("-mintxfee") + " " +
|
|
||||||
_("This is the minimum transaction fee you pay on every transaction."));
|
|
||||||
CWallet::minTxFee = CFeeRate(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
g_wallet_allow_fallback_fee = Params().IsFallbackFeeEnabled();
|
|
||||||
if (gArgs.IsArgSet("-fallbackfee"))
|
|
||||||
{
|
|
||||||
CAmount nFeePerK = 0;
|
|
||||||
if (!ParseMoney(gArgs.GetArg("-fallbackfee", ""), nFeePerK))
|
|
||||||
return InitError(strprintf(_("Invalid amount for -fallbackfee=<amount>: '%s'"), gArgs.GetArg("-fallbackfee", "")));
|
|
||||||
if (nFeePerK > HIGH_TX_FEE_PER_KB)
|
|
||||||
InitWarning(AmountHighWarn("-fallbackfee") + " " +
|
|
||||||
_("This is the transaction fee you may pay when fee estimates are not available."));
|
|
||||||
CWallet::fallbackFee = CFeeRate(nFeePerK);
|
|
||||||
g_wallet_allow_fallback_fee = nFeePerK != 0; //disable fallback fee in case value was set to 0, enable if non-null value
|
|
||||||
}
|
|
||||||
if (gArgs.IsArgSet("-discardfee"))
|
|
||||||
{
|
|
||||||
CAmount nFeePerK = 0;
|
|
||||||
if (!ParseMoney(gArgs.GetArg("-discardfee", ""), nFeePerK))
|
|
||||||
return InitError(strprintf(_("Invalid amount for -discardfee=<amount>: '%s'"), gArgs.GetArg("-discardfee", "")));
|
|
||||||
if (nFeePerK > HIGH_TX_FEE_PER_KB)
|
|
||||||
InitWarning(AmountHighWarn("-discardfee") + " " +
|
|
||||||
_("This is the transaction fee you may discard if change is smaller than dust at this level"));
|
|
||||||
CWallet::m_discard_rate = CFeeRate(nFeePerK);
|
|
||||||
}
|
|
||||||
if (gArgs.IsArgSet("-paytxfee"))
|
|
||||||
{
|
|
||||||
CAmount nFeePerK = 0;
|
|
||||||
if (!ParseMoney(gArgs.GetArg("-paytxfee", ""), nFeePerK))
|
|
||||||
return InitError(AmountErrMsg("paytxfee", gArgs.GetArg("-paytxfee", "")));
|
|
||||||
if (nFeePerK > HIGH_TX_FEE_PER_KB)
|
|
||||||
InitWarning(AmountHighWarn("-paytxfee") + " " +
|
|
||||||
_("This is the transaction fee you will pay if you send a transaction."));
|
|
||||||
|
|
||||||
payTxFee = CFeeRate(nFeePerK, 1000);
|
|
||||||
if (payTxFee < ::minRelayTxFee)
|
|
||||||
{
|
|
||||||
return InitError(strprintf(_("Invalid amount for -paytxfee=<amount>: '%s' (must be at least %s)"),
|
|
||||||
gArgs.GetArg("-paytxfee", ""), ::minRelayTxFee.ToString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (gArgs.IsArgSet("-maxtxfee"))
|
if (gArgs.IsArgSet("-maxtxfee"))
|
||||||
{
|
{
|
||||||
CAmount nMaxFee = 0;
|
CAmount nMaxFee = 0;
|
||||||
|
@ -212,9 +163,6 @@ bool WalletInit::ParameterInteraction() const
|
||||||
gArgs.GetArg("-maxtxfee", ""), ::minRelayTxFee.ToString()));
|
gArgs.GetArg("-maxtxfee", ""), ::minRelayTxFee.ToString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nTxConfirmTarget = gArgs.GetArg("-txconfirmtarget", DEFAULT_TX_CONFIRM_TARGET);
|
|
||||||
bSpendZeroConfChange = gArgs.GetBoolArg("-spendzeroconfchange", DEFAULT_SPEND_ZEROCONF_CHANGE);
|
|
||||||
fWalletRbf = gArgs.GetBoolArg("-walletrbf", DEFAULT_WALLET_RBF);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -529,7 +529,7 @@ UniValue sendtoaddress(const JSONRPCRequest& request)
|
||||||
|
|
||||||
CCoinControl coin_control;
|
CCoinControl coin_control;
|
||||||
if (!request.params[5].isNull()) {
|
if (!request.params[5].isNull()) {
|
||||||
coin_control.signalRbf = request.params[5].get_bool();
|
coin_control.m_signal_bip125_rbf = request.params[5].get_bool();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!request.params[6].isNull()) {
|
if (!request.params[6].isNull()) {
|
||||||
|
@ -1108,7 +1108,7 @@ UniValue sendmany(const JSONRPCRequest& request)
|
||||||
|
|
||||||
CCoinControl coin_control;
|
CCoinControl coin_control;
|
||||||
if (!request.params[5].isNull()) {
|
if (!request.params[5].isNull()) {
|
||||||
coin_control.signalRbf = request.params[5].get_bool();
|
coin_control.m_signal_bip125_rbf = request.params[5].get_bool();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!request.params[6].isNull()) {
|
if (!request.params[6].isNull()) {
|
||||||
|
@ -2757,10 +2757,10 @@ UniValue settxfee(const JSONRPCRequest& request)
|
||||||
return NullUniValue;
|
return NullUniValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.fHelp || request.params.size() < 1 || request.params.size() > 1)
|
if (request.fHelp || request.params.size() < 1 || request.params.size() > 1) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"settxfee amount\n"
|
"settxfee amount\n"
|
||||||
"\nSet the transaction fee per kB. Overwrites the paytxfee parameter.\n"
|
"\nSet the transaction fee per kB for this wallet. Overrides the global -paytxfee command line parameter.\n"
|
||||||
"\nArguments:\n"
|
"\nArguments:\n"
|
||||||
"1. amount (numeric or string, required) The transaction fee in " + CURRENCY_UNIT + "/kB\n"
|
"1. amount (numeric or string, required) The transaction fee in " + CURRENCY_UNIT + "/kB\n"
|
||||||
"\nResult\n"
|
"\nResult\n"
|
||||||
|
@ -2769,13 +2769,13 @@ UniValue settxfee(const JSONRPCRequest& request)
|
||||||
+ HelpExampleCli("settxfee", "0.00001")
|
+ HelpExampleCli("settxfee", "0.00001")
|
||||||
+ HelpExampleRpc("settxfee", "0.00001")
|
+ HelpExampleRpc("settxfee", "0.00001")
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
LOCK2(cs_main, pwallet->cs_wallet);
|
LOCK2(cs_main, pwallet->cs_wallet);
|
||||||
|
|
||||||
// Amount
|
|
||||||
CAmount nAmount = AmountFromValue(request.params[0]);
|
CAmount nAmount = AmountFromValue(request.params[0]);
|
||||||
|
|
||||||
payTxFee = CFeeRate(nAmount, 1000);
|
pwallet->m_pay_tx_fee = CFeeRate(nAmount, 1000);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2836,7 +2836,7 @@ UniValue getwalletinfo(const JSONRPCRequest& request)
|
||||||
if (pwallet->IsCrypted()) {
|
if (pwallet->IsCrypted()) {
|
||||||
obj.pushKV("unlocked_until", pwallet->nRelockTime);
|
obj.pushKV("unlocked_until", pwallet->nRelockTime);
|
||||||
}
|
}
|
||||||
obj.pushKV("paytxfee", ValueFromAmount(payTxFee.GetFeePerK()));
|
obj.pushKV("paytxfee", ValueFromAmount(pwallet->m_pay_tx_fee.GetFeePerK()));
|
||||||
if (!masterKeyID.IsNull())
|
if (!masterKeyID.IsNull())
|
||||||
obj.pushKV("hdmasterkeyid", masterKeyID.GetHex());
|
obj.pushKV("hdmasterkeyid", masterKeyID.GetHex());
|
||||||
return obj;
|
return obj;
|
||||||
|
@ -3217,7 +3217,7 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
|
||||||
subtractFeeFromOutputs = options["subtractFeeFromOutputs"].get_array();
|
subtractFeeFromOutputs = options["subtractFeeFromOutputs"].get_array();
|
||||||
|
|
||||||
if (options.exists("replaceable")) {
|
if (options.exists("replaceable")) {
|
||||||
coinControl.signalRbf = options["replaceable"].get_bool();
|
coinControl.m_signal_bip125_rbf = options["replaceable"].get_bool();
|
||||||
}
|
}
|
||||||
if (options.exists("conf_target")) {
|
if (options.exists("conf_target")) {
|
||||||
if (options.exists("feeRate")) {
|
if (options.exists("feeRate")) {
|
||||||
|
@ -3406,7 +3406,7 @@ UniValue bumpfee(const JSONRPCRequest& request)
|
||||||
// optional parameters
|
// optional parameters
|
||||||
CAmount totalFee = 0;
|
CAmount totalFee = 0;
|
||||||
CCoinControl coin_control;
|
CCoinControl coin_control;
|
||||||
coin_control.signalRbf = true;
|
coin_control.m_signal_bip125_rbf = true;
|
||||||
if (!request.params[1].isNull()) {
|
if (!request.params[1].isNull()) {
|
||||||
UniValue options = request.params[1];
|
UniValue options = request.params[1];
|
||||||
RPCTypeCheckObj(options,
|
RPCTypeCheckObj(options,
|
||||||
|
@ -3430,7 +3430,7 @@ UniValue bumpfee(const JSONRPCRequest& request)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.exists("replaceable")) {
|
if (options.exists("replaceable")) {
|
||||||
coin_control.signalRbf = options["replaceable"].get_bool();
|
coin_control.m_signal_bip125_rbf = options["replaceable"].get_bool();
|
||||||
}
|
}
|
||||||
if (options.exists("estimate_mode")) {
|
if (options.exists("estimate_mode")) {
|
||||||
if (!FeeModeFromString(options["estimate_mode"].get_str(), coin_control.m_fee_mode)) {
|
if (!FeeModeFromString(options["estimate_mode"].get_str(), coin_control.m_fee_mode)) {
|
||||||
|
|
|
@ -72,29 +72,8 @@ CWallet* GetWallet(const std::string& name)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Transaction fee set by the user */
|
|
||||||
CFeeRate payTxFee(DEFAULT_TRANSACTION_FEE);
|
|
||||||
unsigned int nTxConfirmTarget = DEFAULT_TX_CONFIRM_TARGET;
|
|
||||||
bool bSpendZeroConfChange = DEFAULT_SPEND_ZEROCONF_CHANGE;
|
|
||||||
bool fWalletRbf = DEFAULT_WALLET_RBF;
|
|
||||||
bool g_wallet_allow_fallback_fee = true; //<! will be defined via chainparams
|
|
||||||
|
|
||||||
const uint32_t BIP32_HARDENED_KEY_LIMIT = 0x80000000;
|
const uint32_t BIP32_HARDENED_KEY_LIMIT = 0x80000000;
|
||||||
|
|
||||||
/**
|
|
||||||
* Fees smaller than this (in satoshi) are considered zero fee (for transaction creation)
|
|
||||||
* Override with -mintxfee
|
|
||||||
*/
|
|
||||||
CFeeRate CWallet::minTxFee = CFeeRate(DEFAULT_TRANSACTION_MINFEE);
|
|
||||||
/**
|
|
||||||
* If fee estimation does not have enough data to provide estimates, use this fee instead.
|
|
||||||
* Has no effect if not using fee estimation
|
|
||||||
* Override with -fallbackfee
|
|
||||||
*/
|
|
||||||
CFeeRate CWallet::fallbackFee = CFeeRate(DEFAULT_FALLBACK_FEE);
|
|
||||||
|
|
||||||
CFeeRate CWallet::m_discard_rate = CFeeRate(DEFAULT_DISCARD_FEE);
|
|
||||||
|
|
||||||
const uint256 CMerkleTx::ABANDON_HASH(uint256S("0000000000000000000000000000000000000000000000000000000000000001"));
|
const uint256 CMerkleTx::ABANDON_HASH(uint256S("0000000000000000000000000000000000000000000000000000000000000001"));
|
||||||
|
|
||||||
/** @defgroup mapWallet
|
/** @defgroup mapWallet
|
||||||
|
@ -2054,7 +2033,7 @@ bool CWalletTx::IsTrusted() const
|
||||||
return true;
|
return true;
|
||||||
if (nDepth < 0)
|
if (nDepth < 0)
|
||||||
return false;
|
return false;
|
||||||
if (!bSpendZeroConfChange || !IsFromMe(ISMINE_ALL)) // using wtx's cached debit
|
if (!pwallet->m_spend_zero_conf_change || !IsFromMe(ISMINE_ALL)) // using wtx's cached debit
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Don't trust unconfirmed transactions from us unless they are in the mempool.
|
// Don't trust unconfirmed transactions from us unless they are in the mempool.
|
||||||
|
@ -2490,10 +2469,10 @@ bool CWallet::SelectCoinsMinConf(const CAmount& nTargetValue, const CoinEligibil
|
||||||
FeeCalculation feeCalc;
|
FeeCalculation feeCalc;
|
||||||
CCoinControl temp;
|
CCoinControl temp;
|
||||||
temp.m_confirm_target = 1008;
|
temp.m_confirm_target = 1008;
|
||||||
CFeeRate long_term_feerate = GetMinimumFeeRate(temp, ::mempool, ::feeEstimator, &feeCalc);
|
CFeeRate long_term_feerate = GetMinimumFeeRate(*this, temp, ::mempool, ::feeEstimator, &feeCalc);
|
||||||
|
|
||||||
// Calculate cost of change
|
// Calculate cost of change
|
||||||
CAmount cost_of_change = GetDiscardRate(::feeEstimator).GetFee(coin_selection_params.change_spend_size) + coin_selection_params.effective_fee.GetFee(coin_selection_params.change_output_size);
|
CAmount cost_of_change = GetDiscardRate(*this, ::feeEstimator).GetFee(coin_selection_params.change_spend_size) + coin_selection_params.effective_fee.GetFee(coin_selection_params.change_output_size);
|
||||||
|
|
||||||
// Filter by the min conf specs and add to utxo_pool and calculate effective value
|
// Filter by the min conf specs and add to utxo_pool and calculate effective value
|
||||||
for (const COutput &output : vCoins)
|
for (const COutput &output : vCoins)
|
||||||
|
@ -2590,11 +2569,11 @@ bool CWallet::SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAm
|
||||||
bool res = nTargetValue <= nValueFromPresetInputs ||
|
bool res = nTargetValue <= nValueFromPresetInputs ||
|
||||||
SelectCoinsMinConf(nTargetValue - nValueFromPresetInputs, CoinEligibilityFilter(1, 6, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used) ||
|
SelectCoinsMinConf(nTargetValue - nValueFromPresetInputs, CoinEligibilityFilter(1, 6, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used) ||
|
||||||
SelectCoinsMinConf(nTargetValue - nValueFromPresetInputs, CoinEligibilityFilter(1, 1, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used) ||
|
SelectCoinsMinConf(nTargetValue - nValueFromPresetInputs, CoinEligibilityFilter(1, 1, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used) ||
|
||||||
(bSpendZeroConfChange && SelectCoinsMinConf(nTargetValue - nValueFromPresetInputs, CoinEligibilityFilter(0, 1, 2), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) ||
|
(m_spend_zero_conf_change && SelectCoinsMinConf(nTargetValue - nValueFromPresetInputs, CoinEligibilityFilter(0, 1, 2), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) ||
|
||||||
(bSpendZeroConfChange && SelectCoinsMinConf(nTargetValue - nValueFromPresetInputs, CoinEligibilityFilter(0, 1, std::min((size_t)4, nMaxChainLength/3)), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) ||
|
(m_spend_zero_conf_change && SelectCoinsMinConf(nTargetValue - nValueFromPresetInputs, CoinEligibilityFilter(0, 1, std::min((size_t)4, nMaxChainLength/3)), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) ||
|
||||||
(bSpendZeroConfChange && SelectCoinsMinConf(nTargetValue - nValueFromPresetInputs, CoinEligibilityFilter(0, 1, nMaxChainLength/2), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) ||
|
(m_spend_zero_conf_change && SelectCoinsMinConf(nTargetValue - nValueFromPresetInputs, CoinEligibilityFilter(0, 1, nMaxChainLength/2), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) ||
|
||||||
(bSpendZeroConfChange && SelectCoinsMinConf(nTargetValue - nValueFromPresetInputs, CoinEligibilityFilter(0, 1, nMaxChainLength), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) ||
|
(m_spend_zero_conf_change && SelectCoinsMinConf(nTargetValue - nValueFromPresetInputs, CoinEligibilityFilter(0, 1, nMaxChainLength), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) ||
|
||||||
(bSpendZeroConfChange && !fRejectLongChains && SelectCoinsMinConf(nTargetValue - nValueFromPresetInputs, CoinEligibilityFilter(0, 1, std::numeric_limits<uint64_t>::max()), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used));
|
(m_spend_zero_conf_change && !fRejectLongChains && SelectCoinsMinConf(nTargetValue - nValueFromPresetInputs, CoinEligibilityFilter(0, 1, std::numeric_limits<uint64_t>::max()), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used));
|
||||||
|
|
||||||
// because SelectCoinsMinConf clears the setCoinsRet, we now add the possible inputs to the coinset
|
// because SelectCoinsMinConf clears the setCoinsRet, we now add the possible inputs to the coinset
|
||||||
setCoinsRet.insert(setPresetCoins.begin(), setPresetCoins.end());
|
setCoinsRet.insert(setPresetCoins.begin(), setPresetCoins.end());
|
||||||
|
@ -2813,10 +2792,10 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CTransac
|
||||||
CTxOut change_prototype_txout(0, scriptChange);
|
CTxOut change_prototype_txout(0, scriptChange);
|
||||||
coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout, SER_DISK, 0);
|
coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout, SER_DISK, 0);
|
||||||
|
|
||||||
CFeeRate discard_rate = GetDiscardRate(::feeEstimator);
|
CFeeRate discard_rate = GetDiscardRate(*this, ::feeEstimator);
|
||||||
|
|
||||||
// Get the fee rate to use effective values in coin selection
|
// Get the fee rate to use effective values in coin selection
|
||||||
CFeeRate nFeeRateNeeded = GetMinimumFeeRate(coin_control, ::mempool, ::feeEstimator, &feeCalc);
|
CFeeRate nFeeRateNeeded = GetMinimumFeeRate(*this, coin_control, ::mempool, ::feeEstimator, &feeCalc);
|
||||||
|
|
||||||
nFeeRet = 0;
|
nFeeRet = 0;
|
||||||
bool pick_new_inputs = true;
|
bool pick_new_inputs = true;
|
||||||
|
@ -2940,8 +2919,8 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CTransac
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
nFeeNeeded = GetMinimumFee(nBytes, coin_control, ::mempool, ::feeEstimator, &feeCalc);
|
nFeeNeeded = GetMinimumFee(*this, nBytes, coin_control, ::mempool, ::feeEstimator, &feeCalc);
|
||||||
if (feeCalc.reason == FeeReason::FALLBACK && !g_wallet_allow_fallback_fee) {
|
if (feeCalc.reason == FeeReason::FALLBACK && !m_allow_fallback_fee) {
|
||||||
// eventually allow a fallback fee
|
// eventually allow a fallback fee
|
||||||
strFailReason = _("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee.");
|
strFailReason = _("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee.");
|
||||||
return false;
|
return false;
|
||||||
|
@ -2968,7 +2947,7 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CTransac
|
||||||
// change output. Only try this once.
|
// change output. Only try this once.
|
||||||
if (nChangePosInOut == -1 && nSubtractFeeFromAmount == 0 && pick_new_inputs) {
|
if (nChangePosInOut == -1 && nSubtractFeeFromAmount == 0 && pick_new_inputs) {
|
||||||
unsigned int tx_size_with_change = nBytes + coin_selection_params.change_output_size + 2; // Add 2 as a buffer in case increasing # of outputs changes compact size
|
unsigned int tx_size_with_change = 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 = GetMinimumFee(tx_size_with_change, coin_control, ::mempool, ::feeEstimator, nullptr);
|
CAmount fee_needed_with_change = GetMinimumFee(*this, tx_size_with_change, coin_control, ::mempool, ::feeEstimator, nullptr);
|
||||||
CAmount minimum_value_for_change = GetDustThreshold(change_prototype_txout, discard_rate);
|
CAmount minimum_value_for_change = GetDustThreshold(change_prototype_txout, discard_rate);
|
||||||
if (nFeeRet >= fee_needed_with_change + minimum_value_for_change) {
|
if (nFeeRet >= fee_needed_with_change + minimum_value_for_change) {
|
||||||
pick_new_inputs = false;
|
pick_new_inputs = false;
|
||||||
|
@ -3035,7 +3014,7 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CTransac
|
||||||
// to avoid conflicting with other possible uses of nSequence,
|
// to avoid conflicting with other possible uses of nSequence,
|
||||||
// and in the spirit of "smallest possible change from prior
|
// and in the spirit of "smallest possible change from prior
|
||||||
// behavior."
|
// behavior."
|
||||||
const uint32_t nSequence = coin_control.signalRbf ? MAX_BIP125_RBF_SEQUENCE : (CTxIn::SEQUENCE_FINAL - 1);
|
const uint32_t nSequence = coin_control.m_signal_bip125_rbf.get_value_or(m_signal_rbf) ? MAX_BIP125_RBF_SEQUENCE : (CTxIn::SEQUENCE_FINAL - 1);
|
||||||
for (const auto& coin : selected_coins) {
|
for (const auto& coin : selected_coins) {
|
||||||
txNew.vin.push_back(CTxIn(coin.outpoint, CScript(), nSequence));
|
txNew.vin.push_back(CTxIn(coin.outpoint, CScript(), nSequence));
|
||||||
}
|
}
|
||||||
|
@ -4080,6 +4059,66 @@ CWallet* CWallet::CreateWalletFromFile(const std::string& name, const fs::path&
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (gArgs.IsArgSet("-mintxfee")) {
|
||||||
|
CAmount n = 0;
|
||||||
|
if (!ParseMoney(gArgs.GetArg("-mintxfee", ""), n) || 0 == n) {
|
||||||
|
InitError(AmountErrMsg("mintxfee", gArgs.GetArg("-mintxfee", "")));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (n > HIGH_TX_FEE_PER_KB) {
|
||||||
|
InitWarning(AmountHighWarn("-mintxfee") + " " +
|
||||||
|
_("This is the minimum transaction fee you pay on every transaction."));
|
||||||
|
}
|
||||||
|
walletInstance->m_min_fee = CFeeRate(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
walletInstance->m_allow_fallback_fee = Params().IsFallbackFeeEnabled();
|
||||||
|
if (gArgs.IsArgSet("-fallbackfee")) {
|
||||||
|
CAmount nFeePerK = 0;
|
||||||
|
if (!ParseMoney(gArgs.GetArg("-fallbackfee", ""), nFeePerK)) {
|
||||||
|
InitError(strprintf(_("Invalid amount for -fallbackfee=<amount>: '%s'"), gArgs.GetArg("-fallbackfee", "")));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (nFeePerK > HIGH_TX_FEE_PER_KB) {
|
||||||
|
InitWarning(AmountHighWarn("-fallbackfee") + " " +
|
||||||
|
_("This is the transaction fee you may pay when fee estimates are not available."));
|
||||||
|
}
|
||||||
|
walletInstance->m_fallback_fee = CFeeRate(nFeePerK);
|
||||||
|
walletInstance->m_allow_fallback_fee = nFeePerK != 0; //disable fallback fee in case value was set to 0, enable if non-null value
|
||||||
|
}
|
||||||
|
if (gArgs.IsArgSet("-discardfee")) {
|
||||||
|
CAmount nFeePerK = 0;
|
||||||
|
if (!ParseMoney(gArgs.GetArg("-discardfee", ""), nFeePerK)) {
|
||||||
|
InitError(strprintf(_("Invalid amount for -discardfee=<amount>: '%s'"), gArgs.GetArg("-discardfee", "")));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (nFeePerK > HIGH_TX_FEE_PER_KB) {
|
||||||
|
InitWarning(AmountHighWarn("-discardfee") + " " +
|
||||||
|
_("This is the transaction fee you may discard if change is smaller than dust at this level"));
|
||||||
|
}
|
||||||
|
walletInstance->m_discard_rate = CFeeRate(nFeePerK);
|
||||||
|
}
|
||||||
|
if (gArgs.IsArgSet("-paytxfee")) {
|
||||||
|
CAmount nFeePerK = 0;
|
||||||
|
if (!ParseMoney(gArgs.GetArg("-paytxfee", ""), nFeePerK)) {
|
||||||
|
InitError(AmountErrMsg("paytxfee", gArgs.GetArg("-paytxfee", "")));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (nFeePerK > HIGH_TX_FEE_PER_KB) {
|
||||||
|
InitWarning(AmountHighWarn("-paytxfee") + " " +
|
||||||
|
_("This is the transaction fee you will pay if you send a transaction."));
|
||||||
|
}
|
||||||
|
walletInstance->m_pay_tx_fee = CFeeRate(nFeePerK, 1000);
|
||||||
|
if (walletInstance->m_pay_tx_fee < ::minRelayTxFee) {
|
||||||
|
InitError(strprintf(_("Invalid amount for -paytxfee=<amount>: '%s' (must be at least %s)"),
|
||||||
|
gArgs.GetArg("-paytxfee", ""), ::minRelayTxFee.ToString()));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walletInstance->m_confirm_target = gArgs.GetArg("-txconfirmtarget", DEFAULT_TX_CONFIRM_TARGET);
|
||||||
|
walletInstance->m_spend_zero_conf_change = gArgs.GetBoolArg("-spendzeroconfchange", DEFAULT_SPEND_ZEROCONF_CHANGE);
|
||||||
|
walletInstance->m_signal_rbf = gArgs.GetBoolArg("-walletrbf", DEFAULT_WALLET_RBF);
|
||||||
|
|
||||||
LogPrintf(" wallet %15dms\n", GetTimeMillis() - nStart);
|
LogPrintf(" wallet %15dms\n", GetTimeMillis() - nStart);
|
||||||
|
|
||||||
// Try to top up keypool. No-op if the wallet is locked.
|
// Try to top up keypool. No-op if the wallet is locked.
|
||||||
|
|
|
@ -38,22 +38,13 @@ bool HasWallets();
|
||||||
std::vector<CWallet*> GetWallets();
|
std::vector<CWallet*> GetWallets();
|
||||||
CWallet* GetWallet(const std::string& name);
|
CWallet* GetWallet(const std::string& name);
|
||||||
|
|
||||||
/**
|
|
||||||
* Settings
|
|
||||||
*/
|
|
||||||
extern CFeeRate payTxFee;
|
|
||||||
extern unsigned int nTxConfirmTarget;
|
|
||||||
extern bool bSpendZeroConfChange;
|
|
||||||
extern bool fWalletRbf;
|
|
||||||
extern bool g_wallet_allow_fallback_fee;
|
|
||||||
|
|
||||||
//! Default for -keypool
|
//! Default for -keypool
|
||||||
static const unsigned int DEFAULT_KEYPOOL_SIZE = 1000;
|
static const unsigned int DEFAULT_KEYPOOL_SIZE = 1000;
|
||||||
//! -paytxfee default
|
//! -paytxfee default
|
||||||
static const CAmount DEFAULT_TRANSACTION_FEE = 0;
|
constexpr CAmount DEFAULT_PAY_TX_FEE = 0;
|
||||||
//! -fallbackfee default
|
//! -fallbackfee default
|
||||||
static const CAmount DEFAULT_FALLBACK_FEE = 20000;
|
static const CAmount DEFAULT_FALLBACK_FEE = 20000;
|
||||||
//! -m_discard_rate default
|
//! -discardfee default
|
||||||
static const CAmount DEFAULT_DISCARD_FEE = 10000;
|
static const CAmount DEFAULT_DISCARD_FEE = 10000;
|
||||||
//! -mintxfee default
|
//! -mintxfee default
|
||||||
static const CAmount DEFAULT_TRANSACTION_MINFEE = 1000;
|
static const CAmount DEFAULT_TRANSACTION_MINFEE = 1000;
|
||||||
|
@ -968,9 +959,19 @@ public:
|
||||||
bool DummySignTx(CMutableTransaction &txNew, const std::vector<CTxOut> &txouts) const;
|
bool DummySignTx(CMutableTransaction &txNew, const std::vector<CTxOut> &txouts) const;
|
||||||
bool DummySignInput(CTxIn &tx_in, const CTxOut &txout) const;
|
bool DummySignInput(CTxIn &tx_in, const CTxOut &txout) const;
|
||||||
|
|
||||||
static CFeeRate minTxFee;
|
CFeeRate m_pay_tx_fee{DEFAULT_PAY_TX_FEE};
|
||||||
static CFeeRate fallbackFee;
|
unsigned int m_confirm_target{DEFAULT_TX_CONFIRM_TARGET};
|
||||||
static CFeeRate m_discard_rate;
|
bool m_spend_zero_conf_change{DEFAULT_SPEND_ZEROCONF_CHANGE};
|
||||||
|
bool m_signal_rbf{DEFAULT_WALLET_RBF};
|
||||||
|
bool m_allow_fallback_fee{true}; //<! will be defined via chainparams
|
||||||
|
CFeeRate m_min_fee{DEFAULT_TRANSACTION_MINFEE}; //!< Override with -mintxfee
|
||||||
|
/**
|
||||||
|
* If fee estimation does not have enough data to provide estimates, use this fee instead.
|
||||||
|
* Has no effect if not using fee estimation
|
||||||
|
* Override with -fallbackfee
|
||||||
|
*/
|
||||||
|
CFeeRate m_fallback_fee{DEFAULT_FALLBACK_FEE};
|
||||||
|
CFeeRate m_discard_rate{DEFAULT_DISCARD_FEE};
|
||||||
OutputType m_default_address_type{DEFAULT_ADDRESS_TYPE};
|
OutputType m_default_address_type{DEFAULT_ADDRESS_TYPE};
|
||||||
OutputType m_default_change_type{OutputType::NONE}; // Default to OutputType::NONE if not set by -changetype
|
OutputType m_default_change_type{OutputType::NONE}; // Default to OutputType::NONE if not set by -changetype
|
||||||
|
|
||||||
|
|
|
@ -150,5 +150,12 @@ class MultiWalletTest(BitcoinTestFramework):
|
||||||
assert_equal(batch[0]["result"]["chain"], "regtest")
|
assert_equal(batch[0]["result"]["chain"], "regtest")
|
||||||
assert_equal(batch[1]["result"]["walletname"], "w1")
|
assert_equal(batch[1]["result"]["walletname"], "w1")
|
||||||
|
|
||||||
|
self.log.info('Check for per-wallet settxfee call')
|
||||||
|
assert_equal(w1.getwalletinfo()['paytxfee'], 0)
|
||||||
|
assert_equal(w2.getwalletinfo()['paytxfee'], 0)
|
||||||
|
w2.settxfee(4.0)
|
||||||
|
assert_equal(w1.getwalletinfo()['paytxfee'], 0)
|
||||||
|
assert_equal(w2.getwalletinfo()['paytxfee'], 4.0)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
MultiWalletTest().main()
|
MultiWalletTest().main()
|
||||||
|
|
Loading…
Reference in a new issue