Skip to main content

zync_core/client/
lightwalletd.rs

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