diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index 5bd9615366..3f4778f4cd 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -21,6 +21,7 @@ and this library adheres to Rust's notion of - `zcash_client_backend::fees::ChangeValue::orchard` - `zcash_client_backend::wallet`: - `Note::Orchard` +- `zcash_client_backend::PoolType::{TRANSPARENT, SAPLING, ORCHARD}` constants ### Changed - `zcash_client_backend::data_api`: @@ -43,6 +44,10 @@ and this library adheres to Rust's notion of CHANGELOG for details. - `zcash_client_backend::proto::proposal::Proposal::{from_standard_proposal, try_into_standard_proposal` each no longer require a `consensus::Parameters` argument. +- `zcash_client_backend::wallet::Recipient` variants have changed. Instead of + wrapping protocol-address types, the `Recipient` type now wraps a + `zcash_address::ZcashAddress`. This simplifies the process of tracking the + original address to which value was sent. ## [0.11.0-pre-release] Unreleased diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index b00b055893..356ecd08a7 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -671,11 +671,13 @@ where let addr_network = params.address_network().ok_or(ConversionError::User( "Unable to determine network for address.", ))?; - let recipient_address = payment - .recipient_address + + let recipient_zaddr = payment.recipient_address.clone(); + + match recipient_zaddr .clone() - .convert_if_network::
(addr_network)?; - match recipient_address { + .convert_if_network::
(addr_network)? + { Address::Unified(ua) => { let memo = payment .memo @@ -690,10 +692,7 @@ where memo.clone(), )?; sapling_output_meta.push(( - Recipient::Unified( - ua.clone(), - PoolType::Shielded(ShieldedProtocol::Sapling), - ), + Recipient::External(recipient_zaddr, PoolType::SAPLING), payment.amount, Some(memo), )); @@ -715,7 +714,11 @@ where .as_ref() .map_or_else(MemoBytes::empty, |m| m.clone()); builder.add_sapling_output(external_ovk, addr, payment.amount, memo.clone())?; - sapling_output_meta.push((Recipient::Sapling(addr), payment.amount, Some(memo))); + sapling_output_meta.push(( + Recipient::External(recipient_zaddr, PoolType::SAPLING), + payment.amount, + Some(memo), + )); } Address::Transparent(to) => { if payment.memo.is_some() { @@ -723,7 +726,11 @@ where } else { builder.add_transparent_output(&to, payment.amount)?; } - transparent_output_meta.push((to, payment.amount)); + transparent_output_meta.push(( + Recipient::External(recipient_zaddr, PoolType::TRANSPARENT), + to, + payment.amount, + )); } } } @@ -741,10 +748,7 @@ where memo.clone(), )?; sapling_output_meta.push(( - Recipient::InternalAccount( - account, - PoolType::Shielded(ShieldedProtocol::Sapling), - ), + Recipient::Internal(account, PoolType::SAPLING), change_value.value(), Some(memo), )) @@ -752,9 +756,7 @@ where #[cfg(zcash_unstable = "orchard")] ShieldedProtocol::Orchard => { #[cfg(not(feature = "orchard"))] - return Err(Error::UnsupportedPoolType(PoolType::Shielded( - ShieldedProtocol::Orchard, - ))); + return Err(Error::UnsupportedPoolType(PoolType::ORCHARD)); #[cfg(feature = "orchard")] unimplemented!("FIXME: implement Orchard change output creation.") @@ -776,10 +778,7 @@ where .output_index(i) .expect("An output should exist in the transaction for each Sapling payment."); - let received_as = if let Recipient::InternalAccount( - account, - PoolType::Shielded(ShieldedProtocol::Sapling), - ) = recipient + let received_as = if let Recipient::Internal(account, PoolType::SAPLING) = recipient { build_result .transaction() @@ -799,28 +798,27 @@ where SentTransactionOutput::from_parts(output_index, recipient, value, memo, received_as) }); - let transparent_outputs = transparent_output_meta.into_iter().map(|(addr, value)| { - let script = addr.script(); - let output_index = build_result - .transaction() - .transparent_bundle() - .and_then(|b| { - b.vout - .iter() - .enumerate() - .find(|(_, tx_out)| tx_out.script_pubkey == script) - }) - .map(|(index, _)| index) - .expect("An output should exist in the transaction for each transparent payment."); - - SentTransactionOutput::from_parts( - output_index, - Recipient::Transparent(addr), - value, - None, - None, - ) - }); + let transparent_outputs = + transparent_output_meta + .into_iter() + .map(|(recipient, addr, value)| { + let script = addr.script(); + let output_index = build_result + .transaction() + .transparent_bundle() + .and_then(|b| { + b.vout + .iter() + .enumerate() + .find(|(_, tx_out)| tx_out.script_pubkey == script) + }) + .map(|(index, _)| index) + .expect( + "An output should exist in the transaction for each transparent payment.", + ); + + SentTransactionOutput::from_parts(output_index, recipient, value, None, None) + }); wallet_db .store_sent_tx(&SentTransaction { diff --git a/zcash_client_backend/src/lib.rs b/zcash_client_backend/src/lib.rs index 10fe709017..aa61d742b4 100644 --- a/zcash_client_backend/src/lib.rs +++ b/zcash_client_backend/src/lib.rs @@ -108,6 +108,13 @@ pub enum PoolType { Shielded(ShieldedProtocol), } +impl PoolType { + pub const TRANSPARENT: PoolType = PoolType::Transparent; + pub const SAPLING: PoolType = PoolType::Shielded(ShieldedProtocol::Sapling); + #[cfg(zcash_unstable = "orchard")] + pub const ORCHARD: PoolType = PoolType::Shielded(ShieldedProtocol::Orchard); +} + impl fmt::Display for PoolType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { diff --git a/zcash_client_backend/src/wallet.rs b/zcash_client_backend/src/wallet.rs index aaaee8ffb4..2412e5646a 100644 --- a/zcash_client_backend/src/wallet.rs +++ b/zcash_client_backend/src/wallet.rs @@ -2,6 +2,7 @@ //! light client. use incrementalmerkletree::Position; +use zcash_address::ZcashAddress; use zcash_note_encryption::EphemeralKeyBytes; use zcash_primitives::{ consensus::BlockHeight, @@ -17,7 +18,7 @@ use zcash_primitives::{ zip32::{AccountId, Scope}, }; -use crate::{address::UnifiedAddress, fees::sapling as sapling_fees, PoolType, ShieldedProtocol}; +use crate::{fees::sapling as sapling_fees, PoolType, ShieldedProtocol}; #[cfg(feature = "orchard")] use crate::fees::orchard as orchard_fees; @@ -63,10 +64,8 @@ impl NoteId { /// output. #[derive(Debug, Clone)] pub enum Recipient { - Transparent(TransparentAddress), - Sapling(sapling::PaymentAddress), - Unified(UnifiedAddress, PoolType), - InternalAccount(AccountId, PoolType), + External(ZcashAddress, PoolType), + Internal(AccountId, PoolType), } /// A subset of a [`Transaction`] relevant to wallets and light clients. diff --git a/zcash_client_sqlite/CHANGELOG.md b/zcash_client_sqlite/CHANGELOG.md index 88f8cb82f5..f1224a255d 100644 --- a/zcash_client_sqlite/CHANGELOG.md +++ b/zcash_client_sqlite/CHANGELOG.md @@ -15,6 +15,8 @@ and this library adheres to Rust's notion of - `zcash_client_sqlite::error::SqliteClientError` has new error variants: - `SqliteClientError::UnsupportedPoolType` - `SqliteClientError::BalanceError` + - The `Bech32DecodeError` variant has been replaced with a more general + `DecodingError` type. ## [0.8.1] - 2023-10-18 diff --git a/zcash_client_sqlite/src/error.rs b/zcash_client_sqlite/src/error.rs index c1993c39b8..cbeb384765 100644 --- a/zcash_client_sqlite/src/error.rs +++ b/zcash_client_sqlite/src/error.rs @@ -4,10 +4,8 @@ use std::error; use std::fmt; use shardtree::error::ShardTreeError; -use zcash_client_backend::{ - encoding::{Bech32DecodeError, TransparentCodecError}, - PoolType, -}; +use zcash_address::ParseError; +use zcash_client_backend::PoolType; use zcash_primitives::{ consensus::BlockHeight, transaction::components::amount::BalanceError, zip32::AccountId, }; @@ -16,7 +14,10 @@ use crate::wallet::commitment_tree; use crate::PRUNING_DEPTH; #[cfg(feature = "transparent-inputs")] -use zcash_primitives::legacy::TransparentAddress; +use { + zcash_client_backend::encoding::TransparentCodecError, + zcash_primitives::legacy::TransparentAddress, +}; /// The primary error type for the SQLite wallet backend. #[derive(Debug)] @@ -37,8 +38,8 @@ pub enum SqliteClientError { /// Illegal attempt to reinitialize an already-initialized wallet database. TableNotEmpty, - /// A Bech32-encoded key or address decoding error - Bech32DecodeError(Bech32DecodeError), + /// A Zcash key or address decoding error + DecodingError(ParseError), /// An error produced in legacy transparent address derivation #[cfg(feature = "transparent-inputs")] @@ -46,6 +47,7 @@ pub enum SqliteClientError { /// An error encountered in decoding a transparent address from its /// serialized form. + #[cfg(feature = "transparent-inputs")] TransparentAddress(TransparentCodecError), /// Wrapper for rusqlite errors. @@ -115,7 +117,6 @@ impl error::Error for SqliteClientError { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match &self { SqliteClientError::InvalidMemo(e) => Some(e), - SqliteClientError::Bech32DecodeError(Bech32DecodeError::Bech32Error(e)) => Some(e), SqliteClientError::DbError(e) => Some(e), SqliteClientError::Io(e) => Some(e), SqliteClientError::BalanceError(e) => Some(e), @@ -136,9 +137,10 @@ impl fmt::Display for SqliteClientError { write!(f, "The note ID associated with an inserted witness must correspond to a received note."), SqliteClientError::RequestedRewindInvalid(h, r) => write!(f, "A rewind must be either of less than {} blocks, or at least back to block {} for your wallet; the requested height was {}.", PRUNING_DEPTH, h, r), - SqliteClientError::Bech32DecodeError(e) => write!(f, "{}", e), + SqliteClientError::DecodingError(e) => write!(f, "{}", e), #[cfg(feature = "transparent-inputs")] SqliteClientError::HdwalletError(e) => write!(f, "{:?}", e), + #[cfg(feature = "transparent-inputs")] SqliteClientError::TransparentAddress(e) => write!(f, "{}", e), SqliteClientError::TableNotEmpty => write!(f, "Table is not empty"), SqliteClientError::DbError(e) => write!(f, "{}", e), @@ -174,10 +176,9 @@ impl From for SqliteClientError { SqliteClientError::Io(e) } } - -impl From for SqliteClientError { - fn from(e: Bech32DecodeError) -> Self { - SqliteClientError::Bech32DecodeError(e) +impl From for SqliteClientError { + fn from(e: ParseError) -> Self { + SqliteClientError::DecodingError(e) } } @@ -194,6 +195,7 @@ impl From for SqliteClientError { } } +#[cfg(feature = "transparent-inputs")] impl From for SqliteClientError { fn from(e: TransparentCodecError) -> Self { SqliteClientError::TransparentAddress(e) diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index ea488cf291..620722cd4e 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -45,6 +45,7 @@ use std::{ use incrementalmerkletree::Position; use shardtree::{error::ShardTreeError, ShardTree}; +use zcash_keys::address::Address; use zcash_primitives::{ block::BlockHash, consensus::{self, BlockHeight}, @@ -91,7 +92,7 @@ pub mod error; pub mod wallet; use wallet::{ commitment_tree::{self, put_shard_roots}, - SubtreeScanProgress, + Receiver, SubtreeScanProgress, }; #[cfg(test)] @@ -597,9 +598,13 @@ impl WalletWrite for WalletDb match output.transfer_type { TransferType::Outgoing | TransferType::WalletInternal => { let recipient = if output.transfer_type == TransferType::Outgoing { - Recipient::Sapling(output.note.recipient()) + let receiver = Receiver::Sapling(output.note.recipient()); + let wallet_address = wallet::select_receiving_address(&wdb.params, wdb.conn.0,output.account, &receiver)?.unwrap_or_else(|| + Address::Sapling(output.note.recipient()).to_zcash_address(&wdb.params) + ); + Recipient::External(wallet_address, PoolType::SAPLING) } else { - Recipient::InternalAccount( + Recipient::Internal( output.account, PoolType::Shielded(ShieldedProtocol::Sapling) ) @@ -607,7 +612,6 @@ impl WalletWrite for WalletDb wallet::put_sent_output( wdb.conn.0, - &wdb.params, output.account, tx_ref, output.index, @@ -620,7 +624,7 @@ impl WalletWrite for WalletDb Some(&output.memo), )?; - if matches!(recipient, Recipient::InternalAccount(_, _)) { + if matches!(recipient, Recipient::Internal(_, _)) { wallet::sapling::put_received_note(wdb.conn.0, output, tx_ref, None)?; } } @@ -660,13 +664,13 @@ impl WalletWrite for WalletDb ) { for (output_index, txout) in d_tx.tx.transparent_bundle().iter().flat_map(|b| b.vout.iter()).enumerate() { if let Some(address) = txout.recipient_address() { + let recipient_zaddr = Address::Transparent(address).to_zcash_address(&wdb.params); wallet::put_sent_output( wdb.conn.0, - &wdb.params, *account_id, tx_ref, output_index, - &Recipient::Transparent(address), + &Recipient::External(recipient_zaddr, PoolType::TRANSPARENT), txout.value, None )?; @@ -712,13 +716,7 @@ impl WalletWrite for WalletDb } for output in &sent_tx.outputs { - wallet::insert_sent_output( - wdb.conn.0, - &wdb.params, - tx_ref, - sent_tx.account, - output, - )?; + wallet::insert_sent_output(wdb.conn.0, tx_ref, sent_tx.account, output)?; if let Some((account, note)) = output.sapling_change_to() { wallet::sapling::put_received_note( diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index 86fdb51d9d..652dfc52e6 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -73,30 +73,30 @@ use std::io::{self, Cursor}; use std::num::NonZeroU32; use std::ops::RangeInclusive; use tracing::debug; -use zcash_client_backend::data_api::{AccountBalance, Ratio, WalletSummary}; -use zcash_primitives::transaction::components::amount::NonNegativeAmount; -use zcash_primitives::zip32::Scope; - -use zcash_primitives::{ - block::BlockHash, - consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters}, - memo::{Memo, MemoBytes}, - merkle_tree::read_commitment_tree, - transaction::{components::Amount, Transaction, TransactionData, TxId}, - zip32::{AccountId, DiversifierIndex}, -}; - +use zcash_address::ZcashAddress; use zcash_client_backend::{ - address::{Address, UnifiedAddress}, data_api::{ scanning::{ScanPriority, ScanRange}, - AccountBirthday, BlockMetadata, SentTransactionOutput, SAPLING_SHARD_HEIGHT, + AccountBalance, AccountBirthday, BlockMetadata, Ratio, SentTransactionOutput, + WalletSummary, SAPLING_SHARD_HEIGHT, }, encoding::AddressCodec, keys::UnifiedFullViewingKey, wallet::{NoteId, Recipient, WalletTx}, PoolType, ShieldedProtocol, }; +use zcash_keys::address::{Address, UnifiedAddress}; +use zcash_primitives::{ + block::BlockHash, + consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters}, + memo::{Memo, MemoBytes}, + merkle_tree::read_commitment_tree, + transaction::{ + components::{amount::NonNegativeAmount, Amount}, + Transaction, TransactionData, TxId, + }, + zip32::{AccountId, DiversifierIndex, Scope}, +}; use crate::wallet::commitment_tree::{get_max_checkpointed_height, SqliteShardStore}; use crate::DEFAULT_UA_REQUEST; @@ -1612,6 +1612,67 @@ pub(crate) fn put_tx_meta( .map_err(SqliteClientError::from) } +/// A protocol-level receiver of Zcash value. +/// +/// This type represents a reciever of value in a decrypted transaction. +#[allow(dead_code)] +pub(crate) enum Receiver { + #[cfg(feature = "orchard")] + Orchard(orchard::Address), + Sapling(::sapling::PaymentAddress), + #[cfg(feature = "transparent-inputs")] + Transparent(TransparentAddress), +} + +/// Returns the most likely wallet address that corresponds to the protocol-level receiver of a +/// note or UTXO. +pub(crate) fn select_receiving_address( + params: &P, + conn: &rusqlite::Connection, + account: AccountId, + receiver: &Receiver, +) -> Result, SqliteClientError> { + match receiver { + #[cfg(feature = "orchard")] + Receiver::Orchard(_) => unimplemented!(), + Receiver::Sapling(bare_address) => { + // at present, + let mut stmt = + conn.prepare_cached("SELECT address FROM addresses WHERE account = :account")?; + + let mut result = stmt.query(named_params! { ":account": u32::from(account) })?; + while let Some(row) = result.next()? { + let addr_str = &row.get::<_, String>(0)?; + let decoded = addr_str.parse::()?; + if Address::try_from_zcash_address(params, decoded.clone()) + .ok() + .iter() + .any(|addr| matches!(addr, Address::Sapling(r) if r == bare_address)) + { + return Ok(Some(decoded)); + } + } + + Ok(None) + } + #[cfg(feature = "transparent-inputs")] + Receiver::Transparent(taddr) => conn + .query_row( + "SELECT address + FROM addresses + WHERE cached_transparent_receiver_address = :taddr", + named_params! { + ":taddr": taddr.encode(params) + }, + |row| row.get::<_, String>(0), + ) + .optional()? + .map(|addr_str| addr_str.parse::()) + .transpose() + .map_err(SqliteClientError::from), + } +} + /// Inserts full transaction data into the database. pub(crate) fn put_tx_data( conn: &rusqlite::Connection, @@ -1773,26 +1834,16 @@ pub(crate) fn update_expired_notes( // A utility function for creation of parameters for use in `insert_sent_output` // and `put_sent_output` -fn recipient_params( - params: &P, - to: &Recipient, -) -> (Option, Option, PoolType) { +fn recipient_params(to: &Recipient) -> (Option, Option, PoolType) { match to { - Recipient::Transparent(addr) => (Some(addr.encode(params)), None, PoolType::Transparent), - Recipient::Sapling(addr) => ( - Some(addr.encode(params)), - None, - PoolType::Shielded(ShieldedProtocol::Sapling), - ), - Recipient::Unified(addr, pool) => (Some(addr.encode(params)), None, *pool), - Recipient::InternalAccount(id, pool) => (None, Some(u32::from(*id)), *pool), + Recipient::External(addr, pool) => (Some(addr.encode()), None, *pool), + Recipient::Internal(id, pool) => (None, Some(u32::from(*id)), *pool), } } /// Records information about a transaction output that your wallet created. -pub(crate) fn insert_sent_output( +pub(crate) fn insert_sent_output( conn: &rusqlite::Connection, - params: &P, tx_ref: i64, from_account: AccountId, output: &SentTransactionOutput, @@ -1806,7 +1857,7 @@ pub(crate) fn insert_sent_output( :to_address, :to_account, :value, :memo)", )?; - let (to_address, to_account, pool_type) = recipient_params(params, output.recipient()); + let (to_address, to_account, pool_type) = recipient_params(output.recipient()); let sql_args = named_params![ ":tx": &tx_ref, ":output_pool": &pool_code(pool_type), @@ -1835,9 +1886,8 @@ pub(crate) fn insert_sent_output( /// - If `recipient` is an internal account, `output_index` is an index into the Sapling outputs of /// the transaction. #[allow(clippy::too_many_arguments)] -pub(crate) fn put_sent_output( +pub(crate) fn put_sent_output( conn: &rusqlite::Connection, - params: &P, from_account: AccountId, tx_ref: i64, output_index: usize, @@ -1860,7 +1910,7 @@ pub(crate) fn put_sent_output( memo = IFNULL(:memo, memo)", )?; - let (to_address, to_account, pool_type) = recipient_params(params, recipient); + let (to_address, to_account, pool_type) = recipient_params(recipient); let sql_args = named_params![ ":tx": &tx_ref, ":output_pool": &pool_code(pool_type), diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index 14bf46d30f..d576c4ded6 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -2,21 +2,18 @@ use std::fmt; -use rusqlite::{self}; +use rusqlite; use schemer::{Migrator, MigratorError}; use schemer_rusqlite::RusqliteAdapter; use secrecy::SecretVec; use shardtree::error::ShardTreeError; use uuid::Uuid; -use zcash_primitives::{ - consensus::{self}, - transaction::components::amount::BalanceError, -}; +use zcash_primitives::{consensus, transaction::components::amount::BalanceError}; use crate::WalletDb; -use super::commitment_tree::{self}; +use super::commitment_tree; mod migrations; diff --git a/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs b/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs index cbf55606e9..49db05a21e 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs @@ -291,6 +291,7 @@ mod tests { wallet::Recipient, PoolType, ShieldedProtocol, TransferType, }; + use zcash_keys::address::Address; use zcash_keys::keys::{UnifiedFullViewingKey, UnifiedSpendingKey}; use zcash_primitives::{ block::BlockHash, @@ -501,16 +502,18 @@ mod tests { match output.transfer_type { TransferType::Outgoing | TransferType::WalletInternal => { let recipient = if output.transfer_type == TransferType::Outgoing { - Recipient::Sapling(output.note.recipient()) + let address = Address::Sapling(output.note.recipient()) + .to_zcash_address(¶ms); + Recipient::External(address, PoolType::SAPLING) } else { - Recipient::InternalAccount( + Recipient::Internal( output.account, PoolType::Shielded(ShieldedProtocol::Sapling), ) }; // Don't need to bother with sent outputs for this test. - if matches!(recipient, Recipient::InternalAccount(_, _)) { + if matches!(recipient, Recipient::Internal(_, _)) { put_received_note_before_migration( wdb.conn.0, output, tx_ref, None, ) diff --git a/zcash_keys/CHANGELOG.md b/zcash_keys/CHANGELOG.md index 13104a65b0..9e6d9d473d 100644 --- a/zcash_keys/CHANGELOG.md +++ b/zcash_keys/CHANGELOG.md @@ -21,6 +21,7 @@ The entries below are relative to the `zcash_client_backend` crate as of - A new `orchard` feature flag has been added to make it possible to build client code without `orchard` dependendencies. - `zcash_keys::address::Address::to_zcash_address`: +- `zcash_keys::address::Address::try_from_zcash_address`: ### Changed - `zcash_keys::address`: diff --git a/zcash_keys/src/address.rs b/zcash_keys/src/address.rs index 6517c6d026..f07684f2c4 100644 --- a/zcash_keys/src/address.rs +++ b/zcash_keys/src/address.rs @@ -251,9 +251,18 @@ impl TryFromRawAddress for Address { impl Address { pub fn decode(params: &P, s: &str) -> Option { - let addr = ZcashAddress::try_from_encoded(s).ok()?; - addr.convert_if_network(params.address_network().expect("Unrecognized network")) - .ok() + Self::try_from_zcash_address(params, s.parse::().ok()?).ok() + } + + pub fn try_from_zcash_address( + params: &P, + zaddr: ZcashAddress, + ) -> Result> { + zaddr.convert_if_network( + params + .address_network() + .expect("The network should have been recognized."), + ) } pub fn to_zcash_address(&self, params: &P) -> ZcashAddress {