From 40b6f7e0c7aaed2ad53a876ceab021bed35f481c Mon Sep 17 00:00:00 2001 From: random-zebra Date: Wed, 2 Dec 2020 18:36:29 +0100 Subject: [PATCH 1/5] BUG: fix spending of P2CS in RPC sendmany --- src/wallet/rpcwallet.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index e7473bc6c3b0..045e2eae8018 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2060,14 +2060,13 @@ UniValue sendmany(const JSONRPCRequest& request) vecSend.emplace_back(scriptPubKey, nAmount, false); } - isminefilter filter = ISMINE_SPENDABLE; - if ( request.params.size() > 5 && request.params[5].get_bool() ) - filter = filter | ISMINE_SPENDABLE_DELEGATED; + const bool fIncludeDelegated = request.params.size() > 5 && request.params[5].get_bool(); + isminefilter filter = ISMINE_SPENDABLE | (fIncludeDelegated ? ISMINE_SPENDABLE_DELEGATED : ISMINE_NO); EnsureWalletIsUnlocked(); // Check funds - if (totalAmount > pwalletMain->GetLegacyBalance(ISMINE_SPENDABLE, nMinDepth)) { + if (totalAmount > pwalletMain->GetLegacyBalance(filter, nMinDepth)) { throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Wallet has insufficient funds"); } @@ -2076,7 +2075,17 @@ UniValue sendmany(const JSONRPCRequest& request) CAmount nFeeRequired = 0; std::string strFailReason; int nChangePosInOut = -1; - bool fCreated = pwalletMain->CreateTransaction(vecSend, &wtx, keyChange, nFeeRequired, nChangePosInOut, strFailReason); + bool fCreated = pwalletMain->CreateTransaction(vecSend, + &wtx, + keyChange, + nFeeRequired, + nChangePosInOut, + strFailReason, + nullptr, // coinControl + ALL_COINS, // inputType + true, // sign + 0, // nFeePay + fIncludeDelegated); if (!fCreated) throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, strFailReason); const CWallet::CommitResult& res = pwalletMain->CommitTransaction(wtx, keyChange, g_connman.get()); From 267ae07a0d9d2c330eee5053664d82a72e09ff4d Mon Sep 17 00:00:00 2001 From: random-zebra Date: Wed, 2 Dec 2020 15:06:19 +0100 Subject: [PATCH 2/5] [RPC] Redirect sendmany to 'shieldedsendmany "from_transparent"' when at least one of the recipients is a shielded address --- src/wallet/rpcwallet.cpp | 145 +++++++++++++++++++++++++++------------ 1 file changed, 101 insertions(+), 44 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 045e2eae8018..56d29c4f32fc 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -10,6 +10,7 @@ #include "base58.h" #include "coincontrol.h" #include "core_io.h" +#include "destination_io.h" #include "init.h" #include "key_io.h" #include "masternode-sync.h" @@ -1988,55 +1989,21 @@ UniValue getunconfirmedbalance(const JSONRPCRequest& request) return ValueFromAmount(pwalletMain->GetUnconfirmedBalance()); } - -UniValue sendmany(const JSONRPCRequest& request) +/* + * Only used for t->t transactions (via sendmany RPC) + */ +static UniValue legacy_sendmany(const UniValue& sendTo, int nMinDepth, std::string comment, bool fIncludeDelegated) { - if (request.fHelp || request.params.size() < 2 || request.params.size() > 5) - throw std::runtime_error( - "sendmany \"\" {\"address\":amount,...} ( minconf \"comment\" includeDelegated )\n" - "\nSend multiple times. Amounts are double-precision floating point numbers.\n" - + HelpRequiringPassphrase() + "\n" - - "\nArguments:\n" - "1. \"dummy\" (string, required) Must be set to \"\" for backwards compatibility.\n" - "2. \"amounts\" (string, required) A json object with addresses and amounts\n" - " {\n" - " \"address\":amount (numeric) The pivx address is the key, the numeric amount in PIV is the value\n" - " ,...\n" - " }\n" - "3. minconf (numeric, optional, default=1) Only use the balance confirmed at least this many times.\n" - "4. \"comment\" (string, optional) A comment\n" - "5. includeDelegated (bool, optional, default=false) Also include balance delegated to cold stakers\n" - - "\nResult:\n" - "\"transactionid\" (string) The transaction id for the send. Only 1 transaction is created regardless of \n" - " the number of addresses.\n" - - "\nExamples:\n" - "\nSend two amounts to two different addresses:\n" + - HelpExampleCli("sendmany", "\"\" \"{\\\"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\\\":0.01,\\\"DAD3Y6ivr8nPQLT1NEPX84DxGCw9jz9Jvg\\\":0.02}\"") + - "\nSend two amounts to two different addresses setting the confirmation and comment:\n" + - HelpExampleCli("sendmany", "\"\" \"{\\\"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\\\":0.01,\\\"DAD3Y6ivr8nPQLT1NEPX84DxGCw9jz9Jvg\\\":0.02}\" 6 \"testing\"") + - "\nAs a json rpc call\n" + - HelpExampleRpc("sendmany", "\"\", \"{\\\"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\\\":0.01,\\\"DAD3Y6ivr8nPQLT1NEPX84DxGCw9jz9Jvg\\\":0.02}\", 6, \"testing\"") - ); - LOCK2(cs_main, pwalletMain->cs_wallet); if (!g_connman) throw JSONRPCError(RPC_CLIENT_P2P_DISABLED, "Error: Peer-to-peer functionality missing or disabled"); - 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(); - int nMinDepth = 1; - if (request.params.size() > 2) - nMinDepth = request.params[2].get_int(); + isminefilter filter = ISMINE_SPENDABLE | (fIncludeDelegated ? ISMINE_SPENDABLE_DELEGATED : ISMINE_NO); CWalletTx wtx; - if (request.params.size() > 3 && !request.params[3].isNull() && !request.params[3].get_str().empty()) - wtx.mapValue["comment"] = request.params[3].get_str(); + if (!comment.empty()) + wtx.mapValue["comment"] = comment; std::set setAddress; std::vector vecSend; @@ -2060,9 +2027,6 @@ UniValue sendmany(const JSONRPCRequest& request) vecSend.emplace_back(scriptPubKey, nAmount, false); } - const bool fIncludeDelegated = request.params.size() > 5 && request.params[5].get_bool(); - isminefilter filter = ISMINE_SPENDABLE | (fIncludeDelegated ? ISMINE_SPENDABLE_DELEGATED : ISMINE_NO); - EnsureWalletIsUnlocked(); // Check funds @@ -2095,6 +2059,99 @@ UniValue sendmany(const JSONRPCRequest& request) return wtx.GetHash().GetHex(); } +/* + * This function uses [legacy_sendmany] in the background. + * If any recipient is a shielded address, instead it uses [shieldedsendmany "from_transparent"]. + */ +UniValue sendmany(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() < 2 || request.params.size() > 5) + throw std::runtime_error( + "sendmany \"\" {\"address\":amount,...} ( minconf \"comment\" includeDelegated )\n" + "\nSend multiple times. Amounts are double-precision floating point numbers.\n" + + HelpRequiringPassphrase() + "\n" + + "\nArguments:\n" + "1. \"dummy\" (string, required) Must be set to \"\" for backwards compatibility.\n" + "2. \"amounts\" (string, required) A json object with addresses and amounts\n" + " {\n" + " \"address\":amount (numeric) The pivx address is the key, the numeric amount in PIV is the value\n" + " ,...\n" + " }\n" + "3. minconf (numeric, optional, default=1) Only use the balance confirmed at least this many times.\n" + "4. \"comment\" (string, optional) A comment\n" + "5. includeDelegated (bool, optional, default=false) Also include balance delegated to cold stakers\n" + + "\nResult:\n" + "\"transactionid\" (string) The transaction id for the send. Only 1 transaction is created regardless of \n" + " the number of addresses.\n" + + "\nExamples:\n" + "\nSend two amounts to two different addresses:\n" + + HelpExampleCli("sendmany", "\"\" \"{\\\"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\\\":0.01,\\\"DAD3Y6ivr8nPQLT1NEPX84DxGCw9jz9Jvg\\\":0.02}\"") + + "\nSend two amounts to two different addresses setting the confirmation and comment:\n" + + HelpExampleCli("sendmany", "\"\" \"{\\\"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\\\":0.01,\\\"DAD3Y6ivr8nPQLT1NEPX84DxGCw9jz9Jvg\\\":0.02}\" 6 \"testing\"") + + "\nAs a json rpc call\n" + + HelpExampleRpc("sendmany", "\"\", \"{\\\"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\\\":0.01,\\\"DAD3Y6ivr8nPQLT1NEPX84DxGCw9jz9Jvg\\\":0.02}\", 6, \"testing\"") + ); + + // Read Params + if (!request.params[0].isNull() && !request.params[0].get_str().empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Dummy value must be set to \"\""); + } + const UniValue sendTo = request.params[1].get_obj(); + const int nMinDepth = request.params.size() > 2 ? request.params[2].get_int() : 1; + const std::string comment = (request.params.size() > 3 && !request.params[3].isNull() && !request.params[3].get_str().empty()) ? + request.params[3].get_str() : ""; + const bool fIncludeDelegated = (request.params.size() > 5 && request.params[5].get_bool()); + + // Check if any recipient address is shielded + bool fShieldSend = false; + for (const std::string& key : sendTo.getKeys()) { + bool isStaking = false, isShielded = false; + const CWDestination& dest = Standard::DecodeDestination(key, isStaking, isShielded); + if (isShielded) { + fShieldSend = true; + break; + } + } + + if (fShieldSend) { + // convert params to 'shieldedsendmany' format + JSONRPCRequest req; + req.params = UniValue(UniValue::VARR); + req.params.push_back(UniValue("from_transparent")); + UniValue recipients(UniValue::VARR); + for (const std::string& key : sendTo.getKeys()) { + UniValue recipient(UniValue::VOBJ); + recipient.pushKV("address", key); + recipient.pushKV("amount", sendTo[key]); + recipients.push_back(recipient); + } + req.params.push_back(recipients); + req.params.push_back(nMinDepth); + + // send + SaplingOperation operation = CreateShieldedTransaction(req); + std::string txid; + auto res = operation.send(txid); + if (!res) + throw JSONRPCError(RPC_WALLET_ERROR, res.getError()); + + // add comment + if (!comment.empty()) { + const uint256 txHash(txid); + assert(pwalletMain->mapWallet.count(txHash)); + pwalletMain->mapWallet[txHash].mapValue["comment"] = comment; + } + + return txid; + } + + // All recipients are transparent: use Legacy sendmany t->t + return legacy_sendmany(sendTo, nMinDepth, comment, fIncludeDelegated); +} + // Defined in rpc/misc.cpp extern CScript _createmultisig_redeemScript(const UniValue& params); From 8ecf2225eae1647b2dd21fe3322a58300e51c61b Mon Sep 17 00:00:00 2001 From: random-zebra Date: Wed, 2 Dec 2020 16:27:02 +0100 Subject: [PATCH 3/5] [Wallet] Spend from delegations added to SaplingOperation --- src/sapling/sapling_operation.cpp | 9 ++++++++- src/sapling/sapling_operation.h | 3 ++- src/wallet/rpcwallet.cpp | 15 +++++++++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/sapling/sapling_operation.cpp b/src/sapling/sapling_operation.cpp index c79752008d98..cc1553398cd8 100644 --- a/src/sapling/sapling_operation.cpp +++ b/src/sapling/sapling_operation.cpp @@ -244,6 +244,13 @@ void SaplingOperation::setFromAddress(const libzcash::SaplingPaymentAddress& _pa fromAddress = FromAddress(_payment); } +SaplingOperation* SaplingOperation::setSelectTransparentCoins(const bool select, const bool _fIncludeDelegated) +{ + selectFromtaddrs = select; + if (selectFromtaddrs) fIncludeDelegated = _fIncludeDelegated; + return this; +}; + OperationResult SaplingOperation::loadUtxos(TxValues& txValues) { // If the user has selected coins to spend then, directly load them. @@ -267,7 +274,7 @@ OperationResult SaplingOperation::loadUtxos(TxValues& txValues) // No coin control selected, let's find the utxo by our own. std::set destinations; if (fromAddress.isFromTAddress()) destinations.insert(fromAddress.fromTaddr); - CWallet::AvailableCoinsFilter coinsFilter(false, + CWallet::AvailableCoinsFilter coinsFilter(fIncludeDelegated, false, ALL_COINS, true, diff --git a/src/sapling/sapling_operation.h b/src/sapling/sapling_operation.h index d72890369c43..9c8114dcb3c9 100644 --- a/src/sapling/sapling_operation.h +++ b/src/sapling/sapling_operation.h @@ -94,7 +94,7 @@ class SaplingOperation { void setFromAddress(const libzcash::SaplingPaymentAddress&); void clearTx() { txBuilder.Clear(); } // In case of no addressFrom filter selected, it will accept any utxo in the wallet as input. - SaplingOperation* setSelectTransparentCoins(const bool select) { selectFromtaddrs = select; return this; }; + SaplingOperation* setSelectTransparentCoins(const bool select, const bool _fIncludeDelegated = false); SaplingOperation* setSelectShieldedCoins(const bool select) { selectFromShield = select; return this; }; SaplingOperation* setRecipients(std::vector& vec) { recipients = std::move(vec); return this; }; SaplingOperation* setFee(CAmount _fee) { fee = _fee; return this; } @@ -111,6 +111,7 @@ class SaplingOperation { // In case of no addressFrom filter selected, it will accept any utxo in the wallet as input. bool selectFromtaddrs{false}; bool selectFromShield{false}; + bool fIncludeDelegated{false}; const CCoinControl* coinControl{nullptr}; std::vector recipients; std::vector transInputs; diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 56d29c4f32fc..77230cce9027 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -1460,12 +1460,15 @@ static SaplingOperation CreateShieldedTransaction(const JSONRPCRequest& request) SaplingOperation operation(txBuilder); // Param 0: source of funds. Can either be a valid address, sapling address, - // or the string "from_transparent"|"from_shielded" + // or the string "from_transparent"|"from_trans_cold"|"from_shielded" bool fromSapling = false; std::string sendFromStr = request.params[0].get_str(); if (sendFromStr == "from_transparent") { // send from any transparent address operation.setSelectTransparentCoins(true); + } else if (sendFromStr == "from_trans_cold") { + // send from any transparent address + delegations + operation.setSelectTransparentCoins(true, true); } else if (sendFromStr == "from_shielded") { // send from any shielded address operation.setSelectShieldedCoins(true); @@ -1617,6 +1620,8 @@ UniValue shieldedsendmany(const JSONRPCRequest& request) "1. \"fromaddress\" (string, required) The transparent addr or shielded addr to send the funds from.\n" " It can also be the string \"from_transparent\"|\"from_shielded\" to send the funds\n" " from any transparent|shielded address available.\n" + " Additionally, it can be the string \"from_trans_cold\" to select transparent funds,\n" + " possibly including delegated coins, if needed.\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 transparent addr or shielded addr\n" @@ -1658,6 +1663,8 @@ UniValue rawshieldedsendmany(const JSONRPCRequest& request) "1. \"fromaddress\" (string, required) The transparent addr or shielded addr to send the funds from.\n" " It can also be the string \"from_transparent\"|\"from_shielded\" to send the funds\n" " from any transparent|shielded address available.\n" + " Additionally, it can be the string \"from_trans_cold\" to select transparent funds,\n" + " possibly including delegated coins, if needed.\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 transparent addr or shielded addr\n" @@ -2120,7 +2127,11 @@ UniValue sendmany(const JSONRPCRequest& request) // convert params to 'shieldedsendmany' format JSONRPCRequest req; req.params = UniValue(UniValue::VARR); - req.params.push_back(UniValue("from_transparent")); + if (!fIncludeDelegated) { + req.params.push_back(UniValue("from_transparent")); + } else { + req.params.push_back(UniValue("from_trans_cold")); + } UniValue recipients(UniValue::VARR); for (const std::string& key : sendTo.getKeys()) { UniValue recipient(UniValue::VOBJ); From 13ddf937b9c47609017e05225cb532b851d12d02 Mon Sep 17 00:00:00 2001 From: random-zebra Date: Thu, 3 Dec 2020 01:20:46 +0100 Subject: [PATCH 4/5] RPC: Document shielded recipients in sendmany help --- src/wallet/rpcwallet.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 77230cce9027..18fc1b96dd1f 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2075,14 +2075,16 @@ UniValue sendmany(const JSONRPCRequest& request) if (request.fHelp || request.params.size() < 2 || request.params.size() > 5) throw std::runtime_error( "sendmany \"\" {\"address\":amount,...} ( minconf \"comment\" includeDelegated )\n" - "\nSend multiple times. Amounts are double-precision floating point numbers.\n" + "\nSend to multiple destinations. Recipients are transparent or shielded PIVX addresses.\n" + "\nAmounts are double-precision floating point numbers.\n" + HelpRequiringPassphrase() + "\n" "\nArguments:\n" "1. \"dummy\" (string, required) Must be set to \"\" for backwards compatibility.\n" "2. \"amounts\" (string, required) A json object with addresses and amounts\n" " {\n" - " \"address\":amount (numeric) The pivx address is the key, the numeric amount in PIV is the value\n" + " \"address\":amount (numeric) The pivx address (either transparent or shielded) is the key,\n" + " the numeric amount in PIV is the value\n" " ,...\n" " }\n" "3. minconf (numeric, optional, default=1) Only use the balance confirmed at least this many times.\n" @@ -2098,6 +2100,8 @@ UniValue sendmany(const JSONRPCRequest& request) HelpExampleCli("sendmany", "\"\" \"{\\\"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\\\":0.01,\\\"DAD3Y6ivr8nPQLT1NEPX84DxGCw9jz9Jvg\\\":0.02}\"") + "\nSend two amounts to two different addresses setting the confirmation and comment:\n" + HelpExampleCli("sendmany", "\"\" \"{\\\"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\\\":0.01,\\\"DAD3Y6ivr8nPQLT1NEPX84DxGCw9jz9Jvg\\\":0.02}\" 6 \"testing\"") + + "\nSend to shielded address:\n" + + HelpExampleCli("sendmany", "\"\" \"{\\\"ps1u87kylcmn28yclnx2uy0psnvuhs2xn608ukm6n2nshrpg2nzyu3n62ls8j77m9cgp40dx40evej\\\":10}\"") + "\nAs a json rpc call\n" + HelpExampleRpc("sendmany", "\"\", \"{\\\"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\\\":0.01,\\\"DAD3Y6ivr8nPQLT1NEPX84DxGCw9jz9Jvg\\\":0.02}\", 6, \"testing\"") ); From daa5d7f2f20570cb7a69755422d28d3277b46112 Mon Sep 17 00:00:00 2001 From: random-zebra Date: Thu, 3 Dec 2020 02:25:45 +0100 Subject: [PATCH 5/5] Test: add coverage for sendmany + shield address --- test/functional/sapling_wallet.py | 42 +++++++++++++++++++++++++++++++ test/functional/test_runner.py | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/test/functional/sapling_wallet.py b/test/functional/sapling_wallet.py index 930b02616018..3d683011e97a 100755 --- a/test/functional/sapling_wallet.py +++ b/test/functional/sapling_wallet.py @@ -10,6 +10,7 @@ assert_raises_rpc_error, connect_nodes, disconnect_nodes, + satoshi_round, sync_mempools, get_coinstake_address, wait_until, @@ -282,6 +283,47 @@ def run_test(self): # watch only balance assert_equal(self.nodes[3].getshieldedbalance("*", 1, True), Decimal('12.00')) + # Now shield some funds using sendmany + self.log.info("TX11: Shielding coins to multiple destinations with sendmany RPC...") + prev_balance = self.nodes[0].getbalance() + recipients8 = {saplingAddr0: Decimal('8'), saplingAddr1: Decimal('1'), saplingAddr2: Decimal('0.5')} + mytxid11 = self.nodes[0].sendmany("", recipients8) + self.check_tx_priority([mytxid11]) + self.log.info("Done. Checking details and balances...") + + # Decrypted transaction details should be correct + pt = self.nodes[0].viewshieldedtransaction(mytxid11) + fee = pt["fee"] + assert_equal(pt['txid'], mytxid11) + assert_equal(len(pt['spends']), 0) + assert_equal(len(pt['outputs']), 3) + found = [False] * 3 + for out in pt['outputs']: + assert_equal(pt['outputs'].index(out), out['output']) + if out['address'] == saplingAddr0: + assert_equal(out['outgoing'], False) + assert_equal(out['value'], Decimal('8')) + found[0] = True + elif out['address'] == saplingAddr1: + assert_equal(out['outgoing'], True) + assert_equal(out['value'], Decimal('1')) + found[1] = True + else: + assert_equal(out['address'], saplingAddr2) + assert_equal(out['outgoing'], False) + assert_equal(out['value'], Decimal('0.5')) + found[2] = True + assert_equal(found, [True] * 3) + + # Verify balance + self.nodes[2].generate(1) + self.sync_all() + assert_equal(self.nodes[0].getshieldedbalance(saplingAddr0), Decimal('19')) # 11 prev balance + 8 received + assert_equal(self.nodes[1].getshieldedbalance(saplingAddr1), Decimal('2')) # 1 prev balance + 1 received + assert_equal(self.nodes[0].getshieldedbalance(saplingAddr2), Decimal('2.5')) # 2 prev balance + 0.5 received + # Balance of node 0 is: prev_balance - 1 PIV (+fee) sent externally + 250 PIV matured coinbase + assert_equal(self.nodes[0].getbalance(), satoshi_round(prev_balance + Decimal('249') - Decimal(fee))) + self.log.info("All good.") if __name__ == '__main__': diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 95d62c097a9b..d470c5d4e877 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -146,8 +146,8 @@ SAPLING_SCRIPTS = [ # Longest test should go first, to favor running tests in parallel 'sapling_key_import_export.py', # ~ 378 sec + 'sapling_wallet.py', # ~ 350 sec 'sapling_wallet_anchorfork.py', # ~ 345 sec - 'sapling_wallet.py', # ~ 274 sec 'sapling_wallet_nullifiers.py', # ~ 190 sec 'sapling_wallet_listreceived.py', # ~ 157 sec 'sapling_changeaddresses.py', # ~ 151 sec