rgbp/resolvers/
electrum.rs1use core::num::NonZeroU64;
26
27use bpstd::psbt::Utxo;
28use bpstd::{Outpoint, Sats, ScriptPubkey, Terminal, Tx, Txid, UnsignedTx};
29use electrum::client::Client as ElectrumClient;
30use electrum::{ElectrumApi, Error as ElectrumError};
31use rgb::WitnessStatus;
32
33use super::{Resolver, ResolverError};
34
35pub struct ElectrumResolver(ElectrumClient);
36
37impl ElectrumResolver {
38 pub fn new(url: &str) -> Result<Self, ResolverError> { Ok(Self(ElectrumClient::new(url)?)) }
39}
40
41impl Resolver for ElectrumResolver {
42 fn resolve_tx(&self, txid: Txid) -> Result<Option<UnsignedTx>, ResolverError> {
43 let tx = self.0.transaction_get(&txid)?;
44 Ok(tx.map(UnsignedTx::with_sigs_removed))
45 }
46
47 fn resolve_tx_status(&self, txid: Txid) -> Result<WitnessStatus, ResolverError> {
48 let Some(verbose) = self.0.transaction_get_verbose(&txid)? else {
49 return Ok(WitnessStatus::Archived);
50 };
51 if verbose.block_hash.is_none() {
52 return Ok(WitnessStatus::Tentative);
53 };
54 if verbose.time.is_none() {
55 return Ok(WitnessStatus::Tentative);
56 };
57 let last_height = self.last_block_height()?;
58 let height = last_height - verbose.confirmations as u64;
59 let Some(height) = NonZeroU64::new(height) else {
60 return Ok(WitnessStatus::Genesis);
61 };
62 Ok(WitnessStatus::Mined(height))
63 }
64
65 fn resolve_utxos(
66 &self,
67 iter: impl IntoIterator<Item = (Terminal, ScriptPubkey)>,
68 ) -> impl Iterator<Item = Result<Utxo, ResolverError>> {
69 iter.into_iter()
70 .flat_map(|(terminal, spk)| match self.0.script_list_unspent(&spk) {
71 Err(err) => vec![Err(ResolverError::from(err))],
72 Ok(list) => list
73 .into_iter()
74 .map(|res| {
75 Ok(Utxo {
76 outpoint: Outpoint::new(res.tx_hash, res.tx_pos as u32),
77 value: Sats::from_sats(res.value),
78 terminal,
79 })
80 })
81 .collect::<Vec<_>>(),
82 })
83 }
84
85 fn last_block_height(&self) -> Result<u64, ResolverError> {
86 Ok(self.0.block_headers_subscribe()?.height as u64)
87 }
88
89 fn broadcast(&self, tx: &Tx) -> Result<(), ResolverError> {
90 self.0.transaction_broadcast(tx)?;
91 Ok(())
92 }
93}
94
95impl From<ElectrumError> for ResolverError {
96 fn from(err: ElectrumError) -> Self {
97 match err {
98 ElectrumError::IOError(err) => ResolverError::Io(err.into()),
99 ElectrumError::SharedIOError(err) => ResolverError::Io(err.kind().into()),
100
101 ElectrumError::InvalidDNSNameError(_) | ElectrumError::MissingDomain => {
102 ResolverError::Connectivity
103 }
104
105 ElectrumError::CouldNotCreateConnection(_)
106 | ElectrumError::CouldntLockReader
107 | ElectrumError::Mpsc => ResolverError::Local,
108
109 ElectrumError::InvalidResponse(_)
110 | ElectrumError::JSON(_)
111 | ElectrumError::Hex(_)
112 | ElectrumError::JSONRpc(_)
113 | ElectrumError::Bitcoin(_) => ResolverError::Protocol,
114
115 ElectrumError::Protocol(err) => ResolverError::ServerSide(err.message),
116
117 ElectrumError::AlreadySubscribed(_) | ElectrumError::NotSubscribed(_) => {
118 ResolverError::Logic
119 }
120
121 ElectrumError::AllAttemptsErrored(list) => list
122 .into_iter()
123 .next()
124 .map(ResolverError::from)
125 .unwrap_or(ResolverError::Protocol),
126 }
127 }
128}