1mod address;
8mod file;
9mod transaction;
10
11pub use address::{Address, Profile};
12pub use file::{SecureWalletFile, WalletPath};
13
14use std::fmt::Debug;
15use std::fs;
16use std::path::{Path, PathBuf};
17
18use bip39::{Language, Mnemonic, Seed};
19use dusk_bytes::Serializable;
20use dusk_core::abi::CONTRACT_ID_BYTES;
21use dusk_core::signatures::bls::{
22 PublicKey as BlsPublicKey, SecretKey as BlsSecretKey,
23};
24use dusk_core::stake::StakeData;
25use dusk_core::transfer::phoenix::{
26 Note, NoteLeaf, PublicKey as PhoenixPublicKey,
27 SecretKey as PhoenixSecretKey, ViewKey as PhoenixViewKey,
28};
29use dusk_core::BlsScalar;
30use serde::Serialize;
31use wallet_core::prelude::keys::{
32 derive_bls_pk, derive_bls_sk, derive_phoenix_pk, derive_phoenix_sk,
33 derive_phoenix_vk,
34};
35use wallet_core::{phoenix_balance, BalanceInfo};
36use zeroize::Zeroize;
37
38use crate::clients::State;
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::gas::MempoolGasPrices;
46use crate::rues::RuesHttpClient;
47use crate::store::LocalStore;
48use crate::Error;
49
50pub struct Wallet<F: SecureWalletFile + Debug> {
64 profiles: Vec<Profile>,
65 state: Option<State>,
66 store: LocalStore,
67 file: Option<F>,
68 file_version: Option<DatFileVersion>,
69}
70
71impl<F: SecureWalletFile + Debug> Wallet<F> {
72 pub fn file(&self) -> &Option<F> {
74 &self.file
75 }
76}
77
78impl<F: SecureWalletFile + Debug> Wallet<F> {
79 pub fn new<P>(phrase: P) -> Result<Self, Error>
82 where
83 P: Into<String>,
84 {
85 let phrase: String = phrase.into();
87 let try_mnem = Mnemonic::from_phrase(&phrase, Language::English);
88
89 if let Ok(mnemonic) = try_mnem {
90 let seed = Seed::new(&mnemonic, "");
92 let seed_bytes = seed
94 .as_bytes()
95 .try_into()
96 .map_err(|_| Error::InvalidMnemonicPhrase)?;
97
98 let profiles = vec![Profile {
100 shielded_addr: derive_phoenix_pk(&seed_bytes, 0),
101 public_addr: derive_bls_pk(&seed_bytes, 0),
102 }];
103
104 Ok(Wallet {
106 profiles,
107 state: None,
108 store: LocalStore::from(seed_bytes),
109 file: None,
110 file_version: None,
111 })
112 } else {
113 Err(Error::InvalidMnemonicPhrase)
114 }
115 }
116
117 pub fn from_file(file: F) -> Result<Self, Error> {
119 let path = file.path();
120 let pwd = file.pwd();
121
122 let pb = path.inner().clone();
124 if !pb.is_file() {
125 return Err(Error::WalletFileMissing);
126 }
127
128 let bytes = fs::read(&pb)?;
130
131 let file_version = dat::check_version(bytes.get(0..12))?;
132
133 let (seed, address_count) =
134 dat::get_seed_and_address(file_version, bytes, pwd)?;
135
136 if let DatFileVersion::Legacy = file_version {
138 let profiles = vec![Profile {
140 shielded_addr: derive_phoenix_pk(&seed, 0),
141 public_addr: derive_bls_pk(&seed, 0),
142 }];
143
144 return Ok(Self {
146 profiles,
147 store: LocalStore::from(seed),
148 state: None,
149 file: Some(file),
150 file_version: Some(DatFileVersion::Legacy),
151 });
152 }
153
154 let profiles: Vec<_> = (0..address_count)
155 .map(|i| Profile {
156 shielded_addr: derive_phoenix_pk(&seed, i),
157 public_addr: derive_bls_pk(&seed, i),
158 })
159 .collect();
160
161 Ok(Self {
163 profiles,
164 store: LocalStore::from(seed),
165 state: None,
166 file: Some(file),
167 file_version: Some(file_version),
168 })
169 }
170
171 pub fn save(&mut self) -> Result<(), Error> {
173 match &self.file {
174 Some(f) => {
175 let mut header = Vec::with_capacity(12);
176 header.extend_from_slice(&MAGIC.to_be_bytes());
177 header.extend_from_slice(&FILE_TYPE.to_be_bytes());
179 header.extend_from_slice(&RESERVED.to_be_bytes());
181 header.extend_from_slice(&version_bytes(LATEST_VERSION));
183
184 let seed = self.store.get_seed();
186 let mut payload = seed.to_vec();
187
188 payload.push(self.profiles.len() as u8);
189
190 payload = encrypt(&payload, f.pwd())?;
192
193 let mut content =
194 Vec::with_capacity(header.len() + payload.len());
195
196 content.extend_from_slice(&header);
197 content.extend_from_slice(&payload);
198
199 fs::write(&f.path().wallet, content)?;
201 Ok(())
202 }
203 None => Err(Error::WalletFileMissing),
204 }
205 }
206
207 pub fn save_to(&mut self, file: F) -> Result<(), Error> {
211 self.file = Some(file);
213 self.save()
214 }
215
216 pub fn state(&self) -> Result<&State, Error> {
218 if let Some(state) = self.state.as_ref() {
219 Ok(state)
220 } else {
221 Err(Error::Offline)
222 }
223 }
224
225 pub async fn connect_with_status<S: Into<String>>(
228 &mut self,
229 rusk_addr: S,
230 prov_addr: S,
231 status: fn(&str),
232 ) -> Result<(), Error> {
233 let http_state = RuesHttpClient::new(rusk_addr)?;
235 let http_prover = RuesHttpClient::new(prov_addr)?;
236
237 let state_status = http_state.check_connection().await;
238 let prover_status = http_prover.check_connection().await;
239
240 match (&state_status, prover_status) {
241 (Err(e),_)=> println!("Connection to Rusk Failed, some operations won't be available: {e}"),
242 (_,Err(e))=> println!("Connection to Prover Failed, some operations won't be available: {e}"),
243 _=> {},
244 }
245
246 let cache_dir = self.cache_path()?;
247
248 self.state = Some(State::new(
250 &cache_dir,
251 status,
252 http_state,
253 http_prover,
254 self.store.clone(),
255 )?);
256
257 Ok(())
258 }
259
260 pub async fn sync(&self) -> Result<(), Error> {
262 self.state()?.sync().await
263 }
264
265 pub async fn register_sync(&mut self) -> Result<(), Error> {
267 match self.state.as_mut() {
268 Some(w) => w.register_sync().await,
269 None => Err(Error::Offline),
270 }
271 }
272
273 pub async fn is_online(&self) -> bool {
275 if let Some(state) = &self.state {
276 state.check_connection().await
277 } else {
278 false
279 }
280 }
281
282 pub async fn get_all_notes(
284 &self,
285 profile_idx: u8,
286 ) -> Result<Vec<DecodedNote>, Error> {
287 let vk = self.derive_phoenix_vk(profile_idx);
288 let pk = self.shielded_key(profile_idx)?;
289
290 let live_notes = self.state()?.fetch_notes(pk)?;
291 let spent_notes = self.state()?.cache().spent_notes(pk)?;
292
293 let live_notes = live_notes
294 .into_iter()
295 .map(|data| (None, data.note, data.block_height));
296 let spent_notes = spent_notes.into_iter().map(
297 |(nullifier, NoteLeaf { note, block_height })| {
298 (Some(nullifier), note, block_height)
299 },
300 );
301 let history = live_notes
302 .chain(spent_notes)
303 .flat_map(
304 |(nullified_by, note, block_height)| -> Result<_, Error> {
305 let amount = note.value(Some(&vk));
306 if let Ok(amount) = amount {
307 Ok(DecodedNote {
308 note,
309 amount,
310 block_height,
311 nullified_by,
312 })
313 } else {
314 Err(Error::WrongViewKey)
315 }
316 },
317 )
318 .collect();
319
320 Ok(history)
321 }
322
323 pub async fn get_phoenix_balance(
325 &self,
326 profile_idx: u8,
327 ) -> Result<BalanceInfo, Error> {
328 self.sync().await?;
329
330 let notes =
331 self.state()?.fetch_notes(self.shielded_key(profile_idx)?)?;
332
333 Ok(phoenix_balance(
334 &self.derive_phoenix_vk(profile_idx),
335 notes.iter(),
336 ))
337 }
338
339 pub async fn get_moonlight_balance(
341 &self,
342 profile_idx: u8,
343 ) -> Result<Dusk, Error> {
344 let pk = self.public_key(profile_idx)?;
345 let state = self.state()?;
346 let account = state.fetch_account(pk).await?;
347
348 Ok(Dusk::from(account.balance))
349 }
350
351 pub fn add_profile(&mut self) -> u8 {
354 let seed = self.store.get_seed();
355 let index = self.profiles.len() as u8;
356 let addr = Profile {
357 shielded_addr: derive_phoenix_pk(seed, index),
358 public_addr: derive_bls_pk(seed, index),
359 };
360
361 self.profiles.push(addr);
362
363 index
364 }
365
366 pub fn default_address(&self) -> Address {
368 self.default_public_address()
370 }
371
372 pub fn default_shielded_account(&self) -> Address {
374 self.shielded_account(0)
375 .expect("there to be an address at index 0")
376 }
377
378 pub fn default_public_address(&self) -> Address {
380 self.public_address(0)
381 .expect("there to be an address at index 0")
382 }
383
384 pub fn profiles(&self) -> &Vec<Profile> {
386 &self.profiles
387 }
388
389 pub(crate) fn derive_phoenix_sk(&self, index: u8) -> PhoenixSecretKey {
391 let seed = self.store.get_seed();
392 derive_phoenix_sk(seed, index)
393 }
394
395 pub(crate) fn derive_phoenix_vk(&self, index: u8) -> PhoenixViewKey {
397 let seed = self.store.get_seed();
398 derive_phoenix_vk(seed, index)
399 }
400
401 pub(crate) fn cache_path(&self) -> Result<PathBuf, Error> {
403 let cache_dir = {
404 if let Some(file) = &self.file {
405 file.path().cache_dir()
406 } else {
407 return Err(Error::WalletFileMissing);
408 }
409 };
410
411 Ok(cache_dir)
412 }
413
414 pub fn shielded_key(&self, index: u8) -> Result<&PhoenixPublicKey, Error> {
420 let index = usize::from(index);
421 if index >= self.profiles.len() {
422 return Err(Error::Unauthorized);
423 }
424
425 Ok(&self.profiles()[index].shielded_addr)
426 }
427
428 pub(crate) fn derive_bls_sk(&self, index: u8) -> BlsSecretKey {
430 let seed = self.store.get_seed();
431 derive_bls_sk(seed, index)
432 }
433
434 pub fn public_key(&self, index: u8) -> Result<&BlsPublicKey, Error> {
440 let index = usize::from(index);
441 if index >= self.profiles.len() {
442 return Err(Error::Unauthorized);
443 }
444
445 Ok(&self.profiles()[index].public_addr)
446 }
447
448 pub fn public_address(&self, index: u8) -> Result<Address, Error> {
450 let addr = *self.public_key(index)?;
451 Ok(addr.into())
452 }
453
454 pub fn shielded_account(&self, index: u8) -> Result<Address, Error> {
456 let addr = *self.shielded_key(index)?;
457 Ok(addr.into())
458 }
459
460 pub async fn stake_info(
462 &self,
463 profile_idx: u8,
464 ) -> Result<Option<StakeData>, Error> {
465 self.state()?
466 .fetch_stake(self.public_key(profile_idx)?)
467 .await
468 }
469
470 pub fn provisioner_keys(
472 &self,
473 index: u8,
474 ) -> Result<(BlsPublicKey, BlsSecretKey), Error> {
475 let pk = *self.public_key(index)?;
476 let sk = self.derive_bls_sk(index);
477
478 if pk != BlsPublicKey::from(&sk) {
480 return Err(Error::Unauthorized);
481 }
482
483 Ok((pk, sk))
484 }
485
486 pub fn export_provisioner_keys(
488 &self,
489 profile_idx: u8,
490 dir: &Path,
491 filename: Option<String>,
492 pwd: &[u8],
493 ) -> Result<(PathBuf, PathBuf), Error> {
494 if !dir.is_dir() {
496 return Err(Error::NotDirectory);
497 }
498
499 let keys = self.provisioner_keys(profile_idx)?;
501
502 let mut path = PathBuf::from(dir);
504 path.push(filename.unwrap_or(profile_idx.to_string()));
505
506 let bytes = keys.0.to_bytes();
508 fs::write(path.with_extension("cpk"), bytes)?;
509
510 let bls = BlsKeyPair {
512 public_key_bls: keys.0.to_bytes(),
513 secret_key_bls: keys.1.to_bytes(),
514 };
515 let json = serde_json::to_string(&bls)?;
516
517 let mut bytes = json.as_bytes().to_vec();
519 bytes = crate::crypto::encrypt(&bytes, pwd)?;
520
521 fs::write(path.with_extension("keys"), bytes)?;
523
524 Ok((path.with_extension("keys"), path.with_extension("cpk")))
525 }
526
527 pub fn find_index(&self, addr: &Address) -> Result<u8, Error> {
530 for (index, profile) in self.profiles().iter().enumerate() {
533 if match addr {
534 Address::Shielded(addr) => addr == &profile.shielded_addr,
535 Address::Public(addr) => addr == &profile.public_addr,
536 } {
537 return Ok(index as u8);
538 }
539 }
540
541 Err(Error::Unauthorized)
543 }
544
545 pub fn claim(&self, addr: Address) -> Result<Address, Error> {
548 self.find_index(&addr)?;
549 Ok(addr)
550 }
551
552 pub fn get_contract_id(
554 &self,
555 profile_idx: u8,
556 bytes: Vec<u8>,
557 nonce: u64,
558 ) -> Result<[u8; CONTRACT_ID_BYTES], Error> {
559 let owner = self.public_key(profile_idx)?.to_bytes();
560
561 let mut hasher = blake2b_simd::Params::new()
562 .hash_length(CONTRACT_ID_BYTES)
563 .to_state();
564 hasher.update(bytes.as_ref());
565 hasher.update(&nonce.to_le_bytes()[..]);
566 hasher.update(owner.as_ref());
567 hasher
568 .finalize()
569 .as_bytes()
570 .try_into()
571 .map_err(|_| Error::InvalidContractId)
572 }
573
574 pub fn get_file_version(&self) -> Result<DatFileVersion, Error> {
577 if let Some(file_version) = self.file_version {
578 Ok(file_version)
579 } else if let Some(file) = &self.file {
580 Ok(dat::read_file_version(file.path())?)
581 } else {
582 Err(Error::WalletFileMissing)
583 }
584 }
585
586 pub async fn is_synced(&self) -> Result<bool, Error> {
588 let state = self.state()?;
589 let db_pos = state.cache().last_pos()?.unwrap_or(0);
590 let network_last_pos = state.fetch_num_notes().await? - 1;
591
592 Ok(network_last_pos == db_pos)
593 }
594
595 pub fn delete_cache(&mut self) -> Result<(), Error> {
597 let path = self.cache_path()?;
598
599 std::fs::remove_dir_all(path).map_err(Error::IO)
600 }
601
602 pub fn close(&mut self) {
604 self.store.inner_mut().zeroize();
605
606 if let Some(x) = &mut self.state {
608 x.close();
609 }
610 }
611
612 pub async fn get_mempool_gas_prices(
614 &self,
615 ) -> Result<MempoolGasPrices, Error> {
616 let client = self.state()?.client();
617
618 let response = client
619 .call("blocks", None, "gas-price", &[] as &[u8])
620 .await?;
621
622 let gas_prices: MempoolGasPrices = serde_json::from_slice(&response)?;
623
624 Ok(gas_prices)
625 }
626}
627
628pub struct DecodedNote {
630 pub note: Note,
632 pub amount: u64,
634 pub block_height: u64,
636 pub nullified_by: Option<BlsScalar>,
638}
639
640#[derive(Serialize)]
642struct BlsKeyPair {
643 #[serde(with = "base64")]
644 secret_key_bls: [u8; 32],
645 #[serde(with = "base64")]
646 public_key_bls: [u8; 96],
647}
648
649mod base64 {
650 use base64::engine::general_purpose::STANDARD as BASE64;
651 use base64::Engine;
652 use serde::{Serialize, Serializer};
653
654 pub fn serialize<S: Serializer>(v: &[u8], s: S) -> Result<S::Ok, S::Error> {
655 let base64 = BASE64.encode(v);
656 String::serialize(&base64, s)
657 }
658}
659
660#[cfg(test)]
661mod tests {
662
663 use tempfile::tempdir;
664
665 use super::*;
666
667 const TEST_ADDR: &str = "2w7fRQW23Jn9Bgm1GQW9eC2bD9U883dAwqP7HAr2F8g1syzPQaPYrxSyyVZ81yDS5C1rv9L8KjdPBsvYawSx3QCW";
668
669 #[derive(Debug, Clone)]
670 struct WalletFile {
671 path: WalletPath,
672 pwd: Vec<u8>,
673 }
674
675 impl SecureWalletFile for WalletFile {
676 fn path(&self) -> &WalletPath {
677 &self.path
678 }
679
680 fn pwd(&self) -> &[u8] {
681 &self.pwd
682 }
683 }
684
685 #[test]
686 fn wallet_basics() -> Result<(), Box<dyn std::error::Error>> {
687 let mut wallet: Wallet<WalletFile> = Wallet::new("uphold stove tennis fire menu three quick apple close guilt poem garlic volcano giggle comic")?;
689
690 let default_addr = wallet.default_shielded_account();
692 let other_addr_idx = wallet.add_profile();
693 let other_addr =
694 Address::Shielded(*wallet.shielded_key(other_addr_idx)?);
695
696 assert!(format!("{default_addr}").eq(TEST_ADDR));
697 assert_ne!(default_addr, other_addr);
698 assert_eq!(wallet.profiles.len(), 2);
699
700 let wallet: Wallet<WalletFile> = Wallet::new("demise monitor elegant cradle squeeze cheap parrot venture stereo humor scout denial action receive flat")?;
702
703 let addr = wallet.default_shielded_account();
705 assert!(format!("{}", addr).ne(TEST_ADDR));
706
707 let bad_wallet: Result<Wallet<WalletFile>, Error> =
709 Wallet::new("good luck with life");
710 assert!(bad_wallet.is_err());
711
712 Ok(())
713 }
714
715 #[test]
716 fn save_and_load() -> Result<(), Box<dyn std::error::Error>> {
717 let dir = tempdir()?;
719 let path = dir.path().join("my_wallet.dat");
720 let path = WalletPath::from(path);
721
722 let pwd = blake3::hash("mypassword".as_bytes()).as_bytes().to_vec();
724
725 let mut wallet: Wallet<WalletFile> = Wallet::new("uphold stove tennis fire menu three quick apple close guilt poem garlic volcano giggle comic")?;
727 let file = WalletFile { path, pwd };
728 wallet.save_to(file.clone())?;
729
730 let loaded_wallet = Wallet::from_file(file)?;
732
733 let original_addr = wallet.default_shielded_account();
734 let loaded_addr = loaded_wallet.default_shielded_account();
735 assert!(original_addr.eq(&loaded_addr));
736
737 Ok(())
738 }
739}