hive-rs 0.1.0

A Rust client library for the Hive blockchain with 1:1 dhive API parity
Documentation
use std::sync::Arc;
use std::time::Duration;

use serde::de::DeserializeOwned;
use serde_json::Value;

use crate::api::{
    AccountByKeyApi, Blockchain, BroadcastApi, DatabaseApi, HivemindApi, RcApi,
    TransactionStatusApi,
};
use crate::error::Result;
use crate::transport::{BackoffStrategy, FailoverTransport};
use crate::types::ChainId;

#[derive(Debug, Clone)]
pub struct ClientOptions {
    pub timeout: Duration,
    pub failover_threshold: u32,
    pub address_prefix: String,
    pub chain_id: ChainId,
    pub backoff: BackoffStrategy,
}

impl Default for ClientOptions {
    fn default() -> Self {
        #[cfg(feature = "testnet")]
        let chain_id = ChainId::testnet();

        #[cfg(not(feature = "testnet"))]
        let chain_id = ChainId::mainnet();

        Self {
            timeout: Duration::from_secs(10),
            failover_threshold: 3,
            address_prefix: "STM".to_string(),
            chain_id,
            backoff: BackoffStrategy::default(),
        }
    }
}

#[derive(Debug)]
pub(crate) struct ClientInner {
    transport: Arc<FailoverTransport>,
    options: ClientOptions,
}

impl ClientInner {
    pub(crate) fn new(transport: Arc<FailoverTransport>, options: ClientOptions) -> Self {
        Self { transport, options }
    }

    pub(crate) async fn call<T: DeserializeOwned>(
        &self,
        api: &str,
        method: &str,
        params: Value,
    ) -> Result<T> {
        self.transport.call(api, method, params).await
    }

    pub(crate) fn options(&self) -> &ClientOptions {
        &self.options
    }
}

#[derive(Debug, Clone)]
pub struct Client {
    inner: Arc<ClientInner>,

    pub database: DatabaseApi,
    pub broadcast: BroadcastApi,
    pub blockchain: Blockchain,
    pub hivemind: HivemindApi,
    pub rc: RcApi,
    pub keys: AccountByKeyApi,
    pub transaction: TransactionStatusApi,
}

impl Client {
    pub fn new(nodes: Vec<&str>, options: ClientOptions) -> Self {
        let node_urls = nodes.into_iter().map(str::to_string).collect::<Vec<_>>();
        assert!(!node_urls.is_empty(), "at least one node URL is required");

        let transport = Arc::new(
            FailoverTransport::new(
                &node_urls,
                options.timeout,
                options.failover_threshold,
                options.backoff.clone(),
            )
            .expect("failed to initialize transport"),
        );

        let inner = Arc::new(ClientInner::new(transport, options));

        Self {
            database: DatabaseApi::new(inner.clone()),
            broadcast: BroadcastApi::new(inner.clone()),
            blockchain: Blockchain::new(inner.clone()),
            hivemind: HivemindApi::new(inner.clone()),
            rc: RcApi::new(inner.clone()),
            keys: AccountByKeyApi::new(inner.clone()),
            transaction: TransactionStatusApi::new(inner.clone()),
            inner,
        }
    }

    pub fn new_default() -> Self {
        Self::new(
            vec!["https://api.hive.blog", "https://api.openhive.network"],
            ClientOptions::default(),
        )
    }

    pub fn options(&self) -> &ClientOptions {
        self.inner.options()
    }

    pub async fn call<T: DeserializeOwned>(
        &self,
        api: &str,
        method: &str,
        params: Value,
    ) -> Result<T> {
        self.inner.call(api, method, params).await
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;
    use wiremock::matchers::{body_partial_json, method};
    use wiremock::{Mock, MockServer, ResponseTemplate};

    use crate::client::{Client, ClientOptions};

    #[tokio::test]
    async fn raw_call_routes_through_transport() {
        let server = MockServer::start().await;
        Mock::given(method("POST"))
            .and(body_partial_json(json!({
                "method": "call",
                "params": ["condenser_api", "get_config", []]
            })))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
                "id": 0,
                "jsonrpc": "2.0",
                "result": {
                    "ok": true
                }
            })))
            .mount(&server)
            .await;

        let client = Client::new(vec![&server.uri()], ClientOptions::default());
        let value: serde_json::Value = client
            .call("condenser_api", "get_config", json!([]))
            .await
            .expect("call should succeed");
        assert_eq!(value["ok"], json!(true));
    }

    #[tokio::test]
    async fn database_api_is_wired_to_client() {
        let server = MockServer::start().await;
        Mock::given(method("POST"))
            .and(body_partial_json(json!({
                "method": "call",
                "params": ["condenser_api", "get_account_count", []]
            })))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
                "id": 0,
                "jsonrpc": "2.0",
                "result": 1337
            })))
            .mount(&server)
            .await;

        let client = Client::new(vec![&server.uri()], ClientOptions::default());
        let count = client
            .database
            .get_account_count()
            .await
            .expect("database call should succeed");
        assert_eq!(count, 1337);
    }
}