qualia_client_core/wallet/
chronik.rs1use 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 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#[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#[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#[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#[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}