Compare commits

...

12 Commits

Author SHA1 Message Date
str4d 5c6a6a4c86
Merge pull request #1143 from nuttycom/crate_zip321
Extract `zip321` crate from `zcash_client_backend`
2024-04-22 22:46:09 +01:00
Kris Nuttycombe d2aa6cfc7f Apply suggestions from code review
Co-authored-by: str4d <thestr4d@gmail.com>
2024-04-22 14:41:50 -06:00
Kris Nuttycombe aeac544aed Address comments from code review. 2024-04-22 10:55:25 -06:00
Daira-Emma Hopwood ea82dbeb64
Apply straightforward suggestions from code review 2024-04-18 01:12:13 +01:00
Kris Nuttycombe f28aa6b304 `zcash_{keys, client_backend}`: Fix no-default-features build. 2024-04-11 18:00:59 -06:00
Kris Nuttycombe b60600a4c3 zcash_client_sqlite: Use `ZcashAddress` for persistence of sent note addresses
Prior to this change, the recipient of a sent transaction would always
be shown as the protocol-level address, instead of any unified address
intended as the recipient. Now, instead of reencoding the recipient
address, we use the original `ZcashAddress` value from the payment
request.
2024-04-05 16:48:13 -06:00
Kris Nuttycombe 86e1181259 zip321: Make `Payment` fields private. 2024-04-05 16:30:31 -06:00
Kris Nuttycombe 3ea7d84183 zcash_client_backend: Update to use extracted `zip321` crate 2024-04-05 16:25:21 -06:00
Kris Nuttycombe d982d7826a zip321: Replace dependencies on `zcash_keys` types with `zcash_address` 2024-04-05 16:10:22 -06:00
Kris Nuttycombe fdf86ad740 Move `zcash_client_backend::zip321` to the `zip321` crate. 2024-04-03 12:14:20 -06:00
Kris Nuttycombe 07d5aa4a79 zip321: Remove stub lib.rs 2024-04-03 12:14:18 -06:00
Kris Nuttycombe bbb8d1090a Add placeholder for a zip321 crate. 2024-04-03 12:13:47 -06:00
33 changed files with 1177 additions and 481 deletions

13
Cargo.lock generated
View File

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

View File

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

View File

@ -7,6 +7,13 @@ and this library adheres to Rust's notion of
## [Unreleased]
### Added
- `zcash_address::ZcashAddress::{can_receive_memo, can_receive_as, matches_receiver}`
- `zcash_address::unified::Address::{can_receive_memo, has_receiver_of_type, contains_receiver}`
- 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
### Added
- `zcash_address::convert`:

View File

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

View File

@ -1,3 +1,5 @@
use zcash_protocol::{PoolType, ShieldedProtocol};
use super::{private::SealedItem, ParseError, Typecode};
use std::convert::{TryFrom, TryInto};
@ -101,6 +103,30 @@ impl SealedItem for Receiver {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
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 {
/// The HRP for a Bech32m-encoded mainnet Unified Address.
///
@ -133,27 +159,19 @@ impl super::Container for Address {
}
}
#[cfg(any(test, feature = "test-dependencies"))]
pub mod test_vectors;
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use zcash_encoding::MAX_COMPACT_SIZE;
use crate::{
kind::unified::{private::SealedContainer, Container, Encoding},
Network,
};
#[cfg(feature = "test-dependencies")]
pub mod testing {
use proptest::{
array::{uniform11, uniform20, uniform32},
collection::vec,
prelude::*,
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! {
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])
}
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![
Just(Typecode::Sapling),
Just(Typecode::Orchard),
@ -179,7 +199,7 @@ mod tests {
/// A strategy to generate an arbitrary valid set of typecodes without
/// duplication and containing only one of P2sh and P2pkh transparent
/// typecodes. The resulting vector will be sorted in encoding order.
fn arb_typecodes() -> impl Strategy<Value = Vec<Typecode>> {
pub fn arb_typecodes() -> impl Strategy<Value = Vec<Typecode>> {
prop::option::of(arb_transparent_typecode()).prop_flat_map(|transparent| {
prop::collection::hash_set(arb_shielded_typecode(), 1..4).prop_map(move |xs| {
let mut typecodes: Vec<_> = xs.into_iter().chain(transparent).collect();
@ -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>,
) -> impl Strategy<Value = Vec<Receiver>> {
typecodes
@ -206,11 +230,33 @@ mod tests {
.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()
.prop_flat_map(arb_unified_address_for_typecodes)
.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! {
#[test]

View File

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

View File

@ -6,6 +6,8 @@ and this library adheres to Rust's notion of
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- `zcash_protocol::PoolType::{TRANSPARENT, SAPLING, ORCHARD}`
## [0.1.1] - 2024-03-25
### Added

View File

@ -42,6 +42,12 @@ pub enum PoolType {
Shielded(ShieldedProtocol),
}
impl PoolType {
pub const TRANSPARENT: PoolType = PoolType::Transparent;
pub const SAPLING: PoolType = PoolType::Shielded(ShieldedProtocol::Sapling);
pub const ORCHARD: PoolType = PoolType::Shielded(ShieldedProtocol::Orchard);
}
impl fmt::Display for PoolType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,22 @@ and this library adheres to Rust's notion of
- `testing` module
- `zcash_client_backend::sync` module, behind the `sync` feature flag.
### Changed
- `zcash_client_backend::zip321` has been extracted to, and is now a reexport
of the root module of the `zip321` crate. Several of the APIs of this module
have changed as a consequence of this extraction; please see the `zip321`
CHANGELOG for details.
- `zcash_client_backend::data_api`:
- `error::Error` has a new `Address` variant.
- `wallet::input_selection::InputSelectorError` has a new `Address` variant.
- `zcash_client_backend::proto::proposal::Proposal::{from_standard_proposal,
try_into_standard_proposal}` each no longer require a `consensus::Parameters`
argument.
- `zcash_client_backend::wallet::Recipient` variants have changed. Instead of
wrapping protocol-address types, the `Recipient` type now wraps a
`zcash_address::ZcashAddress`. This simplifies the process of tracking the
original address to which value was sent.
## [0.12.1] - 2024-03-27
### Fixed

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ use std::{
};
use nonempty::NonEmpty;
use zcash_address::ConversionError;
use zcash_primitives::{
consensus::{self, BlockHeight},
transaction::{
@ -48,6 +49,8 @@ pub enum InputSelectorError<DbErrT, SelectorErrT> {
Selection(SelectorErrT),
/// Input selection attempted to generate an invalid transaction proposal.
Proposal(ProposalError),
/// An error occurred parsing the address from a payment request.
Address(ConversionError<&'static str>),
/// Insufficient funds were available to satisfy the payment request that inputs were being
/// selected to attempt to satisfy.
InsufficientFunds {
@ -59,6 +62,12 @@ pub enum InputSelectorError<DbErrT, SelectorErrT> {
SyncRequired,
}
impl<E, S> From<ConversionError<&'static str>> for InputSelectorError<E, S> {
fn from(value: ConversionError<&'static str>) -> Self {
InputSelectorError::Address(value)
}
}
impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE, SE> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self {
@ -79,6 +88,13 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
e
)
}
InputSelectorError::Address(e) => {
write!(
f,
"An error occurred decoding the address from a payment request: {}.",
e
)
}
InputSelectorError::InsufficientFunds {
available,
required,
@ -344,43 +360,48 @@ where
let mut orchard_outputs = vec![];
let mut payment_pools = BTreeMap::new();
for (idx, payment) in transaction_request.payments() {
match &payment.recipient_address {
let recipient_address: Address = payment
.recipient_address()
.clone()
.convert_if_network(params.network_type())?;
match recipient_address {
Address::Transparent(addr) => {
payment_pools.insert(*idx, PoolType::Transparent);
transparent_outputs.push(TxOut {
value: payment.amount,
value: payment.amount(),
script_pubkey: addr.script(),
});
}
Address::Sapling(_) => {
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling));
sapling_outputs.push(SaplingPayment(payment.amount));
sapling_outputs.push(SaplingPayment(payment.amount()));
}
Address::Unified(addr) => {
#[cfg(feature = "orchard")]
if addr.orchard().is_some() {
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Orchard));
orchard_outputs.push(OrchardPayment(payment.amount));
orchard_outputs.push(OrchardPayment(payment.amount()));
continue;
}
if addr.sapling().is_some() {
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling));
sapling_outputs.push(SaplingPayment(payment.amount));
sapling_outputs.push(SaplingPayment(payment.amount()));
continue;
}
if let Some(addr) = addr.transparent() {
payment_pools.insert(*idx, PoolType::Transparent);
transparent_outputs.push(TxOut {
value: payment.amount,
value: payment.amount(),
script_pubkey: addr.script(),
});
continue;
}
return Err(InputSelectorError::Selection(
GreedyInputSelectorError::UnsupportedAddress(Box::new(addr.clone())),
GreedyInputSelectorError::UnsupportedAddress(Box::new(addr)),
));
}
}

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
//! light client.
use incrementalmerkletree::Position;
use zcash_keys::address::Address;
use zcash_address::ZcashAddress;
use zcash_note_encryption::EphemeralKeyBytes;
use zcash_primitives::{
consensus::BlockHeight,
@ -19,7 +19,7 @@ use zcash_primitives::{
};
use zcash_protocol::value::BalanceError;
use crate::{address::UnifiedAddress, fees::sapling as sapling_fees, PoolType, ShieldedProtocol};
use crate::{fees::sapling as sapling_fees, PoolType, ShieldedProtocol};
#[cfg(feature = "orchard")]
use crate::fees::orchard as orchard_fees;
@ -68,12 +68,10 @@ impl NoteId {
/// output.
#[derive(Debug, Clone)]
pub enum Recipient<AccountId, N> {
Transparent(TransparentAddress),
Sapling(sapling::PaymentAddress),
Unified(UnifiedAddress, PoolType),
External(ZcashAddress, PoolType),
InternalAccount {
receiving_account: AccountId,
external_address: Option<Address>,
external_address: Option<ZcashAddress>,
note: N,
},
}
@ -81,9 +79,7 @@ pub enum Recipient<AccountId, N> {
impl<AccountId, N> Recipient<AccountId, N> {
pub fn map_internal_account_note<B, F: FnOnce(N) -> B>(self, f: F) -> Recipient<AccountId, B> {
match self {
Recipient::Transparent(t) => Recipient::Transparent(t),
Recipient::Sapling(s) => Recipient::Sapling(s),
Recipient::Unified(u, p) => Recipient::Unified(u, p),
Recipient::External(addr, pool) => Recipient::External(addr, pool),
Recipient::InternalAccount {
receiving_account,
external_address,
@ -100,9 +96,7 @@ impl<AccountId, N> Recipient<AccountId, N> {
impl<AccountId, N> Recipient<AccountId, Option<N>> {
pub fn internal_account_note_transpose_option(self) -> Option<Recipient<AccountId, N>> {
match self {
Recipient::Transparent(t) => Some(Recipient::Transparent(t)),
Recipient::Sapling(s) => Some(Recipient::Sapling(s)),
Recipient::Unified(u, p) => Some(Recipient::Unified(u, p)),
Recipient::External(addr, pool) => Some(Recipient::External(addr, pool)),
Recipient::InternalAccount {
receiving_account,
external_address,

View File

@ -80,6 +80,8 @@ This version was yanked, use 0.10.1 instead.
- `zcash_client_sqlite::error::SqliteClientError` has new error variants:
- `SqliteClientError::UnsupportedPoolType`
- `SqliteClientError::BalanceError`
- The `Bech32DecodeError` variant has been replaced with a more general
`DecodingError` type.
## [0.8.1] - 2023-10-18

View File

@ -4,10 +4,8 @@ use std::error;
use std::fmt;
use shardtree::error::ShardTreeError;
use zcash_client_backend::{
encoding::{Bech32DecodeError, TransparentCodecError},
PoolType,
};
use zcash_address::ParseError;
use zcash_client_backend::PoolType;
use zcash_keys::keys::AddressGenerationError;
use zcash_primitives::zip32;
use zcash_primitives::{consensus::BlockHeight, transaction::components::amount::BalanceError};
@ -16,7 +14,10 @@ use crate::wallet::commitment_tree;
use crate::PRUNING_DEPTH;
#[cfg(feature = "transparent-inputs")]
use zcash_primitives::legacy::TransparentAddress;
use {
zcash_client_backend::encoding::TransparentCodecError,
zcash_primitives::legacy::TransparentAddress,
};
/// The primary error type for the SQLite wallet backend.
#[derive(Debug)]
@ -33,8 +34,8 @@ pub enum SqliteClientError {
/// Illegal attempt to reinitialize an already-initialized wallet database.
TableNotEmpty,
/// A Bech32-encoded key or address decoding error
Bech32DecodeError(Bech32DecodeError),
/// A Zcash key or address decoding error
DecodingError(ParseError),
/// An error produced in legacy transparent address derivation
#[cfg(feature = "transparent-inputs")]
@ -42,6 +43,7 @@ pub enum SqliteClientError {
/// An error encountered in decoding a transparent address from its
/// serialized form.
#[cfg(feature = "transparent-inputs")]
TransparentAddress(TransparentCodecError),
/// Wrapper for rusqlite errors.
@ -116,7 +118,6 @@ impl error::Error for SqliteClientError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self {
SqliteClientError::InvalidMemo(e) => Some(e),
SqliteClientError::Bech32DecodeError(Bech32DecodeError::Bech32Error(e)) => Some(e),
SqliteClientError::DbError(e) => Some(e),
SqliteClientError::Io(e) => Some(e),
SqliteClientError::BalanceError(e) => Some(e),
@ -136,9 +137,10 @@ impl fmt::Display for SqliteClientError {
SqliteClientError::InvalidNote => write!(f, "Invalid note"),
SqliteClientError::RequestedRewindInvalid(h, r) =>
write!(f, "A rewind must be either of less than {} blocks, or at least back to block {} for your wallet; the requested height was {}.", PRUNING_DEPTH, h, r),
SqliteClientError::Bech32DecodeError(e) => write!(f, "{}", e),
SqliteClientError::DecodingError(e) => write!(f, "{}", e),
#[cfg(feature = "transparent-inputs")]
SqliteClientError::HdwalletError(e) => write!(f, "{:?}", e),
#[cfg(feature = "transparent-inputs")]
SqliteClientError::TransparentAddress(e) => write!(f, "{}", e),
SqliteClientError::TableNotEmpty => write!(f, "Table is not empty"),
SqliteClientError::DbError(e) => write!(f, "{}", e),
@ -175,10 +177,9 @@ impl From<std::io::Error> for SqliteClientError {
SqliteClientError::Io(e)
}
}
impl From<Bech32DecodeError> for SqliteClientError {
fn from(e: Bech32DecodeError) -> Self {
SqliteClientError::Bech32DecodeError(e)
impl From<ParseError> for SqliteClientError {
fn from(e: ParseError) -> Self {
SqliteClientError::DecodingError(e)
}
}
@ -195,6 +196,7 @@ impl From<hdwallet::error::Error> for SqliteClientError {
}
}
#[cfg(feature = "transparent-inputs")]
impl From<TransparentCodecError> for SqliteClientError {
fn from(e: TransparentCodecError) -> Self {
SqliteClientError::TransparentAddress(e)

View File

@ -65,7 +65,7 @@ use zcash_client_backend::{
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
DecryptedOutput, PoolType, ShieldedProtocol, TransferType,
};
use zcash_keys::address::Address;
use zcash_keys::address::Receiver;
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight},
@ -1063,11 +1063,22 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
for output in d_tx.sapling_outputs() {
match output.transfer_type() {
TransferType::Outgoing => {
//TODO: Recover the UA, if possible.
let recipient = Recipient::Sapling(output.note().recipient());
let recipient = {
let receiver = Receiver::Sapling(output.note().recipient());
let wallet_address = wallet::select_receiving_address(
&wdb.params,
wdb.conn.0,
*output.account(),
&receiver
)?.unwrap_or_else(||
receiver.to_zcash_address(wdb.params.network_type())
);
Recipient::External(wallet_address, PoolType::Shielded(ShieldedProtocol::Sapling))
};
wallet::put_sent_output(
wdb.conn.0,
&wdb.params,
*output.account(),
tx_ref,
output.index(),
@ -1087,7 +1098,6 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
wallet::put_sent_output(
wdb.conn.0,
&wdb.params,
*output.account(),
tx_ref,
output.index(),
@ -1102,14 +1112,22 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
if let Some(account_id) = funding_account {
let recipient = Recipient::InternalAccount {
receiving_account: *output.account(),
// TODO: recover the actual UA, if possible
external_address: Some(Address::Sapling(output.note().recipient())),
external_address: {
let receiver = Receiver::Sapling(output.note().recipient());
Some(wallet::select_receiving_address(
&wdb.params,
wdb.conn.0,
*output.account(),
&receiver
)?.unwrap_or_else(||
receiver.to_zcash_address(wdb.params.network_type())
))
},
note: Note::Sapling(output.note().clone()),
};
wallet::put_sent_output(
wdb.conn.0,
&wdb.params,
account_id,
tx_ref,
output.index(),
@ -1126,20 +1144,22 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
for output in d_tx.orchard_outputs() {
match output.transfer_type() {
TransferType::Outgoing => {
// TODO: Recover the actual UA, if possible.
let recipient = Recipient::Unified(
UnifiedAddress::from_receivers(
Some(output.note().recipient()),
None,
None,
)
.expect("UA has an Orchard receiver by construction."),
PoolType::Shielded(ShieldedProtocol::Orchard),
);
let recipient = {
let receiver = Receiver::Orchard(output.note().recipient());
let wallet_address = wallet::select_receiving_address(
&wdb.params,
wdb.conn.0,
*output.account(),
&receiver
)?.unwrap_or_else(||
receiver.to_zcash_address(wdb.params.network_type())
);
Recipient::External(wallet_address, PoolType::Shielded(ShieldedProtocol::Orchard))
};
wallet::put_sent_output(
wdb.conn.0,
&wdb.params,
*output.account(),
tx_ref,
output.index(),
@ -1159,7 +1179,6 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
wallet::put_sent_output(
wdb.conn.0,
&wdb.params,
*output.account(),
tx_ref,
output.index(),
@ -1175,19 +1194,22 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
// Even if the recipient address is external, record the send as internal.
let recipient = Recipient::InternalAccount {
receiving_account: *output.account(),
// TODO: recover the actual UA, if possible
external_address: Some(Address::Unified(
UnifiedAddress::from_receivers(
Some(output.note().recipient()),
None,
None,
).expect("UA has an Orchard receiver by construction."))),
external_address: {
let receiver = Receiver::Orchard(output.note().recipient());
Some(wallet::select_receiving_address(
&wdb.params,
wdb.conn.0,
*output.account(),
&receiver
)?.unwrap_or_else(||
receiver.to_zcash_address(wdb.params.network_type())
))
},
note: Note::Orchard(*output.note()),
};
wallet::put_sent_output(
wdb.conn.0,
&wdb.params,
account_id,
tx_ref,
output.index(),
@ -1240,13 +1262,29 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
.enumerate()
{
if let Some(address) = txout.recipient_address() {
let receiver = Receiver::Transparent(address);
#[cfg(feature = "transparent-inputs")]
let recipient_addr = wallet::select_receiving_address(
&wdb.params,
wdb.conn.0,
account_id,
&receiver
)?.unwrap_or_else(||
receiver.to_zcash_address(wdb.params.network_type())
);
#[cfg(not(feature = "transparent-inputs"))]
let recipient_addr = receiver.to_zcash_address(wdb.params.network_type());
let recipient = Recipient::External(recipient_addr, PoolType::Transparent);
wallet::put_sent_output(
wdb.conn.0,
&wdb.params,
account_id,
tx_ref,
output_index,
&Recipient::Transparent(address),
&recipient,
txout.value,
None,
)?;
@ -1305,13 +1343,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
}
for output in sent_tx.outputs() {
wallet::insert_sent_output(
wdb.conn.0,
&wdb.params,
tx_ref,
*sent_tx.account_id(),
output,
)?;
wallet::insert_sent_output(wdb.conn.0, tx_ref, *sent_tx.account_id(), output)?;
match output.recipient() {
Recipient::InternalAccount {
@ -1880,7 +1912,6 @@ mod tests {
.unwrap();
assert!(current_addr.is_some());
// TODO: Add Orchard
let addr2 = st
.wallet_mut()
.get_next_available_address(account.account_id(), DEFAULT_UA_REQUEST)

View File

@ -1896,7 +1896,7 @@ fn check_proposal_serialization_roundtrip(
db_data: &WalletDb<rusqlite::Connection, LocalNetwork>,
proposal: &Proposal<StandardFeeRule, ReceivedNoteId>,
) {
let proposal_proto = proposal::Proposal::from_standard_proposal(&db_data.params, proposal);
let deserialized_proposal = proposal_proto.try_into_standard_proposal(&db_data.params, db_data);
let proposal_proto = proposal::Proposal::from_standard_proposal(proposal);
let deserialized_proposal = proposal_proto.try_into_standard_proposal(db_data);
assert_matches!(deserialized_proposal, Ok(r) if &r == proposal);
}

View File

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

View File

@ -76,12 +76,9 @@ use std::io::{self, Cursor};
use std::num::NonZeroU32;
use std::ops::RangeInclusive;
use tracing::debug;
use zcash_keys::keys::{
AddressGenerationError, UnifiedAddressRequest, UnifiedIncomingViewingKey, UnifiedSpendingKey,
};
use zcash_address::ZcashAddress;
use zcash_client_backend::{
address::{Address, UnifiedAddress},
data_api::{
scanning::{ScanPriority, ScanRange},
AccountBalance, AccountBirthday, AccountSource, BlockMetadata, Ratio,
@ -92,6 +89,13 @@ use zcash_client_backend::{
wallet::{Note, NoteId, Recipient, WalletTx},
PoolType, ShieldedProtocol,
};
use zcash_keys::{
address::{Address, Receiver, UnifiedAddress},
keys::{
AddressGenerationError, UnifiedAddressRequest, UnifiedIncomingViewingKey,
UnifiedSpendingKey,
},
};
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters},
@ -101,8 +105,8 @@ use zcash_primitives::{
components::{amount::NonNegativeAmount, Amount},
Transaction, TransactionData, TxId,
},
zip32::{self, DiversifierIndex, Scope},
};
use zip32::{self, DiversifierIndex, Scope};
use crate::{
error::SqliteClientError,
@ -2366,6 +2370,48 @@ pub(crate) fn put_tx_meta(
.map_err(SqliteClientError::from)
}
/// Returns the most likely wallet address that corresponds to the protocol-level receiver of a
/// note or UTXO.
pub(crate) fn select_receiving_address<P: consensus::Parameters>(
_params: &P,
conn: &rusqlite::Connection,
account: AccountId,
receiver: &Receiver,
) -> Result<Option<ZcashAddress>, SqliteClientError> {
match receiver {
#[cfg(feature = "transparent-inputs")]
Receiver::Transparent(taddr) => conn
.query_row(
"SELECT address
FROM addresses
WHERE cached_transparent_receiver_address = :taddr",
named_params! {
":taddr": Address::Transparent(*taddr).encode(_params)
},
|row| row.get::<_, String>(0),
)
.optional()?
.map(|addr_str| addr_str.parse::<ZcashAddress>())
.transpose()
.map_err(SqliteClientError::from),
receiver => {
let mut stmt =
conn.prepare_cached("SELECT address FROM addresses WHERE account_id = :account")?;
let mut result = stmt.query(named_params! { ":account": account.0 })?;
while let Some(row) = result.next()? {
let addr_str = row.get::<_, String>(0)?;
let decoded = addr_str.parse::<ZcashAddress>()?;
if receiver.corresponds(&decoded) {
return Ok(Some(decoded));
}
}
Ok(None)
}
}
}
/// Inserts full transaction data into the database.
pub(crate) fn put_tx_data(
conn: &rusqlite::Connection,
@ -2515,24 +2561,17 @@ pub(crate) fn put_legacy_transparent_utxo<P: consensus::Parameters>(
// A utility function for creation of parameters for use in `insert_sent_output`
// and `put_sent_output`
fn recipient_params<P: consensus::Parameters>(
params: &P,
fn recipient_params(
to: &Recipient<AccountId, Note>,
) -> (Option<String>, Option<AccountId>, PoolType) {
match to {
Recipient::Transparent(addr) => (Some(addr.encode(params)), None, PoolType::Transparent),
Recipient::Sapling(addr) => (
Some(addr.encode(params)),
None,
PoolType::Shielded(ShieldedProtocol::Sapling),
),
Recipient::Unified(addr, pool) => (Some(addr.encode(params)), None, *pool),
Recipient::External(addr, pool) => (Some(addr.encode()), None, *pool),
Recipient::InternalAccount {
receiving_account,
external_address,
note,
} => (
external_address.as_ref().map(|a| a.encode(params)),
external_address.as_ref().map(|a| a.encode()),
Some(*receiving_account),
PoolType::Shielded(note.protocol()),
),
@ -2540,9 +2579,8 @@ fn recipient_params<P: consensus::Parameters>(
}
/// Records information about a transaction output that your wallet created.
pub(crate) fn insert_sent_output<P: consensus::Parameters>(
pub(crate) fn insert_sent_output(
conn: &rusqlite::Connection,
params: &P,
tx_ref: i64,
from_account: AccountId,
output: &SentTransactionOutput<AccountId>,
@ -2556,7 +2594,7 @@ pub(crate) fn insert_sent_output<P: consensus::Parameters>(
:to_address, :to_account_id, :value, :memo)",
)?;
let (to_address, to_account_id, pool_type) = recipient_params(params, output.recipient());
let (to_address, to_account_id, pool_type) = recipient_params(output.recipient());
let sql_args = named_params![
":tx": &tx_ref,
":output_pool": &pool_code(pool_type),
@ -2585,9 +2623,8 @@ pub(crate) fn insert_sent_output<P: consensus::Parameters>(
/// - If `recipient` is an internal account, `output_index` is an index into the Sapling outputs of
/// the transaction.
#[allow(clippy::too_many_arguments)]
pub(crate) fn put_sent_output<P: consensus::Parameters>(
pub(crate) fn put_sent_output(
conn: &rusqlite::Connection,
params: &P,
from_account: AccountId,
tx_ref: i64,
output_index: usize,
@ -2610,7 +2647,7 @@ pub(crate) fn put_sent_output<P: consensus::Parameters>(
memo = IFNULL(:memo, memo)",
)?;
let (to_address, to_account_id, pool_type) = recipient_params(params, recipient);
let (to_address, to_account_id, pool_type) = recipient_params(recipient);
let sql_args = named_params![
":tx": &tx_ref,
":output_pool": &pool_code(pool_type),

View File

@ -134,11 +134,10 @@ fn sqlite_client_error_to_wallet_migration_error(e: SqliteClientError) -> Wallet
SqliteClientError::InvalidNote => {
WalletMigrationError::CorruptedData("invalid note".into())
}
SqliteClientError::Bech32DecodeError(e) => {
WalletMigrationError::CorruptedData(e.to_string())
}
SqliteClientError::DecodingError(e) => WalletMigrationError::CorruptedData(e.to_string()),
#[cfg(feature = "transparent-inputs")]
SqliteClientError::HdwalletError(e) => WalletMigrationError::CorruptedData(e.to_string()),
#[cfg(feature = "transparent-inputs")]
SqliteClientError::TransparentAddress(e) => {
WalletMigrationError::CorruptedData(e.to_string())
}

View File

@ -5,6 +5,9 @@ and this library adheres to Rust's notion of
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- `zcash_keys::address::Address::try_from_zcash_address`
- `zcash_keys::address::Receiver`
## [0.2.0] - 2024-03-25
@ -65,6 +68,7 @@ The entries below are relative to the `zcash_client_backend` crate as of
- `UnifiedAddressRequest`
- A new `orchard` feature flag has been added to make it possible to
build client code without `orchard` dependendencies.
- `zcash_keys::address::Address::to_zcash_address`
### Changed
- The following methods and enum variants have been placed behind an `orchard`

View File

@ -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.
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Address {
@ -290,12 +347,24 @@ impl TryFromRawAddress for Address {
}
impl Address {
/// Attempts to decode an [`Address`] value from its [`ZcashAddress`] encoded representation.
///
/// Returns `None` if any error is encountered in decoding. Use
/// [`Self::try_from_zcash_address(s.parse()?)?`] if you need detailed error information.
pub fn decode<P: consensus::Parameters>(params: &P, s: &str) -> Option<Self> {
let addr = ZcashAddress::try_from_encoded(s).ok()?;
addr.convert_if_network(params.network_type()).ok()
Self::try_from_zcash_address(params, s.parse::<ZcashAddress>().ok()?).ok()
}
pub fn encode<P: consensus::Parameters>(&self, params: &P) -> String {
/// Attempts to decode an [`Address`] value from its [`ZcashAddress`] encoded representation.
pub fn try_from_zcash_address<P: consensus::Parameters>(
params: &P,
zaddr: ZcashAddress,
) -> Result<Self, ConversionError<&'static str>> {
zaddr.convert_if_network(params.network_type())
}
/// Converts this [`Address`] to its encoded [`ZcashAddress`] representation.
pub fn to_zcash_address<P: consensus::Parameters>(&self, params: &P) -> ZcashAddress {
let net = params.network_type();
match self {
@ -311,9 +380,14 @@ impl Address {
},
Address::Unified(ua) => ua.to_address(net),
}
.to_string()
}
/// Converts this [`Address`] to its encoded string representation.
pub fn encode<P: consensus::Parameters>(&self, params: &P) -> String {
self.to_zcash_address(params).to_string()
}
/// Returns whether or not this [`Address`] can send funds to the specified pool.
pub fn has_receiver(&self, pool_type: PoolType) -> bool {
match self {
#[cfg(feature = "sapling")]