Skip to main content

zync_core/client/
zidecar.rs

1//! zidecar gRPC client
2
3use super::{
4    zidecar_proto::{
5        zidecar_client::ZidecarClient as GrpcClient, BlockId, BlockRange, Empty, ProofRequest,
6        RawTransaction, TransparentAddressFilter, TxFilter,
7    },
8    CompactAction, CompactBlock, SendResult, SyncStatus, TreeState, Utxo,
9};
10use anyhow::Result;
11use tonic::transport::Channel;
12use tracing::{debug, info};
13
14pub struct ZidecarClient {
15    client: GrpcClient<Channel>,
16}
17
18impl ZidecarClient {
19    pub async fn connect(url: &str) -> Result<Self> {
20        info!("connecting to zidecar at {}", url);
21        let client = GrpcClient::connect(url.to_string()).await?;
22        Ok(Self { client })
23    }
24
25    /// get epoch proof + tip proof for full chain
26    pub async fn get_header_proof(&mut self) -> Result<(Vec<u8>, u32, u32)> {
27        let request = tonic::Request::new(ProofRequest {
28            from_height: 0,
29            to_height: 0, // 0 = tip
30        });
31
32        let response = self.client.get_header_proof(request).await?;
33        let proof = response.into_inner();
34
35        debug!(
36            "received proof: {} -> {} ({} bytes)",
37            proof.from_height,
38            proof.to_height,
39            proof.ligerito_proof.len()
40        );
41
42        Ok((proof.ligerito_proof, proof.from_height, proof.to_height))
43    }
44
45    /// get current chain tip
46    pub async fn get_tip(&mut self) -> Result<(u32, Vec<u8>)> {
47        let request = tonic::Request::new(Empty {});
48        let response = self.client.get_tip(request).await?;
49        let tip = response.into_inner();
50
51        Ok((tip.height, tip.hash))
52    }
53
54    /// stream compact blocks for scanning
55    pub async fn get_compact_blocks(
56        &mut self,
57        start_height: u32,
58        end_height: u32,
59    ) -> Result<Vec<CompactBlock>> {
60        let request = tonic::Request::new(BlockRange {
61            start_height,
62            end_height,
63        });
64
65        let mut stream = self.client.get_compact_blocks(request).await?.into_inner();
66        let mut blocks = Vec::new();
67
68        while let Some(block) = stream.message().await? {
69            let actions: Vec<CompactAction> = block
70                .actions
71                .into_iter()
72                .map(|a| {
73                    let mut cmx = [0u8; 32];
74                    let mut ek = [0u8; 32];
75                    let mut nf = [0u8; 32];
76                    if a.cmx.len() == 32 {
77                        cmx.copy_from_slice(&a.cmx);
78                    }
79                    if a.ephemeral_key.len() == 32 {
80                        ek.copy_from_slice(&a.ephemeral_key);
81                    }
82                    if a.nullifier.len() == 32 {
83                        nf.copy_from_slice(&a.nullifier);
84                    }
85                    CompactAction {
86                        cmx,
87                        ephemeral_key: ek,
88                        ciphertext: a.ciphertext,
89                        nullifier: nf,
90                    }
91                })
92                .collect();
93
94            blocks.push(CompactBlock {
95                height: block.height,
96                hash: block.hash,
97                actions,
98            });
99        }
100
101        Ok(blocks)
102    }
103
104    /// get sync status (blockchain height, epoch progress, epoch proof status)
105    pub async fn get_sync_status(&mut self) -> Result<SyncStatus> {
106        let request = tonic::Request::new(Empty {});
107        let response = self.client.get_sync_status(request).await?;
108        let status = response.into_inner();
109
110        Ok(SyncStatus {
111            current_height: status.current_height,
112            current_epoch: status.current_epoch,
113            blocks_in_epoch: status.blocks_in_epoch,
114            complete_epochs: status.complete_epochs,
115            epoch_proof_ready: status.epoch_proof_status == 2, // READY
116            blocks_until_ready: status.blocks_until_ready,
117            last_epoch_proof_height: status.last_epoch_proof_height,
118        })
119    }
120
121    /// send raw transaction to the network
122    pub async fn send_transaction(&mut self, tx_data: Vec<u8>) -> Result<SendResult> {
123        let request = tonic::Request::new(RawTransaction {
124            data: tx_data,
125            height: 0,
126        });
127        let response = self.client.send_transaction(request).await?;
128        let resp = response.into_inner();
129        Ok(SendResult {
130            txid: resp.txid,
131            error_code: resp.error_code,
132            error_message: resp.error_message,
133        })
134    }
135
136    /// get transaction by hash
137    pub async fn get_transaction(&mut self, txid: &[u8; 32]) -> Result<Vec<u8>> {
138        let request = tonic::Request::new(TxFilter {
139            hash: txid.to_vec(),
140        });
141        let response = self.client.get_transaction(request).await?;
142        Ok(response.into_inner().data)
143    }
144
145    /// get tree state at a given height
146    pub async fn get_tree_state(&mut self, height: u32) -> Result<TreeState> {
147        let request = tonic::Request::new(BlockId {
148            height,
149            hash: vec![],
150        });
151        let response = self.client.get_tree_state(request).await?;
152        let state = response.into_inner();
153        Ok(TreeState {
154            height: state.height,
155            hash: state.hash,
156            time: state.time,
157            sapling_tree: state.sapling_tree,
158            orchard_tree: state.orchard_tree,
159        })
160    }
161
162    /// get transparent UTXOs for addresses
163    pub async fn get_address_utxos(&mut self, addresses: Vec<String>) -> Result<Vec<Utxo>> {
164        let request = tonic::Request::new(TransparentAddressFilter {
165            addresses,
166            start_height: 0,
167            max_entries: 0,
168        });
169        let response = self.client.get_address_utxos(request).await?;
170        let utxos = response.into_inner().utxos;
171
172        Ok(utxos
173            .into_iter()
174            .map(|u| {
175                let mut txid = [0u8; 32];
176                if u.txid.len() == 32 {
177                    txid.copy_from_slice(&u.txid);
178                }
179                Utxo {
180                    address: u.address,
181                    txid,
182                    output_index: u.output_index,
183                    script: u.script,
184                    value_zat: u.value_zat,
185                    height: u.height,
186                }
187            })
188            .collect())
189    }
190
191    /// get transparent transaction IDs for addresses
192    pub async fn get_taddress_txids(
193        &mut self,
194        addresses: Vec<String>,
195        start_height: u32,
196    ) -> Result<Vec<[u8; 32]>> {
197        let request = tonic::Request::new(TransparentAddressFilter {
198            addresses,
199            start_height,
200            max_entries: 0,
201        });
202        let response = self.client.get_taddress_txids(request).await?;
203        let txids = response.into_inner().txids;
204
205        Ok(txids
206            .into_iter()
207            .filter_map(|t| {
208                if t.len() == 32 {
209                    let mut arr = [0u8; 32];
210                    arr.copy_from_slice(&t);
211                    Some(arr)
212                } else {
213                    None
214                }
215            })
216            .collect())
217    }
218}