#![warn(missing_docs)]
use std::num::NonZeroU32;
use std::{io::Read, string::String, time::Duration};
use pepper_sync::config::{PerformanceLevel, SyncConfig, TransparentAddressDiscovery};
use pepper_sync::keys::decode_address;
use zcash_address::unified::Fvk;
use zcash_keys::address::UnifiedAddress;
use zcash_keys::encoding::AddressCodec;
use zcash_protocol::consensus::NetworkConstants;
use zcash_protocol::{PoolType, ShieldedPool, consensus};
use zingo_common_components::protocol::{ActivationHeights, ActivationHeightsBuilder};
use crate::lightclient::LightClient;
use crate::lightclient::error::LightClientError;
use crate::wallet::WalletSettings;
use crate::wallet::keys::unified::UnifiedKeyStore;
use crate::wallet::output::SpendStatus;
use crate::wallet::summary::data::{
BasicCoinSummary, BasicNoteSummary, OutgoingNoteSummary, TransactionSummary,
};
#[must_use]
pub fn mock_activation_heights() -> ActivationHeights {
mock_activation_heights_with(|era| era)
}
pub fn mock_activation_heights_with(
adjust: impl FnOnce(ActivationHeightsBuilder) -> ActivationHeightsBuilder,
) -> ActivationHeights {
const GENESIS_SUCCESSOR: Option<u32> = Some(1);
adjust(
ActivationHeights::builder()
.set_overwinter(GENESIS_SUCCESSOR)
.set_sapling(GENESIS_SUCCESSOR)
.set_blossom(GENESIS_SUCCESSOR)
.set_heartwood(GENESIS_SUCCESSOR)
.set_canopy(GENESIS_SUCCESSOR)
.set_nu5(GENESIS_SUCCESSOR)
.set_nu6(GENESIS_SUCCESSOR)
.set_nu6_1(GENESIS_SUCCESSOR)
.set_nu6_2(GENESIS_SUCCESSOR)
.set_nu6_3(GENESIS_SUCCESSOR)
.set_nu7(GENESIS_SUCCESSOR),
)
.build()
}
#[must_use]
pub fn mock_activation_heights_deferring_nu6_3(activation: u32) -> ActivationHeights {
mock_activation_heights_with(|era| era.set_nu6_3(Some(activation)).set_nu7(None))
}
pub mod assertions;
pub mod chain_generics;
pub mod fee_tables;
pub mod lightclient;
pub mod macros;
pub mod migration;
pub mod mock_indexer;
pub mod paths;
pub mod socks5_relay;
pub mod synthetic_wallet;
pub use portpicker;
pub use tempfile;
pub fn default_test_wallet_settings() -> WalletSettings {
WalletSettings {
sync_config: SyncConfig {
transparent_address_discovery: TransparentAddressDiscovery::minimal(),
performance_level: PerformanceLevel::High,
shutdown_on_completion: true,
},
min_confirmations: NonZeroU32::try_from(1).expect("hard-coded non-zero integer"),
}
}
#[must_use]
pub fn build_fvks_from_unified_keystore(unified_keystore: &UnifiedKeyStore) -> [Fvk; 3] {
let orchard_vk: orchard::keys::FullViewingKey = unified_keystore.try_into().unwrap();
let sapling_vk: sapling_crypto::zip32::DiversifiableFullViewingKey =
unified_keystore.try_into().unwrap();
let transparent_vk: zcash_transparent::keys::AccountPubKey =
unified_keystore.try_into().unwrap();
let mut transparent_vk_bytes = [0u8; 65];
transparent_vk_bytes.copy_from_slice(&transparent_vk.serialize());
[
Fvk::Orchard(orchard_vk.to_bytes()),
Fvk::Sapling(sapling_vk.to_bytes()),
Fvk::P2pkh(transparent_vk_bytes),
]
}
pub async fn assert_transaction_summary_exists(
lightclient: &LightClient,
expected: &TransactionSummary,
) {
assert!(
check_transaction_summary_exists(lightclient, expected).await,
"wallet summaries: {}\n\n\nexpected: {}\n\n\n",
lightclient.transaction_summaries(false).await.unwrap(),
expected,
);
}
pub async fn check_transaction_summary_exists(
lightclient: &LightClient,
transaction_summary: &TransactionSummary,
) -> bool {
lightclient
.transaction_summaries(false)
.await
.unwrap()
.iter()
.any(|wallet_summary| {
check_transaction_summary_equality(wallet_summary, transaction_summary)
})
}
pub fn assert_transaction_summary_equality(
observed: &TransactionSummary,
expected: &TransactionSummary,
) {
assert!(
check_transaction_summary_equality(observed, expected),
"observed: {observed}\n\n\nexpected: {expected}\n\n\n",
);
}
#[must_use]
pub fn check_transaction_summary_equality(
first: &TransactionSummary,
second: &TransactionSummary,
) -> bool {
first.status == second.status
&& first.blockheight == second.blockheight
&& first.kind == second.kind
&& first.value == second.value
&& first.fee == second.fee
&& first.zec_price == second.zec_price
&& check_note_summary_equality(&first.orchard_notes, &second.orchard_notes)
&& check_note_summary_equality(&first.sapling_notes, &second.sapling_notes)
&& check_transparent_coin_summary_equality(
&first.transparent_coins,
&second.transparent_coins,
)
&& check_outgoing_note_summary_equality(
&first.outgoing_orchard_notes,
&second.outgoing_orchard_notes,
)
&& check_outgoing_note_summary_equality(
&first.outgoing_sapling_notes,
&second.outgoing_sapling_notes,
)
}
fn check_note_summary_equality(first: &[BasicNoteSummary], second: &[BasicNoteSummary]) -> bool {
if first.len() != second.len() {
return false;
}
for i in 0..first.len() {
if !(first[i].value == second[i].value
&& check_spend_status_equality(first[i].spend_status, second[i].spend_status)
&& first[i].memo == second[i].memo)
{
return false;
}
}
true
}
fn check_outgoing_note_summary_equality(
first: &[OutgoingNoteSummary],
second: &[OutgoingNoteSummary],
) -> bool {
if first.len() != second.len() {
return false;
}
for i in 0..first.len() {
if !(first[i].value == second[i].value
&& first[i].memo == second[i].memo
&& first[i].recipient == second[i].recipient
&& first[i].recipient_unified_address == second[i].recipient_unified_address)
&& first[i].account_id == second[i].account_id
&& first[i].scope == second[i].scope
{
return false;
}
}
true
}
fn check_transparent_coin_summary_equality(
first: &[BasicCoinSummary],
second: &[BasicCoinSummary],
) -> bool {
if first.len() != second.len() {
return false;
}
for i in 0..first.len() {
if !(first[i].value == second[i].value
&& check_spend_status_equality(first[i].spend_summary, second[i].spend_summary))
{
return false;
}
}
true
}
fn check_spend_status_equality(first: SpendStatus, second: SpendStatus) -> bool {
matches!(
(first, second),
(SpendStatus::Unspent, SpendStatus::Unspent)
| (SpendStatus::Spent(_), SpendStatus::Spent(_))
| (
SpendStatus::TransmittedSpent(_),
SpendStatus::TransmittedSpent(_)
)
| (SpendStatus::MempoolSpent(_), SpendStatus::MempoolSpent(_))
)
}
pub async fn sync_to_target_height(
client: &mut LightClient,
target_block_height: u32,
) -> Result<(), LightClientError> {
client.sync_and_await().await?;
while u32::from(
client
.wallet()
.read()
.await
.sync_state
.fully_scanned_height()
.unwrap(),
) < target_block_height
{
tokio::time::sleep(Duration::from_millis(500)).await;
client.sync_and_await().await?;
}
Ok(())
}
pub struct RecordingReader<Reader> {
from: Reader,
read_lengths: Vec<usize>,
}
impl<T> Read for RecordingReader<T>
where
T: Read,
{
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let for_info = self.from.read(buf)?;
log::info!("{for_info:?}");
self.read_lengths.push(for_info);
Ok(for_info)
}
}
#[derive(Debug)]
pub struct TxNotesCount {
pub transparent_tx_notes: usize,
pub sapling_tx_notes: usize,
pub orchard_tx_notes: usize,
}
#[derive(Debug)]
pub struct TxActionsCount {
pub transparent_tx_actions: usize,
pub sapling_tx_actions: usize,
pub orchard_tx_actions: usize,
}
pub fn port_to_localhost_uri(port: impl std::fmt::Display) -> http::Uri {
format!("http://localhost:{port}").parse().unwrap()
}
#[must_use]
pub fn int_to_shieldedprotocol(int: i32) -> ShieldedPool {
match int {
1 => ShieldedPool::Sapling,
2 => ShieldedPool::Orchard,
_ => panic!("invalid protocol"),
}
}
#[must_use]
pub fn int_to_pooltype(int: i32) -> PoolType {
match int {
0 => PoolType::Transparent,
n => PoolType::Shielded(int_to_shieldedprotocol(n)),
}
}
pub fn timestamped_test_log(text: &str) {
tracing::info!("{}: {}", crate::utils::now(), text);
}
pub async fn warm_orchard_proving_key() {
use orchard::circuit::OrchardCircuitVersion;
use zcash_primitives::transaction::builder;
tokio::task::spawn_blocking(|| {
builder::cached_orchard_proving_key(OrchardCircuitVersion::PostNu6_3);
})
.await
.expect("proving-key warmup task must not panic");
}
const PHASE_SECONDS_PRECISION: usize = 3;
pub async fn timed<T>(label: &str, fut: impl std::future::Future<Output = T>) -> T {
let start = std::time::Instant::now();
timestamped_test_log(format!("[phase] {label} started.").as_str());
let output = fut.await;
timestamped_test_log(
format!(
"[phase] {label} finished in {:.precision$}s.",
start.elapsed().as_secs_f64(),
precision = PHASE_SECONDS_PRECISION
)
.as_str(),
);
output
}
#[allow(unused_macros)]
macro_rules! build_method {
($name:ident, $localtype:ty) => {
#[doc = "Set the $name field of the builder."]
pub fn $name(&mut self, $name: $localtype) -> &mut Self {
self.$name = Some($name);
self
}
};
}
#[allow(unused_macros)]
macro_rules! build_method_push {
($name:ident, $localtype:ty) => {
#[doc = "Push a $ty to the builder."]
pub fn $name(&mut self, $name: $localtype) -> &mut Self {
self.$name.push($name);
self
}
};
}
#[allow(unused_macros)]
macro_rules! build_push_list {
($name:ident, $builder:ident, $struct:ident) => {
for i in &$builder.$name {
$struct.$name.push(i.build());
}
};
}
#[allow(unused_imports)]
pub(crate) use build_method;
#[allow(unused_imports)]
pub(crate) use build_method_push;
#[allow(unused_imports)]
pub(crate) use build_push_list;
pub fn interpret_taddr_as_tex_addr(
taddr_bytes: [u8; 20],
p: &impl zcash_protocol::consensus::Parameters,
) -> String {
bech32::encode::<bech32::Bech32m>(
bech32::Hrp::parse_unchecked(p.network_type().hrp_tex_address()),
&taddr_bytes,
)
.unwrap()
}
pub fn encoded_sapling_address_from_ua(
consensus_parameters: &impl consensus::Parameters,
encoded_unified_address: &str,
) -> String {
let zcash_keys::address::Address::Unified(unified_address) =
decode_address(consensus_parameters, encoded_unified_address).unwrap()
else {
panic!("not unified address")
};
unified_address
.sapling()
.expect("no sapling receiver")
.encode(consensus_parameters)
}
pub fn encoded_orchard_only_from_ua(
consensus_parameters: &impl consensus::Parameters,
encoded_unified_address: &str,
) -> String {
let zcash_keys::address::Address::Unified(unified_address) =
decode_address(consensus_parameters, encoded_unified_address).unwrap()
else {
panic!("not unified address")
};
UnifiedAddress::from_receivers(
Some(
unified_address
.orchard()
.copied()
.expect("no orchard receiver"),
),
None,
None,
None,
None,
)
.unwrap()
.encode(consensus_parameters)
}