zecscope_scanner/
types.rs

1//! Types for scanner input/output.
2
3use serde::{Deserialize, Serialize};
4
5/// Which shielded pool a transaction belongs to.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum ShieldedPool {
9    /// Sapling shielded pool (activated at Sapling upgrade)
10    Sapling,
11    /// Orchard shielded pool (activated at NU5)
12    Orchard,
13}
14
15impl std::fmt::Display for ShieldedPool {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        match self {
18            ShieldedPool::Sapling => write!(f, "sapling"),
19            ShieldedPool::Orchard => write!(f, "orchard"),
20        }
21    }
22}
23
24/// Direction of a transaction relative to the viewing key.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "lowercase")]
27pub enum TxDirection {
28    /// Incoming transaction (received funds)
29    In,
30    /// Outgoing transaction (sent funds)
31    Out,
32}
33
34/// A discovered shielded transaction.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase")]
37pub struct ZecTransaction {
38    /// Transaction ID (hex-encoded)
39    pub txid: String,
40    /// Block height where this transaction was mined
41    pub height: u64,
42    /// Block timestamp (Unix seconds)
43    pub time: i64,
44    /// Amount in zatoshis (as string to avoid precision loss)
45    pub amount_zat: String,
46    /// Direction relative to the viewing key
47    pub direction: TxDirection,
48    /// Decoded memo (if available and valid UTF-8)
49    pub memo: Option<String>,
50    /// ID of the viewing key that discovered this transaction
51    pub key_id: String,
52    /// Which shielded pool this transaction is in
53    pub pool: ShieldedPool,
54}
55
56impl ZecTransaction {
57    /// Get the amount in ZEC (floating point).
58    pub fn amount_zec(&self) -> f64 {
59        self.amount_zat
60            .parse::<i64>()
61            .map(|z| z as f64 / 100_000_000.0)
62            .unwrap_or(0.0)
63    }
64
65    /// Get the amount in zatoshis.
66    pub fn amount_zatoshis(&self) -> i64 {
67        self.amount_zat.parse().unwrap_or(0)
68    }
69}
70
71/// Request to scan compact blocks with a viewing key.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ScanRequest {
74    /// Unified Full Viewing Key (uview1...)
75    pub viewing_key: String,
76    /// Identifier for this key (for tracking which key found which tx)
77    pub key_id: String,
78    /// Compact blocks to scan
79    pub compact_blocks: Vec<CompactBlock>,
80}
81
82/// A compact block from lightwalletd.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct CompactBlock {
86    /// Protocol version
87    pub proto_version: u32,
88    /// Block height
89    pub height: u64,
90    /// Block hash (hex-encoded)
91    pub hash: String,
92    /// Previous block hash (hex-encoded)
93    pub prev_hash: String,
94    /// Block timestamp (Unix seconds)
95    pub time: u32,
96    /// Transactions in this block
97    #[serde(default)]
98    pub vtx: Vec<CompactTx>,
99    /// Chain metadata (commitment tree sizes)
100    #[serde(default)]
101    pub chain_metadata: Option<ChainMetadata>,
102}
103
104/// A compact transaction.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct CompactTx {
108    /// Transaction index in block
109    pub index: u64,
110    /// Transaction ID (hex-encoded)
111    pub txid: String,
112    /// Transaction fee (optional)
113    #[serde(default)]
114    pub fee: Option<u32>,
115    /// Sapling spends
116    #[serde(default)]
117    pub spends: Vec<CompactSaplingSpend>,
118    /// Sapling outputs
119    #[serde(default)]
120    pub outputs: Vec<CompactSaplingOutput>,
121    /// Orchard actions
122    #[serde(default)]
123    pub actions: Vec<CompactOrchardAction>,
124}
125
126/// A compact Sapling spend.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct CompactSaplingSpend {
129    /// Nullifier (hex-encoded)
130    pub nf: String,
131}
132
133/// A compact Sapling output.
134#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct CompactSaplingOutput {
137    /// Note commitment (hex-encoded)
138    pub cmu: String,
139    /// Ephemeral key (hex-encoded)
140    pub ephemeral_key: String,
141    /// Encrypted ciphertext (hex-encoded, first 52 bytes)
142    pub ciphertext: String,
143}
144
145/// A compact Orchard action.
146#[derive(Debug, Clone, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct CompactOrchardAction {
149    /// Nullifier (hex-encoded)
150    pub nf: String,
151    /// Note commitment (hex-encoded)
152    pub cmx: String,
153    /// Ephemeral key (hex-encoded)
154    pub ephemeral_key: String,
155    /// Encrypted ciphertext (hex-encoded, first 52 bytes)
156    pub ciphertext: String,
157}
158
159/// Chain metadata from compact blocks.
160#[derive(Debug, Clone, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct ChainMetadata {
163    /// Sapling commitment tree size at this block
164    pub sapling_commitment_tree_size: u32,
165    /// Orchard commitment tree size at this block (if applicable)
166    #[serde(default)]
167    pub orchard_commitment_tree_size: Option<u32>,
168}
169
170/// Result of scanning a range of blocks.
171#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct ScanSummary {
174    /// Discovered transactions
175    pub transactions: Vec<ZecTransaction>,
176    /// Number of blocks scanned
177    pub blocks_scanned: usize,
178    /// Start height
179    pub start_height: u64,
180    /// End height
181    pub end_height: u64,
182    /// Sapling transactions found
183    pub sapling_count: usize,
184    /// Orchard transactions found
185    pub orchard_count: usize,
186}
187
188impl ScanSummary {
189    /// Create a new scan summary from transactions.
190    pub fn from_transactions(txs: Vec<ZecTransaction>, start: u64, end: u64) -> Self {
191        let sapling_count = txs.iter().filter(|t| t.pool == ShieldedPool::Sapling).count();
192        let orchard_count = txs.iter().filter(|t| t.pool == ShieldedPool::Orchard).count();
193        Self {
194            blocks_scanned: (end - start + 1) as usize,
195            start_height: start,
196            end_height: end,
197            sapling_count,
198            orchard_count,
199            transactions: txs,
200        }
201    }
202}