merge bitcoin#19969: Send RPC bug fix and touch-ups

This commit is contained in:
Kittywhiskers Van Gogh 2020-09-17 15:06:39 +02:00
parent 17e2168a4f
commit 76a49addd0
No known key found for this signature in database
GPG Key ID: 30CD0C065E5C4AAD
4 changed files with 31 additions and 28 deletions

View File

@ -9,5 +9,6 @@ RPC changes
prevents adding more inputs if necessary and consequently the RPC fails. prevents adding more inputs if necessary and consequently the RPC fails.
- 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.

View File

@ -21,14 +21,16 @@
CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime) CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime)
{ {
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();

View File

@ -3267,7 +3267,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)},
@ -4006,9 +4006,10 @@ static UniValue listlabels(const JSONRPCRequest& request)
static UniValue send(const JSONRPCRequest& request) static UniValue send(const JSONRPCRequest& request)
{ {
RPCHelpMan{"send", 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.",
{ {
@ -4039,7 +4040,7 @@ static UniValue send(const JSONRPCRequest& request)
{"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"},
@ -4049,7 +4050,7 @@ static UniValue send(const JSONRPCRequest& request)
{"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 funds than you enter in their corresponding amount field.\n" "Those recipients will receive less funds 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.",
@ -4071,8 +4072,8 @@ static UniValue send(const JSONRPCRequest& request)
}, },
RPCExamples{"" RPCExamples{""
"\nSend with a fee rate of 1 " + CURRENCY_ATOM + "/B\n" "\nSend with a fee rate of 1 " + CURRENCY_ATOM + "/B\n"
+ HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.1}' 1 " + CURRENCY_ATOM + "/B\n" + + HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.1}' 1 " + CURRENCY_ATOM + "/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}]}'")
} }
}.Check(request); }.Check(request);
@ -4136,7 +4137,7 @@ static UniValue send(const JSONRPCRequest& request)
// 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) {
@ -4148,13 +4149,11 @@ static UniValue send(const JSONRPCRequest& request)
UniValue result(UniValue::VOBJ); UniValue result(UniValue::VOBJ);
// Serialize the PSBT
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx;
const std::string result_str = EncodeBase64(ssTx.str());
if (psbt_opt_in || !complete || !add_to_wallet) { if (psbt_opt_in || !complete || !add_to_wallet) {
result.pushKV("psbt", result_str); // Serialize the PSBT
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx;
result.pushKV("psbt", EncodeBase64(ssTx.str()));
} }
if (complete) { if (complete) {

View File

@ -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, inputs=None, add_inputs=None, change_address=None, change_position=None,
include_watching=None,locktime=None,lock_unspents=None,subtract_fee_from_outputs=None, include_watching=None, locktime=None, lock_unspents=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)
@ -86,13 +86,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:")
@ -124,7 +124,7 @@ class WalletSendTest(BitcoinTestFramework):
tx = from_wallet.gettransaction(res["txid"]) tx = from_wallet.gettransaction(res["txid"])
assert tx assert tx
# 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:
@ -157,7 +157,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()
@ -181,7 +181,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()
@ -312,7 +312,8 @@ 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("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])