Skip to main content

qualia_client_core/wallet/
chronik.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct ChronikUtxo {
5    pub outpoint: Outpoint,
6    pub block_height: i32,
7    pub is_coinbase: bool,
8    pub value: i64,
9    #[serde(default)]
10    pub slp_meta: Option<SlpMeta>,
11    #[serde(default)]
12    pub slp_token: Option<SlpToken>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Outpoint {
17    pub txid: String,
18    pub out_idx: u32,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct SlpMeta {
23    pub token_type: String,
24    pub tx_type: String,
25    pub token_id: String,
26    pub group_token_id: Option<String>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct SlpToken {
31    pub amount: String,
32    pub is_mint_baton: bool,
33}
34
35pub struct ChronikClient {
36    pub base_url: String,
37    client: reqwest::blocking::Client,
38}
39
40impl ChronikClient {
41    pub fn new(base_url: &str) -> Self {
42        Self {
43            base_url: base_url.to_string(),
44            client: reqwest::blocking::Client::new(),
45        }
46    }
47
48    pub fn fetch_utxos_p2pkh(&self, hash160_hex: &str) -> Result<Vec<ChronikUtxo>, String> {
49        let url = format!("{}/script/p2pkh/{}/utxos", self.base_url, hash160_hex);
50        let resp = self.client.get(&url).send().map_err(|e| e.to_string())?;
51
52        if !resp.status().is_success() {
53            return Err(format!("Chronik API error: {}", resp.status()));
54        }
55
56        #[derive(Deserialize)]
57        struct ScriptUtxos {
58            utxos: Vec<ChronikUtxo>,
59        }
60
61        let result: Vec<ScriptUtxos> = resp.json().map_err(|e| e.to_string())?;
62
63        let mut all_utxos = Vec::new();
64        for script_utxo in result {
65            all_utxos.extend(script_utxo.utxos);
66        }
67        Ok(all_utxos)
68    }
69
70    pub fn broadcast_tx(&self, raw_tx_hex: &str) -> Result<String, String> {
71        let url = format!("{}/broadcast-tx", self.base_url);
72
73        let resp = self
74            .client
75            .post(&url)
76            .header("Content-Type", "text/plain")
77            .body(raw_tx_hex.to_string())
78            .send()
79            .map_err(|e| e.to_string())?;
80
81        if !resp.status().is_success() {
82            let err_text = resp.text().unwrap_or_default();
83            return Err(format!("Broadcast failed: {}", err_text));
84        }
85
86        #[derive(Deserialize)]
87        struct BroadcastResponse {
88            txid: String,
89        }
90
91        let res: BroadcastResponse = resp.json().map_err(|e| e.to_string())?;
92        Ok(res.txid)
93    }
94
95    /// Fetch transaction history for a P2PKH script hash.
96    /// Returns the most recent transactions (up to `page_size`, default 25).
97    pub fn fetch_tx_history_p2pkh(
98        &self,
99        hash160_hex: &str,
100        page: u32,
101        page_size: u32,
102    ) -> Result<ChronikTxHistoryPage, String> {
103        let url = format!(
104            "{}/script/p2pkh/{}/history?page={}&page_size={}",
105            self.base_url, hash160_hex, page, page_size
106        );
107        let resp = self.client.get(&url).send().map_err(|e| e.to_string())?;
108
109        if !resp.status().is_success() {
110            return Err(format!("Chronik history API error: {}", resp.status()));
111        }
112
113        let result: ChronikTxHistoryPage = resp.json().map_err(|e| e.to_string())?;
114        Ok(result)
115    }
116}
117
118/// A page of transaction history from Chronik.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct ChronikTxHistoryPage {
122    #[serde(default)]
123    pub txs: Vec<ChronikTx>,
124    #[serde(default)]
125    pub num_pages: u32,
126    #[serde(default)]
127    pub num_txs: u32,
128}
129
130/// A transaction returned by Chronik's history endpoint.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct ChronikTx {
134    pub txid: String,
135    #[serde(default)]
136    pub version: i32,
137    #[serde(default)]
138    pub inputs: Vec<ChronikTxIO>,
139    #[serde(default)]
140    pub outputs: Vec<ChronikTxIO>,
141    #[serde(default)]
142    pub block: Option<ChronikBlock>,
143    #[serde(default)]
144    pub time_first_seen: i64,
145    #[serde(default)]
146    pub size: u32,
147    #[serde(default)]
148    pub is_coinbase: bool,
149}
150
151/// An input or output in a Chronik transaction.
152#[derive(Debug, Clone, Serialize, Deserialize)]
153#[serde(rename_all = "camelCase")]
154pub struct ChronikTxIO {
155    #[serde(default)]
156    pub value: i64,
157    #[serde(default)]
158    pub output_script: String,
159    #[serde(default)]
160    pub slp_meta: Option<SlpMeta>,
161    #[serde(default)]
162    pub slp_token: Option<SlpToken>,
163}
164
165/// Block info for a confirmed transaction.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167#[serde(rename_all = "camelCase")]
168pub struct ChronikBlock {
169    pub height: i32,
170    #[serde(default)]
171    pub hash: String,
172    #[serde(default)]
173    pub timestamp: i64,
174}