mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-02-03 22:03:01 +01:00
Merge #8456: [RPC] Simplified bumpfee command.
cc0243a[RPC] bumpfee (mrbandrews)52dde66[wallet] Add include_unsafe argument to listunspent RPC (Russell Yanofsky)766e8a4[wallet] Add IsAllFromMe: true if all inputs are from wallet (Suhas Daftuar)
This commit is contained in:
@@ -117,6 +117,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
||||
{ "setnetworkactive", 0, "state" },
|
||||
{ "getmempoolancestors", 1, "verbose" },
|
||||
{ "getmempooldescendants", 1, "verbose" },
|
||||
{ "bumpfee", 1, "options" },
|
||||
// Echo with conversion (For testing only)
|
||||
{ "echojson", 0, "arg0" },
|
||||
{ "echojson", 1, "arg1" },
|
||||
|
||||
@@ -79,16 +79,20 @@ void RPCTypeCheck(const UniValue& params,
|
||||
break;
|
||||
|
||||
const UniValue& v = params[i];
|
||||
if (!((v.type() == t) || (fAllowNull && (v.isNull()))))
|
||||
{
|
||||
string err = strprintf("Expected type %s, got %s",
|
||||
uvTypeName(t), uvTypeName(v.type()));
|
||||
throw JSONRPCError(RPC_TYPE_ERROR, err);
|
||||
if (!(fAllowNull && v.isNull())) {
|
||||
RPCTypeCheckArgument(v, t);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
void RPCTypeCheckArgument(const UniValue& value, UniValue::VType typeExpected)
|
||||
{
|
||||
if (value.type() != typeExpected) {
|
||||
throw JSONRPCError(RPC_TYPE_ERROR, strprintf("Expected type %s, got %s", uvTypeName(typeExpected), uvTypeName(value.type())));
|
||||
}
|
||||
}
|
||||
|
||||
void RPCTypeCheckObj(const UniValue& o,
|
||||
const map<string, UniValueType>& typesExpected,
|
||||
bool fAllowNull,
|
||||
|
||||
@@ -78,6 +78,11 @@ bool RPCIsInWarmup(std::string *statusOut);
|
||||
void RPCTypeCheck(const UniValue& params,
|
||||
const std::list<UniValue::VType>& typesExpected, bool fAllowNull=false);
|
||||
|
||||
/**
|
||||
* Type-check one argument; throws JSONRPCError if wrong type given.
|
||||
*/
|
||||
void RPCTypeCheckArgument(const UniValue& value, UniValue::VType typeExpected);
|
||||
|
||||
/*
|
||||
Check for expected keys/value types in an Object.
|
||||
*/
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
#include "init.h"
|
||||
#include "validation.h"
|
||||
#include "net.h"
|
||||
#include "policy/policy.h"
|
||||
#include "policy/rbf.h"
|
||||
#include "rpc/server.h"
|
||||
#include "script/sign.h"
|
||||
#include "timedata.h"
|
||||
#include "util.h"
|
||||
#include "utilmoneystr.h"
|
||||
@@ -2364,9 +2366,9 @@ UniValue listunspent(const JSONRPCRequest& request)
|
||||
if (!EnsureWalletIsAvailable(request.fHelp))
|
||||
return NullUniValue;
|
||||
|
||||
if (request.fHelp || request.params.size() > 3)
|
||||
if (request.fHelp || request.params.size() > 4)
|
||||
throw runtime_error(
|
||||
"listunspent ( minconf maxconf [\"addresses\",...] )\n"
|
||||
"listunspent ( minconf maxconf [\"addresses\",...] [include_unsafe] )\n"
|
||||
"\nReturns array of unspent transaction outputs\n"
|
||||
"with between minconf and maxconf (inclusive) confirmations.\n"
|
||||
"Optionally filter to only include txouts paid to specified addresses.\n"
|
||||
@@ -2378,6 +2380,10 @@ UniValue listunspent(const JSONRPCRequest& request)
|
||||
" \"address\" (string) bitcoin address\n"
|
||||
" ,...\n"
|
||||
" ]\n"
|
||||
"4. include_unsafe (bool, optional, default=true) Include outputs that are not safe to spend\n"
|
||||
" because they come from unconfirmed untrusted transactions or unconfirmed\n"
|
||||
" replacement transactions (cases where we are less sure that a conflicting\n"
|
||||
" transaction won't be mined).\n"
|
||||
"\nResult\n"
|
||||
"[ (array of json object)\n"
|
||||
" {\n"
|
||||
@@ -2401,18 +2407,21 @@ UniValue listunspent(const JSONRPCRequest& request)
|
||||
+ HelpExampleRpc("listunspent", "6, 9999999 \"[\\\"1PGFqEzfmQch1gKD3ra4k18PNj3tTUUSqg\\\",\\\"1LtvqCaApEdUGFkpKMM4MstjcaL4dKg8SP\\\"]\"")
|
||||
);
|
||||
|
||||
RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VNUM)(UniValue::VNUM)(UniValue::VARR));
|
||||
|
||||
int nMinDepth = 1;
|
||||
if (request.params.size() > 0)
|
||||
if (request.params.size() > 0 && !request.params[0].isNull()) {
|
||||
RPCTypeCheckArgument(request.params[0], UniValue::VNUM);
|
||||
nMinDepth = request.params[0].get_int();
|
||||
}
|
||||
|
||||
int nMaxDepth = 9999999;
|
||||
if (request.params.size() > 1)
|
||||
if (request.params.size() > 1 && !request.params[1].isNull()) {
|
||||
RPCTypeCheckArgument(request.params[1], UniValue::VNUM);
|
||||
nMaxDepth = request.params[1].get_int();
|
||||
}
|
||||
|
||||
set<CBitcoinAddress> setAddress;
|
||||
if (request.params.size() > 2) {
|
||||
if (request.params.size() > 2 && !request.params[2].isNull()) {
|
||||
RPCTypeCheckArgument(request.params[2], UniValue::VARR);
|
||||
UniValue inputs = request.params[2].get_array();
|
||||
for (unsigned int idx = 0; idx < inputs.size(); idx++) {
|
||||
const UniValue& input = inputs[idx];
|
||||
@@ -2425,11 +2434,17 @@ UniValue listunspent(const JSONRPCRequest& request)
|
||||
}
|
||||
}
|
||||
|
||||
bool include_unsafe = true;
|
||||
if (request.params.size() > 3 && !request.params[3].isNull()) {
|
||||
RPCTypeCheckArgument(request.params[3], UniValue::VBOOL);
|
||||
include_unsafe = request.params[3].get_bool();
|
||||
}
|
||||
|
||||
UniValue results(UniValue::VARR);
|
||||
vector<COutput> vecOutputs;
|
||||
assert(pwalletMain != NULL);
|
||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
pwalletMain->AvailableCoins(vecOutputs, false, NULL, true);
|
||||
pwalletMain->AvailableCoins(vecOutputs, !include_unsafe, NULL, true);
|
||||
BOOST_FOREACH(const COutput& out, vecOutputs) {
|
||||
if (out.nDepth < nMinDepth || out.nDepth > nMaxDepth)
|
||||
continue;
|
||||
@@ -2619,6 +2634,261 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
|
||||
return result;
|
||||
}
|
||||
|
||||
UniValue bumpfee(const JSONRPCRequest& request)
|
||||
{
|
||||
if (!EnsureWalletIsAvailable(request.fHelp)) {
|
||||
return NullUniValue;
|
||||
}
|
||||
|
||||
if (request.fHelp || request.params.size() < 1 || request.params.size() > 2) {
|
||||
throw runtime_error(
|
||||
"bumpfee \"txid\" ( options ) \n"
|
||||
"\nBumps the fee of an opt-in-RBF transaction T, replacing it with a new transaction B.\n"
|
||||
"An opt-in RBF transaction with the given txid must be in the wallet.\n"
|
||||
"The command will pay the additional fee by decreasing (or perhaps removing) its change output.\n"
|
||||
"If the change output is not big enough to cover the increased fee, the command will currently fail\n"
|
||||
"instead of adding new inputs to compensate. (A future implementation could improve this.)\n"
|
||||
"The command will fail if the wallet or mempool contains a transaction that spends one of T's outputs.\n"
|
||||
"By default, the new fee will be calculated automatically using estimatefee.\n"
|
||||
"The user can specify a confirmation target for estimatefee.\n"
|
||||
"Alternatively, the user can specify totalFee, or use RPC setpaytxfee to set a higher fee rate.\n"
|
||||
"At a minimum, the new fee rate must be high enough to pay a new relay fee (relay fee amount returned\n"
|
||||
"by getnetworkinfo RPC) and to enter the node's mempool.\n"
|
||||
"\nArguments:\n"
|
||||
"1. txid (string, required) The txid to be bumped\n"
|
||||
"2. options (object, optional)\n"
|
||||
" {\n"
|
||||
" \"confTarget\" (numeric, optional) Confirmation target (in blocks)\n"
|
||||
" \"totalFee\" (numeric, optional) Total fee (NOT feerate) to pay, in satoshis.\n"
|
||||
" In rare cases, the actual fee paid might be slightly higher than the specified\n"
|
||||
" totalFee if the tx change output has to be removed because it is too close to\n"
|
||||
" the dust threshold.\n"
|
||||
" \"replaceable\" (boolean, optional, default true) Whether the new transaction should still be\n"
|
||||
" marked bip-125 replaceable. If true, the sequence numbers in the transaction will\n"
|
||||
" be left unchanged from the original. If false, any input sequence numbers in the\n"
|
||||
" original transaction that were less than 0xfffffffe will be increased to 0xfffffffe\n"
|
||||
" so the new transaction will not be explicitly bip-125 replaceable (though it may\n"
|
||||
" still be replacable in practice, for example if it has unconfirmed ancestors which\n"
|
||||
" are replaceable).\n"
|
||||
" }\n"
|
||||
"\nResult:\n"
|
||||
"{\n"
|
||||
" \"txid\": \"value\", (string) The id of the new transaction\n"
|
||||
" \"oldfee\": n, (numeric) Fee of the replaced transaction\n"
|
||||
" \"fee\": n, (numeric) Fee of the new transaction\n"
|
||||
"}\n"
|
||||
"\nExamples:\n"
|
||||
"\nBump the fee, get the new transaction\'s txid\n" +
|
||||
HelpExampleCli("bumpfee", "<txid>"));
|
||||
}
|
||||
|
||||
RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VOBJ));
|
||||
uint256 hash;
|
||||
hash.SetHex(request.params[0].get_str());
|
||||
|
||||
// retrieve the original tx from the wallet
|
||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
EnsureWalletIsUnlocked();
|
||||
if (!pwalletMain->mapWallet.count(hash)) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id");
|
||||
}
|
||||
CWalletTx& wtx = pwalletMain->mapWallet[hash];
|
||||
|
||||
if (pwalletMain->HasWalletSpend(hash)) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Transaction has descendants in the wallet");
|
||||
}
|
||||
|
||||
{
|
||||
LOCK(mempool.cs);
|
||||
auto it = mempool.mapTx.find(hash);
|
||||
if (it != mempool.mapTx.end() && it->GetCountWithDescendants() > 1) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Transaction has descendants in the mempool");
|
||||
}
|
||||
}
|
||||
|
||||
if (wtx.GetDepthInMainChain() != 0) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction has been mined, or is conflicted with a mined transaction");
|
||||
}
|
||||
|
||||
if (!SignalsOptInRBF(wtx)) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction is not BIP 125 replaceable");
|
||||
}
|
||||
|
||||
if (wtx.mapValue.count("replaced_by_txid")) {
|
||||
throw JSONRPCError(RPC_INVALID_REQUEST, strprintf("Cannot bump transaction %s which was already bumped by transaction %s", hash.ToString(), wtx.mapValue.at("replaced_by_txid")));
|
||||
}
|
||||
|
||||
// check that original tx consists entirely of our inputs
|
||||
// if not, we can't bump the fee, because the wallet has no way of knowing the value of the other inputs (thus the fee)
|
||||
if (!pwalletMain->IsAllFromMe(wtx, ISMINE_SPENDABLE)) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction contains inputs that don't belong to this wallet");
|
||||
}
|
||||
|
||||
// figure out which output was change
|
||||
// if there was no change output or multiple change outputs, fail
|
||||
int nOutput = -1;
|
||||
for (size_t i = 0; i < wtx.tx->vout.size(); ++i) {
|
||||
if (pwalletMain->IsChange(wtx.tx->vout[i])) {
|
||||
if (nOutput != -1) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Transaction has multiple change outputs");
|
||||
}
|
||||
nOutput = i;
|
||||
}
|
||||
}
|
||||
if (nOutput == -1) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Transaction does not have a change output");
|
||||
}
|
||||
|
||||
// optional parameters
|
||||
bool specifiedConfirmTarget = false;
|
||||
int newConfirmTarget = nTxConfirmTarget;
|
||||
CAmount totalFee = 0;
|
||||
bool replaceable = true;
|
||||
if (request.params.size() > 1) {
|
||||
UniValue options = request.params[1];
|
||||
RPCTypeCheckObj(options,
|
||||
{
|
||||
{"confTarget", UniValueType(UniValue::VNUM)},
|
||||
{"totalFee", UniValueType(UniValue::VNUM)},
|
||||
{"replaceable", UniValueType(UniValue::VBOOL)},
|
||||
},
|
||||
true, true);
|
||||
|
||||
if (options.exists("confTarget") && options.exists("totalFee")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "confTarget and totalFee options should not both be set. Please provide either a confirmation target for fee estimation or an explicit total fee for the transaction.");
|
||||
} else if (options.exists("confTarget")) {
|
||||
specifiedConfirmTarget = true;
|
||||
newConfirmTarget = options["confTarget"].get_int();
|
||||
if (newConfirmTarget <= 0) { // upper-bound will be checked by estimatefee/smartfee
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid confTarget (cannot be <= 0)");
|
||||
}
|
||||
} else if (options.exists("totalFee")) {
|
||||
totalFee = options["totalFee"].get_int64();
|
||||
if (totalFee <= 0) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid totalFee (cannot be <= 0)");
|
||||
} else if (totalFee > maxTxFee) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid totalFee (cannot be higher than maxTxFee)");
|
||||
}
|
||||
}
|
||||
|
||||
if (options.exists("replaceable")) {
|
||||
replaceable = options["replaceable"].get_bool();
|
||||
}
|
||||
}
|
||||
|
||||
// signature sizes can vary by a byte, so add 1 for each input when calculating the new fee
|
||||
int64_t txSize = GetVirtualTransactionSize(*(wtx.tx));
|
||||
const int64_t maxNewTxSize = txSize + wtx.tx->vin.size();
|
||||
|
||||
// calculate the old fee and fee-rate
|
||||
CAmount nOldFee = wtx.GetDebit(ISMINE_SPENDABLE) - wtx.tx->GetValueOut();
|
||||
CFeeRate nOldFeeRate(nOldFee, txSize);
|
||||
CAmount nNewFee;
|
||||
CFeeRate nNewFeeRate;
|
||||
|
||||
if (totalFee > 0) {
|
||||
CAmount minTotalFee = nOldFeeRate.GetFee(maxNewTxSize) + minRelayTxFee.GetFee(maxNewTxSize);
|
||||
if (totalFee < minTotalFee) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid totalFee, must be at least %s (oldFee %s + relayFee %s)", FormatMoney(minTotalFee), nOldFeeRate.GetFee(maxNewTxSize), minRelayTxFee.GetFee(maxNewTxSize)));
|
||||
}
|
||||
nNewFee = totalFee;
|
||||
nNewFeeRate = CFeeRate(totalFee, maxNewTxSize);
|
||||
} else {
|
||||
// use the user-defined payTxFee if possible, otherwise use smartfee / fallbackfee
|
||||
if (!specifiedConfirmTarget && payTxFee.GetFeePerK() != 0) {
|
||||
nNewFeeRate = payTxFee;
|
||||
} else {
|
||||
nNewFeeRate = mempool.estimateSmartFee(newConfirmTarget);
|
||||
}
|
||||
if (nNewFeeRate.GetFeePerK() == 0) {
|
||||
nNewFeeRate = CWallet::fallbackFee;
|
||||
}
|
||||
|
||||
// new fee rate must be at least old rate + minimum relay rate
|
||||
if (nNewFeeRate.GetFeePerK() < nOldFeeRate.GetFeePerK() + ::minRelayTxFee.GetFeePerK()) {
|
||||
nNewFeeRate = CFeeRate(nOldFeeRate.GetFeePerK() + ::minRelayTxFee.GetFeePerK());
|
||||
}
|
||||
|
||||
nNewFee = nNewFeeRate.GetFee(maxNewTxSize);
|
||||
}
|
||||
|
||||
// check that fee rate is higher than mempool's minimum fee
|
||||
// (no point in bumping fee if we know that the new tx won't be accepted to the mempool)
|
||||
// This may occur if the user set TotalFee or paytxfee too low, if fallbackfee is too low, or, perhaps,
|
||||
// in a rare situation where the mempool minimum fee increased significantly since the fee estimation just a
|
||||
// moment earlier. In this case, we report an error to the user, who may use totalFee to make an adjustment.
|
||||
CFeeRate minMempoolFeeRate = mempool.GetMinFee(GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000);
|
||||
if (nNewFeeRate.GetFeePerK() < minMempoolFeeRate.GetFeePerK()) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, strprintf("New fee rate (%s) is less than the minimum fee rate (%s) to get into the mempool. totalFee value should to be at least %s or settxfee value should be at least %s to add transaction.", FormatMoney(nNewFeeRate.GetFeePerK()), FormatMoney(minMempoolFeeRate.GetFeePerK()), FormatMoney(minMempoolFeeRate.GetFee(maxNewTxSize)), FormatMoney(minMempoolFeeRate.GetFeePerK())));
|
||||
}
|
||||
|
||||
// Now modify the output to increase the fee.
|
||||
// If the output is not large enough to pay the fee, fail.
|
||||
CAmount nDelta = nNewFee - nOldFee;
|
||||
assert(nDelta > 0);
|
||||
CMutableTransaction tx(*(wtx.tx));
|
||||
CTxOut* poutput = &(tx.vout[nOutput]);
|
||||
if (poutput->nValue < nDelta) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Change output is too small to bump the fee");
|
||||
}
|
||||
|
||||
// If the output would become dust, discard it (converting the dust to fee)
|
||||
poutput->nValue -= nDelta;
|
||||
if (poutput->nValue <= poutput->GetDustThreshold(::minRelayTxFee)) {
|
||||
LogPrint("rpc", "Bumping fee and discarding dust output\n");
|
||||
nNewFee += poutput->nValue;
|
||||
tx.vout.erase(tx.vout.begin() + nOutput);
|
||||
}
|
||||
|
||||
// Mark new tx not replaceable, if requested.
|
||||
if (!replaceable) {
|
||||
for (auto& input : tx.vin) {
|
||||
if (input.nSequence < 0xfffffffe) input.nSequence = 0xfffffffe;
|
||||
}
|
||||
}
|
||||
|
||||
// sign the new tx
|
||||
CTransaction txNewConst(tx);
|
||||
int nIn = 0;
|
||||
for (auto& input : tx.vin) {
|
||||
std::map<uint256, CWalletTx>::const_iterator mi = pwalletMain->mapWallet.find(input.prevout.hash);
|
||||
assert(mi != pwalletMain->mapWallet.end() && input.prevout.n < mi->second.tx->vout.size());
|
||||
const CScript& scriptPubKey = mi->second.tx->vout[input.prevout.n].scriptPubKey;
|
||||
const CAmount& amount = mi->second.tx->vout[input.prevout.n].nValue;
|
||||
SignatureData sigdata;
|
||||
if (!ProduceSignature(TransactionSignatureCreator(pwalletMain, &txNewConst, nIn, amount, SIGHASH_ALL), scriptPubKey, sigdata)) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Can't sign transaction.");
|
||||
}
|
||||
UpdateTransaction(tx, nIn, sigdata);
|
||||
nIn++;
|
||||
}
|
||||
|
||||
// commit/broadcast the tx
|
||||
CReserveKey reservekey(pwalletMain);
|
||||
CWalletTx wtxBumped(pwalletMain, MakeTransactionRef(std::move(tx)));
|
||||
wtxBumped.mapValue["replaces_txid"] = hash.ToString();
|
||||
CValidationState state;
|
||||
if (!pwalletMain->CommitTransaction(wtxBumped, reservekey, g_connman.get(), state) || !state.IsValid()) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Error: The transaction was rejected! Reason given: %s", state.GetRejectReason()));
|
||||
}
|
||||
|
||||
// mark the original tx as bumped
|
||||
if (!pwalletMain->MarkReplaced(wtx.GetHash(), wtxBumped.GetHash())) {
|
||||
// TODO: see if JSON-RPC has a standard way of returning a response
|
||||
// along with an exception. It would be good to return information about
|
||||
// wtxBumped to the caller even if marking the original transaction
|
||||
// replaced does not succeed for some reason.
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Error: Created new bumpfee transaction but could not mark the original transaction as replaced.");
|
||||
}
|
||||
|
||||
UniValue result(UniValue::VOBJ);
|
||||
result.push_back(Pair("txid", wtxBumped.GetHash().GetHex()));
|
||||
result.push_back(Pair("oldfee", ValueFromAmount(nOldFee)));
|
||||
result.push_back(Pair("fee", ValueFromAmount(nNewFee)));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
extern UniValue dumpprivkey(const JSONRPCRequest& request); // in rpcdump.cpp
|
||||
extern UniValue importprivkey(const JSONRPCRequest& request);
|
||||
extern UniValue importaddress(const JSONRPCRequest& request);
|
||||
@@ -2638,6 +2908,7 @@ static const CRPCCommand commands[] =
|
||||
{ "wallet", "addmultisigaddress", &addmultisigaddress, true, {"nrequired","keys","account"} },
|
||||
{ "wallet", "addwitnessaddress", &addwitnessaddress, true, {"address"} },
|
||||
{ "wallet", "backupwallet", &backupwallet, true, {"destination"} },
|
||||
{ "wallet", "bumpfee", &bumpfee, true, {"txid", "options"} },
|
||||
{ "wallet", "dumpprivkey", &dumpprivkey, true, {"address"} },
|
||||
{ "wallet", "dumpwallet", &dumpwallet, true, {"filename"} },
|
||||
{ "wallet", "encryptwallet", &encryptwallet, true, {"passphrase"} },
|
||||
@@ -2666,7 +2937,7 @@ static const CRPCCommand commands[] =
|
||||
{ "wallet", "listreceivedbyaddress", &listreceivedbyaddress, false, {"minconf","include_empty","include_watchonly"} },
|
||||
{ "wallet", "listsinceblock", &listsinceblock, false, {"blockhash","target_confirmations","include_watchonly"} },
|
||||
{ "wallet", "listtransactions", &listtransactions, false, {"account","count","skip","include_watchonly"} },
|
||||
{ "wallet", "listunspent", &listunspent, false, {"minconf","maxconf","addresses"} },
|
||||
{ "wallet", "listunspent", &listunspent, false, {"minconf","maxconf","addresses","include_unsafe"} },
|
||||
{ "wallet", "lockunspent", &lockunspent, true, {"unlock","transactions"} },
|
||||
{ "wallet", "move", &movecmd, false, {"fromaccount","toaccount","amount","minconf","comment"} },
|
||||
{ "wallet", "sendfrom", &sendfrom, false, {"fromaccount","toaddress","amount","minconf","comment","comment_to"} },
|
||||
|
||||
@@ -411,6 +411,13 @@ set<uint256> CWallet::GetConflicts(const uint256& txid) const
|
||||
return result;
|
||||
}
|
||||
|
||||
bool CWallet::HasWalletSpend(const uint256& txid) const
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
auto iter = mapTxSpends.lower_bound(COutPoint(txid, 0));
|
||||
return (iter != mapTxSpends.end() && iter->first.hash == txid);
|
||||
}
|
||||
|
||||
void CWallet::Flush(bool shutdown)
|
||||
{
|
||||
bitdb.Flush(shutdown);
|
||||
@@ -826,6 +833,35 @@ void CWallet::MarkDirty()
|
||||
}
|
||||
}
|
||||
|
||||
bool CWallet::MarkReplaced(const uint256& originalHash, const uint256& newHash)
|
||||
{
|
||||
LOCK(cs_wallet);
|
||||
|
||||
auto mi = mapWallet.find(originalHash);
|
||||
|
||||
// There is a bug if MarkReplaced is not called on an existing wallet transaction.
|
||||
assert(mi != mapWallet.end());
|
||||
|
||||
CWalletTx& wtx = (*mi).second;
|
||||
|
||||
// Ensure for now that we're not overwriting data
|
||||
assert(wtx.mapValue.count("replaced_by_txid") == 0);
|
||||
|
||||
wtx.mapValue["replaced_by_txid"] = newHash.ToString();
|
||||
|
||||
CWalletDB walletdb(strWalletFile, "r+");
|
||||
|
||||
bool success = true;
|
||||
if (!walletdb.WriteTx(wtx)) {
|
||||
LogPrintf("%s: Updating walletdb tx %s failed", __func__, wtx.GetHash().ToString());
|
||||
success = false;
|
||||
}
|
||||
|
||||
NotifyTransactionChanged(this, originalHash, CT_UPDATED);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose)
|
||||
{
|
||||
LOCK(cs_wallet);
|
||||
@@ -1154,6 +1190,8 @@ isminetype CWallet::IsMine(const CTxIn &txin) const
|
||||
return ISMINE_NO;
|
||||
}
|
||||
|
||||
// Note that this function doesn't distinguish between a 0-valued input,
|
||||
// and a not-"is mine" (according to the filter) input.
|
||||
CAmount CWallet::GetDebit(const CTxIn &txin, const isminefilter& filter) const
|
||||
{
|
||||
{
|
||||
@@ -1236,6 +1274,27 @@ CAmount CWallet::GetDebit(const CTransaction& tx, const isminefilter& filter) co
|
||||
return nDebit;
|
||||
}
|
||||
|
||||
bool CWallet::IsAllFromMe(const CTransaction& tx, const isminefilter& filter) const
|
||||
{
|
||||
LOCK(cs_wallet);
|
||||
|
||||
BOOST_FOREACH(const CTxIn& txin, tx.vin)
|
||||
{
|
||||
auto mi = mapWallet.find(txin.prevout.hash);
|
||||
if (mi == mapWallet.end())
|
||||
return false; // any unknown inputs can't be from us
|
||||
|
||||
const CWalletTx& prev = (*mi).second;
|
||||
|
||||
if (txin.prevout.n >= prev.tx->vout.size())
|
||||
return false; // invalid input!
|
||||
|
||||
if (!(IsMine(prev.tx->vout[txin.prevout.n]) & filter))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
CAmount CWallet::GetCredit(const CTransaction& tx, const isminefilter& filter) const
|
||||
{
|
||||
CAmount nCredit = 0;
|
||||
@@ -1958,6 +2017,37 @@ void CWallet::AvailableCoins(vector<COutput>& vCoins, bool fOnlyConfirmed, const
|
||||
if (nDepth == 0 && !pcoin->InMempool())
|
||||
continue;
|
||||
|
||||
// We should not consider coins from transactions that are replacing
|
||||
// other transactions.
|
||||
//
|
||||
// Example: There is a transaction A which is replaced by bumpfee
|
||||
// transaction B. In this case, we want to prevent creation of
|
||||
// a transaction B' which spends an output of B.
|
||||
//
|
||||
// Reason: If transaction A were initially confirmed, transactions B
|
||||
// and B' would no longer be valid, so the user would have to create
|
||||
// a new transaction C to replace B'. However, in the case of a
|
||||
// one-block reorg, transactions B' and C might BOTH be accepted,
|
||||
// when the user only wanted one of them. Specifically, there could
|
||||
// be a 1-block reorg away from the chain where transactions A and C
|
||||
// were accepted to another chain where B, B', and C were all
|
||||
// accepted.
|
||||
if (nDepth == 0 && fOnlyConfirmed && pcoin->mapValue.count("replaces_txid")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Similarly, we should not consider coins from transactions that
|
||||
// have been replaced. In the example above, we would want to prevent
|
||||
// creation of a transaction A' spending an output of A, because if
|
||||
// transaction B were initially confirmed, conflicting with A and
|
||||
// A', we wouldn't want to the user to create a transaction D
|
||||
// intending to replace A', but potentially resulting in a scenario
|
||||
// where A, A', and D could all be accepted (instead of just B and
|
||||
// D, or just A and A' like the user would want).
|
||||
if (nDepth == 0 && fOnlyConfirmed && pcoin->mapValue.count("replaced_by_txid")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (unsigned int i = 0; i < pcoin->tx->vout.size(); i++) {
|
||||
isminetype mine = IsMine(pcoin->tx->vout[i]);
|
||||
if (!(IsSpent(wtxid, i)) && mine != ISMINE_NO &&
|
||||
|
||||
@@ -825,6 +825,10 @@ public:
|
||||
std::set<CTxDestination> GetAccountAddresses(const std::string& strAccount) const;
|
||||
|
||||
isminetype IsMine(const CTxIn& txin) const;
|
||||
/**
|
||||
* Returns amount of debit if the input matches the
|
||||
* filter, otherwise returns 0
|
||||
*/
|
||||
CAmount GetDebit(const CTxIn& txin, const isminefilter& filter) const;
|
||||
isminetype IsMine(const CTxOut& txout) const;
|
||||
CAmount GetCredit(const CTxOut& txout, const isminefilter& filter) const;
|
||||
@@ -834,6 +838,8 @@ public:
|
||||
/** should probably be renamed to IsRelevantToMe */
|
||||
bool IsFromMe(const CTransaction& tx) const;
|
||||
CAmount GetDebit(const CTransaction& tx, const isminefilter& filter) const;
|
||||
/** Returns whether all of the inputs match the filter */
|
||||
bool IsAllFromMe(const CTransaction& tx, const isminefilter& filter) const;
|
||||
CAmount GetCredit(const CTransaction& tx, const isminefilter& filter) const;
|
||||
CAmount GetChange(const CTransaction& tx) const;
|
||||
void SetBestChain(const CBlockLocator& loc);
|
||||
@@ -885,6 +891,9 @@ public:
|
||||
//! Get wallet transactions that conflict with given transaction (spend same outputs)
|
||||
std::set<uint256> GetConflicts(const uint256& txid) const;
|
||||
|
||||
//! Check if a given transaction has any of its outputs spent by another transaction in the wallet
|
||||
bool HasWalletSpend(const uint256& txid) const;
|
||||
|
||||
//! Flush wallet (bitdb flush)
|
||||
void Flush(bool shutdown=false);
|
||||
|
||||
@@ -921,6 +930,9 @@ public:
|
||||
/* Mark a transaction (and it in-wallet descendants) as abandoned so its inputs may be respent. */
|
||||
bool AbandonTransaction(const uint256& hashTx);
|
||||
|
||||
/** Mark a transaction as replaced by another transaction (e.g., BIP 125). */
|
||||
bool MarkReplaced(const uint256& originalHash, const uint256& newHash);
|
||||
|
||||
/* Returns the wallets help message */
|
||||
static std::string GetWalletHelpString(bool showDebug);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user