mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-03-10 09:06:15 +01:00
Merge #19969: Send RPC bug fix and touch-ups
f7b331ea85
rpc: add brackets to ConstructTransaction (Sjors Provoost)d813d26f06
[rpc] send: various touch-ups (Sjors Provoost)0fc1c685e1
[rpc] send: fix parsing replaceable option (Sjors Provoost)efc9b85e6f
Mark send RPC experimental (Sjors Provoost) Pull request description: Followup based on #16378 nits. It also fixes an argument parsing error (uncaught because the test wasn't sufficiently thorough). I marked the RPC as experimental so we can tweak it a bit over the next release cycle. ACKs for top commit: meshcollider: utACKf7b331ea85
fjahr: utACKf7b331ea85
kallewoof: ACKf7b331ea85
Tree-SHA512: 82dd8ac76a6558872db3f5249d4d6440469400aaa339153bc627d1ee673a91ecfadecb486bc1939ba87ebbd80e26ff29698e93e358599f3d26fde0e526892afe
This commit is contained in:
commit
e36aa351a3
4 changed files with 39 additions and 32 deletions
|
@ -1,5 +1,6 @@
|
||||||
RPC
|
RPC
|
||||||
---
|
---
|
||||||
- A new `send` RPC with similar syntax to `walletcreatefundedpsbt`, including
|
- A new `send` RPC with similar syntax to `walletcreatefundedpsbt`, including
|
||||||
support for coin selection and a custom fee rate. Using the new `send` method
|
support for coin selection and a custom fee rate. The `send` RPC is experimental
|
||||||
is encouraged: `sendmany` and `sendtoaddress` may be deprecated in a future release.
|
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.
|
||||||
|
|
|
@ -21,14 +21,16 @@
|
||||||
|
|
||||||
CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, bool rbf)
|
CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, bool rbf)
|
||||||
{
|
{
|
||||||
if (outputs_in.isNull())
|
if (outputs_in.isNull()) {
|
||||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output argument must be non-null");
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output argument must be non-null");
|
||||||
|
}
|
||||||
|
|
||||||
UniValue inputs;
|
UniValue inputs;
|
||||||
if (inputs_in.isNull())
|
if (inputs_in.isNull()) {
|
||||||
inputs = UniValue::VARR;
|
inputs = UniValue::VARR;
|
||||||
else
|
} else {
|
||||||
inputs = inputs_in.get_array();
|
inputs = inputs_in.get_array();
|
||||||
|
}
|
||||||
|
|
||||||
const bool outputs_is_obj = outputs_in.isObject();
|
const bool outputs_is_obj = outputs_in.isObject();
|
||||||
UniValue outputs = outputs_is_obj ? outputs_in.get_obj() : outputs_in.get_array();
|
UniValue outputs = outputs_is_obj ? outputs_in.get_obj() : outputs_in.get_array();
|
||||||
|
|
|
@ -3041,7 +3041,7 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
|
||||||
{"lockUnspents", UniValueType(UniValue::VBOOL)},
|
{"lockUnspents", UniValueType(UniValue::VBOOL)},
|
||||||
{"lock_unspents", UniValueType(UniValue::VBOOL)},
|
{"lock_unspents", UniValueType(UniValue::VBOOL)},
|
||||||
{"locktime", UniValueType(UniValue::VNUM)},
|
{"locktime", UniValueType(UniValue::VNUM)},
|
||||||
{"feeRate", UniValueType()}, // will be checked below,
|
{"feeRate", UniValueType()}, // will be checked below
|
||||||
{"psbt", UniValueType(UniValue::VBOOL)},
|
{"psbt", UniValueType(UniValue::VBOOL)},
|
||||||
{"subtractFeeFromOutputs", UniValueType(UniValue::VARR)},
|
{"subtractFeeFromOutputs", UniValueType(UniValue::VARR)},
|
||||||
{"subtract_fee_from_outputs", UniValueType(UniValue::VARR)},
|
{"subtract_fee_from_outputs", UniValueType(UniValue::VARR)},
|
||||||
|
@ -3959,9 +3959,10 @@ static RPCHelpMan listlabels()
|
||||||
static RPCHelpMan send()
|
static RPCHelpMan send()
|
||||||
{
|
{
|
||||||
return RPCHelpMan{"send",
|
return RPCHelpMan{"send",
|
||||||
|
"\nEXPERIMENTAL warning: this call may be changed in future releases.\n"
|
||||||
"\nSend a transaction.\n",
|
"\nSend a transaction.\n",
|
||||||
{
|
{
|
||||||
{"outputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "a json array with outputs (key-value pairs), where none of the keys are duplicated.\n"
|
{"outputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "A JSON array with outputs (key-value pairs), where none of the keys are duplicated.\n"
|
||||||
"That is, each address can only appear once and there can only be one 'data' object.\n"
|
"That is, each address can only appear once and there can only be one 'data' object.\n"
|
||||||
"For convenience, a dictionary, which holds the key-value pairs directly, is also accepted.",
|
"For convenience, a dictionary, which holds the key-value pairs directly, is also accepted.",
|
||||||
{
|
{
|
||||||
|
@ -3993,7 +3994,7 @@ static RPCHelpMan send()
|
||||||
{"include_watching", RPCArg::Type::BOOL, /* default */ "true for watch-only wallets, otherwise false", "Also select inputs which are watch only.\n"
|
{"include_watching", RPCArg::Type::BOOL, /* default */ "true for watch-only wallets, otherwise false", "Also select inputs which are watch only.\n"
|
||||||
"Only solvable inputs can be used. Watch-only destinations are solvable if the public key and/or output script was imported,\n"
|
"Only solvable inputs can be used. Watch-only destinations are solvable if the public key and/or output script was imported,\n"
|
||||||
"e.g. with 'importpubkey' or 'importmulti' with the 'pubkeys' or 'desc' field."},
|
"e.g. with 'importpubkey' or 'importmulti' with the 'pubkeys' or 'desc' field."},
|
||||||
{"inputs", RPCArg::Type::ARR, /* default */ "empty array", "Specify inputs instead of adding them automatically. A json array of json objects",
|
{"inputs", RPCArg::Type::ARR, /* default */ "empty array", "Specify inputs instead of adding them automatically. A JSON array of JSON objects",
|
||||||
{
|
{
|
||||||
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"},
|
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"},
|
||||||
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"},
|
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"},
|
||||||
|
@ -4003,7 +4004,7 @@ static RPCHelpMan send()
|
||||||
{"locktime", RPCArg::Type::NUM, /* default */ "0", "Raw locktime. Non-0 value also locktime-activates inputs"},
|
{"locktime", RPCArg::Type::NUM, /* default */ "0", "Raw locktime. Non-0 value also locktime-activates inputs"},
|
||||||
{"lock_unspents", RPCArg::Type::BOOL, /* default */ "false", "Lock selected unspent outputs"},
|
{"lock_unspents", RPCArg::Type::BOOL, /* default */ "false", "Lock selected unspent outputs"},
|
||||||
{"psbt", RPCArg::Type::BOOL, /* default */ "automatic", "Always return a PSBT, implies add_to_wallet=false."},
|
{"psbt", RPCArg::Type::BOOL, /* default */ "automatic", "Always return a PSBT, implies add_to_wallet=false."},
|
||||||
{"subtract_fee_from_outputs", RPCArg::Type::ARR, /* default */ "empty array", "A json array of integers.\n"
|
{"subtract_fee_from_outputs", RPCArg::Type::ARR, /* default */ "empty array", "A JSON array of integers.\n"
|
||||||
"The fee will be equally deducted from the amount of each specified output.\n"
|
"The fee will be equally deducted from the amount of each specified output.\n"
|
||||||
"Those recipients will receive less bitcoins than you enter in their corresponding amount field.\n"
|
"Those recipients will receive less bitcoins than you enter in their corresponding amount field.\n"
|
||||||
"If no outputs are specified here, the sender pays the fee.",
|
"If no outputs are specified here, the sender pays the fee.",
|
||||||
|
@ -4027,8 +4028,8 @@ static RPCHelpMan send()
|
||||||
},
|
},
|
||||||
RPCExamples{""
|
RPCExamples{""
|
||||||
"\nSend with a fee rate of 1 satoshi per byte\n"
|
"\nSend with a fee rate of 1 satoshi per byte\n"
|
||||||
+ HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.1}' 1 sat/b\n" +
|
+ HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.1}' 1 sat/b\n") +
|
||||||
"\nCreate a transaction that should confirm the next block, with a specific input, and return result without adding to wallet or broadcasting to the network\n")
|
"\nCreate a transaction that should confirm the next block, with a specific input, and return result without adding to wallet or broadcasting to the network\n"
|
||||||
+ HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.1}' 1 economical '{\"add_to_wallet\": false, \"inputs\": [{\"txid\":\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\", \"vout\":1}]}'")
|
+ HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.1}' 1 economical '{\"add_to_wallet\": false, \"inputs\": [{\"txid\":\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\", \"vout\":1}]}'")
|
||||||
},
|
},
|
||||||
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
||||||
|
@ -4079,7 +4080,7 @@ static RPCHelpMan send()
|
||||||
int change_position;
|
int change_position;
|
||||||
bool rbf = pwallet->m_signal_rbf;
|
bool rbf = pwallet->m_signal_rbf;
|
||||||
if (options.exists("replaceable")) {
|
if (options.exists("replaceable")) {
|
||||||
rbf = options["add_to_wallet"].get_bool();
|
rbf = options["replaceable"].get_bool();
|
||||||
}
|
}
|
||||||
CMutableTransaction rawTx = ConstructTransaction(options["inputs"], request.params[0], options["locktime"], rbf);
|
CMutableTransaction rawTx = ConstructTransaction(options["inputs"], request.params[0], options["locktime"], rbf);
|
||||||
CCoinControl coin_control;
|
CCoinControl coin_control;
|
||||||
|
@ -4096,7 +4097,7 @@ static RPCHelpMan send()
|
||||||
// Make a blank psbt
|
// Make a blank psbt
|
||||||
PartiallySignedTransaction psbtx(rawTx);
|
PartiallySignedTransaction psbtx(rawTx);
|
||||||
|
|
||||||
// Fill transaction with out data and sign
|
// Fill transaction with our data and sign
|
||||||
bool complete = true;
|
bool complete = true;
|
||||||
const TransactionError err = pwallet->FillPSBT(psbtx, complete, SIGHASH_ALL, true, false);
|
const TransactionError err = pwallet->FillPSBT(psbtx, complete, SIGHASH_ALL, true, false);
|
||||||
if (err != TransactionError::OK) {
|
if (err != TransactionError::OK) {
|
||||||
|
@ -4108,13 +4109,11 @@ static RPCHelpMan send()
|
||||||
|
|
||||||
UniValue result(UniValue::VOBJ);
|
UniValue result(UniValue::VOBJ);
|
||||||
|
|
||||||
|
if (psbt_opt_in || !complete || !add_to_wallet) {
|
||||||
// Serialize the PSBT
|
// Serialize the PSBT
|
||||||
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
|
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
|
||||||
ssTx << psbtx;
|
ssTx << psbtx;
|
||||||
const std::string result_str = EncodeBase64(ssTx.str());
|
result.pushKV("psbt", EncodeBase64(ssTx.str()));
|
||||||
|
|
||||||
if (psbt_opt_in || !complete || !add_to_wallet) {
|
|
||||||
result.pushKV("psbt", result_str);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (complete) {
|
if (complete) {
|
||||||
|
|
|
@ -29,9 +29,9 @@ class WalletSendTest(BitcoinTestFramework):
|
||||||
|
|
||||||
def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
|
def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
|
||||||
arg_conf_target=None, arg_estimate_mode=None,
|
arg_conf_target=None, arg_estimate_mode=None,
|
||||||
conf_target=None, estimate_mode=None, add_to_wallet=None,psbt=None,
|
conf_target=None, estimate_mode=None, add_to_wallet=None, psbt=None,
|
||||||
inputs=None,add_inputs=None,change_address=None,change_position=None,change_type=None,
|
inputs=None, add_inputs=None, change_address=None, change_position=None, change_type=None,
|
||||||
include_watching=None,locktime=None,lock_unspents=None,replaceable=None,subtract_fee_from_outputs=None,
|
include_watching=None, locktime=None, lock_unspents=None, replaceable=None, subtract_fee_from_outputs=None,
|
||||||
expect_error=None):
|
expect_error=None):
|
||||||
assert (amount is None) != (data is None)
|
assert (amount is None) != (data is None)
|
||||||
|
|
||||||
|
@ -92,13 +92,13 @@ class WalletSendTest(BitcoinTestFramework):
|
||||||
res = from_wallet.send(outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, options=options)
|
res = from_wallet.send(outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, options=options)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
assert_raises_rpc_error(expect_error[0],expect_error[1],from_wallet.send,
|
assert_raises_rpc_error(expect_error[0], expect_error[1], from_wallet.send,
|
||||||
outputs=outputs,conf_target=arg_conf_target,estimate_mode=arg_estimate_mode,options=options)
|
outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, options=options)
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
# Provide debug info if the test fails
|
# Provide debug info if the test fails
|
||||||
self.log.error("Unexpected successful result:")
|
self.log.error("Unexpected successful result:")
|
||||||
self.log.error(options)
|
self.log.error(options)
|
||||||
res = from_wallet.send(outputs=outputs,conf_target=arg_conf_target,estimate_mode=arg_estimate_mode,options=options)
|
res = from_wallet.send(outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, options=options)
|
||||||
self.log.error(res)
|
self.log.error(res)
|
||||||
if "txid" in res and add_to_wallet:
|
if "txid" in res and add_to_wallet:
|
||||||
self.log.error("Transaction details:")
|
self.log.error("Transaction details:")
|
||||||
|
@ -131,7 +131,7 @@ class WalletSendTest(BitcoinTestFramework):
|
||||||
assert tx
|
assert tx
|
||||||
assert_equal(tx["bip125-replaceable"], "yes" if replaceable else "no")
|
assert_equal(tx["bip125-replaceable"], "yes" if replaceable else "no")
|
||||||
# Ensure transaction exists in the mempool:
|
# Ensure transaction exists in the mempool:
|
||||||
tx = from_wallet.getrawtransaction(res["txid"],True)
|
tx = from_wallet.getrawtransaction(res["txid"], True)
|
||||||
assert tx
|
assert tx
|
||||||
if amount:
|
if amount:
|
||||||
if subtract_fee_from_outputs:
|
if subtract_fee_from_outputs:
|
||||||
|
@ -164,7 +164,7 @@ class WalletSendTest(BitcoinTestFramework):
|
||||||
self.nodes[1].createwallet(wallet_name="w2")
|
self.nodes[1].createwallet(wallet_name="w2")
|
||||||
w2 = self.nodes[1].get_wallet_rpc("w2")
|
w2 = self.nodes[1].get_wallet_rpc("w2")
|
||||||
# w3 is a watch-only wallet, based on w2
|
# w3 is a watch-only wallet, based on w2
|
||||||
self.nodes[1].createwallet(wallet_name="w3",disable_private_keys=True)
|
self.nodes[1].createwallet(wallet_name="w3", disable_private_keys=True)
|
||||||
w3 = self.nodes[1].get_wallet_rpc("w3")
|
w3 = self.nodes[1].get_wallet_rpc("w3")
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
a2_receive = w2.getnewaddress()
|
a2_receive = w2.getnewaddress()
|
||||||
|
@ -188,7 +188,7 @@ class WalletSendTest(BitcoinTestFramework):
|
||||||
self.sync_blocks()
|
self.sync_blocks()
|
||||||
|
|
||||||
# w4 has private keys enabled, but only contains watch-only keys (from w2)
|
# w4 has private keys enabled, but only contains watch-only keys (from w2)
|
||||||
self.nodes[1].createwallet(wallet_name="w4",disable_private_keys=False)
|
self.nodes[1].createwallet(wallet_name="w4", disable_private_keys=False)
|
||||||
w4 = self.nodes[1].get_wallet_rpc("w4")
|
w4 = self.nodes[1].get_wallet_rpc("w4")
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
a2_receive = w2.getnewaddress()
|
a2_receive = w2.getnewaddress()
|
||||||
|
@ -253,7 +253,7 @@ class WalletSendTest(BitcoinTestFramework):
|
||||||
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=-1, estimate_mode="sat/b",
|
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=-1, estimate_mode="sat/b",
|
||||||
expect_error=(-3, "Amount out of range"))
|
expect_error=(-3, "Amount out of range"))
|
||||||
# Fee rate of 0.1 satoshi per byte should throw an error
|
# Fee rate of 0.1 satoshi per byte should throw an error
|
||||||
# TODO: error should say 1.000 sat/b
|
# TODO: error should use sat/b
|
||||||
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.1, estimate_mode="sat/b",
|
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.1, estimate_mode="sat/b",
|
||||||
expect_error=(-4, "Fee rate (0.00000100 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)"))
|
expect_error=(-4, "Fee rate (0.00000100 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)"))
|
||||||
|
|
||||||
|
@ -325,11 +325,16 @@ class WalletSendTest(BitcoinTestFramework):
|
||||||
locked_coins = w0.listlockunspent()
|
locked_coins = w0.listlockunspent()
|
||||||
assert_equal(len(locked_coins), 1)
|
assert_equal(len(locked_coins), 1)
|
||||||
# Locked coins are automatically unlocked when manually selected
|
# Locked coins are automatically unlocked when manually selected
|
||||||
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, inputs=[utxo1],add_to_wallet=False)
|
res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, inputs=[utxo1], add_to_wallet=False)
|
||||||
|
assert res["complete"]
|
||||||
|
|
||||||
self.log.info("Replaceable...")
|
self.log.info("Replaceable...")
|
||||||
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=False, replaceable=True)
|
res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=True, replaceable=True)
|
||||||
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=False, replaceable=False)
|
assert res["complete"]
|
||||||
|
assert_equal(self.nodes[0].gettransaction(res["txid"])["bip125-replaceable"], "yes")
|
||||||
|
res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=True, replaceable=False)
|
||||||
|
assert res["complete"]
|
||||||
|
assert_equal(self.nodes[0].gettransaction(res["txid"])["bip125-replaceable"], "no")
|
||||||
|
|
||||||
self.log.info("Subtract fee from output")
|
self.log.info("Subtract fee from output")
|
||||||
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, subtract_fee_from_outputs=[0])
|
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, subtract_fee_from_outputs=[0])
|
||||||
|
|
Loading…
Add table
Reference in a new issue