Skip to content

Commit 92326d8

Browse files
committed
[rpc] add send method
1 parent 2c2a144 commit 92326d8

File tree

5 files changed

+534
-1
lines changed

5 files changed

+534
-1
lines changed

doc/release-notes-16378.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
RPC
2+
---
3+
- 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.

src/rpc/client.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ static const CRPCConvertParam vRPCConvertParams[] =
125125
{ "gettxoutproof", 0, "txids" },
126126
{ "lockunspent", 0, "unlock" },
127127
{ "lockunspent", 1, "transactions" },
128+
{ "send", 0, "outputs" },
129+
{ "send", 1, "conf_target" },
130+
{ "send", 3, "options" },
128131
{ "importprivkey", 2, "rescan" },
129132
{ "importaddress", 2, "rescan" },
130133
{ "importaddress", 3, "p2sh" },

src/wallet/rpcwallet.cpp

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <outputtype.h>
1212
#include <policy/feerate.h>
1313
#include <policy/fees.h>
14+
#include <policy/policy.h>
1415
#include <policy/rbf.h>
1516
#include <rpc/rawtransaction_util.h>
1617
#include <rpc/server.h>
@@ -2955,16 +2956,20 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
29552956
RPCTypeCheckObj(options,
29562957
{
29572958
{"add_inputs", UniValueType(UniValue::VBOOL)},
2959+
{"add_to_wallet", UniValueType(UniValue::VBOOL)},
29582960
{"changeAddress", UniValueType(UniValue::VSTR)},
29592961
{"change_address", UniValueType(UniValue::VSTR)},
29602962
{"changePosition", UniValueType(UniValue::VNUM)},
29612963
{"change_position", UniValueType(UniValue::VNUM)},
29622964
{"change_type", UniValueType(UniValue::VSTR)},
29632965
{"includeWatching", UniValueType(UniValue::VBOOL)},
29642966
{"include_watching", UniValueType(UniValue::VBOOL)},
2967+
{"inputs", UniValueType(UniValue::VARR)},
29652968
{"lockUnspents", UniValueType(UniValue::VBOOL)},
29662969
{"lock_unspents", UniValueType(UniValue::VBOOL)},
2967-
{"feeRate", UniValueType()}, // will be checked below
2970+
{"locktime", UniValueType(UniValue::VNUM)},
2971+
{"feeRate", UniValueType()}, // will be checked below,
2972+
{"psbt", UniValueType(UniValue::VBOOL)},
29682973
{"subtractFeeFromOutputs", UniValueType(UniValue::VARR)},
29692974
{"subtract_fee_from_outputs", UniValueType(UniValue::VARR)},
29702975
{"replaceable", UniValueType(UniValue::VBOOL)},
@@ -3866,6 +3871,185 @@ static UniValue listlabels(const JSONRPCRequest& request)
38663871
return ret;
38673872
}
38683873

3874+
static RPCHelpMan send()
3875+
{
3876+
return RPCHelpMan{"send",
3877+
"\nSend a transaction.\n",
3878+
{
3879+
{"outputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "a json array with outputs (key-value pairs), where none of the keys are duplicated.\n"
3880+
"That is, each address can only appear once and there can only be one 'data' object.\n"
3881+
"For convenience, a dictionary, which holds the key-value pairs directly, is also accepted.",
3882+
{
3883+
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
3884+
{
3885+
{"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in " + CURRENCY_UNIT + ""},
3886+
},
3887+
},
3888+
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
3889+
{
3890+
{"data", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "A key-value pair. The key must be \"data\", the value is hex-encoded data"},
3891+
},
3892+
},
3893+
},
3894+
},
3895+
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"},
3896+
{"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
3897+
" \"" + FeeModes("\"\n\"") + "\""},
3898+
{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "",
3899+
{
3900+
{"add_inputs", RPCArg::Type::BOOL, /* default */ "false", "If inputs are specified, automatically include more if they are not enough."},
3901+
{"add_to_wallet", RPCArg::Type::BOOL, /* default */ "true", "When false, returns a serialized transaction which will not be added to the wallet or broadcast"},
3902+
{"change_address", RPCArg::Type::STR_HEX, /* default */ "pool address", "The bitcoin address to receive the change"},
3903+
{"change_position", RPCArg::Type::NUM, /* default */ "random", "The index of the change output"},
3904+
{"change_type", RPCArg::Type::STR, /* default */ "set by -changetype", "The output type to use. Only valid if change_address is not specified. Options are \"legacy\", \"p2sh-segwit\", and \"bech32\"."},
3905+
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"},
3906+
{"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
3907+
" \"" + FeeModes("\"\n\"") + "\""},
3908+
{"include_watching", RPCArg::Type::BOOL, /* default */ "true for watch-only wallets, otherwise false", "Also select inputs which are watch only.\n"
3909+
"Only solvable inputs can be used. Watch-only destinations are solvable if the public key and/or output script was imported,\n"
3910+
"e.g. with 'importpubkey' or 'importmulti' with the 'pubkeys' or 'desc' field."},
3911+
{"inputs", RPCArg::Type::ARR, /* default */ "empty array", "Specify inputs instead of adding them automatically. A json array of json objects",
3912+
{
3913+
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"},
3914+
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"},
3915+
{"sequence", RPCArg::Type::NUM, RPCArg::Optional::NO, "The sequence number"},
3916+
},
3917+
},
3918+
{"locktime", RPCArg::Type::NUM, /* default */ "0", "Raw locktime. Non-0 value also locktime-activates inputs"},
3919+
{"lock_unspents", RPCArg::Type::BOOL, /* default */ "false", "Lock selected unspent outputs"},
3920+
{"psbt", RPCArg::Type::BOOL, /* default */ "automatic", "Always return a PSBT, implies add_to_wallet=false."},
3921+
{"subtract_fee_from_outputs", RPCArg::Type::ARR, /* default */ "empty array", "A json array of integers.\n"
3922+
"The fee will be equally deducted from the amount of each specified output.\n"
3923+
"Those recipients will receive less bitcoins than you enter in their corresponding amount field.\n"
3924+
"If no outputs are specified here, the sender pays the fee.",
3925+
{
3926+
{"vout_index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "The zero-based output index, before a change output is added."},
3927+
},
3928+
},
3929+
{"replaceable", RPCArg::Type::BOOL, /* default */ "wallet default", "Marks this transaction as BIP125 replaceable.\n"
3930+
" Allows this transaction to be replaced by a transaction with higher fees"},
3931+
},
3932+
"options"},
3933+
},
3934+
RPCResult{
3935+
RPCResult::Type::OBJ, "", "",
3936+
{
3937+
{RPCResult::Type::BOOL, "complete", "If the transaction has a complete set of signatures"},
3938+
{RPCResult::Type::STR_HEX, "txid", "The transaction id for the send. Only 1 transaction is created regardless of the number of addresses."},
3939+
{RPCResult::Type::STR_HEX, "hex", "If add_to_wallet is false, the hex-encoded raw transaction with signature(s)"},
3940+
{RPCResult::Type::STR, "psbt", "If more signatures are needed, or if add_to_wallet is false, the base64-encoded (partially) signed transaction"}
3941+
}
3942+
},
3943+
RPCExamples{""
3944+
"\nSend with a fee rate of 1 satoshi per byte\n"
3945+
+ HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.1}' 1 sat/b\n" +
3946+
"\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")
3947+
+ HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.1}' 1 economical '{\"add_to_wallet\": false, \"inputs\": [{\"txid\":\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\", \"vout\":1}]}'")
3948+
},
3949+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
3950+
{
3951+
RPCTypeCheck(request.params, {
3952+
UniValueType(), // ARR or OBJ, checked later
3953+
UniValue::VNUM,
3954+
UniValue::VSTR,
3955+
UniValue::VOBJ
3956+
}, true
3957+
);
3958+
3959+
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
3960+
if (!wallet) return NullUniValue;
3961+
CWallet* const pwallet = wallet.get();
3962+
3963+
UniValue options = request.params[3];
3964+
if (options.exists("feeRate") || options.exists("fee_rate") || options.exists("estimate_mode") || options.exists("conf_target")) {
3965+
if (!request.params[1].isNull() || !request.params[2].isNull()) {
3966+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use either conf_target and estimate_mode or the options dictionary to control fee rate");
3967+
}
3968+
} else {
3969+
options.pushKV("conf_target", request.params[1]);
3970+
options.pushKV("estimate_mode", request.params[2]);
3971+
}
3972+
if (!options["conf_target"].isNull() && (options["estimate_mode"].isNull() || (options["estimate_mode"].get_str() == "unset"))) {
3973+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Specify estimate_mode");
3974+
}
3975+
if (options.exists("changeAddress")) {
3976+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use change_address");
3977+
}
3978+
if (options.exists("changePosition")) {
3979+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use change_position");
3980+
}
3981+
if (options.exists("includeWatching")) {
3982+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use include_watching");
3983+
}
3984+
if (options.exists("lockUnspents")) {
3985+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use lock_unspents");
3986+
}
3987+
if (options.exists("subtractFeeFromOutputs")) {
3988+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use subtract_fee_from_outputs");
3989+
}
3990+
3991+
const bool psbt_opt_in = options.exists("psbt") && options["psbt"].get_bool();
3992+
3993+
CAmount fee;
3994+
int change_position;
3995+
bool rbf = pwallet->m_signal_rbf;
3996+
if (options.exists("replaceable")) {
3997+
rbf = options["add_to_wallet"].get_bool();
3998+
}
3999+
CMutableTransaction rawTx = ConstructTransaction(options["inputs"], request.params[0], options["locktime"], rbf);
4000+
CCoinControl coin_control;
4001+
// Automatically select coins, unless at least one is manually selected. Can
4002+
// be overriden by options.add_inputs.
4003+
coin_control.m_add_inputs = rawTx.vin.size() == 0;
4004+
FundTransaction(pwallet, rawTx, fee, change_position, options, coin_control);
4005+
4006+
bool add_to_wallet = true;
4007+
if (options.exists("add_to_wallet")) {
4008+
add_to_wallet = options["add_to_wallet"].get_bool();
4009+
}
4010+
4011+
// Make a blank psbt
4012+
PartiallySignedTransaction psbtx(rawTx);
4013+
4014+
// Fill transaction with out data and sign
4015+
bool complete = true;
4016+
const TransactionError err = pwallet->FillPSBT(psbtx, complete, SIGHASH_ALL, true, false);
4017+
if (err != TransactionError::OK) {
4018+
throw JSONRPCTransactionError(err);
4019+
}
4020+
4021+
CMutableTransaction mtx;
4022+
complete = FinalizeAndExtractPSBT(psbtx, mtx);
4023+
4024+
UniValue result(UniValue::VOBJ);
4025+
4026+
// Serialize the PSBT
4027+
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
4028+
ssTx << psbtx;
4029+
const std::string result_str = EncodeBase64(ssTx.str());
4030+
4031+
if (psbt_opt_in || !complete || !add_to_wallet) {
4032+
result.pushKV("psbt", result_str);
4033+
}
4034+
4035+
if (complete) {
4036+
std::string err_string;
4037+
std::string hex = EncodeHexTx(CTransaction(mtx));
4038+
CTransactionRef tx(MakeTransactionRef(std::move(mtx)));
4039+
result.pushKV("txid", tx->GetHash().GetHex());
4040+
if (add_to_wallet && !psbt_opt_in) {
4041+
pwallet->CommitTransaction(tx, {}, {} /* orderForm */);
4042+
} else {
4043+
result.pushKV("hex", hex);
4044+
}
4045+
}
4046+
result.pushKV("complete", complete);
4047+
4048+
return result;
4049+
}
4050+
};
4051+
}
4052+
38694053
UniValue sethdseed(const JSONRPCRequest& request)
38704054
{
38714055
RPCHelpMan{"sethdseed",
@@ -4223,6 +4407,7 @@ static const CRPCCommand commands[] =
42234407
{ "wallet", "lockunspent", &lockunspent, {"unlock","transactions"} },
42244408
{ "wallet", "removeprunedfunds", &removeprunedfunds, {"txid"} },
42254409
{ "wallet", "rescanblockchain", &rescanblockchain, {"start_height", "stop_height"} },
4410+
{ "wallet", "send", &send, {"outputs","conf_target","estimate_mode","options"} },
42264411
{ "wallet", "sendmany", &sendmany, {"dummy","amounts","minconf","comment","subtractfeefrom","replaceable","conf_target","estimate_mode"} },
42274412
{ "wallet", "sendtoaddress", &sendtoaddress, {"address","amount","comment","comment_to","subtractfeefromamount","replaceable","conf_target","estimate_mode","avoid_reuse"} },
42284413
{ "wallet", "sethdseed", &sethdseed, {"newkeypool","seed"} },

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@
225225
'rpc_estimatefee.py',
226226
'rpc_getblockstats.py',
227227
'wallet_create_tx.py',
228+
'wallet_send.py',
228229
'p2p_fingerprint.py',
229230
'feature_uacomment.py',
230231
'wallet_coinbase_category.py',

0 commit comments

Comments
 (0)