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 <teor@riseup.net> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
parent
d413790838
commit
31382d2a24
|
@ -119,4 +119,19 @@ impl Address {
|
||||||
pub fn is_transparent(&self) -> bool {
|
pub fn is_transparent(&self) -> bool {
|
||||||
matches!(self, Self::Transparent(_))
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ use jsonrpc_core::{self, BoxFuture, Error, ErrorCode, Result};
|
||||||
use jsonrpc_derive::rpc;
|
use jsonrpc_derive::rpc;
|
||||||
use tower::{buffer::Buffer, Service, ServiceExt};
|
use tower::{buffer::Buffer, Service, ServiceExt};
|
||||||
|
|
||||||
use zcash_address;
|
use zcash_address::{self, unified::Encoding, TryFromAddress};
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
amount::Amount,
|
amount::Amount,
|
||||||
|
@ -47,7 +47,7 @@ use crate::methods::{
|
||||||
peer_info::PeerInfo,
|
peer_info::PeerInfo,
|
||||||
submit_block,
|
submit_block,
|
||||||
subsidy::{BlockSubsidy, FundingStream},
|
subsidy::{BlockSubsidy, FundingStream},
|
||||||
validate_address,
|
unified_address, validate_address,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
height_from_signed_int, GetBlockHash, MISSING_BLOCK_ERROR_CODE,
|
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)
|
/// zcashd reference: [`getdifficulty`](https://zcash.github.io/rpc/getdifficulty.html)
|
||||||
#[rpc(name = "getdifficulty")]
|
#[rpc(name = "getdifficulty")]
|
||||||
fn get_difficulty(&self) -> BoxFuture<Result<f64>>;
|
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.
|
/// RPC method implementations.
|
||||||
|
@ -982,6 +991,67 @@ where
|
||||||
}
|
}
|
||||||
.boxed()
|
.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();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(unified_address::Response::new(
|
||||||
|
orchard, sapling, p2pkh, p2sh,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put support functions in a submodule, to keep this file small.
|
// Put support functions in a submodule, to keep this file small.
|
||||||
|
|
|
@ -9,5 +9,6 @@ pub mod peer_info;
|
||||||
pub mod submit_block;
|
pub mod submit_block;
|
||||||
pub mod subsidy;
|
pub mod subsidy;
|
||||||
pub mod transaction;
|
pub mod transaction;
|
||||||
|
pub mod unified_address;
|
||||||
pub mod validate_address;
|
pub mod validate_address;
|
||||||
pub mod zec;
|
pub mod zec;
|
||||||
|
|
|
@ -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<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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,7 +42,7 @@ use crate::methods::{
|
||||||
peer_info::PeerInfo,
|
peer_info::PeerInfo,
|
||||||
submit_block,
|
submit_block,
|
||||||
subsidy::BlockSubsidy,
|
subsidy::BlockSubsidy,
|
||||||
validate_address,
|
unified_address, validate_address,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tests::utils::fake_history_tree,
|
tests::utils::fake_history_tree,
|
||||||
|
@ -403,6 +403,24 @@ pub async fn test_responses<State, ReadState>(
|
||||||
let get_difficulty = get_difficulty.expect("unexpected error in getdifficulty RPC call");
|
let get_difficulty = get_difficulty.expect("unexpected error in getdifficulty RPC call");
|
||||||
|
|
||||||
snapshot_rpc_getdifficulty(get_difficulty, &settings);
|
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.
|
/// 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) {
|
fn snapshot_rpc_getdifficulty(difficulty: f64, settings: &insta::Settings) {
|
||||||
settings.bind(|| insta::assert_json_snapshot!("get_difficulty", difficulty));
|
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)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -1592,3 +1592,59 @@ async fn rpc_getdifficulty() {
|
||||||
|
|
||||||
assert_eq!(format!("{:.2}", get_difficulty.unwrap()), "4096.00");
|
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);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue