Merge pull request #5500 from str4d/ua-wallet-balance-rpcs

Implement balance RPC methods that support Unified Addresses
This commit is contained in:
str4d 2022-01-27 20:11:38 +00:00 committed by GitHub
commit 992a47103d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 173 additions and 13 deletions

View File

@ -4,6 +4,7 @@
# file COPYING or https://www.opensource.org/licenses/mit-license.php .
from test_framework.authproxy import JSONRPCException
from test_framework.mininode import COIN
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
@ -27,6 +28,34 @@ class WalletAccountsTest(BitcoinTestFramework):
actual = self.nodes[0].z_listunifiedreceivers(ua)
assert_equal(set(expected), set(actual))
# Check we only have balances in the expected pools.
# Remember that empty pools are omitted from the output.
def check_account_balance(self, account, expected, minconf=None):
if minconf is None:
actual = self.nodes[0].z_getbalanceforaccount(account)
else:
actual = self.nodes[0].z_getbalanceforaccount(account, minconf)
assert_equal(set(expected), set(actual['pools']))
for pool in expected:
assert_equal(expected[pool] * COIN, actual['pools'][pool]['valueZat'])
assert_equal(actual['minimum_confirmations'], 1 if minconf is None else minconf)
# Check we only have balances in the expected pools.
# Remember that empty pools are omitted from the output.
def check_address_balance(self, address, expected, minconf=None):
if minconf is None:
actual = self.nodes[0].z_getbalanceforaddress(address)
else:
actual = self.nodes[0].z_getbalanceforaddress(address, minconf)
assert_equal(set(expected), set(actual['pools']))
for pool in expected:
assert_equal(expected[pool] * COIN, actual['pools'][pool]['valueZat'])
assert_equal(actual['minimum_confirmations'], 1 if minconf is None else minconf)
def check_balance(self, account, address, expected, minconf=None):
self.check_account_balance(account, expected, minconf)
self.check_address_balance(address, expected, minconf)
def run_test(self):
# With a new wallet, the first account will be 0.
account0 = self.nodes[0].z_getnewaccount()
@ -65,6 +94,10 @@ class WalletAccountsTest(BitcoinTestFramework):
self.check_receiver_types(ua0, ['transparent', 'sapling'])
self.check_receiver_types(ua1, ['transparent', 'sapling'])
# The balances of the accounts are all zero.
self.check_balance(0, ua0, {})
self.check_balance(1, ua1, {})
# Manually send funds to one of the receivers in the UA.
# TODO: Once z_sendmany supports UAs, receive to the UA instead of the receiver.
sapling0 = self.nodes[0].z_listunifiedreceivers(ua0)['sapling']
@ -78,10 +111,18 @@ class WalletAccountsTest(BitcoinTestFramework):
assert_equal(tx_details['outputs'][0]['type'], 'sapling')
assert_equal(tx_details['outputs'][0]['address'], ua0)
# The new balance should not be visible with the default minconf, but should be
# visible with minconf=0.
self.sync_all()
self.check_balance(0, ua0, {})
self.check_balance(0, ua0, {'sapling': 10}, 0)
self.nodes[2].generate(1)
self.sync_all()
# The default minconf should now detect the balance.
self.check_balance(0, ua0, {'sapling': 10})
# Manually send funds from the UA receiver.
# TODO: Once z_sendmany supports UAs, send from the UA instead of the receiver.
node1sapling = self.nodes[1].z_getnewaddress('sapling')
@ -95,6 +136,14 @@ class WalletAccountsTest(BitcoinTestFramework):
assert_equal(tx_details['spends'][0]['type'], 'sapling')
assert_equal(tx_details['spends'][0]['address'], ua0)
# The balances of the account should reflect whether zero-conf transactions are
# being considered. We will show either 0 (because the spent 10-ZEC note is never
# shown, as that transaction has been created and broadcast, and _might_ get mined
# up until the transaction expires), or 9 (if we include the unmined transaction).
self.sync_all()
self.check_balance(0, ua0, {})
self.check_balance(0, ua0, {'sapling': 9}, 0)
if __name__ == '__main__':
WalletAccountsTest().main()

View File

@ -3649,6 +3649,14 @@ UniValue z_getbalanceforaddress(const UniValue& params, bool fHelp)
if (!fExperimentalOrchardWallet) {
throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Error: the Orchard wallet experimental extensions are disabled.");
}
KeyIO keyIO(Params());
auto decoded = keyIO.DecodePaymentAddress(params[0].get_str());
if (!decoded.has_value()) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address");
}
auto address = decoded.value();
int minconf = 1;
if (params.size() > 1) {
minconf = params[1].get_int();
@ -3656,11 +3664,45 @@ UniValue z_getbalanceforaddress(const UniValue& params, bool fHelp)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0");
}
}
LOCK(pwalletMain->cs_wallet);
// Get the receivers for this address.
auto selector = pwalletMain->ToZTXOSelector(address, false);
if (!selector.has_value()) {
// The only way we'd reach this is if the address is a unified address for which
// we do not know its UFVK.
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Error: wallet does not have the Unified Full Viewing Key for the given address");
}
auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, minconf);
CAmount transparentBalance = 0;
CAmount sproutBalance = 0;
CAmount saplingBalance = 0;
for (const auto& t : spendableInputs.utxos) {
transparentBalance += t.Value();
}
for (const auto& t : spendableInputs.sproutNoteEntries) {
sproutBalance += t.note.value();
}
for (const auto& t : spendableInputs.saplingNoteEntries) {
saplingBalance += t.note.value();
}
UniValue pools(UniValue::VOBJ);
pools.pushKV("transparent", 99999.99);
pools.pushKV("sprout", 99999.99);
pools.pushKV("sapling", 99999.99);
pools.pushKV("orchard", 99999.99);
auto renderBalance = [&](std::string poolName, CAmount balance) {
if (balance > 0) {
UniValue pool(UniValue::VOBJ);
pool.pushKV("valueZat", balance);
pools.pushKV(poolName, pool);
}
};
renderBalance("transparent", transparentBalance);
renderBalance("sprout", sproutBalance);
renderBalance("sapling", saplingBalance);
UniValue result(UniValue::VOBJ);
result.pushKV("pools", pools);
@ -3687,9 +3729,6 @@ UniValue z_getbalanceforaccount(const UniValue& params, bool fHelp)
" \"transparent\": {\n"
" \"valueZat\": amount (numeric) The amount held in the transparent pool by this account\n"
" \"},\n"
" \"sprout\": {\n"
" \"valueZat\": amount (numeric) The amount held in the sprout pool by this account\n"
" \"},\n"
" \"sapling\": {\n"
" \"valueZat\": amount (numeric) The amount held in the sapling pool by this account\n"
" \"},\n"
@ -3713,7 +3752,13 @@ UniValue z_getbalanceforaccount(const UniValue& params, bool fHelp)
if (!fExperimentalOrchardWallet) {
throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Error: the Orchard wallet experimental extensions are disabled.");
}
int64_t account = params[0].get_int64();
int64_t accountInt = params[0].get_int64();
if (accountInt < 0 || accountInt >= ZCASH_LEGACY_ACCOUNT) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid account number, must be 0 <= account <= (2^31)-2.");
}
libzcash::AccountId account = accountInt;
int minconf = 1;
if (params.size() > 1) {
minconf = params[1].get_int();
@ -3721,11 +3766,40 @@ UniValue z_getbalanceforaccount(const UniValue& params, bool fHelp)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0");
}
}
LOCK(pwalletMain->cs_wallet);
// Get the receivers for this account.
auto selector = pwalletMain->ZTXOSelectorForAccount(account, false);
if (!selector.has_value()) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
tfm::format("Error: account %d has not been generated by z_getnewaccount.", account));
}
auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, minconf);
// Accounts never contain Sprout notes.
assert(spendableInputs.sproutNoteEntries.empty());
CAmount transparentBalance = 0;
CAmount saplingBalance = 0;
for (const auto& t : spendableInputs.utxos) {
transparentBalance += t.Value();
}
for (const auto& t : spendableInputs.saplingNoteEntries) {
saplingBalance += t.note.value();
}
UniValue pools(UniValue::VOBJ);
pools.pushKV("transparent", 99999.99);
pools.pushKV("sprout", 99999.99);
pools.pushKV("sapling", 99999.99);
pools.pushKV("orchard", 99999.99);
auto renderBalance = [&](std::string poolName, CAmount balance) {
if (balance > 0) {
UniValue pool(UniValue::VOBJ);
pool.pushKV("valueZat", balance);
pools.pushKV(poolName, pool);
}
};
renderBalance("transparent", transparentBalance);
renderBalance("sapling", saplingBalance);
UniValue result(UniValue::VOBJ);
result.pushKV("pools", pools);

View File

@ -469,8 +469,15 @@ std::optional<libzcash::ZcashdUnifiedSpendingKey>
// metadata that can be used to re-derive the spending key along with
// the fingerprint of the associated full viewing key.
// Set up the bidirectional maps between the account ID and the UFVK ID.
auto metaKey = std::make_pair(skmeta.GetSeedFingerprint(), skmeta.GetAccountId());
mapUnifiedAccountKeys.insert({metaKey, skmeta.GetKeyID()});
mapUnifiedAccountKeys.insert({metaKey, ufvkid});
// We set up the UFVKAddressMetadata with the correct account ID (so we identify
// the UFVK as corresponding to this account) and empty receivers data (as we
// haven't generated any addresses yet). We don't need to persist this directly,
// because we persist skmeta below, and mapUfvkAddressMetadata is populated in
// LoadUnifiedAccountMetadata().
mapUfvkAddressMetadata.insert({ufvkid, UFVKAddressMetadata(accountId)});
// Add Transparent component to the wallet
AddTransparentSecretKey(
@ -1363,6 +1370,22 @@ void CWallet::SyncMetaData(pair<typename TxSpendMap<T>::iterator, typename TxSpe
// Zcash transaction output selectors
//
std::optional<ZTXOSelector> CWallet::ZTXOSelectorForAccount(
libzcash::AccountId account,
bool requireSpendingKey,
std::set<libzcash::ReceiverType> receiverTypes) const
{
if (mnemonicHDChain.has_value() &&
mapUnifiedAccountKeys.count(
std::make_pair(mnemonicHDChain.value().GetSeedFingerprint(), account)
) > 0)
{
return ZTXOSelector(AccountZTXOPattern(account, receiverTypes), requireSpendingKey);
} else {
return std::nullopt;
}
}
std::optional<ZTXOSelector> CWallet::ToZTXOSelector(const libzcash::PaymentAddress& addr, bool requireSpendingKey) const {
auto self = this;
std::optional<ZTXOPattern> pattern = std::nullopt;

View File

@ -1222,6 +1222,20 @@ public:
*/
static bool SelectCoinsMinConf(const CAmount& nTargetValue, int nConfMine, int nConfTheirs, std::vector<COutput> vCoins, std::set<std::pair<const CWalletTx*,unsigned int> >& setCoinsRet, CAmount& nValueRet);
/**
* Obtain the ZTXO selector for the specified account ID.
*
* Returns `std::nullopt` if the account ID has not been generated yet by
* the wallet.
*
* If the `requireSpendingKey` flag is set, this will only return a selector
* that will choose outputs for which this wallet holds the spending keys.
*/
std::optional<ZTXOSelector> ZTXOSelectorForAccount(
libzcash::AccountId account,
bool requireSpendingKey,
std::set<libzcash::ReceiverType> receiverTypes={}) const;
/**
* Obtain the ZTXO selector for the specified payment address. If the
* `requireSpendingKey` flag is set, this will only return a selector