dusk_wallet/
wallet.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) DUSK NETWORK. All rights reserved.
6
7mod address;
8mod file;
9pub mod gas;
10
11pub use address::Address;
12use dusk_plonk::prelude::BlsScalar;
13pub use file::{SecureWalletFile, WalletPath};
14
15use bip39::{Language, Mnemonic, Seed};
16use dusk_bytes::{DeserializableSlice, Serializable};
17use ff::Field;
18use flume::Receiver;
19use phoenix_core::transaction::ModuleId;
20use phoenix_core::Note;
21use rkyv::ser::serializers::AllocSerializer;
22use serde::Serialize;
23use std::fmt::Debug;
24use std::fs;
25use std::path::{Path, PathBuf};
26
27use dusk_bls12_381_sign::{PublicKey, SecretKey};
28use dusk_wallet_core::{
29    BalanceInfo, StakeInfo, StateClient, Store, Transaction,
30    Wallet as WalletCore, MAX_CALL_SIZE,
31};
32use rand::prelude::StdRng;
33use rand::SeedableRng;
34
35use dusk_pki::{PublicSpendKey, SecretSpendKey};
36
37use crate::cache::NoteData;
38use crate::clients::{Prover, StateStore};
39use crate::crypto::encrypt;
40use crate::currency::Dusk;
41use crate::dat::{
42    self, version_bytes, DatFileVersion, FILE_TYPE, LATEST_VERSION, MAGIC,
43    RESERVED,
44};
45use crate::store::LocalStore;
46use crate::{Error, RuskHttpClient};
47use gas::Gas;
48
49use crate::store;
50
51/// The interface to the Dusk Network
52///
53/// The Wallet exposes all methods available to interact with the Dusk Network.
54///
55/// A new [`Wallet`] can be created from a bip39-compatible mnemonic phrase or
56/// an existing wallet file.
57///
58/// The user can generate as many [`Address`] as needed without an active
59/// connection to the network by calling [`Wallet::new_address`] repeatedly.
60///
61/// A wallet must connect to the network using a [`RuskEndpoint`] in order to be
62/// able to perform common operations such as checking balance, transfernig
63/// funds, or staking Dusk.
64pub struct Wallet<F: SecureWalletFile + Debug> {
65    wallet: Option<WalletCore<LocalStore, StateStore, Prover>>,
66    addresses: Vec<Address>,
67    store: LocalStore,
68    file: Option<F>,
69    file_version: Option<DatFileVersion>,
70    status: fn(status: &str),
71    /// Recieve the status/errors of the sync procss
72    pub sync_rx: Option<Receiver<String>>,
73}
74
75impl<F: SecureWalletFile + Debug> Wallet<F> {
76    /// Returns the file used for the wallet
77    pub fn file(&self) -> &Option<F> {
78        &self.file
79    }
80
81    /// Returns spending key pair for a given address
82    pub fn spending_keys(
83        &self,
84        addr: &Address,
85    ) -> Result<(PublicSpendKey, SecretSpendKey), Error> {
86        // make sure we own the address
87        if !addr.is_owned() {
88            return Err(Error::Unauthorized);
89        }
90
91        let index = addr.index()? as u64;
92
93        // retrieve keys
94        let ssk = self.store.retrieve_ssk(index)?;
95        let psk: PublicSpendKey = ssk.public_spend_key();
96
97        Ok((psk, ssk))
98    }
99}
100
101impl<F: SecureWalletFile + Debug> Wallet<F> {
102    /// Creates a new wallet instance deriving its seed from a valid BIP39
103    /// mnemonic
104    pub fn new<P>(phrase: P) -> Result<Self, Error>
105    where
106        P: Into<String>,
107    {
108        // generate mnemonic
109        let phrase: String = phrase.into();
110        let try_mnem = Mnemonic::from_phrase(&phrase, Language::English);
111
112        if let Ok(mnemonic) = try_mnem {
113            // derive the mnemonic seed
114            let seed = Seed::new(&mnemonic, "");
115            // Takes the mnemonic seed as bytes
116            let mut bytes = seed.as_bytes();
117
118            // Generate a Store Seed type from the mnemonic Seed bytes
119            let seed = store::Seed::from_reader(&mut bytes)?;
120
121            let store = LocalStore::new(seed);
122
123            // Generate the default address
124            let ssk = store
125                .retrieve_ssk(0)
126                .expect("wallet seed should be available");
127
128            let address = Address::new(0, ssk.public_spend_key());
129
130            // return new wallet instance
131            Ok(Wallet {
132                wallet: None,
133                addresses: vec![address],
134                store,
135                file: None,
136                file_version: None,
137                status: |_| {},
138                sync_rx: None,
139            })
140        } else {
141            Err(Error::InvalidMnemonicPhrase)
142        }
143    }
144
145    /// Loads wallet given a session
146    pub fn from_file(file: F) -> Result<Self, Error> {
147        let path = file.path();
148        let pwd = file.pwd();
149
150        // make sure file exists
151        let pb = path.inner().clone();
152        if !pb.is_file() {
153            return Err(Error::WalletFileMissing);
154        }
155
156        // attempt to load and decode wallet
157        let bytes = fs::read(&pb)?;
158
159        let file_version = dat::check_version(bytes.get(0..12))?;
160
161        let (seed, address_count) =
162            dat::get_seed_and_address(file_version, bytes, pwd)?;
163
164        let store = LocalStore::new(seed);
165
166        // return early if its legacy
167        if let DatFileVersion::Legacy = file_version {
168            let ssk = store
169                .retrieve_ssk(0)
170                .expect("wallet seed should be available");
171
172            let address = Address::new(0, ssk.public_spend_key());
173
174            // return the store
175            return Ok(Self {
176                wallet: None,
177                addresses: vec![address],
178                store,
179                file: Some(file),
180                file_version: Some(DatFileVersion::Legacy),
181                status: |_| {},
182                sync_rx: None,
183            });
184        }
185
186        let addresses: Vec<_> = (0..address_count)
187            .map(|i| {
188                let ssk = store
189                    .retrieve_ssk(i as u64)
190                    .expect("wallet seed should be available");
191
192                Address::new(i, ssk.public_spend_key())
193            })
194            .collect();
195
196        // create and return
197        Ok(Self {
198            wallet: None,
199            addresses,
200            store,
201            file: Some(file),
202            file_version: Some(file_version),
203            status: |_| {},
204            sync_rx: None,
205        })
206    }
207
208    /// Saves wallet to file from which it was loaded
209    pub fn save(&mut self) -> Result<(), Error> {
210        match &self.file {
211            Some(f) => {
212                let mut header = Vec::with_capacity(12);
213                header.extend_from_slice(&MAGIC.to_be_bytes());
214                // File type = Rusk Wallet (0x02)
215                header.extend_from_slice(&FILE_TYPE.to_be_bytes());
216                // Reserved (0x0)
217                header.extend_from_slice(&RESERVED.to_be_bytes());
218                // Version
219                header.extend_from_slice(&version_bytes(LATEST_VERSION));
220
221                // create file payload
222                let seed = self.store.get_seed()?;
223                let mut payload = seed.to_vec();
224
225                payload.push(self.addresses.len() as u8);
226
227                // encrypt the payload
228                payload = encrypt(&payload, f.pwd())?;
229
230                let mut content =
231                    Vec::with_capacity(header.len() + payload.len());
232
233                content.extend_from_slice(&header);
234                content.extend_from_slice(&payload);
235
236                // write the content to file
237                fs::write(&f.path().wallet, content)?;
238                Ok(())
239            }
240            None => Err(Error::WalletFileMissing),
241        }
242    }
243
244    /// Saves wallet to the provided file, changing the previous file path for
245    /// the wallet if any. Note that any subsequent calls to [`save`] will
246    /// use this new file.
247    pub fn save_to(&mut self, file: F) -> Result<(), Error> {
248        // set our new file and save
249        self.file = Some(file);
250        self.save()
251    }
252
253    /// Connect the wallet to the network providing a callback for status
254    /// updates
255    pub async fn connect_with_status<S>(
256        &mut self,
257        rusk_addr: S,
258        prov_addr: S,
259        status: fn(&str),
260    ) -> Result<(), Error>
261    where
262        S: Into<String>,
263    {
264        // attempt connection
265        let http_state = RuskHttpClient::new(rusk_addr.into());
266        let http_prover = RuskHttpClient::new(prov_addr.into());
267
268        let state_status = http_state.check_connection().await;
269        let prover_status = http_prover.check_connection().await;
270
271        match (&state_status, prover_status) {
272            (Err(e),_)=> println!("Connection to Rusk Failed, some operations won't be available: {e}"),
273            (_,Err(e))=> println!("Connection to Prover Failed, some operations won't be available: {e}"),
274            _=> {},
275        }
276
277        // create a prover client
278        let mut prover = Prover::new(http_state.clone(), http_prover.clone());
279        prover.set_status_callback(status);
280
281        let cache_dir = {
282            if let Some(file) = &self.file {
283                file.path().cache_dir()
284            } else {
285                return Err(Error::WalletFileMissing);
286            }
287        };
288
289        // create a state client
290        let state = StateStore::new(
291            http_state,
292            &cache_dir,
293            self.store.clone(),
294            status,
295        )?;
296
297        // create wallet instance
298        self.wallet = Some(WalletCore::new(self.store.clone(), state, prover));
299
300        // set our own status callback
301        self.status = status;
302
303        Ok(())
304    }
305
306    /// Sync wallet state
307    pub async fn sync(&self) -> Result<(), Error> {
308        self.connected_wallet().await?.state().sync().await
309    }
310
311    /// Helper function to register for async-sync outside of connect
312    pub async fn register_sync(&mut self) -> Result<(), Error> {
313        match self.wallet.as_ref() {
314            Some(w) => {
315                let (sync_tx, sync_rx) = flume::unbounded::<String>();
316                w.state().register_sync(sync_tx).await?;
317                self.sync_rx = Some(sync_rx);
318                Ok(())
319            }
320            None => Err(Error::Offline),
321        }
322    }
323
324    /// Checks if the wallet has an active connection to the network
325    pub async fn is_online(&self) -> bool {
326        match self.wallet.as_ref() {
327            Some(w) => w.state().check_connection().await.is_ok(),
328            None => false,
329        }
330    }
331
332    pub(crate) async fn connected_wallet(
333        &self,
334    ) -> Result<&WalletCore<LocalStore, StateStore, Prover>, Error> {
335        match self.wallet.as_ref() {
336            Some(w) => {
337                w.state().check_connection().await?;
338                Ok(w)
339            }
340            None => Err(Error::Offline),
341        }
342    }
343
344    /// Fetches the notes from the state.
345    pub async fn get_all_notes(
346        &self,
347        addr: &Address,
348    ) -> Result<Vec<DecodedNote>, Error> {
349        if !addr.is_owned() {
350            return Err(Error::Unauthorized);
351        }
352
353        let wallet = self.connected_wallet().await?;
354        let ssk_index = addr.index()? as u64;
355        let ssk = self.store.retrieve_ssk(ssk_index).unwrap();
356        let vk = ssk.view_key();
357        let psk = vk.public_spend_key();
358
359        let live_notes = wallet.state().fetch_notes(&vk).unwrap();
360        let spent_notes = wallet.state().cache().spent_notes(&psk)?;
361
362        let live_notes = live_notes
363            .into_iter()
364            .map(|(note, height)| (None, note, height));
365        let spent_notes = spent_notes.into_iter().map(
366            |(nullifier, NoteData { note, height })| {
367                (Some(nullifier), note, height)
368            },
369        );
370        let history = live_notes
371            .chain(spent_notes)
372            .map(|(nullified_by, note, block_height)| {
373                let amount = note.value(Some(&vk)).unwrap();
374                DecodedNote {
375                    note,
376                    amount,
377                    block_height,
378                    nullified_by,
379                }
380            })
381            .collect();
382
383        Ok(history)
384    }
385
386    /// Obtain balance information for a given address
387    pub async fn get_balance(
388        &self,
389        addr: &Address,
390    ) -> Result<BalanceInfo, Error> {
391        // make sure we own this address
392        if !addr.is_owned() {
393            return Err(Error::Unauthorized);
394        }
395
396        // get balance
397        if let Some(wallet) = &self.wallet {
398            let index = addr.index()? as u64;
399            Ok(wallet.get_balance(index)?)
400        } else {
401            Err(Error::Offline)
402        }
403    }
404
405    /// Creates a new public address.
406    /// The addresses generated are deterministic across sessions.
407    pub fn new_address(&mut self) -> &Address {
408        let len = self.addresses.len();
409        let ssk = self
410            .store
411            .retrieve_ssk(len as u64)
412            .expect("wallet seed should be available");
413        let addr = Address::new(len as u8, ssk.public_spend_key());
414
415        self.addresses.push(addr);
416        self.addresses.last().unwrap()
417    }
418
419    /// Default public address for this wallet
420    pub fn default_address(&self) -> &Address {
421        &self.addresses[0]
422    }
423
424    /// Addresses that have been generated by the user
425    pub fn addresses(&self) -> &Vec<Address> {
426        &self.addresses
427    }
428
429    /// Executes a generic contract call
430    pub async fn execute<C>(
431        &self,
432        sender: &Address,
433        contract_id: ModuleId,
434        call_name: String,
435        call_data: C,
436        gas: Gas,
437    ) -> Result<Transaction, Error>
438    where
439        C: rkyv::Serialize<AllocSerializer<MAX_CALL_SIZE>>,
440    {
441        let wallet = self.connected_wallet().await?;
442        // make sure we own the sender address
443        if !sender.is_owned() {
444            return Err(Error::Unauthorized);
445        }
446
447        // check gas limits
448        if !gas.is_enough() {
449            return Err(Error::NotEnoughGas);
450        }
451
452        let mut rng = StdRng::from_entropy();
453        let sender_index =
454            sender.index().expect("owned address should have an index");
455
456        // transfer
457        let tx = wallet.execute(
458            &mut rng,
459            contract_id.into(),
460            call_name,
461            call_data,
462            sender_index as u64,
463            sender.psk(),
464            gas.limit,
465            gas.price,
466        )?;
467        Ok(tx)
468    }
469
470    /// Transfers funds between addresses
471    pub async fn transfer(
472        &self,
473        sender: &Address,
474        rcvr: &Address,
475        amt: Dusk,
476        gas: Gas,
477    ) -> Result<Transaction, Error> {
478        let wallet = self.connected_wallet().await?;
479        // make sure we own the sender address
480        if !sender.is_owned() {
481            return Err(Error::Unauthorized);
482        }
483        // make sure amount is positive
484        if amt == 0 {
485            return Err(Error::AmountIsZero);
486        }
487        // check gas limits
488        if !gas.is_enough() {
489            return Err(Error::NotEnoughGas);
490        }
491
492        let mut rng = StdRng::from_entropy();
493        let ref_id = BlsScalar::random(&mut rng);
494        let sender_index =
495            sender.index().expect("owned address should have an index");
496
497        // transfer
498        let tx = wallet.transfer(
499            &mut rng,
500            sender_index as u64,
501            sender.psk(),
502            rcvr.psk(),
503            *amt,
504            gas.limit,
505            gas.price,
506            ref_id,
507        )?;
508        Ok(tx)
509    }
510
511    /// Stakes Dusk
512    pub async fn stake(
513        &self,
514        addr: &Address,
515        amt: Dusk,
516        gas: Gas,
517    ) -> Result<Transaction, Error> {
518        let wallet = self.connected_wallet().await?;
519        // make sure we own the staking address
520        if !addr.is_owned() {
521            return Err(Error::Unauthorized);
522        }
523        // make sure amount is positive
524        if amt == 0 {
525            return Err(Error::AmountIsZero);
526        }
527        // check if the gas is enough
528        if !gas.is_enough() {
529            return Err(Error::NotEnoughGas);
530        }
531
532        let mut rng = StdRng::from_entropy();
533        let sender_index = addr.index()?;
534
535        // stake
536        let tx = wallet.stake(
537            &mut rng,
538            sender_index as u64,
539            sender_index as u64,
540            addr.psk(),
541            *amt,
542            gas.limit,
543            gas.price,
544        )?;
545        Ok(tx)
546    }
547
548    /// Obtains stake information for a given address
549    pub async fn stake_info(&self, addr: &Address) -> Result<StakeInfo, Error> {
550        let wallet = self.connected_wallet().await?;
551        // make sure we own the staking address
552        if !addr.is_owned() {
553            return Err(Error::Unauthorized);
554        }
555        let index = addr.index()? as u64;
556        wallet.get_stake(index).map_err(Error::from)
557    }
558
559    /// Unstakes Dusk
560    pub async fn unstake(
561        &self,
562        addr: &Address,
563        gas: Gas,
564    ) -> Result<Transaction, Error> {
565        let wallet = self.connected_wallet().await?;
566        // make sure we own the staking address
567        if !addr.is_owned() {
568            return Err(Error::Unauthorized);
569        }
570
571        let mut rng = StdRng::from_entropy();
572        let index = addr.index()? as u64;
573
574        let tx = wallet.unstake(
575            &mut rng,
576            index,
577            index,
578            addr.psk(),
579            gas.limit,
580            gas.price,
581        )?;
582        Ok(tx)
583    }
584
585    /// Withdraw accumulated staking reward for a given address
586    pub async fn withdraw_reward(
587        &self,
588        addr: &Address,
589        gas: Gas,
590    ) -> Result<Transaction, Error> {
591        let wallet = self.connected_wallet().await?;
592        // make sure we own the staking address
593        if !addr.is_owned() {
594            return Err(Error::Unauthorized);
595        }
596
597        let mut rng = StdRng::from_entropy();
598        let index = addr.index()? as u64;
599
600        let tx = wallet.withdraw(
601            &mut rng,
602            index,
603            index,
604            addr.psk(),
605            gas.limit,
606            gas.price,
607        )?;
608        Ok(tx)
609    }
610
611    /// Returns bls key pair for provisioner nodes
612    pub fn provisioner_keys(
613        &self,
614        addr: &Address,
615    ) -> Result<(PublicKey, SecretKey), Error> {
616        // make sure we own the staking address
617        if !addr.is_owned() {
618            return Err(Error::Unauthorized);
619        }
620
621        let index = addr.index()? as u64;
622
623        // retrieve keys
624        let sk = self.store.retrieve_sk(index)?;
625        let pk: PublicKey = From::from(&sk);
626
627        Ok((pk, sk))
628    }
629
630    /// Export bls key pair for provisioners in node-compatible format
631    pub fn export_keys(
632        &self,
633        addr: &Address,
634        dir: &Path,
635        filename: Option<String>,
636        pwd: &[u8],
637    ) -> Result<(PathBuf, PathBuf), Error> {
638        // we're expecting a directory here
639        if !dir.is_dir() {
640            return Err(Error::NotDirectory);
641        }
642
643        // get our keys for this address
644        let keys = self.provisioner_keys(addr)?;
645
646        // set up the path
647        let mut path = PathBuf::from(dir);
648        path.push(filename.unwrap_or(addr.to_string()));
649
650        // export public key to disk
651        let bytes = keys.0.to_bytes();
652        fs::write(path.with_extension("cpk"), bytes)?;
653
654        // create node-compatible json structure
655        let bls = BlsKeyPair {
656            public_key_bls: keys.0.to_bytes(),
657            secret_key_bls: keys.1.to_bytes(),
658        };
659        let json = serde_json::to_string(&bls)?;
660
661        // encrypt data
662        let mut bytes = json.as_bytes().to_vec();
663        bytes = crate::crypto::encrypt(&bytes, pwd)?;
664
665        // export key pair to disk
666        fs::write(path.with_extension("keys"), bytes)?;
667
668        Ok((path.with_extension("keys"), path.with_extension("cpk")))
669    }
670
671    /// Obtain the owned `Address` for a given address
672    pub fn claim_as_address(&self, addr: Address) -> Result<&Address, Error> {
673        self.addresses()
674            .iter()
675            .find(|a| a.psk == addr.psk)
676            .ok_or(Error::AddressNotOwned)
677    }
678
679    /// Return the dat file version from memory or by reading the file
680    /// In order to not read the file version more than once per execution
681    pub fn get_file_version(&self) -> Result<DatFileVersion, Error> {
682        if let Some(file_version) = self.file_version {
683            Ok(file_version)
684        } else if let Some(file) = &self.file {
685            Ok(dat::read_file_version(file.path())?)
686        } else {
687            Err(Error::WalletFileMissing)
688        }
689    }
690}
691
692/// This structs represent a Note decoded enriched with useful chain information
693pub struct DecodedNote {
694    /// The phoenix note
695    pub note: Note,
696    /// The decoded amount
697    pub amount: u64,
698    /// The block height
699    pub block_height: u64,
700    /// Nullified by
701    pub nullified_by: Option<BlsScalar>,
702}
703
704/// Bls key pair helper structure
705#[derive(Serialize)]
706struct BlsKeyPair {
707    #[serde(with = "base64")]
708    secret_key_bls: [u8; 32],
709    #[serde(with = "base64")]
710    public_key_bls: [u8; 96],
711}
712
713mod base64 {
714    use serde::{Serialize, Serializer};
715
716    pub fn serialize<S: Serializer>(v: &[u8], s: S) -> Result<S::Ok, S::Error> {
717        let base64 = base64::encode(v);
718        String::serialize(&base64, s)
719    }
720}
721
722#[cfg(test)]
723mod tests {
724
725    use super::*;
726    use tempfile::tempdir;
727
728    const TEST_ADDR: &str = "2w7fRQW23Jn9Bgm1GQW9eC2bD9U883dAwqP7HAr2F8g1syzPQaPYrxSyyVZ81yDS5C1rv9L8KjdPBsvYawSx3QCW";
729
730    #[derive(Debug, Clone)]
731    struct WalletFile {
732        path: WalletPath,
733        pwd: Vec<u8>,
734    }
735
736    impl SecureWalletFile for WalletFile {
737        fn path(&self) -> &WalletPath {
738            &self.path
739        }
740
741        fn pwd(&self) -> &[u8] {
742            &self.pwd
743        }
744    }
745
746    #[test]
747    fn wallet_basics() -> Result<(), Box<dyn std::error::Error>> {
748        // create a wallet from a mnemonic phrase
749        let mut wallet: Wallet<WalletFile> = Wallet::new("uphold stove tennis fire menu three quick apple close guilt poem garlic volcano giggle comic")?;
750
751        // check address generation
752        let default_addr = wallet.default_address().clone();
753        let other_addr = wallet.new_address();
754
755        assert!(format!("{}", default_addr).eq(TEST_ADDR));
756        assert_ne!(&default_addr, other_addr);
757        assert_eq!(wallet.addresses.len(), 2);
758
759        // create another wallet with different mnemonic
760        let wallet: Wallet<WalletFile> = Wallet::new("demise monitor elegant cradle squeeze cheap parrot venture stereo humor scout denial action receive flat")?;
761
762        // check addresses are different
763        let addr = wallet.default_address();
764        assert!(format!("{}", addr).ne(TEST_ADDR));
765
766        // attempt to create a wallet from an invalid mnemonic
767        let bad_wallet: Result<Wallet<WalletFile>, Error> =
768            Wallet::new("good luck with life");
769        assert!(bad_wallet.is_err());
770
771        Ok(())
772    }
773
774    #[test]
775    fn save_and_load() -> Result<(), Box<dyn std::error::Error>> {
776        // prepare a tmp path
777        let dir = tempdir()?;
778        let path = dir.path().join("my_wallet.dat");
779        let path = WalletPath::from(path);
780
781        // we'll need a password too
782        let pwd = blake3::hash("mypassword".as_bytes()).as_bytes().to_vec();
783
784        // create and save
785        let mut wallet: Wallet<WalletFile> = Wallet::new("uphold stove tennis fire menu three quick apple close guilt poem garlic volcano giggle comic")?;
786        let file = WalletFile { path, pwd };
787        wallet.save_to(file.clone())?;
788
789        // load from file and check
790        let loaded_wallet = Wallet::from_file(file)?;
791
792        let original_addr = wallet.default_address();
793        let loaded_addr = loaded_wallet.default_address();
794        assert!(original_addr.eq(loaded_addr));
795
796        Ok(())
797    }
798}