1mod 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
51pub 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 pub sync_rx: Option<Receiver<String>>,
73}
74
75impl<F: SecureWalletFile + Debug> Wallet<F> {
76 pub fn file(&self) -> &Option<F> {
78 &self.file
79 }
80
81 pub fn spending_keys(
83 &self,
84 addr: &Address,
85 ) -> Result<(PublicSpendKey, SecretSpendKey), Error> {
86 if !addr.is_owned() {
88 return Err(Error::Unauthorized);
89 }
90
91 let index = addr.index()? as u64;
92
93 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 pub fn new<P>(phrase: P) -> Result<Self, Error>
105 where
106 P: Into<String>,
107 {
108 let phrase: String = phrase.into();
110 let try_mnem = Mnemonic::from_phrase(&phrase, Language::English);
111
112 if let Ok(mnemonic) = try_mnem {
113 let seed = Seed::new(&mnemonic, "");
115 let mut bytes = seed.as_bytes();
117
118 let seed = store::Seed::from_reader(&mut bytes)?;
120
121 let store = LocalStore::new(seed);
122
123 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 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 pub fn from_file(file: F) -> Result<Self, Error> {
147 let path = file.path();
148 let pwd = file.pwd();
149
150 let pb = path.inner().clone();
152 if !pb.is_file() {
153 return Err(Error::WalletFileMissing);
154 }
155
156 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 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 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 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 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 header.extend_from_slice(&FILE_TYPE.to_be_bytes());
216 header.extend_from_slice(&RESERVED.to_be_bytes());
218 header.extend_from_slice(&version_bytes(LATEST_VERSION));
220
221 let seed = self.store.get_seed()?;
223 let mut payload = seed.to_vec();
224
225 payload.push(self.addresses.len() as u8);
226
227 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 fs::write(&f.path().wallet, content)?;
238 Ok(())
239 }
240 None => Err(Error::WalletFileMissing),
241 }
242 }
243
244 pub fn save_to(&mut self, file: F) -> Result<(), Error> {
248 self.file = Some(file);
250 self.save()
251 }
252
253 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 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 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 let state = StateStore::new(
291 http_state,
292 &cache_dir,
293 self.store.clone(),
294 status,
295 )?;
296
297 self.wallet = Some(WalletCore::new(self.store.clone(), state, prover));
299
300 self.status = status;
302
303 Ok(())
304 }
305
306 pub async fn sync(&self) -> Result<(), Error> {
308 self.connected_wallet().await?.state().sync().await
309 }
310
311 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 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 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 pub async fn get_balance(
388 &self,
389 addr: &Address,
390 ) -> Result<BalanceInfo, Error> {
391 if !addr.is_owned() {
393 return Err(Error::Unauthorized);
394 }
395
396 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 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 pub fn default_address(&self) -> &Address {
421 &self.addresses[0]
422 }
423
424 pub fn addresses(&self) -> &Vec<Address> {
426 &self.addresses
427 }
428
429 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 if !sender.is_owned() {
444 return Err(Error::Unauthorized);
445 }
446
447 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 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 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 if !sender.is_owned() {
481 return Err(Error::Unauthorized);
482 }
483 if amt == 0 {
485 return Err(Error::AmountIsZero);
486 }
487 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 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 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 if !addr.is_owned() {
521 return Err(Error::Unauthorized);
522 }
523 if amt == 0 {
525 return Err(Error::AmountIsZero);
526 }
527 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 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 pub async fn stake_info(&self, addr: &Address) -> Result<StakeInfo, Error> {
550 let wallet = self.connected_wallet().await?;
551 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 pub async fn unstake(
561 &self,
562 addr: &Address,
563 gas: Gas,
564 ) -> Result<Transaction, Error> {
565 let wallet = self.connected_wallet().await?;
566 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 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 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 pub fn provisioner_keys(
613 &self,
614 addr: &Address,
615 ) -> Result<(PublicKey, SecretKey), Error> {
616 if !addr.is_owned() {
618 return Err(Error::Unauthorized);
619 }
620
621 let index = addr.index()? as u64;
622
623 let sk = self.store.retrieve_sk(index)?;
625 let pk: PublicKey = From::from(&sk);
626
627 Ok((pk, sk))
628 }
629
630 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 if !dir.is_dir() {
640 return Err(Error::NotDirectory);
641 }
642
643 let keys = self.provisioner_keys(addr)?;
645
646 let mut path = PathBuf::from(dir);
648 path.push(filename.unwrap_or(addr.to_string()));
649
650 let bytes = keys.0.to_bytes();
652 fs::write(path.with_extension("cpk"), bytes)?;
653
654 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 let mut bytes = json.as_bytes().to_vec();
663 bytes = crate::crypto::encrypt(&bytes, pwd)?;
664
665 fs::write(path.with_extension("keys"), bytes)?;
667
668 Ok((path.with_extension("keys"), path.with_extension("cpk")))
669 }
670
671 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 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
692pub struct DecodedNote {
694 pub note: Note,
696 pub amount: u64,
698 pub block_height: u64,
700 pub nullified_by: Option<BlsScalar>,
702}
703
704#[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 let mut wallet: Wallet<WalletFile> = Wallet::new("uphold stove tennis fire menu three quick apple close guilt poem garlic volcano giggle comic")?;
750
751 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 let wallet: Wallet<WalletFile> = Wallet::new("demise monitor elegant cradle squeeze cheap parrot venture stereo humor scout denial action receive flat")?;
761
762 let addr = wallet.default_address();
764 assert!(format!("{}", addr).ne(TEST_ADDR));
765
766 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 let dir = tempdir()?;
778 let path = dir.path().join("my_wallet.dat");
779 let path = WalletPath::from(path);
780
781 let pwd = blake3::hash("mypassword".as_bytes()).as_bytes().to_vec();
783
784 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 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}