rustywallet_electrum/
client.rs1use std::sync::atomic::{AtomicU64, Ordering};
7
8use serde_json::json;
9
10use crate::error::{ElectrumError, Result};
11use crate::scripthash::address_to_scripthash;
12use crate::transport::Transport;
13use crate::types::{Balance, ClientConfig, ServerVersion, TxHistory, Utxo};
14
15pub struct ElectrumClient {
36 transport: Transport,
37 request_id: AtomicU64,
38}
39
40impl ElectrumClient {
41 pub async fn new(server: &str) -> Result<Self> {
45 Self::with_config(ClientConfig::ssl(server)).await
46 }
47
48 pub async fn with_config(config: ClientConfig) -> Result<Self> {
50 let transport = Transport::connect(config).await?;
51 Ok(Self {
52 transport,
53 request_id: AtomicU64::new(1),
54 })
55 }
56
57 fn next_id(&self) -> u64 {
59 self.request_id.fetch_add(1, Ordering::SeqCst)
60 }
61
62 pub async fn get_balance(&self, address: &str) -> Result<Balance> {
72 let scripthash = address_to_scripthash(address)?;
73 self.get_balance_scripthash(&scripthash).await
74 }
75
76 pub async fn get_balance_scripthash(&self, scripthash: &str) -> Result<Balance> {
78 let id = self.next_id();
79 let result = self
80 .transport
81 .request(
82 id,
83 "blockchain.scripthash.get_balance",
84 vec![json!(scripthash)],
85 )
86 .await?;
87
88 serde_json::from_value(result).map_err(|e| ElectrumError::InvalidResponse(e.to_string()))
89 }
90
91 pub async fn get_balances(&self, addresses: &[&str]) -> Result<Vec<Balance>> {
101 if addresses.is_empty() {
102 return Ok(vec![]);
103 }
104
105 let scripthashes: Vec<String> = addresses
107 .iter()
108 .map(|a| address_to_scripthash(a))
109 .collect::<Result<Vec<_>>>()?;
110
111 self.get_balances_scripthash(&scripthashes).await
112 }
113
114 pub async fn get_balances_scripthash(&self, scripthashes: &[String]) -> Result<Vec<Balance>> {
116 if scripthashes.is_empty() {
117 return Ok(vec![]);
118 }
119
120 let requests: Vec<(u64, &str, Vec<serde_json::Value>)> = scripthashes
121 .iter()
122 .map(|sh| {
123 (
124 self.next_id(),
125 "blockchain.scripthash.get_balance",
126 vec![json!(sh)],
127 )
128 })
129 .collect();
130
131 let results = self.transport.batch_request(requests).await?;
132
133 results
134 .into_iter()
135 .map(|r| {
136 serde_json::from_value(r)
137 .map_err(|e| ElectrumError::InvalidResponse(e.to_string()))
138 })
139 .collect()
140 }
141
142 pub async fn list_unspent(&self, address: &str) -> Result<Vec<Utxo>> {
152 let scripthash = address_to_scripthash(address)?;
153 self.list_unspent_scripthash(&scripthash).await
154 }
155
156 pub async fn list_unspent_scripthash(&self, scripthash: &str) -> Result<Vec<Utxo>> {
158 let id = self.next_id();
159 let result = self
160 .transport
161 .request(
162 id,
163 "blockchain.scripthash.listunspent",
164 vec![json!(scripthash)],
165 )
166 .await?;
167
168 serde_json::from_value(result).map_err(|e| ElectrumError::InvalidResponse(e.to_string()))
169 }
170
171 pub async fn get_transaction(&self, txid: &str) -> Result<String> {
181 let id = self.next_id();
182 let result = self
183 .transport
184 .request(id, "blockchain.transaction.get", vec![json!(txid)])
185 .await?;
186
187 result
188 .as_str()
189 .map(|s| s.to_string())
190 .ok_or_else(|| ElectrumError::InvalidResponse("Expected string".into()))
191 }
192
193 pub async fn broadcast(&self, raw_tx: &str) -> Result<String> {
201 let id = self.next_id();
202 let result = self
203 .transport
204 .request(id, "blockchain.transaction.broadcast", vec![json!(raw_tx)])
205 .await?;
206
207 result
208 .as_str()
209 .map(|s| s.to_string())
210 .ok_or_else(|| ElectrumError::InvalidResponse("Expected txid string".into()))
211 }
212
213 pub async fn get_history(&self, address: &str) -> Result<Vec<TxHistory>> {
221 let scripthash = address_to_scripthash(address)?;
222 self.get_history_scripthash(&scripthash).await
223 }
224
225 pub async fn get_history_scripthash(&self, scripthash: &str) -> Result<Vec<TxHistory>> {
227 let id = self.next_id();
228 let result = self
229 .transport
230 .request(
231 id,
232 "blockchain.scripthash.get_history",
233 vec![json!(scripthash)],
234 )
235 .await?;
236
237 serde_json::from_value(result).map_err(|e| ElectrumError::InvalidResponse(e.to_string()))
238 }
239
240 pub async fn server_version(&self) -> Result<ServerVersion> {
246 let id = self.next_id();
247 let result = self
248 .transport
249 .request(
250 id,
251 "server.version",
252 vec![json!("rustywallet-electrum"), json!("1.4")],
253 )
254 .await?;
255
256 let arr = result
257 .as_array()
258 .ok_or_else(|| ElectrumError::InvalidResponse("Expected array".into()))?;
259
260 if arr.len() < 2 {
261 return Err(ElectrumError::InvalidResponse(
262 "Expected [server, protocol]".into(),
263 ));
264 }
265
266 Ok(ServerVersion {
267 server_software: arr[0].as_str().unwrap_or("unknown").to_string(),
268 protocol_version: arr[1].as_str().unwrap_or("unknown").to_string(),
269 })
270 }
271
272 pub async fn ping(&self) -> Result<()> {
274 let id = self.next_id();
275 self.transport
276 .request(id, "server.ping", vec![])
277 .await?;
278 Ok(())
279 }
280
281 pub async fn get_block_height(&self) -> Result<u64> {
283 let id = self.next_id();
284 let result = self
285 .transport
286 .request(id, "blockchain.headers.subscribe", vec![])
287 .await?;
288
289 result
290 .get("height")
291 .and_then(|h| h.as_u64())
292 .ok_or_else(|| ElectrumError::InvalidResponse("Expected height".into()))
293 }
294
295 pub async fn estimate_fee(&self, blocks: u32) -> Result<f64> {
300 let id = self.next_id();
301 let result = self
302 .transport
303 .request(id, "blockchain.estimatefee", vec![json!(blocks)])
304 .await?;
305
306 result
307 .as_f64()
308 .ok_or_else(|| ElectrumError::InvalidResponse("Expected fee rate".into()))
309 }
310}