diff --git a/Cargo.lock b/Cargo.lock index 89cced7b6..aa1d6e47c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2941,6 +2941,7 @@ dependencies = [ "tempfile", "tokio", "tower 0.4.13", + "tracing", "zaino-common", "zaino-fetch", "zaino-proto", @@ -8710,6 +8711,7 @@ dependencies = [ "memuse", "nonempty", "rand_core 0.6.4", + "sapling-crypto", "secrecy", "subtle", "tracing", diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 4153201d0..7c4f76684 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -10,6 +10,11 @@ edition = { workspace = true } license = { workspace = true } version = { workspace = true } +[dependencies] +zaino-fetch = { workspace = true } +zaino-testutils = { workspace = true } +tracing.workspace = true + [dev-dependencies] anyhow = { workspace = true } @@ -23,7 +28,7 @@ zaino-state = { workspace = true, features = ["test_dependencies"] } zebra-chain.workspace = true zebra-state.workspace = true zebra-rpc.workspace = true -zip32 = {workspace = true} +zip32 = { workspace = true } core2 = { workspace = true } prost = { workspace = true } diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index 13de7609a..210f68663 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -1 +1,132 @@ //! Helpers for integration-tests go here. +//! +//! This crate also exposes test-vectors. + +pub mod rpc { + pub mod json_rpc { + pub const VALID_P2PKH_ADDRESS: &str = "tmVqEASZxBNKFTbmASZikGa5fPLkd68iJyx"; + pub const VALID_P2SH_ADDRESS: &str = "t2MjoXQ2iDrjG9QXNZNCY9io8ecN4FJYK1u"; + + pub const VALID_SPROUT_ADDRESS: &str = "ztfhKyLouqi8sSwjRm4YMQdWPjTmrJ4QgtziVQ1Kd1e9EsRHYKofjoJdF438FwcUQnix8yrbSrzPpJJNABewgNffs5d4YZJ"; + pub const VALID_PAYING_KEY: &str = + "c8e8797f1fb5e9cf6b2d000177c5994119279a2629970a4f669aed1362a4cca5"; + pub const VALID_TRANSMISSION_KEY: &str = + "480f78d61bdd7fc4b4edeef9f6305b29753057ab1008d42ded1a3364dac2d83c"; + + pub const VALID_SAPLING_ADDRESS: &str = "zregtestsapling1jalqhycwumq3unfxlzyzcktq3n478n82k2wacvl8gwfxk6ahshkxmtp2034qj28n7gl92ka5wca"; + pub const VALID_DIVERSIFIER: &str = "977e0b930ee6c11e4d26f8"; + pub const VALID_DIVERSIFIED_TRANSMISSION_KEY: &str = + "553ef2f328096a7c2aac6dec85b76b6b9243e733dc9db2eacce3eb8c60592c88"; + + pub const VALID_UNIFIED_ADDRESS: &str = "uregtest1njwg60x0jarhyuuxrcdvw854p68cgdfe85822lmclc7z9vy9xqr7t49n3d97k2dwlee82skwwe0ens0rc06p4vr04tvd3j9ckl3qry83ckay4l4ngdq9atg7vuj9z58tfjs0mnsgyrnprtqfv8almu564z498zy6tp2aa569tk8fyhdazyhytel2m32awe4kuy6qq996um3ljaajj36"; + } + + pub mod z_validate_address { + use std::future::Future; + + use zaino_fetch::jsonrpsee::response::z_validate_address::{ + KnownZValidateAddress, ValidZValidateAddress, ZValidateAddressResponse, + }; + use zaino_testutils::ValidatorKind; + + use crate::rpc::json_rpc::{ + VALID_DIVERSIFIED_TRANSMISSION_KEY, VALID_DIVERSIFIER, VALID_P2PKH_ADDRESS, + VALID_P2SH_ADDRESS, VALID_SAPLING_ADDRESS, VALID_UNIFIED_ADDRESS, + }; + + pub fn assert_known_valid_eq( + resp: ZValidateAddressResponse, + expected: ValidZValidateAddress, + label: &str, + ) { + match resp { + ZValidateAddressResponse::Known(KnownZValidateAddress::Valid(actual)) => { + assert_eq!(actual, expected, "mismatch for {label}") + } + other => panic!( + "Unexpected ZValidateAddressResponse for {label}: {:#?}", + other + ), + } + } + + pub async fn run_z_validate_suite(rpc_call: &F) + where + // Any callable that takes an address and returns the response (you can unwrap inside) + F: Fn(String) -> Fut, + Fut: Future, + { + // P2PKH + let expected_p2pkh = ValidZValidateAddress::p2pkh(VALID_P2PKH_ADDRESS.to_string()); + assert_known_valid_eq( + rpc_call(VALID_P2PKH_ADDRESS.to_string()).await, + expected_p2pkh, + "P2PKH", + ); + + // P2SH + let expected_p2sh = ValidZValidateAddress::p2sh(VALID_P2SH_ADDRESS.to_string()); + assert_known_valid_eq( + rpc_call(VALID_P2SH_ADDRESS.to_string()).await, + expected_p2sh, + "P2SH", + ); + + // Note: It could be the case that Zaino needs to support Sprout. For now, it's been disabled. + + // let expected_sprout = ZValidateAddress::sprout("ztfhKyLouqi8sSwjRm4YMQdWPjTmrJ4QgtziVQ1Kd1e9EsRHYKofjoJdF438FwcUQnix8yrbSrzPpJJNABewgNffs5d4YZJ".to_string(), "c8e8797f1fb5e9cf6b2d000177c5994119279a2629970a4f669aed1362a4cca5".to_string(), "480f78d61bdd7fc4b4edeef9f6305b29753057ab1008d42ded1a3364dac2d83c".to_string()); + + // let fs_sprout = zcashd_subscriber + // .z_validate_address("ztfhKyLouqi8sSwjRm4YMQdWPjTmrJ4QgtziVQ1Kd1e9EsRHYKofjoJdF438FwcUQnix8yrbSrzPpJJNABewgNffs5d4YZJ".to_string()) + // .await + // .unwrap(); + + // assert_eq!(fs_sprout, expected_sprout); + + // Sapling (differs by validator) + + // Unified (differs by validator) + let expected_unified = + ValidZValidateAddress::unified(VALID_UNIFIED_ADDRESS.to_string()); + assert_known_valid_eq( + rpc_call(VALID_UNIFIED_ADDRESS.to_string()).await, + expected_unified, + "Unified", + ); + + // Invalids + let by_len = rpc_call("t1123456789ABCDEFGHJKLMNPQRSTUVWXY".to_string()).await; + let all_zeroes = rpc_call("t1000000000000000000000000000000000".to_string()).await; + assert_eq!(by_len, ZValidateAddressResponse::invalid()); + assert_eq!(all_zeroes, ZValidateAddressResponse::invalid()); + } + + pub async fn run_z_validate_sapling(rpc_call: &F) + where + // Any callable that takes an address and returns the response (you can unwrap inside) + F: Fn(String) -> Fut, + Fut: Future, + { + // Sapling (differs by validator) + let expected_sapling = ValidZValidateAddress::sapling( + VALID_SAPLING_ADDRESS.to_string(), + Some(VALID_DIVERSIFIER.to_string()), + Some(VALID_DIVERSIFIED_TRANSMISSION_KEY.to_string()), + ); + } + + pub async fn run_z_validate_sapling_legacy(rpc_call: &F) + where + // Any callable that takes an address and returns the response (you can unwrap inside) + F: Fn(String) -> Fut, + Fut: Future, + { + // Sapling (differs by validator) + let expected_sapling = ValidZValidateAddress::sapling( + VALID_SAPLING_ADDRESS.to_string(), + None::, + None::, + ); + } + } +} diff --git a/integration-tests/tests/fetch_service.rs b/integration-tests/tests/fetch_service.rs index 36e038d15..59b661b5c 100644 --- a/integration-tests/tests/fetch_service.rs +++ b/integration-tests/tests/fetch_service.rs @@ -994,6 +994,44 @@ async fn fetch_service_validate_address(validator: &ValidatorKi test_manager.close().await; } +/// This test only runs validation for regtest addresses. +/// The following addresses were generated with `zcashd` on regtest: +/// +/// - P2PKH: `tmVqEASZxBNKFTbmASZikGa5fPLkd68iJyx` +/// - P2SH: `t2MjoXQ2iDrjG9QXNZNCY9io8ecN4FJYK1u` +/// - Sprout: `ztfhKyLouqi8sSwjRm4YMQdWPjTmrJ4QgtziVQ1Kd1e9EsRHYKofjoJdF438FwcUQnix8yrbSrzPpJJNABewgNffs5d4YZJ` +/// - Sapling: `zregtestsapling1jalqhycwumq3unfxlzyzcktq3n478n82k2wacvl8gwfxk6ahshkxmtp2034qj28n7gl92ka5wca` +/// - unified: `uregtest1njwg60x0jarhyuuxrcdvw854p68cgdfe85822lmclc7z9vy9xqr7t49n3d97k2dwlee82skwwe0ens0rc06p4vr04tvd3j9ckl3qry83ckay4l4ngdq9atg7vuj9z58tfjs0mnsgyrnprtqfv8almu564z498zy6tp2aa569tk8fyhdazyhytel2m32awe4kuy6qq996um3ljaajj36` +/// - invalid (length shorter than expected by 1 char): `t1123456789ABCDEFGHJKLMNPQRSTUVWXY` +/// - invalid (all zeroes): `t1000000000000000000000000000000000` +#[allow(deprecated)] +async fn fetch_service_z_validate_address(validator: &ValidatorKind) { + let mut test_manager = TestManager::::launch( + validator, + &BackendType::Fetch, + None, + None, + None, + true, + false, + false, + ) + .await + .unwrap(); + + let fetch_service_subscriber = test_manager.service_subscriber.take().unwrap(); + + let rpc_call = |addr: String| { + let subscriber = &fetch_service_subscriber; + async move { subscriber.z_validate_address(addr).await.unwrap() } + }; + + integration_tests::rpc::z_validate_address::run_z_validate_suite(&rpc_call).await; + integration_tests::rpc::z_validate_address::run_z_validate_sapling_legacy(&rpc_call).await; + + test_manager.close().await; +} + #[allow(deprecated)] async fn fetch_service_get_block_nullifiers(validator: &ValidatorKind) { let mut test_manager = @@ -1795,6 +1833,11 @@ mod zcashd { pub(crate) async fn validate_address() { fetch_service_validate_address::(&ValidatorKind::Zcashd).await; } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + pub(crate) async fn z_validate_address() { + fetch_service_z_validate_address(&ValidatorKind::Zcashd).await; + } } mod get { @@ -2068,6 +2111,11 @@ mod zebrad { pub(crate) async fn validate_address() { fetch_service_validate_address::(&ValidatorKind::Zebrad).await; } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + pub(crate) async fn z_validate_address() { + fetch_service_z_validate_address(&ValidatorKind::Zebrad).await; + } } mod get { diff --git a/integration-tests/tests/json_server.rs b/integration-tests/tests/json_server.rs index c349b600c..cabfc0810 100644 --- a/integration-tests/tests/json_server.rs +++ b/integration-tests/tests/json_server.rs @@ -899,6 +899,22 @@ mod zcashd { validate_address_inner().await; } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn z_validate_address() { + let (mut test_manager, _zcashd_service, zcashd_subscriber, _zaino_service, _zaino_sub) = + create_test_manager_and_fetch_services(false).await; + + let rpc_call = |addr: String| { + let subscriber = &zcashd_subscriber; + async move { subscriber.z_validate_address(addr).await.unwrap() } + }; + + integration_tests::rpc::z_validate_address::run_z_validate_suite(&rpc_call).await; + integration_tests::rpc::z_validate_address::run_z_validate_sapling(&rpc_call).await; + + test_manager.close().await; + } + #[tokio::test(flavor = "multi_thread")] async fn z_get_block() { z_get_block_inner().await; diff --git a/integration-tests/tests/state_service.rs b/integration-tests/tests/state_service.rs index 7b189df1b..60532ec27 100644 --- a/integration-tests/tests/state_service.rs +++ b/integration-tests/tests/state_service.rs @@ -1635,9 +1635,58 @@ mod zebra { } } - mod z { - use zcash_local_net::validator::zebrad::Zebrad; + mod validation { + + use zaino_state::ZcashIndexer; + use zaino_testutils::ValidatorKind; + + use crate::create_test_manager_and_services; + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + pub(crate) async fn z_validate_address() { + // const VALID_P2PKH_ADDRESS: &str = "tmVqEASZxBNKFTbmASZikGa5fPLkd68iJyx"; + // const VALID_P2SH_ADDRESS: &str = "t2MjoXQ2iDrjG9QXNZNCY9io8ecN4FJYK1u"; + + // const VALID_SPROUT_ADDRESS: &str = "ztfhKyLouqi8sSwjRm4YMQdWPjTmrJ4QgtziVQ1Kd1e9EsRHYKofjoJdF438FwcUQnix8yrbSrzPpJJNABewgNffs5d4YZJ"; + // const VALID_PAYING_KEY: &str = "VALID_SPROUT_ADDRESS"; + // const VALID_TRANSMISSION_KEY: &str = + // "480f78d61bdd7fc4b4edeef9f6305b29753057ab1008d42ded1a3364dac2d83c"; + + // const VALID_SAPLING_ADDRESS: &str = "zregtestsapling1jalqhycwumq3unfxlzyzcktq3n478n82k2wacvl8gwfxk6ahshkxmtp2034qj28n7gl92ka5wca"; + // const VALID_DIVERSIFIER: &str = "977e0b930ee6c11e4d26f8"; + // const VALID_DIVERSIFIED_TRANSMISSION_KEY: &str = + // "553ef2f328096a7c2aac6dec85b76b6b9243e733dc9db2eacce3eb8c60592c88"; + + // const VALID_UNIFIED_ADDRESS: &str = "uregtest1njwg60x0jarhyuuxrcdvw854p68cgdfe85822lmclc7z9vy9xqr7t49n3d97k2dwlee82skwwe0ens0rc06p4vr04tvd3j9ckl3qry83ckay4l4ngdq9atg7vuj9z58tfjs0mnsgyrnprtqfv8almu564z498zy6tp2aa569tk8fyhdazyhytel2m32awe4kuy6qq996um3ljaajj36"; + + let ( + mut test_manager, + _fetch_service, + _fetch_service_subscriber, + _state_service, + state_service_subscriber, + ) = create_test_manager_and_services( + &ValidatorKind::Zebrad, + None, + true, + true, + None, + ) + .await; + + let rpc_call = |addr: String| { + let subscriber = &state_service_subscriber; + async move { subscriber.z_validate_address(addr).await.unwrap() } + }; + integration_tests::rpc::z_validate_address::run_z_validate_suite(&rpc_call).await; + integration_tests::rpc::z_validate_address::run_z_validate_sapling(&rpc_call).await; + + test_manager.close().await; + } + } + + mod z { use super::*; #[tokio::test(flavor = "multi_thread")] diff --git a/zaino-fetch/src/jsonrpsee/connector.rs b/zaino-fetch/src/jsonrpsee/connector.rs index 8c7148ccc..18ead7cee 100644 --- a/zaino-fetch/src/jsonrpsee/connector.rs +++ b/zaino-fetch/src/jsonrpsee/connector.rs @@ -32,6 +32,7 @@ use crate::jsonrpsee::{ block_subsidy::GetBlockSubsidy, mining_info::GetMiningInfoWire, peer_info::GetPeerInfo, + z_validate_address::{ZValidateAddressError, ZValidateAddressResponse}, GetBalanceError, GetBalanceResponse, GetBlockCountResponse, GetBlockError, GetBlockHash, GetBlockResponse, GetBlockchainInfoResponse, GetInfoResponse, GetMempoolInfoResponse, GetSubtreesError, GetSubtreesResponse, GetTransactionResponse, GetTreestateError, @@ -652,6 +653,23 @@ impl JsonRpSeeConnector { self.send_request("validateaddress", params).await } + /// Return information about the given address. + /// + /// # Parameters + /// - `address`: (string, required) The address to validate. + /// + /// zcashd reference: [`z_validateaddress`](https://zcash.github.io/rpc/z_validateaddress.html) + /// method: post + /// tags: util + pub async fn z_validate_address( + &self, + address: String, + ) -> Result> { + tracing::debug!("Sending jsonrpsee connecter z_validate_address."); + let params = vec![serde_json::to_value(address).map_err(RpcRequestError::JsonRpc)?]; + self.send_request("z_validateaddress", params).await + } + /// Returns all transaction ids in the memory pool, as a JSON array. /// /// zcashd reference: [`getrawmempool`](https://zcash.github.io/rpc/getrawmempool.html) diff --git a/zaino-fetch/src/jsonrpsee/response.rs b/zaino-fetch/src/jsonrpsee/response.rs index fbd5787c8..f7faf415c 100644 --- a/zaino-fetch/src/jsonrpsee/response.rs +++ b/zaino-fetch/src/jsonrpsee/response.rs @@ -10,6 +10,7 @@ pub mod block_subsidy; pub mod common; pub mod mining_info; pub mod peer_info; +pub mod z_validate_address; use std::{convert::Infallible, num::ParseIntError}; diff --git a/zaino-fetch/src/jsonrpsee/response/z_validate_address.rs b/zaino-fetch/src/jsonrpsee/response/z_validate_address.rs new file mode 100644 index 000000000..8713c1a3a --- /dev/null +++ b/zaino-fetch/src/jsonrpsee/response/z_validate_address.rs @@ -0,0 +1,812 @@ +//! Types associated with the `z_validateaddress` RPC request. + +use std::collections::BTreeMap; + +use serde::{ + de, + ser::{SerializeMap, SerializeStruct}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_json::Value; + +use crate::jsonrpsee::connector::{ResponseToError, RpcError}; + +/// Error type for the `z_validateaddress` RPC. +#[derive(Debug, thiserror::Error)] +pub enum ZValidateAddressError { + /// Invalid address encoding + #[error("Invalid encoding: {0}")] + InvalidEncoding(String), +} + +/// Response type for the `z_validateaddress` RPC. +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(untagged)] +pub enum ZValidateAddressResponse { + /// A response containing a known JSON schema. + Known(KnownZValidateAddress), + + /// A response containing an unknown JSON schema. + Unknown(BTreeMap), +} + +impl ZValidateAddressResponse { + /// Constructs a response with a [`ZValidateAddressResponse::Unknown`] schema. + pub fn unknown() -> Self { + ZValidateAddressResponse::Unknown(BTreeMap::new()) + } + + /// Constructs an invalid address object. + pub fn invalid() -> Self { + ZValidateAddressResponse::Known(KnownZValidateAddress::Invalid( + InvalidZValidateAddress::new(), + )) + } + + /// Constructs a response for a valid P2PKH address. + pub fn p2pkh(address: impl Into) -> Self { + ZValidateAddressResponse::Known(KnownZValidateAddress::Valid(ValidZValidateAddress::p2pkh( + address, + ))) + } + + /// Constructs a response for a valid P2SH address. + pub fn p2sh(address: impl Into) -> Self { + ZValidateAddressResponse::Known(KnownZValidateAddress::Valid(ValidZValidateAddress::p2sh( + address, + ))) + } + + /// Constructs a response for a valid Sapling address. + pub fn sapling( + address: impl Into, + diversifier: Option, + diversified_transmission_key: Option, + ) -> Self { + ZValidateAddressResponse::Known(KnownZValidateAddress::Valid( + ValidZValidateAddress::sapling(address, diversifier, diversified_transmission_key), + )) + } + + /// Constructs a response for a valid Sprout address. + pub fn sprout( + address: impl Into, + paying_key: Option, + transmission_key: Option, + ) -> Self { + ZValidateAddressResponse::Known(KnownZValidateAddress::Valid( + ValidZValidateAddress::sprout(address, paying_key, transmission_key), + )) + } + + /// Constructs a response for a valid Unified address. + pub fn unified(address: impl Into) -> Self { + ZValidateAddressResponse::Known(KnownZValidateAddress::Valid( + ValidZValidateAddress::unified(address), + )) + } +} + +impl ResponseToError for ZValidateAddressResponse { + type RpcError = ZValidateAddressError; +} + +impl TryFrom for ZValidateAddressError { + type Error = RpcError; + + fn try_from(value: RpcError) -> Result { + Err(value) + } +} + +/// An enum that represents the known JSON schema for the `z_validateaddress` RPC. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum KnownZValidateAddress { + /// Valid response. + Valid(ValidZValidateAddress), + + /// Invalid response. + Invalid(InvalidZValidateAddress), +} + +/// The "invalid" shape is just `{ "isvalid": false }`. +#[derive(Clone, Debug, PartialEq, Default)] +pub struct InvalidZValidateAddress; + +impl InvalidZValidateAddress { + /// Creates a new InvalidZValidateAddress. + pub fn new() -> Self { + Self + } +} + +impl Serialize for InvalidZValidateAddress { + fn serialize(&self, s: S) -> Result { + let mut serialized_struct = s.serialize_struct("InvalidZValidateAddress", 1)?; + serialized_struct.serialize_field("isvalid", &false)?; + serialized_struct.end() + } +} + +impl<'de> Deserialize<'de> for InvalidZValidateAddress { + fn deserialize>(d: D) -> Result { + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct Raw { + #[serde(rename = "isvalid")] + is_valid: bool, + } + let Raw { is_valid } = Raw::deserialize(d)?; + if is_valid { + Err(de::Error::custom("invalid branch must have isvalid=false")) + } else { + Ok(InvalidZValidateAddress) + } + } +} + +// TODO: `AddressData` should probably be private and exposed through an `inner` accessor. +/// Represents the "valid" response. The other fields are part of [`AddressData`]. +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(transparent)] +pub struct ValidZValidateAddress(AddressData); + +impl<'de> Deserialize<'de> for ValidZValidateAddress { + fn deserialize>(d: D) -> Result { + let inner = AddressData::deserialize(d)?; + if !inner.common().is_valid { + return Err(de::Error::custom("valid branch must have isvalid=true")); + } + + Ok(ValidZValidateAddress(inner)) + } +} + +/// The "valid" response. Can be P2PKH, P2SH, Sprout, Sapling, or Unified. +impl ValidZValidateAddress { + /// Creates a response for a P2PKH address. + pub fn p2pkh(address: impl Into) -> Self { + Self(AddressData::P2pkh { + common: CommonFields::valid(address, ZValidateAddressType::P2pkh), + }) + } + + /// Creates a response for a P2SH address. + pub fn p2sh(address: impl Into) -> Self { + Self(AddressData::P2sh { + common: CommonFields::valid(address, ZValidateAddressType::P2sh), + }) + } + + /// Creates a response for a Sprout address. + pub fn sprout>( + address: impl Into, + paying_key: Option, + transmission_key: Option, + ) -> Self { + Self(AddressData::Sprout { + common: CommonFields::valid(address, ZValidateAddressType::Sprout), + paying_key: paying_key.map(|x| x.into()), + transmission_key: transmission_key.map(|x| x.into()), + }) + } + + /// Creates a response for a Sapling address. + pub fn sapling>( + address: impl Into, + diversifier: Option, + diversified_transmission_key: Option, + ) -> Self { + Self(AddressData::Sapling { + common: CommonFields::valid(address, ZValidateAddressType::Sapling), + diversifier: diversifier.map(|x| x.into()), + diversified_transmission_key: diversified_transmission_key.map(|x| x.into()), + }) + } + + /// Creates a response for a Unified address. + pub fn unified(address: impl Into) -> Self { + Self(AddressData::Unified { + common: CommonFields::valid(address, ZValidateAddressType::Unified), + }) + } + + /// Optional setters (mirror zcashd’s conditional fields) + pub fn with_legacy_type(mut self, t: ZValidateAddressType) -> Self { + self.common_mut().legacy_type = Some(t); + self + } + + /// Returns the address. + pub fn address(&self) -> &str { + self.common().address.as_str() + } + + /// Returns the address type. + pub fn address_type(&self) -> ZValidateAddressType { + match &self.0 { + AddressData::P2pkh { .. } => ZValidateAddressType::P2pkh, + AddressData::P2sh { .. } => ZValidateAddressType::P2sh, + AddressData::Sprout { .. } => ZValidateAddressType::Sprout, + AddressData::Sapling { .. } => ZValidateAddressType::Sapling, + AddressData::Unified { .. } => ZValidateAddressType::Unified, + } + } + + /// Returns the legacy field for the address type. + pub fn legacy_type(&self) -> Option { + self.common().legacy_type + } + + /// Returns the `payingkey` and `transmissionkey` fields. + pub fn sprout_keys(&self) -> Option<(&str, &str)> { + if let AddressData::Sprout { + paying_key: Some(paying_key), + transmission_key: Some(transmission_key), + .. + } = &self.0 + { + Some((paying_key.as_str(), transmission_key.as_str())) + } else { + None + } + } + + /// Returns the `diversifier` and `diversifiedtransmissionkey` fields. + pub fn sapling_keys(&self) -> Option<(&str, &str)> { + if let AddressData::Sapling { + diversifier: Some(diversifier), + diversified_transmission_key: Some(diversified_transmission_key), + .. + } = &self.0 + { + Some((diversifier.as_str(), diversified_transmission_key.as_str())) + } else { + None + } + } + + fn common(&self) -> &CommonFields { + match &self.0 { + AddressData::P2pkh { common, .. } + | AddressData::P2sh { common, .. } + | AddressData::Sprout { common, .. } + | AddressData::Sapling { common, .. } + | AddressData::Unified { common, .. } => common, + } + } + fn common_mut(&mut self) -> &mut CommonFields { + match &mut self.0 { + AddressData::P2pkh { common, .. } + | AddressData::P2sh { common, .. } + | AddressData::Sprout { common, .. } + | AddressData::Sapling { common, .. } + | AddressData::Unified { common, .. } => common, + } + } + + /// Returns the address data. + pub fn inner(&self) -> &AddressData { + &self.0 + } +} + +/// Common fields that appear for all valid responses. +#[derive(Clone, Debug, PartialEq)] +pub struct CommonFields { + is_valid: bool, + + /// The address originally provided. + pub address: String, + + /// Deprecated alias for the type. Only present if the node exposes it. + pub legacy_type: Option, +} + +impl CommonFields { + fn valid(address: impl Into, legacy_type: ZValidateAddressType) -> Self { + Self { + is_valid: true, + address: address.into(), + legacy_type: Some(legacy_type), + } + } + + /// Returns whether the address is valid. + pub fn is_valid(&self) -> bool { + true + } +} + +/// Response for the Valid branch of the `z_validateaddress` RPC. +/// Note that the `ismine` field is present for `zcashd` but intentionally omitted here. +#[derive(Clone, Debug, PartialEq)] +pub enum AddressData { + /// Transparent P2PKH. + P2pkh { + /// Common address fields. + common: CommonFields, + }, + + /// Transparent P2SH + P2sh { + /// Common address fields + common: CommonFields, + }, + + /// Sprout address type + Sprout { + /// Common address fields + common: CommonFields, + + /// Hex of `a_pk` + paying_key: Option, + + /// The hex value of the transmission key, pk_enc + transmission_key: Option, + }, + + /// Sapling address type + Sapling { + /// Common address fields + common: CommonFields, + + /// Hex of the diversifier `d` + diversifier: Option, + + /// Hex of `pk_d` + diversified_transmission_key: Option, + }, + + /// Unified Address (UA). `zcashd` currently returns no extra fields for UA. + Unified { + /// Common address fields + common: CommonFields, + }, +} + +impl AddressData { + fn common(&self) -> &CommonFields { + match self { + AddressData::P2pkh { common, .. } + | AddressData::P2sh { common, .. } + | AddressData::Sprout { common, .. } + | AddressData::Sapling { common, .. } + | AddressData::Unified { common, .. } => common, + } + } + + fn variant_type(&self) -> ZValidateAddressType { + match self { + AddressData::P2pkh { .. } => ZValidateAddressType::P2pkh, + AddressData::P2sh { .. } => ZValidateAddressType::P2sh, + AddressData::Sprout { .. } => ZValidateAddressType::Sprout, + AddressData::Sapling { .. } => ZValidateAddressType::Sapling, + AddressData::Unified { .. } => ZValidateAddressType::Unified, + } + } +} + +impl Serialize for AddressData { + fn serialize(&self, s: S) -> Result { + let tag = self.variant_type(); + + let mut map = s.serialize_map(None)?; + // Mirror tags on output + map.serialize_entry("address_type", &tag)?; + map.serialize_entry("type", &tag)?; + + // Common + let c = self.common(); + map.serialize_entry("isvalid", &true)?; + map.serialize_entry("address", &c.address)?; + + // Different variants + match self { + AddressData::P2pkh { .. } | AddressData::P2sh { .. } => (), + AddressData::Sprout { + paying_key, + transmission_key, + .. + } => { + if let Some(pk) = paying_key { + map.serialize_entry("payingkey", pk)?; + } + if let Some(tk) = transmission_key { + map.serialize_entry("transmissionkey", tk)?; + } + } + AddressData::Sapling { + diversifier, + diversified_transmission_key, + .. + } => { + dbg!(&diversifier); + dbg!(&diversified_transmission_key); + if let Some(d) = diversifier { + map.serialize_entry("diversifier", d)?; + } + if let Some(dtk) = diversified_transmission_key { + map.serialize_entry("diversifiedtransmissionkey", dtk)?; + } + } + AddressData::Unified { .. } => (), + } + + map.end() + } +} + +impl<'de> Deserialize<'de> for AddressData { + fn deserialize>(d: D) -> Result { + let mut v = Value::deserialize(d)?; + let obj = v + .as_object_mut() + .ok_or_else(|| de::Error::custom("expected object"))?; + + let address_type: Option = obj + .get("address_type") + .and_then(|x| x.as_str()) + .map(ToOwned::to_owned); + let legacy_type: Option = obj + .get("type") + .and_then(|x| x.as_str()) + .map(ToOwned::to_owned); + + let (chosen, needs_address_type, needs_legacy_type) = + match (address_type.as_deref(), legacy_type.as_deref()) { + (Some(a), Some(t)) if a != t => { + return Err(de::Error::custom("`type` must match `address_type`")); + } + (Some(a), Some(_)) => (a.to_string(), false, false), + (Some(a), None) => (a.to_string(), false, true), + (None, Some(t)) => (t.to_string(), true, false), + (None, None) => return Err(de::Error::custom("missing `address_type` and `type`")), + }; + + if needs_address_type { + obj.insert("address_type".into(), Value::String(chosen.clone())); + } + if needs_legacy_type { + obj.insert("type".into(), Value::String(chosen.clone())); + } + + let is_valid = obj + .get("isvalid") + .and_then(|b| b.as_bool()) + .ok_or_else(|| de::Error::custom("missing `isvalid`"))?; + if !is_valid { + return Err(de::Error::custom("valid branch must have isvalid=true")); + } + + let address = obj + .get("address") + .and_then(|s| s.as_str()) + .ok_or_else(|| de::Error::custom("missing `address`"))? + .to_owned(); + + let tag = match chosen.as_str() { + "p2pkh" => ZValidateAddressType::P2pkh, + "p2sh" => ZValidateAddressType::P2sh, + "sprout" => ZValidateAddressType::Sprout, + "sapling" => ZValidateAddressType::Sapling, + "unified" => ZValidateAddressType::Unified, + other => { + return Err(de::Error::unknown_variant( + other, + &["p2pkh", "p2sh", "sprout", "sapling", "unified"], + )) + } + }; + + let common = CommonFields { + is_valid: true, + address, + legacy_type: Some(tag), + }; + + Ok(match tag { + ZValidateAddressType::P2pkh => AddressData::P2pkh { common }, + ZValidateAddressType::P2sh => AddressData::P2sh { common }, + ZValidateAddressType::Sprout => { + let paying_key = obj + .get("payingkey") + .and_then(|s| s.as_str()) + .map(str::to_owned); + let transmission_key = obj + .get("transmissionkey") + .and_then(|s| s.as_str()) + .map(str::to_owned); + AddressData::Sprout { + common, + paying_key, + transmission_key, + } + } + ZValidateAddressType::Sapling => { + let diversifier = obj + .get("diversifier") + .and_then(|s| s.as_str()) + .map(str::to_owned); + let diversified_transmission_key = obj + .get("diversifiedtransmissionkey") + .and_then(|s| s.as_str()) + .map(str::to_owned); + + dbg!(&diversifier); + dbg!(&diversified_transmission_key); + AddressData::Sapling { + common, + diversifier, + diversified_transmission_key, + } + } + ZValidateAddressType::Unified => AddressData::Unified { common }, + }) + } +} + +/// Address types returned by `zcashd`. +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ZValidateAddressType { + /// Transparent P2PKH + P2pkh, + + /// Transparent P2SH + P2sh, + + /// Sprout + Sprout, + + /// Sapling + Sapling, + + /// Unified + Unified, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::{json, Value}; + + /// Verifies that a type can be serialized and deserialized with the same shape. + /// + /// If the type does not have the same shape after serialization and deserialization, this function will panic. + fn roundtrip(value: &T) + where + T: serde::Serialize + for<'de> serde::Deserialize<'de> + std::fmt::Debug + PartialEq, + { + let s = serde_json::to_string(value).unwrap(); + let back: T = serde_json::from_str(&s).unwrap(); + assert_eq!(&back, value); + } + + #[test] + fn invalid_roundtrip_and_shape() { + let invalid_response = ZValidateAddressResponse::invalid(); + roundtrip(&invalid_response); + + let json_value = serde_json::to_value(&invalid_response).unwrap(); + assert_eq!(json_value, json!({ "isvalid": false })); + + // Invalid must reject isvalid=true when deserialized directly + let bad = r#"{ "isvalid": true }"#; + let err = serde_json::from_str::(bad).unwrap_err(); + assert!(err.to_string().contains("isvalid=false")); + } + + #[test] + fn valid_p2pkh_roundtrip_and_fields() { + let valid = + ValidZValidateAddress::p2pkh("t1abc").with_legacy_type(ZValidateAddressType::P2pkh); + + let top = ZValidateAddressResponse::Known(KnownZValidateAddress::Valid(valid.clone())); + roundtrip(&top); + + let json_value = serde_json::to_value(&top).unwrap(); + + // Compare as Value so we don't care about field order + assert_eq!( + json_value, + json!({ + "isvalid": true, + "address": "t1abc", + "type": "p2pkh", + "address_type": "p2pkh", + }) + ); + + if let ZValidateAddressResponse::Known(KnownZValidateAddress::Valid(v)) = top { + assert_eq!(v.address(), "t1abc"); + assert_eq!(v.address_type(), ZValidateAddressType::P2pkh); + assert_eq!(v.legacy_type(), Some(ZValidateAddressType::P2pkh)); + assert!(v.sprout_keys().is_none()); + assert!(v.sapling_keys().is_none()); + } else { + panic!("expected valid p2pkh"); + } + } + + #[test] + fn valid_p2sh() { + let valid = ValidZValidateAddress::p2sh("t3zzz"); + let top = ZValidateAddressResponse::Known(KnownZValidateAddress::Valid(valid.clone())); + roundtrip(&top); + + let json_value = serde_json::to_value(&top).unwrap(); + assert_eq!( + json_value, + json!({ + "isvalid": true, + "address": "t3zzz", + "address_type": "p2sh", + "type": "p2sh", + }) + ); + + if let ZValidateAddressResponse::Known(KnownZValidateAddress::Valid(v)) = top { + assert_eq!(v.address_type(), ZValidateAddressType::P2sh); + } + } + + #[test] + fn valid_sprout_roundtrip_and_fields() { + let valid = ValidZValidateAddress::sprout("zc1qq", Some("apkhex"), Some("pkenc")); + let top = ZValidateAddressResponse::Known(KnownZValidateAddress::Valid(valid.clone())); + roundtrip(&top); + + let json_value = serde_json::to_value(&top).unwrap(); + assert_eq!( + json_value, + json!({ + "isvalid": true, + "address": "zc1qq", + "address_type": "sprout", + "type": "sprout", + "payingkey": "apkhex", + "transmissionkey": "pkenc", + }) + ); + + if let ZValidateAddressResponse::Known(KnownZValidateAddress::Valid(v)) = top { + assert_eq!(v.address_type(), ZValidateAddressType::Sprout); + assert_eq!(v.sprout_keys(), Some(("apkhex", "pkenc"))); + assert!(v.sapling_keys().is_none()); + } + } + + #[test] + fn valid_sapling_roundtrip_and_fields() { + let valid = ValidZValidateAddress::sapling("zs1xx", Some("dhex"), Some("pkdhex")) + .with_legacy_type(ZValidateAddressType::Sapling); + let top = ZValidateAddressResponse::Known(KnownZValidateAddress::Valid(valid.clone())); + roundtrip(&top); + + let json_value = serde_json::to_value(&top).unwrap(); + assert_eq!( + json_value, + json!({ + "isvalid": true, + "address": "zs1xx", + "type": "sapling", + "address_type": "sapling", + "diversifier": "dhex", + "diversifiedtransmissionkey": "pkdhex", + }) + ); + + if let ZValidateAddressResponse::Known(KnownZValidateAddress::Valid(v)) = top { + assert_eq!(v.address_type(), ZValidateAddressType::Sapling); + assert_eq!(v.sapling_keys(), Some(("dhex", "pkdhex"))); + assert!(v.sprout_keys().is_none()); + } + } + + #[test] + fn valid_unified_has_no_ismine_and_no_legacy_type() { + let valid = ValidZValidateAddress::unified("u1blah"); + let top = ZValidateAddressResponse::Known(KnownZValidateAddress::Valid(valid.clone())); + roundtrip(&top); + + // Assert that "ismine" is absent + let json_value = serde_json::to_value(&top).unwrap(); + assert_eq!( + json_value, + json!({ + "isvalid": true, + "address": "u1blah", + "address_type": "unified", + "type": "unified" + }) + ); + + if let ZValidateAddressResponse::Known(KnownZValidateAddress::Valid(v)) = top { + assert_eq!(v.address_type(), ZValidateAddressType::Unified); + assert_eq!(v.legacy_type(), Some(ZValidateAddressType::Unified)); + } + } + + #[test] + fn invalid_branch_enforces_isvalid_false_no_other() { + // This JSON looks like sapling but has isvalid=false, so it must fail for ValidZValidateAddress + let bad = r#" + { + "isvalid": false, + "address": "zs1bad", + "address_type": "sapling", + "diversifier": "aa", + "diversifiedtransmissionkey": "bb" + }"#; + + let err = serde_json::from_str::(bad).unwrap_err(); + assert!(err.to_string().contains("isvalid=true")); + + // It will also fail for the Invalid branch, as it must ONLY contain isvalid=false + let bad_invalid = serde_json::from_str::(bad); + assert!(bad_invalid.is_err()); + } + + #[test] + fn missing_address_type_is_rejected_for_valid() { + // Missing "address_type" means AddressData can't be chosen + let bad = r#"{ "isvalid": true, "address": "zs1nope" }"#; + let result = serde_json::from_str::(bad); + assert!(result.is_err()); + } + + #[test] + fn top_level_unknown_on_null() { + // Untagged enum with a unit variant means `null` maps to `Unknown`. + let null_value: ZValidateAddressResponse = serde_json::from_str("{}").unwrap(); + match null_value { + ZValidateAddressResponse::Unknown(_) => {} + _ => panic!("expected Unknown"), + } + + // Serializing Unknown produces `null`. + let null_serialized = + serde_json::to_string(&ZValidateAddressResponse::Unknown(BTreeMap::new())).unwrap(); + assert_eq!(null_serialized, "{}"); + } + + #[test] + fn ismine_state_json_behavior() { + let valid_p2pkh = ZValidateAddressResponse::Known(KnownZValidateAddress::Valid( + ValidZValidateAddress::p2pkh("t1omitted"), + )); + let json_value = serde_json::to_value(&valid_p2pkh).unwrap(); + assert_eq!(json_value.get("ismine"), None); + + // True/false encoded when set + let v_true = ZValidateAddressResponse::Known(KnownZValidateAddress::Valid( + ValidZValidateAddress::p2pkh("t1mine"), + )); + let v_false = ZValidateAddressResponse::Known(KnownZValidateAddress::Valid( + ValidZValidateAddress::p2pkh("t1not"), + )); + let j_true = serde_json::to_value(&v_true).unwrap(); + let j_false = serde_json::to_value(&v_false).unwrap(); + assert_eq!(j_true.get("ismine"), None); + assert_eq!(j_false.get("ismine"), None); + } + + #[test] + fn helpers_return_expected_values() { + let sapling_with_ismine = + ValidZValidateAddress::sapling("zs1addr", Some("dhex"), Some("pkdhex")); + assert_eq!(sapling_with_ismine.address(), "zs1addr"); + assert_eq!( + sapling_with_ismine.address_type(), + ZValidateAddressType::Sapling + ); + assert_eq!( + sapling_with_ismine.legacy_type(), + Some(ZValidateAddressType::Sapling) + ); + assert_eq!(sapling_with_ismine.sapling_keys(), Some(("dhex", "pkdhex"))); + assert!(sapling_with_ismine.sprout_keys().is_none()); + } +} diff --git a/zaino-serve/src/rpc/jsonrpc/service.rs b/zaino-serve/src/rpc/jsonrpc/service.rs index 2825ffa1f..244c0935e 100644 --- a/zaino-serve/src/rpc/jsonrpc/service.rs +++ b/zaino-serve/src/rpc/jsonrpc/service.rs @@ -5,6 +5,7 @@ use zaino_fetch::jsonrpsee::response::block_header::GetBlockHeader; use zaino_fetch::jsonrpsee::response::block_subsidy::GetBlockSubsidy; use zaino_fetch::jsonrpsee::response::mining_info::GetMiningInfoWire; use zaino_fetch::jsonrpsee::response::peer_info::GetPeerInfo; +use zaino_fetch::jsonrpsee::response::z_validate_address::ZValidateAddressResponse; use zaino_fetch::jsonrpsee::response::{GetMempoolInfoResponse, GetNetworkSolPsResponse}; use zaino_state::{LightWalletIndexer, ZcashIndexer}; @@ -148,6 +149,20 @@ pub trait ZcashIndexerRpc { address: String, ) -> Result; + /// Return information about the given address. + /// + /// # Parameters + /// - `address`: (string, required) The address to validate. + /// + /// zcashd reference: [`z_validateaddress`](https://zcash.github.io/rpc/z_validateaddress.html) + /// method: post + /// tags: util + #[method(name = "z_validateaddress")] + async fn z_validate_address( + &self, + address: String, + ) -> Result; + /// Returns the total balance of a provided `addresses` in an [`AddressBalance`] instance. /// /// zcashd reference: [`getaddressbalance`](https://zcash.github.io/rpc/getaddressbalance.html) @@ -551,6 +566,24 @@ impl ZcashIndexerRpcServer for JsonR }) } + async fn z_validate_address( + &self, + address: String, + ) -> Result { + tracing::debug!("Sending ZValidateAddress to jsonRPC inner Indexer."); + self.service_subscriber + .inner_ref() + .z_validate_address(address) + .await + .map_err(|e| { + ErrorObjectOwned::owned( + ErrorCode::InvalidParams.code(), + "Internal server error", + Some(e.to_string()), + ) + }) + } + async fn z_get_address_balance( &self, address_strings: GetAddressBalanceRequest, diff --git a/zaino-state/Cargo.toml b/zaino-state/Cargo.toml index a6a360b91..468cfd7ea 100644 --- a/zaino-state/Cargo.toml +++ b/zaino-state/Cargo.toml @@ -19,7 +19,7 @@ zaino-proto = { workspace = true } # LibRustZcash zcash_address = { workspace = true } -zcash_keys = { workspace = true } +zcash_keys = { workspace = true, features = ["sapling"] } zcash_primitives = { workspace = true } zcash_protocol = { workspace = true } zcash_transparent = { workspace = true } diff --git a/zaino-state/src/backends/fetch.rs b/zaino-state/src/backends/fetch.rs index 5c5b7f519..f0ae5c7b9 100644 --- a/zaino-state/src/backends/fetch.rs +++ b/zaino-state/src/backends/fetch.rs @@ -32,6 +32,7 @@ use zaino_fetch::{ block_subsidy::GetBlockSubsidy, mining_info::GetMiningInfoWire, peer_info::GetPeerInfo, + z_validate_address::ZValidateAddressResponse, GetMempoolInfoResponse, GetNetworkSolPsResponse, }, }, @@ -503,6 +504,14 @@ impl ZcashIndexer for FetchServiceSubscriber { Ok(self.fetcher.validate_address(address).await?) } + async fn z_validate_address( + &self, + address: String, + ) -> Result { + tracing::debug!("Triggering fetch service fetcher z_validate_address."); + Ok(self.fetcher.z_validate_address(address).await?) + } + /// Returns all transaction ids in the memory pool, as a JSON array. /// /// zcashd reference: [`getrawmempool`](https://zcash.github.io/rpc/getrawmempool.html) diff --git a/zaino-state/src/backends/state.rs b/zaino-state/src/backends/state.rs index de7e70ec5..33ea3f005 100644 --- a/zaino-state/src/backends/state.rs +++ b/zaino-state/src/backends/state.rs @@ -20,6 +20,7 @@ use crate::{ utils::{blockid_to_hashorheight, get_build_info, ServiceMetadata}, BackendType, MempoolKey, }; +use zcash_keys::{address::Address, encoding::AddressCodec}; use nonempty::NonEmpty; use tokio_stream::StreamExt as _; @@ -34,6 +35,9 @@ use zaino_fetch::{ block_subsidy::GetBlockSubsidy, mining_info::GetMiningInfoWire, peer_info::GetPeerInfo, + z_validate_address::{ + InvalidZValidateAddress, KnownZValidateAddress, ZValidateAddressResponse, + }, GetMempoolInfoResponse, GetNetworkSolPsResponse, GetSubtreesResponse, }, }, @@ -47,6 +51,7 @@ use zaino_proto::proto::{ }, }; +use zcash_primitives::legacy::TransparentAddress; use zcash_protocol::consensus::NetworkType; use zebra_chain::{ amount::{Amount, NonNegative}, @@ -1572,7 +1577,6 @@ impl ZcashIndexer for StateServiceSubscriber { &self, raw_address: String, ) -> Result { - use zcash_keys::address::Address; use zcash_transparent::address::TransparentAddress; let Ok(address) = raw_address.parse::() else { @@ -1593,7 +1597,6 @@ impl ZcashIndexer for StateServiceSubscriber { } }; - // we want to match zcashd's behaviour Ok(match address { Address::Transparent(taddr) => ValidateAddressResponse::new( true, @@ -1604,6 +1607,62 @@ impl ZcashIndexer for StateServiceSubscriber { }) } + async fn z_validate_address( + &self, + address: String, + ) -> Result { + tracing::debug!("State service backend z_validate_address."); + + let Ok(parsed_address) = address.parse::() else { + return Ok(ZValidateAddressResponse::Known( + KnownZValidateAddress::Invalid(InvalidZValidateAddress::new()), + )); + }; + + let converted_address = match parsed_address.convert_if_network::
( + match self.config.network.to_zebra_network().kind() { + NetworkKind::Mainnet => NetworkType::Main, + NetworkKind::Testnet => NetworkType::Test, + NetworkKind::Regtest => NetworkType::Regtest, + }, + ) { + Ok(address) => address, + Err(err) => { + tracing::debug!(?err, "conversion error"); + return Ok(ZValidateAddressResponse::Known( + KnownZValidateAddress::Invalid(InvalidZValidateAddress::new()), + )); + } + }; + + // Note: It could be the case that Zaino needs to support Sprout. For now, it's been disabled. + match converted_address { + Address::Transparent(t) => match t { + TransparentAddress::PublicKeyHash(_) => { + Ok(ZValidateAddressResponse::p2pkh(address)) + } + TransparentAddress::ScriptHash(_) => Ok(ZValidateAddressResponse::p2sh(address)), + }, + Address::Unified(u) => Ok(ZValidateAddressResponse::unified( + u.encode(&self.network().to_zebra_network()), + )), + Address::Sapling(s) => { + let bytes = s.to_bytes(); + let mut pk_d = bytes[11..].to_vec(); // TODO: See if in a newer version this is no longer needed + pk_d.reverse(); + + Ok(ZValidateAddressResponse::sapling( + s.encode(&self.network().to_zebra_network()), + Some(hex::encode(s.diversifier().0)), + Some(hex::encode(pk_d)), + )) + } + _ => Ok(ZValidateAddressResponse::Known( + KnownZValidateAddress::Invalid(InvalidZValidateAddress::new()), + )), + } + } + async fn z_get_subtrees_by_index( &self, pool: String, diff --git a/zaino-state/src/indexer.rs b/zaino-state/src/indexer.rs index 0c92b2283..b27e78ddb 100644 --- a/zaino-state/src/indexer.rs +++ b/zaino-state/src/indexer.rs @@ -11,6 +11,7 @@ use zaino_fetch::jsonrpsee::response::{ block_subsidy::GetBlockSubsidy, mining_info::GetMiningInfoWire, peer_info::GetPeerInfo, + z_validate_address::ZValidateAddressResponse, GetMempoolInfoResponse, GetNetworkSolPsResponse, }; use zaino_proto::proto::{ @@ -350,12 +351,25 @@ pub trait ZcashIndexer: Send + Sync + 'static { /// /// zcashd reference: [`validateaddress`](https://zcash.github.io/rpc/validateaddress.html) /// method: post - /// tags: blockchain + /// tags: util async fn validate_address( &self, address: String, ) -> Result; + /// Return information about the given address. + /// + /// # Parameters + /// - `address`: (string, required) The address to validate. + /// + /// zcashd reference: [`z_validateaddress`](https://zcash.github.io/rpc/z_validateaddress.html) + /// method: post + /// tags: util + async fn z_validate_address( + &self, + address: String, + ) -> Result; + /// Returns the hash of the best block (tip) of the longest chain. /// online zcashd reference: [`getbestblockhash`](https://zcash.github.io/rpc/getbestblockhash.html) /// The zcashd doc reference above says there are no parameters and the result is a "hex" (string) of the block hash hex encoded. diff --git a/zaino-testutils/src/lib.rs b/zaino-testutils/src/lib.rs index 6a89d32f4..36738a097 100644 --- a/zaino-testutils/src/lib.rs +++ b/zaino-testutils/src/lib.rs @@ -132,7 +132,7 @@ pub static ZEBRAD_TESTNET_CACHE_DIR: Lazy> = Lazy::new(|| { Some(home_path.join(".cache/zebra")) }); -#[derive(PartialEq, Clone, Copy)] +#[derive(PartialEq, Clone, Copy, Debug)] /// Represents the type of validator to launch. pub enum ValidatorKind { /// Zcashd.