Skip to main content

zingolib/
wallet.rs

1//! In all cases in this file "external_version" refers to a serialization version that is interpreted
2//! from a source outside of the code-base e.g. a wallet-file.
3//! TODO: Add Mod Description Here
4
5use 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
36//these mods contain pieces of the impl LightWallet
37pub 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
47/// TODO: Add Doc Comment Here!
48// TODO: move to utils
49pub 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
56/// Data used to initialize new instance of LightWallet
57pub enum WalletBase {
58    /// Generate a wallet with a new seed.
59    FreshEntropy,
60    /// Generate a wallet from a seed (account index = 0).
61    SeedBytes([u8; 32]),
62    /// Generate a wallet from a mnemonic phrase (account index = 0).
63    MnemonicPhrase(String),
64    /// Generate a wallet from a mnemonic (account index = 0).
65    Mnemonic(Mnemonic),
66    /// Generate a wallet from a seed and account index.
67    SeedBytesAndAccount([u8; 32], u32),
68    /// Generate a wallet from a mnemonic phrase and account index.
69    MnemonicPhraseAndAccount(String, u32),
70    /// Generate a wallet from a mnemonic and account index.
71    MnemonicAndAccount(Mnemonic, u32),
72    /// Generate a wallet from a unified full viewing key.
73    Ufvk(String),
74    /// Generate a wallet from a unified spending key.
75    Usk(Vec<u8>),
76}
77
78impl WalletBase {
79    /// TODO: Add Doc Comment Here!
80    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/// In-memory wallet data struct
90///
91/// The `mnemonic` can be `None` in the case of a wallet created directly from UFVKs or USKs.
92///
93/// As no relevant transactions related to this wallet will exist below the wallet's birthday, sync will start from
94/// `birthday` block height.
95///
96/// When wallet state is changed due to sync, send or creating addresses, `save_required` will be set to `true`
97/// automatically. Calling [`crate::wallet::LightWallet::save`] will serialize the wallet and reset `save_required`
98/// to false, returning the bytes to be persisted. Also see [`crate::lightclient::LightClient::save_task`] and related
99/// methods for a save task implementation.
100#[derive(Debug)]
101pub struct LightWallet {
102    /// Network type
103    pub network: ChainType,
104    /// The seed for the wallet, stored as a zip339 Mnemonic, and the account index.
105    // TODO: we seem to support generating keys for a single account of choice which is stored here, this should be
106    // reworked to support multiple accounts during sync integration
107    mnemonic: Option<(Mnemonic, u32)>,
108    /// The block height at which the wallet was created.
109    pub birthday: BlockHeight,
110    /// Unified key store
111    pub unified_key_store: UnifiedKeyStore,
112    /// Unified_addresses
113    pub unified_addresses: BTreeMap<UnifiedAddressId, UnifiedAddress>,
114    /// Transparent addresses
115    pub transparent_addresses: BTreeMap<TransparentAddressId, String>,
116    /// Wallet blocks
117    pub wallet_blocks: BTreeMap<BlockHeight, WalletBlock>,
118    /// Wallet transactions
119    pub wallet_transactions: HashMap<TxId, WalletTransaction>,
120    /// Nullifier map
121    pub nullifier_map: NullifierMap,
122    /// Outpoint map
123    pub outpoint_map: BTreeMap<OutputId, Locator>,
124    /// Shard trees
125    pub shard_trees: ShardTrees,
126    /// Sync state
127    pub sync_state: SyncState,
128    /// Wallet settings.
129    pub wallet_settings: WalletSettings,
130    /// The current and historical daily price of zec.
131    pub price_list: PriceList,
132    /// Progress of an outgoing transaction
133    pub send_progress: SendProgress,
134    /// Boolean for tracking whether the wallet state has changed since last save.
135    pub save_required: bool,
136}
137
138impl LightWallet {
139    /// Create a new in-memory wallet.
140    ///
141    /// For wallets from fresh entropy, it is worth considering setting `birthday` to 100 blocks below current height
142    /// of block chain to protect from re-orgs.
143    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                // Create a random seed.
153                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    // Set the previous send's result as a JSON string.
268    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    /// If the wallet state has changed since last save, serializes the wallet and returns the wallet bytes.
274    /// Returns `Ok(None)` if the wallet state has not changed and save is not required.
275    /// Returns error if serialization fails.
276    ///
277    /// Intended to be called from a save task which calls `save` in a loop, awaiting the wallet lock and checking
278    /// `self.save_required` status, writing the returned wallet bytes to persistance.
279    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    /// Update and return current price of ZEC.
292    ///
293    /// Will fetch via tor if a `tor_client` is provided.
294    /// Currently only USD is supported.
295    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    /// Updates historical daily price list.
310    /// Prunes any unused price data in the wallet after it's been updated.
311    /// If this is the first time update has been called, initialises the price list from the wallet data.
312    ///
313    /// Currently only USD is supported.
314    // TODO: under development
315    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    /// Prunes historical prices to days containing transactions in the wallet.
340    ///
341    /// Avoids pruning above fully scanned height.
342    // TODO: under development
343    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    /// Clears all wallet data obtained from the block chain including the sync state.
368    ///
369    /// Adds locators to the new sync state to prioritise scanning relevant parts of the chain on rescan.
370    /// Addresses are not cleared.
371    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/// Wallet settings.
399#[derive(Debug, Clone)]
400pub struct WalletSettings {
401    /// Sync configuration.
402    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        // These commitment values copied from zcash/orchard, and were originally derived from the bundle
413        // data that was generated for testing commitment tree construction inside of zcashd here.
414        // https://github.com/zcash/zcash/blob/ecec1f9769a5e37eb3f7fd89a4fcfb35bc28eed7/src/test/data/merkle_roots_orchard.h
415
416        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        // This value was produced by the Python test vector generation code implemented here:
450        // https://github.com/zcash-hackworks/zcash-test-vectors/blob/f4d756410c8f2456f5d84cedf6dac6eb8c068eed/orchard_merkle_tree.py
451        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}