use crate::api::MyceliumClient; use crate::error::AppResult; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PeerEndpoint { pub proto: String, pub socket_addr: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PeerInfo { pub endpoint: PeerEndpoint, #[serde(rename = "type")] pub kind: String, // "static" | "inbound" | "linkLocalDiscovery" pub connection_state: String, // "alive" | "connecting" | "dead" #[serde(default)] pub tx_bytes: u64, #[serde(default)] pub rx_bytes: u64, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct AggregatedStats { pub peers_total: usize, pub peers_alive: usize, pub peers_connecting: usize, pub peers_dead: usize, pub tx_bytes: u64, pub rx_bytes: u64, } pub fn aggregate(peers: &[PeerInfo]) -> AggregatedStats { let mut s = AggregatedStats { peers_total: peers.len(), ..Default::default() }; for p in peers { s.tx_bytes = s.tx_bytes.saturating_add(p.tx_bytes); s.rx_bytes = s.rx_bytes.saturating_add(p.rx_bytes); match p.connection_state.as_str() { "alive" => s.peers_alive += 1, "connecting" => s.peers_connecting += 1, "dead" => s.peers_dead += 1, _ => {} } } s } #[derive(Debug, Serialize)] struct AddPeerBody<'a> { endpoint: &'a str, } impl MyceliumClient { pub async fn list_peers(&self) -> AppResult> { let resp = self.http().get(self.url("/admin/peers")).send().await?; Self::parse(resp).await } /// The upstream OpenAPI doesn't pin the request body shape; the daemon /// accepts `{"endpoint": "://:"}`. pub async fn add_peer(&self, endpoint: &str) -> AppResult<()> { let resp = self .http() .post(self.url("/admin/peers")) .json(&AddPeerBody { endpoint }) .send() .await?; Self::check_status(resp).await } pub async fn remove_peer(&self, endpoint: &str) -> AppResult<()> { // Endpoints look like `tcp://188.40.132.242:9651` — the slashes and // colons must be percent-encoded for the path segment. let encoded = url_encode_path_segment(endpoint); let resp = self .http() .delete(self.url(&format!("/admin/peers/{encoded}"))) .send() .await?; Self::check_status(resp).await } } fn url_encode_path_segment(s: &str) -> String { // Minimal ASCII percent-encoder for the chars that would otherwise // break path parsing. Avoids pulling a full url-encoding crate. let mut out = String::with_capacity(s.len()); for b in s.bytes() { match b { b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => out.push(b as char), _ => out.push_str(&format!("%{:02X}", b)), } } out }