1use std::collections::{BTreeMap, HashMap};
6use std::time::SystemTime;
7
8use bip0039::Mnemonic;
9use rand::Rng;
10use rand::rngs::OsRng;
11
12use zcash_client_backend::tor;
13use zcash_keys::address::UnifiedAddress;
14use zcash_primitives::legacy::keys::NonHardenedChildIndex;
15use zcash_primitives::{consensus::BlockHeight, transaction::TxId};
16
17use crate::config::ChainType;
18use error::{PriceError, WalletError};
19use keys::unified::{UnifiedAddressId, UnifiedKeyStore};
20use pepper_sync::keys::transparent::{self, TransparentScope};
21use pepper_sync::wallet::ShardTrees;
22use pepper_sync::{
23 keys::transparent::TransparentAddressId,
24 wallet::{Locator, NullifierMap, OutputId, SyncState, WalletBlock, WalletTransaction},
25};
26use send::SendProgress;
27use zingo_price::PriceList;
28
29pub mod data;
30pub mod error;
31pub mod keys;
32pub(crate) mod legacy;
33pub mod traits;
34pub mod utils;
35
36pub mod describe;
38pub mod disk;
39pub mod output;
40pub mod propose;
41pub mod send;
42pub mod summary;
43pub mod sync;
44pub mod transaction;
45mod zcb_traits;
46
47pub fn now() -> u32 {
50 SystemTime::now()
51 .duration_since(SystemTime::UNIX_EPOCH)
52 .expect("should never fail when comparing with an instant so far in the past")
53 .as_secs() as u32
54}
55
56pub enum WalletBase {
58 FreshEntropy,
60 SeedBytes([u8; 32]),
62 MnemonicPhrase(String),
64 Mnemonic(Mnemonic),
66 SeedBytesAndAccount([u8; 32], u32),
68 MnemonicPhraseAndAccount(String, u32),
70 MnemonicAndAccount(Mnemonic, u32),
72 Ufvk(String),
74 Usk(Vec<u8>),
76}
77
78impl WalletBase {
79 pub fn from_string(base: String) -> WalletBase {
81 if (&base[0..5]) == "uview" {
82 WalletBase::Ufvk(base)
83 } else {
84 WalletBase::MnemonicPhrase(base)
85 }
86 }
87}
88
89#[derive(Debug)]
101pub struct LightWallet {
102 pub network: ChainType,
104 mnemonic: Option<(Mnemonic, u32)>,
108 pub birthday: BlockHeight,
110 pub unified_key_store: UnifiedKeyStore,
112 pub unified_addresses: BTreeMap<UnifiedAddressId, UnifiedAddress>,
114 pub transparent_addresses: BTreeMap<TransparentAddressId, String>,
116 pub wallet_blocks: BTreeMap<BlockHeight, WalletBlock>,
118 pub wallet_transactions: HashMap<TxId, WalletTransaction>,
120 pub nullifier_map: NullifierMap,
122 pub outpoint_map: BTreeMap<OutputId, Locator>,
124 pub shard_trees: ShardTrees,
126 pub sync_state: SyncState,
128 pub wallet_settings: WalletSettings,
130 pub price_list: PriceList,
132 pub send_progress: SendProgress,
134 pub save_required: bool,
136}
137
138impl LightWallet {
139 pub fn new(
144 network: ChainType,
145 wallet_base: WalletBase,
146 birthday: BlockHeight,
147 wallet_settings: WalletSettings,
148 ) -> Result<Self, WalletError> {
149 let (unified_key_store, mnemonic) = match wallet_base {
150 WalletBase::FreshEntropy => {
151 let mut seed_bytes = [0u8; 32];
152 let mut system_rng = OsRng;
154 system_rng.fill(&mut seed_bytes);
155 return Self::new(
156 network,
157 WalletBase::SeedBytes(seed_bytes),
158 birthday,
159 wallet_settings,
160 );
161 }
162 WalletBase::SeedBytes(seed_bytes) => {
163 return Self::new(
164 network,
165 WalletBase::SeedBytesAndAccount(seed_bytes, 0),
166 birthday,
167 wallet_settings,
168 );
169 }
170 WalletBase::SeedBytesAndAccount(seed_bytes, account_index) => {
171 let mnemonic = Mnemonic::from_entropy(seed_bytes)?;
172 return Self::new(
173 network,
174 WalletBase::MnemonicAndAccount(mnemonic, account_index),
175 birthday,
176 wallet_settings,
177 );
178 }
179 WalletBase::MnemonicPhrase(phrase) => {
180 return Self::new(
181 network,
182 WalletBase::MnemonicPhraseAndAccount(phrase, 0),
183 birthday,
184 wallet_settings,
185 );
186 }
187 WalletBase::MnemonicPhraseAndAccount(phrase, account_index) => {
188 let mnemonic = Mnemonic::<bip0039::English>::from_phrase(phrase)?;
189 return Self::new(
190 network,
191 WalletBase::MnemonicAndAccount(mnemonic, account_index),
192 birthday,
193 wallet_settings,
194 );
195 }
196 WalletBase::Mnemonic(mnemonic) => {
197 return Self::new(
198 network,
199 WalletBase::MnemonicAndAccount(mnemonic, 0),
200 birthday,
201 wallet_settings,
202 );
203 }
204 WalletBase::MnemonicAndAccount(mnemonic, account_index) => {
205 let unified_key_store =
206 UnifiedKeyStore::new_from_mnemonic(&network, &mnemonic, account_index)?;
207 (unified_key_store, Some((mnemonic, account_index)))
208 }
209 WalletBase::Ufvk(ufvk_encoded) => {
210 let unified_key_store = UnifiedKeyStore::new_from_ufvk(&network, ufvk_encoded)?;
211 (unified_key_store, None)
212 }
213 WalletBase::Usk(unified_spending_key) => {
214 let unified_key_store =
215 UnifiedKeyStore::new_from_usk(unified_spending_key.as_slice())?;
216 (unified_key_store, None)
217 }
218 };
219
220 let first_address_index = 0;
221 let first_unified_address = unified_key_store.generate_unified_address(
222 first_address_index,
223 unified_key_store.can_view(),
224 false,
225 )?;
226 let mut unified_addresses = BTreeMap::new();
227 unified_addresses.insert(
228 UnifiedAddressId {
229 account_id: zip32::AccountId::ZERO,
230 address_index: first_address_index,
231 },
232 first_unified_address.clone(),
233 );
234
235 let mut transparent_addresses = BTreeMap::new();
236 if let Some(transparent_address) = first_unified_address.transparent() {
237 transparent_addresses.insert(
238 TransparentAddressId::new(
239 zip32::AccountId::ZERO,
240 TransparentScope::External,
241 NonHardenedChildIndex::from_index(first_address_index).expect("infallible"),
242 ),
243 transparent::encode_address(&network, *transparent_address),
244 );
245 }
246
247 Ok(Self {
248 network,
249 mnemonic,
250 birthday: BlockHeight::from_u32(birthday.into()),
251 unified_key_store,
252 unified_addresses,
253 transparent_addresses,
254 wallet_blocks: BTreeMap::new(),
255 wallet_transactions: HashMap::new(),
256 nullifier_map: NullifierMap::new(),
257 outpoint_map: BTreeMap::new(),
258 shard_trees: ShardTrees::new(),
259 sync_state: SyncState::new(),
260 wallet_settings,
261 price_list: PriceList::new(),
262 save_required: true,
263 send_progress: SendProgress::new(0),
264 })
265 }
266
267 pub(super) fn set_send_result(&mut self, result: String) {
269 self.send_progress.is_send_in_progress = false;
270 self.send_progress.last_result = Some(result);
271 }
272
273 pub async fn save(&mut self) -> std::io::Result<Option<Vec<u8>>> {
280 if self.save_required {
281 let network = self.network;
282 let mut wallet_bytes: Vec<u8> = vec![];
283 self.write(&mut wallet_bytes, &network).await?;
284 self.save_required = false;
285 Ok(Some(wallet_bytes))
286 } else {
287 Ok(None)
288 }
289 }
290
291 pub async fn update_current_price(
296 &mut self,
297 tor_client: Option<&tor::Client>,
298 ) -> Result<f32, PriceError> {
299 let current_price = self
300 .price_list
301 .update_current_price(tor_client)
302 .await?
303 .price_usd;
304 self.save_required = true;
305
306 Ok(current_price)
307 }
308
309 pub async fn update_historical_prices(&mut self) -> Result<(), PriceError> {
316 if self
317 .price_list
318 .time_historical_prices_last_updated()
319 .is_none()
320 {
321 let Some(birthday) = self.sync_state.wallet_birthday() else {
322 return Err(PriceError::NotInitialised);
323 };
324 let birthday_block = match self.wallet_blocks.get(&birthday) {
325 Some(block) => block.clone(),
326 None => {
327 return Err(PriceError::NotInitialised);
328 }
329 };
330 self.price_list.set_start_time(birthday_block.time());
331 }
332 self.price_list.update_historical_price_list().await?;
333 self.prune_price_list();
334 self.save_required = true;
335
336 todo!()
337 }
338
339 pub fn prune_price_list(&mut self) {
344 let Some(fully_scanned_height) = self.sync_state.fully_scanned_height() else {
345 return;
346 };
347 let transaction_times = self
348 .wallet_transactions
349 .values()
350 .filter(|transaction| {
351 transaction
352 .status()
353 .get_confirmed_height()
354 .is_some_and(|height| height <= fully_scanned_height)
355 })
356 .map(|transaction| transaction.datetime())
357 .collect();
358
359 let prune_below = self
360 .wallet_blocks
361 .get(&fully_scanned_height)
362 .expect("fully scanned height should always be on a scan range boundary")
363 .time();
364 self.price_list.prune(transaction_times, prune_below);
365 }
366
367 pub fn clear_all(&mut self) {
372 self.sync_state = SyncState::new();
373 pepper_sync::add_scan_targets(
374 &mut self.sync_state,
375 &self
376 .wallet_transactions
377 .values()
378 .filter_map(|transaction| {
379 transaction
380 .status()
381 .get_confirmed_height()
382 .map(|height| (height, transaction.txid()))
383 })
384 .collect::<Vec<_>>(),
385 );
386
387 self.wallet_blocks.clear();
388 self.wallet_transactions.clear();
389 self.nullifier_map.clear();
390 self.outpoint_map.clear();
391 self.shard_trees = ShardTrees::new();
392 self.price_list = PriceList::new();
393
394 self.save_required = true;
395 }
396}
397
398#[derive(Debug, Clone)]
400pub struct WalletSettings {
401 pub sync_config: pepper_sync::sync::SyncConfig,
403}
404
405#[cfg(test)]
406mod tests {
407 use incrementalmerkletree::frontier::CommitmentTree;
408 use orchard::tree::MerkleHashOrchard;
409
410 #[test]
411 fn anchor_from_tree_works() {
412 let commitments = [
417 [
418 0x68, 0x13, 0x5c, 0xf4, 0x99, 0x33, 0x22, 0x90, 0x99, 0xa4, 0x4e, 0xc9, 0x9a, 0x75,
419 0xe1, 0xe1, 0xcb, 0x46, 0x40, 0xf9, 0xb5, 0xbd, 0xec, 0x6b, 0x32, 0x23, 0x85, 0x6f,
420 0xea, 0x16, 0x39, 0x0a,
421 ],
422 [
423 0x78, 0x31, 0x50, 0x08, 0xfb, 0x29, 0x98, 0xb4, 0x30, 0xa5, 0x73, 0x1d, 0x67, 0x26,
424 0x20, 0x7d, 0xc0, 0xf0, 0xec, 0x81, 0xea, 0x64, 0xaf, 0x5c, 0xf6, 0x12, 0x95, 0x69,
425 0x01, 0xe7, 0x2f, 0x0e,
426 ],
427 [
428 0xee, 0x94, 0x88, 0x05, 0x3a, 0x30, 0xc5, 0x96, 0xb4, 0x30, 0x14, 0x10, 0x5d, 0x34,
429 0x77, 0xe6, 0xf5, 0x78, 0xc8, 0x92, 0x40, 0xd1, 0xd1, 0xee, 0x17, 0x43, 0xb7, 0x7b,
430 0xb6, 0xad, 0xc4, 0x0a,
431 ],
432 [
433 0x9d, 0xdc, 0xe7, 0xf0, 0x65, 0x01, 0xf3, 0x63, 0x76, 0x8c, 0x5b, 0xca, 0x3f, 0x26,
434 0x46, 0x60, 0x83, 0x4d, 0x4d, 0xf4, 0x46, 0xd1, 0x3e, 0xfc, 0xd7, 0xc6, 0xf1, 0x7b,
435 0x16, 0x7a, 0xac, 0x1a,
436 ],
437 [
438 0xbd, 0x86, 0x16, 0x81, 0x1c, 0x6f, 0x5f, 0x76, 0x9e, 0xa4, 0x53, 0x9b, 0xba, 0xff,
439 0x0f, 0x19, 0x8a, 0x6c, 0xdf, 0x3b, 0x28, 0x0d, 0xd4, 0x99, 0x26, 0x16, 0x3b, 0xd5,
440 0x3f, 0x53, 0xa1, 0x21,
441 ],
442 ];
443 let mut orchard_tree: CommitmentTree<MerkleHashOrchard, 32> = CommitmentTree::empty();
444 for commitment in commitments {
445 orchard_tree
446 .append(MerkleHashOrchard::from_bytes(&commitment).unwrap())
447 .unwrap()
448 }
449 let anchor = [
452 0xc8, 0x75, 0xbe, 0x2d, 0x60, 0x87, 0x3f, 0x8b, 0xcd, 0xeb, 0x91, 0x28, 0x2e, 0x64,
453 0x2e, 0x0c, 0xc6, 0x5f, 0xf7, 0xd0, 0x64, 0x2d, 0x13, 0x7b, 0x28, 0xcf, 0x28, 0xcc,
454 0x9c, 0x52, 0x7f, 0x0e,
455 ];
456 let anchor = orchard::Anchor::from(MerkleHashOrchard::from_bytes(&anchor).unwrap());
457 assert_eq!(orchard::Anchor::from(orchard_tree.root()), anchor);
458 }
459}