urma_runtime/
endpoints.rs1use 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}