Skip to content

Commit eb16fc7

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

File tree

2 files changed

+174
-0
lines changed

2 files changed

+174
-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: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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.
43+
#[serde(skip_serializing_if = "Option::is_none")]
44+
fully_synced_height: Option<u32>,
45+
46+
/// The height up to which the wallet has full information.
47+
///
48+
/// Omitted if the wallet is fully synced (which requires that `fully_synced_height`
49+
/// is equal to `wallet_tip.height`).
50+
#[serde(skip_serializing_if = "Option::is_none")]
51+
sync_work_remaining: Option<SyncWorkRemaining>,
52+
}
53+
54+
#[derive(Clone, Debug, Serialize, JsonSchema)]
55+
struct ChainTip {
56+
/// The hash of the block at the chain tip.
57+
blockhash: String,
58+
59+
/// The height of the block in the chain.
60+
height: u32,
61+
}
62+
63+
#[derive(Clone, Debug, Serialize, JsonSchema)]
64+
struct SyncWorkRemaining {
65+
unscanned_blocks: u32,
66+
67+
// TODO: Replace these with accurate unscanned note counts, which we can determine
68+
// because Zallet tracks the chain tip very closely.
69+
progress_numerator: u64,
70+
progress_denominator: u64,
71+
}
72+
73+
pub(crate) async fn call(wallet: &DbConnection, chain: FetchServiceSubscriber) -> Response {
74+
let node_tip = chain
75+
.get_latest_block()
76+
.await
77+
// TODO: Better error.
78+
.map_err(|e| LegacyCode::Database.with_message(e.to_string()))?;
79+
80+
let (wallet_tip, block_fully_scanned, scan_ranges, wallet_summary) =
81+
wallet
82+
.with(wallet_data)
83+
.map_err(|e| LegacyCode::Database.with_message(e.to_string()))?;
84+
85+
let sync_work_remaining = wallet_summary.and_then(|s| {
86+
let unscanned_blocks = scan_ranges
87+
.iter()
88+
.map(|r| r.block_range().end - r.block_range().start)
89+
.sum::<u32>();
90+
91+
let (progress_numerator, progress_denominator) =
92+
if let Some(recovery) = s.progress().recovery() {
93+
(
94+
s.progress().scan().numerator() + recovery.numerator(),
95+
s.progress().scan().denominator() + recovery.denominator(),
96+
)
97+
} else {
98+
(
99+
*s.progress().scan().numerator(),
100+
*s.progress().scan().denominator(),
101+
)
102+
};
103+
104+
if unscanned_blocks == 0 && progress_numerator == progress_denominator {
105+
None
106+
} else {
107+
Some(SyncWorkRemaining {
108+
unscanned_blocks,
109+
progress_numerator,
110+
progress_denominator,
111+
})
112+
}
113+
});
114+
115+
Ok(GetWalletStatus {
116+
node_tip: ChainTip {
117+
blockhash: BlockHash::try_from_slice(node_tip.hash.as_slice())
118+
.expect("block hash missing")
119+
.to_string(),
120+
height: u32::try_from(node_tip.height)
121+
// TODO: Better error.
122+
.map_err(|e| LegacyCode::Database.with_message(e.to_string()))?,
123+
},
124+
wallet_tip,
125+
fully_synced_height: block_fully_scanned.map(|b| b.block_height().into()),
126+
sync_work_remaining,
127+
})
128+
}
129+
130+
fn wallet_data(
131+
wallet: WalletDb<&rusqlite::Connection, Network, SystemClock, OsRng>,
132+
) -> Result<
133+
(
134+
Option<ChainTip>,
135+
Option<BlockMetadata>,
136+
Vec<ScanRange>,
137+
Option<WalletSummary<AccountUuid>>,
138+
),
139+
SqliteClientError,
140+
> {
141+
let tip_height = wallet.chain_height()?;
142+
let tip_hash = if let Some(block_height) = tip_height {
143+
wallet.get_block_hash(block_height)?
144+
} else {
145+
None
146+
};
147+
148+
if let Some((tip_hash, tip_height)) = tip_hash.zip(tip_height) {
149+
let block_fully_scanned = wallet.block_fully_scanned()?;
150+
let scan_ranges = wallet.suggest_scan_ranges()?;
151+
let summary = wallet.get_wallet_summary(ConfirmationsPolicy::MIN)?;
152+
153+
Ok((
154+
Some(ChainTip {
155+
blockhash: tip_hash.to_string(),
156+
height: tip_height.into(),
157+
}),
158+
block_fully_scanned,
159+
scan_ranges,
160+
summary,
161+
))
162+
} else {
163+
Ok((None, None, vec![], None))
164+
}
165+
}

0 commit comments

Comments
 (0)