Skip to main content

zync_core/client/
lightwalletd.rs

1//! lightwalletd gRPC client (public endpoint fallback)
2
3use anyhow::Result;
4use super::{
5    lightwalletd_proto::{
6        compact_tx_streamer_client::CompactTxStreamerClient as GrpcClient,
7        ChainSpec, BlockId, BlockRange, TxFilter, GetAddressUtxosArg,
8        RawTransaction, Empty,
9    },
10    TreeState, Utxo, SendResult, CompactBlock, CompactAction,
11};
12use tonic::transport::Channel;
13use tracing::{info, debug, warn};
14
15pub struct LightwalletdClient {
16    client: GrpcClient<Channel>,
17    chain_name: String,
18}
19
20impl LightwalletdClient {
21    pub async fn connect(url: &str) -> Result<Self> {
22        info!("connecting to lightwalletd at {}", url);
23        let client = GrpcClient::connect(url.to_string()).await?;
24        Ok(Self {
25            client,
26            chain_name: String::new(),
27        })
28    }
29
30    /// get lightwalletd server info
31    pub async fn get_lightd_info(&mut self) -> Result<LightdInfo> {
32        let request = tonic::Request::new(Empty {});
33        let response = self.client.get_lightd_info(request).await?;
34        let info = response.into_inner();
35
36        self.chain_name = info.chain_name.clone();
37
38        Ok(LightdInfo {
39            version: info.version,
40            vendor: info.vendor,
41            taddr_support: info.taddr_support,
42            chain_name: info.chain_name,
43            sapling_activation_height: info.sapling_activation_height,
44            consensus_branch_id: info.consensus_branch_id,
45            block_height: info.block_height,
46            estimated_height: info.estimated_height,
47        })
48    }
49
50    /// get latest block height and hash
51    pub async fn get_latest_block(&mut self) -> Result<(u64, Vec<u8>)> {
52        let request = tonic::Request::new(ChainSpec {});
53        let response = self.client.get_latest_block(request).await?;
54        let block = response.into_inner();
55
56        Ok((block.height, block.hash))
57    }
58
59    /// stream compact blocks for scanning
60    pub async fn get_block_range(
61        &mut self,
62        start_height: u64,
63        end_height: u64,
64    ) -> Result<Vec<CompactBlock>> {
65        let request = tonic::Request::new(BlockRange {
66            start: Some(BlockId { height: start_height, hash: vec![] }),
67            end: Some(BlockId { height: end_height, hash: vec![] }),
68        });
69
70        let mut stream = self.client.get_block_range(request).await?.into_inner();
71        let mut blocks = Vec::new();
72
73        while let Some(block) = stream.message().await? {
74            let mut actions = Vec::new();
75
76            // extract orchard actions from transactions
77            for tx in block.vtx {
78                for action in tx.actions {
79                    let mut cmx = [0u8; 32];
80                    let mut ek = [0u8; 32];
81                    let mut nf = [0u8; 32];
82                    if action.cmx.len() == 32 { cmx.copy_from_slice(&action.cmx); }
83                    if action.ephemeral_key.len() == 32 { ek.copy_from_slice(&action.ephemeral_key); }
84                    if action.nullifier.len() == 32 { nf.copy_from_slice(&action.nullifier); }
85                    actions.push(CompactAction {
86                        cmx,
87                        ephemeral_key: ek,
88                        ciphertext: action.ciphertext,
89                        nullifier: nf,
90                    });
91                }
92            }
93
94            blocks.push(CompactBlock {
95                height: block.height as u32,
96                hash: block.hash,
97                actions,
98            });
99        }
100
101        Ok(blocks)
102    }
103
104    /// send raw transaction to the network
105    pub async fn send_transaction(&mut self, tx_data: Vec<u8>) -> Result<SendResult> {
106        let request = tonic::Request::new(RawTransaction {
107            data: tx_data,
108            height: 0,
109        });
110        let response = self.client.send_transaction(request).await?;
111        let resp = response.into_inner();
112
113        Ok(SendResult {
114            txid: String::new(), // lightwalletd doesn't return txid
115            error_code: resp.error_code,
116            error_message: resp.error_message,
117        })
118    }
119
120    /// get transaction by hash
121    pub async fn get_transaction(&mut self, txid: &[u8; 32]) -> Result<Vec<u8>> {
122        let request = tonic::Request::new(TxFilter {
123            block: None,
124            index: 0,
125            hash: txid.to_vec(),
126        });
127        let response = self.client.get_transaction(request).await?;
128        Ok(response.into_inner().data)
129    }
130
131    /// get tree state at a given height
132    pub async fn get_tree_state(&mut self, height: u64) -> Result<TreeState> {
133        let request = tonic::Request::new(BlockId {
134            height,
135            hash: vec![],
136        });
137        let response = self.client.get_tree_state(request).await?;
138        let state = response.into_inner();
139
140        Ok(TreeState {
141            height: state.height as u32,
142            hash: hex::decode(&state.hash).unwrap_or_default(),
143            time: state.time as u64,
144            sapling_tree: state.sapling_tree,
145            orchard_tree: state.orchard_tree,
146        })
147    }
148
149    /// get transparent UTXOs for addresses
150    pub async fn get_address_utxos(&mut self, addresses: Vec<String>) -> Result<Vec<Utxo>> {
151        let request = tonic::Request::new(GetAddressUtxosArg {
152            addresses,
153            start_height: 0,
154            max_entries: 0,
155        });
156        let response = self.client.get_address_utxos(request).await?;
157        let utxos = response.into_inner().address_utxos;
158
159        Ok(utxos.into_iter().map(|u| {
160            let mut txid = [0u8; 32];
161            if u.txid.len() == 32 { txid.copy_from_slice(&u.txid); }
162            Utxo {
163                address: u.address,
164                txid,
165                output_index: u.index as u32,
166                script: u.script,
167                value_zat: u.value_zat as u64,
168                height: u.height as u32,
169            }
170        }).collect())
171    }
172}
173
174/// lightwalletd server info
175#[derive(Debug, Clone)]
176pub struct LightdInfo {
177    pub version: String,
178    pub vendor: String,
179    pub taddr_support: bool,
180    pub chain_name: String,
181    pub sapling_activation_height: u64,
182    pub consensus_branch_id: String,
183    pub block_height: u64,
184    pub estimated_height: u64,
185}