Compare commits
12 Commits
5df164b2a4
...
5c6a6a4c86
Author | SHA1 | Date |
---|---|---|
str4d | 5c6a6a4c86 | |
Kris Nuttycombe | d2aa6cfc7f | |
Kris Nuttycombe | aeac544aed | |
Daira-Emma Hopwood | ea82dbeb64 | |
Kris Nuttycombe | f28aa6b304 | |
Kris Nuttycombe | b60600a4c3 | |
Kris Nuttycombe | 86e1181259 | |
Kris Nuttycombe | 3ea7d84183 | |
Kris Nuttycombe | d982d7826a | |
Kris Nuttycombe | fdf86ad740 | |
Kris Nuttycombe | 07d5aa4a79 | |
Kris Nuttycombe | bbb8d1090a |
|
@ -3068,6 +3068,7 @@ dependencies = [
|
||||||
"zcash_proofs",
|
"zcash_proofs",
|
||||||
"zcash_protocol",
|
"zcash_protocol",
|
||||||
"zip32",
|
"zip32",
|
||||||
|
"zip321",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3327,3 +3328,15 @@ dependencies = [
|
||||||
"memuse",
|
"memuse",
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip321"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"nom",
|
||||||
|
"percent-encoding",
|
||||||
|
"proptest",
|
||||||
|
"zcash_address",
|
||||||
|
"zcash_protocol",
|
||||||
|
]
|
||||||
|
|
|
@ -5,6 +5,7 @@ members = [
|
||||||
"components/zcash_address",
|
"components/zcash_address",
|
||||||
"components/zcash_encoding",
|
"components/zcash_encoding",
|
||||||
"components/zcash_protocol",
|
"components/zcash_protocol",
|
||||||
|
"components/zip321",
|
||||||
"zcash_client_backend",
|
"zcash_client_backend",
|
||||||
"zcash_client_sqlite",
|
"zcash_client_sqlite",
|
||||||
"zcash_extensions",
|
"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_encoding = { version = "0.2", path = "components/zcash_encoding" }
|
||||||
zcash_keys = { version = "0.2", path = "zcash_keys" }
|
zcash_keys = { version = "0.2", path = "zcash_keys" }
|
||||||
zcash_protocol = { version = "0.1", path = "components/zcash_protocol" }
|
zcash_protocol = { version = "0.1", path = "components/zcash_protocol" }
|
||||||
|
zip321 = { version = "0.0", path = "components/zip321" }
|
||||||
|
|
||||||
zcash_note_encryption = "0.4"
|
zcash_note_encryption = "0.4"
|
||||||
zcash_primitives = { version = "0.15", path = "zcash_primitives", default-features = false }
|
zcash_primitives = { version = "0.15", path = "zcash_primitives", default-features = false }
|
||||||
|
|
|
@ -7,6 +7,13 @@ and this library adheres to Rust's notion of
|
||||||
|
|
||||||
## [Unreleased]
|
## [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}`
|
||||||
|
- Module `zcash_address::testing` under the `test-dependencies` feature.
|
||||||
|
- Module `zcash_address::unified::address::testing` under the
|
||||||
|
`test-dependencies` feature.
|
||||||
|
|
||||||
## [0.3.2] - 2024-03-06
|
## [0.3.2] - 2024-03-06
|
||||||
### Added
|
### Added
|
||||||
- `zcash_address::convert`:
|
- `zcash_address::convert`:
|
||||||
|
|
|
@ -19,18 +19,18 @@ all-features = true
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bech32 = "0.9"
|
bech32.workspace = true
|
||||||
bs58 = { version = "0.5", features = ["check"] }
|
bs58.workspace = true
|
||||||
f4jumble = { version = "0.1", path = "../f4jumble" }
|
f4jumble = { version = "0.1", path = "../f4jumble" }
|
||||||
zcash_protocol.workspace = true
|
zcash_protocol.workspace = true
|
||||||
zcash_encoding.workspace = true
|
zcash_encoding.workspace = true
|
||||||
|
proptest = { workspace = true, optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_matches = "1.3.0"
|
assert_matches.workspace = true
|
||||||
proptest = "1"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
test-dependencies = []
|
test-dependencies = ["dep:proptest"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
bench = false
|
bench = false
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use zcash_protocol::{PoolType, ShieldedProtocol};
|
||||||
|
|
||||||
use super::{private::SealedItem, ParseError, Typecode};
|
use super::{private::SealedItem, ParseError, Typecode};
|
||||||
|
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::convert::{TryFrom, TryInto};
|
||||||
|
@ -101,6 +103,30 @@ impl SealedItem for Receiver {
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct Address(pub(crate) Vec<Receiver>);
|
pub struct Address(pub(crate) Vec<Receiver>);
|
||||||
|
|
||||||
|
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.0.iter().any(|r| match r {
|
||||||
|
Receiver::Orchard(_) => pool_type == PoolType::Shielded(ShieldedProtocol::Orchard),
|
||||||
|
Receiver::Sapling(_) => pool_type == PoolType::Shielded(ShieldedProtocol::Sapling),
|
||||||
|
Receiver::P2pkh(_) | Receiver::P2sh(_) => pool_type == PoolType::Transparent,
|
||||||
|
Receiver::Unknown { .. } => false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether this address contains the given receiver.
|
||||||
|
pub fn contains_receiver(&self, receiver: &Receiver) -> bool {
|
||||||
|
self.0.contains(receiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether this address can receive a memo.
|
||||||
|
pub fn can_receive_memo(&self) -> bool {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.any(|r| matches!(r, Receiver::Sapling(_) | Receiver::Orchard(_)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl super::private::SealedContainer for Address {
|
impl super::private::SealedContainer for Address {
|
||||||
/// The HRP for a Bech32m-encoded mainnet Unified Address.
|
/// The HRP for a Bech32m-encoded mainnet Unified Address.
|
||||||
///
|
///
|
||||||
|
@ -133,27 +159,19 @@ impl super::Container for Address {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-dependencies"))]
|
#[cfg(feature = "test-dependencies")]
|
||||||
pub mod test_vectors;
|
pub mod testing {
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use assert_matches::assert_matches;
|
|
||||||
use zcash_encoding::MAX_COMPACT_SIZE;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
kind::unified::{private::SealedContainer, Container, Encoding},
|
|
||||||
Network,
|
|
||||||
};
|
|
||||||
|
|
||||||
use proptest::{
|
use proptest::{
|
||||||
array::{uniform11, uniform20, uniform32},
|
array::{uniform11, uniform20, uniform32},
|
||||||
collection::vec,
|
collection::vec,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
sample::select,
|
sample::select,
|
||||||
|
strategy::Strategy,
|
||||||
};
|
};
|
||||||
|
use zcash_encoding::MAX_COMPACT_SIZE;
|
||||||
|
|
||||||
use super::{Address, ParseError, Receiver, Typecode};
|
use super::{Address, Receiver};
|
||||||
|
use crate::unified::Typecode;
|
||||||
|
|
||||||
prop_compose! {
|
prop_compose! {
|
||||||
fn uniform43()(a in uniform11(0u8..), b in uniform32(0u8..)) -> [u8; 43] {
|
fn uniform43()(a in uniform11(0u8..), b in uniform32(0u8..)) -> [u8; 43] {
|
||||||
|
@ -164,11 +182,13 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn arb_transparent_typecode() -> impl Strategy<Value = Typecode> {
|
/// A strategy to generate an arbitrary transparent typecode.
|
||||||
|
pub fn arb_transparent_typecode() -> impl Strategy<Value = Typecode> {
|
||||||
select(vec![Typecode::P2pkh, Typecode::P2sh])
|
select(vec![Typecode::P2pkh, Typecode::P2sh])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn arb_shielded_typecode() -> impl Strategy<Value = Typecode> {
|
/// A strategy to generate an arbitrary shielded (Sapling, Orchard, or unknown) typecode.
|
||||||
|
pub fn arb_shielded_typecode() -> impl Strategy<Value = Typecode> {
|
||||||
prop_oneof![
|
prop_oneof![
|
||||||
Just(Typecode::Sapling),
|
Just(Typecode::Sapling),
|
||||||
Just(Typecode::Orchard),
|
Just(Typecode::Orchard),
|
||||||
|
@ -179,7 +199,7 @@ mod tests {
|
||||||
/// A strategy to generate an arbitrary valid set of typecodes without
|
/// A strategy to generate an arbitrary valid set of typecodes without
|
||||||
/// duplication and containing only one of P2sh and P2pkh transparent
|
/// duplication and containing only one of P2sh and P2pkh transparent
|
||||||
/// typecodes. The resulting vector will be sorted in encoding order.
|
/// typecodes. The resulting vector will be sorted in encoding order.
|
||||||
fn arb_typecodes() -> impl Strategy<Value = Vec<Typecode>> {
|
pub fn arb_typecodes() -> impl Strategy<Value = Vec<Typecode>> {
|
||||||
prop::option::of(arb_transparent_typecode()).prop_flat_map(|transparent| {
|
prop::option::of(arb_transparent_typecode()).prop_flat_map(|transparent| {
|
||||||
prop::collection::hash_set(arb_shielded_typecode(), 1..4).prop_map(move |xs| {
|
prop::collection::hash_set(arb_shielded_typecode(), 1..4).prop_map(move |xs| {
|
||||||
let mut typecodes: Vec<_> = xs.into_iter().chain(transparent).collect();
|
let mut typecodes: Vec<_> = xs.into_iter().chain(transparent).collect();
|
||||||
|
@ -189,7 +209,11 @@ mod tests {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn arb_unified_address_for_typecodes(
|
/// Generates an arbitrary Unified address containing receivers corresponding to the provided
|
||||||
|
/// set of typecodes. The receivers of this address are likely to not represent valid protocol
|
||||||
|
/// receivers, and should only be used for testing parsing and/or encoding functions that do
|
||||||
|
/// not concern themselves with the validity of the underlying receivers.
|
||||||
|
pub fn arb_unified_address_for_typecodes(
|
||||||
typecodes: Vec<Typecode>,
|
typecodes: Vec<Typecode>,
|
||||||
) -> impl Strategy<Value = Vec<Receiver>> {
|
) -> impl Strategy<Value = Vec<Receiver>> {
|
||||||
typecodes
|
typecodes
|
||||||
|
@ -206,11 +230,33 @@ mod tests {
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn arb_unified_address() -> impl Strategy<Value = Address> {
|
/// Generates an arbitrary Unified address. The receivers of this address are likely to not
|
||||||
|
/// represent valid protocol receivers, and should only be used for testing parsing and/or
|
||||||
|
/// encoding functions that do not concern themselves with the validity of the underlying
|
||||||
|
/// receivers.
|
||||||
|
pub fn arb_unified_address() -> impl Strategy<Value = Address> {
|
||||||
arb_typecodes()
|
arb_typecodes()
|
||||||
.prop_flat_map(arb_unified_address_for_typecodes)
|
.prop_flat_map(arb_unified_address_for_typecodes)
|
||||||
.prop_map(Address)
|
.prop_map(Address)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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, Container, Encoding},
|
||||||
|
unified::address::testing::arb_unified_address,
|
||||||
|
Network,
|
||||||
|
};
|
||||||
|
|
||||||
|
use proptest::{prelude::*, sample::select};
|
||||||
|
|
||||||
|
use super::{Address, ParseError, Receiver, Typecode};
|
||||||
|
|
||||||
proptest! {
|
proptest! {
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -141,7 +141,9 @@ pub use convert::{
|
||||||
};
|
};
|
||||||
pub use encoding::ParseError;
|
pub use encoding::ParseError;
|
||||||
pub use kind::unified;
|
pub use kind::unified;
|
||||||
|
use kind::unified::Receiver;
|
||||||
pub use zcash_protocol::consensus::NetworkType as Network;
|
pub use zcash_protocol::consensus::NetworkType as Network;
|
||||||
|
use zcash_protocol::{PoolType, ShieldedProtocol};
|
||||||
|
|
||||||
/// A Zcash address.
|
/// A Zcash address.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ and this library adheres to Rust's notion of
|
||||||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Added
|
||||||
|
- `zcash_protocol::PoolType::{TRANSPARENT, SAPLING, ORCHARD}`
|
||||||
|
|
||||||
## [0.1.1] - 2024-03-25
|
## [0.1.1] - 2024-03-25
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -42,6 +42,12 @@ pub enum PoolType {
|
||||||
Shielded(ShieldedProtocol),
|
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 {
|
impl fmt::Display for PoolType {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
@ -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`
|
|
@ -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
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
|
@ -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.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//! Reference implementation of the ZIP-321 standard for payment requests.
|
//! 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.
|
//! for interpreting and producing valid ZIP 321 URIs.
|
||||||
//!
|
//!
|
||||||
//! The specification for ZIP 321 URIs may be found at <https://zips.z.cash/zip-0321>
|
//! 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,
|
character::complete::char, combinator::all_consuming, multi::separated_list0,
|
||||||
sequence::preceded,
|
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.
|
/// Errors that may be produced in decoding of payment requests.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
@ -45,6 +45,12 @@ pub enum Zip321Error {
|
||||||
ParseError(String),
|
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 {
|
impl Display for Zip321Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
@ -92,14 +98,14 @@ impl std::error::Error for Zip321Error {
|
||||||
|
|
||||||
/// Converts a [`MemoBytes`] value to a ZIP 321 compatible base64-encoded string.
|
/// 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 {
|
pub fn memo_to_base64(memo: &MemoBytes) -> String {
|
||||||
BASE64_URL_SAFE_NO_PAD.encode(memo.as_slice())
|
BASE64_URL_SAFE_NO_PAD.encode(memo.as_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a [`MemoBytes`] value from a ZIP 321 compatible base64-encoded string.
|
/// 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> {
|
pub fn memo_from_base64(s: &str) -> Result<MemoBytes, Zip321Error> {
|
||||||
BASE64_URL_SAFE_NO_PAD
|
BASE64_URL_SAFE_NO_PAD
|
||||||
.decode(s)
|
.decode(s)
|
||||||
|
@ -110,29 +116,55 @@ pub fn memo_from_base64(s: &str) -> Result<MemoBytes, Zip321Error> {
|
||||||
/// A single payment being requested.
|
/// A single payment being requested.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Payment {
|
pub struct Payment {
|
||||||
/// The payment address to which the payment should be sent.
|
/// The address to which the payment should be sent.
|
||||||
pub recipient_address: Address,
|
recipient_address: ZcashAddress,
|
||||||
/// The amount of the payment that is being requested.
|
/// 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.
|
/// A memo that, if included, must be provided with the payment.
|
||||||
/// If a memo is present and [`recipient_address`] is not a shielded
|
/// If a memo is present and [`recipient_address`] is not a shielded
|
||||||
/// address, the wallet should report an error.
|
/// address, the wallet should report an error.
|
||||||
///
|
///
|
||||||
/// [`recipient_address`]: #structfield.recipient_address
|
/// [`recipient_address`]: #structfield.recipient_address
|
||||||
pub memo: Option<MemoBytes>,
|
memo: Option<MemoBytes>,
|
||||||
/// A human-readable label for this payment within the larger structure
|
/// A human-readable label for this payment within the larger structure
|
||||||
/// of the transaction request.
|
/// of the transaction request.
|
||||||
pub label: Option<String>,
|
label: Option<String>,
|
||||||
/// A human-readable message to be displayed to the user describing the
|
/// A human-readable message to be displayed to the user describing the
|
||||||
/// purpose of this payment.
|
/// purpose of this payment.
|
||||||
pub message: Option<String>,
|
message: Option<String>,
|
||||||
/// A list of other arbitrary key/value pairs associated with this payment.
|
/// 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 {
|
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.
|
/// 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 {
|
Self {
|
||||||
recipient_address,
|
recipient_address,
|
||||||
amount,
|
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.
|
/// A utility for use in tests to help check round-trip serialization properties.
|
||||||
#[cfg(any(test, feature = "test-dependencies"))]
|
#[cfg(any(test, feature = "test-dependencies"))]
|
||||||
pub(in crate::zip321) fn normalize(&mut self) {
|
pub(crate) fn normalize(&mut self) {
|
||||||
self.other_params.sort();
|
self.other_params.sort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -182,10 +246,7 @@ impl TransactionRequest {
|
||||||
|
|
||||||
// Enforce validity requirements.
|
// Enforce validity requirements.
|
||||||
if !request.payments.is_empty() {
|
if !request.payments.is_empty() {
|
||||||
// It doesn't matter what params we use here, as none of the validity
|
TransactionRequest::from_uri(&request.to_uri())?;
|
||||||
// requirements depend on them.
|
|
||||||
let params = consensus::MAIN_NETWORK;
|
|
||||||
TransactionRequest::from_uri(¶ms, &request.to_uri(¶ms))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(request)
|
Ok(request)
|
||||||
|
@ -218,19 +279,19 @@ impl TransactionRequest {
|
||||||
///
|
///
|
||||||
/// Returns `Err` in the case of overflow, or if the value is
|
/// Returns `Err` in the case of overflow, or if the value is
|
||||||
/// outside the range `0..=MAX_MONEY` zatoshis.
|
/// outside the range `0..=MAX_MONEY` zatoshis.
|
||||||
pub fn total(&self) -> Result<NonNegativeAmount, BalanceError> {
|
pub fn total(&self) -> Result<Zatoshis, BalanceError> {
|
||||||
self.payments
|
self.payments
|
||||||
.values()
|
.values()
|
||||||
.map(|p| p.amount)
|
.map(|p| p.amount)
|
||||||
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| {
|
.fold(Ok(Zatoshis::ZERO), |acc, a| {
|
||||||
(acc? + a).ok_or(BalanceError::Overflow)
|
(acc? + a).ok_or(BalanceError::Overflow)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A utility for use in tests to help check round-trip serialization properties.
|
/// A utility for use in tests to help check round-trip serialization properties.
|
||||||
#[cfg(any(test, feature = "test-dependencies"))]
|
#[cfg(any(test, feature = "test-dependencies"))]
|
||||||
pub(in crate::zip321) fn normalize(&mut self) {
|
pub(crate) fn normalize(&mut self) {
|
||||||
for p in self.payments.values_mut() {
|
for p in &mut self.payments.values_mut() {
|
||||||
p.normalize();
|
p.normalize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -238,10 +299,7 @@ impl TransactionRequest {
|
||||||
/// A utility for use in tests to help check round-trip serialization properties.
|
/// A utility for use in tests to help check round-trip serialization properties.
|
||||||
/// by comparing a two transaction requests for equality after normalization.
|
/// by comparing a two transaction requests for equality after normalization.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(in crate::zip321) fn normalize_and_eq(
|
pub(crate) fn normalize_and_eq(a: &mut TransactionRequest, b: &mut TransactionRequest) -> bool {
|
||||||
a: &mut TransactionRequest,
|
|
||||||
b: &mut TransactionRequest,
|
|
||||||
) -> bool {
|
|
||||||
a.normalize();
|
a.normalize();
|
||||||
b.normalize();
|
b.normalize();
|
||||||
|
|
||||||
|
@ -251,7 +309,7 @@ impl TransactionRequest {
|
||||||
/// Convert this request to a URI string.
|
/// Convert this request to a URI string.
|
||||||
///
|
///
|
||||||
/// Returns None if the payment request is empty.
|
/// 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(
|
fn payment_params(
|
||||||
payment: &Payment,
|
payment: &Payment,
|
||||||
payment_index: Option<usize>,
|
payment_index: Option<usize>,
|
||||||
|
@ -294,7 +352,7 @@ impl TransactionRequest {
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"zcash:{}{}{}",
|
"zcash:{}{}{}",
|
||||||
payment.recipient_address.encode(params),
|
payment.recipient_address.encode(),
|
||||||
if query_params.is_empty() { "" } else { "?" },
|
if query_params.is_empty() { "" } else { "?" },
|
||||||
query_params.join("&")
|
query_params.join("&")
|
||||||
)
|
)
|
||||||
|
@ -307,7 +365,7 @@ impl TransactionRequest {
|
||||||
let idx = if *i == 0 { None } else { Some(*i) };
|
let idx = if *i == 0 { None } else { Some(*i) };
|
||||||
let primary_address = payment.recipient_address.clone();
|
let primary_address = payment.recipient_address.clone();
|
||||||
std::iter::empty()
|
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))
|
.chain(payment_params(payment, idx))
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
|
@ -318,9 +376,9 @@ impl TransactionRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the provided URI to a payment request value.
|
/// 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>
|
// 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)))?;
|
.map_err(|e| Zip321Error::ParseError(format!("Error parsing lead address: {}", e)))?;
|
||||||
|
|
||||||
// Parse the remaining parameters as an undifferentiated list
|
// Parse the remaining parameters as an undifferentiated list
|
||||||
|
@ -329,7 +387,7 @@ impl TransactionRequest {
|
||||||
} else {
|
} else {
|
||||||
all_consuming(preceded(
|
all_consuming(preceded(
|
||||||
char('?'),
|
char('?'),
|
||||||
separated_list0(char('&'), parse::zcashparam(params)),
|
separated_list0(char('&'), parse::zcashparam),
|
||||||
))(rest)
|
))(rest)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
Zip321Error::ParseError(format!("Error parsing query parameters: {}", e))
|
Zip321Error::ParseError(format!("Error parsing query parameters: {}", e))
|
||||||
|
@ -372,13 +430,13 @@ impl TransactionRequest {
|
||||||
|
|
||||||
mod render {
|
mod render {
|
||||||
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
||||||
|
use zcash_address::ZcashAddress;
|
||||||
use zcash_primitives::{
|
use zcash_protocol::{
|
||||||
consensus, transaction::components::amount::NonNegativeAmount,
|
memo::MemoBytes,
|
||||||
transaction::components::amount::COIN,
|
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
|
/// 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
|
/// 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
|
/// Constructs an "address" key/value pair containing the encoded recipient address
|
||||||
/// at the specified parameter index.
|
/// at the specified parameter index.
|
||||||
pub fn addr_param<P: consensus::Parameters>(
|
pub fn addr_param(addr: &ZcashAddress, idx: Option<usize>) -> String {
|
||||||
params: &P,
|
format!("address{}={}", param_index(idx), addr.encode())
|
||||||
addr: &Address,
|
|
||||||
idx: Option<usize>,
|
|
||||||
) -> String {
|
|
||||||
format!("address{}={}", param_index(idx), addr.encode(params))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a [`NonNegativeAmount`] value to a correctly formatted decimal ZEC
|
/// Converts a [`Zatoshis`] value to a correctly formatted decimal ZEC
|
||||||
/// string for inclusion in a ZIP 321 URI.
|
/// value for inclusion in a ZIP 321 URI.
|
||||||
pub fn amount_str(amount: NonNegativeAmount) -> String {
|
pub fn amount_str(amount: Zatoshis) -> String {
|
||||||
let coins = u64::from(amount) / COIN;
|
let coins = u64::from(amount) / COIN;
|
||||||
let zats = u64::from(amount) % COIN;
|
let zats = u64::from(amount) % COIN;
|
||||||
if zats == 0 {
|
if zats == 0 {
|
||||||
|
@ -442,7 +496,7 @@ mod render {
|
||||||
|
|
||||||
/// Constructs an "amount" key/value pair containing the encoded ZEC amount
|
/// Constructs an "amount" key/value pair containing the encoded ZEC amount
|
||||||
/// at the specified parameter index.
|
/// 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))
|
format!("amount{}={}", param_index(idx), amount_str(amount))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -475,23 +529,22 @@ mod parse {
|
||||||
AsChar, IResult, InputTakeAtPosition,
|
AsChar, IResult, InputTakeAtPosition,
|
||||||
};
|
};
|
||||||
use percent_encoding::percent_decode;
|
use percent_encoding::percent_decode;
|
||||||
use zcash_primitives::{
|
use zcash_address::ZcashAddress;
|
||||||
consensus, transaction::components::amount::NonNegativeAmount,
|
|
||||||
transaction::components::amount::COIN,
|
|
||||||
};
|
|
||||||
use zcash_protocol::value::BalanceError;
|
use zcash_protocol::value::BalanceError;
|
||||||
|
use zcash_protocol::{
|
||||||
|
memo::MemoBytes,
|
||||||
|
value::{Zatoshis, COIN},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::address::Address;
|
use super::{memo_from_base64, Payment, Zip321Error};
|
||||||
|
|
||||||
use super::{memo_from_base64, MemoBytes, Payment, Zip321Error};
|
|
||||||
|
|
||||||
/// A data type that defines the possible parameter types which may occur within a
|
/// A data type that defines the possible parameter types which may occur within a
|
||||||
/// ZIP 321 URI.
|
/// ZIP 321 URI.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum Param {
|
pub enum Param {
|
||||||
Addr(Box<Address>),
|
Addr(Box<ZcashAddress>),
|
||||||
Amount(NonNegativeAmount),
|
Amount(Zatoshis),
|
||||||
Memo(MemoBytes),
|
Memo(Box<MemoBytes>),
|
||||||
Label(String),
|
Label(String),
|
||||||
Message(String),
|
Message(String),
|
||||||
Other(String, String),
|
Other(String, String),
|
||||||
|
@ -551,7 +604,7 @@ mod parse {
|
||||||
|
|
||||||
let mut payment = Payment {
|
let mut payment = Payment {
|
||||||
recipient_address: *addr.ok_or(Zip321Error::RecipientMissing(i))?,
|
recipient_address: *addr.ok_or(Zip321Error::RecipientMissing(i))?,
|
||||||
amount: NonNegativeAmount::ZERO,
|
amount: Zatoshis::ZERO,
|
||||||
memo: None,
|
memo: None,
|
||||||
label: None,
|
label: None,
|
||||||
message: None,
|
message: None,
|
||||||
|
@ -561,11 +614,13 @@ mod parse {
|
||||||
for v in vs {
|
for v in vs {
|
||||||
match v {
|
match v {
|
||||||
Param::Amount(a) => payment.amount = a,
|
Param::Amount(a) => payment.amount = a,
|
||||||
Param::Memo(m) => match payment.recipient_address {
|
Param::Memo(m) => {
|
||||||
Address::Sapling(_) | Address::Unified(_) => payment.memo = Some(m),
|
if payment.recipient_address.can_receive_memo() {
|
||||||
Address::Transparent(_) => return Err(Zip321Error::TransparentMemo(i)),
|
payment.memo = Some(*m);
|
||||||
},
|
} else {
|
||||||
|
return Err(Zip321Error::TransparentMemo(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
Param::Label(m) => payment.label = Some(m),
|
Param::Label(m) => payment.label = Some(m),
|
||||||
Param::Message(m) => payment.message = Some(m),
|
Param::Message(m) => payment.message = Some(m),
|
||||||
Param::Other(n, m) => payment.other_params.push((n, m)),
|
Param::Other(n, m) => payment.other_params.push((n, m)),
|
||||||
|
@ -577,41 +632,35 @@ mod parse {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses and consumes the leading "zcash:\[address\]" from a ZIP 321 URI.
|
/// Parses and consumes the leading "zcash:\[address\]" from a ZIP 321 URI.
|
||||||
pub fn lead_addr<P: consensus::Parameters>(
|
pub fn lead_addr(input: &str) -> IResult<&str, Option<IndexedParam>> {
|
||||||
params: &P,
|
|
||||||
) -> impl Fn(&str) -> IResult<&str, Option<IndexedParam>> + '_ {
|
|
||||||
move |input: &str| {
|
|
||||||
map_opt(
|
map_opt(
|
||||||
preceded(tag("zcash:"), take_till(|c| c == '?')),
|
preceded(tag("zcash:"), take_till(|c| c == '?')),
|
||||||
|addr_str: &str| {
|
|addr_str: &str| {
|
||||||
if addr_str.is_empty() {
|
if addr_str.is_empty() {
|
||||||
Some(None) // no address is ok, so wrap in `Some`
|
Some(None) // no address is ok, so wrap in `Some`
|
||||||
} else {
|
} else {
|
||||||
// `decode` returns `None` on error, which we want to
|
// `try_from_encoded(..).ok()` returns `None` on error, which we want to then
|
||||||
// then cause `map_opt` to fail.
|
// cause `map_opt` to fail.
|
||||||
Address::decode(params, addr_str).map(|a| {
|
ZcashAddress::try_from_encoded(addr_str)
|
||||||
|
.map(|a| {
|
||||||
Some(IndexedParam {
|
Some(IndexedParam {
|
||||||
param: Param::Addr(Box::new(a)),
|
param: Param::Addr(Box::new(a)),
|
||||||
payment_index: 0,
|
payment_index: 0,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.ok()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)(input)
|
)(input)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// The primary parser for <name>=<value> query-string parameter pair.
|
/// The primary parser for <name>=<value> query-string parameter pair.
|
||||||
pub fn zcashparam<P: consensus::Parameters>(
|
pub fn zcashparam(input: &str) -> IResult<&str, IndexedParam> {
|
||||||
params: &P,
|
|
||||||
) -> impl Fn(&str) -> IResult<&str, IndexedParam> + '_ {
|
|
||||||
move |input| {
|
|
||||||
map_res(
|
map_res(
|
||||||
separated_pair(indexed_name, char('='), recognize(qchars)),
|
separated_pair(indexed_name, char('='), recognize(qchars)),
|
||||||
move |r| to_indexed_param(params, r),
|
to_indexed_param,
|
||||||
)(input)
|
)(input)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Extension for the `alphanumeric0` parser which extends that parser
|
/// Extension for the `alphanumeric0` parser which extends that parser
|
||||||
/// by also permitting the characters that are members of the `allowed`
|
/// by also permitting the characters that are members of the `allowed`
|
||||||
|
@ -652,7 +701,7 @@ mod parse {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a value in decimal ZEC.
|
/// 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(
|
map_res(
|
||||||
all_consuming(tuple((
|
all_consuming(tuple((
|
||||||
digit1,
|
digit1,
|
||||||
|
@ -678,28 +727,29 @@ mod parse {
|
||||||
.checked_mul(COIN)
|
.checked_mul(COIN)
|
||||||
.and_then(|coin_zats| coin_zats.checked_add(zats))
|
.and_then(|coin_zats| coin_zats.checked_add(zats))
|
||||||
.ok_or(BalanceError::Overflow)
|
.ok_or(BalanceError::Overflow)
|
||||||
.and_then(NonNegativeAmount::from_u64)
|
.and_then(Zatoshis::from_u64)
|
||||||
.map_err(|_| format!("Not a valid amount: {} ZEC", input))
|
.map_err(|_| format!("Not a valid zat amount: {}.{}", coins, zats))
|
||||||
},
|
},
|
||||||
)(input)
|
)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_indexed_param<'a, P: consensus::Parameters>(
|
fn to_indexed_param(
|
||||||
params: &'a P,
|
|
||||||
((name, iopt), value): ((&str, Option<&str>), &str),
|
((name, iopt), value): ((&str, Option<&str>), &str),
|
||||||
) -> Result<IndexedParam, String> {
|
) -> Result<IndexedParam, String> {
|
||||||
let param = match name {
|
let param = match name {
|
||||||
"address" => Address::decode(params, value)
|
"address" => ZcashAddress::try_from_encoded(value)
|
||||||
.map(Box::new)
|
.map(Box::new)
|
||||||
.map(Param::Addr)
|
.map(Param::Addr)
|
||||||
.ok_or(format!(
|
.map_err(|err| {
|
||||||
"Could not interpret {} as a valid Zcash address.",
|
format!(
|
||||||
value
|
"Could not interpret {} as a valid Zcash address: {}",
|
||||||
)),
|
value, err
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
|
||||||
"amount" => parse_amount(value)
|
"amount" => parse_amount(value)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
.map(|(_, amt)| Param::Amount(amt)),
|
.map(|(_, a)| Param::Amount(a)),
|
||||||
|
|
||||||
"label" => percent_decode(value.as_bytes())
|
"label" => percent_decode(value.as_bytes())
|
||||||
.decode_utf8()
|
.decode_utf8()
|
||||||
|
@ -712,6 +762,7 @@ mod parse {
|
||||||
.map_err(|e| e.to_string()),
|
.map_err(|e| e.to_string()),
|
||||||
|
|
||||||
"memo" => memo_from_base64(value)
|
"memo" => memo_from_base64(value)
|
||||||
|
.map(Box::new)
|
||||||
.map(Param::Memo)
|
.map(Param::Memo)
|
||||||
.map_err(|e| format!("Decoded memo was invalid: {:?}", e)),
|
.map_err(|e| format!("Decoded memo was invalid: {:?}", e)),
|
||||||
|
|
||||||
|
@ -743,25 +794,13 @@ pub mod testing {
|
||||||
use proptest::collection::vec;
|
use proptest::collection::vec;
|
||||||
use proptest::option;
|
use proptest::option;
|
||||||
use proptest::prelude::{any, prop_compose};
|
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};
|
use super::{MemoBytes, Payment, TransactionRequest};
|
||||||
pub const VALID_PARAMNAME: &str = "[a-zA-Z][a-zA-Z0-9+-]*";
|
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! {
|
prop_compose! {
|
||||||
pub fn arb_valid_memo()(bytes in vec(any::<u8>(), 0..512)) -> MemoBytes {
|
pub fn arb_valid_memo()(bytes in vec(any::<u8>(), 0..512)) -> MemoBytes {
|
||||||
MemoBytes::from_bytes(&bytes).unwrap()
|
MemoBytes::from_bytes(&bytes).unwrap()
|
||||||
|
@ -769,24 +808,20 @@ pub mod testing {
|
||||||
}
|
}
|
||||||
|
|
||||||
prop_compose! {
|
prop_compose! {
|
||||||
pub fn arb_zip321_payment()(
|
pub fn arb_zip321_payment(network: NetworkType)(
|
||||||
recipient_address in arb_addr(UA_REQUEST),
|
recipient_address in arb_address(network),
|
||||||
amount in arb_nonnegative_amount(),
|
amount in arb_zatoshis(),
|
||||||
memo in option::of(arb_valid_memo()),
|
memo in option::of(arb_valid_memo()),
|
||||||
message in option::of(any::<String>()),
|
message in option::of(any::<String>()),
|
||||||
label in option::of(any::<String>()),
|
label in option::of(any::<String>()),
|
||||||
// prevent duplicates by generating a set rather than a vec
|
// prevent duplicates by generating a set rather than a vec
|
||||||
other_params in btree_map(VALID_PARAMNAME, any::<String>(), 0..3),
|
other_params in btree_map(VALID_PARAMNAME, any::<String>(), 0..3),
|
||||||
) -> Payment {
|
) -> Payment {
|
||||||
let is_shielded = match recipient_address {
|
let memo = memo.filter(|_| recipient_address.can_receive_memo());
|
||||||
Address::Transparent(_) => false,
|
|
||||||
Address::Sapling(_) | Address::Unified(_) => true,
|
|
||||||
};
|
|
||||||
|
|
||||||
Payment {
|
Payment {
|
||||||
recipient_address,
|
recipient_address,
|
||||||
amount,
|
amount,
|
||||||
memo: memo.filter(|_| is_shielded),
|
memo,
|
||||||
label,
|
label,
|
||||||
message,
|
message,
|
||||||
other_params: other_params.into_iter().collect(),
|
other_params: other_params.into_iter().collect(),
|
||||||
|
@ -795,7 +830,9 @@ pub mod testing {
|
||||||
}
|
}
|
||||||
|
|
||||||
prop_compose! {
|
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();
|
let mut req = TransactionRequest::from_indexed(payments).unwrap();
|
||||||
req.normalize(); // just to make test comparisons easier
|
req.normalize(); // just to make test comparisons easier
|
||||||
req
|
req
|
||||||
|
@ -803,7 +840,9 @@ pub mod testing {
|
||||||
}
|
}
|
||||||
|
|
||||||
prop_compose! {
|
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();
|
let mut req = TransactionRequest::new(payments).unwrap();
|
||||||
req.normalize(); // just to make test comparisons easier
|
req.normalize(); // just to make test comparisons easier
|
||||||
req
|
req
|
||||||
|
@ -811,16 +850,16 @@ pub mod testing {
|
||||||
}
|
}
|
||||||
|
|
||||||
prop_compose! {
|
prop_compose! {
|
||||||
pub fn arb_zip321_uri()(req in arb_zip321_request()) -> String {
|
pub fn arb_zip321_uri(network: NetworkType)(req in arb_zip321_request(network)) -> String {
|
||||||
req.to_uri(&TEST_NETWORK)
|
req.to_uri()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prop_compose! {
|
prop_compose! {
|
||||||
pub fn arb_addr_str()(
|
pub fn arb_addr_str(network: NetworkType)(
|
||||||
recipient_address in arb_addr(UA_REQUEST)
|
recipient_address in arb_address(network)
|
||||||
) -> String {
|
) -> String {
|
||||||
recipient_address.encode(&TEST_NETWORK)
|
recipient_address.encode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -830,29 +869,27 @@ mod tests {
|
||||||
use proptest::prelude::{any, proptest};
|
use proptest::prelude::{any, proptest};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use zcash_keys::address::testing::arb_addr;
|
use zcash_address::{testing::arb_address, ZcashAddress};
|
||||||
use zcash_primitives::{
|
use zcash_protocol::{
|
||||||
memo::Memo,
|
consensus::NetworkType,
|
||||||
transaction::components::amount::{testing::arb_nonnegative_amount, NonNegativeAmount},
|
memo::{Memo, MemoBytes},
|
||||||
|
value::{testing::arb_zatoshis, Zatoshis},
|
||||||
};
|
};
|
||||||
use zcash_protocol::consensus::{NetworkConstants, NetworkType, TEST_NETWORK};
|
|
||||||
|
|
||||||
#[cfg(feature = "local-consensus")]
|
#[cfg(feature = "local-consensus")]
|
||||||
use zcash_primitives::{local_consensus::LocalNetwork, BlockHeight};
|
use zcash_protocol::{local_consensus::LocalNetwork, BlockHeight};
|
||||||
|
|
||||||
use crate::{address::Address, encoding::decode_payment_address, zip321::testing::UA_REQUEST};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
memo_from_base64, memo_to_base64,
|
memo_from_base64, memo_to_base64,
|
||||||
parse::{parse_amount, zcashparam, Param},
|
parse::{parse_amount, zcashparam, Param},
|
||||||
render::{amount_str, memo_param, str_param},
|
render::{amount_str, memo_param, str_param},
|
||||||
testing::{arb_addr_str, arb_valid_memo, arb_zip321_request, arb_zip321_uri},
|
testing::{arb_addr_str, arb_valid_memo, arb_zip321_request, arb_zip321_uri},
|
||||||
MemoBytes, Payment, TransactionRequest,
|
Payment, TransactionRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn check_roundtrip(req: TransactionRequest) {
|
fn check_roundtrip(req: TransactionRequest) {
|
||||||
let req_uri = req.to_uri(&TEST_NETWORK);
|
let req_uri = req.to_uri();
|
||||||
let parsed = TransactionRequest::from_uri(&TEST_NETWORK, &req_uri).unwrap();
|
let parsed = TransactionRequest::from_uri(&req_uri).unwrap();
|
||||||
assert_eq!(parsed, req);
|
assert_eq!(parsed, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -861,7 +898,7 @@ mod tests {
|
||||||
let amounts = vec![1u64, 1000u64, 100000u64, 100000000u64, 100000000000u64];
|
let amounts = vec![1u64, 1000u64, 100000u64, 100000000u64, 100000000000u64];
|
||||||
|
|
||||||
for amt_u64 in amounts {
|
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);
|
let amt_str = amount_str(amt);
|
||||||
assert_eq!(amt, parse_amount(&amt_str).unwrap().1);
|
assert_eq!(amt, parse_amount(&amt_str).unwrap().1);
|
||||||
}
|
}
|
||||||
|
@ -871,20 +908,20 @@ mod tests {
|
||||||
fn test_zip321_parse_empty_message() {
|
fn test_zip321_parse_empty_message() {
|
||||||
let fragment = "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()));
|
assert_eq!(result, Param::Message("".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_zip321_parse_simple() {
|
fn test_zip321_parse_simple() {
|
||||||
let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k?amount=3768769.02796286&message=";
|
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(
|
let expected = TransactionRequest::new(
|
||||||
vec![
|
vec![
|
||||||
Payment {
|
Payment {
|
||||||
recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
|
recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(),
|
||||||
amount: NonNegativeAmount::const_from_u64(376876902796286),
|
amount: Zatoshis::const_from_u64(376876902796286),
|
||||||
memo: None,
|
memo: None,
|
||||||
label: None,
|
label: None,
|
||||||
message: Some("".to_string()),
|
message: Some("".to_string()),
|
||||||
|
@ -899,13 +936,13 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_zip321_parse_no_query_params() {
|
fn test_zip321_parse_no_query_params() {
|
||||||
let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k";
|
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(
|
let expected = TransactionRequest::new(
|
||||||
vec![
|
vec![
|
||||||
Payment {
|
Payment {
|
||||||
recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
|
recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(),
|
||||||
amount: NonNegativeAmount::ZERO,
|
amount: Zatoshis::ZERO,
|
||||||
memo: None,
|
memo: None,
|
||||||
label: None,
|
label: None,
|
||||||
message: None,
|
message: None,
|
||||||
|
@ -922,8 +959,8 @@ mod tests {
|
||||||
let req = TransactionRequest::new(
|
let req = TransactionRequest::new(
|
||||||
vec![
|
vec![
|
||||||
Payment {
|
Payment {
|
||||||
recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
|
recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(),
|
||||||
amount: NonNegativeAmount::ZERO,
|
amount: Zatoshis::ZERO,
|
||||||
memo: None,
|
memo: None,
|
||||||
label: None,
|
label: None,
|
||||||
message: Some("".to_string()),
|
message: Some("".to_string()),
|
||||||
|
@ -957,48 +994,48 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_zip321_spec_valid_examples() {
|
fn test_zip321_spec_valid_examples() {
|
||||||
let valid_0 = "zcash:";
|
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());
|
assert!(v0r.payments.is_empty());
|
||||||
|
|
||||||
let valid_0 = "zcash:?";
|
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());
|
assert!(v0r.payments.is_empty());
|
||||||
|
|
||||||
let valid_1 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase";
|
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!(
|
assert_eq!(
|
||||||
v1r.payments.get(&0).map(|p| p.amount),
|
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 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();
|
v2r.normalize();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
v2r.payments.get(&0).map(|p| p.amount),
|
v2r.payments.get(&0).map(|p| p.amount),
|
||||||
Some(NonNegativeAmount::const_from_u64(12345600000))
|
Some(Zatoshis::const_from_u64(12345600000))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
v2r.payments.get(&1).map(|p| p.amount),
|
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
|
// valid; amount just less than MAX_MONEY
|
||||||
// 20999999.99999999
|
// 20999999.99999999
|
||||||
let valid_3 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=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!(
|
assert_eq!(
|
||||||
v3r.payments.get(&0).map(|p| p.amount),
|
v3r.payments.get(&0).map(|p| p.amount),
|
||||||
Some(NonNegativeAmount::const_from_u64(2099999999999999u64))
|
Some(Zatoshis::const_from_u64(2099999999999999))
|
||||||
);
|
);
|
||||||
|
|
||||||
// valid; MAX_MONEY
|
// valid; MAX_MONEY
|
||||||
// 21000000
|
// 21000000
|
||||||
let valid_4 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=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!(
|
assert_eq!(
|
||||||
v4r.payments.get(&0).map(|p| p.amount),
|
v4r.payments.get(&0).map(|p| p.amount),
|
||||||
Some(NonNegativeAmount::const_from_u64(2100000000000000u64))
|
Some(Zatoshis::const_from_u64(2100000000000000))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1019,7 +1056,7 @@ mod tests {
|
||||||
let v1r = TransactionRequest::from_uri(¶ms, valid_1).unwrap();
|
let v1r = TransactionRequest::from_uri(¶ms, valid_1).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
v1r.payments.get(&0).map(|p| p.amount),
|
v1r.payments.get(&0).map(|p| p.amount),
|
||||||
Some(NonNegativeAmount::const_from_u64(100000000))
|
Some(Zatoshis::const_from_u64(100000000))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1027,91 +1064,91 @@ mod tests {
|
||||||
fn test_zip321_spec_invalid_examples() {
|
fn test_zip321_spec_invalid_examples() {
|
||||||
// invalid; empty string
|
// invalid; empty string
|
||||||
let invalid_0 = "";
|
let invalid_0 = "";
|
||||||
let i0r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_0);
|
let i0r = TransactionRequest::from_uri(invalid_0);
|
||||||
assert!(i0r.is_err());
|
assert!(i0r.is_err());
|
||||||
|
|
||||||
// invalid; missing `address=`
|
// invalid; missing `address=`
|
||||||
let invalid_1 = "zcash:?amount=3491405.05201255&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=5740296.87793245";
|
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());
|
assert!(i1r.is_err());
|
||||||
|
|
||||||
// invalid; missing `address.1=`
|
// invalid; missing `address.1=`
|
||||||
let invalid_2 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=1&amount.1=2&address.2=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez";
|
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());
|
assert!(i2r.is_err());
|
||||||
|
|
||||||
// invalid; `address.0=` and `amount.0=` are not permitted (leading 0s).
|
// invalid; `address.0=` and `amount.0=` are not permitted (leading 0s).
|
||||||
let invalid_3 = "zcash:?address.0=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.0=2";
|
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());
|
assert!(i3r.is_err());
|
||||||
|
|
||||||
// invalid; duplicate `amount=` field
|
// invalid; duplicate `amount=` field
|
||||||
let invalid_4 =
|
let invalid_4 =
|
||||||
"zcash:?amount=1.234&amount=2.345&address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
|
"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());
|
assert!(i4r.is_err());
|
||||||
|
|
||||||
// invalid; duplicate `amount.1=` field
|
// invalid; duplicate `amount.1=` field
|
||||||
let invalid_5 =
|
let invalid_5 =
|
||||||
"zcash:?amount.1=1.234&amount.1=2.345&address.1=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
|
"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());
|
assert!(i5r.is_err());
|
||||||
|
|
||||||
//invalid; memo associated with t-addr
|
//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 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());
|
assert!(i6r.is_err());
|
||||||
|
|
||||||
// invalid; amount component exceeds an i64
|
// invalid; amount component exceeds an i64
|
||||||
// 9223372036854775808 = i64::MAX + 1
|
// 9223372036854775808 = i64::MAX + 1
|
||||||
let invalid_7 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=9223372036854775808";
|
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());
|
assert!(i7r.is_err());
|
||||||
|
|
||||||
// invalid; amount component wraps into a valid small positive i64
|
// invalid; amount component wraps into a valid small positive i64
|
||||||
// 18446744073709551624
|
// 18446744073709551624
|
||||||
let invalid_7a = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=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());
|
assert!(i7ar.is_err());
|
||||||
|
|
||||||
// invalid; amount component is MAX_MONEY
|
// invalid; amount component is MAX_MONEY
|
||||||
// 21000000.00000001
|
// 21000000.00000001
|
||||||
let invalid_8 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=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());
|
assert!(i8r.is_err());
|
||||||
|
|
||||||
// invalid; negative amount
|
// invalid; negative amount
|
||||||
let invalid_9 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=-1";
|
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());
|
assert!(i9r.is_err());
|
||||||
|
|
||||||
// invalid; parameter index too large
|
// invalid; parameter index too large
|
||||||
let invalid_10 =
|
let invalid_10 =
|
||||||
"zcash:?amount.10000=1.23&address.10000=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
|
"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());
|
assert!(i10r.is_err());
|
||||||
|
|
||||||
// invalid: bad amount format
|
// invalid: bad amount format
|
||||||
let invalid_11 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.";
|
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());
|
assert!(i11r.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
proptest! {
|
proptest! {
|
||||||
#[test]
|
#[test]
|
||||||
fn prop_zip321_roundtrip_address(addr in arb_addr(UA_REQUEST)) {
|
fn prop_zip321_roundtrip_address(addr in arb_address(NetworkType::Test)) {
|
||||||
let a = addr.encode(&TEST_NETWORK);
|
let a = addr.encode();
|
||||||
assert_eq!(Address::decode(&TEST_NETWORK, &a), Some(addr));
|
assert_eq!(ZcashAddress::try_from_encoded(&a), Ok(addr));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn prop_zip321_roundtrip_address_str(a in arb_addr_str()) {
|
fn prop_zip321_roundtrip_address_str(a in arb_addr_str(NetworkType::Test)) {
|
||||||
let addr = Address::decode(&TEST_NETWORK, &a).unwrap();
|
let addr = ZcashAddress::try_from_encoded(&a).unwrap();
|
||||||
assert_eq!(addr.encode(&TEST_NETWORK), a);
|
assert_eq!(addr.encode(), a);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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);
|
let amt_str = amount_str(amt);
|
||||||
assert_eq!(amt, parse_amount(&amt_str).unwrap().1);
|
assert_eq!(amt, parse_amount(&amt_str).unwrap().1);
|
||||||
}
|
}
|
||||||
|
@ -1120,7 +1157,7 @@ mod tests {
|
||||||
fn prop_zip321_roundtrip_str_param(
|
fn prop_zip321_roundtrip_str_param(
|
||||||
message in any::<String>(), i in proptest::option::of(0usize..2000)) {
|
message in any::<String>(), i in proptest::option::of(0usize..2000)) {
|
||||||
let fragment = str_param("message", &message, i);
|
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!(rest, "");
|
||||||
assert_eq!(iparam.param, Param::Message(message));
|
assert_eq!(iparam.param, Param::Message(message));
|
||||||
assert_eq!(iparam.payment_index, i.unwrap_or(0));
|
assert_eq!(iparam.payment_index, i.unwrap_or(0));
|
||||||
|
@ -1130,24 +1167,24 @@ mod tests {
|
||||||
fn prop_zip321_roundtrip_memo_param(
|
fn prop_zip321_roundtrip_memo_param(
|
||||||
memo in arb_valid_memo(), i in proptest::option::of(0usize..2000)) {
|
memo in arb_valid_memo(), i in proptest::option::of(0usize..2000)) {
|
||||||
let fragment = memo_param(&memo, i);
|
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!(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));
|
assert_eq!(iparam.payment_index, i.unwrap_or(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn prop_zip321_roundtrip_request(mut req in arb_zip321_request()) {
|
fn prop_zip321_roundtrip_request(mut req in arb_zip321_request(NetworkType::Test)) {
|
||||||
let req_uri = req.to_uri(&TEST_NETWORK);
|
let req_uri = req.to_uri();
|
||||||
let mut parsed = TransactionRequest::from_uri(&TEST_NETWORK, &req_uri).unwrap();
|
let mut parsed = TransactionRequest::from_uri(&req_uri).unwrap();
|
||||||
assert!(TransactionRequest::normalize_and_eq(&mut parsed, &mut req));
|
assert!(TransactionRequest::normalize_and_eq(&mut parsed, &mut req));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn prop_zip321_roundtrip_uri(uri in arb_zip321_uri()) {
|
fn prop_zip321_roundtrip_uri(uri in arb_zip321_uri(NetworkType::Test)) {
|
||||||
let mut parsed = TransactionRequest::from_uri(&TEST_NETWORK, &uri).unwrap();
|
let mut parsed = TransactionRequest::from_uri(&uri).unwrap();
|
||||||
parsed.normalize();
|
parsed.normalize();
|
||||||
let serialized = parsed.to_uri(&TEST_NETWORK);
|
let serialized = parsed.to_uri();
|
||||||
assert_eq!(serialized, uri)
|
assert_eq!(serialized, uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,6 +14,22 @@ and this library adheres to Rust's notion of
|
||||||
- `testing` module
|
- `testing` module
|
||||||
- `zcash_client_backend::sync` module, behind the `sync` feature flag.
|
- `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
|
## [0.12.1] - 2024-03-27
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
@ -41,6 +41,7 @@ zcash_note_encryption.workspace = true
|
||||||
zcash_primitives.workspace = true
|
zcash_primitives.workspace = true
|
||||||
zcash_protocol.workspace = true
|
zcash_protocol.workspace = true
|
||||||
zip32.workspace = true
|
zip32.workspace = true
|
||||||
|
zip321.workspace = true
|
||||||
|
|
||||||
# Dependencies exposed in a public API:
|
# Dependencies exposed in a public API:
|
||||||
# (Breaking upgrades to these require a breaking upgrade to this crate.)
|
# (Breaking upgrades to these require a breaking upgrade to this crate.)
|
||||||
|
|
|
@ -99,7 +99,7 @@ use {
|
||||||
zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint},
|
zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "test-dependencies")]
|
#[cfg(any(test, feature = "test-dependencies"))]
|
||||||
use zcash_primitives::consensus::NetworkUpgrade;
|
use zcash_primitives::consensus::NetworkUpgrade;
|
||||||
|
|
||||||
pub mod chain;
|
pub mod chain;
|
||||||
|
@ -1334,7 +1334,7 @@ impl AccountBirthday {
|
||||||
///
|
///
|
||||||
/// This API is intended primarily to be used in testing contexts; under normal circumstances,
|
/// This API is intended primarily to be used in testing contexts; under normal circumstances,
|
||||||
/// [`AccountBirthday::from_treestate`] should be used instead.
|
/// [`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 {
|
pub fn from_parts(prior_chain_state: ChainState, recover_until: Option<BlockHeight>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
prior_chain_state,
|
prior_chain_state,
|
||||||
|
|
|
@ -4,6 +4,7 @@ use std::error;
|
||||||
use std::fmt::{self, Debug, Display};
|
use std::fmt::{self, Debug, Display};
|
||||||
|
|
||||||
use shardtree::error::ShardTreeError;
|
use shardtree::error::ShardTreeError;
|
||||||
|
use zcash_address::ConversionError;
|
||||||
use zcash_primitives::transaction::components::amount::NonNegativeAmount;
|
use zcash_primitives::transaction::components::amount::NonNegativeAmount;
|
||||||
use zcash_primitives::transaction::{
|
use zcash_primitives::transaction::{
|
||||||
builder,
|
builder,
|
||||||
|
@ -81,6 +82,9 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
|
||||||
/// full viewing key for an account.
|
/// full viewing key for an account.
|
||||||
NoteMismatch(NoteId),
|
NoteMismatch(NoteId),
|
||||||
|
|
||||||
|
/// An error occurred parsing the address from a payment request.
|
||||||
|
Address(ConversionError<&'static str>),
|
||||||
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
AddressNotRecognized(TransparentAddress),
|
AddressNotRecognized(TransparentAddress),
|
||||||
}
|
}
|
||||||
|
@ -145,6 +149,9 @@ where
|
||||||
Error::NoSpendingKey(addr) => write!(f, "No spending key available for address: {}", addr),
|
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::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")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
Error::AddressNotRecognized(_) => {
|
Error::AddressNotRecognized(_) => {
|
||||||
write!(f, "The specified transparent address was not recognized as belonging to the wallet.")
|
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> {
|
impl<DE, CE, SE, FE> From<InputSelectorError<DE, SE>> for Error<DE, CE, SE, FE> {
|
||||||
fn from(e: InputSelectorError<DE, SE>) -> Self {
|
fn from(e: InputSelectorError<DE, SE>) -> Self {
|
||||||
match e {
|
match e {
|
||||||
|
@ -198,6 +211,7 @@ impl<DE, CE, SE, FE> From<InputSelectorError<DE, SE>> for Error<DE, CE, SE, FE>
|
||||||
required,
|
required,
|
||||||
},
|
},
|
||||||
InputSelectorError::SyncRequired => Error::ScanRequired,
|
InputSelectorError::SyncRequired => Error::ScanRequired,
|
||||||
|
InputSelectorError::Address(e) => Error::Address(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -497,14 +497,15 @@ where
|
||||||
>,
|
>,
|
||||||
DbT::NoteRef: Copy + Eq + Ord,
|
DbT::NoteRef: Copy + Eq + Ord,
|
||||||
{
|
{
|
||||||
let request = zip321::TransactionRequest::new(vec![Payment {
|
let request = zip321::TransactionRequest::new(vec![Payment::new(
|
||||||
recipient_address: to.clone(),
|
to.to_zcash_address(params),
|
||||||
amount,
|
amount,
|
||||||
memo,
|
memo,
|
||||||
label: None,
|
None,
|
||||||
message: None,
|
None,
|
||||||
other_params: vec![],
|
vec![],
|
||||||
}])
|
)
|
||||||
|
.ok_or(Error::MemoForbidden)?])
|
||||||
.expect(
|
.expect(
|
||||||
"It should not be possible for this to violate ZIP 321 request construction invariants.",
|
"It should not be possible for this to violate ZIP 321 request construction invariants.",
|
||||||
);
|
);
|
||||||
|
@ -848,13 +849,16 @@ where
|
||||||
// the transaction in payment index order, so we can use dead reckoning to
|
// the transaction in payment index order, so we can use dead reckoning to
|
||||||
// figure out which output it ended up being.
|
// figure out which output it ended up being.
|
||||||
let (prior_step, result) = &prior_step_results[input_ref.step_index()];
|
let (prior_step, result) = &prior_step_results[input_ref.step_index()];
|
||||||
let recipient_address = match &prior_step
|
let recipient_address = &prior_step
|
||||||
.transaction_request()
|
.transaction_request()
|
||||||
.payments()
|
.payments()
|
||||||
.get(&i)
|
.get(&i)
|
||||||
.expect("Payment step references are checked at construction")
|
.expect("Payment step references are checked at construction")
|
||||||
.recipient_address
|
.recipient_address()
|
||||||
{
|
.clone()
|
||||||
|
.convert_if_network(params.network_type())?;
|
||||||
|
|
||||||
|
let recipient_taddr = match recipient_address {
|
||||||
Address::Transparent(t) => Some(t),
|
Address::Transparent(t) => Some(t),
|
||||||
Address::Unified(uaddr) => uaddr.transparent(),
|
Address::Unified(uaddr) => uaddr.transparent(),
|
||||||
_ => None,
|
_ => None,
|
||||||
|
@ -879,7 +883,7 @@ where
|
||||||
.ok_or(Error::Proposal(ProposalError::ReferenceError(*input_ref)))?
|
.ok_or(Error::Proposal(ProposalError::ReferenceError(*input_ref)))?
|
||||||
.vout[outpoint.n() as usize];
|
.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!(),
|
proposal::StepOutputIndex::Change(_) => unreachable!(),
|
||||||
}
|
}
|
||||||
|
@ -953,12 +957,14 @@ where
|
||||||
(payment, output_pool)
|
(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) => {
|
Address::Unified(ua) => {
|
||||||
let memo = payment
|
let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
|
||||||
.memo
|
|
||||||
.as_ref()
|
|
||||||
.map_or_else(MemoBytes::empty, |m| m.clone());
|
|
||||||
|
|
||||||
match output_pool {
|
match output_pool {
|
||||||
#[cfg(not(feature = "orchard"))]
|
#[cfg(not(feature = "orchard"))]
|
||||||
|
@ -970,15 +976,15 @@ where
|
||||||
builder.add_orchard_output(
|
builder.add_orchard_output(
|
||||||
orchard_external_ovk.clone(),
|
orchard_external_ovk.clone(),
|
||||||
*ua.orchard().expect("The mapping between payment pool and receiver is checked in step construction"),
|
*ua.orchard().expect("The mapping between payment pool and receiver is checked in step construction"),
|
||||||
payment.amount.into(),
|
payment.amount().into(),
|
||||||
memo.clone(),
|
memo.clone(),
|
||||||
)?;
|
)?;
|
||||||
orchard_output_meta.push((
|
orchard_output_meta.push((
|
||||||
Recipient::Unified(
|
Recipient::External(
|
||||||
ua.clone(),
|
payment.recipient_address().clone(),
|
||||||
PoolType::Shielded(ShieldedProtocol::Orchard),
|
PoolType::Shielded(ShieldedProtocol::Orchard),
|
||||||
),
|
),
|
||||||
payment.amount,
|
payment.amount(),
|
||||||
Some(memo),
|
Some(memo),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -987,51 +993,56 @@ where
|
||||||
builder.add_sapling_output(
|
builder.add_sapling_output(
|
||||||
sapling_external_ovk,
|
sapling_external_ovk,
|
||||||
*ua.sapling().expect("The mapping between payment pool and receiver is checked in step construction"),
|
*ua.sapling().expect("The mapping between payment pool and receiver is checked in step construction"),
|
||||||
payment.amount,
|
payment.amount(),
|
||||||
memo.clone(),
|
memo.clone(),
|
||||||
)?;
|
)?;
|
||||||
sapling_output_meta.push((
|
sapling_output_meta.push((
|
||||||
Recipient::Unified(
|
Recipient::External(
|
||||||
ua.clone(),
|
payment.recipient_address().clone(),
|
||||||
PoolType::Shielded(ShieldedProtocol::Sapling),
|
PoolType::Shielded(ShieldedProtocol::Sapling),
|
||||||
),
|
),
|
||||||
payment.amount,
|
payment.amount(),
|
||||||
Some(memo),
|
Some(memo),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
PoolType::Transparent => {
|
PoolType::Transparent => {
|
||||||
if payment.memo.is_some() {
|
if payment.memo().is_some() {
|
||||||
return Err(Error::MemoForbidden);
|
return Err(Error::MemoForbidden);
|
||||||
} else {
|
} else {
|
||||||
builder.add_transparent_output(
|
builder.add_transparent_output(
|
||||||
ua.transparent().expect("The mapping between payment pool and receiver is checked in step construction."),
|
ua.transparent().expect("The mapping between payment pool and receiver is checked in step construction."),
|
||||||
payment.amount
|
payment.amount()
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Address::Sapling(addr) => {
|
Address::Sapling(addr) => {
|
||||||
let memo = payment
|
let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
|
||||||
.memo
|
|
||||||
.as_ref()
|
|
||||||
.map_or_else(MemoBytes::empty, |m| m.clone());
|
|
||||||
builder.add_sapling_output(
|
builder.add_sapling_output(
|
||||||
sapling_external_ovk,
|
sapling_external_ovk,
|
||||||
*addr,
|
addr,
|
||||||
payment.amount,
|
payment.amount(),
|
||||||
memo.clone(),
|
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) => {
|
Address::Transparent(to) => {
|
||||||
if payment.memo.is_some() {
|
if payment.memo().is_some() {
|
||||||
return Err(Error::MemoForbidden);
|
return Err(Error::MemoForbidden);
|
||||||
} else {
|
} 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,7 +1164,10 @@ where
|
||||||
SentTransactionOutput::from_parts(output_index, recipient, value, memo)
|
SentTransactionOutput::from_parts(output_index, recipient, value, memo)
|
||||||
});
|
});
|
||||||
|
|
||||||
let transparent_outputs = transparent_output_meta.into_iter().map(|(addr, value)| {
|
let transparent_outputs =
|
||||||
|
transparent_output_meta
|
||||||
|
.into_iter()
|
||||||
|
.map(|(recipient, addr, value)| {
|
||||||
let script = addr.script();
|
let script = addr.script();
|
||||||
let output_index = build_result
|
let output_index = build_result
|
||||||
.transaction()
|
.transaction()
|
||||||
|
@ -1165,9 +1179,11 @@ where
|
||||||
.find(|(_, tx_out)| tx_out.script_pubkey == script)
|
.find(|(_, tx_out)| tx_out.script_pubkey == script)
|
||||||
})
|
})
|
||||||
.map(|(index, _)| index)
|
.map(|(index, _)| index)
|
||||||
.expect("An output should exist in the transaction for each transparent payment.");
|
.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![];
|
let mut outputs = vec![];
|
||||||
|
|
|
@ -8,6 +8,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use nonempty::NonEmpty;
|
use nonempty::NonEmpty;
|
||||||
|
use zcash_address::ConversionError;
|
||||||
use zcash_primitives::{
|
use zcash_primitives::{
|
||||||
consensus::{self, BlockHeight},
|
consensus::{self, BlockHeight},
|
||||||
transaction::{
|
transaction::{
|
||||||
|
@ -48,6 +49,8 @@ pub enum InputSelectorError<DbErrT, SelectorErrT> {
|
||||||
Selection(SelectorErrT),
|
Selection(SelectorErrT),
|
||||||
/// Input selection attempted to generate an invalid transaction proposal.
|
/// Input selection attempted to generate an invalid transaction proposal.
|
||||||
Proposal(ProposalError),
|
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
|
/// Insufficient funds were available to satisfy the payment request that inputs were being
|
||||||
/// selected to attempt to satisfy.
|
/// selected to attempt to satisfy.
|
||||||
InsufficientFunds {
|
InsufficientFunds {
|
||||||
|
@ -59,6 +62,12 @@ pub enum InputSelectorError<DbErrT, SelectorErrT> {
|
||||||
SyncRequired,
|
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> {
|
impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE, SE> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match &self {
|
match &self {
|
||||||
|
@ -79,6 +88,13 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
|
||||||
e
|
e
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
InputSelectorError::Address(e) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"An error occurred decoding the address from a payment request: {}.",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
InputSelectorError::InsufficientFunds {
|
InputSelectorError::InsufficientFunds {
|
||||||
available,
|
available,
|
||||||
required,
|
required,
|
||||||
|
@ -344,43 +360,48 @@ where
|
||||||
let mut orchard_outputs = vec![];
|
let mut orchard_outputs = vec![];
|
||||||
let mut payment_pools = BTreeMap::new();
|
let mut payment_pools = BTreeMap::new();
|
||||||
for (idx, payment) in transaction_request.payments() {
|
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) => {
|
Address::Transparent(addr) => {
|
||||||
payment_pools.insert(*idx, PoolType::Transparent);
|
payment_pools.insert(*idx, PoolType::Transparent);
|
||||||
transparent_outputs.push(TxOut {
|
transparent_outputs.push(TxOut {
|
||||||
value: payment.amount,
|
value: payment.amount(),
|
||||||
script_pubkey: addr.script(),
|
script_pubkey: addr.script(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Address::Sapling(_) => {
|
Address::Sapling(_) => {
|
||||||
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling));
|
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling));
|
||||||
sapling_outputs.push(SaplingPayment(payment.amount));
|
sapling_outputs.push(SaplingPayment(payment.amount()));
|
||||||
}
|
}
|
||||||
Address::Unified(addr) => {
|
Address::Unified(addr) => {
|
||||||
#[cfg(feature = "orchard")]
|
#[cfg(feature = "orchard")]
|
||||||
if addr.orchard().is_some() {
|
if addr.orchard().is_some() {
|
||||||
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Orchard));
|
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Orchard));
|
||||||
orchard_outputs.push(OrchardPayment(payment.amount));
|
orchard_outputs.push(OrchardPayment(payment.amount()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if addr.sapling().is_some() {
|
if addr.sapling().is_some() {
|
||||||
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling));
|
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling));
|
||||||
sapling_outputs.push(SaplingPayment(payment.amount));
|
sapling_outputs.push(SaplingPayment(payment.amount()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(addr) = addr.transparent() {
|
if let Some(addr) = addr.transparent() {
|
||||||
payment_pools.insert(*idx, PoolType::Transparent);
|
payment_pools.insert(*idx, PoolType::Transparent);
|
||||||
transparent_outputs.push(TxOut {
|
transparent_outputs.push(TxOut {
|
||||||
value: payment.amount,
|
value: payment.amount(),
|
||||||
script_pubkey: addr.script(),
|
script_pubkey: addr.script(),
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err(InputSelectorError::Selection(
|
return Err(InputSelectorError::Selection(
|
||||||
GreedyInputSelectorError::UnsupportedAddress(Box::new(addr.clone())),
|
GreedyInputSelectorError::UnsupportedAddress(Box::new(addr)),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ pub mod proto;
|
||||||
pub mod scan;
|
pub mod scan;
|
||||||
pub mod scanning;
|
pub mod scanning;
|
||||||
pub mod wallet;
|
pub mod wallet;
|
||||||
pub mod zip321;
|
pub use zip321;
|
||||||
|
|
||||||
#[cfg(feature = "sync")]
|
#[cfg(feature = "sync")]
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
|
|
|
@ -377,7 +377,7 @@ impl<NoteRef> Step<NoteRef> {
|
||||||
.payments()
|
.payments()
|
||||||
.get(idx)
|
.get(idx)
|
||||||
.iter()
|
.iter()
|
||||||
.any(|payment| payment.recipient_address.has_receiver(*pool))
|
.any(|payment| payment.recipient_address().can_receive_as(*pool))
|
||||||
{
|
{
|
||||||
return Err(ProposalError::PaymentPoolsMismatch);
|
return Err(ProposalError::PaymentPoolsMismatch);
|
||||||
}
|
}
|
||||||
|
@ -404,13 +404,12 @@ impl<NoteRef> Step<NoteRef> {
|
||||||
.get(s_ref.step_index)
|
.get(s_ref.step_index)
|
||||||
.ok_or(ProposalError::ReferenceError(*s_ref))?;
|
.ok_or(ProposalError::ReferenceError(*s_ref))?;
|
||||||
Ok(match s_ref.output_index {
|
Ok(match s_ref.output_index {
|
||||||
StepOutputIndex::Payment(i) => {
|
StepOutputIndex::Payment(i) => step
|
||||||
step.transaction_request
|
.transaction_request
|
||||||
.payments()
|
.payments()
|
||||||
.get(&i)
|
.get(&i)
|
||||||
.ok_or(ProposalError::ReferenceError(*s_ref))?
|
.ok_or(ProposalError::ReferenceError(*s_ref))?
|
||||||
.amount
|
.amount(),
|
||||||
}
|
|
||||||
StepOutputIndex::Change(i) => step
|
StepOutputIndex::Change(i) => step
|
||||||
.balance
|
.balance
|
||||||
.proposed_change()
|
.proposed_change()
|
||||||
|
|
|
@ -13,7 +13,7 @@ use sapling::{self, note::ExtractedNoteCommitment, Node};
|
||||||
use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE};
|
use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE};
|
||||||
use zcash_primitives::{
|
use zcash_primitives::{
|
||||||
block::{BlockHash, BlockHeader},
|
block::{BlockHash, BlockHeader},
|
||||||
consensus::{self, BlockHeight, Parameters},
|
consensus::BlockHeight,
|
||||||
memo::{self, MemoBytes},
|
memo::{self, MemoBytes},
|
||||||
merkle_tree::read_commitment_tree,
|
merkle_tree::read_commitment_tree,
|
||||||
transaction::{components::amount::NonNegativeAmount, fees::StandardFeeRule, TxId},
|
transaction::{components::amount::NonNegativeAmount, fees::StandardFeeRule, TxId},
|
||||||
|
@ -485,17 +485,14 @@ impl From<ShieldedProtocol> for proposal::ValuePool {
|
||||||
impl proposal::Proposal {
|
impl proposal::Proposal {
|
||||||
/// Serializes a [`Proposal`] based upon a supported [`StandardFeeRule`] to its protobuf
|
/// Serializes a [`Proposal`] based upon a supported [`StandardFeeRule`] to its protobuf
|
||||||
/// representation.
|
/// representation.
|
||||||
pub fn from_standard_proposal<P: Parameters, NoteRef>(
|
pub fn from_standard_proposal<NoteRef>(value: &Proposal<StandardFeeRule, NoteRef>) -> Self {
|
||||||
params: &P,
|
|
||||||
value: &Proposal<StandardFeeRule, NoteRef>,
|
|
||||||
) -> Self {
|
|
||||||
use proposal::proposed_input;
|
use proposal::proposed_input;
|
||||||
use proposal::{PriorStepChange, PriorStepOutput, ReceivedOutput};
|
use proposal::{PriorStepChange, PriorStepOutput, ReceivedOutput};
|
||||||
let steps = value
|
let steps = value
|
||||||
.steps()
|
.steps()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|step| {
|
.map(|step| {
|
||||||
let transaction_request = step.transaction_request().to_uri(params);
|
let transaction_request = step.transaction_request().to_uri();
|
||||||
|
|
||||||
let anchor_height = step
|
let anchor_height = step
|
||||||
.shielded_inputs()
|
.shielded_inputs()
|
||||||
|
@ -607,9 +604,8 @@ impl proposal::Proposal {
|
||||||
|
|
||||||
/// Attempts to parse a [`Proposal`] based upon a supported [`StandardFeeRule`] from its
|
/// Attempts to parse a [`Proposal`] based upon a supported [`StandardFeeRule`] from its
|
||||||
/// protobuf representation.
|
/// protobuf representation.
|
||||||
pub fn try_into_standard_proposal<P: consensus::Parameters, DbT, DbError>(
|
pub fn try_into_standard_proposal<DbT, DbError>(
|
||||||
&self,
|
&self,
|
||||||
params: &P,
|
|
||||||
wallet_db: &DbT,
|
wallet_db: &DbT,
|
||||||
) -> Result<Proposal<StandardFeeRule, DbT::NoteRef>, ProposalDecodingError<DbError>>
|
) -> Result<Proposal<StandardFeeRule, DbT::NoteRef>, ProposalDecodingError<DbError>>
|
||||||
where
|
where
|
||||||
|
@ -631,7 +627,7 @@ impl proposal::Proposal {
|
||||||
let mut steps = Vec::with_capacity(self.steps.len());
|
let mut steps = Vec::with_capacity(self.steps.len());
|
||||||
for step in &self.steps {
|
for step in &self.steps {
|
||||||
let transaction_request =
|
let transaction_request =
|
||||||
TransactionRequest::from_uri(params, &step.transaction_request)?;
|
TransactionRequest::from_uri(&step.transaction_request)?;
|
||||||
|
|
||||||
let payment_pools = step
|
let payment_pools = step
|
||||||
.payment_output_pools
|
.payment_output_pools
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
//! light client.
|
//! light client.
|
||||||
|
|
||||||
use incrementalmerkletree::Position;
|
use incrementalmerkletree::Position;
|
||||||
use zcash_keys::address::Address;
|
use zcash_address::ZcashAddress;
|
||||||
use zcash_note_encryption::EphemeralKeyBytes;
|
use zcash_note_encryption::EphemeralKeyBytes;
|
||||||
use zcash_primitives::{
|
use zcash_primitives::{
|
||||||
consensus::BlockHeight,
|
consensus::BlockHeight,
|
||||||
|
@ -19,7 +19,7 @@ use zcash_primitives::{
|
||||||
};
|
};
|
||||||
use zcash_protocol::value::BalanceError;
|
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")]
|
#[cfg(feature = "orchard")]
|
||||||
use crate::fees::orchard as orchard_fees;
|
use crate::fees::orchard as orchard_fees;
|
||||||
|
@ -68,12 +68,10 @@ impl NoteId {
|
||||||
/// output.
|
/// output.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Recipient<AccountId, N> {
|
pub enum Recipient<AccountId, N> {
|
||||||
Transparent(TransparentAddress),
|
External(ZcashAddress, PoolType),
|
||||||
Sapling(sapling::PaymentAddress),
|
|
||||||
Unified(UnifiedAddress, PoolType),
|
|
||||||
InternalAccount {
|
InternalAccount {
|
||||||
receiving_account: AccountId,
|
receiving_account: AccountId,
|
||||||
external_address: Option<Address>,
|
external_address: Option<ZcashAddress>,
|
||||||
note: N,
|
note: N,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -81,9 +79,7 @@ pub enum Recipient<AccountId, N> {
|
||||||
impl<AccountId, N> 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> {
|
pub fn map_internal_account_note<B, F: FnOnce(N) -> B>(self, f: F) -> Recipient<AccountId, B> {
|
||||||
match self {
|
match self {
|
||||||
Recipient::Transparent(t) => Recipient::Transparent(t),
|
Recipient::External(addr, pool) => Recipient::External(addr, pool),
|
||||||
Recipient::Sapling(s) => Recipient::Sapling(s),
|
|
||||||
Recipient::Unified(u, p) => Recipient::Unified(u, p),
|
|
||||||
Recipient::InternalAccount {
|
Recipient::InternalAccount {
|
||||||
receiving_account,
|
receiving_account,
|
||||||
external_address,
|
external_address,
|
||||||
|
@ -100,9 +96,7 @@ impl<AccountId, N> Recipient<AccountId, N> {
|
||||||
impl<AccountId, N> Recipient<AccountId, Option<N>> {
|
impl<AccountId, N> Recipient<AccountId, Option<N>> {
|
||||||
pub fn internal_account_note_transpose_option(self) -> Option<Recipient<AccountId, N>> {
|
pub fn internal_account_note_transpose_option(self) -> Option<Recipient<AccountId, N>> {
|
||||||
match self {
|
match self {
|
||||||
Recipient::Transparent(t) => Some(Recipient::Transparent(t)),
|
Recipient::External(addr, pool) => Some(Recipient::External(addr, pool)),
|
||||||
Recipient::Sapling(s) => Some(Recipient::Sapling(s)),
|
|
||||||
Recipient::Unified(u, p) => Some(Recipient::Unified(u, p)),
|
|
||||||
Recipient::InternalAccount {
|
Recipient::InternalAccount {
|
||||||
receiving_account,
|
receiving_account,
|
||||||
external_address,
|
external_address,
|
||||||
|
|
|
@ -80,6 +80,8 @@ This version was yanked, use 0.10.1 instead.
|
||||||
- `zcash_client_sqlite::error::SqliteClientError` has new error variants:
|
- `zcash_client_sqlite::error::SqliteClientError` has new error variants:
|
||||||
- `SqliteClientError::UnsupportedPoolType`
|
- `SqliteClientError::UnsupportedPoolType`
|
||||||
- `SqliteClientError::BalanceError`
|
- `SqliteClientError::BalanceError`
|
||||||
|
- The `Bech32DecodeError` variant has been replaced with a more general
|
||||||
|
`DecodingError` type.
|
||||||
|
|
||||||
## [0.8.1] - 2023-10-18
|
## [0.8.1] - 2023-10-18
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,8 @@ use std::error;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use shardtree::error::ShardTreeError;
|
use shardtree::error::ShardTreeError;
|
||||||
use zcash_client_backend::{
|
use zcash_address::ParseError;
|
||||||
encoding::{Bech32DecodeError, TransparentCodecError},
|
use zcash_client_backend::PoolType;
|
||||||
PoolType,
|
|
||||||
};
|
|
||||||
use zcash_keys::keys::AddressGenerationError;
|
use zcash_keys::keys::AddressGenerationError;
|
||||||
use zcash_primitives::zip32;
|
use zcash_primitives::zip32;
|
||||||
use zcash_primitives::{consensus::BlockHeight, transaction::components::amount::BalanceError};
|
use zcash_primitives::{consensus::BlockHeight, transaction::components::amount::BalanceError};
|
||||||
|
@ -16,7 +14,10 @@ use crate::wallet::commitment_tree;
|
||||||
use crate::PRUNING_DEPTH;
|
use crate::PRUNING_DEPTH;
|
||||||
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[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.
|
/// The primary error type for the SQLite wallet backend.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -33,8 +34,8 @@ pub enum SqliteClientError {
|
||||||
/// Illegal attempt to reinitialize an already-initialized wallet database.
|
/// Illegal attempt to reinitialize an already-initialized wallet database.
|
||||||
TableNotEmpty,
|
TableNotEmpty,
|
||||||
|
|
||||||
/// A Bech32-encoded key or address decoding error
|
/// A Zcash key or address decoding error
|
||||||
Bech32DecodeError(Bech32DecodeError),
|
DecodingError(ParseError),
|
||||||
|
|
||||||
/// An error produced in legacy transparent address derivation
|
/// An error produced in legacy transparent address derivation
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
|
@ -42,6 +43,7 @@ pub enum SqliteClientError {
|
||||||
|
|
||||||
/// An error encountered in decoding a transparent address from its
|
/// An error encountered in decoding a transparent address from its
|
||||||
/// serialized form.
|
/// serialized form.
|
||||||
|
#[cfg(feature = "transparent-inputs")]
|
||||||
TransparentAddress(TransparentCodecError),
|
TransparentAddress(TransparentCodecError),
|
||||||
|
|
||||||
/// Wrapper for rusqlite errors.
|
/// Wrapper for rusqlite errors.
|
||||||
|
@ -116,7 +118,6 @@ impl error::Error for SqliteClientError {
|
||||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||||
match &self {
|
match &self {
|
||||||
SqliteClientError::InvalidMemo(e) => Some(e),
|
SqliteClientError::InvalidMemo(e) => Some(e),
|
||||||
SqliteClientError::Bech32DecodeError(Bech32DecodeError::Bech32Error(e)) => Some(e),
|
|
||||||
SqliteClientError::DbError(e) => Some(e),
|
SqliteClientError::DbError(e) => Some(e),
|
||||||
SqliteClientError::Io(e) => Some(e),
|
SqliteClientError::Io(e) => Some(e),
|
||||||
SqliteClientError::BalanceError(e) => Some(e),
|
SqliteClientError::BalanceError(e) => Some(e),
|
||||||
|
@ -136,9 +137,10 @@ impl fmt::Display for SqliteClientError {
|
||||||
SqliteClientError::InvalidNote => write!(f, "Invalid note"),
|
SqliteClientError::InvalidNote => write!(f, "Invalid note"),
|
||||||
SqliteClientError::RequestedRewindInvalid(h, r) =>
|
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),
|
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")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
SqliteClientError::HdwalletError(e) => write!(f, "{:?}", e),
|
SqliteClientError::HdwalletError(e) => write!(f, "{:?}", e),
|
||||||
|
#[cfg(feature = "transparent-inputs")]
|
||||||
SqliteClientError::TransparentAddress(e) => write!(f, "{}", e),
|
SqliteClientError::TransparentAddress(e) => write!(f, "{}", e),
|
||||||
SqliteClientError::TableNotEmpty => write!(f, "Table is not empty"),
|
SqliteClientError::TableNotEmpty => write!(f, "Table is not empty"),
|
||||||
SqliteClientError::DbError(e) => write!(f, "{}", e),
|
SqliteClientError::DbError(e) => write!(f, "{}", e),
|
||||||
|
@ -175,10 +177,9 @@ impl From<std::io::Error> for SqliteClientError {
|
||||||
SqliteClientError::Io(e)
|
SqliteClientError::Io(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl From<ParseError> for SqliteClientError {
|
||||||
impl From<Bech32DecodeError> for SqliteClientError {
|
fn from(e: ParseError) -> Self {
|
||||||
fn from(e: Bech32DecodeError) -> Self {
|
SqliteClientError::DecodingError(e)
|
||||||
SqliteClientError::Bech32DecodeError(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,6 +196,7 @@ impl From<hdwallet::error::Error> for SqliteClientError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "transparent-inputs")]
|
||||||
impl From<TransparentCodecError> for SqliteClientError {
|
impl From<TransparentCodecError> for SqliteClientError {
|
||||||
fn from(e: TransparentCodecError) -> Self {
|
fn from(e: TransparentCodecError) -> Self {
|
||||||
SqliteClientError::TransparentAddress(e)
|
SqliteClientError::TransparentAddress(e)
|
||||||
|
|
|
@ -65,7 +65,7 @@ use zcash_client_backend::{
|
||||||
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
|
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
|
||||||
DecryptedOutput, PoolType, ShieldedProtocol, TransferType,
|
DecryptedOutput, PoolType, ShieldedProtocol, TransferType,
|
||||||
};
|
};
|
||||||
use zcash_keys::address::Address;
|
use zcash_keys::address::Receiver;
|
||||||
use zcash_primitives::{
|
use zcash_primitives::{
|
||||||
block::BlockHash,
|
block::BlockHash,
|
||||||
consensus::{self, BlockHeight},
|
consensus::{self, BlockHeight},
|
||||||
|
@ -1063,11 +1063,22 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
||||||
for output in d_tx.sapling_outputs() {
|
for output in d_tx.sapling_outputs() {
|
||||||
match output.transfer_type() {
|
match output.transfer_type() {
|
||||||
TransferType::Outgoing => {
|
TransferType::Outgoing => {
|
||||||
//TODO: Recover the UA, if possible.
|
let recipient = {
|
||||||
let recipient = Recipient::Sapling(output.note().recipient());
|
let receiver = Receiver::Sapling(output.note().recipient());
|
||||||
|
let wallet_address = wallet::select_receiving_address(
|
||||||
|
&wdb.params,
|
||||||
|
wdb.conn.0,
|
||||||
|
*output.account(),
|
||||||
|
&receiver
|
||||||
|
)?.unwrap_or_else(||
|
||||||
|
receiver.to_zcash_address(wdb.params.network_type())
|
||||||
|
);
|
||||||
|
|
||||||
|
Recipient::External(wallet_address, PoolType::Shielded(ShieldedProtocol::Sapling))
|
||||||
|
};
|
||||||
|
|
||||||
wallet::put_sent_output(
|
wallet::put_sent_output(
|
||||||
wdb.conn.0,
|
wdb.conn.0,
|
||||||
&wdb.params,
|
|
||||||
*output.account(),
|
*output.account(),
|
||||||
tx_ref,
|
tx_ref,
|
||||||
output.index(),
|
output.index(),
|
||||||
|
@ -1087,7 +1098,6 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
||||||
|
|
||||||
wallet::put_sent_output(
|
wallet::put_sent_output(
|
||||||
wdb.conn.0,
|
wdb.conn.0,
|
||||||
&wdb.params,
|
|
||||||
*output.account(),
|
*output.account(),
|
||||||
tx_ref,
|
tx_ref,
|
||||||
output.index(),
|
output.index(),
|
||||||
|
@ -1102,14 +1112,22 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
||||||
if let Some(account_id) = funding_account {
|
if let Some(account_id) = funding_account {
|
||||||
let recipient = Recipient::InternalAccount {
|
let recipient = Recipient::InternalAccount {
|
||||||
receiving_account: *output.account(),
|
receiving_account: *output.account(),
|
||||||
// TODO: recover the actual UA, if possible
|
external_address: {
|
||||||
external_address: Some(Address::Sapling(output.note().recipient())),
|
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()),
|
note: Note::Sapling(output.note().clone()),
|
||||||
};
|
};
|
||||||
|
|
||||||
wallet::put_sent_output(
|
wallet::put_sent_output(
|
||||||
wdb.conn.0,
|
wdb.conn.0,
|
||||||
&wdb.params,
|
|
||||||
account_id,
|
account_id,
|
||||||
tx_ref,
|
tx_ref,
|
||||||
output.index(),
|
output.index(),
|
||||||
|
@ -1126,20 +1144,22 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
||||||
for output in d_tx.orchard_outputs() {
|
for output in d_tx.orchard_outputs() {
|
||||||
match output.transfer_type() {
|
match output.transfer_type() {
|
||||||
TransferType::Outgoing => {
|
TransferType::Outgoing => {
|
||||||
// TODO: Recover the actual UA, if possible.
|
let recipient = {
|
||||||
let recipient = Recipient::Unified(
|
let receiver = Receiver::Orchard(output.note().recipient());
|
||||||
UnifiedAddress::from_receivers(
|
let wallet_address = wallet::select_receiving_address(
|
||||||
Some(output.note().recipient()),
|
&wdb.params,
|
||||||
None,
|
wdb.conn.0,
|
||||||
None,
|
*output.account(),
|
||||||
)
|
&receiver
|
||||||
.expect("UA has an Orchard receiver by construction."),
|
)?.unwrap_or_else(||
|
||||||
PoolType::Shielded(ShieldedProtocol::Orchard),
|
receiver.to_zcash_address(wdb.params.network_type())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Recipient::External(wallet_address, PoolType::Shielded(ShieldedProtocol::Orchard))
|
||||||
|
};
|
||||||
|
|
||||||
wallet::put_sent_output(
|
wallet::put_sent_output(
|
||||||
wdb.conn.0,
|
wdb.conn.0,
|
||||||
&wdb.params,
|
|
||||||
*output.account(),
|
*output.account(),
|
||||||
tx_ref,
|
tx_ref,
|
||||||
output.index(),
|
output.index(),
|
||||||
|
@ -1159,7 +1179,6 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
||||||
|
|
||||||
wallet::put_sent_output(
|
wallet::put_sent_output(
|
||||||
wdb.conn.0,
|
wdb.conn.0,
|
||||||
&wdb.params,
|
|
||||||
*output.account(),
|
*output.account(),
|
||||||
tx_ref,
|
tx_ref,
|
||||||
output.index(),
|
output.index(),
|
||||||
|
@ -1175,19 +1194,22 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
||||||
// Even if the recipient address is external, record the send as internal.
|
// Even if the recipient address is external, record the send as internal.
|
||||||
let recipient = Recipient::InternalAccount {
|
let recipient = Recipient::InternalAccount {
|
||||||
receiving_account: *output.account(),
|
receiving_account: *output.account(),
|
||||||
// TODO: recover the actual UA, if possible
|
external_address: {
|
||||||
external_address: Some(Address::Unified(
|
let receiver = Receiver::Orchard(output.note().recipient());
|
||||||
UnifiedAddress::from_receivers(
|
Some(wallet::select_receiving_address(
|
||||||
Some(output.note().recipient()),
|
&wdb.params,
|
||||||
None,
|
wdb.conn.0,
|
||||||
None,
|
*output.account(),
|
||||||
).expect("UA has an Orchard receiver by construction."))),
|
&receiver
|
||||||
|
)?.unwrap_or_else(||
|
||||||
|
receiver.to_zcash_address(wdb.params.network_type())
|
||||||
|
))
|
||||||
|
},
|
||||||
note: Note::Orchard(*output.note()),
|
note: Note::Orchard(*output.note()),
|
||||||
};
|
};
|
||||||
|
|
||||||
wallet::put_sent_output(
|
wallet::put_sent_output(
|
||||||
wdb.conn.0,
|
wdb.conn.0,
|
||||||
&wdb.params,
|
|
||||||
account_id,
|
account_id,
|
||||||
tx_ref,
|
tx_ref,
|
||||||
output.index(),
|
output.index(),
|
||||||
|
@ -1240,13 +1262,29 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
{
|
||||||
if let Some(address) = txout.recipient_address() {
|
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(
|
wallet::put_sent_output(
|
||||||
wdb.conn.0,
|
wdb.conn.0,
|
||||||
&wdb.params,
|
|
||||||
account_id,
|
account_id,
|
||||||
tx_ref,
|
tx_ref,
|
||||||
output_index,
|
output_index,
|
||||||
&Recipient::Transparent(address),
|
&recipient,
|
||||||
txout.value,
|
txout.value,
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
@ -1305,13 +1343,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
||||||
}
|
}
|
||||||
|
|
||||||
for output in sent_tx.outputs() {
|
for output in sent_tx.outputs() {
|
||||||
wallet::insert_sent_output(
|
wallet::insert_sent_output(wdb.conn.0, tx_ref, *sent_tx.account_id(), output)?;
|
||||||
wdb.conn.0,
|
|
||||||
&wdb.params,
|
|
||||||
tx_ref,
|
|
||||||
*sent_tx.account_id(),
|
|
||||||
output,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
match output.recipient() {
|
match output.recipient() {
|
||||||
Recipient::InternalAccount {
|
Recipient::InternalAccount {
|
||||||
|
@ -1880,7 +1912,6 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(current_addr.is_some());
|
assert!(current_addr.is_some());
|
||||||
|
|
||||||
// TODO: Add Orchard
|
|
||||||
let addr2 = st
|
let addr2 = st
|
||||||
.wallet_mut()
|
.wallet_mut()
|
||||||
.get_next_available_address(account.account_id(), DEFAULT_UA_REQUEST)
|
.get_next_available_address(account.account_id(), DEFAULT_UA_REQUEST)
|
||||||
|
|
|
@ -1896,7 +1896,7 @@ fn check_proposal_serialization_roundtrip(
|
||||||
db_data: &WalletDb<rusqlite::Connection, LocalNetwork>,
|
db_data: &WalletDb<rusqlite::Connection, LocalNetwork>,
|
||||||
proposal: &Proposal<StandardFeeRule, ReceivedNoteId>,
|
proposal: &Proposal<StandardFeeRule, ReceivedNoteId>,
|
||||||
) {
|
) {
|
||||||
let proposal_proto = proposal::Proposal::from_standard_proposal(&db_data.params, proposal);
|
let proposal_proto = proposal::Proposal::from_standard_proposal(proposal);
|
||||||
let deserialized_proposal = proposal_proto.try_into_standard_proposal(&db_data.params, db_data);
|
let deserialized_proposal = proposal_proto.try_into_standard_proposal(db_data);
|
||||||
assert_matches!(deserialized_proposal, Ok(r) if &r == proposal);
|
assert_matches!(deserialized_proposal, Ok(r) if &r == proposal);
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,14 +168,10 @@ pub(crate) fn send_single_step_proposed_transfer<T: ShieldedPoolTester>() {
|
||||||
|
|
||||||
let to_extsk = T::sk(&[0xf5; 32]);
|
let to_extsk = T::sk(&[0xf5; 32]);
|
||||||
let to: Address = T::sk_default_address(&to_extsk);
|
let to: Address = T::sk_default_address(&to_extsk);
|
||||||
let request = zip321::TransactionRequest::new(vec![Payment {
|
let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
|
||||||
recipient_address: to,
|
to.to_zcash_address(&st.network()),
|
||||||
amount: NonNegativeAmount::const_from_u64(10000),
|
NonNegativeAmount::const_from_u64(10000),
|
||||||
memo: None, // this should result in the creation of an empty memo
|
)])
|
||||||
label: None,
|
|
||||||
message: None,
|
|
||||||
other_params: vec![],
|
|
||||||
}])
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// TODO: This test was originally written to use the pre-zip-313 fee rule
|
// TODO: This test was originally written to use the pre-zip-313 fee rule
|
||||||
|
@ -337,14 +333,10 @@ pub(crate) fn send_multi_step_proposed_transfer<T: ShieldedPoolTester>() {
|
||||||
|
|
||||||
// The first step will deshield to the wallet's default transparent address
|
// The first step will deshield to the wallet's default transparent address
|
||||||
let to0 = Address::Transparent(account.usk().default_transparent_address().0);
|
let to0 = Address::Transparent(account.usk().default_transparent_address().0);
|
||||||
let request0 = zip321::TransactionRequest::new(vec![Payment {
|
let request0 = zip321::TransactionRequest::new(vec![Payment::without_memo(
|
||||||
recipient_address: to0,
|
to0.to_zcash_address(&st.network()),
|
||||||
amount: NonNegativeAmount::const_from_u64(50000),
|
NonNegativeAmount::const_from_u64(50000),
|
||||||
memo: None,
|
)])
|
||||||
label: None,
|
|
||||||
message: None,
|
|
||||||
other_params: vec![],
|
|
||||||
}])
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let fee_rule = StandardFeeRule::Zip317;
|
let fee_rule = StandardFeeRule::Zip317;
|
||||||
|
@ -382,14 +374,10 @@ pub(crate) fn send_multi_step_proposed_transfer<T: ShieldedPoolTester>() {
|
||||||
.default_address()
|
.default_address()
|
||||||
.0,
|
.0,
|
||||||
);
|
);
|
||||||
let request1 = zip321::TransactionRequest::new(vec![Payment {
|
let request1 = zip321::TransactionRequest::new(vec![Payment::without_memo(
|
||||||
recipient_address: to1,
|
to1.to_zcash_address(&st.network()),
|
||||||
amount: NonNegativeAmount::const_from_u64(40000),
|
NonNegativeAmount::const_from_u64(40000),
|
||||||
memo: None,
|
)])
|
||||||
label: None,
|
|
||||||
message: None,
|
|
||||||
other_params: vec![],
|
|
||||||
}])
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let step1 = Step::from_parts(
|
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 addr2 = T::fvk_default_address(&dfvk2);
|
||||||
let req = TransactionRequest::new(vec![
|
let req = TransactionRequest::new(vec![
|
||||||
// payment to an external recipient
|
// payment to an external recipient
|
||||||
Payment {
|
Payment::without_memo(addr2.to_zcash_address(&st.network()), amount_sent),
|
||||||
recipient_address: addr2,
|
|
||||||
amount: amount_sent,
|
|
||||||
memo: None,
|
|
||||||
label: None,
|
|
||||||
message: None,
|
|
||||||
other_params: vec![],
|
|
||||||
},
|
|
||||||
// payment back to the originating wallet, simulating legacy change
|
// payment back to the originating wallet, simulating legacy change
|
||||||
Payment {
|
Payment::without_memo(addr.to_zcash_address(&st.network()), amount_legacy_change),
|
||||||
recipient_address: addr,
|
|
||||||
amount: amount_legacy_change,
|
|
||||||
memo: None,
|
|
||||||
label: None,
|
|
||||||
message: None,
|
|
||||||
other_params: vec![],
|
|
||||||
},
|
|
||||||
])
|
])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -1151,14 +1125,10 @@ pub(crate) fn zip317_spend<T: ShieldedPoolTester>() {
|
||||||
let input_selector = input_selector(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
|
let input_selector = input_selector(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
|
||||||
|
|
||||||
// This first request will fail due to insufficient non-dust funds
|
// This first request will fail due to insufficient non-dust funds
|
||||||
let req = TransactionRequest::new(vec![Payment {
|
let req = TransactionRequest::new(vec![Payment::without_memo(
|
||||||
recipient_address: T::fvk_default_address(&dfvk),
|
T::fvk_default_address(&dfvk).to_zcash_address(&st.network()),
|
||||||
amount: NonNegativeAmount::const_from_u64(50000),
|
NonNegativeAmount::const_from_u64(50000),
|
||||||
memo: None,
|
)])
|
||||||
label: None,
|
|
||||||
message: None,
|
|
||||||
other_params: vec![],
|
|
||||||
}])
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_matches!(
|
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
|
// 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
|
// ZAT fee in addition to the 41000 ZAT output to the recipient
|
||||||
let req = TransactionRequest::new(vec![Payment {
|
let req = TransactionRequest::new(vec![Payment::without_memo(
|
||||||
recipient_address: T::fvk_default_address(&dfvk),
|
T::fvk_default_address(&dfvk).to_zcash_address(&st.network()),
|
||||||
amount: NonNegativeAmount::const_from_u64(41000),
|
NonNegativeAmount::const_from_u64(41000),
|
||||||
memo: None,
|
)])
|
||||||
label: None,
|
|
||||||
message: None,
|
|
||||||
other_params: vec![],
|
|
||||||
}])
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let txid = st
|
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 transfer_amount = NonNegativeAmount::const_from_u64(200000);
|
||||||
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment {
|
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment::without_memo(
|
||||||
recipient_address: p1_to,
|
p1_to.to_zcash_address(&st.network()),
|
||||||
amount: transfer_amount,
|
transfer_amount,
|
||||||
memo: None,
|
)])
|
||||||
label: None,
|
|
||||||
message: None,
|
|
||||||
other_params: vec![],
|
|
||||||
}])
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let fee_rule = StandardFeeRule::Zip317;
|
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 transfer_amount = NonNegativeAmount::const_from_u64(200000);
|
||||||
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment {
|
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment::without_memo(
|
||||||
recipient_address: p1_to,
|
p1_to.to_zcash_address(&st.network()),
|
||||||
amount: transfer_amount,
|
transfer_amount,
|
||||||
memo: None,
|
)])
|
||||||
label: None,
|
|
||||||
message: None,
|
|
||||||
other_params: vec![],
|
|
||||||
}])
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let fee_rule = StandardFeeRule::Zip317;
|
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 transfer_amount = NonNegativeAmount::const_from_u64(200000);
|
||||||
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment {
|
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment::without_memo(
|
||||||
recipient_address: Address::Transparent(p1_to),
|
Address::Transparent(p1_to).to_zcash_address(&st.network()),
|
||||||
amount: transfer_amount,
|
transfer_amount,
|
||||||
memo: None,
|
)])
|
||||||
label: None,
|
|
||||||
message: None,
|
|
||||||
other_params: vec![],
|
|
||||||
}])
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let fee_rule = StandardFeeRule::Zip317;
|
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
|
// First, send funds just to P0
|
||||||
let transfer_amount = NonNegativeAmount::const_from_u64(200000);
|
let transfer_amount = NonNegativeAmount::const_from_u64(200000);
|
||||||
let p0_transfer = zip321::TransactionRequest::new(vec![Payment::without_memo(
|
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,
|
transfer_amount,
|
||||||
)])
|
)])
|
||||||
.unwrap();
|
.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
|
// In the next block, send funds to both P0 and P1
|
||||||
let both_transfer = zip321::TransactionRequest::new(vec![
|
let both_transfer = zip321::TransactionRequest::new(vec![
|
||||||
Payment::without_memo(P0::random_address(&mut st.rng), transfer_amount),
|
Payment::without_memo(
|
||||||
Payment::without_memo(P1::random_address(&mut st.rng), transfer_amount),
|
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();
|
.unwrap();
|
||||||
let res = st
|
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
|
// We can spend the received notes
|
||||||
let req = TransactionRequest::new(vec![Payment {
|
let req = TransactionRequest::new(vec![Payment::without_memo(
|
||||||
recipient_address: T::fvk_default_address(&dfvk),
|
T::fvk_default_address(&dfvk).to_zcash_address(&st.network()),
|
||||||
amount: NonNegativeAmount::const_from_u64(110_000),
|
NonNegativeAmount::const_from_u64(110_000),
|
||||||
memo: None,
|
)])
|
||||||
label: None,
|
|
||||||
message: None,
|
|
||||||
other_params: vec![],
|
|
||||||
}])
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
|
|
|
@ -76,12 +76,9 @@ use std::io::{self, Cursor};
|
||||||
use std::num::NonZeroU32;
|
use std::num::NonZeroU32;
|
||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use zcash_keys::keys::{
|
|
||||||
AddressGenerationError, UnifiedAddressRequest, UnifiedIncomingViewingKey, UnifiedSpendingKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
use zcash_address::ZcashAddress;
|
||||||
use zcash_client_backend::{
|
use zcash_client_backend::{
|
||||||
address::{Address, UnifiedAddress},
|
|
||||||
data_api::{
|
data_api::{
|
||||||
scanning::{ScanPriority, ScanRange},
|
scanning::{ScanPriority, ScanRange},
|
||||||
AccountBalance, AccountBirthday, AccountSource, BlockMetadata, Ratio,
|
AccountBalance, AccountBirthday, AccountSource, BlockMetadata, Ratio,
|
||||||
|
@ -92,6 +89,13 @@ use zcash_client_backend::{
|
||||||
wallet::{Note, NoteId, Recipient, WalletTx},
|
wallet::{Note, NoteId, Recipient, WalletTx},
|
||||||
PoolType, ShieldedProtocol,
|
PoolType, ShieldedProtocol,
|
||||||
};
|
};
|
||||||
|
use zcash_keys::{
|
||||||
|
address::{Address, Receiver, UnifiedAddress},
|
||||||
|
keys::{
|
||||||
|
AddressGenerationError, UnifiedAddressRequest, UnifiedIncomingViewingKey,
|
||||||
|
UnifiedSpendingKey,
|
||||||
|
},
|
||||||
|
};
|
||||||
use zcash_primitives::{
|
use zcash_primitives::{
|
||||||
block::BlockHash,
|
block::BlockHash,
|
||||||
consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters},
|
consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters},
|
||||||
|
@ -101,8 +105,8 @@ use zcash_primitives::{
|
||||||
components::{amount::NonNegativeAmount, Amount},
|
components::{amount::NonNegativeAmount, Amount},
|
||||||
Transaction, TransactionData, TxId,
|
Transaction, TransactionData, TxId,
|
||||||
},
|
},
|
||||||
zip32::{self, DiversifierIndex, Scope},
|
|
||||||
};
|
};
|
||||||
|
use zip32::{self, DiversifierIndex, Scope};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::SqliteClientError,
|
error::SqliteClientError,
|
||||||
|
@ -2366,6 +2370,48 @@ pub(crate) fn put_tx_meta(
|
||||||
.map_err(SqliteClientError::from)
|
.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(*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.
|
/// Inserts full transaction data into the database.
|
||||||
pub(crate) fn put_tx_data(
|
pub(crate) fn put_tx_data(
|
||||||
conn: &rusqlite::Connection,
|
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`
|
// A utility function for creation of parameters for use in `insert_sent_output`
|
||||||
// and `put_sent_output`
|
// and `put_sent_output`
|
||||||
fn recipient_params<P: consensus::Parameters>(
|
fn recipient_params(
|
||||||
params: &P,
|
|
||||||
to: &Recipient<AccountId, Note>,
|
to: &Recipient<AccountId, Note>,
|
||||||
) -> (Option<String>, Option<AccountId>, PoolType) {
|
) -> (Option<String>, Option<AccountId>, PoolType) {
|
||||||
match to {
|
match to {
|
||||||
Recipient::Transparent(addr) => (Some(addr.encode(params)), None, PoolType::Transparent),
|
Recipient::External(addr, pool) => (Some(addr.encode()), None, *pool),
|
||||||
Recipient::Sapling(addr) => (
|
|
||||||
Some(addr.encode(params)),
|
|
||||||
None,
|
|
||||||
PoolType::Shielded(ShieldedProtocol::Sapling),
|
|
||||||
),
|
|
||||||
Recipient::Unified(addr, pool) => (Some(addr.encode(params)), None, *pool),
|
|
||||||
Recipient::InternalAccount {
|
Recipient::InternalAccount {
|
||||||
receiving_account,
|
receiving_account,
|
||||||
external_address,
|
external_address,
|
||||||
note,
|
note,
|
||||||
} => (
|
} => (
|
||||||
external_address.as_ref().map(|a| a.encode(params)),
|
external_address.as_ref().map(|a| a.encode()),
|
||||||
Some(*receiving_account),
|
Some(*receiving_account),
|
||||||
PoolType::Shielded(note.protocol()),
|
PoolType::Shielded(note.protocol()),
|
||||||
),
|
),
|
||||||
|
@ -2540,9 +2579,8 @@ fn recipient_params<P: consensus::Parameters>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Records information about a transaction output that your wallet created.
|
/// 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,
|
conn: &rusqlite::Connection,
|
||||||
params: &P,
|
|
||||||
tx_ref: i64,
|
tx_ref: i64,
|
||||||
from_account: AccountId,
|
from_account: AccountId,
|
||||||
output: &SentTransactionOutput<AccountId>,
|
output: &SentTransactionOutput<AccountId>,
|
||||||
|
@ -2556,7 +2594,7 @@ pub(crate) fn insert_sent_output<P: consensus::Parameters>(
|
||||||
:to_address, :to_account_id, :value, :memo)",
|
: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![
|
let sql_args = named_params![
|
||||||
":tx": &tx_ref,
|
":tx": &tx_ref,
|
||||||
":output_pool": &pool_code(pool_type),
|
":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
|
/// - If `recipient` is an internal account, `output_index` is an index into the Sapling outputs of
|
||||||
/// the transaction.
|
/// the transaction.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn put_sent_output<P: consensus::Parameters>(
|
pub(crate) fn put_sent_output(
|
||||||
conn: &rusqlite::Connection,
|
conn: &rusqlite::Connection,
|
||||||
params: &P,
|
|
||||||
from_account: AccountId,
|
from_account: AccountId,
|
||||||
tx_ref: i64,
|
tx_ref: i64,
|
||||||
output_index: usize,
|
output_index: usize,
|
||||||
|
@ -2610,7 +2647,7 @@ pub(crate) fn put_sent_output<P: consensus::Parameters>(
|
||||||
memo = IFNULL(:memo, memo)",
|
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![
|
let sql_args = named_params![
|
||||||
":tx": &tx_ref,
|
":tx": &tx_ref,
|
||||||
":output_pool": &pool_code(pool_type),
|
":output_pool": &pool_code(pool_type),
|
||||||
|
|
|
@ -134,11 +134,10 @@ fn sqlite_client_error_to_wallet_migration_error(e: SqliteClientError) -> Wallet
|
||||||
SqliteClientError::InvalidNote => {
|
SqliteClientError::InvalidNote => {
|
||||||
WalletMigrationError::CorruptedData("invalid note".into())
|
WalletMigrationError::CorruptedData("invalid note".into())
|
||||||
}
|
}
|
||||||
SqliteClientError::Bech32DecodeError(e) => {
|
SqliteClientError::DecodingError(e) => WalletMigrationError::CorruptedData(e.to_string()),
|
||||||
WalletMigrationError::CorruptedData(e.to_string())
|
|
||||||
}
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
SqliteClientError::HdwalletError(e) => WalletMigrationError::CorruptedData(e.to_string()),
|
SqliteClientError::HdwalletError(e) => WalletMigrationError::CorruptedData(e.to_string()),
|
||||||
|
#[cfg(feature = "transparent-inputs")]
|
||||||
SqliteClientError::TransparentAddress(e) => {
|
SqliteClientError::TransparentAddress(e) => {
|
||||||
WalletMigrationError::CorruptedData(e.to_string())
|
WalletMigrationError::CorruptedData(e.to_string())
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,9 @@ and this library adheres to Rust's notion of
|
||||||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Added
|
||||||
|
- `zcash_keys::address::Address::try_from_zcash_address`
|
||||||
|
- `zcash_keys::address::Receiver`
|
||||||
|
|
||||||
## [0.2.0] - 2024-03-25
|
## [0.2.0] - 2024-03-25
|
||||||
|
|
||||||
|
@ -65,6 +68,7 @@ The entries below are relative to the `zcash_client_backend` crate as of
|
||||||
- `UnifiedAddressRequest`
|
- `UnifiedAddressRequest`
|
||||||
- A new `orchard` feature flag has been added to make it possible to
|
- A new `orchard` feature flag has been added to make it possible to
|
||||||
build client code without `orchard` dependendencies.
|
build client code without `orchard` dependendencies.
|
||||||
|
- `zcash_keys::address::Address::to_zcash_address`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- The following methods and enum variants have been placed behind an `orchard`
|
- The following methods and enum variants have been placed behind an `orchard`
|
||||||
|
|
|
@ -233,6 +233,63 @@ impl UnifiedAddress {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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::Receiver::Orchard(addr.to_raw_address_bytes());
|
||||||
|
let ua = unified::Address::try_from_items(vec![receiver])
|
||||||
|
.expect("A unified address may contain a single Orchard receiver.");
|
||||||
|
ZcashAddress::from_unified(net, ua)
|
||||||
|
}
|
||||||
|
#[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.
|
/// An address that funds can be sent to.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub enum Address {
|
pub enum Address {
|
||||||
|
@ -290,12 +347,24 @@ impl TryFromRawAddress for Address {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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> {
|
pub fn decode<P: consensus::Parameters>(params: &P, s: &str) -> Option<Self> {
|
||||||
let addr = ZcashAddress::try_from_encoded(s).ok()?;
|
Self::try_from_zcash_address(params, s.parse::<ZcashAddress>().ok()?).ok()
|
||||||
addr.convert_if_network(params.network_type()).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();
|
let net = params.network_type();
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
|
@ -311,9 +380,14 @@ impl Address {
|
||||||
},
|
},
|
||||||
Address::Unified(ua) => ua.to_address(net),
|
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 {
|
pub fn has_receiver(&self, pool_type: PoolType) -> bool {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature = "sapling")]
|
#[cfg(feature = "sapling")]
|
||||||
|
|
Loading…
Reference in New Issue