kaspa_wrpc_client/
resolver.rs

1//!
2//! Module implementing [`Resolver`] client for obtaining public Kaspa wRPC endpoints.
3//!
4
5use std::sync::OnceLock;
6
7use crate::error::Error;
8use crate::imports::*;
9use crate::node::NodeDescriptor;
10pub use futures::future::join_all;
11use rand::seq::SliceRandom;
12use rand::thread_rng;
13use workflow_core::runtime;
14use workflow_http::get_json;
15
16const CURRENT_VERSION: usize = 2;
17const RESOLVER_CONFIG: &str = include_str!("../Resolvers.toml");
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ResolverRecord {
21    pub address: String,
22    pub enable: Option<bool>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct ResolverGroup {
27    pub template: String,
28    pub nodes: Vec<String>,
29    pub enable: Option<bool>,
30}
31
32#[derive(Clone, Debug, Serialize, Deserialize)]
33pub struct ResolverConfig {
34    #[serde(rename = "group")]
35    groups: Vec<ResolverGroup>,
36    #[serde(rename = "resolver")]
37    resolvers: Vec<ResolverRecord>,
38}
39
40fn try_parse_resolvers(toml: &str) -> Result<Vec<Arc<String>>> {
41    let config = toml::from_str::<ResolverConfig>(toml)?;
42
43    let mut resolvers = config
44        .resolvers
45        .into_iter()
46        .filter_map(|resolver| resolver.enable.unwrap_or(true).then_some(resolver.address))
47        .collect::<Vec<_>>();
48
49    let groups = config.groups.into_iter().filter(|group| group.enable.unwrap_or(true)).collect::<Vec<_>>();
50
51    for group in groups {
52        let ResolverGroup { template, nodes, .. } = group;
53        for node in nodes {
54            resolvers.push(template.replace('*', &node));
55        }
56    }
57
58    Ok(resolvers.into_iter().map(Arc::new).collect::<Vec<_>>())
59}
60
61#[derive(Debug)]
62struct Inner {
63    pub urls: Vec<Arc<String>>,
64    pub tls: bool,
65    public: bool,
66}
67
68impl Inner {
69    pub fn new(urls: Option<Vec<Arc<String>>>, tls: bool) -> Self {
70        if urls.as_ref().is_some_and(|urls| urls.is_empty()) {
71            panic!("Resolver: Empty URL list supplied to the constructor.");
72        }
73
74        let mut public = false;
75        let urls = urls.unwrap_or_else(|| {
76            public = true;
77            try_parse_resolvers(RESOLVER_CONFIG).expect("TOML: Unable to parse RPC Resolver list")
78        });
79
80        Self { urls, tls, public }
81    }
82}
83
84///
85/// # Resolver - a client for obtaining public Kaspa wRPC endpoints.
86///
87/// This client operates against [Kaspa Resolver](https://github.com/aspectron/kaspa-resolver) service
88/// that provides load-balancing and failover capabilities for Kaspa wRPC endpoints. The default
89/// configuration allows access to public Kaspa nodes, while custom configurations can be supplied
90/// if you are running your own custom Kaspa node cluster.
91///
92#[derive(Debug, Clone)]
93pub struct Resolver {
94    inner: Arc<Inner>,
95}
96
97impl Default for Resolver {
98    fn default() -> Self {
99        Self { inner: Arc::new(Inner::new(None, false)) }
100    }
101}
102
103impl Resolver {
104    /// Create a new [`Resolver`] client with the specified list of resolver URLs and an optional `tls` flag.
105    /// The `tls` flag can be used to enforce secure connection to the node.
106    pub fn new(urls: Option<Vec<Arc<String>>>, tls: bool) -> Self {
107        Self { inner: Arc::new(Inner::new(urls, tls)) }
108    }
109
110    /// Obtain a list of URLs in the resolver client. (This function
111    /// returns `None` if the resolver is configured to use public
112    /// node endpoints.)
113    pub fn urls(&self) -> Option<Vec<Arc<String>>> {
114        if self.inner.public {
115            None
116        } else {
117            Some(self.inner.urls.clone())
118        }
119    }
120
121    /// Obtain the `tls` flag in the resolver client.
122    pub fn tls(&self) -> bool {
123        self.inner.tls
124    }
125
126    fn tls_as_str(&self) -> &'static str {
127        if self.inner.tls {
128            "tls"
129        } else {
130            "any"
131        }
132    }
133
134    fn make_url(&self, url: &str, encoding: Encoding, network_id: NetworkId) -> String {
135        static TLS: OnceLock<&'static str> = OnceLock::new();
136
137        let tls = *TLS.get_or_init(|| {
138            if runtime::is_web() {
139                let tls = js_sys::Reflect::get(&js_sys::global(), &"location".into())
140                    .and_then(|location| js_sys::Reflect::get(&location, &"protocol".into()))
141                    .ok()
142                    .and_then(|protocol| protocol.as_string())
143                    .map(|protocol| protocol.starts_with("https"))
144                    .unwrap_or(false);
145                if tls {
146                    "tls"
147                } else {
148                    self.tls_as_str()
149                }
150            } else {
151                self.tls_as_str()
152            }
153        });
154
155        format!("{url}/v{CURRENT_VERSION}/kaspa/{network_id}/{tls}/wrpc/{encoding}")
156    }
157
158    // query a single resolver service
159    async fn fetch_node_info(&self, url: &str, encoding: Encoding, network_id: NetworkId) -> Result<NodeDescriptor> {
160        let url = self.make_url(url, encoding, network_id);
161        let node =
162            get_json::<NodeDescriptor>(&url).await.map_err(|error| Error::custom(format!("Unable to connect to {url}: {error}")))?;
163        Ok(node)
164    }
165
166    // query multiple resolver services in random order
167    async fn fetch(&self, encoding: Encoding, network_id: NetworkId) -> Result<NodeDescriptor> {
168        let mut urls = self.inner.urls.clone();
169        urls.shuffle(&mut thread_rng());
170
171        let mut errors = Vec::default();
172        for url in urls {
173            match self.fetch_node_info(&url, encoding, network_id).await {
174                Ok(node) => return Ok(node),
175                Err(error) => errors.push(error),
176            }
177        }
178        Err(Error::Custom(format!("Failed to connect: {:?}", errors)))
179    }
180
181    /// Obtain a Kaspa p2p [`NodeDescriptor`] from the resolver based on the supplied [`Encoding`] and [`NetworkId`].
182    pub async fn get_node(&self, encoding: Encoding, network_id: NetworkId) -> Result<NodeDescriptor> {
183        self.fetch(encoding, network_id).await
184    }
185
186    /// Returns a Kaspa wRPC URL from the resolver based on the supplied [`Encoding`] and [`NetworkId`].
187    pub async fn get_url(&self, encoding: Encoding, network_id: NetworkId) -> Result<String> {
188        let nodes = self.fetch(encoding, network_id).await?;
189        Ok(nodes.url.clone())
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_resolver_config_1() {
199        let toml = r#"
200            [[group]]
201            enable = true
202            template = "https://*.example.org"
203            nodes = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta"]
204
205            [[group]]
206            enable = true
207            template = "https://*.example.com"
208            nodes = ["iota", "kappa", "lambda", "mu", "nu", "xi", "omicron", "pi"]
209
210            [[resolver]]
211            enable = true
212            address = "http://127.0.0.1:8888"
213        "#;
214
215        let urls = try_parse_resolvers(toml).expect("TOML: Unable to parse RPC Resolver list");
216        // println!("{:#?}", urls);
217        assert_eq!(urls.len(), 17);
218    }
219
220    #[test]
221    fn test_resolver_config_2() {
222        let _urls = try_parse_resolvers(RESOLVER_CONFIG).expect("TOML: Unable to parse RPC Resolver list");
223        // println!("{:#?}", urls);
224    }
225}