diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -13,6 +13,14 @@ purposes. This is a client-side version of the former `generate` RPC. See the help for details. +## Low-level RPC Changes + +- To make RPC `sendtoaddress` more consistent with `sendmany` the following error + `sendtoaddress` codes were changed from `-4` to `-6`: + - Insufficient funds + - Fee estimation failed + - Transaction has too long of a mempool chain + ## Notification changes `-walletnotify` notifications are now sent for wallet transactions that are diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -354,47 +354,64 @@ return NullUniValue; } -static CTransactionRef SendMoney(CWallet *const pwallet, - const CTxDestination &address, Amount nValue, - bool fSubtractFeeFromAmount, - const CCoinControl &coin_control, - mapValue_t mapValue) { - Amount curBalance = - pwallet->GetBalance(0, coin_control.m_avoid_address_reuse) - .m_mine_trusted; +void ParseRecipients(const UniValue &address_amounts, + const UniValue &subtract_fee_outputs, + std::vector &recipients, + const CChainParams &chainParams) { + std::set destinations; + int i = 0; + for (const std::string &address : address_amounts.getKeys()) { + CTxDestination dest = DecodeDestination(address, chainParams); + if (!IsValidDestination(dest)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, + std::string("Invalid Bitcoin address: ") + + address); + } - // Check amount - if (nValue <= Amount::zero()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid amount"); - } + if (destinations.count(dest)) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + std::string("Invalid parameter, duplicated address: ") + + address); + } + destinations.insert(dest); + + CScript script_pub_key = GetScriptForDestination(dest); + Amount amount = AmountFromValue(address_amounts[i++]); + + bool subtract_fee = false; + for (unsigned int idx = 0; idx < subtract_fee_outputs.size(); idx++) { + const UniValue &addr = subtract_fee_outputs[idx]; + if (addr.get_str() == address) { + subtract_fee = true; + } + } - if (nValue > curBalance) { - throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient funds"); + CRecipient recipient = {script_pub_key, amount, subtract_fee}; + recipients.push_back(recipient); } +} - // Parse Bitcoin address - CScript scriptPubKey = GetScriptForDestination(address); +UniValue SendMoney(CWallet *const pwallet, const CCoinControl &coin_control, + std::vector &recipients, mapValue_t map_value) { + EnsureWalletIsUnlocked(pwallet); + + // Shuffle recipient list + std::shuffle(recipients.begin(), recipients.end(), FastRandomContext()); - // Create and send the transaction + // Send Amount nFeeRequired = Amount::zero(); - bilingual_str error; - std::vector vecSend; int nChangePosRet = -1; - CRecipient recipient = {scriptPubKey, nValue, fSubtractFeeFromAmount}; - vecSend.push_back(recipient); - + bilingual_str error; CTransactionRef tx; - if (!pwallet->CreateTransaction(vecSend, tx, nFeeRequired, nChangePosRet, - error, coin_control)) { - if (!fSubtractFeeFromAmount && nValue + nFeeRequired > curBalance) { - error = strprintf(Untranslated("Error: This transaction requires a " - "transaction fee of at least %s"), - FormatMoney(nFeeRequired)); - } - throw JSONRPCError(RPC_WALLET_ERROR, error.original); + bool fCreated = pwallet->CreateTransaction( + recipients, tx, nFeeRequired, nChangePosRet, error, coin_control, + !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)); + if (!fCreated) { + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, error.original); } - pwallet->CommitTransaction(tx, std::move(mapValue), {} /* orderForm */); - return tx; + pwallet->CommitTransaction(tx, std::move(map_value), {} /* orderForm */); + return tx->GetId().GetHex(); } static UniValue sendtoaddress(const Config &config, @@ -457,18 +474,6 @@ LOCK(pwallet->cs_wallet); - CTxDestination dest = DecodeDestination(request.params[0].get_str(), - wallet->GetChainParams()); - if (!IsValidDestination(dest)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address"); - } - - // Amount - Amount nAmount = AmountFromValue(request.params[1]); - if (nAmount <= Amount::zero()) { - throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount for send"); - } - // Wallet comments mapValue_t mapValue; if (!request.params[2].isNull() && !request.params[2].get_str().empty()) { @@ -491,10 +496,19 @@ EnsureWalletIsUnlocked(pwallet); - CTransactionRef tx = - SendMoney(pwallet, dest, nAmount, fSubtractFeeFromAmount, coin_control, - std::move(mapValue)); - return tx->GetId().GetHex(); + UniValue address_amounts(UniValue::VOBJ); + const std::string address = request.params[0].get_str(); + address_amounts.pushKV(address, request.params[1]); + UniValue subtractFeeFromAmount(UniValue::VARR); + if (fSubtractFeeFromAmount) { + subtractFeeFromAmount.push_back(address); + } + + std::vector recipients; + ParseRecipients(address_amounts, subtractFeeFromAmount, recipients, + wallet->GetChainParams()); + + return SendMoney(pwallet, coin_control, recipients, mapValue); } static UniValue listaddressgroupings(const Config &config, @@ -991,62 +1005,12 @@ subtractFeeFromAmount = request.params[4].get_array(); } - std::set destinations; - std::vector vecSend; - - std::vector keys = sendTo.getKeys(); - for (const std::string &name_ : keys) { - CTxDestination dest = - DecodeDestination(name_, wallet->GetChainParams()); - if (!IsValidDestination(dest)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, - std::string("Invalid Bitcoin address: ") + - name_); - } - - if (destinations.count(dest)) { - throw JSONRPCError( - RPC_INVALID_PARAMETER, - std::string("Invalid parameter, duplicated address: ") + name_); - } - destinations.insert(dest); - - CScript scriptPubKey = GetScriptForDestination(dest); - Amount nAmount = AmountFromValue(sendTo[name_]); - if (nAmount <= Amount::zero()) { - throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount for send"); - } + std::vector recipients; + ParseRecipients(sendTo, subtractFeeFromAmount, recipients, + wallet->GetChainParams()); - bool fSubtractFeeFromAmount = false; - for (size_t idx = 0; idx < subtractFeeFromAmount.size(); idx++) { - const UniValue &addr = subtractFeeFromAmount[idx]; - if (addr.get_str() == name_) { - fSubtractFeeFromAmount = true; - } - } - - CRecipient recipient = {scriptPubKey, nAmount, fSubtractFeeFromAmount}; - vecSend.push_back(recipient); - } - - EnsureWalletIsUnlocked(pwallet); - - // Shuffle recipient list - std::shuffle(vecSend.begin(), vecSend.end(), FastRandomContext()); - - // Send - Amount nFeeRequired = Amount::zero(); - int nChangePosRet = -1; - bilingual_str error; - CTransactionRef tx; - CCoinControl coinControl; - bool fCreated = pwallet->CreateTransaction( - vecSend, tx, nFeeRequired, nChangePosRet, error, coinControl); - if (!fCreated) { - throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, error.original); - } - pwallet->CommitTransaction(tx, std::move(mapValue), {} /* orderForm */); - return tx->GetId().GetHex(); + CCoinControl coin_control; + return SendMoney(pwallet, coin_control, recipients, std::move(mapValue)); } static UniValue addmultisigaddress(const Config &config, diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -124,7 +124,7 @@ self.nodes[2].lockunspent(False, [unspent_0]) assert_raises_rpc_error(-8, "Invalid parameter, output already locked", self.nodes[2].lockunspent, False, [unspent_0]) - assert_raises_rpc_error(-4, "Insufficient funds", + assert_raises_rpc_error(-6, "Insufficient funds", self.nodes[2].sendtoaddress, self.nodes[2].getnewaddress(), 20000000) assert_equal([unspent_0], self.nodes[2].listlockunspent()) self.nodes[2].lockunspent(True, [unspent_0]) @@ -346,6 +346,13 @@ assert_equal(tx_obj['amount'], Decimal('-1000')) # General checks for errors from incorrect inputs + # This will raise an exception because the amount is negative + assert_raises_rpc_error(-3, + "Amount out of range", + self.nodes[0].sendtoaddress, + self.nodes[2].getnewaddress(), + "-1") + # This will raise an exception because the amount type is wrong assert_raises_rpc_error(-3, "Invalid amount", self.nodes[0].sendtoaddress, self.nodes[2].getnewaddress(), "1f-4") @@ -560,7 +567,7 @@ node0_balance = self.nodes[0].getbalance() # With walletrejectlongchains we will not create the tx and store it in # our wallet. - assert_raises_rpc_error(-4, "Transaction has too long of a mempool chain", + assert_raises_rpc_error(-6, "Transaction has too long of a mempool chain", self.nodes[0].sendtoaddress, sending_addr, node0_balance - Decimal('10000')) # Verify nothing new in wallet