Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions doc/release-notes-5861.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
RPC changes
-----------
- The `walletcreatefundedpsbt` RPC call will now fail with
`Insufficient funds` when inputs are manually selected but are not enough to cover
the outputs and fee. Additional inputs can automatically be added through the
new `add_inputs` option.

- The `fundrawtransaction` RPC now supports `add_inputs` option that when `false`
prevents adding more inputs if necessary and consequently the RPC fails.

- A new `send` RPC with similar syntax to `walletcreatefundedpsbt`, including
support for coin selection and a custom fee rate. The `send` RPC is experimental
and may change in subsequent releases. Using it is encouraged once it's no
longer experimental: `sendmany` and `sendtoaddress` may be deprecated in a future release.
3 changes: 3 additions & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "gettxoutsetinfo", 2, "use_index"},
{ "lockunspent", 0, "unlock" },
{ "lockunspent", 1, "transactions" },
{ "send", 0, "outputs" },
{ "send", 1, "conf_target" },
{ "send", 3, "options" },
{ "importprivkey", 2, "rescan" },
{ "importelectrumwallet", 1, "index" },
{ "importaddress", 2, "rescan" },
Expand Down
13 changes: 10 additions & 3 deletions src/rpc/rawtransaction_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,17 @@

CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime)
{
if (inputs_in.isNull() || outputs_in.isNull())
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, arguments 1 and 2 must be non-null");
if (outputs_in.isNull()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output argument must be non-null");
}

UniValue inputs;
if (inputs_in.isNull()) {
inputs = UniValue::VARR;
} else {
inputs = inputs_in.get_array();
}

UniValue inputs = inputs_in.get_array();
const bool outputs_is_obj = outputs_in.isObject();
UniValue outputs = outputs_is_obj ? outputs_in.get_obj() : outputs_in.get_array();

Expand Down
2 changes: 1 addition & 1 deletion src/wallet/coincontrol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
void CCoinControl::SetNull(bool fResetCoinType)
{
destChange = CNoDestination();
m_add_inputs = true;
fAllowOtherInputs = false;
fAllowWatchOnly = false;
m_avoid_partial_spends = gArgs.GetBoolArg("-avoidpartialspends", DEFAULT_AVOIDPARTIALSPENDS);
Expand All @@ -26,4 +27,3 @@ void CCoinControl::SetNull(bool fResetCoinType)
nCoinType = CoinType::ALL_COINS;
}
}

2 changes: 2 additions & 0 deletions src/wallet/coincontrol.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class CCoinControl
{
public:
CTxDestination destChange;
//! If false, only selected inputs are used
bool m_add_inputs;
//! If false, allows unselected inputs, but requires all selected inputs be used if fAllowOtherInputs is true (default)
bool fAllowOtherInputs;
//! If false, only include as many inputs as necessary to fulfill a coin selection request. Only usable together with fAllowOtherInputs
Expand Down
233 changes: 215 additions & 18 deletions src/wallet/rpcwallet.cpp

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/wallet/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2625,6 +2625,11 @@ void CWallet::AvailableCoins(std::vector<COutput> &vCoins, bool fOnlySafe, const
}
if(!found) continue;

// Only consider selected coins if add_inputs is false
if (coinControl && !coinControl->m_add_inputs && !coinControl->IsSelected(COutPoint(wtxid, i))) {
continue;
}

if (pcoin->tx->vout[i].nValue < nMinimumAmount || pcoin->tx->vout[i].nValue > nMaximumAmount)
continue;

Expand Down
16 changes: 13 additions & 3 deletions test/functional/rpc_fundrawtransaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def test_invalid_change_address(self):
dec_tx = self.nodes[2].decoderawtransaction(rawtx)
assert_equal(utx['txid'], dec_tx['vin'][0]['txid'])

assert_raises_rpc_error(-5, "changeAddress must be a valid Dash address", self.nodes[2].fundrawtransaction, rawtx, {'changeAddress':'foobar'})
assert_raises_rpc_error(-5, "Change address must be a valid Dash address", self.nodes[2].fundrawtransaction, rawtx, {'changeAddress':'foobar'})

def test_valid_change_address(self):
self.log.info("Test fundrawtxn with a provided change address")
Expand Down Expand Up @@ -264,7 +264,11 @@ def test_coin_selection(self):
assert_equal(utx['txid'], dec_tx['vin'][0]['txid'])
assert_equal("00", dec_tx['vin'][0]['scriptSig']['hex'])

# Should fail without add_inputs:
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
# add_inputs is enabled by default
rawtxfund = self.nodes[2].fundrawtransaction(rawtx)

dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex'])
totalOut = 0
matchingOuts = 0
Expand Down Expand Up @@ -292,7 +296,10 @@ def test_two_vin(self):
dec_tx = self.nodes[2].decoderawtransaction(rawtx)
assert_equal(utx['txid'], dec_tx['vin'][0]['txid'])

rawtxfund = self.nodes[2].fundrawtransaction(rawtx)
# Should fail without add_inputs:
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
rawtxfund = self.nodes[2].fundrawtransaction(rawtx, {"add_inputs": True})

dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex'])
totalOut = 0
matchingOuts = 0
Expand Down Expand Up @@ -323,7 +330,10 @@ def test_two_vin_two_vout(self):
dec_tx = self.nodes[2].decoderawtransaction(rawtx)
assert_equal(utx['txid'], dec_tx['vin'][0]['txid'])

rawtxfund = self.nodes[2].fundrawtransaction(rawtx)
# Should fail without add_inputs:
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
rawtxfund = self.nodes[2].fundrawtransaction(rawtx, {"add_inputs": True})

dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex'])
totalOut = 0
matchingOuts = 0
Expand Down
22 changes: 16 additions & 6 deletions test/functional/rpc_psbt.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
from decimal import Decimal
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_approx,
assert_equal,
assert_greater_than,
assert_raises_rpc_error,
find_output
)
Expand All @@ -31,6 +31,16 @@ def run_test(self):
# Create and fund a raw tx for sending 10 DASH
psbtx1 = self.nodes[0].walletcreatefundedpsbt([], {self.nodes[2].getnewaddress():10})['psbt']

# If inputs are specified, do not automatically add more:
utxo1 = self.nodes[0].listunspent()[0]
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[0].walletcreatefundedpsbt, [{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():900})

psbtx1 = self.nodes[0].walletcreatefundedpsbt([{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():900}, 0, {"add_inputs": True})['psbt']
assert_equal(len(self.nodes[0].decodepsbt(psbtx1)['tx']['vin']), 2)

# Inputs argument can be null
self.nodes[0].walletcreatefundedpsbt(None, {self.nodes[2].getnewaddress():10})

# Node 1 should not be able to add anything to it but still return the psbtx same as before
psbtx = self.nodes[1].walletprocesspsbt(psbtx1)['psbt']
assert_equal(psbtx1, psbtx)
Expand Down Expand Up @@ -96,16 +106,16 @@ def run_test(self):
self.nodes[1].sendrawtransaction(self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex'])

# feeRate of 0.1 DASH / KB produces a total fee slightly below -maxtxfee (~0.06650000):
res = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():9.99}, 0, {"feeRate": 0.1}, False)
assert_greater_than(res["fee"], 0.03)
assert_greater_than(0.04, res["fee"])
res = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():9.99}, 0, {"feeRate": 0.1, "add_inputs": True}, False)
assert_approx(res["fee"], 0.04, 0.03)
decoded_psbt = self.nodes[0].decodepsbt(res['psbt'])
for psbt_in in decoded_psbt["inputs"]:
assert "bip32_derivs" not in psbt_in

# feeRate of 10 DASH / KB produces a total fee well above -maxtxfee
# previously this was silently capped at -maxtxfee
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", self.nodes[1].walletcreatefundedpsbt, [{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():9.99}, 0, {"feeRate": 10})
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", self.nodes[1].walletcreatefundedpsbt, [{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():9.99}, 0, {"feeRate": 10, "add_inputs": True})
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", self.nodes[1].walletcreatefundedpsbt, [{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():1}, 0, {"feeRate": 10, "add_inputs": True})

# partially sign multisig things with node 1
psbtx = wmulti.walletcreatefundedpsbt(inputs=[{"txid":txid,"vout":p2sh_pos}], outputs={self.nodes[1].getnewaddress():9.99}, options={'changeAddress': self.nodes[1].getrawchangeaddress()})['psbt']
Expand Down Expand Up @@ -185,7 +195,7 @@ def run_test(self):

# Regression test for 14473 (mishandling of already-signed witness transaction):
unspent = self.nodes[0].listunspent()[0]
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}])
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], 0, {"add_inputs": True})
complete_psbt = self.nodes[0].walletprocesspsbt(psbtx_info["psbt"])
double_processed_psbt = self.nodes[0].walletprocesspsbt(complete_psbt["psbt"])
assert_equal(complete_psbt, double_processed_psbt)
Expand Down
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@
'rpc_verifyislock.py',
'rpc_verifychainlock.py',
'wallet_create_tx.py',
'wallet_send.py',
'p2p_fingerprint.py',
'rpc_platform_filter.py',
'rpc_wipewallettxes.py',
Expand Down
1 change: 1 addition & 0 deletions test/functional/wallet_import_rescan.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ def run_test(self):
self.nodes[0].generate(1) # Generate one block for each send
variant.confirmation_height = self.nodes[0].getblockcount()
variant.timestamp = self.nodes[0].getblockheader(self.nodes[0].getbestblockhash())["time"]
self.sync_all() # Conclude sync before calling setmocktime to avoid timeouts

# Generate a block further in the future (past the rescan window).
assert_equal(self.nodes[0].getrawmempool(), [])
Expand Down
Loading