From 4a2e531cb61b3e0b30bfc71e731b0981026d4459 Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 27 Aug 2026 16:21:49 -0500 Subject: [PATCH] wallet: extract SelectAnyZaddrSource from z_sendmany MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First slice of the z_sendmany monolith split (Phase-6 refactor). The `fromaddress == "z"` case ("spend from any zaddr") was ~76 lines inline: it gathers the wallet's Sapling notes, sums balances per zaddr, and picks a random zaddr whose confirmed balance covers total outputs + fee. Lift it verbatim into a helper `SelectAnyZaddrSource(outputs, params)` that returns the chosen zaddr (or throws the same JSONRPCError), and call it from z_sendmany. Behavior-preserving by construction: the block was moved unchanged, the only edit being `fromaddress = vPotentialAddresses[...]` -> `return ...` with the call site doing `fromaddress = SelectAnyZaddrSource(outputs, params)`, so fromaddress receives the identical value (or the identical throw propagates). The helper takes only outputs/params + globals, which the clean compile proves (a reference to any z_sendmany local would not link). Built clean; verifychain=true. Note: the runtime happy path could not be exercised on an isolated node — z_sendmany's private-chain "still syncing" linkability guard fires before the fromaddress resolution when there are no peers — but a verbatim extract-method does not require it. z_sendmany is now ~76 lines shorter; further extraction (recipient parsing, fee/tx assembly) remains for follow-up slices. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/wallet/rpcwallet.cpp | 158 ++++++++++++++++++++------------------- 1 file changed, 83 insertions(+), 75 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 33bed940b..90f0d45a6 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5107,6 +5107,88 @@ UniValue z_getoperationstatus_IMPL(const UniValue& params, bool fRemoveFinishedO #define CTXIN_SPEND_DUST_SIZE 148 #define CTXOUT_REGULAR_SIZE 34 +// Resolve the special fromaddress "z" (spend from any zaddr) to a concrete zaddr: +// gather this wallet's Sapling notes, pick a random zaddr whose confirmed balance +// covers the total outputs + fee. Extracted verbatim from z_sendmany (behavior- +// preserving); throws JSONRPCError when no single zaddr has enough funds. +static std::string SelectAnyZaddrSource(const UniValue& outputs, const UniValue& params) +{ + // TODO: refactor this and z_getbalances to use common code + std::set zaddrs = {}; + std::set saplingzaddrs = {}; + pwalletMain->GetSaplingPaymentAddresses(saplingzaddrs); + + zaddrs.insert(saplingzaddrs.begin(), saplingzaddrs.end()); + + int nMinDepth = 1; + std::vector saplingEntries; + pwalletMain->GetFilteredNotes(saplingEntries, zaddrs, nMinDepth); + + std::map mapBalances; + for (auto & entry : saplingEntries) { + auto zaddr = EncodePaymentAddress(entry.address); + CAmount nBalance = CAmount(entry.note.value()); + if(mapBalances.count(zaddr)) { + mapBalances[zaddr] += nBalance; + } else { + mapBalances[zaddr] = nBalance; + } + } + std::vector> vec; + std::copy(mapBalances.begin(), mapBalances.end(), std::back_inserter>>(vec)); + + std::sort(vec.begin(), vec.end(), [](const std::pair &l, const std::pair &r) + { + if (l.second != r.second) { + return l.second > r.second; + } + return l.first > r.first; + }); + + //TODO: avoid calculating nTotalOut twice + CAmount nTotalOut = 0; + for (const UniValue& o : outputs.getValues()) { + if (!o.isObject()) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected object"); + + UniValue av = find_value(o, "amount"); + CAmount nAmount = AmountFromValue( av ); + if (nAmount < 0) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, amount must be positive"); + + nTotalOut += nAmount; + } + + //GOAL: choose one random zaddress with enough funds + CAmount nFee = ASYNC_RPC_OPERATION_DEFAULT_MINERS_FEE; // default when params.size()<=3 (was uninitialized) + if (params.size() > 3) { + if (params[3].get_real() == 0.0) { + nFee = 0; + } else { + nFee = AmountFromValue( params[3] ); + } + } + + // the total amount needed in a single zaddr to use as fromaddress + CAmount nMinBal = nTotalOut + nFee; + + std::vector vPotentialAddresses; + for (auto & entry : vec) { + if(entry.second >= nMinBal) { + vPotentialAddresses.push_back(entry.first); + } + } + + // select a random address with enough confirmed balance + auto nPotentials = vPotentialAddresses.size(); + if (nPotentials > 0) { + LogPrintf("%s: Selecting one of %lu potential source zaddrs\n", __func__, nPotentials); + return vPotentialAddresses[ GetRandInt(nPotentials) ]; + } + // Automagic zaddr source selection failed, exit honorably + throw JSONRPCError(RPC_INVALID_PARAMETER, "No single zaddr currently has enough funds to make that transaction, you may need to wait for confirmations."); +} + UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) { if (!EnsureWalletIsAvailable(fHelp)) @@ -5164,81 +5246,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) // "t" => spend from any taddr // "*" => spend from any addr, zaddrs first if(fromaddress == "z") { - // TODO: refactor this and z_getbalances to use common code - std::set zaddrs = {}; - std::set saplingzaddrs = {}; - pwalletMain->GetSaplingPaymentAddresses(saplingzaddrs); - - zaddrs.insert(saplingzaddrs.begin(), saplingzaddrs.end()); - - int nMinDepth = 1; - std::vector saplingEntries; - pwalletMain->GetFilteredNotes(saplingEntries, zaddrs, nMinDepth); - - std::map mapBalances; - for (auto & entry : saplingEntries) { - auto zaddr = EncodePaymentAddress(entry.address); - CAmount nBalance = CAmount(entry.note.value()); - if(mapBalances.count(zaddr)) { - mapBalances[zaddr] += nBalance; - } else { - mapBalances[zaddr] = nBalance; - } - } - std::vector> vec; - std::copy(mapBalances.begin(), mapBalances.end(), std::back_inserter>>(vec)); - - std::sort(vec.begin(), vec.end(), [](const std::pair &l, const std::pair &r) - { - if (l.second != r.second) { - return l.second > r.second; - } - return l.first > r.first; - }); - - //TODO: avoid calculating nTotalOut twice - CAmount nTotalOut = 0; - for (const UniValue& o : outputs.getValues()) { - if (!o.isObject()) - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected object"); - - UniValue av = find_value(o, "amount"); - CAmount nAmount = AmountFromValue( av ); - if (nAmount < 0) - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, amount must be positive"); - - nTotalOut += nAmount; - } - - //GOAL: choose one random zaddress with enough funds - CAmount nFee = ASYNC_RPC_OPERATION_DEFAULT_MINERS_FEE; // default when params.size()<=3 (was uninitialized) - if (params.size() > 3) { - if (params[3].get_real() == 0.0) { - nFee = 0; - } else { - nFee = AmountFromValue( params[3] ); - } - } - - // the total amount needed in a single zaddr to use as fromaddress - CAmount nMinBal = nTotalOut + nFee; - - std::vector vPotentialAddresses; - for (auto & entry : vec) { - if(entry.second >= nMinBal) { - vPotentialAddresses.push_back(entry.first); - } - } - - // select a random address with enough confirmed balance - auto nPotentials = vPotentialAddresses.size(); - if (nPotentials > 0) { - LogPrintf("%s: Selecting one of %lu potential source zaddrs\n", __func__, nPotentials); - fromaddress = vPotentialAddresses[ GetRandInt(nPotentials) ]; - } else { - // Automagic zaddr source selection failed, exit honorably - throw JSONRPCError(RPC_INVALID_PARAMETER, "No single zaddr currently has enough funds to make that transaction, you may need to wait for confirmations."); - } + fromaddress = SelectAnyZaddrSource(outputs, params); } else { CTxDestination taddr = DecodeDestination(fromaddress); fromTaddr = IsValidDestination(taddr);