Compare commits

...

20 Commits

Author SHA1 Message Date
Kris Nuttycombe 807487b71a
Merge 728e2f0cf3 into 5c6a6a4c86 2024-04-22 23:59:51 +00:00
Kris Nuttycombe 728e2f0cf3 Allow transparent-only unified addresses and viewing keys. 2024-04-22 17:55:03 -06:00
Kris Nuttycombe cab4d84464 Apply suggestions from code review
Co-authored-by: Daira-Emma Hopwood <daira@jacaranda.org>
2024-04-22 17:54:32 -06:00
Kris Nuttycombe aea04d1ad2 `zcash_address`: Add support for ZIP 316, Revision 1 2024-04-22 17:53:44 -06:00
Kris Nuttycombe c0542c9589 zcash_keys: Box `Address` elements to avoid large variations in enum variant sizes 2024-04-22 17:52:33 -06:00
Kris Nuttycombe 34ec1e5bdb zcash_keys: Update key and address types to include ZIP-316 metadata items. 2024-04-22 17:52:28 -06:00
Kris Nuttycombe 0341171c84 zcash_address: Add handling for Unified Metadata Items 2024-04-22 17:18:53 -06:00
Kris Nuttycombe c3e1750007 zcash_keys: Add missing entries to `zcash_keys-0.1.0` CHANGELOG 2024-04-22 16:27:08 -06:00
str4d 5c6a6a4c86
Merge pull request #1143 from nuttycom/crate_zip321
Extract `zip321` crate from `zcash_client_backend`
2024-04-22 22:46:09 +01:00
Kris Nuttycombe d2aa6cfc7f Apply suggestions from code review
Co-authored-by: str4d <thestr4d@gmail.com>
2024-04-22 14:41:50 -06:00
Kris Nuttycombe aeac544aed Address comments from code review. 2024-04-22 10:55:25 -06:00
Daira-Emma Hopwood ea82dbeb64
Apply straightforward suggestions from code review 2024-04-18 01:12:13 +01:00
Kris Nuttycombe f28aa6b304 `zcash_{keys, client_backend}`: Fix no-default-features build. 2024-04-11 18:00:59 -06:00
Kris Nuttycombe b60600a4c3 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.
2024-04-05 16:48:13 -06:00
Kris Nuttycombe 86e1181259 zip321: Make `Payment` fields private. 2024-04-05 16:30:31 -06:00
Kris Nuttycombe 3ea7d84183 zcash_client_backend: Update to use extracted `zip321` crate 2024-04-05 16:25:21 -06:00
Kris Nuttycombe d982d7826a zip321: Replace dependencies on `zcash_keys` types with `zcash_address` 2024-04-05 16:10:22 -06:00
Kris Nuttycombe fdf86ad740 Move `zcash_client_backend::zip321` to the `zip321` crate. 2024-04-03 12:14:20 -06:00
Kris Nuttycombe 07d5aa4a79 zip321: Remove stub lib.rs 2024-04-03 12:14:18 -06:00
Kris Nuttycombe bbb8d1090a Add placeholder for a zip321 crate. 2024-04-03 12:13:47 -06:00
44 changed files with 2471 additions and 1148 deletions

13
Cargo.lock generated
View File

@ -3068,6 +3068,7 @@ dependencies = [
"zcash_proofs",
"zcash_protocol",
"zip32",
"zip321",
]
[[package]]
@ -3327,3 +3328,15 @@ dependencies = [
"memuse",
"subtle",
]
[[package]]
name = "zip321"
version = "0.0.0"
dependencies = [
"base64",
"nom",
"percent-encoding",
"proptest",
"zcash_address",
"zcash_protocol",
]

View File

@ -5,6 +5,7 @@ members = [
"components/zcash_address",
"components/zcash_encoding",
"components/zcash_protocol",
"components/zip321",
"zcash_client_backend",
"zcash_client_sqlite",
"zcash_extensions",
@ -34,6 +35,7 @@ zcash_client_backend = { version = "0.12", path = "zcash_client_backend" }
zcash_encoding = { version = "0.2", path = "components/zcash_encoding" }
zcash_keys = { version = "0.2", path = "zcash_keys" }
zcash_protocol = { version = "0.1", path = "components/zcash_protocol" }
zip321 = { version = "0.0", path = "components/zip321" }
zcash_note_encryption = "0.4"
zcash_primitives = { version = "0.15", path = "zcash_primitives", default-features = false }

View File

@ -7,6 +7,31 @@ and this library adheres to Rust's notion of
## [Unreleased]
### Added
- `zcash_address::ZcashAddress::{can_receive_memo, can_receive_as, matches_receiver}`
- `zcash_address::unified`:
- `Address::{can_receive_memo, has_receiver_of_type, contains_receiver, receivers}`
- `Container::revision`
- `DataTypecode`
- `Item`
- `MetadataItem`
- `MetadataTypecode`
- `Revision`
- Module `zcash_address::testing` under the `test-dependencies` feature.
- Module `zcash_address::unified::address::testing` under the
`test-dependencies` feature.
### Changed
- `zcash_address::unified`:
- `Typecode` has changed. Instead of having a variant for each receiver type,
it now has two variants, `Typecode::Data` and `Typecode::Metadata`.
- `Encoding::try_from_items` now takes an additional `Revision` argument.
### Removed
- `zcash_address::unified::Container::items` Preference order is only
significant when considering unified address receivers; use
`Address::receivers` instead.
## [0.3.2] - 2024-03-06
### Added
- `zcash_address::convert`:

View File

@ -19,18 +19,19 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
bech32 = "0.9"
bs58 = { version = "0.5", features = ["check"] }
bech32.workspace = true
bs58.workspace = true
f4jumble = { version = "0.1", path = "../f4jumble" }
zcash_protocol.workspace = true
zcash_encoding.workspace = true
proptest = { workspace = true, optional = true }
[dev-dependencies]
assert_matches = "1.3.0"
proptest = "1"
assert_matches.workspace = true
proptest.workspace = true
[features]
test-dependencies = []
test-dependencies = ["dep:proptest"]
[lib]
bench = false

View File

@ -180,7 +180,11 @@ mod tests {
use assert_matches::assert_matches;
use super::*;
use crate::{kind::unified, Network};
use crate::{
kind::unified,
unified::{Item, Receiver, Revision},
Network,
};
fn encoding(encoded: &str, decoded: ZcashAddress) {
assert_eq!(decoded.to_string(), encoded);
@ -230,21 +234,30 @@ mod tests {
"u1qpatys4zruk99pg59gcscrt7y6akvl9vrhcfyhm9yxvxz7h87q6n8cgrzzpe9zru68uq39uhmlpp5uefxu0su5uqyqfe5zp3tycn0ecl",
ZcashAddress {
net: Network::Main,
kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])),
kind: AddressKind::Unified(unified::Address {
revision: Revision::R0,
receivers: vec![Item::Data(Receiver::Sapling([0; 43]))]
}),
},
);
encoding(
"utest10c5kutapazdnf8ztl3pu43nkfsjx89fy3uuff8tsmxm6s86j37pe7uz94z5jhkl49pqe8yz75rlsaygexk6jpaxwx0esjr8wm5ut7d5s",
ZcashAddress {
net: Network::Test,
kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])),
kind: AddressKind::Unified(unified::Address {
revision: Revision::R0,
receivers: vec![Item::Data(Receiver::Sapling([0; 43]))]
}),
},
);
encoding(
"uregtest15xk7vj4grjkay6mnfl93dhsflc2yeunhxwdh38rul0rq3dfhzzxgm5szjuvtqdha4t4p2q02ks0jgzrhjkrav70z9xlvq0plpcjkd5z3",
ZcashAddress {
net: Network::Regtest,
kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])),
kind: AddressKind::Unified(unified::Address {
revision: Revision::R0,
receivers: vec![Item::Data(Receiver::Sapling([0; 43]))]
}),
},
);

View File

@ -6,6 +6,7 @@ use std::convert::{TryFrom, TryInto};
use std::error::Error;
use std::fmt;
use std::num::TryFromIntError;
use zcash_encoding::MAX_COMPACT_SIZE;
use crate::Network;
@ -22,9 +23,9 @@ const PADDING_LEN: usize = 16;
/// The known Receiver and Viewing Key types.
///
/// The typecodes `0xFFFA..=0xFFFF` reserved for experiments are currently not
/// distinguished from unknown values, and will be parsed as [`Typecode::Unknown`].
/// distinguished from unknown values, and will be parsed as [`DataTypecode::Unknown`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Typecode {
pub enum DataTypecode {
/// A transparent P2PKH address, FVK, or IVK encoding as specified in [ZIP 316](https://zips.z.cash/zip-0316).
P2pkh,
/// A transparent P2SH address.
@ -39,7 +40,37 @@ pub enum Typecode {
Unknown(u32),
}
impl Typecode {
impl TryFrom<u32> for DataTypecode {
type Error = ();
fn try_from(typecode: u32) -> Result<Self, Self::Error> {
match typecode {
0x00 => Ok(DataTypecode::P2pkh),
0x01 => Ok(DataTypecode::P2sh),
0x02 => Ok(DataTypecode::Sapling),
0x03 => Ok(DataTypecode::Orchard),
0x04..=0xBF | 0xFD..=MAX_COMPACT_SIZE => Ok(DataTypecode::Unknown(typecode)),
_ => Err(()),
}
}
}
impl From<DataTypecode> for u32 {
fn from(t: DataTypecode) -> Self {
match t {
DataTypecode::P2pkh => 0x00,
DataTypecode::P2sh => 0x01,
DataTypecode::Sapling => 0x02,
DataTypecode::Orchard => 0x03,
DataTypecode::Unknown(typecode) => typecode,
}
}
}
impl DataTypecode {
/// A total ordering over the data typecodes that can be used to sort
/// receivers and/or key items in order of decreasing priority,
/// as specified in [ZIP 316](https://zips.z.cash/zip-0316#encoding-of-unified-addresses)
pub fn preference_order(a: &Self, b: &Self) -> cmp::Ordering {
match (a, b) {
// Trivial equality checks.
@ -69,51 +100,213 @@ impl Typecode {
(_, Self::P2pkh) => cmp::Ordering::Greater,
}
}
}
pub fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering {
u32::from(*a).cmp(&u32::from(*b))
/// The known Metadata Typecodes
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum MetadataTypecode {
/// Expiration height metadata as specified in [ZIP 316, Revision 1](https://zips.z.cash/zip-0316)
ExpiryHeight,
/// Expiration height metadata as specified in [ZIP 316, Revision 1](https://zips.z.cash/zip-0316)
ExpiryTime,
/// An unknown MUST-understand metadata item as specified in
/// [ZIP 316, Revision 1](https://zips.z.cash/zip-0316)
///
/// A parser encountering this typecode MUST halt with an error.
MustUnderstand(u32),
/// An unknown metadata item as specified in [ZIP 316, Revision 1](https://zips.z.cash/zip-0316)
Unknown(u32),
}
impl TryFrom<u32> for MetadataTypecode {
type Error = ();
fn try_from(typecode: u32) -> Result<Self, Self::Error> {
match typecode {
0xC0..=0xDF => Ok(MetadataTypecode::Unknown(typecode)),
0xE0 => Ok(MetadataTypecode::ExpiryHeight),
0xE1 => Ok(MetadataTypecode::ExpiryTime),
0xE2..=0xFC => Ok(MetadataTypecode::MustUnderstand(typecode)),
_ => Err(()),
}
}
}
impl From<MetadataTypecode> for u32 {
fn from(t: MetadataTypecode) -> Self {
match t {
MetadataTypecode::ExpiryHeight => 0xE0,
MetadataTypecode::ExpiryTime => 0xE1,
MetadataTypecode::MustUnderstand(value) => value,
MetadataTypecode::Unknown(value) => value,
}
}
}
/// An enumeration of the Unified Container Item Typecodes.
///
/// Unified Address Items are partitioned into two sets: data items, which include
/// receivers and viewing keys, and metadata items.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Typecode {
/// A data (receiver or viewing key) typecode.
Data(DataTypecode),
/// A metadata typecode.
Metadata(MetadataTypecode),
}
impl Typecode {
/// The typecode for p2pkh data items.
pub const P2PKH: Typecode = Typecode::Data(DataTypecode::P2pkh);
/// The typecode for p2sh data items.
pub const P2SH: Typecode = Typecode::Data(DataTypecode::P2sh);
/// The typecode for Sapling data items.
pub const SAPLING: Typecode = Typecode::Data(DataTypecode::Sapling);
/// The typecode for Orchard data items.
pub const ORCHARD: Typecode = Typecode::Data(DataTypecode::Orchard);
}
impl TryFrom<u32> for Typecode {
type Error = ParseError;
fn try_from(typecode: u32) -> Result<Self, Self::Error> {
match typecode {
0x00 => Ok(Typecode::P2pkh),
0x01 => Ok(Typecode::P2sh),
0x02 => Ok(Typecode::Sapling),
0x03 => Ok(Typecode::Orchard),
0x04..=0x02000000 => Ok(Typecode::Unknown(typecode)),
0x02000001..=u32::MAX => Err(ParseError::InvalidTypecodeValue(typecode as u64)),
}
DataTypecode::try_from(typecode)
.map_or_else(
|()| MetadataTypecode::try_from(typecode).map(Typecode::Metadata),
|t| Ok(Typecode::Data(t)),
)
.map_err(|()| ParseError::InvalidTypecodeValue(typecode))
}
}
impl From<Typecode> for u32 {
fn from(t: Typecode) -> Self {
match t {
Typecode::P2pkh => 0x00,
Typecode::P2sh => 0x01,
Typecode::Sapling => 0x02,
Typecode::Orchard => 0x03,
Typecode::Unknown(typecode) => typecode,
Typecode::Data(tc) => tc.into(),
Typecode::Metadata(tc) => tc.into(),
}
}
}
impl TryFrom<Typecode> for usize {
type Error = TryFromIntError;
fn try_from(t: Typecode) -> Result<Self, Self::Error> {
u32::from(t).try_into()
}
}
impl Typecode {
fn is_transparent(&self) -> bool {
// Unknown typecodes are treated as not transparent for the purpose of disallowing
// only-transparent UAs, which can be represented with existing address encodings.
matches!(self, Typecode::P2pkh | Typecode::P2sh)
/// An enumeration of known Unified Metadata Item types.
///
/// Unknown MUST-understand metadata items are NOT represented using this type, as the presence of
/// an unrecognized metadata item with a typecode in the `MUST-understand` range will result in a
/// parse failure, instead of the construction of a metadata item.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum MetadataItem {
/// The expiry height for a Unified Address or Unified Viewing Key
ExpiryHeight(u32),
/// The expiry time for a Unified Address or Unified Viewing Key
ExpiryTime(u64),
/// A Metadata Item with an unrecognized Typecode. MUST-understand metadata items are NOT
/// represented using this type, as the presence of an unrecognized metadata item with a
/// typecode in the `MUST-understand` range will result in a parse failure.
Unknown { typecode: u32, data: Vec<u8> },
}
impl MetadataItem {
/// Parse a metadata item for the specified metadata typecode from the provided bytes.
pub fn parse(
revision: Revision,
typecode: MetadataTypecode,
data: &[u8],
) -> Result<Self, ParseError> {
use MetadataTypecode::*;
use Revision::*;
match (revision, typecode) {
(R1, ExpiryHeight) => data
.try_into()
.map(u32::from_le_bytes)
.map(MetadataItem::ExpiryHeight)
.map_err(|_| {
ParseError::InvalidEncoding(
"Expiry height must be a 32-bit little-endian value.".to_string(),
)
}),
(R1, ExpiryTime) => data
.try_into()
.map(u64::from_le_bytes)
.map(MetadataItem::ExpiryTime)
.map_err(|_| {
ParseError::InvalidEncoding(
"Expiry time must be a 64-bit little-endian value.".to_string(),
)
}),
(R0, ExpiryHeight | ExpiryTime) => Err(ParseError::NotUnderstood(typecode.into())),
(R0 | R1, MustUnderstand(tc)) => Err(ParseError::NotUnderstood(tc)),
// This implementation treats the 0xC0..OxFD range as unknown metadata for both R0 and
// R1, as no typecodes were specified in this range for R0 and were "reclaimed" as
// metadata codes by ZIP 316 at the time R1 was introduced.
(R0 | R1, Unknown(typecode)) => Ok(MetadataItem::Unknown {
typecode,
data: data.to_vec(),
}),
}
}
/// Returns the typecode for this metadata item.
pub fn typecode(&self) -> MetadataTypecode {
match self {
MetadataItem::ExpiryHeight(_) => MetadataTypecode::ExpiryHeight,
MetadataItem::ExpiryTime(_) => MetadataTypecode::ExpiryTime,
MetadataItem::Unknown { typecode, .. } => MetadataTypecode::Unknown(*typecode),
}
}
/// Returns the raw bytes of this metadata item.
pub fn data(&self) -> Vec<u8> {
match self {
MetadataItem::ExpiryHeight(h) => h.to_le_bytes().to_vec(),
MetadataItem::ExpiryTime(t) => t.to_le_bytes().to_vec(),
MetadataItem::Unknown { data, .. } => data.clone(),
}
}
}
/// A Unified Encoding Item.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Item<T> {
/// A data item; either a receiver (for Unified Addresses) or a key (for Unified Viewing Keys)
Data(T),
/// A metadata item.
Metadata(MetadataItem),
}
impl<T: private::SealedDataItem> Item<T> {
/// Returns the typecode for this item.
pub fn typecode(&self) -> Typecode {
match self {
Item::Data(d) => Typecode::Data(d.typecode()),
Item::Metadata(m) => Typecode::Metadata(m.typecode()),
}
}
/// The total ordering over items by their typecodes, used for encoding as specified
/// in [ZIP 316](https://zips.z.cash/zip-0316#encoding-of-unified-addresses)
pub fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering {
u32::from(a.typecode()).cmp(&u32::from(b.typecode()))
}
/// Returns the raw binary representation of the data for this item.
pub fn data(&self) -> Vec<u8> {
match self {
Item::Data(d) => d.data().to_vec(),
Item::Metadata(m) => m.data(),
}
}
/// Returns whether this item is a transparent receiver or key.
pub fn is_transparent_data_item(&self) -> bool {
self.typecode() == Typecode::P2PKH || self.typecode() == Typecode::P2SH
}
}
@ -125,7 +318,7 @@ pub enum ParseError {
/// The unified container contains a duplicated typecode.
DuplicateTypecode(Typecode),
/// The parsed typecode exceeds the maximum allowed CompactSize value.
InvalidTypecodeValue(u64),
InvalidTypecodeValue(u32),
/// The string is an invalid encoding.
InvalidEncoding(String),
/// The items in the unified container are not in typecode order.
@ -136,6 +329,8 @@ pub enum ParseError {
NotUnified,
/// The Bech32m string has an unrecognized human-readable prefix.
UnknownPrefix(String),
/// A `MUST-understand` metadata item was not recognized.
NotUnderstood(u32),
}
impl fmt::Display for ParseError {
@ -151,67 +346,93 @@ impl fmt::Display for ParseError {
ParseError::UnknownPrefix(s) => {
write!(f, "Unrecognized Bech32m human-readable prefix: {}", s)
}
ParseError::NotUnderstood(tc) => {
write!(
f,
"MUST-understand metadata item with typecode {} was not recognized; please upgrade.",
tc
)
}
}
}
}
impl Error for ParseError {}
/// The revision of the Unified Address standard that an address was parsed under.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Revision {
R0,
R1,
}
pub(crate) mod private {
use super::{ParseError, Typecode, PADDING_LEN};
use crate::Network;
use super::{DataTypecode, ParseError, Revision, Typecode, PADDING_LEN};
use crate::{
unified::{Item, MetadataItem},
Network,
};
use std::{
cmp,
convert::{TryFrom, TryInto},
io::Write,
};
use zcash_encoding::CompactSize;
/// A raw address or viewing key.
pub trait SealedItem: for<'a> TryFrom<(u32, &'a [u8]), Error = ParseError> + Clone {
fn typecode(&self) -> Typecode;
pub trait SealedDataItem: Clone {
/// Parse a data item for the specified data typecode from the provided bytes.
fn parse(tc: DataTypecode, value: &[u8]) -> Result<Self, ParseError>;
/// Returns the typecode of this data item.
fn typecode(&self) -> DataTypecode;
/// Returns the raw bytes of this data item.
fn data(&self) -> &[u8];
fn preference_order(a: &Self, b: &Self) -> cmp::Ordering {
match Typecode::preference_order(&a.typecode(), &b.typecode()) {
cmp::Ordering::Equal => a.data().cmp(b.data()),
res => res,
}
}
fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering {
match Typecode::encoding_order(&a.typecode(), &b.typecode()) {
cmp::Ordering::Equal => a.data().cmp(b.data()),
res => res,
}
}
}
/// A Unified Container containing addresses or viewing keys.
pub trait SealedContainer: super::Container + std::marker::Sized {
const MAINNET: &'static str;
const TESTNET: &'static str;
const REGTEST: &'static str;
const MAINNET_R0: &'static str;
const TESTNET_R0: &'static str;
const REGTEST_R0: &'static str;
const MAINNET_R1: &'static str;
const TESTNET_R1: &'static str;
const REGTEST_R1: &'static str;
/// Implementations of this method should act as unchecked constructors
/// of the container type; the caller is guaranteed to check the
/// general invariants that apply to all unified containers.
fn from_inner(items: Vec<Self::Item>) -> Self;
fn from_inner(revision: Revision, items: Vec<Item<Self::DataItem>>) -> Self;
fn network_hrp(network: &Network) -> &'static str {
match network {
Network::Main => Self::MAINNET,
Network::Test => Self::TESTNET,
Network::Regtest => Self::REGTEST,
fn network_hrp(revision: Revision, network: &Network) -> &'static str {
match (revision, network) {
(Revision::R0, Network::Main) => Self::MAINNET_R0,
(Revision::R0, Network::Test) => Self::TESTNET_R0,
(Revision::R0, Network::Regtest) => Self::REGTEST_R0,
(Revision::R1, Network::Main) => Self::MAINNET_R1,
(Revision::R1, Network::Test) => Self::TESTNET_R1,
(Revision::R1, Network::Regtest) => Self::REGTEST_R1,
}
}
fn hrp_revision(hrp: &str) -> Option<Revision> {
if hrp == Self::MAINNET_R0 || hrp == Self::TESTNET_R0 || hrp == Self::REGTEST_R0 {
Some(Revision::R0)
} else if hrp == Self::MAINNET_R1 || hrp == Self::TESTNET_R1 || hrp == Self::REGTEST_R1
{
Some(Revision::R1)
} else {
None
}
}
fn hrp_network(hrp: &str) -> Option<Network> {
if hrp == Self::MAINNET {
if hrp == Self::MAINNET_R0 || hrp == Self::MAINNET_R1 {
Some(Network::Main)
} else if hrp == Self::TESTNET {
} else if hrp == Self::TESTNET_R0 || hrp == Self::TESTNET_R1 {
Some(Network::Test)
} else if hrp == Self::REGTEST {
} else if hrp == Self::REGTEST_R0 || hrp == Self::REGTEST_R1 {
Some(Network::Regtest)
} else {
None
@ -227,7 +448,7 @@ pub(crate) mod private {
)
.unwrap();
CompactSize::write(&mut writer, data.len()).unwrap();
writer.write_all(data).unwrap();
writer.write_all(&data).unwrap();
}
}
@ -248,10 +469,15 @@ pub(crate) mod private {
}
/// Parse the items of the unified container.
fn parse_items<T: Into<Vec<u8>>>(hrp: &str, buf: T) -> Result<Vec<Self::Item>, ParseError> {
fn read_receiver<R: SealedItem>(
#[allow(clippy::type_complexity)]
fn parse_items<T: Into<Vec<u8>>>(
hrp: &str,
buf: T,
) -> Result<(Revision, Vec<Item<Self::DataItem>>), ParseError> {
fn read_item<R: SealedDataItem>(
revision: Revision,
mut cursor: &mut std::io::Cursor<&[u8]>,
) -> Result<R, ParseError> {
) -> Result<Item<R>, ParseError> {
let typecode = CompactSize::read(&mut cursor)
.map(|v| u32::try_from(v).expect("CompactSize::read enforces MAX_SIZE limit"))
.map_err(|e| {
@ -279,12 +505,18 @@ pub(crate) mod private {
length
)));
}
let result = R::try_from((
typecode,
&buf[cursor.position() as usize..addr_end as usize],
));
// The "as usize" casts cannot change the values, because both
// cursor.position() and addr_end are u64 values <= buf.len()
// which is usize.
let data = &buf[cursor.position() as usize..addr_end as usize];
let result = match Typecode::try_from(typecode)? {
Typecode::Data(tc) => Item::Data(R::parse(tc, data)?),
Typecode::Metadata(tc) => {
Item::Metadata(MetadataItem::parse(revision, tc, data)?)
}
};
cursor.set_position(addr_end);
result
Ok(result)
}
// Here we allocate if necessary to get a mutable Vec<u8> to unjumble.
@ -308,22 +540,27 @@ pub(crate) mod private {
)),
}?;
let revision = Self::hrp_revision(hrp)
.ok_or_else(|| ParseError::UnknownPrefix(hrp.to_string()))?;
let mut cursor = std::io::Cursor::new(encoded);
let mut result = vec![];
while cursor.position() < encoded.len().try_into().unwrap() {
result.push(read_receiver(&mut cursor)?);
result.push(read_item(revision, &mut cursor)?);
}
assert_eq!(cursor.position(), encoded.len().try_into().unwrap());
Ok(result)
Ok((revision, result))
}
/// A private function that constructs a unified container with the
/// specified items, which must be in ascending typecode order.
fn try_from_items_internal(items: Vec<Self::Item>) -> Result<Self, ParseError> {
assert!(u32::from(Typecode::P2sh) == u32::from(Typecode::P2pkh) + 1);
fn try_from_items_internal(
revision: Revision,
items: Vec<Item<Self::DataItem>>,
) -> Result<Self, ParseError> {
assert!(u32::from(Typecode::P2SH) == u32::from(Typecode::P2PKH) + 1);
let mut only_transparent = true;
let mut prev_code = None; // less than any Some
for item in &items {
let t = item.typecode();
@ -332,47 +569,46 @@ pub(crate) mod private {
return Err(ParseError::InvalidTypecodeOrder);
} else if t_code == prev_code {
return Err(ParseError::DuplicateTypecode(t));
} else if t == Typecode::P2sh && prev_code == Some(u32::from(Typecode::P2pkh)) {
} else if t == Typecode::P2SH && prev_code == Some(u32::from(DataTypecode::P2pkh)) {
// P2pkh and P2sh can only be in that order and next to each other,
// otherwise we would detect an out-of-order or duplicate typecode.
return Err(ParseError::BothP2phkAndP2sh);
} else {
prev_code = t_code;
only_transparent = only_transparent && t.is_transparent();
}
}
if only_transparent {
Err(ParseError::OnlyTransparent)
} else {
// All checks pass!
Ok(Self::from_inner(items))
}
// All checks pass!
Ok(Self::from_inner(revision, items))
}
fn parse_internal<T: Into<Vec<u8>>>(hrp: &str, buf: T) -> Result<Self, ParseError> {
Self::parse_items(hrp, buf).and_then(Self::try_from_items_internal)
Self::parse_items(hrp, buf)
.and_then(|(revision, items)| Self::try_from_items_internal(revision, items))
}
}
}
use private::SealedItem;
use private::SealedDataItem;
/// Trait providing common encoding and decoding logic for Unified containers.
pub trait Encoding: private::SealedContainer {
/// Constructs a value of a unified container type from a vector
/// of container items, sorted according to typecode as specified
/// in ZIP 316.
/// Constructs a value of a unified container type from a vector of container
/// items. These items will be sorted according to typecode as specified in ZIP
/// 316, so this method is not necessarily round-trip compatible with
/// [`Container::items_as_parsed`].
///
/// This function will return an error in the case that the following ZIP 316
/// invariants concerning the composition of a unified container are
/// violated:
/// * the item list may not contain two items having the same typecode
/// * the item list may not contain only transparent items (or no items)
/// * the item list may not contain both P2PKH and P2SH items.
fn try_from_items(mut items: Vec<Self::Item>) -> Result<Self, ParseError> {
items.sort_unstable_by(Self::Item::encoding_order);
Self::try_from_items_internal(items)
fn try_from_items(
revision: Revision,
mut items: Vec<Item<Self::DataItem>>,
) -> Result<Self, ParseError> {
items.sort_unstable_by(Item::encoding_order);
Self::try_from_items_internal(revision, items)
}
/// Decodes a unified container from its string representation, preserving
@ -399,7 +635,7 @@ pub trait Encoding: private::SealedContainer {
/// ordering of the contained items such that it correctly obeys round-trip
/// serialization invariants.
fn encode(&self, network: &Network) -> String {
let hrp = Self::network_hrp(network);
let hrp = Self::network_hrp(self.revision(), network);
bech32::encode(
hrp,
self.to_jumbled_bytes(hrp).to_base32(),
@ -411,20 +647,13 @@ pub trait Encoding: private::SealedContainer {
/// Trait for for Unified containers, that exposes the items within them.
pub trait Container {
/// The type of item in this unified container.
type Item: SealedItem;
/// The type of data items in this unified container.
type DataItem: SealedDataItem;
/// Returns the items contained within this container, sorted in preference order.
fn items(&self) -> Vec<Self::Item> {
let mut items = self.items_as_parsed().to_vec();
// Unstable sorting is fine, because all items are guaranteed by construction
// to have distinct typecodes.
items.sort_unstable_by(Self::Item::preference_order);
items
}
/// Returns the items in encoding order.
fn items_as_parsed(&self) -> &[Item<Self::DataItem>];
/// Returns the items in the order they were parsed from the string encoding.
///
/// This API is for advanced usage; in most cases you should use `Self::items`.
fn items_as_parsed(&self) -> &[Self::Item];
/// Returns the revision of the ZIP 316 standard that this unified container
/// conforms to.
fn revision(&self) -> Revision;
}

View File

@ -1,8 +1,10 @@
use super::{private::SealedItem, ParseError, Typecode};
use zcash_protocol::{PoolType, ShieldedProtocol};
use std::convert::{TryFrom, TryInto};
use super::{private::SealedDataItem, DataTypecode, Item, ParseError, Revision};
/// The set of known Receivers for Unified Addresses.
use std::{cmp, convert::TryInto};
/// The enumeration of Unified Address Receivers of known types.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Receiver {
Orchard([u8; 43]),
@ -12,34 +14,39 @@ pub enum Receiver {
Unknown { typecode: u32, data: Vec<u8> },
}
impl TryFrom<(u32, &[u8])> for Receiver {
type Error = ParseError;
fn try_from((typecode, addr): (u32, &[u8])) -> Result<Self, Self::Error> {
match typecode.try_into()? {
Typecode::P2pkh => addr.try_into().map(Receiver::P2pkh),
Typecode::P2sh => addr.try_into().map(Receiver::P2sh),
Typecode::Sapling => addr.try_into().map(Receiver::Sapling),
Typecode::Orchard => addr.try_into().map(Receiver::Orchard),
Typecode::Unknown(_) => Ok(Receiver::Unknown {
typecode,
data: addr.to_vec(),
}),
}
.map_err(|e| {
ParseError::InvalidEncoding(format!("Invalid address for typecode {}: {}", typecode, e))
})
impl Receiver {
fn preference_order(a: &Self, b: &Self) -> cmp::Ordering {
DataTypecode::preference_order(&a.typecode(), &b.typecode())
}
}
impl SealedItem for Receiver {
fn typecode(&self) -> Typecode {
impl SealedDataItem for Receiver {
fn parse(typecode: DataTypecode, data: &[u8]) -> Result<Self, ParseError> {
match typecode {
DataTypecode::P2pkh => data.try_into().map(Receiver::P2pkh),
DataTypecode::P2sh => data.try_into().map(Receiver::P2sh),
DataTypecode::Sapling => data.try_into().map(Receiver::Sapling),
DataTypecode::Orchard => data.try_into().map(Receiver::Orchard),
DataTypecode::Unknown(typecode) => Ok(Receiver::Unknown {
typecode,
data: data.to_vec(),
}),
}
.map_err(|e| {
ParseError::InvalidEncoding(format!(
"Invalid address for typecode {:?}: {:?}",
typecode, e
))
})
}
fn typecode(&self) -> DataTypecode {
match self {
Receiver::P2pkh(_) => Typecode::P2pkh,
Receiver::P2sh(_) => Typecode::P2sh,
Receiver::Sapling(_) => Typecode::Sapling,
Receiver::Orchard(_) => Typecode::Orchard,
Receiver::Unknown { typecode, .. } => Typecode::Unknown(*typecode),
Receiver::P2pkh(_) => DataTypecode::P2pkh,
Receiver::P2sh(_) => DataTypecode::P2sh,
Receiver::Sapling(_) => DataTypecode::Sapling,
Receiver::Orchard(_) => DataTypecode::Orchard,
Receiver::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode),
}
}
@ -62,7 +69,7 @@ impl SealedItem for Receiver {
/// # use std::convert::Infallible;
/// # use std::error::Error;
/// use zcash_address::{
/// unified::{self, Container, Encoding},
/// unified::{self, Container, Encoding, Item, Revision},
/// ConversionError, TryFromRawAddress, ZcashAddress,
/// };
///
@ -90,70 +97,144 @@ impl SealedItem for Receiver {
///
/// // We can obtain the receivers for the UA in preference order
/// // (the order in which wallets should prefer to use them):
/// let receivers: Vec<unified::Receiver> = ua.items();
/// let receivers: Vec<unified::Receiver> = ua.receivers();
///
/// // And we can create the UA from a list of receivers:
/// let new_ua = unified::Address::try_from_items(receivers)?;
/// let new_ua = unified::Address::try_from_items(Revision::R0, receivers.into_iter().map(Item::Data).collect())?;
/// assert_eq!(new_ua, ua);
/// # Ok(())
/// # }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Address(pub(crate) Vec<Receiver>);
pub struct Address {
pub(crate) revision: Revision,
pub(crate) receivers: Vec<Item<Receiver>>,
}
impl Address {
/// Returns the receiver items for this address, in order of decreasing preference.
///
/// The receiver for a wallet to send to can safely be chosen by selecting the first receiver
/// of a type that wallet supports from the result.
pub fn receivers(&self) -> Vec<Receiver> {
let mut result = self
.receivers
.iter()
.filter_map(|item| match item {
Item::Data(r) => Some(r.clone()),
Item::Metadata(_) => None,
})
.collect::<Vec<Receiver>>();
result.sort_unstable_by(Receiver::preference_order);
result
}
}
impl Address {
/// Returns whether this address has the ability to receive transfers of the given pool type.
pub fn has_receiver_of_type(&self, pool_type: PoolType) -> bool {
self.receivers.iter().any(|item| match item {
Item::Data(Receiver::Orchard(_)) => {
pool_type == PoolType::Shielded(ShieldedProtocol::Orchard)
}
Item::Data(Receiver::Sapling(_)) => {
pool_type == PoolType::Shielded(ShieldedProtocol::Sapling)
}
Item::Data(Receiver::P2pkh(_)) | Item::Data(Receiver::P2sh(_)) => {
pool_type == PoolType::Transparent
}
Item::Data(Receiver::Unknown { .. }) => false,
Item::Metadata(_) => false,
})
}
/// Returns whether this address contains the given receiver.
pub fn contains_receiver(&self, receiver: &Receiver) -> bool {
self.receivers
.iter()
.any(|item| matches!(item, Item::Data(r) if r == receiver))
}
/// Returns whether this address can receive a memo.
pub fn can_receive_memo(&self) -> bool {
self.receivers.iter().any(|r| {
matches!(
r,
Item::Data(Receiver::Sapling(_)) | Item::Data(Receiver::Orchard(_))
)
})
}
}
impl super::private::SealedContainer for Address {
/// The HRP for a Bech32m-encoded mainnet Unified Address.
/// The HRP for a Bech32m-encoded mainnet Revision 0 Unified Address.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET: &'static str = "u";
const MAINNET_R0: &'static str = "u";
/// The HRP for a Bech32m-encoded testnet Unified Address.
/// The HRP for a Bech32m-encoded testnet Revision 0 Unified Address.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET: &'static str = "utest";
const TESTNET_R0: &'static str = "utest";
/// The HRP for a Bech32m-encoded regtest Unified Address.
const REGTEST: &'static str = "uregtest";
/// The HRP for a Bech32m-encoded regtest Revision 0 Unified Address.
const REGTEST_R0: &'static str = "uregtest";
fn from_inner(receivers: Vec<Self::Item>) -> Self {
Self(receivers)
/// The HRP for a Bech32m-encoded mainnet Revision 1 Unified Address.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET_R1: &'static str = "ur";
/// The HRP for a Bech32m-encoded testnet Revision 1 Unified Address.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET_R1: &'static str = "urtest";
/// The HRP for a Bech32m-encoded regtest Revision 1 Unified Address.
const REGTEST_R1: &'static str = "urregtest";
fn from_inner(revision: Revision, receivers: Vec<Item<Self::DataItem>>) -> Self {
Self {
revision,
receivers,
}
}
}
impl super::Encoding for Address {}
impl super::Container for Address {
type Item = Receiver;
type DataItem = Receiver;
fn items_as_parsed(&self) -> &[Receiver] {
&self.0
fn items_as_parsed(&self) -> &[Item<Receiver>] {
&self.receivers
}
fn revision(&self) -> Revision {
self.revision
}
}
#[cfg(any(test, feature = "test-dependencies"))]
pub mod test_vectors;
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use zcash_encoding::MAX_COMPACT_SIZE;
use crate::{
kind::unified::{private::SealedContainer, Container, Encoding},
Network,
};
pub mod testing {
use proptest::{
array::{uniform11, uniform20, uniform32},
collection::vec,
prelude::*,
sample::select,
strategy::Strategy,
};
use super::{Address, ParseError, Receiver, Typecode};
use super::{Address, Receiver};
use crate::unified::{DataTypecode, Item, Revision};
use zcash_encoding::MAX_COMPACT_SIZE;
prop_compose! {
fn uniform43()(a in uniform11(0u8..), b in uniform32(0u8..)) -> [u8; 43] {
@ -164,53 +245,85 @@ mod tests {
}
}
fn arb_transparent_typecode() -> impl Strategy<Value = Typecode> {
select(vec![Typecode::P2pkh, Typecode::P2sh])
/// A strategy to generate an arbitrary transparent typecode.
fn arb_transparent_typecode() -> impl Strategy<Value = DataTypecode> {
select(vec![DataTypecode::P2pkh, DataTypecode::P2sh])
}
fn arb_shielded_typecode() -> impl Strategy<Value = Typecode> {
/// A strategy to generate an arbitrary shielded (Sapling, Orchard, or unknown) typecode.
fn arb_shielded_typecode() -> impl Strategy<Value = DataTypecode> {
prop_oneof![
Just(Typecode::Sapling),
Just(Typecode::Orchard),
((<u32>::from(Typecode::Orchard) + 1)..MAX_COMPACT_SIZE).prop_map(Typecode::Unknown)
Just(DataTypecode::Sapling),
Just(DataTypecode::Orchard),
((<u32>::from(DataTypecode::Orchard) + 1)..MAX_COMPACT_SIZE)
.prop_map(DataTypecode::Unknown)
]
}
/// A strategy to generate an arbitrary valid set of typecodes without
/// duplication and containing only one of P2sh and P2pkh transparent
/// typecodes. The resulting vector will be sorted in encoding order.
fn arb_typecodes() -> impl Strategy<Value = Vec<Typecode>> {
fn arb_typecodes() -> impl Strategy<Value = Vec<DataTypecode>> {
prop::option::of(arb_transparent_typecode()).prop_flat_map(|transparent| {
prop::collection::hash_set(arb_shielded_typecode(), 1..4).prop_map(move |xs| {
let mut typecodes: Vec<_> = xs.into_iter().chain(transparent).collect();
typecodes.sort_unstable_by(Typecode::encoding_order);
typecodes
})
prop::collection::hash_set(arb_shielded_typecode(), 1..4)
.prop_map(move |xs| xs.into_iter().chain(transparent).collect::<Vec<_>>())
})
}
fn arb_unified_address_for_typecodes(
typecodes: Vec<Typecode>,
/// A strategy to generate a vector of unified address receivers containing random data. The
/// resulting receivers may not be valid according to protocol rules; this generator is only
/// intended for use in testing parsing and serialization.
fn arb_unified_address_receivers(
typecodes: Vec<DataTypecode>,
) -> impl Strategy<Value = Vec<Receiver>> {
typecodes
.into_iter()
.map(|tc| match tc {
Typecode::P2pkh => uniform20(0u8..).prop_map(Receiver::P2pkh).boxed(),
Typecode::P2sh => uniform20(0u8..).prop_map(Receiver::P2sh).boxed(),
Typecode::Sapling => uniform43().prop_map(Receiver::Sapling).boxed(),
Typecode::Orchard => uniform43().prop_map(Receiver::Orchard).boxed(),
Typecode::Unknown(typecode) => vec(any::<u8>(), 32..256)
DataTypecode::P2pkh => uniform20(0u8..).prop_map(Receiver::P2pkh).boxed(),
DataTypecode::P2sh => uniform20(0u8..).prop_map(Receiver::P2sh).boxed(),
DataTypecode::Sapling => uniform43().prop_map(Receiver::Sapling).boxed(),
DataTypecode::Orchard => uniform43().prop_map(Receiver::Orchard).boxed(),
DataTypecode::Unknown(typecode) => vec(any::<u8>(), 32..256)
.prop_map(move |data| Receiver::Unknown { typecode, data })
.boxed(),
})
.collect::<Vec<_>>()
}
fn arb_unified_address() -> impl Strategy<Value = Address> {
/// A strategy to generate an arbitrary Unified Address containing only receivers, without
/// additional metadata. The items in this address will be sorted in encoding order. The
/// receivers in the resulting address may not be valid according to protocol rules; this
/// generator is only intended for use in testing parsing and serialization.
pub fn arb_unified_address() -> impl Strategy<Value = Address> {
arb_typecodes()
.prop_flat_map(arb_unified_address_for_typecodes)
.prop_map(Address)
.prop_flat_map(arb_unified_address_receivers)
.prop_map(|rs| {
let mut receivers = rs.into_iter().map(Item::Data).collect::<Vec<_>>();
receivers.sort_unstable_by(Item::encoding_order);
Address {
revision: Revision::R0,
receivers,
}
})
}
}
#[cfg(any(test, feature = "test-dependencies"))]
pub mod test_vectors;
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use crate::{
kind::unified::{private::SealedContainer, Encoding},
unified::{address::testing::arb_unified_address, Item, Revision, Typecode},
Network,
};
use proptest::{prelude::*, sample::select};
use super::{Address, ParseError, Receiver};
proptest! {
#[test]
@ -239,7 +352,7 @@ mod tests {
0x7b, 0x28, 0x69, 0xc9, 0x84,
];
assert_eq!(
Address::parse_internal(Address::MAINNET, &invalid_padding[..]),
Address::parse_internal(Address::MAINNET_R0, &invalid_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -254,7 +367,7 @@ mod tests {
0x4b, 0x31, 0xee, 0x5a,
];
assert_eq!(
Address::parse_internal(Address::MAINNET, &truncated_padding[..]),
Address::parse_internal(Address::MAINNET_R0, &truncated_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -279,7 +392,7 @@ mod tests {
0xc6, 0x5e, 0x68, 0xa2, 0x78, 0x6c, 0x9e,
];
assert_matches!(
Address::parse_internal(Address::MAINNET, &truncated_sapling_data[..]),
Address::parse_internal(Address::MAINNET_R0, &truncated_sapling_data[..]),
Err(ParseError::InvalidEncoding(_))
);
@ -292,7 +405,7 @@ mod tests {
0xe6, 0x70, 0x36, 0x5b, 0x7b, 0x9e,
];
assert_matches!(
Address::parse_internal(Address::MAINNET, &truncated_after_sapling_typecode[..]),
Address::parse_internal(Address::MAINNET_R0, &truncated_after_sapling_typecode[..]),
Err(ParseError::InvalidEncoding(_))
);
}
@ -301,11 +414,17 @@ mod tests {
fn duplicate_typecode() {
// Construct and serialize an invalid UA. This must be done using private
// methods, as the public API does not permit construction of such invalid values.
let ua = Address(vec![Receiver::Sapling([1; 43]), Receiver::Sapling([2; 43])]);
let encoded = ua.to_jumbled_bytes(Address::MAINNET);
let ua = Address {
revision: Revision::R0,
receivers: vec![
Item::Data(Receiver::Sapling([1; 43])),
Item::Data(Receiver::Sapling([2; 43])),
],
};
let encoded = ua.to_jumbled_bytes(Address::MAINNET_R0);
assert_eq!(
Address::parse_internal(Address::MAINNET, &encoded[..]),
Err(ParseError::DuplicateTypecode(Typecode::Sapling))
Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
Err(ParseError::DuplicateTypecode(Typecode::SAPLING))
);
}
@ -313,11 +432,17 @@ mod tests {
fn p2pkh_and_p2sh() {
// Construct and serialize an invalid UA. This must be done using private
// methods, as the public API does not permit construction of such invalid values.
let ua = Address(vec![Receiver::P2pkh([0; 20]), Receiver::P2sh([0; 20])]);
let encoded = ua.to_jumbled_bytes(Address::MAINNET);
let ua = Address {
revision: Revision::R0,
receivers: vec![
Item::Data(Receiver::P2pkh([0; 20])),
Item::Data(Receiver::P2sh([0; 20])),
],
};
let encoded = ua.to_jumbled_bytes(Address::MAINNET_R0);
// ensure that decoding catches the error
assert_eq!(
Address::parse_internal(Address::MAINNET, &encoded[..]),
Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
Err(ParseError::BothP2phkAndP2sh)
);
}
@ -326,11 +451,17 @@ mod tests {
fn addresses_out_of_order() {
// Construct and serialize an invalid UA. This must be done using private
// methods, as the public API does not permit construction of such invalid values.
let ua = Address(vec![Receiver::Sapling([0; 43]), Receiver::P2pkh([0; 20])]);
let encoded = ua.to_jumbled_bytes(Address::MAINNET);
let ua = Address {
revision: Revision::R0,
receivers: vec![
Item::Data(Receiver::Sapling([0; 43])),
Item::Data(Receiver::P2pkh([0; 20])),
],
};
let encoded = ua.to_jumbled_bytes(Address::MAINNET_R0);
// ensure that decoding catches the error
assert_eq!(
Address::parse_internal(Address::MAINNET, &encoded[..]),
Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
Err(ParseError::InvalidTypecodeOrder)
);
}
@ -349,7 +480,7 @@ mod tests {
// with only one of them we don't have sufficient data for F4Jumble (so we hit a
// different error).
assert_matches!(
Address::parse_internal(Address::MAINNET, &encoded[..]),
Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
Err(ParseError::InvalidEncoding(_))
);
}
@ -357,19 +488,22 @@ mod tests {
#[test]
fn receivers_are_sorted() {
// Construct a UA with receivers in an unsorted order.
let ua = Address(vec![
Receiver::P2pkh([0; 20]),
Receiver::Orchard([0; 43]),
Receiver::Unknown {
typecode: 0xff,
data: vec![],
},
Receiver::Sapling([0; 43]),
]);
let ua = Address {
revision: Revision::R0,
receivers: vec![
Item::Data(Receiver::P2pkh([0; 20])),
Item::Data(Receiver::Orchard([0; 43])),
Item::Data(Receiver::Unknown {
typecode: 0xff,
data: vec![],
}),
Item::Data(Receiver::Sapling([0; 43])),
],
};
// `Address::receivers` sorts the receivers in priority order.
assert_eq!(
ua.items(),
ua.receivers(),
vec![
Receiver::Orchard([0; 43]),
Receiver::Sapling([0; 43]),

View File

@ -1,8 +1,8 @@
use std::convert::{TryFrom, TryInto};
use std::convert::TryInto;
use super::{
private::{SealedContainer, SealedItem},
Container, Encoding, ParseError, Typecode,
private::{SealedContainer, SealedDataItem},
Container, DataTypecode, Encoding, Item, ParseError, Revision,
};
/// The set of known FVKs for Unified FVKs.
@ -39,31 +39,27 @@ pub enum Fvk {
},
}
impl TryFrom<(u32, &[u8])> for Fvk {
type Error = ParseError;
fn try_from((typecode, data): (u32, &[u8])) -> Result<Self, Self::Error> {
impl SealedDataItem for Fvk {
fn parse(typecode: DataTypecode, data: &[u8]) -> Result<Self, ParseError> {
let data = data.to_vec();
match typecode.try_into()? {
Typecode::P2pkh => data.try_into().map(Fvk::P2pkh),
Typecode::P2sh => Err(data),
Typecode::Sapling => data.try_into().map(Fvk::Sapling),
Typecode::Orchard => data.try_into().map(Fvk::Orchard),
Typecode::Unknown(_) => Ok(Fvk::Unknown { typecode, data }),
match typecode {
DataTypecode::P2pkh => data.try_into().map(Fvk::P2pkh),
DataTypecode::P2sh => Err(data),
DataTypecode::Sapling => data.try_into().map(Fvk::Sapling),
DataTypecode::Orchard => data.try_into().map(Fvk::Orchard),
DataTypecode::Unknown(typecode) => Ok(Fvk::Unknown { typecode, data }),
}
.map_err(|e| {
ParseError::InvalidEncoding(format!("Invalid fvk for typecode {}: {:?}", typecode, e))
ParseError::InvalidEncoding(format!("Invalid fvk for typecode {:?}: {:?}", typecode, e))
})
}
}
impl SealedItem for Fvk {
fn typecode(&self) -> Typecode {
fn typecode(&self) -> DataTypecode {
match self {
Fvk::P2pkh(_) => Typecode::P2pkh,
Fvk::Sapling(_) => Typecode::Sapling,
Fvk::Orchard(_) => Typecode::Orchard,
Fvk::Unknown { typecode, .. } => Typecode::Unknown(*typecode),
Fvk::P2pkh(_) => DataTypecode::P2pkh,
Fvk::Sapling(_) => DataTypecode::Sapling,
Fvk::Orchard(_) => DataTypecode::Orchard,
Fvk::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode),
}
}
@ -83,7 +79,7 @@ impl SealedItem for Fvk {
///
/// ```
/// # use std::error::Error;
/// use zcash_address::unified::{self, Container, Encoding};
/// use zcash_address::unified::{self, Container, Encoding, Item, Revision};
///
/// # fn main() -> Result<(), Box<dyn Error>> {
/// # let ufvk_from_user = || "uview1cgrqnry478ckvpr0f580t6fsahp0a5mj2e9xl7hv2d2jd4ldzy449mwwk2l9yeuts85wjls6hjtghdsy5vhhvmjdw3jxl3cxhrg3vs296a3czazrycrr5cywjhwc5c3ztfyjdhmz0exvzzeyejamyp0cr9z8f9wj0953fzht0m4lenk94t70ruwgjxag2tvp63wn9ftzhtkh20gyre3w5s24f6wlgqxnjh40gd2lxe75sf3z8h5y2x0atpxcyf9t3em4h0evvsftluruqne6w4sm066sw0qe5y8qg423grple5fftxrqyy7xmqmatv7nzd7tcjadu8f7mqz4l83jsyxy4t8pkayytyk7nrp467ds85knekdkvnd7hqkfer8mnqd7pv";
@ -91,54 +87,72 @@ impl SealedItem for Fvk {
///
/// let (network, ufvk) = unified::Ufvk::decode(example_ufvk)?;
///
/// // We can obtain the pool-specific Full Viewing Keys for the UFVK in preference
/// // order (the order in which wallets should prefer to use their corresponding
/// // address receivers):
/// let fvks: Vec<unified::Fvk> = ufvk.items();
/// // We can obtain the pool-specific Full Viewing Keys for the UFVK.
/// let fvks: &[Item<unified::Fvk>] = ufvk.items_as_parsed();
///
/// // And we can create the UFVK from a list of FVKs:
/// let new_ufvk = unified::Ufvk::try_from_items(fvks)?;
/// let new_ufvk = unified::Ufvk::try_from_items(Revision::R0, fvks.to_vec())?;
/// assert_eq!(new_ufvk, ufvk);
/// # Ok(())
/// # }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Ufvk(pub(crate) Vec<Fvk>);
pub struct Ufvk {
pub(crate) revision: Revision,
pub(crate) fvks: Vec<Item<Fvk>>,
}
impl Container for Ufvk {
type Item = Fvk;
type DataItem = Fvk;
/// Returns the FVKs contained within this UFVK, in the order they were
/// parsed from the string encoding.
///
/// This API is for advanced usage; in most cases you should use `Ufvk::receivers`.
fn items_as_parsed(&self) -> &[Fvk] {
&self.0
fn items_as_parsed(&self) -> &[Item<Fvk>] {
&self.fvks
}
fn revision(&self) -> Revision {
self.revision
}
}
impl Encoding for Ufvk {}
impl SealedContainer for Ufvk {
/// The HRP for a Bech32m-encoded mainnet Unified FVK.
/// The HRP for a Bech32m-encoded mainnet Revision 0 Unified FVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET: &'static str = "uview";
const MAINNET_R0: &'static str = "uview";
/// The HRP for a Bech32m-encoded testnet Unified FVK.
/// The HRP for a Bech32m-encoded testnet Revision 0 Unified FVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET: &'static str = "uviewtest";
const TESTNET_R0: &'static str = "uviewtest";
/// The HRP for a Bech32m-encoded regtest Unified FVK.
const REGTEST: &'static str = "uviewregtest";
/// The HRP for a Bech32m-encoded regtest Revision 0 Unified FVK.
const REGTEST_R0: &'static str = "uviewregtest";
fn from_inner(fvks: Vec<Self::Item>) -> Self {
Self(fvks)
/// The HRP for a Bech32m-encoded mainnet Revision 1 Unified FVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET_R1: &'static str = "urview";
/// The HRP for a Bech32m-encoded testnet Revision 1 Unified FVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET_R1: &'static str = "urviewtest";
/// The HRP for a Bech32m-encoded regtest Revision 1 Unified FVK.
const REGTEST_R1: &'static str = "urviewregtest";
fn from_inner(revision: Revision, fvks: Vec<Item<Self::DataItem>>) -> Self {
Self { revision, fvks }
}
}
@ -148,12 +162,10 @@ mod tests {
use proptest::{array::uniform1, array::uniform32, prelude::*, sample::select};
use super::{Fvk, ParseError, Typecode, Ufvk};
use super::{Fvk, ParseError, Ufvk};
use crate::{
kind::unified::{
private::{SealedContainer, SealedItem},
Container, Encoding,
},
kind::unified::{private::SealedContainer, Encoding},
unified::{Item, Revision, Typecode},
Network,
};
@ -211,9 +223,9 @@ mod tests {
shielded in arb_shielded_fvk(),
transparent in prop::option::of(arb_transparent_fvk()),
) -> Ufvk {
let mut items: Vec<_> = transparent.into_iter().chain(shielded).collect();
items.sort_unstable_by(Fvk::encoding_order);
Ufvk(items)
let mut fvks: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect();
fvks.sort_unstable_by(Item::encoding_order);
Ufvk { revision: Revision::R0, fvks }
}
}
@ -245,7 +257,7 @@ mod tests {
0xdf, 0x63, 0xe7, 0xef, 0x65, 0x6b, 0x18, 0x23, 0xf7, 0x3e, 0x35, 0x7c, 0xf3, 0xc4,
];
assert_eq!(
Ufvk::parse_internal(Ufvk::MAINNET, &invalid_padding[..]),
Ufvk::parse_internal(Ufvk::MAINNET_R0, &invalid_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -263,7 +275,7 @@ mod tests {
0x43, 0x8e, 0xc0, 0x3e, 0x9f, 0xf4, 0xf1, 0x80, 0x32, 0xcf, 0x2f, 0x7e, 0x7f, 0x91,
];
assert_eq!(
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_padding[..]),
Ufvk::parse_internal(Ufvk::MAINNET_R0, &truncated_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -295,7 +307,7 @@ mod tests {
0x8c, 0x7a, 0xbf, 0x7b, 0x9a, 0xdd, 0xee, 0x18, 0x2c, 0x2d, 0xc2, 0xfc,
];
assert_matches!(
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_sapling_data[..]),
Ufvk::parse_internal(Ufvk::MAINNET_R0, &truncated_sapling_data[..]),
Err(ParseError::InvalidEncoding(_))
);
@ -310,7 +322,7 @@ mod tests {
0x54, 0xd1, 0x9e, 0xec, 0x8b, 0xef, 0x35, 0xb8, 0x44, 0xdd, 0xab, 0x9a, 0x8d,
];
assert_matches!(
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_after_sapling_typecode[..]),
Ufvk::parse_internal(Ufvk::MAINNET_R0, &truncated_after_sapling_typecode[..]),
Err(ParseError::InvalidEncoding(_))
);
}
@ -319,11 +331,17 @@ mod tests {
fn duplicate_typecode() {
// Construct and serialize an invalid Ufvk. This must be done using private
// methods, as the public API does not permit construction of such invalid values.
let ufvk = Ufvk(vec![Fvk::Sapling([1; 128]), Fvk::Sapling([2; 128])]);
let encoded = ufvk.to_jumbled_bytes(Ufvk::MAINNET);
let ufvk = Ufvk {
revision: Revision::R0,
fvks: vec![
Item::Data(Fvk::Sapling([1; 128])),
Item::Data(Fvk::Sapling([2; 128])),
],
};
let encoded = ufvk.to_jumbled_bytes(Ufvk::MAINNET_R0);
assert_eq!(
Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..]),
Err(ParseError::DuplicateTypecode(Typecode::Sapling))
Ufvk::parse_internal(Ufvk::MAINNET_R0, &encoded[..]),
Err(ParseError::DuplicateTypecode(Typecode::SAPLING))
);
}
@ -339,37 +357,9 @@ mod tests {
0xf4, 0xf5, 0x16, 0xef, 0x5c, 0xe0, 0x26, 0xbc, 0x23, 0x73, 0x76, 0x3f, 0x4b,
];
assert_eq!(
Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..]),
Err(ParseError::OnlyTransparent)
assert_matches!(
Ufvk::parse_internal(Ufvk::MAINNET_R0, &encoded[..]),
Ok(_)
);
}
#[test]
fn fvks_are_sorted() {
// Construct a UFVK with fvks in an unsorted order.
let ufvk = Ufvk(vec![
Fvk::P2pkh([0; 65]),
Fvk::Orchard([0; 96]),
Fvk::Unknown {
typecode: 0xff,
data: vec![],
},
Fvk::Sapling([0; 128]),
]);
// `Ufvk::items` sorts the fvks in priority order.
assert_eq!(
ufvk.items(),
vec![
Fvk::Orchard([0; 96]),
Fvk::Sapling([0; 128]),
Fvk::P2pkh([0; 65]),
Fvk::Unknown {
typecode: 0xff,
data: vec![],
},
]
)
}
}

View File

@ -1,8 +1,8 @@
use std::convert::{TryFrom, TryInto};
use std::convert::TryInto;
use super::{
private::{SealedContainer, SealedItem},
Container, Encoding, ParseError, Typecode,
private::{SealedContainer, SealedDataItem},
Container, DataTypecode, Encoding, Item, ParseError, Revision,
};
/// The set of known IVKs for Unified IVKs.
@ -44,31 +44,27 @@ pub enum Ivk {
},
}
impl TryFrom<(u32, &[u8])> for Ivk {
type Error = ParseError;
fn try_from((typecode, data): (u32, &[u8])) -> Result<Self, Self::Error> {
impl SealedDataItem for Ivk {
fn parse(typecode: DataTypecode, data: &[u8]) -> Result<Self, ParseError> {
let data = data.to_vec();
match typecode.try_into()? {
Typecode::P2pkh => data.try_into().map(Ivk::P2pkh),
Typecode::P2sh => Err(data),
Typecode::Sapling => data.try_into().map(Ivk::Sapling),
Typecode::Orchard => data.try_into().map(Ivk::Orchard),
Typecode::Unknown(_) => Ok(Ivk::Unknown { typecode, data }),
match typecode {
DataTypecode::P2pkh => data.try_into().map(Ivk::P2pkh),
DataTypecode::P2sh => Err(data),
DataTypecode::Sapling => data.try_into().map(Ivk::Sapling),
DataTypecode::Orchard => data.try_into().map(Ivk::Orchard),
DataTypecode::Unknown(typecode) => Ok(Ivk::Unknown { typecode, data }),
}
.map_err(|e| {
ParseError::InvalidEncoding(format!("Invalid ivk for typecode {}: {:?}", typecode, e))
ParseError::InvalidEncoding(format!("Invalid ivk for typecode {:?}: {:?}", typecode, e))
})
}
}
impl SealedItem for Ivk {
fn typecode(&self) -> Typecode {
fn typecode(&self) -> DataTypecode {
match self {
Ivk::P2pkh(_) => Typecode::P2pkh,
Ivk::Sapling(_) => Typecode::Sapling,
Ivk::Orchard(_) => Typecode::Orchard,
Ivk::Unknown { typecode, .. } => Typecode::Unknown(*typecode),
Ivk::P2pkh(_) => DataTypecode::P2pkh,
Ivk::Sapling(_) => DataTypecode::Sapling,
Ivk::Orchard(_) => DataTypecode::Orchard,
Ivk::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode),
}
}
@ -88,7 +84,7 @@ impl SealedItem for Ivk {
///
/// ```
/// # use std::error::Error;
/// use zcash_address::unified::{self, Container, Encoding};
/// use zcash_address::unified::{self, Container, Encoding, Item, Revision};
///
/// # fn main() -> Result<(), Box<dyn Error>> {
/// # let uivk_from_user = || "uivk1djetqg3fws7y7qu5tekynvcdhz69gsyq07ewvppmzxdqhpfzdgmx8urnkqzv7ylz78ez43ux266pqjhecd59fzhn7wpe6zarnzh804hjtkyad25ryqla5pnc8p5wdl3phj9fczhz64zprun3ux7y9jc08567xryumuz59rjmg4uuflpjqwnq0j0tzce0x74t4tv3gfjq7nczkawxy6y7hse733ae3vw7qfjd0ss0pytvezxp42p6rrpzeh6t2zrz7zpjk0xhngcm6gwdppxs58jkx56gsfflugehf5vjlmu7vj3393gj6u37wenavtqyhdvcdeaj86s6jczl4zq";
@ -96,54 +92,72 @@ impl SealedItem for Ivk {
///
/// let (network, uivk) = unified::Uivk::decode(example_uivk)?;
///
/// // We can obtain the pool-specific Incoming Viewing Keys for the UIVK in
/// // preference order (the order in which wallets should prefer to use their
/// // corresponding address receivers):
/// let ivks: Vec<unified::Ivk> = uivk.items();
/// // We can obtain the pool-specific Incoming Viewing Keys for the UIVK.
/// let ivks: &[Item<unified::Ivk>] = uivk.items_as_parsed();
///
/// // And we can create the UIVK from a list of IVKs:
/// let new_uivk = unified::Uivk::try_from_items(ivks)?;
/// // And we can create the UIVK from a vector of IVKs:
/// let new_uivk = unified::Uivk::try_from_items(Revision::R0, ivks.to_vec())?;
/// assert_eq!(new_uivk, uivk);
/// # Ok(())
/// # }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Uivk(pub(crate) Vec<Ivk>);
pub struct Uivk {
pub(crate) revision: Revision,
pub(crate) ivks: Vec<Item<Ivk>>,
}
impl Container for Uivk {
type Item = Ivk;
type DataItem = Ivk;
/// Returns the IVKs contained within this UIVK, in the order they were
/// parsed from the string encoding.
///
/// This API is for advanced usage; in most cases you should use `Uivk::items`.
fn items_as_parsed(&self) -> &[Ivk] {
&self.0
fn items_as_parsed(&self) -> &[Item<Ivk>] {
&self.ivks
}
fn revision(&self) -> Revision {
self.revision
}
}
impl Encoding for Uivk {}
impl SealedContainer for Uivk {
/// The HRP for a Bech32m-encoded mainnet Unified IVK.
/// The HRP for a Bech32m-encoded mainnet Revision 0 Unified IVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET: &'static str = "uivk";
const MAINNET_R0: &'static str = "uivk";
/// The HRP for a Bech32m-encoded testnet Unified IVK.
/// The HRP for a Bech32m-encoded testnet Revision 0 Unified IVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET: &'static str = "uivktest";
const TESTNET_R0: &'static str = "uivktest";
/// The HRP for a Bech32m-encoded regtest Unified IVK.
const REGTEST: &'static str = "uivkregtest";
/// The HRP for a Bech32m-encoded regtest Revision 0 Unified IVK.
const REGTEST_R0: &'static str = "uivkregtest";
fn from_inner(ivks: Vec<Self::Item>) -> Self {
Self(ivks)
/// The HRP for a Bech32m-encoded mainnet Revision 1 Unified IVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET_R1: &'static str = "urivk";
/// The HRP for a Bech32m-encoded testnet Revision 1 Unified IVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET_R1: &'static str = "urivktest";
/// The HRP for a Bech32m-encoded regtest Revision 1 Unified IVK.
const REGTEST_R1: &'static str = "urivkregtest";
fn from_inner(revision: Revision, ivks: Vec<Item<Self::DataItem>>) -> Self {
Self { revision, ivks }
}
}
@ -157,12 +171,10 @@ mod tests {
sample::select,
};
use super::{Ivk, ParseError, Typecode, Uivk};
use super::{Ivk, ParseError, Uivk};
use crate::{
kind::unified::{
private::{SealedContainer, SealedItem},
Container, Encoding,
},
kind::unified::{private::SealedContainer, Encoding},
unified::{Item, Revision, Typecode},
Network,
};
@ -204,9 +216,9 @@ mod tests {
shielded in arb_shielded_ivk(),
transparent in prop::option::of(arb_transparent_ivk()),
) -> Uivk {
let mut items: Vec<_> = transparent.into_iter().chain(shielded).collect();
items.sort_unstable_by(Ivk::encoding_order);
Uivk(items)
let mut ivks: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect();
ivks.sort_unstable_by(Item::encoding_order);
Uivk { revision: Revision::R0, ivks }
}
}
@ -236,7 +248,7 @@ mod tests {
0x83, 0xe8, 0x92, 0x18, 0x28, 0x70, 0x1e, 0x81, 0x76, 0x56, 0xb6, 0x15,
];
assert_eq!(
Uivk::parse_internal(Uivk::MAINNET, &invalid_padding[..]),
Uivk::parse_internal(Uivk::MAINNET_R0, &invalid_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -252,7 +264,7 @@ mod tests {
0xf9, 0x65, 0x49, 0x14, 0xab, 0x7c, 0x55, 0x7b, 0x39, 0x47,
];
assert_eq!(
Uivk::parse_internal(Uivk::MAINNET, &truncated_padding[..]),
Uivk::parse_internal(Uivk::MAINNET_R0, &truncated_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -280,7 +292,7 @@ mod tests {
0xf5, 0xd5, 0x8a, 0xb5, 0x1a,
];
assert_matches!(
Uivk::parse_internal(Uivk::MAINNET, &truncated_sapling_data[..]),
Uivk::parse_internal(Uivk::MAINNET_R0, &truncated_sapling_data[..]),
Err(ParseError::InvalidEncoding(_))
);
@ -293,7 +305,7 @@ mod tests {
0xd8, 0x21, 0x5e, 0x8, 0xa, 0x82, 0x95, 0x21, 0x74,
];
assert_matches!(
Uivk::parse_internal(Uivk::MAINNET, &truncated_after_sapling_typecode[..]),
Uivk::parse_internal(Uivk::MAINNET_R0, &truncated_after_sapling_typecode[..]),
Err(ParseError::InvalidEncoding(_))
);
}
@ -301,11 +313,17 @@ mod tests {
#[test]
fn duplicate_typecode() {
// Construct and serialize an invalid UIVK.
let uivk = Uivk(vec![Ivk::Sapling([1; 64]), Ivk::Sapling([2; 64])]);
let uivk = Uivk {
revision: Revision::R0,
ivks: vec![
Item::Data(Ivk::Sapling([1; 64])),
Item::Data(Ivk::Sapling([2; 64])),
],
};
let encoded = uivk.encode(&Network::Main);
assert_eq!(
Uivk::decode(&encoded),
Err(ParseError::DuplicateTypecode(Typecode::Sapling))
Err(ParseError::DuplicateTypecode(Typecode::SAPLING))
);
}
@ -321,37 +339,9 @@ mod tests {
0xbd, 0xfe, 0xa4, 0xb7, 0x47, 0x20, 0x92, 0x6, 0xf0, 0x0, 0xf9, 0x64,
];
assert_eq!(
Uivk::parse_internal(Uivk::MAINNET, &encoded[..]),
Err(ParseError::OnlyTransparent)
assert_matches!(
Uivk::parse_internal(Uivk::MAINNET_R0, &encoded[..]),
Ok(_)
);
}
#[test]
fn ivks_are_sorted() {
// Construct a UIVK with ivks in an unsorted order.
let uivk = Uivk(vec![
Ivk::P2pkh([0; 65]),
Ivk::Orchard([0; 64]),
Ivk::Unknown {
typecode: 0xff,
data: vec![],
},
Ivk::Sapling([0; 64]),
]);
// `Uivk::items` sorts the ivks in priority order.
assert_eq!(
uivk.items(),
vec![
Ivk::Orchard([0; 64]),
Ivk::Sapling([0; 64]),
Ivk::P2pkh([0; 65]),
Ivk::Unknown {
typecode: 0xff,
data: vec![],
},
]
)
}
}

View File

@ -141,7 +141,9 @@ 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};
/// A Zcash address.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
@ -266,4 +268,116 @@ impl ZcashAddress {
}),
}
}
/// Returns whether this address has the ability to receive transfers of the given pool type.
pub fn can_receive_as(&self, pool_type: PoolType) -> bool {
use AddressKind::*;
match &self.kind {
Sprout(_) => false,
Sapling(_) => pool_type == PoolType::Shielded(ShieldedProtocol::Sapling),
Unified(addr) => addr.has_receiver_of_type(pool_type),
P2pkh(_) | P2sh(_) | Tex(_) => pool_type == PoolType::Transparent,
}
}
/// Returns whether this address can receive a memo.
pub fn can_receive_memo(&self) -> bool {
use AddressKind::*;
match &self.kind {
Sprout(_) | Sapling(_) => true,
Unified(addr) => addr.can_receive_memo(),
P2pkh(_) | P2sh(_) | 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 (&self.kind, receiver) {
(AddressKind::Unified(ua), r) => ua.contains_receiver(r),
(AddressKind::Sapling(d), Receiver::Sapling(r)) => r == d,
(AddressKind::P2pkh(d), Receiver::P2pkh(r)) => r == d,
(AddressKind::Tex(d), Receiver::P2pkh(r)) => r == d,
(AddressKind::P2sh(d), Receiver::P2sh(r)) => r == d,
_ => false,
}
}
}
#[cfg(feature = "test-dependencies")]
pub mod testing {
use std::convert::TryInto;
use proptest::{array::uniform20, collection::vec, prelude::any, prop_compose, prop_oneof};
use crate::{unified::address::testing::arb_unified_address, AddressKind, ZcashAddress};
use zcash_protocol::consensus::NetworkType;
prop_compose! {
fn arb_sprout_addr_kind()(
r_bytes in vec(any::<u8>(), 64)
) -> AddressKind {
AddressKind::Sprout(r_bytes.try_into().unwrap())
}
}
prop_compose! {
fn arb_sapling_addr_kind()(
r_bytes in vec(any::<u8>(), 43)
) -> AddressKind {
AddressKind::Sapling(r_bytes.try_into().unwrap())
}
}
prop_compose! {
fn arb_p2pkh_addr_kind()(
r_bytes in uniform20(any::<u8>())
) -> AddressKind {
AddressKind::P2pkh(r_bytes)
}
}
prop_compose! {
fn arb_p2sh_addr_kind()(
r_bytes in uniform20(any::<u8>())
) -> AddressKind {
AddressKind::P2sh(r_bytes)
}
}
prop_compose! {
fn arb_unified_addr_kind()(
uaddr in arb_unified_address()
) -> AddressKind {
AddressKind::Unified(uaddr)
}
}
prop_compose! {
fn arb_tex_addr_kind()(
r_bytes in uniform20(any::<u8>())
) -> AddressKind {
AddressKind::Tex(r_bytes)
}
}
prop_compose! {
/// Create an arbitrary, structurally-valid `ZcashAddress` value.
///
/// Note that the data contained in the generated address does _not_ necessarily correspond
/// to a valid address according to the Zcash protocol; binary data in the resulting value
/// is entirely random.
pub fn arb_address(net: NetworkType)(
kind in prop_oneof!(
arb_sprout_addr_kind(),
arb_sapling_addr_kind(),
arb_p2pkh_addr_kind(),
arb_p2sh_addr_kind(),
arb_unified_addr_kind(),
arb_tex_addr_kind()
)
) -> ZcashAddress {
ZcashAddress { net, kind }
}
}
}

View File

@ -8,6 +8,7 @@ use {
unified::{
self,
address::{test_vectors::TEST_VECTORS, Receiver},
Item, Revision,
},
Network, ToAddress, ZcashAddress,
},
@ -36,9 +37,16 @@ fn unified() {
data: data.to_vec(),
})
}))
.map(Item::Data)
.collect();
let expected_addr = ZcashAddress::from_unified(Network::Main, unified::Address(receivers));
let expected_addr = ZcashAddress::from_unified(
Network::Main,
unified::Address {
revision: Revision::R0,
receivers,
},
);
// Test parsing
let addr: ZcashAddress = tv.unified_addr.parse().unwrap();

View File

@ -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

View File

@ -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 {

View File

@ -0,0 +1,34 @@
# Changelog
All notable changes to this library will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this library adheres to Rust's notion of
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
The entries below are relative to the `zcash_client_backend` crate as of
`zcash_client_backend-0.10.0`.
### Added
- `zip321::Payment::new`
- `impl From<zcash_address:ConversionError<E>> for Zip321Error`
### Changed
- Fields of `zip321::Payment` are now private. Accessors have been provided for
the fields that are no longer public, and `Payment::new` has been added to
serve the needs of payment construction.
- `zip321::Payment::recipient_address()` returns `zcash_address::ZcashAddress`
- `zip321::Payment::without_memo` now takes a `zcash_address::ZcashAddress` for
its `recipient_address` argument.
- Uses of `zcash_primitives::transaction::components::amount::NonNegartiveAmount`
have been replace with `zcash_protocol::value::Zatoshis`. Also, some incorrect
uses of the signed `zcash_primitives::transaction::components::Amount`
type have been corrected via replacement with the `Zatoshis` type.
- The following methods that previously required a
`zcash_primitives::consensus::Parameters` argument to facilitate address
parsing no longer take such an argument.
- `zip321::TransactionRequest::{to_uri, from_uri}`
- `zip321::render::addr_param`
- `zip321::parse::{lead_addr, zcashparam}`
- `zip321::Param::Memo` now boxes its argument.
- `zip321::Param::Addr` now wraps a `zcash_address::ZcashAddress`

View File

@ -0,0 +1,28 @@
[package]
name = "zip321"
description = "Parsing functions and data types for Zcash ZIP 321 Payment Request URIs"
version = "0.0.0"
authors = [
"Kris Nuttycombe <kris@electriccoin.co>"
]
homepage = "https://github.com/zcash/librustzcash"
repository.workspace = true
readme = "README.md"
license.workspace = true
edition.workspace = true
rust-version.workspace = true
categories.workspace = true
[dependencies]
zcash_address.workspace = true
zcash_protocol.workspace = true
# - Parsing and Encoding
nom = "7"
base64.workspace = true
percent-encoding.workspace = true
[dev-dependencies]
zcash_address = { workspace = true, features = ["test-dependencies"] }
zcash_protocol = { workspace = true, features = ["test-dependencies"] }
proptest.workspace = true

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017-2024 Electric Coin Company
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,22 @@
# zip321
This library contains Rust parsing functions and data types for working with
Zcash ZIP 321 Payment Request URIs.
## License
Licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in the work by you, as defined in the Apache-2.0
license, shall be dual licensed as above, without any additional terms or
conditions.

View File

@ -1,6 +1,6 @@
//! Reference implementation of the ZIP-321 standard for payment requests.
//!
//! This module provides data structures, parsing, and rendering functions
//! This crate provides data structures, parsing, and rendering functions
//! for interpreting and producing valid ZIP 321 URIs.
//!
//! The specification for ZIP 321 URIs may be found at <https://zips.z.cash/zip-0321>
@ -15,13 +15,13 @@ use nom::{
character::complete::char, combinator::all_consuming, multi::separated_list0,
sequence::preceded,
};
use zcash_primitives::{
memo::{self, MemoBytes},
transaction::components::amount::NonNegativeAmount,
};
use zcash_protocol::{consensus, value::BalanceError};
use crate::address::Address;
use zcash_address::{ConversionError, ZcashAddress};
use zcash_protocol::{
memo::{self, MemoBytes},
value::BalanceError,
value::Zatoshis,
};
/// Errors that may be produced in decoding of payment requests.
#[derive(Debug, Clone, PartialEq, Eq)]
@ -45,6 +45,12 @@ pub enum Zip321Error {
ParseError(String),
}
impl<E: Display> From<ConversionError<E>> for Zip321Error {
fn from(value: ConversionError<E>) -> Self {
Zip321Error::ParseError(format!("Address parsing failed: {}", value))
}
}
impl Display for Zip321Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
@ -92,14 +98,14 @@ impl std::error::Error for Zip321Error {
/// Converts a [`MemoBytes`] value to a ZIP 321 compatible base64-encoded string.
///
/// [`MemoBytes`]: zcash_primitives::memo::MemoBytes
/// [`MemoBytes`]: zcash_protocol::memo::MemoBytes
pub fn memo_to_base64(memo: &MemoBytes) -> String {
BASE64_URL_SAFE_NO_PAD.encode(memo.as_slice())
}
/// Parse a [`MemoBytes`] value from a ZIP 321 compatible base64-encoded string.
///
/// [`MemoBytes`]: zcash_primitives::memo::MemoBytes
/// [`MemoBytes`]: zcash_protocol::memo::MemoBytes
pub fn memo_from_base64(s: &str) -> Result<MemoBytes, Zip321Error> {
BASE64_URL_SAFE_NO_PAD
.decode(s)
@ -110,29 +116,55 @@ pub fn memo_from_base64(s: &str) -> Result<MemoBytes, Zip321Error> {
/// A single payment being requested.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Payment {
/// The payment address to which the payment should be sent.
pub recipient_address: Address,
/// The address to which the payment should be sent.
recipient_address: ZcashAddress,
/// The amount of the payment that is being requested.
pub amount: NonNegativeAmount,
amount: Zatoshis,
/// A memo that, if included, must be provided with the payment.
/// If a memo is present and [`recipient_address`] is not a shielded
/// address, the wallet should report an error.
///
/// [`recipient_address`]: #structfield.recipient_address
pub memo: Option<MemoBytes>,
memo: Option<MemoBytes>,
/// A human-readable label for this payment within the larger structure
/// of the transaction request.
pub label: Option<String>,
label: Option<String>,
/// A human-readable message to be displayed to the user describing the
/// purpose of this payment.
pub message: Option<String>,
message: Option<String>,
/// A list of other arbitrary key/value pairs associated with this payment.
pub other_params: Vec<(String, String)>,
other_params: Vec<(String, String)>,
}
impl Payment {
/// Constructs a new [`Payment`] from its constituent parts.
///
/// Returns `None` if the payment requests that a memo be sent to a recipient that cannot
/// receive a memo.
pub fn new(
recipient_address: ZcashAddress,
amount: Zatoshis,
memo: Option<MemoBytes>,
label: Option<String>,
message: Option<String>,
other_params: Vec<(String, String)>,
) -> Option<Self> {
if memo.is_none() || recipient_address.can_receive_memo() {
Some(Self {
recipient_address,
amount,
memo,
label,
message,
other_params,
})
} else {
None
}
}
/// Constructs a new [`Payment`] paying the given address the specified amount.
pub fn without_memo(recipient_address: Address, amount: NonNegativeAmount) -> Self {
pub fn without_memo(recipient_address: ZcashAddress, amount: Zatoshis) -> Self {
Self {
recipient_address,
amount,
@ -143,9 +175,41 @@ impl Payment {
}
}
/// Returns the payment address to which the payment should be sent.
pub fn recipient_address(&self) -> &ZcashAddress {
&self.recipient_address
}
/// Returns the value of the payment that is being requested, in zatoshis.
pub fn amount(&self) -> Zatoshis {
self.amount
}
/// Returns the memo that, if included, must be provided with the payment.
pub fn memo(&self) -> Option<&MemoBytes> {
self.memo.as_ref()
}
/// A human-readable label for this payment within the larger structure
/// of the transaction request.
pub fn label(&self) -> Option<&String> {
self.label.as_ref()
}
/// A human-readable message to be displayed to the user describing the
/// purpose of this payment.
pub fn message(&self) -> Option<&String> {
self.message.as_ref()
}
/// A list of other arbitrary key/value pairs associated with this payment.
pub fn other_params(&self) -> &[(String, String)] {
self.other_params.as_ref()
}
/// A utility for use in tests to help check round-trip serialization properties.
#[cfg(any(test, feature = "test-dependencies"))]
pub(in crate::zip321) fn normalize(&mut self) {
pub(crate) fn normalize(&mut self) {
self.other_params.sort();
}
}
@ -182,10 +246,7 @@ impl TransactionRequest {
// Enforce validity requirements.
if !request.payments.is_empty() {
// It doesn't matter what params we use here, as none of the validity
// requirements depend on them.
let params = consensus::MAIN_NETWORK;
TransactionRequest::from_uri(&params, &request.to_uri(&params))?;
TransactionRequest::from_uri(&request.to_uri())?;
}
Ok(request)
@ -218,19 +279,19 @@ impl TransactionRequest {
///
/// Returns `Err` in the case of overflow, or if the value is
/// outside the range `0..=MAX_MONEY` zatoshis.
pub fn total(&self) -> Result<NonNegativeAmount, BalanceError> {
pub fn total(&self) -> Result<Zatoshis, BalanceError> {
self.payments
.values()
.map(|p| p.amount)
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| {
.fold(Ok(Zatoshis::ZERO), |acc, a| {
(acc? + a).ok_or(BalanceError::Overflow)
})
}
/// A utility for use in tests to help check round-trip serialization properties.
#[cfg(any(test, feature = "test-dependencies"))]
pub(in crate::zip321) fn normalize(&mut self) {
for p in self.payments.values_mut() {
pub(crate) fn normalize(&mut self) {
for p in &mut self.payments.values_mut() {
p.normalize();
}
}
@ -238,10 +299,7 @@ impl TransactionRequest {
/// A utility for use in tests to help check round-trip serialization properties.
/// by comparing a two transaction requests for equality after normalization.
#[cfg(test)]
pub(in crate::zip321) fn normalize_and_eq(
a: &mut TransactionRequest,
b: &mut TransactionRequest,
) -> bool {
pub(crate) fn normalize_and_eq(a: &mut TransactionRequest, b: &mut TransactionRequest) -> bool {
a.normalize();
b.normalize();
@ -251,7 +309,7 @@ impl TransactionRequest {
/// Convert this request to a URI string.
///
/// Returns None if the payment request is empty.
pub fn to_uri<P: consensus::Parameters>(&self, params: &P) -> String {
pub fn to_uri(&self) -> String {
fn payment_params(
payment: &Payment,
payment_index: Option<usize>,
@ -294,7 +352,7 @@ impl TransactionRequest {
format!(
"zcash:{}{}{}",
payment.recipient_address.encode(params),
payment.recipient_address.encode(),
if query_params.is_empty() { "" } else { "?" },
query_params.join("&")
)
@ -307,7 +365,7 @@ impl TransactionRequest {
let idx = if *i == 0 { None } else { Some(*i) };
let primary_address = payment.recipient_address.clone();
std::iter::empty()
.chain(Some(render::addr_param(params, &primary_address, idx)))
.chain(Some(render::addr_param(&primary_address, idx)))
.chain(payment_params(payment, idx))
})
.collect::<Vec<String>>();
@ -318,9 +376,9 @@ impl TransactionRequest {
}
/// Parse the provided URI to a payment request value.
pub fn from_uri<P: consensus::Parameters>(params: &P, uri: &str) -> Result<Self, Zip321Error> {
pub fn from_uri(uri: &str) -> Result<Self, Zip321Error> {
// Parse the leading zcash:<address>
let (rest, primary_addr_param) = parse::lead_addr(params)(uri)
let (rest, primary_addr_param) = parse::lead_addr(uri)
.map_err(|e| Zip321Error::ParseError(format!("Error parsing lead address: {}", e)))?;
// Parse the remaining parameters as an undifferentiated list
@ -329,7 +387,7 @@ impl TransactionRequest {
} else {
all_consuming(preceded(
char('?'),
separated_list0(char('&'), parse::zcashparam(params)),
separated_list0(char('&'), parse::zcashparam),
))(rest)
.map_err(|e| {
Zip321Error::ParseError(format!("Error parsing query parameters: {}", e))
@ -372,13 +430,13 @@ impl TransactionRequest {
mod render {
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use zcash_primitives::{
consensus, transaction::components::amount::NonNegativeAmount,
transaction::components::amount::COIN,
use zcash_address::ZcashAddress;
use zcash_protocol::{
memo::MemoBytes,
value::{Zatoshis, COIN},
};
use super::{memo_to_base64, Address, MemoBytes};
use super::memo_to_base64;
/// The set of ASCII characters that must be percent-encoded according
/// to the definition of ZIP 321. This is the complement of the subset of
@ -418,17 +476,13 @@ mod render {
/// Constructs an "address" key/value pair containing the encoded recipient address
/// at the specified parameter index.
pub fn addr_param<P: consensus::Parameters>(
params: &P,
addr: &Address,
idx: Option<usize>,
) -> String {
format!("address{}={}", param_index(idx), addr.encode(params))
pub fn addr_param(addr: &ZcashAddress, idx: Option<usize>) -> String {
format!("address{}={}", param_index(idx), addr.encode())
}
/// Converts a [`NonNegativeAmount`] value to a correctly formatted decimal ZEC
/// string for inclusion in a ZIP 321 URI.
pub fn amount_str(amount: NonNegativeAmount) -> String {
/// Converts a [`Zatoshis`] value to a correctly formatted decimal ZEC
/// value for inclusion in a ZIP 321 URI.
pub fn amount_str(amount: Zatoshis) -> String {
let coins = u64::from(amount) / COIN;
let zats = u64::from(amount) % COIN;
if zats == 0 {
@ -442,7 +496,7 @@ mod render {
/// Constructs an "amount" key/value pair containing the encoded ZEC amount
/// at the specified parameter index.
pub fn amount_param(amount: NonNegativeAmount, idx: Option<usize>) -> String {
pub fn amount_param(amount: Zatoshis, idx: Option<usize>) -> String {
format!("amount{}={}", param_index(idx), amount_str(amount))
}
@ -475,23 +529,22 @@ mod parse {
AsChar, IResult, InputTakeAtPosition,
};
use percent_encoding::percent_decode;
use zcash_primitives::{
consensus, transaction::components::amount::NonNegativeAmount,
transaction::components::amount::COIN,
};
use zcash_address::ZcashAddress;
use zcash_protocol::value::BalanceError;
use zcash_protocol::{
memo::MemoBytes,
value::{Zatoshis, COIN},
};
use crate::address::Address;
use super::{memo_from_base64, MemoBytes, Payment, Zip321Error};
use super::{memo_from_base64, Payment, Zip321Error};
/// A data type that defines the possible parameter types which may occur within a
/// ZIP 321 URI.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Param {
Addr(Box<Address>),
Amount(NonNegativeAmount),
Memo(MemoBytes),
Addr(Box<ZcashAddress>),
Amount(Zatoshis),
Memo(Box<MemoBytes>),
Label(String),
Message(String),
Other(String, String),
@ -551,7 +604,7 @@ mod parse {
let mut payment = Payment {
recipient_address: *addr.ok_or(Zip321Error::RecipientMissing(i))?,
amount: NonNegativeAmount::ZERO,
amount: Zatoshis::ZERO,
memo: None,
label: None,
message: None,
@ -561,11 +614,13 @@ mod parse {
for v in vs {
match v {
Param::Amount(a) => payment.amount = a,
Param::Memo(m) => match payment.recipient_address {
Address::Sapling(_) | Address::Unified(_) => payment.memo = Some(m),
Address::Transparent(_) => return Err(Zip321Error::TransparentMemo(i)),
},
Param::Memo(m) => {
if payment.recipient_address.can_receive_memo() {
payment.memo = Some(*m);
} else {
return Err(Zip321Error::TransparentMemo(i));
}
}
Param::Label(m) => payment.label = Some(m),
Param::Message(m) => payment.message = Some(m),
Param::Other(n, m) => payment.other_params.push((n, m)),
@ -577,40 +632,34 @@ mod parse {
}
/// Parses and consumes the leading "zcash:\[address\]" from a ZIP 321 URI.
pub fn lead_addr<P: consensus::Parameters>(
params: &P,
) -> impl Fn(&str) -> IResult<&str, Option<IndexedParam>> + '_ {
move |input: &str| {
map_opt(
preceded(tag("zcash:"), take_till(|c| c == '?')),
|addr_str: &str| {
if addr_str.is_empty() {
Some(None) // no address is ok, so wrap in `Some`
} else {
// `decode` returns `None` on error, which we want to
// then cause `map_opt` to fail.
Address::decode(params, addr_str).map(|a| {
pub fn lead_addr(input: &str) -> IResult<&str, Option<IndexedParam>> {
map_opt(
preceded(tag("zcash:"), take_till(|c| c == '?')),
|addr_str: &str| {
if addr_str.is_empty() {
Some(None) // no address is ok, so wrap in `Some`
} else {
// `try_from_encoded(..).ok()` returns `None` on error, which we want to then
// cause `map_opt` to fail.
ZcashAddress::try_from_encoded(addr_str)
.map(|a| {
Some(IndexedParam {
param: Param::Addr(Box::new(a)),
payment_index: 0,
})
})
}
},
)(input)
}
.ok()
}
},
)(input)
}
/// The primary parser for <name>=<value> query-string parameter pair.
pub fn zcashparam<P: consensus::Parameters>(
params: &P,
) -> impl Fn(&str) -> IResult<&str, IndexedParam> + '_ {
move |input| {
map_res(
separated_pair(indexed_name, char('='), recognize(qchars)),
move |r| to_indexed_param(params, r),
)(input)
}
pub fn zcashparam(input: &str) -> IResult<&str, IndexedParam> {
map_res(
separated_pair(indexed_name, char('='), recognize(qchars)),
to_indexed_param,
)(input)
}
/// Extension for the `alphanumeric0` parser which extends that parser
@ -652,7 +701,7 @@ mod parse {
}
/// Parses a value in decimal ZEC.
pub fn parse_amount(input: &str) -> IResult<&str, NonNegativeAmount> {
pub fn parse_amount(input: &str) -> IResult<&str, Zatoshis> {
map_res(
all_consuming(tuple((
digit1,
@ -678,28 +727,29 @@ mod parse {
.checked_mul(COIN)
.and_then(|coin_zats| coin_zats.checked_add(zats))
.ok_or(BalanceError::Overflow)
.and_then(NonNegativeAmount::from_u64)
.map_err(|_| format!("Not a valid amount: {} ZEC", input))
.and_then(Zatoshis::from_u64)
.map_err(|_| format!("Not a valid zat amount: {}.{}", coins, zats))
},
)(input)
}
fn to_indexed_param<'a, P: consensus::Parameters>(
params: &'a P,
fn to_indexed_param(
((name, iopt), value): ((&str, Option<&str>), &str),
) -> Result<IndexedParam, String> {
let param = match name {
"address" => Address::decode(params, value)
"address" => ZcashAddress::try_from_encoded(value)
.map(Box::new)
.map(Param::Addr)
.ok_or(format!(
"Could not interpret {} as a valid Zcash address.",
value
)),
.map_err(|err| {
format!(
"Could not interpret {} as a valid Zcash address: {}",
value, err
)
}),
"amount" => parse_amount(value)
.map_err(|e| e.to_string())
.map(|(_, amt)| Param::Amount(amt)),
.map(|(_, a)| Param::Amount(a)),
"label" => percent_decode(value.as_bytes())
.decode_utf8()
@ -712,6 +762,7 @@ mod parse {
.map_err(|e| e.to_string()),
"memo" => memo_from_base64(value)
.map(Box::new)
.map(Param::Memo)
.map_err(|e| format!("Decoded memo was invalid: {:?}", e)),
@ -743,25 +794,14 @@ pub mod testing {
use proptest::collection::vec;
use proptest::option;
use proptest::prelude::{any, prop_compose};
use zcash_keys::address::testing::arb_addr;
use zcash_keys::keys::UnifiedAddressRequest;
use zcash_primitives::{
consensus::TEST_NETWORK, transaction::components::amount::testing::arb_nonnegative_amount,
};
use crate::address::Address;
use zcash_address::testing::arb_address;
use zcash_protocol::{consensus::NetworkType, value::testing::arb_zatoshis};
use super::{MemoBytes, Payment, TransactionRequest};
pub const VALID_PARAMNAME: &str = "[a-zA-Z][a-zA-Z0-9+-]*";
#[cfg(feature = "transparent-inputs")]
const TRANSPARENT_INPUTS_ENABLED: bool = true;
#[cfg(not(feature = "transparent-inputs"))]
const TRANSPARENT_INPUTS_ENABLED: bool = false;
pub(crate) const UA_REQUEST: UnifiedAddressRequest =
UnifiedAddressRequest::unsafe_new(false, true, TRANSPARENT_INPUTS_ENABLED);
prop_compose! {
pub fn arb_valid_memo()(bytes in vec(any::<u8>(), 0..512)) -> MemoBytes {
MemoBytes::from_bytes(&bytes).unwrap()
@ -769,24 +809,20 @@ pub mod testing {
}
prop_compose! {
pub fn arb_zip321_payment()(
recipient_address in arb_addr(UA_REQUEST),
amount in arb_nonnegative_amount(),
pub fn arb_zip321_payment(network: NetworkType)(
recipient_address in arb_address(network),
amount in arb_zatoshis(),
memo in option::of(arb_valid_memo()),
message in option::of(any::<String>()),
label in option::of(any::<String>()),
// prevent duplicates by generating a set rather than a vec
other_params in btree_map(VALID_PARAMNAME, any::<String>(), 0..3),
) -> Payment {
let is_shielded = match recipient_address {
Address::Transparent(_) => false,
Address::Sapling(_) | Address::Unified(_) => true,
};
let memo = memo.filter(|_| recipient_address.can_receive_memo());
Payment {
recipient_address,
amount,
memo: memo.filter(|_| is_shielded),
memo,
label,
message,
other_params: other_params.into_iter().collect(),
@ -795,7 +831,9 @@ pub mod testing {
}
prop_compose! {
pub fn arb_zip321_request()(payments in btree_map(0usize..10000, arb_zip321_payment(), 1..10)) -> TransactionRequest {
pub fn arb_zip321_request(network: NetworkType)(
payments in btree_map(0usize..10000, arb_zip321_payment(network), 1..10)
) -> TransactionRequest {
let mut req = TransactionRequest::from_indexed(payments).unwrap();
req.normalize(); // just to make test comparisons easier
req
@ -803,7 +841,9 @@ pub mod testing {
}
prop_compose! {
pub fn arb_zip321_request_sequential()(payments in vec(arb_zip321_payment(), 1..10)) -> TransactionRequest {
pub fn arb_zip321_request_sequential(network: NetworkType)(
payments in vec(arb_zip321_payment(network), 1..10)
) -> TransactionRequest {
let mut req = TransactionRequest::new(payments).unwrap();
req.normalize(); // just to make test comparisons easier
req
@ -811,16 +851,16 @@ pub mod testing {
}
prop_compose! {
pub fn arb_zip321_uri()(req in arb_zip321_request()) -> String {
req.to_uri(&TEST_NETWORK)
pub fn arb_zip321_uri(network: NetworkType)(req in arb_zip321_request(network)) -> String {
req.to_uri()
}
}
prop_compose! {
pub fn arb_addr_str()(
recipient_address in arb_addr(UA_REQUEST)
pub fn arb_addr_str(network: NetworkType)(
recipient_address in arb_address(network)
) -> String {
recipient_address.encode(&TEST_NETWORK)
recipient_address.encode()
}
}
}
@ -830,29 +870,27 @@ mod tests {
use proptest::prelude::{any, proptest};
use std::str::FromStr;
use zcash_keys::address::testing::arb_addr;
use zcash_primitives::{
memo::Memo,
transaction::components::amount::{testing::arb_nonnegative_amount, NonNegativeAmount},
use zcash_address::{testing::arb_address, ZcashAddress};
use zcash_protocol::{
consensus::NetworkType,
memo::{Memo, MemoBytes},
value::{testing::arb_zatoshis, Zatoshis},
};
use zcash_protocol::consensus::{NetworkConstants, NetworkType, TEST_NETWORK};
#[cfg(feature = "local-consensus")]
use zcash_primitives::{local_consensus::LocalNetwork, BlockHeight};
use crate::{address::Address, encoding::decode_payment_address, zip321::testing::UA_REQUEST};
use zcash_protocol::{local_consensus::LocalNetwork, BlockHeight};
use super::{
memo_from_base64, memo_to_base64,
parse::{parse_amount, zcashparam, Param},
render::{amount_str, memo_param, str_param},
testing::{arb_addr_str, arb_valid_memo, arb_zip321_request, arb_zip321_uri},
MemoBytes, Payment, TransactionRequest,
Payment, TransactionRequest,
};
fn check_roundtrip(req: TransactionRequest) {
let req_uri = req.to_uri(&TEST_NETWORK);
let parsed = TransactionRequest::from_uri(&TEST_NETWORK, &req_uri).unwrap();
let req_uri = req.to_uri();
let parsed = TransactionRequest::from_uri(&req_uri).unwrap();
assert_eq!(parsed, req);
}
@ -861,7 +899,7 @@ mod tests {
let amounts = vec![1u64, 1000u64, 100000u64, 100000000u64, 100000000000u64];
for amt_u64 in amounts {
let amt = NonNegativeAmount::from_u64(amt_u64).unwrap();
let amt = Zatoshis::const_from_u64(amt_u64);
let amt_str = amount_str(amt);
assert_eq!(amt, parse_amount(&amt_str).unwrap().1);
}
@ -871,20 +909,20 @@ mod tests {
fn test_zip321_parse_empty_message() {
let fragment = "message=";
let result = zcashparam(&TEST_NETWORK)(fragment).unwrap().1.param;
let result = zcashparam(fragment).unwrap().1.param;
assert_eq!(result, Param::Message("".to_string()));
}
#[test]
fn test_zip321_parse_simple() {
let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k?amount=3768769.02796286&message=";
let parse_result = TransactionRequest::from_uri(&TEST_NETWORK, uri).unwrap();
let parse_result = TransactionRequest::from_uri(uri).unwrap();
let expected = TransactionRequest::new(
vec![
Payment {
recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
amount: NonNegativeAmount::const_from_u64(376876902796286),
recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(),
amount: Zatoshis::const_from_u64(376876902796286),
memo: None,
label: None,
message: Some("".to_string()),
@ -899,13 +937,13 @@ mod tests {
#[test]
fn test_zip321_parse_no_query_params() {
let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k";
let parse_result = TransactionRequest::from_uri(&TEST_NETWORK, uri).unwrap();
let parse_result = TransactionRequest::from_uri(uri).unwrap();
let expected = TransactionRequest::new(
vec![
Payment {
recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
amount: NonNegativeAmount::ZERO,
recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(),
amount: Zatoshis::ZERO,
memo: None,
label: None,
message: None,
@ -922,8 +960,8 @@ mod tests {
let req = TransactionRequest::new(
vec![
Payment {
recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
amount: NonNegativeAmount::ZERO,
recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(),
amount: Zatoshis::ZERO,
memo: None,
label: None,
message: Some("".to_string()),
@ -957,48 +995,48 @@ mod tests {
#[test]
fn test_zip321_spec_valid_examples() {
let valid_0 = "zcash:";
let v0r = TransactionRequest::from_uri(&TEST_NETWORK, valid_0).unwrap();
let v0r = TransactionRequest::from_uri(valid_0).unwrap();
assert!(v0r.payments.is_empty());
let valid_0 = "zcash:?";
let v0r = TransactionRequest::from_uri(&TEST_NETWORK, valid_0).unwrap();
let v0r = TransactionRequest::from_uri(valid_0).unwrap();
assert!(v0r.payments.is_empty());
let valid_1 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase";
let v1r = TransactionRequest::from_uri(&TEST_NETWORK, valid_1).unwrap();
let v1r = TransactionRequest::from_uri(valid_1).unwrap();
assert_eq!(
v1r.payments.get(&0).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(100000000))
Some(Zatoshis::const_from_u64(100000000))
);
let valid_2 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok";
let mut v2r = TransactionRequest::from_uri(&TEST_NETWORK, valid_2).unwrap();
let mut v2r = TransactionRequest::from_uri(valid_2).unwrap();
v2r.normalize();
assert_eq!(
v2r.payments.get(&0).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(12345600000))
Some(Zatoshis::const_from_u64(12345600000))
);
assert_eq!(
v2r.payments.get(&1).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(78900000))
Some(Zatoshis::const_from_u64(78900000))
);
// valid; amount just less than MAX_MONEY
// 20999999.99999999
let valid_3 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=20999999.99999999";
let v3r = TransactionRequest::from_uri(&TEST_NETWORK, valid_3).unwrap();
let v3r = TransactionRequest::from_uri(valid_3).unwrap();
assert_eq!(
v3r.payments.get(&0).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(2099999999999999u64))
Some(Zatoshis::const_from_u64(2099999999999999))
);
// valid; MAX_MONEY
// 21000000
let valid_4 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000";
let v4r = TransactionRequest::from_uri(&TEST_NETWORK, valid_4).unwrap();
let v4r = TransactionRequest::from_uri(valid_4).unwrap();
assert_eq!(
v4r.payments.get(&0).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(2100000000000000u64))
Some(Zatoshis::const_from_u64(2100000000000000))
);
}
@ -1019,7 +1057,7 @@ mod tests {
let v1r = TransactionRequest::from_uri(&params, valid_1).unwrap();
assert_eq!(
v1r.payments.get(&0).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(100000000))
Some(Zatoshis::const_from_u64(100000000))
);
}
@ -1027,91 +1065,91 @@ mod tests {
fn test_zip321_spec_invalid_examples() {
// invalid; empty string
let invalid_0 = "";
let i0r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_0);
let i0r = TransactionRequest::from_uri(invalid_0);
assert!(i0r.is_err());
// invalid; missing `address=`
let invalid_1 = "zcash:?amount=3491405.05201255&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=5740296.87793245";
let i1r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_1);
let i1r = TransactionRequest::from_uri(invalid_1);
assert!(i1r.is_err());
// invalid; missing `address.1=`
let invalid_2 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=1&amount.1=2&address.2=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez";
let i2r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_2);
let i2r = TransactionRequest::from_uri(invalid_2);
assert!(i2r.is_err());
// invalid; `address.0=` and `amount.0=` are not permitted (leading 0s).
let invalid_3 = "zcash:?address.0=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.0=2";
let i3r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_3);
let i3r = TransactionRequest::from_uri(invalid_3);
assert!(i3r.is_err());
// invalid; duplicate `amount=` field
let invalid_4 =
"zcash:?amount=1.234&amount=2.345&address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
let i4r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_4);
let i4r = TransactionRequest::from_uri(invalid_4);
assert!(i4r.is_err());
// invalid; duplicate `amount.1=` field
let invalid_5 =
"zcash:?amount.1=1.234&amount.1=2.345&address.1=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
let i5r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_5);
let i5r = TransactionRequest::from_uri(invalid_5);
assert!(i5r.is_err());
//invalid; memo associated with t-addr
let invalid_6 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&memo=eyAia2V5IjogIlRoaXMgaXMgYSBKU09OLXN0cnVjdHVyZWQgbWVtby4iIH0&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok";
let i6r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_6);
let i6r = TransactionRequest::from_uri(invalid_6);
assert!(i6r.is_err());
// invalid; amount component exceeds an i64
// 9223372036854775808 = i64::MAX + 1
let invalid_7 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=9223372036854775808";
let i7r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_7);
let i7r = TransactionRequest::from_uri(invalid_7);
assert!(i7r.is_err());
// invalid; amount component wraps into a valid small positive i64
// 18446744073709551624
let invalid_7a = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=18446744073709551624";
let i7ar = TransactionRequest::from_uri(&TEST_NETWORK, invalid_7a);
let i7ar = TransactionRequest::from_uri(invalid_7a);
assert!(i7ar.is_err());
// invalid; amount component is MAX_MONEY
// 21000000.00000001
let invalid_8 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000.00000001";
let i8r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_8);
let i8r = TransactionRequest::from_uri(invalid_8);
assert!(i8r.is_err());
// invalid; negative amount
let invalid_9 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=-1";
let i9r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_9);
let i9r = TransactionRequest::from_uri(invalid_9);
assert!(i9r.is_err());
// invalid; parameter index too large
let invalid_10 =
"zcash:?amount.10000=1.23&address.10000=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
let i10r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_10);
let i10r = TransactionRequest::from_uri(invalid_10);
assert!(i10r.is_err());
// invalid: bad amount format
let invalid_11 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.";
let i11r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_11);
let i11r = TransactionRequest::from_uri(invalid_11);
assert!(i11r.is_err());
}
proptest! {
#[test]
fn prop_zip321_roundtrip_address(addr in arb_addr(UA_REQUEST)) {
let a = addr.encode(&TEST_NETWORK);
assert_eq!(Address::decode(&TEST_NETWORK, &a), Some(addr));
fn prop_zip321_roundtrip_address(addr in arb_address(NetworkType::Test)) {
let a = addr.encode();
assert_eq!(ZcashAddress::try_from_encoded(&a), Ok(addr));
}
#[test]
fn prop_zip321_roundtrip_address_str(a in arb_addr_str()) {
let addr = Address::decode(&TEST_NETWORK, &a).unwrap();
assert_eq!(addr.encode(&TEST_NETWORK), a);
fn prop_zip321_roundtrip_address_str(a in arb_addr_str(NetworkType::Test)) {
let addr = ZcashAddress::try_from_encoded(&a).unwrap();
assert_eq!(addr.encode(), a);
}
#[test]
fn prop_zip321_roundtrip_amount(amt in arb_nonnegative_amount()) {
fn prop_zip321_roundtrip_amount(amt in arb_zatoshis()) {
let amt_str = amount_str(amt);
assert_eq!(amt, parse_amount(&amt_str).unwrap().1);
}
@ -1120,7 +1158,7 @@ mod tests {
fn prop_zip321_roundtrip_str_param(
message in any::<String>(), i in proptest::option::of(0usize..2000)) {
let fragment = str_param("message", &message, i);
let (rest, iparam) = zcashparam(&TEST_NETWORK)(&fragment).unwrap();
let (rest, iparam) = zcashparam(&fragment).unwrap();
assert_eq!(rest, "");
assert_eq!(iparam.param, Param::Message(message));
assert_eq!(iparam.payment_index, i.unwrap_or(0));
@ -1130,24 +1168,24 @@ mod tests {
fn prop_zip321_roundtrip_memo_param(
memo in arb_valid_memo(), i in proptest::option::of(0usize..2000)) {
let fragment = memo_param(&memo, i);
let (rest, iparam) = zcashparam(&TEST_NETWORK)(&fragment).unwrap();
let (rest, iparam) = zcashparam(&fragment).unwrap();
assert_eq!(rest, "");
assert_eq!(iparam.param, Param::Memo(memo));
assert_eq!(iparam.param, Param::Memo(Box::new(memo)));
assert_eq!(iparam.payment_index, i.unwrap_or(0));
}
#[test]
fn prop_zip321_roundtrip_request(mut req in arb_zip321_request()) {
let req_uri = req.to_uri(&TEST_NETWORK);
let mut parsed = TransactionRequest::from_uri(&TEST_NETWORK, &req_uri).unwrap();
fn prop_zip321_roundtrip_request(mut req in arb_zip321_request(NetworkType::Test)) {
let req_uri = req.to_uri();
let mut parsed = TransactionRequest::from_uri(&req_uri).unwrap();
assert!(TransactionRequest::normalize_and_eq(&mut parsed, &mut req));
}
#[test]
fn prop_zip321_roundtrip_uri(uri in arb_zip321_uri()) {
let mut parsed = TransactionRequest::from_uri(&TEST_NETWORK, &uri).unwrap();
fn prop_zip321_roundtrip_uri(uri in arb_zip321_uri(NetworkType::Test)) {
let mut parsed = TransactionRequest::from_uri(&uri).unwrap();
parsed.normalize();
let serialized = parsed.to_uri(&TEST_NETWORK);
let serialized = parsed.to_uri();
assert_eq!(serialized, uri)
}
}

View File

@ -14,6 +14,22 @@ and this library adheres to Rust's notion of
- `testing` module
- `zcash_client_backend::sync` module, behind the `sync` feature flag.
### Changed
- `zcash_client_backend::zip321` has been extracted to, and is now a reexport
of the root module of the `zip321` crate. Several of the APIs of this module
have changed as a consequence of this extraction; please see the `zip321`
CHANGELOG for details.
- `zcash_client_backend::data_api`:
- `error::Error` has a new `Address` variant.
- `wallet::input_selection::InputSelectorError` has a new `Address` variant.
- `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
### Fixed
@ -117,6 +133,9 @@ and this library adheres to Rust's notion of
- Arguments to `ChangeStrategy::compute_balance` have changed.
- `ChangeError::DustInputs` now has an `orchard` field behind the `orchard`
feature flag.
- `zcash_client_backend::wallet`:
- The address variants of `Recipient` now `Box` their contents to avoid large
discrepancies in enum variant sizing.
- `zcash_client_backend::proto`:
- `ProposalDecodingError` has a new variant `TransparentMemo`.
- `zcash_client_backend::wallet::Recipient::InternalAccount` is now a structured

View File

@ -41,6 +41,7 @@ zcash_note_encryption.workspace = true
zcash_primitives.workspace = true
zcash_protocol.workspace = true
zip32.workspace = true
zip321.workspace = true
# Dependencies exposed in a public API:
# (Breaking upgrades to these require a breaking upgrade to this crate.)

View File

@ -99,7 +99,7 @@ use {
zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint},
};
#[cfg(feature = "test-dependencies")]
#[cfg(any(test, feature = "test-dependencies"))]
use zcash_primitives::consensus::NetworkUpgrade;
pub mod chain;
@ -1334,7 +1334,7 @@ impl AccountBirthday {
///
/// This API is intended primarily to be used in testing contexts; under normal circumstances,
/// [`AccountBirthday::from_treestate`] should be used instead.
#[cfg(feature = "test-dependencies")]
#[cfg(any(test, feature = "test-dependencies"))]
pub fn from_parts(prior_chain_state: ChainState, recover_until: Option<BlockHeight>) -> Self {
Self {
prior_chain_state,

View File

@ -4,6 +4,7 @@ use std::error;
use std::fmt::{self, Debug, Display};
use shardtree::error::ShardTreeError;
use zcash_address::ConversionError;
use zcash_primitives::transaction::components::amount::NonNegativeAmount;
use zcash_primitives::transaction::{
builder,
@ -81,6 +82,9 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
/// full viewing key for an account.
NoteMismatch(NoteId),
/// An error occurred parsing the address from a payment request.
Address(ConversionError<&'static str>),
#[cfg(feature = "transparent-inputs")]
AddressNotRecognized(TransparentAddress),
}
@ -145,6 +149,9 @@ where
Error::NoSpendingKey(addr) => write!(f, "No spending key available for address: {}", addr),
Error::NoteMismatch(n) => write!(f, "A note being spent ({:?}) does not correspond to either the internal or external full viewing key for the provided spending key.", n),
Error::Address(e) => {
write!(f, "An error occurred decoding the address from a payment request: {}.", e)
}
#[cfg(feature = "transparent-inputs")]
Error::AddressNotRecognized(_) => {
write!(f, "The specified transparent address was not recognized as belonging to the wallet.")
@ -184,6 +191,12 @@ impl<DE, CE, SE, FE> From<BalanceError> for Error<DE, CE, SE, FE> {
}
}
impl<DE, CE, SE, FE> From<ConversionError<&'static str>> for Error<DE, CE, SE, FE> {
fn from(value: ConversionError<&'static str>) -> Self {
Error::Address(value)
}
}
impl<DE, CE, SE, FE> From<InputSelectorError<DE, SE>> for Error<DE, CE, SE, FE> {
fn from(e: InputSelectorError<DE, SE>) -> Self {
match e {
@ -198,6 +211,7 @@ impl<DE, CE, SE, FE> From<InputSelectorError<DE, SE>> for Error<DE, CE, SE, FE>
required,
},
InputSelectorError::SyncRequired => Error::ScanRequired,
InputSelectorError::Address(e) => Error::Address(e),
}
}
}

View File

@ -497,14 +497,15 @@ where
>,
DbT::NoteRef: Copy + Eq + Ord,
{
let request = zip321::TransactionRequest::new(vec![Payment {
recipient_address: to.clone(),
let request = zip321::TransactionRequest::new(vec![Payment::new(
to.to_zcash_address(params),
amount,
memo,
label: None,
message: None,
other_params: vec![],
}])
None,
None,
vec![],
)
.ok_or(Error::MemoForbidden)?])
.expect(
"It should not be possible for this to violate ZIP 321 request construction invariants.",
);
@ -848,14 +849,17 @@ where
// the transaction in payment index order, so we can use dead reckoning to
// figure out which output it ended up being.
let (prior_step, result) = &prior_step_results[input_ref.step_index()];
let recipient_address = match &prior_step
let recipient_address = &prior_step
.transaction_request()
.payments()
.get(&i)
.expect("Payment step references are checked at construction")
.recipient_address
{
Address::Transparent(t) => Some(t),
.recipient_address()
.clone()
.convert_if_network(params.network_type())?;
let recipient_taddr = match recipient_address {
Address::Transparent(t) => Some(t.as_ref()),
Address::Unified(uaddr) => uaddr.transparent(),
_ => None,
}
@ -879,7 +883,7 @@ where
.ok_or(Error::Proposal(ProposalError::ReferenceError(*input_ref)))?
.vout[outpoint.n() as usize];
add_transparent_input(recipient_address, outpoint, utxo.clone())?;
add_transparent_input(recipient_taddr, outpoint, utxo.clone())?;
}
proposal::StepOutputIndex::Change(_) => unreachable!(),
}
@ -953,12 +957,14 @@ where
(payment, output_pool)
})
{
match &payment.recipient_address {
let recipient_address: Address = payment
.recipient_address()
.clone()
.convert_if_network(params.network_type())?;
match recipient_address {
Address::Unified(ua) => {
let memo = payment
.memo
.as_ref()
.map_or_else(MemoBytes::empty, |m| m.clone());
let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
match output_pool {
#[cfg(not(feature = "orchard"))]
@ -970,15 +976,15 @@ where
builder.add_orchard_output(
orchard_external_ovk.clone(),
*ua.orchard().expect("The mapping between payment pool and receiver is checked in step construction"),
payment.amount.into(),
payment.amount().into(),
memo.clone(),
)?;
orchard_output_meta.push((
Recipient::Unified(
ua.clone(),
Recipient::External(
payment.recipient_address().clone(),
PoolType::Shielded(ShieldedProtocol::Orchard),
),
payment.amount,
payment.amount(),
Some(memo),
));
}
@ -987,51 +993,56 @@ where
builder.add_sapling_output(
sapling_external_ovk,
*ua.sapling().expect("The mapping between payment pool and receiver is checked in step construction"),
payment.amount,
payment.amount(),
memo.clone(),
)?;
sapling_output_meta.push((
Recipient::Unified(
ua.clone(),
Recipient::External(
payment.recipient_address().clone(),
PoolType::Shielded(ShieldedProtocol::Sapling),
),
payment.amount,
payment.amount(),
Some(memo),
));
}
PoolType::Transparent => {
if payment.memo.is_some() {
if payment.memo().is_some() {
return Err(Error::MemoForbidden);
} else {
builder.add_transparent_output(
ua.transparent().expect("The mapping between payment pool and receiver is checked in step construction."),
payment.amount
payment.amount()
)?;
}
}
}
}
Address::Sapling(addr) => {
let memo = payment
.memo
.as_ref()
.map_or_else(MemoBytes::empty, |m| m.clone());
let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
builder.add_sapling_output(
sapling_external_ovk,
*addr,
payment.amount,
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() {
if payment.memo().is_some() {
return Err(Error::MemoForbidden);
} else {
builder.add_transparent_output(to, payment.amount)?;
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(),
));
}
}
}
@ -1153,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")]

View File

@ -8,6 +8,7 @@ use std::{
};
use nonempty::NonEmpty;
use zcash_address::{ConversionError, ZcashAddress};
use zcash_primitives::{
consensus::{self, BlockHeight},
transaction::{
@ -20,7 +21,7 @@ use zcash_primitives::{
};
use crate::{
address::{Address, UnifiedAddress},
address::Address,
data_api::{InputSource, SimpleNoteRetention, SpendableNotes},
fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy},
proposal::{Proposal, ProposalError, ShieldedInputs},
@ -48,6 +49,8 @@ pub enum InputSelectorError<DbErrT, SelectorErrT> {
Selection(SelectorErrT),
/// Input selection attempted to generate an invalid transaction proposal.
Proposal(ProposalError),
/// An error occurred parsing the address from a payment request.
Address(ConversionError<&'static str>),
/// Insufficient funds were available to satisfy the payment request that inputs were being
/// selected to attempt to satisfy.
InsufficientFunds {
@ -59,6 +62,12 @@ pub enum InputSelectorError<DbErrT, SelectorErrT> {
SyncRequired,
}
impl<E, S> From<ConversionError<&'static str>> for InputSelectorError<E, S> {
fn from(value: ConversionError<&'static str>) -> Self {
InputSelectorError::Address(value)
}
}
impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE, SE> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self {
@ -79,6 +88,13 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
e
)
}
InputSelectorError::Address(e) => {
write!(
f,
"An error occurred decoding the address from a payment request: {}.",
e
)
}
InputSelectorError::InsufficientFunds {
available,
required,
@ -205,7 +221,7 @@ pub enum GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT> {
/// An intermediate value overflowed or underflowed the valid monetary range.
Balance(BalanceError),
/// A unified address did not contain a supported receiver.
UnsupportedAddress(Box<UnifiedAddress>),
UnsupportedAddress(ZcashAddress),
/// An error was encountered in change selection.
Change(ChangeError<ChangeStrategyErrT, NoteRefT>),
}
@ -218,10 +234,12 @@ impl<CE: fmt::Display, N: fmt::Display> fmt::Display for GreedyInputSelectorErro
"A balance calculation violated amount validity bounds: {:?}.",
e
),
GreedyInputSelectorError::UnsupportedAddress(_) => {
// we can't encode the UA to its string representation because we
// don't have network parameters here
write!(f, "Unified address contains no supported receivers.")
GreedyInputSelectorError::UnsupportedAddress(addr) => {
write!(
f,
"Unified address {} contains no supported receivers.",
addr.encode()
)
}
GreedyInputSelectorError::Change(err) => {
write!(f, "An error occurred computing change and fees: {}", err)
@ -344,43 +362,50 @@ where
let mut orchard_outputs = vec![];
let mut payment_pools = BTreeMap::new();
for (idx, payment) in transaction_request.payments() {
match &payment.recipient_address {
let recipient_address: Address = payment
.recipient_address()
.clone()
.convert_if_network(params.network_type())?;
match recipient_address {
Address::Transparent(addr) => {
payment_pools.insert(*idx, PoolType::Transparent);
transparent_outputs.push(TxOut {
value: payment.amount,
value: payment.amount(),
script_pubkey: addr.script(),
});
}
Address::Sapling(_) => {
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling));
sapling_outputs.push(SaplingPayment(payment.amount));
sapling_outputs.push(SaplingPayment(payment.amount()));
}
Address::Unified(addr) => {
#[cfg(feature = "orchard")]
if addr.orchard().is_some() {
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Orchard));
orchard_outputs.push(OrchardPayment(payment.amount));
orchard_outputs.push(OrchardPayment(payment.amount()));
continue;
}
if addr.sapling().is_some() {
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling));
sapling_outputs.push(SaplingPayment(payment.amount));
sapling_outputs.push(SaplingPayment(payment.amount()));
continue;
}
if let Some(addr) = addr.transparent() {
payment_pools.insert(*idx, PoolType::Transparent);
transparent_outputs.push(TxOut {
value: payment.amount,
value: payment.amount(),
script_pubkey: addr.script(),
});
continue;
}
return Err(InputSelectorError::Selection(
GreedyInputSelectorError::UnsupportedAddress(Box::new(addr.clone())),
GreedyInputSelectorError::UnsupportedAddress(
payment.recipient_address().clone(),
),
));
}
}

View File

@ -72,7 +72,7 @@ pub mod proto;
pub mod scan;
pub mod scanning;
pub mod wallet;
pub mod zip321;
pub use zip321;
#[cfg(feature = "sync")]
pub mod sync;

View File

@ -377,7 +377,7 @@ impl<NoteRef> Step<NoteRef> {
.payments()
.get(idx)
.iter()
.any(|payment| payment.recipient_address.has_receiver(*pool))
.any(|payment| payment.recipient_address().can_receive_as(*pool))
{
return Err(ProposalError::PaymentPoolsMismatch);
}
@ -404,13 +404,12 @@ impl<NoteRef> Step<NoteRef> {
.get(s_ref.step_index)
.ok_or(ProposalError::ReferenceError(*s_ref))?;
Ok(match s_ref.output_index {
StepOutputIndex::Payment(i) => {
step.transaction_request
.payments()
.get(&i)
.ok_or(ProposalError::ReferenceError(*s_ref))?
.amount
}
StepOutputIndex::Payment(i) => step
.transaction_request
.payments()
.get(&i)
.ok_or(ProposalError::ReferenceError(*s_ref))?
.amount(),
StepOutputIndex::Change(i) => step
.balance
.proposed_change()

View File

@ -13,7 +13,7 @@ use sapling::{self, note::ExtractedNoteCommitment, Node};
use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE};
use zcash_primitives::{
block::{BlockHash, BlockHeader},
consensus::{self, BlockHeight, Parameters},
consensus::BlockHeight,
memo::{self, MemoBytes},
merkle_tree::read_commitment_tree,
transaction::{components::amount::NonNegativeAmount, fees::StandardFeeRule, TxId},
@ -485,17 +485,14 @@ impl From<ShieldedProtocol> for proposal::ValuePool {
impl proposal::Proposal {
/// Serializes a [`Proposal`] based upon a supported [`StandardFeeRule`] to its protobuf
/// representation.
pub fn from_standard_proposal<P: Parameters, NoteRef>(
params: &P,
value: &Proposal<StandardFeeRule, NoteRef>,
) -> Self {
pub fn from_standard_proposal<NoteRef>(value: &Proposal<StandardFeeRule, NoteRef>) -> Self {
use proposal::proposed_input;
use proposal::{PriorStepChange, PriorStepOutput, ReceivedOutput};
let steps = value
.steps()
.iter()
.map(|step| {
let transaction_request = step.transaction_request().to_uri(params);
let transaction_request = step.transaction_request().to_uri();
let anchor_height = step
.shielded_inputs()
@ -607,9 +604,8 @@ impl proposal::Proposal {
/// Attempts to parse a [`Proposal`] based upon a supported [`StandardFeeRule`] from its
/// protobuf representation.
pub fn try_into_standard_proposal<P: consensus::Parameters, DbT, DbError>(
pub fn try_into_standard_proposal<DbT, DbError>(
&self,
params: &P,
wallet_db: &DbT,
) -> Result<Proposal<StandardFeeRule, DbT::NoteRef>, ProposalDecodingError<DbError>>
where
@ -631,7 +627,7 @@ impl proposal::Proposal {
let mut steps = Vec::with_capacity(self.steps.len());
for step in &self.steps {
let transaction_request =
TransactionRequest::from_uri(params, &step.transaction_request)?;
TransactionRequest::from_uri(&step.transaction_request)?;
let payment_pools = step
.payment_output_pools

View File

@ -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<AccountId, N> {
Transparent(TransparentAddress),
Sapling(sapling::PaymentAddress),
Unified(UnifiedAddress, PoolType),
External(ZcashAddress, PoolType),
InternalAccount {
receiving_account: AccountId,
external_address: Option<Address>,
external_address: Option<ZcashAddress>,
note: N,
},
}
@ -81,9 +79,7 @@ pub enum Recipient<AccountId, N> {
impl<AccountId, N> Recipient<AccountId, N> {
pub fn map_internal_account_note<B, F: FnOnce(N) -> B>(self, f: F) -> Recipient<AccountId, B> {
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<AccountId, N> Recipient<AccountId, N> {
impl<AccountId, N> Recipient<AccountId, Option<N>> {
pub fn internal_account_note_transpose_option(self) -> Option<Recipient<AccountId, N>> {
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,

View File

@ -80,6 +80,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

View File

@ -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<std::io::Error> for SqliteClientError {
SqliteClientError::Io(e)
}
}
impl From<Bech32DecodeError> for SqliteClientError {
fn from(e: Bech32DecodeError) -> Self {
SqliteClientError::Bech32DecodeError(e)
impl From<ParseError> for SqliteClientError {
fn from(e: ParseError) -> Self {
SqliteClientError::DecodingError(e)
}
}
@ -195,6 +196,7 @@ impl From<hdwallet::error::Error> for SqliteClientError {
}
}
#[cfg(feature = "transparent-inputs")]
impl From<TransparentCodecError> for SqliteClientError {
fn from(e: TransparentCodecError) -> Self {
SqliteClientError::TransparentAddress(e)

View File

@ -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},
@ -133,7 +133,7 @@ pub(crate) const UA_TRANSPARENT: bool = false;
pub(crate) const UA_TRANSPARENT: bool = true;
pub(crate) const DEFAULT_UA_REQUEST: UnifiedAddressRequest =
UnifiedAddressRequest::unsafe_new(UA_ORCHARD, true, UA_TRANSPARENT);
UnifiedAddressRequest::unsafe_new_without_expiry(UA_ORCHARD, true, UA_TRANSPARENT);
/// The ID type for accounts.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
@ -1063,11 +1063,22 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
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<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
wallet::put_sent_output(
wdb.conn.0,
&wdb.params,
*output.account(),
tx_ref,
output.index(),
@ -1102,14 +1112,22 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
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,21 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
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 +1178,6 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
wallet::put_sent_output(
wdb.conn.0,
&wdb.params,
*output.account(),
tx_ref,
output.index(),
@ -1175,19 +1193,22 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
// 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 +1261,29 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
.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 +1342,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
}
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 +1911,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)

View File

@ -1682,7 +1682,7 @@ fn fake_compact_block_spending<P: consensus::Parameters, Fvk: TestFvk>(
compact_sapling_output(
params,
height,
recipient,
*recipient,
value,
fvk.sapling_ovk(),
&mut rng,
@ -1896,7 +1896,7 @@ fn check_proposal_serialization_roundtrip(
db_data: &WalletDb<rusqlite::Connection, LocalNetwork>,
proposal: &Proposal<StandardFeeRule, ReceivedNoteId>,
) {
let proposal_proto = proposal::Proposal::from_standard_proposal(&db_data.params, proposal);
let deserialized_proposal = proposal_proto.try_into_standard_proposal(&db_data.params, db_data);
let proposal_proto = proposal::Proposal::from_standard_proposal(proposal);
let deserialized_proposal = proposal_proto.try_into_standard_proposal(db_data);
assert_matches!(deserialized_proposal, Ok(r) if &r == proposal);
}

View File

@ -168,14 +168,10 @@ pub(crate) fn send_single_step_proposed_transfer<T: ShieldedPoolTester>() {
let to_extsk = T::sk(&[0xf5; 32]);
let to: Address = T::sk_default_address(&to_extsk);
let request = zip321::TransactionRequest::new(vec![Payment {
recipient_address: to,
amount: NonNegativeAmount::const_from_u64(10000),
memo: None, // this should result in the creation of an empty memo
label: None,
message: None,
other_params: vec![],
}])
let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
to.to_zcash_address(&st.network()),
NonNegativeAmount::const_from_u64(10000),
)])
.unwrap();
// TODO: This test was originally written to use the pre-zip-313 fee rule
@ -336,15 +332,11 @@ pub(crate) fn send_multi_step_proposed_transfer<T: ShieldedPoolTester>() {
// spends the first step's output.
// The first step will deshield to the wallet's default transparent address
let to0 = Address::Transparent(account.usk().default_transparent_address().0);
let request0 = zip321::TransactionRequest::new(vec![Payment {
recipient_address: to0,
amount: NonNegativeAmount::const_from_u64(50000),
memo: None,
label: None,
message: None,
other_params: vec![],
}])
let to0 = Address::from(account.usk().default_transparent_address().0);
let request0 = zip321::TransactionRequest::new(vec![Payment::without_memo(
to0.to_zcash_address(&st.network()),
NonNegativeAmount::const_from_u64(50000),
)])
.unwrap();
let fee_rule = StandardFeeRule::Zip317;
@ -372,7 +364,7 @@ pub(crate) fn send_multi_step_proposed_transfer<T: ShieldedPoolTester>() {
// We'll use an internal transparent address that hasn't been added to the wallet
// to simulate an external transparent recipient.
let to1 = Address::Transparent(
let to1 = Address::from(
account
.usk()
.transparent()
@ -382,14 +374,10 @@ pub(crate) fn send_multi_step_proposed_transfer<T: ShieldedPoolTester>() {
.default_address()
.0,
);
let request1 = zip321::TransactionRequest::new(vec![Payment {
recipient_address: to1,
amount: NonNegativeAmount::const_from_u64(40000),
memo: None,
label: None,
message: None,
other_params: vec![],
}])
let request1 = zip321::TransactionRequest::new(vec![Payment::without_memo(
to1.to_zcash_address(&st.network()),
NonNegativeAmount::const_from_u64(40000),
)])
.unwrap();
let step1 = Step::from_parts(
@ -1042,23 +1030,9 @@ pub(crate) fn external_address_change_spends_detected_in_restore_from_seed<
let addr2 = T::fvk_default_address(&dfvk2);
let req = TransactionRequest::new(vec![
// payment to an external recipient
Payment {
recipient_address: addr2,
amount: amount_sent,
memo: None,
label: None,
message: None,
other_params: vec![],
},
Payment::without_memo(addr2.to_zcash_address(&st.network()), amount_sent),
// payment back to the originating wallet, simulating legacy change
Payment {
recipient_address: addr,
amount: amount_legacy_change,
memo: None,
label: None,
message: None,
other_params: vec![],
},
Payment::without_memo(addr.to_zcash_address(&st.network()), amount_legacy_change),
])
.unwrap();
@ -1151,14 +1125,10 @@ pub(crate) fn zip317_spend<T: ShieldedPoolTester>() {
let input_selector = input_selector(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
// This first request will fail due to insufficient non-dust funds
let req = TransactionRequest::new(vec![Payment {
recipient_address: T::fvk_default_address(&dfvk),
amount: NonNegativeAmount::const_from_u64(50000),
memo: None,
label: None,
message: None,
other_params: vec![],
}])
let req = TransactionRequest::new(vec![Payment::without_memo(
T::fvk_default_address(&dfvk).to_zcash_address(&st.network()),
NonNegativeAmount::const_from_u64(50000),
)])
.unwrap();
assert_matches!(
@ -1176,14 +1146,10 @@ pub(crate) fn zip317_spend<T: ShieldedPoolTester>() {
// This request will succeed, spending a single dust input to pay the 10000
// ZAT fee in addition to the 41000 ZAT output to the recipient
let req = TransactionRequest::new(vec![Payment {
recipient_address: T::fvk_default_address(&dfvk),
amount: NonNegativeAmount::const_from_u64(41000),
memo: None,
label: None,
message: None,
other_params: vec![],
}])
let req = TransactionRequest::new(vec![Payment::without_memo(
T::fvk_default_address(&dfvk).to_zcash_address(&st.network()),
NonNegativeAmount::const_from_u64(41000),
)])
.unwrap();
let txid = st
@ -1479,14 +1445,10 @@ pub(crate) fn pool_crossing_required<P0: ShieldedPoolTester, P1: ShieldedPoolTes
);
let transfer_amount = NonNegativeAmount::const_from_u64(200000);
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment {
recipient_address: p1_to,
amount: transfer_amount,
memo: None,
label: None,
message: None,
other_params: vec![],
}])
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment::without_memo(
p1_to.to_zcash_address(&st.network()),
transfer_amount,
)])
.unwrap();
let fee_rule = StandardFeeRule::Zip317;
@ -1570,14 +1532,10 @@ pub(crate) fn fully_funded_fully_private<P0: ShieldedPoolTester, P1: ShieldedPoo
);
let transfer_amount = NonNegativeAmount::const_from_u64(200000);
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment {
recipient_address: p1_to,
amount: transfer_amount,
memo: None,
label: None,
message: None,
other_params: vec![],
}])
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment::without_memo(
p1_to.to_zcash_address(&st.network()),
transfer_amount,
)])
.unwrap();
let fee_rule = StandardFeeRule::Zip317;
@ -1661,14 +1619,10 @@ pub(crate) fn fully_funded_send_to_t<P0: ShieldedPoolTester, P1: ShieldedPoolTes
);
let transfer_amount = NonNegativeAmount::const_from_u64(200000);
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment {
recipient_address: Address::Transparent(p1_to),
amount: transfer_amount,
memo: None,
label: None,
message: None,
other_params: vec![],
}])
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment::without_memo(
Address::Transparent(Box::new(p1_to)).to_zcash_address(&st.network()),
transfer_amount,
)])
.unwrap();
let fee_rule = StandardFeeRule::Zip317;
@ -1777,7 +1731,7 @@ pub(crate) fn multi_pool_checkpoint<P0: ShieldedPoolTester, P1: ShieldedPoolTest
// First, send funds just to P0
let transfer_amount = NonNegativeAmount::const_from_u64(200000);
let p0_transfer = zip321::TransactionRequest::new(vec![Payment::without_memo(
P0::random_address(&mut st.rng),
P0::random_address(&mut st.rng).to_zcash_address(&st.network()),
transfer_amount,
)])
.unwrap();
@ -1802,8 +1756,14 @@ pub(crate) fn multi_pool_checkpoint<P0: ShieldedPoolTester, P1: ShieldedPoolTest
// In the next block, send funds to both P0 and P1
let both_transfer = zip321::TransactionRequest::new(vec![
Payment::without_memo(P0::random_address(&mut st.rng), transfer_amount),
Payment::without_memo(P1::random_address(&mut st.rng), transfer_amount),
Payment::without_memo(
P0::random_address(&mut st.rng).to_zcash_address(&st.network()),
transfer_amount,
),
Payment::without_memo(
P1::random_address(&mut st.rng).to_zcash_address(&st.network()),
transfer_amount,
),
])
.unwrap();
let res = st
@ -2109,14 +2069,10 @@ pub(crate) fn scan_cached_blocks_allows_blocks_out_of_order<T: ShieldedPoolTeste
);
// We can spend the received notes
let req = TransactionRequest::new(vec![Payment {
recipient_address: T::fvk_default_address(&dfvk),
amount: NonNegativeAmount::const_from_u64(110_000),
memo: None,
label: None,
message: None,
other_params: vec![],
}])
let req = TransactionRequest::new(vec![Payment::without_memo(
T::fvk_default_address(&dfvk).to_zcash_address(&st.network()),
NonNegativeAmount::const_from_u64(110_000),
)])
.unwrap();
#[allow(deprecated)]

View File

@ -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,
@ -551,7 +555,7 @@ pub(crate) fn get_current_address<P: consensus::Parameters>(
SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned())
})
.and_then(|addr| match addr {
Address::Unified(ua) => Ok(ua),
Address::Unified(ua) => Ok(*ua),
_ => Err(SqliteClientError::CorruptedData(format!(
"Addresses table contains {} which is not a unified address",
addr_str,
@ -679,7 +683,7 @@ pub(crate) fn get_legacy_transparent_address<P: consensus::Parameters>(
conn: &rusqlite::Connection,
account_id: AccountId,
) -> Result<Option<(TransparentAddress, NonHardenedChildIndex)>, SqliteClientError> {
use zcash_address::unified::Container;
use zcash_address::unified::{Container, Item};
use zcash_primitives::legacy::keys::ExternalIvk;
// Get the UIVK for the account.
@ -701,9 +705,9 @@ pub(crate) fn get_legacy_transparent_address<P: consensus::Parameters>(
}
// Derive the default transparent address (if it wasn't already part of a derived UA).
for item in uivk.items() {
if let Ivk::P2pkh(tivk_bytes) = item {
let tivk = ExternalIvk::deserialize(&tivk_bytes)?;
for item in uivk.items_as_parsed() {
if let Item::Data(Ivk::P2pkh(tivk_bytes)) = item {
let tivk = ExternalIvk::deserialize(tivk_bytes)?;
return Ok(Some(tivk.default_address()));
}
}
@ -2366,6 +2370,48 @@ 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<P: consensus::Parameters>(
_params: &P,
conn: &rusqlite::Connection,
account: AccountId,
receiver: &Receiver,
) -> Result<Option<ZcashAddress>, 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(Box::new(*taddr)).encode(_params)
},
|row| row.get::<_, String>(0),
)
.optional()?
.map(|addr_str| addr_str.parse::<ZcashAddress>())
.transpose()
.map_err(SqliteClientError::from),
receiver => {
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::<ZcashAddress>()?;
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 +2561,17 @@ pub(crate) fn put_legacy_transparent_utxo<P: consensus::Parameters>(
// A utility function for creation of parameters for use in `insert_sent_output`
// and `put_sent_output`
fn recipient_params<P: consensus::Parameters>(
params: &P,
fn recipient_params(
to: &Recipient<AccountId, Note>,
) -> (Option<String>, Option<AccountId>, 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 +2579,8 @@ fn recipient_params<P: consensus::Parameters>(
}
/// Records information about a transaction output that your wallet created.
pub(crate) fn insert_sent_output<P: consensus::Parameters>(
pub(crate) fn insert_sent_output(
conn: &rusqlite::Connection,
params: &P,
tx_ref: i64,
from_account: AccountId,
output: &SentTransactionOutput<AccountId>,
@ -2556,7 +2594,7 @@ pub(crate) fn insert_sent_output<P: consensus::Parameters>(
: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 +2623,8 @@ pub(crate) fn insert_sent_output<P: consensus::Parameters>(
/// - 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<P: consensus::Parameters>(
pub(crate) fn put_sent_output(
conn: &rusqlite::Connection,
params: &P,
from_account: AccountId,
tx_ref: i64,
output_index: usize,
@ -2610,7 +2647,7 @@ pub(crate) fn put_sent_output<P: consensus::Parameters>(
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),

View File

@ -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())
}
@ -1419,8 +1418,9 @@ mod tests {
// Unified addresses at the time of the addition of migrations did not contain an
// Orchard component.
let ua_request = UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT);
let address_str = Address::Unified(
let ua_request =
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT);
let address_str = Address::from(
ufvk.default_address(ua_request)
.expect("A valid default address exists for the UFVK")
.0,
@ -1439,7 +1439,7 @@ mod tests {
// add a transparent "sent note"
#[cfg(feature = "transparent-inputs")]
{
let taddr = Address::Transparent(
let taddr = Address::from(
*ufvk
.default_address(ua_request)
.expect("A valid default address exists for the UFVK")
@ -1546,7 +1546,8 @@ mod tests {
assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork));
// hardcoded with knowledge of what's coming next
let ua_request = UnifiedAddressRequest::unsafe_new(false, true, true);
let ua_request =
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, true);
db_data
.get_next_available_address(account_id, ua_request)
.unwrap()

View File

@ -443,12 +443,12 @@ mod tests {
let usk = UnifiedSpendingKey::from_seed(&network, &[0u8; 32][..], AccountId::ZERO).unwrap();
let ufvk = usk.to_unified_full_viewing_key();
let (ua, _) = ufvk
.default_address(UnifiedAddressRequest::unsafe_new(
.default_address(UnifiedAddressRequest::unsafe_new_without_expiry(
false,
true,
UA_TRANSPARENT,
))
.expect("A valid default address exists for the UFVK");
.unwrap();
let taddr = ufvk
.transparent()
.and_then(|k| {

View File

@ -79,20 +79,20 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
))
})?;
let decoded_address = if let Address::Unified(ua) = decoded {
ua
*ua
} else {
return Err(WalletMigrationError::CorruptedData(
"Address in accounts table was not a Unified Address.".to_string(),
));
};
let (expected_address, idx) = ufvk.default_address(
UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT),
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT),
)?;
if decoded_address != expected_address {
return Err(WalletMigrationError::CorruptedData(format!(
"Decoded UA {} does not match the UFVK's default address {} at {:?}.",
address,
Address::Unified(expected_address).encode(&self.params),
Address::from(expected_address).encode(&self.params),
idx,
)));
}
@ -110,7 +110,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
let decoded_transparent_address = if let Address::Transparent(addr) =
decoded_transparent
{
addr
*addr
} else {
return Err(WalletMigrationError::CorruptedData(
"Address in transparent_address column of accounts table was not a transparent address.".to_string(),
@ -157,11 +157,9 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
],
)?;
let (address, d_idx) = ufvk.default_address(UnifiedAddressRequest::unsafe_new(
false,
true,
UA_TRANSPARENT,
))?;
let (address, d_idx) = ufvk.default_address(
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT),
)?;
insert_address(transaction, &self.params, account, d_idx, &address)?;
}

View File

@ -70,7 +70,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
};
let (default_addr, diversifier_index) = uivk.default_address(
UnifiedAddressRequest::unsafe_new(UA_ORCHARD, true, UA_TRANSPARENT),
UnifiedAddressRequest::unsafe_new_without_expiry(UA_ORCHARD, true, UA_TRANSPARENT),
)?;
let mut di_be = *diversifier_index.as_bytes();
@ -144,7 +144,7 @@ mod tests {
.unwrap();
let (addr, diversifier_index) = ufvk
.default_address(UnifiedAddressRequest::unsafe_new(
.default_address(UnifiedAddressRequest::unsafe_new_without_expiry(
false,
true,
UA_TRANSPARENT,

View File

@ -83,7 +83,8 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
// our second assumption above, and we report this as corrupted data.
let mut seed_is_relevant = false;
let ua_request = UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT);
let ua_request =
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT);
let mut rows = stmt_fetch_accounts.query([])?;
while let Some(row) = rows.next()? {
// We only need to check for the presence of the seed if we have keys that
@ -119,12 +120,12 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
let dfvk = ufvk.sapling().ok_or_else(||
WalletMigrationError::CorruptedData("Derivation should have produced a UFVK containing a Sapling component.".to_owned()))?;
let (idx, expected_address) = dfvk.default_address();
if decoded_address != expected_address {
if *decoded_address != expected_address {
return Err(if seed_is_relevant {
WalletMigrationError::CorruptedData(
format!("Decoded Sapling address {} does not match the ufvk's Sapling address {} at {:?}.",
address,
Address::Sapling(expected_address).encode(&self.params),
Address::from(expected_address).encode(&self.params),
idx))
} else {
WalletMigrationError::SeedNotRelevant
@ -137,12 +138,12 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
}
Address::Unified(decoded_address) => {
let (expected_address, idx) = ufvk.default_address(ua_request)?;
if decoded_address != expected_address {
if *decoded_address != expected_address {
return Err(if seed_is_relevant {
WalletMigrationError::CorruptedData(
format!("Decoded unified address {} does not match the ufvk's default address {} at {:?}.",
address,
Address::Unified(expected_address).encode(&self.params),
Address::from(expected_address).encode(&self.params),
idx))
} else {
WalletMigrationError::SeedNotRelevant

View File

@ -463,7 +463,6 @@ pub(crate) mod tests {
None,
None,
)
.unwrap()
.into()
}
@ -545,9 +544,7 @@ pub(crate) mod tests {
return Ok(result.map(|(note, addr, memo)| {
(
Note::Orchard(note),
UnifiedAddress::from_receivers(Some(addr), None, None)
.unwrap()
.into(),
UnifiedAddress::from_receivers(Some(addr), None, None).into(),
MemoBytes::from_bytes(&memo).expect("correct length"),
)
}));

View File

@ -5,11 +5,19 @@ 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
### Added
- `zcash_keys::address::Address::has_receiver`
- `zcash_keys::address`:
- `Address::has_receiver`
- `UnifiedAddress::{
new, expiry_height, expiry_time,
unknown_data, unknown_metadata
}`
- `impl Display for zcash_keys::keys::AddressGenerationError`
- `impl std::error::Error for zcash_keys::keys::AddressGenerationError`
- `impl From<hdwallet::error::Error> for zcash_keys::keys::DerivationError`
@ -28,10 +36,15 @@ and this library adheres to Rust's notion of
must be enabled for the `keys` module to be accessible.
- Updated to `zcash_primitives-0.15.0`
### Changed
- `zcash_keys::address::Address` variants now `Box` their contents to
avoid large discrepancies in enum variant sizing.
### Removed
- `UnifiedFullViewingKey::new` has been placed behind the `test-dependencies`
feature flag. UFVKs should only be produced by derivation from the USK, or
parsed from their string representation.
- `zcash_keys::address::UnifiedAddress::from_receivers`
### Fixed
- `UnifiedFullViewingKey::find_address` can now find an address for a diversifier
@ -65,28 +78,48 @@ The entries below are relative to the `zcash_client_backend` crate as of
- `UnifiedAddressRequest`
- 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`
- A new `sapling` feature flag has been added to make it possible to
build client code without `sapling` dependendencies.
- A new `transparent-inputs` feature flag has been added to make it possible to
build client code without providing support for generating transparent
addresses.
### Changed
- The following methods and enum variants have been placed behind an `orchard`
feature flag:
- The following methods, method arguments, and enum variants have been placed
behind the `orchard` feature flag:
- `zcash_keys::address::UnifiedAddress::from_receivers` no longer takes an
Orchard receiver argument unless the `orchard` feature is enabled.
- `zcash_keys::keys::UnifiedFullViewingKey::new` no longer takes
an Orchard key argument unless the `orchard` feature is enabled.
- `zcash_keys::address::UnifiedAddress::orchard`
- `zcash_keys::keys::DerivationError::Orchard`
- `zcash_keys::keys::UnifiedSpendingKey::orchard`
- `zcash_keys::keys::UnifiedFullViewingKey::orchard`
- The following methods and method arguments have been placed behind the
`sapling` feature flag:
- `UnifiedAddress::from_receivers` no longer takes a Sapling receiver
argument unless the `sapling` feature is enabled.
- `zcash_keys::keys::UnifiedFullViewingKey::new` no longer takes
a Sapling key argument unless the `sapling` feature is enabled.
- `zcash_keys::address::UnifiedAddress::sapling`
- `zcash_keys::keys::UnifiedSpendingKey::sapling`
- `zcash_keys::keys::UnifiedFullViewingKey::sapling`
- The following methods and method arguments have been placed behind the
`transparent-inputs` feature flag:
- `zcash_keys::keys::UnifiedFullViewingKey::transparent` no longer takes
a transparent key argument unless the `transparent-inputs` feature is enabled.
- `zcash_keys::keys::UnifiedSpendingKey::transparent`
- `zcash_keys::keys::UnifiedFullViewingKey::transparent`
- `zcash_keys::address`:
- `RecipientAddress` has been renamed to `Address`.
- `Address::Shielded` has been renamed to `Address::Sapling`.
- `UnifiedAddress::from_receivers` no longer takes an Orchard receiver
argument unless the `orchard` feature is enabled.
- `zcash_keys::keys`:
- `UnifiedSpendingKey::address` now takes an argument that specifies the
receivers to be generated in the resulting address. Also, it now returns
`Result<UnifiedAddress, AddressGenerationError>` instead of
`Option<UnifiedAddress>` so that we may better report to the user how
address generation has failed.
- `UnifiedSpendingKey::transparent` is now only available when the
`transparent-inputs` feature is enabled.
- `UnifiedFullViewingKey::new` no longer takes an Orchard full viewing key
argument unless the `orchard` feature is enabled.
### Removed
- `zcash_keys::address::AddressMetadata`

View File

@ -1,15 +1,17 @@
//! Structs for handling supported address types.
use zcash_address::{
unified::{self, Container, Encoding, Typecode},
unified::{self, Container, DataTypecode, Encoding, Item, Revision, Typecode},
ConversionError, ToAddress, TryFromRawAddress, ZcashAddress,
};
use zcash_primitives::legacy::TransparentAddress;
use zcash_protocol::consensus::{self, NetworkType};
use zcash_protocol::{
consensus::{self, BlockHeight, NetworkType},
PoolType, ShieldedProtocol,
};
#[cfg(feature = "sapling")]
use sapling::PaymentAddress;
use zcash_protocol::{PoolType, ShieldedProtocol};
/// A Unified Address.
#[derive(Clone, Debug, PartialEq, Eq)]
@ -19,7 +21,10 @@ pub struct UnifiedAddress {
#[cfg(feature = "sapling")]
sapling: Option<PaymentAddress>,
transparent: Option<TransparentAddress>,
unknown: Vec<(u32, Vec<u8>)>,
unknown_data: Vec<(u32, Vec<u8>)>,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
unknown_metadata: Vec<(u32, Vec<u8>)>,
}
impl TryFrom<unified::Address> for UnifiedAddress {
@ -31,14 +36,16 @@ impl TryFrom<unified::Address> for UnifiedAddress {
#[cfg(feature = "sapling")]
let mut sapling = None;
let mut transparent = None;
let mut unknown: Vec<(u32, Vec<u8>)> = vec![];
let mut unknown_data = vec![];
let mut expiry_height = None;
let mut expiry_time = None;
let mut unknown_metadata = vec![];
// We can use as-parsed order here for efficiency, because we're breaking out the
// receivers we support from the unknown receivers.
for item in ua.items_as_parsed() {
match item {
unified::Receiver::Orchard(data) => {
Item::Data(unified::Receiver::Orchard(data)) => {
#[cfg(feature = "orchard")]
{
orchard = Some(
@ -48,11 +55,11 @@ impl TryFrom<unified::Address> for UnifiedAddress {
}
#[cfg(not(feature = "orchard"))]
{
unknown.push((unified::Typecode::Orchard.into(), data.to_vec()));
unknown_data.push((unified::Typecode::ORCHARD.into(), data.to_vec()));
}
}
unified::Receiver::Sapling(data) => {
Item::Data(unified::Receiver::Sapling(data)) => {
#[cfg(feature = "sapling")]
{
sapling = Some(
@ -62,20 +69,26 @@ impl TryFrom<unified::Address> for UnifiedAddress {
}
#[cfg(not(feature = "sapling"))]
{
unknown.push((unified::Typecode::Sapling.into(), data.to_vec()));
unknown_data.push((unified::Typecode::SAPLING.into(), data.to_vec()));
}
}
unified::Receiver::P2pkh(data) => {
Item::Data(unified::Receiver::P2pkh(data)) => {
transparent = Some(TransparentAddress::PublicKeyHash(*data));
}
unified::Receiver::P2sh(data) => {
Item::Data(unified::Receiver::P2sh(data)) => {
transparent = Some(TransparentAddress::ScriptHash(*data));
}
unified::Receiver::Unknown { typecode, data } => {
unknown.push((*typecode, data.clone()));
Item::Data(unified::Receiver::Unknown { typecode, data }) => {
unknown_data.push((*typecode, data.clone()));
}
Item::Metadata(unified::MetadataItem::ExpiryHeight(h)) => {
expiry_height = Some(BlockHeight::from(*h));
}
Item::Metadata(unified::MetadataItem::ExpiryTime(t)) => {
expiry_time = Some(*t);
}
Item::Metadata(unified::MetadataItem::Unknown { typecode, data }) => {
unknown_metadata.push((*typecode, data.clone()));
}
}
}
@ -86,7 +99,10 @@ impl TryFrom<unified::Address> for UnifiedAddress {
#[cfg(feature = "sapling")]
sapling,
transparent,
unknown,
unknown_data,
expiry_height,
expiry_time,
unknown_metadata,
})
}
}
@ -94,36 +110,43 @@ impl TryFrom<unified::Address> for UnifiedAddress {
impl UnifiedAddress {
/// Constructs a Unified Address from a given set of receivers.
///
/// Returns `None` if the receivers would produce an invalid Unified Address (namely,
/// if no shielded receiver is provided).
/// This method is only available when the `test-dependencies` feature is enabled, as
/// derivation from the UFVK or UIVK, or deserialization from the serialized form should be
/// used instead.
#[cfg(any(test, feature = "test-dependencies"))]
pub fn from_receivers(
#[cfg(feature = "orchard")] orchard: Option<orchard::Address>,
#[cfg(feature = "sapling")] sapling: Option<PaymentAddress>,
transparent: Option<TransparentAddress>,
// TODO: Add handling for address metadata items.
) -> Option<Self> {
#[cfg(feature = "orchard")]
let has_orchard = orchard.is_some();
#[cfg(not(feature = "orchard"))]
let has_orchard = false;
) -> Self {
Self::new_internal(
#[cfg(feature = "orchard")]
orchard,
#[cfg(feature = "sapling")]
sapling,
transparent,
None,
None,
)
}
#[cfg(feature = "sapling")]
let has_sapling = sapling.is_some();
#[cfg(not(feature = "sapling"))]
let has_sapling = false;
if has_orchard || has_sapling {
Some(Self {
#[cfg(feature = "orchard")]
orchard,
#[cfg(feature = "sapling")]
sapling,
transparent,
unknown: vec![],
})
} else {
// UAs require at least one shielded receiver.
None
pub(crate) fn new_internal(
#[cfg(feature = "orchard")] orchard: Option<orchard::Address>,
#[cfg(feature = "sapling")] sapling: Option<PaymentAddress>,
transparent: Option<TransparentAddress>,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
) -> Self {
Self {
#[cfg(feature = "orchard")]
orchard,
#[cfg(feature = "sapling")]
sapling,
transparent,
unknown_data: vec![],
expiry_height,
expiry_time,
unknown_metadata: vec![],
}
}
@ -168,22 +191,60 @@ impl UnifiedAddress {
self.transparent.as_ref()
}
/// Returns the set of unknown receivers of the unified address.
pub fn unknown(&self) -> &[(u32, Vec<u8>)] {
&self.unknown
/// Returns any unknown data items parsed from the encoded form of the address.
pub fn unknown_data(&self) -> &[(u32, Vec<u8>)] {
self.unknown_data.as_ref()
}
/// Returns the expiration height for this address.
pub fn expiry_height(&self) -> Option<BlockHeight> {
self.expiry_height
}
/// Sets the expiry height of this address.
pub fn set_expiry_height(&mut self, height: BlockHeight) {
self.expiry_height = Some(height);
}
/// Removes the expiry height from this address.
pub fn unset_expiry_height(&mut self) {
self.expiry_height = None;
}
/// Returns the expiration time for this address as a Unix Epoch Time.
pub fn expiry_time(&self) -> Option<u64> {
self.expiry_time
}
/// Sets the expiry time of this address.
pub fn set_expiry_time(&mut self, time: u64) {
self.expiry_time = Some(time);
}
/// Removes the expiry time from this address.
pub fn unset_expiry_time(&mut self) {
self.expiry_time = None;
}
/// Returns any unknown metadata items parsed from the encoded form of the address.
///
/// Unknown metadata items are guaranteed by construction and parsing to not have keys in the
/// MUST-understand metadata typecode range.
pub fn unknown_metadata(&self) -> &[(u32, Vec<u8>)] {
self.unknown_metadata.as_ref()
}
fn to_address(&self, net: NetworkType) -> ZcashAddress {
let items = self
.unknown
.iter()
.map(|(typecode, data)| unified::Receiver::Unknown {
typecode: *typecode,
data: data.clone(),
});
let data_items =
self.unknown_data
.iter()
.map(|(typecode, data)| unified::Receiver::Unknown {
typecode: *typecode,
data: data.clone(),
});
#[cfg(feature = "orchard")]
let items = items.chain(
let data_items = data_items.chain(
self.orchard
.as_ref()
.map(|addr| addr.to_raw_address_bytes())
@ -191,20 +252,46 @@ impl UnifiedAddress {
);
#[cfg(feature = "sapling")]
let items = items.chain(
let data_items = data_items.chain(
self.sapling
.as_ref()
.map(|pa| pa.to_bytes())
.map(unified::Receiver::Sapling),
);
let items = items.chain(self.transparent.as_ref().map(|taddr| match taddr {
let data_items = data_items.chain(self.transparent.as_ref().map(|taddr| match taddr {
TransparentAddress::PublicKeyHash(data) => unified::Receiver::P2pkh(*data),
TransparentAddress::ScriptHash(data) => unified::Receiver::P2sh(*data),
}));
let ua = unified::Address::try_from_items(items.collect())
.expect("UnifiedAddress should only be constructed safely");
let meta_items = self
.unknown_metadata
.iter()
.map(|(typecode, data)| unified::MetadataItem::Unknown {
typecode: *typecode,
data: data.clone(),
})
.chain(
self.expiry_height
.map(|h| unified::MetadataItem::ExpiryHeight(u32::from(h))),
)
.chain(self.expiry_time.map(unified::MetadataItem::ExpiryTime));
let ua = unified::Address::try_from_items(
if self.expiry_height().is_some()
|| self.expiry_time().is_some()
|| !(self.has_orchard() || self.has_sapling())
{
Revision::R1
} else {
Revision::R0
},
data_items
.map(Item::Data)
.chain(meta_items.map(Item::Metadata))
.collect(),
)
.expect("UnifiedAddress should only be constructed safely");
ZcashAddress::from_unified(net, ua)
}
@ -217,47 +304,105 @@ impl UnifiedAddress {
pub fn receiver_types(&self) -> Vec<Typecode> {
let result = std::iter::empty();
#[cfg(feature = "orchard")]
let result = result.chain(self.orchard.map(|_| Typecode::Orchard));
let result = result.chain(self.orchard.map(|_| Typecode::ORCHARD));
#[cfg(feature = "sapling")]
let result = result.chain(self.sapling.map(|_| Typecode::Sapling));
let result = result.chain(self.sapling.map(|_| Typecode::SAPLING));
let result = result.chain(self.transparent.map(|taddr| match taddr {
TransparentAddress::PublicKeyHash(_) => Typecode::P2pkh,
TransparentAddress::ScriptHash(_) => Typecode::P2sh,
TransparentAddress::PublicKeyHash(_) => Typecode::P2PKH,
TransparentAddress::ScriptHash(_) => Typecode::P2SH,
}));
let result = result.chain(
self.unknown()
self.unknown_data()
.iter()
.map(|(typecode, _)| Typecode::Unknown(*typecode)),
.map(|(typecode, _)| Typecode::Data(DataTypecode::Unknown(*typecode))),
);
result.collect()
}
}
/// An enumeration of protocol-level receiver types.
///
/// While these correspond to unified address receiver types, this is a distinct type because it is
/// used to represent the protocol-level recipient of a transfer, instead of a part of an encoded
/// address.
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 {
#[cfg(feature = "orchard")]
Receiver::Orchard(addr) => {
let receiver =
unified::Item::Data(unified::Receiver::Orchard(addr.to_raw_address_bytes()));
let ua = unified::Address::try_from_items(Revision::R0, vec![receiver])
.expect("A unified address may contain a single Orchard receiver.");
ZcashAddress::from_unified(net, ua)
}
#[cfg(feature = "sapling")]
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)
}
}
}
/// Returns whether or not this receiver corresponds to `addr`, or is contained
/// in `addr` when the latter is a Unified Address.
pub fn corresponds(&self, addr: &ZcashAddress) -> bool {
addr.matches_receiver(&match self {
#[cfg(feature = "orchard")]
Receiver::Orchard(addr) => unified::Receiver::Orchard(addr.to_raw_address_bytes()),
#[cfg(feature = "sapling")]
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 {
#[cfg(feature = "sapling")]
Sapling(PaymentAddress),
Transparent(TransparentAddress),
Unified(UnifiedAddress),
Sapling(Box<PaymentAddress>),
Transparent(Box<TransparentAddress>),
Unified(Box<UnifiedAddress>),
}
#[cfg(feature = "sapling")]
impl From<PaymentAddress> for Address {
fn from(addr: PaymentAddress) -> Self {
Address::Sapling(addr)
Address::Sapling(Box::new(addr))
}
}
impl From<TransparentAddress> for Address {
fn from(addr: TransparentAddress) -> Self {
Address::Transparent(addr)
Address::Transparent(Box::new(addr))
}
}
impl From<UnifiedAddress> for Address {
fn from(addr: UnifiedAddress) -> Self {
Address::Unified(addr)
Address::Unified(Box::new(addr))
}
}
@ -290,30 +435,47 @@ impl TryFromRawAddress for Address {
}
impl Address {
/// Attempts to decode an [`Address`] value from its [`ZcashAddress`] encoded representation.
///
/// Returns `None` if any error is encountered in decoding. Use
/// [`Self::try_from_zcash_address(s.parse()?)?`] if you need detailed error information.
pub fn decode<P: consensus::Parameters>(params: &P, s: &str) -> Option<Self> {
let addr = ZcashAddress::try_from_encoded(s).ok()?;
addr.convert_if_network(params.network_type()).ok()
Self::try_from_zcash_address(params, s.parse::<ZcashAddress>().ok()?).ok()
}
pub fn encode<P: consensus::Parameters>(&self, params: &P) -> String {
/// Attempts to decode an [`Address`] value from its [`ZcashAddress`] encoded representation.
pub fn try_from_zcash_address<P: consensus::Parameters>(
params: &P,
zaddr: ZcashAddress,
) -> Result<Self, ConversionError<&'static str>> {
zaddr.convert_if_network(params.network_type())
}
/// Converts this [`Address`] to its encoded [`ZcashAddress`] representation.
pub fn to_zcash_address<P: consensus::Parameters>(&self, params: &P) -> ZcashAddress {
let net = params.network_type();
match self {
#[cfg(feature = "sapling")]
Address::Sapling(pa) => ZcashAddress::from_sapling(net, pa.to_bytes()),
Address::Transparent(addr) => match addr {
Address::Transparent(addr) => match **addr {
TransparentAddress::PublicKeyHash(data) => {
ZcashAddress::from_transparent_p2pkh(net, *data)
ZcashAddress::from_transparent_p2pkh(net, data)
}
TransparentAddress::ScriptHash(data) => {
ZcashAddress::from_transparent_p2sh(net, *data)
ZcashAddress::from_transparent_p2sh(net, data)
}
},
Address::Unified(ua) => ua.to_address(net),
}
.to_string()
}
/// Converts this [`Address`] to its encoded string representation.
pub fn encode<P: consensus::Parameters>(&self, params: &P) -> String {
self.to_zcash_address(params).to_string()
}
/// Returns whether or not this [`Address`] can send funds to the specified pool.
pub fn has_receiver(&self, pool_type: PoolType) -> bool {
match self {
#[cfg(feature = "sapling")]
@ -366,23 +528,23 @@ pub mod testing {
params: Network,
request: UnifiedAddressRequest,
) -> impl Strategy<Value = UnifiedAddress> {
arb_unified_spending_key(params).prop_map(move |k| k.default_address(request).0)
arb_unified_spending_key(params).prop_map(move |k| k.default_address(request).unwrap().0)
}
#[cfg(feature = "sapling")]
pub fn arb_addr(request: UnifiedAddressRequest) -> impl Strategy<Value = Address> {
prop_oneof![
arb_payment_address().prop_map(Address::Sapling),
arb_transparent_addr().prop_map(Address::Transparent),
arb_unified_addr(Network::TestNetwork, request).prop_map(Address::Unified),
arb_payment_address().prop_map(Address::from),
arb_transparent_addr().prop_map(Address::from),
arb_unified_addr(Network::TestNetwork, request).prop_map(Address::from),
]
}
#[cfg(not(feature = "sapling"))]
pub fn arb_addr(request: UnifiedAddressRequest) -> impl Strategy<Value = Address> {
return prop_oneof![
arb_transparent_addr().prop_map(Address::Transparent),
arb_unified_addr(Network::TestNetwork, request).prop_map(Address::Unified),
arb_transparent_addr().prop_map(Address::from),
arb_unified_addr(Network::TestNetwork, request).prop_map(Address::from),
];
}
}
@ -421,15 +583,15 @@ mod tests {
let transparent = None;
#[cfg(all(feature = "orchard", feature = "sapling"))]
let ua = UnifiedAddress::from_receivers(orchard, sapling, transparent).unwrap();
let ua = UnifiedAddress::new_internal(orchard, sapling, transparent, None, None);
#[cfg(all(not(feature = "orchard"), feature = "sapling"))]
let ua = UnifiedAddress::from_receivers(sapling, transparent).unwrap();
let ua = UnifiedAddress::new_internal(sapling, transparent, None, None);
#[cfg(all(feature = "orchard", not(feature = "sapling")))]
let ua = UnifiedAddress::from_receivers(orchard, transparent).unwrap();
let ua = UnifiedAddress::new_internal(orchard, transparent, None, None);
let addr = Address::Unified(ua);
let addr = Address::from(ua);
let addr_str = addr.encode(&MAIN_NETWORK);
assert_eq!(Address::decode(&MAIN_NETWORK, &addr_str), Some(addr));
}
@ -438,7 +600,7 @@ mod tests {
#[cfg(not(any(feature = "orchard", feature = "sapling")))]
fn ua_round_trip() {
let transparent = None;
assert_eq!(UnifiedAddress::from_receivers(transparent), None)
assert_eq!(UnifiedAddress::new_internal(transparent, None, None), None)
}
#[test]

View File

@ -3,8 +3,8 @@ use std::{
error,
fmt::{self, Display},
};
use zcash_address::unified::{self, Container, Encoding, Typecode, Ufvk, Uivk};
use zcash_address::unified::{self, Container, Encoding, Item, MetadataItem, Revision, Typecode};
use zcash_primitives::consensus::BlockHeight;
use zcash_protocol::consensus;
use zip32::{AccountId, DiversifierIndex};
@ -258,7 +258,10 @@ impl UnifiedSpendingKey {
sapling: Some(self.sapling.to_diversifiable_full_viewing_key()),
#[cfg(feature = "orchard")]
orchard: Some((&self.orchard).into()),
unknown: vec![],
unknown_data: vec![],
expiry_height: None,
expiry_time: None,
unknown_metadata: vec![],
}
}
@ -298,7 +301,7 @@ impl UnifiedSpendingKey {
#[cfg(feature = "orchard")]
{
let orchard_key = self.orchard();
CompactSize::write(&mut result, usize::try_from(Typecode::Orchard).unwrap()).unwrap();
CompactSize::write(&mut result, usize::try_from(Typecode::ORCHARD).unwrap()).unwrap();
let orchard_key_bytes = orchard_key.to_bytes();
CompactSize::write(&mut result, orchard_key_bytes.len()).unwrap();
@ -308,7 +311,7 @@ impl UnifiedSpendingKey {
#[cfg(feature = "sapling")]
{
let sapling_key = self.sapling();
CompactSize::write(&mut result, usize::try_from(Typecode::Sapling).unwrap()).unwrap();
CompactSize::write(&mut result, usize::try_from(Typecode::SAPLING).unwrap()).unwrap();
let sapling_key_bytes = sapling_key.to_bytes();
CompactSize::write(&mut result, sapling_key_bytes.len()).unwrap();
@ -318,7 +321,7 @@ impl UnifiedSpendingKey {
#[cfg(feature = "transparent-inputs")]
{
let account_tkey = self.transparent();
CompactSize::write(&mut result, usize::try_from(Typecode::P2pkh).unwrap()).unwrap();
CompactSize::write(&mut result, usize::try_from(Typecode::P2PKH).unwrap()).unwrap();
let account_tkey_bytes = account_tkey.to_bytes();
CompactSize::write(&mut result, account_tkey_bytes.len()).unwrap();
@ -334,6 +337,8 @@ impl UnifiedSpendingKey {
#[allow(clippy::unnecessary_unwrap)]
#[cfg(feature = "unstable")]
pub fn from_bytes(era: Era, encoded: &[u8]) -> Result<Self, DecodingError> {
use zcash_address::unified::DataTypecode;
let mut source = std::io::Cursor::new(encoded);
let decoded_era = source
.read_u32::<LittleEndian>()
@ -353,21 +358,23 @@ impl UnifiedSpendingKey {
loop {
let tc = CompactSize::read_t::<_, u32>(&mut source)
.map_err(|_| DecodingError::ReadError("typecode"))
.and_then(|v| Typecode::try_from(v).map_err(|_| DecodingError::TypecodeInvalid))?;
.and_then(|v| {
DataTypecode::try_from(v).map_err(|_| DecodingError::TypecodeInvalid)
})?;
let len = CompactSize::read_t::<_, u32>(&mut source)
.map_err(|_| DecodingError::ReadError("key length"))?;
match tc {
Typecode::Orchard => {
DataTypecode::Orchard => {
if len != 32 {
return Err(DecodingError::LengthMismatch(Typecode::Orchard, len));
return Err(DecodingError::LengthMismatch(Typecode::ORCHARD, len));
}
let mut key = [0u8; 32];
source
.read_exact(&mut key)
.map_err(|_| DecodingError::InsufficientData(Typecode::Orchard))?;
.map_err(|_| DecodingError::InsufficientData(Typecode::ORCHARD))?;
#[cfg(feature = "orchard")]
{
@ -375,43 +382,43 @@ impl UnifiedSpendingKey {
Option::<orchard::keys::SpendingKey>::from(
orchard::keys::SpendingKey::from_bytes(key),
)
.ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard))?,
.ok_or(DecodingError::KeyDataInvalid(Typecode::ORCHARD))?,
);
}
}
Typecode::Sapling => {
DataTypecode::Sapling => {
if len != 169 {
return Err(DecodingError::LengthMismatch(Typecode::Sapling, len));
return Err(DecodingError::LengthMismatch(Typecode::SAPLING, len));
}
let mut key = [0u8; 169];
source
.read_exact(&mut key)
.map_err(|_| DecodingError::InsufficientData(Typecode::Sapling))?;
.map_err(|_| DecodingError::InsufficientData(Typecode::SAPLING))?;
#[cfg(feature = "sapling")]
{
sapling = Some(
sapling::ExtendedSpendingKey::from_bytes(&key)
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::Sapling))?,
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::SAPLING))?,
);
}
}
Typecode::P2pkh => {
DataTypecode::P2pkh => {
if len != 64 {
return Err(DecodingError::LengthMismatch(Typecode::P2pkh, len));
return Err(DecodingError::LengthMismatch(Typecode::P2PKH, len));
}
let mut key = [0u8; 64];
source
.read_exact(&mut key)
.map_err(|_| DecodingError::InsufficientData(Typecode::P2pkh))?;
.map_err(|_| DecodingError::InsufficientData(Typecode::P2PKH))?;
#[cfg(feature = "transparent-inputs")]
{
transparent = Some(
legacy::AccountPrivKey::from_bytes(&key)
.ok_or(DecodingError::KeyDataInvalid(Typecode::P2pkh))?,
.ok_or(DecodingError::KeyDataInvalid(Typecode::P2PKH))?,
);
}
}
@ -444,7 +451,7 @@ impl UnifiedSpendingKey {
#[cfg(feature = "orchard")]
orchard.unwrap(),
)
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh));
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH));
}
}
}
@ -453,10 +460,8 @@ impl UnifiedSpendingKey {
pub fn default_address(
&self,
request: UnifiedAddressRequest,
) -> (UnifiedAddress, DiversifierIndex) {
self.to_unified_full_viewing_key()
.default_address(request)
.unwrap()
) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> {
self.to_unified_full_viewing_key().default_address(request)
}
#[cfg(all(
@ -549,22 +554,28 @@ pub struct UnifiedAddressRequest {
has_orchard: bool,
has_sapling: bool,
has_p2pkh: bool,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
}
impl UnifiedAddressRequest {
/// Construct a new unified address request from its constituent parts.
///
/// Returns `None` if the resulting unified address would not include at least one shielded receiver.
pub fn new(has_orchard: bool, has_sapling: bool, has_p2pkh: bool) -> Option<Self> {
let has_shielded_receiver = has_orchard || has_sapling;
if !has_shielded_receiver {
/// Construct a new unified address request from its constituent parts
pub fn new(
has_orchard: bool,
has_sapling: bool,
has_p2pkh: bool,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
) -> Option<Self> {
if !(has_sapling || has_orchard || has_p2pkh) {
None
} else {
Some(Self {
has_orchard,
has_sapling,
has_p2pkh,
expiry_height,
expiry_time,
})
}
}
@ -584,21 +595,27 @@ impl UnifiedAddressRequest {
#[cfg(feature = "transparent-inputs")]
let _has_p2pkh = true;
Self::new(_has_orchard, _has_sapling, _has_p2pkh)
Self::new(_has_orchard, _has_sapling, _has_p2pkh, None, None)
}
/// Construct a new unified address request from its constituent parts.
///
/// Panics: at least one of `has_orchard` or `has_sapling` must be `true`.
pub const fn unsafe_new(has_orchard: bool, has_sapling: bool, has_p2pkh: bool) -> Self {
if !(has_orchard || has_sapling) {
panic!("At least one shielded receiver must be requested.")
/// Panics: at least one of `has_orchard`, `has_sapling`, or `has_p2pkh` must be `true`.
pub const fn unsafe_new_without_expiry(
has_orchard: bool,
has_sapling: bool,
has_p2pkh: bool,
) -> Self {
if !(has_orchard || has_sapling || has_p2pkh) {
panic!("At least one receiver must be requested.")
}
Self {
has_orchard,
has_sapling,
has_p2pkh,
expiry_height: None,
expiry_time: None,
}
}
}
@ -619,7 +636,10 @@ pub struct UnifiedFullViewingKey {
sapling: Option<sapling::DiversifiableFullViewingKey>,
#[cfg(feature = "orchard")]
orchard: Option<orchard::keys::FullViewingKey>,
unknown: Vec<(u32, Vec<u8>)>,
unknown_data: Vec<(u32, Vec<u8>)>,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
unknown_metadata: Vec<(u32, Vec<u8>)>,
}
impl UnifiedFullViewingKey {
@ -645,6 +665,9 @@ impl UnifiedFullViewingKey {
// We don't currently allow constructing new UFVKs with unknown items, but we store
// this to allow parsing such UFVKs.
vec![],
None,
None,
vec![],
)
}
@ -654,7 +677,10 @@ impl UnifiedFullViewingKey {
#[cfg(feature = "transparent-inputs")] transparent: Option<legacy::AccountPubKey>,
#[cfg(feature = "sapling")] sapling: Option<sapling::DiversifiableFullViewingKey>,
#[cfg(feature = "orchard")] orchard: Option<orchard::keys::FullViewingKey>,
unknown: Vec<(u32, Vec<u8>)>,
unknown_data: Vec<(u32, Vec<u8>)>,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
unknown_metadata: Vec<(u32, Vec<u8>)>,
) -> Result<UnifiedFullViewingKey, DerivationError> {
// Verify that IVK derivation succeeds; we don't want to construct a UFVK
// that can't derive transparent addresses.
@ -671,7 +697,10 @@ impl UnifiedFullViewingKey {
sapling,
#[cfg(feature = "orchard")]
orchard,
unknown,
unknown_data,
expiry_height,
expiry_time,
unknown_metadata,
})
}
@ -679,7 +708,8 @@ impl UnifiedFullViewingKey {
///
/// [ZIP 316]: https://zips.z.cash/zip-0316
pub fn decode<P: consensus::Parameters>(params: &P, encoding: &str) -> Result<Self, String> {
let (net, ufvk) = unified::Ufvk::decode(encoding).map_err(|e| e.to_string())?;
let (net, ufvk) =
zcash_address::unified::Ufvk::decode(encoding).map_err(|e| e.to_string())?;
let expected_net = params.network_type();
if net != expected_net {
return Err(format!(
@ -694,64 +724,71 @@ impl UnifiedFullViewingKey {
/// Parses a `UnifiedFullViewingKey` from its [ZIP 316] string encoding.
///
/// [ZIP 316]: https://zips.z.cash/zip-0316
pub fn parse(ufvk: &Ufvk) -> Result<Self, DecodingError> {
pub fn parse(ufvk: &zcash_address::unified::Ufvk) -> Result<Self, DecodingError> {
#[cfg(feature = "orchard")]
let mut orchard = None;
#[cfg(feature = "sapling")]
let mut sapling = None;
#[cfg(feature = "transparent-inputs")]
let mut transparent = None;
let mut unknown_data = vec![];
let mut expiry_height = None;
let mut expiry_time = None;
let mut unknown_metadata = vec![];
// We can use as-parsed order here for efficiency, because we're breaking out the
// receivers we support from the unknown receivers.
let unknown = ufvk
.items_as_parsed()
.iter()
.filter_map(|receiver| match receiver {
#[cfg(feature = "orchard")]
unified::Fvk::Orchard(data) => orchard::keys::FullViewingKey::from_bytes(data)
.ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard))
.map(|addr| {
orchard = Some(addr);
None
})
.transpose(),
#[cfg(not(feature = "orchard"))]
unified::Fvk::Orchard(data) => Some(Ok::<_, DecodingError>((
u32::from(unified::Typecode::Orchard),
data.to_vec(),
))),
#[cfg(feature = "sapling")]
unified::Fvk::Sapling(data) => {
sapling::DiversifiableFullViewingKey::from_bytes(data)
.ok_or(DecodingError::KeyDataInvalid(Typecode::Sapling))
.map(|pa| {
sapling = Some(pa);
None
})
.transpose()
for item in ufvk.items_as_parsed() {
match item {
Item::Data(unified::Fvk::Orchard(data)) => {
#[cfg(feature = "orchard")]
{
orchard = Some(
orchard::keys::FullViewingKey::from_bytes(data)
.ok_or(DecodingError::KeyDataInvalid(Typecode::ORCHARD))?,
);
}
#[cfg(not(feature = "orchard"))]
unknown_data.push((unified::DataTypecode::Orchard.into(), data.to_vec()));
}
#[cfg(not(feature = "sapling"))]
unified::Fvk::Sapling(data) => Some(Ok::<_, DecodingError>((
u32::from(unified::Typecode::Sapling),
data.to_vec(),
))),
#[cfg(feature = "transparent-inputs")]
unified::Fvk::P2pkh(data) => legacy::AccountPubKey::deserialize(data)
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh))
.map(|tfvk| {
transparent = Some(tfvk);
None
})
.transpose(),
#[cfg(not(feature = "transparent-inputs"))]
unified::Fvk::P2pkh(data) => Some(Ok::<_, DecodingError>((
u32::from(unified::Typecode::P2pkh),
data.to_vec(),
))),
unified::Fvk::Unknown { typecode, data } => Some(Ok((*typecode, data.clone()))),
})
.collect::<Result<_, _>>()?;
Item::Data(unified::Fvk::Sapling(data)) => {
#[cfg(feature = "sapling")]
{
sapling = Some(
sapling::DiversifiableFullViewingKey::from_bytes(data)
.ok_or(DecodingError::KeyDataInvalid(Typecode::SAPLING))?,
);
}
#[cfg(not(feature = "sapling"))]
unknown_data.push((unified::Typecode::SAPLING.into(), data.to_vec()));
}
Item::Data(unified::Fvk::P2pkh(data)) => {
#[cfg(feature = "transparent-inputs")]
{
transparent = Some(
legacy::AccountPubKey::deserialize(data)
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH))?,
);
}
#[cfg(not(feature = "transparent-inputs"))]
unknown_data.push((unified::DataTypecode::P2pkh.into(), data.to_vec()));
}
Item::Data(unified::Fvk::Unknown { typecode, data }) => {
unknown_data.push((*typecode, data.clone()));
}
Item::Metadata(MetadataItem::ExpiryHeight(h)) => {
expiry_height = Some(BlockHeight::from(*h));
}
Item::Metadata(MetadataItem::ExpiryTime(t)) => {
expiry_time = Some(*t);
}
Item::Metadata(MetadataItem::Unknown { typecode, data }) => {
unknown_metadata.push((*typecode, data.clone()));
}
}
}
Self::from_checked_parts(
#[cfg(feature = "transparent-inputs")]
@ -760,9 +797,12 @@ impl UnifiedFullViewingKey {
sapling,
#[cfg(feature = "orchard")]
orchard,
unknown,
unknown_data,
expiry_height,
expiry_time,
unknown_metadata,
)
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh))
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH))
}
/// Returns the string encoding of this `UnifiedFullViewingKey` for the given network.
@ -771,37 +811,64 @@ impl UnifiedFullViewingKey {
}
/// Returns the string encoding of this `UnifiedFullViewingKey` for the given network.
fn to_ufvk(&self) -> Ufvk {
let items = std::iter::empty().chain(self.unknown.iter().map(|(typecode, data)| {
unified::Fvk::Unknown {
typecode: *typecode,
data: data.clone(),
}
}));
fn to_ufvk(&self) -> zcash_address::unified::Ufvk {
let data_items =
std::iter::empty().chain(self.unknown_data.iter().map(|(typecode, data)| {
unified::Fvk::Unknown {
typecode: *typecode,
data: data.clone(),
}
}));
#[cfg(feature = "orchard")]
let items = items.chain(
let data_items = data_items.chain(
self.orchard
.as_ref()
.map(|fvk| fvk.to_bytes())
.map(unified::Fvk::Orchard),
);
#[cfg(feature = "sapling")]
let items = items.chain(
let data_items = data_items.chain(
self.sapling
.as_ref()
.map(|dfvk| dfvk.to_bytes())
.map(unified::Fvk::Sapling),
);
#[cfg(feature = "transparent-inputs")]
let items = items.chain(
let data_items = data_items.chain(
self.transparent
.as_ref()
.map(|tfvk| tfvk.serialize().try_into().unwrap())
.map(unified::Fvk::P2pkh),
);
unified::Ufvk::try_from_items(items.collect())
.expect("UnifiedFullViewingKey should only be constructed safely")
let meta_items = std::iter::empty()
.chain(self.unknown_metadata.iter().map(|(typecode, data)| {
unified::MetadataItem::Unknown {
typecode: *typecode,
data: data.clone(),
}
}))
.chain(
self.expiry_height
.map(|h| unified::MetadataItem::ExpiryHeight(u32::from(h))),
)
.chain(self.expiry_time.map(unified::MetadataItem::ExpiryTime));
zcash_address::unified::Ufvk::try_from_items(
if self.expiry_height().is_some()
|| self.expiry_time().is_some()
|| !(self.sapling.is_some() || self.orchard.is_some())
{
Revision::R1
} else {
Revision::R0
},
data_items
.map(Item::Data)
.chain(meta_items.map(Item::Metadata))
.collect(),
)
.expect("UnifiedFullViewingKey should only be constructed safely")
}
/// Derives a Unified Incoming Viewing Key from this Unified Full Viewing Key.
@ -816,7 +883,11 @@ impl UnifiedFullViewingKey {
sapling: self.sapling.as_ref().map(|s| s.to_external_ivk()),
#[cfg(feature = "orchard")]
orchard: self.orchard.as_ref().map(|o| o.to_ivk(Scope::External)),
unknown: Vec::new(),
expiry_height: self.expiry_height,
expiry_time: self.expiry_time,
// We cannot translate unknown data or metadata items, as they may not be relevant to the IVK
unknown_data: vec![],
unknown_metadata: vec![],
}
}
@ -839,6 +910,26 @@ impl UnifiedFullViewingKey {
self.orchard.as_ref()
}
/// Returns any unknown data items parsed from the encoded form of the key.
pub fn unknown_data(&self) -> &[(u32, Vec<u8>)] {
self.unknown_data.as_ref()
}
/// Returns the expiration height for this key.
pub fn expiry_height(&self) -> Option<BlockHeight> {
self.expiry_height
}
/// Returns the expiration time for this key.
pub fn expiry_time(&self) -> Option<u64> {
self.expiry_time
}
/// Returns any unknown metadata items parsed from the encoded form of the key.
pub fn unknown_metadata(&self) -> &[(u32, Vec<u8>)] {
self.unknown_metadata.as_ref()
}
/// Attempts to derive the Unified Address for the given diversifier index and
/// receiver types.
///
@ -857,10 +948,9 @@ impl UnifiedFullViewingKey {
///
/// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features
/// required to satisfy the unified address request are not properly enabled.
#[allow(unused_mut)]
pub fn find_address(
&self,
mut j: DiversifierIndex,
j: DiversifierIndex,
request: UnifiedAddressRequest,
) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> {
self.to_unified_incoming_viewing_key()
@ -889,8 +979,10 @@ pub struct UnifiedIncomingViewingKey {
sapling: Option<::sapling::zip32::IncomingViewingKey>,
#[cfg(feature = "orchard")]
orchard: Option<orchard::keys::IncomingViewingKey>,
/// Stores the unrecognized elements of the unified encoding.
unknown: Vec<(u32, Vec<u8>)>,
unknown_data: Vec<(u32, Vec<u8>)>,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
unknown_metadata: Vec<(u32, Vec<u8>)>,
}
impl UnifiedIncomingViewingKey {
@ -906,7 +998,10 @@ impl UnifiedIncomingViewingKey {
>,
#[cfg(feature = "sapling")] sapling: Option<::sapling::zip32::IncomingViewingKey>,
#[cfg(feature = "orchard")] orchard: Option<orchard::keys::IncomingViewingKey>,
// TODO: Implement construction of UIVKs with metadata items.
unknown_data: Vec<(u32, Vec<u8>)>,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
unknown_metadata: Vec<(u32, Vec<u8>)>,
) -> UnifiedIncomingViewingKey {
UnifiedIncomingViewingKey {
#[cfg(feature = "transparent-inputs")]
@ -917,7 +1012,10 @@ impl UnifiedIncomingViewingKey {
orchard,
// We don't allow constructing new UFVKs with unknown items, but we store
// this to allow parsing such UFVKs.
unknown: vec![],
unknown_data,
expiry_height,
expiry_time,
unknown_metadata,
}
}
@ -925,7 +1023,7 @@ impl UnifiedIncomingViewingKey {
///
/// [ZIP 316]: https://zips.z.cash/zip-0316
pub fn decode<P: consensus::Parameters>(params: &P, encoding: &str) -> Result<Self, String> {
let (net, ufvk) = unified::Uivk::decode(encoding).map_err(|e| e.to_string())?;
let (net, uivk) = unified::Uivk::decode(encoding).map_err(|e| e.to_string())?;
let expected_net = params.network_type();
if net != expected_net {
return Err(format!(
@ -934,62 +1032,73 @@ impl UnifiedIncomingViewingKey {
));
}
Self::parse(&ufvk).map_err(|e| e.to_string())
Self::parse(&uivk).map_err(|e| e.to_string())
}
/// Constructs a unified incoming viewing key from a parsed unified encoding.
fn parse(uivk: &Uivk) -> Result<Self, DecodingError> {
fn parse(uivk: &zcash_address::unified::Uivk) -> Result<Self, DecodingError> {
#[cfg(feature = "orchard")]
let mut orchard = None;
#[cfg(feature = "sapling")]
let mut sapling = None;
#[cfg(feature = "transparent-inputs")]
let mut transparent = None;
let mut unknown = vec![];
let mut unknown_data = vec![];
let mut expiry_height = None;
let mut expiry_time = None;
let mut unknown_metadata = vec![];
// We can use as-parsed order here for efficiency, because we're breaking out the
// receivers we support from the unknown receivers.
for receiver in uivk.items_as_parsed() {
match receiver {
unified::Ivk::Orchard(data) => {
Item::Data(unified::Ivk::Orchard(data)) => {
#[cfg(feature = "orchard")]
{
orchard = Some(
Option::from(orchard::keys::IncomingViewingKey::from_bytes(data))
.ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard))?,
.ok_or(DecodingError::KeyDataInvalid(Typecode::ORCHARD))?,
);
}
#[cfg(not(feature = "orchard"))]
unknown.push((u32::from(unified::Typecode::Orchard), data.to_vec()));
unknown_data.push((u32::from(unified::Typecode::ORCHARD), data.to_vec()));
}
unified::Ivk::Sapling(data) => {
Item::Data(unified::Ivk::Sapling(data)) => {
#[cfg(feature = "sapling")]
{
sapling = Some(
Option::from(::sapling::zip32::IncomingViewingKey::from_bytes(data))
.ok_or(DecodingError::KeyDataInvalid(Typecode::Sapling))?,
.ok_or(DecodingError::KeyDataInvalid(Typecode::SAPLING))?,
);
}
#[cfg(not(feature = "sapling"))]
unknown.push((u32::from(unified::Typecode::Sapling), data.to_vec()));
unknown_data.push((u32::from(unified::Typecode::SAPLING), data.to_vec()));
}
unified::Ivk::P2pkh(data) => {
Item::Data(unified::Ivk::P2pkh(data)) => {
#[cfg(feature = "transparent-inputs")]
{
transparent = Some(
legacy::ExternalIvk::deserialize(data)
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh))?,
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH))?,
);
}
#[cfg(not(feature = "transparent-inputs"))]
unknown.push((u32::from(unified::Typecode::P2pkh), data.to_vec()));
unknown_data.push((u32::from(unified::Typecode::P2PKH), data.to_vec()));
}
unified::Ivk::Unknown { typecode, data } => {
unknown.push((*typecode, data.clone()));
Item::Data(unified::Ivk::Unknown { typecode, data }) => {
unknown_data.push((*typecode, data.clone()));
}
Item::Metadata(MetadataItem::ExpiryHeight(h)) => {
expiry_height = Some(BlockHeight::from(*h));
}
Item::Metadata(MetadataItem::ExpiryTime(t)) => {
expiry_time = Some(*t);
}
Item::Metadata(MetadataItem::Unknown { typecode, data }) => {
unknown_metadata.push((*typecode, data.clone()));
}
}
}
@ -1001,7 +1110,10 @@ impl UnifiedIncomingViewingKey {
sapling,
#[cfg(feature = "orchard")]
orchard,
unknown,
unknown_data,
expiry_height,
expiry_time,
unknown_metadata,
})
}
@ -1011,37 +1123,61 @@ impl UnifiedIncomingViewingKey {
}
/// Converts this unified incoming viewing key to a unified encoding.
fn render(&self) -> Uivk {
let items = std::iter::empty().chain(self.unknown.iter().map(|(typecode, data)| {
unified::Ivk::Unknown {
typecode: *typecode,
data: data.clone(),
}
}));
fn render(&self) -> zcash_address::unified::Uivk {
let data_items =
std::iter::empty().chain(self.unknown_data.iter().map(|(typecode, data)| {
unified::Ivk::Unknown {
typecode: *typecode,
data: data.clone(),
}
}));
#[cfg(feature = "orchard")]
let items = items.chain(
let data_items = data_items.chain(
self.orchard
.as_ref()
.map(|ivk| ivk.to_bytes())
.map(unified::Ivk::Orchard),
);
#[cfg(feature = "sapling")]
let items = items.chain(
let data_items = data_items.chain(
self.sapling
.as_ref()
.map(|divk| divk.to_bytes())
.map(unified::Ivk::Sapling),
);
#[cfg(feature = "transparent-inputs")]
let items = items.chain(
let data_items = data_items.chain(
self.transparent
.as_ref()
.map(|tivk| tivk.serialize().try_into().unwrap())
.map(unified::Ivk::P2pkh),
);
unified::Uivk::try_from_items(items.collect())
.expect("UnifiedIncomingViewingKey should only be constructed safely.")
let meta_items = std::iter::empty()
.chain(self.unknown_metadata.iter().map(|(typecode, data)| {
unified::MetadataItem::Unknown {
typecode: *typecode,
data: data.clone(),
}
}))
.chain(
self.expiry_height
.map(|h| unified::MetadataItem::ExpiryHeight(u32::from(h))),
)
.chain(self.expiry_time.map(unified::MetadataItem::ExpiryTime));
zcash_address::unified::Uivk::try_from_items(
if self.expiry_height.is_some() || self.expiry_time.is_some() {
Revision::R1
} else {
Revision::R0
},
data_items
.map(Item::Data)
.chain(meta_items.map(Item::Metadata))
.collect(),
)
.expect("UnifiedIncomingViewingKey should only be constructed safely.")
}
/// Returns the Transparent external IVK, if present.
@ -1062,6 +1198,26 @@ impl UnifiedIncomingViewingKey {
&self.orchard
}
/// Returns any unknown data items parsed from the encoded form of the key.
pub fn unknown_data(&self) -> &[(u32, Vec<u8>)] {
self.unknown_data.as_ref()
}
/// Returns the expiration height for this key.
pub fn expiry_height(&self) -> Option<BlockHeight> {
self.expiry_height
}
/// Returns the expiration time for this key.
pub fn expiry_time(&self) -> Option<u64> {
self.expiry_time
}
/// Returns any unknown metadata items parsed from the encoded form of the key.
pub fn unknown_metadata(&self) -> &[(u32, Vec<u8>)] {
self.unknown_metadata.as_ref()
}
/// Attempts to derive the Unified Address for the given diversifier index and
/// receiver types.
///
@ -1076,7 +1232,7 @@ impl UnifiedIncomingViewingKey {
if request.has_orchard {
#[cfg(not(feature = "orchard"))]
return Err(AddressGenerationError::ReceiverTypeNotSupported(
Typecode::Orchard,
Typecode::ORCHARD,
));
#[cfg(feature = "orchard")]
@ -1084,7 +1240,7 @@ impl UnifiedIncomingViewingKey {
let orchard_j = orchard::keys::DiversifierIndex::from(*_j.as_bytes());
orchard = Some(oivk.address_at(orchard_j))
} else {
return Err(AddressGenerationError::KeyNotAvailable(Typecode::Orchard));
return Err(AddressGenerationError::KeyNotAvailable(Typecode::ORCHARD));
}
}
@ -1093,7 +1249,7 @@ impl UnifiedIncomingViewingKey {
if request.has_sapling {
#[cfg(not(feature = "sapling"))]
return Err(AddressGenerationError::ReceiverTypeNotSupported(
Typecode::Sapling,
Typecode::SAPLING,
));
#[cfg(feature = "sapling")]
@ -1106,7 +1262,7 @@ impl UnifiedIncomingViewingKey {
.ok_or(AddressGenerationError::InvalidSaplingDiversifierIndex(_j))?,
);
} else {
return Err(AddressGenerationError::KeyNotAvailable(Typecode::Sapling));
return Err(AddressGenerationError::KeyNotAvailable(Typecode::SAPLING));
}
}
@ -1115,7 +1271,7 @@ impl UnifiedIncomingViewingKey {
if request.has_p2pkh {
#[cfg(not(feature = "transparent-inputs"))]
return Err(AddressGenerationError::ReceiverTypeNotSupported(
Typecode::P2pkh,
Typecode::P2PKH,
));
#[cfg(feature = "transparent-inputs")]
@ -1131,20 +1287,21 @@ impl UnifiedIncomingViewingKey {
.map_err(|_| AddressGenerationError::InvalidTransparentChildIndex(_j))?,
);
} else {
return Err(AddressGenerationError::KeyNotAvailable(Typecode::P2pkh));
return Err(AddressGenerationError::KeyNotAvailable(Typecode::P2PKH));
}
}
#[cfg(not(feature = "transparent-inputs"))]
let transparent = None;
UnifiedAddress::from_receivers(
Ok(UnifiedAddress::new_internal(
#[cfg(feature = "orchard")]
orchard,
#[cfg(feature = "sapling")]
sapling,
transparent,
)
.ok_or(AddressGenerationError::ShieldedReceiverRequired)
std::cmp::min(self.expiry_height, request.expiry_height),
std::cmp::min(self.expiry_time, request.expiry_time),
))
}
/// Searches the diversifier space starting at diversifier index `j` for one which will
@ -1153,7 +1310,6 @@ impl UnifiedIncomingViewingKey {
///
/// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features
/// required to satisfy the unified address request are not properly enabled.
#[allow(unused_mut)]
pub fn find_address(
&self,
mut j: DiversifierIndex,
@ -1181,6 +1337,8 @@ impl UnifiedIncomingViewingKey {
Err(AddressGenerationError::InvalidSaplingDiversifierIndex(_)) => {
if j.increment().is_err() {
return Err(AddressGenerationError::DiversifierSpaceExhausted);
} else {
continue;
}
}
Err(other) => {
@ -1236,7 +1394,7 @@ mod tests {
#[cfg(any(feature = "sapling", feature = "orchard"))]
use {
super::{UnifiedFullViewingKey, UnifiedIncomingViewingKey},
zcash_address::unified::{Encoding, Uivk},
zcash_address::unified::Encoding,
};
#[cfg(feature = "orchard")]
@ -1372,13 +1530,13 @@ mod tests {
feature = "sapling",
feature = "transparent-inputs"
))]
assert_eq!(decoded_with_t.unknown.len(), 0);
assert_eq!(decoded_with_t.unknown_data.len(), 0);
#[cfg(all(
feature = "orchard",
feature = "sapling",
not(feature = "transparent-inputs")
))]
assert_eq!(decoded_with_t.unknown.len(), 1);
assert_eq!(decoded_with_t.unknown_data.len(), 1);
// Orchard enabled
#[cfg(all(
@ -1386,13 +1544,13 @@ mod tests {
not(feature = "sapling"),
feature = "transparent-inputs"
))]
assert_eq!(decoded_with_t.unknown.len(), 1);
assert_eq!(decoded_with_t.unknown_data.len(), 1);
#[cfg(all(
feature = "orchard",
not(feature = "sapling"),
not(feature = "transparent-inputs")
))]
assert_eq!(decoded_with_t.unknown.len(), 2);
assert_eq!(decoded_with_t.unknown_data.len(), 2);
// Sapling enabled
#[cfg(all(
@ -1400,13 +1558,13 @@ mod tests {
feature = "sapling",
feature = "transparent-inputs"
))]
assert_eq!(decoded_with_t.unknown.len(), 1);
assert_eq!(decoded_with_t.unknown_data.len(), 1);
#[cfg(all(
not(feature = "orchard"),
feature = "sapling",
not(feature = "transparent-inputs")
))]
assert_eq!(decoded_with_t.unknown.len(), 2);
assert_eq!(decoded_with_t.unknown_data.len(), 2);
}
#[test]
@ -1435,7 +1593,10 @@ mod tests {
}
let ua = ufvk
.address(d_idx, UnifiedAddressRequest::unsafe_new(false, true, true))
.address(
d_idx,
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, true),
)
.unwrap_or_else(|err| {
panic!(
"unified address generation failed for account {}: {:?}",
@ -1497,6 +1658,10 @@ mod tests {
sapling,
#[cfg(feature = "orchard")]
orchard,
vec![],
None,
None,
vec![],
);
let encoded = uivk.render().encode(&NetworkType::Main);
@ -1517,7 +1682,7 @@ mod tests {
assert_eq!(encoded, _encoded_no_t);
}
let decoded = UnifiedIncomingViewingKey::parse(&Uivk::decode(&encoded).unwrap().1).unwrap();
let decoded = UnifiedIncomingViewingKey::decode(&MAIN_NETWORK, &encoded).unwrap();
let reencoded = decoded.render().encode(&NetworkType::Main);
assert_eq!(encoded, reencoded);
@ -1538,7 +1703,7 @@ mod tests {
);
let decoded_with_t =
UnifiedIncomingViewingKey::parse(&Uivk::decode(encoded_with_t).unwrap().1).unwrap();
UnifiedIncomingViewingKey::decode(&MAIN_NETWORK, encoded_with_t).unwrap();
#[cfg(feature = "transparent-inputs")]
assert_eq!(
decoded_with_t.transparent.map(|t| t.serialize()),
@ -1551,13 +1716,13 @@ mod tests {
feature = "sapling",
feature = "transparent-inputs"
))]
assert_eq!(decoded_with_t.unknown.len(), 0);
assert_eq!(decoded_with_t.unknown_data.len(), 0);
#[cfg(all(
feature = "orchard",
feature = "sapling",
not(feature = "transparent-inputs")
))]
assert_eq!(decoded_with_t.unknown.len(), 1);
assert_eq!(decoded_with_t.unknown_data.len(), 1);
// Orchard enabled
#[cfg(all(
@ -1565,13 +1730,13 @@ mod tests {
not(feature = "sapling"),
feature = "transparent-inputs"
))]
assert_eq!(decoded_with_t.unknown.len(), 1);
assert_eq!(decoded_with_t.unknown_data.len(), 1);
#[cfg(all(
feature = "orchard",
not(feature = "sapling"),
not(feature = "transparent-inputs")
))]
assert_eq!(decoded_with_t.unknown.len(), 2);
assert_eq!(decoded_with_t.unknown_data.len(), 2);
// Sapling enabled
#[cfg(all(
@ -1579,13 +1744,13 @@ mod tests {
feature = "sapling",
feature = "transparent-inputs"
))]
assert_eq!(decoded_with_t.unknown.len(), 1);
assert_eq!(decoded_with_t.unknown_data.len(), 1);
#[cfg(all(
not(feature = "orchard"),
feature = "sapling",
not(feature = "transparent-inputs")
))]
assert_eq!(decoded_with_t.unknown.len(), 2);
assert_eq!(decoded_with_t.unknown_data.len(), 2);
}
#[test]
@ -1616,7 +1781,10 @@ mod tests {
}
let ua = uivk
.address(d_idx, UnifiedAddressRequest::unsafe_new(false, true, true))
.address(
d_idx,
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, true),
)
.unwrap_or_else(|err| {
panic!(
"unified address generation failed for account {}: {:?}",