From 31382d2a24285c8f5f0b513f6dc7f64537d3596e Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Mon, 20 Feb 2023 09:22:37 -0300 Subject: [PATCH] feat(rpc): Implement `z_listunifiedreceivers` (#6171) * implement `z_listunifiedreceivers` * add test vector * add snapshots * simplify sapling payment address * send network argument to payment_address method * use expect for impossible address errors * remove network argument * use already present network conversion * add additional snapshot * Derive common traits on the RPC struct --------- Co-authored-by: teor Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- zebra-chain/src/primitives/address.rs | 15 ++++ .../src/methods/get_block_template_rpcs.rs | 74 ++++++++++++++++++- .../methods/get_block_template_rpcs/types.rs | 1 + .../types/unified_address.rs | 62 ++++++++++++++++ .../tests/snapshot/get_block_template_rpcs.rs | 31 +++++++- ...list_unified_receivers_ua1@mainnet_10.snap | 9 +++ ...list_unified_receivers_ua1@testnet_10.snap | 9 +++ ...list_unified_receivers_ua2@mainnet_10.snap | 9 +++ ...list_unified_receivers_ua2@testnet_10.snap | 9 +++ zebra-rpc/src/methods/tests/vectors.rs | 56 ++++++++++++++ 10 files changed, 272 insertions(+), 3 deletions(-) create mode 100644 zebra-rpc/src/methods/get_block_template_rpcs/types/unified_address.rs create mode 100644 zebra-rpc/src/methods/tests/snapshot/snapshots/z_list_unified_receivers_ua1@mainnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshot/snapshots/z_list_unified_receivers_ua1@testnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshot/snapshots/z_list_unified_receivers_ua2@mainnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshot/snapshots/z_list_unified_receivers_ua2@testnet_10.snap diff --git a/zebra-chain/src/primitives/address.rs b/zebra-chain/src/primitives/address.rs index fc8629b6..8753f856 100644 --- a/zebra-chain/src/primitives/address.rs +++ b/zebra-chain/src/primitives/address.rs @@ -119,4 +119,19 @@ impl Address { pub fn is_transparent(&self) -> bool { matches!(self, Self::Transparent(_)) } + + /// Returns the payment address for transparent or sapling addresses. + pub fn payment_address(&self) -> Option { + use zcash_address::{ToAddress, ZcashAddress}; + + match &self { + Self::Transparent(address) => Some(address.to_string()), + Self::Sapling { address, network } => { + let data = address.to_bytes(); + let address = ZcashAddress::from_sapling((*network).into(), data); + Some(address.encode()) + } + Self::Unified { .. } => None, + } + } } diff --git a/zebra-rpc/src/methods/get_block_template_rpcs.rs b/zebra-rpc/src/methods/get_block_template_rpcs.rs index 417758f8..2af2bca7 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs.rs @@ -7,7 +7,7 @@ use jsonrpc_core::{self, BoxFuture, Error, ErrorCode, Result}; use jsonrpc_derive::rpc; use tower::{buffer::Buffer, Service, ServiceExt}; -use zcash_address; +use zcash_address::{self, unified::Encoding, TryFromAddress}; use zebra_chain::{ amount::Amount, @@ -47,7 +47,7 @@ use crate::methods::{ peer_info::PeerInfo, submit_block, subsidy::{BlockSubsidy, FundingStream}, - validate_address, + unified_address, validate_address, }, }, height_from_signed_int, GetBlockHash, MISSING_BLOCK_ERROR_CODE, @@ -194,6 +194,15 @@ pub trait GetBlockTemplateRpc { /// zcashd reference: [`getdifficulty`](https://zcash.github.io/rpc/getdifficulty.html) #[rpc(name = "getdifficulty")] fn get_difficulty(&self) -> BoxFuture>; + + /// Returns the list of individual payment addresses given a unified address. + /// + /// zcashd reference: [`z_listunifiedreceivers`](https://zcash.github.io/rpc/z_listunifiedreceivers.html) + #[rpc(name = "z_listunifiedreceivers")] + fn z_list_unified_receivers( + &self, + address: String, + ) -> BoxFuture>; } /// RPC method implementations. @@ -982,6 +991,67 @@ where } .boxed() } + + fn z_list_unified_receivers( + &self, + address: String, + ) -> BoxFuture> { + use zcash_address::unified::Container; + + async move { + let (network, unified_address): ( + zcash_address::Network, + zcash_address::unified::Address, + ) = zcash_address::unified::Encoding::decode(address.clone().as_str()).map_err( + |error| Error { + code: ErrorCode::ServerError(0), + message: error.to_string(), + data: None, + }, + )?; + + let mut p2pkh = String::new(); + let mut p2sh = String::new(); + let mut orchard = String::new(); + let mut sapling = String::new(); + + for item in unified_address.items() { + match item { + zcash_address::unified::Receiver::Orchard(_data) => { + let addr = zcash_address::unified::Address::try_from_items(vec![item]) + .expect("using data already decoded as valid"); + orchard = addr.encode(&network); + } + zcash_address::unified::Receiver::Sapling(data) => { + let addr = + zebra_chain::primitives::Address::try_from_sapling(network, data) + .expect("using data already decoded as valid"); + sapling = addr.payment_address().unwrap_or_default(); + } + zcash_address::unified::Receiver::P2pkh(data) => { + let addr = zebra_chain::primitives::Address::try_from_transparent_p2pkh( + network, data, + ) + .expect("using data already decoded as valid"); + p2pkh = addr.payment_address().unwrap_or_default(); + } + zcash_address::unified::Receiver::P2sh(data) => { + let addr = zebra_chain::primitives::Address::try_from_transparent_p2sh( + network, data, + ) + .expect("using data already decoded as valid"); + p2sh = addr.payment_address().unwrap_or_default(); + } + _ => (), + } + } + + Ok(unified_address::Response::new( + orchard, sapling, p2pkh, p2sh, + )) + } + .boxed() + } } // Put support functions in a submodule, to keep this file small. diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types.rs index 305ed58c..c52c196a 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/types.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types.rs @@ -9,5 +9,6 @@ pub mod peer_info; pub mod submit_block; pub mod subsidy; pub mod transaction; +pub mod unified_address; pub mod validate_address; pub mod zec; diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types/unified_address.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types/unified_address.rs new file mode 100644 index 00000000..a8d5c3a1 --- /dev/null +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types/unified_address.rs @@ -0,0 +1,62 @@ +//! Types for unified addresses + +/// `z_listunifiedreceivers` response +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Response { + #[serde(skip_serializing_if = "String::is_empty")] + orchard: String, + #[serde(skip_serializing_if = "String::is_empty")] + sapling: String, + #[serde(skip_serializing_if = "String::is_empty")] + p2pkh: String, + #[serde(skip_serializing_if = "String::is_empty")] + p2sh: String, +} + +impl Response { + /// Create a new response for z_listunifiedreceivers given individual addresses. + pub fn new(orchard: String, sapling: String, p2pkh: String, p2sh: String) -> Response { + Response { + orchard, + sapling, + p2pkh, + p2sh, + } + } + + #[cfg(test)] + /// Return the orchard payment address from a response, if any. + pub fn orchard(&self) -> Option { + match self.orchard.is_empty() { + true => None, + false => Some(self.orchard.clone()), + } + } + + #[cfg(test)] + /// Return the sapling payment address from a response, if any. + pub fn sapling(&self) -> Option { + match self.sapling.is_empty() { + true => None, + false => Some(self.sapling.clone()), + } + } + + #[cfg(test)] + /// Return the p2pkh payment address from a response, if any. + pub fn p2pkh(&self) -> Option { + match self.p2pkh.is_empty() { + true => None, + false => Some(self.p2pkh.clone()), + } + } + + #[cfg(test)] + /// Return the p2sh payment address from a response, if any. + pub fn p2sh(&self) -> Option { + match self.p2sh.is_empty() { + true => None, + false => Some(self.p2sh.clone()), + } + } +} diff --git a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs index 8aef0649..1b0035b4 100644 --- a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs @@ -42,7 +42,7 @@ use crate::methods::{ peer_info::PeerInfo, submit_block, subsidy::BlockSubsidy, - validate_address, + unified_address, validate_address, }, }, tests::utils::fake_history_tree, @@ -403,6 +403,24 @@ pub async fn test_responses( let get_difficulty = get_difficulty.expect("unexpected error in getdifficulty RPC call"); snapshot_rpc_getdifficulty(get_difficulty, &settings); + + let ua1 = String::from("u1l8xunezsvhq8fgzfl7404m450nwnd76zshscn6nfys7vyz2ywyh4cc5daaq0c7q2su5lqfh23sp7fkf3kt27ve5948mzpfdvckzaect2jtte308mkwlycj2u0eac077wu70vqcetkxf"); + let z_list_unified_receivers = + tokio::spawn(get_block_template_rpc.z_list_unified_receivers(ua1)) + .await + .expect("unexpected panic in z_list_unified_receivers RPC task") + .expect("unexpected error in z_list_unified_receivers RPC call"); + + snapshot_rpc_z_listunifiedreceivers("ua1", z_list_unified_receivers, &settings); + + let ua2 = String::from("u1uf4qsmh037x2jp6k042h9d2w22wfp39y9cqdf8kcg0gqnkma2gf4g80nucnfeyde8ev7a6kf0029gnwqsgadvaye9740gzzpmr67nfkjjvzef7rkwqunqga4u4jges4tgptcju5ysd0"); + let z_list_unified_receivers = + tokio::spawn(get_block_template_rpc.z_list_unified_receivers(ua2)) + .await + .expect("unexpected panic in z_list_unified_receivers RPC task") + .expect("unexpected error in z_list_unified_receivers RPC call"); + + snapshot_rpc_z_listunifiedreceivers("ua2", z_list_unified_receivers, &settings); } /// Snapshot `getblockcount` response, using `cargo insta` and JSON serialization. @@ -484,3 +502,14 @@ fn snapshot_rpc_validateaddress( fn snapshot_rpc_getdifficulty(difficulty: f64, settings: &insta::Settings) { settings.bind(|| insta::assert_json_snapshot!("get_difficulty", difficulty)); } + +/// Snapshot `snapshot_rpc_z_listunifiedreceivers` response, using `cargo insta` and JSON serialization. +fn snapshot_rpc_z_listunifiedreceivers( + variant: &'static str, + response: unified_address::Response, + settings: &insta::Settings, +) { + settings.bind(|| { + insta::assert_json_snapshot!(format!("z_list_unified_receivers_{variant}"), response) + }); +} diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/z_list_unified_receivers_ua1@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/z_list_unified_receivers_ua1@mainnet_10.snap new file mode 100644 index 00000000..dbcc72bc --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/z_list_unified_receivers_ua1@mainnet_10.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +assertion_line: 533 +expression: response +--- +{ + "sapling": "zs1mrhc9y7jdh5r9ece8u5khgvj9kg0zgkxzdduyv0whkg7lkcrkx5xqem3e48avjq9wn2rukydkwn", + "p2pkh": "t1V9mnyk5Z5cTNMCkLbaDwSskgJZucTLdgW" +} diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/z_list_unified_receivers_ua1@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/z_list_unified_receivers_ua1@testnet_10.snap new file mode 100644 index 00000000..dbcc72bc --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/z_list_unified_receivers_ua1@testnet_10.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +assertion_line: 533 +expression: response +--- +{ + "sapling": "zs1mrhc9y7jdh5r9ece8u5khgvj9kg0zgkxzdduyv0whkg7lkcrkx5xqem3e48avjq9wn2rukydkwn", + "p2pkh": "t1V9mnyk5Z5cTNMCkLbaDwSskgJZucTLdgW" +} diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/z_list_unified_receivers_ua2@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/z_list_unified_receivers_ua2@mainnet_10.snap new file mode 100644 index 00000000..5e5e9772 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/z_list_unified_receivers_ua2@mainnet_10.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +assertion_line: 533 +expression: response +--- +{ + "orchard": "u10c5q7qkhu6f0ktaz7jqu4sfsujg0gpsglzudmy982mku7t0uma52jmsaz8h24a3wa7p0jwtsjqt8shpg25cvyexzlsw3jtdz4v6w70lv", + "p2sh": "t3dvVE3SQEi7kqNzwrfNePxZ1d4hUyztBA1" +} diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/z_list_unified_receivers_ua2@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/z_list_unified_receivers_ua2@testnet_10.snap new file mode 100644 index 00000000..5e5e9772 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/z_list_unified_receivers_ua2@testnet_10.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +assertion_line: 533 +expression: response +--- +{ + "orchard": "u10c5q7qkhu6f0ktaz7jqu4sfsujg0gpsglzudmy982mku7t0uma52jmsaz8h24a3wa7p0jwtsjqt8shpg25cvyexzlsw3jtdz4v6w70lv", + "p2sh": "t3dvVE3SQEi7kqNzwrfNePxZ1d4hUyztBA1" +} diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index a524ef78..e3a9f8aa 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -1592,3 +1592,59 @@ async fn rpc_getdifficulty() { assert_eq!(format!("{:.2}", get_difficulty.unwrap()), "4096.00"); } + +#[cfg(feature = "getblocktemplate-rpcs")] +#[tokio::test(flavor = "multi_thread")] +async fn rpc_z_listunifiedreceivers() { + let _init_guard = zebra_test::init(); + + use zebra_chain::{chain_sync_status::MockSyncStatus, chain_tip::mock::MockChainTip}; + use zebra_network::address_book_peers::MockAddressBookPeers; + + let _init_guard = zebra_test::init(); + + let (mock_chain_tip, _mock_chain_tip_sender) = MockChainTip::new(); + + // Init RPC + let get_block_template_rpc = get_block_template_rpcs::GetBlockTemplateRpcImpl::new( + Mainnet, + Default::default(), + Buffer::new(MockService::build().for_unit_tests(), 1), + MockService::build().for_unit_tests(), + mock_chain_tip, + MockService::build().for_unit_tests(), + MockSyncStatus::default(), + MockAddressBookPeers::default(), + ); + + // invalid address + assert!(get_block_template_rpc + .z_list_unified_receivers("invalid string for an address".to_string()) + .await + .is_err()); + + // address taken from https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/test-vectors/zcash/unified_address.json#L4 + let response = get_block_template_rpc.z_list_unified_receivers("u1l8xunezsvhq8fgzfl7404m450nwnd76zshscn6nfys7vyz2ywyh4cc5daaq0c7q2su5lqfh23sp7fkf3kt27ve5948mzpfdvckzaect2jtte308mkwlycj2u0eac077wu70vqcetkxf".to_string()).await.unwrap(); + assert_eq!(response.orchard(), None); + assert_eq!( + response.sapling(), + Some(String::from( + "zs1mrhc9y7jdh5r9ece8u5khgvj9kg0zgkxzdduyv0whkg7lkcrkx5xqem3e48avjq9wn2rukydkwn" + )) + ); + assert_eq!( + response.p2pkh(), + Some(String::from("t1V9mnyk5Z5cTNMCkLbaDwSskgJZucTLdgW")) + ); + assert_eq!(response.p2sh(), None); + + // address taken from https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/test-vectors/zcash/unified_address.json#L39 + let response = get_block_template_rpc.z_list_unified_receivers("u12acx92vw49jek4lwwnjtzm0cssn2wxfneu7ryj4amd8kvnhahdrq0htsnrwhqvl92yg92yut5jvgygk0rqfs4lgthtycsewc4t57jyjn9p2g6ffxek9rdg48xe5kr37hxxh86zxh2ef0u2lu22n25xaf3a45as6mtxxlqe37r75mndzu9z2fe4h77m35c5mrzf4uqru3fjs39ednvw9ay8nf9r8g9jx8rgj50mj098exdyq803hmqsek3dwlnz4g5whc88mkvvjnfmjldjs9hm8rx89ctn5wxcc2e05rcz7m955zc7trfm07gr7ankf96jxwwfcqppmdefj8gc6508gep8ndrml34rdpk9tpvwzgdcv7lk2d70uh5jqacrpk6zsety33qcc554r3cls4ajktg03d9fye6exk8gnve562yadzsfmfh9d7v6ctl5ufm9ewpr6se25c47huk4fh2hakkwerkdd2yy3093snsgree5lt6smejfvse8v".to_string()).await.unwrap(); + assert_eq!(response.orchard(), Some(String::from("u10c5q7qkhu6f0ktaz7jqu4sfsujg0gpsglzudmy982mku7t0uma52jmsaz8h24a3wa7p0jwtsjqt8shpg25cvyexzlsw3jtdz4v6w70lv"))); + assert_eq!(response.sapling(), None); + assert_eq!( + response.p2pkh(), + Some(String::from("t1dMjwmwM2a6NtavQ6SiPP8i9ofx4cgfYYP")) + ); + assert_eq!(response.p2sh(), None); +}