use incrementalmerkletree::{Position, Retention};
use orchard::tree::MerkleHashOrchard;
use orchard::value::NoteValue;
use shardtree::store::{Checkpoint, ShardStore as _};
use zcash_keys::keys::UnifiedSpendingKey;
use pepper_sync::sync::{ScanPriority, ScanRange};
use pepper_sync::wallet::{
IronwoodNote, OrchardNote, OutputId, SaplingNote, SyncState, TransparentCoin, WalletTransaction,
};
use zcash_primitives::transaction::TxId;
use zcash_protocol::consensus::BlockHeight;
use zcash_protocol::consensus::Parameters as _;
use zcash_protocol::memo::Memo;
use zcash_protocol::value::Zatoshis;
use zingo_common_components::protocol::ActivationHeights;
use zingo_status::confirmation_status::ConfirmationStatus;
use crate::config::{ChainType, WalletConfig};
use crate::mocks::SaplingCryptoNoteBuilder;
use crate::mocks::nullifier::{OrchardNullifierBuilder, SaplingNullifierBuilder};
use crate::mocks::orchard_note::OrchardCryptoNoteBuilder;
use crate::testutils::default_test_wallet_settings;
use crate::wallet::LightWallet;
#[cfg(test)]
use crate::wallet::keys::unified::ReceiverSelection;
#[cfg(test)]
use zcash_address::ZcashAddress;
#[cfg(test)]
use zcash_keys::address::UnifiedAddress;
pub struct SyntheticWalletBuilder {
mnemonic: String,
tip: u32,
activation_heights: ActivationHeights,
ironwood_note_values: Vec<u64>,
orchard_note_values: Vec<u64>,
sapling_note_values: Vec<u64>,
transparent_coin_values: Vec<u64>,
}
impl SyntheticWalletBuilder {
pub fn new(mnemonic: &str) -> Self {
Self {
mnemonic: mnemonic.to_string(),
tip: 20,
activation_heights: crate::testutils::mock_activation_heights(),
ironwood_note_values: Vec::new(),
orchard_note_values: Vec::new(),
sapling_note_values: Vec::new(),
transparent_coin_values: Vec::new(),
}
}
pub fn tip(mut self, tip: u32) -> Self {
self.tip = tip;
self
}
pub fn activation_heights(mut self, heights: ActivationHeights) -> Self {
self.activation_heights = heights;
self
}
pub fn ironwood_note(mut self, value: u64) -> Self {
self.ironwood_note_values.push(value);
self
}
pub fn orchard_note(mut self, value: u64) -> Self {
self.orchard_note_values.push(value);
self
}
pub fn sapling_note(mut self, value: u64) -> Self {
self.sapling_note_values.push(value);
self
}
pub fn transparent_coin(mut self, value: u64) -> Self {
self.transparent_coin_values.push(value);
self
}
pub fn build(&self) -> LightWallet {
assert!(
self.tip as usize
> self.ironwood_note_values.len()
+ self.orchard_note_values.len()
+ self.sapling_note_values.len()
+ self.transparent_coin_values.len()
+ 1,
"tip must exceed the highest note confirmation height"
);
let mut wallet = LightWallet::new(
ChainType::Regtest(self.activation_heights),
WalletConfig::MnemonicPhrase {
mnemonic_phrase: self.mnemonic.clone(),
no_of_accounts: 1.try_into().expect("hard-coded non-zero"),
birthday: 1,
wallet_settings: default_test_wallet_settings(),
},
)
.expect("synthetic wallet construction from mnemonic succeeds");
wallet.sync_state = SyncState::new_for_test(vec![ScanRange::from_parts(
BlockHeight::from_u32(1)..BlockHeight::from_u32(self.tip + 1),
ScanPriority::Scanned,
)]);
let unified_spending_key: UnifiedSpendingKey = wallet
.unified_key_store
.get(&zip32::AccountId::ZERO)
.expect("a fresh mnemonic wallet carries account zero")
.try_into()
.expect("mnemonic wallets carry spend capability");
let unified_full_viewing_key = unified_spending_key.to_unified_full_viewing_key();
let mut nullifiers = OrchardNullifierBuilder::new();
let orchard_recipient = unified_full_viewing_key
.orchard()
.expect("unified key carries an orchard fvk")
.address_at(0u32, zip32::Scope::External);
for (index, value) in self.ironwood_note_values.iter().enumerate() {
let txid = TxId::from_bytes([u8::try_from(index).unwrap() + 1; 32]);
let crypto_note = OrchardCryptoNoteBuilder::default()
.recipient(orchard_recipient)
.value(NoteValue::from_raw(*value))
.build();
wallet
.shard_trees
.ironwood
.append(
MerkleHashOrchard::from_cmx(&crypto_note.commitment().into()),
Retention::Marked,
)
.expect("appending to the in-memory orchard tree succeeds");
let note = IronwoodNote::new_for_test(
OutputId::new(txid, 0),
zip32::AccountId::ZERO,
zip32::Scope::External,
crypto_note,
Memo::Empty,
Some(Position::from(index as u64)),
)
.with_nullifier_for_test(nullifiers.assign_unique_nullifier().build());
wallet.wallet_transactions.insert(
txid,
WalletTransaction::new_for_test_with_ironwood_notes(
txid,
ConfirmationStatus::Confirmed(BlockHeight::from_u32(
2 + u32::try_from(index).unwrap(),
)),
vec![note],
vec![],
),
);
}
let mut note_count = self.ironwood_note_values.len();
for (index, value) in self.orchard_note_values.iter().enumerate() {
let txid = TxId::from_bytes([0x40 + u8::try_from(index).unwrap(); 32]);
let crypto_note = OrchardCryptoNoteBuilder::default()
.recipient(orchard_recipient)
.value(NoteValue::from_raw(*value))
.note_version(orchard::NoteVersion::V2)
.build();
wallet
.shard_trees
.orchard
.append(
MerkleHashOrchard::from_cmx(&crypto_note.commitment().into()),
Retention::Marked,
)
.expect("appending to the in-memory orchard tree succeeds");
let note = OrchardNote::new_for_test(
OutputId::new(txid, 0),
zip32::AccountId::ZERO,
zip32::Scope::External,
crypto_note,
Memo::Empty,
Some(Position::from(index as u64)),
)
.with_nullifier_for_test(nullifiers.assign_unique_nullifier().build());
wallet.wallet_transactions.insert(
txid,
WalletTransaction::new_for_test_with_orchard_notes(
txid,
ConfirmationStatus::Confirmed(BlockHeight::from_u32(
2 + u32::try_from(note_count + index).unwrap(),
)),
vec![note],
vec![],
),
);
}
let mut sapling_nullifiers = SaplingNullifierBuilder::new();
note_count += self.orchard_note_values.len();
let sapling_recipient = unified_full_viewing_key
.sapling()
.expect("unified key carries a sapling dfvk")
.default_address()
.1;
for (index, value) in self.sapling_note_values.iter().enumerate() {
let txid = TxId::from_bytes([0x80 + u8::try_from(index).unwrap(); 32]);
let mut crypto_note = SaplingCryptoNoteBuilder::default();
crypto_note
.recipient(sapling_recipient)
.value(sapling_crypto::value::NoteValue::from_raw(*value));
let crypto_note = crypto_note.build();
wallet
.shard_trees
.sapling
.append(
sapling_crypto::Node::from_cmu(&crypto_note.cmu()),
Retention::Marked,
)
.expect("appending to the in-memory sapling tree succeeds");
let note = SaplingNote::new_for_test(
OutputId::new(txid, 0),
zip32::AccountId::ZERO,
zip32::Scope::External,
crypto_note,
Memo::Empty,
Some(Position::from(index as u64)),
)
.with_nullifier_for_test(sapling_nullifiers.assign_unique_nullifier().build());
wallet.wallet_transactions.insert(
txid,
WalletTransaction::new_for_test(
txid,
ConfirmationStatus::Confirmed(BlockHeight::from_u32(
2 + u32::try_from(note_count + index).unwrap(),
)),
)
.with_sapling_notes_for_test(vec![note]),
);
}
let tip = BlockHeight::from_u32(self.tip);
let checkpoint_covering = |leaf_count: usize| {
if leaf_count == 0 {
Checkpoint::tree_empty()
} else {
Checkpoint::at_position(Position::from(leaf_count as u64 - 1))
}
};
wallet
.shard_trees
.sapling
.store_mut()
.add_checkpoint(tip, checkpoint_covering(self.sapling_note_values.len()))
.expect("infallible on the memory store");
wallet
.shard_trees
.orchard
.store_mut()
.add_checkpoint(tip, checkpoint_covering(self.orchard_note_values.len()))
.expect("infallible on the memory store");
wallet
.shard_trees
.ironwood
.store_mut()
.add_checkpoint(tip, checkpoint_covering(self.ironwood_note_values.len()))
.expect("infallible on the memory store");
let shielded_note_count = self.ironwood_note_values.len()
+ self.orchard_note_values.len()
+ self.sapling_note_values.len();
for (index, value) in self.transparent_coin_values.iter().enumerate() {
let txid = TxId::from_bytes([0xC0 + u8::try_from(index).unwrap(); 32]);
let (key_id, address) = wallet
.transparent_addresses()
.iter()
.next()
.map(|(key_id, address)| (*key_id, address.clone()))
.expect("a fresh wallet carries one transparent address");
let script = zcash_address::ZcashAddress::try_from_encoded(&address)
.expect("wallet-generated address encodes validly")
.convert_if_network::<zcash_transparent::address::TransparentAddress>(
wallet.chain_type().network_type(),
)
.expect("wallet-generated address matches the wallet network")
.script()
.into();
let coin = TransparentCoin::new_for_test(
OutputId::new(txid, 0),
key_id,
address,
script,
Zatoshis::from_u64(*value).expect("coin values are valid zatoshis"),
);
wallet.wallet_transactions.insert(
txid,
WalletTransaction::new_for_test(
txid,
ConfirmationStatus::Confirmed(BlockHeight::from_u32(
2 + u32::try_from(shielded_note_count + index).unwrap(),
)),
)
.with_transparent_coins_for_test(vec![coin]),
);
}
wallet
}
}
#[cfg(test)]
fn stranger_wallet() -> LightWallet {
SyntheticWalletBuilder::new(zingo_test_vectors::seeds::CHIMNEY_BETTER_SEED).build()
}
#[cfg(test)]
fn stranger_orchard_address() -> (ChainType, UnifiedAddress) {
let mut stranger = stranger_wallet();
let (_, unified_address) = stranger
.generate_unified_address(ReceiverSelection::orchard_only(), zip32::AccountId::ZERO)
.expect("a seed-built synthetic wallet holds account zero's Orchard key");
(stranger.chain_type(), unified_address)
}
#[cfg(test)]
pub(crate) fn external_orchard_address() -> String {
let (chain_type, unified_address) = stranger_orchard_address();
unified_address.encode(&chain_type)
}
#[cfg(test)]
pub(crate) fn external_orchard_zcash_address() -> ZcashAddress {
let (chain_type, unified_address) = stranger_orchard_address();
unified_address.to_zcash_address(chain_type.network_type())
}
#[cfg(test)]
pub(crate) fn external_transparent_address() -> String {
stranger_wallet()
.transparent_addresses()
.values()
.next()
.expect("a seed-built synthetic wallet derives account zero's first transparent address")
.clone()
}
pub fn inject_confirmed_orchard_notes(wallet: &mut LightWallet, count: u32, value: u64, tip: u32) {
let orchard_fvk: orchard::keys::FullViewingKey = wallet
.unified_key_store
.get(&zip32::AccountId::ZERO)
.expect("account zero exists")
.try_into()
.expect("mnemonic wallets carry an orchard key");
let recipient = orchard_fvk.address_at(0u32, zip32::Scope::External);
for index in 0..count {
let txid = TxId::from_bytes([0x40 + u8::try_from(index).expect("small note corpus"); 32]);
let crypto_note = OrchardCryptoNoteBuilder::default()
.recipient(recipient)
.value(NoteValue::from_raw(value))
.note_version(orchard::NoteVersion::V2)
.build();
wallet
.shard_trees
.orchard
.append(
MerkleHashOrchard::from_cmx(&crypto_note.commitment().into()),
Retention::Marked,
)
.expect("appending to the in-memory orchard tree succeeds");
let nullifier = crypto_note.nullifier(&orchard_fvk);
let note = OrchardNote::new_for_test(
OutputId::new(txid, 0),
zip32::AccountId::ZERO,
zip32::Scope::External,
crypto_note,
Memo::Empty,
Some(Position::from(u64::from(index))),
)
.with_nullifier_for_test(nullifier);
wallet.wallet_transactions.insert(
txid,
WalletTransaction::new_for_test_with_orchard_notes(
txid,
ConfirmationStatus::Confirmed(BlockHeight::from_u32(2 + index)),
vec![note],
vec![],
),
);
}
let tip = BlockHeight::from_u32(tip);
wallet
.shard_trees
.orchard
.store_mut()
.remove_checkpoint(&tip)
.expect("infallible on the memory store");
wallet
.shard_trees
.orchard
.store_mut()
.add_checkpoint(
tip,
Checkpoint::at_position(Position::from(u64::from(count) - 1)),
)
.expect("infallible on the memory store");
}