diff --git a/zingocli/tests/integration_tests.rs b/zingocli/tests/integration_tests.rs index 01ee86318..582119887 100644 --- a/zingocli/tests/integration_tests.rs +++ b/zingocli/tests/integration_tests.rs @@ -1144,11 +1144,7 @@ async fn handling_of_nonregenerated_diversified_addresses_after_seed_restore() { .to_string(), ); let recipient_restored = client_builder - .build_newseed_client( - seed_of_recipient["seed"].as_str().unwrap().to_string(), - 0, - true, - ) + .build_newseed_client(seed_of_recipient.seed_phrase.clone(), 0, true) .await; let seed_of_recipient_restored = { recipient_restored.do_sync(true).await.unwrap(); @@ -1528,10 +1524,12 @@ async fn load_wallet_from_v26_dat_file() { .map_err(|e| format!("Cannot deserialize LightWallet version 26 file: {}", e)) .unwrap(); - let expected_mnemonic = Mnemonic::from_phrase(TEST_SEED.to_string()).unwrap(); + let expected_mnemonic = (Mnemonic::from_phrase(TEST_SEED.to_string()).unwrap(), 0); assert_eq!(wallet.mnemonic(), Some(&expected_mnemonic)); - let expected_wc = WalletCapability::new_from_phrase(&config, &expected_mnemonic, 0).unwrap(); + let expected_wc = + WalletCapability::new_from_phrase(&config, &expected_mnemonic.0, expected_mnemonic.1) + .unwrap(); let wc = wallet.wallet_capability(); // We don't want the WalletCapability to impl. `Eq` (because it stores secret keys) diff --git a/zingolib/src/commands.rs b/zingolib/src/commands.rs index e22898bf5..f6eea4b95 100644 --- a/zingolib/src/commands.rs +++ b/zingolib/src/commands.rs @@ -1020,7 +1020,11 @@ impl Command for SeedCommand { fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { RT.block_on(async move { match lightclient.do_seed_phrase().await { - Ok(j) => j, + Ok(m) => object! { + "seed" => m.seed_phrase, + "birthday" => m.birthday, + "account_index" => m.account_index, + }, Err(e) => object! { "error" => e }, } .pretty(2) diff --git a/zingolib/src/lightclient.rs b/zingolib/src/lightclient.rs index 4e9e8f91b..2f8ed1e61 100644 --- a/zingolib/src/lightclient.rs +++ b/zingolib/src/lightclient.rs @@ -349,6 +349,13 @@ impl PoolBalances { } } +#[derive(Clone, Debug, PartialEq)] +pub struct AccountBackupInfo { + pub seed_phrase: String, + pub birthday: u64, + pub account_index: u32, +} + impl LightClient { fn add_nonchange_notes<'a, 'b, 'c>( &'a self, @@ -976,17 +983,18 @@ impl LightClient { .block_on(async move { self.do_save_to_buffer().await }) } - pub async fn do_seed_phrase(&self) -> Result { + pub async fn do_seed_phrase(&self) -> Result { match self.wallet.mnemonic() { - Some(m) => Ok(object! { - "seed" => m.to_string(), - "birthday" => self.wallet.get_birthday().await + Some(m) => Ok(AccountBackupInfo { + seed_phrase: m.0.phrase().to_string(), + birthday: self.wallet.get_birthday().await, + account_index: m.1, }), - None => Err("This wallet is watch-only."), + None => Err("This wallet is watch-only or was created without a mnemonic."), } } - pub fn do_seed_phrase_sync(&self) -> Result { + pub fn do_seed_phrase_sync(&self) -> Result { Runtime::new() .unwrap() .block_on(async move { self.do_seed_phrase().await }) diff --git a/zingolib/src/wallet.rs b/zingolib/src/wallet.rs index 67bd6fc1c..6031a4557 100644 --- a/zingolib/src/wallet.rs +++ b/zingolib/src/wallet.rs @@ -187,6 +187,9 @@ pub enum WalletBase { SeedBytes([u8; 32]), MnemonicPhrase(String), Mnemonic(Mnemonic), + SeedBytesAndIndex([u8; 32], u32), + MnemonicPhraseAndIndex(String, u32), + MnemonicAndIndex(Mnemonic, u32), /// Unified full viewing key Ufvk(String), /// Unified spending key @@ -207,9 +210,10 @@ pub struct LightWallet { // will start from here. birthday: AtomicU64, - /// The seed for the wallet, stored as a bip0039 Mnemonic - /// Can be `None` in case of wallet without spending capability. - mnemonic: Option, + /// The seed for the wallet, stored as a bip0039 Mnemonic, and the account index. + /// Can be `None` in case of wallet without spending capability + /// or created directly from spending keys. + mnemonic: Option<(Mnemonic, u32)>, // The last 100 blocks, used if something gets re-orged pub blocks: Arc>>, @@ -562,7 +566,7 @@ impl LightWallet { } } - pub fn mnemonic(&self) -> Option<&Mnemonic> { + pub fn mnemonic(&self) -> Option<&(Mnemonic, u32)> { self.mnemonic.as_ref() } @@ -576,15 +580,29 @@ impl LightWallet { return Self::new(config, WalletBase::SeedBytes(seed_bytes), height); } WalletBase::SeedBytes(seed_bytes) => { + return Self::new(config, WalletBase::SeedBytesAndIndex(seed_bytes, 0), height); + } + WalletBase::SeedBytesAndIndex(seed_bytes, position) => { let mnemonic = Mnemonic::from_entropy(seed_bytes).map_err(|e| { Error::new( ErrorKind::InvalidData, format!("Error parsing phrase: {}", e), ) })?; - return Self::new(config, WalletBase::Mnemonic(mnemonic), height); + return Self::new( + config, + WalletBase::MnemonicAndIndex(mnemonic, position), + height, + ); } WalletBase::MnemonicPhrase(phrase) => { + return Self::new( + config, + WalletBase::MnemonicPhraseAndIndex(phrase, 0), + height, + ); + } + WalletBase::MnemonicPhraseAndIndex(phrase, position) => { let mnemonic = Mnemonic::from_phrase(phrase) .and_then(|m| Mnemonic::from_entropy(m.entropy())) .map_err(|e| { @@ -597,12 +615,19 @@ impl LightWallet { // should be a no-op, but seems to be needed on android for some reason // TODO: Test the this cfg actually works //#[cfg(target_os = "android")] - return Self::new(config, WalletBase::Mnemonic(mnemonic), height); + return Self::new( + config, + WalletBase::MnemonicAndIndex(mnemonic, position), + height, + ); } WalletBase::Mnemonic(mnemonic) => { - let wc = WalletCapability::new_from_phrase(&config, &mnemonic, 0) + return Self::new(config, WalletBase::MnemonicAndIndex(mnemonic, 0), height); + } + WalletBase::MnemonicAndIndex(mnemonic, position) => { + let wc = WalletCapability::new_from_phrase(&config, &mnemonic, position) .map_err(|e| Error::new(ErrorKind::InvalidData, e))?; - (wc, Some(mnemonic)) + (wc, Some((mnemonic, position))) } WalletBase::Ufvk(ufvk_encoded) => { let wc = WalletCapability::new_from_ufvk(&config, ufvk_encoded).map_err(|e| { @@ -799,10 +824,16 @@ impl LightWallet { let seed_bytes = Vector::read(&mut reader, |r| r.read_u8())?; let mnemonic = if !seed_bytes.is_empty() { - Some( + let account_index = if external_version >= 28 { + reader.read_u32::()? + } else { + 0 + }; + Some(( Mnemonic::from_entropy(seed_bytes) .map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()))?, - ) + account_index, + )) } else { None }; @@ -1434,7 +1465,7 @@ impl LightWallet { } pub const fn serialized_version() -> u64 { - 27 + 28 } pub async fn set_blocks(&self, new_blocks: Vec) { @@ -1679,11 +1710,16 @@ impl LightWallet { self.price.read().await.write(&mut writer)?; let seed_bytes = match &self.mnemonic { - Some(m) => m.clone().into_entropy(), + Some(m) => m.0.clone().into_entropy(), None => vec![], }; Vector::write(&mut writer, &seed_bytes, |w, byte| w.write_u8(*byte))?; + match &self.mnemonic { + Some(m) => writer.write_u32::(m.1)?, + None => (), + } + Ok(()) } }