diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index d08e2d55d1d..2df2e077a65 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -130,6 +130,9 @@ static const CRPCConvertParam vRPCConvertParams[] = { "walletcreatefundedpsbt", 2, "locktime" }, { "walletcreatefundedpsbt", 3, "options" }, { "walletcreatefundedpsbt", 4, "bip32derivs" }, + { "walletdeniabilizecoin", 0, "inputs" }, + { "walletdeniabilizecoin", 1, "conf_target" }, + { "walletdeniabilizecoin", 2, "add_to_wallet" }, { "walletprocesspsbt", 1, "sign" }, { "walletprocesspsbt", 3, "bip32derivs" }, { "walletprocesspsbt", 4, "finalize" }, diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp index a5b1f594bfe..20e553427a2 100644 --- a/src/wallet/rpc/spend.cpp +++ b/src/wallet/rpc/spend.cpp @@ -1723,4 +1723,126 @@ RPCHelpMan walletcreatefundedpsbt() }, }; } + +// clang-format off +RPCHelpMan walletdeniabilizecoin() +{ + return RPCHelpMan{"walletdeniabilizecoin", + "\nDeniabilize one or more UTXOs that share the same address.\n", + { + {"inputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "Specify inputs (must share the same address). A JSON array of JSON objects", + { + {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", + { + {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, + {"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"}, + } + } + }, + }, + {"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks"}, + {"add_to_wallet", RPCArg::Type::BOOL, RPCArg::Default{true}, "When false, returns the serialized transaction without broadcasting or adding it to the wallet"}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_HEX, "txid", "The deniabilization transaction id."}, + {RPCResult::Type::STR_AMOUNT, "fee", "The fee used in the deniabilization transaction."}, + {RPCResult::Type::STR_HEX, "hex", /*optional=*/true, "If add_to_wallet is false, the hex-encoded raw transaction with signature(s)"}, + } + }, + RPCExamples{ + "\nDeniabilize a single UTXO\n" + + HelpExampleCli("walletdeniabilizecoin", "\"[{\"txid\":\"4c14d20709daef476854fe7ef75bdfcfd5a7636a431b4622ec9481f297e12e8c\", \"vout\": 0}]\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue + { + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return UniValue::VNULL; + + std::optional shared_address; + std::set inputs; + unsigned int deniabilization_cycles = UINT_MAX; + for (const UniValue& input : request.params[0].get_array().getValues()) { + uint256 txid = ParseHashO(input, "txid"); + + const UniValue& vout_v = input.find_value("vout"); + if (!vout_v.isNum()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, missing vout key"); + } + int nOutput = vout_v.getInt(); + if (nOutput < 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout cannot be negative"); + } + + COutPoint outpoint(txid, nOutput); + LOCK(pwallet->cs_wallet); + auto walletTx = pwallet->GetWalletTx(outpoint.hash); + if (!walletTx) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, txid not found in wallet."); + } + if (outpoint.n >= walletTx->tx->vout.size()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout is out of range"); + } + const auto& output = walletTx->tx->vout[outpoint.n]; + + isminetype mine = pwallet->IsMine(output); + if (mine == ISMINE_NO) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, transaction's output doesn't belong to this wallet."); + } + + bool spendable = (mine & ISMINE_SPENDABLE) != ISMINE_NO; + + CTxDestination address; + if (spendable && ExtractDestination(FindNonChangeParentOutput(*pwallet, outpoint).scriptPubKey, address)) { + if (!shared_address) { + shared_address = address; + } + else if (!(*shared_address == address)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, inputs must share the same address"); + } + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, inputs must be spendable and have a valid address"); + } + + inputs.emplace(outpoint); + auto cycles_res = CalculateDeniabilizationCycles(*pwallet, outpoint); + deniabilization_cycles = std::min(deniabilization_cycles, cycles_res.first); + } + + if (inputs.empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, inputs must not be empty"); + } + + unsigned int confirm_target = !request.params[1].isNull() ? request.params[1].getInt() : 6; + const bool add_to_wallet = !request.params[2].isNull() ? request.params[2].get_bool() : true; + + CTransactionRef tx; + CAmount tx_fee = 0; + { + bool sign = !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS); + bool insufficient_amount = false; + auto res = CreateDeniabilizationTransaction(*pwallet, inputs, confirm_target, deniabilization_cycles, sign, insufficient_amount); + if (!res) { + throw JSONRPCError(RPC_TRANSACTION_ERROR, ErrorString(res).original); + } + tx = res->tx; + tx_fee = res->fee; + } + + UniValue result(UniValue::VOBJ); + result.pushKV("txid", tx->GetHash().GetHex()); + if (add_to_wallet) { + pwallet->CommitTransaction(tx, {}, /*orderForm=*/{}); + } else { + std::string hex{EncodeHexTx(*tx)}; + result.pushKV("hex", hex); + } + result.pushKV("fee", ValueFromAmount(tx_fee)); + return result; + } + }; +} +// clang-format on + } // namespace wallet diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index a22862bfa94..2a7f3d2f454 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -869,6 +869,7 @@ RPCHelpMan send(); RPCHelpMan sendall(); RPCHelpMan walletprocesspsbt(); RPCHelpMan walletcreatefundedpsbt(); +RPCHelpMan walletdeniabilizecoin(); RPCHelpMan signrawtransactionwithwallet(); // signmessage @@ -956,6 +957,7 @@ Span GetWalletRPCCommands() {"wallet", &walletpassphrase}, {"wallet", &walletpassphrasechange}, {"wallet", &walletprocesspsbt}, + {"wallet", &walletdeniabilizecoin}, }; return commands; }