rgbp/resolvers/
electrum.rs

1// Wallet Library for RGB smart contracts
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Designed in 2019-2025 by Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
6// Written in 2024-2025 by Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland.
9// Copyright (C) 2024-2025 LNP/BP Laboratories,
10//                         Institute for Distributed and Cognitive Systems (InDCS), Switzerland.
11// Copyright (C) 2025 RGB Consortium, Switzerland.
12// Copyright (C) 2019-2025 Dr Maxim Orlovsky.
13// All rights under the above copyrights are reserved.
14//
15// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
16// in compliance with the License. You may obtain a copy of the License at
17//
18//        http://www.apache.org/licenses/LICENSE-2.0
19//
20// Unless required by applicable law or agreed to in writing, software distributed under the License
21// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
22// or implied. See the License for the specific language governing permissions and limitations under
23// the License.
24
25use 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}