Skip to content

Commit

Permalink
Add get_wallet_summary to WalletRead
Browse files Browse the repository at this point in the history
The intent of this API is to provide a single API which returns in a
single call:

* per-account balances, including pending values
* wallet sync progress

Fixes #865
Fixes #900
  • Loading branch information
nuttycom committed Sep 1, 2023
1 parent 229f6e8 commit 095971a
Show file tree
Hide file tree
Showing 11 changed files with 635 additions and 134 deletions.
164 changes: 145 additions & 19 deletions zcash_client_backend/src/data_api.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
//! Interfaces for wallet data persistence & low-level wallet utilities.

use std::fmt::Debug;
use std::io;
use std::num::NonZeroU32;
use std::{collections::HashMap, num::TryFromIntError};
use std::{
collections::{BTreeMap, HashMap},
fmt::Debug,
io,
num::{NonZeroU32, TryFromIntError},
};

use incrementalmerkletree::{frontier::Frontier, Retention};
use secrecy::SecretVec;
Expand All @@ -15,7 +17,10 @@ use zcash_primitives::{
memo::{Memo, MemoBytes},
sapling::{self, Node, NOTE_COMMITMENT_TREE_DEPTH},
transaction::{
components::{amount::Amount, OutPoint},
components::{
amount::{Amount, NonNegativeAmount},
OutPoint,
},
Transaction, TxId,
},
zip32::{AccountId, ExtendedFullViewingKey},
Expand Down Expand Up @@ -46,6 +51,131 @@ pub enum NullifierQuery {
All,
}

#[derive(Debug, Clone, Copy)]
pub struct Balance {
/// The value in the account that may currently be spent; it is possible to compute witnesses
/// for all the notes that comprise this value, and all of this value is confirmed to the
/// required confirmation depth.
pub spendable_value: NonNegativeAmount,

/// The value in the account of shielded change notes that do not yet have sufficient
/// confirmations to be spendable.
pub change_pending_confirmation: NonNegativeAmount,

/// The value in the account of all remaining received notes that either do not have sufficient
/// confirmations to be spendable, or for which witnesses cannot yet be constructed without
/// additional scanning.
pub value_pending_spendability: NonNegativeAmount,
}

impl Balance {
pub const ZERO: Self = Self {
spendable_value: NonNegativeAmount::ZERO,
change_pending_confirmation: NonNegativeAmount::ZERO,
value_pending_spendability: NonNegativeAmount::ZERO,
};

pub fn total(&self) -> NonNegativeAmount {
(self.spendable_value + self.change_pending_confirmation + self.value_pending_spendability)
.expect("Balance cannot overflow MAX_MONEY")
}
}

/// Balance information for a single account. The sum of this struct's fields is the total balance
/// of the wallet.
#[derive(Debug, Clone, Copy)]
pub struct AccountBalance {
/// The value of unspent Sapling outputs belonging to the account.
pub sapling_balance: Balance,

/// The value of all unspent transparent outputs belonging to the account, irrespective of
/// confirmation depth.
///
/// Unshielded balances are not subject to confirmation-depth constraints, because the only
/// possible operation on a transparent balance is to shield it, it is possible to create a
/// zero-conf transaction to perform that shielding, and the resulting shielded notes will be
/// subject to normal confirmation rules.
pub unshielded: NonNegativeAmount,
}

impl AccountBalance {
pub fn total(&self) -> NonNegativeAmount {
(self.sapling_balance.total() + self.unshielded)
.expect("Account balance cannot overflow MAX_MONEY")
}
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Ratio<T> {
numerator: T,
denominator: T,
}

impl<T> Ratio<T> {
pub fn new(numerator: T, denominator: T) -> Self {
Self {
numerator,
denominator,
}
}

pub fn numerator(&self) -> &T {
&self.numerator
}

pub fn denominator(&self) -> &T {
&self.denominator
}
}

/// A type representing the potentially-spendable value of unspent outputs in the wallet.
///
/// The balances reported using this data structure may overestimate the total spendable value of
/// the wallet, in the case that the spend of a previously received shielded note has not yet been
/// detected by the process of scanning the chain. The balances reported using this data structure
/// can only be certain to be unspent in the case that [`Self::chain_tip_height`] is equal to
/// [`Self::fully_scanned_height]`, and even in this circumstance it is possible that a newly
/// created transaction could conflict with a not-yet-mined transaction in the mempool.
#[derive(Debug, Clone)]
pub struct WalletSummary {
account_balances: BTreeMap<AccountId, AccountBalance>,
chain_tip_height: BlockHeight,
fully_scanned_height: BlockHeight,
sapling_scan_progress: Ratio<u64>,
}

impl WalletSummary {
pub fn new(
account_balances: BTreeMap<AccountId, AccountBalance>,
chain_tip_height: BlockHeight,
fully_scanned_height: BlockHeight,
sapling_scan_progress: Ratio<u64>,
) -> Self {
Self {
account_balances,
chain_tip_height,
fully_scanned_height,
sapling_scan_progress,
}
}

pub fn account_balances(&self) -> &BTreeMap<AccountId, AccountBalance> {
&self.account_balances
}

pub fn chain_tip_height(&self) -> BlockHeight {
self.chain_tip_height
}

pub fn fully_scanned_height(&self) -> BlockHeight {
self.fully_scanned_height
}

pub fn sapling_scan_progress(&self) -> Ratio<u64> {
self.sapling_scan_progress
}
}

/// Read-only operations required for light wallet functions.
///
/// This trait defines the read-only portion of the storage interface atop which
Expand Down Expand Up @@ -157,15 +287,12 @@ pub trait WalletRead {
extfvk: &ExtendedFullViewingKey,
) -> Result<bool, Self::Error>;

/// Returns the wallet balance for an account as of the specified block height.
///
/// This may be used to obtain a balance that ignores notes that have been received so recently
/// that they are not yet deemed spendable.
fn get_balance_at(
/// Returns the wallet balances and sync status for an account given the specified minimum
/// number of confirmations, or `Ok(None)` if the wallet has no balance data available.
fn get_wallet_summary(
&self,
account: AccountId,
anchor_height: BlockHeight,
) -> Result<Amount, Self::Error>;
min_confirmations: u32,
) -> Result<Option<WalletSummary>, Self::Error>;

/// Returns the memo for a note.
///
Expand Down Expand Up @@ -747,7 +874,7 @@ pub mod testing {
use super::{
chain::CommitmentTreeRoot, scanning::ScanRange, AccountBirthday, BlockMetadata,
DecryptedTransaction, NoteId, NullifierQuery, ScannedBlock, SentTransaction,
WalletCommitmentTrees, WalletRead, WalletWrite, SAPLING_SHARD_HEIGHT,
WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
};

pub struct MockWalletDb {
Expand Down Expand Up @@ -853,12 +980,11 @@ pub mod testing {
Ok(false)
}

fn get_balance_at(
fn get_wallet_summary(
&self,
_account: AccountId,
_anchor_height: BlockHeight,
) -> Result<Amount, Self::Error> {
Ok(Amount::zero())
_min_confirmations: u32,
) -> Result<Option<WalletSummary>, Self::Error> {
Ok(None)
}

fn get_memo(&self, _id_note: NoteId) -> Result<Option<Memo>, Self::Error> {
Expand Down
11 changes: 5 additions & 6 deletions zcash_client_sqlite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ use zcash_client_backend::{
scanning::{ScanPriority, ScanRange},
AccountBirthday, BlockMetadata, DecryptedTransaction, NoteId, NullifierQuery, PoolType,
Recipient, ScannedBlock, SentTransaction, ShieldedProtocol, WalletCommitmentTrees,
WalletRead, WalletWrite, SAPLING_SHARD_HEIGHT,
WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
},
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
proto::compact_formats::CompactBlock,
Expand Down Expand Up @@ -243,12 +243,11 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
wallet::is_valid_account_extfvk(self.conn.borrow(), &self.params, account, extfvk)
}

fn get_balance_at(
fn get_wallet_summary(
&self,
account: AccountId,
anchor_height: BlockHeight,
) -> Result<Amount, Self::Error> {
wallet::get_balance_at(self.conn.borrow(), account, anchor_height)
min_confirmations: u32,
) -> Result<Option<WalletSummary>, Self::Error> {
wallet::get_wallet_summary(self.conn.borrow(), min_confirmations)
}

fn get_memo(&self, note_id: NoteId) -> Result<Option<Memo>, Self::Error> {
Expand Down
64 changes: 59 additions & 5 deletions zcash_client_sqlite/src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ use zcash_primitives::{
Note, Nullifier, PaymentAddress,
},
transaction::{
components::{amount::BalanceError, Amount},
components::{
amount::{BalanceError, NonNegativeAmount},
Amount,
},
fees::FeeRule,
TxId,
},
Expand All @@ -56,7 +59,9 @@ use zcash_primitives::{
use crate::{
chain::init::init_cache_database,
error::SqliteClientError,
wallet::{commitment_tree, init::init_wallet_db, sapling::tests::test_prover},
wallet::{
commitment_tree, get_wallet_summary, init::init_wallet_db, sapling::tests::test_prover,
},
AccountId, ReceivedNoteId, WalletDb,
};

Expand All @@ -65,9 +70,7 @@ use super::BlockDb;
#[cfg(feature = "transparent-inputs")]
use {
zcash_client_backend::data_api::wallet::{propose_shielding, shield_transparent_funds},
zcash_primitives::{
legacy::TransparentAddress, transaction::components::amount::NonNegativeAmount,
},
zcash_primitives::legacy::TransparentAddress,
};

#[cfg(feature = "unstable")]
Expand Down Expand Up @@ -286,6 +289,57 @@ where
limit,
)
}

pub(crate) fn get_total_balance(&self, account: AccountId) -> NonNegativeAmount {
get_wallet_summary(&self.wallet().conn, 0)
.unwrap()
.unwrap()
.account_balances()
.get(&account)
.unwrap()
.total()
}

pub(crate) fn get_spendable_balance(
&self,
account: AccountId,
min_confirmations: u32,
) -> NonNegativeAmount {
let binding = get_wallet_summary(&self.wallet().conn, min_confirmations)
.unwrap()
.unwrap();
let balance = binding.account_balances().get(&account).unwrap();

balance.sapling_balance.spendable_value
}

pub(crate) fn get_pending_shielded_balance(
&self,
account: AccountId,
min_confirmations: u32,
) -> NonNegativeAmount {
let binding = get_wallet_summary(&self.wallet().conn, min_confirmations)
.unwrap()
.unwrap();
let balance = binding.account_balances().get(&account).unwrap();

(balance.sapling_balance.value_pending_spendability
+ balance.sapling_balance.change_pending_confirmation)
.unwrap()
}

pub(crate) fn get_pending_change(
&self,
account: AccountId,
min_confirmations: u32,
) -> NonNegativeAmount {
let binding = get_wallet_summary(&self.wallet().conn, min_confirmations)
.unwrap()
.unwrap();
let balance = binding.account_balances().get(&account).unwrap();

balance.sapling_balance.change_pending_confirmation
}
}

impl<Cache> TestState<Cache> {
Expand Down
Loading

0 comments on commit 095971a

Please sign in to comment.