Skip to content

Commit e36aa35

Browse files
committed
Merge #19969: Send RPC bug fix and touch-ups
f7b331e rpc: add brackets to ConstructTransaction (Sjors Provoost) d813d26 [rpc] send: various touch-ups (Sjors Provoost) 0fc1c68 [rpc] send: fix parsing replaceable option (Sjors Provoost) efc9b85 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: utACK f7b331e fjahr: utACK f7b331e kallewoof: ACK f7b331e Tree-SHA512: 82dd8ac76a6558872db3f5249d4d6440469400aaa339153bc627d1ee673a91ecfadecb486bc1939ba87ebbd80e26ff29698e93e358599f3d26fde0e526892afe
2 parents 7ea6499 + f7b331e commit e36aa35

File tree

4 files changed

+39
-32
lines changed

4 files changed

+39
-32
lines changed

doc/release-notes-16378.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
RPC
22
---
33
- A new `send` RPC with similar syntax to `walletcreatefundedpsbt`, including
4-
support for coin selection and a custom fee rate. Using the new `send` method
5-
is encouraged: `sendmany` and `sendtoaddress` may be deprecated in a future release.
4+
support for coin selection and a custom fee rate. The `send` RPC is experimental
5+
and may change in subsequent releases. Using it is encouraged once it's no
6+
longer experimental: `sendmany` and `sendtoaddress` may be deprecated in a future release.

src/rpc/rawtransaction_util.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,16 @@
2121

2222
CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, bool rbf)
2323
{
24-
if (outputs_in.isNull())
24+
if (outputs_in.isNull()) {
2525
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output argument must be non-null");
26+
}
2627

2728
UniValue inputs;
28-
if (inputs_in.isNull())
29+
if (inputs_in.isNull()) {
2930
inputs = UniValue::VARR;
30-
else
31+
} else {
3132
inputs = inputs_in.get_array();
33+
}
3234

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

src/wallet/rpcwallet.cpp

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3041,7 +3041,7 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
30413041
{"lockUnspents", UniValueType(UniValue::VBOOL)},
30423042
{"lock_unspents", UniValueType(UniValue::VBOOL)},
30433043
{"locktime", UniValueType(UniValue::VNUM)},
3044-
{"feeRate", UniValueType()}, // will be checked below,
3044+
{"feeRate", UniValueType()}, // will be checked below
30453045
{"psbt", UniValueType(UniValue::VBOOL)},
30463046
{"subtractFeeFromOutputs", UniValueType(UniValue::VARR)},
30473047
{"subtract_fee_from_outputs", UniValueType(UniValue::VARR)},
@@ -3959,9 +3959,10 @@ static RPCHelpMan listlabels()
39593959
static RPCHelpMan send()
39603960
{
39613961
return RPCHelpMan{"send",
3962+
"\nEXPERIMENTAL warning: this call may be changed in future releases.\n"
39623963
"\nSend a transaction.\n",
39633964
{
3964-
{"outputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "a json array with outputs (key-value pairs), where none of the keys are duplicated.\n"
3965+
{"outputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "A JSON array with outputs (key-value pairs), where none of the keys are duplicated.\n"
39653966
"That is, each address can only appear once and there can only be one 'data' object.\n"
39663967
"For convenience, a dictionary, which holds the key-value pairs directly, is also accepted.",
39673968
{
@@ -3993,7 +3994,7 @@ static RPCHelpMan send()
39933994
{"include_watching", RPCArg::Type::BOOL, /* default */ "true for watch-only wallets, otherwise false", "Also select inputs which are watch only.\n"
39943995
"Only solvable inputs can be used. Watch-only destinations are solvable if the public key and/or output script was imported,\n"
39953996
"e.g. with 'importpubkey' or 'importmulti' with the 'pubkeys' or 'desc' field."},
3996-
{"inputs", RPCArg::Type::ARR, /* default */ "empty array", "Specify inputs instead of adding them automatically. A json array of json objects",
3997+
{"inputs", RPCArg::Type::ARR, /* default */ "empty array", "Specify inputs instead of adding them automatically. A JSON array of JSON objects",
39973998
{
39983999
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"},
39994000
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"},
@@ -4003,7 +4004,7 @@ static RPCHelpMan send()
40034004
{"locktime", RPCArg::Type::NUM, /* default */ "0", "Raw locktime. Non-0 value also locktime-activates inputs"},
40044005
{"lock_unspents", RPCArg::Type::BOOL, /* default */ "false", "Lock selected unspent outputs"},
40054006
{"psbt", RPCArg::Type::BOOL, /* default */ "automatic", "Always return a PSBT, implies add_to_wallet=false."},
4006-
{"subtract_fee_from_outputs", RPCArg::Type::ARR, /* default */ "empty array", "A json array of integers.\n"
4007+
{"subtract_fee_from_outputs", RPCArg::Type::ARR, /* default */ "empty array", "A JSON array of integers.\n"
40074008
"The fee will be equally deducted from the amount of each specified output.\n"
40084009
"Those recipients will receive less bitcoins than you enter in their corresponding amount field.\n"
40094010
"If no outputs are specified here, the sender pays the fee.",
@@ -4027,8 +4028,8 @@ static RPCHelpMan send()
40274028
},
40284029
RPCExamples{""
40294030
"\nSend with a fee rate of 1 satoshi per byte\n"
4030-
+ HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.1}' 1 sat/b\n" +
4031-
"\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")
4031+
+ HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.1}' 1 sat/b\n") +
4032+
"\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"
40324033
+ HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.1}' 1 economical '{\"add_to_wallet\": false, \"inputs\": [{\"txid\":\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\", \"vout\":1}]}'")
40334034
},
40344035
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
@@ -4079,7 +4080,7 @@ static RPCHelpMan send()
40794080
int change_position;
40804081
bool rbf = pwallet->m_signal_rbf;
40814082
if (options.exists("replaceable")) {
4082-
rbf = options["add_to_wallet"].get_bool();
4083+
rbf = options["replaceable"].get_bool();
40834084
}
40844085
CMutableTransaction rawTx = ConstructTransaction(options["inputs"], request.params[0], options["locktime"], rbf);
40854086
CCoinControl coin_control;
@@ -4096,7 +4097,7 @@ static RPCHelpMan send()
40964097
// Make a blank psbt
40974098
PartiallySignedTransaction psbtx(rawTx);
40984099

4099-
// Fill transaction with out data and sign
4100+
// Fill transaction with our data and sign
41004101
bool complete = true;
41014102
const TransactionError err = pwallet->FillPSBT(psbtx, complete, SIGHASH_ALL, true, false);
41024103
if (err != TransactionError::OK) {
@@ -4108,13 +4109,11 @@ static RPCHelpMan send()
41084109

41094110
UniValue result(UniValue::VOBJ);
41104111

4111-
// Serialize the PSBT
4112-
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
4113-
ssTx << psbtx;
4114-
const std::string result_str = EncodeBase64(ssTx.str());
4115-
41164112
if (psbt_opt_in || !complete || !add_to_wallet) {
4117-
result.pushKV("psbt", result_str);
4113+
// Serialize the PSBT
4114+
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
4115+
ssTx << psbtx;
4116+
result.pushKV("psbt", EncodeBase64(ssTx.str()));
41184117
}
41194118

41204119
if (complete) {

test/functional/wallet_send.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ def skip_test_if_missing_module(self):
2929

3030
def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
3131
arg_conf_target=None, arg_estimate_mode=None,
32-
conf_target=None, estimate_mode=None, add_to_wallet=None,psbt=None,
33-
inputs=None,add_inputs=None,change_address=None,change_position=None,change_type=None,
34-
include_watching=None,locktime=None,lock_unspents=None,replaceable=None,subtract_fee_from_outputs=None,
32+
conf_target=None, estimate_mode=None, add_to_wallet=None, psbt=None,
33+
inputs=None, add_inputs=None, change_address=None, change_position=None, change_type=None,
34+
include_watching=None, locktime=None, lock_unspents=None, replaceable=None, subtract_fee_from_outputs=None,
3535
expect_error=None):
3636
assert (amount is None) != (data is None)
3737

@@ -92,13 +92,13 @@ def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
9292
res = from_wallet.send(outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, options=options)
9393
else:
9494
try:
95-
assert_raises_rpc_error(expect_error[0],expect_error[1],from_wallet.send,
96-
outputs=outputs,conf_target=arg_conf_target,estimate_mode=arg_estimate_mode,options=options)
95+
assert_raises_rpc_error(expect_error[0], expect_error[1], from_wallet.send,
96+
outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, options=options)
9797
except AssertionError:
9898
# Provide debug info if the test fails
9999
self.log.error("Unexpected successful result:")
100100
self.log.error(options)
101-
res = from_wallet.send(outputs=outputs,conf_target=arg_conf_target,estimate_mode=arg_estimate_mode,options=options)
101+
res = from_wallet.send(outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, options=options)
102102
self.log.error(res)
103103
if "txid" in res and add_to_wallet:
104104
self.log.error("Transaction details:")
@@ -131,7 +131,7 @@ def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
131131
assert tx
132132
assert_equal(tx["bip125-replaceable"], "yes" if replaceable else "no")
133133
# Ensure transaction exists in the mempool:
134-
tx = from_wallet.getrawtransaction(res["txid"],True)
134+
tx = from_wallet.getrawtransaction(res["txid"], True)
135135
assert tx
136136
if amount:
137137
if subtract_fee_from_outputs:
@@ -164,7 +164,7 @@ def run_test(self):
164164
self.nodes[1].createwallet(wallet_name="w2")
165165
w2 = self.nodes[1].get_wallet_rpc("w2")
166166
# w3 is a watch-only wallet, based on w2
167-
self.nodes[1].createwallet(wallet_name="w3",disable_private_keys=True)
167+
self.nodes[1].createwallet(wallet_name="w3", disable_private_keys=True)
168168
w3 = self.nodes[1].get_wallet_rpc("w3")
169169
for _ in range(3):
170170
a2_receive = w2.getnewaddress()
@@ -188,7 +188,7 @@ def run_test(self):
188188
self.sync_blocks()
189189

190190
# w4 has private keys enabled, but only contains watch-only keys (from w2)
191-
self.nodes[1].createwallet(wallet_name="w4",disable_private_keys=False)
191+
self.nodes[1].createwallet(wallet_name="w4", disable_private_keys=False)
192192
w4 = self.nodes[1].get_wallet_rpc("w4")
193193
for _ in range(3):
194194
a2_receive = w2.getnewaddress()
@@ -253,7 +253,7 @@ def run_test(self):
253253
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=-1, estimate_mode="sat/b",
254254
expect_error=(-3, "Amount out of range"))
255255
# Fee rate of 0.1 satoshi per byte should throw an error
256-
# TODO: error should say 1.000 sat/b
256+
# TODO: error should use sat/b
257257
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.1, estimate_mode="sat/b",
258258
expect_error=(-4, "Fee rate (0.00000100 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)"))
259259

@@ -325,11 +325,16 @@ def run_test(self):
325325
locked_coins = w0.listlockunspent()
326326
assert_equal(len(locked_coins), 1)
327327
# Locked coins are automatically unlocked when manually selected
328-
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, inputs=[utxo1],add_to_wallet=False)
328+
res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, inputs=[utxo1], add_to_wallet=False)
329+
assert res["complete"]
329330

330331
self.log.info("Replaceable...")
331-
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=False, replaceable=True)
332-
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=False, replaceable=False)
332+
res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=True, replaceable=True)
333+
assert res["complete"]
334+
assert_equal(self.nodes[0].gettransaction(res["txid"])["bip125-replaceable"], "yes")
335+
res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=True, replaceable=False)
336+
assert res["complete"]
337+
assert_equal(self.nodes[0].gettransaction(res["txid"])["bip125-replaceable"], "no")
333338

334339
self.log.info("Subtract fee from output")
335340
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, subtract_fee_from_outputs=[0])

0 commit comments

Comments
 (0)