diff --git a/zebra-crosslink/wallet/src/lib.rs b/zebra-crosslink/wallet/src/lib.rs index 9405867a..7e4b5f20 100644 --- a/zebra-crosslink/wallet/src/lib.rs +++ b/zebra-crosslink/wallet/src/lib.rs @@ -113,6 +113,33 @@ pub static FAUCET_REQUEST: Mutex> = Mutex::new(None pub static USER_UFVK_STRING: Mutex> = Mutex::new(None); pub static GUI_ENABLE_MINE: Mutex = Mutex::new(true); +/// Cheap, point-in-time snapshot of the headless wallet's own note-scan progress and +/// balances, updated as a handful of plain field assignments alongside the existing +/// `WalletState` update (see the write site in `wallet_main`) -- no new per-block cost. +/// Mirrors `zebra_state::crosslink::WalletSyncStatusSnapshot`; kept as a separate, +/// dependency-free struct here so this crate doesn't need to depend on zebra-state just to +/// hold this value. The RPC layer (which already depends on both crates) converts between +/// the two at the point where it builds the RPC response. +#[derive(Clone, Copy, Debug, Default)] +pub struct WalletSyncStatus { + pub sync_height: u32, + pub tip_height: u32, + pub user_shielded_spendable_zats: u64, + pub user_shielded_pending_zats: u64, + pub user_unshielded_zats: u64, + pub staked_zats: u64, + pub withdrawable_zats: u64, +} +pub static WALLET_SYNC_STATUS: Mutex = Mutex::new(WalletSyncStatus { + sync_height: 0, + tip_height: 0, + user_shielded_spendable_zats: 0, + user_shielded_pending_zats: 0, + user_unshielded_zats: 0, + staked_zats: 0, + withdrawable_zats: 0, +}); + pub static STAKING_STAGE: Mutex>)>> = Mutex::new(None); #[derive(Clone)] @@ -4357,6 +4384,16 @@ pub async fn wallet_main(wallet_state: Arc>) { lock.staked_balance = user_staked_funds; lock.withdrawable_balance = user_withdrawable_funds; + + *WALLET_SYNC_STATUS.lock().unwrap() = WalletSyncStatus { + sync_height: wallets_sync_h.0, + tip_height: network_tip_h.0, + user_shielded_spendable_zats: user_shielded_spendable_funds, + user_shielded_pending_zats: user_shielded_pending_funds, + user_unshielded_zats: user_unshielded_funds, + staked_zats: user_staked_funds, + withdrawable_zats: user_withdrawable_funds, + }; } diff --git a/zebra-crosslink/zebra-crosslink/src/lib.rs b/zebra-crosslink/zebra-crosslink/src/lib.rs index 511e438a..9f06b43c 100644 --- a/zebra-crosslink/zebra-crosslink/src/lib.rs +++ b/zebra-crosslink/zebra-crosslink/src/lib.rs @@ -1840,6 +1840,19 @@ async fn tfl_service_incoming_request( TFLServiceRequest::StakingCmd(String) => Err(TFLServiceError::NotImplemented), TFLServiceRequest::WalletUfvk => Ok(TFLServiceResponse::WalletUfvk(wallet::USER_UFVK_STRING.lock().unwrap().clone())), + + TFLServiceRequest::WalletSyncStatus => { + let s = *wallet::WALLET_SYNC_STATUS.lock().unwrap(); + Ok(TFLServiceResponse::WalletSyncStatus(WalletSyncStatusSnapshot { + sync_height: s.sync_height, + tip_height: s.tip_height, + user_shielded_spendable_zats: s.user_shielded_spendable_zats, + user_shielded_pending_zats: s.user_shielded_pending_zats, + user_unshielded_zats: s.user_unshielded_zats, + staked_zats: s.staked_zats, + withdrawable_zats: s.withdrawable_zats, + })) + } } } diff --git a/zebra-crosslink/zebra-rpc/src/methods.rs b/zebra-crosslink/zebra-rpc/src/methods.rs index 06b3d893..48048c1b 100644 --- a/zebra-crosslink/zebra-rpc/src/methods.rs +++ b/zebra-crosslink/zebra-rpc/src/methods.rs @@ -88,7 +88,7 @@ use zebra_chain::{ use zebra_consensus::{funding_stream_address, ParameterCheckpoint, RouterError}; use zebra_network::{address_book_peers::AddressBookPeers, PeerSocketAddr}; use zebra_node_services::mempool; -use zebra_state::crosslink::{TFLBlockFinality, TFLServiceRequest, TFLServiceResponse}; +use zebra_state::crosslink::{TFLBlockFinality, TFLServiceRequest, TFLServiceResponse, WalletSyncStatusSnapshot}; use zebra_state::{HashOrHeight, OutputLocation, ReadRequest, ReadResponse, TransactionLocation}; use crate::{ @@ -540,6 +540,23 @@ pub trait Rpc { #[method(name = "get_wallet_ufvk")] async fn get_wallet_ufvk(&self) -> Option; + /// Get the headless wallet's own note-scan progress and balances. + /// + /// `sync_height` is how far the wallet's own (in-memory, restart-resets-to-zero) note-scan + /// has reached -- distinct from the node's raw chain-sync height (see `getblockchaininfo`), + /// which can be fully caught up while this still lags behind, particularly right after a + /// restart. Balances (and anything derived from wallet-tracked notes, e.g. + /// `wallet_staking_action`) are only accurate once `sync_height` is close to `tip_height`. + /// + /// ## Example Usage + /// ```bash + /// curl -X POST -H "Content-Type: application/json" -d \ + /// '{ "jsonrpc": "2.0", "method": "get_wallet_sync_status", "params": [], "id": 1 }' \ + /// http://127.0.0.1:8232 + /// ``` + #[method(name = "get_wallet_sync_status")] + async fn get_wallet_sync_status(&self) -> Result; + /// send a staking action from the given wallet #[method(name = "wallet_staking_action")] async fn wallet_staking_action(&self, staking_action: StakingActionRequest) -> Result; @@ -2363,6 +2380,21 @@ where } } + async fn get_wallet_sync_status(&self) -> Result { + let res = self + .tfl_service + .clone() + .ready() + .await + .unwrap() + .call(TFLServiceRequest::WalletSyncStatus) + .await; + match res { + Ok(TFLServiceResponse::WalletSyncStatus(status)) => Ok(status), + _ => Ok(WalletSyncStatusSnapshot::default()), + } + } + async fn wallet_staking_action(&self, staking_action: StakingActionRequest) -> Result { let res = self .tfl_service diff --git a/zebra-crosslink/zebra-state/src/crosslink.rs b/zebra-crosslink/zebra-state/src/crosslink.rs index 251e4a36..7b41c822 100644 --- a/zebra-crosslink/zebra-state/src/crosslink.rs +++ b/zebra-crosslink/zebra-state/src/crosslink.rs @@ -11,6 +11,33 @@ use serde_with::serde_as; pub use zcash_primitives::bft::{FinalizerRecencyStatus, TFLRecencyStatus, ScanInfo}; use zcash_primitives::transaction::StakingActionRequest; +/// A cheap, point-in-time snapshot of the headless wallet's own note-scan progress and +/// balances. Updated as a handful of plain field assignments once per wallet loop iteration +/// (see `wallet::WALLET_SYNC_STATUS`), so reading it via RPC costs nothing beyond what the +/// wallet already computes for its own internal use -- unlike deriving this information from +/// the `DUMP_NOTES` debug log, which reprints the *entire* notes list from scratch every +/// iteration and does not scale as that list grows. +#[derive(Debug, Default, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] +pub struct WalletSyncStatusSnapshot { + /// The highest block height the headless wallet's own note-scan has processed so far. + /// Distinct from the raw node's chain-sync height (see `getblockchaininfo`) -- this can + /// lag behind it, particularly right after a restart, since wallet note-scanning is + /// in-memory only and always restarts from genesis. + pub sync_height: u32, + /// The chain tip height the wallet is scanning towards, as of the same snapshot. + pub tip_height: u32, + /// The user wallet's confirmed, spendable (not staked, not pending) balance, in zatoshis. + pub user_shielded_spendable_zats: u64, + /// The user wallet's unconfirmed/pending shielded balance, in zatoshis. + pub user_shielded_pending_zats: u64, + /// The user wallet's transparent balance, in zatoshis. + pub user_unshielded_zats: u64, + /// Total currently staked (bonded) balance, in zatoshis. + pub staked_zats: u64, + /// Balance available to withdraw from completed unbonding, in zatoshis. + pub withdrawable_zats: u64, +} + /// The finality status of a block #[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] pub enum TFLBlockFinality { @@ -63,6 +90,8 @@ pub enum TFLServiceRequest { WalletUfvk, /// Send staking action from wallet WalletStakingAction(StakingActionRequest), + /// Get the headless wallet's own note-scan progress and balances + WalletSyncStatus, } /// Types of responses that can be returned by the TFLService. @@ -98,6 +127,8 @@ pub enum TFLServiceResponse { WalletUfvk(Option), /// Send staking action from wallet WalletStakingAction(Result), + /// The headless wallet's own note-scan progress and balances + WalletSyncStatus(WalletSyncStatusSnapshot), } /// Errors that can occur when interacting with the TFLService.