Skip to main content

hive_rs/
client.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use serde::de::DeserializeOwned;
5use serde_json::Value;
6
7use crate::api::{
8    AccountByKeyApi, Blockchain, BroadcastApi, DatabaseApi, HivemindApi, RcApi,
9    TransactionStatusApi,
10};
11use crate::error::Result;
12use crate::transport::{BackoffStrategy, FailoverTransport};
13use crate::types::ChainId;
14
15#[derive(Debug, Clone)]
16pub struct ClientOptions {
17    pub timeout: Duration,
18    pub failover_threshold: u32,
19    pub address_prefix: String,
20    pub chain_id: ChainId,
21    pub backoff: BackoffStrategy,
22}
23
24impl Default for ClientOptions {
25    fn default() -> Self {
26        #[cfg(feature = "testnet")]
27        let chain_id = ChainId::testnet();
28
29        #[cfg(not(feature = "testnet"))]
30        let chain_id = ChainId::mainnet();
31
32        Self {
33            timeout: Duration::from_secs(10),
34            failover_threshold: 3,
35            address_prefix: "STM".to_string(),
36            chain_id,
37            backoff: BackoffStrategy::default(),
38        }
39    }
40}
41
42#[derive(Debug)]
43pub(crate) struct ClientInner {
44    transport: Arc<FailoverTransport>,
45    options: ClientOptions,
46}
47
48impl ClientInner {
49    pub(crate) fn new(transport: Arc<FailoverTransport>, options: ClientOptions) -> Self {
50        Self { transport, options }
51    }
52
53    pub(crate) async fn call<T: DeserializeOwned>(
54        &self,
55        api: &str,
56        method: &str,
57        params: Value,
58    ) -> Result<T> {
59        self.transport.call(api, method, params).await
60    }
61
62    pub(crate) fn options(&self) -> &ClientOptions {
63        &self.options
64    }
65}
66
67#[derive(Debug, Clone)]
68pub struct Client {
69    inner: Arc<ClientInner>,
70
71    pub database: DatabaseApi,
72    pub broadcast: BroadcastApi,
73    pub blockchain: Blockchain,
74    pub hivemind: HivemindApi,
75    pub rc: RcApi,
76    pub keys: AccountByKeyApi,
77    pub transaction: TransactionStatusApi,
78}
79
80impl Client {
81    pub fn new(nodes: Vec<&str>, options: ClientOptions) -> Self {
82        let node_urls = nodes.into_iter().map(str::to_string).collect::<Vec<_>>();
83        assert!(!node_urls.is_empty(), "at least one node URL is required");
84
85        let transport = Arc::new(
86            FailoverTransport::new(
87                &node_urls,
88                options.timeout,
89                options.failover_threshold,
90                options.backoff.clone(),
91            )
92            .expect("failed to initialize transport"),
93        );
94
95        let inner = Arc::new(ClientInner::new(transport, options));
96
97        Self {
98            database: DatabaseApi::new(inner.clone()),
99            broadcast: BroadcastApi::new(inner.clone()),
100            blockchain: Blockchain::new(inner.clone()),
101            hivemind: HivemindApi::new(inner.clone()),
102            rc: RcApi::new(inner.clone()),
103            keys: AccountByKeyApi::new(inner.clone()),
104            transaction: TransactionStatusApi::new(inner.clone()),
105            inner,
106        }
107    }
108
109    pub fn new_default() -> Self {
110        Self::new(
111            vec!["https://api.hive.blog", "https://api.openhive.network"],
112            ClientOptions::default(),
113        )
114    }
115
116    pub fn options(&self) -> &ClientOptions {
117        self.inner.options()
118    }
119
120    pub async fn call<T: DeserializeOwned>(
121        &self,
122        api: &str,
123        method: &str,
124        params: Value,
125    ) -> Result<T> {
126        self.inner.call(api, method, params).await
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use serde_json::json;
133    use wiremock::matchers::{body_partial_json, method};
134    use wiremock::{Mock, MockServer, ResponseTemplate};
135
136    use crate::client::{Client, ClientOptions};
137
138    #[tokio::test]
139    async fn raw_call_routes_through_transport() {
140        let server = MockServer::start().await;
141        Mock::given(method("POST"))
142            .and(body_partial_json(json!({
143                "method": "call",
144                "params": ["condenser_api", "get_config", []]
145            })))
146            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
147                "id": 0,
148                "jsonrpc": "2.0",
149                "result": {
150                    "ok": true
151                }
152            })))
153            .mount(&server)
154            .await;
155
156        let client = Client::new(vec![&server.uri()], ClientOptions::default());
157        let value: serde_json::Value = client
158            .call("condenser_api", "get_config", json!([]))
159            .await
160            .expect("call should succeed");
161        assert_eq!(value["ok"], json!(true));
162    }
163
164    #[tokio::test]
165    async fn database_api_is_wired_to_client() {
166        let server = MockServer::start().await;
167        Mock::given(method("POST"))
168            .and(body_partial_json(json!({
169                "method": "call",
170                "params": ["condenser_api", "get_account_count", []]
171            })))
172            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
173                "id": 0,
174                "jsonrpc": "2.0",
175                "result": 1337
176            })))
177            .mount(&server)
178            .await;
179
180        let client = Client::new(vec![&server.uri()], ClientOptions::default());
181        let count = client
182            .database
183            .get_account_count()
184            .await
185            .expect("database call should succeed");
186        assert_eq!(count, 1337);
187    }
188}