use std::{
collections::{BTreeMap, HashMap},
io::{self, Error, ErrorKind, Read, Write},
};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use log::info;
use bip0039::Mnemonic;
use zcash_client_backend::proto::service::TreeState;
use zcash_encoding::{Optional, Vector};
use zcash_keys::keys::UnifiedSpendingKey;
use zcash_primitives::{legacy::keys::NonHardenedChildIndex, transaction::TxId};
use zcash_protocol::consensus::{self, BlockHeight};
use zingo_price::PriceList;
use zip32::AccountId;
use super::LightWallet;
use super::keys::unified::{ReceiverSelection, UnifiedAddressId};
use crate::wallet::{SendProgress, WalletSettings, legacy::WalletZecPriceInfo, utils};
use crate::wallet::{legacy::WalletOptions, traits::ReadableWriteable};
use crate::{
config::ChainType,
wallet::{
keys::{legacy::WalletCapability, unified::UnifiedKeyStore},
legacy::{BlockData, TxMap},
},
};
use pepper_sync::{
keys::transparent::{self, TransparentAddressId, TransparentScope},
sync::{SyncConfig, TransparentAddressDiscovery},
wallet::{NullifierMap, OutputId, ShardTrees, SyncState, WalletBlock, WalletTransaction},
};
impl LightWallet {
pub const fn serialized_version() -> u64 {
34
}
pub async fn write<W: Write>(
&mut self,
mut writer: W,
consensus_parameters: &impl consensus::Parameters,
) -> io::Result<()> {
writer.write_u64::<LittleEndian>(Self::serialized_version())?;
utils::write_string(&mut writer, &self.network.to_string())?;
let seed_bytes = match &self.mnemonic {
Some(m) => m.0.clone().into_entropy(),
None => vec![],
};
Vector::write(&mut writer, &seed_bytes, |w, byte| w.write_u8(*byte))?;
if let Some(m) = &self.mnemonic {
writer.write_u32::<LittleEndian>(m.1)?;
}
writer.write_u32::<LittleEndian>(self.birthday.into())?;
self.unified_key_store.write(&mut writer, self.network)?;
Vector::write(
&mut writer,
&self.unified_addresses.iter().collect::<Vec<_>>(),
|w, (address_id, address)| {
w.write_u32::<LittleEndian>(address_id.account_id.into())?;
w.write_u32::<LittleEndian>(address_id.address_index)?;
ReceiverSelection {
orchard: address.orchard().is_some(),
sapling: address.sapling().is_some(),
transparent: address.transparent().is_some(),
}
.write(w, ())
},
)?;
Vector::write(
&mut writer,
&self.transparent_addresses.keys().collect::<Vec<_>>(),
|w, address_id| {
w.write_u32::<LittleEndian>(address_id.account_id().into())?;
w.write_u8(address_id.scope() as u8)?;
w.write_u32::<LittleEndian>(address_id.address_index().index())
},
)?;
Vector::write(
&mut writer,
&self.wallet_blocks.values().collect::<Vec<_>>(),
|w, &block| block.write(w),
)?;
Vector::write(
&mut writer,
&self.wallet_transactions.values().collect::<Vec<_>>(),
|w, &transaction| transaction.write(w, consensus_parameters),
)?;
self.nullifier_map.write(&mut writer)?;
Vector::write(
&mut writer,
&self.outpoint_map.iter().collect::<Vec<_>>(),
|w, &(&output_id, &locator)| {
output_id.txid().write(&mut *w)?;
w.write_u16::<LittleEndian>(output_id.output_index())?;
w.write_u32::<LittleEndian>(locator.0.into())?;
locator.1.write(w)
},
)?;
self.shard_trees.write(&mut writer)?;
self.sync_state.write(&mut writer)?;
self.wallet_settings.sync_config.write(&mut writer)?;
self.price_list.write(&mut writer)
}
pub fn read<R: Read>(mut reader: R, network: ChainType) -> io::Result<Self> {
let version = reader.read_u64::<LittleEndian>()?;
info!("Reading wallet version {}", version);
match version {
..32 => Self::read_v0(reader, network, version),
32..=34 => Self::read_v32(reader, network, version),
_ => Err(io::Error::new(
ErrorKind::InvalidData,
format!(
"Failed to read wallet version {}. Do you have the latest version?\n{}",
version, "Note: wallet files from zecwallet or beta zingo are not compatible"
),
)),
}
}
fn read_v0<R: Read>(mut reader: R, network: ChainType, version: u64) -> io::Result<Self> {
let mut wallet_capability = WalletCapability::read(&mut reader, network)?;
let mut _blocks = Vector::read(&mut reader, |r| BlockData::read(r))?;
let transactions = if version <= 14 {
TxMap::read_old(&mut reader, &wallet_capability)?
} else {
TxMap::read(&mut reader, &wallet_capability)?
};
let chain_name = utils::read_string(&mut reader)?;
if chain_name != network.to_string() {
return Err(Error::new(
ErrorKind::InvalidData,
format!(
"Wallet chain name {} doesn't match expected {}",
chain_name, network
),
));
}
let _wallet_options = if version <= 23 {
WalletOptions::default()
} else {
WalletOptions::read(&mut reader)?
};
let birthday = BlockHeight::from_u32(
reader
.read_u64::<LittleEndian>()?
.try_into()
.expect("should never overflow"),
);
if version <= 22 {
let _sapling_tree_verified = if version <= 12 {
true
} else {
reader.read_u8()? == 1
};
}
let _verified_tree = if version <= 21 {
None
} else {
Optional::read(&mut reader, |r| {
use prost::Message;
let buf = Vector::read(r, |r| r.read_u8())?;
TreeState::decode(&buf[..])
.map_err(|e| io::Error::new(ErrorKind::InvalidData, e.to_string()))
})?
};
let _price = if version <= 13 {
WalletZecPriceInfo::default()
} else {
WalletZecPriceInfo::read(&mut reader)?
};
let _orchard_anchor_height_pairs = if version == 25 {
Vector::read(&mut reader, |r| {
let mut anchor_bytes = [0; 32];
r.read_exact(&mut anchor_bytes)?;
let block_height = BlockHeight::from_u32(r.read_u32::<LittleEndian>()?);
Ok((
Option::<orchard::Anchor>::from(orchard::Anchor::from_bytes(anchor_bytes))
.ok_or(Error::new(ErrorKind::InvalidData, "Bad orchard anchor"))?,
block_height,
))
})?
} else {
Vec::new()
};
let seed_bytes = Vector::read(&mut reader, |r| r.read_u8())?;
let mnemonic = if !seed_bytes.is_empty() {
let account_index = if version >= 28 {
reader.read_u32::<LittleEndian>()?
} else {
0
};
Some((
Mnemonic::from_entropy(seed_bytes)
.map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()))?,
account_index,
))
} else {
None
};
if version < 29 {
if let Some(mnemonic) = mnemonic.as_ref() {
wallet_capability.unified_key_store = UnifiedKeyStore::Spend(Box::new(
UnifiedSpendingKey::from_seed(
&network,
&mnemonic.0.to_seed(""),
AccountId::ZERO,
)
.map_err(|e| {
Error::new(
ErrorKind::InvalidData,
format!(
"Failed to derive unified spending key from stored seed bytes. {}",
e
),
)
})?,
));
} else if let UnifiedKeyStore::Spend(_) = &wallet_capability.unified_key_store {
return Err(io::Error::new(
ErrorKind::Other,
"loading from legacy spending keys with no seed phrase to recover",
));
}
}
let unified_key_store = wallet_capability.unified_key_store;
info!("Keys in this wallet:");
match &unified_key_store {
UnifiedKeyStore::Spend(_) => {
info!(" - orchard spending key");
info!(" - sapling extended spending key");
info!(" - transparent extended private key");
}
UnifiedKeyStore::View(ufvk) => {
if ufvk.orchard().is_some() {
info!(" - orchard full viewing key");
}
if ufvk.sapling().is_some() {
info!(" - sapling diversifiable full viewing key");
}
if ufvk.transparent().is_some() {
info!(" - transparent extended public key");
}
}
UnifiedKeyStore::Empty => info!(" - no keys found"),
}
let mut sync_state = SyncState::new();
pepper_sync::add_scan_targets(
&mut sync_state,
&transactions
.transaction_records_by_id
.0
.values()
.filter_map(|transaction| {
transaction
.status
.get_confirmed_height()
.map(|height| (height, transaction.txid))
})
.collect::<Vec<_>>(),
);
let first_address_index = 0;
let first_unified_address = unified_key_store
.generate_unified_address(first_address_index, unified_key_store.can_view(), false)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let mut unified_addresses = BTreeMap::new();
unified_addresses.insert(
UnifiedAddressId {
account_id: zip32::AccountId::ZERO,
address_index: first_address_index,
},
first_unified_address.clone(),
);
let lw = Self {
mnemonic,
birthday,
unified_key_store,
send_progress: SendProgress::new(0),
price_list: PriceList::new(),
wallet_blocks: BTreeMap::new(),
wallet_transactions: HashMap::new(),
nullifier_map: NullifierMap::new(),
outpoint_map: BTreeMap::new(),
shard_trees: ShardTrees::new(),
sync_state,
transparent_addresses: BTreeMap::new(),
unified_addresses,
network,
save_required: false,
wallet_settings: WalletSettings {
sync_config: SyncConfig {
transparent_address_discovery: TransparentAddressDiscovery::minimal(),
},
},
};
Ok(lw)
}
fn read_v32<R: Read>(mut reader: R, network: ChainType, version: u64) -> io::Result<Self> {
let saved_network = utils::read_string(&mut reader)?;
if saved_network != network.to_string() {
return Err(Error::new(
ErrorKind::InvalidData,
format!(
"Wallet chain name {} doesn't match expected {}",
saved_network, network
),
));
}
let seed_bytes = Vector::read(&mut reader, |r| r.read_u8())?;
let mnemonic = if !seed_bytes.is_empty() {
let account_index = reader.read_u32::<LittleEndian>()?;
Some((
<Mnemonic>::from_entropy(seed_bytes)
.map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()))?,
account_index,
))
} else {
None
};
let birthday = BlockHeight::from_u32(reader.read_u32::<LittleEndian>()?);
let unified_key_store = UnifiedKeyStore::read(&mut reader, network)?;
let unified_addresses = Vector::read(&mut reader, |r| {
let account_id = zip32::AccountId::try_from(r.read_u32::<LittleEndian>()?)
.expect("only valid account ids are stored");
let address_index = r.read_u32::<LittleEndian>()?;
let receivers = ReceiverSelection::read(r, ())?;
Ok((
UnifiedAddressId {
account_id,
address_index,
},
unified_key_store
.generate_unified_address(address_index, receivers, false)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
))
})?
.into_iter()
.collect::<BTreeMap<_, _>>();
let transparent_addresses = Vector::read(&mut reader, |r| {
let account_id = zip32::AccountId::try_from(r.read_u32::<LittleEndian>()?)
.expect("only valid account ids are stored");
let scope = TransparentScope::try_from(r.read_u8()?)?;
let address_index = r.read_u32::<LittleEndian>()?;
Ok((
TransparentAddressId::new(
account_id,
scope,
NonHardenedChildIndex::from_index(address_index)
.expect("only non-hardened child indexes should be written"),
),
transparent::encode_address(
&network,
unified_key_store
.generate_transparent_address(address_index, scope, false)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
),
))
})?
.into_iter()
.collect::<BTreeMap<_, _>>();
let wallet_blocks = Vector::read(&mut reader, |r| WalletBlock::read(r))?
.into_iter()
.map(|block| (block.block_height(), block))
.collect::<BTreeMap<_, _>>();
let wallet_transactions =
Vector::read(&mut reader, |r| WalletTransaction::read(r, &network))?
.into_iter()
.map(|transaction| (transaction.txid(), transaction))
.collect::<HashMap<_, _>>();
let nullifier_map = NullifierMap::read(&mut reader)?;
let outpoint_map = Vector::read(&mut reader, |mut r| {
let outpoint_txid = TxId::read(&mut r)?;
let output_index = r.read_u16::<LittleEndian>()?;
let locator_height = BlockHeight::from_u32(r.read_u32::<LittleEndian>()?);
let locator_txid = TxId::read(&mut r)?;
Ok((
OutputId::new(outpoint_txid, output_index),
(locator_height, locator_txid),
))
})?
.into_iter()
.collect::<BTreeMap<_, _>>();
let shard_trees = ShardTrees::read(&mut reader)?;
let sync_state = SyncState::read(&mut reader)?;
let wallet_settings = if version >= 33 {
WalletSettings {
sync_config: SyncConfig::read(&mut reader)?,
}
} else {
WalletSettings {
sync_config: SyncConfig {
transparent_address_discovery: TransparentAddressDiscovery::minimal(),
},
}
};
let price_list = if version >= 34 {
PriceList::read(&mut reader)?
} else {
PriceList::new()
};
Ok(Self {
network,
mnemonic,
birthday,
unified_key_store,
unified_addresses,
transparent_addresses,
wallet_blocks,
wallet_transactions,
nullifier_map,
outpoint_map,
shard_trees,
sync_state,
wallet_settings,
price_list,
send_progress: SendProgress::new(0),
save_required: false,
})
}
}
#[cfg(any(test, feature = "test-elevation"))]
pub mod testing;