Skip to main content

tensora/api/
coordinator.rs

1//! Coordinator API client for epoch and VRF data
2
3use crate::errors::{Result, TensoraError};
4use crate::types::EpochInfo;
5use ethers::types::{Address, H256};
6use reqwest::Client;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use url::Url;
10
11/// Validator/miner assignment from coordinator
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Assignment {
14    /// Subnet ID
15    pub subnet_id: u32,
16    /// Assigned address
17    pub address: Address,
18    /// Assignment type (validator or miner)
19    pub role: String,
20    /// VRF proof
21    pub vrf_proof: Option<Vec<u8>>,
22}
23
24/// Client for the Tensora Coordinator API
25pub struct CoordinatorClient {
26    base_url: Url,
27    client: Client,
28}
29
30impl CoordinatorClient {
31    /// Create a new Coordinator client
32    ///
33    /// # Arguments
34    /// * `base_url` - Base URL of the coordinator API (e.g., https://coordinator.tensora.sh)
35    pub fn new(base_url: &str) -> Result<Self> {
36        let base_url = Url::parse(base_url)
37            .map_err(|e| TensoraError::Api(format!("Invalid coordinator URL: {}", e)))?;
38        
39        let client = Client::builder()
40            .timeout(std::time::Duration::from_secs(30))
41            .build()
42            .map_err(|e| TensoraError::Api(format!("Failed to create HTTP client: {}", e)))?;
43
44        Ok(Self { base_url, client })
45    }
46
47    /// Get current epoch information
48    ///
49    /// # Example
50    /// ```no_run
51    /// # use tensora::api::CoordinatorClient;
52    /// # #[tokio::main]
53    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
54    /// let coordinator = CoordinatorClient::new("https://coordinator.tensora.sh")?;
55    /// let epoch = coordinator.get_current_epoch().await?;
56    /// println!("Current epoch: {}", epoch.epoch);
57    /// # Ok(())
58    /// # }
59    /// ```
60    pub async fn get_current_epoch(&self) -> Result<EpochInfo> {
61        let url = self.base_url.join("/vrf/epoch")
62            .map_err(|e| TensoraError::Api(format!("Invalid URL: {}", e)))?;
63
64        let response = self.client
65            .get(url)
66            .send()
67            .await?;
68
69        if !response.status().is_success() {
70            return Err(TensoraError::Api(format!("HTTP {}", response.status())));
71        }
72
73        let epoch: EpochInfo = response.json().await?;
74        Ok(epoch)
75    }
76
77    /// Get validator/miner assignments for current epoch
78    ///
79    /// # Arguments
80    /// * `subnet_id` - Optional subnet ID filter
81    pub async fn get_assignments(&self, subnet_id: Option<u32>) -> Result<Vec<Assignment>> {
82        let path = if let Some(id) = subnet_id {
83            format!("/assignments?subnet_id={}", id)
84        } else {
85            "/assignments".to_string()
86        };
87
88        let url = self.base_url.join(&path)
89            .map_err(|e| TensoraError::Api(format!("Invalid URL: {}", e)))?;
90
91        let response = self.client
92            .get(url)
93            .send()
94            .await?;
95
96        if !response.status().is_success() {
97            return Err(TensoraError::Api(format!("HTTP {}", response.status())));
98        }
99
100        let assignments: Vec<Assignment> = response.json().await?;
101        Ok(assignments)
102    }
103
104    /// Get VRF seed for a specific epoch
105    ///
106    /// # Arguments
107    /// * `epoch` - Epoch number
108    pub async fn get_vrf_seed(&self, epoch: u64) -> Result<H256> {
109        let url = self.base_url.join(&format!("/vrf/epoch/{}/seed", epoch))
110            .map_err(|e| TensoraError::Api(format!("Invalid URL: {}", e)))?;
111
112        let response = self.client
113            .get(url)
114            .send()
115            .await?;
116
117        if !response.status().is_success() {
118            return Err(TensoraError::Api(format!("HTTP {}", response.status())));
119        }
120
121        let data: Value = response.json().await?;
122        let seed_str = data["seed"]
123            .as_str()
124            .ok_or_else(|| TensoraError::Api("Invalid VRF seed response".to_string()))?;
125
126        let seed: H256 = seed_str
127            .parse()
128            .map_err(|e| TensoraError::Api(format!("Invalid VRF seed format: {}", e)))?;
129
130        Ok(seed)
131    }
132}
133
134impl Default for CoordinatorClient {
135    fn default() -> Self {
136        Self::new("https://coordinator.tensora.sh")
137            .expect("Failed to create default CoordinatorClient")
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn test_coordinator_client_creation() {
147        let client = CoordinatorClient::new("https://coordinator.tensora.sh");
148        assert!(client.is_ok());
149    }
150
151    #[test]
152    fn test_invalid_url() {
153        let client = CoordinatorClient::new("not a url");
154        assert!(client.is_err());
155    }
156}
157