accumulate-sdk 2.1.0

Accumulate Rust SDK (V2/V3 unified) with DevNet-first flows
Documentation
//! GENERATED BY Accumulate gen-sdk. DO NOT EDIT.

#![allow(missing_docs)]

use reqwest::Client;
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::time::Duration;
use thiserror::Error;
use url::Url;

#[derive(Error, Debug)]
pub enum JsonRpcError {
    #[error("HTTP request failed: {0}")]
    Http(#[from] reqwest::Error),

    #[error("JSON serialization/deserialization failed: {0}")]
    Json(#[from] serde_json::Error),

    #[error("JSON-RPC error: code={code}, message={message}")]
    Rpc { code: i32, message: String },

    #[error("Invalid URL: {0}")]
    InvalidUrl(#[from] url::ParseError),

    #[error("General error: {0}")]
    General(#[from] anyhow::Error),
}

/// JSON-RPC client for Accumulate API calls
#[derive(Debug, Clone)]
pub struct JsonRpcClient {
    pub base_url: Url,
    pub http: Client,
}

impl JsonRpcClient {
    /// Create a new JSON-RPC client
    pub fn new(base_url: Url) -> Result<Self, JsonRpcError> {
        let http = Client::builder().timeout(Duration::from_secs(30)).build()?;

        Ok(Self { base_url, http })
    }

    /// Create a new JSON-RPC client with custom HTTP client
    pub fn with_client(base_url: Url, http: Client) -> Result<Self, JsonRpcError> {
        Ok(Self { base_url, http })
    }

    /// Make a generic JSON-RPC call
    pub async fn call<T: DeserializeOwned>(
        &self,
        method: &str,
        params: Value,
    ) -> Result<T, JsonRpcError> {
        let request_body = serde_json::json!({
            "jsonrpc": "2.0",
            "id": 1,
            "method": method,
            "params": params
        });

        let response = self
            .http
            .post(self.base_url.clone())
            .json(&request_body)
            .send()
            .await?;

        let response_text = response.text().await?;
        let response_json: Value = serde_json::from_str(&response_text)?;

        if let Some(error) = response_json.get("error") {
            let code = error.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) as i32;
            let message = error
                .get("message")
                .and_then(|m| m.as_str())
                .unwrap_or("Unknown error")
                .to_string();
            return Err(JsonRpcError::Rpc { code, message });
        }

        let result = response_json.get("result").cloned().unwrap_or(Value::Null);
        Ok(serde_json::from_value(result)?)
    }

    /// Make a V2 API call (non-JSON-RPC)
    pub async fn call_v2<T: DeserializeOwned>(
        &self,
        method_path: &str,
        payload: Option<Value>,
    ) -> Result<T, JsonRpcError> {
        let url = self.base_url.join(method_path)?;

        let request = match payload {
            Some(body) => self.http.post(url).json(&body),
            None => self.http.get(url),
        };

        let response = request.send().await?;
        let response_text = response.text().await?;

        Ok(serde_json::from_str(&response_text)?)
    }

    /// Make a V3 API call
    pub async fn call_v3<T: DeserializeOwned>(
        &self,
        method: &str,
        params: Value,
    ) -> Result<T, JsonRpcError> {
        self.call(method, params).await
    }
}

/// Convert a JSON value to canonical JSON string with deterministic ordering
pub fn canonical_json(value: &Value) -> String {
    crate::canonjson::canonicalize(value)
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn test_canonical_json() {
        let value = json!({
            "b": 2,
            "a": 1,
            "c": {
                "z": 3,
                "y": 4
            }
        });

        let canonical = canonical_json(&value);
        assert!(canonical.contains(r#""a":1"#));
        assert!(canonical.contains(r#""b":2"#));

        // Keys should be in alphabetical order
        let a_pos = canonical.find(r#""a":"#).unwrap();
        let b_pos = canonical.find(r#""b":"#).unwrap();
        assert!(a_pos < b_pos);
    }
}