use std::sync::atomic::{AtomicU64, Ordering};
use serde_json::json;
use crate::error::{ElectrumError, Result};
use crate::scripthash::address_to_scripthash;
use crate::transport::Transport;
use crate::types::{Balance, ClientConfig, ServerVersion, TxHistory, Utxo};
pub struct ElectrumClient {
transport: Transport,
request_id: AtomicU64,
}
impl ElectrumClient {
pub async fn new(server: &str) -> Result<Self> {
Self::with_config(ClientConfig::ssl(server)).await
}
pub async fn with_config(config: ClientConfig) -> Result<Self> {
let transport = Transport::connect(config).await?;
Ok(Self {
transport,
request_id: AtomicU64::new(1),
})
}
fn next_id(&self) -> u64 {
self.request_id.fetch_add(1, Ordering::SeqCst)
}
pub async fn get_balance(&self, address: &str) -> Result<Balance> {
let scripthash = address_to_scripthash(address)?;
self.get_balance_scripthash(&scripthash).await
}
pub async fn get_balance_scripthash(&self, scripthash: &str) -> Result<Balance> {
let id = self.next_id();
let result = self
.transport
.request(
id,
"blockchain.scripthash.get_balance",
vec![json!(scripthash)],
)
.await?;
serde_json::from_value(result).map_err(|e| ElectrumError::InvalidResponse(e.to_string()))
}
pub async fn get_balances(&self, addresses: &[&str]) -> Result<Vec<Balance>> {
if addresses.is_empty() {
return Ok(vec![]);
}
let scripthashes: Vec<String> = addresses
.iter()
.map(|a| address_to_scripthash(a))
.collect::<Result<Vec<_>>>()?;
self.get_balances_scripthash(&scripthashes).await
}
pub async fn get_balances_scripthash(&self, scripthashes: &[String]) -> Result<Vec<Balance>> {
if scripthashes.is_empty() {
return Ok(vec![]);
}
let requests: Vec<(u64, &str, Vec<serde_json::Value>)> = scripthashes
.iter()
.map(|sh| {
(
self.next_id(),
"blockchain.scripthash.get_balance",
vec![json!(sh)],
)
})
.collect();
let results = self.transport.batch_request(requests).await?;
results
.into_iter()
.map(|r| {
serde_json::from_value(r)
.map_err(|e| ElectrumError::InvalidResponse(e.to_string()))
})
.collect()
}
pub async fn list_unspent(&self, address: &str) -> Result<Vec<Utxo>> {
let scripthash = address_to_scripthash(address)?;
self.list_unspent_scripthash(&scripthash).await
}
pub async fn list_unspent_scripthash(&self, scripthash: &str) -> Result<Vec<Utxo>> {
let id = self.next_id();
let result = self
.transport
.request(
id,
"blockchain.scripthash.listunspent",
vec![json!(scripthash)],
)
.await?;
serde_json::from_value(result).map_err(|e| ElectrumError::InvalidResponse(e.to_string()))
}
pub async fn get_transaction(&self, txid: &str) -> Result<String> {
let id = self.next_id();
let result = self
.transport
.request(id, "blockchain.transaction.get", vec![json!(txid)])
.await?;
result
.as_str()
.map(|s| s.to_string())
.ok_or_else(|| ElectrumError::InvalidResponse("Expected string".into()))
}
pub async fn broadcast(&self, raw_tx: &str) -> Result<String> {
let id = self.next_id();
let result = self
.transport
.request(id, "blockchain.transaction.broadcast", vec![json!(raw_tx)])
.await?;
result
.as_str()
.map(|s| s.to_string())
.ok_or_else(|| ElectrumError::InvalidResponse("Expected txid string".into()))
}
pub async fn get_history(&self, address: &str) -> Result<Vec<TxHistory>> {
let scripthash = address_to_scripthash(address)?;
self.get_history_scripthash(&scripthash).await
}
pub async fn get_history_scripthash(&self, scripthash: &str) -> Result<Vec<TxHistory>> {
let id = self.next_id();
let result = self
.transport
.request(
id,
"blockchain.scripthash.get_history",
vec![json!(scripthash)],
)
.await?;
serde_json::from_value(result).map_err(|e| ElectrumError::InvalidResponse(e.to_string()))
}
pub async fn server_version(&self) -> Result<ServerVersion> {
let id = self.next_id();
let result = self
.transport
.request(
id,
"server.version",
vec![json!("rustywallet-electrum"), json!("1.4")],
)
.await?;
let arr = result
.as_array()
.ok_or_else(|| ElectrumError::InvalidResponse("Expected array".into()))?;
if arr.len() < 2 {
return Err(ElectrumError::InvalidResponse(
"Expected [server, protocol]".into(),
));
}
Ok(ServerVersion {
server_software: arr[0].as_str().unwrap_or("unknown").to_string(),
protocol_version: arr[1].as_str().unwrap_or("unknown").to_string(),
})
}
pub async fn ping(&self) -> Result<()> {
let id = self.next_id();
self.transport
.request(id, "server.ping", vec![])
.await?;
Ok(())
}
pub async fn get_block_height(&self) -> Result<u64> {
let id = self.next_id();
let result = self
.transport
.request(id, "blockchain.headers.subscribe", vec![])
.await?;
result
.get("height")
.and_then(|h| h.as_u64())
.ok_or_else(|| ElectrumError::InvalidResponse("Expected height".into()))
}
pub async fn estimate_fee(&self, blocks: u32) -> Result<f64> {
let id = self.next_id();
let result = self
.transport
.request(id, "blockchain.estimatefee", vec![json!(blocks)])
.await?;
result
.as_f64()
.ok_or_else(|| ElectrumError::InvalidResponse("Expected fee rate".into()))
}
}