From b60600a4c343ae3caa7653d009a08f5b0485df3b Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Sat, 27 Jan 2024 21:13:32 -0700 Subject: [PATCH] zcash_client_sqlite: Use `ZcashAddress` for persistence of sent note addresses Prior to this change, the recipient of a sent transaction would always be shown as the protocol-level address, instead of any unified address intended as the recipient. Now, instead of reencoding the recipient address, we use the original `ZcashAddress` value from the payment request. --- components/zcash_address/CHANGELOG.md | 4 +- .../zcash_address/src/kind/unified/address.rs | 7 +- components/zcash_address/src/lib.rs | 16 ++- components/zcash_protocol/CHANGELOG.md | 2 + components/zcash_protocol/src/lib.rs | 6 + zcash_client_backend/CHANGELOG.md | 4 + zcash_client_backend/src/data_api/wallet.rs | 55 +++++---- zcash_client_backend/src/wallet.rs | 18 +-- zcash_client_sqlite/CHANGELOG.md | 2 + zcash_client_sqlite/src/error.rs | 28 ++--- zcash_client_sqlite/src/lib.rs | 107 +++++++++++------- zcash_client_sqlite/src/wallet.rs | 80 +++++++++---- zcash_client_sqlite/src/wallet/init.rs | 5 +- zcash_keys/CHANGELOG.md | 3 + zcash_keys/src/address.rs | 58 +++++++++- 15 files changed, 281 insertions(+), 114 deletions(-) diff --git a/components/zcash_address/CHANGELOG.md b/components/zcash_address/CHANGELOG.md index 565e9b5d2..daa450611 100644 --- a/components/zcash_address/CHANGELOG.md +++ b/components/zcash_address/CHANGELOG.md @@ -8,8 +8,8 @@ and this library adheres to Rust's notion of ## [Unreleased] ### Added -- `zcash_address::ZcashAddress::{can_receive_memo, can_receive_as}` -- `zcash_address::unified::Address::{can_receive_memo, has_receiver}` +- `zcash_address::ZcashAddress::{can_receive_memo, can_receive_as, matches_receiver}` +- `zcash_address::unified::Address::{can_receive_memo, has_receiver_of_type, contains_receiver}` - Module `zcash_address::testing` under the `test-dependencies` feature. - Module `zcash_address::unified::address::testing` under the `test-dependencies` feature. diff --git a/components/zcash_address/src/kind/unified/address.rs b/components/zcash_address/src/kind/unified/address.rs index 736e81479..4afa9b8bb 100644 --- a/components/zcash_address/src/kind/unified/address.rs +++ b/components/zcash_address/src/kind/unified/address.rs @@ -105,7 +105,7 @@ pub struct Address(pub(crate) Vec); impl Address { /// Returns whether this address has the ability to receive transfers of the given pool type. - pub fn has_receiver(&self, pool_type: PoolType) -> bool { + pub fn has_receiver_of_type(&self, pool_type: PoolType) -> bool { self.0.iter().any(|r| match r { Receiver::Orchard(_) => pool_type == PoolType::Shielded(ShieldedProtocol::Orchard), Receiver::Sapling(_) => pool_type == PoolType::Shielded(ShieldedProtocol::Sapling), @@ -115,6 +115,11 @@ impl Address { }) } + /// Returns whether this address contains the given receiver. + pub fn contains_receiver(&self, receiver: &Receiver) -> bool { + self.0.contains(receiver) + } + /// Returns whether this address can receive a memo. pub fn can_receive_memo(&self) -> bool { self.0 diff --git a/components/zcash_address/src/lib.rs b/components/zcash_address/src/lib.rs index 900d4c68d..24445afb6 100644 --- a/components/zcash_address/src/lib.rs +++ b/components/zcash_address/src/lib.rs @@ -141,6 +141,7 @@ pub use convert::{ }; pub use encoding::ParseError; pub use kind::unified; +use kind::unified::Receiver; pub use zcash_protocol::consensus::NetworkType as Network; use zcash_protocol::{PoolType, ShieldedProtocol}; @@ -273,7 +274,7 @@ impl ZcashAddress { match &self.kind { AddressKind::Sprout(_) => false, AddressKind::Sapling(_) => pool_type == PoolType::Shielded(ShieldedProtocol::Sapling), - AddressKind::Unified(addr) => addr.has_receiver(pool_type), + AddressKind::Unified(addr) => addr.has_receiver_of_type(pool_type), AddressKind::P2pkh(_) => pool_type == PoolType::Transparent, AddressKind::P2sh(_) => pool_type == PoolType::Transparent, AddressKind::Tex(_) => pool_type == PoolType::Transparent, @@ -291,6 +292,19 @@ impl ZcashAddress { AddressKind::Tex(_) => false, } } + + /// Returns whether or not this address contains or corresponds to the given unified address + /// receiver. + pub fn matches_receiver(&self, receiver: &Receiver) -> bool { + match (receiver, &self.kind) { + (r, AddressKind::Unified(ua)) => ua.contains_receiver(r), + (Receiver::Sapling(r), AddressKind::Sapling(d)) => r == d, + (Receiver::P2pkh(r), AddressKind::P2pkh(d)) => r == d, + (Receiver::P2pkh(r), AddressKind::Tex(d)) => r == d, + (Receiver::P2sh(r), AddressKind::P2sh(d)) => r == d, + _ => false, + } + } } #[cfg(feature = "test-dependencies")] diff --git a/components/zcash_protocol/CHANGELOG.md b/components/zcash_protocol/CHANGELOG.md index 10c319205..930c92f52 100644 --- a/components/zcash_protocol/CHANGELOG.md +++ b/components/zcash_protocol/CHANGELOG.md @@ -6,6 +6,8 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- `zcash_protocol::PoolType::{TRANSPARENT, SAPLING, ORCHARD}` ## [0.1.1] - 2024-03-25 ### Added diff --git a/components/zcash_protocol/src/lib.rs b/components/zcash_protocol/src/lib.rs index 2976a0264..f73564751 100644 --- a/components/zcash_protocol/src/lib.rs +++ b/components/zcash_protocol/src/lib.rs @@ -42,6 +42,12 @@ pub enum PoolType { Shielded(ShieldedProtocol), } +impl PoolType { + pub const TRANSPARENT: PoolType = PoolType::Transparent; + pub const SAPLING: PoolType = PoolType::Shielded(ShieldedProtocol::Sapling); + 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/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index 72d71e193..ec95efb2d 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -25,6 +25,10 @@ and this library adheres to Rust's notion of - `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.12.1] - 2024-03-27 diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index fa097d2d2..69cc88f93 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -980,8 +980,8 @@ where memo.clone(), )?; orchard_output_meta.push(( - Recipient::Unified( - ua.clone(), + Recipient::External( + payment.recipient_address().clone(), PoolType::Shielded(ShieldedProtocol::Orchard), ), payment.amount(), @@ -997,8 +997,8 @@ where memo.clone(), )?; sapling_output_meta.push(( - Recipient::Unified( - ua.clone(), + Recipient::External( + payment.recipient_address().clone(), PoolType::Shielded(ShieldedProtocol::Sapling), ), payment.amount(), @@ -1026,7 +1026,11 @@ where payment.amount(), memo.clone(), )?; - sapling_output_meta.push((Recipient::Sapling(addr), payment.amount(), Some(memo))); + sapling_output_meta.push(( + Recipient::External(payment.recipient_address().clone(), PoolType::SAPLING), + payment.amount(), + Some(memo), + )); } Address::Transparent(to) => { if payment.memo().is_some() { @@ -1034,7 +1038,11 @@ where } else { builder.add_transparent_output(&to, payment.amount())?; } - transparent_output_meta.push((to, payment.amount())); + transparent_output_meta.push(( + Recipient::External(payment.recipient_address().clone(), PoolType::TRANSPARENT), + to, + payment.amount(), + )); } } } @@ -1156,22 +1164,27 @@ where SentTransactionOutput::from_parts(output_index, recipient, value, memo) }); - 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."); + 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::Transparent(addr), value, None) - }); + SentTransactionOutput::from_parts(output_index, recipient, value, None) + }); let mut outputs = vec![]; #[cfg(feature = "orchard")] diff --git a/zcash_client_backend/src/wallet.rs b/zcash_client_backend/src/wallet.rs index 418bb3e3a..7d555b07f 100644 --- a/zcash_client_backend/src/wallet.rs +++ b/zcash_client_backend/src/wallet.rs @@ -2,7 +2,7 @@ //! light client. use incrementalmerkletree::Position; -use zcash_keys::address::Address; +use zcash_address::ZcashAddress; use zcash_note_encryption::EphemeralKeyBytes; use zcash_primitives::{ consensus::BlockHeight, @@ -19,7 +19,7 @@ use zcash_primitives::{ }; use zcash_protocol::value::BalanceError; -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; @@ -68,12 +68,10 @@ impl NoteId { /// output. #[derive(Debug, Clone)] pub enum Recipient { - Transparent(TransparentAddress), - Sapling(sapling::PaymentAddress), - Unified(UnifiedAddress, PoolType), + External(ZcashAddress, PoolType), InternalAccount { receiving_account: AccountId, - external_address: Option
, + external_address: Option, note: N, }, } @@ -81,9 +79,7 @@ pub enum Recipient { impl Recipient { pub fn map_internal_account_note B>(self, f: F) -> Recipient { match self { - Recipient::Transparent(t) => Recipient::Transparent(t), - Recipient::Sapling(s) => Recipient::Sapling(s), - Recipient::Unified(u, p) => Recipient::Unified(u, p), + Recipient::External(addr, pool) => Recipient::External(addr, pool), Recipient::InternalAccount { receiving_account, external_address, @@ -100,9 +96,7 @@ impl Recipient { impl Recipient> { pub fn internal_account_note_transpose_option(self) -> Option> { match self { - Recipient::Transparent(t) => Some(Recipient::Transparent(t)), - Recipient::Sapling(s) => Some(Recipient::Sapling(s)), - Recipient::Unified(u, p) => Some(Recipient::Unified(u, p)), + Recipient::External(addr, pool) => Some(Recipient::External(addr, pool)), Recipient::InternalAccount { receiving_account, external_address, diff --git a/zcash_client_sqlite/CHANGELOG.md b/zcash_client_sqlite/CHANGELOG.md index 00eb394ec..49bbb1b76 100644 --- a/zcash_client_sqlite/CHANGELOG.md +++ b/zcash_client_sqlite/CHANGELOG.md @@ -71,6 +71,8 @@ This version was yanked, use 0.10.1 instead. - `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 6796c4142..2f961853c 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_keys::keys::AddressGenerationError; use zcash_primitives::zip32; use zcash_primitives::{consensus::BlockHeight, transaction::components::amount::BalanceError}; @@ -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)] @@ -33,8 +34,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")] @@ -42,6 +43,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. @@ -116,7 +118,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 { SqliteClientError::InvalidNote => write!(f, "Invalid 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), @@ -175,10 +177,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) } } @@ -195,6 +196,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 ca4245330..733c35973 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -65,7 +65,7 @@ use zcash_client_backend::{ wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput}, DecryptedOutput, PoolType, ShieldedProtocol, TransferType, }; -use zcash_keys::address::Address; +use zcash_keys::address::Receiver; use zcash_primitives::{ block::BlockHash, consensus::{self, BlockHeight}, @@ -1063,11 +1063,22 @@ impl WalletWrite for WalletDb for output in d_tx.sapling_outputs() { match output.transfer_type() { TransferType::Outgoing => { - //TODO: Recover the UA, if possible. - let recipient = Recipient::Sapling(output.note().recipient()); + let 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(|| + receiver.to_zcash_address(wdb.params.network_type()) + ); + + Recipient::External(wallet_address, PoolType::Shielded(ShieldedProtocol::Sapling)) + }; + wallet::put_sent_output( wdb.conn.0, - &wdb.params, *output.account(), tx_ref, output.index(), @@ -1087,7 +1098,6 @@ impl WalletWrite for WalletDb wallet::put_sent_output( wdb.conn.0, - &wdb.params, *output.account(), tx_ref, output.index(), @@ -1102,14 +1112,22 @@ impl WalletWrite for WalletDb if let Some(account_id) = funding_account { let recipient = Recipient::InternalAccount { receiving_account: *output.account(), - // TODO: recover the actual UA, if possible - external_address: Some(Address::Sapling(output.note().recipient())), + external_address: { + let receiver = Receiver::Sapling(output.note().recipient()); + Some(wallet::select_receiving_address( + &wdb.params, + wdb.conn.0, + *output.account(), + &receiver + )?.unwrap_or_else(|| + receiver.to_zcash_address(wdb.params.network_type()) + )) + }, note: Note::Sapling(output.note().clone()), }; wallet::put_sent_output( wdb.conn.0, - &wdb.params, account_id, tx_ref, output.index(), @@ -1126,20 +1144,22 @@ impl WalletWrite for WalletDb for output in d_tx.orchard_outputs() { match output.transfer_type() { TransferType::Outgoing => { - // TODO: Recover the actual UA, if possible. - let recipient = Recipient::Unified( - UnifiedAddress::from_receivers( - Some(output.note().recipient()), - None, - None, - ) - .expect("UA has an Orchard receiver by construction."), - PoolType::Shielded(ShieldedProtocol::Orchard), - ); + let recipient = { + let receiver = Receiver::Orchard(output.note().recipient()); + let wallet_address = wallet::select_receiving_address( + &wdb.params, + wdb.conn.0, + *output.account(), + &receiver + )?.unwrap_or_else(|| + receiver.to_zcash_address(wdb.params.network_type()) + ); + + Recipient::External(wallet_address, PoolType::Shielded(ShieldedProtocol::Orchard)) + }; wallet::put_sent_output( wdb.conn.0, - &wdb.params, *output.account(), tx_ref, output.index(), @@ -1159,7 +1179,6 @@ impl WalletWrite for WalletDb wallet::put_sent_output( wdb.conn.0, - &wdb.params, *output.account(), tx_ref, output.index(), @@ -1175,19 +1194,22 @@ impl WalletWrite for WalletDb // Even if the recipient address is external, record the send as internal. let recipient = Recipient::InternalAccount { receiving_account: *output.account(), - // TODO: recover the actual UA, if possible - external_address: Some(Address::Unified( - UnifiedAddress::from_receivers( - Some(output.note().recipient()), - None, - None, - ).expect("UA has an Orchard receiver by construction."))), + external_address: { + let receiver = Receiver::Orchard(output.note().recipient()); + Some(wallet::select_receiving_address( + &wdb.params, + wdb.conn.0, + *output.account(), + &receiver + )?.unwrap_or_else(|| + receiver.to_zcash_address(wdb.params.network_type()) + )) + }, note: Note::Orchard(*output.note()), }; wallet::put_sent_output( wdb.conn.0, - &wdb.params, account_id, tx_ref, output.index(), @@ -1240,13 +1262,29 @@ impl WalletWrite for WalletDb .enumerate() { if let Some(address) = txout.recipient_address() { + let receiver = Receiver::Transparent(address); + + #[cfg(feature = "transparent-inputs")] + let recipient_addr = wallet::select_receiving_address( + &wdb.params, + wdb.conn.0, + account_id, + &receiver + )?.unwrap_or_else(|| + receiver.to_zcash_address(wdb.params.network_type()) + ); + + #[cfg(not(feature = "transparent-inputs"))] + let recipient_addr = receiver.to_zcash_address(wdb.params.network_type()); + + let recipient = Recipient::External(recipient_addr, PoolType::Transparent); + wallet::put_sent_output( wdb.conn.0, - &wdb.params, account_id, tx_ref, output_index, - &Recipient::Transparent(address), + &recipient, txout.value, None, )?; @@ -1305,13 +1343,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_id(), - output, - )?; + wallet::insert_sent_output(wdb.conn.0, tx_ref, *sent_tx.account_id(), output)?; match output.recipient() { Recipient::InternalAccount { @@ -1880,7 +1912,6 @@ mod tests { .unwrap(); assert!(current_addr.is_some()); - // TODO: Add Orchard let addr2 = st .wallet_mut() .get_next_available_address(account.account_id(), DEFAULT_UA_REQUEST) diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index 1593bb98c..ef1f399e7 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -76,12 +76,9 @@ use std::io::{self, Cursor}; use std::num::NonZeroU32; use std::ops::RangeInclusive; use tracing::debug; -use zcash_keys::keys::{ - AddressGenerationError, UnifiedAddressRequest, UnifiedIncomingViewingKey, UnifiedSpendingKey, -}; +use zcash_address::ZcashAddress; use zcash_client_backend::{ - address::{Address, UnifiedAddress}, data_api::{ scanning::{ScanPriority, ScanRange}, AccountBalance, AccountBirthday, AccountSource, BlockMetadata, Ratio, @@ -92,6 +89,13 @@ use zcash_client_backend::{ wallet::{Note, NoteId, Recipient, WalletTx}, PoolType, ShieldedProtocol, }; +use zcash_keys::{ + address::{Address, Receiver, UnifiedAddress}, + keys::{ + AddressGenerationError, UnifiedAddressRequest, UnifiedIncomingViewingKey, + UnifiedSpendingKey, + }, +}; use zcash_primitives::{ block::BlockHash, consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters}, @@ -101,8 +105,8 @@ use zcash_primitives::{ components::{amount::NonNegativeAmount, Amount}, Transaction, TransactionData, TxId, }, - zip32::{self, DiversifierIndex, Scope}, }; +use zip32::{self, DiversifierIndex, Scope}; use crate::{ error::SqliteClientError, @@ -2366,6 +2370,49 @@ pub(crate) fn put_tx_meta( .map_err(SqliteClientError::from) } +/// 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 = "transparent-inputs")] + Receiver::Transparent(taddr) => conn + .query_row( + "SELECT address + FROM addresses + WHERE cached_transparent_receiver_address = :taddr", + named_params! { + ":taddr": Address::Transparent(*taddr).encode(params) + }, + |row| row.get::<_, String>(0), + ) + .optional()? + .map(|addr_str| addr_str.parse::()) + .transpose() + .map_err(SqliteClientError::from), + receiver => { + // at present, + let mut stmt = + conn.prepare_cached("SELECT address FROM addresses WHERE account_id = :account")?; + + let mut result = stmt.query(named_params! { ":account": account.0 })?; + while let Some(row) = result.next()? { + let addr_str = row.get::<_, String>(0)?; + let decoded = addr_str.parse::()?; + if receiver.corresponds(&decoded) { + return Ok(Some(decoded)); + } + } + + Ok(None) + } + } +} + /// Inserts full transaction data into the database. pub(crate) fn put_tx_data( conn: &rusqlite::Connection, @@ -2515,24 +2562,17 @@ pub(crate) fn put_legacy_transparent_utxo( // A utility function for creation of parameters for use in `insert_sent_output` // and `put_sent_output` -fn recipient_params( - params: &P, +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::External(addr, pool) => (Some(addr.encode()), None, *pool), Recipient::InternalAccount { receiving_account, external_address, note, } => ( - external_address.as_ref().map(|a| a.encode(params)), + external_address.as_ref().map(|a| a.encode()), Some(*receiving_account), PoolType::Shielded(note.protocol()), ), @@ -2540,9 +2580,8 @@ fn recipient_params( } /// 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, @@ -2556,7 +2595,7 @@ pub(crate) fn insert_sent_output( :to_address, :to_account_id, :value, :memo)", )?; - let (to_address, to_account_id, pool_type) = recipient_params(params, output.recipient()); + let (to_address, to_account_id, pool_type) = recipient_params(output.recipient()); let sql_args = named_params![ ":tx": &tx_ref, ":output_pool": &pool_code(pool_type), @@ -2585,9 +2624,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, @@ -2610,7 +2648,7 @@ pub(crate) fn put_sent_output( memo = IFNULL(:memo, memo)", )?; - let (to_address, to_account_id, pool_type) = recipient_params(params, recipient); + let (to_address, to_account_id, 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 622a6a0e0..9fff7e3a3 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -134,11 +134,10 @@ fn sqlite_client_error_to_wallet_migration_error(e: SqliteClientError) -> Wallet SqliteClientError::InvalidNote => { WalletMigrationError::CorruptedData("invalid note".into()) } - SqliteClientError::Bech32DecodeError(e) => { - WalletMigrationError::CorruptedData(e.to_string()) - } + SqliteClientError::DecodingError(e) => WalletMigrationError::CorruptedData(e.to_string()), #[cfg(feature = "transparent-inputs")] SqliteClientError::HdwalletError(e) => WalletMigrationError::CorruptedData(e.to_string()), + #[cfg(feature = "transparent-inputs")] SqliteClientError::TransparentAddress(e) => { WalletMigrationError::CorruptedData(e.to_string()) } diff --git a/zcash_keys/CHANGELOG.md b/zcash_keys/CHANGELOG.md index f700a2ed5..b53022695 100644 --- a/zcash_keys/CHANGELOG.md +++ b/zcash_keys/CHANGELOG.md @@ -5,6 +5,9 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- `zcash_keys::address::Address::try_from_zcash_address` +- `zcash_keys::address::Receiver` ## [0.2.0] - 2024-03-25 diff --git a/zcash_keys/src/address.rs b/zcash_keys/src/address.rs index 2e30aa861..09b63dc04 100644 --- a/zcash_keys/src/address.rs +++ b/zcash_keys/src/address.rs @@ -233,6 +233,54 @@ impl UnifiedAddress { } } +/// An enumeration of protocol-level receiver types. While these correspond to unified address +/// receiver +pub enum Receiver { + #[cfg(feature = "orchard")] + Orchard(orchard::Address), + #[cfg(feature = "sapling")] + Sapling(PaymentAddress), + Transparent(TransparentAddress), +} + +impl Receiver { + /// Converts this receiver to a [`ZcashAddress`] for the given network. + /// + /// This conversion function selects the least-capable address format possible; this means that + /// Orchard receivers will be rendered as Unified addresses, Sapling receivers will be rendered + /// as bare Sapling addresses, and Transparent receivers will be rendered as taddrs. + pub fn to_zcash_address(&self, net: NetworkType) -> ZcashAddress { + match self { + Receiver::Orchard(addr) => { + let receiver = unified::Receiver::Orchard(addr.to_raw_address_bytes()); + let ua = unified::Address::try_from_items(vec![receiver]) + .expect("A unified address may contain a single Orchard receiver."); + ZcashAddress::from_unified(net, ua) + } + Receiver::Sapling(addr) => ZcashAddress::from_sapling(net, addr.to_bytes()), + Receiver::Transparent(TransparentAddress::PublicKeyHash(data)) => { + ZcashAddress::from_transparent_p2pkh(net, *data) + } + Receiver::Transparent(TransparentAddress::ScriptHash(data)) => { + ZcashAddress::from_transparent_p2sh(net, *data) + } + } + } + + pub fn corresponds(&self, addr: &ZcashAddress) -> bool { + addr.matches_receiver(&match self { + Receiver::Orchard(addr) => unified::Receiver::Orchard(addr.to_raw_address_bytes()), + Receiver::Sapling(addr) => unified::Receiver::Sapling(addr.to_bytes()), + Receiver::Transparent(TransparentAddress::PublicKeyHash(data)) => { + unified::Receiver::P2pkh(*data) + } + Receiver::Transparent(TransparentAddress::ScriptHash(data)) => { + unified::Receiver::P2sh(*data) + } + }) + } +} + /// An address that funds can be sent to. #[derive(Debug, PartialEq, Eq, Clone)] pub enum Address { @@ -291,8 +339,14 @@ 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.network_type()).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.network_type()) } pub fn to_zcash_address(&self, params: &P) -> ZcashAddress {