scanning_results_reader/
main.rs

1//! Displays Zebra's scanning results:
2//!
3//! 1. Opens Zebra's scanning storage and reads the results containing scanning keys and TXIDs.
4//! 2. Fetches the transactions by their TXIDs from Zebra using the `getrawtransaction` RPC.
5//! 3. Decrypts the tx outputs using the corresponding scanning key.
6//! 4. Prints the memos in the outputs.
7
8use std::collections::HashMap;
9
10use hex::ToHex;
11use jsonrpc::simple_http::SimpleHttpTransport;
12use jsonrpc::Client;
13
14use zcash_client_backend::decrypt_transaction;
15use zcash_primitives::consensus::{BlockHeight, BranchId};
16use zcash_primitives::transaction::Transaction;
17use zip32::AccountId;
18
19use zebra_scan::scan::{dfvk_to_ufvk, sapling_key_to_dfvk};
20use zebra_scan::{storage::Storage, Config};
21
22/// Prints the memos of transactions from Zebra's scanning results storage.
23///
24/// Reads the results storage, iterates through all decrypted memos, and prints the them to standard
25/// output. Filters out some frequent and uninteresting memos typically associated with ZECPages.
26///
27/// Notes:
28///
29/// - `#[allow(clippy::print_stdout)]` is set to allow usage of `println!` for displaying the memos.
30/// - This function expects Zebra's RPC server to be available.
31///
32/// # Panics
33///
34/// When:
35///
36/// - The Sapling key from the storage is not valid.
37/// - There is no diversifiable full viewing key (dfvk) available.
38/// - The RPC response cannot be decoded from a hex string to bytes.
39/// - The transaction fetched via RPC cannot be deserialized from raw bytes.
40#[allow(clippy::print_stdout)]
41pub fn main() {
42    let network = zebra_chain::parameters::Network::Mainnet;
43    let zp_network = zebra_scan::scan::zp_network(&network);
44    let storage = Storage::new(&Config::default(), &network, true);
45    // If the first memo is empty, it doesn't get printed. But we never print empty memos anyway.
46    let mut prev_memo = "".to_owned();
47
48    for (key, _) in storage.sapling_keys_last_heights().iter() {
49        let ufvks = HashMap::from([(
50            AccountId::ZERO,
51            dfvk_to_ufvk(&sapling_key_to_dfvk(key, &network).expect("dfvk")).expect("ufvk"),
52        )]);
53
54        for (height, txids) in storage.sapling_results(key) {
55            let height = BlockHeight::from(height.0);
56
57            for txid in txids.iter() {
58                let tx = Transaction::read(
59                    &hex::decode(fetch_tx_via_rpc(txid.encode_hex()))
60                        .expect("RPC response should be decodable from hex string to bytes")[..],
61                    BranchId::for_height(&zp_network, height),
62                )
63                .expect("TX fetched via RPC should be deserializable from raw bytes");
64
65                for output in decrypt_transaction(&zp_network, Some(height), None, &tx, &ufvks)
66                    .sapling_outputs()
67                {
68                    let memo = memo_bytes_to_string(output.memo().as_array());
69
70                    if !memo.is_empty()
71                        // Filter out some uninteresting and repeating memos from ZECPages.
72                        && !memo.contains("LIKE:")
73                        && !memo.contains("VOTE:")
74                        && memo != prev_memo
75                    {
76                        println!("{memo}\n");
77                        prev_memo = memo;
78                    }
79                }
80            }
81        }
82    }
83}
84
85/// Trims trailing zeroes from a memo, and returns the memo as a [`String`].
86fn memo_bytes_to_string(memo: &[u8; 512]) -> String {
87    match memo.iter().rposition(|&byte| byte != 0) {
88        Some(i) => String::from_utf8_lossy(&memo[..=i]).into_owned(),
89        None => "".to_owned(),
90    }
91}
92
93/// Uses the `getrawtransaction` RPC to retrieve a transaction by its TXID.
94fn fetch_tx_via_rpc(txid: String) -> String {
95    let client = Client::with_transport(
96        SimpleHttpTransport::builder()
97            .url("127.0.0.1:8232")
98            .expect("Zebra's URL should be valid")
99            .build(),
100    );
101
102    client
103        .send_request(client.build_request("getrawtransaction", Some(&jsonrpc::arg([txid]))))
104        .expect("Sending the `getrawtransaction` request should succeed")
105        .result()
106        .expect("Zebra's RPC response should contain a valid result")
107}