Merge pull request #5500 from str4d/ua-wallet-balance-rpcs
Implement balance RPC methods that support Unified Addresses
This commit is contained in:
commit
992a47103d
|
@ -4,6 +4,7 @@
|
||||||
# file COPYING or https://www.opensource.org/licenses/mit-license.php .
|
# file COPYING or https://www.opensource.org/licenses/mit-license.php .
|
||||||
|
|
||||||
from test_framework.authproxy import JSONRPCException
|
from test_framework.authproxy import JSONRPCException
|
||||||
|
from test_framework.mininode import COIN
|
||||||
from test_framework.test_framework import BitcoinTestFramework
|
from test_framework.test_framework import BitcoinTestFramework
|
||||||
from test_framework.util import (
|
from test_framework.util import (
|
||||||
assert_equal,
|
assert_equal,
|
||||||
|
@ -27,6 +28,34 @@ class WalletAccountsTest(BitcoinTestFramework):
|
||||||
actual = self.nodes[0].z_listunifiedreceivers(ua)
|
actual = self.nodes[0].z_listunifiedreceivers(ua)
|
||||||
assert_equal(set(expected), set(actual))
|
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):
|
def run_test(self):
|
||||||
# With a new wallet, the first account will be 0.
|
# With a new wallet, the first account will be 0.
|
||||||
account0 = self.nodes[0].z_getnewaccount()
|
account0 = self.nodes[0].z_getnewaccount()
|
||||||
|
@ -65,6 +94,10 @@ class WalletAccountsTest(BitcoinTestFramework):
|
||||||
self.check_receiver_types(ua0, ['transparent', 'sapling'])
|
self.check_receiver_types(ua0, ['transparent', 'sapling'])
|
||||||
self.check_receiver_types(ua1, ['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.
|
# 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.
|
# TODO: Once z_sendmany supports UAs, receive to the UA instead of the receiver.
|
||||||
sapling0 = self.nodes[0].z_listunifiedreceivers(ua0)['sapling']
|
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]['type'], 'sapling')
|
||||||
assert_equal(tx_details['outputs'][0]['address'], ua0)
|
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.sync_all()
|
||||||
|
self.check_balance(0, ua0, {})
|
||||||
|
self.check_balance(0, ua0, {'sapling': 10}, 0)
|
||||||
|
|
||||||
self.nodes[2].generate(1)
|
self.nodes[2].generate(1)
|
||||||
self.sync_all()
|
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.
|
# Manually send funds from the UA receiver.
|
||||||
# TODO: Once z_sendmany supports UAs, send from the UA instead of the receiver.
|
# TODO: Once z_sendmany supports UAs, send from the UA instead of the receiver.
|
||||||
node1sapling = self.nodes[1].z_getnewaddress('sapling')
|
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]['type'], 'sapling')
|
||||||
assert_equal(tx_details['spends'][0]['address'], ua0)
|
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__':
|
if __name__ == '__main__':
|
||||||
WalletAccountsTest().main()
|
WalletAccountsTest().main()
|
||||||
|
|
|
@ -3649,6 +3649,14 @@ UniValue z_getbalanceforaddress(const UniValue& params, bool fHelp)
|
||||||
if (!fExperimentalOrchardWallet) {
|
if (!fExperimentalOrchardWallet) {
|
||||||
throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Error: the Orchard wallet experimental extensions are disabled.");
|
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;
|
int minconf = 1;
|
||||||
if (params.size() > 1) {
|
if (params.size() > 1) {
|
||||||
minconf = params[1].get_int();
|
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");
|
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);
|
UniValue pools(UniValue::VOBJ);
|
||||||
pools.pushKV("transparent", 99999.99);
|
auto renderBalance = [&](std::string poolName, CAmount balance) {
|
||||||
pools.pushKV("sprout", 99999.99);
|
if (balance > 0) {
|
||||||
pools.pushKV("sapling", 99999.99);
|
UniValue pool(UniValue::VOBJ);
|
||||||
pools.pushKV("orchard", 99999.99);
|
pool.pushKV("valueZat", balance);
|
||||||
|
pools.pushKV(poolName, pool);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
renderBalance("transparent", transparentBalance);
|
||||||
|
renderBalance("sprout", sproutBalance);
|
||||||
|
renderBalance("sapling", saplingBalance);
|
||||||
|
|
||||||
UniValue result(UniValue::VOBJ);
|
UniValue result(UniValue::VOBJ);
|
||||||
result.pushKV("pools", pools);
|
result.pushKV("pools", pools);
|
||||||
|
@ -3687,9 +3729,6 @@ UniValue z_getbalanceforaccount(const UniValue& params, bool fHelp)
|
||||||
" \"transparent\": {\n"
|
" \"transparent\": {\n"
|
||||||
" \"valueZat\": amount (numeric) The amount held in the transparent pool by this account\n"
|
" \"valueZat\": amount (numeric) The amount held in the transparent pool by this account\n"
|
||||||
" \"},\n"
|
" \"},\n"
|
||||||
" \"sprout\": {\n"
|
|
||||||
" \"valueZat\": amount (numeric) The amount held in the sprout pool by this account\n"
|
|
||||||
" \"},\n"
|
|
||||||
" \"sapling\": {\n"
|
" \"sapling\": {\n"
|
||||||
" \"valueZat\": amount (numeric) The amount held in the sapling pool by this account\n"
|
" \"valueZat\": amount (numeric) The amount held in the sapling pool by this account\n"
|
||||||
" \"},\n"
|
" \"},\n"
|
||||||
|
@ -3713,7 +3752,13 @@ UniValue z_getbalanceforaccount(const UniValue& params, bool fHelp)
|
||||||
if (!fExperimentalOrchardWallet) {
|
if (!fExperimentalOrchardWallet) {
|
||||||
throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Error: the Orchard wallet experimental extensions are disabled.");
|
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;
|
int minconf = 1;
|
||||||
if (params.size() > 1) {
|
if (params.size() > 1) {
|
||||||
minconf = params[1].get_int();
|
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");
|
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);
|
UniValue pools(UniValue::VOBJ);
|
||||||
pools.pushKV("transparent", 99999.99);
|
auto renderBalance = [&](std::string poolName, CAmount balance) {
|
||||||
pools.pushKV("sprout", 99999.99);
|
if (balance > 0) {
|
||||||
pools.pushKV("sapling", 99999.99);
|
UniValue pool(UniValue::VOBJ);
|
||||||
pools.pushKV("orchard", 99999.99);
|
pool.pushKV("valueZat", balance);
|
||||||
|
pools.pushKV(poolName, pool);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
renderBalance("transparent", transparentBalance);
|
||||||
|
renderBalance("sapling", saplingBalance);
|
||||||
|
|
||||||
UniValue result(UniValue::VOBJ);
|
UniValue result(UniValue::VOBJ);
|
||||||
result.pushKV("pools", pools);
|
result.pushKV("pools", pools);
|
||||||
|
|
|
@ -469,8 +469,15 @@ std::optional<libzcash::ZcashdUnifiedSpendingKey>
|
||||||
// metadata that can be used to re-derive the spending key along with
|
// metadata that can be used to re-derive the spending key along with
|
||||||
// the fingerprint of the associated full viewing key.
|
// 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());
|
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
|
// Add Transparent component to the wallet
|
||||||
AddTransparentSecretKey(
|
AddTransparentSecretKey(
|
||||||
|
@ -1363,6 +1370,22 @@ void CWallet::SyncMetaData(pair<typename TxSpendMap<T>::iterator, typename TxSpe
|
||||||
// Zcash transaction output selectors
|
// 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 {
|
std::optional<ZTXOSelector> CWallet::ToZTXOSelector(const libzcash::PaymentAddress& addr, bool requireSpendingKey) const {
|
||||||
auto self = this;
|
auto self = this;
|
||||||
std::optional<ZTXOPattern> pattern = std::nullopt;
|
std::optional<ZTXOPattern> pattern = std::nullopt;
|
||||||
|
|
|
@ -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);
|
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
|
* Obtain the ZTXO selector for the specified payment address. If the
|
||||||
* `requireSpendingKey` flag is set, this will only return a selector
|
* `requireSpendingKey` flag is set, this will only return a selector
|
||||||
|
|
Loading…
Reference in New Issue