Skip to content

Commit 7838054

Browse files
committed
rpc: Add getwalletstatus
Closes #316.
1 parent c4d1266 commit 7838054

File tree

2 files changed

+176
-0
lines changed

2 files changed

+176
-0
lines changed

zallet/src/components/json_rpc/methods.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ mod get_operation;
2929
mod get_raw_transaction;
3030
#[cfg(zallet_build = "wallet")]
3131
mod get_wallet_info;
32+
mod get_wallet_status;
3233
#[cfg(zallet_build = "wallet")]
3334
mod help;
3435
mod list_accounts;
@@ -57,6 +58,10 @@ mod z_send_many;
5758
/// The general JSON-RPC interface, containing the methods provided in all Zallet builds.
5859
#[rpc(server)]
5960
pub(crate) trait Rpc {
61+
/// Returns wallet status information.
62+
#[method(name = "getwalletstatus")]
63+
async fn get_wallet_status(&self) -> get_wallet_status::Response;
64+
6065
/// Returns the list of accounts created with `z_getnewaccount` or `z_recoveraccounts`.
6166
///
6267
/// # Arguments
@@ -551,6 +556,10 @@ impl WalletRpcImpl {
551556

552557
#[async_trait]
553558
impl RpcServer for RpcImpl {
559+
async fn get_wallet_status(&self) -> get_wallet_status::Response {
560+
get_wallet_status::call(self.wallet().await?.as_ref(), self.chain().await?).await
561+
}
562+
554563
async fn list_accounts(&self, include_addresses: Option<bool>) -> list_accounts::Response {
555564
list_accounts::call(self.wallet().await?.as_ref(), include_addresses)
556565
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
use documented::Documented;
2+
use jsonrpsee::core::RpcResult;
3+
use rand::rngs::OsRng;
4+
use schemars::JsonSchema;
5+
use serde::Serialize;
6+
use zaino_state::{FetchServiceSubscriber, LightWalletIndexer};
7+
use zcash_client_backend::data_api::{
8+
BlockMetadata, WalletRead, WalletSummary, scanning::ScanRange, wallet::ConfirmationsPolicy,
9+
};
10+
use zcash_client_sqlite::{AccountUuid, WalletDb, error::SqliteClientError, util::SystemClock};
11+
use zcash_primitives::block::BlockHash;
12+
13+
use crate::{
14+
components::{database::DbConnection, json_rpc::server::LegacyCode},
15+
network::Network,
16+
};
17+
18+
/// Response to a `getwalletstatus` RPC request.
19+
pub(crate) type Response = RpcResult<ResultType>;
20+
pub(crate) type ResultType = GetWalletStatus;
21+
22+
/// The wallet status information.
23+
#[derive(Clone, Debug, Serialize, Documented, JsonSchema)]
24+
pub(crate) struct GetWalletStatus {
25+
/// The backing full node's view of the chain tip.
26+
node_tip: ChainTip,
27+
28+
/// The wallet's view of the chain tip.
29+
///
30+
/// This should only diverge from `node_tip` for very short periods of time.
31+
///
32+
/// Omitted if the wallet has just been started for the first time and has not yet
33+
/// begun syncing.
34+
#[serde(skip_serializing_if = "Option::is_none")]
35+
wallet_tip: Option<ChainTip>,
36+
37+
/// The height to which the wallet is fully synced.
38+
///
39+
/// The wallet only has a partial view of chain data above this height.
40+
///
41+
/// Omitted if the wallet does not have any accounts or birthday data and thus has
42+
/// nowhere to sync from, or if the wallet birthday itself has not yet been synced.
43+
/// The latter occurs when a recovered wallet first starts and is scanning the chain
44+
/// tip region.
45+
#[serde(skip_serializing_if = "Option::is_none")]
46+
fully_synced_height: Option<u32>,
47+
48+
/// The height up to which the wallet has full information.
49+
///
50+
/// Omitted if the wallet is fully synced (which requires that `fully_synced_height`
51+
/// is equal to `wallet_tip.height`).
52+
#[serde(skip_serializing_if = "Option::is_none")]
53+
sync_work_remaining: Option<SyncWorkRemaining>,
54+
}
55+
56+
#[derive(Clone, Debug, Serialize, JsonSchema)]
57+
struct ChainTip {
58+
/// The hash of the block at the chain tip.
59+
blockhash: String,
60+
61+
/// The height of the block in the chain.
62+
height: u32,
63+
}
64+
65+
#[derive(Clone, Debug, Serialize, JsonSchema)]
66+
struct SyncWorkRemaining {
67+
unscanned_blocks: u32,
68+
69+
// TODO: Replace these with accurate unscanned note counts, which we can determine
70+
// because Zallet tracks the chain tip very closely.
71+
progress_numerator: u64,
72+
progress_denominator: u64,
73+
}
74+
75+
pub(crate) async fn call(wallet: &DbConnection, chain: FetchServiceSubscriber) -> Response {
76+
let node_tip = chain
77+
.get_latest_block()
78+
.await
79+
// TODO: Better error.
80+
.map_err(|e| LegacyCode::Database.with_message(e.to_string()))?;
81+
82+
let (wallet_tip, block_fully_scanned, scan_ranges, wallet_summary) =
83+
wallet
84+
.with(wallet_data)
85+
.map_err(|e| LegacyCode::Database.with_message(e.to_string()))?;
86+
87+
let sync_work_remaining = wallet_summary.and_then(|s| {
88+
let unscanned_blocks = scan_ranges
89+
.iter()
90+
.map(|r| r.block_range().end - r.block_range().start)
91+
.sum::<u32>();
92+
93+
let (progress_numerator, progress_denominator) =
94+
if let Some(recovery) = s.progress().recovery() {
95+
(
96+
s.progress().scan().numerator() + recovery.numerator(),
97+
s.progress().scan().denominator() + recovery.denominator(),
98+
)
99+
} else {
100+
(
101+
*s.progress().scan().numerator(),
102+
*s.progress().scan().denominator(),
103+
)
104+
};
105+
106+
if unscanned_blocks == 0 && progress_numerator == progress_denominator {
107+
None
108+
} else {
109+
Some(SyncWorkRemaining {
110+
unscanned_blocks,
111+
progress_numerator,
112+
progress_denominator,
113+
})
114+
}
115+
});
116+
117+
Ok(GetWalletStatus {
118+
node_tip: ChainTip {
119+
blockhash: BlockHash::try_from_slice(node_tip.hash.as_slice())
120+
.expect("block hash missing")
121+
.to_string(),
122+
height: u32::try_from(node_tip.height)
123+
// TODO: Better error.
124+
.map_err(|e| LegacyCode::Database.with_message(e.to_string()))?,
125+
},
126+
wallet_tip,
127+
fully_synced_height: block_fully_scanned.map(|b| b.block_height().into()),
128+
sync_work_remaining,
129+
})
130+
}
131+
132+
fn wallet_data(
133+
wallet: WalletDb<&rusqlite::Connection, Network, SystemClock, OsRng>,
134+
) -> Result<
135+
(
136+
Option<ChainTip>,
137+
Option<BlockMetadata>,
138+
Vec<ScanRange>,
139+
Option<WalletSummary<AccountUuid>>,
140+
),
141+
SqliteClientError,
142+
> {
143+
let tip_height = wallet.chain_height()?;
144+
let tip_metadata = if let Some(block_height) = tip_height {
145+
wallet.block_metadata(block_height)?
146+
} else {
147+
None
148+
};
149+
150+
if let Some(tip_metadata) = tip_metadata {
151+
let block_fully_scanned = wallet.block_fully_scanned()?;
152+
let scan_ranges = wallet.suggest_scan_ranges()?;
153+
let summary = wallet.get_wallet_summary(ConfirmationsPolicy::MIN)?;
154+
155+
Ok((
156+
Some(ChainTip {
157+
blockhash: tip_metadata.block_hash().to_string(),
158+
height: tip_metadata.block_height().into(),
159+
}),
160+
block_fully_scanned,
161+
scan_ranges,
162+
summary,
163+
))
164+
} else {
165+
Ok((None, None, vec![], None))
166+
}
167+
}

0 commit comments

Comments
 (0)