use std::{
collections::{BTreeMap, HashMap},
fmt,
};
use bech32::primitives::decode::CheckedHrpstring;
use bip0039::{
ChineseSimplified, ChineseTraditional, Czech, English, French, Italian, Japanese, Korean,
Mnemonic, Portuguese, Spanish,
};
use rand::RngCore;
use secrecy::{ExposeSecret, SecretVec};
use zcash_client_backend::{
data_api::{
Account as _, AccountBirthday, AccountPurpose, WalletRead, WalletWrite, Zip32Derivation,
chain::ChainState, wallet::decrypt_and_store_transaction,
},
wallet::{Exposure, TransparentAddressMetadata},
};
use zcash_keys::{
encoding::AddressCodec,
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
};
use zcash_primitives::{block::BlockHash, transaction::Transaction};
use zcash_protocol::{
PoolType, ShieldedPool,
consensus::{
self, BlockHeight, BranchId, NetworkConstants as _, NetworkType, NetworkUpgrade, Parameters,
},
};
use zip32::fingerprint::SeedFingerprint;
use ::transparent::{address::TransparentAddress, keys::TransparentKeyScope};
use crate::{AccountUuid, WalletDb, error::SqliteClientError, util::Clock};
use ::zewif::MnemonicLanguage as L;
use incrementalmerkletree::frontier::Frontier;
use zcash_script::script::{Code, Redeem};
const SEED_FP_HRP: &str = "zip32seedfp";
pub trait SecretSink {
type Error: std::error::Error;
fn store_seed(&mut self, entry: &::zewif::SeedEntry) -> Result<(), Self::Error>;
fn store_transparent_key(
&mut self,
entry: &::zewif::TransparentKeyEntry,
) -> Result<(), Self::Error>;
fn store_sapling_key(&mut self, entry: &::zewif::SaplingKeyEntry) -> Result<(), Self::Error>;
fn store_sprout_key(&mut self, entry: &::zewif::SproutKeyEntry) -> Result<(), Self::Error>;
fn store_unified_key(&mut self, entry: &::zewif::UnifiedKeyEntry) -> Result<(), Self::Error>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct DiscardSecrets;
impl SecretSink for DiscardSecrets {
type Error = core::convert::Infallible;
fn store_seed(&mut self, _entry: &::zewif::SeedEntry) -> Result<(), Self::Error> {
Ok(())
}
fn store_transparent_key(
&mut self,
_entry: &::zewif::TransparentKeyEntry,
) -> Result<(), Self::Error> {
Ok(())
}
fn store_sapling_key(&mut self, _entry: &::zewif::SaplingKeyEntry) -> Result<(), Self::Error> {
Ok(())
}
fn store_sprout_key(&mut self, _entry: &::zewif::SproutKeyEntry) -> Result<(), Self::Error> {
Ok(())
}
fn store_unified_key(&mut self, _entry: &::zewif::UnifiedKeyEntry) -> Result<(), Self::Error> {
Ok(())
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ZewifImportError<S> {
NetworkMismatch {
document: ::zewif::Network,
expected: NetworkType,
},
RegtestActivationMismatch {
branch_id: u32,
document_height: u32,
expected: Option<u32>,
},
EncryptedSecrets,
UfvkDecoding {
account_name: String,
message: String,
},
SaplingFvkDecoding {
account_name: String,
},
DerivedKeyMismatch {
account_name: String,
},
SeedFingerprintDecoding {
encoding: String,
},
SeedFingerprintMismatch {
claimed: String,
},
InvalidMnemonic {
fingerprint: String,
source: bip0039::Error,
},
InvalidSeedLength {
fingerprint: String,
},
InvalidAccountIndex {
account_name: String,
index: u32,
},
InvalidLegacyAddressIndex {
account_name: String,
index: u32,
},
InvalidMerkleNode {
account_name: String,
pool: ShieldedPool,
},
InvalidFrontier {
account_name: String,
pool: ShieldedPool,
source: incrementalmerkletree::frontier::FrontierError,
},
InvalidTransparentPubKey {
source: secp256k1::Error,
},
InvalidTransparentKeyEncoding {
address: String,
},
TransparentKeyMismatch {
address: String,
},
TransactionParse {
txid: zcash_protocol::TxId,
source: std::io::Error,
},
TxidMismatch {
recorded: zcash_protocol::TxId,
parsed: zcash_protocol::TxId,
},
Wallet(SqliteClientError),
Sink(S),
}
impl<S: fmt::Display> fmt::Display for ZewifImportError<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ZewifImportError::NetworkMismatch { document, expected } => write!(
f,
"Document was recorded for network {document:?}, but the wallet database is for {expected:?}."
),
ZewifImportError::RegtestActivationMismatch {
branch_id,
document_height,
expected,
} => write!(
f,
"The document's regtest activation height ({document_height}) for consensus branch ID {branch_id:#010x} does not match the wallet database's configured schedule ({expected:?})."
),
ZewifImportError::EncryptedSecrets => write!(
f,
"The document's secret store is encrypted; decrypt it before import."
),
ZewifImportError::UfvkDecoding {
account_name,
message,
} => write!(
f,
"Unable to parse the unified full viewing key of account \"{account_name}\": {message}"
),
ZewifImportError::SaplingFvkDecoding { account_name } => write!(
f,
"Unable to parse the Sapling extended full viewing key of account \"{account_name}\"."
),
ZewifImportError::DerivedKeyMismatch { account_name } => write!(
f,
"The seed and account index recorded for account \"{account_name}\" do not reproduce its recorded unified full viewing key."
),
ZewifImportError::SeedFingerprintDecoding { encoding } => write!(
f,
"\"{encoding}\" is not a valid zip32seedfp Bech32m seed fingerprint encoding."
),
ZewifImportError::SeedFingerprintMismatch { claimed } => write!(
f,
"Seed material does not match the fingerprint {claimed} under which it was recorded."
),
ZewifImportError::InvalidMnemonic {
fingerprint,
source,
} => write!(
f,
"The mnemonic recorded under seed fingerprint {fingerprint} is invalid: {source}"
),
ZewifImportError::InvalidSeedLength { fingerprint } => write!(
f,
"The seed recorded under fingerprint {fingerprint} has a length outside the range ZIP 32 permits."
),
ZewifImportError::InvalidAccountIndex {
account_name,
index,
} => write!(
f,
"Account \"{account_name}\" records ZIP 32 account index {index}, which is outside the valid range."
),
ZewifImportError::InvalidLegacyAddressIndex {
account_name,
index,
} => write!(
f,
"Account \"{account_name}\" records legacy address index {index}, which is outside the valid range."
),
ZewifImportError::InvalidMerkleNode { account_name, pool } => {
let pool = PoolType::Shielded(*pool);
write!(
f,
"The {pool} tree frontier in the birthday of account \"{account_name}\" contains an invalid node hash."
)
}
ZewifImportError::InvalidFrontier {
account_name,
pool,
source,
} => {
let pool = PoolType::Shielded(*pool);
write!(
f,
"The {pool} tree frontier in the birthday of account \"{account_name}\" is invalid: {source:?}"
)
}
ZewifImportError::InvalidTransparentPubKey { source } => write!(
f,
"A transparent public key in the secret store is not a valid secp256k1 point: {source}"
),
ZewifImportError::InvalidTransparentKeyEncoding { address } => write!(
f,
"The transparent spending key recorded for {address} is not a valid WIF encoding for the document's network."
),
ZewifImportError::TransparentKeyMismatch { address } => write!(
f,
"The transparent spending key recorded for {address} does not correspond to its recorded public key."
),
ZewifImportError::TransactionParse { txid, source } => write!(
f,
"Unable to parse the raw data of transaction {txid}: {source}"
),
ZewifImportError::TxidMismatch { recorded, parsed } => write!(
f,
"The raw data recorded for transaction {recorded} parses to a transaction with id {parsed}."
),
ZewifImportError::Wallet(e) => write!(f, "Wallet database error: {e}"),
ZewifImportError::Sink(e) => write!(f, "Secret sink error: {e}"),
}
}
}
impl<S: std::error::Error + 'static> std::error::Error for ZewifImportError<S> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ZewifImportError::InvalidMnemonic { source, .. } => Some(source),
ZewifImportError::InvalidTransparentPubKey { source } => Some(source),
ZewifImportError::TransactionParse { source, .. } => Some(source),
ZewifImportError::Wallet(e) => Some(e),
ZewifImportError::Sink(e) => Some(e),
_ => None,
}
}
}
impl<S> From<SqliteClientError> for ZewifImportError<S> {
fn from(e: SqliteClientError) -> Self {
ZewifImportError::Wallet(e)
}
}
impl<S> From<rusqlite::Error> for ZewifImportError<S> {
fn from(e: rusqlite::Error) -> Self {
ZewifImportError::Wallet(SqliteClientError::from(e))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccountSkipReason {
SproutViewingKey,
TransparentAddressSetWithoutSeed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BirthdayBasis {
ChainState,
BirthdayHeight,
SaplingActivation,
}
#[derive(Debug, Clone)]
pub struct ImportedAccount {
pub name: String,
pub account_uuid: AccountUuid,
pub birthday_basis: BirthdayBasis,
}
#[derive(Debug, Clone)]
pub struct SkippedAccount {
pub name: String,
pub reason: AccountSkipReason,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransparentKeySkipReason {
UncompressedPubKey,
NoOwningAccount,
}
#[derive(Debug, Clone)]
pub struct SkippedTransparentKey {
pub address: Option<String>,
pub reason: TransparentKeySkipReason,
}
#[derive(Debug, Clone, Default)]
pub struct ZewifImportReport {
pub imported_accounts: Vec<ImportedAccount>,
pub skipped_accounts: Vec<SkippedAccount>,
pub transparent_keys_registered: usize,
pub skipped_transparent_keys: Vec<SkippedTransparentKey>,
pub redeem_scripts_registered: usize,
pub redeem_scripts_not_representable: usize,
pub addresses_marked_exposed: usize,
pub addresses_not_recognized: usize,
pub addresses_never_exposed: usize,
pub transactions_stored: usize,
pub transactions_without_wallet_relevance: usize,
pub transactions_without_raw_data: usize,
pub address_book_entries_not_imported: usize,
}
struct AvailableSecrets {
seeds: HashMap<[u8; 32], SecretVec<u8>>,
sapling_fvks: Vec<String>,
unified_fvks: Vec<String>,
}
impl AvailableSecrets {
fn empty() -> Self {
AvailableSecrets {
seeds: HashMap::new(),
sapling_fvks: vec![],
unified_fvks: vec![],
}
}
}
fn decode_seed_fingerprint<S>(encoding: &str) -> Result<SeedFingerprint, ZewifImportError<S>> {
let err = || ZewifImportError::SeedFingerprintDecoding {
encoding: encoding.to_owned(),
};
let checked = CheckedHrpstring::new::<bech32::Bech32m>(encoding).map_err(|_| err())?;
if checked.hrp().to_lowercase() != SEED_FP_HRP {
return Err(err());
}
let bytes = checked.byte_iter().collect::<Vec<_>>();
let bytes: [u8; 32] = bytes.try_into().map_err(|_| err())?;
Ok(SeedFingerprint::from_bytes(bytes))
}
fn seed_entry_bytes<S>(
entry: &::zewif::SeedEntry,
) -> Result<(SeedFingerprint, SecretVec<u8>), ZewifImportError<S>> {
let claimed_encoding = entry.fingerprint().encoding();
let claimed = decode_seed_fingerprint(claimed_encoding)?;
let seed_bytes: Vec<u8> = match entry.material() {
::zewif::SeedMaterial::Bip39Mnemonic(m) => {
mnemonic_to_seed(m).map_err(|source| ZewifImportError::InvalidMnemonic {
fingerprint: claimed_encoding.to_owned(),
source,
})?
}
::zewif::SeedMaterial::LegacySeed(seed) => seed.as_bytes().to_vec(),
};
let computed =
SeedFingerprint::from_seed(&seed_bytes).ok_or(ZewifImportError::InvalidSeedLength {
fingerprint: claimed_encoding.to_owned(),
})?;
if computed.to_bytes() != claimed.to_bytes() {
return Err(ZewifImportError::SeedFingerprintMismatch {
claimed: claimed_encoding.to_owned(),
});
}
Ok((claimed, SecretVec::new(seed_bytes)))
}
fn mnemonic_to_seed(m: &::zewif::Bip39Mnemonic) -> Result<Vec<u8>, bip0039::Error> {
fn seed<L: bip0039::Language>(phrase: &str) -> Result<Vec<u8>, bip0039::Error> {
Ok(Mnemonic::<L>::from_phrase(phrase)?.to_seed("").to_vec())
}
let phrase = m.mnemonic().as_str();
match m.language() {
None | Some(L::English) => seed::<English>(phrase),
Some(L::SimplifiedChinese) => seed::<ChineseSimplified>(phrase),
Some(L::TraditionalChinese) => seed::<ChineseTraditional>(phrase),
Some(L::Czech) => seed::<Czech>(phrase),
Some(L::French) => seed::<French>(phrase),
Some(L::Italian) => seed::<Italian>(phrase),
Some(L::Japanese) => seed::<Japanese>(phrase),
Some(L::Korean) => seed::<Korean>(phrase),
Some(L::Portuguese) => seed::<Portuguese>(phrase),
Some(L::Spanish) => seed::<Spanish>(phrase),
Some(L::Other(_)) => Err(bip0039::Error::BadWordCount(0)),
}
}
fn convert_frontier<H, S, const DEPTH: u8>(
account_name: &str,
pool: ShieldedPool,
frontier: Option<&::zewif::Frontier>,
read_node: impl Fn(&::zewif::MerkleNode) -> Option<H>,
) -> Result<incrementalmerkletree::frontier::Frontier<H, DEPTH>, ZewifImportError<S>>
where
H: Clone,
{
let invalid_node = || ZewifImportError::InvalidMerkleNode {
account_name: account_name.to_owned(),
pool,
};
match frontier {
None | Some(::zewif::Frontier::Empty) => Ok(Frontier::empty()),
Some(::zewif::Frontier::NonEmpty(data)) => {
let leaf = read_node(data.leaf()).ok_or_else(invalid_node)?;
let ommers = data
.ommers()
.iter()
.map(&read_node)
.collect::<Option<Vec<_>>>()
.ok_or_else(invalid_node)?;
Frontier::from_parts(
incrementalmerkletree::Position::from(data.position()),
leaf,
ommers,
)
.map_err(|source| ZewifImportError::InvalidFrontier {
account_name: account_name.to_owned(),
pool,
source,
})
}
}
}
fn account_birthday<P: Parameters, S>(
params: &P,
account: &::zewif::Account,
) -> Result<(AccountBirthday, BirthdayBasis), ZewifImportError<S>> {
let zewif_block_hash =
|h: Option<::zewif::BlockHash>| h.map_or(BlockHash([0; 32]), |h| BlockHash(*h.as_bytes()));
if let Some(cs) = account.birthday_chain_state() {
let sapling = convert_frontier(
account.name(),
ShieldedPool::Sapling,
cs.sapling_tree(),
|n| {
let repr = *n.as_bytes();
Option::from(::sapling::Node::from_bytes(repr))
},
)?;
let orchard = convert_frontier(
account.name(),
ShieldedPool::Orchard,
cs.orchard_tree(),
|n| Option::from(orchard::tree::MerkleHashOrchard::from_bytes(n.as_bytes())),
)?;
let ironwood = convert_frontier(
account.name(),
ShieldedPool::Ironwood,
cs.ironwood_tree(),
|n| Option::from(orchard::tree::MerkleHashOrchard::from_bytes(n.as_bytes())),
)?;
let chain_state = ChainState::new(
BlockHeight::from(u32::from(cs.height())),
zewif_block_hash(cs.block_hash()),
sapling,
orchard,
ironwood,
);
Ok((
AccountBirthday::from_parts(
chain_state,
account
.recover_until_height()
.map(|h| BlockHeight::from(u32::from(h))),
),
BirthdayBasis::ChainState,
))
} else {
let (prior_height, basis) = match account.birthday_height() {
Some(h) => (
BlockHeight::from(u32::from(h)).saturating_sub(1),
BirthdayBasis::BirthdayHeight,
),
None => {
let sapling_activation = params
.activation_height(NetworkUpgrade::Sapling)
.unwrap_or_else(|| BlockHeight::from(0));
(
sapling_activation.saturating_sub(1),
BirthdayBasis::SaplingActivation,
)
}
};
Ok((
AccountBirthday::from_parts(
ChainState::empty(prior_height, BlockHash([0; 32])),
account
.recover_until_height()
.map(|h| BlockHeight::from(u32::from(h))),
),
basis,
))
}
}
fn plaintext_secret_store<E>(
document: &::zewif::Zewif,
) -> Result<Option<&::zewif::SecretStore>, ZewifImportError<E>> {
match document.secrets() {
None => Ok(None),
Some(::zewif::Secrets::Plain(store)) => Ok(Some(store)),
Some(::zewif::Secrets::Encrypted(_)) => Err(ZewifImportError::EncryptedSecrets),
}
}
fn derived_account_index<S>(
account_name: &str,
source: &::zewif::DerivedKeySource,
) -> Result<zip32::AccountId, ZewifImportError<S>> {
zip32::AccountId::try_from(source.account_index()).map_err(|_| {
ZewifImportError::InvalidAccountIndex {
account_name: account_name.to_owned(),
index: source.account_index(),
}
})
}
fn zip32_derivation<S>(
account_name: &str,
source: &::zewif::DerivedKeySource,
) -> Result<Zip32Derivation, ZewifImportError<S>> {
let seed_fp = decode_seed_fingerprint(source.seed_fingerprint().encoding())?;
let account_index = derived_account_index(account_name, source)?;
let legacy_address_index = source
.legacy_address_index()
.map(|i| {
zcash_keys::keys::zcashd::LegacyAddressIndex::try_from(i).map_err(|_| {
ZewifImportError::InvalidLegacyAddressIndex {
account_name: account_name.to_owned(),
index: i,
}
})
})
.transpose()?;
Ok(Zip32Derivation::new(
seed_fp,
account_index,
legacy_address_index,
))
}
fn verify_regtest_activations<P, S>(
params: &P,
regtest: &::zewif::RegtestParams,
) -> Result<(), ZewifImportError<S>>
where
P: Parameters,
{
for (&branch_id, &document_height) in regtest.activations() {
let expected = BranchId::try_from(branch_id)
.ok()
.and_then(|id| id.network_upgrade())
.and_then(|nu| params.activation_height(nu))
.map(u32::from);
if expected != Some(document_height) {
return Err(ZewifImportError::RegtestActivationMismatch {
branch_id,
document_height,
expected,
});
}
}
Ok(())
}
pub fn import_wallet<C, P, CL, R, S>(
wdb: &mut WalletDb<C, P, CL, R>,
document: &::zewif::Zewif,
sink: &mut S,
) -> Result<ZewifImportReport, ZewifImportError<S::Error>>
where
C: std::borrow::BorrowMut<rusqlite::Connection>,
P: consensus::Parameters,
CL: Clock,
R: RngCore,
S: SecretSink,
{
let params = wdb.params().clone();
let expected = params.network_type();
for wallet in document.wallets() {
match (wallet.network(), expected) {
(::zewif::Network::Mainnet, NetworkType::Main) => {}
(::zewif::Network::Testnet, NetworkType::Test) => {}
(::zewif::Network::Regtest(rp), NetworkType::Regtest) => {
verify_regtest_activations::<_, S::Error>(¶ms, rp)?;
}
(document_network, _) => {
return Err(ZewifImportError::NetworkMismatch {
document: document_network.clone(),
expected,
});
}
}
}
let secret_store = plaintext_secret_store::<S::Error>(document)?;
let mut available = AvailableSecrets::empty();
if let Some(store) = secret_store {
for entry in store.seeds() {
sink.store_seed(entry).map_err(ZewifImportError::Sink)?;
let (fingerprint, seed_bytes) = seed_entry_bytes(entry)?;
available.seeds.insert(fingerprint.to_bytes(), seed_bytes);
}
for entry in store.transparent_keys() {
sink.store_transparent_key(entry)
.map_err(ZewifImportError::Sink)?;
}
for entry in store.sapling_keys() {
sink.store_sapling_key(entry)
.map_err(ZewifImportError::Sink)?;
available
.sapling_fvks
.push(entry.fvk().encoding().to_owned());
}
for entry in store.sprout_keys() {
sink.store_sprout_key(entry)
.map_err(ZewifImportError::Sink)?;
}
for entry in store.unified_keys() {
sink.store_unified_key(entry)
.map_err(ZewifImportError::Sink)?;
available
.unified_fvks
.push(entry.fvk().encoding().to_owned());
}
}
wdb.transactionally::<_, _, ZewifImportError<S::Error>>(|wdb| {
let mut report = ZewifImportReport::default();
let mut taddrs = TransparentAddressRecords::default();
for wallet in document.wallets() {
report.address_book_entries_not_imported += wallet.address_book().len();
for account in wallet.accounts() {
import_account(wdb, ¶ms, account, &available, &mut taddrs, &mut report)?;
}
}
register_transparent_keys(wdb, ¶ms, secret_store, &taddrs, &mut report)?;
let accounts: std::collections::HashSet<AccountUuid> =
taddrs.owners.values().map(|(uuid, _)| *uuid).collect();
let pre_existing_exposures = exposed_receivers(wdb, &accounts)?;
import_transactions(wdb, ¶ms, document, &mut report)?;
mark_addresses_exposed(
wdb,
¶ms,
&taddrs,
&accounts,
&pre_existing_exposures,
&mut report,
)?;
Ok(report)
})
}
#[derive(Default)]
struct TransparentAddressRecords {
owners: HashMap<String, (AccountUuid, Option<BlockHeight>)>,
redeem_scripts: Vec<(AccountUuid, Vec<u8>)>,
}
fn decode_wif(expected_prefix: u8, wif: &str) -> Option<secp256k1::SecretKey> {
let payload = bs58::decode(wif).with_check(None).into_vec().ok()?;
match payload.as_slice() {
[prefix, key_data @ ..] if *prefix == expected_prefix && key_data.len() == 32 => {
secp256k1::SecretKey::from_slice(key_data).ok()
}
[prefix, key_data @ .., 0x01] if *prefix == expected_prefix && key_data.len() == 32 => {
secp256k1::SecretKey::from_slice(key_data).ok()
}
_ => None,
}
}
fn register_transparent_keys<DbT, P, S>(
wdb: &mut DbT,
params: &P,
store: Option<&::zewif::SecretStore>,
taddrs: &TransparentAddressRecords,
report: &mut ZewifImportReport,
) -> Result<(), ZewifImportError<S>>
where
DbT: WalletRead<AccountId = AccountUuid, Error = SqliteClientError> + WalletWrite,
P: consensus::Parameters,
S: std::error::Error,
{
let wif_prefix = match params.network_type() {
NetworkType::Main => 0x80,
NetworkType::Test | NetworkType::Regtest => 0xEF,
};
let secp = secp256k1::Secp256k1::new();
for entry in store.map_or(&[][..], |s| s.transparent_keys()) {
if !entry.pubkey().is_compressed() {
report.skipped_transparent_keys.push(SkippedTransparentKey {
address: None,
reason: TransparentKeySkipReason::UncompressedPubKey,
});
continue;
}
let pubkey = secp256k1::PublicKey::from_slice(entry.pubkey().as_slice())
.map_err(|source| ZewifImportError::InvalidTransparentPubKey { source })?;
let address = TransparentAddress::from_pubkey(&pubkey).encode(params);
let secret_key =
decode_wif(wif_prefix, entry.spending_key().encoding()).ok_or_else(|| {
ZewifImportError::InvalidTransparentKeyEncoding {
address: address.clone(),
}
})?;
if secret_key.public_key(&secp) != pubkey {
return Err(ZewifImportError::TransparentKeyMismatch { address });
}
match taddrs.owners.get(&address) {
Some((account_uuid, _)) => {
wdb.import_standalone_transparent_pubkey(*account_uuid, pubkey)
.map_err(ZewifImportError::Wallet)?;
report.transparent_keys_registered += 1;
}
None => {
report.skipped_transparent_keys.push(SkippedTransparentKey {
address: Some(address),
reason: TransparentKeySkipReason::NoOwningAccount,
});
}
}
}
for (account_uuid, script_bytes) in &taddrs.redeem_scripts {
let parsed = Redeem::parse(&Code(script_bytes.clone()));
match parsed {
Ok(redeem) => {
match wdb.import_standalone_transparent_script(*account_uuid, redeem) {
Ok(()) => report.redeem_scripts_registered += 1,
Err(SqliteClientError::BadAccountData(_)) => {
report.redeem_scripts_not_representable += 1;
}
Err(e) => return Err(ZewifImportError::Wallet(e)),
}
}
Err(_) => {
report.redeem_scripts_not_representable += 1;
}
}
}
Ok(())
}
fn exposed_receivers<DbT, S>(
wdb: &DbT,
accounts: &std::collections::HashSet<AccountUuid>,
) -> Result<std::collections::HashSet<TransparentAddress>, ZewifImportError<S>>
where
DbT: WalletRead<AccountId = AccountUuid, Error = SqliteClientError> + WalletWrite,
S: std::error::Error,
{
let mut exposed = std::collections::HashSet::new();
for account_uuid in accounts {
exposed.extend(
wdb.get_transparent_receivers(*account_uuid, true, true)
.map_err(ZewifImportError::Wallet)?
.into_iter()
.filter(|(_, meta)| matches!(meta.exposure(), Exposure::Exposed { .. }))
.map(|(taddr, _)| taddr),
);
}
Ok(exposed)
}
fn mark_addresses_exposed<DbT, P, S>(
wdb: &mut DbT,
params: &P,
taddrs: &TransparentAddressRecords,
accounts: &std::collections::HashSet<AccountUuid>,
pre_existing_exposures: &std::collections::HashSet<TransparentAddress>,
report: &mut ZewifImportReport,
) -> Result<(), ZewifImportError<S>>
where
DbT: WalletRead<AccountId = AccountUuid, Error = SqliteClientError> + WalletWrite,
P: consensus::Parameters,
S: std::error::Error,
{
let mut known: HashMap<TransparentAddress, (AccountUuid, TransparentAddressMetadata)> =
HashMap::new();
for account_uuid in accounts {
known.extend(
wdb.get_transparent_receivers(*account_uuid, true, true)
.map_err(ZewifImportError::Wallet)?
.into_iter()
.map(|(taddr, meta)| (taddr, (*account_uuid, meta))),
);
}
let mut exposed_indices: HashMap<
(AccountUuid, TransparentKeyScope),
BTreeMap<u32, BlockHeight>,
> = HashMap::new();
fn note_exposure(
exposed_indices: &mut HashMap<
(AccountUuid, TransparentKeyScope),
BTreeMap<u32, BlockHeight>,
>,
account_uuid: AccountUuid,
meta: &TransparentAddressMetadata,
height: BlockHeight,
) {
if let (Some(scope), Some(index)) = (meta.scope(), meta.address_index()) {
exposed_indices
.entry((account_uuid, scope))
.or_default()
.entry(index.index())
.and_modify(|h| *h = std::cmp::min(*h, height))
.or_insert(height);
}
}
let already_exposed: std::collections::HashSet<TransparentAddress> = known
.iter()
.filter_map(|(taddr, (account_uuid, meta))| match meta.exposure() {
Exposure::Exposed { at_height, .. } => {
if !pre_existing_exposures.contains(taddr) {
note_exposure(&mut exposed_indices, *account_uuid, meta, at_height);
}
Some(*taddr)
}
Exposure::Unknown | Exposure::CannotKnow => None,
})
.collect();
let mut exposures: Vec<(TransparentAddress, BlockHeight)> = vec![];
let mut unheighted: Vec<TransparentAddress> = vec![];
for (address_str, (_, exposure_height)) in &taddrs.owners {
let Some((taddr, (account_uuid, meta))) = TransparentAddress::decode(params, address_str)
.ok()
.and_then(|taddr| known.get(&taddr).map(|owner| (taddr, owner)))
else {
report.addresses_not_recognized += 1;
continue;
};
match exposure_height {
Some(height) => {
exposures.push((taddr, *height));
note_exposure(&mut exposed_indices, *account_uuid, meta, *height);
}
None => unheighted.push(taddr),
}
}
for taddr in unheighted {
if already_exposed.contains(&taddr) {
continue;
}
let (account_uuid, meta) = known.get(&taddr).expect("checked above");
let implied = meta
.scope()
.zip(meta.address_index())
.and_then(|(scope, index)| {
exposed_indices
.get(&(*account_uuid, scope))?
.range((index.index() + 1)..)
.map(|(_, height)| *height)
.min()
});
match implied {
Some(height) => exposures.push((taddr, height)),
None => report.addresses_never_exposed += 1,
}
}
exposures.sort();
if !exposures.is_empty() {
wdb.mark_transparent_addresses_exposed(&exposures)
.map_err(ZewifImportError::Wallet)?;
}
report.addresses_marked_exposed = exposures.len();
Ok(())
}
fn import_transactions<DbT, P, S>(
wdb: &mut DbT,
params: &P,
document: &::zewif::Zewif,
report: &mut ZewifImportReport,
) -> Result<(), ZewifImportError<S>>
where
DbT: WalletRead<AccountId = AccountUuid, Error = SqliteClientError> + WalletWrite,
P: consensus::Parameters,
S: std::error::Error,
{
let mut txs: Vec<&::zewif::Transaction> = document.transactions().values().collect();
txs.sort_by_key(|tx| {
(
tx.mined_height().map_or(u32::MAX, u32::from),
*tx.txid().as_bytes(),
)
});
let assumed_mempool_height = txs
.iter()
.filter_map(|tx| tx.mined_height())
.max()
.map_or(document.export_height(), |h| h + 1);
let chain_tip_known = wdb
.chain_height()
.map_err(ZewifImportError::Wallet)?
.is_some();
for tx in txs {
let raw = match tx.tx_data() {
Some(::zewif::TransactionData::Raw(raw)) => raw.data(),
Some(::zewif::TransactionData::Compact(_)) | None => {
report.transactions_without_raw_data += 1;
continue;
}
};
if !chain_tip_known {
report.transactions_without_wallet_relevance += 1;
continue;
}
let recorded_txid = zcash_protocol::TxId::from_bytes(*tx.txid().as_bytes());
let mined_height = tx.mined_height().map(|h| BlockHeight::from(u32::from(h)));
let branch_height = mined_height.unwrap_or_else(|| {
BlockHeight::from(u32::from(
tx.expiry_height()
.filter(|h| u32::from(*h) != 0)
.unwrap_or(assumed_mempool_height),
))
});
let parsed = Transaction::read(raw.as_slice(), BranchId::for_height(params, branch_height))
.map_err(|source| ZewifImportError::TransactionParse {
txid: recorded_txid,
source,
})?;
if parsed.txid() != recorded_txid {
return Err(ZewifImportError::TxidMismatch {
recorded: recorded_txid,
parsed: parsed.txid(),
});
}
decrypt_and_store_transaction(params, wdb, &parsed, mined_height)
.map_err(ZewifImportError::Wallet)?;
let stored = wdb
.get_transaction(recorded_txid)
.map_err(ZewifImportError::Wallet)?
.is_some();
if stored {
report.transactions_stored += 1;
} else {
report.transactions_without_wallet_relevance += 1;
}
}
Ok(())
}
fn import_account<DbT, P, S>(
wdb: &mut DbT,
params: &P,
account: &::zewif::Account,
available: &AvailableSecrets,
taddrs: &mut TransparentAddressRecords,
report: &mut ZewifImportReport,
) -> Result<(), ZewifImportError<S>>
where
DbT: WalletRead<AccountId = AccountUuid, Error = SqliteClientError> + WalletWrite,
P: consensus::Parameters,
S: std::error::Error,
{
let (birthday, birthday_basis) = account_birthday(params, account)?;
let key_source = account.provenance().or(Some("zewif"));
let derived_source = match account.key_source() {
Some(::zewif::KeySource::Derived(d)) => Some(d),
_ => None,
};
let available_seed = derived_source.and_then(|d| {
let fingerprint = decode_seed_fingerprint::<S>(d.seed_fingerprint().encoding()).ok()?;
available
.seeds
.get(&fingerprint.to_bytes())
.map(|seed| (seed, d))
});
let hd_derivation = match account.viewing_key() {
::zewif::AccountViewingKey::Ufvk(_) | ::zewif::AccountViewingKey::TransparentAddressSet => {
available_seed
}
_ => None,
};
let account_uuid = if let Some((seed, source)) = hd_derivation {
let account_index = derived_account_index(account.name(), source)?;
if let ::zewif::AccountViewingKey::Ufvk(ufvk) = account.viewing_key() {
verify_hd_derivation(params, account.name(), ufvk, seed, account_index)?;
}
let (imported, _usk) = wdb
.import_account_hd(account.name(), seed, account_index, &birthday, key_source)
.map_err(ZewifImportError::Wallet)?;
imported.id()
} else {
match account.viewing_key() {
::zewif::AccountViewingKey::Ufvk(ufvk) => {
let decoded =
UnifiedFullViewingKey::decode(params, ufvk.encoding()).map_err(|message| {
ZewifImportError::UfvkDecoding {
account_name: account.name().to_owned(),
message,
}
})?;
let purpose = account_purpose(
account,
available.unified_fvks.iter().any(|s| s == ufvk.encoding()),
derived_source,
)?;
let imported = wdb
.import_account_ufvk(account.name(), &decoded, &birthday, purpose, key_source)
.map_err(ZewifImportError::Wallet)?;
imported.id()
}
::zewif::AccountViewingKey::SaplingExtFvk(efvk) => {
let decoded = zcash_keys::encoding::decode_extended_full_viewing_key(
params
.network_type()
.hrp_sapling_extended_full_viewing_key(),
efvk.encoding(),
)
.map_err(|_| ZewifImportError::SaplingFvkDecoding {
account_name: account.name().to_owned(),
})?;
let ufvk = UnifiedFullViewingKey::from_sapling_extended_full_viewing_key(decoded)
.map_err(|e| ZewifImportError::UfvkDecoding {
account_name: account.name().to_owned(),
message: e.to_string(),
})?;
let purpose = account_purpose(
account,
available.sapling_fvks.iter().any(|s| s == efvk.encoding()),
derived_source,
)?;
let imported = wdb
.import_account_ufvk(account.name(), &ufvk, &birthday, purpose, key_source)
.map_err(ZewifImportError::Wallet)?;
imported.id()
}
::zewif::AccountViewingKey::SproutViewingKey(_) => {
report.skipped_accounts.push(SkippedAccount {
name: account.name().to_owned(),
reason: AccountSkipReason::SproutViewingKey,
});
return Ok(());
}
::zewif::AccountViewingKey::TransparentAddressSet => {
report.skipped_accounts.push(SkippedAccount {
name: account.name().to_owned(),
reason: AccountSkipReason::TransparentAddressSetWithoutSeed,
});
return Ok(());
}
}
};
for address in account.addresses() {
if let ::zewif::ProtocolAddress::Transparent(taddr) = address.address() {
let exposure_height = address
.exposed_at_height()
.map(|h| BlockHeight::from(u32::from(h)));
taddrs
.owners
.insert(taddr.address().to_owned(), (account_uuid, exposure_height));
if let Some(script) = taddr.redeem_script() {
taddrs
.redeem_scripts
.push((account_uuid, script.as_ref().to_vec()));
}
}
}
report.imported_accounts.push(ImportedAccount {
name: account.name().to_owned(),
account_uuid,
birthday_basis,
});
Ok(())
}
fn account_purpose<S>(
account: &::zewif::Account,
spending_key_available: bool,
derived_source: Option<&::zewif::DerivedKeySource>,
) -> Result<AccountPurpose, ZewifImportError<S>> {
let derivation = derived_source
.map(|d| zip32_derivation(account.name(), d))
.transpose()?;
Ok(match account.purpose() {
Some(::zewif::AccountPurpose::ViewOnly) => AccountPurpose::ViewOnly,
Some(::zewif::AccountPurpose::Spending) => AccountPurpose::Spending { derivation },
None => {
if spending_key_available {
AccountPurpose::Spending { derivation }
} else {
AccountPurpose::ViewOnly
}
}
})
}
fn ufvk_components_consistent(
derived: &UnifiedFullViewingKey,
recorded: &UnifiedFullViewingKey,
) -> bool {
let transparent_ok = match (derived.transparent(), recorded.transparent()) {
(Some(a), Some(b)) => a.serialize() == b.serialize(),
_ => true,
};
let sapling_ok = match (derived.sapling(), recorded.sapling()) {
(Some(a), Some(b)) => a.to_bytes() == b.to_bytes(),
_ => true,
};
let orchard_ok = match (derived.orchard(), recorded.orchard()) {
(Some(a), Some(b)) => a.to_bytes() == b.to_bytes(),
_ => true,
};
transparent_ok && sapling_ok && orchard_ok
}
fn verify_hd_derivation<P: Parameters, S>(
params: &P,
account_name: &str,
ufvk: &::zewif::UnifiedFullViewingKey,
seed: &SecretVec<u8>,
account_index: zip32::AccountId,
) -> Result<(), ZewifImportError<S>> {
let recorded = UnifiedFullViewingKey::decode(params, ufvk.encoding()).map_err(|message| {
ZewifImportError::UfvkDecoding {
account_name: account_name.to_owned(),
message,
}
})?;
let derived = UnifiedSpendingKey::from_seed(params, seed.expose_secret(), account_index)
.map_err(|_| ZewifImportError::DerivedKeyMismatch {
account_name: account_name.to_owned(),
})?
.to_unified_full_viewing_key();
if ufvk_components_consistent(&derived, &recorded) {
Ok(())
} else {
Err(ZewifImportError::DerivedKeyMismatch {
account_name: account_name.to_owned(),
})
}
}
#[cfg(test)]
mod tests {
use ::transparent::{
address::Script,
bundle as transparent,
bundle::{Authorized, OutPoint, TxIn, TxOut},
keys::{IncomingViewingKey, NonHardenedChildIndex},
};
use bip0039::{English, Mnemonic};
use incrementalmerkletree::Hashable as _;
use std::collections::BTreeMap;
use tempfile::NamedTempFile;
use zcash_client_backend::{
data_api::{Account as _, AccountPurpose, AccountSource, WalletRead},
wallet::Exposure,
};
use zcash_keys::{
encoding::AddressCodec,
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
};
use zcash_protocol::{consensus, local_consensus::LocalNetwork, value::Zatoshis};
use zip32::fingerprint::SeedFingerprint;
use super::*;
use crate::{
testing::db::{test_clock, test_rng},
wallet::init::WalletMigrator,
};
use zcash_primitives::transaction::{TransactionData, TxVersion};
const TEST_NETWORK: consensus::Network = consensus::Network::TestNetwork;
#[derive(Default)]
struct RecordingSink {
seeds: Vec<String>,
transparent: Vec<Vec<u8>>,
sapling: Vec<String>,
sprout: Vec<String>,
unified: Vec<String>,
}
impl SecretSink for RecordingSink {
type Error = core::convert::Infallible;
fn store_seed(&mut self, entry: &::zewif::SeedEntry) -> Result<(), Self::Error> {
self.seeds.push(entry.fingerprint().encoding().to_owned());
Ok(())
}
fn store_transparent_key(
&mut self,
entry: &::zewif::TransparentKeyEntry,
) -> Result<(), Self::Error> {
self.transparent.push(entry.pubkey().as_slice().to_vec());
Ok(())
}
fn store_sapling_key(
&mut self,
entry: &::zewif::SaplingKeyEntry,
) -> Result<(), Self::Error> {
self.sapling.push(entry.fvk().encoding().to_owned());
Ok(())
}
fn store_sprout_key(&mut self, entry: &::zewif::SproutKeyEntry) -> Result<(), Self::Error> {
self.sprout.push(entry.address().to_owned());
Ok(())
}
fn store_unified_key(
&mut self,
entry: &::zewif::UnifiedKeyEntry,
) -> Result<(), Self::Error> {
self.unified.push(entry.fvk().encoding().to_owned());
Ok(())
}
}
fn test_wallet_db() -> (
NamedTempFile,
WalletDb<
rusqlite::Connection,
consensus::Network,
crate::util::testing::FixedClock,
rand_chacha::ChaChaRng,
>,
) {
let db_file = NamedTempFile::new().unwrap();
let mut wdb =
WalletDb::for_path(db_file.path(), TEST_NETWORK, test_clock(), test_rng()).unwrap();
WalletMigrator::new().init_or_migrate(&mut wdb).unwrap();
(db_file, wdb)
}
fn regtest_local_network() -> LocalNetwork {
let one = Some(BlockHeight::from_u32(1));
LocalNetwork {
overwinter: one,
sapling: one,
blossom: one,
heartwood: one,
canopy: one,
nu5: one,
nu6: one,
nu6_1: one,
nu6_2: one,
nu6_3: one,
#[cfg(zcash_unstable = "nu7")]
nu7: one,
}
}
fn regtest_wallet_db() -> (
NamedTempFile,
WalletDb<
rusqlite::Connection,
LocalNetwork,
crate::util::testing::FixedClock,
rand_chacha::ChaChaRng,
>,
) {
let db_file = NamedTempFile::new().unwrap();
let mut wdb = WalletDb::for_path(
db_file.path(),
regtest_local_network(),
test_clock(),
test_rng(),
)
.unwrap();
WalletMigrator::new().init_or_migrate(&mut wdb).unwrap();
(db_file, wdb)
}
fn regtest_network(activations: BTreeMap<u32, u32>) -> ::zewif::Network {
::zewif::Network::Regtest(::zewif::RegtestParams::new(activations))
}
#[test]
fn regtest_empty_schedule_is_trusted() {
let params = regtest_local_network();
let regtest = ::zewif::RegtestParams::new(BTreeMap::new());
assert!(
verify_regtest_activations::<_, core::convert::Infallible>(¶ms, ®test).is_ok()
);
}
#[test]
fn regtest_matching_schedule_is_accepted() {
let params = regtest_local_network();
let activations = BTreeMap::from([
(u32::from(BranchId::Sapling), 1),
(u32::from(BranchId::Nu5), 1),
(u32::from(BranchId::Nu6_3), 1),
]);
let regtest = ::zewif::RegtestParams::new(activations);
assert!(
verify_regtest_activations::<_, core::convert::Infallible>(¶ms, ®test).is_ok()
);
}
#[test]
fn regtest_mismatched_height_is_rejected() {
let params = regtest_local_network();
let activations = BTreeMap::from([(u32::from(BranchId::Sapling), 2)]);
let regtest = ::zewif::RegtestParams::new(activations);
let err = verify_regtest_activations::<_, core::convert::Infallible>(¶ms, ®test)
.unwrap_err();
assert!(matches!(
err,
ZewifImportError::RegtestActivationMismatch {
branch_id,
document_height: 2,
expected: Some(1),
} if branch_id == u32::from(BranchId::Sapling)
));
}
#[test]
fn regtest_unknown_branch_id_is_rejected() {
let params = regtest_local_network();
let activations = BTreeMap::from([(0xDEAD_BEEFu32, 1)]);
let regtest = ::zewif::RegtestParams::new(activations);
let err = verify_regtest_activations::<_, core::convert::Infallible>(¶ms, ®test)
.unwrap_err();
assert!(matches!(
err,
ZewifImportError::RegtestActivationMismatch {
branch_id: 0xDEAD_BEEF,
document_height: 1,
expected: None,
}
));
}
#[test]
fn regtest_document_imports_against_regtest_params() {
let (_file, mut wdb) = regtest_wallet_db();
let (mut doc, wallet) = document(regtest_network(BTreeMap::new()));
doc.add_wallet(wallet);
let report = import_wallet(&mut wdb, &doc, &mut DiscardSecrets).unwrap();
assert!(report.imported_accounts.is_empty());
assert!(report.skipped_accounts.is_empty());
}
#[test]
fn regtest_document_with_mismatched_schedule_is_rejected() {
let (_file, mut wdb) = regtest_wallet_db();
let activations = BTreeMap::from([(u32::from(BranchId::Sapling), 999)]);
let (mut doc, wallet) = document(regtest_network(activations));
doc.add_wallet(wallet);
let err = import_wallet(&mut wdb, &doc, &mut DiscardSecrets).unwrap_err();
assert!(matches!(
err,
ZewifImportError::RegtestActivationMismatch { .. }
));
}
#[test]
fn regtest_document_against_testnet_params_is_a_mismatch() {
let (_file, mut wdb) = test_wallet_db();
let (mut doc, wallet) = document(regtest_network(BTreeMap::new()));
doc.add_wallet(wallet);
let err = import_wallet(&mut wdb, &doc, &mut DiscardSecrets).unwrap_err();
assert!(matches!(err, ZewifImportError::NetworkMismatch { .. }));
}
fn encode_seed_fp(fp: &SeedFingerprint) -> String {
bech32::encode::<bech32::Bech32m>(bech32::Hrp::parse(SEED_FP_HRP).unwrap(), &fp.to_bytes())
.unwrap()
}
struct TestSeed {
mnemonic_phrase: String,
fingerprint_encoding: String,
ufvk: UnifiedFullViewingKey,
}
fn test_seed(account_index: u32) -> TestSeed {
let mnemonic = <Mnemonic<English>>::from_entropy([0xAB; 32]).unwrap();
let seed = mnemonic.to_seed("");
let fp = SeedFingerprint::from_seed(&seed).unwrap();
let usk = UnifiedSpendingKey::from_seed(
&TEST_NETWORK,
&seed,
zip32::AccountId::try_from(account_index).unwrap(),
)
.unwrap();
TestSeed {
mnemonic_phrase: mnemonic.phrase().to_owned(),
fingerprint_encoding: encode_seed_fp(&fp),
ufvk: usk.to_unified_full_viewing_key(),
}
}
fn seed_entry(ts: &TestSeed) -> ::zewif::SeedEntry {
::zewif::SeedEntry::new(
::zewif::SeedFingerprint::new(ts.fingerprint_encoding.clone()),
::zewif::SeedMaterial::Bip39Mnemonic(::zewif::Bip39Mnemonic::new(
ts.mnemonic_phrase.clone(),
None,
)),
)
}
fn document(network: ::zewif::Network) -> (::zewif::Zewif, ::zewif::ZewifWallet) {
let doc = ::zewif::Zewif::new(
::zewif::BlockHeight::from(3_000_000),
::zewif::BlockHash::from_bytes([0xEE; 32]),
);
let wallet = ::zewif::ZewifWallet::new(network);
(doc, wallet)
}
#[test]
fn network_mismatch_is_rejected() {
let (_file, mut wdb) = test_wallet_db();
let (mut doc, wallet) = document(::zewif::Network::Mainnet);
doc.add_wallet(wallet);
let result = import_wallet(&mut wdb, &doc, &mut DiscardSecrets);
assert!(matches!(
result,
Err(ZewifImportError::NetworkMismatch { .. })
));
}
#[test]
fn view_only_ufvk_account_with_chain_state_birthday() {
let (_file, mut wdb) = test_wallet_db();
let ts = test_seed(0);
let mut account = ::zewif::Account::new(::zewif::AccountViewingKey::Ufvk(
::zewif::UnifiedFullViewingKey::new(ts.ufvk.encode(&TEST_NETWORK)),
));
account.set_name("viewing");
account.set_birthday_height(::zewif::BlockHeight::from(2_600_000));
let mut chain_state = ::zewif::ChainState::new(::zewif::BlockHeight::from(2_599_999));
chain_state.set_block_hash(::zewif::BlockHash::from_bytes([0xBB; 32]));
chain_state.set_sapling_tree(::zewif::Frontier::NonEmpty(
::zewif::FrontierData::from_parts(
0,
::zewif::MerkleNode::new(::sapling::Node::empty_root(0.into()).to_bytes()),
vec![],
),
));
chain_state.set_orchard_tree(::zewif::Frontier::Empty);
chain_state.set_ironwood_tree(::zewif::Frontier::Empty);
account.set_birthday_chain_state(chain_state);
account.set_recover_until_height(::zewif::BlockHeight::from(2_900_000));
let (mut doc, mut wallet) = document(::zewif::Network::Testnet);
wallet.add_account(account);
doc.add_wallet(wallet);
let report = import_wallet(&mut wdb, &doc, &mut DiscardSecrets).unwrap();
assert_eq!(report.imported_accounts.len(), 1);
assert_eq!(report.imported_accounts[0].name, "viewing");
assert_eq!(
report.imported_accounts[0].birthday_basis,
BirthdayBasis::ChainState
);
let account_uuid = report.imported_accounts[0].account_uuid;
let imported = wdb.get_account(account_uuid).unwrap().unwrap();
assert!(matches!(
imported.source(),
AccountSource::Imported {
purpose: AccountPurpose::ViewOnly,
..
}
));
assert_eq!(
wdb.get_wallet_birthday().unwrap(),
Some(consensus::BlockHeight::from(2_600_000))
);
}
#[test]
fn derived_account_is_imported_via_hd_derivation() {
let (_file, mut wdb) = test_wallet_db();
let ts = test_seed(0);
let mut store = ::zewif::SecretStore::new();
store.add_seed(seed_entry(&ts));
let mut account = ::zewif::Account::new(::zewif::AccountViewingKey::Ufvk(
::zewif::UnifiedFullViewingKey::new(ts.ufvk.encode(&TEST_NETWORK)),
));
account.set_name("derived");
account.set_birthday_height(::zewif::BlockHeight::from(2_600_000));
account.set_key_source(::zewif::KeySource::Derived(::zewif::DerivedKeySource::new(
::zewif::SeedFingerprint::new(ts.fingerprint_encoding.clone()),
0,
None,
)));
let (mut doc, mut wallet) = document(::zewif::Network::Testnet);
wallet.add_account(account);
doc.add_wallet(wallet);
doc.set_secrets(::zewif::Secrets::Plain(store));
let mut sink = RecordingSink::default();
let report = import_wallet(&mut wdb, &doc, &mut sink).unwrap();
assert_eq!(sink.seeds, vec![ts.fingerprint_encoding.clone()]);
assert_eq!(report.imported_accounts.len(), 1);
assert_eq!(
report.imported_accounts[0].birthday_basis,
BirthdayBasis::BirthdayHeight
);
let account_uuid = report.imported_accounts[0].account_uuid;
let imported = wdb.get_account(account_uuid).unwrap().unwrap();
match imported.source() {
AccountSource::Derived { derivation, .. } => {
assert_eq!(
derivation.seed_fingerprint().to_bytes(),
SeedFingerprint::from_seed(
&<Mnemonic<English>>::from_phrase(&ts.mnemonic_phrase)
.unwrap()
.to_seed("")
)
.unwrap()
.to_bytes()
);
assert_eq!(derivation.account_index(), zip32::AccountId::ZERO);
}
other => panic!("expected a derived account, got {other:?}"),
}
assert_eq!(
imported.ufvk().map(|k| k.encode(&TEST_NETWORK)),
Some(ts.ufvk.encode(&TEST_NETWORK)),
);
}
#[test]
fn sprout_accounts_are_skipped() {
let (_file, mut wdb) = test_wallet_db();
let mut account = ::zewif::Account::new(::zewif::AccountViewingKey::SproutViewingKey(
::zewif::sprout::SproutViewingKey::new("ZiVtTestViewingKey"),
));
account.set_name("sprout");
let (mut doc, mut wallet) = document(::zewif::Network::Testnet);
wallet.add_account(account);
doc.add_wallet(wallet);
let report = import_wallet(&mut wdb, &doc, &mut DiscardSecrets).unwrap();
assert!(report.imported_accounts.is_empty());
assert_eq!(report.skipped_accounts.len(), 1);
assert_eq!(
report.skipped_accounts[0].reason,
AccountSkipReason::SproutViewingKey
);
}
#[test]
fn secrets_are_delivered_to_the_sink() {
let (_file, mut wdb) = test_wallet_db();
let ts = test_seed(0);
let secp = secp256k1::Secp256k1::new();
let secret_key = secp256k1::SecretKey::from_slice(&[0x42; 32]).unwrap();
let pubkey = secret_key.public_key(&secp);
let mut wif_payload = vec![0xEF];
wif_payload.extend_from_slice(&secret_key.secret_bytes());
wif_payload.push(0x01);
let wif = bs58::encode(wif_payload).with_check().into_string();
let mut store = ::zewif::SecretStore::new();
store.add_seed(seed_entry(&ts));
store.add_transparent_key(::zewif::TransparentKeyEntry::new(
::zewif::transparent::TransparentPubKey::from_bytes(pubkey.serialize().to_vec())
.unwrap(),
::zewif::transparent::TransparentSpendingKey::new(wif),
));
store.add_sapling_key(::zewif::SaplingKeyEntry::new(
::zewif::sapling::SaplingExtendedFullViewingKey::new("zxviewtestsapling1aaaa"),
::zewif::sapling::SaplingExtendedSpendingKey::new("secret-extended-key-test1aaaa"),
));
store.add_sprout_key(::zewif::SproutKeyEntry::new(
"ztTestSproutAddress",
::zewif::sprout::SproutSpendingKey::new("SKTestSproutKey"),
));
let (mut doc, wallet) = document(::zewif::Network::Testnet);
doc.add_wallet(wallet);
doc.set_secrets(::zewif::Secrets::Plain(store));
let mut sink = RecordingSink::default();
let report = import_wallet(&mut wdb, &doc, &mut sink).unwrap();
assert_eq!(sink.seeds.len(), 1);
assert_eq!(sink.transparent.len(), 1);
assert_eq!(sink.sapling.len(), 1);
assert_eq!(sink.sprout.len(), 1);
assert_eq!(report.transparent_keys_registered, 0);
assert_eq!(report.skipped_transparent_keys.len(), 1);
assert_eq!(
report.skipped_transparent_keys[0].reason,
TransparentKeySkipReason::NoOwningAccount
);
}
#[test]
fn owned_transparent_key_is_registered_and_exposed() {
let (_file, mut wdb) = test_wallet_db();
let ts = test_seed(0);
let secp = secp256k1::Secp256k1::new();
let secret_key = secp256k1::SecretKey::from_slice(&[0x42; 32]).unwrap();
let pubkey = secret_key.public_key(&secp);
let address = TransparentAddress::from_pubkey(&pubkey).encode(&TEST_NETWORK);
let mut wif_payload = vec![0xEF];
wif_payload.extend_from_slice(&secret_key.secret_bytes());
wif_payload.push(0x01);
let wif = bs58::encode(wif_payload).with_check().into_string();
let mut store = ::zewif::SecretStore::new();
store.add_seed(seed_entry(&ts));
store.add_transparent_key(::zewif::TransparentKeyEntry::new(
::zewif::transparent::TransparentPubKey::from_bytes(pubkey.serialize().to_vec())
.unwrap(),
::zewif::transparent::TransparentSpendingKey::new(wif),
));
let mut account = ::zewif::Account::new(::zewif::AccountViewingKey::TransparentAddressSet);
account.set_name("legacy");
account.set_birthday_height(::zewif::BlockHeight::from(2_600_000));
account.set_key_source(::zewif::KeySource::Derived(::zewif::DerivedKeySource::new(
::zewif::SeedFingerprint::new(ts.fingerprint_encoding.clone()),
0x7FFF_FFFF,
None,
)));
let mut taddr = ::zewif::transparent::Address::new(address.clone());
taddr.set_pubkey(
::zewif::transparent::TransparentPubKey::from_bytes(pubkey.serialize().to_vec())
.unwrap(),
);
let mut exposed = ::zewif::Address::new(::zewif::ProtocolAddress::Transparent(taddr));
exposed.set_exposed_at_height(::zewif::BlockHeight::from(2_600_050));
account.add_address(exposed);
let (mut doc, mut wallet) = document(::zewif::Network::Testnet);
wallet.add_account(account);
doc.add_wallet(wallet);
doc.set_secrets(::zewif::Secrets::Plain(store));
let mut sink = RecordingSink::default();
let report = import_wallet(&mut wdb, &doc, &mut sink).unwrap();
assert_eq!(report.imported_accounts.len(), 1);
assert_eq!(report.transparent_keys_registered, 1);
assert!(report.skipped_transparent_keys.is_empty());
assert_eq!(report.addresses_marked_exposed, 1);
assert_eq!(report.addresses_not_recognized, 0);
assert_eq!(report.addresses_never_exposed, 0);
}
#[test]
fn unexposed_transparent_key_is_registered_but_not_exposed() {
let (_file, mut wdb) = test_wallet_db();
let ts = test_seed(0);
let secp = secp256k1::Secp256k1::new();
let secret_key = secp256k1::SecretKey::from_slice(&[0x42; 32]).unwrap();
let pubkey = secret_key.public_key(&secp);
let address = TransparentAddress::from_pubkey(&pubkey).encode(&TEST_NETWORK);
let mut wif_payload = vec![0xEF];
wif_payload.extend_from_slice(&secret_key.secret_bytes());
wif_payload.push(0x01);
let wif = bs58::encode(wif_payload).with_check().into_string();
let mut store = ::zewif::SecretStore::new();
store.add_seed(seed_entry(&ts));
store.add_transparent_key(::zewif::TransparentKeyEntry::new(
::zewif::transparent::TransparentPubKey::from_bytes(pubkey.serialize().to_vec())
.unwrap(),
::zewif::transparent::TransparentSpendingKey::new(wif),
));
let mut account = ::zewif::Account::new(::zewif::AccountViewingKey::TransparentAddressSet);
account.set_name("legacy");
account.set_birthday_height(::zewif::BlockHeight::from(2_600_000));
account.set_key_source(::zewif::KeySource::Derived(::zewif::DerivedKeySource::new(
::zewif::SeedFingerprint::new(ts.fingerprint_encoding.clone()),
0x7FFF_FFFF,
None,
)));
let mut taddr = ::zewif::transparent::Address::new(address.clone());
taddr.set_pubkey(
::zewif::transparent::TransparentPubKey::from_bytes(pubkey.serialize().to_vec())
.unwrap(),
);
account.add_address(::zewif::Address::new(
::zewif::ProtocolAddress::Transparent(taddr),
));
let (mut doc, mut wallet) = document(::zewif::Network::Testnet);
wallet.add_account(account);
doc.add_wallet(wallet);
doc.set_secrets(::zewif::Secrets::Plain(store));
let mut sink = RecordingSink::default();
let report = import_wallet(&mut wdb, &doc, &mut sink).unwrap();
assert_eq!(report.imported_accounts.len(), 1);
assert_eq!(report.transparent_keys_registered, 1);
assert!(report.skipped_transparent_keys.is_empty());
assert_eq!(report.addresses_marked_exposed, 0);
assert_eq!(report.addresses_not_recognized, 0);
assert_eq!(report.addresses_never_exposed, 1);
}
#[test]
fn address_below_an_exposed_index_is_exposed() {
let (_file, mut wdb) = test_wallet_db();
let ts = test_seed(0);
let mut store = ::zewif::SecretStore::new();
store.add_seed(seed_entry(&ts));
let external_ivk = ts
.ufvk
.transparent()
.expect("the test UFVK has a transparent component")
.derive_external_ivk()
.unwrap();
let addr_at = |index: u32| {
external_ivk
.derive_address(NonHardenedChildIndex::from_index(index).unwrap())
.unwrap()
.encode(&TEST_NETWORK)
};
let mut account = hd_account(&ts, ts.fingerprint_encoding.clone(), 0);
for index in [0, 1, 4] {
account.add_address(::zewif::Address::new(
::zewif::ProtocolAddress::Transparent(::zewif::transparent::Address::new(addr_at(
index,
))),
));
}
let mut exposed = ::zewif::Address::new(::zewif::ProtocolAddress::Transparent(
::zewif::transparent::Address::new(addr_at(2)),
));
exposed.set_exposed_at_height(::zewif::BlockHeight::from(2_600_050));
account.add_address(exposed);
let (mut doc, mut wallet) = document(::zewif::Network::Testnet);
wallet.add_account(account);
doc.add_wallet(wallet);
doc.set_secrets(::zewif::Secrets::Plain(store));
let report = import_wallet(&mut wdb, &doc, &mut RecordingSink::default()).unwrap();
assert_eq!(report.imported_accounts.len(), 1);
assert_eq!(report.addresses_not_recognized, 0);
assert_eq!(report.addresses_marked_exposed, 3);
assert_eq!(report.addresses_never_exposed, 1);
let receivers = wdb
.get_transparent_receivers(report.imported_accounts[0].account_uuid, true, true)
.unwrap();
let exposure_at = |index: u32| {
receivers
.get(&TransparentAddress::decode(&TEST_NETWORK, &addr_at(index)).unwrap())
.expect("the address is a receiver of the imported account")
.exposure()
};
for index in [0, 1, 2] {
assert!(
matches!(
exposure_at(index),
Exposure::Exposed { at_height, .. } if at_height == BlockHeight::from(2_600_050)
),
"address at index {index} should be exposed at the recorded height, \
but was {:?}",
exposure_at(index)
);
}
assert!(!matches!(exposure_at(4), Exposure::Exposed { .. }));
}
#[test]
fn transactions_without_raw_data_are_counted() {
let (_file, mut wdb) = test_wallet_db();
let (mut doc, wallet) = document(::zewif::Network::Testnet);
doc.add_wallet(wallet);
let txid = ::zewif::TxId::from_bytes([0x11; 32]);
doc.add_transaction(txid, ::zewif::Transaction::new(txid));
let report = import_wallet(&mut wdb, &doc, &mut DiscardSecrets).unwrap();
assert_eq!(report.transactions_without_raw_data, 1);
assert_eq!(report.transactions_stored, 0);
assert_eq!(report.transactions_without_wallet_relevance, 0);
}
fn test_seed_bytes(ts: &TestSeed) -> Vec<u8> {
<Mnemonic<English>>::from_phrase(&ts.mnemonic_phrase)
.unwrap()
.to_seed("")
.to_vec()
}
fn hd_account(
ts: &TestSeed,
fingerprint_encoding: String,
account_index: u32,
) -> ::zewif::Account {
let mut account = ::zewif::Account::new(::zewif::AccountViewingKey::Ufvk(
::zewif::UnifiedFullViewingKey::new(ts.ufvk.encode(&TEST_NETWORK)),
));
account.set_name("hd");
account.set_birthday_height(::zewif::BlockHeight::from(2_600_000));
account.set_key_source(::zewif::KeySource::Derived(::zewif::DerivedKeySource::new(
::zewif::SeedFingerprint::new(fingerprint_encoding),
account_index,
None,
)));
account
}
#[test]
fn seed_is_matched_by_a_case_differing_fingerprint_encoding() {
let (_file, mut wdb) = test_wallet_db();
let ts = test_seed(0);
let mut store = ::zewif::SecretStore::new();
store.add_seed(seed_entry(&ts));
let account = hd_account(&ts, ts.fingerprint_encoding.to_uppercase(), 0);
let (mut doc, mut wallet) = document(::zewif::Network::Testnet);
wallet.add_account(account);
doc.add_wallet(wallet);
doc.set_secrets(::zewif::Secrets::Plain(store));
let mut sink = RecordingSink::default();
let report = import_wallet(&mut wdb, &doc, &mut sink).unwrap();
assert_eq!(report.imported_accounts.len(), 1);
let imported = wdb
.get_account(report.imported_accounts[0].account_uuid)
.unwrap()
.unwrap();
assert!(matches!(imported.source(), AccountSource::Derived { .. }));
}
#[test]
fn hd_derivation_inconsistent_with_recorded_ufvk_is_rejected() {
let (_file, mut wdb) = test_wallet_db();
let ts = test_seed(0);
let mut store = ::zewif::SecretStore::new();
store.add_seed(seed_entry(&ts));
let account = hd_account(&ts, ts.fingerprint_encoding.clone(), 1);
let (mut doc, mut wallet) = document(::zewif::Network::Testnet);
wallet.add_account(account);
doc.add_wallet(wallet);
doc.set_secrets(::zewif::Secrets::Plain(store));
let result = import_wallet(&mut wdb, &doc, &mut RecordingSink::default());
assert!(matches!(
result,
Err(ZewifImportError::DerivedKeyMismatch { .. })
));
}
#[test]
fn unified_account_is_spending_when_its_key_was_delivered() {
let (_file, mut wdb) = test_wallet_db();
let ts = test_seed(0);
let mut store = ::zewif::SecretStore::new();
store.add_unified_key(::zewif::UnifiedKeyEntry::new(
::zewif::UnifiedFullViewingKey::new(ts.ufvk.encode(&TEST_NETWORK)),
::zewif::UnifiedSpendingKey::new("usk1testspendingkey"),
));
let mut account = ::zewif::Account::new(::zewif::AccountViewingKey::Ufvk(
::zewif::UnifiedFullViewingKey::new(ts.ufvk.encode(&TEST_NETWORK)),
));
account.set_name("spending");
account.set_birthday_height(::zewif::BlockHeight::from(2_600_000));
let (mut doc, mut wallet) = document(::zewif::Network::Testnet);
wallet.add_account(account);
doc.add_wallet(wallet);
doc.set_secrets(::zewif::Secrets::Plain(store));
let report = import_wallet(&mut wdb, &doc, &mut RecordingSink::default()).unwrap();
assert_eq!(report.imported_accounts.len(), 1);
let imported = wdb
.get_account(report.imported_accounts[0].account_uuid)
.unwrap()
.unwrap();
assert!(matches!(
imported.source(),
AccountSource::Imported {
purpose: AccountPurpose::Spending { .. },
..
}
));
}
fn transparent_tx_to(
to: &TransparentAddress,
value: u64,
height: u32,
) -> (zcash_protocol::TxId, Vec<u8>) {
let height = consensus::BlockHeight::from(height);
let tx = TransactionData::from_parts(
TxVersion::V5,
BranchId::for_height(&TEST_NETWORK, height),
0,
height + 100,
#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
Zatoshis::ZERO,
Some(transparent::Bundle {
vin: vec![TxIn::from_parts(OutPoint::fake(), Script::default(), 0)],
vout: vec![TxOut::new(
Zatoshis::const_from_u64(value),
to.script().into(),
)],
authorization: Authorized,
}),
None,
None,
None,
)
.freeze()
.unwrap();
let mut bytes = vec![];
tx.write(&mut bytes).unwrap();
(tx.txid(), bytes)
}
fn transparent_tx_to_many(
outputs: &[(&TransparentAddress, u64)],
height: u32,
) -> (zcash_protocol::TxId, Vec<u8>) {
let height = consensus::BlockHeight::from(height);
let tx = TransactionData::from_parts(
TxVersion::V5,
BranchId::for_height(&TEST_NETWORK, height),
0,
height + 100,
#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
Zatoshis::ZERO,
Some(transparent::Bundle {
vin: vec![TxIn::from_parts(OutPoint::fake(), Script::default(), 0)],
vout: outputs
.iter()
.map(|(to, value)| {
TxOut::new(Zatoshis::const_from_u64(*value), to.script().into())
})
.collect(),
authorization: Authorized,
}),
None,
None,
None,
)
.freeze()
.unwrap();
let mut bytes = vec![];
tx.write(&mut bytes).unwrap();
(tx.txid(), bytes)
}
fn raw_zewif_tx(
txid: zcash_protocol::TxId,
raw: &[u8],
mined_height: u32,
) -> ::zewif::Transaction {
let mut tx = ::zewif::Transaction::new(::zewif::TxId::from_bytes(*txid.as_ref()));
tx.set_tx_data(::zewif::TransactionData::Raw(::zewif::RawTxData::new(
::zewif::Data::from_bytes(raw),
)));
tx.set_mined_height(::zewif::BlockHeight::from(mined_height));
tx
}
#[test]
fn raw_transaction_relevant_to_account_is_stored() {
let (_file, mut wdb) = test_wallet_db();
let ts = test_seed(0);
let seed = test_seed_bytes(&ts);
let usk =
UnifiedSpendingKey::from_seed(&TEST_NETWORK, &seed, zip32::AccountId::ZERO).unwrap();
let (taddr, _) = usk.default_transparent_address();
let height = 2_600_000;
let (txid, raw) = transparent_tx_to(&taddr, 100_000, height);
let mut store = ::zewif::SecretStore::new();
store.add_seed(seed_entry(&ts));
let account = hd_account(&ts, ts.fingerprint_encoding.clone(), 0);
let (mut doc, mut wallet) = document(::zewif::Network::Testnet);
wallet.add_account(account);
doc.add_wallet(wallet);
doc.set_secrets(::zewif::Secrets::Plain(store));
let tx = raw_zewif_tx(txid, &raw, height);
doc.add_transaction(tx.txid(), tx);
let report = import_wallet(&mut wdb, &doc, &mut RecordingSink::default()).unwrap();
assert_eq!(report.imported_accounts.len(), 1);
assert_eq!(report.transactions_stored, 1);
assert_eq!(report.transactions_without_wallet_relevance, 0);
assert_eq!(report.transactions_without_raw_data, 0);
}
#[test]
fn address_without_height_appearing_in_a_stored_transaction_is_exposed() {
let (_file, mut wdb) = test_wallet_db();
let ts = test_seed(0);
let seed = test_seed_bytes(&ts);
let usk =
UnifiedSpendingKey::from_seed(&TEST_NETWORK, &seed, zip32::AccountId::ZERO).unwrap();
let (taddr, _) = usk.default_transparent_address();
let height = 2_600_000;
let (txid, raw) = transparent_tx_to(&taddr, 100_000, height);
let mut store = ::zewif::SecretStore::new();
store.add_seed(seed_entry(&ts));
let mut account = hd_account(&ts, ts.fingerprint_encoding.clone(), 0);
account.add_address(::zewif::Address::new(
::zewif::ProtocolAddress::Transparent(::zewif::transparent::Address::new(
taddr.encode(&TEST_NETWORK),
)),
));
let (mut doc, mut wallet) = document(::zewif::Network::Testnet);
wallet.add_account(account);
doc.add_wallet(wallet);
doc.set_secrets(::zewif::Secrets::Plain(store));
let tx = raw_zewif_tx(txid, &raw, height);
doc.add_transaction(tx.txid(), tx);
let report = import_wallet(&mut wdb, &doc, &mut RecordingSink::default()).unwrap();
assert_eq!(report.imported_accounts.len(), 1);
assert_eq!(report.transactions_stored, 1);
assert_eq!(report.addresses_never_exposed, 0);
let receivers = wdb
.get_transparent_receivers(report.imported_accounts[0].account_uuid, true, true)
.unwrap();
let exposure = receivers
.get(&taddr)
.expect("the receiver is tracked by the account")
.exposure();
assert!(
matches!(exposure, Exposure::Exposed { at_height, .. } if at_height == BlockHeight::from(height)),
"expected exposure at mined height, got {exposure:?}",
);
}
#[test]
fn wallet_owned_change_address_in_a_stored_payment_is_exposed() {
let (_file, mut wdb) = test_wallet_db();
let ts = test_seed(0);
let seed = test_seed_bytes(&ts);
let usk =
UnifiedSpendingKey::from_seed(&TEST_NETWORK, &seed, zip32::AccountId::ZERO).unwrap();
let change_addr = usk
.transparent()
.to_account_pubkey()
.derive_internal_ivk()
.unwrap()
.derive_address(NonHardenedChildIndex::ZERO)
.unwrap();
let payee = TransparentAddress::PublicKeyHash([7u8; 20]);
let height = 2_600_000;
let (txid, raw) =
transparent_tx_to_many(&[(&payee, 90_000), (&change_addr, 10_000)], height);
let mut store = ::zewif::SecretStore::new();
store.add_seed(seed_entry(&ts));
let account = hd_account(&ts, ts.fingerprint_encoding.clone(), 0);
let (mut doc, mut wallet) = document(::zewif::Network::Testnet);
wallet.add_account(account);
doc.add_wallet(wallet);
doc.set_secrets(::zewif::Secrets::Plain(store));
let tx = raw_zewif_tx(txid, &raw, height);
doc.add_transaction(tx.txid(), tx);
let report = import_wallet(&mut wdb, &doc, &mut RecordingSink::default()).unwrap();
assert_eq!(report.imported_accounts.len(), 1);
assert_eq!(report.transactions_stored, 1);
let receivers = wdb
.get_transparent_receivers(report.imported_accounts[0].account_uuid, true, true)
.unwrap();
let exposure = receivers
.get(&change_addr)
.expect("the change address is tracked by the account")
.exposure();
assert!(
matches!(exposure, Exposure::Exposed { at_height, .. } if at_height == BlockHeight::from(height)),
"expected the wallet-owned change address to be exposed, got {exposure:?}",
);
}
#[test]
fn transactions_are_deferred_when_no_account_establishes_a_chain_tip() {
let (_file, mut wdb) = test_wallet_db();
let ts = test_seed(0);
let seed = test_seed_bytes(&ts);
let usk =
UnifiedSpendingKey::from_seed(&TEST_NETWORK, &seed, zip32::AccountId::ZERO).unwrap();
let (taddr, _) = usk.default_transparent_address();
let height = 2_600_000;
let (txid, raw) = transparent_tx_to(&taddr, 100_000, height);
let mut account = ::zewif::Account::new(::zewif::AccountViewingKey::SproutViewingKey(
::zewif::sprout::SproutViewingKey::new("ZiVtTestViewingKey"),
));
account.set_name("sprout");
let (mut doc, mut wallet) = document(::zewif::Network::Testnet);
wallet.add_account(account);
doc.add_wallet(wallet);
let tx = raw_zewif_tx(txid, &raw, height);
doc.add_transaction(tx.txid(), tx);
let report = import_wallet(&mut wdb, &doc, &mut DiscardSecrets).unwrap();
assert!(report.imported_accounts.is_empty());
assert_eq!(report.transactions_stored, 0);
assert_eq!(report.transactions_without_wallet_relevance, 1);
assert_eq!(report.transactions_without_raw_data, 0);
}
#[test]
fn a_failed_import_commits_nothing() {
let (_file, mut wdb) = test_wallet_db();
let ts = test_seed(0);
let mut account = ::zewif::Account::new(::zewif::AccountViewingKey::Ufvk(
::zewif::UnifiedFullViewingKey::new(ts.ufvk.encode(&TEST_NETWORK)),
));
account.set_name("viewing");
account.set_birthday_height(::zewif::BlockHeight::from(2_600_000));
let seed = test_seed_bytes(&ts);
let usk =
UnifiedSpendingKey::from_seed(&TEST_NETWORK, &seed, zip32::AccountId::ZERO).unwrap();
let (taddr, _) = usk.default_transparent_address();
let height = 2_600_000;
let (_txid, raw) = transparent_tx_to(&taddr, 100_000, height);
let wrong_txid = zcash_protocol::TxId::from_bytes([0xFF; 32]);
let (mut doc, mut wallet) = document(::zewif::Network::Testnet);
wallet.add_account(account);
doc.add_wallet(wallet);
let tx = raw_zewif_tx(wrong_txid, &raw, height);
doc.add_transaction(tx.txid(), tx);
let result = import_wallet(&mut wdb, &doc, &mut DiscardSecrets);
assert!(matches!(result, Err(ZewifImportError::TxidMismatch { .. })));
assert_eq!(wdb.get_wallet_birthday().unwrap(), None);
}
}