Compare commits
34 Commits
078b4ffa45
...
728e2f0cf3
Author | SHA1 | Date |
---|---|---|
Kris Nuttycombe | 728e2f0cf3 | |
Kris Nuttycombe | cab4d84464 | |
Kris Nuttycombe | aea04d1ad2 | |
Kris Nuttycombe | c0542c9589 | |
Kris Nuttycombe | 34ec1e5bdb | |
Kris Nuttycombe | 0341171c84 | |
Kris Nuttycombe | c3e1750007 | |
str4d | 5c6a6a4c86 | |
Kris Nuttycombe | d2aa6cfc7f | |
Kris Nuttycombe | aeac544aed | |
str4d | 5df164b2a4 | |
Jack Grigg | d4b405f03f | |
Jack Grigg | cc1bcb8a4a | |
str4d | 3877c38a2c | |
str4d | 390b929663 | |
Daira-Emma Hopwood | ea82dbeb64 | |
Kris Nuttycombe | f28aa6b304 | |
dependabot[bot] | 5cf83c5a44 | |
dependabot[bot] | b42b4984ad | |
Kris Nuttycombe | 19fce07c4d | |
Kris Nuttycombe | a7de8a3dba | |
Kris Nuttycombe | e37e490450 | |
Kris Nuttycombe | f7c29f0111 | |
Kris Nuttycombe | 20e8bca8d9 | |
Kris Nuttycombe | b60600a4c3 | |
Kris Nuttycombe | 86e1181259 | |
Kris Nuttycombe | 3ea7d84183 | |
Kris Nuttycombe | d982d7826a | |
Kris Nuttycombe | fdf86ad740 | |
Kris Nuttycombe | 07d5aa4a79 | |
Kris Nuttycombe | bbb8d1090a | |
Kris Nuttycombe | 430212cd53 | |
Jack Grigg | 7f017bc126 | |
Jack Grigg | 24277a6ba4 |
|
@ -28,6 +28,7 @@ runs:
|
|||
bundled-prover
|
||||
download-params
|
||||
lightwalletd-tonic
|
||||
sync
|
||||
temporary-zcashd
|
||||
transparent-inputs
|
||||
unstable
|
||||
|
|
|
@ -31,7 +31,7 @@ jobs:
|
|||
mv ./target/doc ./book/book/rustdoc/latest
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./book/book
|
||||
|
|
|
@ -318,7 +318,7 @@ jobs:
|
|||
- id: prepare
|
||||
uses: ./.github/actions/prepare
|
||||
- name: Install protoc
|
||||
uses: supplypike/setup-bin@v3
|
||||
uses: supplypike/setup-bin@v4
|
||||
with:
|
||||
uri: 'https://github.com/protocolbuffers/protobuf/releases/download/v25.1/protoc-25.1-linux-x86_64.zip'
|
||||
name: 'protoc'
|
||||
|
|
|
@ -783,6 +783,17 @@ version = "0.3.29"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.29"
|
||||
|
@ -802,9 +813,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-macro",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3021,6 +3034,7 @@ dependencies = [
|
|||
"byteorder",
|
||||
"crossbeam-channel",
|
||||
"document-features",
|
||||
"futures-util",
|
||||
"group",
|
||||
"gumdrop",
|
||||
"hdwallet",
|
||||
|
@ -3054,11 +3068,12 @@ dependencies = [
|
|||
"zcash_proofs",
|
||||
"zcash_protocol",
|
||||
"zip32",
|
||||
"zip321",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zcash_client_sqlite"
|
||||
version = "0.10.2"
|
||||
version = "0.10.3"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"bls12_381",
|
||||
|
@ -3313,3 +3328,15 @@ dependencies = [
|
|||
"memuse",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip321"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"nom",
|
||||
"percent-encoding",
|
||||
"proptest",
|
||||
"zcash_address",
|
||||
"zcash_protocol",
|
||||
]
|
||||
|
|
|
@ -5,6 +5,7 @@ members = [
|
|||
"components/zcash_address",
|
||||
"components/zcash_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 }
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
// Catch documentation errors caused by code changes.
|
||||
#![deny(rustdoc::broken_intra_doc_links)]
|
||||
|
||||
mod minimal;
|
||||
mod params;
|
||||
mod verify;
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
use std::io::Cursor;
|
||||
use std::mem::size_of;
|
||||
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
|
||||
use crate::params::Params;
|
||||
|
||||
pub(crate) fn expand_array(vin: &[u8], bit_len: usize, byte_pad: usize) -> Vec<u8> {
|
||||
assert!(bit_len >= 8);
|
||||
assert!(u32::BITS as usize >= 7 + bit_len);
|
||||
|
||||
let out_width = (bit_len + 7) / 8 + byte_pad;
|
||||
let out_len = 8 * out_width * vin.len() / bit_len;
|
||||
|
||||
// Shortcut for parameters where expansion is a no-op
|
||||
if out_len == vin.len() {
|
||||
return vin.to_vec();
|
||||
}
|
||||
|
||||
let mut vout: Vec<u8> = vec![0; out_len];
|
||||
let bit_len_mask: u32 = (1 << bit_len) - 1;
|
||||
|
||||
// The acc_bits least-significant bits of acc_value represent a bit sequence
|
||||
// in big-endian order.
|
||||
let mut acc_bits = 0;
|
||||
let mut acc_value: u32 = 0;
|
||||
|
||||
let mut j = 0;
|
||||
for b in vin {
|
||||
acc_value = (acc_value << 8) | u32::from(*b);
|
||||
acc_bits += 8;
|
||||
|
||||
// When we have bit_len or more bits in the accumulator, write the next
|
||||
// output element.
|
||||
if acc_bits >= bit_len {
|
||||
acc_bits -= bit_len;
|
||||
for x in byte_pad..out_width {
|
||||
vout[j + x] = ((
|
||||
// Big-endian
|
||||
acc_value >> (acc_bits + (8 * (out_width - x - 1)))
|
||||
) & (
|
||||
// Apply bit_len_mask across byte boundaries
|
||||
(bit_len_mask >> (8 * (out_width - x - 1))) & 0xFF
|
||||
)) as u8;
|
||||
}
|
||||
j += out_width;
|
||||
}
|
||||
}
|
||||
|
||||
vout
|
||||
}
|
||||
|
||||
/// Returns `None` if the parameters are invalid for this minimal encoding.
|
||||
pub(crate) fn indices_from_minimal(p: Params, minimal: &[u8]) -> Option<Vec<u32>> {
|
||||
let c_bit_len = p.collision_bit_length();
|
||||
// Division is exact because k >= 3.
|
||||
if minimal.len() != ((1 << p.k) * (c_bit_len + 1)) / 8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
assert!(((c_bit_len + 1) + 7) / 8 <= size_of::<u32>());
|
||||
let len_indices = u32::BITS as usize * minimal.len() / (c_bit_len + 1);
|
||||
let byte_pad = size_of::<u32>() - ((c_bit_len + 1) + 7) / 8;
|
||||
|
||||
let mut csr = Cursor::new(expand_array(minimal, c_bit_len + 1, byte_pad));
|
||||
let mut ret = Vec::with_capacity(len_indices);
|
||||
|
||||
// Big-endian so that lexicographic array comparison is equivalent to integer
|
||||
// comparison
|
||||
while let Ok(i) = csr.read_u32::<BigEndian>() {
|
||||
ret.push(i);
|
||||
}
|
||||
|
||||
Some(ret)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{expand_array, indices_from_minimal, Params};
|
||||
|
||||
#[test]
|
||||
fn array_expansion() {
|
||||
let check_array = |(bit_len, byte_pad), compact, expanded| {
|
||||
assert_eq!(expand_array(compact, bit_len, byte_pad), expanded);
|
||||
};
|
||||
|
||||
// 8 11-bit chunks, all-ones
|
||||
check_array(
|
||||
(11, 0),
|
||||
&[
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
],
|
||||
&[
|
||||
0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff,
|
||||
0x07, 0xff,
|
||||
][..],
|
||||
);
|
||||
// 8 21-bit chunks, alternating 1s and 0s
|
||||
check_array(
|
||||
(21, 0),
|
||||
&[
|
||||
0xaa, 0xaa, 0xad, 0x55, 0x55, 0x6a, 0xaa, 0xab, 0x55, 0x55, 0x5a, 0xaa, 0xaa, 0xd5,
|
||||
0x55, 0x56, 0xaa, 0xaa, 0xb5, 0x55, 0x55,
|
||||
],
|
||||
&[
|
||||
0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55,
|
||||
0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55,
|
||||
][..],
|
||||
);
|
||||
// 8 21-bit chunks, based on example in the spec
|
||||
check_array(
|
||||
(21, 0),
|
||||
&[
|
||||
0x00, 0x02, 0x20, 0x00, 0x0a, 0x7f, 0xff, 0xfe, 0x00, 0x12, 0x30, 0x22, 0xb3, 0x82,
|
||||
0x26, 0xac, 0x19, 0xbd, 0xf2, 0x34, 0x56,
|
||||
],
|
||||
&[
|
||||
0x00, 0x00, 0x44, 0x00, 0x00, 0x29, 0x1f, 0xff, 0xff, 0x00, 0x01, 0x23, 0x00, 0x45,
|
||||
0x67, 0x00, 0x89, 0xab, 0x00, 0xcd, 0xef, 0x12, 0x34, 0x56,
|
||||
][..],
|
||||
);
|
||||
// 16 14-bit chunks, alternating 11s and 00s
|
||||
check_array(
|
||||
(14, 0),
|
||||
&[
|
||||
0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33, 0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33,
|
||||
0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33, 0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33,
|
||||
],
|
||||
&[
|
||||
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
|
||||
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
|
||||
0x33, 0x33, 0x33, 0x33,
|
||||
][..],
|
||||
);
|
||||
// 8 11-bit chunks, all-ones, 2-byte padding
|
||||
check_array(
|
||||
(11, 2),
|
||||
&[
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
],
|
||||
&[
|
||||
0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00,
|
||||
0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff,
|
||||
0x00, 0x00, 0x07, 0xff,
|
||||
][..],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimal_solution_repr() {
|
||||
let check_repr = |minimal, indices| {
|
||||
assert_eq!(
|
||||
indices_from_minimal(Params { n: 80, k: 3 }, minimal).unwrap(),
|
||||
indices,
|
||||
);
|
||||
};
|
||||
|
||||
// The solutions here are not intended to be valid.
|
||||
check_repr(
|
||||
&[
|
||||
0x00, 0x00, 0x08, 0x00, 0x00, 0x40, 0x00, 0x02, 0x00, 0x00, 0x10, 0x00, 0x00, 0x80,
|
||||
0x00, 0x04, 0x00, 0x00, 0x20, 0x00, 0x01,
|
||||
],
|
||||
&[1, 1, 1, 1, 1, 1, 1, 1],
|
||||
);
|
||||
check_repr(
|
||||
&[
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
],
|
||||
&[
|
||||
2097151, 2097151, 2097151, 2097151, 2097151, 2097151, 2097151, 2097151,
|
||||
],
|
||||
);
|
||||
check_repr(
|
||||
&[
|
||||
0x0f, 0xff, 0xf8, 0x00, 0x20, 0x03, 0xff, 0xfe, 0x00, 0x08, 0x00, 0xff, 0xff, 0x80,
|
||||
0x02, 0x00, 0x3f, 0xff, 0xe0, 0x00, 0x80,
|
||||
],
|
||||
&[131071, 128, 131071, 128, 131071, 128, 131071, 128],
|
||||
);
|
||||
check_repr(
|
||||
&[
|
||||
0x00, 0x02, 0x20, 0x00, 0x0a, 0x7f, 0xff, 0xfe, 0x00, 0x4d, 0x10, 0x01, 0x4c, 0x80,
|
||||
0x0f, 0xfc, 0x00, 0x00, 0x2f, 0xff, 0xff,
|
||||
],
|
||||
&[68, 41, 2097151, 1233, 665, 1023, 1, 1048575],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct Params {
|
||||
pub(crate) n: u32,
|
||||
pub(crate) k: u32,
|
||||
}
|
||||
|
||||
impl Params {
|
||||
/// Returns `None` if the parameters are invalid.
|
||||
pub(crate) fn new(n: u32, k: u32) -> Option<Self> {
|
||||
// We place the following requirements on the parameters:
|
||||
// - n is a multiple of 8, so the hash output has an exact byte length.
|
||||
// - k >= 3 so the encoded solutions have an exact byte length.
|
||||
// - k < n, so the collision bit length is at least 1.
|
||||
// - n is a multiple of k + 1, so we have an integer collision bit length.
|
||||
if (n % 8 == 0) && (k >= 3) && (k < n) && (n % (k + 1) == 0) {
|
||||
Some(Params { n, k })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub(crate) fn indices_per_hash_output(&self) -> u32 {
|
||||
512 / self.n
|
||||
}
|
||||
pub(crate) fn hash_output(&self) -> u8 {
|
||||
(self.indices_per_hash_output() * self.n / 8) as u8
|
||||
}
|
||||
pub(crate) fn collision_bit_length(&self) -> usize {
|
||||
(self.n / (self.k + 1)) as usize
|
||||
}
|
||||
pub(crate) fn collision_byte_length(&self) -> usize {
|
||||
(self.collision_bit_length() + 7) / 8
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(crate) fn hash_length(&self) -> usize {
|
||||
((self.k as usize) + 1) * self.collision_byte_length()
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use crate::verify::{Kind, Params};
|
||||
use crate::{params::Params, verify::Kind};
|
||||
|
||||
pub(crate) struct TestVector {
|
||||
pub(crate) params: Params,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::verify::Params;
|
||||
use crate::params::Params;
|
||||
|
||||
pub(crate) struct TestVector {
|
||||
pub(crate) params: Params,
|
||||
|
|
|
@ -3,16 +3,13 @@
|
|||
//! [Equihash]: https://zips.z.cash/protocol/protocol.pdf#equihash
|
||||
|
||||
use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams, State as Blake2bState};
|
||||
use byteorder::{BigEndian, LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::mem::size_of;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct Params {
|
||||
pub(crate) n: u32,
|
||||
pub(crate) k: u32,
|
||||
}
|
||||
use crate::{
|
||||
minimal::{expand_array, indices_from_minimal},
|
||||
params::Params,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Node {
|
||||
|
@ -20,37 +17,6 @@ struct Node {
|
|||
indices: Vec<u32>,
|
||||
}
|
||||
|
||||
impl Params {
|
||||
fn new(n: u32, k: u32) -> Result<Self, Error> {
|
||||
// We place the following requirements on the parameters:
|
||||
// - n is a multiple of 8, so the hash output has an exact byte length.
|
||||
// - k >= 3 so the encoded solutions have an exact byte length.
|
||||
// - k < n, so the collision bit length is at least 1.
|
||||
// - n is a multiple of k + 1, so we have an integer collision bit length.
|
||||
if (n % 8 == 0) && (k >= 3) && (k < n) && (n % (k + 1) == 0) {
|
||||
Ok(Params { n, k })
|
||||
} else {
|
||||
Err(Error(Kind::InvalidParams))
|
||||
}
|
||||
}
|
||||
fn indices_per_hash_output(&self) -> u32 {
|
||||
512 / self.n
|
||||
}
|
||||
fn hash_output(&self) -> u8 {
|
||||
(self.indices_per_hash_output() * self.n / 8) as u8
|
||||
}
|
||||
fn collision_bit_length(&self) -> usize {
|
||||
(self.n / (self.k + 1)) as usize
|
||||
}
|
||||
fn collision_byte_length(&self) -> usize {
|
||||
(self.collision_bit_length() + 7) / 8
|
||||
}
|
||||
#[cfg(test)]
|
||||
fn hash_length(&self) -> usize {
|
||||
((self.k as usize) + 1) * self.collision_byte_length()
|
||||
}
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn new(p: &Params, state: &Blake2bState, i: u32) -> Self {
|
||||
let hash = generate_hash(state, i / p.indices_per_hash_output());
|
||||
|
@ -168,74 +134,6 @@ fn generate_hash(base_state: &Blake2bState, i: u32) -> Blake2bHash {
|
|||
state.finalize()
|
||||
}
|
||||
|
||||
fn expand_array(vin: &[u8], bit_len: usize, byte_pad: usize) -> Vec<u8> {
|
||||
assert!(bit_len >= 8);
|
||||
assert!(u32::BITS as usize >= 7 + bit_len);
|
||||
|
||||
let out_width = (bit_len + 7) / 8 + byte_pad;
|
||||
let out_len = 8 * out_width * vin.len() / bit_len;
|
||||
|
||||
// Shortcut for parameters where expansion is a no-op
|
||||
if out_len == vin.len() {
|
||||
return vin.to_vec();
|
||||
}
|
||||
|
||||
let mut vout: Vec<u8> = vec![0; out_len];
|
||||
let bit_len_mask: u32 = (1 << bit_len) - 1;
|
||||
|
||||
// The acc_bits least-significant bits of acc_value represent a bit sequence
|
||||
// in big-endian order.
|
||||
let mut acc_bits = 0;
|
||||
let mut acc_value: u32 = 0;
|
||||
|
||||
let mut j = 0;
|
||||
for b in vin {
|
||||
acc_value = (acc_value << 8) | u32::from(*b);
|
||||
acc_bits += 8;
|
||||
|
||||
// When we have bit_len or more bits in the accumulator, write the next
|
||||
// output element.
|
||||
if acc_bits >= bit_len {
|
||||
acc_bits -= bit_len;
|
||||
for x in byte_pad..out_width {
|
||||
vout[j + x] = ((
|
||||
// Big-endian
|
||||
acc_value >> (acc_bits + (8 * (out_width - x - 1)))
|
||||
) & (
|
||||
// Apply bit_len_mask across byte boundaries
|
||||
(bit_len_mask >> (8 * (out_width - x - 1))) & 0xFF
|
||||
)) as u8;
|
||||
}
|
||||
j += out_width;
|
||||
}
|
||||
}
|
||||
|
||||
vout
|
||||
}
|
||||
|
||||
fn indices_from_minimal(p: Params, minimal: &[u8]) -> Result<Vec<u32>, Error> {
|
||||
let c_bit_len = p.collision_bit_length();
|
||||
// Division is exact because k >= 3.
|
||||
if minimal.len() != ((1 << p.k) * (c_bit_len + 1)) / 8 {
|
||||
return Err(Error(Kind::InvalidParams));
|
||||
}
|
||||
|
||||
assert!(((c_bit_len + 1) + 7) / 8 <= size_of::<u32>());
|
||||
let len_indices = u32::BITS as usize * minimal.len() / (c_bit_len + 1);
|
||||
let byte_pad = size_of::<u32>() - ((c_bit_len + 1) + 7) / 8;
|
||||
|
||||
let mut csr = Cursor::new(expand_array(minimal, c_bit_len + 1, byte_pad));
|
||||
let mut ret = Vec::with_capacity(len_indices);
|
||||
|
||||
// Big-endian so that lexicographic array comparison is equivalent to integer
|
||||
// comparison
|
||||
while let Ok(i) = csr.read_u32::<BigEndian>() {
|
||||
ret.push(i);
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn has_collision(a: &Node, b: &Node, len: usize) -> bool {
|
||||
a.hash
|
||||
.iter()
|
||||
|
@ -347,8 +245,8 @@ pub fn is_valid_solution(
|
|||
nonce: &[u8],
|
||||
soln: &[u8],
|
||||
) -> Result<(), Error> {
|
||||
let p = Params::new(n, k)?;
|
||||
let indices = indices_from_minimal(p, soln)?;
|
||||
let p = Params::new(n, k).ok_or(Error(Kind::InvalidParams))?;
|
||||
let indices = indices_from_minimal(p, soln).ok_or(Error(Kind::InvalidParams))?;
|
||||
|
||||
// Recursive validation is faster
|
||||
is_valid_solution_recursive(p, input, nonce, &indices)
|
||||
|
@ -356,122 +254,9 @@ pub fn is_valid_solution(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
expand_array, indices_from_minimal, is_valid_solution, is_valid_solution_iterative,
|
||||
is_valid_solution_recursive, Params,
|
||||
};
|
||||
use super::{is_valid_solution, is_valid_solution_iterative, is_valid_solution_recursive};
|
||||
use crate::test_vectors::{INVALID_TEST_VECTORS, VALID_TEST_VECTORS};
|
||||
|
||||
#[test]
|
||||
fn array_expansion() {
|
||||
let check_array = |(bit_len, byte_pad), compact, expanded| {
|
||||
assert_eq!(expand_array(compact, bit_len, byte_pad), expanded);
|
||||
};
|
||||
|
||||
// 8 11-bit chunks, all-ones
|
||||
check_array(
|
||||
(11, 0),
|
||||
&[
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
],
|
||||
&[
|
||||
0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff,
|
||||
0x07, 0xff,
|
||||
][..],
|
||||
);
|
||||
// 8 21-bit chunks, alternating 1s and 0s
|
||||
check_array(
|
||||
(21, 0),
|
||||
&[
|
||||
0xaa, 0xaa, 0xad, 0x55, 0x55, 0x6a, 0xaa, 0xab, 0x55, 0x55, 0x5a, 0xaa, 0xaa, 0xd5,
|
||||
0x55, 0x56, 0xaa, 0xaa, 0xb5, 0x55, 0x55,
|
||||
],
|
||||
&[
|
||||
0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55,
|
||||
0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55,
|
||||
][..],
|
||||
);
|
||||
// 8 21-bit chunks, based on example in the spec
|
||||
check_array(
|
||||
(21, 0),
|
||||
&[
|
||||
0x00, 0x02, 0x20, 0x00, 0x0a, 0x7f, 0xff, 0xfe, 0x00, 0x12, 0x30, 0x22, 0xb3, 0x82,
|
||||
0x26, 0xac, 0x19, 0xbd, 0xf2, 0x34, 0x56,
|
||||
],
|
||||
&[
|
||||
0x00, 0x00, 0x44, 0x00, 0x00, 0x29, 0x1f, 0xff, 0xff, 0x00, 0x01, 0x23, 0x00, 0x45,
|
||||
0x67, 0x00, 0x89, 0xab, 0x00, 0xcd, 0xef, 0x12, 0x34, 0x56,
|
||||
][..],
|
||||
);
|
||||
// 16 14-bit chunks, alternating 11s and 00s
|
||||
check_array(
|
||||
(14, 0),
|
||||
&[
|
||||
0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33, 0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33,
|
||||
0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33, 0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33,
|
||||
],
|
||||
&[
|
||||
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
|
||||
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
|
||||
0x33, 0x33, 0x33, 0x33,
|
||||
][..],
|
||||
);
|
||||
// 8 11-bit chunks, all-ones, 2-byte padding
|
||||
check_array(
|
||||
(11, 2),
|
||||
&[
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
],
|
||||
&[
|
||||
0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00,
|
||||
0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff,
|
||||
0x00, 0x00, 0x07, 0xff,
|
||||
][..],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimal_solution_repr() {
|
||||
let check_repr = |minimal, indices| {
|
||||
assert_eq!(
|
||||
indices_from_minimal(Params { n: 80, k: 3 }, minimal).unwrap(),
|
||||
indices,
|
||||
);
|
||||
};
|
||||
|
||||
// The solutions here are not intended to be valid.
|
||||
check_repr(
|
||||
&[
|
||||
0x00, 0x00, 0x08, 0x00, 0x00, 0x40, 0x00, 0x02, 0x00, 0x00, 0x10, 0x00, 0x00, 0x80,
|
||||
0x00, 0x04, 0x00, 0x00, 0x20, 0x00, 0x01,
|
||||
],
|
||||
&[1, 1, 1, 1, 1, 1, 1, 1],
|
||||
);
|
||||
check_repr(
|
||||
&[
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
],
|
||||
&[
|
||||
2097151, 2097151, 2097151, 2097151, 2097151, 2097151, 2097151, 2097151,
|
||||
],
|
||||
);
|
||||
check_repr(
|
||||
&[
|
||||
0x0f, 0xff, 0xf8, 0x00, 0x20, 0x03, 0xff, 0xfe, 0x00, 0x08, 0x00, 0xff, 0xff, 0x80,
|
||||
0x02, 0x00, 0x3f, 0xff, 0xe0, 0x00, 0x80,
|
||||
],
|
||||
&[131071, 128, 131071, 128, 131071, 128, 131071, 128],
|
||||
);
|
||||
check_repr(
|
||||
&[
|
||||
0x00, 0x02, 0x20, 0x00, 0x0a, 0x7f, 0xff, 0xfe, 0x00, 0x4d, 0x10, 0x01, 0x4c, 0x80,
|
||||
0x0f, 0xfc, 0x00, 0x00, 0x2f, 0xff, 0xff,
|
||||
],
|
||||
&[68, 41, 2097151, 1233, 665, 1023, 1, 1048575],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_test_vectors() {
|
||||
for tv in VALID_TEST_VECTORS {
|
||||
|
|
|
@ -7,6 +7,31 @@ and this library adheres to Rust's notion of
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- `zcash_address::ZcashAddress::{can_receive_memo, can_receive_as, matches_receiver}`
|
||||
- `zcash_address::unified`:
|
||||
- `Address::{can_receive_memo, has_receiver_of_type, contains_receiver, receivers}`
|
||||
- `Container::revision`
|
||||
- `DataTypecode`
|
||||
- `Item`
|
||||
- `MetadataItem`
|
||||
- `MetadataTypecode`
|
||||
- `Revision`
|
||||
- Module `zcash_address::testing` under the `test-dependencies` feature.
|
||||
- Module `zcash_address::unified::address::testing` under the
|
||||
`test-dependencies` feature.
|
||||
|
||||
### Changed
|
||||
- `zcash_address::unified`:
|
||||
- `Typecode` has changed. Instead of having a variant for each receiver type,
|
||||
it now has two variants, `Typecode::Data` and `Typecode::Metadata`.
|
||||
- `Encoding::try_from_items` now takes an additional `Revision` argument.
|
||||
|
||||
### Removed
|
||||
- `zcash_address::unified::Container::items` Preference order is only
|
||||
significant when considering unified address receivers; use
|
||||
`Address::receivers` instead.
|
||||
|
||||
## [0.3.2] - 2024-03-06
|
||||
### Added
|
||||
- `zcash_address::convert`:
|
||||
|
|
|
@ -19,18 +19,19 @@ all-features = true
|
|||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[dependencies]
|
||||
bech32 = "0.9"
|
||||
bs58 = { version = "0.5", features = ["check"] }
|
||||
bech32.workspace = true
|
||||
bs58.workspace = true
|
||||
f4jumble = { version = "0.1", path = "../f4jumble" }
|
||||
zcash_protocol.workspace = true
|
||||
zcash_encoding.workspace = true
|
||||
proptest = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.3.0"
|
||||
proptest = "1"
|
||||
assert_matches.workspace = true
|
||||
proptest.workspace = true
|
||||
|
||||
[features]
|
||||
test-dependencies = []
|
||||
test-dependencies = ["dep:proptest"]
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
|
|
@ -180,7 +180,11 @@ mod tests {
|
|||
use assert_matches::assert_matches;
|
||||
|
||||
use super::*;
|
||||
use crate::{kind::unified, Network};
|
||||
use crate::{
|
||||
kind::unified,
|
||||
unified::{Item, Receiver, Revision},
|
||||
Network,
|
||||
};
|
||||
|
||||
fn encoding(encoded: &str, decoded: ZcashAddress) {
|
||||
assert_eq!(decoded.to_string(), encoded);
|
||||
|
@ -230,21 +234,30 @@ mod tests {
|
|||
"u1qpatys4zruk99pg59gcscrt7y6akvl9vrhcfyhm9yxvxz7h87q6n8cgrzzpe9zru68uq39uhmlpp5uefxu0su5uqyqfe5zp3tycn0ecl",
|
||||
ZcashAddress {
|
||||
net: Network::Main,
|
||||
kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])),
|
||||
kind: AddressKind::Unified(unified::Address {
|
||||
revision: Revision::R0,
|
||||
receivers: vec![Item::Data(Receiver::Sapling([0; 43]))]
|
||||
}),
|
||||
},
|
||||
);
|
||||
encoding(
|
||||
"utest10c5kutapazdnf8ztl3pu43nkfsjx89fy3uuff8tsmxm6s86j37pe7uz94z5jhkl49pqe8yz75rlsaygexk6jpaxwx0esjr8wm5ut7d5s",
|
||||
ZcashAddress {
|
||||
net: Network::Test,
|
||||
kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])),
|
||||
kind: AddressKind::Unified(unified::Address {
|
||||
revision: Revision::R0,
|
||||
receivers: vec![Item::Data(Receiver::Sapling([0; 43]))]
|
||||
}),
|
||||
},
|
||||
);
|
||||
encoding(
|
||||
"uregtest15xk7vj4grjkay6mnfl93dhsflc2yeunhxwdh38rul0rq3dfhzzxgm5szjuvtqdha4t4p2q02ks0jgzrhjkrav70z9xlvq0plpcjkd5z3",
|
||||
ZcashAddress {
|
||||
net: Network::Regtest,
|
||||
kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])),
|
||||
kind: AddressKind::Unified(unified::Address {
|
||||
revision: Revision::R0,
|
||||
receivers: vec![Item::Data(Receiver::Sapling([0; 43]))]
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ use std::convert::{TryFrom, TryInto};
|
|||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::num::TryFromIntError;
|
||||
use zcash_encoding::MAX_COMPACT_SIZE;
|
||||
|
||||
use crate::Network;
|
||||
|
||||
|
@ -22,9 +23,9 @@ const PADDING_LEN: usize = 16;
|
|||
/// The known Receiver and Viewing Key types.
|
||||
///
|
||||
/// The typecodes `0xFFFA..=0xFFFF` reserved for experiments are currently not
|
||||
/// distinguished from unknown values, and will be parsed as [`Typecode::Unknown`].
|
||||
/// distinguished from unknown values, and will be parsed as [`DataTypecode::Unknown`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Typecode {
|
||||
pub enum DataTypecode {
|
||||
/// A transparent P2PKH address, FVK, or IVK encoding as specified in [ZIP 316](https://zips.z.cash/zip-0316).
|
||||
P2pkh,
|
||||
/// A transparent P2SH address.
|
||||
|
@ -39,7 +40,37 @@ pub enum Typecode {
|
|||
Unknown(u32),
|
||||
}
|
||||
|
||||
impl Typecode {
|
||||
impl TryFrom<u32> for DataTypecode {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(typecode: u32) -> Result<Self, Self::Error> {
|
||||
match typecode {
|
||||
0x00 => Ok(DataTypecode::P2pkh),
|
||||
0x01 => Ok(DataTypecode::P2sh),
|
||||
0x02 => Ok(DataTypecode::Sapling),
|
||||
0x03 => Ok(DataTypecode::Orchard),
|
||||
0x04..=0xBF | 0xFD..=MAX_COMPACT_SIZE => Ok(DataTypecode::Unknown(typecode)),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DataTypecode> for u32 {
|
||||
fn from(t: DataTypecode) -> Self {
|
||||
match t {
|
||||
DataTypecode::P2pkh => 0x00,
|
||||
DataTypecode::P2sh => 0x01,
|
||||
DataTypecode::Sapling => 0x02,
|
||||
DataTypecode::Orchard => 0x03,
|
||||
DataTypecode::Unknown(typecode) => typecode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataTypecode {
|
||||
/// A total ordering over the data typecodes that can be used to sort
|
||||
/// receivers and/or key items in order of decreasing priority,
|
||||
/// as specified in [ZIP 316](https://zips.z.cash/zip-0316#encoding-of-unified-addresses)
|
||||
pub fn preference_order(a: &Self, b: &Self) -> cmp::Ordering {
|
||||
match (a, b) {
|
||||
// Trivial equality checks.
|
||||
|
@ -69,51 +100,213 @@ impl Typecode {
|
|||
(_, Self::P2pkh) => cmp::Ordering::Greater,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering {
|
||||
u32::from(*a).cmp(&u32::from(*b))
|
||||
/// The known Metadata Typecodes
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum MetadataTypecode {
|
||||
/// Expiration height metadata as specified in [ZIP 316, Revision 1](https://zips.z.cash/zip-0316)
|
||||
ExpiryHeight,
|
||||
/// Expiration height metadata as specified in [ZIP 316, Revision 1](https://zips.z.cash/zip-0316)
|
||||
ExpiryTime,
|
||||
/// An unknown MUST-understand metadata item as specified in
|
||||
/// [ZIP 316, Revision 1](https://zips.z.cash/zip-0316)
|
||||
///
|
||||
/// A parser encountering this typecode MUST halt with an error.
|
||||
MustUnderstand(u32),
|
||||
/// An unknown metadata item as specified in [ZIP 316, Revision 1](https://zips.z.cash/zip-0316)
|
||||
Unknown(u32),
|
||||
}
|
||||
|
||||
impl TryFrom<u32> for MetadataTypecode {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(typecode: u32) -> Result<Self, Self::Error> {
|
||||
match typecode {
|
||||
0xC0..=0xDF => Ok(MetadataTypecode::Unknown(typecode)),
|
||||
0xE0 => Ok(MetadataTypecode::ExpiryHeight),
|
||||
0xE1 => Ok(MetadataTypecode::ExpiryTime),
|
||||
0xE2..=0xFC => Ok(MetadataTypecode::MustUnderstand(typecode)),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MetadataTypecode> for u32 {
|
||||
fn from(t: MetadataTypecode) -> Self {
|
||||
match t {
|
||||
MetadataTypecode::ExpiryHeight => 0xE0,
|
||||
MetadataTypecode::ExpiryTime => 0xE1,
|
||||
MetadataTypecode::MustUnderstand(value) => value,
|
||||
MetadataTypecode::Unknown(value) => value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An enumeration of the Unified Container Item Typecodes.
|
||||
///
|
||||
/// Unified Address Items are partitioned into two sets: data items, which include
|
||||
/// receivers and viewing keys, and metadata items.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Typecode {
|
||||
/// A data (receiver or viewing key) typecode.
|
||||
Data(DataTypecode),
|
||||
/// A metadata typecode.
|
||||
Metadata(MetadataTypecode),
|
||||
}
|
||||
|
||||
impl Typecode {
|
||||
/// The typecode for p2pkh data items.
|
||||
pub const P2PKH: Typecode = Typecode::Data(DataTypecode::P2pkh);
|
||||
/// The typecode for p2sh data items.
|
||||
pub const P2SH: Typecode = Typecode::Data(DataTypecode::P2sh);
|
||||
/// The typecode for Sapling data items.
|
||||
pub const SAPLING: Typecode = Typecode::Data(DataTypecode::Sapling);
|
||||
/// The typecode for Orchard data items.
|
||||
pub const ORCHARD: Typecode = Typecode::Data(DataTypecode::Orchard);
|
||||
}
|
||||
|
||||
impl TryFrom<u32> for Typecode {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from(typecode: u32) -> Result<Self, Self::Error> {
|
||||
match typecode {
|
||||
0x00 => Ok(Typecode::P2pkh),
|
||||
0x01 => Ok(Typecode::P2sh),
|
||||
0x02 => Ok(Typecode::Sapling),
|
||||
0x03 => Ok(Typecode::Orchard),
|
||||
0x04..=0x02000000 => Ok(Typecode::Unknown(typecode)),
|
||||
0x02000001..=u32::MAX => Err(ParseError::InvalidTypecodeValue(typecode as u64)),
|
||||
}
|
||||
DataTypecode::try_from(typecode)
|
||||
.map_or_else(
|
||||
|()| MetadataTypecode::try_from(typecode).map(Typecode::Metadata),
|
||||
|t| Ok(Typecode::Data(t)),
|
||||
)
|
||||
.map_err(|()| ParseError::InvalidTypecodeValue(typecode))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Typecode> for u32 {
|
||||
fn from(t: Typecode) -> Self {
|
||||
match t {
|
||||
Typecode::P2pkh => 0x00,
|
||||
Typecode::P2sh => 0x01,
|
||||
Typecode::Sapling => 0x02,
|
||||
Typecode::Orchard => 0x03,
|
||||
Typecode::Unknown(typecode) => typecode,
|
||||
Typecode::Data(tc) => tc.into(),
|
||||
Typecode::Metadata(tc) => tc.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Typecode> for usize {
|
||||
type Error = TryFromIntError;
|
||||
|
||||
fn try_from(t: Typecode) -> Result<Self, Self::Error> {
|
||||
u32::from(t).try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Typecode {
|
||||
fn is_transparent(&self) -> bool {
|
||||
// Unknown typecodes are treated as not transparent for the purpose of disallowing
|
||||
// only-transparent UAs, which can be represented with existing address encodings.
|
||||
matches!(self, Typecode::P2pkh | Typecode::P2sh)
|
||||
/// An enumeration of known Unified Metadata Item types.
|
||||
///
|
||||
/// Unknown MUST-understand metadata items are NOT represented using this type, as the presence of
|
||||
/// an unrecognized metadata item with a typecode in the `MUST-understand` range will result in a
|
||||
/// parse failure, instead of the construction of a metadata item.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum MetadataItem {
|
||||
/// The expiry height for a Unified Address or Unified Viewing Key
|
||||
ExpiryHeight(u32),
|
||||
/// The expiry time for a Unified Address or Unified Viewing Key
|
||||
ExpiryTime(u64),
|
||||
/// A Metadata Item with an unrecognized Typecode. MUST-understand metadata items are NOT
|
||||
/// represented using this type, as the presence of an unrecognized metadata item with a
|
||||
/// typecode in the `MUST-understand` range will result in a parse failure.
|
||||
Unknown { typecode: u32, data: Vec<u8> },
|
||||
}
|
||||
|
||||
impl MetadataItem {
|
||||
/// Parse a metadata item for the specified metadata typecode from the provided bytes.
|
||||
pub fn parse(
|
||||
revision: Revision,
|
||||
typecode: MetadataTypecode,
|
||||
data: &[u8],
|
||||
) -> Result<Self, ParseError> {
|
||||
use MetadataTypecode::*;
|
||||
use Revision::*;
|
||||
match (revision, typecode) {
|
||||
(R1, ExpiryHeight) => data
|
||||
.try_into()
|
||||
.map(u32::from_le_bytes)
|
||||
.map(MetadataItem::ExpiryHeight)
|
||||
.map_err(|_| {
|
||||
ParseError::InvalidEncoding(
|
||||
"Expiry height must be a 32-bit little-endian value.".to_string(),
|
||||
)
|
||||
}),
|
||||
(R1, ExpiryTime) => data
|
||||
.try_into()
|
||||
.map(u64::from_le_bytes)
|
||||
.map(MetadataItem::ExpiryTime)
|
||||
.map_err(|_| {
|
||||
ParseError::InvalidEncoding(
|
||||
"Expiry time must be a 64-bit little-endian value.".to_string(),
|
||||
)
|
||||
}),
|
||||
(R0, ExpiryHeight | ExpiryTime) => Err(ParseError::NotUnderstood(typecode.into())),
|
||||
(R0 | R1, MustUnderstand(tc)) => Err(ParseError::NotUnderstood(tc)),
|
||||
// This implementation treats the 0xC0..OxFD range as unknown metadata for both R0 and
|
||||
// R1, as no typecodes were specified in this range for R0 and were "reclaimed" as
|
||||
// metadata codes by ZIP 316 at the time R1 was introduced.
|
||||
(R0 | R1, Unknown(typecode)) => Ok(MetadataItem::Unknown {
|
||||
typecode,
|
||||
data: data.to_vec(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the typecode for this metadata item.
|
||||
pub fn typecode(&self) -> MetadataTypecode {
|
||||
match self {
|
||||
MetadataItem::ExpiryHeight(_) => MetadataTypecode::ExpiryHeight,
|
||||
MetadataItem::ExpiryTime(_) => MetadataTypecode::ExpiryTime,
|
||||
MetadataItem::Unknown { typecode, .. } => MetadataTypecode::Unknown(*typecode),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the raw bytes of this metadata item.
|
||||
pub fn data(&self) -> Vec<u8> {
|
||||
match self {
|
||||
MetadataItem::ExpiryHeight(h) => h.to_le_bytes().to_vec(),
|
||||
MetadataItem::ExpiryTime(t) => t.to_le_bytes().to_vec(),
|
||||
MetadataItem::Unknown { data, .. } => data.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Unified Encoding Item.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Item<T> {
|
||||
/// A data item; either a receiver (for Unified Addresses) or a key (for Unified Viewing Keys)
|
||||
Data(T),
|
||||
/// A metadata item.
|
||||
Metadata(MetadataItem),
|
||||
}
|
||||
|
||||
impl<T: private::SealedDataItem> Item<T> {
|
||||
/// Returns the typecode for this item.
|
||||
pub fn typecode(&self) -> Typecode {
|
||||
match self {
|
||||
Item::Data(d) => Typecode::Data(d.typecode()),
|
||||
Item::Metadata(m) => Typecode::Metadata(m.typecode()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The total ordering over items by their typecodes, used for encoding as specified
|
||||
/// in [ZIP 316](https://zips.z.cash/zip-0316#encoding-of-unified-addresses)
|
||||
pub fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering {
|
||||
u32::from(a.typecode()).cmp(&u32::from(b.typecode()))
|
||||
}
|
||||
|
||||
/// Returns the raw binary representation of the data for this item.
|
||||
pub fn data(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Item::Data(d) => d.data().to_vec(),
|
||||
Item::Metadata(m) => m.data(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether this item is a transparent receiver or key.
|
||||
pub fn is_transparent_data_item(&self) -> bool {
|
||||
self.typecode() == Typecode::P2PKH || self.typecode() == Typecode::P2SH
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,7 +318,7 @@ pub enum ParseError {
|
|||
/// The unified container contains a duplicated typecode.
|
||||
DuplicateTypecode(Typecode),
|
||||
/// The parsed typecode exceeds the maximum allowed CompactSize value.
|
||||
InvalidTypecodeValue(u64),
|
||||
InvalidTypecodeValue(u32),
|
||||
/// The string is an invalid encoding.
|
||||
InvalidEncoding(String),
|
||||
/// The items in the unified container are not in typecode order.
|
||||
|
@ -136,6 +329,8 @@ pub enum ParseError {
|
|||
NotUnified,
|
||||
/// The Bech32m string has an unrecognized human-readable prefix.
|
||||
UnknownPrefix(String),
|
||||
/// A `MUST-understand` metadata item was not recognized.
|
||||
NotUnderstood(u32),
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
|
@ -151,67 +346,93 @@ impl fmt::Display for ParseError {
|
|||
ParseError::UnknownPrefix(s) => {
|
||||
write!(f, "Unrecognized Bech32m human-readable prefix: {}", s)
|
||||
}
|
||||
ParseError::NotUnderstood(tc) => {
|
||||
write!(
|
||||
f,
|
||||
"MUST-understand metadata item with typecode {} was not recognized; please upgrade.",
|
||||
tc
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ParseError {}
|
||||
|
||||
/// The revision of the Unified Address standard that an address was parsed under.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Revision {
|
||||
R0,
|
||||
R1,
|
||||
}
|
||||
|
||||
pub(crate) mod private {
|
||||
use super::{ParseError, Typecode, PADDING_LEN};
|
||||
use crate::Network;
|
||||
use super::{DataTypecode, ParseError, Revision, Typecode, PADDING_LEN};
|
||||
use crate::{
|
||||
unified::{Item, MetadataItem},
|
||||
Network,
|
||||
};
|
||||
use std::{
|
||||
cmp,
|
||||
convert::{TryFrom, TryInto},
|
||||
io::Write,
|
||||
};
|
||||
use zcash_encoding::CompactSize;
|
||||
|
||||
/// A raw address or viewing key.
|
||||
pub trait SealedItem: for<'a> TryFrom<(u32, &'a [u8]), Error = ParseError> + Clone {
|
||||
fn typecode(&self) -> Typecode;
|
||||
pub trait SealedDataItem: Clone {
|
||||
/// Parse a data item for the specified data typecode from the provided bytes.
|
||||
fn parse(tc: DataTypecode, value: &[u8]) -> Result<Self, ParseError>;
|
||||
|
||||
/// Returns the typecode of this data item.
|
||||
fn typecode(&self) -> DataTypecode;
|
||||
|
||||
/// Returns the raw bytes of this data item.
|
||||
fn data(&self) -> &[u8];
|
||||
|
||||
fn preference_order(a: &Self, b: &Self) -> cmp::Ordering {
|
||||
match Typecode::preference_order(&a.typecode(), &b.typecode()) {
|
||||
cmp::Ordering::Equal => a.data().cmp(b.data()),
|
||||
res => res,
|
||||
}
|
||||
}
|
||||
|
||||
fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering {
|
||||
match Typecode::encoding_order(&a.typecode(), &b.typecode()) {
|
||||
cmp::Ordering::Equal => a.data().cmp(b.data()),
|
||||
res => res,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Unified Container containing addresses or viewing keys.
|
||||
pub trait SealedContainer: super::Container + std::marker::Sized {
|
||||
const MAINNET: &'static str;
|
||||
const TESTNET: &'static str;
|
||||
const REGTEST: &'static str;
|
||||
const MAINNET_R0: &'static str;
|
||||
const TESTNET_R0: &'static str;
|
||||
const REGTEST_R0: &'static str;
|
||||
|
||||
const MAINNET_R1: &'static str;
|
||||
const TESTNET_R1: &'static str;
|
||||
const REGTEST_R1: &'static str;
|
||||
|
||||
/// Implementations of this method should act as unchecked constructors
|
||||
/// of the container type; the caller is guaranteed to check the
|
||||
/// general invariants that apply to all unified containers.
|
||||
fn from_inner(items: Vec<Self::Item>) -> Self;
|
||||
fn from_inner(revision: Revision, items: Vec<Item<Self::DataItem>>) -> Self;
|
||||
|
||||
fn network_hrp(network: &Network) -> &'static str {
|
||||
match network {
|
||||
Network::Main => Self::MAINNET,
|
||||
Network::Test => Self::TESTNET,
|
||||
Network::Regtest => Self::REGTEST,
|
||||
fn network_hrp(revision: Revision, network: &Network) -> &'static str {
|
||||
match (revision, network) {
|
||||
(Revision::R0, Network::Main) => Self::MAINNET_R0,
|
||||
(Revision::R0, Network::Test) => Self::TESTNET_R0,
|
||||
(Revision::R0, Network::Regtest) => Self::REGTEST_R0,
|
||||
(Revision::R1, Network::Main) => Self::MAINNET_R1,
|
||||
(Revision::R1, Network::Test) => Self::TESTNET_R1,
|
||||
(Revision::R1, Network::Regtest) => Self::REGTEST_R1,
|
||||
}
|
||||
}
|
||||
|
||||
fn hrp_revision(hrp: &str) -> Option<Revision> {
|
||||
if hrp == Self::MAINNET_R0 || hrp == Self::TESTNET_R0 || hrp == Self::REGTEST_R0 {
|
||||
Some(Revision::R0)
|
||||
} else if hrp == Self::MAINNET_R1 || hrp == Self::TESTNET_R1 || hrp == Self::REGTEST_R1
|
||||
{
|
||||
Some(Revision::R1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn hrp_network(hrp: &str) -> Option<Network> {
|
||||
if hrp == Self::MAINNET {
|
||||
if hrp == Self::MAINNET_R0 || hrp == Self::MAINNET_R1 {
|
||||
Some(Network::Main)
|
||||
} else if hrp == Self::TESTNET {
|
||||
} else if hrp == Self::TESTNET_R0 || hrp == Self::TESTNET_R1 {
|
||||
Some(Network::Test)
|
||||
} else if hrp == Self::REGTEST {
|
||||
} else if hrp == Self::REGTEST_R0 || hrp == Self::REGTEST_R1 {
|
||||
Some(Network::Regtest)
|
||||
} else {
|
||||
None
|
||||
|
@ -227,7 +448,7 @@ pub(crate) mod private {
|
|||
)
|
||||
.unwrap();
|
||||
CompactSize::write(&mut writer, data.len()).unwrap();
|
||||
writer.write_all(data).unwrap();
|
||||
writer.write_all(&data).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,10 +469,15 @@ pub(crate) mod private {
|
|||
}
|
||||
|
||||
/// Parse the items of the unified container.
|
||||
fn parse_items<T: Into<Vec<u8>>>(hrp: &str, buf: T) -> Result<Vec<Self::Item>, ParseError> {
|
||||
fn read_receiver<R: SealedItem>(
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn parse_items<T: Into<Vec<u8>>>(
|
||||
hrp: &str,
|
||||
buf: T,
|
||||
) -> Result<(Revision, Vec<Item<Self::DataItem>>), ParseError> {
|
||||
fn read_item<R: SealedDataItem>(
|
||||
revision: Revision,
|
||||
mut cursor: &mut std::io::Cursor<&[u8]>,
|
||||
) -> Result<R, ParseError> {
|
||||
) -> Result<Item<R>, ParseError> {
|
||||
let typecode = CompactSize::read(&mut cursor)
|
||||
.map(|v| u32::try_from(v).expect("CompactSize::read enforces MAX_SIZE limit"))
|
||||
.map_err(|e| {
|
||||
|
@ -279,12 +505,18 @@ pub(crate) mod private {
|
|||
length
|
||||
)));
|
||||
}
|
||||
let result = R::try_from((
|
||||
typecode,
|
||||
&buf[cursor.position() as usize..addr_end as usize],
|
||||
));
|
||||
// The "as usize" casts cannot change the values, because both
|
||||
// cursor.position() and addr_end are u64 values <= buf.len()
|
||||
// which is usize.
|
||||
let data = &buf[cursor.position() as usize..addr_end as usize];
|
||||
let result = match Typecode::try_from(typecode)? {
|
||||
Typecode::Data(tc) => Item::Data(R::parse(tc, data)?),
|
||||
Typecode::Metadata(tc) => {
|
||||
Item::Metadata(MetadataItem::parse(revision, tc, data)?)
|
||||
}
|
||||
};
|
||||
cursor.set_position(addr_end);
|
||||
result
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// Here we allocate if necessary to get a mutable Vec<u8> to unjumble.
|
||||
|
@ -308,22 +540,27 @@ pub(crate) mod private {
|
|||
)),
|
||||
}?;
|
||||
|
||||
let revision = Self::hrp_revision(hrp)
|
||||
.ok_or_else(|| ParseError::UnknownPrefix(hrp.to_string()))?;
|
||||
|
||||
let mut cursor = std::io::Cursor::new(encoded);
|
||||
let mut result = vec![];
|
||||
while cursor.position() < encoded.len().try_into().unwrap() {
|
||||
result.push(read_receiver(&mut cursor)?);
|
||||
result.push(read_item(revision, &mut cursor)?);
|
||||
}
|
||||
assert_eq!(cursor.position(), encoded.len().try_into().unwrap());
|
||||
|
||||
Ok(result)
|
||||
Ok((revision, result))
|
||||
}
|
||||
|
||||
/// A private function that constructs a unified container with the
|
||||
/// specified items, which must be in ascending typecode order.
|
||||
fn try_from_items_internal(items: Vec<Self::Item>) -> Result<Self, ParseError> {
|
||||
assert!(u32::from(Typecode::P2sh) == u32::from(Typecode::P2pkh) + 1);
|
||||
fn try_from_items_internal(
|
||||
revision: Revision,
|
||||
items: Vec<Item<Self::DataItem>>,
|
||||
) -> Result<Self, ParseError> {
|
||||
assert!(u32::from(Typecode::P2SH) == u32::from(Typecode::P2PKH) + 1);
|
||||
|
||||
let mut only_transparent = true;
|
||||
let mut prev_code = None; // less than any Some
|
||||
for item in &items {
|
||||
let t = item.typecode();
|
||||
|
@ -332,47 +569,46 @@ pub(crate) mod private {
|
|||
return Err(ParseError::InvalidTypecodeOrder);
|
||||
} else if t_code == prev_code {
|
||||
return Err(ParseError::DuplicateTypecode(t));
|
||||
} else if t == Typecode::P2sh && prev_code == Some(u32::from(Typecode::P2pkh)) {
|
||||
} else if t == Typecode::P2SH && prev_code == Some(u32::from(DataTypecode::P2pkh)) {
|
||||
// P2pkh and P2sh can only be in that order and next to each other,
|
||||
// otherwise we would detect an out-of-order or duplicate typecode.
|
||||
return Err(ParseError::BothP2phkAndP2sh);
|
||||
} else {
|
||||
prev_code = t_code;
|
||||
only_transparent = only_transparent && t.is_transparent();
|
||||
}
|
||||
}
|
||||
|
||||
if only_transparent {
|
||||
Err(ParseError::OnlyTransparent)
|
||||
} else {
|
||||
// All checks pass!
|
||||
Ok(Self::from_inner(items))
|
||||
}
|
||||
// All checks pass!
|
||||
Ok(Self::from_inner(revision, items))
|
||||
}
|
||||
|
||||
fn parse_internal<T: Into<Vec<u8>>>(hrp: &str, buf: T) -> Result<Self, ParseError> {
|
||||
Self::parse_items(hrp, buf).and_then(Self::try_from_items_internal)
|
||||
Self::parse_items(hrp, buf)
|
||||
.and_then(|(revision, items)| Self::try_from_items_internal(revision, items))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use private::SealedItem;
|
||||
use private::SealedDataItem;
|
||||
|
||||
/// Trait providing common encoding and decoding logic for Unified containers.
|
||||
pub trait Encoding: private::SealedContainer {
|
||||
/// Constructs a value of a unified container type from a vector
|
||||
/// of container items, sorted according to typecode as specified
|
||||
/// in ZIP 316.
|
||||
/// Constructs a value of a unified container type from a vector of container
|
||||
/// items. These items will be sorted according to typecode as specified in ZIP
|
||||
/// 316, so this method is not necessarily round-trip compatible with
|
||||
/// [`Container::items_as_parsed`].
|
||||
///
|
||||
/// This function will return an error in the case that the following ZIP 316
|
||||
/// invariants concerning the composition of a unified container are
|
||||
/// violated:
|
||||
/// * the item list may not contain two items having the same typecode
|
||||
/// * the item list may not contain only transparent items (or no items)
|
||||
/// * the item list may not contain both P2PKH and P2SH items.
|
||||
fn try_from_items(mut items: Vec<Self::Item>) -> Result<Self, ParseError> {
|
||||
items.sort_unstable_by(Self::Item::encoding_order);
|
||||
Self::try_from_items_internal(items)
|
||||
fn try_from_items(
|
||||
revision: Revision,
|
||||
mut items: Vec<Item<Self::DataItem>>,
|
||||
) -> Result<Self, ParseError> {
|
||||
items.sort_unstable_by(Item::encoding_order);
|
||||
Self::try_from_items_internal(revision, items)
|
||||
}
|
||||
|
||||
/// Decodes a unified container from its string representation, preserving
|
||||
|
@ -399,7 +635,7 @@ pub trait Encoding: private::SealedContainer {
|
|||
/// ordering of the contained items such that it correctly obeys round-trip
|
||||
/// serialization invariants.
|
||||
fn encode(&self, network: &Network) -> String {
|
||||
let hrp = Self::network_hrp(network);
|
||||
let hrp = Self::network_hrp(self.revision(), network);
|
||||
bech32::encode(
|
||||
hrp,
|
||||
self.to_jumbled_bytes(hrp).to_base32(),
|
||||
|
@ -411,20 +647,13 @@ pub trait Encoding: private::SealedContainer {
|
|||
|
||||
/// Trait for for Unified containers, that exposes the items within them.
|
||||
pub trait Container {
|
||||
/// The type of item in this unified container.
|
||||
type Item: SealedItem;
|
||||
/// The type of data items in this unified container.
|
||||
type DataItem: SealedDataItem;
|
||||
|
||||
/// Returns the items contained within this container, sorted in preference order.
|
||||
fn items(&self) -> Vec<Self::Item> {
|
||||
let mut items = self.items_as_parsed().to_vec();
|
||||
// Unstable sorting is fine, because all items are guaranteed by construction
|
||||
// to have distinct typecodes.
|
||||
items.sort_unstable_by(Self::Item::preference_order);
|
||||
items
|
||||
}
|
||||
/// Returns the items in encoding order.
|
||||
fn items_as_parsed(&self) -> &[Item<Self::DataItem>];
|
||||
|
||||
/// Returns the items in the order they were parsed from the string encoding.
|
||||
///
|
||||
/// This API is for advanced usage; in most cases you should use `Self::items`.
|
||||
fn items_as_parsed(&self) -> &[Self::Item];
|
||||
/// Returns the revision of the ZIP 316 standard that this unified container
|
||||
/// conforms to.
|
||||
fn revision(&self) -> Revision;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
use super::{private::SealedItem, ParseError, Typecode};
|
||||
use zcash_protocol::{PoolType, ShieldedProtocol};
|
||||
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use super::{private::SealedDataItem, DataTypecode, Item, ParseError, Revision};
|
||||
|
||||
/// The set of known Receivers for Unified Addresses.
|
||||
use std::{cmp, convert::TryInto};
|
||||
|
||||
/// The enumeration of Unified Address Receivers of known types.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Receiver {
|
||||
Orchard([u8; 43]),
|
||||
|
@ -12,34 +14,39 @@ pub enum Receiver {
|
|||
Unknown { typecode: u32, data: Vec<u8> },
|
||||
}
|
||||
|
||||
impl TryFrom<(u32, &[u8])> for Receiver {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from((typecode, addr): (u32, &[u8])) -> Result<Self, Self::Error> {
|
||||
match typecode.try_into()? {
|
||||
Typecode::P2pkh => addr.try_into().map(Receiver::P2pkh),
|
||||
Typecode::P2sh => addr.try_into().map(Receiver::P2sh),
|
||||
Typecode::Sapling => addr.try_into().map(Receiver::Sapling),
|
||||
Typecode::Orchard => addr.try_into().map(Receiver::Orchard),
|
||||
Typecode::Unknown(_) => Ok(Receiver::Unknown {
|
||||
typecode,
|
||||
data: addr.to_vec(),
|
||||
}),
|
||||
}
|
||||
.map_err(|e| {
|
||||
ParseError::InvalidEncoding(format!("Invalid address for typecode {}: {}", typecode, e))
|
||||
})
|
||||
impl Receiver {
|
||||
fn preference_order(a: &Self, b: &Self) -> cmp::Ordering {
|
||||
DataTypecode::preference_order(&a.typecode(), &b.typecode())
|
||||
}
|
||||
}
|
||||
|
||||
impl SealedItem for Receiver {
|
||||
fn typecode(&self) -> Typecode {
|
||||
impl SealedDataItem for Receiver {
|
||||
fn parse(typecode: DataTypecode, data: &[u8]) -> Result<Self, ParseError> {
|
||||
match typecode {
|
||||
DataTypecode::P2pkh => data.try_into().map(Receiver::P2pkh),
|
||||
DataTypecode::P2sh => data.try_into().map(Receiver::P2sh),
|
||||
DataTypecode::Sapling => data.try_into().map(Receiver::Sapling),
|
||||
DataTypecode::Orchard => data.try_into().map(Receiver::Orchard),
|
||||
DataTypecode::Unknown(typecode) => Ok(Receiver::Unknown {
|
||||
typecode,
|
||||
data: data.to_vec(),
|
||||
}),
|
||||
}
|
||||
.map_err(|e| {
|
||||
ParseError::InvalidEncoding(format!(
|
||||
"Invalid address for typecode {:?}: {:?}",
|
||||
typecode, e
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn typecode(&self) -> DataTypecode {
|
||||
match self {
|
||||
Receiver::P2pkh(_) => Typecode::P2pkh,
|
||||
Receiver::P2sh(_) => Typecode::P2sh,
|
||||
Receiver::Sapling(_) => Typecode::Sapling,
|
||||
Receiver::Orchard(_) => Typecode::Orchard,
|
||||
Receiver::Unknown { typecode, .. } => Typecode::Unknown(*typecode),
|
||||
Receiver::P2pkh(_) => DataTypecode::P2pkh,
|
||||
Receiver::P2sh(_) => DataTypecode::P2sh,
|
||||
Receiver::Sapling(_) => DataTypecode::Sapling,
|
||||
Receiver::Orchard(_) => DataTypecode::Orchard,
|
||||
Receiver::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,7 +69,7 @@ impl SealedItem for Receiver {
|
|||
/// # use std::convert::Infallible;
|
||||
/// # use std::error::Error;
|
||||
/// use zcash_address::{
|
||||
/// unified::{self, Container, Encoding},
|
||||
/// unified::{self, Container, Encoding, Item, Revision},
|
||||
/// ConversionError, TryFromRawAddress, ZcashAddress,
|
||||
/// };
|
||||
///
|
||||
|
@ -90,70 +97,144 @@ impl SealedItem for Receiver {
|
|||
///
|
||||
/// // We can obtain the receivers for the UA in preference order
|
||||
/// // (the order in which wallets should prefer to use them):
|
||||
/// let receivers: Vec<unified::Receiver> = ua.items();
|
||||
/// let receivers: Vec<unified::Receiver> = ua.receivers();
|
||||
///
|
||||
/// // And we can create the UA from a list of receivers:
|
||||
/// let new_ua = unified::Address::try_from_items(receivers)?;
|
||||
/// let new_ua = unified::Address::try_from_items(Revision::R0, receivers.into_iter().map(Item::Data).collect())?;
|
||||
/// assert_eq!(new_ua, ua);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Address(pub(crate) Vec<Receiver>);
|
||||
pub struct Address {
|
||||
pub(crate) revision: Revision,
|
||||
pub(crate) receivers: Vec<Item<Receiver>>,
|
||||
}
|
||||
|
||||
impl Address {
|
||||
/// Returns the receiver items for this address, in order of decreasing preference.
|
||||
///
|
||||
/// The receiver for a wallet to send to can safely be chosen by selecting the first receiver
|
||||
/// of a type that wallet supports from the result.
|
||||
pub fn receivers(&self) -> Vec<Receiver> {
|
||||
let mut result = self
|
||||
.receivers
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
Item::Data(r) => Some(r.clone()),
|
||||
Item::Metadata(_) => None,
|
||||
})
|
||||
.collect::<Vec<Receiver>>();
|
||||
result.sort_unstable_by(Receiver::preference_order);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl Address {
|
||||
/// Returns whether this address has the ability to receive transfers of the given pool type.
|
||||
pub fn has_receiver_of_type(&self, pool_type: PoolType) -> bool {
|
||||
self.receivers.iter().any(|item| match item {
|
||||
Item::Data(Receiver::Orchard(_)) => {
|
||||
pool_type == PoolType::Shielded(ShieldedProtocol::Orchard)
|
||||
}
|
||||
Item::Data(Receiver::Sapling(_)) => {
|
||||
pool_type == PoolType::Shielded(ShieldedProtocol::Sapling)
|
||||
}
|
||||
Item::Data(Receiver::P2pkh(_)) | Item::Data(Receiver::P2sh(_)) => {
|
||||
pool_type == PoolType::Transparent
|
||||
}
|
||||
Item::Data(Receiver::Unknown { .. }) => false,
|
||||
Item::Metadata(_) => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns whether this address contains the given receiver.
|
||||
pub fn contains_receiver(&self, receiver: &Receiver) -> bool {
|
||||
self.receivers
|
||||
.iter()
|
||||
.any(|item| matches!(item, Item::Data(r) if r == receiver))
|
||||
}
|
||||
|
||||
/// Returns whether this address can receive a memo.
|
||||
pub fn can_receive_memo(&self) -> bool {
|
||||
self.receivers.iter().any(|r| {
|
||||
matches!(
|
||||
r,
|
||||
Item::Data(Receiver::Sapling(_)) | Item::Data(Receiver::Orchard(_))
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl super::private::SealedContainer for Address {
|
||||
/// The HRP for a Bech32m-encoded mainnet Unified Address.
|
||||
/// The HRP for a Bech32m-encoded mainnet Revision 0 Unified Address.
|
||||
///
|
||||
/// Defined in [ZIP 316][zip-0316].
|
||||
///
|
||||
/// [zip-0316]: https://zips.z.cash/zip-0316
|
||||
const MAINNET: &'static str = "u";
|
||||
const MAINNET_R0: &'static str = "u";
|
||||
|
||||
/// The HRP for a Bech32m-encoded testnet Unified Address.
|
||||
/// The HRP for a Bech32m-encoded testnet Revision 0 Unified Address.
|
||||
///
|
||||
/// Defined in [ZIP 316][zip-0316].
|
||||
///
|
||||
/// [zip-0316]: https://zips.z.cash/zip-0316
|
||||
const TESTNET: &'static str = "utest";
|
||||
const TESTNET_R0: &'static str = "utest";
|
||||
|
||||
/// The HRP for a Bech32m-encoded regtest Unified Address.
|
||||
const REGTEST: &'static str = "uregtest";
|
||||
/// The HRP for a Bech32m-encoded regtest Revision 0 Unified Address.
|
||||
const REGTEST_R0: &'static str = "uregtest";
|
||||
|
||||
fn from_inner(receivers: Vec<Self::Item>) -> Self {
|
||||
Self(receivers)
|
||||
/// The HRP for a Bech32m-encoded mainnet Revision 1 Unified Address.
|
||||
///
|
||||
/// Defined in [ZIP 316][zip-0316].
|
||||
///
|
||||
/// [zip-0316]: https://zips.z.cash/zip-0316
|
||||
const MAINNET_R1: &'static str = "ur";
|
||||
|
||||
/// The HRP for a Bech32m-encoded testnet Revision 1 Unified Address.
|
||||
///
|
||||
/// Defined in [ZIP 316][zip-0316].
|
||||
///
|
||||
/// [zip-0316]: https://zips.z.cash/zip-0316
|
||||
const TESTNET_R1: &'static str = "urtest";
|
||||
|
||||
/// The HRP for a Bech32m-encoded regtest Revision 1 Unified Address.
|
||||
const REGTEST_R1: &'static str = "urregtest";
|
||||
|
||||
fn from_inner(revision: Revision, receivers: Vec<Item<Self::DataItem>>) -> Self {
|
||||
Self {
|
||||
revision,
|
||||
receivers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Encoding for Address {}
|
||||
impl super::Container for Address {
|
||||
type Item = Receiver;
|
||||
type DataItem = Receiver;
|
||||
|
||||
fn items_as_parsed(&self) -> &[Receiver] {
|
||||
&self.0
|
||||
fn items_as_parsed(&self) -> &[Item<Receiver>] {
|
||||
&self.receivers
|
||||
}
|
||||
|
||||
fn revision(&self) -> Revision {
|
||||
self.revision
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-dependencies"))]
|
||||
pub mod test_vectors;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
use zcash_encoding::MAX_COMPACT_SIZE;
|
||||
|
||||
use crate::{
|
||||
kind::unified::{private::SealedContainer, Container, Encoding},
|
||||
Network,
|
||||
};
|
||||
|
||||
pub mod testing {
|
||||
use proptest::{
|
||||
array::{uniform11, uniform20, uniform32},
|
||||
collection::vec,
|
||||
prelude::*,
|
||||
sample::select,
|
||||
strategy::Strategy,
|
||||
};
|
||||
|
||||
use super::{Address, ParseError, Receiver, Typecode};
|
||||
use super::{Address, Receiver};
|
||||
use crate::unified::{DataTypecode, Item, Revision};
|
||||
use zcash_encoding::MAX_COMPACT_SIZE;
|
||||
|
||||
prop_compose! {
|
||||
fn uniform43()(a in uniform11(0u8..), b in uniform32(0u8..)) -> [u8; 43] {
|
||||
|
@ -164,53 +245,85 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn arb_transparent_typecode() -> impl Strategy<Value = Typecode> {
|
||||
select(vec![Typecode::P2pkh, Typecode::P2sh])
|
||||
/// A strategy to generate an arbitrary transparent typecode.
|
||||
fn arb_transparent_typecode() -> impl Strategy<Value = DataTypecode> {
|
||||
select(vec![DataTypecode::P2pkh, DataTypecode::P2sh])
|
||||
}
|
||||
|
||||
fn arb_shielded_typecode() -> impl Strategy<Value = Typecode> {
|
||||
/// A strategy to generate an arbitrary shielded (Sapling, Orchard, or unknown) typecode.
|
||||
fn arb_shielded_typecode() -> impl Strategy<Value = DataTypecode> {
|
||||
prop_oneof![
|
||||
Just(Typecode::Sapling),
|
||||
Just(Typecode::Orchard),
|
||||
((<u32>::from(Typecode::Orchard) + 1)..MAX_COMPACT_SIZE).prop_map(Typecode::Unknown)
|
||||
Just(DataTypecode::Sapling),
|
||||
Just(DataTypecode::Orchard),
|
||||
((<u32>::from(DataTypecode::Orchard) + 1)..MAX_COMPACT_SIZE)
|
||||
.prop_map(DataTypecode::Unknown)
|
||||
]
|
||||
}
|
||||
|
||||
/// A strategy to generate an arbitrary valid set of typecodes without
|
||||
/// duplication and containing only one of P2sh and P2pkh transparent
|
||||
/// typecodes. The resulting vector will be sorted in encoding order.
|
||||
fn arb_typecodes() -> impl Strategy<Value = Vec<Typecode>> {
|
||||
fn arb_typecodes() -> impl Strategy<Value = Vec<DataTypecode>> {
|
||||
prop::option::of(arb_transparent_typecode()).prop_flat_map(|transparent| {
|
||||
prop::collection::hash_set(arb_shielded_typecode(), 1..4).prop_map(move |xs| {
|
||||
let mut typecodes: Vec<_> = xs.into_iter().chain(transparent).collect();
|
||||
typecodes.sort_unstable_by(Typecode::encoding_order);
|
||||
typecodes
|
||||
})
|
||||
prop::collection::hash_set(arb_shielded_typecode(), 1..4)
|
||||
.prop_map(move |xs| xs.into_iter().chain(transparent).collect::<Vec<_>>())
|
||||
})
|
||||
}
|
||||
|
||||
fn arb_unified_address_for_typecodes(
|
||||
typecodes: Vec<Typecode>,
|
||||
/// A strategy to generate a vector of unified address receivers containing random data. The
|
||||
/// resulting receivers may not be valid according to protocol rules; this generator is only
|
||||
/// intended for use in testing parsing and serialization.
|
||||
fn arb_unified_address_receivers(
|
||||
typecodes: Vec<DataTypecode>,
|
||||
) -> impl Strategy<Value = Vec<Receiver>> {
|
||||
typecodes
|
||||
.into_iter()
|
||||
.map(|tc| match tc {
|
||||
Typecode::P2pkh => uniform20(0u8..).prop_map(Receiver::P2pkh).boxed(),
|
||||
Typecode::P2sh => uniform20(0u8..).prop_map(Receiver::P2sh).boxed(),
|
||||
Typecode::Sapling => uniform43().prop_map(Receiver::Sapling).boxed(),
|
||||
Typecode::Orchard => uniform43().prop_map(Receiver::Orchard).boxed(),
|
||||
Typecode::Unknown(typecode) => vec(any::<u8>(), 32..256)
|
||||
DataTypecode::P2pkh => uniform20(0u8..).prop_map(Receiver::P2pkh).boxed(),
|
||||
DataTypecode::P2sh => uniform20(0u8..).prop_map(Receiver::P2sh).boxed(),
|
||||
DataTypecode::Sapling => uniform43().prop_map(Receiver::Sapling).boxed(),
|
||||
DataTypecode::Orchard => uniform43().prop_map(Receiver::Orchard).boxed(),
|
||||
DataTypecode::Unknown(typecode) => vec(any::<u8>(), 32..256)
|
||||
.prop_map(move |data| Receiver::Unknown { typecode, data })
|
||||
.boxed(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn arb_unified_address() -> impl Strategy<Value = Address> {
|
||||
/// A strategy to generate an arbitrary Unified Address containing only receivers, without
|
||||
/// additional metadata. The items in this address will be sorted in encoding order. The
|
||||
/// receivers in the resulting address may not be valid according to protocol rules; this
|
||||
/// generator is only intended for use in testing parsing and serialization.
|
||||
pub fn arb_unified_address() -> impl Strategy<Value = Address> {
|
||||
arb_typecodes()
|
||||
.prop_flat_map(arb_unified_address_for_typecodes)
|
||||
.prop_map(Address)
|
||||
.prop_flat_map(arb_unified_address_receivers)
|
||||
.prop_map(|rs| {
|
||||
let mut receivers = rs.into_iter().map(Item::Data).collect::<Vec<_>>();
|
||||
receivers.sort_unstable_by(Item::encoding_order);
|
||||
Address {
|
||||
revision: Revision::R0,
|
||||
receivers,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-dependencies"))]
|
||||
pub mod test_vectors;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use crate::{
|
||||
kind::unified::{private::SealedContainer, Encoding},
|
||||
unified::{address::testing::arb_unified_address, Item, Revision, Typecode},
|
||||
Network,
|
||||
};
|
||||
|
||||
use proptest::{prelude::*, sample::select};
|
||||
|
||||
use super::{Address, ParseError, Receiver};
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
|
@ -239,7 +352,7 @@ mod tests {
|
|||
0x7b, 0x28, 0x69, 0xc9, 0x84,
|
||||
];
|
||||
assert_eq!(
|
||||
Address::parse_internal(Address::MAINNET, &invalid_padding[..]),
|
||||
Address::parse_internal(Address::MAINNET_R0, &invalid_padding[..]),
|
||||
Err(ParseError::InvalidEncoding(
|
||||
"Invalid padding bytes".to_owned()
|
||||
))
|
||||
|
@ -254,7 +367,7 @@ mod tests {
|
|||
0x4b, 0x31, 0xee, 0x5a,
|
||||
];
|
||||
assert_eq!(
|
||||
Address::parse_internal(Address::MAINNET, &truncated_padding[..]),
|
||||
Address::parse_internal(Address::MAINNET_R0, &truncated_padding[..]),
|
||||
Err(ParseError::InvalidEncoding(
|
||||
"Invalid padding bytes".to_owned()
|
||||
))
|
||||
|
@ -279,7 +392,7 @@ mod tests {
|
|||
0xc6, 0x5e, 0x68, 0xa2, 0x78, 0x6c, 0x9e,
|
||||
];
|
||||
assert_matches!(
|
||||
Address::parse_internal(Address::MAINNET, &truncated_sapling_data[..]),
|
||||
Address::parse_internal(Address::MAINNET_R0, &truncated_sapling_data[..]),
|
||||
Err(ParseError::InvalidEncoding(_))
|
||||
);
|
||||
|
||||
|
@ -292,7 +405,7 @@ mod tests {
|
|||
0xe6, 0x70, 0x36, 0x5b, 0x7b, 0x9e,
|
||||
];
|
||||
assert_matches!(
|
||||
Address::parse_internal(Address::MAINNET, &truncated_after_sapling_typecode[..]),
|
||||
Address::parse_internal(Address::MAINNET_R0, &truncated_after_sapling_typecode[..]),
|
||||
Err(ParseError::InvalidEncoding(_))
|
||||
);
|
||||
}
|
||||
|
@ -301,11 +414,17 @@ mod tests {
|
|||
fn duplicate_typecode() {
|
||||
// Construct and serialize an invalid UA. This must be done using private
|
||||
// methods, as the public API does not permit construction of such invalid values.
|
||||
let ua = Address(vec![Receiver::Sapling([1; 43]), Receiver::Sapling([2; 43])]);
|
||||
let encoded = ua.to_jumbled_bytes(Address::MAINNET);
|
||||
let ua = Address {
|
||||
revision: Revision::R0,
|
||||
receivers: vec![
|
||||
Item::Data(Receiver::Sapling([1; 43])),
|
||||
Item::Data(Receiver::Sapling([2; 43])),
|
||||
],
|
||||
};
|
||||
let encoded = ua.to_jumbled_bytes(Address::MAINNET_R0);
|
||||
assert_eq!(
|
||||
Address::parse_internal(Address::MAINNET, &encoded[..]),
|
||||
Err(ParseError::DuplicateTypecode(Typecode::Sapling))
|
||||
Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
|
||||
Err(ParseError::DuplicateTypecode(Typecode::SAPLING))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -313,11 +432,17 @@ mod tests {
|
|||
fn p2pkh_and_p2sh() {
|
||||
// Construct and serialize an invalid UA. This must be done using private
|
||||
// methods, as the public API does not permit construction of such invalid values.
|
||||
let ua = Address(vec![Receiver::P2pkh([0; 20]), Receiver::P2sh([0; 20])]);
|
||||
let encoded = ua.to_jumbled_bytes(Address::MAINNET);
|
||||
let ua = Address {
|
||||
revision: Revision::R0,
|
||||
receivers: vec![
|
||||
Item::Data(Receiver::P2pkh([0; 20])),
|
||||
Item::Data(Receiver::P2sh([0; 20])),
|
||||
],
|
||||
};
|
||||
let encoded = ua.to_jumbled_bytes(Address::MAINNET_R0);
|
||||
// ensure that decoding catches the error
|
||||
assert_eq!(
|
||||
Address::parse_internal(Address::MAINNET, &encoded[..]),
|
||||
Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
|
||||
Err(ParseError::BothP2phkAndP2sh)
|
||||
);
|
||||
}
|
||||
|
@ -326,11 +451,17 @@ mod tests {
|
|||
fn addresses_out_of_order() {
|
||||
// Construct and serialize an invalid UA. This must be done using private
|
||||
// methods, as the public API does not permit construction of such invalid values.
|
||||
let ua = Address(vec![Receiver::Sapling([0; 43]), Receiver::P2pkh([0; 20])]);
|
||||
let encoded = ua.to_jumbled_bytes(Address::MAINNET);
|
||||
let ua = Address {
|
||||
revision: Revision::R0,
|
||||
receivers: vec![
|
||||
Item::Data(Receiver::Sapling([0; 43])),
|
||||
Item::Data(Receiver::P2pkh([0; 20])),
|
||||
],
|
||||
};
|
||||
let encoded = ua.to_jumbled_bytes(Address::MAINNET_R0);
|
||||
// ensure that decoding catches the error
|
||||
assert_eq!(
|
||||
Address::parse_internal(Address::MAINNET, &encoded[..]),
|
||||
Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
|
||||
Err(ParseError::InvalidTypecodeOrder)
|
||||
);
|
||||
}
|
||||
|
@ -349,7 +480,7 @@ mod tests {
|
|||
// with only one of them we don't have sufficient data for F4Jumble (so we hit a
|
||||
// different error).
|
||||
assert_matches!(
|
||||
Address::parse_internal(Address::MAINNET, &encoded[..]),
|
||||
Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
|
||||
Err(ParseError::InvalidEncoding(_))
|
||||
);
|
||||
}
|
||||
|
@ -357,19 +488,22 @@ mod tests {
|
|||
#[test]
|
||||
fn receivers_are_sorted() {
|
||||
// Construct a UA with receivers in an unsorted order.
|
||||
let ua = Address(vec![
|
||||
Receiver::P2pkh([0; 20]),
|
||||
Receiver::Orchard([0; 43]),
|
||||
Receiver::Unknown {
|
||||
typecode: 0xff,
|
||||
data: vec![],
|
||||
},
|
||||
Receiver::Sapling([0; 43]),
|
||||
]);
|
||||
let ua = Address {
|
||||
revision: Revision::R0,
|
||||
receivers: vec![
|
||||
Item::Data(Receiver::P2pkh([0; 20])),
|
||||
Item::Data(Receiver::Orchard([0; 43])),
|
||||
Item::Data(Receiver::Unknown {
|
||||
typecode: 0xff,
|
||||
data: vec![],
|
||||
}),
|
||||
Item::Data(Receiver::Sapling([0; 43])),
|
||||
],
|
||||
};
|
||||
|
||||
// `Address::receivers` sorts the receivers in priority order.
|
||||
assert_eq!(
|
||||
ua.items(),
|
||||
ua.receivers(),
|
||||
vec![
|
||||
Receiver::Orchard([0; 43]),
|
||||
Receiver::Sapling([0; 43]),
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use std::convert::{TryFrom, TryInto};
|
||||
use std::convert::TryInto;
|
||||
|
||||
use super::{
|
||||
private::{SealedContainer, SealedItem},
|
||||
Container, Encoding, ParseError, Typecode,
|
||||
private::{SealedContainer, SealedDataItem},
|
||||
Container, DataTypecode, Encoding, Item, ParseError, Revision,
|
||||
};
|
||||
|
||||
/// The set of known FVKs for Unified FVKs.
|
||||
|
@ -39,31 +39,27 @@ pub enum Fvk {
|
|||
},
|
||||
}
|
||||
|
||||
impl TryFrom<(u32, &[u8])> for Fvk {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from((typecode, data): (u32, &[u8])) -> Result<Self, Self::Error> {
|
||||
impl SealedDataItem for Fvk {
|
||||
fn parse(typecode: DataTypecode, data: &[u8]) -> Result<Self, ParseError> {
|
||||
let data = data.to_vec();
|
||||
match typecode.try_into()? {
|
||||
Typecode::P2pkh => data.try_into().map(Fvk::P2pkh),
|
||||
Typecode::P2sh => Err(data),
|
||||
Typecode::Sapling => data.try_into().map(Fvk::Sapling),
|
||||
Typecode::Orchard => data.try_into().map(Fvk::Orchard),
|
||||
Typecode::Unknown(_) => Ok(Fvk::Unknown { typecode, data }),
|
||||
match typecode {
|
||||
DataTypecode::P2pkh => data.try_into().map(Fvk::P2pkh),
|
||||
DataTypecode::P2sh => Err(data),
|
||||
DataTypecode::Sapling => data.try_into().map(Fvk::Sapling),
|
||||
DataTypecode::Orchard => data.try_into().map(Fvk::Orchard),
|
||||
DataTypecode::Unknown(typecode) => Ok(Fvk::Unknown { typecode, data }),
|
||||
}
|
||||
.map_err(|e| {
|
||||
ParseError::InvalidEncoding(format!("Invalid fvk for typecode {}: {:?}", typecode, e))
|
||||
ParseError::InvalidEncoding(format!("Invalid fvk for typecode {:?}: {:?}", typecode, e))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SealedItem for Fvk {
|
||||
fn typecode(&self) -> Typecode {
|
||||
fn typecode(&self) -> DataTypecode {
|
||||
match self {
|
||||
Fvk::P2pkh(_) => Typecode::P2pkh,
|
||||
Fvk::Sapling(_) => Typecode::Sapling,
|
||||
Fvk::Orchard(_) => Typecode::Orchard,
|
||||
Fvk::Unknown { typecode, .. } => Typecode::Unknown(*typecode),
|
||||
Fvk::P2pkh(_) => DataTypecode::P2pkh,
|
||||
Fvk::Sapling(_) => DataTypecode::Sapling,
|
||||
Fvk::Orchard(_) => DataTypecode::Orchard,
|
||||
Fvk::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,7 +79,7 @@ impl SealedItem for Fvk {
|
|||
///
|
||||
/// ```
|
||||
/// # use std::error::Error;
|
||||
/// use zcash_address::unified::{self, Container, Encoding};
|
||||
/// use zcash_address::unified::{self, Container, Encoding, Item, Revision};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// # let ufvk_from_user = || "uview1cgrqnry478ckvpr0f580t6fsahp0a5mj2e9xl7hv2d2jd4ldzy449mwwk2l9yeuts85wjls6hjtghdsy5vhhvmjdw3jxl3cxhrg3vs296a3czazrycrr5cywjhwc5c3ztfyjdhmz0exvzzeyejamyp0cr9z8f9wj0953fzht0m4lenk94t70ruwgjxag2tvp63wn9ftzhtkh20gyre3w5s24f6wlgqxnjh40gd2lxe75sf3z8h5y2x0atpxcyf9t3em4h0evvsftluruqne6w4sm066sw0qe5y8qg423grple5fftxrqyy7xmqmatv7nzd7tcjadu8f7mqz4l83jsyxy4t8pkayytyk7nrp467ds85knekdkvnd7hqkfer8mnqd7pv";
|
||||
|
@ -91,54 +87,72 @@ impl SealedItem for Fvk {
|
|||
///
|
||||
/// let (network, ufvk) = unified::Ufvk::decode(example_ufvk)?;
|
||||
///
|
||||
/// // We can obtain the pool-specific Full Viewing Keys for the UFVK in preference
|
||||
/// // order (the order in which wallets should prefer to use their corresponding
|
||||
/// // address receivers):
|
||||
/// let fvks: Vec<unified::Fvk> = ufvk.items();
|
||||
/// // We can obtain the pool-specific Full Viewing Keys for the UFVK.
|
||||
/// let fvks: &[Item<unified::Fvk>] = ufvk.items_as_parsed();
|
||||
///
|
||||
/// // And we can create the UFVK from a list of FVKs:
|
||||
/// let new_ufvk = unified::Ufvk::try_from_items(fvks)?;
|
||||
/// let new_ufvk = unified::Ufvk::try_from_items(Revision::R0, fvks.to_vec())?;
|
||||
/// assert_eq!(new_ufvk, ufvk);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Ufvk(pub(crate) Vec<Fvk>);
|
||||
pub struct Ufvk {
|
||||
pub(crate) revision: Revision,
|
||||
pub(crate) fvks: Vec<Item<Fvk>>,
|
||||
}
|
||||
|
||||
impl Container for Ufvk {
|
||||
type Item = Fvk;
|
||||
type DataItem = Fvk;
|
||||
|
||||
/// Returns the FVKs contained within this UFVK, in the order they were
|
||||
/// parsed from the string encoding.
|
||||
///
|
||||
/// This API is for advanced usage; in most cases you should use `Ufvk::receivers`.
|
||||
fn items_as_parsed(&self) -> &[Fvk] {
|
||||
&self.0
|
||||
fn items_as_parsed(&self) -> &[Item<Fvk>] {
|
||||
&self.fvks
|
||||
}
|
||||
|
||||
fn revision(&self) -> Revision {
|
||||
self.revision
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoding for Ufvk {}
|
||||
|
||||
impl SealedContainer for Ufvk {
|
||||
/// The HRP for a Bech32m-encoded mainnet Unified FVK.
|
||||
/// The HRP for a Bech32m-encoded mainnet Revision 0 Unified FVK.
|
||||
///
|
||||
/// Defined in [ZIP 316][zip-0316].
|
||||
///
|
||||
/// [zip-0316]: https://zips.z.cash/zip-0316
|
||||
const MAINNET: &'static str = "uview";
|
||||
const MAINNET_R0: &'static str = "uview";
|
||||
|
||||
/// The HRP for a Bech32m-encoded testnet Unified FVK.
|
||||
/// The HRP for a Bech32m-encoded testnet Revision 0 Unified FVK.
|
||||
///
|
||||
/// Defined in [ZIP 316][zip-0316].
|
||||
///
|
||||
/// [zip-0316]: https://zips.z.cash/zip-0316
|
||||
const TESTNET: &'static str = "uviewtest";
|
||||
const TESTNET_R0: &'static str = "uviewtest";
|
||||
|
||||
/// The HRP for a Bech32m-encoded regtest Unified FVK.
|
||||
const REGTEST: &'static str = "uviewregtest";
|
||||
/// The HRP for a Bech32m-encoded regtest Revision 0 Unified FVK.
|
||||
const REGTEST_R0: &'static str = "uviewregtest";
|
||||
|
||||
fn from_inner(fvks: Vec<Self::Item>) -> Self {
|
||||
Self(fvks)
|
||||
/// The HRP for a Bech32m-encoded mainnet Revision 1 Unified FVK.
|
||||
///
|
||||
/// Defined in [ZIP 316][zip-0316].
|
||||
///
|
||||
/// [zip-0316]: https://zips.z.cash/zip-0316
|
||||
const MAINNET_R1: &'static str = "urview";
|
||||
|
||||
/// The HRP for a Bech32m-encoded testnet Revision 1 Unified FVK.
|
||||
///
|
||||
/// Defined in [ZIP 316][zip-0316].
|
||||
///
|
||||
/// [zip-0316]: https://zips.z.cash/zip-0316
|
||||
const TESTNET_R1: &'static str = "urviewtest";
|
||||
|
||||
/// The HRP for a Bech32m-encoded regtest Revision 1 Unified FVK.
|
||||
const REGTEST_R1: &'static str = "urviewregtest";
|
||||
|
||||
fn from_inner(revision: Revision, fvks: Vec<Item<Self::DataItem>>) -> Self {
|
||||
Self { revision, fvks }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,12 +162,10 @@ mod tests {
|
|||
|
||||
use proptest::{array::uniform1, array::uniform32, prelude::*, sample::select};
|
||||
|
||||
use super::{Fvk, ParseError, Typecode, Ufvk};
|
||||
use super::{Fvk, ParseError, Ufvk};
|
||||
use crate::{
|
||||
kind::unified::{
|
||||
private::{SealedContainer, SealedItem},
|
||||
Container, Encoding,
|
||||
},
|
||||
kind::unified::{private::SealedContainer, Encoding},
|
||||
unified::{Item, Revision, Typecode},
|
||||
Network,
|
||||
};
|
||||
|
||||
|
@ -211,9 +223,9 @@ mod tests {
|
|||
shielded in arb_shielded_fvk(),
|
||||
transparent in prop::option::of(arb_transparent_fvk()),
|
||||
) -> Ufvk {
|
||||
let mut items: Vec<_> = transparent.into_iter().chain(shielded).collect();
|
||||
items.sort_unstable_by(Fvk::encoding_order);
|
||||
Ufvk(items)
|
||||
let mut fvks: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect();
|
||||
fvks.sort_unstable_by(Item::encoding_order);
|
||||
Ufvk { revision: Revision::R0, fvks }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,7 +257,7 @@ mod tests {
|
|||
0xdf, 0x63, 0xe7, 0xef, 0x65, 0x6b, 0x18, 0x23, 0xf7, 0x3e, 0x35, 0x7c, 0xf3, 0xc4,
|
||||
];
|
||||
assert_eq!(
|
||||
Ufvk::parse_internal(Ufvk::MAINNET, &invalid_padding[..]),
|
||||
Ufvk::parse_internal(Ufvk::MAINNET_R0, &invalid_padding[..]),
|
||||
Err(ParseError::InvalidEncoding(
|
||||
"Invalid padding bytes".to_owned()
|
||||
))
|
||||
|
@ -263,7 +275,7 @@ mod tests {
|
|||
0x43, 0x8e, 0xc0, 0x3e, 0x9f, 0xf4, 0xf1, 0x80, 0x32, 0xcf, 0x2f, 0x7e, 0x7f, 0x91,
|
||||
];
|
||||
assert_eq!(
|
||||
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_padding[..]),
|
||||
Ufvk::parse_internal(Ufvk::MAINNET_R0, &truncated_padding[..]),
|
||||
Err(ParseError::InvalidEncoding(
|
||||
"Invalid padding bytes".to_owned()
|
||||
))
|
||||
|
@ -295,7 +307,7 @@ mod tests {
|
|||
0x8c, 0x7a, 0xbf, 0x7b, 0x9a, 0xdd, 0xee, 0x18, 0x2c, 0x2d, 0xc2, 0xfc,
|
||||
];
|
||||
assert_matches!(
|
||||
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_sapling_data[..]),
|
||||
Ufvk::parse_internal(Ufvk::MAINNET_R0, &truncated_sapling_data[..]),
|
||||
Err(ParseError::InvalidEncoding(_))
|
||||
);
|
||||
|
||||
|
@ -310,7 +322,7 @@ mod tests {
|
|||
0x54, 0xd1, 0x9e, 0xec, 0x8b, 0xef, 0x35, 0xb8, 0x44, 0xdd, 0xab, 0x9a, 0x8d,
|
||||
];
|
||||
assert_matches!(
|
||||
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_after_sapling_typecode[..]),
|
||||
Ufvk::parse_internal(Ufvk::MAINNET_R0, &truncated_after_sapling_typecode[..]),
|
||||
Err(ParseError::InvalidEncoding(_))
|
||||
);
|
||||
}
|
||||
|
@ -319,11 +331,17 @@ mod tests {
|
|||
fn duplicate_typecode() {
|
||||
// Construct and serialize an invalid Ufvk. This must be done using private
|
||||
// methods, as the public API does not permit construction of such invalid values.
|
||||
let ufvk = Ufvk(vec![Fvk::Sapling([1; 128]), Fvk::Sapling([2; 128])]);
|
||||
let encoded = ufvk.to_jumbled_bytes(Ufvk::MAINNET);
|
||||
let ufvk = Ufvk {
|
||||
revision: Revision::R0,
|
||||
fvks: vec![
|
||||
Item::Data(Fvk::Sapling([1; 128])),
|
||||
Item::Data(Fvk::Sapling([2; 128])),
|
||||
],
|
||||
};
|
||||
let encoded = ufvk.to_jumbled_bytes(Ufvk::MAINNET_R0);
|
||||
assert_eq!(
|
||||
Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..]),
|
||||
Err(ParseError::DuplicateTypecode(Typecode::Sapling))
|
||||
Ufvk::parse_internal(Ufvk::MAINNET_R0, &encoded[..]),
|
||||
Err(ParseError::DuplicateTypecode(Typecode::SAPLING))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -339,37 +357,9 @@ mod tests {
|
|||
0xf4, 0xf5, 0x16, 0xef, 0x5c, 0xe0, 0x26, 0xbc, 0x23, 0x73, 0x76, 0x3f, 0x4b,
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..]),
|
||||
Err(ParseError::OnlyTransparent)
|
||||
assert_matches!(
|
||||
Ufvk::parse_internal(Ufvk::MAINNET_R0, &encoded[..]),
|
||||
Ok(_)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fvks_are_sorted() {
|
||||
// Construct a UFVK with fvks in an unsorted order.
|
||||
let ufvk = Ufvk(vec![
|
||||
Fvk::P2pkh([0; 65]),
|
||||
Fvk::Orchard([0; 96]),
|
||||
Fvk::Unknown {
|
||||
typecode: 0xff,
|
||||
data: vec![],
|
||||
},
|
||||
Fvk::Sapling([0; 128]),
|
||||
]);
|
||||
|
||||
// `Ufvk::items` sorts the fvks in priority order.
|
||||
assert_eq!(
|
||||
ufvk.items(),
|
||||
vec![
|
||||
Fvk::Orchard([0; 96]),
|
||||
Fvk::Sapling([0; 128]),
|
||||
Fvk::P2pkh([0; 65]),
|
||||
Fvk::Unknown {
|
||||
typecode: 0xff,
|
||||
data: vec![],
|
||||
},
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use std::convert::{TryFrom, TryInto};
|
||||
use std::convert::TryInto;
|
||||
|
||||
use super::{
|
||||
private::{SealedContainer, SealedItem},
|
||||
Container, Encoding, ParseError, Typecode,
|
||||
private::{SealedContainer, SealedDataItem},
|
||||
Container, DataTypecode, Encoding, Item, ParseError, Revision,
|
||||
};
|
||||
|
||||
/// The set of known IVKs for Unified IVKs.
|
||||
|
@ -44,31 +44,27 @@ pub enum Ivk {
|
|||
},
|
||||
}
|
||||
|
||||
impl TryFrom<(u32, &[u8])> for Ivk {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from((typecode, data): (u32, &[u8])) -> Result<Self, Self::Error> {
|
||||
impl SealedDataItem for Ivk {
|
||||
fn parse(typecode: DataTypecode, data: &[u8]) -> Result<Self, ParseError> {
|
||||
let data = data.to_vec();
|
||||
match typecode.try_into()? {
|
||||
Typecode::P2pkh => data.try_into().map(Ivk::P2pkh),
|
||||
Typecode::P2sh => Err(data),
|
||||
Typecode::Sapling => data.try_into().map(Ivk::Sapling),
|
||||
Typecode::Orchard => data.try_into().map(Ivk::Orchard),
|
||||
Typecode::Unknown(_) => Ok(Ivk::Unknown { typecode, data }),
|
||||
match typecode {
|
||||
DataTypecode::P2pkh => data.try_into().map(Ivk::P2pkh),
|
||||
DataTypecode::P2sh => Err(data),
|
||||
DataTypecode::Sapling => data.try_into().map(Ivk::Sapling),
|
||||
DataTypecode::Orchard => data.try_into().map(Ivk::Orchard),
|
||||
DataTypecode::Unknown(typecode) => Ok(Ivk::Unknown { typecode, data }),
|
||||
}
|
||||
.map_err(|e| {
|
||||
ParseError::InvalidEncoding(format!("Invalid ivk for typecode {}: {:?}", typecode, e))
|
||||
ParseError::InvalidEncoding(format!("Invalid ivk for typecode {:?}: {:?}", typecode, e))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SealedItem for Ivk {
|
||||
fn typecode(&self) -> Typecode {
|
||||
fn typecode(&self) -> DataTypecode {
|
||||
match self {
|
||||
Ivk::P2pkh(_) => Typecode::P2pkh,
|
||||
Ivk::Sapling(_) => Typecode::Sapling,
|
||||
Ivk::Orchard(_) => Typecode::Orchard,
|
||||
Ivk::Unknown { typecode, .. } => Typecode::Unknown(*typecode),
|
||||
Ivk::P2pkh(_) => DataTypecode::P2pkh,
|
||||
Ivk::Sapling(_) => DataTypecode::Sapling,
|
||||
Ivk::Orchard(_) => DataTypecode::Orchard,
|
||||
Ivk::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,7 +84,7 @@ impl SealedItem for Ivk {
|
|||
///
|
||||
/// ```
|
||||
/// # use std::error::Error;
|
||||
/// use zcash_address::unified::{self, Container, Encoding};
|
||||
/// use zcash_address::unified::{self, Container, Encoding, Item, Revision};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// # let uivk_from_user = || "uivk1djetqg3fws7y7qu5tekynvcdhz69gsyq07ewvppmzxdqhpfzdgmx8urnkqzv7ylz78ez43ux266pqjhecd59fzhn7wpe6zarnzh804hjtkyad25ryqla5pnc8p5wdl3phj9fczhz64zprun3ux7y9jc08567xryumuz59rjmg4uuflpjqwnq0j0tzce0x74t4tv3gfjq7nczkawxy6y7hse733ae3vw7qfjd0ss0pytvezxp42p6rrpzeh6t2zrz7zpjk0xhngcm6gwdppxs58jkx56gsfflugehf5vjlmu7vj3393gj6u37wenavtqyhdvcdeaj86s6jczl4zq";
|
||||
|
@ -96,54 +92,72 @@ impl SealedItem for Ivk {
|
|||
///
|
||||
/// let (network, uivk) = unified::Uivk::decode(example_uivk)?;
|
||||
///
|
||||
/// // We can obtain the pool-specific Incoming Viewing Keys for the UIVK in
|
||||
/// // preference order (the order in which wallets should prefer to use their
|
||||
/// // corresponding address receivers):
|
||||
/// let ivks: Vec<unified::Ivk> = uivk.items();
|
||||
/// // We can obtain the pool-specific Incoming Viewing Keys for the UIVK.
|
||||
/// let ivks: &[Item<unified::Ivk>] = uivk.items_as_parsed();
|
||||
///
|
||||
/// // And we can create the UIVK from a list of IVKs:
|
||||
/// let new_uivk = unified::Uivk::try_from_items(ivks)?;
|
||||
/// // And we can create the UIVK from a vector of IVKs:
|
||||
/// let new_uivk = unified::Uivk::try_from_items(Revision::R0, ivks.to_vec())?;
|
||||
/// assert_eq!(new_uivk, uivk);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Uivk(pub(crate) Vec<Ivk>);
|
||||
pub struct Uivk {
|
||||
pub(crate) revision: Revision,
|
||||
pub(crate) ivks: Vec<Item<Ivk>>,
|
||||
}
|
||||
|
||||
impl Container for Uivk {
|
||||
type Item = Ivk;
|
||||
type DataItem = Ivk;
|
||||
|
||||
/// Returns the IVKs contained within this UIVK, in the order they were
|
||||
/// parsed from the string encoding.
|
||||
///
|
||||
/// This API is for advanced usage; in most cases you should use `Uivk::items`.
|
||||
fn items_as_parsed(&self) -> &[Ivk] {
|
||||
&self.0
|
||||
fn items_as_parsed(&self) -> &[Item<Ivk>] {
|
||||
&self.ivks
|
||||
}
|
||||
|
||||
fn revision(&self) -> Revision {
|
||||
self.revision
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoding for Uivk {}
|
||||
|
||||
impl SealedContainer for Uivk {
|
||||
/// The HRP for a Bech32m-encoded mainnet Unified IVK.
|
||||
/// The HRP for a Bech32m-encoded mainnet Revision 0 Unified IVK.
|
||||
///
|
||||
/// Defined in [ZIP 316][zip-0316].
|
||||
///
|
||||
/// [zip-0316]: https://zips.z.cash/zip-0316
|
||||
const MAINNET: &'static str = "uivk";
|
||||
const MAINNET_R0: &'static str = "uivk";
|
||||
|
||||
/// The HRP for a Bech32m-encoded testnet Unified IVK.
|
||||
/// The HRP for a Bech32m-encoded testnet Revision 0 Unified IVK.
|
||||
///
|
||||
/// Defined in [ZIP 316][zip-0316].
|
||||
///
|
||||
/// [zip-0316]: https://zips.z.cash/zip-0316
|
||||
const TESTNET: &'static str = "uivktest";
|
||||
const TESTNET_R0: &'static str = "uivktest";
|
||||
|
||||
/// The HRP for a Bech32m-encoded regtest Unified IVK.
|
||||
const REGTEST: &'static str = "uivkregtest";
|
||||
/// The HRP for a Bech32m-encoded regtest Revision 0 Unified IVK.
|
||||
const REGTEST_R0: &'static str = "uivkregtest";
|
||||
|
||||
fn from_inner(ivks: Vec<Self::Item>) -> Self {
|
||||
Self(ivks)
|
||||
/// The HRP for a Bech32m-encoded mainnet Revision 1 Unified IVK.
|
||||
///
|
||||
/// Defined in [ZIP 316][zip-0316].
|
||||
///
|
||||
/// [zip-0316]: https://zips.z.cash/zip-0316
|
||||
const MAINNET_R1: &'static str = "urivk";
|
||||
|
||||
/// The HRP for a Bech32m-encoded testnet Revision 1 Unified IVK.
|
||||
///
|
||||
/// Defined in [ZIP 316][zip-0316].
|
||||
///
|
||||
/// [zip-0316]: https://zips.z.cash/zip-0316
|
||||
const TESTNET_R1: &'static str = "urivktest";
|
||||
|
||||
/// The HRP for a Bech32m-encoded regtest Revision 1 Unified IVK.
|
||||
const REGTEST_R1: &'static str = "urivkregtest";
|
||||
|
||||
fn from_inner(revision: Revision, ivks: Vec<Item<Self::DataItem>>) -> Self {
|
||||
Self { revision, ivks }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,12 +171,10 @@ mod tests {
|
|||
sample::select,
|
||||
};
|
||||
|
||||
use super::{Ivk, ParseError, Typecode, Uivk};
|
||||
use super::{Ivk, ParseError, Uivk};
|
||||
use crate::{
|
||||
kind::unified::{
|
||||
private::{SealedContainer, SealedItem},
|
||||
Container, Encoding,
|
||||
},
|
||||
kind::unified::{private::SealedContainer, Encoding},
|
||||
unified::{Item, Revision, Typecode},
|
||||
Network,
|
||||
};
|
||||
|
||||
|
@ -204,9 +216,9 @@ mod tests {
|
|||
shielded in arb_shielded_ivk(),
|
||||
transparent in prop::option::of(arb_transparent_ivk()),
|
||||
) -> Uivk {
|
||||
let mut items: Vec<_> = transparent.into_iter().chain(shielded).collect();
|
||||
items.sort_unstable_by(Ivk::encoding_order);
|
||||
Uivk(items)
|
||||
let mut ivks: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect();
|
||||
ivks.sort_unstable_by(Item::encoding_order);
|
||||
Uivk { revision: Revision::R0, ivks }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,7 +248,7 @@ mod tests {
|
|||
0x83, 0xe8, 0x92, 0x18, 0x28, 0x70, 0x1e, 0x81, 0x76, 0x56, 0xb6, 0x15,
|
||||
];
|
||||
assert_eq!(
|
||||
Uivk::parse_internal(Uivk::MAINNET, &invalid_padding[..]),
|
||||
Uivk::parse_internal(Uivk::MAINNET_R0, &invalid_padding[..]),
|
||||
Err(ParseError::InvalidEncoding(
|
||||
"Invalid padding bytes".to_owned()
|
||||
))
|
||||
|
@ -252,7 +264,7 @@ mod tests {
|
|||
0xf9, 0x65, 0x49, 0x14, 0xab, 0x7c, 0x55, 0x7b, 0x39, 0x47,
|
||||
];
|
||||
assert_eq!(
|
||||
Uivk::parse_internal(Uivk::MAINNET, &truncated_padding[..]),
|
||||
Uivk::parse_internal(Uivk::MAINNET_R0, &truncated_padding[..]),
|
||||
Err(ParseError::InvalidEncoding(
|
||||
"Invalid padding bytes".to_owned()
|
||||
))
|
||||
|
@ -280,7 +292,7 @@ mod tests {
|
|||
0xf5, 0xd5, 0x8a, 0xb5, 0x1a,
|
||||
];
|
||||
assert_matches!(
|
||||
Uivk::parse_internal(Uivk::MAINNET, &truncated_sapling_data[..]),
|
||||
Uivk::parse_internal(Uivk::MAINNET_R0, &truncated_sapling_data[..]),
|
||||
Err(ParseError::InvalidEncoding(_))
|
||||
);
|
||||
|
||||
|
@ -293,7 +305,7 @@ mod tests {
|
|||
0xd8, 0x21, 0x5e, 0x8, 0xa, 0x82, 0x95, 0x21, 0x74,
|
||||
];
|
||||
assert_matches!(
|
||||
Uivk::parse_internal(Uivk::MAINNET, &truncated_after_sapling_typecode[..]),
|
||||
Uivk::parse_internal(Uivk::MAINNET_R0, &truncated_after_sapling_typecode[..]),
|
||||
Err(ParseError::InvalidEncoding(_))
|
||||
);
|
||||
}
|
||||
|
@ -301,11 +313,17 @@ mod tests {
|
|||
#[test]
|
||||
fn duplicate_typecode() {
|
||||
// Construct and serialize an invalid UIVK.
|
||||
let uivk = Uivk(vec![Ivk::Sapling([1; 64]), Ivk::Sapling([2; 64])]);
|
||||
let uivk = Uivk {
|
||||
revision: Revision::R0,
|
||||
ivks: vec![
|
||||
Item::Data(Ivk::Sapling([1; 64])),
|
||||
Item::Data(Ivk::Sapling([2; 64])),
|
||||
],
|
||||
};
|
||||
let encoded = uivk.encode(&Network::Main);
|
||||
assert_eq!(
|
||||
Uivk::decode(&encoded),
|
||||
Err(ParseError::DuplicateTypecode(Typecode::Sapling))
|
||||
Err(ParseError::DuplicateTypecode(Typecode::SAPLING))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -321,37 +339,9 @@ mod tests {
|
|||
0xbd, 0xfe, 0xa4, 0xb7, 0x47, 0x20, 0x92, 0x6, 0xf0, 0x0, 0xf9, 0x64,
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
Uivk::parse_internal(Uivk::MAINNET, &encoded[..]),
|
||||
Err(ParseError::OnlyTransparent)
|
||||
assert_matches!(
|
||||
Uivk::parse_internal(Uivk::MAINNET_R0, &encoded[..]),
|
||||
Ok(_)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ivks_are_sorted() {
|
||||
// Construct a UIVK with ivks in an unsorted order.
|
||||
let uivk = Uivk(vec![
|
||||
Ivk::P2pkh([0; 65]),
|
||||
Ivk::Orchard([0; 64]),
|
||||
Ivk::Unknown {
|
||||
typecode: 0xff,
|
||||
data: vec![],
|
||||
},
|
||||
Ivk::Sapling([0; 64]),
|
||||
]);
|
||||
|
||||
// `Uivk::items` sorts the ivks in priority order.
|
||||
assert_eq!(
|
||||
uivk.items(),
|
||||
vec![
|
||||
Ivk::Orchard([0; 64]),
|
||||
Ivk::Sapling([0; 64]),
|
||||
Ivk::P2pkh([0; 65]),
|
||||
Ivk::Unknown {
|
||||
typecode: 0xff,
|
||||
data: vec![],
|
||||
},
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ use {
|
|||
unified::{
|
||||
self,
|
||||
address::{test_vectors::TEST_VECTORS, Receiver},
|
||||
Item, Revision,
|
||||
},
|
||||
Network, ToAddress, ZcashAddress,
|
||||
},
|
||||
|
@ -36,9 +37,16 @@ fn unified() {
|
|||
data: data.to_vec(),
|
||||
})
|
||||
}))
|
||||
.map(Item::Data)
|
||||
.collect();
|
||||
|
||||
let expected_addr = ZcashAddress::from_unified(Network::Main, unified::Address(receivers));
|
||||
let expected_addr = ZcashAddress::from_unified(
|
||||
Network::Main,
|
||||
unified::Address {
|
||||
revision: Revision::R0,
|
||||
receivers,
|
||||
},
|
||||
);
|
||||
|
||||
// Test parsing
|
||||
let addr: ZcashAddress = tv.unified_addr.parse().unwrap();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
# Changelog
|
||||
All notable changes to this library will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this library adheres to Rust's notion of
|
||||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
The entries below are relative to the `zcash_client_backend` crate as of
|
||||
`zcash_client_backend-0.10.0`.
|
||||
|
||||
### Added
|
||||
- `zip321::Payment::new`
|
||||
- `impl From<zcash_address:ConversionError<E>> for Zip321Error`
|
||||
|
||||
### Changed
|
||||
- Fields of `zip321::Payment` are now private. Accessors have been provided for
|
||||
the fields that are no longer public, and `Payment::new` has been added to
|
||||
serve the needs of payment construction.
|
||||
- `zip321::Payment::recipient_address()` returns `zcash_address::ZcashAddress`
|
||||
- `zip321::Payment::without_memo` now takes a `zcash_address::ZcashAddress` for
|
||||
its `recipient_address` argument.
|
||||
- Uses of `zcash_primitives::transaction::components::amount::NonNegartiveAmount`
|
||||
have been replace with `zcash_protocol::value::Zatoshis`. Also, some incorrect
|
||||
uses of the signed `zcash_primitives::transaction::components::Amount`
|
||||
type have been corrected via replacement with the `Zatoshis` type.
|
||||
- The following methods that previously required a
|
||||
`zcash_primitives::consensus::Parameters` argument to facilitate address
|
||||
parsing no longer take such an argument.
|
||||
- `zip321::TransactionRequest::{to_uri, from_uri}`
|
||||
- `zip321::render::addr_param`
|
||||
- `zip321::parse::{lead_addr, zcashparam}`
|
||||
- `zip321::Param::Memo` now boxes its argument.
|
||||
- `zip321::Param::Addr` now wraps a `zcash_address::ZcashAddress`
|
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "zip321"
|
||||
description = "Parsing functions and data types for Zcash ZIP 321 Payment Request URIs"
|
||||
version = "0.0.0"
|
||||
authors = [
|
||||
"Kris Nuttycombe <kris@electriccoin.co>"
|
||||
]
|
||||
homepage = "https://github.com/zcash/librustzcash"
|
||||
repository.workspace = true
|
||||
readme = "README.md"
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[dependencies]
|
||||
zcash_address.workspace = true
|
||||
zcash_protocol.workspace = true
|
||||
|
||||
# - Parsing and Encoding
|
||||
nom = "7"
|
||||
base64.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
zcash_address = { workspace = true, features = ["test-dependencies"] }
|
||||
zcash_protocol = { workspace = true, features = ["test-dependencies"] }
|
||||
proptest.workspace = true
|
|
@ -0,0 +1,202 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017-2024 Electric Coin Company
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
|
@ -0,0 +1,22 @@
|
|||
# zip321
|
||||
|
||||
This library contains Rust parsing functions and data types for working with
|
||||
Zcash ZIP 321 Payment Request URIs.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
||||
http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally
|
||||
submitted for inclusion in the work by you, as defined in the Apache-2.0
|
||||
license, shall be dual licensed as above, without any additional terms or
|
||||
conditions.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//! Reference implementation of the ZIP-321 standard for payment requests.
|
||||
//!
|
||||
//! 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(¶ms, &request.to_uri(¶ms))?;
|
||||
TransactionRequest::from_uri(&request.to_uri())?;
|
||||
}
|
||||
|
||||
Ok(request)
|
||||
|
@ -218,19 +279,19 @@ impl TransactionRequest {
|
|||
///
|
||||
/// Returns `Err` in the case of overflow, or if the value is
|
||||
/// outside the range `0..=MAX_MONEY` zatoshis.
|
||||
pub fn total(&self) -> Result<NonNegativeAmount, BalanceError> {
|
||||
pub fn total(&self) -> Result<Zatoshis, BalanceError> {
|
||||
self.payments
|
||||
.values()
|
||||
.map(|p| p.amount)
|
||||
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| {
|
||||
.fold(Ok(Zatoshis::ZERO), |acc, a| {
|
||||
(acc? + a).ok_or(BalanceError::Overflow)
|
||||
})
|
||||
}
|
||||
|
||||
/// A utility for use in tests to help check round-trip serialization properties.
|
||||
#[cfg(any(test, feature = "test-dependencies"))]
|
||||
pub(in crate::zip321) fn normalize(&mut self) {
|
||||
for p in self.payments.values_mut() {
|
||||
pub(crate) fn normalize(&mut self) {
|
||||
for p in &mut self.payments.values_mut() {
|
||||
p.normalize();
|
||||
}
|
||||
}
|
||||
|
@ -238,10 +299,7 @@ impl TransactionRequest {
|
|||
/// A utility for use in tests to help check round-trip serialization properties.
|
||||
/// by comparing a two transaction requests for equality after normalization.
|
||||
#[cfg(test)]
|
||||
pub(in crate::zip321) fn normalize_and_eq(
|
||||
a: &mut TransactionRequest,
|
||||
b: &mut TransactionRequest,
|
||||
) -> bool {
|
||||
pub(crate) fn normalize_and_eq(a: &mut TransactionRequest, b: &mut TransactionRequest) -> bool {
|
||||
a.normalize();
|
||||
b.normalize();
|
||||
|
||||
|
@ -251,7 +309,7 @@ impl TransactionRequest {
|
|||
/// Convert this request to a URI string.
|
||||
///
|
||||
/// Returns None if the payment request is empty.
|
||||
pub fn to_uri<P: consensus::Parameters>(&self, params: &P) -> String {
|
||||
pub fn to_uri(&self) -> String {
|
||||
fn payment_params(
|
||||
payment: &Payment,
|
||||
payment_index: Option<usize>,
|
||||
|
@ -294,7 +352,7 @@ impl TransactionRequest {
|
|||
|
||||
format!(
|
||||
"zcash:{}{}{}",
|
||||
payment.recipient_address.encode(params),
|
||||
payment.recipient_address.encode(),
|
||||
if query_params.is_empty() { "" } else { "?" },
|
||||
query_params.join("&")
|
||||
)
|
||||
|
@ -307,7 +365,7 @@ impl TransactionRequest {
|
|||
let idx = if *i == 0 { None } else { Some(*i) };
|
||||
let primary_address = payment.recipient_address.clone();
|
||||
std::iter::empty()
|
||||
.chain(Some(render::addr_param(params, &primary_address, idx)))
|
||||
.chain(Some(render::addr_param(&primary_address, idx)))
|
||||
.chain(payment_params(payment, idx))
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
@ -318,9 +376,9 @@ impl TransactionRequest {
|
|||
}
|
||||
|
||||
/// Parse the provided URI to a payment request value.
|
||||
pub fn from_uri<P: consensus::Parameters>(params: &P, uri: &str) -> Result<Self, Zip321Error> {
|
||||
pub fn from_uri(uri: &str) -> Result<Self, Zip321Error> {
|
||||
// Parse the leading zcash:<address>
|
||||
let (rest, primary_addr_param) = parse::lead_addr(params)(uri)
|
||||
let (rest, primary_addr_param) = parse::lead_addr(uri)
|
||||
.map_err(|e| Zip321Error::ParseError(format!("Error parsing lead address: {}", e)))?;
|
||||
|
||||
// Parse the remaining parameters as an undifferentiated list
|
||||
|
@ -329,7 +387,7 @@ impl TransactionRequest {
|
|||
} else {
|
||||
all_consuming(preceded(
|
||||
char('?'),
|
||||
separated_list0(char('&'), parse::zcashparam(params)),
|
||||
separated_list0(char('&'), parse::zcashparam),
|
||||
))(rest)
|
||||
.map_err(|e| {
|
||||
Zip321Error::ParseError(format!("Error parsing query parameters: {}", e))
|
||||
|
@ -372,13 +430,13 @@ impl TransactionRequest {
|
|||
|
||||
mod render {
|
||||
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus, transaction::components::amount::NonNegativeAmount,
|
||||
transaction::components::amount::COIN,
|
||||
use zcash_address::ZcashAddress;
|
||||
use zcash_protocol::{
|
||||
memo::MemoBytes,
|
||||
value::{Zatoshis, COIN},
|
||||
};
|
||||
|
||||
use super::{memo_to_base64, Address, MemoBytes};
|
||||
use super::memo_to_base64;
|
||||
|
||||
/// The set of ASCII characters that must be percent-encoded according
|
||||
/// to the definition of ZIP 321. This is the complement of the subset of
|
||||
|
@ -418,17 +476,13 @@ mod render {
|
|||
|
||||
/// Constructs an "address" key/value pair containing the encoded recipient address
|
||||
/// at the specified parameter index.
|
||||
pub fn addr_param<P: consensus::Parameters>(
|
||||
params: &P,
|
||||
addr: &Address,
|
||||
idx: Option<usize>,
|
||||
) -> String {
|
||||
format!("address{}={}", param_index(idx), addr.encode(params))
|
||||
pub fn addr_param(addr: &ZcashAddress, idx: Option<usize>) -> String {
|
||||
format!("address{}={}", param_index(idx), addr.encode())
|
||||
}
|
||||
|
||||
/// Converts a [`NonNegativeAmount`] value to a correctly formatted decimal ZEC
|
||||
/// string for inclusion in a ZIP 321 URI.
|
||||
pub fn amount_str(amount: NonNegativeAmount) -> String {
|
||||
/// Converts a [`Zatoshis`] value to a correctly formatted decimal ZEC
|
||||
/// value for inclusion in a ZIP 321 URI.
|
||||
pub fn amount_str(amount: Zatoshis) -> String {
|
||||
let coins = u64::from(amount) / COIN;
|
||||
let zats = u64::from(amount) % COIN;
|
||||
if zats == 0 {
|
||||
|
@ -442,7 +496,7 @@ mod render {
|
|||
|
||||
/// Constructs an "amount" key/value pair containing the encoded ZEC amount
|
||||
/// at the specified parameter index.
|
||||
pub fn amount_param(amount: NonNegativeAmount, idx: Option<usize>) -> String {
|
||||
pub fn amount_param(amount: Zatoshis, idx: Option<usize>) -> String {
|
||||
format!("amount{}={}", param_index(idx), amount_str(amount))
|
||||
}
|
||||
|
||||
|
@ -475,23 +529,22 @@ mod parse {
|
|||
AsChar, IResult, InputTakeAtPosition,
|
||||
};
|
||||
use percent_encoding::percent_decode;
|
||||
use zcash_primitives::{
|
||||
consensus, transaction::components::amount::NonNegativeAmount,
|
||||
transaction::components::amount::COIN,
|
||||
};
|
||||
use zcash_address::ZcashAddress;
|
||||
use zcash_protocol::value::BalanceError;
|
||||
use zcash_protocol::{
|
||||
memo::MemoBytes,
|
||||
value::{Zatoshis, COIN},
|
||||
};
|
||||
|
||||
use crate::address::Address;
|
||||
|
||||
use super::{memo_from_base64, MemoBytes, Payment, Zip321Error};
|
||||
use super::{memo_from_base64, Payment, Zip321Error};
|
||||
|
||||
/// A data type that defines the possible parameter types which may occur within a
|
||||
/// ZIP 321 URI.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Param {
|
||||
Addr(Box<Address>),
|
||||
Amount(NonNegativeAmount),
|
||||
Memo(MemoBytes),
|
||||
Addr(Box<ZcashAddress>),
|
||||
Amount(Zatoshis),
|
||||
Memo(Box<MemoBytes>),
|
||||
Label(String),
|
||||
Message(String),
|
||||
Other(String, String),
|
||||
|
@ -551,7 +604,7 @@ mod parse {
|
|||
|
||||
let mut payment = Payment {
|
||||
recipient_address: *addr.ok_or(Zip321Error::RecipientMissing(i))?,
|
||||
amount: NonNegativeAmount::ZERO,
|
||||
amount: Zatoshis::ZERO,
|
||||
memo: None,
|
||||
label: None,
|
||||
message: None,
|
||||
|
@ -561,11 +614,13 @@ mod parse {
|
|||
for v in vs {
|
||||
match v {
|
||||
Param::Amount(a) => payment.amount = a,
|
||||
Param::Memo(m) => match payment.recipient_address {
|
||||
Address::Sapling(_) | Address::Unified(_) => payment.memo = Some(m),
|
||||
Address::Transparent(_) => return Err(Zip321Error::TransparentMemo(i)),
|
||||
},
|
||||
|
||||
Param::Memo(m) => {
|
||||
if payment.recipient_address.can_receive_memo() {
|
||||
payment.memo = Some(*m);
|
||||
} else {
|
||||
return Err(Zip321Error::TransparentMemo(i));
|
||||
}
|
||||
}
|
||||
Param::Label(m) => payment.label = Some(m),
|
||||
Param::Message(m) => payment.message = Some(m),
|
||||
Param::Other(n, m) => payment.other_params.push((n, m)),
|
||||
|
@ -577,40 +632,34 @@ mod parse {
|
|||
}
|
||||
|
||||
/// Parses and consumes the leading "zcash:\[address\]" from a ZIP 321 URI.
|
||||
pub fn lead_addr<P: consensus::Parameters>(
|
||||
params: &P,
|
||||
) -> impl Fn(&str) -> IResult<&str, Option<IndexedParam>> + '_ {
|
||||
move |input: &str| {
|
||||
map_opt(
|
||||
preceded(tag("zcash:"), take_till(|c| c == '?')),
|
||||
|addr_str: &str| {
|
||||
if addr_str.is_empty() {
|
||||
Some(None) // no address is ok, so wrap in `Some`
|
||||
} else {
|
||||
// `decode` returns `None` on error, which we want to
|
||||
// then cause `map_opt` to fail.
|
||||
Address::decode(params, addr_str).map(|a| {
|
||||
pub fn lead_addr(input: &str) -> IResult<&str, Option<IndexedParam>> {
|
||||
map_opt(
|
||||
preceded(tag("zcash:"), take_till(|c| c == '?')),
|
||||
|addr_str: &str| {
|
||||
if addr_str.is_empty() {
|
||||
Some(None) // no address is ok, so wrap in `Some`
|
||||
} else {
|
||||
// `try_from_encoded(..).ok()` returns `None` on error, which we want to then
|
||||
// cause `map_opt` to fail.
|
||||
ZcashAddress::try_from_encoded(addr_str)
|
||||
.map(|a| {
|
||||
Some(IndexedParam {
|
||||
param: Param::Addr(Box::new(a)),
|
||||
payment_index: 0,
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
)(input)
|
||||
}
|
||||
.ok()
|
||||
}
|
||||
},
|
||||
)(input)
|
||||
}
|
||||
|
||||
/// The primary parser for <name>=<value> query-string parameter pair.
|
||||
pub fn zcashparam<P: consensus::Parameters>(
|
||||
params: &P,
|
||||
) -> impl Fn(&str) -> IResult<&str, IndexedParam> + '_ {
|
||||
move |input| {
|
||||
map_res(
|
||||
separated_pair(indexed_name, char('='), recognize(qchars)),
|
||||
move |r| to_indexed_param(params, r),
|
||||
)(input)
|
||||
}
|
||||
pub fn zcashparam(input: &str) -> IResult<&str, IndexedParam> {
|
||||
map_res(
|
||||
separated_pair(indexed_name, char('='), recognize(qchars)),
|
||||
to_indexed_param,
|
||||
)(input)
|
||||
}
|
||||
|
||||
/// Extension for the `alphanumeric0` parser which extends that parser
|
||||
|
@ -652,7 +701,7 @@ mod parse {
|
|||
}
|
||||
|
||||
/// Parses a value in decimal ZEC.
|
||||
pub fn parse_amount(input: &str) -> IResult<&str, NonNegativeAmount> {
|
||||
pub fn parse_amount(input: &str) -> IResult<&str, Zatoshis> {
|
||||
map_res(
|
||||
all_consuming(tuple((
|
||||
digit1,
|
||||
|
@ -678,28 +727,29 @@ mod parse {
|
|||
.checked_mul(COIN)
|
||||
.and_then(|coin_zats| coin_zats.checked_add(zats))
|
||||
.ok_or(BalanceError::Overflow)
|
||||
.and_then(NonNegativeAmount::from_u64)
|
||||
.map_err(|_| format!("Not a valid amount: {} ZEC", input))
|
||||
.and_then(Zatoshis::from_u64)
|
||||
.map_err(|_| format!("Not a valid zat amount: {}.{}", coins, zats))
|
||||
},
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn to_indexed_param<'a, P: consensus::Parameters>(
|
||||
params: &'a P,
|
||||
fn to_indexed_param(
|
||||
((name, iopt), value): ((&str, Option<&str>), &str),
|
||||
) -> Result<IndexedParam, String> {
|
||||
let param = match name {
|
||||
"address" => Address::decode(params, value)
|
||||
"address" => ZcashAddress::try_from_encoded(value)
|
||||
.map(Box::new)
|
||||
.map(Param::Addr)
|
||||
.ok_or(format!(
|
||||
"Could not interpret {} as a valid Zcash address.",
|
||||
value
|
||||
)),
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"Could not interpret {} as a valid Zcash address: {}",
|
||||
value, err
|
||||
)
|
||||
}),
|
||||
|
||||
"amount" => parse_amount(value)
|
||||
.map_err(|e| e.to_string())
|
||||
.map(|(_, amt)| Param::Amount(amt)),
|
||||
.map(|(_, a)| Param::Amount(a)),
|
||||
|
||||
"label" => percent_decode(value.as_bytes())
|
||||
.decode_utf8()
|
||||
|
@ -712,6 +762,7 @@ mod parse {
|
|||
.map_err(|e| e.to_string()),
|
||||
|
||||
"memo" => memo_from_base64(value)
|
||||
.map(Box::new)
|
||||
.map(Param::Memo)
|
||||
.map_err(|e| format!("Decoded memo was invalid: {:?}", e)),
|
||||
|
||||
|
@ -743,25 +794,14 @@ pub mod testing {
|
|||
use proptest::collection::vec;
|
||||
use proptest::option;
|
||||
use proptest::prelude::{any, prop_compose};
|
||||
use zcash_keys::address::testing::arb_addr;
|
||||
use zcash_keys::keys::UnifiedAddressRequest;
|
||||
use zcash_primitives::{
|
||||
consensus::TEST_NETWORK, transaction::components::amount::testing::arb_nonnegative_amount,
|
||||
};
|
||||
|
||||
use crate::address::Address;
|
||||
use zcash_address::testing::arb_address;
|
||||
use zcash_protocol::{consensus::NetworkType, value::testing::arb_zatoshis};
|
||||
|
||||
use super::{MemoBytes, Payment, TransactionRequest};
|
||||
|
||||
pub const VALID_PARAMNAME: &str = "[a-zA-Z][a-zA-Z0-9+-]*";
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
const TRANSPARENT_INPUTS_ENABLED: bool = true;
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
const TRANSPARENT_INPUTS_ENABLED: bool = false;
|
||||
|
||||
pub(crate) const UA_REQUEST: UnifiedAddressRequest =
|
||||
UnifiedAddressRequest::unsafe_new(false, true, TRANSPARENT_INPUTS_ENABLED);
|
||||
|
||||
prop_compose! {
|
||||
pub fn arb_valid_memo()(bytes in vec(any::<u8>(), 0..512)) -> MemoBytes {
|
||||
MemoBytes::from_bytes(&bytes).unwrap()
|
||||
|
@ -769,24 +809,20 @@ pub mod testing {
|
|||
}
|
||||
|
||||
prop_compose! {
|
||||
pub fn arb_zip321_payment()(
|
||||
recipient_address in arb_addr(UA_REQUEST),
|
||||
amount in arb_nonnegative_amount(),
|
||||
pub fn arb_zip321_payment(network: NetworkType)(
|
||||
recipient_address in arb_address(network),
|
||||
amount in arb_zatoshis(),
|
||||
memo in option::of(arb_valid_memo()),
|
||||
message in option::of(any::<String>()),
|
||||
label in option::of(any::<String>()),
|
||||
// prevent duplicates by generating a set rather than a vec
|
||||
other_params in btree_map(VALID_PARAMNAME, any::<String>(), 0..3),
|
||||
) -> Payment {
|
||||
let is_shielded = match recipient_address {
|
||||
Address::Transparent(_) => false,
|
||||
Address::Sapling(_) | Address::Unified(_) => true,
|
||||
};
|
||||
|
||||
let memo = memo.filter(|_| recipient_address.can_receive_memo());
|
||||
Payment {
|
||||
recipient_address,
|
||||
amount,
|
||||
memo: memo.filter(|_| is_shielded),
|
||||
memo,
|
||||
label,
|
||||
message,
|
||||
other_params: other_params.into_iter().collect(),
|
||||
|
@ -795,7 +831,9 @@ pub mod testing {
|
|||
}
|
||||
|
||||
prop_compose! {
|
||||
pub fn arb_zip321_request()(payments in btree_map(0usize..10000, arb_zip321_payment(), 1..10)) -> TransactionRequest {
|
||||
pub fn arb_zip321_request(network: NetworkType)(
|
||||
payments in btree_map(0usize..10000, arb_zip321_payment(network), 1..10)
|
||||
) -> TransactionRequest {
|
||||
let mut req = TransactionRequest::from_indexed(payments).unwrap();
|
||||
req.normalize(); // just to make test comparisons easier
|
||||
req
|
||||
|
@ -803,7 +841,9 @@ pub mod testing {
|
|||
}
|
||||
|
||||
prop_compose! {
|
||||
pub fn arb_zip321_request_sequential()(payments in vec(arb_zip321_payment(), 1..10)) -> TransactionRequest {
|
||||
pub fn arb_zip321_request_sequential(network: NetworkType)(
|
||||
payments in vec(arb_zip321_payment(network), 1..10)
|
||||
) -> TransactionRequest {
|
||||
let mut req = TransactionRequest::new(payments).unwrap();
|
||||
req.normalize(); // just to make test comparisons easier
|
||||
req
|
||||
|
@ -811,16 +851,16 @@ pub mod testing {
|
|||
}
|
||||
|
||||
prop_compose! {
|
||||
pub fn arb_zip321_uri()(req in arb_zip321_request()) -> String {
|
||||
req.to_uri(&TEST_NETWORK)
|
||||
pub fn arb_zip321_uri(network: NetworkType)(req in arb_zip321_request(network)) -> String {
|
||||
req.to_uri()
|
||||
}
|
||||
}
|
||||
|
||||
prop_compose! {
|
||||
pub fn arb_addr_str()(
|
||||
recipient_address in arb_addr(UA_REQUEST)
|
||||
pub fn arb_addr_str(network: NetworkType)(
|
||||
recipient_address in arb_address(network)
|
||||
) -> String {
|
||||
recipient_address.encode(&TEST_NETWORK)
|
||||
recipient_address.encode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -830,29 +870,27 @@ mod tests {
|
|||
use proptest::prelude::{any, proptest};
|
||||
use std::str::FromStr;
|
||||
|
||||
use zcash_keys::address::testing::arb_addr;
|
||||
use zcash_primitives::{
|
||||
memo::Memo,
|
||||
transaction::components::amount::{testing::arb_nonnegative_amount, NonNegativeAmount},
|
||||
use zcash_address::{testing::arb_address, ZcashAddress};
|
||||
use zcash_protocol::{
|
||||
consensus::NetworkType,
|
||||
memo::{Memo, MemoBytes},
|
||||
value::{testing::arb_zatoshis, Zatoshis},
|
||||
};
|
||||
use zcash_protocol::consensus::{NetworkConstants, NetworkType, TEST_NETWORK};
|
||||
|
||||
#[cfg(feature = "local-consensus")]
|
||||
use zcash_primitives::{local_consensus::LocalNetwork, BlockHeight};
|
||||
|
||||
use crate::{address::Address, encoding::decode_payment_address, zip321::testing::UA_REQUEST};
|
||||
use zcash_protocol::{local_consensus::LocalNetwork, BlockHeight};
|
||||
|
||||
use super::{
|
||||
memo_from_base64, memo_to_base64,
|
||||
parse::{parse_amount, zcashparam, Param},
|
||||
render::{amount_str, memo_param, str_param},
|
||||
testing::{arb_addr_str, arb_valid_memo, arb_zip321_request, arb_zip321_uri},
|
||||
MemoBytes, Payment, TransactionRequest,
|
||||
Payment, TransactionRequest,
|
||||
};
|
||||
|
||||
fn check_roundtrip(req: TransactionRequest) {
|
||||
let req_uri = req.to_uri(&TEST_NETWORK);
|
||||
let parsed = TransactionRequest::from_uri(&TEST_NETWORK, &req_uri).unwrap();
|
||||
let req_uri = req.to_uri();
|
||||
let parsed = TransactionRequest::from_uri(&req_uri).unwrap();
|
||||
assert_eq!(parsed, req);
|
||||
}
|
||||
|
||||
|
@ -861,7 +899,7 @@ mod tests {
|
|||
let amounts = vec![1u64, 1000u64, 100000u64, 100000000u64, 100000000000u64];
|
||||
|
||||
for amt_u64 in amounts {
|
||||
let amt = NonNegativeAmount::from_u64(amt_u64).unwrap();
|
||||
let amt = Zatoshis::const_from_u64(amt_u64);
|
||||
let amt_str = amount_str(amt);
|
||||
assert_eq!(amt, parse_amount(&amt_str).unwrap().1);
|
||||
}
|
||||
|
@ -871,20 +909,20 @@ mod tests {
|
|||
fn test_zip321_parse_empty_message() {
|
||||
let fragment = "message=";
|
||||
|
||||
let result = zcashparam(&TEST_NETWORK)(fragment).unwrap().1.param;
|
||||
let result = zcashparam(fragment).unwrap().1.param;
|
||||
assert_eq!(result, Param::Message("".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zip321_parse_simple() {
|
||||
let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k?amount=3768769.02796286&message=";
|
||||
let parse_result = TransactionRequest::from_uri(&TEST_NETWORK, uri).unwrap();
|
||||
let parse_result = TransactionRequest::from_uri(uri).unwrap();
|
||||
|
||||
let expected = TransactionRequest::new(
|
||||
vec![
|
||||
Payment {
|
||||
recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
|
||||
amount: NonNegativeAmount::const_from_u64(376876902796286),
|
||||
recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(),
|
||||
amount: Zatoshis::const_from_u64(376876902796286),
|
||||
memo: None,
|
||||
label: None,
|
||||
message: Some("".to_string()),
|
||||
|
@ -899,13 +937,13 @@ mod tests {
|
|||
#[test]
|
||||
fn test_zip321_parse_no_query_params() {
|
||||
let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k";
|
||||
let parse_result = TransactionRequest::from_uri(&TEST_NETWORK, uri).unwrap();
|
||||
let parse_result = TransactionRequest::from_uri(uri).unwrap();
|
||||
|
||||
let expected = TransactionRequest::new(
|
||||
vec![
|
||||
Payment {
|
||||
recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
|
||||
amount: NonNegativeAmount::ZERO,
|
||||
recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(),
|
||||
amount: Zatoshis::ZERO,
|
||||
memo: None,
|
||||
label: None,
|
||||
message: None,
|
||||
|
@ -922,8 +960,8 @@ mod tests {
|
|||
let req = TransactionRequest::new(
|
||||
vec![
|
||||
Payment {
|
||||
recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
|
||||
amount: NonNegativeAmount::ZERO,
|
||||
recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(),
|
||||
amount: Zatoshis::ZERO,
|
||||
memo: None,
|
||||
label: None,
|
||||
message: Some("".to_string()),
|
||||
|
@ -957,48 +995,48 @@ mod tests {
|
|||
#[test]
|
||||
fn test_zip321_spec_valid_examples() {
|
||||
let valid_0 = "zcash:";
|
||||
let v0r = TransactionRequest::from_uri(&TEST_NETWORK, valid_0).unwrap();
|
||||
let v0r = TransactionRequest::from_uri(valid_0).unwrap();
|
||||
assert!(v0r.payments.is_empty());
|
||||
|
||||
let valid_0 = "zcash:?";
|
||||
let v0r = TransactionRequest::from_uri(&TEST_NETWORK, valid_0).unwrap();
|
||||
let v0r = TransactionRequest::from_uri(valid_0).unwrap();
|
||||
assert!(v0r.payments.is_empty());
|
||||
|
||||
let valid_1 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase";
|
||||
let v1r = TransactionRequest::from_uri(&TEST_NETWORK, valid_1).unwrap();
|
||||
let v1r = TransactionRequest::from_uri(valid_1).unwrap();
|
||||
assert_eq!(
|
||||
v1r.payments.get(&0).map(|p| p.amount),
|
||||
Some(NonNegativeAmount::const_from_u64(100000000))
|
||||
Some(Zatoshis::const_from_u64(100000000))
|
||||
);
|
||||
|
||||
let valid_2 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok";
|
||||
let mut v2r = TransactionRequest::from_uri(&TEST_NETWORK, valid_2).unwrap();
|
||||
let mut v2r = TransactionRequest::from_uri(valid_2).unwrap();
|
||||
v2r.normalize();
|
||||
assert_eq!(
|
||||
v2r.payments.get(&0).map(|p| p.amount),
|
||||
Some(NonNegativeAmount::const_from_u64(12345600000))
|
||||
Some(Zatoshis::const_from_u64(12345600000))
|
||||
);
|
||||
assert_eq!(
|
||||
v2r.payments.get(&1).map(|p| p.amount),
|
||||
Some(NonNegativeAmount::const_from_u64(78900000))
|
||||
Some(Zatoshis::const_from_u64(78900000))
|
||||
);
|
||||
|
||||
// valid; amount just less than MAX_MONEY
|
||||
// 20999999.99999999
|
||||
let valid_3 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=20999999.99999999";
|
||||
let v3r = TransactionRequest::from_uri(&TEST_NETWORK, valid_3).unwrap();
|
||||
let v3r = TransactionRequest::from_uri(valid_3).unwrap();
|
||||
assert_eq!(
|
||||
v3r.payments.get(&0).map(|p| p.amount),
|
||||
Some(NonNegativeAmount::const_from_u64(2099999999999999u64))
|
||||
Some(Zatoshis::const_from_u64(2099999999999999))
|
||||
);
|
||||
|
||||
// valid; MAX_MONEY
|
||||
// 21000000
|
||||
let valid_4 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000";
|
||||
let v4r = TransactionRequest::from_uri(&TEST_NETWORK, valid_4).unwrap();
|
||||
let v4r = TransactionRequest::from_uri(valid_4).unwrap();
|
||||
assert_eq!(
|
||||
v4r.payments.get(&0).map(|p| p.amount),
|
||||
Some(NonNegativeAmount::const_from_u64(2100000000000000u64))
|
||||
Some(Zatoshis::const_from_u64(2100000000000000))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1019,7 +1057,7 @@ mod tests {
|
|||
let v1r = TransactionRequest::from_uri(¶ms, valid_1).unwrap();
|
||||
assert_eq!(
|
||||
v1r.payments.get(&0).map(|p| p.amount),
|
||||
Some(NonNegativeAmount::const_from_u64(100000000))
|
||||
Some(Zatoshis::const_from_u64(100000000))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1027,91 +1065,91 @@ mod tests {
|
|||
fn test_zip321_spec_invalid_examples() {
|
||||
// invalid; empty string
|
||||
let invalid_0 = "";
|
||||
let i0r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_0);
|
||||
let i0r = TransactionRequest::from_uri(invalid_0);
|
||||
assert!(i0r.is_err());
|
||||
|
||||
// invalid; missing `address=`
|
||||
let invalid_1 = "zcash:?amount=3491405.05201255&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=5740296.87793245";
|
||||
let i1r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_1);
|
||||
let i1r = TransactionRequest::from_uri(invalid_1);
|
||||
assert!(i1r.is_err());
|
||||
|
||||
// invalid; missing `address.1=`
|
||||
let invalid_2 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=1&amount.1=2&address.2=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez";
|
||||
let i2r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_2);
|
||||
let i2r = TransactionRequest::from_uri(invalid_2);
|
||||
assert!(i2r.is_err());
|
||||
|
||||
// invalid; `address.0=` and `amount.0=` are not permitted (leading 0s).
|
||||
let invalid_3 = "zcash:?address.0=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.0=2";
|
||||
let i3r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_3);
|
||||
let i3r = TransactionRequest::from_uri(invalid_3);
|
||||
assert!(i3r.is_err());
|
||||
|
||||
// invalid; duplicate `amount=` field
|
||||
let invalid_4 =
|
||||
"zcash:?amount=1.234&amount=2.345&address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
|
||||
let i4r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_4);
|
||||
let i4r = TransactionRequest::from_uri(invalid_4);
|
||||
assert!(i4r.is_err());
|
||||
|
||||
// invalid; duplicate `amount.1=` field
|
||||
let invalid_5 =
|
||||
"zcash:?amount.1=1.234&amount.1=2.345&address.1=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
|
||||
let i5r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_5);
|
||||
let i5r = TransactionRequest::from_uri(invalid_5);
|
||||
assert!(i5r.is_err());
|
||||
|
||||
//invalid; memo associated with t-addr
|
||||
let invalid_6 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&memo=eyAia2V5IjogIlRoaXMgaXMgYSBKU09OLXN0cnVjdHVyZWQgbWVtby4iIH0&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok";
|
||||
let i6r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_6);
|
||||
let i6r = TransactionRequest::from_uri(invalid_6);
|
||||
assert!(i6r.is_err());
|
||||
|
||||
// invalid; amount component exceeds an i64
|
||||
// 9223372036854775808 = i64::MAX + 1
|
||||
let invalid_7 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=9223372036854775808";
|
||||
let i7r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_7);
|
||||
let i7r = TransactionRequest::from_uri(invalid_7);
|
||||
assert!(i7r.is_err());
|
||||
|
||||
// invalid; amount component wraps into a valid small positive i64
|
||||
// 18446744073709551624
|
||||
let invalid_7a = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=18446744073709551624";
|
||||
let i7ar = TransactionRequest::from_uri(&TEST_NETWORK, invalid_7a);
|
||||
let i7ar = TransactionRequest::from_uri(invalid_7a);
|
||||
assert!(i7ar.is_err());
|
||||
|
||||
// invalid; amount component is MAX_MONEY
|
||||
// 21000000.00000001
|
||||
let invalid_8 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000.00000001";
|
||||
let i8r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_8);
|
||||
let i8r = TransactionRequest::from_uri(invalid_8);
|
||||
assert!(i8r.is_err());
|
||||
|
||||
// invalid; negative amount
|
||||
let invalid_9 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=-1";
|
||||
let i9r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_9);
|
||||
let i9r = TransactionRequest::from_uri(invalid_9);
|
||||
assert!(i9r.is_err());
|
||||
|
||||
// invalid; parameter index too large
|
||||
let invalid_10 =
|
||||
"zcash:?amount.10000=1.23&address.10000=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
|
||||
let i10r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_10);
|
||||
let i10r = TransactionRequest::from_uri(invalid_10);
|
||||
assert!(i10r.is_err());
|
||||
|
||||
// invalid: bad amount format
|
||||
let invalid_11 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.";
|
||||
let i11r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_11);
|
||||
let i11r = TransactionRequest::from_uri(invalid_11);
|
||||
assert!(i11r.is_err());
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn prop_zip321_roundtrip_address(addr in arb_addr(UA_REQUEST)) {
|
||||
let a = addr.encode(&TEST_NETWORK);
|
||||
assert_eq!(Address::decode(&TEST_NETWORK, &a), Some(addr));
|
||||
fn prop_zip321_roundtrip_address(addr in arb_address(NetworkType::Test)) {
|
||||
let a = addr.encode();
|
||||
assert_eq!(ZcashAddress::try_from_encoded(&a), Ok(addr));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prop_zip321_roundtrip_address_str(a in arb_addr_str()) {
|
||||
let addr = Address::decode(&TEST_NETWORK, &a).unwrap();
|
||||
assert_eq!(addr.encode(&TEST_NETWORK), a);
|
||||
fn prop_zip321_roundtrip_address_str(a in arb_addr_str(NetworkType::Test)) {
|
||||
let addr = ZcashAddress::try_from_encoded(&a).unwrap();
|
||||
assert_eq!(addr.encode(), a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prop_zip321_roundtrip_amount(amt in arb_nonnegative_amount()) {
|
||||
fn prop_zip321_roundtrip_amount(amt in arb_zatoshis()) {
|
||||
let amt_str = amount_str(amt);
|
||||
assert_eq!(amt, parse_amount(&amt_str).unwrap().1);
|
||||
}
|
||||
|
@ -1120,7 +1158,7 @@ mod tests {
|
|||
fn prop_zip321_roundtrip_str_param(
|
||||
message in any::<String>(), i in proptest::option::of(0usize..2000)) {
|
||||
let fragment = str_param("message", &message, i);
|
||||
let (rest, iparam) = zcashparam(&TEST_NETWORK)(&fragment).unwrap();
|
||||
let (rest, iparam) = zcashparam(&fragment).unwrap();
|
||||
assert_eq!(rest, "");
|
||||
assert_eq!(iparam.param, Param::Message(message));
|
||||
assert_eq!(iparam.payment_index, i.unwrap_or(0));
|
||||
|
@ -1130,24 +1168,24 @@ mod tests {
|
|||
fn prop_zip321_roundtrip_memo_param(
|
||||
memo in arb_valid_memo(), i in proptest::option::of(0usize..2000)) {
|
||||
let fragment = memo_param(&memo, i);
|
||||
let (rest, iparam) = zcashparam(&TEST_NETWORK)(&fragment).unwrap();
|
||||
let (rest, iparam) = zcashparam(&fragment).unwrap();
|
||||
assert_eq!(rest, "");
|
||||
assert_eq!(iparam.param, Param::Memo(memo));
|
||||
assert_eq!(iparam.param, Param::Memo(Box::new(memo)));
|
||||
assert_eq!(iparam.payment_index, i.unwrap_or(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prop_zip321_roundtrip_request(mut req in arb_zip321_request()) {
|
||||
let req_uri = req.to_uri(&TEST_NETWORK);
|
||||
let mut parsed = TransactionRequest::from_uri(&TEST_NETWORK, &req_uri).unwrap();
|
||||
fn prop_zip321_roundtrip_request(mut req in arb_zip321_request(NetworkType::Test)) {
|
||||
let req_uri = req.to_uri();
|
||||
let mut parsed = TransactionRequest::from_uri(&req_uri).unwrap();
|
||||
assert!(TransactionRequest::normalize_and_eq(&mut parsed, &mut req));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prop_zip321_roundtrip_uri(uri in arb_zip321_uri()) {
|
||||
let mut parsed = TransactionRequest::from_uri(&TEST_NETWORK, &uri).unwrap();
|
||||
fn prop_zip321_roundtrip_uri(uri in arb_zip321_uri(NetworkType::Test)) {
|
||||
let mut parsed = TransactionRequest::from_uri(&uri).unwrap();
|
||||
parsed.normalize();
|
||||
let serialized = parsed.to_uri(&TEST_NETWORK);
|
||||
let serialized = parsed.to_uri();
|
||||
assert_eq!(serialized, uri)
|
||||
}
|
||||
}
|
|
@ -7,6 +7,29 @@ and this library adheres to Rust's notion of
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- `zcash_client_backend::data_api`:
|
||||
- `chain::BlockCache` trait, behind the `sync` feature flag.
|
||||
- `zcash_client_backend::scanning`:
|
||||
- `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
|
||||
|
@ -39,7 +62,6 @@ and this library adheres to Rust's notion of
|
|||
- `WalletSummary::next_orchard_subtree_index`
|
||||
- `chain::ChainState`
|
||||
- `chain::ScanSummary::{spent_orchard_note_count, received_orchard_note_count}`
|
||||
- `chain::BlockCache` trait
|
||||
- `impl Debug for chain::CommitmentTreeRoot`
|
||||
- `zcash_client_backend::fees`:
|
||||
- `orchard`
|
||||
|
@ -55,7 +77,6 @@ and this library adheres to Rust's notion of
|
|||
- `Nullifiers::{orchard, extend_orchard, retain_orchard}`
|
||||
- `TaggedOrchardBatch`
|
||||
- `TaggedOrchardBatchRunner`
|
||||
- `testing` module
|
||||
- `zcash_client_backend::wallet`:
|
||||
- `Note::Orchard`
|
||||
- `WalletOrchardSpend`
|
||||
|
@ -112,6 +133,9 @@ and this library adheres to Rust's notion of
|
|||
- Arguments to `ChangeStrategy::compute_balance` have changed.
|
||||
- `ChangeError::DustInputs` now has an `orchard` field behind the `orchard`
|
||||
feature flag.
|
||||
- `zcash_client_backend::wallet`:
|
||||
- The address variants of `Recipient` now `Box` their contents to avoid large
|
||||
discrepancies in enum variant sizing.
|
||||
- `zcash_client_backend::proto`:
|
||||
- `ProposalDecodingError` has a new variant `TransparentMemo`.
|
||||
- `zcash_client_backend::wallet::Recipient::InternalAccount` is now a structured
|
||||
|
|
|
@ -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.)
|
||||
|
@ -66,7 +67,7 @@ tracing.workspace = true
|
|||
# - Protobuf interfaces and gRPC bindings
|
||||
hex.workspace = true
|
||||
prost.workspace = true
|
||||
tonic = { workspace = true, optional = true, features = ["prost", "codegen"]}
|
||||
tonic = { workspace = true, optional = true, features = ["prost", "codegen"] }
|
||||
|
||||
# - Secret management
|
||||
secrecy.workspace = true
|
||||
|
@ -78,6 +79,10 @@ group.workspace = true
|
|||
orchard = { workspace = true, optional = true }
|
||||
sapling.workspace = true
|
||||
|
||||
# - Sync engine
|
||||
async-trait = { version = "0.1", optional = true }
|
||||
futures-util = { version = "0.3", optional = true }
|
||||
|
||||
# - Note commitment trees
|
||||
incrementalmerkletree.workspace = true
|
||||
shardtree.workspace = true
|
||||
|
@ -89,9 +94,6 @@ jubjub = { workspace = true, optional = true }
|
|||
# - ZIP 321
|
||||
nom = "7"
|
||||
|
||||
# - Asychronous
|
||||
async-trait = "0.1.78"
|
||||
|
||||
# Dependencies used internally:
|
||||
# (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.)
|
||||
# - Documentation
|
||||
|
@ -106,7 +108,7 @@ crossbeam-channel.workspace = true
|
|||
rayon.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = { workspace = true, features = ["prost"]}
|
||||
tonic-build = { workspace = true, features = ["prost"] }
|
||||
which = "4"
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -141,6 +143,13 @@ transparent-inputs = [
|
|||
## Enables receiving and spending Orchard funds.
|
||||
orchard = ["dep:orchard", "zcash_keys/orchard"]
|
||||
|
||||
## Exposes a wallet synchronization function that implements the necessary state machine.
|
||||
sync = [
|
||||
"lightwalletd-tonic",
|
||||
"dep:async-trait",
|
||||
"dep:futures-util",
|
||||
]
|
||||
|
||||
## Exposes APIs that are useful for testing, such as `proptest` strategies.
|
||||
test-dependencies = [
|
||||
"dep:proptest",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -151,9 +151,8 @@
|
|||
//! # }
|
||||
//! ```
|
||||
|
||||
use std::ops::{Add, Range};
|
||||
use std::ops::Range;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use incrementalmerkletree::frontier::Frontier;
|
||||
use subtle::ConditionallySelectable;
|
||||
use zcash_primitives::{
|
||||
|
@ -162,15 +161,20 @@ use zcash_primitives::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
data_api::{scanning::ScanRange, NullifierQuery, WalletWrite},
|
||||
data_api::{NullifierQuery, WalletWrite},
|
||||
proto::compact_formats::CompactBlock,
|
||||
scanning::{scan_block_with_runners, BatchRunners, Nullifiers, ScanningKeys},
|
||||
};
|
||||
|
||||
#[cfg(feature = "sync")]
|
||||
use {
|
||||
super::scanning::ScanPriority, crate::data_api::scanning::ScanRange, async_trait::async_trait,
|
||||
};
|
||||
|
||||
pub mod error;
|
||||
use error::Error;
|
||||
|
||||
use super::{scanning::ScanPriority, WalletRead};
|
||||
use super::WalletRead;
|
||||
|
||||
/// A struct containing metadata about a subtree root of the note commitment tree.
|
||||
///
|
||||
|
@ -300,7 +304,7 @@ pub trait BlockSource {
|
|||
/// Ok(())
|
||||
/// }
|
||||
///
|
||||
/// async fn delete(&self, range: &ScanRange) -> Result<(), Self::Error> {
|
||||
/// async fn delete(&self, range: ScanRange) -> Result<(), Self::Error> {
|
||||
/// self.cached_blocks
|
||||
/// .lock()
|
||||
/// .unwrap()
|
||||
|
@ -368,11 +372,12 @@ pub trait BlockSource {
|
|||
///
|
||||
/// // Delete blocks from the block cache
|
||||
/// rt.block_on(async {
|
||||
/// block_cache.delete(&range).await.unwrap();
|
||||
/// block_cache.delete(range).await.unwrap();
|
||||
/// });
|
||||
/// assert_eq!(block_cache.cached_blocks.lock().unwrap().len(), 0);
|
||||
/// assert_eq!(block_cache.get_tip_height(None).unwrap(), None);
|
||||
/// ```
|
||||
#[cfg(feature = "sync")]
|
||||
#[async_trait]
|
||||
pub trait BlockCache: BlockSource + Send + Sync
|
||||
where
|
||||
|
@ -404,10 +409,10 @@ where
|
|||
/// Removes all cached blocks above a specified block height.
|
||||
async fn truncate(&self, block_height: BlockHeight) -> Result<(), Self::Error> {
|
||||
if let Some(latest) = self.get_tip_height(None)? {
|
||||
self.delete(&ScanRange::from_parts(
|
||||
self.delete(ScanRange::from_parts(
|
||||
Range {
|
||||
start: block_height.add(1),
|
||||
end: latest.add(1),
|
||||
start: block_height + 1,
|
||||
end: latest + 1,
|
||||
},
|
||||
ScanPriority::Ignored,
|
||||
))
|
||||
|
@ -421,7 +426,7 @@ where
|
|||
/// # Errors
|
||||
///
|
||||
/// In the case of an error, some blocks requested for deletion may remain in the block cache.
|
||||
async fn delete(&self, range: &ScanRange) -> Result<(), Self::Error>;
|
||||
async fn delete(&self, range: ScanRange) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
/// Metadata about modifications to the wallet state made in the course of scanning a set of
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -497,14 +497,15 @@ where
|
|||
>,
|
||||
DbT::NoteRef: Copy + Eq + Ord,
|
||||
{
|
||||
let request = zip321::TransactionRequest::new(vec![Payment {
|
||||
recipient_address: to.clone(),
|
||||
let request = zip321::TransactionRequest::new(vec![Payment::new(
|
||||
to.to_zcash_address(params),
|
||||
amount,
|
||||
memo,
|
||||
label: None,
|
||||
message: None,
|
||||
other_params: vec![],
|
||||
}])
|
||||
None,
|
||||
None,
|
||||
vec![],
|
||||
)
|
||||
.ok_or(Error::MemoForbidden)?])
|
||||
.expect(
|
||||
"It should not be possible for this to violate ZIP 321 request construction invariants.",
|
||||
);
|
||||
|
@ -848,14 +849,17 @@ where
|
|||
// the transaction in payment index order, so we can use dead reckoning to
|
||||
// figure out which output it ended up being.
|
||||
let (prior_step, result) = &prior_step_results[input_ref.step_index()];
|
||||
let recipient_address = match &prior_step
|
||||
let recipient_address = &prior_step
|
||||
.transaction_request()
|
||||
.payments()
|
||||
.get(&i)
|
||||
.expect("Payment step references are checked at construction")
|
||||
.recipient_address
|
||||
{
|
||||
Address::Transparent(t) => Some(t),
|
||||
.recipient_address()
|
||||
.clone()
|
||||
.convert_if_network(params.network_type())?;
|
||||
|
||||
let recipient_taddr = match recipient_address {
|
||||
Address::Transparent(t) => Some(t.as_ref()),
|
||||
Address::Unified(uaddr) => uaddr.transparent(),
|
||||
_ => None,
|
||||
}
|
||||
|
@ -879,7 +883,7 @@ where
|
|||
.ok_or(Error::Proposal(ProposalError::ReferenceError(*input_ref)))?
|
||||
.vout[outpoint.n() as usize];
|
||||
|
||||
add_transparent_input(recipient_address, outpoint, utxo.clone())?;
|
||||
add_transparent_input(recipient_taddr, outpoint, utxo.clone())?;
|
||||
}
|
||||
proposal::StepOutputIndex::Change(_) => unreachable!(),
|
||||
}
|
||||
|
@ -953,12 +957,14 @@ where
|
|||
(payment, output_pool)
|
||||
})
|
||||
{
|
||||
match &payment.recipient_address {
|
||||
let recipient_address: Address = payment
|
||||
.recipient_address()
|
||||
.clone()
|
||||
.convert_if_network(params.network_type())?;
|
||||
|
||||
match recipient_address {
|
||||
Address::Unified(ua) => {
|
||||
let memo = payment
|
||||
.memo
|
||||
.as_ref()
|
||||
.map_or_else(MemoBytes::empty, |m| m.clone());
|
||||
let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
|
||||
|
||||
match output_pool {
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
|
@ -970,15 +976,15 @@ where
|
|||
builder.add_orchard_output(
|
||||
orchard_external_ovk.clone(),
|
||||
*ua.orchard().expect("The mapping between payment pool and receiver is checked in step construction"),
|
||||
payment.amount.into(),
|
||||
payment.amount().into(),
|
||||
memo.clone(),
|
||||
)?;
|
||||
orchard_output_meta.push((
|
||||
Recipient::Unified(
|
||||
ua.clone(),
|
||||
Recipient::External(
|
||||
payment.recipient_address().clone(),
|
||||
PoolType::Shielded(ShieldedProtocol::Orchard),
|
||||
),
|
||||
payment.amount,
|
||||
payment.amount(),
|
||||
Some(memo),
|
||||
));
|
||||
}
|
||||
|
@ -987,51 +993,56 @@ where
|
|||
builder.add_sapling_output(
|
||||
sapling_external_ovk,
|
||||
*ua.sapling().expect("The mapping between payment pool and receiver is checked in step construction"),
|
||||
payment.amount,
|
||||
payment.amount(),
|
||||
memo.clone(),
|
||||
)?;
|
||||
sapling_output_meta.push((
|
||||
Recipient::Unified(
|
||||
ua.clone(),
|
||||
Recipient::External(
|
||||
payment.recipient_address().clone(),
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling),
|
||||
),
|
||||
payment.amount,
|
||||
payment.amount(),
|
||||
Some(memo),
|
||||
));
|
||||
}
|
||||
|
||||
PoolType::Transparent => {
|
||||
if payment.memo.is_some() {
|
||||
if payment.memo().is_some() {
|
||||
return Err(Error::MemoForbidden);
|
||||
} else {
|
||||
builder.add_transparent_output(
|
||||
ua.transparent().expect("The mapping between payment pool and receiver is checked in step construction."),
|
||||
payment.amount
|
||||
payment.amount()
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Address::Sapling(addr) => {
|
||||
let memo = payment
|
||||
.memo
|
||||
.as_ref()
|
||||
.map_or_else(MemoBytes::empty, |m| m.clone());
|
||||
let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
|
||||
builder.add_sapling_output(
|
||||
sapling_external_ovk,
|
||||
*addr,
|
||||
payment.amount,
|
||||
payment.amount(),
|
||||
memo.clone(),
|
||||
)?;
|
||||
sapling_output_meta.push((Recipient::Sapling(*addr), payment.amount, Some(memo)));
|
||||
sapling_output_meta.push((
|
||||
Recipient::External(payment.recipient_address().clone(), PoolType::SAPLING),
|
||||
payment.amount(),
|
||||
Some(memo),
|
||||
));
|
||||
}
|
||||
Address::Transparent(to) => {
|
||||
if payment.memo.is_some() {
|
||||
if payment.memo().is_some() {
|
||||
return Err(Error::MemoForbidden);
|
||||
} else {
|
||||
builder.add_transparent_output(to, payment.amount)?;
|
||||
builder.add_transparent_output(&to, payment.amount())?;
|
||||
}
|
||||
transparent_output_meta.push((to, payment.amount));
|
||||
transparent_output_meta.push((
|
||||
Recipient::External(payment.recipient_address().clone(), PoolType::TRANSPARENT),
|
||||
to,
|
||||
payment.amount(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1153,22 +1164,27 @@ where
|
|||
SentTransactionOutput::from_parts(output_index, recipient, value, memo)
|
||||
});
|
||||
|
||||
let transparent_outputs = transparent_output_meta.into_iter().map(|(addr, value)| {
|
||||
let script = addr.script();
|
||||
let output_index = build_result
|
||||
.transaction()
|
||||
.transparent_bundle()
|
||||
.and_then(|b| {
|
||||
b.vout
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, tx_out)| tx_out.script_pubkey == script)
|
||||
})
|
||||
.map(|(index, _)| index)
|
||||
.expect("An output should exist in the transaction for each transparent payment.");
|
||||
let transparent_outputs =
|
||||
transparent_output_meta
|
||||
.into_iter()
|
||||
.map(|(recipient, addr, value)| {
|
||||
let script = addr.script();
|
||||
let output_index = build_result
|
||||
.transaction()
|
||||
.transparent_bundle()
|
||||
.and_then(|b| {
|
||||
b.vout
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, tx_out)| tx_out.script_pubkey == script)
|
||||
})
|
||||
.map(|(index, _)| index)
|
||||
.expect(
|
||||
"An output should exist in the transaction for each transparent payment.",
|
||||
);
|
||||
|
||||
SentTransactionOutput::from_parts(output_index, Recipient::Transparent(*addr), value, None)
|
||||
});
|
||||
SentTransactionOutput::from_parts(output_index, recipient, value, None)
|
||||
});
|
||||
|
||||
let mut outputs = vec![];
|
||||
#[cfg(feature = "orchard")]
|
||||
|
|
|
@ -8,6 +8,7 @@ use std::{
|
|||
};
|
||||
|
||||
use nonempty::NonEmpty;
|
||||
use zcash_address::{ConversionError, ZcashAddress};
|
||||
use zcash_primitives::{
|
||||
consensus::{self, BlockHeight},
|
||||
transaction::{
|
||||
|
@ -20,7 +21,7 @@ use zcash_primitives::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
address::{Address, UnifiedAddress},
|
||||
address::Address,
|
||||
data_api::{InputSource, SimpleNoteRetention, SpendableNotes},
|
||||
fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy},
|
||||
proposal::{Proposal, ProposalError, ShieldedInputs},
|
||||
|
@ -48,6 +49,8 @@ pub enum InputSelectorError<DbErrT, SelectorErrT> {
|
|||
Selection(SelectorErrT),
|
||||
/// Input selection attempted to generate an invalid transaction proposal.
|
||||
Proposal(ProposalError),
|
||||
/// An error occurred parsing the address from a payment request.
|
||||
Address(ConversionError<&'static str>),
|
||||
/// Insufficient funds were available to satisfy the payment request that inputs were being
|
||||
/// selected to attempt to satisfy.
|
||||
InsufficientFunds {
|
||||
|
@ -59,6 +62,12 @@ pub enum InputSelectorError<DbErrT, SelectorErrT> {
|
|||
SyncRequired,
|
||||
}
|
||||
|
||||
impl<E, S> From<ConversionError<&'static str>> for InputSelectorError<E, S> {
|
||||
fn from(value: ConversionError<&'static str>) -> Self {
|
||||
InputSelectorError::Address(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE, SE> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match &self {
|
||||
|
@ -79,6 +88,13 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
|
|||
e
|
||||
)
|
||||
}
|
||||
InputSelectorError::Address(e) => {
|
||||
write!(
|
||||
f,
|
||||
"An error occurred decoding the address from a payment request: {}.",
|
||||
e
|
||||
)
|
||||
}
|
||||
InputSelectorError::InsufficientFunds {
|
||||
available,
|
||||
required,
|
||||
|
@ -205,7 +221,7 @@ pub enum GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT> {
|
|||
/// An intermediate value overflowed or underflowed the valid monetary range.
|
||||
Balance(BalanceError),
|
||||
/// A unified address did not contain a supported receiver.
|
||||
UnsupportedAddress(Box<UnifiedAddress>),
|
||||
UnsupportedAddress(ZcashAddress),
|
||||
/// An error was encountered in change selection.
|
||||
Change(ChangeError<ChangeStrategyErrT, NoteRefT>),
|
||||
}
|
||||
|
@ -218,10 +234,12 @@ impl<CE: fmt::Display, N: fmt::Display> fmt::Display for GreedyInputSelectorErro
|
|||
"A balance calculation violated amount validity bounds: {:?}.",
|
||||
e
|
||||
),
|
||||
GreedyInputSelectorError::UnsupportedAddress(_) => {
|
||||
// we can't encode the UA to its string representation because we
|
||||
// don't have network parameters here
|
||||
write!(f, "Unified address contains no supported receivers.")
|
||||
GreedyInputSelectorError::UnsupportedAddress(addr) => {
|
||||
write!(
|
||||
f,
|
||||
"Unified address {} contains no supported receivers.",
|
||||
addr.encode()
|
||||
)
|
||||
}
|
||||
GreedyInputSelectorError::Change(err) => {
|
||||
write!(f, "An error occurred computing change and fees: {}", err)
|
||||
|
@ -344,43 +362,50 @@ where
|
|||
let mut orchard_outputs = vec![];
|
||||
let mut payment_pools = BTreeMap::new();
|
||||
for (idx, payment) in transaction_request.payments() {
|
||||
match &payment.recipient_address {
|
||||
let recipient_address: Address = payment
|
||||
.recipient_address()
|
||||
.clone()
|
||||
.convert_if_network(params.network_type())?;
|
||||
|
||||
match recipient_address {
|
||||
Address::Transparent(addr) => {
|
||||
payment_pools.insert(*idx, PoolType::Transparent);
|
||||
transparent_outputs.push(TxOut {
|
||||
value: payment.amount,
|
||||
value: payment.amount(),
|
||||
script_pubkey: addr.script(),
|
||||
});
|
||||
}
|
||||
Address::Sapling(_) => {
|
||||
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling));
|
||||
sapling_outputs.push(SaplingPayment(payment.amount));
|
||||
sapling_outputs.push(SaplingPayment(payment.amount()));
|
||||
}
|
||||
Address::Unified(addr) => {
|
||||
#[cfg(feature = "orchard")]
|
||||
if addr.orchard().is_some() {
|
||||
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Orchard));
|
||||
orchard_outputs.push(OrchardPayment(payment.amount));
|
||||
orchard_outputs.push(OrchardPayment(payment.amount()));
|
||||
continue;
|
||||
}
|
||||
|
||||
if addr.sapling().is_some() {
|
||||
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling));
|
||||
sapling_outputs.push(SaplingPayment(payment.amount));
|
||||
sapling_outputs.push(SaplingPayment(payment.amount()));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(addr) = addr.transparent() {
|
||||
payment_pools.insert(*idx, PoolType::Transparent);
|
||||
transparent_outputs.push(TxOut {
|
||||
value: payment.amount,
|
||||
value: payment.amount(),
|
||||
script_pubkey: addr.script(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
return Err(InputSelectorError::Selection(
|
||||
GreedyInputSelectorError::UnsupportedAddress(Box::new(addr.clone())),
|
||||
GreedyInputSelectorError::UnsupportedAddress(
|
||||
payment.recipient_address().clone(),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,7 +72,10 @@ pub mod proto;
|
|||
pub mod scan;
|
||||
pub mod scanning;
|
||||
pub mod wallet;
|
||||
pub mod zip321;
|
||||
pub use zip321;
|
||||
|
||||
#[cfg(feature = "sync")]
|
||||
pub mod sync;
|
||||
|
||||
#[cfg(feature = "unstable-serialization")]
|
||||
pub mod serialization;
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,491 @@
|
|||
//! Implementation of the synchronization flow described in the crate root.
|
||||
//!
|
||||
//! This is currently a simple implementation that does not yet implement a few features:
|
||||
//!
|
||||
//! - Block batches are not downloaded in parallel with scanning.
|
||||
//! - Transactions are not enhanced once detected (that is, after an output is detected in
|
||||
//! a transaction, the full transaction is not downloaded and scanned).
|
||||
//! - There is no mechanism for notifying the caller of progress updates.
|
||||
//! - There is no mechanism for interrupting the synchronization flow, other than ending
|
||||
//! the process.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use futures_util::TryStreamExt;
|
||||
use shardtree::error::ShardTreeError;
|
||||
use subtle::ConditionallySelectable;
|
||||
use tonic::{
|
||||
body::BoxBody,
|
||||
client::GrpcService,
|
||||
codegen::{Body, Bytes, StdError},
|
||||
};
|
||||
use tracing::{debug, info};
|
||||
use zcash_primitives::{
|
||||
consensus::{BlockHeight, Parameters},
|
||||
merkle_tree::HashSer,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
data_api::{
|
||||
chain::{
|
||||
error::Error as ChainError, scan_cached_blocks, BlockCache, ChainState,
|
||||
CommitmentTreeRoot,
|
||||
},
|
||||
scanning::{ScanPriority, ScanRange},
|
||||
WalletCommitmentTrees, WalletRead, WalletWrite,
|
||||
},
|
||||
proto::service::{self, compact_tx_streamer_client::CompactTxStreamerClient, BlockId},
|
||||
scanning::ScanError,
|
||||
};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
use orchard::tree::MerkleHashOrchard;
|
||||
|
||||
/// Scans the chain until the wallet is up-to-date.
|
||||
pub async fn run<P, ChT, CaT, DbT>(
|
||||
client: &mut CompactTxStreamerClient<ChT>,
|
||||
params: &P,
|
||||
db_cache: &CaT,
|
||||
db_data: &mut DbT,
|
||||
batch_size: u32,
|
||||
) -> Result<(), Error<CaT::Error, <DbT as WalletRead>::Error, <DbT as WalletCommitmentTrees>::Error>>
|
||||
where
|
||||
P: Parameters + Send + 'static,
|
||||
ChT: GrpcService<BoxBody>,
|
||||
ChT::Error: Into<StdError>,
|
||||
ChT::ResponseBody: Body<Data = Bytes> + Send + 'static,
|
||||
<ChT::ResponseBody as Body>::Error: Into<StdError> + Send,
|
||||
CaT: BlockCache,
|
||||
CaT::Error: std::error::Error + Send + Sync + 'static,
|
||||
DbT: WalletWrite + WalletCommitmentTrees,
|
||||
DbT::AccountId: ConditionallySelectable + Default + Send + 'static,
|
||||
<DbT as WalletRead>::Error: std::error::Error + Send + Sync + 'static,
|
||||
<DbT as WalletCommitmentTrees>::Error: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
// 1) Download note commitment tree data from lightwalletd
|
||||
// 2) Pass the commitment tree data to the database.
|
||||
update_subtree_roots(client, db_data).await?;
|
||||
|
||||
while running(client, params, db_cache, db_data, batch_size).await? {}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn running<P, ChT, CaT, DbT, TrErr>(
|
||||
client: &mut CompactTxStreamerClient<ChT>,
|
||||
params: &P,
|
||||
db_cache: &CaT,
|
||||
db_data: &mut DbT,
|
||||
batch_size: u32,
|
||||
) -> Result<bool, Error<CaT::Error, <DbT as WalletRead>::Error, TrErr>>
|
||||
where
|
||||
P: Parameters + Send + 'static,
|
||||
ChT: GrpcService<BoxBody>,
|
||||
ChT::Error: Into<StdError>,
|
||||
ChT::ResponseBody: Body<Data = Bytes> + Send + 'static,
|
||||
<ChT::ResponseBody as Body>::Error: Into<StdError> + Send,
|
||||
CaT: BlockCache,
|
||||
CaT::Error: std::error::Error + Send + Sync + 'static,
|
||||
DbT: WalletWrite,
|
||||
DbT::AccountId: ConditionallySelectable + Default + Send + 'static,
|
||||
DbT::Error: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
// 3) Download chain tip metadata from lightwalletd
|
||||
// 4) Notify the wallet of the updated chain tip.
|
||||
update_chain_tip(client, db_data).await?;
|
||||
|
||||
// 5) Get the suggested scan ranges from the wallet database
|
||||
let mut scan_ranges = db_data.suggest_scan_ranges().map_err(Error::Wallet)?;
|
||||
|
||||
// Store the handles to cached block deletions (which we spawn into separate
|
||||
// tasks to allow us to continue downloading and scanning other ranges).
|
||||
let mut block_deletions = vec![];
|
||||
|
||||
// 6) Run the following loop until the wallet's view of the chain tip as of
|
||||
// the previous wallet session is valid.
|
||||
loop {
|
||||
// If there is a range of blocks that needs to be verified, it will always
|
||||
// be returned as the first element of the vector of suggested ranges.
|
||||
match scan_ranges.first() {
|
||||
Some(scan_range) if scan_range.priority() == ScanPriority::Verify => {
|
||||
// Download the blocks in `scan_range` into the block source,
|
||||
// overwriting any existing blocks in this range.
|
||||
download_blocks(client, db_cache, scan_range).await?;
|
||||
|
||||
let chain_state =
|
||||
download_chain_state(client, scan_range.block_range().start - 1).await?;
|
||||
|
||||
// Scan the downloaded blocks and check for scanning errors that
|
||||
// indicate the wallet's chain tip is out of sync with blockchain
|
||||
// history.
|
||||
let scan_ranges_updated =
|
||||
scan_blocks(params, db_cache, db_data, &chain_state, scan_range).await?;
|
||||
|
||||
// Delete the now-scanned blocks, because keeping the entire chain
|
||||
// in CompactBlock files on disk is horrendous for the filesystem.
|
||||
block_deletions.push(db_cache.delete(scan_range.clone()));
|
||||
|
||||
if scan_ranges_updated {
|
||||
// The suggested scan ranges have been updated, so we re-request.
|
||||
scan_ranges = db_data.suggest_scan_ranges().map_err(Error::Wallet)?;
|
||||
} else {
|
||||
// At this point, the cache and scanned data are locally
|
||||
// consistent (though not necessarily consistent with the
|
||||
// latest chain tip - this would be discovered the next time
|
||||
// this codepath is executed after new blocks are received) so
|
||||
// we can break out of the loop.
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Nothing to verify; break out of the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7) Loop over the remaining suggested scan ranges, retrieving the requested data
|
||||
// and calling `scan_cached_blocks` on each range.
|
||||
let scan_ranges = db_data.suggest_scan_ranges().map_err(Error::Wallet)?;
|
||||
debug!("Suggested ranges: {:?}", scan_ranges);
|
||||
for scan_range in scan_ranges.into_iter().flat_map(|r| {
|
||||
// Limit the number of blocks we download and scan at any one time.
|
||||
(0..).scan(r, |acc, _| {
|
||||
if acc.is_empty() {
|
||||
None
|
||||
} else if let Some((cur, next)) = acc.split_at(acc.block_range().start + batch_size) {
|
||||
*acc = next;
|
||||
Some(cur)
|
||||
} else {
|
||||
let cur = acc.clone();
|
||||
let end = acc.block_range().end;
|
||||
*acc = ScanRange::from_parts(end..end, acc.priority());
|
||||
Some(cur)
|
||||
}
|
||||
})
|
||||
}) {
|
||||
// Download the blocks in `scan_range` into the block source.
|
||||
download_blocks(client, db_cache, &scan_range).await?;
|
||||
|
||||
let chain_state = download_chain_state(client, scan_range.block_range().start - 1).await?;
|
||||
|
||||
// Scan the downloaded blocks.
|
||||
let scan_ranges_updated =
|
||||
scan_blocks(params, db_cache, db_data, &chain_state, &scan_range).await?;
|
||||
|
||||
// Delete the now-scanned blocks.
|
||||
block_deletions.push(db_cache.delete(scan_range));
|
||||
|
||||
if scan_ranges_updated {
|
||||
// The suggested scan ranges have been updated (either due to a continuity
|
||||
// error or because a higher priority range has been added).
|
||||
info!("Waiting for cached blocks to be deleted...");
|
||||
for deletion in block_deletions {
|
||||
deletion.await.map_err(Error::Cache)?;
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Waiting for cached blocks to be deleted...");
|
||||
for deletion in block_deletions {
|
||||
deletion.await.map_err(Error::Cache)?;
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn update_subtree_roots<ChT, DbT, CaErr, DbErr>(
|
||||
client: &mut CompactTxStreamerClient<ChT>,
|
||||
db_data: &mut DbT,
|
||||
) -> Result<(), Error<CaErr, DbErr, <DbT as WalletCommitmentTrees>::Error>>
|
||||
where
|
||||
ChT: GrpcService<BoxBody>,
|
||||
ChT::Error: Into<StdError>,
|
||||
ChT::ResponseBody: Body<Data = Bytes> + Send + 'static,
|
||||
<ChT::ResponseBody as Body>::Error: Into<StdError> + Send,
|
||||
DbT: WalletCommitmentTrees,
|
||||
<DbT as WalletCommitmentTrees>::Error: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
let mut request = service::GetSubtreeRootsArg::default();
|
||||
request.set_shielded_protocol(service::ShieldedProtocol::Sapling);
|
||||
// Hack to work around a bug in the initial lightwalletd implementation.
|
||||
request.max_entries = 65536;
|
||||
|
||||
let sapling_roots: Vec<CommitmentTreeRoot<sapling::Node>> = client
|
||||
.get_subtree_roots(request)
|
||||
.await?
|
||||
.into_inner()
|
||||
.and_then(|root| async move {
|
||||
let root_hash = sapling::Node::read(&root.root_hash[..])?;
|
||||
Ok(CommitmentTreeRoot::from_parts(
|
||||
BlockHeight::from_u32(root.completing_block_height as u32),
|
||||
root_hash,
|
||||
))
|
||||
})
|
||||
.try_collect()
|
||||
.await?;
|
||||
|
||||
info!("Sapling tree has {} subtrees", sapling_roots.len());
|
||||
db_data
|
||||
.put_sapling_subtree_roots(0, &sapling_roots)
|
||||
.map_err(Error::WalletTrees)?;
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
{
|
||||
let mut request = service::GetSubtreeRootsArg::default();
|
||||
request.set_shielded_protocol(service::ShieldedProtocol::Orchard);
|
||||
// Hack to work around a bug in the initial lightwalletd implementation.
|
||||
request.max_entries = 65536;
|
||||
let orchard_roots: Vec<CommitmentTreeRoot<MerkleHashOrchard>> = client
|
||||
.get_subtree_roots(request)
|
||||
.await?
|
||||
.into_inner()
|
||||
.and_then(|root| async move {
|
||||
let root_hash = MerkleHashOrchard::read(&root.root_hash[..])?;
|
||||
Ok(CommitmentTreeRoot::from_parts(
|
||||
BlockHeight::from_u32(root.completing_block_height as u32),
|
||||
root_hash,
|
||||
))
|
||||
})
|
||||
.try_collect()
|
||||
.await?;
|
||||
|
||||
info!("Orchard tree has {} subtrees", orchard_roots.len());
|
||||
db_data
|
||||
.put_orchard_subtree_roots(0, &orchard_roots)
|
||||
.map_err(Error::WalletTrees)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_chain_tip<ChT, DbT, CaErr, TrErr>(
|
||||
client: &mut CompactTxStreamerClient<ChT>,
|
||||
db_data: &mut DbT,
|
||||
) -> Result<(), Error<CaErr, <DbT as WalletRead>::Error, TrErr>>
|
||||
where
|
||||
ChT: GrpcService<BoxBody>,
|
||||
ChT::Error: Into<StdError>,
|
||||
ChT::ResponseBody: Body<Data = Bytes> + Send + 'static,
|
||||
<ChT::ResponseBody as Body>::Error: Into<StdError> + Send,
|
||||
DbT: WalletWrite,
|
||||
DbT::Error: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
let tip_height: BlockHeight = client
|
||||
.get_latest_block(service::ChainSpec::default())
|
||||
.await?
|
||||
.get_ref()
|
||||
.height
|
||||
.try_into()
|
||||
.map_err(|_| Error::MisbehavingServer)?;
|
||||
|
||||
info!("Latest block height is {}", tip_height);
|
||||
db_data
|
||||
.update_chain_tip(tip_height)
|
||||
.map_err(Error::Wallet)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_blocks<ChT, CaT, DbErr, TrErr>(
|
||||
client: &mut CompactTxStreamerClient<ChT>,
|
||||
db_cache: &CaT,
|
||||
scan_range: &ScanRange,
|
||||
) -> Result<(), Error<CaT::Error, DbErr, TrErr>>
|
||||
where
|
||||
ChT: GrpcService<BoxBody>,
|
||||
ChT::Error: Into<StdError>,
|
||||
ChT::ResponseBody: Body<Data = Bytes> + Send + 'static,
|
||||
<ChT::ResponseBody as Body>::Error: Into<StdError> + Send,
|
||||
CaT: BlockCache,
|
||||
CaT::Error: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
info!("Fetching {}", scan_range);
|
||||
let mut start = service::BlockId::default();
|
||||
start.height = scan_range.block_range().start.into();
|
||||
let mut end = service::BlockId::default();
|
||||
end.height = (scan_range.block_range().end - 1).into();
|
||||
let range = service::BlockRange {
|
||||
start: Some(start),
|
||||
end: Some(end),
|
||||
};
|
||||
let compact_blocks = client
|
||||
.get_block_range(range)
|
||||
.await?
|
||||
.into_inner()
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
|
||||
db_cache
|
||||
.insert(compact_blocks)
|
||||
.await
|
||||
.map_err(Error::Cache)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_chain_state<ChT, CaErr, DbErr, TrErr>(
|
||||
client: &mut CompactTxStreamerClient<ChT>,
|
||||
block_height: BlockHeight,
|
||||
) -> Result<ChainState, Error<CaErr, DbErr, TrErr>>
|
||||
where
|
||||
ChT: GrpcService<BoxBody>,
|
||||
ChT::Error: Into<StdError>,
|
||||
ChT::ResponseBody: Body<Data = Bytes> + Send + 'static,
|
||||
<ChT::ResponseBody as Body>::Error: Into<StdError> + Send,
|
||||
{
|
||||
let tree_state = client
|
||||
.get_tree_state(BlockId {
|
||||
height: block_height.into(),
|
||||
hash: vec![],
|
||||
})
|
||||
.await?;
|
||||
|
||||
tree_state
|
||||
.into_inner()
|
||||
.to_chain_state()
|
||||
.map_err(|_| Error::MisbehavingServer)
|
||||
}
|
||||
|
||||
/// Scans the given block range and checks for scanning errors that indicate the wallet's
|
||||
/// chain tip is out of sync with blockchain history.
|
||||
///
|
||||
/// Returns `true` if scanning these blocks materially changed the suggested scan ranges.
|
||||
async fn scan_blocks<P, CaT, DbT, TrErr>(
|
||||
params: &P,
|
||||
db_cache: &CaT,
|
||||
db_data: &mut DbT,
|
||||
initial_chain_state: &ChainState,
|
||||
scan_range: &ScanRange,
|
||||
) -> Result<bool, Error<CaT::Error, <DbT as WalletRead>::Error, TrErr>>
|
||||
where
|
||||
P: Parameters + Send + 'static,
|
||||
CaT: BlockCache,
|
||||
CaT::Error: std::error::Error + Send + Sync + 'static,
|
||||
DbT: WalletWrite,
|
||||
DbT::AccountId: ConditionallySelectable + Default + Send + 'static,
|
||||
DbT::Error: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
info!("Scanning {}", scan_range);
|
||||
let scan_result = scan_cached_blocks(
|
||||
params,
|
||||
db_cache,
|
||||
db_data,
|
||||
scan_range.block_range().start,
|
||||
initial_chain_state,
|
||||
scan_range.len(),
|
||||
);
|
||||
|
||||
match scan_result {
|
||||
Err(ChainError::Scan(err)) if err.is_continuity_error() => {
|
||||
// Pick a height to rewind to, which must be at least one block before the
|
||||
// height at which the error occurred, but may be an earlier height determined
|
||||
// based on heuristics such as the platform, available bandwidth, size of
|
||||
// recent CompactBlocks, etc.
|
||||
let rewind_height = err.at_height().saturating_sub(10);
|
||||
info!(
|
||||
"Chain reorg detected at {}, rewinding to {}",
|
||||
err.at_height(),
|
||||
rewind_height,
|
||||
);
|
||||
|
||||
// Rewind to the chosen height.
|
||||
db_data
|
||||
.truncate_to_height(rewind_height)
|
||||
.map_err(Error::Wallet)?;
|
||||
|
||||
// Delete cached blocks from rewind_height onwards.
|
||||
//
|
||||
// This does imply that assumed-valid blocks will be re-downloaded, but it is
|
||||
// also possible that in the intervening time, a chain reorg has occurred that
|
||||
// orphaned some of those blocks.
|
||||
db_cache
|
||||
.truncate(rewind_height)
|
||||
.await
|
||||
.map_err(Error::Cache)?;
|
||||
|
||||
// The database was truncated, invalidating prior suggested ranges.
|
||||
Ok(true)
|
||||
}
|
||||
Ok(_) => {
|
||||
// If scanning these blocks caused a suggested range to be added that has a
|
||||
// higher priority than the current range, invalidate the current ranges.
|
||||
let latest_ranges = db_data.suggest_scan_ranges().map_err(Error::Wallet)?;
|
||||
|
||||
Ok(if let Some(range) = latest_ranges.first() {
|
||||
range.priority() > scan_range.priority()
|
||||
} else {
|
||||
false
|
||||
})
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can occur while syncing.
|
||||
#[derive(Debug)]
|
||||
pub enum Error<CaErr, DbErr, TrErr> {
|
||||
/// An error while interacting with a [`BlockCache`].
|
||||
Cache(CaErr),
|
||||
/// The lightwalletd server returned invalid information, and is misbehaving.
|
||||
MisbehavingServer,
|
||||
/// An error while scanning blocks.
|
||||
Scan(ScanError),
|
||||
/// An error while communicating with the lightwalletd server.
|
||||
Server(tonic::Status),
|
||||
/// An error while interacting with a wallet database via [`WalletRead`] or
|
||||
/// [`WalletWrite`].
|
||||
Wallet(DbErr),
|
||||
/// An error while interacting with a wallet database via [`WalletCommitmentTrees`].
|
||||
WalletTrees(ShardTreeError<TrErr>),
|
||||
}
|
||||
|
||||
impl<CaErr, DbErr, TrErr> fmt::Display for Error<CaErr, DbErr, TrErr>
|
||||
where
|
||||
CaErr: fmt::Display,
|
||||
DbErr: fmt::Display,
|
||||
TrErr: fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::Cache(e) => write!(f, "Error while interacting with block cache: {}", e),
|
||||
Error::MisbehavingServer => write!(f, "lightwalletd server is misbehaving"),
|
||||
Error::Scan(e) => write!(f, "Error while scanning blocks: {}", e),
|
||||
Error::Server(e) => write!(
|
||||
f,
|
||||
"Error while communicating with lightwalletd server: {}",
|
||||
e
|
||||
),
|
||||
Error::Wallet(e) => write!(f, "Error while interacting with wallet database: {}", e),
|
||||
Error::WalletTrees(e) => write!(
|
||||
f,
|
||||
"Error while interacting with wallet commitment trees: {}",
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<CaErr, DbErr, TrErr> std::error::Error for Error<CaErr, DbErr, TrErr>
|
||||
where
|
||||
CaErr: std::error::Error,
|
||||
DbErr: std::error::Error,
|
||||
TrErr: std::error::Error,
|
||||
{
|
||||
}
|
||||
|
||||
impl<CaErr, DbErr, TrErr> From<ChainError<DbErr, CaErr>> for Error<CaErr, DbErr, TrErr> {
|
||||
fn from(e: ChainError<DbErr, CaErr>) -> Self {
|
||||
match e {
|
||||
ChainError::Wallet(e) => Error::Wallet(e),
|
||||
ChainError::BlockSource(e) => Error::Cache(e),
|
||||
ChainError::Scan(e) => Error::Scan(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<CaErr, DbErr, TrErr> From<tonic::Status> for Error<CaErr, DbErr, TrErr> {
|
||||
fn from(status: tonic::Status) -> Self {
|
||||
Error::Server(status)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -7,10 +7,19 @@ and this library adheres to Rust's notion of
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.10.3] - 2024-04-08
|
||||
|
||||
### Added
|
||||
- Added a migration to ensure that the default address for existing wallets is
|
||||
upgraded to include an Orchard receiver.
|
||||
|
||||
### Fixed
|
||||
- A bug in the SQL query for `WalletDb::get_account_birthday` was fixed.
|
||||
|
||||
## [0.10.2] - 2024-03-27
|
||||
|
||||
### Fixed
|
||||
- A bug in the SQL querey for `WalletDb::get_unspent_transparent_output` was fixed.
|
||||
- A bug in the SQL query for `WalletDb::get_unspent_transparent_output` was fixed.
|
||||
|
||||
## [0.10.1] - 2024-03-25
|
||||
|
||||
|
@ -71,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
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "zcash_client_sqlite"
|
||||
description = "An SQLite-based Zcash light client"
|
||||
version = "0.10.2"
|
||||
version = "0.10.3"
|
||||
authors = [
|
||||
"Jack Grigg <jack@z.cash>",
|
||||
"Kris Nuttycombe <kris@electriccoin.co>"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -65,7 +65,7 @@ use zcash_client_backend::{
|
|||
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
|
||||
DecryptedOutput, PoolType, ShieldedProtocol, TransferType,
|
||||
};
|
||||
use zcash_keys::address::Address;
|
||||
use zcash_keys::address::Receiver;
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus::{self, BlockHeight},
|
||||
|
@ -133,7 +133,7 @@ pub(crate) const UA_TRANSPARENT: bool = false;
|
|||
pub(crate) const UA_TRANSPARENT: bool = true;
|
||||
|
||||
pub(crate) const DEFAULT_UA_REQUEST: UnifiedAddressRequest =
|
||||
UnifiedAddressRequest::unsafe_new(UA_ORCHARD, true, UA_TRANSPARENT);
|
||||
UnifiedAddressRequest::unsafe_new_without_expiry(UA_ORCHARD, true, UA_TRANSPARENT);
|
||||
|
||||
/// The ID type for accounts.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
|
||||
|
@ -1063,11 +1063,22 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
|||
for output in d_tx.sapling_outputs() {
|
||||
match output.transfer_type() {
|
||||
TransferType::Outgoing => {
|
||||
//TODO: Recover the UA, if possible.
|
||||
let recipient = Recipient::Sapling(output.note().recipient());
|
||||
let recipient = {
|
||||
let receiver = Receiver::Sapling(output.note().recipient());
|
||||
let wallet_address = wallet::select_receiving_address(
|
||||
&wdb.params,
|
||||
wdb.conn.0,
|
||||
*output.account(),
|
||||
&receiver
|
||||
)?.unwrap_or_else(||
|
||||
receiver.to_zcash_address(wdb.params.network_type())
|
||||
);
|
||||
|
||||
Recipient::External(wallet_address, PoolType::Shielded(ShieldedProtocol::Sapling))
|
||||
};
|
||||
|
||||
wallet::put_sent_output(
|
||||
wdb.conn.0,
|
||||
&wdb.params,
|
||||
*output.account(),
|
||||
tx_ref,
|
||||
output.index(),
|
||||
|
@ -1087,7 +1098,6 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
|||
|
||||
wallet::put_sent_output(
|
||||
wdb.conn.0,
|
||||
&wdb.params,
|
||||
*output.account(),
|
||||
tx_ref,
|
||||
output.index(),
|
||||
|
@ -1102,14 +1112,22 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
|||
if let Some(account_id) = funding_account {
|
||||
let recipient = Recipient::InternalAccount {
|
||||
receiving_account: *output.account(),
|
||||
// TODO: recover the actual UA, if possible
|
||||
external_address: Some(Address::Sapling(output.note().recipient())),
|
||||
external_address: {
|
||||
let receiver = Receiver::Sapling(output.note().recipient());
|
||||
Some(wallet::select_receiving_address(
|
||||
&wdb.params,
|
||||
wdb.conn.0,
|
||||
*output.account(),
|
||||
&receiver
|
||||
)?.unwrap_or_else(||
|
||||
receiver.to_zcash_address(wdb.params.network_type())
|
||||
))
|
||||
},
|
||||
note: Note::Sapling(output.note().clone()),
|
||||
};
|
||||
|
||||
wallet::put_sent_output(
|
||||
wdb.conn.0,
|
||||
&wdb.params,
|
||||
account_id,
|
||||
tx_ref,
|
||||
output.index(),
|
||||
|
@ -1126,20 +1144,21 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
|||
for output in d_tx.orchard_outputs() {
|
||||
match output.transfer_type() {
|
||||
TransferType::Outgoing => {
|
||||
// TODO: Recover the actual UA, if possible.
|
||||
let recipient = Recipient::Unified(
|
||||
UnifiedAddress::from_receivers(
|
||||
Some(output.note().recipient()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("UA has an Orchard receiver by construction."),
|
||||
PoolType::Shielded(ShieldedProtocol::Orchard),
|
||||
);
|
||||
let recipient = {
|
||||
let receiver = Receiver::Orchard(output.note().recipient());
|
||||
let wallet_address = wallet::select_receiving_address(
|
||||
&wdb.params,
|
||||
wdb.conn.0,
|
||||
*output.account(),
|
||||
&receiver
|
||||
)?.unwrap_or_else(||
|
||||
receiver.to_zcash_address(wdb.params.network_type())
|
||||
);
|
||||
|
||||
Recipient::External(wallet_address, PoolType::Shielded(ShieldedProtocol::Orchard))
|
||||
};
|
||||
wallet::put_sent_output(
|
||||
wdb.conn.0,
|
||||
&wdb.params,
|
||||
*output.account(),
|
||||
tx_ref,
|
||||
output.index(),
|
||||
|
@ -1159,7 +1178,6 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
|||
|
||||
wallet::put_sent_output(
|
||||
wdb.conn.0,
|
||||
&wdb.params,
|
||||
*output.account(),
|
||||
tx_ref,
|
||||
output.index(),
|
||||
|
@ -1175,19 +1193,22 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
|||
// Even if the recipient address is external, record the send as internal.
|
||||
let recipient = Recipient::InternalAccount {
|
||||
receiving_account: *output.account(),
|
||||
// TODO: recover the actual UA, if possible
|
||||
external_address: Some(Address::Unified(
|
||||
UnifiedAddress::from_receivers(
|
||||
Some(output.note().recipient()),
|
||||
None,
|
||||
None,
|
||||
).expect("UA has an Orchard receiver by construction."))),
|
||||
external_address: {
|
||||
let receiver = Receiver::Orchard(output.note().recipient());
|
||||
Some(wallet::select_receiving_address(
|
||||
&wdb.params,
|
||||
wdb.conn.0,
|
||||
*output.account(),
|
||||
&receiver
|
||||
)?.unwrap_or_else(||
|
||||
receiver.to_zcash_address(wdb.params.network_type())
|
||||
))
|
||||
},
|
||||
note: Note::Orchard(*output.note()),
|
||||
};
|
||||
|
||||
wallet::put_sent_output(
|
||||
wdb.conn.0,
|
||||
&wdb.params,
|
||||
account_id,
|
||||
tx_ref,
|
||||
output.index(),
|
||||
|
@ -1240,13 +1261,29 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
|||
.enumerate()
|
||||
{
|
||||
if let Some(address) = txout.recipient_address() {
|
||||
let receiver = Receiver::Transparent(address);
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
let recipient_addr = wallet::select_receiving_address(
|
||||
&wdb.params,
|
||||
wdb.conn.0,
|
||||
account_id,
|
||||
&receiver
|
||||
)?.unwrap_or_else(||
|
||||
receiver.to_zcash_address(wdb.params.network_type())
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
let recipient_addr = receiver.to_zcash_address(wdb.params.network_type());
|
||||
|
||||
let recipient = Recipient::External(recipient_addr, PoolType::Transparent);
|
||||
|
||||
wallet::put_sent_output(
|
||||
wdb.conn.0,
|
||||
&wdb.params,
|
||||
account_id,
|
||||
tx_ref,
|
||||
output_index,
|
||||
&Recipient::Transparent(address),
|
||||
&recipient,
|
||||
txout.value,
|
||||
None,
|
||||
)?;
|
||||
|
@ -1305,13 +1342,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
|||
}
|
||||
|
||||
for output in sent_tx.outputs() {
|
||||
wallet::insert_sent_output(
|
||||
wdb.conn.0,
|
||||
&wdb.params,
|
||||
tx_ref,
|
||||
*sent_tx.account_id(),
|
||||
output,
|
||||
)?;
|
||||
wallet::insert_sent_output(wdb.conn.0, tx_ref, *sent_tx.account_id(), output)?;
|
||||
|
||||
match output.recipient() {
|
||||
Recipient::InternalAccount {
|
||||
|
@ -1880,7 +1911,6 @@ mod tests {
|
|||
.unwrap();
|
||||
assert!(current_addr.is_some());
|
||||
|
||||
// TODO: Add Orchard
|
||||
let addr2 = st
|
||||
.wallet_mut()
|
||||
.get_next_available_address(account.account_id(), DEFAULT_UA_REQUEST)
|
||||
|
|
|
@ -1682,7 +1682,7 @@ fn fake_compact_block_spending<P: consensus::Parameters, Fvk: TestFvk>(
|
|||
compact_sapling_output(
|
||||
params,
|
||||
height,
|
||||
recipient,
|
||||
*recipient,
|
||||
value,
|
||||
fvk.sapling_ovk(),
|
||||
&mut rng,
|
||||
|
@ -1896,7 +1896,7 @@ fn check_proposal_serialization_roundtrip(
|
|||
db_data: &WalletDb<rusqlite::Connection, LocalNetwork>,
|
||||
proposal: &Proposal<StandardFeeRule, ReceivedNoteId>,
|
||||
) {
|
||||
let proposal_proto = proposal::Proposal::from_standard_proposal(&db_data.params, proposal);
|
||||
let deserialized_proposal = proposal_proto.try_into_standard_proposal(&db_data.params, db_data);
|
||||
let proposal_proto = proposal::Proposal::from_standard_proposal(proposal);
|
||||
let deserialized_proposal = proposal_proto.try_into_standard_proposal(db_data);
|
||||
assert_matches!(deserialized_proposal, Ok(r) if &r == proposal);
|
||||
}
|
||||
|
|
|
@ -168,14 +168,10 @@ pub(crate) fn send_single_step_proposed_transfer<T: ShieldedPoolTester>() {
|
|||
|
||||
let to_extsk = T::sk(&[0xf5; 32]);
|
||||
let to: Address = T::sk_default_address(&to_extsk);
|
||||
let request = zip321::TransactionRequest::new(vec![Payment {
|
||||
recipient_address: to,
|
||||
amount: NonNegativeAmount::const_from_u64(10000),
|
||||
memo: None, // this should result in the creation of an empty memo
|
||||
label: None,
|
||||
message: None,
|
||||
other_params: vec![],
|
||||
}])
|
||||
let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
|
||||
to.to_zcash_address(&st.network()),
|
||||
NonNegativeAmount::const_from_u64(10000),
|
||||
)])
|
||||
.unwrap();
|
||||
|
||||
// TODO: This test was originally written to use the pre-zip-313 fee rule
|
||||
|
@ -336,15 +332,11 @@ pub(crate) fn send_multi_step_proposed_transfer<T: ShieldedPoolTester>() {
|
|||
// spends the first step's output.
|
||||
|
||||
// The first step will deshield to the wallet's default transparent address
|
||||
let to0 = Address::Transparent(account.usk().default_transparent_address().0);
|
||||
let request0 = zip321::TransactionRequest::new(vec![Payment {
|
||||
recipient_address: to0,
|
||||
amount: NonNegativeAmount::const_from_u64(50000),
|
||||
memo: None,
|
||||
label: None,
|
||||
message: None,
|
||||
other_params: vec![],
|
||||
}])
|
||||
let to0 = Address::from(account.usk().default_transparent_address().0);
|
||||
let request0 = zip321::TransactionRequest::new(vec![Payment::without_memo(
|
||||
to0.to_zcash_address(&st.network()),
|
||||
NonNegativeAmount::const_from_u64(50000),
|
||||
)])
|
||||
.unwrap();
|
||||
|
||||
let fee_rule = StandardFeeRule::Zip317;
|
||||
|
@ -372,7 +364,7 @@ pub(crate) fn send_multi_step_proposed_transfer<T: ShieldedPoolTester>() {
|
|||
|
||||
// We'll use an internal transparent address that hasn't been added to the wallet
|
||||
// to simulate an external transparent recipient.
|
||||
let to1 = Address::Transparent(
|
||||
let to1 = Address::from(
|
||||
account
|
||||
.usk()
|
||||
.transparent()
|
||||
|
@ -382,14 +374,10 @@ pub(crate) fn send_multi_step_proposed_transfer<T: ShieldedPoolTester>() {
|
|||
.default_address()
|
||||
.0,
|
||||
);
|
||||
let request1 = zip321::TransactionRequest::new(vec![Payment {
|
||||
recipient_address: to1,
|
||||
amount: NonNegativeAmount::const_from_u64(40000),
|
||||
memo: None,
|
||||
label: None,
|
||||
message: None,
|
||||
other_params: vec![],
|
||||
}])
|
||||
let request1 = zip321::TransactionRequest::new(vec![Payment::without_memo(
|
||||
to1.to_zcash_address(&st.network()),
|
||||
NonNegativeAmount::const_from_u64(40000),
|
||||
)])
|
||||
.unwrap();
|
||||
|
||||
let step1 = Step::from_parts(
|
||||
|
@ -1042,23 +1030,9 @@ pub(crate) fn external_address_change_spends_detected_in_restore_from_seed<
|
|||
let addr2 = T::fvk_default_address(&dfvk2);
|
||||
let req = TransactionRequest::new(vec![
|
||||
// payment to an external recipient
|
||||
Payment {
|
||||
recipient_address: addr2,
|
||||
amount: amount_sent,
|
||||
memo: None,
|
||||
label: None,
|
||||
message: None,
|
||||
other_params: vec![],
|
||||
},
|
||||
Payment::without_memo(addr2.to_zcash_address(&st.network()), amount_sent),
|
||||
// payment back to the originating wallet, simulating legacy change
|
||||
Payment {
|
||||
recipient_address: addr,
|
||||
amount: amount_legacy_change,
|
||||
memo: None,
|
||||
label: None,
|
||||
message: None,
|
||||
other_params: vec![],
|
||||
},
|
||||
Payment::without_memo(addr.to_zcash_address(&st.network()), amount_legacy_change),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
|
@ -1151,14 +1125,10 @@ pub(crate) fn zip317_spend<T: ShieldedPoolTester>() {
|
|||
let input_selector = input_selector(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
|
||||
|
||||
// This first request will fail due to insufficient non-dust funds
|
||||
let req = TransactionRequest::new(vec![Payment {
|
||||
recipient_address: T::fvk_default_address(&dfvk),
|
||||
amount: NonNegativeAmount::const_from_u64(50000),
|
||||
memo: None,
|
||||
label: None,
|
||||
message: None,
|
||||
other_params: vec![],
|
||||
}])
|
||||
let req = TransactionRequest::new(vec![Payment::without_memo(
|
||||
T::fvk_default_address(&dfvk).to_zcash_address(&st.network()),
|
||||
NonNegativeAmount::const_from_u64(50000),
|
||||
)])
|
||||
.unwrap();
|
||||
|
||||
assert_matches!(
|
||||
|
@ -1176,14 +1146,10 @@ pub(crate) fn zip317_spend<T: ShieldedPoolTester>() {
|
|||
|
||||
// This request will succeed, spending a single dust input to pay the 10000
|
||||
// ZAT fee in addition to the 41000 ZAT output to the recipient
|
||||
let req = TransactionRequest::new(vec![Payment {
|
||||
recipient_address: T::fvk_default_address(&dfvk),
|
||||
amount: NonNegativeAmount::const_from_u64(41000),
|
||||
memo: None,
|
||||
label: None,
|
||||
message: None,
|
||||
other_params: vec![],
|
||||
}])
|
||||
let req = TransactionRequest::new(vec![Payment::without_memo(
|
||||
T::fvk_default_address(&dfvk).to_zcash_address(&st.network()),
|
||||
NonNegativeAmount::const_from_u64(41000),
|
||||
)])
|
||||
.unwrap();
|
||||
|
||||
let txid = st
|
||||
|
@ -1479,14 +1445,10 @@ pub(crate) fn pool_crossing_required<P0: ShieldedPoolTester, P1: ShieldedPoolTes
|
|||
);
|
||||
|
||||
let transfer_amount = NonNegativeAmount::const_from_u64(200000);
|
||||
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment {
|
||||
recipient_address: p1_to,
|
||||
amount: transfer_amount,
|
||||
memo: None,
|
||||
label: None,
|
||||
message: None,
|
||||
other_params: vec![],
|
||||
}])
|
||||
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment::without_memo(
|
||||
p1_to.to_zcash_address(&st.network()),
|
||||
transfer_amount,
|
||||
)])
|
||||
.unwrap();
|
||||
|
||||
let fee_rule = StandardFeeRule::Zip317;
|
||||
|
@ -1570,14 +1532,10 @@ pub(crate) fn fully_funded_fully_private<P0: ShieldedPoolTester, P1: ShieldedPoo
|
|||
);
|
||||
|
||||
let transfer_amount = NonNegativeAmount::const_from_u64(200000);
|
||||
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment {
|
||||
recipient_address: p1_to,
|
||||
amount: transfer_amount,
|
||||
memo: None,
|
||||
label: None,
|
||||
message: None,
|
||||
other_params: vec![],
|
||||
}])
|
||||
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment::without_memo(
|
||||
p1_to.to_zcash_address(&st.network()),
|
||||
transfer_amount,
|
||||
)])
|
||||
.unwrap();
|
||||
|
||||
let fee_rule = StandardFeeRule::Zip317;
|
||||
|
@ -1661,14 +1619,10 @@ pub(crate) fn fully_funded_send_to_t<P0: ShieldedPoolTester, P1: ShieldedPoolTes
|
|||
);
|
||||
|
||||
let transfer_amount = NonNegativeAmount::const_from_u64(200000);
|
||||
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment {
|
||||
recipient_address: Address::Transparent(p1_to),
|
||||
amount: transfer_amount,
|
||||
memo: None,
|
||||
label: None,
|
||||
message: None,
|
||||
other_params: vec![],
|
||||
}])
|
||||
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment::without_memo(
|
||||
Address::Transparent(Box::new(p1_to)).to_zcash_address(&st.network()),
|
||||
transfer_amount,
|
||||
)])
|
||||
.unwrap();
|
||||
|
||||
let fee_rule = StandardFeeRule::Zip317;
|
||||
|
@ -1777,7 +1731,7 @@ pub(crate) fn multi_pool_checkpoint<P0: ShieldedPoolTester, P1: ShieldedPoolTest
|
|||
// First, send funds just to P0
|
||||
let transfer_amount = NonNegativeAmount::const_from_u64(200000);
|
||||
let p0_transfer = zip321::TransactionRequest::new(vec![Payment::without_memo(
|
||||
P0::random_address(&mut st.rng),
|
||||
P0::random_address(&mut st.rng).to_zcash_address(&st.network()),
|
||||
transfer_amount,
|
||||
)])
|
||||
.unwrap();
|
||||
|
@ -1802,8 +1756,14 @@ pub(crate) fn multi_pool_checkpoint<P0: ShieldedPoolTester, P1: ShieldedPoolTest
|
|||
|
||||
// In the next block, send funds to both P0 and P1
|
||||
let both_transfer = zip321::TransactionRequest::new(vec![
|
||||
Payment::without_memo(P0::random_address(&mut st.rng), transfer_amount),
|
||||
Payment::without_memo(P1::random_address(&mut st.rng), transfer_amount),
|
||||
Payment::without_memo(
|
||||
P0::random_address(&mut st.rng).to_zcash_address(&st.network()),
|
||||
transfer_amount,
|
||||
),
|
||||
Payment::without_memo(
|
||||
P1::random_address(&mut st.rng).to_zcash_address(&st.network()),
|
||||
transfer_amount,
|
||||
),
|
||||
])
|
||||
.unwrap();
|
||||
let res = st
|
||||
|
@ -2109,14 +2069,10 @@ pub(crate) fn scan_cached_blocks_allows_blocks_out_of_order<T: ShieldedPoolTeste
|
|||
);
|
||||
|
||||
// We can spend the received notes
|
||||
let req = TransactionRequest::new(vec![Payment {
|
||||
recipient_address: T::fvk_default_address(&dfvk),
|
||||
amount: NonNegativeAmount::const_from_u64(110_000),
|
||||
memo: None,
|
||||
label: None,
|
||||
message: None,
|
||||
other_params: vec![],
|
||||
}])
|
||||
let req = TransactionRequest::new(vec![Payment::without_memo(
|
||||
T::fvk_default_address(&dfvk).to_zcash_address(&st.network()),
|
||||
NonNegativeAmount::const_from_u64(110_000),
|
||||
)])
|
||||
.unwrap();
|
||||
|
||||
#[allow(deprecated)]
|
||||
|
|
|
@ -76,12 +76,9 @@ use std::io::{self, Cursor};
|
|||
use std::num::NonZeroU32;
|
||||
use std::ops::RangeInclusive;
|
||||
use tracing::debug;
|
||||
use zcash_keys::keys::{
|
||||
AddressGenerationError, UnifiedAddressRequest, UnifiedIncomingViewingKey, UnifiedSpendingKey,
|
||||
};
|
||||
|
||||
use zcash_address::ZcashAddress;
|
||||
use zcash_client_backend::{
|
||||
address::{Address, UnifiedAddress},
|
||||
data_api::{
|
||||
scanning::{ScanPriority, ScanRange},
|
||||
AccountBalance, AccountBirthday, AccountSource, BlockMetadata, Ratio,
|
||||
|
@ -92,6 +89,13 @@ use zcash_client_backend::{
|
|||
wallet::{Note, NoteId, Recipient, WalletTx},
|
||||
PoolType, ShieldedProtocol,
|
||||
};
|
||||
use zcash_keys::{
|
||||
address::{Address, Receiver, UnifiedAddress},
|
||||
keys::{
|
||||
AddressGenerationError, UnifiedAddressRequest, UnifiedIncomingViewingKey,
|
||||
UnifiedSpendingKey,
|
||||
},
|
||||
};
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters},
|
||||
|
@ -101,8 +105,8 @@ use zcash_primitives::{
|
|||
components::{amount::NonNegativeAmount, Amount},
|
||||
Transaction, TransactionData, TxId,
|
||||
},
|
||||
zip32::{self, DiversifierIndex, Scope},
|
||||
};
|
||||
use zip32::{self, DiversifierIndex, Scope};
|
||||
|
||||
use crate::{
|
||||
error::SqliteClientError,
|
||||
|
@ -551,7 +555,7 @@ pub(crate) fn get_current_address<P: consensus::Parameters>(
|
|||
SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned())
|
||||
})
|
||||
.and_then(|addr| match addr {
|
||||
Address::Unified(ua) => Ok(ua),
|
||||
Address::Unified(ua) => Ok(*ua),
|
||||
_ => Err(SqliteClientError::CorruptedData(format!(
|
||||
"Addresses table contains {} which is not a unified address",
|
||||
addr_str,
|
||||
|
@ -679,7 +683,7 @@ pub(crate) fn get_legacy_transparent_address<P: consensus::Parameters>(
|
|||
conn: &rusqlite::Connection,
|
||||
account_id: AccountId,
|
||||
) -> Result<Option<(TransparentAddress, NonHardenedChildIndex)>, SqliteClientError> {
|
||||
use zcash_address::unified::Container;
|
||||
use zcash_address::unified::{Container, Item};
|
||||
use zcash_primitives::legacy::keys::ExternalIvk;
|
||||
|
||||
// Get the UIVK for the account.
|
||||
|
@ -701,9 +705,9 @@ pub(crate) fn get_legacy_transparent_address<P: consensus::Parameters>(
|
|||
}
|
||||
|
||||
// Derive the default transparent address (if it wasn't already part of a derived UA).
|
||||
for item in uivk.items() {
|
||||
if let Ivk::P2pkh(tivk_bytes) = item {
|
||||
let tivk = ExternalIvk::deserialize(&tivk_bytes)?;
|
||||
for item in uivk.items_as_parsed() {
|
||||
if let Item::Data(Ivk::P2pkh(tivk_bytes)) = item {
|
||||
let tivk = ExternalIvk::deserialize(tivk_bytes)?;
|
||||
return Ok(Some(tivk.default_address()));
|
||||
}
|
||||
}
|
||||
|
@ -1591,7 +1595,7 @@ pub(crate) fn account_birthday(
|
|||
conn.query_row(
|
||||
"SELECT birthday_height
|
||||
FROM accounts
|
||||
WHERE account = :account_id",
|
||||
WHERE id = :account_id",
|
||||
named_params![":account_id": account.0],
|
||||
|row| row.get::<_, u32>(0).map(BlockHeight::from),
|
||||
)
|
||||
|
@ -2366,6 +2370,48 @@ pub(crate) fn put_tx_meta(
|
|||
.map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
/// Returns the most likely wallet address that corresponds to the protocol-level receiver of a
|
||||
/// note or UTXO.
|
||||
pub(crate) fn select_receiving_address<P: consensus::Parameters>(
|
||||
_params: &P,
|
||||
conn: &rusqlite::Connection,
|
||||
account: AccountId,
|
||||
receiver: &Receiver,
|
||||
) -> Result<Option<ZcashAddress>, SqliteClientError> {
|
||||
match receiver {
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
Receiver::Transparent(taddr) => conn
|
||||
.query_row(
|
||||
"SELECT address
|
||||
FROM addresses
|
||||
WHERE cached_transparent_receiver_address = :taddr",
|
||||
named_params! {
|
||||
":taddr": Address::Transparent(Box::new(*taddr)).encode(_params)
|
||||
},
|
||||
|row| row.get::<_, String>(0),
|
||||
)
|
||||
.optional()?
|
||||
.map(|addr_str| addr_str.parse::<ZcashAddress>())
|
||||
.transpose()
|
||||
.map_err(SqliteClientError::from),
|
||||
receiver => {
|
||||
let mut stmt =
|
||||
conn.prepare_cached("SELECT address FROM addresses WHERE account_id = :account")?;
|
||||
|
||||
let mut result = stmt.query(named_params! { ":account": account.0 })?;
|
||||
while let Some(row) = result.next()? {
|
||||
let addr_str = row.get::<_, String>(0)?;
|
||||
let decoded = addr_str.parse::<ZcashAddress>()?;
|
||||
if receiver.corresponds(&decoded) {
|
||||
return Ok(Some(decoded));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts full transaction data into the database.
|
||||
pub(crate) fn put_tx_data(
|
||||
conn: &rusqlite::Connection,
|
||||
|
@ -2515,24 +2561,17 @@ pub(crate) fn put_legacy_transparent_utxo<P: consensus::Parameters>(
|
|||
|
||||
// A utility function for creation of parameters for use in `insert_sent_output`
|
||||
// and `put_sent_output`
|
||||
fn recipient_params<P: consensus::Parameters>(
|
||||
params: &P,
|
||||
fn recipient_params(
|
||||
to: &Recipient<AccountId, Note>,
|
||||
) -> (Option<String>, Option<AccountId>, PoolType) {
|
||||
match to {
|
||||
Recipient::Transparent(addr) => (Some(addr.encode(params)), None, PoolType::Transparent),
|
||||
Recipient::Sapling(addr) => (
|
||||
Some(addr.encode(params)),
|
||||
None,
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling),
|
||||
),
|
||||
Recipient::Unified(addr, pool) => (Some(addr.encode(params)), None, *pool),
|
||||
Recipient::External(addr, pool) => (Some(addr.encode()), None, *pool),
|
||||
Recipient::InternalAccount {
|
||||
receiving_account,
|
||||
external_address,
|
||||
note,
|
||||
} => (
|
||||
external_address.as_ref().map(|a| a.encode(params)),
|
||||
external_address.as_ref().map(|a| a.encode()),
|
||||
Some(*receiving_account),
|
||||
PoolType::Shielded(note.protocol()),
|
||||
),
|
||||
|
@ -2540,9 +2579,8 @@ fn recipient_params<P: consensus::Parameters>(
|
|||
}
|
||||
|
||||
/// Records information about a transaction output that your wallet created.
|
||||
pub(crate) fn insert_sent_output<P: consensus::Parameters>(
|
||||
pub(crate) fn insert_sent_output(
|
||||
conn: &rusqlite::Connection,
|
||||
params: &P,
|
||||
tx_ref: i64,
|
||||
from_account: AccountId,
|
||||
output: &SentTransactionOutput<AccountId>,
|
||||
|
@ -2556,7 +2594,7 @@ pub(crate) fn insert_sent_output<P: consensus::Parameters>(
|
|||
:to_address, :to_account_id, :value, :memo)",
|
||||
)?;
|
||||
|
||||
let (to_address, to_account_id, pool_type) = recipient_params(params, output.recipient());
|
||||
let (to_address, to_account_id, pool_type) = recipient_params(output.recipient());
|
||||
let sql_args = named_params![
|
||||
":tx": &tx_ref,
|
||||
":output_pool": &pool_code(pool_type),
|
||||
|
@ -2585,9 +2623,8 @@ pub(crate) fn insert_sent_output<P: consensus::Parameters>(
|
|||
/// - If `recipient` is an internal account, `output_index` is an index into the Sapling outputs of
|
||||
/// the transaction.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn put_sent_output<P: consensus::Parameters>(
|
||||
pub(crate) fn put_sent_output(
|
||||
conn: &rusqlite::Connection,
|
||||
params: &P,
|
||||
from_account: AccountId,
|
||||
tx_ref: i64,
|
||||
output_index: usize,
|
||||
|
@ -2610,7 +2647,7 @@ pub(crate) fn put_sent_output<P: consensus::Parameters>(
|
|||
memo = IFNULL(:memo, memo)",
|
||||
)?;
|
||||
|
||||
let (to_address, to_account_id, pool_type) = recipient_params(params, recipient);
|
||||
let (to_address, to_account_id, pool_type) = recipient_params(recipient);
|
||||
let sql_args = named_params![
|
||||
":tx": &tx_ref,
|
||||
":output_pool": &pool_code(pool_type),
|
||||
|
@ -2805,6 +2842,8 @@ mod tests {
|
|||
AccountId,
|
||||
};
|
||||
|
||||
use super::account_birthday;
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use {
|
||||
crate::PRUNING_DEPTH,
|
||||
|
@ -3201,4 +3240,18 @@ mod tests {
|
|||
// ranges have been connected.
|
||||
assert_eq!(block_fully_scanned(&st), Some(end_height));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_account_birthday() {
|
||||
let st = TestBuilder::new()
|
||||
.with_block_cache()
|
||||
.with_account_from_sapling_activation(BlockHash([0; 32]))
|
||||
.build();
|
||||
|
||||
let account_id = st.test_account().unwrap().account_id();
|
||||
assert_matches!(
|
||||
account_birthday(&st.wallet().conn, account_id),
|
||||
Ok(birthday) if birthday == st.sapling_activation_height()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -134,11 +134,10 @@ fn sqlite_client_error_to_wallet_migration_error(e: SqliteClientError) -> Wallet
|
|||
SqliteClientError::InvalidNote => {
|
||||
WalletMigrationError::CorruptedData("invalid note".into())
|
||||
}
|
||||
SqliteClientError::Bech32DecodeError(e) => {
|
||||
WalletMigrationError::CorruptedData(e.to_string())
|
||||
}
|
||||
SqliteClientError::DecodingError(e) => WalletMigrationError::CorruptedData(e.to_string()),
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
SqliteClientError::HdwalletError(e) => WalletMigrationError::CorruptedData(e.to_string()),
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
SqliteClientError::TransparentAddress(e) => {
|
||||
WalletMigrationError::CorruptedData(e.to_string())
|
||||
}
|
||||
|
@ -1419,8 +1418,9 @@ mod tests {
|
|||
|
||||
// Unified addresses at the time of the addition of migrations did not contain an
|
||||
// Orchard component.
|
||||
let ua_request = UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT);
|
||||
let address_str = Address::Unified(
|
||||
let ua_request =
|
||||
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT);
|
||||
let address_str = Address::from(
|
||||
ufvk.default_address(ua_request)
|
||||
.expect("A valid default address exists for the UFVK")
|
||||
.0,
|
||||
|
@ -1439,7 +1439,7 @@ mod tests {
|
|||
// add a transparent "sent note"
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
{
|
||||
let taddr = Address::Transparent(
|
||||
let taddr = Address::from(
|
||||
*ufvk
|
||||
.default_address(ua_request)
|
||||
.expect("A valid default address exists for the UFVK")
|
||||
|
@ -1546,7 +1546,8 @@ mod tests {
|
|||
assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork));
|
||||
|
||||
// hardcoded with knowledge of what's coming next
|
||||
let ua_request = UnifiedAddressRequest::unsafe_new(false, true, true);
|
||||
let ua_request =
|
||||
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, true);
|
||||
db_data
|
||||
.get_next_available_address(account_id, ua_request)
|
||||
.unwrap()
|
||||
|
|
|
@ -443,12 +443,12 @@ mod tests {
|
|||
let usk = UnifiedSpendingKey::from_seed(&network, &[0u8; 32][..], AccountId::ZERO).unwrap();
|
||||
let ufvk = usk.to_unified_full_viewing_key();
|
||||
let (ua, _) = ufvk
|
||||
.default_address(UnifiedAddressRequest::unsafe_new(
|
||||
.default_address(UnifiedAddressRequest::unsafe_new_without_expiry(
|
||||
false,
|
||||
true,
|
||||
UA_TRANSPARENT,
|
||||
))
|
||||
.expect("A valid default address exists for the UFVK");
|
||||
.unwrap();
|
||||
let taddr = ufvk
|
||||
.transparent()
|
||||
.and_then(|k| {
|
||||
|
|
|
@ -79,20 +79,20 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
|||
))
|
||||
})?;
|
||||
let decoded_address = if let Address::Unified(ua) = decoded {
|
||||
ua
|
||||
*ua
|
||||
} else {
|
||||
return Err(WalletMigrationError::CorruptedData(
|
||||
"Address in accounts table was not a Unified Address.".to_string(),
|
||||
));
|
||||
};
|
||||
let (expected_address, idx) = ufvk.default_address(
|
||||
UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT),
|
||||
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT),
|
||||
)?;
|
||||
if decoded_address != expected_address {
|
||||
return Err(WalletMigrationError::CorruptedData(format!(
|
||||
"Decoded UA {} does not match the UFVK's default address {} at {:?}.",
|
||||
address,
|
||||
Address::Unified(expected_address).encode(&self.params),
|
||||
Address::from(expected_address).encode(&self.params),
|
||||
idx,
|
||||
)));
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
|||
let decoded_transparent_address = if let Address::Transparent(addr) =
|
||||
decoded_transparent
|
||||
{
|
||||
addr
|
||||
*addr
|
||||
} else {
|
||||
return Err(WalletMigrationError::CorruptedData(
|
||||
"Address in transparent_address column of accounts table was not a transparent address.".to_string(),
|
||||
|
@ -157,11 +157,9 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
|||
],
|
||||
)?;
|
||||
|
||||
let (address, d_idx) = ufvk.default_address(UnifiedAddressRequest::unsafe_new(
|
||||
false,
|
||||
true,
|
||||
UA_TRANSPARENT,
|
||||
))?;
|
||||
let (address, d_idx) = ufvk.default_address(
|
||||
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT),
|
||||
)?;
|
||||
insert_address(transaction, &self.params, account, d_idx, &address)?;
|
||||
}
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
|||
};
|
||||
|
||||
let (default_addr, diversifier_index) = uivk.default_address(
|
||||
UnifiedAddressRequest::unsafe_new(UA_ORCHARD, true, UA_TRANSPARENT),
|
||||
UnifiedAddressRequest::unsafe_new_without_expiry(UA_ORCHARD, true, UA_TRANSPARENT),
|
||||
)?;
|
||||
|
||||
let mut di_be = *diversifier_index.as_bytes();
|
||||
|
@ -144,7 +144,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
let (addr, diversifier_index) = ufvk
|
||||
.default_address(UnifiedAddressRequest::unsafe_new(
|
||||
.default_address(UnifiedAddressRequest::unsafe_new_without_expiry(
|
||||
false,
|
||||
true,
|
||||
UA_TRANSPARENT,
|
||||
|
|
|
@ -83,7 +83,8 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
|||
// our second assumption above, and we report this as corrupted data.
|
||||
let mut seed_is_relevant = false;
|
||||
|
||||
let ua_request = UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT);
|
||||
let ua_request =
|
||||
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT);
|
||||
let mut rows = stmt_fetch_accounts.query([])?;
|
||||
while let Some(row) = rows.next()? {
|
||||
// We only need to check for the presence of the seed if we have keys that
|
||||
|
@ -119,12 +120,12 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
|||
let dfvk = ufvk.sapling().ok_or_else(||
|
||||
WalletMigrationError::CorruptedData("Derivation should have produced a UFVK containing a Sapling component.".to_owned()))?;
|
||||
let (idx, expected_address) = dfvk.default_address();
|
||||
if decoded_address != expected_address {
|
||||
if *decoded_address != expected_address {
|
||||
return Err(if seed_is_relevant {
|
||||
WalletMigrationError::CorruptedData(
|
||||
format!("Decoded Sapling address {} does not match the ufvk's Sapling address {} at {:?}.",
|
||||
address,
|
||||
Address::Sapling(expected_address).encode(&self.params),
|
||||
Address::from(expected_address).encode(&self.params),
|
||||
idx))
|
||||
} else {
|
||||
WalletMigrationError::SeedNotRelevant
|
||||
|
@ -137,12 +138,12 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
|||
}
|
||||
Address::Unified(decoded_address) => {
|
||||
let (expected_address, idx) = ufvk.default_address(ua_request)?;
|
||||
if decoded_address != expected_address {
|
||||
if *decoded_address != expected_address {
|
||||
return Err(if seed_is_relevant {
|
||||
WalletMigrationError::CorruptedData(
|
||||
format!("Decoded unified address {} does not match the ufvk's default address {} at {:?}.",
|
||||
address,
|
||||
Address::Unified(expected_address).encode(&self.params),
|
||||
Address::from(expected_address).encode(&self.params),
|
||||
idx))
|
||||
} else {
|
||||
WalletMigrationError::SeedNotRelevant
|
||||
|
|
|
@ -463,7 +463,6 @@ pub(crate) mod tests {
|
|||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
.into()
|
||||
}
|
||||
|
||||
|
@ -545,9 +544,7 @@ pub(crate) mod tests {
|
|||
return Ok(result.map(|(note, addr, memo)| {
|
||||
(
|
||||
Note::Orchard(note),
|
||||
UnifiedAddress::from_receivers(Some(addr), None, None)
|
||||
.unwrap()
|
||||
.into(),
|
||||
UnifiedAddress::from_receivers(Some(addr), None, None).into(),
|
||||
MemoBytes::from_bytes(&memo).expect("correct length"),
|
||||
)
|
||||
}));
|
||||
|
|
|
@ -5,11 +5,19 @@ and this library adheres to Rust's notion of
|
|||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- `zcash_keys::address::Address::try_from_zcash_address`
|
||||
- `zcash_keys::address::Receiver`
|
||||
|
||||
## [0.2.0] - 2024-03-25
|
||||
|
||||
### Added
|
||||
- `zcash_keys::address::Address::has_receiver`
|
||||
- `zcash_keys::address`:
|
||||
- `Address::has_receiver`
|
||||
- `UnifiedAddress::{
|
||||
new, expiry_height, expiry_time,
|
||||
unknown_data, unknown_metadata
|
||||
}`
|
||||
- `impl Display for zcash_keys::keys::AddressGenerationError`
|
||||
- `impl std::error::Error for zcash_keys::keys::AddressGenerationError`
|
||||
- `impl From<hdwallet::error::Error> for zcash_keys::keys::DerivationError`
|
||||
|
@ -28,10 +36,15 @@ and this library adheres to Rust's notion of
|
|||
must be enabled for the `keys` module to be accessible.
|
||||
- Updated to `zcash_primitives-0.15.0`
|
||||
|
||||
### Changed
|
||||
- `zcash_keys::address::Address` variants now `Box` their contents to
|
||||
avoid large discrepancies in enum variant sizing.
|
||||
|
||||
### Removed
|
||||
- `UnifiedFullViewingKey::new` has been placed behind the `test-dependencies`
|
||||
feature flag. UFVKs should only be produced by derivation from the USK, or
|
||||
parsed from their string representation.
|
||||
- `zcash_keys::address::UnifiedAddress::from_receivers`
|
||||
|
||||
### Fixed
|
||||
- `UnifiedFullViewingKey::find_address` can now find an address for a diversifier
|
||||
|
@ -65,28 +78,48 @@ The entries below are relative to the `zcash_client_backend` crate as of
|
|||
- `UnifiedAddressRequest`
|
||||
- A new `orchard` feature flag has been added to make it possible to
|
||||
build client code without `orchard` dependendencies.
|
||||
- `zcash_keys::address::Address::to_zcash_address`
|
||||
- A new `sapling` feature flag has been added to make it possible to
|
||||
build client code without `sapling` dependendencies.
|
||||
- A new `transparent-inputs` feature flag has been added to make it possible to
|
||||
build client code without providing support for generating transparent
|
||||
addresses.
|
||||
|
||||
### Changed
|
||||
- The following methods and enum variants have been placed behind an `orchard`
|
||||
feature flag:
|
||||
- The following methods, method arguments, and enum variants have been placed
|
||||
behind the `orchard` feature flag:
|
||||
- `zcash_keys::address::UnifiedAddress::from_receivers` no longer takes an
|
||||
Orchard receiver argument unless the `orchard` feature is enabled.
|
||||
- `zcash_keys::keys::UnifiedFullViewingKey::new` no longer takes
|
||||
an Orchard key argument unless the `orchard` feature is enabled.
|
||||
- `zcash_keys::address::UnifiedAddress::orchard`
|
||||
- `zcash_keys::keys::DerivationError::Orchard`
|
||||
- `zcash_keys::keys::UnifiedSpendingKey::orchard`
|
||||
- `zcash_keys::keys::UnifiedFullViewingKey::orchard`
|
||||
- The following methods and method arguments have been placed behind the
|
||||
`sapling` feature flag:
|
||||
- `UnifiedAddress::from_receivers` no longer takes a Sapling receiver
|
||||
argument unless the `sapling` feature is enabled.
|
||||
- `zcash_keys::keys::UnifiedFullViewingKey::new` no longer takes
|
||||
a Sapling key argument unless the `sapling` feature is enabled.
|
||||
- `zcash_keys::address::UnifiedAddress::sapling`
|
||||
- `zcash_keys::keys::UnifiedSpendingKey::sapling`
|
||||
- `zcash_keys::keys::UnifiedFullViewingKey::sapling`
|
||||
- The following methods and method arguments have been placed behind the
|
||||
`transparent-inputs` feature flag:
|
||||
- `zcash_keys::keys::UnifiedFullViewingKey::transparent` no longer takes
|
||||
a transparent key argument unless the `transparent-inputs` feature is enabled.
|
||||
- `zcash_keys::keys::UnifiedSpendingKey::transparent`
|
||||
- `zcash_keys::keys::UnifiedFullViewingKey::transparent`
|
||||
- `zcash_keys::address`:
|
||||
- `RecipientAddress` has been renamed to `Address`.
|
||||
- `Address::Shielded` has been renamed to `Address::Sapling`.
|
||||
- `UnifiedAddress::from_receivers` no longer takes an Orchard receiver
|
||||
argument unless the `orchard` feature is enabled.
|
||||
- `zcash_keys::keys`:
|
||||
- `UnifiedSpendingKey::address` now takes an argument that specifies the
|
||||
receivers to be generated in the resulting address. Also, it now returns
|
||||
`Result<UnifiedAddress, AddressGenerationError>` instead of
|
||||
`Option<UnifiedAddress>` so that we may better report to the user how
|
||||
address generation has failed.
|
||||
- `UnifiedSpendingKey::transparent` is now only available when the
|
||||
`transparent-inputs` feature is enabled.
|
||||
- `UnifiedFullViewingKey::new` no longer takes an Orchard full viewing key
|
||||
argument unless the `orchard` feature is enabled.
|
||||
|
||||
### Removed
|
||||
- `zcash_keys::address::AddressMetadata`
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
//! Structs for handling supported address types.
|
||||
|
||||
use zcash_address::{
|
||||
unified::{self, Container, Encoding, Typecode},
|
||||
unified::{self, Container, DataTypecode, Encoding, Item, Revision, Typecode},
|
||||
ConversionError, ToAddress, TryFromRawAddress, ZcashAddress,
|
||||
};
|
||||
use zcash_primitives::legacy::TransparentAddress;
|
||||
use zcash_protocol::consensus::{self, NetworkType};
|
||||
use zcash_protocol::{
|
||||
consensus::{self, BlockHeight, NetworkType},
|
||||
PoolType, ShieldedProtocol,
|
||||
};
|
||||
|
||||
#[cfg(feature = "sapling")]
|
||||
use sapling::PaymentAddress;
|
||||
use zcash_protocol::{PoolType, ShieldedProtocol};
|
||||
|
||||
/// A Unified Address.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
@ -19,7 +21,10 @@ pub struct UnifiedAddress {
|
|||
#[cfg(feature = "sapling")]
|
||||
sapling: Option<PaymentAddress>,
|
||||
transparent: Option<TransparentAddress>,
|
||||
unknown: Vec<(u32, Vec<u8>)>,
|
||||
unknown_data: Vec<(u32, Vec<u8>)>,
|
||||
expiry_height: Option<BlockHeight>,
|
||||
expiry_time: Option<u64>,
|
||||
unknown_metadata: Vec<(u32, Vec<u8>)>,
|
||||
}
|
||||
|
||||
impl TryFrom<unified::Address> for UnifiedAddress {
|
||||
|
@ -31,14 +36,16 @@ impl TryFrom<unified::Address> for UnifiedAddress {
|
|||
#[cfg(feature = "sapling")]
|
||||
let mut sapling = None;
|
||||
let mut transparent = None;
|
||||
|
||||
let mut unknown: Vec<(u32, Vec<u8>)> = vec![];
|
||||
let mut unknown_data = vec![];
|
||||
let mut expiry_height = None;
|
||||
let mut expiry_time = None;
|
||||
let mut unknown_metadata = vec![];
|
||||
|
||||
// We can use as-parsed order here for efficiency, because we're breaking out the
|
||||
// receivers we support from the unknown receivers.
|
||||
for item in ua.items_as_parsed() {
|
||||
match item {
|
||||
unified::Receiver::Orchard(data) => {
|
||||
Item::Data(unified::Receiver::Orchard(data)) => {
|
||||
#[cfg(feature = "orchard")]
|
||||
{
|
||||
orchard = Some(
|
||||
|
@ -48,11 +55,11 @@ impl TryFrom<unified::Address> for UnifiedAddress {
|
|||
}
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
{
|
||||
unknown.push((unified::Typecode::Orchard.into(), data.to_vec()));
|
||||
unknown_data.push((unified::Typecode::ORCHARD.into(), data.to_vec()));
|
||||
}
|
||||
}
|
||||
|
||||
unified::Receiver::Sapling(data) => {
|
||||
Item::Data(unified::Receiver::Sapling(data)) => {
|
||||
#[cfg(feature = "sapling")]
|
||||
{
|
||||
sapling = Some(
|
||||
|
@ -62,20 +69,26 @@ impl TryFrom<unified::Address> for UnifiedAddress {
|
|||
}
|
||||
#[cfg(not(feature = "sapling"))]
|
||||
{
|
||||
unknown.push((unified::Typecode::Sapling.into(), data.to_vec()));
|
||||
unknown_data.push((unified::Typecode::SAPLING.into(), data.to_vec()));
|
||||
}
|
||||
}
|
||||
|
||||
unified::Receiver::P2pkh(data) => {
|
||||
Item::Data(unified::Receiver::P2pkh(data)) => {
|
||||
transparent = Some(TransparentAddress::PublicKeyHash(*data));
|
||||
}
|
||||
|
||||
unified::Receiver::P2sh(data) => {
|
||||
Item::Data(unified::Receiver::P2sh(data)) => {
|
||||
transparent = Some(TransparentAddress::ScriptHash(*data));
|
||||
}
|
||||
|
||||
unified::Receiver::Unknown { typecode, data } => {
|
||||
unknown.push((*typecode, data.clone()));
|
||||
Item::Data(unified::Receiver::Unknown { typecode, data }) => {
|
||||
unknown_data.push((*typecode, data.clone()));
|
||||
}
|
||||
Item::Metadata(unified::MetadataItem::ExpiryHeight(h)) => {
|
||||
expiry_height = Some(BlockHeight::from(*h));
|
||||
}
|
||||
Item::Metadata(unified::MetadataItem::ExpiryTime(t)) => {
|
||||
expiry_time = Some(*t);
|
||||
}
|
||||
Item::Metadata(unified::MetadataItem::Unknown { typecode, data }) => {
|
||||
unknown_metadata.push((*typecode, data.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +99,10 @@ impl TryFrom<unified::Address> for UnifiedAddress {
|
|||
#[cfg(feature = "sapling")]
|
||||
sapling,
|
||||
transparent,
|
||||
unknown,
|
||||
unknown_data,
|
||||
expiry_height,
|
||||
expiry_time,
|
||||
unknown_metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -94,36 +110,43 @@ impl TryFrom<unified::Address> for UnifiedAddress {
|
|||
impl UnifiedAddress {
|
||||
/// Constructs a Unified Address from a given set of receivers.
|
||||
///
|
||||
/// Returns `None` if the receivers would produce an invalid Unified Address (namely,
|
||||
/// if no shielded receiver is provided).
|
||||
/// This method is only available when the `test-dependencies` feature is enabled, as
|
||||
/// derivation from the UFVK or UIVK, or deserialization from the serialized form should be
|
||||
/// used instead.
|
||||
#[cfg(any(test, feature = "test-dependencies"))]
|
||||
pub fn from_receivers(
|
||||
#[cfg(feature = "orchard")] orchard: Option<orchard::Address>,
|
||||
#[cfg(feature = "sapling")] sapling: Option<PaymentAddress>,
|
||||
transparent: Option<TransparentAddress>,
|
||||
// TODO: Add handling for address metadata items.
|
||||
) -> Option<Self> {
|
||||
#[cfg(feature = "orchard")]
|
||||
let has_orchard = orchard.is_some();
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
let has_orchard = false;
|
||||
) -> Self {
|
||||
Self::new_internal(
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
#[cfg(feature = "sapling")]
|
||||
sapling,
|
||||
transparent,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "sapling")]
|
||||
let has_sapling = sapling.is_some();
|
||||
#[cfg(not(feature = "sapling"))]
|
||||
let has_sapling = false;
|
||||
|
||||
if has_orchard || has_sapling {
|
||||
Some(Self {
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
#[cfg(feature = "sapling")]
|
||||
sapling,
|
||||
transparent,
|
||||
unknown: vec![],
|
||||
})
|
||||
} else {
|
||||
// UAs require at least one shielded receiver.
|
||||
None
|
||||
pub(crate) fn new_internal(
|
||||
#[cfg(feature = "orchard")] orchard: Option<orchard::Address>,
|
||||
#[cfg(feature = "sapling")] sapling: Option<PaymentAddress>,
|
||||
transparent: Option<TransparentAddress>,
|
||||
expiry_height: Option<BlockHeight>,
|
||||
expiry_time: Option<u64>,
|
||||
) -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
#[cfg(feature = "sapling")]
|
||||
sapling,
|
||||
transparent,
|
||||
unknown_data: vec![],
|
||||
expiry_height,
|
||||
expiry_time,
|
||||
unknown_metadata: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,22 +191,60 @@ impl UnifiedAddress {
|
|||
self.transparent.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the set of unknown receivers of the unified address.
|
||||
pub fn unknown(&self) -> &[(u32, Vec<u8>)] {
|
||||
&self.unknown
|
||||
/// Returns any unknown data items parsed from the encoded form of the address.
|
||||
pub fn unknown_data(&self) -> &[(u32, Vec<u8>)] {
|
||||
self.unknown_data.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the expiration height for this address.
|
||||
pub fn expiry_height(&self) -> Option<BlockHeight> {
|
||||
self.expiry_height
|
||||
}
|
||||
|
||||
/// Sets the expiry height of this address.
|
||||
pub fn set_expiry_height(&mut self, height: BlockHeight) {
|
||||
self.expiry_height = Some(height);
|
||||
}
|
||||
|
||||
/// Removes the expiry height from this address.
|
||||
pub fn unset_expiry_height(&mut self) {
|
||||
self.expiry_height = None;
|
||||
}
|
||||
|
||||
/// Returns the expiration time for this address as a Unix Epoch Time.
|
||||
pub fn expiry_time(&self) -> Option<u64> {
|
||||
self.expiry_time
|
||||
}
|
||||
|
||||
/// Sets the expiry time of this address.
|
||||
pub fn set_expiry_time(&mut self, time: u64) {
|
||||
self.expiry_time = Some(time);
|
||||
}
|
||||
|
||||
/// Removes the expiry time from this address.
|
||||
pub fn unset_expiry_time(&mut self) {
|
||||
self.expiry_time = None;
|
||||
}
|
||||
|
||||
/// Returns any unknown metadata items parsed from the encoded form of the address.
|
||||
///
|
||||
/// Unknown metadata items are guaranteed by construction and parsing to not have keys in the
|
||||
/// MUST-understand metadata typecode range.
|
||||
pub fn unknown_metadata(&self) -> &[(u32, Vec<u8>)] {
|
||||
self.unknown_metadata.as_ref()
|
||||
}
|
||||
|
||||
fn to_address(&self, net: NetworkType) -> ZcashAddress {
|
||||
let items = self
|
||||
.unknown
|
||||
.iter()
|
||||
.map(|(typecode, data)| unified::Receiver::Unknown {
|
||||
typecode: *typecode,
|
||||
data: data.clone(),
|
||||
});
|
||||
let data_items =
|
||||
self.unknown_data
|
||||
.iter()
|
||||
.map(|(typecode, data)| unified::Receiver::Unknown {
|
||||
typecode: *typecode,
|
||||
data: data.clone(),
|
||||
});
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
let items = items.chain(
|
||||
let data_items = data_items.chain(
|
||||
self.orchard
|
||||
.as_ref()
|
||||
.map(|addr| addr.to_raw_address_bytes())
|
||||
|
@ -191,20 +252,46 @@ impl UnifiedAddress {
|
|||
);
|
||||
|
||||
#[cfg(feature = "sapling")]
|
||||
let items = items.chain(
|
||||
let data_items = data_items.chain(
|
||||
self.sapling
|
||||
.as_ref()
|
||||
.map(|pa| pa.to_bytes())
|
||||
.map(unified::Receiver::Sapling),
|
||||
);
|
||||
|
||||
let items = items.chain(self.transparent.as_ref().map(|taddr| match taddr {
|
||||
let data_items = data_items.chain(self.transparent.as_ref().map(|taddr| match taddr {
|
||||
TransparentAddress::PublicKeyHash(data) => unified::Receiver::P2pkh(*data),
|
||||
TransparentAddress::ScriptHash(data) => unified::Receiver::P2sh(*data),
|
||||
}));
|
||||
|
||||
let ua = unified::Address::try_from_items(items.collect())
|
||||
.expect("UnifiedAddress should only be constructed safely");
|
||||
let meta_items = self
|
||||
.unknown_metadata
|
||||
.iter()
|
||||
.map(|(typecode, data)| unified::MetadataItem::Unknown {
|
||||
typecode: *typecode,
|
||||
data: data.clone(),
|
||||
})
|
||||
.chain(
|
||||
self.expiry_height
|
||||
.map(|h| unified::MetadataItem::ExpiryHeight(u32::from(h))),
|
||||
)
|
||||
.chain(self.expiry_time.map(unified::MetadataItem::ExpiryTime));
|
||||
|
||||
let ua = unified::Address::try_from_items(
|
||||
if self.expiry_height().is_some()
|
||||
|| self.expiry_time().is_some()
|
||||
|| !(self.has_orchard() || self.has_sapling())
|
||||
{
|
||||
Revision::R1
|
||||
} else {
|
||||
Revision::R0
|
||||
},
|
||||
data_items
|
||||
.map(Item::Data)
|
||||
.chain(meta_items.map(Item::Metadata))
|
||||
.collect(),
|
||||
)
|
||||
.expect("UnifiedAddress should only be constructed safely");
|
||||
ZcashAddress::from_unified(net, ua)
|
||||
}
|
||||
|
||||
|
@ -217,47 +304,105 @@ impl UnifiedAddress {
|
|||
pub fn receiver_types(&self) -> Vec<Typecode> {
|
||||
let result = std::iter::empty();
|
||||
#[cfg(feature = "orchard")]
|
||||
let result = result.chain(self.orchard.map(|_| Typecode::Orchard));
|
||||
let result = result.chain(self.orchard.map(|_| Typecode::ORCHARD));
|
||||
#[cfg(feature = "sapling")]
|
||||
let result = result.chain(self.sapling.map(|_| Typecode::Sapling));
|
||||
let result = result.chain(self.sapling.map(|_| Typecode::SAPLING));
|
||||
let result = result.chain(self.transparent.map(|taddr| match taddr {
|
||||
TransparentAddress::PublicKeyHash(_) => Typecode::P2pkh,
|
||||
TransparentAddress::ScriptHash(_) => Typecode::P2sh,
|
||||
TransparentAddress::PublicKeyHash(_) => Typecode::P2PKH,
|
||||
TransparentAddress::ScriptHash(_) => Typecode::P2SH,
|
||||
}));
|
||||
let result = result.chain(
|
||||
self.unknown()
|
||||
self.unknown_data()
|
||||
.iter()
|
||||
.map(|(typecode, _)| Typecode::Unknown(*typecode)),
|
||||
.map(|(typecode, _)| Typecode::Data(DataTypecode::Unknown(*typecode))),
|
||||
);
|
||||
result.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// An enumeration of protocol-level receiver types.
|
||||
///
|
||||
/// While these correspond to unified address receiver types, this is a distinct type because it is
|
||||
/// used to represent the protocol-level recipient of a transfer, instead of a part of an encoded
|
||||
/// address.
|
||||
pub enum Receiver {
|
||||
#[cfg(feature = "orchard")]
|
||||
Orchard(orchard::Address),
|
||||
#[cfg(feature = "sapling")]
|
||||
Sapling(PaymentAddress),
|
||||
Transparent(TransparentAddress),
|
||||
}
|
||||
|
||||
impl Receiver {
|
||||
/// Converts this receiver to a [`ZcashAddress`] for the given network.
|
||||
///
|
||||
/// This conversion function selects the least-capable address format possible; this means that
|
||||
/// Orchard receivers will be rendered as Unified addresses, Sapling receivers will be rendered
|
||||
/// as bare Sapling addresses, and Transparent receivers will be rendered as taddrs.
|
||||
pub fn to_zcash_address(&self, net: NetworkType) -> ZcashAddress {
|
||||
match self {
|
||||
#[cfg(feature = "orchard")]
|
||||
Receiver::Orchard(addr) => {
|
||||
let receiver =
|
||||
unified::Item::Data(unified::Receiver::Orchard(addr.to_raw_address_bytes()));
|
||||
let ua = unified::Address::try_from_items(Revision::R0, vec![receiver])
|
||||
.expect("A unified address may contain a single Orchard receiver.");
|
||||
ZcashAddress::from_unified(net, ua)
|
||||
}
|
||||
#[cfg(feature = "sapling")]
|
||||
Receiver::Sapling(addr) => ZcashAddress::from_sapling(net, addr.to_bytes()),
|
||||
Receiver::Transparent(TransparentAddress::PublicKeyHash(data)) => {
|
||||
ZcashAddress::from_transparent_p2pkh(net, *data)
|
||||
}
|
||||
Receiver::Transparent(TransparentAddress::ScriptHash(data)) => {
|
||||
ZcashAddress::from_transparent_p2sh(net, *data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether or not this receiver corresponds to `addr`, or is contained
|
||||
/// in `addr` when the latter is a Unified Address.
|
||||
pub fn corresponds(&self, addr: &ZcashAddress) -> bool {
|
||||
addr.matches_receiver(&match self {
|
||||
#[cfg(feature = "orchard")]
|
||||
Receiver::Orchard(addr) => unified::Receiver::Orchard(addr.to_raw_address_bytes()),
|
||||
#[cfg(feature = "sapling")]
|
||||
Receiver::Sapling(addr) => unified::Receiver::Sapling(addr.to_bytes()),
|
||||
Receiver::Transparent(TransparentAddress::PublicKeyHash(data)) => {
|
||||
unified::Receiver::P2pkh(*data)
|
||||
}
|
||||
Receiver::Transparent(TransparentAddress::ScriptHash(data)) => {
|
||||
unified::Receiver::P2sh(*data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An address that funds can be sent to.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Address {
|
||||
#[cfg(feature = "sapling")]
|
||||
Sapling(PaymentAddress),
|
||||
Transparent(TransparentAddress),
|
||||
Unified(UnifiedAddress),
|
||||
Sapling(Box<PaymentAddress>),
|
||||
Transparent(Box<TransparentAddress>),
|
||||
Unified(Box<UnifiedAddress>),
|
||||
}
|
||||
|
||||
#[cfg(feature = "sapling")]
|
||||
impl From<PaymentAddress> for Address {
|
||||
fn from(addr: PaymentAddress) -> Self {
|
||||
Address::Sapling(addr)
|
||||
Address::Sapling(Box::new(addr))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TransparentAddress> for Address {
|
||||
fn from(addr: TransparentAddress) -> Self {
|
||||
Address::Transparent(addr)
|
||||
Address::Transparent(Box::new(addr))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UnifiedAddress> for Address {
|
||||
fn from(addr: UnifiedAddress) -> Self {
|
||||
Address::Unified(addr)
|
||||
Address::Unified(Box::new(addr))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -290,30 +435,47 @@ impl TryFromRawAddress for Address {
|
|||
}
|
||||
|
||||
impl Address {
|
||||
/// Attempts to decode an [`Address`] value from its [`ZcashAddress`] encoded representation.
|
||||
///
|
||||
/// Returns `None` if any error is encountered in decoding. Use
|
||||
/// [`Self::try_from_zcash_address(s.parse()?)?`] if you need detailed error information.
|
||||
pub fn decode<P: consensus::Parameters>(params: &P, s: &str) -> Option<Self> {
|
||||
let addr = ZcashAddress::try_from_encoded(s).ok()?;
|
||||
addr.convert_if_network(params.network_type()).ok()
|
||||
Self::try_from_zcash_address(params, s.parse::<ZcashAddress>().ok()?).ok()
|
||||
}
|
||||
|
||||
pub fn encode<P: consensus::Parameters>(&self, params: &P) -> String {
|
||||
/// Attempts to decode an [`Address`] value from its [`ZcashAddress`] encoded representation.
|
||||
pub fn try_from_zcash_address<P: consensus::Parameters>(
|
||||
params: &P,
|
||||
zaddr: ZcashAddress,
|
||||
) -> Result<Self, ConversionError<&'static str>> {
|
||||
zaddr.convert_if_network(params.network_type())
|
||||
}
|
||||
|
||||
/// Converts this [`Address`] to its encoded [`ZcashAddress`] representation.
|
||||
pub fn to_zcash_address<P: consensus::Parameters>(&self, params: &P) -> ZcashAddress {
|
||||
let net = params.network_type();
|
||||
|
||||
match self {
|
||||
#[cfg(feature = "sapling")]
|
||||
Address::Sapling(pa) => ZcashAddress::from_sapling(net, pa.to_bytes()),
|
||||
Address::Transparent(addr) => match addr {
|
||||
Address::Transparent(addr) => match **addr {
|
||||
TransparentAddress::PublicKeyHash(data) => {
|
||||
ZcashAddress::from_transparent_p2pkh(net, *data)
|
||||
ZcashAddress::from_transparent_p2pkh(net, data)
|
||||
}
|
||||
TransparentAddress::ScriptHash(data) => {
|
||||
ZcashAddress::from_transparent_p2sh(net, *data)
|
||||
ZcashAddress::from_transparent_p2sh(net, data)
|
||||
}
|
||||
},
|
||||
Address::Unified(ua) => ua.to_address(net),
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Converts this [`Address`] to its encoded string representation.
|
||||
pub fn encode<P: consensus::Parameters>(&self, params: &P) -> String {
|
||||
self.to_zcash_address(params).to_string()
|
||||
}
|
||||
|
||||
/// Returns whether or not this [`Address`] can send funds to the specified pool.
|
||||
pub fn has_receiver(&self, pool_type: PoolType) -> bool {
|
||||
match self {
|
||||
#[cfg(feature = "sapling")]
|
||||
|
@ -366,23 +528,23 @@ pub mod testing {
|
|||
params: Network,
|
||||
request: UnifiedAddressRequest,
|
||||
) -> impl Strategy<Value = UnifiedAddress> {
|
||||
arb_unified_spending_key(params).prop_map(move |k| k.default_address(request).0)
|
||||
arb_unified_spending_key(params).prop_map(move |k| k.default_address(request).unwrap().0)
|
||||
}
|
||||
|
||||
#[cfg(feature = "sapling")]
|
||||
pub fn arb_addr(request: UnifiedAddressRequest) -> impl Strategy<Value = Address> {
|
||||
prop_oneof![
|
||||
arb_payment_address().prop_map(Address::Sapling),
|
||||
arb_transparent_addr().prop_map(Address::Transparent),
|
||||
arb_unified_addr(Network::TestNetwork, request).prop_map(Address::Unified),
|
||||
arb_payment_address().prop_map(Address::from),
|
||||
arb_transparent_addr().prop_map(Address::from),
|
||||
arb_unified_addr(Network::TestNetwork, request).prop_map(Address::from),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "sapling"))]
|
||||
pub fn arb_addr(request: UnifiedAddressRequest) -> impl Strategy<Value = Address> {
|
||||
return prop_oneof![
|
||||
arb_transparent_addr().prop_map(Address::Transparent),
|
||||
arb_unified_addr(Network::TestNetwork, request).prop_map(Address::Unified),
|
||||
arb_transparent_addr().prop_map(Address::from),
|
||||
arb_unified_addr(Network::TestNetwork, request).prop_map(Address::from),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -421,15 +583,15 @@ mod tests {
|
|||
let transparent = None;
|
||||
|
||||
#[cfg(all(feature = "orchard", feature = "sapling"))]
|
||||
let ua = UnifiedAddress::from_receivers(orchard, sapling, transparent).unwrap();
|
||||
let ua = UnifiedAddress::new_internal(orchard, sapling, transparent, None, None);
|
||||
|
||||
#[cfg(all(not(feature = "orchard"), feature = "sapling"))]
|
||||
let ua = UnifiedAddress::from_receivers(sapling, transparent).unwrap();
|
||||
let ua = UnifiedAddress::new_internal(sapling, transparent, None, None);
|
||||
|
||||
#[cfg(all(feature = "orchard", not(feature = "sapling")))]
|
||||
let ua = UnifiedAddress::from_receivers(orchard, transparent).unwrap();
|
||||
let ua = UnifiedAddress::new_internal(orchard, transparent, None, None);
|
||||
|
||||
let addr = Address::Unified(ua);
|
||||
let addr = Address::from(ua);
|
||||
let addr_str = addr.encode(&MAIN_NETWORK);
|
||||
assert_eq!(Address::decode(&MAIN_NETWORK, &addr_str), Some(addr));
|
||||
}
|
||||
|
@ -438,7 +600,7 @@ mod tests {
|
|||
#[cfg(not(any(feature = "orchard", feature = "sapling")))]
|
||||
fn ua_round_trip() {
|
||||
let transparent = None;
|
||||
assert_eq!(UnifiedAddress::from_receivers(transparent), None)
|
||||
assert_eq!(UnifiedAddress::new_internal(transparent, None, None), None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -3,8 +3,8 @@ use std::{
|
|||
error,
|
||||
fmt::{self, Display},
|
||||
};
|
||||
|
||||
use zcash_address::unified::{self, Container, Encoding, Typecode, Ufvk, Uivk};
|
||||
use zcash_address::unified::{self, Container, Encoding, Item, MetadataItem, Revision, Typecode};
|
||||
use zcash_primitives::consensus::BlockHeight;
|
||||
use zcash_protocol::consensus;
|
||||
use zip32::{AccountId, DiversifierIndex};
|
||||
|
||||
|
@ -258,7 +258,10 @@ impl UnifiedSpendingKey {
|
|||
sapling: Some(self.sapling.to_diversifiable_full_viewing_key()),
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard: Some((&self.orchard).into()),
|
||||
unknown: vec![],
|
||||
unknown_data: vec![],
|
||||
expiry_height: None,
|
||||
expiry_time: None,
|
||||
unknown_metadata: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -298,7 +301,7 @@ impl UnifiedSpendingKey {
|
|||
#[cfg(feature = "orchard")]
|
||||
{
|
||||
let orchard_key = self.orchard();
|
||||
CompactSize::write(&mut result, usize::try_from(Typecode::Orchard).unwrap()).unwrap();
|
||||
CompactSize::write(&mut result, usize::try_from(Typecode::ORCHARD).unwrap()).unwrap();
|
||||
|
||||
let orchard_key_bytes = orchard_key.to_bytes();
|
||||
CompactSize::write(&mut result, orchard_key_bytes.len()).unwrap();
|
||||
|
@ -308,7 +311,7 @@ impl UnifiedSpendingKey {
|
|||
#[cfg(feature = "sapling")]
|
||||
{
|
||||
let sapling_key = self.sapling();
|
||||
CompactSize::write(&mut result, usize::try_from(Typecode::Sapling).unwrap()).unwrap();
|
||||
CompactSize::write(&mut result, usize::try_from(Typecode::SAPLING).unwrap()).unwrap();
|
||||
|
||||
let sapling_key_bytes = sapling_key.to_bytes();
|
||||
CompactSize::write(&mut result, sapling_key_bytes.len()).unwrap();
|
||||
|
@ -318,7 +321,7 @@ impl UnifiedSpendingKey {
|
|||
#[cfg(feature = "transparent-inputs")]
|
||||
{
|
||||
let account_tkey = self.transparent();
|
||||
CompactSize::write(&mut result, usize::try_from(Typecode::P2pkh).unwrap()).unwrap();
|
||||
CompactSize::write(&mut result, usize::try_from(Typecode::P2PKH).unwrap()).unwrap();
|
||||
|
||||
let account_tkey_bytes = account_tkey.to_bytes();
|
||||
CompactSize::write(&mut result, account_tkey_bytes.len()).unwrap();
|
||||
|
@ -334,6 +337,8 @@ impl UnifiedSpendingKey {
|
|||
#[allow(clippy::unnecessary_unwrap)]
|
||||
#[cfg(feature = "unstable")]
|
||||
pub fn from_bytes(era: Era, encoded: &[u8]) -> Result<Self, DecodingError> {
|
||||
use zcash_address::unified::DataTypecode;
|
||||
|
||||
let mut source = std::io::Cursor::new(encoded);
|
||||
let decoded_era = source
|
||||
.read_u32::<LittleEndian>()
|
||||
|
@ -353,21 +358,23 @@ impl UnifiedSpendingKey {
|
|||
loop {
|
||||
let tc = CompactSize::read_t::<_, u32>(&mut source)
|
||||
.map_err(|_| DecodingError::ReadError("typecode"))
|
||||
.and_then(|v| Typecode::try_from(v).map_err(|_| DecodingError::TypecodeInvalid))?;
|
||||
.and_then(|v| {
|
||||
DataTypecode::try_from(v).map_err(|_| DecodingError::TypecodeInvalid)
|
||||
})?;
|
||||
|
||||
let len = CompactSize::read_t::<_, u32>(&mut source)
|
||||
.map_err(|_| DecodingError::ReadError("key length"))?;
|
||||
|
||||
match tc {
|
||||
Typecode::Orchard => {
|
||||
DataTypecode::Orchard => {
|
||||
if len != 32 {
|
||||
return Err(DecodingError::LengthMismatch(Typecode::Orchard, len));
|
||||
return Err(DecodingError::LengthMismatch(Typecode::ORCHARD, len));
|
||||
}
|
||||
|
||||
let mut key = [0u8; 32];
|
||||
source
|
||||
.read_exact(&mut key)
|
||||
.map_err(|_| DecodingError::InsufficientData(Typecode::Orchard))?;
|
||||
.map_err(|_| DecodingError::InsufficientData(Typecode::ORCHARD))?;
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
{
|
||||
|
@ -375,43 +382,43 @@ impl UnifiedSpendingKey {
|
|||
Option::<orchard::keys::SpendingKey>::from(
|
||||
orchard::keys::SpendingKey::from_bytes(key),
|
||||
)
|
||||
.ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard))?,
|
||||
.ok_or(DecodingError::KeyDataInvalid(Typecode::ORCHARD))?,
|
||||
);
|
||||
}
|
||||
}
|
||||
Typecode::Sapling => {
|
||||
DataTypecode::Sapling => {
|
||||
if len != 169 {
|
||||
return Err(DecodingError::LengthMismatch(Typecode::Sapling, len));
|
||||
return Err(DecodingError::LengthMismatch(Typecode::SAPLING, len));
|
||||
}
|
||||
|
||||
let mut key = [0u8; 169];
|
||||
source
|
||||
.read_exact(&mut key)
|
||||
.map_err(|_| DecodingError::InsufficientData(Typecode::Sapling))?;
|
||||
.map_err(|_| DecodingError::InsufficientData(Typecode::SAPLING))?;
|
||||
|
||||
#[cfg(feature = "sapling")]
|
||||
{
|
||||
sapling = Some(
|
||||
sapling::ExtendedSpendingKey::from_bytes(&key)
|
||||
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::Sapling))?,
|
||||
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::SAPLING))?,
|
||||
);
|
||||
}
|
||||
}
|
||||
Typecode::P2pkh => {
|
||||
DataTypecode::P2pkh => {
|
||||
if len != 64 {
|
||||
return Err(DecodingError::LengthMismatch(Typecode::P2pkh, len));
|
||||
return Err(DecodingError::LengthMismatch(Typecode::P2PKH, len));
|
||||
}
|
||||
|
||||
let mut key = [0u8; 64];
|
||||
source
|
||||
.read_exact(&mut key)
|
||||
.map_err(|_| DecodingError::InsufficientData(Typecode::P2pkh))?;
|
||||
.map_err(|_| DecodingError::InsufficientData(Typecode::P2PKH))?;
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
{
|
||||
transparent = Some(
|
||||
legacy::AccountPrivKey::from_bytes(&key)
|
||||
.ok_or(DecodingError::KeyDataInvalid(Typecode::P2pkh))?,
|
||||
.ok_or(DecodingError::KeyDataInvalid(Typecode::P2PKH))?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -444,7 +451,7 @@ impl UnifiedSpendingKey {
|
|||
#[cfg(feature = "orchard")]
|
||||
orchard.unwrap(),
|
||||
)
|
||||
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh));
|
||||
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -453,10 +460,8 @@ impl UnifiedSpendingKey {
|
|||
pub fn default_address(
|
||||
&self,
|
||||
request: UnifiedAddressRequest,
|
||||
) -> (UnifiedAddress, DiversifierIndex) {
|
||||
self.to_unified_full_viewing_key()
|
||||
.default_address(request)
|
||||
.unwrap()
|
||||
) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> {
|
||||
self.to_unified_full_viewing_key().default_address(request)
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
|
@ -549,22 +554,28 @@ pub struct UnifiedAddressRequest {
|
|||
has_orchard: bool,
|
||||
has_sapling: bool,
|
||||
has_p2pkh: bool,
|
||||
expiry_height: Option<BlockHeight>,
|
||||
expiry_time: Option<u64>,
|
||||
}
|
||||
|
||||
impl UnifiedAddressRequest {
|
||||
/// Construct a new unified address request from its constituent parts.
|
||||
///
|
||||
/// Returns `None` if the resulting unified address would not include at least one shielded receiver.
|
||||
pub fn new(has_orchard: bool, has_sapling: bool, has_p2pkh: bool) -> Option<Self> {
|
||||
let has_shielded_receiver = has_orchard || has_sapling;
|
||||
|
||||
if !has_shielded_receiver {
|
||||
/// Construct a new unified address request from its constituent parts
|
||||
pub fn new(
|
||||
has_orchard: bool,
|
||||
has_sapling: bool,
|
||||
has_p2pkh: bool,
|
||||
expiry_height: Option<BlockHeight>,
|
||||
expiry_time: Option<u64>,
|
||||
) -> Option<Self> {
|
||||
if !(has_sapling || has_orchard || has_p2pkh) {
|
||||
None
|
||||
} else {
|
||||
Some(Self {
|
||||
has_orchard,
|
||||
has_sapling,
|
||||
has_p2pkh,
|
||||
expiry_height,
|
||||
expiry_time,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -584,21 +595,27 @@ impl UnifiedAddressRequest {
|
|||
#[cfg(feature = "transparent-inputs")]
|
||||
let _has_p2pkh = true;
|
||||
|
||||
Self::new(_has_orchard, _has_sapling, _has_p2pkh)
|
||||
Self::new(_has_orchard, _has_sapling, _has_p2pkh, None, None)
|
||||
}
|
||||
|
||||
/// Construct a new unified address request from its constituent parts.
|
||||
///
|
||||
/// Panics: at least one of `has_orchard` or `has_sapling` must be `true`.
|
||||
pub const fn unsafe_new(has_orchard: bool, has_sapling: bool, has_p2pkh: bool) -> Self {
|
||||
if !(has_orchard || has_sapling) {
|
||||
panic!("At least one shielded receiver must be requested.")
|
||||
/// Panics: at least one of `has_orchard`, `has_sapling`, or `has_p2pkh` must be `true`.
|
||||
pub const fn unsafe_new_without_expiry(
|
||||
has_orchard: bool,
|
||||
has_sapling: bool,
|
||||
has_p2pkh: bool,
|
||||
) -> Self {
|
||||
if !(has_orchard || has_sapling || has_p2pkh) {
|
||||
panic!("At least one receiver must be requested.")
|
||||
}
|
||||
|
||||
Self {
|
||||
has_orchard,
|
||||
has_sapling,
|
||||
has_p2pkh,
|
||||
expiry_height: None,
|
||||
expiry_time: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -619,7 +636,10 @@ pub struct UnifiedFullViewingKey {
|
|||
sapling: Option<sapling::DiversifiableFullViewingKey>,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard: Option<orchard::keys::FullViewingKey>,
|
||||
unknown: Vec<(u32, Vec<u8>)>,
|
||||
unknown_data: Vec<(u32, Vec<u8>)>,
|
||||
expiry_height: Option<BlockHeight>,
|
||||
expiry_time: Option<u64>,
|
||||
unknown_metadata: Vec<(u32, Vec<u8>)>,
|
||||
}
|
||||
|
||||
impl UnifiedFullViewingKey {
|
||||
|
@ -645,6 +665,9 @@ impl UnifiedFullViewingKey {
|
|||
// We don't currently allow constructing new UFVKs with unknown items, but we store
|
||||
// this to allow parsing such UFVKs.
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
vec![],
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -654,7 +677,10 @@ impl UnifiedFullViewingKey {
|
|||
#[cfg(feature = "transparent-inputs")] transparent: Option<legacy::AccountPubKey>,
|
||||
#[cfg(feature = "sapling")] sapling: Option<sapling::DiversifiableFullViewingKey>,
|
||||
#[cfg(feature = "orchard")] orchard: Option<orchard::keys::FullViewingKey>,
|
||||
unknown: Vec<(u32, Vec<u8>)>,
|
||||
unknown_data: Vec<(u32, Vec<u8>)>,
|
||||
expiry_height: Option<BlockHeight>,
|
||||
expiry_time: Option<u64>,
|
||||
unknown_metadata: Vec<(u32, Vec<u8>)>,
|
||||
) -> Result<UnifiedFullViewingKey, DerivationError> {
|
||||
// Verify that IVK derivation succeeds; we don't want to construct a UFVK
|
||||
// that can't derive transparent addresses.
|
||||
|
@ -671,7 +697,10 @@ impl UnifiedFullViewingKey {
|
|||
sapling,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
unknown,
|
||||
unknown_data,
|
||||
expiry_height,
|
||||
expiry_time,
|
||||
unknown_metadata,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -679,7 +708,8 @@ impl UnifiedFullViewingKey {
|
|||
///
|
||||
/// [ZIP 316]: https://zips.z.cash/zip-0316
|
||||
pub fn decode<P: consensus::Parameters>(params: &P, encoding: &str) -> Result<Self, String> {
|
||||
let (net, ufvk) = unified::Ufvk::decode(encoding).map_err(|e| e.to_string())?;
|
||||
let (net, ufvk) =
|
||||
zcash_address::unified::Ufvk::decode(encoding).map_err(|e| e.to_string())?;
|
||||
let expected_net = params.network_type();
|
||||
if net != expected_net {
|
||||
return Err(format!(
|
||||
|
@ -694,64 +724,71 @@ impl UnifiedFullViewingKey {
|
|||
/// Parses a `UnifiedFullViewingKey` from its [ZIP 316] string encoding.
|
||||
///
|
||||
/// [ZIP 316]: https://zips.z.cash/zip-0316
|
||||
pub fn parse(ufvk: &Ufvk) -> Result<Self, DecodingError> {
|
||||
pub fn parse(ufvk: &zcash_address::unified::Ufvk) -> Result<Self, DecodingError> {
|
||||
#[cfg(feature = "orchard")]
|
||||
let mut orchard = None;
|
||||
#[cfg(feature = "sapling")]
|
||||
let mut sapling = None;
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
let mut transparent = None;
|
||||
let mut unknown_data = vec![];
|
||||
let mut expiry_height = None;
|
||||
let mut expiry_time = None;
|
||||
let mut unknown_metadata = vec![];
|
||||
|
||||
// We can use as-parsed order here for efficiency, because we're breaking out the
|
||||
// receivers we support from the unknown receivers.
|
||||
let unknown = ufvk
|
||||
.items_as_parsed()
|
||||
.iter()
|
||||
.filter_map(|receiver| match receiver {
|
||||
#[cfg(feature = "orchard")]
|
||||
unified::Fvk::Orchard(data) => orchard::keys::FullViewingKey::from_bytes(data)
|
||||
.ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard))
|
||||
.map(|addr| {
|
||||
orchard = Some(addr);
|
||||
None
|
||||
})
|
||||
.transpose(),
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
unified::Fvk::Orchard(data) => Some(Ok::<_, DecodingError>((
|
||||
u32::from(unified::Typecode::Orchard),
|
||||
data.to_vec(),
|
||||
))),
|
||||
#[cfg(feature = "sapling")]
|
||||
unified::Fvk::Sapling(data) => {
|
||||
sapling::DiversifiableFullViewingKey::from_bytes(data)
|
||||
.ok_or(DecodingError::KeyDataInvalid(Typecode::Sapling))
|
||||
.map(|pa| {
|
||||
sapling = Some(pa);
|
||||
None
|
||||
})
|
||||
.transpose()
|
||||
for item in ufvk.items_as_parsed() {
|
||||
match item {
|
||||
Item::Data(unified::Fvk::Orchard(data)) => {
|
||||
#[cfg(feature = "orchard")]
|
||||
{
|
||||
orchard = Some(
|
||||
orchard::keys::FullViewingKey::from_bytes(data)
|
||||
.ok_or(DecodingError::KeyDataInvalid(Typecode::ORCHARD))?,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
unknown_data.push((unified::DataTypecode::Orchard.into(), data.to_vec()));
|
||||
}
|
||||
#[cfg(not(feature = "sapling"))]
|
||||
unified::Fvk::Sapling(data) => Some(Ok::<_, DecodingError>((
|
||||
u32::from(unified::Typecode::Sapling),
|
||||
data.to_vec(),
|
||||
))),
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
unified::Fvk::P2pkh(data) => legacy::AccountPubKey::deserialize(data)
|
||||
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh))
|
||||
.map(|tfvk| {
|
||||
transparent = Some(tfvk);
|
||||
None
|
||||
})
|
||||
.transpose(),
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
unified::Fvk::P2pkh(data) => Some(Ok::<_, DecodingError>((
|
||||
u32::from(unified::Typecode::P2pkh),
|
||||
data.to_vec(),
|
||||
))),
|
||||
unified::Fvk::Unknown { typecode, data } => Some(Ok((*typecode, data.clone()))),
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
Item::Data(unified::Fvk::Sapling(data)) => {
|
||||
#[cfg(feature = "sapling")]
|
||||
{
|
||||
sapling = Some(
|
||||
sapling::DiversifiableFullViewingKey::from_bytes(data)
|
||||
.ok_or(DecodingError::KeyDataInvalid(Typecode::SAPLING))?,
|
||||
);
|
||||
}
|
||||
#[cfg(not(feature = "sapling"))]
|
||||
unknown_data.push((unified::Typecode::SAPLING.into(), data.to_vec()));
|
||||
}
|
||||
Item::Data(unified::Fvk::P2pkh(data)) => {
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
{
|
||||
transparent = Some(
|
||||
legacy::AccountPubKey::deserialize(data)
|
||||
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH))?,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
unknown_data.push((unified::DataTypecode::P2pkh.into(), data.to_vec()));
|
||||
}
|
||||
Item::Data(unified::Fvk::Unknown { typecode, data }) => {
|
||||
unknown_data.push((*typecode, data.clone()));
|
||||
}
|
||||
Item::Metadata(MetadataItem::ExpiryHeight(h)) => {
|
||||
expiry_height = Some(BlockHeight::from(*h));
|
||||
}
|
||||
Item::Metadata(MetadataItem::ExpiryTime(t)) => {
|
||||
expiry_time = Some(*t);
|
||||
}
|
||||
Item::Metadata(MetadataItem::Unknown { typecode, data }) => {
|
||||
unknown_metadata.push((*typecode, data.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self::from_checked_parts(
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
|
@ -760,9 +797,12 @@ impl UnifiedFullViewingKey {
|
|||
sapling,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
unknown,
|
||||
unknown_data,
|
||||
expiry_height,
|
||||
expiry_time,
|
||||
unknown_metadata,
|
||||
)
|
||||
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh))
|
||||
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH))
|
||||
}
|
||||
|
||||
/// Returns the string encoding of this `UnifiedFullViewingKey` for the given network.
|
||||
|
@ -771,37 +811,64 @@ impl UnifiedFullViewingKey {
|
|||
}
|
||||
|
||||
/// Returns the string encoding of this `UnifiedFullViewingKey` for the given network.
|
||||
fn to_ufvk(&self) -> Ufvk {
|
||||
let items = std::iter::empty().chain(self.unknown.iter().map(|(typecode, data)| {
|
||||
unified::Fvk::Unknown {
|
||||
typecode: *typecode,
|
||||
data: data.clone(),
|
||||
}
|
||||
}));
|
||||
fn to_ufvk(&self) -> zcash_address::unified::Ufvk {
|
||||
let data_items =
|
||||
std::iter::empty().chain(self.unknown_data.iter().map(|(typecode, data)| {
|
||||
unified::Fvk::Unknown {
|
||||
typecode: *typecode,
|
||||
data: data.clone(),
|
||||
}
|
||||
}));
|
||||
#[cfg(feature = "orchard")]
|
||||
let items = items.chain(
|
||||
let data_items = data_items.chain(
|
||||
self.orchard
|
||||
.as_ref()
|
||||
.map(|fvk| fvk.to_bytes())
|
||||
.map(unified::Fvk::Orchard),
|
||||
);
|
||||
#[cfg(feature = "sapling")]
|
||||
let items = items.chain(
|
||||
let data_items = data_items.chain(
|
||||
self.sapling
|
||||
.as_ref()
|
||||
.map(|dfvk| dfvk.to_bytes())
|
||||
.map(unified::Fvk::Sapling),
|
||||
);
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
let items = items.chain(
|
||||
let data_items = data_items.chain(
|
||||
self.transparent
|
||||
.as_ref()
|
||||
.map(|tfvk| tfvk.serialize().try_into().unwrap())
|
||||
.map(unified::Fvk::P2pkh),
|
||||
);
|
||||
|
||||
unified::Ufvk::try_from_items(items.collect())
|
||||
.expect("UnifiedFullViewingKey should only be constructed safely")
|
||||
let meta_items = std::iter::empty()
|
||||
.chain(self.unknown_metadata.iter().map(|(typecode, data)| {
|
||||
unified::MetadataItem::Unknown {
|
||||
typecode: *typecode,
|
||||
data: data.clone(),
|
||||
}
|
||||
}))
|
||||
.chain(
|
||||
self.expiry_height
|
||||
.map(|h| unified::MetadataItem::ExpiryHeight(u32::from(h))),
|
||||
)
|
||||
.chain(self.expiry_time.map(unified::MetadataItem::ExpiryTime));
|
||||
|
||||
zcash_address::unified::Ufvk::try_from_items(
|
||||
if self.expiry_height().is_some()
|
||||
|| self.expiry_time().is_some()
|
||||
|| !(self.sapling.is_some() || self.orchard.is_some())
|
||||
{
|
||||
Revision::R1
|
||||
} else {
|
||||
Revision::R0
|
||||
},
|
||||
data_items
|
||||
.map(Item::Data)
|
||||
.chain(meta_items.map(Item::Metadata))
|
||||
.collect(),
|
||||
)
|
||||
.expect("UnifiedFullViewingKey should only be constructed safely")
|
||||
}
|
||||
|
||||
/// Derives a Unified Incoming Viewing Key from this Unified Full Viewing Key.
|
||||
|
@ -816,7 +883,11 @@ impl UnifiedFullViewingKey {
|
|||
sapling: self.sapling.as_ref().map(|s| s.to_external_ivk()),
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard: self.orchard.as_ref().map(|o| o.to_ivk(Scope::External)),
|
||||
unknown: Vec::new(),
|
||||
expiry_height: self.expiry_height,
|
||||
expiry_time: self.expiry_time,
|
||||
// We cannot translate unknown data or metadata items, as they may not be relevant to the IVK
|
||||
unknown_data: vec![],
|
||||
unknown_metadata: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -839,6 +910,26 @@ impl UnifiedFullViewingKey {
|
|||
self.orchard.as_ref()
|
||||
}
|
||||
|
||||
/// Returns any unknown data items parsed from the encoded form of the key.
|
||||
pub fn unknown_data(&self) -> &[(u32, Vec<u8>)] {
|
||||
self.unknown_data.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the expiration height for this key.
|
||||
pub fn expiry_height(&self) -> Option<BlockHeight> {
|
||||
self.expiry_height
|
||||
}
|
||||
|
||||
/// Returns the expiration time for this key.
|
||||
pub fn expiry_time(&self) -> Option<u64> {
|
||||
self.expiry_time
|
||||
}
|
||||
|
||||
/// Returns any unknown metadata items parsed from the encoded form of the key.
|
||||
pub fn unknown_metadata(&self) -> &[(u32, Vec<u8>)] {
|
||||
self.unknown_metadata.as_ref()
|
||||
}
|
||||
|
||||
/// Attempts to derive the Unified Address for the given diversifier index and
|
||||
/// receiver types.
|
||||
///
|
||||
|
@ -857,10 +948,9 @@ impl UnifiedFullViewingKey {
|
|||
///
|
||||
/// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features
|
||||
/// required to satisfy the unified address request are not properly enabled.
|
||||
#[allow(unused_mut)]
|
||||
pub fn find_address(
|
||||
&self,
|
||||
mut j: DiversifierIndex,
|
||||
j: DiversifierIndex,
|
||||
request: UnifiedAddressRequest,
|
||||
) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> {
|
||||
self.to_unified_incoming_viewing_key()
|
||||
|
@ -889,8 +979,10 @@ pub struct UnifiedIncomingViewingKey {
|
|||
sapling: Option<::sapling::zip32::IncomingViewingKey>,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard: Option<orchard::keys::IncomingViewingKey>,
|
||||
/// Stores the unrecognized elements of the unified encoding.
|
||||
unknown: Vec<(u32, Vec<u8>)>,
|
||||
unknown_data: Vec<(u32, Vec<u8>)>,
|
||||
expiry_height: Option<BlockHeight>,
|
||||
expiry_time: Option<u64>,
|
||||
unknown_metadata: Vec<(u32, Vec<u8>)>,
|
||||
}
|
||||
|
||||
impl UnifiedIncomingViewingKey {
|
||||
|
@ -906,7 +998,10 @@ impl UnifiedIncomingViewingKey {
|
|||
>,
|
||||
#[cfg(feature = "sapling")] sapling: Option<::sapling::zip32::IncomingViewingKey>,
|
||||
#[cfg(feature = "orchard")] orchard: Option<orchard::keys::IncomingViewingKey>,
|
||||
// TODO: Implement construction of UIVKs with metadata items.
|
||||
unknown_data: Vec<(u32, Vec<u8>)>,
|
||||
expiry_height: Option<BlockHeight>,
|
||||
expiry_time: Option<u64>,
|
||||
unknown_metadata: Vec<(u32, Vec<u8>)>,
|
||||
) -> UnifiedIncomingViewingKey {
|
||||
UnifiedIncomingViewingKey {
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
|
@ -917,7 +1012,10 @@ impl UnifiedIncomingViewingKey {
|
|||
orchard,
|
||||
// We don't allow constructing new UFVKs with unknown items, but we store
|
||||
// this to allow parsing such UFVKs.
|
||||
unknown: vec![],
|
||||
unknown_data,
|
||||
expiry_height,
|
||||
expiry_time,
|
||||
unknown_metadata,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -925,7 +1023,7 @@ impl UnifiedIncomingViewingKey {
|
|||
///
|
||||
/// [ZIP 316]: https://zips.z.cash/zip-0316
|
||||
pub fn decode<P: consensus::Parameters>(params: &P, encoding: &str) -> Result<Self, String> {
|
||||
let (net, ufvk) = unified::Uivk::decode(encoding).map_err(|e| e.to_string())?;
|
||||
let (net, uivk) = unified::Uivk::decode(encoding).map_err(|e| e.to_string())?;
|
||||
let expected_net = params.network_type();
|
||||
if net != expected_net {
|
||||
return Err(format!(
|
||||
|
@ -934,62 +1032,73 @@ impl UnifiedIncomingViewingKey {
|
|||
));
|
||||
}
|
||||
|
||||
Self::parse(&ufvk).map_err(|e| e.to_string())
|
||||
Self::parse(&uivk).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Constructs a unified incoming viewing key from a parsed unified encoding.
|
||||
fn parse(uivk: &Uivk) -> Result<Self, DecodingError> {
|
||||
fn parse(uivk: &zcash_address::unified::Uivk) -> Result<Self, DecodingError> {
|
||||
#[cfg(feature = "orchard")]
|
||||
let mut orchard = None;
|
||||
#[cfg(feature = "sapling")]
|
||||
let mut sapling = None;
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
let mut transparent = None;
|
||||
|
||||
let mut unknown = vec![];
|
||||
let mut unknown_data = vec![];
|
||||
let mut expiry_height = None;
|
||||
let mut expiry_time = None;
|
||||
let mut unknown_metadata = vec![];
|
||||
|
||||
// We can use as-parsed order here for efficiency, because we're breaking out the
|
||||
// receivers we support from the unknown receivers.
|
||||
for receiver in uivk.items_as_parsed() {
|
||||
match receiver {
|
||||
unified::Ivk::Orchard(data) => {
|
||||
Item::Data(unified::Ivk::Orchard(data)) => {
|
||||
#[cfg(feature = "orchard")]
|
||||
{
|
||||
orchard = Some(
|
||||
Option::from(orchard::keys::IncomingViewingKey::from_bytes(data))
|
||||
.ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard))?,
|
||||
.ok_or(DecodingError::KeyDataInvalid(Typecode::ORCHARD))?,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
unknown.push((u32::from(unified::Typecode::Orchard), data.to_vec()));
|
||||
unknown_data.push((u32::from(unified::Typecode::ORCHARD), data.to_vec()));
|
||||
}
|
||||
unified::Ivk::Sapling(data) => {
|
||||
Item::Data(unified::Ivk::Sapling(data)) => {
|
||||
#[cfg(feature = "sapling")]
|
||||
{
|
||||
sapling = Some(
|
||||
Option::from(::sapling::zip32::IncomingViewingKey::from_bytes(data))
|
||||
.ok_or(DecodingError::KeyDataInvalid(Typecode::Sapling))?,
|
||||
.ok_or(DecodingError::KeyDataInvalid(Typecode::SAPLING))?,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "sapling"))]
|
||||
unknown.push((u32::from(unified::Typecode::Sapling), data.to_vec()));
|
||||
unknown_data.push((u32::from(unified::Typecode::SAPLING), data.to_vec()));
|
||||
}
|
||||
unified::Ivk::P2pkh(data) => {
|
||||
Item::Data(unified::Ivk::P2pkh(data)) => {
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
{
|
||||
transparent = Some(
|
||||
legacy::ExternalIvk::deserialize(data)
|
||||
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh))?,
|
||||
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH))?,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
unknown.push((u32::from(unified::Typecode::P2pkh), data.to_vec()));
|
||||
unknown_data.push((u32::from(unified::Typecode::P2PKH), data.to_vec()));
|
||||
}
|
||||
unified::Ivk::Unknown { typecode, data } => {
|
||||
unknown.push((*typecode, data.clone()));
|
||||
Item::Data(unified::Ivk::Unknown { typecode, data }) => {
|
||||
unknown_data.push((*typecode, data.clone()));
|
||||
}
|
||||
Item::Metadata(MetadataItem::ExpiryHeight(h)) => {
|
||||
expiry_height = Some(BlockHeight::from(*h));
|
||||
}
|
||||
Item::Metadata(MetadataItem::ExpiryTime(t)) => {
|
||||
expiry_time = Some(*t);
|
||||
}
|
||||
Item::Metadata(MetadataItem::Unknown { typecode, data }) => {
|
||||
unknown_metadata.push((*typecode, data.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1001,7 +1110,10 @@ impl UnifiedIncomingViewingKey {
|
|||
sapling,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
unknown,
|
||||
unknown_data,
|
||||
expiry_height,
|
||||
expiry_time,
|
||||
unknown_metadata,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1011,37 +1123,61 @@ impl UnifiedIncomingViewingKey {
|
|||
}
|
||||
|
||||
/// Converts this unified incoming viewing key to a unified encoding.
|
||||
fn render(&self) -> Uivk {
|
||||
let items = std::iter::empty().chain(self.unknown.iter().map(|(typecode, data)| {
|
||||
unified::Ivk::Unknown {
|
||||
typecode: *typecode,
|
||||
data: data.clone(),
|
||||
}
|
||||
}));
|
||||
fn render(&self) -> zcash_address::unified::Uivk {
|
||||
let data_items =
|
||||
std::iter::empty().chain(self.unknown_data.iter().map(|(typecode, data)| {
|
||||
unified::Ivk::Unknown {
|
||||
typecode: *typecode,
|
||||
data: data.clone(),
|
||||
}
|
||||
}));
|
||||
#[cfg(feature = "orchard")]
|
||||
let items = items.chain(
|
||||
let data_items = data_items.chain(
|
||||
self.orchard
|
||||
.as_ref()
|
||||
.map(|ivk| ivk.to_bytes())
|
||||
.map(unified::Ivk::Orchard),
|
||||
);
|
||||
#[cfg(feature = "sapling")]
|
||||
let items = items.chain(
|
||||
let data_items = data_items.chain(
|
||||
self.sapling
|
||||
.as_ref()
|
||||
.map(|divk| divk.to_bytes())
|
||||
.map(unified::Ivk::Sapling),
|
||||
);
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
let items = items.chain(
|
||||
let data_items = data_items.chain(
|
||||
self.transparent
|
||||
.as_ref()
|
||||
.map(|tivk| tivk.serialize().try_into().unwrap())
|
||||
.map(unified::Ivk::P2pkh),
|
||||
);
|
||||
|
||||
unified::Uivk::try_from_items(items.collect())
|
||||
.expect("UnifiedIncomingViewingKey should only be constructed safely.")
|
||||
let meta_items = std::iter::empty()
|
||||
.chain(self.unknown_metadata.iter().map(|(typecode, data)| {
|
||||
unified::MetadataItem::Unknown {
|
||||
typecode: *typecode,
|
||||
data: data.clone(),
|
||||
}
|
||||
}))
|
||||
.chain(
|
||||
self.expiry_height
|
||||
.map(|h| unified::MetadataItem::ExpiryHeight(u32::from(h))),
|
||||
)
|
||||
.chain(self.expiry_time.map(unified::MetadataItem::ExpiryTime));
|
||||
|
||||
zcash_address::unified::Uivk::try_from_items(
|
||||
if self.expiry_height.is_some() || self.expiry_time.is_some() {
|
||||
Revision::R1
|
||||
} else {
|
||||
Revision::R0
|
||||
},
|
||||
data_items
|
||||
.map(Item::Data)
|
||||
.chain(meta_items.map(Item::Metadata))
|
||||
.collect(),
|
||||
)
|
||||
.expect("UnifiedIncomingViewingKey should only be constructed safely.")
|
||||
}
|
||||
|
||||
/// Returns the Transparent external IVK, if present.
|
||||
|
@ -1062,6 +1198,26 @@ impl UnifiedIncomingViewingKey {
|
|||
&self.orchard
|
||||
}
|
||||
|
||||
/// Returns any unknown data items parsed from the encoded form of the key.
|
||||
pub fn unknown_data(&self) -> &[(u32, Vec<u8>)] {
|
||||
self.unknown_data.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the expiration height for this key.
|
||||
pub fn expiry_height(&self) -> Option<BlockHeight> {
|
||||
self.expiry_height
|
||||
}
|
||||
|
||||
/// Returns the expiration time for this key.
|
||||
pub fn expiry_time(&self) -> Option<u64> {
|
||||
self.expiry_time
|
||||
}
|
||||
|
||||
/// Returns any unknown metadata items parsed from the encoded form of the key.
|
||||
pub fn unknown_metadata(&self) -> &[(u32, Vec<u8>)] {
|
||||
self.unknown_metadata.as_ref()
|
||||
}
|
||||
|
||||
/// Attempts to derive the Unified Address for the given diversifier index and
|
||||
/// receiver types.
|
||||
///
|
||||
|
@ -1076,7 +1232,7 @@ impl UnifiedIncomingViewingKey {
|
|||
if request.has_orchard {
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
return Err(AddressGenerationError::ReceiverTypeNotSupported(
|
||||
Typecode::Orchard,
|
||||
Typecode::ORCHARD,
|
||||
));
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
|
@ -1084,7 +1240,7 @@ impl UnifiedIncomingViewingKey {
|
|||
let orchard_j = orchard::keys::DiversifierIndex::from(*_j.as_bytes());
|
||||
orchard = Some(oivk.address_at(orchard_j))
|
||||
} else {
|
||||
return Err(AddressGenerationError::KeyNotAvailable(Typecode::Orchard));
|
||||
return Err(AddressGenerationError::KeyNotAvailable(Typecode::ORCHARD));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1093,7 +1249,7 @@ impl UnifiedIncomingViewingKey {
|
|||
if request.has_sapling {
|
||||
#[cfg(not(feature = "sapling"))]
|
||||
return Err(AddressGenerationError::ReceiverTypeNotSupported(
|
||||
Typecode::Sapling,
|
||||
Typecode::SAPLING,
|
||||
));
|
||||
|
||||
#[cfg(feature = "sapling")]
|
||||
|
@ -1106,7 +1262,7 @@ impl UnifiedIncomingViewingKey {
|
|||
.ok_or(AddressGenerationError::InvalidSaplingDiversifierIndex(_j))?,
|
||||
);
|
||||
} else {
|
||||
return Err(AddressGenerationError::KeyNotAvailable(Typecode::Sapling));
|
||||
return Err(AddressGenerationError::KeyNotAvailable(Typecode::SAPLING));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1115,7 +1271,7 @@ impl UnifiedIncomingViewingKey {
|
|||
if request.has_p2pkh {
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
return Err(AddressGenerationError::ReceiverTypeNotSupported(
|
||||
Typecode::P2pkh,
|
||||
Typecode::P2PKH,
|
||||
));
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
|
@ -1131,20 +1287,21 @@ impl UnifiedIncomingViewingKey {
|
|||
.map_err(|_| AddressGenerationError::InvalidTransparentChildIndex(_j))?,
|
||||
);
|
||||
} else {
|
||||
return Err(AddressGenerationError::KeyNotAvailable(Typecode::P2pkh));
|
||||
return Err(AddressGenerationError::KeyNotAvailable(Typecode::P2PKH));
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
let transparent = None;
|
||||
|
||||
UnifiedAddress::from_receivers(
|
||||
Ok(UnifiedAddress::new_internal(
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
#[cfg(feature = "sapling")]
|
||||
sapling,
|
||||
transparent,
|
||||
)
|
||||
.ok_or(AddressGenerationError::ShieldedReceiverRequired)
|
||||
std::cmp::min(self.expiry_height, request.expiry_height),
|
||||
std::cmp::min(self.expiry_time, request.expiry_time),
|
||||
))
|
||||
}
|
||||
|
||||
/// Searches the diversifier space starting at diversifier index `j` for one which will
|
||||
|
@ -1153,7 +1310,6 @@ impl UnifiedIncomingViewingKey {
|
|||
///
|
||||
/// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features
|
||||
/// required to satisfy the unified address request are not properly enabled.
|
||||
#[allow(unused_mut)]
|
||||
pub fn find_address(
|
||||
&self,
|
||||
mut j: DiversifierIndex,
|
||||
|
@ -1181,6 +1337,8 @@ impl UnifiedIncomingViewingKey {
|
|||
Err(AddressGenerationError::InvalidSaplingDiversifierIndex(_)) => {
|
||||
if j.increment().is_err() {
|
||||
return Err(AddressGenerationError::DiversifierSpaceExhausted);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Err(other) => {
|
||||
|
@ -1236,7 +1394,7 @@ mod tests {
|
|||
#[cfg(any(feature = "sapling", feature = "orchard"))]
|
||||
use {
|
||||
super::{UnifiedFullViewingKey, UnifiedIncomingViewingKey},
|
||||
zcash_address::unified::{Encoding, Uivk},
|
||||
zcash_address::unified::Encoding,
|
||||
};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
|
@ -1372,13 +1530,13 @@ mod tests {
|
|||
feature = "sapling",
|
||||
feature = "transparent-inputs"
|
||||
))]
|
||||
assert_eq!(decoded_with_t.unknown.len(), 0);
|
||||
assert_eq!(decoded_with_t.unknown_data.len(), 0);
|
||||
#[cfg(all(
|
||||
feature = "orchard",
|
||||
feature = "sapling",
|
||||
not(feature = "transparent-inputs")
|
||||
))]
|
||||
assert_eq!(decoded_with_t.unknown.len(), 1);
|
||||
assert_eq!(decoded_with_t.unknown_data.len(), 1);
|
||||
|
||||
// Orchard enabled
|
||||
#[cfg(all(
|
||||
|
@ -1386,13 +1544,13 @@ mod tests {
|
|||
not(feature = "sapling"),
|
||||
feature = "transparent-inputs"
|
||||
))]
|
||||
assert_eq!(decoded_with_t.unknown.len(), 1);
|
||||
assert_eq!(decoded_with_t.unknown_data.len(), 1);
|
||||
#[cfg(all(
|
||||
feature = "orchard",
|
||||
not(feature = "sapling"),
|
||||
not(feature = "transparent-inputs")
|
||||
))]
|
||||
assert_eq!(decoded_with_t.unknown.len(), 2);
|
||||
assert_eq!(decoded_with_t.unknown_data.len(), 2);
|
||||
|
||||
// Sapling enabled
|
||||
#[cfg(all(
|
||||
|
@ -1400,13 +1558,13 @@ mod tests {
|
|||
feature = "sapling",
|
||||
feature = "transparent-inputs"
|
||||
))]
|
||||
assert_eq!(decoded_with_t.unknown.len(), 1);
|
||||
assert_eq!(decoded_with_t.unknown_data.len(), 1);
|
||||
#[cfg(all(
|
||||
not(feature = "orchard"),
|
||||
feature = "sapling",
|
||||
not(feature = "transparent-inputs")
|
||||
))]
|
||||
assert_eq!(decoded_with_t.unknown.len(), 2);
|
||||
assert_eq!(decoded_with_t.unknown_data.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1435,7 +1593,10 @@ mod tests {
|
|||
}
|
||||
|
||||
let ua = ufvk
|
||||
.address(d_idx, UnifiedAddressRequest::unsafe_new(false, true, true))
|
||||
.address(
|
||||
d_idx,
|
||||
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, true),
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"unified address generation failed for account {}: {:?}",
|
||||
|
@ -1497,6 +1658,10 @@ mod tests {
|
|||
sapling,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
vec![],
|
||||
);
|
||||
|
||||
let encoded = uivk.render().encode(&NetworkType::Main);
|
||||
|
@ -1517,7 +1682,7 @@ mod tests {
|
|||
assert_eq!(encoded, _encoded_no_t);
|
||||
}
|
||||
|
||||
let decoded = UnifiedIncomingViewingKey::parse(&Uivk::decode(&encoded).unwrap().1).unwrap();
|
||||
let decoded = UnifiedIncomingViewingKey::decode(&MAIN_NETWORK, &encoded).unwrap();
|
||||
let reencoded = decoded.render().encode(&NetworkType::Main);
|
||||
assert_eq!(encoded, reencoded);
|
||||
|
||||
|
@ -1538,7 +1703,7 @@ mod tests {
|
|||
);
|
||||
|
||||
let decoded_with_t =
|
||||
UnifiedIncomingViewingKey::parse(&Uivk::decode(encoded_with_t).unwrap().1).unwrap();
|
||||
UnifiedIncomingViewingKey::decode(&MAIN_NETWORK, encoded_with_t).unwrap();
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
assert_eq!(
|
||||
decoded_with_t.transparent.map(|t| t.serialize()),
|
||||
|
@ -1551,13 +1716,13 @@ mod tests {
|
|||
feature = "sapling",
|
||||
feature = "transparent-inputs"
|
||||
))]
|
||||
assert_eq!(decoded_with_t.unknown.len(), 0);
|
||||
assert_eq!(decoded_with_t.unknown_data.len(), 0);
|
||||
#[cfg(all(
|
||||
feature = "orchard",
|
||||
feature = "sapling",
|
||||
not(feature = "transparent-inputs")
|
||||
))]
|
||||
assert_eq!(decoded_with_t.unknown.len(), 1);
|
||||
assert_eq!(decoded_with_t.unknown_data.len(), 1);
|
||||
|
||||
// Orchard enabled
|
||||
#[cfg(all(
|
||||
|
@ -1565,13 +1730,13 @@ mod tests {
|
|||
not(feature = "sapling"),
|
||||
feature = "transparent-inputs"
|
||||
))]
|
||||
assert_eq!(decoded_with_t.unknown.len(), 1);
|
||||
assert_eq!(decoded_with_t.unknown_data.len(), 1);
|
||||
#[cfg(all(
|
||||
feature = "orchard",
|
||||
not(feature = "sapling"),
|
||||
not(feature = "transparent-inputs")
|
||||
))]
|
||||
assert_eq!(decoded_with_t.unknown.len(), 2);
|
||||
assert_eq!(decoded_with_t.unknown_data.len(), 2);
|
||||
|
||||
// Sapling enabled
|
||||
#[cfg(all(
|
||||
|
@ -1579,13 +1744,13 @@ mod tests {
|
|||
feature = "sapling",
|
||||
feature = "transparent-inputs"
|
||||
))]
|
||||
assert_eq!(decoded_with_t.unknown.len(), 1);
|
||||
assert_eq!(decoded_with_t.unknown_data.len(), 1);
|
||||
#[cfg(all(
|
||||
not(feature = "orchard"),
|
||||
feature = "sapling",
|
||||
not(feature = "transparent-inputs")
|
||||
))]
|
||||
assert_eq!(decoded_with_t.unknown.len(), 2);
|
||||
assert_eq!(decoded_with_t.unknown_data.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1616,7 +1781,10 @@ mod tests {
|
|||
}
|
||||
|
||||
let ua = uivk
|
||||
.address(d_idx, UnifiedAddressRequest::unsafe_new(false, true, true))
|
||||
.address(
|
||||
d_idx,
|
||||
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, true),
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"unified address generation failed for account {}: {:?}",
|
||||
|
|
Loading…
Reference in New Issue