From 92092d3afebe962f662fa9bb03dcc61878257390 Mon Sep 17 00:00:00 2001 From: timemarkovqtum Date: Tue, 2 Jul 2024 16:10:04 +0200 Subject: [PATCH] Port rpc spend --- src/rpc/rawtransaction_util.cpp | 8 + src/rpc/rawtransaction_util.h | 4 + src/wallet/rpc/addresses.cpp | 140 ++++++- src/wallet/rpc/spend.cpp | 636 ++++++++++++++++++++++++++++++-- src/wallet/rpc/util.cpp | 3 + src/wallet/rpc/wallet.cpp | 8 + 6 files changed, 744 insertions(+), 55 deletions(-) diff --git a/src/rpc/rawtransaction_util.cpp b/src/rpc/rawtransaction_util.cpp index a9e11622a7..c0f505fdf6 100644 --- a/src/rpc/rawtransaction_util.cpp +++ b/src/rpc/rawtransaction_util.cpp @@ -302,6 +302,10 @@ void ParsePrevouts(const UniValue& prevTxsUnival, FillableSigningProvider* keyst } } +void CheckSenderSignatures(CMutableTransaction& mtx) +{ +} + void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map& coins, const UniValue& hashType, UniValue& result) { int nHashType = ParseSighashString(hashType); @@ -334,3 +338,7 @@ void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const result.pushKV("errors", vErrors); } } + +void SignTransactionOutputResultToJSON(CMutableTransaction &mtx, bool complete, std::map &output_errors, UniValue &result) +{ +} diff --git a/src/rpc/rawtransaction_util.h b/src/rpc/rawtransaction_util.h index 964d0b095b..950365b4ee 100644 --- a/src/rpc/rawtransaction_util.h +++ b/src/rpc/rawtransaction_util.h @@ -55,4 +55,8 @@ void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in); /** Create a transaction from univalue parameters */ CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional rbf); +void SignTransactionOutputResultToJSON(CMutableTransaction& mtx, bool complete, std::map& output_errors, UniValue& result); + +void CheckSenderSignatures(CMutableTransaction& mtx); + #endif // BITCOIN_RPC_RAWTRANSACTION_UTIL_H diff --git a/src/wallet/rpc/addresses.cpp b/src/wallet/rpc/addresses.cpp index acaa2d8b15..e2af05612d 100644 --- a/src/wallet/rpc/addresses.cpp +++ b/src/wallet/rpc/addresses.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include @@ -23,7 +24,7 @@ namespace wallet { RPCHelpMan getnewaddress() { return RPCHelpMan{"getnewaddress", - "\nReturns a new Bitcoin address for receiving payments.\n" + "\nReturns a new Qtum address for receiving payments.\n" "If 'label' is specified, it is added to the address book \n" "so payments received with the address will be associated with 'label'.\n", { @@ -31,7 +32,7 @@ RPCHelpMan getnewaddress() {"address_type", RPCArg::Type::STR, RPCArg::DefaultHint{"set by -addresstype"}, "The address type to use. Options are \"legacy\", \"p2sh-segwit\", \"bech32\", and \"bech32m\"."}, }, RPCResult{ - RPCResult::Type::STR, "address", "The new bitcoin address" + RPCResult::Type::STR, "address", "The new qtum address" }, RPCExamples{ HelpExampleCli("getnewaddress", "") @@ -75,7 +76,7 @@ RPCHelpMan getnewaddress() RPCHelpMan getrawchangeaddress() { return RPCHelpMan{"getrawchangeaddress", - "\nReturns a new Bitcoin address, for receiving change.\n" + "\nReturns a new Qtum address, for receiving change.\n" "This is for use with raw transactions, NOT normal use.\n", { {"address_type", RPCArg::Type::STR, RPCArg::DefaultHint{"set by -changetype"}, "The address type to use. Options are \"legacy\", \"p2sh-segwit\", \"bech32\", and \"bech32m\"."}, @@ -124,7 +125,7 @@ RPCHelpMan setlabel() return RPCHelpMan{"setlabel", "\nSets the label associated with the given address.\n", { - {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The bitcoin address to be associated with a label."}, + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The qtum address to be associated with a label."}, {"label", RPCArg::Type::STR, RPCArg::Optional::NO, "The label to assign to the address."}, }, RPCResult{RPCResult::Type::NONE, "", ""}, @@ -141,7 +142,7 @@ RPCHelpMan setlabel() CTxDestination dest = DecodeDestination(request.params[0].get_str()); if (!IsValidDestination(dest)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address"); + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Qtum address"); } const std::string label{LabelFromValue(request.params[1])}; @@ -171,7 +172,7 @@ RPCHelpMan listaddressgroupings() { {RPCResult::Type::ARR_FIXED, "", "", { - {RPCResult::Type::STR, "address", "The bitcoin address"}, + {RPCResult::Type::STR, "address", "The qtum address"}, {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT}, {RPCResult::Type::STR, "label", /*optional=*/true, "The label"}, }}, @@ -221,16 +222,16 @@ RPCHelpMan addmultisigaddress() { return RPCHelpMan{"addmultisigaddress", "\nAdd an nrequired-to-sign multisignature address to the wallet. Requires a new wallet backup.\n" - "Each key is a Bitcoin address or hex-encoded public key.\n" + "Each key is a Qtum address or hex-encoded public key.\n" "This functionality is only intended for use with non-watchonly addresses.\n" "See `importaddress` for watchonly p2sh address support.\n" "If 'label' is specified, assign address to that label.\n" "Note: This command is only compatible with legacy wallets.\n", { {"nrequired", RPCArg::Type::NUM, RPCArg::Optional::NO, "The number of required signatures out of the n keys or addresses."}, - {"keys", RPCArg::Type::ARR, RPCArg::Optional::NO, "The bitcoin addresses or hex-encoded public keys", + {"keys", RPCArg::Type::ARR, RPCArg::Optional::NO, "The qtum addresses or hex-encoded public keys", { - {"key", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "bitcoin address or hex-encoded public key"}, + {"key", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "qtum address or hex-encoded public key"}, }, }, {"label", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A label to assign the addresses to."}, @@ -482,7 +483,7 @@ class DescribeWalletAddressVisitor UniValue operator()(const WitnessUnknown& id) const { return UniValue(UniValue::VOBJ); } }; -static UniValue DescribeWalletAddress(const CWallet& wallet, const CTxDestination& dest) +UniValue DescribeWalletAddress(const CWallet& wallet, const CTxDestination& dest) { UniValue ret(UniValue::VOBJ); UniValue detail = DescribeAddress(dest); @@ -497,15 +498,15 @@ static UniValue DescribeWalletAddress(const CWallet& wallet, const CTxDestinatio RPCHelpMan getaddressinfo() { return RPCHelpMan{"getaddressinfo", - "\nReturn information about the given bitcoin address.\n" + "\nReturn information about the given qtum address.\n" "Some of the information will only be present if the address is in the active wallet.\n", { - {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The bitcoin address for which to get information."}, + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The qtum address for which to get information."}, }, RPCResult{ RPCResult::Type::OBJ, "", "", { - {RPCResult::Type::STR, "address", "The bitcoin address validated."}, + {RPCResult::Type::STR, "address", "The qtum address validated."}, {RPCResult::Type::STR_HEX, "scriptPubKey", "The hex-encoded scriptPubKey generated by the address."}, {RPCResult::Type::BOOL, "ismine", "If the address is yours."}, {RPCResult::Type::BOOL, "iswatchonly", "If the address is watchonly."}, @@ -762,7 +763,7 @@ RPCHelpMan walletdisplayaddress() "walletdisplayaddress", "Display address on an external signer for verification.", { - {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "bitcoin address to display"}, + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "qtum address to display"}, }, RPCResult{ RPCResult::Type::OBJ,"","", @@ -797,4 +798,115 @@ RPCHelpMan walletdisplayaddress() }; } #endif // ENABLE_EXTERNAL_SIGNER + +/////////////////////////////////////////////////////////////////////// +bool getAddressToPubKey(const CWallet& wallet, std::string addr, std::string& pubkey) +{ + CTxDestination dest = DecodeDestination(addr); + // Make sure the destination is valid + if (!IsValidDestination(dest)) { + return false; + } + UniValue detail = DescribeWalletAddress(wallet, dest); + if(detail.exists("pubkey") && detail["pubkey"].isStr()) + { + pubkey = detail["pubkey"].get_str(); + return true; + } + + return false; +} + +RPCHelpMan createmultisig() +{ + return RPCHelpMan{"createmultisig", + "\nCreates a multi-signature address with n signature of m keys required.\n" + "It returns a json object with the address and redeemScript.\n", + { + {"nrequired", RPCArg::Type::NUM, RPCArg::Optional::NO, "The number of required signatures out of the n keys."}, + {"keys", RPCArg::Type::ARR, RPCArg::Optional::NO, "A json array of keys which are qtum addresses or hex-encoded public keys.", + { + {"key", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "qtum address or hex-encoded public key"}, + }}, + {"address_type", RPCArg::Type::STR, RPCArg::Default{"legacy"}, "The address type to use. Options are \"legacy\", \"p2sh-segwit\", and \"bech32\"."}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR, "address", "The value of the new multisig address."}, + {RPCResult::Type::STR_HEX, "redeemScript", "The string value of the hex-encoded redemption script."}, + {RPCResult::Type::STR, "descriptor", "The descriptor for this multisig"}, + {RPCResult::Type::ARR, "warnings", /* optional */ true, "Any warnings resulting from the creation of this multisig", + { + {RPCResult::Type::STR, "", ""}, + }}, + } + }, + RPCExamples{ + "\nCreate a multisig address from 2 public keys\n" + + HelpExampleCli("createmultisig", "2 \"[\\\"QjWnDZxwLhrJDcp4Hisse8RfBo2jRDZY5Z\\\",\\\"Q6sSauSf5pF2UkUwvKGq4qjNRzBZYqgEL5\\\"]\"") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("createmultisig", "2, [\"QjWnDZxwLhrJDcp4Hisse8RfBo2jRDZY5Z\",\"Q6sSauSf5pF2UkUwvKGq4qjNRzBZYqgEL5\"]") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + + std::shared_ptr const pwallet = wallet::GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + LOCK(pwallet->cs_wallet); + std::string pubkey; + + int required = request.params[0].getInt(); + + // Get the public keys + const UniValue& keys = request.params[1].get_array(); + std::vector pubkeys; + for (unsigned int i = 0; i < keys.size(); ++i) { + if (IsHex(keys[i].get_str()) && (keys[i].get_str().length() == 66 || keys[i].get_str().length() == 130)) { + pubkeys.push_back(HexToPubKey(keys[i].get_str())); + } else if (getAddressToPubKey(*pwallet, keys[i].get_str(), pubkey)){ + pubkeys.push_back(HexToPubKey(pubkey)); + } else { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid public key: %s\n.", keys[i].get_str())); + } + } + + // Get the output type + OutputType output_type = OutputType::LEGACY; + if (!request.params[2].isNull()) { + std::optional parsed = ParseOutputType(request.params[2].get_str()); + if (!parsed) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", request.params[2].get_str())); + } else if (parsed.value() == OutputType::BECH32M) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "createmultisig cannot create bech32m multisig addresses"); + } + output_type = parsed.value(); + } + + // Construct using pay-to-script-hash: + FillableSigningProvider keystore; + CScript inner; + const CTxDestination dest = AddAndGetMultisigDestination(required, pubkeys, output_type, keystore, inner); + + // Make the descriptor + std::unique_ptr descriptor = InferDescriptor(GetScriptForDestination(dest), keystore); + + UniValue result(UniValue::VOBJ); + result.pushKV("address", EncodeDestination(dest)); + result.pushKV("redeemScript", HexStr(inner)); + result.pushKV("descriptor", descriptor->ToString()); + + UniValue warnings(UniValue::VARR); + if (descriptor->GetOutputType() != output_type) { + // Only warns if the user has explicitly chosen an address type we cannot generate + warnings.push_back("Unable to make chosen address type, please ensure no uncompressed public keys are present."); + } + PushWarnings(warnings, result); + + return result; +}, + }; +} + } // namespace wallet diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp index 6060f017ce..aed3db81a7 100644 --- a/src/wallet/rpc/spend.cpp +++ b/src/wallet/rpc/spend.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -163,6 +164,11 @@ UniValue SendMoney(CWallet& wallet, const CCoinControl &coin_control, std::vecto throw JSONRPCError(RPC_WALLET_ERROR, "Error: Private keys are disabled for this wallet"); } + if (wallet.m_wallet_unlock_staking_only) + { + throw JSONRPCError(RPC_WALLET_ERROR, "Error: Wallet unlocked for staking only, unable to create transaction."); + } + // Shuffle recipient list std::shuffle(recipients.begin(), recipients.end(), FastRandomContext()); @@ -226,7 +232,7 @@ RPCHelpMan sendtoaddress() "\nSend an amount to a given address." + HELP_REQUIRING_PASSPHRASE, { - {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The bitcoin address to send to."}, + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The qtum address to send to."}, {"amount", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "The amount in " + CURRENCY_UNIT + " to send. eg 0.1"}, {"comment", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A comment used to store what the transaction is for.\n" "This is not part of the transaction, just kept in your wallet."}, @@ -234,7 +240,7 @@ RPCHelpMan sendtoaddress() "to which you're sending the transaction. This is not part of the \n" "transaction, just kept in your wallet."}, {"subtractfeefromamount", RPCArg::Type::BOOL, RPCArg::Default{false}, "The fee will be deducted from the amount being sent.\n" - "The recipient will receive less bitcoins than you enter in the amount field."}, + "The recipient will receive less qtums than you enter in the amount field."}, {"replaceable", RPCArg::Type::BOOL, RPCArg::DefaultHint{"wallet default"}, "Signal that this transaction can be replaced by a transaction (BIP 125)"}, {"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks"}, {"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"unset"}, "The fee estimate mode, must be one of (case insensitive):\n" @@ -243,6 +249,8 @@ RPCHelpMan sendtoaddress() "dirty if they have previously been used in a transaction. If true, this also activates avoidpartialspends, grouping outputs by their addresses."}, {"fee_rate", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"not set, fall back to wallet fee estimation"}, "Specify a fee rate in " + CURRENCY_ATOM + "/vB."}, {"verbose", RPCArg::Type::BOOL, RPCArg::Default{false}, "If true, return extra information about the transaction."}, + {"senderaddress", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The qtum address that will be used to send money from."}, + {"changetosender", RPCArg::Type::BOOL, RPCArg::Default{false}, "Return the change to the sender."}, }, { RPCResult{"if verbose is not set or set to false", @@ -257,17 +265,18 @@ RPCHelpMan sendtoaddress() }, }, RPCExamples{ - "\nSend 0.1 BTC\n" + "\nSend 0.1 QTUM\n" + HelpExampleCli("sendtoaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 0.1") + - "\nSend 0.1 BTC with a confirmation target of 6 blocks in economical fee estimate mode using positional arguments\n" + "\nSend 0.1 QTUM with a confirmation target of 6 blocks in economical fee estimate mode using positional arguments\n" + HelpExampleCli("sendtoaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 0.1 \"donation\" \"sean's outpost\" false true 6 economical") + - "\nSend 0.1 BTC with a fee rate of 1.1 " + CURRENCY_ATOM + "/vB, subtract fee from amount, BIP125-replaceable, using positional arguments\n" - + HelpExampleCli("sendtoaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 0.1 \"drinks\" \"room77\" true true null \"unset\" null 1.1") + - "\nSend 0.2 BTC with a confirmation target of 6 blocks in economical fee estimate mode using named arguments\n" + "\nSend 0.1 QTUM with a fee rate of 400 " + CURRENCY_ATOM + "/vB, subtract fee from amount, BIP125-replaceable, using positional arguments\n" + + HelpExampleCli("sendtoaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 0.1 \"drinks\" \"room77\" true true null \"unset\" null 400") + + "\nSend 0.2 QTUM with a confirmation target of 6 blocks in economical fee estimate mode using named arguments\n" + HelpExampleCli("-named sendtoaddress", "address=\"" + EXAMPLE_ADDRESS[0] + "\" amount=0.2 conf_target=6 estimate_mode=\"economical\"") + - "\nSend 0.5 BTC with a fee rate of 25 " + CURRENCY_ATOM + "/vB using named arguments\n" - + HelpExampleCli("-named sendtoaddress", "address=\"" + EXAMPLE_ADDRESS[0] + "\" amount=0.5 fee_rate=25") - + HelpExampleCli("-named sendtoaddress", "address=\"" + EXAMPLE_ADDRESS[0] + "\" amount=0.5 fee_rate=25 subtractfeefromamount=false replaceable=true avoid_reuse=true comment=\"2 pizzas\" comment_to=\"jeremy\" verbose=true") + "\nSend 0.5 QTUM with a fee rate of 425 " + CURRENCY_ATOM + "/vB using named arguments\n" + + HelpExampleCli("-named sendtoaddress", "address=\"" + EXAMPLE_ADDRESS[0] + "\" amount=0.5 fee_rate=425") + + HelpExampleCli("-named sendtoaddress", "address=\"" + EXAMPLE_ADDRESS[0] + "\" amount=0.5 fee_rate=425 subtractfeefromamount=false replaceable=true avoid_reuse=true comment=\"2 pizzas\" comment_to=\"jeremy\" verbose=true") + + HelpExampleCli("sendtoaddress", "\"" + EXAMPLE_ADDRESS[0] + "\", 0.1, \"donation\", \"seans outpost\", false, null, null, \"unset\", null, null, false, \"" + EXAMPLE_ADDRESS[1] + "\", true") }, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { @@ -309,6 +318,53 @@ RPCHelpMan sendtoaddress() ); const bool verbose{request.params[10].isNull() ? false : request.params[10].get_bool()}; + bool fHasSender=false; + CTxDestination senderAddress; + if (!request.params[11].isNull()){ + senderAddress = DecodeDestination(request.params[11].get_str()); + if (!IsValidDestination(senderAddress)) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Qtum address to send from"); + else + fHasSender=true; + } + + bool fChangeToSender=false; + if (!request.params[12].isNull()){ + fChangeToSender=request.params[12].get_bool(); + } + + if(fHasSender){ + //find a UTXO with sender address + + UniValue results(UniValue::VARR); + + coin_control.m_allow_other_inputs=true; + + assert(pwallet != NULL); + std::vector vecOutputs = AvailableCoins(*pwallet, &coin_control).All(); + + for(const COutput& out : vecOutputs) { + CTxDestination destAdress; + const CScript& scriptPubKey = out.txout.scriptPubKey; + bool fValidAddress = ExtractDestination(scriptPubKey, destAdress, nullptr, true); + + if (!fValidAddress || senderAddress != destAdress) + continue; + + coin_control.Select(out.outpoint); + + break; + + } + + if(!coin_control.HasSelected()){ + throw JSONRPCError(RPC_TYPE_ERROR, "Sender address does not have any unspent outputs"); + } + if(fChangeToSender){ + coin_control.destChange=senderAddress; + } + } + return SendMoney(*pwallet, coin_control, recipients, mapValue, verbose); }, }; @@ -326,14 +382,14 @@ RPCHelpMan sendmany() }}, {"amounts", RPCArg::Type::OBJ_USER_KEYS, RPCArg::Optional::NO, "The addresses and amounts", { - {"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "The bitcoin address is the key, the numeric amount (can be string) in " + CURRENCY_UNIT + " is the value"}, + {"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "The qtum address is the key, the numeric amount (can be string) in " + CURRENCY_UNIT + " is the value"}, }, }, {"minconf", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Ignored dummy value"}, {"comment", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A comment"}, {"subtractfeefrom", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "The addresses.\n" "The fee will be equally deducted from the amount of each selected address.\n" - "Those recipients will receive less bitcoins than you enter in their corresponding amount field.\n" + "Those recipients will receive less qtums than you enter in their corresponding amount field.\n" "If no addresses are specified here, the sender pays the fee.", { {"address", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Subtract fee from this address"}, @@ -420,8 +476,8 @@ RPCHelpMan settxfee() RPCResult::Type::BOOL, "", "Returns true if successful" }, RPCExamples{ - HelpExampleCli("settxfee", "0.00001") - + HelpExampleRpc("settxfee", "0.00001") + HelpExampleCli("settxfee", "0.00400") + + HelpExampleRpc("settxfee", "0.00400") }, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { @@ -547,7 +603,7 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact CTxDestination dest = DecodeDestination(change_address_str); if (!IsValidDestination(dest)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Change address must be a valid bitcoin address"); + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Change address must be a valid qtum address"); } coinControl.destChange = dest; @@ -755,7 +811,7 @@ RPCHelpMan fundrawtransaction() "If that happens, you will need to fund the transaction with different inputs and republish it."}, {"minconf", RPCArg::Type::NUM, RPCArg::Default{0}, "If add_inputs is specified, require inputs with at least this many confirmations."}, {"maxconf", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "If add_inputs is specified, require inputs with at most this many confirmations."}, - {"changeAddress", RPCArg::Type::STR, RPCArg::DefaultHint{"automatic"}, "The bitcoin address to receive the change"}, + {"changeAddress", RPCArg::Type::STR, RPCArg::DefaultHint{"automatic"}, "The qtum address to receive the change"}, {"changePosition", RPCArg::Type::NUM, RPCArg::DefaultHint{"random"}, "The index of the change output"}, {"change_type", RPCArg::Type::STR, RPCArg::DefaultHint{"set by -changetype"}, "The output type to use. Only valid if changeAddress is not specified. Options are \"legacy\", \"p2sh-segwit\", \"bech32\", and \"bech32m\"."}, {"includeWatching", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Also select inputs which are watch only.\n" @@ -766,7 +822,7 @@ RPCHelpMan fundrawtransaction() {"feeRate", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"not set, fall back to wallet fee estimation"}, "Specify a fee rate in " + CURRENCY_UNIT + "/kvB."}, {"subtractFeeFromOutputs", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "The integers.\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 qtums than you enter in their corresponding amount field.\n" "If no outputs are specified here, the sender pays the fee.", { {"vout_index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "The zero-based output index, before a change output is added."}, @@ -948,6 +1004,7 @@ RPCHelpMan signrawtransactionwithwallet() // Script verification errors std::map input_errors; + CheckSenderSignatures(mtx); bool complete = pwallet->SignTransaction(mtx, coins, nHashType, input_errors); UniValue result(UniValue::VOBJ); SignTransactionResultToJSON(mtx, complete, coins, input_errors, result); @@ -964,7 +1021,7 @@ static std::vector OutputsDoc() { {"", RPCArg::Type::OBJ_USER_KEYS, RPCArg::Optional::OMITTED, "", { - {"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "A key-value pair. The key (string) is the bitcoin address,\n" + {"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "A key-value pair. The key (string) is the qtum address,\n" "the value (float or string) is the amount in " + CURRENCY_UNIT + ""}, }, }, @@ -1211,7 +1268,7 @@ RPCHelpMan send() {"minconf", RPCArg::Type::NUM, RPCArg::Default{0}, "If add_inputs is specified, require inputs with at least this many confirmations."}, {"maxconf", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "If add_inputs is specified, require inputs with at most this many confirmations."}, {"add_to_wallet", RPCArg::Type::BOOL, RPCArg::Default{true}, "When false, returns a serialized transaction which will not be added to the wallet or broadcast"}, - {"change_address", RPCArg::Type::STR, RPCArg::DefaultHint{"automatic"}, "The bitcoin address to receive the change"}, + {"change_address", RPCArg::Type::STR, RPCArg::DefaultHint{"automatic"}, "The qtum address to receive the change"}, {"change_position", RPCArg::Type::NUM, RPCArg::DefaultHint{"random"}, "The index of the change output"}, {"change_type", RPCArg::Type::STR, RPCArg::DefaultHint{"set by -changetype"}, "The output type to use. Only valid if change_address is not specified. Options are \"legacy\", \"p2sh-segwit\", \"bech32\" and \"bech32m\"."}, {"fee_rate", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"not set, fall back to wallet fee estimation"}, "Specify a fee rate in " + CURRENCY_ATOM + "/vB.", RPCArgOptions{.also_positional = true}}, @@ -1235,7 +1292,7 @@ RPCHelpMan send() {"psbt", RPCArg::Type::BOOL, RPCArg::DefaultHint{"automatic"}, "Always return a PSBT, implies add_to_wallet=false."}, {"subtract_fee_from_outputs", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Outputs to subtract the fee from, specified as integer indices.\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 qtums than you enter in their corresponding amount field.\n" "If no outputs are specified here, the sender pays the fee.", { {"vout_index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "The zero-based output index, before a change output is added."}, @@ -1255,14 +1312,14 @@ RPCHelpMan send() } }, RPCExamples{"" - "\nSend 0.1 BTC with a confirmation target of 6 blocks in economical fee estimate mode\n" + "\nSend 0.1 QTUM with a confirmation target of 6 blocks in economical fee estimate mode\n" + HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.1}' 6 economical\n") + - "Send 0.2 BTC with a fee rate of 1.1 " + CURRENCY_ATOM + "/vB using positional arguments\n" - + HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.2}' null \"unset\" 1.1\n") + - "Send 0.2 BTC with a fee rate of 1 " + CURRENCY_ATOM + "/vB using the options argument\n" - + HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.2}' null \"unset\" null '{\"fee_rate\": 1}'\n") + - "Send 0.3 BTC with a fee rate of 25 " + CURRENCY_ATOM + "/vB using named arguments\n" - + HelpExampleCli("-named send", "outputs='{\"" + EXAMPLE_ADDRESS[0] + "\": 0.3}' fee_rate=25\n") + + "Send 0.2 QTUM with a fee rate of 400.1 " + CURRENCY_ATOM + "/vB using positional arguments\n" + + HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.2}' null \"unset\" 400.1\n") + + "Send 0.2 QTUM with a fee rate of 400 " + CURRENCY_ATOM + "/vB using the options argument\n" + + HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.2}' null \"unset\" null '{\"fee_rate\": 400}'\n") + + "Send 0.3 QTUM with a fee rate of 425 " + CURRENCY_ATOM + "/vB using named arguments\n" + + HelpExampleCli("-named send", "outputs='{\"" + EXAMPLE_ADDRESS[0] + "\": 0.3}' fee_rate=425\n") + "Create 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}]}'") }, @@ -1310,10 +1367,10 @@ RPCHelpMan sendall() {"recipients", RPCArg::Type::ARR, RPCArg::Optional::NO, "The sendall destinations. Each address may only appear once.\n" "Optionally some recipients can be specified with an amount to perform payments, but at least one address must appear without a specified amount.\n", { - {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "A bitcoin address which receives an equal share of the unspecified amount."}, + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "A qtum address which receives an equal share of the unspecified amount."}, {"", RPCArg::Type::OBJ_USER_KEYS, RPCArg::Optional::OMITTED, "", { - {"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 + ""}, + {"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "A key-value pair. The key (string) is the qtum address, the value (float or string) is the amount in " + CURRENCY_UNIT + ""}, }, }, }, @@ -1364,16 +1421,16 @@ RPCHelpMan sendall() } }, RPCExamples{"" - "\nSpend all UTXOs from the wallet with a fee rate of 1 " + CURRENCY_ATOM + "/vB using named arguments\n" - + HelpExampleCli("-named sendall", "recipients='[\"" + EXAMPLE_ADDRESS[0] + "\"]' fee_rate=1\n") + - "Spend all UTXOs with a fee rate of 1.1 " + CURRENCY_ATOM + "/vB using positional arguments\n" - + HelpExampleCli("sendall", "'[\"" + EXAMPLE_ADDRESS[0] + "\"]' null \"unset\" 1.1\n") + - "Spend all UTXOs split into equal amounts to two addresses with a fee rate of 1.5 " + CURRENCY_ATOM + "/vB using the options argument\n" - + HelpExampleCli("sendall", "'[\"" + EXAMPLE_ADDRESS[0] + "\", \"" + EXAMPLE_ADDRESS[1] + "\"]' null \"unset\" null '{\"fee_rate\": 1.5}'\n") + - "Leave dust UTXOs in wallet, spend only UTXOs with positive effective value with a fee rate of 10 " + CURRENCY_ATOM + "/vB using the options argument\n" - + HelpExampleCli("sendall", "'[\"" + EXAMPLE_ADDRESS[0] + "\"]' null \"unset\" null '{\"fee_rate\": 10, \"send_max\": true}'\n") + - "Spend all UTXOs with a fee rate of 1.3 " + CURRENCY_ATOM + "/vB using named arguments and sending a 0.25 " + CURRENCY_UNIT + " to another recipient\n" - + HelpExampleCli("-named sendall", "recipients='[{\"" + EXAMPLE_ADDRESS[1] + "\": 0.25}, \""+ EXAMPLE_ADDRESS[0] + "\"]' fee_rate=1.3\n") + "\nSpend all UTXOs from the wallet with a fee rate of 400 " + CURRENCY_ATOM + "/vB using named arguments\n" + + HelpExampleCli("-named sendall", "recipients='[\"" + EXAMPLE_ADDRESS[0] + "\"]' fee_rate=400\n") + + "Spend all UTXOs with a fee rate of 400.1 " + CURRENCY_ATOM + "/vB using positional arguments\n" + + HelpExampleCli("sendall", "'[\"" + EXAMPLE_ADDRESS[0] + "\"]' null \"unset\" 400.1\n") + + "Spend all UTXOs split into equal amounts to two addresses with a fee rate of 400.5 " + CURRENCY_ATOM + "/vB using the options argument\n" + + HelpExampleCli("sendall", "'[\"" + EXAMPLE_ADDRESS[0] + "\", \"" + EXAMPLE_ADDRESS[1] + "\"]' null \"unset\" null '{\"fee_rate\": 400.5}'\n") + + "Leave dust UTXOs in wallet, spend only UTXOs with positive effective value with a fee rate of 410 " + CURRENCY_ATOM + "/vB using the options argument\n" + + HelpExampleCli("sendall", "'[\"" + EXAMPLE_ADDRESS[0] + "\"]' null \"unset\" null '{\"fee_rate\": 410, \"send_max\": true}'\n") + + "Spend all UTXOs with a fee rate of 400.3 " + CURRENCY_ATOM + "/vB using named arguments and sending a 0.25 " + CURRENCY_UNIT + " to another recipient\n" + + HelpExampleCli("-named sendall", "recipients='[{\"" + EXAMPLE_ADDRESS[1] + "\": 0.25}, \""+ EXAMPLE_ADDRESS[0] + "\"]' fee_rate=400.3\n") }, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { @@ -1675,7 +1732,7 @@ RPCHelpMan walletcreatefundedpsbt() "If that happens, you will need to fund the transaction with different inputs and republish it."}, {"minconf", RPCArg::Type::NUM, RPCArg::Default{0}, "If add_inputs is specified, require inputs with at least this many confirmations."}, {"maxconf", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "If add_inputs is specified, require inputs with at most this many confirmations."}, - {"changeAddress", RPCArg::Type::STR, RPCArg::DefaultHint{"automatic"}, "The bitcoin address to receive the change"}, + {"changeAddress", RPCArg::Type::STR, RPCArg::DefaultHint{"automatic"}, "The qtum address to receive the change"}, {"changePosition", RPCArg::Type::NUM, RPCArg::DefaultHint{"random"}, "The index of the change output"}, {"change_type", RPCArg::Type::STR, RPCArg::DefaultHint{"set by -changetype"}, "The output type to use. Only valid if changeAddress is not specified. Options are \"legacy\", \"p2sh-segwit\", \"bech32\", and \"bech32m\"."}, {"includeWatching", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Also select inputs which are watch only"}, @@ -1684,7 +1741,7 @@ RPCHelpMan walletcreatefundedpsbt() {"feeRate", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"not set, fall back to wallet fee estimation"}, "Specify a fee rate in " + CURRENCY_UNIT + "/kvB."}, {"subtractFeeFromOutputs", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "The outputs to subtract the fee from.\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 qtums than you enter in their corresponding amount field.\n" "If no outputs are specified here, the sender pays the fee.", { {"vout_index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "The zero-based output index, before a change output is added."}, @@ -1761,4 +1818,501 @@ RPCHelpMan walletcreatefundedpsbt() }, }; } + +void SplitRemainder(std::vector& vecSend, CAmount& remainder, CAmount maxValue) +{ + if(remainder > 0) + { + for(int i = vecSend.size() - 1; i >= 0 ; i--) + { + CAmount diffAmount = maxValue - vecSend[i].nAmount; + if(diffAmount > 0) + { + if((remainder - diffAmount) > 0) + { + vecSend[i].nAmount = vecSend[i].nAmount + diffAmount; + remainder -= diffAmount; + } + else + { + vecSend[i].nAmount = vecSend[i].nAmount + remainder; + remainder = 0; + } + } + + if(remainder <= 0) + break; + } + } +} + +CTransactionRef SplitUTXOs(std::shared_ptr const pwallet, const CTxDestination &address, CAmount nValue, CAmount maxValue, const CCoinControl& coin_control, CAmount nTotal, int maxOutputs, CAmount& nSplited, bool sign) +{ + // Check amount + if (nValue <= 0) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid amount"); + + if (nValue > nTotal) + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient funds"); + + // Split into utxos with nValue + std::vector vecSend; + constexpr int RANDOM_CHANGE_POSITION = -1; + int numOfRecipients = static_cast(nTotal / nValue); + + // Compute the number of recipients + CAmount remainder = nTotal % nValue; + if(remainder == 0 && numOfRecipients > 0) + { + numOfRecipients -= 1; + remainder = nValue; + } + if(numOfRecipients > maxOutputs) + { + numOfRecipients = maxOutputs; + remainder = 0; + } + + // Split coins between recipients + CAmount nTxAmount = 0; + nSplited = 0; + CRecipient recipient = {address, nValue, false}; + for(int i = 0; i < numOfRecipients; i++) { + vecSend.push_back(recipient); + } + SplitRemainder(vecSend, remainder, maxValue); + + // Get the total amount of the outputs + for(CRecipient rec : vecSend) + { + nTxAmount += rec.nAmount; + } + + // Create the transaction + CTransactionRef tx; + if((nTxAmount + pwallet->m_default_max_tx_fee) <= nTotal) + { + auto res = CreateTransaction(*pwallet, vecSend, RANDOM_CHANGE_POSITION, coin_control, sign, 0, false); + if (!res) { + throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original); + } + tx = res->tx; + nSplited = res->fee; + } + else if (vecSend.size() > 0) + { + // Pay the fee for the tx with the last recipient + CRecipient lastRecipient = vecSend[vecSend.size() - 1]; + lastRecipient.fSubtractFeeFromAmount = true; + vecSend[vecSend.size() - 1] = lastRecipient; + CAmount nFeeRequired = 0; + { + auto res = CreateTransaction(*pwallet, vecSend, RANDOM_CHANGE_POSITION, coin_control, sign, 0, false); + if (!res) { + throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original); + } + tx = res->tx; + nFeeRequired = res->fee; + } + + // Combine the last 2 outputs when the last output have value less than nValue due to paying the fee + if(vecSend.size() >= 2) + { + if((lastRecipient.nAmount - nFeeRequired) < nValue) + { + bool payFeeRemainder = (nTotal - nTxAmount) > nFeeRequired * 1.1; + if(payFeeRemainder) + { + // Pay the fee with the remainder + lastRecipient.fSubtractFeeFromAmount = false; + vecSend.pop_back(); + vecSend.push_back(lastRecipient); + } + else + { + // Combine the last 2 outputs + CAmount nValueLast2 = lastRecipient.nAmount + vecSend[vecSend.size() - 2].nAmount; + lastRecipient.nAmount = lastRecipient.nAmount + nFeeRequired; + lastRecipient.fSubtractFeeFromAmount = true; + nValueLast2 -= lastRecipient.nAmount; + vecSend.pop_back(); + vecSend.pop_back(); + vecSend.push_back(lastRecipient); + + // Split the rest with the others + SplitRemainder(vecSend, nValueLast2, maxValue); + } + + auto res = CreateTransaction(*pwallet, vecSend, RANDOM_CHANGE_POSITION, coin_control, sign, 0, false); + if (!res) { + throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original); + } + tx = res->tx; + nFeeRequired = res->fee; + if(payFeeRemainder) + { + nSplited = nFeeRequired; + } + } + } + } + + // Compute the splited amount + for(CRecipient rec : vecSend) + { + nSplited += rec.nAmount; + } + + // Send the transaction + if(sign) pwallet->CommitTransaction(tx, {} /* mapValue */, {} /* orderForm */); + + return tx; +} + +RPCHelpMan splitutxosforaddress() +{ + return RPCHelpMan{"splitutxosforaddress", + "\nSplit an address coins into utxo between min and max value." + + HELP_REQUIRING_PASSPHRASE, + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The qtum address to split utxos."}, + {"minvalue", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "Select utxo which value is smaller than value (minimum 0.1 COIN)"}, + {"maxvalue", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "Select utxo which value is greater than value (minimum 0.1 COIN)"}, + {"maxoutputs", RPCArg::Type::NUM, RPCArg::Default{100}, "Maximum outputs to create"}, + {"psbt", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Create partially signed transaction."}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_HEX, "txid", /*optional=*/true, "The hex-encoded transaction id"}, + {RPCResult::Type::STR, "psbt", /*optional=*/true, "The base64-encoded unsigned PSBT of the new transaction."}, + {RPCResult::Type::STR, "selected", "Selected amount of coins"}, + {RPCResult::Type::STR, "splited", "Splited amount of coins"}, + } + }, + RPCExamples{ + HelpExampleCli("splitutxosforaddress", "\"QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\" 100 200") + + HelpExampleCli("splitutxosforaddress", "\"QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\" 100 200 100") + + HelpExampleRpc("splitutxosforaddress", "\"QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\" 100 200") + + HelpExampleRpc("splitutxosforaddress", "\"QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\" 100 200 100") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return UniValue::VNULL; + + // Make sure the results are valid at least up to the most recent block + // the user could have gotten from another RPC command prior to now + pwallet->BlockUntilSyncedToCurrentChain(); + + LOCK(pwallet->cs_wallet); + + // Address + CTxDestination address = DecodeDestination(request.params[0].get_str()); + + if (!IsValidDestination(address)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Qtum address"); + } + CScript scriptPubKey = GetScriptForDestination(address); + if (!pwallet->IsMine(scriptPubKey)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Address not found in wallet"); + } + + // minimum value + CAmount minValue = AmountFromValue(request.params[1]); + + // maximum value + CAmount maxValue = AmountFromValue(request.params[2]); + + if (minValue < COIN/10 || maxValue <= 0 || minValue > maxValue) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid values for minimum and maximum"); + } + + // Maximum outputs + int maxOutputs = !request.params[3].isNull() ? request.params[3].getInt() : 100; + if (maxOutputs < 1) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid value for maximum outputs"); + } + + // Is psbt + bool fPsbt=pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS); + if (!request.params[4].isNull()){ + fPsbt=request.params[4].get_bool(); + } + + // Amount + CAmount nSplitAmount = minValue; + CAmount nRequiredAmount = nSplitAmount * maxOutputs; + + CCoinControl coin_control; + coin_control.destChange = address; + if(fPsbt) coin_control.fAllowWatchOnly = true; + + // Find UTXOs for a address with value smaller than minValue and greater then maxValue + coin_control.m_allow_other_inputs=true; + + assert(pwallet != NULL); + std::vector vecOutputs = AvailableCoins(*pwallet, &coin_control).All(); + + CAmount total = 0; + CAmount nSelectedAmount = 0; + for(const COutput& out : vecOutputs) { + CTxDestination destAdress; + const CScript& scriptPubKey = out.txout.scriptPubKey; + bool fValidAddress = ExtractDestination(scriptPubKey, destAdress, nullptr, true); + + CAmount val = out.txout.nValue; + if (!fValidAddress || address != destAdress || (val >= minValue && val <= maxValue ) ) + continue; + + if(nSelectedAmount <= nRequiredAmount) + { + coin_control.Select(out.outpoint); + nSelectedAmount += val; + } + total += val; + } + + CAmount splited = 0; + UniValue obj(UniValue::VOBJ); + if(coin_control.HasSelected() && nSplitAmount < nSelectedAmount){ + EnsureWalletIsUnlocked(*pwallet); + CTransactionRef tx = SplitUTXOs(pwallet, address, nSplitAmount, maxValue, coin_control, nSelectedAmount, maxOutputs, splited, !fPsbt); + if(fPsbt){ + // Make a blank psbt + PartiallySignedTransaction psbtx; + CMutableTransaction rawTx = CMutableTransaction(*tx); + psbtx.tx = rawTx; + for (unsigned int i = 0; i < rawTx.vin.size(); ++i) { + psbtx.inputs.push_back(PSBTInput()); + } + for (unsigned int i = 0; i < rawTx.vout.size(); ++i) { + psbtx.outputs.push_back(PSBTOutput()); + } + + // Fill transaction with out data but don't sign + bool bip32derivs = true; + bool complete = true; + const TransactionError err = pwallet->FillPSBT(psbtx, complete, 1, false, bip32derivs); + if (err != TransactionError::OK) { + throw JSONRPCTransactionError(err); + } + + // Serialize the PSBT + DataStream ssTx; + ssTx << psbtx; + obj.pushKV("psbt", EncodeBase64(ssTx.str())); + } + else + { + obj.pushKV("txid", tx->GetHash().GetHex()); + } + } + + obj.pushKV("selected", FormatMoney(total)); + obj.pushKV("splited", FormatMoney(splited)); + return obj; +}, + }; +} + +RPCHelpMan sendmanywithdupes() +{ + return RPCHelpMan{"sendmanywithdupes", + "Send multiple times. Amounts are double-precision floating point numbers. Supports duplicate addresses" + + HELP_REQUIRING_PASSPHRASE, + { + {"dummy", RPCArg::Type::STR, RPCArg::Default{"\"\""}, "Must be set to \"\" for backwards compatibility.", + RPCArgOptions{ + .oneline_description = "\"\"", + }}, + {"amounts", RPCArg::Type::OBJ_USER_KEYS, RPCArg::Optional::NO, "A json object with addresses and amounts", + { + {"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "The qtum address is the key, the numeric amount (can be string) in " + CURRENCY_UNIT + " is the value"}, + }, + }, + {"minconf", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Ignored dummy value"}, + {"comment", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A comment"}, + {"subtractfeefrom", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "A json array with addresses.\n" + "The fee will be equally deducted from the amount of each selected address.\n" + "Those recipients will receive less qtums than you enter in their corresponding amount field.\n" + "If no addresses are specified here, the sender pays the fee.", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Subtract fee from this address"}, + }, + }, + {"replaceable", RPCArg::Type::BOOL, RPCArg::DefaultHint{"wallet default"}, "Marks this transaction as BIP125 replaceable.\n" + "Allows this transaction to be replaced by a transaction with higher fees"}, + {"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks"}, + {"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"unset"}, std::string() + "The fee estimate mode, must be one of (case insensitive):\n" + " \"" + FeeModes("\"\n\"") + "\""}, + }, + RPCResult{ + RPCResult::Type::STR_HEX, "txid", "The transaction id for the send. Only 1 transaction is created regardless of\n" + "the number of addresses." + }, + RPCExamples{ + "\nSend two amounts to two different addresses:\n" + + HelpExampleCli("sendmanywithdupes", "\"\" \"{\\\"QD1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\\\":0.01,\\\"Q353tsE8YMTA4EuV7dgUXGjNFf9KpVvKHz\\\":0.02}\"") + + "\nSend two amounts to two different addresses setting the confirmation and comment:\n" + + HelpExampleCli("sendmanywithdupes", "\"\" \"{\\\"QD1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\\\":0.01,\\\"Q353tsE8YMTA4EuV7dgUXGjNFf9KpVvKHz\\\":0.02}\" 6 \"testing\"") + + "\nSend two amounts to two different addresses, subtract fee from amount:\n" + + HelpExampleCli("sendmanywithdupes", "\"\" \"{\\\"QD1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\\\":0.01,\\\"Q353tsE8YMTA4EuV7dgUXGjNFf9KpVvKHz\\\":0.02}\" 1 \"\" \"[\\\"QD1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\\\",\\\"Q353tsE8YMTA4EuV7dgUXGjNFf9KpVvKHz\\\"]\"") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("sendmanywithdupes", "\"\", {\"QD1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\":0.01,\"Q353tsE8YMTA4EuV7dgUXGjNFf9KpVvKHz\":0.02}, 6, \"testing\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + if (!wallet) return UniValue::VNULL; + CWallet* const pwallet = wallet.get(); + + // Make sure the results are valid at least up to the most recent block + // the user could have gotten from another RPC command prior to now + pwallet->BlockUntilSyncedToCurrentChain(); + + LOCK(pwallet->cs_wallet); + + if (!request.params[0].isNull() && !request.params[0].get_str().empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Dummy value must be set to \"\""); + } + UniValue sendTo = request.params[1].get_obj(); + + mapValue_t mapValue; + if (!request.params[3].isNull() && !request.params[3].get_str().empty()) + mapValue["comment"] = request.params[3].get_str(); + + UniValue subtractFeeFromAmount(UniValue::VARR); + if (!request.params[4].isNull()) + subtractFeeFromAmount = request.params[4].get_array(); + + CCoinControl coin_control; + if (!request.params[5].isNull()) { + coin_control.m_signal_bip125_rbf = request.params[5].get_bool(); + } + + if (!request.params[6].isNull()) { + coin_control.m_confirm_target = ParseConfirmTarget(request.params[6], pwallet->chain().estimateMaxBlocks()); + } + + if (!request.params[7].isNull()) { + if (!FeeModeFromString(request.params[7].get_str(), coin_control.m_fee_mode)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid estimate_mode parameter"); + } + } + + std::set destinations; + std::vector vecSend; + + std::vector keys = sendTo.getKeys(); + int i=0; + for (const std::string& name_ : keys) { + CTxDestination dest = DecodeDestination(name_); + if (!IsValidDestination(dest)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Qtum address: ") + name_); + } + + destinations.insert(dest); + + CAmount nAmount = AmountFromValue(sendTo[i]); + if (nAmount <= 0) + throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount for send"); + + bool fSubtractFeeFromAmount = false; + for (unsigned int idx = 0; idx < subtractFeeFromAmount.size(); idx++) { + const UniValue& addr = subtractFeeFromAmount[idx]; + if (addr.get_str() == name_) + fSubtractFeeFromAmount = true; + } + + CRecipient recipient = {dest, nAmount, fSubtractFeeFromAmount}; + vecSend.push_back(recipient); + i++; + } + + EnsureWalletIsUnlocked(*pwallet); + + // Shuffle recipient list + std::shuffle(vecSend.begin(), vecSend.end(), FastRandomContext()); + + // Send + constexpr int RANDOM_CHANGE_POSITION = -1; + auto res = CreateTransaction(*pwallet, vecSend, RANDOM_CHANGE_POSITION, coin_control); + if (!res) { + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, util::ErrorString(res).original); + } + const CTransactionRef& tx = res->tx; + pwallet->CommitTransaction(tx, std::move(mapValue), {} /* orderForm */); + + return tx->GetHash().GetHex(); +}, + }; +} + +RPCHelpMan signrawsendertransactionwithwallet() +{ + return RPCHelpMan{"signrawsendertransactionwithwallet", + "\nSign OP_SENDER outputs for raw transaction (serialized, hex-encoded).\n" + + HELP_REQUIRING_PASSPHRASE, + { + {"hexstring", RPCArg::Type::STR, RPCArg::Optional::NO, "The transaction hex string"}, + {"sighashtype", RPCArg::Type::STR, RPCArg::Default{"ALL"}, "The signature hash type. Must be one of\n" + " \"ALL\"\n" + " \"NONE\"\n" + " \"SINGLE\"\n" + " \"ALL|ANYONECANPAY\"\n" + " \"NONE|ANYONECANPAY\"\n" + " \"SINGLE|ANYONECANPAY\""}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_HEX, "hex", "The hex-encoded raw transaction with signature(s)"}, + {RPCResult::Type::BOOL, "complete", "If the transaction has a complete set of signatures"}, + {RPCResult::Type::ARR, "errors", /*optional=*/true, "Script verification errors (if there are any)", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_AMOUNT, "amount", "The amount of the output"}, + {RPCResult::Type::STR_HEX, "scriptPubKey", "The hex-encoded public key script of the output"}, + {RPCResult::Type::STR, "error", "Verification or signing error related to the output"}, + }}, + }}, + } + }, + RPCExamples{ + HelpExampleCli("signrawsendertransactionwithwallet", "\"myhex\"") + + HelpExampleRpc("signrawsendertransactionwithwallet", "\"myhex\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return UniValue::VNULL; + + CMutableTransaction mtx; + if (!DecodeHexTx(mtx, request.params[0].get_str(), true)) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); + } + + // Sign the transaction + LOCK(pwallet->cs_wallet); + EnsureWalletIsUnlocked(*pwallet); + + UniValue sigHashType = "ALL"; + if (!request.params[1].isNull()) { + sigHashType = request.params[1]; + } + int nHashType = ParseSighashString(sigHashType); + + // Script verification errors + std::map output_errors; + + bool complete = pwallet->SignTransactionOutput(mtx, nHashType, output_errors); + UniValue result(UniValue::VOBJ); + SignTransactionOutputResultToJSON(mtx, complete, output_errors, result); + return result; +}, + }; +} } // namespace wallet diff --git a/src/wallet/rpc/util.cpp b/src/wallet/rpc/util.cpp index 06ec7db1bc..5b8737aef8 100644 --- a/src/wallet/rpc/util.cpp +++ b/src/wallet/rpc/util.cpp @@ -98,6 +98,9 @@ void EnsureWalletIsUnlocked(const CWallet& wallet) if (wallet.IsLocked()) { throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Error: Please enter the wallet passphrase with walletpassphrase first."); } + if (wallet.m_wallet_unlock_staking_only) { + throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Error: Wallet is unlocked for staking only."); + } } WalletContext& EnsureWalletContext(const std::any& context) diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index b2d57ae895..e5f0827842 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -1274,6 +1274,7 @@ RPCHelpMan listlabels(); #ifdef ENABLE_EXTERNAL_SIGNER RPCHelpMan walletdisplayaddress(); #endif // ENABLE_EXTERNAL_SIGNER +RPCHelpMan createmultisig(); // backup RPCHelpMan dumpprivkey(); @@ -1318,6 +1319,9 @@ RPCHelpMan sendall(); RPCHelpMan walletprocesspsbt(); RPCHelpMan walletcreatefundedpsbt(); RPCHelpMan signrawtransactionwithwallet(); +RPCHelpMan sendmanywithdupes(); +RPCHelpMan splitutxosforaddress(); +RPCHelpMan signrawsendertransactionwithwallet(); // signmessage RPCHelpMan signmessage(); @@ -1385,13 +1389,16 @@ Span GetWalletRPCCommands() {"wallet", &rescanblockchain}, {"wallet", &send}, {"wallet", &sendmany}, + {"wallet", &sendmanywithdupes}, {"wallet", &sendtoaddress}, + {"wallet", &splitutxosforaddress}, {"wallet", &sethdseed}, {"wallet", &setlabel}, {"wallet", &settxfee}, {"wallet", &setwalletflag}, {"wallet", &signmessage}, {"wallet", &signrawtransactionwithwallet}, + {"wallet", &signrawsendertransactionwithwallet}, {"wallet", &simulaterawtransaction}, {"wallet", &sendall}, {"wallet", &unloadwallet}, @@ -1409,6 +1416,7 @@ Span GetWalletRPCCommands() {"wallet", &listsuperstakercustomvalues}, {"wallet", &listsuperstakervaluesforaddress}, {"wallet", &removesuperstakervaluesforaddress}, + {"util", &createmultisig}, }; return commands; }