wallet: extract SelectAnyZaddrSource from z_sendmany

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) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 16:21:49 -05:00
parent c92e69a13f
commit 4a2e531cb6

View File

@@ -5107,63 +5107,12 @@ UniValue z_getoperationstatus_IMPL(const UniValue& params, bool fRemoveFinishedO
#define CTXIN_SPEND_DUST_SIZE 148 #define CTXIN_SPEND_DUST_SIZE 148
#define CTXOUT_REGULAR_SIZE 34 #define CTXOUT_REGULAR_SIZE 34
UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) // 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)
{ {
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() < 2 || params.size() > 5)
throw runtime_error(
"z_sendmany \"fromaddress\" [{\"address\":... ,\"amount\":...},...] ( minconf ) ( fee ) (opreturn)\n"
"\nSend multiple times. Amounts are decimal numbers with at most 8 digits of precision."
"\nChange generated from a taddr flows to a new taddr address, while change generated from a zaddr returns to itself."
"\nWhen sending coinbase UTXOs to a zaddr, change is not allowed. The entire value of the UTXO(s) must be consumed."
+ HelpRequiringPassphrase() + "\n"
"\nArguments:\n"
"1. \"fromaddress\" (string, required) The taddr or zaddr to send the funds from. Use 'z' to spend from any zaddr.\n"
"2. \"amounts\" (array, required) An array of json objects representing the amounts to send.\n"
" [{\n"
" \"address\":address (string, required) The address is a taddr or zaddr\n"
" \"amount\":amount (numeric, required) The amount to send this address\n"
" \"memo\":memo (string, optional) If the address is a zaddr, raw data represented in hexadecimal string format\n"
" }, ... ]\n"
"3. minconf (numeric, optional, default=1) Only use funds confirmed at least this many times.\n"
"4. fee (numeric, optional, default="
+ strprintf("%s", FormatMoney(ASYNC_RPC_OPERATION_DEFAULT_MINERS_FEE)) + ") The fee amount to attach to this transaction.\n"
"5. opreturn (string, optional) Hex encoded data for OP_RETURN. Or a utf8 string prefixed with 'utf8:' which will be automatically converted to hex\n"
"\nResult:\n"
"\"operationid\" (string) An operationid to pass to z_getoperationstatus to get the result of the operation.\n"
"\nExamples:\n"
+ HelpExampleCli("z_sendmany", "\"RD6GgnrMpPaTSMn8vai6yiGA7mN4QGPV\" '[{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 5.0}]'")
+ HelpExampleRpc("z_sendmany", "\"RD6GgnrMpPaTSMn8vai6yiGA7mN4QGPV\", [{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 5.0}]")
+ HelpExampleCli("z_sendmany", "\"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" '[{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 3.14}]'")
+ HelpExampleRpc("z_sendmany", "\"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\", [{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 3.14}]")
+ HelpExampleCli("z_sendmany", "\"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" '[{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 3.14}]' 1 0.0001 \"utf8: this will be converted to hex")
+ HelpExampleRpc("z_sendmany", "\"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" '[{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 3.14}]' 1 0.0001 \"utf8: this will be converted to hex")
);
LOCK2(cs_main, pwalletMain->cs_wallet);
// Guard against building shielded transactions before the chain is fully synced;
// sending while behind the tip can leak metadata usable for linkability analysis.
THROW_IF_SYNCING(HUSH_INSYNC);
// Check that the from address is valid.
auto fromaddress = params[0].get_str();
bool fromTaddr = false;
bool fromSapling = false;
uint32_t branchId = CurrentEpochBranchId(chainActive.Height(), Params().GetConsensus());
UniValue outputs = params[1].get_array();
if (outputs.size()==0)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, amounts array is empty.");
// TODO: implement special symbolic fromaddrs
// TODO: list of (zaddr,amount)
// "z" => spend from any zaddr
// "t" => spend from any taddr
// "*" => spend from any addr, zaddrs first
if(fromaddress == "z") {
// TODO: refactor this and z_getbalances to use common code // TODO: refactor this and z_getbalances to use common code
std::set<libzcash::PaymentAddress> zaddrs = {}; std::set<libzcash::PaymentAddress> zaddrs = {};
std::set<libzcash::SaplingPaymentAddress> saplingzaddrs = {}; std::set<libzcash::SaplingPaymentAddress> saplingzaddrs = {};
@@ -5234,11 +5183,70 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk)
auto nPotentials = vPotentialAddresses.size(); auto nPotentials = vPotentialAddresses.size();
if (nPotentials > 0) { if (nPotentials > 0) {
LogPrintf("%s: Selecting one of %lu potential source zaddrs\n", __func__, nPotentials); LogPrintf("%s: Selecting one of %lu potential source zaddrs\n", __func__, nPotentials);
fromaddress = vPotentialAddresses[ GetRandInt(nPotentials) ]; return vPotentialAddresses[ GetRandInt(nPotentials) ];
} else { }
// Automagic zaddr source selection failed, exit honorably // 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."); 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))
return NullUniValue;
if (fHelp || params.size() < 2 || params.size() > 5)
throw runtime_error(
"z_sendmany \"fromaddress\" [{\"address\":... ,\"amount\":...},...] ( minconf ) ( fee ) (opreturn)\n"
"\nSend multiple times. Amounts are decimal numbers with at most 8 digits of precision."
"\nChange generated from a taddr flows to a new taddr address, while change generated from a zaddr returns to itself."
"\nWhen sending coinbase UTXOs to a zaddr, change is not allowed. The entire value of the UTXO(s) must be consumed."
+ HelpRequiringPassphrase() + "\n"
"\nArguments:\n"
"1. \"fromaddress\" (string, required) The taddr or zaddr to send the funds from. Use 'z' to spend from any zaddr.\n"
"2. \"amounts\" (array, required) An array of json objects representing the amounts to send.\n"
" [{\n"
" \"address\":address (string, required) The address is a taddr or zaddr\n"
" \"amount\":amount (numeric, required) The amount to send this address\n"
" \"memo\":memo (string, optional) If the address is a zaddr, raw data represented in hexadecimal string format\n"
" }, ... ]\n"
"3. minconf (numeric, optional, default=1) Only use funds confirmed at least this many times.\n"
"4. fee (numeric, optional, default="
+ strprintf("%s", FormatMoney(ASYNC_RPC_OPERATION_DEFAULT_MINERS_FEE)) + ") The fee amount to attach to this transaction.\n"
"5. opreturn (string, optional) Hex encoded data for OP_RETURN. Or a utf8 string prefixed with 'utf8:' which will be automatically converted to hex\n"
"\nResult:\n"
"\"operationid\" (string) An operationid to pass to z_getoperationstatus to get the result of the operation.\n"
"\nExamples:\n"
+ HelpExampleCli("z_sendmany", "\"RD6GgnrMpPaTSMn8vai6yiGA7mN4QGPV\" '[{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 5.0}]'")
+ HelpExampleRpc("z_sendmany", "\"RD6GgnrMpPaTSMn8vai6yiGA7mN4QGPV\", [{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 5.0}]")
+ HelpExampleCli("z_sendmany", "\"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" '[{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 3.14}]'")
+ HelpExampleRpc("z_sendmany", "\"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\", [{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 3.14}]")
+ HelpExampleCli("z_sendmany", "\"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" '[{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 3.14}]' 1 0.0001 \"utf8: this will be converted to hex")
+ HelpExampleRpc("z_sendmany", "\"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" '[{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 3.14}]' 1 0.0001 \"utf8: this will be converted to hex")
);
LOCK2(cs_main, pwalletMain->cs_wallet);
// Guard against building shielded transactions before the chain is fully synced;
// sending while behind the tip can leak metadata usable for linkability analysis.
THROW_IF_SYNCING(HUSH_INSYNC);
// Check that the from address is valid.
auto fromaddress = params[0].get_str();
bool fromTaddr = false;
bool fromSapling = false;
uint32_t branchId = CurrentEpochBranchId(chainActive.Height(), Params().GetConsensus());
UniValue outputs = params[1].get_array();
if (outputs.size()==0)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, amounts array is empty.");
// TODO: implement special symbolic fromaddrs
// TODO: list of (zaddr,amount)
// "z" => spend from any zaddr
// "t" => spend from any taddr
// "*" => spend from any addr, zaddrs first
if(fromaddress == "z") {
fromaddress = SelectAnyZaddrSource(outputs, params);
} else { } else {
CTxDestination taddr = DecodeDestination(fromaddress); CTxDestination taddr = DecodeDestination(fromaddress);
fromTaddr = IsValidDestination(taddr); fromTaddr = IsValidDestination(taddr);