Skip to content

Commit 542c349

Browse files
committed
rpc: Add z_getbalances
Closes #101.
1 parent c4d1266 commit 542c349

2 files changed

Lines changed: 254 additions & 0 deletions

File tree

zallet/src/components/json_rpc/methods.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use {
2121
mod get_account;
2222
mod get_address_for_account;
2323
#[cfg(zallet_build = "wallet")]
24+
mod get_balances;
25+
#[cfg(zallet_build = "wallet")]
2426
mod get_new_account;
2527
#[cfg(zallet_build = "wallet")]
2628
mod get_notes_count;
@@ -334,6 +336,27 @@ pub(crate) trait WalletRpc {
334336
accounts: Vec<recover_accounts::AccountParameter<'_>>,
335337
) -> recover_accounts::Response;
336338

339+
/// Returns the balances available for each independent spending authority held by the
340+
/// wallet, and optionally the balances and received amounts associated with imported
341+
/// watch-only addresses and viewing keys.
342+
///
343+
/// This includes funds held by each HD-derived Unified Account in the wallet,
344+
/// spending keys imported with `z_importkey`, and (if enabled) the legacy transparent
345+
/// pool of funds.
346+
///
347+
/// # Arguments
348+
///
349+
/// - `minconf` (numeric, optional, default=1) Only include transactions confirmed at
350+
/// least this many times.
351+
/// - `include_watchonly` (bool, optional, default=false) Also include balance in
352+
/// watchonly addresses (see 'importaddress' and 'z_importviewingkey').
353+
#[method(name = "z_getbalances")]
354+
async fn get_balances(
355+
&self,
356+
minconf: Option<u32>,
357+
include_watchonly: Option<bool>,
358+
) -> get_balances::Response;
359+
337360
/// Returns the total value of funds stored in the node's wallet.
338361
///
339362
/// TODO: Currently watchonly addresses cannot be omitted; `include_watchonly` must be
@@ -698,6 +721,14 @@ impl WalletRpcServer for WalletRpcImpl {
698721
.await
699722
}
700723

724+
async fn get_balances(
725+
&self,
726+
minconf: Option<u32>,
727+
include_watchonly: Option<bool>,
728+
) -> get_balances::Response {
729+
get_balances::call(self.wallet().await?.as_ref(), minconf, include_watchonly)
730+
}
731+
701732
async fn z_get_total_balance(
702733
&self,
703734
minconf: Option<u32>,
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
use std::num::NonZeroU32;
2+
3+
use documented::Documented;
4+
use jsonrpsee::core::RpcResult;
5+
use schemars::JsonSchema;
6+
use serde::Serialize;
7+
use zcash_client_backend::data_api::{WalletRead, wallet::ConfirmationsPolicy};
8+
use zcash_protocol::value::Zatoshis;
9+
10+
use crate::components::{
11+
database::DbConnection,
12+
json_rpc::{
13+
server::LegacyCode,
14+
utils::{JsonZec, value_from_zatoshis},
15+
},
16+
};
17+
18+
/// Response to a `z_getbalances` RPC request.
19+
pub(crate) type Response = RpcResult<ResultType>;
20+
pub(crate) type ResultType = Balances;
21+
22+
/// The balances available for each independent spending authority held by the wallet.
23+
#[derive(Clone, Debug, Serialize, Documented, JsonSchema)]
24+
pub(crate) struct Balances {
25+
/// The balances held by each Unified Account spending authority in the wallet.
26+
accounts: Vec<AccountBalance>,
27+
28+
/// The balance of transparent funds held by legacy transparent keys.
29+
///
30+
/// All funds held in legacy transparent addresses are treated as though they are
31+
/// associated with a single spending authority.
32+
///
33+
/// Omitted if `features.legacy_pool_seed_fingerprint` is unset in the Zallet config,
34+
/// or no legacy transparent funds are present.
35+
legacy_transparent: Option<TransparentBalance>,
36+
37+
/// The total of all funds for which this wallet controls spending keys.
38+
total: Balance,
39+
}
40+
41+
#[derive(Clone, Debug, Serialize, JsonSchema)]
42+
struct AccountBalance {
43+
/// The account's UUID within this Zallet instance.
44+
account_uuid: String,
45+
46+
/// The balance held by the account in the transparent pool.
47+
///
48+
/// Omitted if no transparent funds are present.
49+
transparent: Option<TransparentBalance>,
50+
51+
/// The balance held by the account in the Sapling shielded pool.
52+
///
53+
/// Omitted if no Sapling funds are present.
54+
sapling: Option<Balance>,
55+
56+
/// The balance held by the account in the Orchard shielded pool.
57+
///
58+
/// Omitted if no Orchard funds are present.
59+
orchard: Option<Balance>,
60+
61+
/// The total funds in all poold held by the account.
62+
total: Balance,
63+
}
64+
65+
#[derive(Clone, Debug, Serialize, JsonSchema)]
66+
struct TransparentBalance {
67+
/// The transparent balance excluding coinbase outputs.
68+
///
69+
/// Omitted if no non-coinbase transparent funds are present.
70+
regular: Option<Balance>,
71+
72+
/// The transparent balance in coinbase outputs that have reached maturity.
73+
///
74+
/// Omitted if no unspent mature transparent coinbase outputs are present.
75+
coinbase: Option<Balance>,
76+
77+
/// The transparent balance in coinbase outputs that have not yet reached maturity.
78+
///
79+
/// Omitted if no unspent immature transparent coinbase outputs are present.
80+
coinbase_immature: Option<Balance>,
81+
82+
/// The total transparent funds.
83+
total: Balance,
84+
}
85+
86+
#[derive(Clone, Debug, Serialize, JsonSchema)]
87+
struct Balance {
88+
/// The balance in ZEC.
89+
value: JsonZec,
90+
91+
/// The balance in zatoshis.
92+
#[serde(rename = "valueZat")]
93+
value_zat: u64,
94+
}
95+
96+
pub(super) const PARAM_MINCONF_DESC: &str =
97+
"Only include transactions confirmed at least this many times.";
98+
pub(super) const PARAM_INCLUDE_WATCHONLY_DESC: &str =
99+
"Also include balance in watchonly addresses.";
100+
101+
pub(crate) fn call(
102+
wallet: &DbConnection,
103+
minconf: Option<u32>,
104+
include_watchonly: Option<bool>,
105+
) -> Response {
106+
match include_watchonly {
107+
Some(true) => Ok(()),
108+
None | Some(false) => Err(LegacyCode::Misc
109+
.with_message("include_watchonly argument must be set to true (for now)")),
110+
}?;
111+
112+
let confirmations_policy = match minconf {
113+
Some(minconf) => match NonZeroU32::new(minconf) {
114+
Some(c) => ConfirmationsPolicy::new_symmetrical(c, false),
115+
None => ConfirmationsPolicy::new_symmetrical(NonZeroU32::MIN, true),
116+
},
117+
None => ConfirmationsPolicy::new_symmetrical(NonZeroU32::MIN, false),
118+
};
119+
120+
let summary = match wallet
121+
.get_wallet_summary(confirmations_policy)
122+
.map_err(|e| LegacyCode::Database.with_message(e.to_string()))?
123+
{
124+
Some(summary) => summary,
125+
None => return Err(LegacyCode::InWarmup.with_static("Wallet sync required")),
126+
};
127+
128+
let total = summary
129+
.account_balances()
130+
.values()
131+
.map(|account| account.spendable_value())
132+
.sum::<Option<Zatoshis>>()
133+
.ok_or(
134+
LegacyCode::Database
135+
.with_static("Wallet database is corrupt: storing more than MAX_MONEY"),
136+
)?;
137+
138+
let accounts = summary
139+
.account_balances()
140+
.iter()
141+
.map(|(account_uuid, account)| {
142+
// TODO: Separate out transparent coinbase.
143+
let transparent_regular = account.unshielded_balance().spendable_value();
144+
let transparent_coinbase = Zatoshis::ZERO;
145+
let transparent_coinbase_immature = Zatoshis::ZERO;
146+
147+
let sapling = account.sapling_balance().spendable_value();
148+
let orchard = account.orchard_balance().spendable_value();
149+
150+
let total = [
151+
transparent_regular,
152+
transparent_coinbase,
153+
transparent_coinbase_immature,
154+
sapling,
155+
orchard,
156+
]
157+
.iter()
158+
.sum::<Option<_>>()
159+
.ok_or(
160+
LegacyCode::Database
161+
.with_static("Wallet database is corrupt: storing more than MAX_MONEY"),
162+
)?;
163+
164+
Ok(AccountBalance {
165+
account_uuid: account_uuid.expose_uuid().to_string(),
166+
transparent: opt_transparent_balance(
167+
transparent_regular,
168+
transparent_coinbase,
169+
transparent_coinbase_immature,
170+
)?,
171+
sapling: opt_balance(sapling),
172+
orchard: opt_balance(orchard),
173+
total: balance(total),
174+
})
175+
})
176+
.collect::<RpcResult<_>>()?;
177+
178+
Ok(Balances {
179+
accounts,
180+
// TODO: Fetch legacy transparent balance once supported.
181+
legacy_transparent: None,
182+
total: balance(total),
183+
})
184+
}
185+
186+
fn opt_transparent_balance(
187+
regular: Zatoshis,
188+
coinbase: Zatoshis,
189+
coinbase_immature: Zatoshis,
190+
) -> RpcResult<Option<TransparentBalance>> {
191+
if regular.is_zero() && coinbase.is_zero() && coinbase_immature.is_zero() {
192+
Ok(None)
193+
} else {
194+
Ok(Some(TransparentBalance {
195+
regular: opt_balance(regular),
196+
coinbase: opt_balance(coinbase),
197+
coinbase_immature: opt_balance(coinbase_immature),
198+
total: balance(
199+
[regular, coinbase, coinbase_immature]
200+
.iter()
201+
.sum::<Option<_>>()
202+
.ok_or(
203+
LegacyCode::Database
204+
.with_static("Wallet database is corrupt: storing more than MAX_MONEY"),
205+
)?,
206+
),
207+
}))
208+
}
209+
}
210+
211+
fn balance(value: Zatoshis) -> Balance {
212+
Balance {
213+
value: value_from_zatoshis(value),
214+
value_zat: value.into_u64(),
215+
}
216+
}
217+
218+
fn opt_balance(value: Zatoshis) -> Option<Balance> {
219+
(!value.is_zero()).then(|| Balance {
220+
value: value_from_zatoshis(value),
221+
value_zat: value.into_u64(),
222+
})
223+
}

0 commit comments

Comments
 (0)