Skip to main content

urma_runtime/
endpoints.rs

1use crate::error::{Context, Error, ensure};
2use urma_chain::observation::Chain;
3
4#[derive(Clone, Debug)]
5pub enum PublicEndpoint {
6    Rpc(String),
7    Esplora(String),
8    Electrum(String),
9}
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum Wire {
13    Tls,
14    PlainTcp,
15}
16
17#[derive(Clone, Debug)]
18pub struct ElectrumTarget {
19    pub host: String,
20    pub port: u16,
21    pub wire: Wire,
22}
23
24impl PublicEndpoint {
25    pub fn url(&self) -> &str {
26        match self {
27            Self::Rpc(url) | Self::Esplora(url) | Self::Electrum(url) => url,
28        }
29    }
30
31    pub fn validate(&self) -> Result<(), Error> {
32        let url = url::Url::parse(self.url())?;
33        ensure!(
34            url.username().is_empty()
35                && url.password().iter().count() == 0
36                && url.query().iter().count() == 0
37                && url.fragment().iter().count() == 0,
38            "public sources never accept embedded credentials"
39        );
40        match self {
41            Self::Rpc(_) | Self::Esplora(_) => {
42                ensure!(url.scheme() == "https", "public sources require HTTPS");
43            }
44            Self::Electrum(_) => {
45                self.electrum_target()?;
46            }
47        }
48        Ok(())
49    }
50
51    pub fn electrum_target(&self) -> Result<ElectrumTarget, Error> {
52        let url = url::Url::parse(self.url())?;
53        let wire = match url.scheme() {
54            "ssl" => Wire::Tls,
55            "tcp" => Wire::PlainTcp,
56            scheme => {
57                return Err(Error::Invalid(format!(
58                    "electrum endpoints use ssl:// or tcp://, not {scheme}://"
59                )));
60            }
61        };
62        ensure!(
63            url.path().is_empty() || url.path() == "/",
64            "electrum endpoints carry no path"
65        );
66        let host = url.host_str().context("electrum endpoint host missing")?;
67        ensure!(
68            host.bytes()
69                .all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'-'),
70            "electrum endpoint host must be a DNS name or IPv4 address"
71        );
72        Ok(ElectrumTarget {
73            host: host.to_owned(),
74            port: url.port().context("electrum endpoint port missing")?,
75            wire,
76        })
77    }
78}
79
80pub fn defaults(chain: Chain) -> Vec<PublicEndpoint> {
81    match chain {
82        Chain::BitcoinMainnet => vec![
83            PublicEndpoint::Electrum("ssl://electrum.blockstream.info:50002".into()),
84            PublicEndpoint::Electrum("ssl://bitcoin.lu.ke:50002".into()),
85            PublicEndpoint::Esplora("https://blockstream.info/api".into()),
86            PublicEndpoint::Esplora("https://mempool.space/api".into()),
87        ],
88        Chain::LitecoinMainnet => vec![
89            PublicEndpoint::Electrum("ssl://electrum.ltc.xurious.com:50002".into()),
90            PublicEndpoint::Electrum("ssl://electrum-ltc.bysh.me:50002".into()),
91            PublicEndpoint::Electrum("ssl://backup.electrum-ltc.org:443".into()),
92            PublicEndpoint::Electrum("ssl://electrum1.cipig.net:20063".into()),
93            PublicEndpoint::Esplora("https://litecoinspace.org/api".into()),
94            PublicEndpoint::Rpc("https://litecoin-mainnet.gateway.tatum.io".into()),
95        ],
96        Chain::LitecoinTestnet => vec![
97            PublicEndpoint::Electrum("ssl://electrum-ltc.bysh.me:51002".into()),
98            PublicEndpoint::Electrum("ssl://electrum.ltc.xurious.com:51002".into()),
99            PublicEndpoint::Esplora("https://litecoinspace.org/testnet/api".into()),
100            PublicEndpoint::Rpc("https://litecoin-testnet.gateway.tatum.io".into()),
101        ],
102        Chain::BitcoinTestnet4 => vec![PublicEndpoint::Esplora(
103            "https://mempool.space/testnet4/api".into(),
104        )],
105        Chain::BitcoinRegtest => Vec::new(),
106    }
107}