From 3092a89ff2dadb0ba705487f224adb7e9def334e Mon Sep 17 00:00:00 2001 From: dorianvp Date: Fri, 20 Mar 2026 01:47:38 -0300 Subject: [PATCH 1/2] chore: wip expand API --- zingo-netutils/src/lib.rs | 379 ++++++++++++++++++++++++-------------- 1 file changed, 245 insertions(+), 134 deletions(-) diff --git a/zingo-netutils/src/lib.rs b/zingo-netutils/src/lib.rs index 0a6ad4b..5e9238c 100644 --- a/zingo-netutils/src/lib.rs +++ b/zingo-netutils/src/lib.rs @@ -1,7 +1,8 @@ //! `zingo-netutils` //! -//! This crate provides the [`Indexer`] trait for communicating with a Zcash chain indexer, -//! and [`GrpcIndexer`], a concrete implementation that connects to a zainod server via gRPC. +//! This crate provides the [`IndexerClient`] trait for communicating with a Zcash +//! chain indexer, and [`GrpcIndexerClient`], a concrete implementation that +//! connects to a lightwalletd-compatible server via gRPC. use std::future::Future; use std::time::Duration; @@ -13,6 +14,58 @@ use zcash_client_backend::proto::service::{ compact_tx_streamer_client::CompactTxStreamerClient, }; +const DEFAULT_GRPC_TIMEOUT: Duration = Duration::from_secs(10); + +/// Indexer server metadata. +/// +/// This intentionally avoids exposing protobuf-generated types in the public +/// trait boundary. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServerInfo { + pub chain_name: String, + pub vendor: String, + pub version: String, + pub block_height: u64, + pub sapling_activation_height: u64, + pub consensus_branch_id: String, +} + +impl From for ServerInfo { + fn from(value: LightdInfo) -> Self { + Self { + chain_name: value.chain_name, + vendor: value.vendor, + version: value.version, + block_height: value.block_height, + sapling_activation_height: value.sapling_activation_height, + consensus_branch_id: value.consensus_branch_id, + } + } +} + +/// A block identifier. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BlockRef { + pub height: u64, + pub hash: Vec, +} + +impl From for BlockRef { + fn from(value: BlockId) -> Self { + Self { + height: value.height, + hash: value.hash, + } + } +} + +/// The successful result of transaction submission. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SentTransaction { + pub txid: String, +} + +/// Error type for [`GrpcIndexerClient`] construction and transport setup. #[derive(Debug, thiserror::Error)] pub enum GetClientError { #[error("bad uri: invalid scheme")] @@ -25,8 +78,24 @@ pub enum GetClientError { Transport(#[from] tonic::transport::Error), } +/// Unified error type for indexer client operations. +/// +/// The public trait uses one semantic error type rather than leaking per-RPC +/// tonic/protobuf details into callers. +#[derive(Debug, thiserror::Error)] +pub enum IndexerClientError { + #[error(transparent)] + GetClient(#[from] GetClientError), + + #[error("gRPC error: {0}")] + Grpc(#[from] tonic::Status), + + #[error("send rejected: {0}")] + SendRejected(String), +} + fn client_tls_config() -> ClientTlsConfig { - // Allow self-signed certs in tests + // Allow self-signed certs in tests. #[cfg(test)] { ClientTlsConfig::new() @@ -39,89 +108,87 @@ fn client_tls_config() -> ClientTlsConfig { ClientTlsConfig::new().with_webpki_roots() } -const DEFAULT_GRPC_TIMEOUT: Duration = Duration::from_secs(10); - -/// Error type for [`GrpcIndexer::get_info`]. -#[derive(Debug, thiserror::Error)] -pub enum GetInfoError { - #[error(transparent)] - GetClientError(#[from] GetClientError), - - #[error("gRPC error: {0}")] - GetLightdInfoError(#[from] tonic::Status), +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CallTimeouts { + pub get_info: Duration, + pub get_latest_block: Duration, + pub send_transaction: Duration, + pub get_tree_state: Duration, } -/// Error type for [`GrpcIndexer::get_latest_block`]. -#[derive(Debug, thiserror::Error)] -pub enum GetLatestBlockError { - #[error(transparent)] - GetClientError(#[from] GetClientError), +impl CallTimeouts { + pub const fn new(all: Duration) -> Self { + Self { + get_info: all, + get_latest_block: all, + send_transaction: all, + get_tree_state: all, + } + } - #[error("gRPC error: {0}")] - GetLatestBlockError(#[from] tonic::Status), -} + pub const fn with_get_info(mut self, timeout: Duration) -> Self { + self.get_info = timeout; + self + } -/// Error type for [`GrpcIndexer::send_transaction`]. -#[derive(Debug, thiserror::Error)] -pub enum SendTransactionError { - #[error(transparent)] - GetClientError(#[from] GetClientError), + pub const fn with_get_latest_block(mut self, timeout: Duration) -> Self { + self.get_latest_block = timeout; + self + } - #[error("gRPC error: {0}")] - SendTransactionError(#[from] tonic::Status), + pub const fn with_send_transaction(mut self, timeout: Duration) -> Self { + self.send_transaction = timeout; + self + } - #[error("send rejected: {0}")] - SendRejected(String), + pub const fn with_get_tree_state(mut self, timeout: Duration) -> Self { + self.get_tree_state = timeout; + self + } } -/// Error type for [`GrpcIndexer::get_trees`]. -#[derive(Debug, thiserror::Error)] -pub enum GetTreesError { - #[error(transparent)] - GetClientError(#[from] GetClientError), - - #[error("gRPC error: {0}")] - GetTreeStateError(#[from] tonic::Status), +impl Default for CallTimeouts { + fn default() -> Self { + Self::new(DEFAULT_GRPC_TIMEOUT) + } } /// Trait for communicating with a Zcash chain indexer. -pub trait Indexer { - type GetInfoError; - type GetLatestBlockError; - type SendTransactionError; - type GetTreesError; - - fn get_info(&self) -> impl Future>; - fn get_latest_block(&self) -> impl Future>; +/// +/// This trait exposes crate-local semantic types rather than protobuf-generated +/// transport types, which keeps the rest of the codebase decoupled from the +/// wire format and makes mocking/testing easier. +pub trait IndexerClient { + fn get_info(&self) -> impl Future>; + fn get_latest_block(&self) -> impl Future>; fn send_transaction( &self, - tx_bytes: Box<[u8]>, - ) -> impl Future>; - fn get_trees( + tx_bytes: &[u8], + ) -> impl Future>; + fn get_tree_state( &self, height: u64, - ) -> impl Future>; + ) -> impl Future>; } -/// gRPC-backed [`Indexer`] that connects to a lightwalletd server. +/// gRPC-backed [`IndexerClient`] that connects to a lightwalletd-compatible +/// server. #[derive(Clone)] -pub struct GrpcIndexer { +pub struct GrpcIndexerClient { uri: http::Uri, - scheme: String, - authority: http::uri::Authority, - endpoint: Endpoint, + channel: Channel, + call_timeouts: CallTimeouts, } -impl std::fmt::Debug for GrpcIndexer { +impl std::fmt::Debug for GrpcIndexerClient { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("GrpcIndexer") - .field("scheme", &self.scheme) - .field("authority", &self.authority) + f.debug_struct("GrpcIndexerClient") + .field("uri", &self.uri) .finish_non_exhaustive() } } -impl GrpcIndexer { +impl GrpcIndexerClient { pub fn new(uri: http::Uri) -> Result { let scheme = uri .scheme_str() @@ -130,102 +197,105 @@ impl GrpcIndexer { if scheme != "http" && scheme != "https" { return Err(GetClientError::InvalidScheme); } - let authority = uri + + let _authority = uri .authority() .ok_or(GetClientError::InvalidAuthority)? .clone(); - let endpoint = Endpoint::from_shared(uri.to_string())?.tcp_nodelay(true); + let endpoint = Endpoint::from_shared(uri.to_string())? + .tcp_nodelay(true) + .http2_keep_alive_interval(Duration::from_secs(30)) + .keep_alive_timeout(Duration::from_secs(10)); + let endpoint = if scheme == "https" { endpoint.tls_config(client_tls_config())? } else { endpoint }; + let channel = endpoint.connect_lazy(); + Ok(Self { uri, - scheme, - authority, - endpoint, + channel, + call_timeouts: CallTimeouts::default(), }) } + pub fn with_default_timeout(mut self, timeout: Duration) -> Self { + self.call_timeouts = CallTimeouts::new(timeout); + self + } + + pub fn with_call_timeouts(mut self, call_timeouts: CallTimeouts) -> Self { + self.call_timeouts = call_timeouts; + self + } + pub fn uri(&self) -> &http::Uri { &self.uri } - /// Connect to the pre-configured endpoint and return a gRPC client. - pub async fn get_client(&self) -> Result, GetClientError> { - let channel = self.endpoint.connect().await?; - Ok(CompactTxStreamerClient::new(channel)) + fn client(&self) -> CompactTxStreamerClient { + CompactTxStreamerClient::new(self.channel.clone()) } } -impl Indexer for GrpcIndexer { - type GetInfoError = GetInfoError; - type GetLatestBlockError = GetLatestBlockError; - type SendTransactionError = SendTransactionError; - type GetTreesError = GetTreesError; - - async fn get_info(&self) -> Result { - let mut client = self.get_client().await?; +impl IndexerClient for GrpcIndexerClient { + async fn get_info(&self) -> Result { + let mut client = self.client(); let mut request = Request::new(Empty {}); - request.set_timeout(DEFAULT_GRPC_TIMEOUT); + request.set_timeout(self.call_timeouts.get_info); let response = client.get_lightd_info(request).await?; - Ok(response.into_inner()) + Ok(response.into_inner().into()) } - async fn get_latest_block(&self) -> Result { - let mut client = self.get_client().await?; + async fn get_latest_block(&self) -> Result { + let mut client = self.client(); let mut request = Request::new(ChainSpec {}); - request.set_timeout(DEFAULT_GRPC_TIMEOUT); + request.set_timeout(self.call_timeouts.get_latest_block); let response = client.get_latest_block(request).await?; - Ok(response.into_inner()) + Ok(response.into_inner().into()) } - async fn send_transaction(&self, tx_bytes: Box<[u8]>) -> Result { - let mut client = self.get_client().await?; + async fn send_transaction( + &self, + tx_bytes: &[u8], + ) -> Result { + let mut client = self.client(); let mut request = Request::new(RawTransaction { data: tx_bytes.to_vec(), height: 0, }); - request.set_timeout(DEFAULT_GRPC_TIMEOUT); + request.set_timeout(self.call_timeouts.send_transaction); + let response = client.send_transaction(request).await?; - let sendresponse = response.into_inner(); - if sendresponse.error_code == 0 { - let mut transaction_id = sendresponse.error_message; - if transaction_id.starts_with('\"') && transaction_id.ends_with('\"') { - transaction_id = transaction_id[1..transaction_id.len() - 1].to_string(); + let send_response = response.into_inner(); + + if send_response.error_code == 0 { + let mut txid = send_response.error_message; + if txid.starts_with('"') && txid.ends_with('"') && txid.len() >= 2 { + txid = txid[1..txid.len() - 1].to_string(); } - Ok(transaction_id) + Ok(SentTransaction { txid }) } else { - Err(SendTransactionError::SendRejected(format!( - "{sendresponse:?}" + Err(IndexerClientError::SendRejected(format!( + "{send_response:?}" ))) } } - async fn get_trees(&self, height: u64) -> Result { - let mut client = self.get_client().await?; - let response = client - .get_tree_state(Request::new(BlockId { - height, - hash: vec![], - })) - .await?; - Ok(response.into_inner()) - } -} - -#[cfg(test)] -mod indexer_implementation { + async fn get_tree_state(&self, height: u64) -> Result { + let mut client = self.client(); + let mut request = Request::new(BlockId { + height, + hash: vec![], + }); + request.set_timeout(self.call_timeouts.get_tree_state); - mod get_info { - #[tokio::test] - async fn call_get_info() { - assert_eq!(1, 1); - //let grpc_index = GrpcIndexer::new(); - } + let response = client.get_tree_state(request).await?; + Ok(response.into_inner()) } } @@ -237,7 +307,7 @@ mod tests { //! - TLS test asset sanity (`test-data/localhost.pem` + `.key`) //! - Rustls plumbing (adding a local cert to a root store) //! - Connector correctness (scheme validation, HTTP/2 expectations) - //! - URI rewrite behavior (no panics; returns structured errors) + //! - Public semantic type mapping (`LightdInfo -> ServerInfo`, `BlockId -> BlockRef`) //! //! Notes: //! - Some tests spin up an in-process TLS server and use aggressive timeouts to @@ -262,7 +332,6 @@ mod tests { fn add_test_cert_to_roots(roots: &mut RootCertStore) { use tonic::transport::CertificateDer; - eprintln!("Adding test cert to roots"); const TEST_PEMFILE_PATH: &str = "test-data/localhost.pem"; @@ -280,6 +349,59 @@ mod tests { roots.add_parsable_certificates(certs); } + #[test] + fn lightd_info_maps_to_server_info() { + let info = LightdInfo { + version: "1.2.3".to_string(), + vendor: "zingo".to_string(), + taddr_support: false, + chain_name: "main".to_string(), + sapling_activation_height: 419_200, + consensus_branch_id: "76b809bb".to_string(), + block_height: 2_345_678, + git_commit: String::new(), + branch: String::new(), + build_date: String::new(), + build_user: String::new(), + estimated_height: 0, + zcashd_build: String::new(), + zcashd_subversion: String::new(), + donation_address: String::new(), + }; + + let mapped = ServerInfo::from(info); + + assert_eq!( + mapped, + ServerInfo { + chain_name: "main".to_string(), + vendor: "zingo".to_string(), + version: "1.2.3".to_string(), + block_height: 2_345_678, + sapling_activation_height: 419_200, + consensus_branch_id: "76b809bb".to_string(), + } + ); + } + + #[test] + fn block_id_maps_to_block_ref() { + let block = BlockId { + height: 123, + hash: vec![1, 2, 3, 4], + }; + + let mapped = BlockRef::from(block); + + assert_eq!( + mapped, + BlockRef { + height: 123, + hash: vec![1, 2, 3, 4], + } + ); + } + /// Ensures the committed localhost test certificate exists and is parseable as X.509. /// /// This catches: @@ -371,16 +493,9 @@ mod tests { std::sync::Arc::new(config) } + /// Smoke test: adding the committed localhost cert to a rustls root store enables /// a client to complete a TLS handshake and perform an HTTP request. - /// - /// Implementation notes: - /// - Uses a local TLS server with the committed cert/key. - /// - Uses strict timeouts to prevent hangs under nextest. - /// - Explicitly drains the request body and disables keep-alive so that - /// `serve_connection` terminates deterministically. - /// - Installs the rustls crypto provider to avoid provider - /// selection panics in test binaries. #[tokio::test] async fn add_test_cert_to_roots_enables_tls_handshake() { use http_body_util::Full; @@ -498,19 +613,15 @@ mod tests { #[test] fn rejects_non_http_schemes() { let uri: http::Uri = "ftp://example.com:1234".parse().unwrap(); - let res = GrpcIndexer::new(uri); + let res = GrpcIndexerClient::new(uri); assert!( res.is_err(), - "expected GrpcIndexer::new() to reject non-http(s) schemes, but got Ok" + "expected GrpcIndexerClient::new() to reject non-http(s) schemes, but got Ok" ); } - /// Demonstrates the HTTPS downgrade hazard: the underlying client can successfully - /// talk to an HTTP/1.1-only TLS server if the HTTPS branch does not enforce HTTP/2. - /// - /// This is intentionally written as a “should be HTTP/2” test so it fails until - /// the HTTPS client is constructed with `http2_only(true)`. + /// A gRPC (HTTP/2) client must not succeed against an HTTP/1.1-only TLS server. #[tokio::test] async fn https_connector_must_not_downgrade_to_http1() { use http_body_util::Full; @@ -566,14 +677,14 @@ mod tests { #[tokio::test] async fn connects_to_public_mainnet_indexer_and_gets_info() { let endpoint = "https://zec.rocks:443".to_string(); - let uri: http::Uri = endpoint.parse().expect("bad mainnet indexer URI"); - let response = GrpcIndexer::new(uri) - .expect("URI to be valid.") + let response = GrpcIndexerClient::new(uri) + .expect("URI to be valid") .get_info() .await .expect("to get info"); + assert!( !response.chain_name.is_empty(), "chain_name should not be empty" From d20eb4e9ae124cd8cb7a5c144ff3b1c75e6a3044 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Fri, 20 Mar 2026 17:49:42 -0300 Subject: [PATCH 2/2] chore: wip types --- Cargo.lock | 1 + zingo-netutils/Cargo.toml | 1 + zingo-netutils/src/lib.rs | 385 ++++++++++++++++++++++++++++------ zingo-netutils/src/types.rs | 397 ++++++++++++++++++++++++++++++++++++ 4 files changed, 719 insertions(+), 65 deletions(-) create mode 100644 zingo-netutils/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index 1dfd1ce..9304592 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6620,6 +6620,7 @@ dependencies = [ name = "zingo-netutils" version = "3.0.0" dependencies = [ + "futures-core", "http", "http-body-util", "hyper", diff --git a/zingo-netutils/Cargo.toml b/zingo-netutils/Cargo.toml index 9144ffb..9531cf5 100644 --- a/zingo-netutils/Cargo.toml +++ b/zingo-netutils/Cargo.toml @@ -24,6 +24,7 @@ zcash_client_backend = { workspace = true, features = [ "lightwalletd-tonic", "tor", ] } +futures-core = "0.3.32" [dev-dependencies] rustls-pemfile.workspace = true diff --git a/zingo-netutils/src/lib.rs b/zingo-netutils/src/lib.rs index 5e9238c..1ac4305 100644 --- a/zingo-netutils/src/lib.rs +++ b/zingo-netutils/src/lib.rs @@ -4,61 +4,26 @@ //! chain indexer, and [`GrpcIndexerClient`], a concrete implementation that //! connects to a lightwalletd-compatible server via gRPC. +pub mod types; + use std::future::Future; use std::time::Duration; +use futures_core::Stream; +use futures_core::stream::BoxStream; use tonic::Request; use tonic::transport::{Channel, ClientTlsConfig, Endpoint}; -use zcash_client_backend::proto::service::{ - BlockId, ChainSpec, Empty, LightdInfo, RawTransaction, TreeState, - compact_tx_streamer_client::CompactTxStreamerClient, +use zcash_client_backend::proto::service::compact_tx_streamer_client::CompactTxStreamerClient; +use zcash_client_backend::proto::service::{BlockId, ChainSpec, Empty}; + +use crate::types::{ + Address, AddressList, AddressUtxo, Balance, BlockRange, BlockRef, CompactBlock, CompactTx, + GetAddressUtxosRequest, GetSubtreeRootsRequest, MempoolTxRequest, PingResponse, RawTransaction, + ServerInfo, SubtreeRoot, TransparentAddressBlockFilter, TreeState, TxFilter, }; const DEFAULT_GRPC_TIMEOUT: Duration = Duration::from_secs(10); -/// Indexer server metadata. -/// -/// This intentionally avoids exposing protobuf-generated types in the public -/// trait boundary. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ServerInfo { - pub chain_name: String, - pub vendor: String, - pub version: String, - pub block_height: u64, - pub sapling_activation_height: u64, - pub consensus_branch_id: String, -} - -impl From for ServerInfo { - fn from(value: LightdInfo) -> Self { - Self { - chain_name: value.chain_name, - vendor: value.vendor, - version: value.version, - block_height: value.block_height, - sapling_activation_height: value.sapling_activation_height, - consensus_branch_id: value.consensus_branch_id, - } - } -} - -/// A block identifier. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct BlockRef { - pub height: u64, - pub hash: Vec, -} - -impl From for BlockRef { - fn from(value: BlockId) -> Self { - Self { - height: value.height, - hash: value.hash, - } - } -} - /// The successful result of transaction submission. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SentTransaction { @@ -155,22 +120,205 @@ impl Default for CallTimeouts { /// Trait for communicating with a Zcash chain indexer. /// -/// This trait exposes crate-local semantic types rather than protobuf-generated -/// transport types, which keeps the rest of the codebase decoupled from the -/// wire format and makes mocking/testing easier. +/// This trait mirrors the canonical `CompactTxStreamer` service surface while +/// exposing crate-local semantic types instead of protobuf-generated transport +/// types. That keeps the rest of the codebase decoupled from the wire format +/// and makes mocking and testing easier. pub trait IndexerClient { - fn get_info(&self) -> impl Future>; - fn get_latest_block(&self) -> impl Future>; + /// The error type returned by this client. + type Error: std::error::Error + Send + Sync + 'static; + + /// Stream of compact blocks. + type BlockStream: Stream>; + + /// Stream of full raw transactions. + type RawTransactionStream: Stream>; + + /// Stream of compact transactions. + type CompactTxStream: Stream>; + + /// Stream of subtree roots for a note commitment tree. + type SubtreeRootStream: Stream>; + + /// Stream of transparent UTXOs. + type AddressUtxoStream: Stream>; + + /// Returns the block identifier of the block at the tip of the best chain. + fn get_latest_block(&self) -> impl Future>; + + /// Returns information about this lightwalletd instance and the state of + /// the blockchain. + fn get_lightd_info(&self) -> impl Future>; + + /// Returns the compact block corresponding to the given block identifier. + /// + /// Compact blocks contain the minimum block and transaction data needed by a + /// wallet to detect relevant shielded activity, update witnesses, and, when + /// provided by the server, detect transparent UTXOs relevant to the wallet. + /// + /// Compact transactions may include transparent inputs (`vin`) and outputs + /// (`vout`) in addition to shielded data. + fn get_block(&self, block: BlockRef) + -> impl Future>; + + /// Returns a compact block containing only shielded nullifier information. + /// + /// Transparent transaction data, Sapling outputs, full Orchard action + /// data, and commitment tree sizes are not included. + /// + /// Deprecated in the protocol; prefer [`Self::get_block_range`] with the + /// appropriate pool filters. + #[deprecated(note = "Protocol-deprecated; prefer get_block_range with pool filters")] + fn get_block_nullifiers( + &self, + block: BlockRef, + ) -> impl Future>; + + /// Returns a stream of consecutive compact blocks in the specified range. + /// + /// The range is inclusive of `range.end`. If `range.start <= range.end`, + /// blocks are returned in increasing height order; otherwise they are + /// returned in decreasing height order. + /// + /// Upstream protocol notes that if no pool types are specified, servers + /// should default to the legacy behavior of returning only data relevant + /// to the shielded Sapling and Orchard pools. Clients must verify server + /// support before requesting pruned and/or transparent data via pool + /// filters. + fn get_block_range( + &self, + range: BlockRange, + ) -> impl Future>; + + /// Returns a stream of compact blocks containing only shielded nullifier + /// information. + /// + /// Transparent transaction data, Sapling outputs, full Orchard action + /// data, and commitment tree sizes are not included. Implementations must + /// ignore any transparent pool type in the request. + /// + /// Deprecated in the protocol; prefer [`Self::get_block_range`] with the + /// appropriate pool filters. + #[deprecated(note = "Protocol-deprecated; prefer get_block_range with pool filters")] + fn get_block_range_nullifiers( + &self, + range: BlockRange, + ) -> impl Future>; + + /// Returns the requested full, non-compact transaction. + /// + /// In the upstream protocol, this corresponds to the full transaction as + /// returned by `zcashd`. + fn get_transaction( + &self, + tx_filter: TxFilter, + ) -> impl Future>; + + /// Submits the given transaction to the Zcash network. fn send_transaction( &self, - tx_bytes: &[u8], - ) -> impl Future>; + tx: &[u8], + ) -> impl Future>; + + /// Returns full transactions that match the given transparent address + /// filter. + /// + /// Despite its historical name, the upstream RPC returns complete raw + /// transactions, not transaction IDs. + /// + /// Deprecated in the protocol; prefer + /// [`Self::get_taddress_transactions`]. + #[deprecated(note = "Protocol-deprecated; use get_taddress_transactions")] + fn get_taddress_txids( + &self, + filter: TransparentAddressBlockFilter, + ) -> impl Future>; + + /// Returns the transactions corresponding to the given transparent address + /// within the specified block range. + /// + /// Mempool transactions are not included. + fn get_taddress_transactions( + &self, + filter: TransparentAddressBlockFilter, + ) -> impl Future>; + + /// Returns the balance for the given set of transparent addresses. + fn get_taddress_balance( + &self, + addresses: AddressList, + ) -> impl Future>; + + /// Returns the balance for a streamed set of transparent addresses. + fn get_taddress_balance_stream( + &self, + addresses: impl Stream, + ) -> impl Future>; + + /// Returns a stream of compact transactions currently in the mempool. + /// + /// Results may be a few seconds out of date. If the excluded txid suffix + /// list is empty, all transactions are returned; otherwise all + /// non-excluded transactions are returned. Suffixes may be shortened to + /// reduce bandwidth. If multiple mempool transactions match a given + /// suffix, none of them are excluded. + fn get_mempool_tx( + &self, + request: MempoolTxRequest, + ) -> impl Future>; + + /// Returns a stream of current mempool transactions. + /// + /// The upstream server keeps the stream open while mempool transactions + /// are available, and closes it when a new block is mined. + fn get_mempool_stream( + &self, + ) -> impl Future>; + + /// Returns the note commitment tree state corresponding to the given + /// block. + /// + /// This is derived from the Zcash `z_gettreestate` RPC. The block may be + /// specified by either height or hash, though upstream notes that support + /// for selection by hash is not mandatory across all methods. fn get_tree_state( &self, - height: u64, - ) -> impl Future>; -} + block: BlockRef, + ) -> impl Future>; + + /// Returns the note commitment tree state at the tip of the best chain. + fn get_latest_tree_state(&self) -> impl Future>; + + /// Returns a stream of subtree roots for the specified shielded protocol. + /// + /// The upstream protocol defines this in terms of Sapling or Orchard note + /// commitment tree subtrees. + fn get_subtree_roots( + &self, + request: GetSubtreeRootsRequest, + ) -> impl Future>; + /// Returns the transparent UTXOs matching the given request. + /// + /// Upstream results are sorted by height, which makes it easy to issue a + /// follow-up request that continues where the previous one left off. + fn get_address_utxos( + &self, + request: GetAddressUtxosRequest, + ) -> impl Future, Self::Error>>; + + /// Returns a stream of transparent UTXOs matching the given request. + fn get_address_utxos_stream( + &self, + request: GetAddressUtxosRequest, + ) -> impl Future>; + + /// Testing-only RPC used to simulate delay and observe concurrency. + /// + /// On upstream `lightwalletd`, this requires `--ping-very-insecure` and + /// should not be enabled in production. + fn ping(&self, delay: Duration) -> impl Future>; +} /// gRPC-backed [`IndexerClient`] that connects to a lightwalletd-compatible /// server. #[derive(Clone)] @@ -243,7 +391,15 @@ impl GrpcIndexerClient { } impl IndexerClient for GrpcIndexerClient { - async fn get_info(&self) -> Result { + type Error = IndexerClientError; + + type BlockStream = BoxStream<'static, Result>; + type RawTransactionStream = BoxStream<'static, Result>; + type CompactTxStream = BoxStream<'static, Result>; + type SubtreeRootStream = BoxStream<'static, Result>; + type AddressUtxoStream = BoxStream<'static, Result>; + + async fn get_lightd_info(&self) -> Result { let mut client = self.client(); let mut request = Request::new(Empty {}); request.set_timeout(self.call_timeouts.get_info); @@ -264,7 +420,7 @@ impl IndexerClient for GrpcIndexerClient { tx_bytes: &[u8], ) -> Result { let mut client = self.client(); - let mut request = Request::new(RawTransaction { + let mut request = Request::new(zcash_client_backend::proto::service::RawTransaction { data: tx_bytes.to_vec(), height: 0, }); @@ -286,16 +442,105 @@ impl IndexerClient for GrpcIndexerClient { } } - async fn get_tree_state(&self, height: u64) -> Result { + async fn get_tree_state(&self, height: BlockRef) -> Result { let mut client = self.client(); - let mut request = Request::new(BlockId { - height, - hash: vec![], - }); + let mut request: Request = Request::new(height.into()); request.set_timeout(self.call_timeouts.get_tree_state); let response = client.get_tree_state(request).await?; - Ok(response.into_inner()) + Ok(response.into_inner().into()) + } + + async fn get_block(&self, block: BlockRef) -> Result { + let mut request: Request = Request::new(block.into()); + request.set_timeout(self.call_timeouts.get_tree_state); + + let response = self.client().get_block(request).await?; + Ok(response.into_inner().into()) + } + + fn get_block_nullifiers(&self, block: BlockRef) -> Result { + todo!() + } + + fn get_block_range(&self, range: BlockRange) -> Result { + todo!() + } + + fn get_block_range_nullifiers( + &self, + range: BlockRange, + ) -> Result { + todo!() + } + + fn get_transaction(&self, tx_filter: TxFilter) -> Result { + todo!() + } + + fn get_taddress_txids( + &self, + filter: TransparentAddressBlockFilter, + ) -> Result { + todo!() + } + + fn get_taddress_transactions( + &self, + filter: TransparentAddressBlockFilter, + ) -> Result { + todo!() + } + + fn get_taddress_balance(&self, addresses: AddressList) -> Result { + todo!() + } + + fn get_taddress_balance_stream( + &self, + addresses: impl Stream, + ) -> Result { + todo!() + } + + fn get_mempool_tx( + &self, + request: MempoolTxRequest, + ) -> Result { + todo!() + } + + fn get_mempool_stream(&self) -> Result { + todo!() + } + + fn get_latest_tree_state(&self) -> Result { + todo!() + } + + fn get_subtree_roots( + &self, + request: GetSubtreeRootsRequest, + ) -> Result { + todo!() + } + + fn get_address_utxos( + &self, + request: GetAddressUtxosRequest, + ) -> Result, Self::Error> { + todo!() + } + + fn get_address_utxos_stream( + &self, + request: GetAddressUtxosRequest, + ) -> Result { + todo!() + } + + fn ping(&self, delay: Duration) -> Result { + todo!() } } @@ -325,6 +570,7 @@ mod tests { use hyper_util::rt::TokioIo; use tokio::{net::TcpListener, sync::oneshot, time::timeout}; use tokio_rustls::{TlsAcceptor, rustls}; + use zcash_client_backend::proto::service::LightdInfo; use super::*; @@ -380,6 +626,15 @@ mod tests { block_height: 2_345_678, sapling_activation_height: 419_200, consensus_branch_id: "76b809bb".to_string(), + taddr_support: false, + git_commit: String::new(), + branch: String::new(), + build_date: String::new(), + build_user: String::new(), + estimated_height: 0, + zcashd_build: String::new(), + zcashd_subversion: String::new(), + donation_address: String::new() } ); } @@ -681,7 +936,7 @@ mod tests { let response = GrpcIndexerClient::new(uri) .expect("URI to be valid") - .get_info() + .get_lightd_info() .await .expect("to get info"); diff --git a/zingo-netutils/src/types.rs b/zingo-netutils/src/types.rs new file mode 100644 index 0000000..a218a21 --- /dev/null +++ b/zingo-netutils/src/types.rs @@ -0,0 +1,397 @@ +use zcash_client_backend::proto::service::LightdInfo; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BlockRef { + pub height: u64, + pub hash: Vec, +} + +impl From for BlockRef { + fn from(value: zcash_client_backend::proto::service::BlockId) -> Self { + Self { + height: value.height, + hash: value.hash, + } + } +} + +impl From for zcash_client_backend::proto::service::BlockId { + fn from(value: BlockRef) -> Self { + Self { + height: value.height, + hash: value.hash, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PoolType { + Transparent, + Sapling, + Orchard, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BlockRange { + pub start: BlockRef, + pub end: BlockRef, + pub pool_types: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TxFilter { + pub block: Option, + pub index: Option, + pub hash: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RawTransactionStatus { + InMempool, + Mined { height: u64 }, + MinedOnStaleChain, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawTransaction { + pub data: Vec, + pub status: RawTransactionStatus, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SentTransaction { + pub txid: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServerInfo { + pub version: String, + pub vendor: String, + pub taddr_support: bool, + pub chain_name: String, + pub sapling_activation_height: u64, + pub consensus_branch_id: String, + pub block_height: u64, + pub git_commit: String, + pub branch: String, + pub build_date: String, + pub build_user: String, + pub estimated_height: u64, + pub zcashd_build: String, + pub zcashd_subversion: String, + pub donation_address: String, +} + +impl From for ServerInfo { + fn from(info: LightdInfo) -> Self { + Self { + version: info.version, + vendor: info.vendor, + taddr_support: info.taddr_support, + chain_name: info.chain_name, + sapling_activation_height: info.sapling_activation_height, + consensus_branch_id: info.consensus_branch_id, + block_height: info.block_height, + git_commit: info.git_commit, + branch: info.branch, + build_date: info.build_date, + build_user: info.build_user, + estimated_height: info.estimated_height, + zcashd_build: info.zcashd_build, + zcashd_subversion: info.zcashd_subversion, + donation_address: info.donation_address, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TransparentAddressBlockFilter { + pub address: String, + pub range: BlockRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Address { + pub address: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AddressList { + pub addresses: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Balance { + pub value_zat: i64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MempoolTxRequest { + pub exclude_txid_suffixes: Vec>, + pub pool_types: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TreeState { + pub network: String, + pub height: u64, + pub hash: String, + pub time: u32, + pub sapling_tree: String, + pub orchard_tree: String, +} + +impl From for TreeState { + fn from(value: zcash_client_backend::proto::service::TreeState) -> Self { + Self { + network: value.network, + height: value.height, + hash: value.hash, + time: value.time, + sapling_tree: value.sapling_tree, + orchard_tree: value.orchard_tree, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ShieldedProtocol { + Sapling, + Orchard, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GetSubtreeRootsRequest { + pub start_index: u32, + pub shielded_protocol: ShieldedProtocol, + pub max_entries: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SubtreeRoot { + pub root_hash: Vec, + pub completing_block_hash: Vec, + pub completing_block_height: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GetAddressUtxosRequest { + pub addresses: Vec, + pub start_height: u64, + pub max_entries: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AddressUtxo { + pub address: String, + pub txid: Vec, + pub index: i32, + pub script: Vec, + pub value_zat: i64, + pub height: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PingResponse { + pub entry: i64, + pub exit: i64, +} + +// Compact representations from compact_formats.proto. +// You may decide to keep these crate-local, or to expose a separate +// low-level "compact" module if you do not want these in the top-level API. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ChainMetadata { + pub sapling_commitment_tree_size: u32, + pub orchard_commitment_tree_size: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompactSaplingSpend { + pub nf: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompactSaplingSpends(pub Vec); + +impl From + for CompactSaplingSpend +{ + fn from(value: zcash_client_backend::proto::compact_formats::CompactSaplingSpend) -> Self { + Self { nf: value.nf } + } +} + +impl From> + for CompactSaplingSpends +{ + fn from(value: Vec) -> Self { + Self(value.into_iter().map(Into::into).collect()) + } +} + +impl From> for CompactSaplingSpends { + fn from(value: Vec) -> Self { + Self(value) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompactSaplingOutput { + pub cmu: Vec, + pub ephemeral_key: Vec, + pub ciphertext: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompactSaplingOutputs(pub Vec); + +impl From + for CompactSaplingOutput +{ + fn from(value: zcash_client_backend::proto::compact_formats::CompactSaplingOutput) -> Self { + Self { + cmu: value.cmu, + ephemeral_key: value.ephemeral_key, + ciphertext: value.ciphertext, + } + } +} + +impl From> + for CompactSaplingOutputs +{ + fn from( + value: Vec, + ) -> Self { + Self(value.into_iter().map(Into::into).collect()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompactOrchardAction { + pub nullifier: Vec, + pub cmx: Vec, + pub ephemeral_key: Vec, + pub ciphertext: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompactOrchardActions(pub Vec); + +impl From + for CompactOrchardAction +{ + fn from(value: zcash_client_backend::proto::compact_formats::CompactOrchardAction) -> Self { + Self { + nullifier: value.nullifier, + cmx: value.cmx, + ephemeral_key: value.ephemeral_key, + ciphertext: value.ciphertext, + } + } +} + +impl From> + for CompactOrchardActions +{ + fn from( + value: Vec, + ) -> Self { + Self(value.into_iter().map(Into::into).collect()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompactTx { + pub index: u64, + pub hash: Vec, + pub fee: u32, + pub spends: CompactSaplingSpends, + pub outputs: CompactSaplingOutputs, + pub actions: CompactOrchardActions, + pub vin: CompactTransparentInputs, + pub vout: CompactTransparentOutputs, +} + +impl From for CompactTx { + fn from(value: zcash_client_backend::proto::compact_formats::CompactTx) -> Self { + Self { + index: value.index, + hash: value.hash, + fee: value.fee, + spends: value.spends.into(), + outputs: value.outputs.into(), + actions: value.actions.into(), + vin: value.vin, + vout: value.vout, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompactTransparentInput { + pub prevout_hash: Vec, + pub prevout_index: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompactTransparentInputs(pub Vec); + +impl From + for CompactTransparentInput +{ + fn from(value: zcash_client_backend::proto::compact_formats::CompactTransparentInput) -> Self { + Self { + prevout_hash: value.prevout_hash, + prevout_index: value.prevout_index, + } + } +} + +impl From> + for CompactTransparentInputs +{ + fn from( + value: Vec, + ) -> Self { + Self(value.into_iter().map(Into::into).collect()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompactTransparentOutput { + pub value: u64, + pub script: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompactTransparentOutputs(pub Vec); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompactBlock { + pub proto_version: u32, + pub height: u64, + pub hash: Vec, + pub prev_hash: Vec, + pub time: u32, + pub header: Vec, + pub vtx: Vec, + pub chain_metadata: Option, +} + +impl From for CompactBlock { + fn from(value: zcash_client_backend::proto::compact_formats::CompactBlock) -> Self { + Self { + proto_version: value.proto_version, + height: value.height, + hash: value.hash, + prev_hash: value.prev_hash, + time: value.time, + header: value.header, + vtx: value.vtx, + chain_metadata: value.chain_metadata, + } + } +}