From a2f28f958d6aef87c27b31fbc60449b36f8d71b5 Mon Sep 17 00:00:00 2001 From: timemarkovqtum Date: Wed, 28 Feb 2024 16:05:12 +0100 Subject: [PATCH] Port rpc wallet --- src/wallet/rpc/addresses.cpp | 138 +++++++++- src/wallet/rpc/transactions.cpp | 132 +++++++-- src/wallet/rpc/wallet.cpp | 458 +++++++++++++++++++++++++++++++- 3 files changed, 684 insertions(+), 44 deletions(-) diff --git a/src/wallet/rpc/addresses.cpp b/src/wallet/rpc/addresses.cpp index 8df1bedef3..80ae25b80e 100644 --- a/src/wallet/rpc/addresses.cpp +++ b/src/wallet/rpc/addresses.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include @@ -19,7 +20,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", { @@ -27,7 +28,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", "") @@ -71,7 +72,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\"."}, @@ -120,7 +121,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, "", ""}, @@ -137,7 +138,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])}; @@ -167,7 +168,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"}, }}, @@ -217,16 +218,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."}, @@ -493,15 +494,15 @@ UniValue DescribeWalletAddress(const CWallet& wallet, const CTxDestination& dest 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."}, @@ -758,7 +759,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,"","", @@ -793,4 +794,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/transactions.cpp b/src/wallet/rpc/transactions.cpp index c34391e6e8..f394c939c8 100644 --- a/src/wallet/rpc/transactions.cpp +++ b/src/wallet/rpc/transactions.cpp @@ -20,7 +20,7 @@ static void WalletTxToJSON(const CWallet& wallet, const CWalletTx& wtx, UniValue interfaces::Chain& chain = wallet.chain(); int confirms = wallet.GetTxDepthInMainChain(wtx); entry.pushKV("confirmations", confirms); - if (wtx.IsCoinBase()) + if (wtx.IsCoinBase() || wtx.IsCoinStake()) entry.pushKV("generated", true); if (auto* conf = wtx.state()) { @@ -103,8 +103,8 @@ static UniValue ListReceived(const CWallet& wallet, const UniValue& params, cons continue; // Coinbase with less than 1 confirmation is no longer in the main chain - if ((wtx.IsCoinBase() && (nDepth < 1)) - || (wallet.IsTxImmatureCoinBase(wtx) && !include_immature_coinbase)) { + if (((wtx.IsCoinBase() || wtx.IsCoinStake()) && (nDepth < 1)) + || (wallet.IsTxImmature(wtx) && !include_immature_coinbase)) { continue; } @@ -326,6 +326,18 @@ static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nM CachedTxGetAmounts(wallet, wtx, listReceived, listSent, nFee, filter_ismine, include_change); + // Check if the coinstake transactions is mined by the wallet + if(wtx.IsCoinStake() && listSent.size() > 0 && listReceived.size() > 0) + { + // Condense all of the coinstake inputs and outputs into one output and compute its value + CAmount amount = CachedTxGetCredit(wallet, wtx, filter_ismine) - CachedTxGetDebit(wallet, wtx, filter_ismine); + COutputEntry output = *listReceived.begin(); + output.amount = amount; + listReceived.clear(); + listSent.clear(); + listReceived.push_back(output); + } + bool involvesWatchonly = CachedTxIsFromMe(wallet, wtx, ISMINE_WATCH_ONLY); // Sent @@ -371,11 +383,11 @@ static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nM } MaybePushAddress(entry, r.destination); PushParentDescriptors(wallet, wtx.tx->vout.at(r.vout).scriptPubKey, entry); - if (wtx.IsCoinBase()) + if (wtx.IsCoinBase() || wtx.IsCoinStake()) { if (wallet.GetTxDepthInMainChain(wtx) < 1) entry.pushKV("category", "orphan"); - else if (wallet.IsTxImmatureCoinBase(wtx)) + else if (wallet.IsTxImmature(wtx)) entry.pushKV("category", "immature"); else entry.pushKV("category", "generate"); @@ -450,10 +462,10 @@ RPCHelpMan listtransactions() {RPCResult::Type::STR, "address", /*optional=*/true, "The bitcoin address of the transaction (not returned if the output does not have an address, e.g. OP_RETURN null data)."}, {RPCResult::Type::STR, "category", "The transaction category.\n" "\"send\" Transactions sent.\n" - "\"receive\" Non-coinbase transactions received.\n" - "\"generate\" Coinbase transactions received with more than 100 confirmations.\n" - "\"immature\" Coinbase transactions received with 100 or fewer confirmations.\n" - "\"orphan\" Orphaned coinbase transactions received."}, + "\"receive\" Non-coinbase and non-coinstake transactions received.\n" + "\"generate\" Coinbase or coinstake transactions received with more than 100 confirmations.\n" + "\"immature\" Coinbase or coinstake transactions received with 100 or fewer confirmations.\n" + "\"orphan\" Orphaned coinbase or coinstake transactions received."}, {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT + ". This is negative for the 'send' category, and is positive\n" "for all other categories"}, {RPCResult::Type::STR, "label", /*optional=*/true, "A comment for the address/transaction, if any"}, @@ -561,13 +573,13 @@ RPCHelpMan listsinceblock() {RPCResult::Type::OBJ, "", "", Cat(Cat>( { {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction."}, - {RPCResult::Type::STR, "address", /*optional=*/true, "The bitcoin address of the transaction (not returned if the output does not have an address, e.g. OP_RETURN null data)."}, + {RPCResult::Type::STR, "address", /*optional=*/true, "The qtum address of the transaction (not returned if the output does not have an address, e.g. OP_RETURN null data)."}, {RPCResult::Type::STR, "category", "The transaction category.\n" "\"send\" Transactions sent.\n" - "\"receive\" Non-coinbase transactions received.\n" - "\"generate\" Coinbase transactions received with more than 100 confirmations.\n" - "\"immature\" Coinbase transactions received with 100 or fewer confirmations.\n" - "\"orphan\" Orphaned coinbase transactions received."}, + "\"receive\" Non-coinbase and non-coinstake transactions received.\n" + "\"generate\" Coinbase or coinstake transactions received with more than 100 confirmations.\n" + "\"immature\" Coinbase or coinstake transactions received with 100 or fewer confirmations.\n" + "\"orphan\" Orphaned coinbase or coinstake transactions received."}, {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT + ". This is negative for the 'send' category, and is positive\n" "for all other categories"}, {RPCResult::Type::NUM, "vout", "the vout value"}, @@ -693,6 +705,7 @@ RPCHelpMan gettransaction() "Whether to include watch-only addresses in balance calculation and details[]"}, {"verbose", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include a `decoded` field containing the decoded transaction (equivalent to RPC decoderawtransaction)"}, + {"waitconf", RPCArg::Type::NUM, RPCArg::Default{0}, "Wait for enough confirmations before returning."}, }, RPCResult{ RPCResult::Type::OBJ, "", "", Cat(Cat>( @@ -711,10 +724,10 @@ RPCHelpMan gettransaction() {RPCResult::Type::STR, "address", /*optional=*/true, "The bitcoin address involved in the transaction."}, {RPCResult::Type::STR, "category", "The transaction category.\n" "\"send\" Transactions sent.\n" - "\"receive\" Non-coinbase transactions received.\n" - "\"generate\" Coinbase transactions received with more than 100 confirmations.\n" - "\"immature\" Coinbase transactions received with 100 or fewer confirmations.\n" - "\"orphan\" Orphaned coinbase transactions received."}, + "\"receive\" Non-coinbase and non-coinstake transactions received.\n" + "\"generate\" Coinbase or coinstake transactions received with more than 100 confirmations.\n" + "\"immature\" Coinbase or coinstake transactions received with 100 or fewer confirmations.\n" + "\"orphan\" Orphaned coinbase or coinstake transactions received."}, {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT}, {RPCResult::Type::STR, "label", /*optional=*/true, "A comment for the address/transaction, if any"}, {RPCResult::Type::NUM, "vout", "the vout value"}, @@ -740,8 +753,11 @@ RPCHelpMan gettransaction() + HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\" false true") + HelpExampleRpc("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"") }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue + [&](const RPCHelpMan& self, const JSONRPCRequest& request_) -> UniValue { + // long-poll + JSONRPCRequest& request = (JSONRPCRequest&) request_; + const std::shared_ptr pwallet = GetWalletForJSONRPCRequest(request); if (!pwallet) return UniValue::VNULL; @@ -749,8 +765,6 @@ RPCHelpMan gettransaction() // the user could have gotten from another RPC command prior to now pwallet->BlockUntilSyncedToCurrentChain(); - LOCK(pwallet->cs_wallet); - uint256 hash(ParseHashV(request.params[0], "txid")); isminefilter filter = ISMINE_SPENDABLE; @@ -760,14 +774,68 @@ RPCHelpMan gettransaction() } bool verbose = request.params[2].isNull() ? false : request.params[2].get_bool(); + { + LOCK(pwallet->cs_wallet); + auto it = pwallet->mapWallet.find(hash); + if (it == pwallet->mapWallet.end()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id"); + } + } - UniValue entry(UniValue::VOBJ); - auto it = pwallet->mapWallet.find(hash); - if (it == pwallet->mapWallet.end()) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id"); + int waitconf = 0; + if(!request.params[3].isNull()) { + waitconf = request.params[3].getInt(); + } + + bool shouldWaitConf = !request.params[3].isNull() && waitconf > 0; + + { + LOCK(pwallet->cs_wallet); + auto it = pwallet->mapWallet.find(hash); + if (it == pwallet->mapWallet.end()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id"); + } + } + + const CWalletTx* _wtx = nullptr; + + // avoid long-poll if API caller does not specify waitconf + if (!shouldWaitConf) { + { + LOCK(pwallet->cs_wallet); + _wtx = &pwallet->mapWallet.at(hash); + } + + } else { + if(!request.httpreq) + throw JSONRPCError(RPC_INTERNAL_ERROR, "No HTTP connection. Waitconf is available from qtum-cli, not qtum-qt"); + + request.PollStart(); + while (true) { + { + LOCK(pwallet->cs_wallet); + _wtx = &pwallet->mapWallet.at(hash); + + if (pwallet->GetTxDepthInMainChain(*_wtx) >= waitconf) { + break; + } + } + + request.PollPing(); + + std::unique_lock lock(cs_blockchange); + cond_blockchange.wait_for(lock, std::chrono::milliseconds(300)); + + if (!request.PollAlive() || !IsRPCRunning()) { + return NullUniValue; + } + } } - const CWalletTx& wtx = it->second; + LOCK(pwallet->cs_wallet); + const CWalletTx& wtx = *_wtx; + + UniValue entry(UniValue::VOBJ); CAmount nCredit = CachedTxGetCredit(*pwallet, wtx, filter); CAmount nDebit = CachedTxGetDebit(*pwallet, wtx, filter); CAmount nNet = nCredit - nDebit; @@ -777,6 +845,18 @@ RPCHelpMan gettransaction() if (CachedTxIsFromMe(*pwallet, wtx, filter)) entry.pushKV("fee", ValueFromAmount(nFee)); + if(wtx.IsCoinStake()) + { + CAmount amount = nNet; + entry.pushKV("amount", ValueFromAmount(amount)); + } + else + { + entry.pushKV("amount", ValueFromAmount(nNet - nFee)); + if (CachedTxIsFromMe(*pwallet, wtx, filter)) + entry.pushKV("fee", ValueFromAmount(nFee)); + } + WalletTxToJSON(*pwallet, wtx, entry); UniValue details(UniValue::VARR); diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index c635093344..e5c6de91c1 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -14,6 +14,9 @@ #include #include #include +#include +#include +#include #include @@ -37,6 +40,432 @@ bool HaveKey(const SigningProvider& wallet, const CKey& key) return wallet.HaveKey(key.GetPubKey().GetID()) || wallet.HaveKey(key2.GetPubKey().GetID()); } +UniValue GetJsonSuperStakerConfig(const CSuperStakerInfo& superStaker) +{ + // Fill the json object with information + UniValue result(UniValue::VOBJ); + result.pushKV("address", EncodeDestination(PKHash(superStaker.stakerAddress))); + result.pushKV("customconfig", superStaker.fCustomConfig); + if(superStaker.fCustomConfig) + { + result.pushKV("stakingminfee", (int64_t)superStaker.nMinFee); + result.pushKV("stakingminutxovalue", FormatMoney(superStaker.nMinDelegateUtxo)); + UniValue addressList(UniValue::VARR); + for(uint160 address : superStaker.delegateAddressList) + { + addressList.push_back(EncodeDestination(PKHash(address))); + } + if(interfaces::AllowList == superStaker.nDelegateAddressType) + { + result.pushKV("allow", addressList); + } + if(interfaces::ExcludeList == superStaker.nDelegateAddressType) + { + result.pushKV("exclude", addressList); + } + } + + return result; +} + +static RPCHelpMan setsuperstakervaluesforaddress() +{ + return RPCHelpMan{"setsuperstakervaluesforaddress", + "\nList super staker configuration values for address." + + HELP_REQUIRING_PASSPHRASE, + { + {"params", RPCArg::Type::OBJ, RPCArg::Optional::NO, "", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The address of the staker"}, + {"stakingminutxovalue", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "The output number"}, + {"stakingminfee", RPCArg::Type::NUM, RPCArg::Optional::NO, "depends on the value of the 'replaceable' and 'locktime' arguments"}, + {"allow", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "A json array with allow delegate addresses.", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The delegate address"}, + }, + }, + {"exclude", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "A json array with exclude delegate addresses.", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The delegate address"}, + }, + }, + }, + }, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR, "address", "Address of the staker."}, + {RPCResult::Type::BOOL, "customconfig", "Custom configuration exist."}, + {RPCResult::Type::NUM, "stakingminfee", true, "Minimum fee for delegate."}, + {RPCResult::Type::STR, "stakingminutxovalue", true, "Minimum UTXO value for delegate."}, + {RPCResult::Type::ARR, "allow", true, "List of allowed delegate addresses.", + { + {RPCResult::Type::STR, "address", "The delegate address"}, + }, + }, + {RPCResult::Type::ARR, "exclude", true, "List of excluded delegate addresses.", + { + {RPCResult::Type::STR, "address", "The delegate address"}, + }, + } + } + }, + RPCExamples{ + HelpExampleCli("setsuperstakervaluesforaddress", "\"{\\\"address\\\":\\\"QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\\\",\\\"stakingminutxovalue\\\": \\\"100\\\",\\\"stakingminfee\\\": 10}\"") + + HelpExampleCli("setsuperstakervaluesforaddress", "\"{\\\"address\\\":\\\"QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\\\",\\\"stakingminutxovalue\\\": \\\"100\\\",\\\"stakingminfee\\\": 10,\\\"allow\\\":[\\\"QD1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\\\"]}\"") + + HelpExampleCli("setsuperstakervaluesforaddress", "\"{\\\"address\\\":\\\"QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\\\",\\\"stakingminutxovalue\\\": \\\"100\\\",\\\"stakingminfee\\\": 10,\\\"exclude\\\":[\\\"QD1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\\\"]}\"") + + HelpExampleRpc("setsuperstakervaluesforaddress", "\"{\\\"address\\\":\\\"QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\\\",\\\"stakingminutxovalue\\\": \\\"100\\\",\\\"stakingminfee\\\": 10}\"") + + HelpExampleRpc("setsuperstakervaluesforaddress", "\"{\\\"address\\\":\\\"QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\\\",\\\"stakingminutxovalue\\\": \\\"100\\\",\\\"stakingminfee\\\": 10,\\\"allow\\\":[\\\"QD1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\\\"]}\"") + + HelpExampleRpc("setsuperstakervaluesforaddress", "\"{\\\"address\\\":\\\"QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\\\",\\\"stakingminutxovalue\\\": \\\"100\\\",\\\"stakingminfee\\\": 10,\\\"exclude\\\":[\\\"QD1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\\\"]}\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return UniValue::VNULL; + + LOCK(pwallet->cs_wallet); + + // Get params for the super staker + UniValue params(UniValue::VOBJ); + params = request.params[0].get_obj(); + + // Parse the super staker address + if(!params.exists("address")) + throw JSONRPCError(RPC_TYPE_ERROR, "The super staker address doesn't exist"); + CTxDestination destStaker = DecodeDestination(params["address"].get_str()); + if (!std::holds_alternative(destStaker)) { + throw JSONRPCError(RPC_TYPE_ERROR, "Invalid address for staker. Only P2PK and P2PKH allowed"); + } + PKHash pkhStaker = std::get(destStaker); + + // Parse the staking min utxo value + if(!params.exists("stakingminutxovalue")) + throw JSONRPCError(RPC_TYPE_ERROR, "The staking min utxo value doesn't exist"); + CAmount nMinUtxoValue = AmountFromValue(params["stakingminutxovalue"]); + if (nMinUtxoValue < 0) + throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount for staking min utxo value"); + + // Parse the staking min fee + if(!params.exists("stakingminfee")) + throw JSONRPCError(RPC_TYPE_ERROR, "The staking min fee doesn't exist"); + CAmount nMinFee = params["stakingminfee"].getInt(); + if (nMinFee < 0 || nMinFee > 100) + throw JSONRPCError(RPC_TYPE_ERROR, "Invalid value for staking min fee"); + + // Parse the delegation address lists + if(params.exists("allow") && params.exists("exclude")) + throw JSONRPCError(RPC_TYPE_ERROR, "The delegation address lists can be empty, or have either allow list or exclude list"); + + // Parse the delegation address lists + int nDelegateAddressType = interfaces::AcceptAll; + std::vector addressList; + if(params.exists("allow")) + { + addressList = params["allow"].get_array().getValues(); + nDelegateAddressType = interfaces::AllowList; + } + else if(params.exists("exclude")) + { + addressList = params["exclude"].get_array().getValues(); + nDelegateAddressType = interfaces::ExcludeList; + } + std::vector delegateAddressList; + for(UniValue address : addressList) + { + CTxDestination destAddress = DecodeDestination(address.get_str()); + if (!std::holds_alternative(destAddress)) { + throw JSONRPCError(RPC_TYPE_ERROR, "Invalid address for delegate in allow list or exclude list. Only P2PK and P2PKH allowed"); + } + PKHash pkhAddress = std::get(destAddress); + delegateAddressList.push_back(uint160(pkhAddress)); + } + + // Search for super staker + CSuperStakerInfo superStaker; + bool found = false; + for(auto item : pwallet->mapSuperStaker) + { + if(PKHash(item.second.stakerAddress) == pkhStaker) + { + superStaker = item.second; + found = true; + break; + } + } + + if(found) + { + // Set custom configuration + superStaker.fCustomConfig = true; + superStaker.nMinFee = nMinFee; + superStaker.nMinDelegateUtxo = nMinUtxoValue; + superStaker.nDelegateAddressType = nDelegateAddressType; + superStaker.delegateAddressList = delegateAddressList; + + // Update super staker data + if(!pwallet->AddSuperStakerEntry(superStaker)) + throw JSONRPCError(RPC_TYPE_ERROR, "Failed to update the super staker"); + } + else + { + throw JSONRPCError(RPC_TYPE_ERROR, "Failed to find the super staker"); + } + + return GetJsonSuperStakerConfig(superStaker); +}, + }; +} + +static RPCHelpMan listsuperstakercustomvalues() +{ + return RPCHelpMan{"listsuperstakercustomvalues", + "\nList custom super staker configurations values." + + HELP_REQUIRING_PASSPHRASE, + {}, + RPCResult{ + RPCResult::Type::ARR, "", "", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR, "address", "Address of the staker."}, + {RPCResult::Type::BOOL, "customconfig", "Custom configuration exist."}, + {RPCResult::Type::NUM, "stakingminfee", true, "Minimum fee for delegate."}, + {RPCResult::Type::STR, "stakingminutxovalue", true, "Minimum UTXO value for delegate."}, + {RPCResult::Type::ARR, "allow", true, "List of allowed delegate addresses.", + { + {RPCResult::Type::STR, "address", "The delegate address"}, + }, + }, + {RPCResult::Type::ARR, "exclude", true, "List of excluded delegate addresses.", + { + {RPCResult::Type::STR, "address", "The delegate address"}, + }, + } + }} + }, + }, + RPCExamples{ + HelpExampleCli("listsuperstakercustomvalues", "") + + HelpExampleRpc("listsuperstakercustomvalues", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return UniValue::VNULL; + + LOCK(pwallet->cs_wallet); + + // Search for super stakers + UniValue result(UniValue::VARR); + for(auto item : pwallet->mapSuperStaker) + { + CSuperStakerInfo superStaker = item.second; + if(superStaker.fCustomConfig) + { + result.push_back(GetJsonSuperStakerConfig(superStaker)); + } + } + + return result; +}, + }; +} + +static RPCHelpMan listsuperstakervaluesforaddress() +{ + return RPCHelpMan{"listsuperstakervaluesforaddress", + "\nList super staker configuration values for address." + + HELP_REQUIRING_PASSPHRASE, + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The super staker Qtum address."}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR, "address", "Address of the staker."}, + {RPCResult::Type::BOOL, "customconfig", "Custom configuration exist."}, + {RPCResult::Type::NUM, "stakingminfee", true, "Minimum fee for delegate."}, + {RPCResult::Type::STR, "stakingminutxovalue", true, "Minimum UTXO value for delegate."}, + {RPCResult::Type::ARR, "allow", true, "List of allowed delegate addresses.", + { + {RPCResult::Type::STR, "address", "The delegate address"}, + }, + }, + {RPCResult::Type::ARR, "exclude", true, "List of excluded delegate addresses.", + { + {RPCResult::Type::STR, "address", "The delegate address"}, + }, + } + } + }, + RPCExamples{ + HelpExampleCli("listsuperstakervaluesforaddress", "QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd") + + HelpExampleRpc("listsuperstakervaluesforaddress", "QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return UniValue::VNULL; + + LOCK(pwallet->cs_wallet); + + // Parse the super staker address + CTxDestination destStaker = DecodeDestination(request.params[0].get_str()); + if (!std::holds_alternative(destStaker)) { + throw JSONRPCError(RPC_TYPE_ERROR, "Invalid address for staker. Only P2PK and P2PKH allowed"); + } + PKHash pkhStaker = std::get(destStaker); + + // Search for super staker + CSuperStakerInfo superStaker; + bool found = false; + for(auto item : pwallet->mapSuperStaker) + { + if(PKHash(item.second.stakerAddress) == pkhStaker) + { + superStaker = item.second; + found = true; + break; + } + } + + if(!found) + { + throw JSONRPCError(RPC_TYPE_ERROR, "Failed to find the super staker"); + } + + return GetJsonSuperStakerConfig(superStaker); +}, + }; +} + +static RPCHelpMan removesuperstakervaluesforaddress() +{ + return RPCHelpMan{"removesuperstakervaluesforaddress", + "\nRemove super staker configuration values for address." + + HELP_REQUIRING_PASSPHRASE, + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The super staker Qtum address."}, + }, + RPCResult{RPCResult::Type::NONE, "", ""}, + RPCExamples{ + HelpExampleCli("removesuperstakervaluesforaddress", "QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd") + + HelpExampleRpc("removesuperstakervaluesforaddress", "QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return UniValue::VNULL; + + LOCK(pwallet->cs_wallet); + + // Parse the super staker address + CTxDestination destStaker = DecodeDestination(request.params[0].get_str()); + if (!std::holds_alternative(destStaker)) { + throw JSONRPCError(RPC_TYPE_ERROR, "Invalid address for staker. Only P2PK and P2PKH allowed"); + } + PKHash pkhStaker = std::get(destStaker); + + // Search for super staker + CSuperStakerInfo superStaker; + bool found = false; + for(auto item : pwallet->mapSuperStaker) + { + if(PKHash(item.second.stakerAddress) == pkhStaker && + item.second.fCustomConfig) + { + superStaker = item.second; + found = true; + break; + } + } + + if(found) + { + // Remove custom configuration + superStaker.fCustomConfig = false; + superStaker.nMinFee = 0; + superStaker.nMinDelegateUtxo = 0; + superStaker.nDelegateAddressType = 0; + superStaker.delegateAddressList.clear(); + + // Update super staker data + if(!pwallet->AddSuperStakerEntry(superStaker)) + throw JSONRPCError(RPC_TYPE_ERROR, "Failed to update the super staker"); + } + else + { + throw JSONRPCError(RPC_TYPE_ERROR, "Failed to find the super staker"); + } + + return UniValue::VNULL; +}, + }; +} + +static RPCHelpMan reservebalance() +{ + return RPCHelpMan{"reservebalance", + "\nSet reserve amount not participating in network protection." + "\nIf no parameters provided current setting is printed.\n", + { + {"reserve", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED,"is true or false to turn balance reserve on or off."}, + {"amount", RPCArg::Type::AMOUNT, RPCArg::Optional::OMITTED, "is a real and rounded to cent."}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::BOOL, "reserve", "Balance reserve on or off"}, + {RPCResult::Type::STR_AMOUNT, "amount", "Amount reserve rounded to cent"} + } + }, + RPCExamples{ + "\nSet reserve balance to 100\n" + + HelpExampleCli("reservebalance", "true 100") + + "\nSet reserve balance to 0\n" + + HelpExampleCli("reservebalance", "false") + + "\nGet reserve balance\n" + + HelpExampleCli("reservebalance", "") }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return UniValue::VNULL; + + if (request.params.size() > 0) + { + bool fReserve = request.params[0].get_bool(); + if (fReserve) + { + if (request.params.size() == 1) + throw std::runtime_error("must provide amount to reserve balance.\n"); + int64_t nAmount = AmountFromValue(request.params[1]); + nAmount = (nAmount / CENT) * CENT; // round to cent + if (nAmount < 0) + throw std::runtime_error("amount cannot be negative.\n"); + pwallet->m_reserve_balance = nAmount; + } + else + { + if (request.params.size() > 1) + throw std::runtime_error("cannot specify amount to turn off reserve.\n"); + pwallet->m_reserve_balance = 0; + } + } + + UniValue result(UniValue::VOBJ); + result.pushKV("reserve", (pwallet->m_reserve_balance > 0)); + result.pushKV("amount", ValueFromAmount(pwallet->m_reserve_balance)); + return result; +}, + }; +} + static RPCHelpMan getwalletinfo() { return RPCHelpMan{"getwalletinfo", @@ -50,6 +479,7 @@ static RPCHelpMan getwalletinfo() {RPCResult::Type::NUM, "walletversion", "the wallet version"}, {RPCResult::Type::STR, "format", "the database format (bdb or sqlite)"}, {RPCResult::Type::STR_AMOUNT, "balance", "DEPRECATED. Identical to getbalances().mine.trusted"}, + {RPCResult::Type::STR_AMOUNT, "stake", "DEPRECATED. Identical to getbalances().mine.stake"}, {RPCResult::Type::STR_AMOUNT, "unconfirmed_balance", "DEPRECATED. Identical to getbalances().mine.untrusted_pending"}, {RPCResult::Type::STR_AMOUNT, "immature_balance", "DEPRECATED. Identical to getbalances().mine.immature"}, {RPCResult::Type::NUM, "txcount", "the total number of transactions in the wallet"}, @@ -92,12 +522,25 @@ static RPCHelpMan getwalletinfo() size_t kpExternalSize = pwallet->KeypoolCountExternalKeys(); const auto bal = GetBalance(*pwallet); + CAmount balance = bal.m_mine_trusted; + CAmount stake = bal.m_mine_stake; + CAmount unconfirmedBalance = bal.m_mine_untrusted_pending; + CAmount immatureBalance = bal.m_mine_immature; + bool privateKeysEnabled = !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS); + if(!privateKeysEnabled) + { + balance += bal.m_watchonly_trusted; + stake += bal.m_watchonly_stake; + unconfirmedBalance += bal.m_watchonly_untrusted_pending; + immatureBalance += bal.m_watchonly_immature; + } obj.pushKV("walletname", pwallet->GetName()); obj.pushKV("walletversion", pwallet->GetVersion()); obj.pushKV("format", pwallet->GetDatabase().Format()); - obj.pushKV("balance", ValueFromAmount(bal.m_mine_trusted)); - obj.pushKV("unconfirmed_balance", ValueFromAmount(bal.m_mine_untrusted_pending)); - obj.pushKV("immature_balance", ValueFromAmount(bal.m_mine_immature)); + obj.pushKV("balance", ValueFromAmount(balance)); + obj.pushKV("stake", ValueFromAmount(stake)); + obj.pushKV("unconfirmed_balance", ValueFromAmount(unconfirmedBalance)); + obj.pushKV("immature_balance", ValueFromAmount(immatureBalance)); obj.pushKV("txcount", (int)pwallet->mapWallet.size()); const auto kp_oldest = pwallet->GetOldestKeyPoolTime(); if (kp_oldest.has_value()) { @@ -120,7 +563,7 @@ static RPCHelpMan getwalletinfo() obj.pushKV("unlocked_until", pwallet->nRelockTime); } obj.pushKV("paytxfee", ValueFromAmount(pwallet->m_pay_tx_fee.GetFeePerK())); - obj.pushKV("private_keys_enabled", !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)); + obj.pushKV("private_keys_enabled", privateKeysEnabled); obj.pushKV("avoid_reuse", pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)); if (pwallet->IsScanning()) { UniValue scanning(UniValue::VOBJ); @@ -215,7 +658,7 @@ static RPCHelpMan loadwallet() { return RPCHelpMan{"loadwallet", "\nLoads a wallet from a wallet file or directory." - "\nNote that all wallet command-line options used when starting bitcoind will be" + "\nNote that all wallet command-line options used when starting qtumd will be" "\napplied to the new wallet.\n", { {"filename", RPCArg::Type::STR, RPCArg::Optional::NO, "The wallet directory or .dat file."}, @@ -957,6 +1400,11 @@ Span GetWalletRPCCommands() {"wallet", &walletpassphrase}, {"wallet", &walletpassphrasechange}, {"wallet", &walletprocesspsbt}, + {"wallet", &reservebalance, }, + {"wallet", &setsuperstakervaluesforaddress}, + {"wallet", &listsuperstakercustomvalues}, + {"wallet", &listsuperstakervaluesforaddress}, + {"wallet", &removesuperstakervaluesforaddress}, }; return commands; }