zecscope_scanner/
scanner.rs1use crate::error::{ScanError, ScanResult};
4use crate::types::*;
5use zcash_client_backend::{
6 data_api::BlockMetadata,
7 proto::compact_formats,
8 scanning::{scan_block, Nullifiers, ScanningKeys},
9};
10use zcash_keys::keys::UnifiedFullViewingKey;
11use zcash_protocol::consensus::Network;
12use zip32::Scope;
13
14pub struct Scanner {
19 network: Network,
20}
21
22impl Scanner {
23 pub fn new(network: Network) -> Self {
25 Self { network }
26 }
27
28 pub fn mainnet() -> Self {
30 Self::new(Network::MainNetwork)
31 }
32
33 pub fn testnet() -> Self {
35 Self::new(Network::TestNetwork)
36 }
37
38 pub fn scan(&self, request: &ScanRequest) -> ScanResult<Vec<ZecTransaction>> {
42 let viewing_key = normalize_viewing_key(&request.viewing_key);
44
45 let ufvk = UnifiedFullViewingKey::decode(&self.network, &viewing_key)
47 .map_err(|e| ScanError::InvalidViewingKey(e.to_string()))?;
48
49 let blocks = request
51 .compact_blocks
52 .iter()
53 .map(|b| map_compact_block(b))
54 .collect::<ScanResult<Vec<_>>>()?;
55
56 type AccountId = u32;
58 let scanning_keys: ScanningKeys<AccountId, (AccountId, Scope)> =
59 ScanningKeys::from_account_ufvks(std::iter::once((0u32, ufvk)));
60 let nullifiers = Nullifiers::<AccountId>::empty();
61
62 let mut prior_meta: Option<BlockMetadata> = None;
63 let mut transactions = Vec::new();
64
65 for block in blocks {
66 let scanned = scan_block(
67 &self.network,
68 block,
69 &scanning_keys,
70 &nullifiers,
71 prior_meta.as_ref(),
72 )
73 .map_err(|e| ScanError::ScanFailed {
74 height: e.at_height().into(),
75 message: e.to_string(),
76 })?;
77
78 let height: u32 = scanned.height().into();
79 let height = height as u64;
80 let time = scanned.block_time() as i64;
81
82 for wtx in scanned.transactions() {
83 let txid = wtx.txid();
84 let txid_hex = hex::encode(txid.as_ref());
85
86 for out in wtx.sapling_outputs() {
88 if out.is_change() {
89 continue;
90 }
91 let note = out.note();
92 let v = note.value().inner();
93 if v == 0 {
94 continue;
95 }
96
97 transactions.push(ZecTransaction {
98 txid: txid_hex.clone(),
99 height,
100 time,
101 amount_zat: v.to_string(),
102 direction: TxDirection::In,
103 memo: None,
104 key_id: request.key_id.clone(),
105 pool: ShieldedPool::Sapling,
106 });
107 }
108
109 #[cfg(feature = "orchard")]
111 for out in wtx.orchard_outputs() {
112 if out.is_change() {
113 continue;
114 }
115 let note = out.note();
116 let v: u64 = note.value().inner();
117 if v == 0 {
118 continue;
119 }
120
121 transactions.push(ZecTransaction {
122 txid: txid_hex.clone(),
123 height,
124 time,
125 amount_zat: v.to_string(),
126 direction: TxDirection::In,
127 memo: None,
128 key_id: request.key_id.clone(),
129 pool: ShieldedPool::Orchard,
130 });
131 }
132 }
133
134 prior_meta = Some(scanned.to_block_metadata());
135 }
136
137 Ok(transactions)
138 }
139
140 pub fn scan_json(&self, request_json: &str) -> ScanResult<String> {
145 let request: ScanRequest = serde_json::from_str(request_json)?;
146 let transactions = self.scan(&request)?;
147 Ok(serde_json::to_string(&transactions)?)
148 }
149}
150
151fn normalize_viewing_key(raw: &str) -> String {
156 let trimmed = raw.trim();
157 if let Some(idx) = trimmed.find('|') {
158 trimmed[..idx].to_string()
159 } else {
160 trimmed.to_string()
161 }
162}
163
164fn decode_hex(s: &str, field: &str) -> ScanResult<Vec<u8>> {
166 hex::decode(s).map_err(|e| ScanError::InvalidHex {
167 field: field.to_string(),
168 message: e.to_string(),
169 })
170}
171
172fn map_compact_block(block: &CompactBlock) -> ScanResult<compact_formats::CompactBlock> {
174 let vtx = block
175 .vtx
176 .iter()
177 .map(map_compact_tx)
178 .collect::<ScanResult<Vec<_>>>()?;
179
180 let chain_metadata = block.chain_metadata.as_ref().map(|m| {
181 compact_formats::ChainMetadata {
182 sapling_commitment_tree_size: m.sapling_commitment_tree_size,
183 orchard_commitment_tree_size: m.orchard_commitment_tree_size.unwrap_or(0),
184 }
185 });
186
187 Ok(compact_formats::CompactBlock {
188 proto_version: block.proto_version,
189 height: block.height,
190 hash: decode_hex(&block.hash, "block hash")?,
191 prev_hash: decode_hex(&block.prev_hash, "block prevHash")?,
192 time: block.time,
193 header: Vec::new(),
194 vtx,
195 chain_metadata,
196 })
197}
198
199fn map_compact_tx(tx: &CompactTx) -> ScanResult<compact_formats::CompactTx> {
201 let hash = decode_hex(&tx.txid, "txid")?;
202
203 let spends = tx
204 .spends
205 .iter()
206 .map(|s| {
207 Ok(compact_formats::CompactSaplingSpend {
208 nf: decode_hex(&s.nf, "sapling spend nf")?,
209 })
210 })
211 .collect::<ScanResult<Vec<_>>>()?;
212
213 let outputs = tx
214 .outputs
215 .iter()
216 .map(|o| {
217 Ok(compact_formats::CompactSaplingOutput {
218 cmu: decode_hex(&o.cmu, "sapling output cmu")?,
219 ephemeral_key: decode_hex(&o.ephemeral_key, "sapling output ephemeralKey")?,
220 ciphertext: decode_hex(&o.ciphertext, "sapling output ciphertext")?,
221 })
222 })
223 .collect::<ScanResult<Vec<_>>>()?;
224
225 let actions = tx
226 .actions
227 .iter()
228 .map(|a| {
229 Ok(compact_formats::CompactOrchardAction {
230 nullifier: decode_hex(&a.nf, "orchard action nf")?,
231 cmx: decode_hex(&a.cmx, "orchard action cmx")?,
232 ephemeral_key: decode_hex(&a.ephemeral_key, "orchard action ephemeralKey")?,
233 ciphertext: decode_hex(&a.ciphertext, "orchard action ciphertext")?,
234 })
235 })
236 .collect::<ScanResult<Vec<_>>>()?;
237
238 Ok(compact_formats::CompactTx {
239 index: tx.index,
240 hash,
241 fee: tx.fee.unwrap_or(0),
242 spends,
243 outputs,
244 actions,
245 })
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_normalize_viewing_key() {
254 assert_eq!(
256 normalize_viewing_key("uview1abc123"),
257 "uview1abc123"
258 );
259
260 assert_eq!(
262 normalize_viewing_key("uview1abc123|uivk1xyz789"),
263 "uview1abc123"
264 );
265
266 assert_eq!(
268 normalize_viewing_key(" uview1abc123 "),
269 "uview1abc123"
270 );
271 }
272}