Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rpc): Implement z_listunifiedreceivers #6171

Merged
merged 11 commits into from
Feb 20, 2023
15 changes: 15 additions & 0 deletions zebra-chain/src/primitives/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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,
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
74 changes: 72 additions & 2 deletions zebra-rpc/src/methods/get_block_template_rpcs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Result<f64>>;

/// 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<Result<unified_address::Response>>;
}

/// RPC method implementations.
Expand Down Expand Up @@ -982,6 +991,67 @@ where
}
.boxed()
}

fn z_list_unified_receivers(
&self,
address: String,
) -> BoxFuture<Result<unified_address::Response>> {
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();
}
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
_ => (),
}
}

Ok(unified_address::Response::new(
orchard, sapling, p2pkh, p2sh,
))
}
.boxed()
}
}

// Put support functions in a submodule, to keep this file small.
1 change: 1 addition & 0 deletions zebra-rpc/src/methods/get_block_template_rpcs/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
@@ -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,
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
}

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<String> {
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<String> {
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<String> {
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<String> {
match self.p2sh.is_empty() {
true => None,
false => Some(self.p2sh.clone()),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -403,6 +403,24 @@ pub async fn test_responses<State, ReadState>(
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");
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down Expand Up @@ -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)
});
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
56 changes: 56 additions & 0 deletions zebra-rpc/src/methods/tests/vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}