Skip to main content

sal_mycelium/
lib.rs

1//! SAL Mycelium - Client interface for interacting with Mycelium node's HTTP API
2//!
3//! This crate provides a client interface for interacting with a Mycelium node's HTTP API.
4//! Mycelium is a decentralized networking project, and this SAL module allows Rust applications
5//! and `herodo` Rhai scripts to manage and communicate over a Mycelium network.
6//!
7//! The module enables operations such as:
8//! - Querying node status and information
9//! - Managing peer connections (listing, adding, removing)
10//! - Inspecting routing tables (selected and fallback routes)
11//! - Sending messages to other Mycelium nodes
12//! - Receiving messages from subscribed topics
13//!
14//! All interactions with the Mycelium API are performed asynchronously.
15
16use base64::{engine::general_purpose, Engine as _};
17use reqwest::Client;
18use serde_json::Value;
19use std::time::Duration;
20
21pub mod rhai;
22
23/// Get information about the Mycelium node
24///
25/// # Arguments
26///
27/// * `api_url` - The URL of the Mycelium API
28///
29/// # Returns
30///
31/// * `Result<Value, String>` - The node information as a JSON value, or an error message
32pub async fn get_node_info(api_url: &str) -> Result<Value, String> {
33    let client = Client::new();
34    let url = format!("{}/api/v1/admin", api_url);
35
36    let response = client
37        .get(&url)
38        .send()
39        .await
40        .map_err(|e| format!("Failed to send request: {}", e))?;
41
42    let status = response.status();
43    if !status.is_success() {
44        return Err(format!("Request failed with status: {}", status));
45    }
46
47    let result: Value = response
48        .json()
49        .await
50        .map_err(|e| format!("Failed to parse response: {}", e))?;
51
52    Ok(result)
53}
54
55/// List all peers connected to the Mycelium node
56///
57/// # Arguments
58///
59/// * `api_url` - The URL of the Mycelium API
60///
61/// # Returns
62///
63/// * `Result<Value, String>` - The list of peers as a JSON value, or an error message
64pub async fn list_peers(api_url: &str) -> Result<Value, String> {
65    let client = Client::new();
66    let url = format!("{}/api/v1/admin/peers", api_url);
67
68    let response = client
69        .get(&url)
70        .send()
71        .await
72        .map_err(|e| format!("Failed to send request: {}", e))?;
73
74    let status = response.status();
75    if !status.is_success() {
76        return Err(format!("Request failed with status: {}", status));
77    }
78
79    let result: Value = response
80        .json()
81        .await
82        .map_err(|e| format!("Failed to parse response: {}", e))?;
83
84    Ok(result)
85}
86
87/// Add a new peer to the Mycelium node
88///
89/// # Arguments
90///
91/// * `api_url` - The URL of the Mycelium API
92/// * `peer_address` - The address of the peer to add
93///
94/// # Returns
95///
96/// * `Result<Value, String>` - The result of the operation as a JSON value, or an error message
97pub async fn add_peer(api_url: &str, peer_address: &str) -> Result<Value, String> {
98    let client = Client::new();
99    let url = format!("{}/api/v1/admin/peers", api_url);
100
101    let response = client
102        .post(&url)
103        .json(&serde_json::json!({
104            "endpoint": peer_address
105        }))
106        .send()
107        .await
108        .map_err(|e| format!("Failed to send request: {}", e))?;
109
110    let status = response.status();
111    if status == reqwest::StatusCode::NO_CONTENT {
112        // Successfully added, but no content to parse
113        return Ok(serde_json::json!({"success": true}));
114    }
115    if !status.is_success() {
116        return Err(format!("Request failed with status: {}", status));
117    }
118
119    // For other success statuses that might have a body
120    let result: Value = response
121        .json()
122        .await
123        .map_err(|e| format!("Failed to parse response: {}", e))?;
124
125    Ok(result)
126}
127
128/// Remove a peer from the Mycelium node
129///
130/// # Arguments
131///
132/// * `api_url` - The URL of the Mycelium API
133/// * `peer_id` - The ID of the peer to remove
134///
135/// # Returns
136///
137/// * `Result<Value, String>` - The result of the operation as a JSON value, or an error message
138pub async fn remove_peer(api_url: &str, peer_id: &str) -> Result<Value, String> {
139    let client = Client::new();
140    let peer_id_url_encoded = urlencoding::encode(peer_id);
141    let url = format!("{}/api/v1/admin/peers/{}", api_url, peer_id_url_encoded);
142
143    let response = client
144        .delete(&url)
145        .send()
146        .await
147        .map_err(|e| format!("Failed to send request: {}", e))?;
148
149    let status = response.status();
150    if status == reqwest::StatusCode::NO_CONTENT {
151        // Successfully removed, but no content to parse
152        return Ok(serde_json::json!({"success": true}));
153    }
154    if !status.is_success() {
155        return Err(format!("Request failed with status: {}", status));
156    }
157
158    let result: Value = response
159        .json()
160        .await
161        .map_err(|e| format!("Failed to parse response: {}", e))?;
162
163    Ok(result)
164}
165
166/// List all selected routes in the Mycelium node
167///
168/// # Arguments
169///
170/// * `api_url` - The URL of the Mycelium API
171///
172/// # Returns
173///
174/// * `Result<Value, String>` - The list of selected routes as a JSON value, or an error message
175pub async fn list_selected_routes(api_url: &str) -> Result<Value, String> {
176    let client = Client::new();
177    let url = format!("{}/api/v1/admin/routes/selected", api_url);
178
179    let response = client
180        .get(&url)
181        .send()
182        .await
183        .map_err(|e| format!("Failed to send request: {}", e))?;
184
185    let status = response.status();
186    if !status.is_success() {
187        return Err(format!("Request failed with status: {}", status));
188    }
189
190    let result: Value = response
191        .json()
192        .await
193        .map_err(|e| format!("Failed to parse response: {}", e))?;
194
195    Ok(result)
196}
197
198/// List all fallback routes in the Mycelium node
199///
200/// # Arguments
201///
202/// * `api_url` - The URL of the Mycelium API
203///
204/// # Returns
205///
206/// * `Result<Value, String>` - The list of fallback routes as a JSON value, or an error message
207pub async fn list_fallback_routes(api_url: &str) -> Result<Value, String> {
208    let client = Client::new();
209    let url = format!("{}/api/v1/admin/routes/fallback", api_url);
210
211    let response = client
212        .get(&url)
213        .send()
214        .await
215        .map_err(|e| format!("Failed to send request: {}", e))?;
216
217    let status = response.status();
218    if !status.is_success() {
219        return Err(format!("Request failed with status: {}", status));
220    }
221
222    let result: Value = response
223        .json()
224        .await
225        .map_err(|e| format!("Failed to parse response: {}", e))?;
226
227    Ok(result)
228}
229
230/// Send a message to a destination via the Mycelium node
231///
232/// # Arguments
233///
234/// * `api_url` - The URL of the Mycelium API
235/// * `destination` - The destination address
236/// * `topic` - The message topic
237/// * `message` - The message content
238/// * `reply_deadline` - The deadline in seconds; pass `-1` to indicate we do not want to wait on a reply
239///
240/// # Returns
241///
242/// * `Result<Value, String>` - The result of the operation as a JSON value, or an error message
243pub async fn send_message(
244    api_url: &str,
245    destination: &str,
246    topic: &str,
247    message: &str,
248    reply_deadline: Option<Duration>, // This is passed in URL query
249) -> Result<Value, String> {
250    let client = Client::new();
251    let url = format!("{}/api/v1/messages", api_url);
252
253    let mut request = client.post(&url);
254    if let Some(deadline) = reply_deadline {
255        request = request.query(&[("reply_timeout", deadline.as_secs())]);
256    }
257
258    let response = request
259        .json(&serde_json::json!({
260                "dst": { "ip": destination },
261                "topic": general_purpose::STANDARD.encode(topic),
262                "payload": general_purpose::STANDARD.encode(message)
263        }))
264        .send()
265        .await
266        .map_err(|e| format!("Failed to send request: {}", e))?;
267
268    let status = response.status();
269    if !status.is_success() {
270        return Err(format!("Request failed with status: {}", status));
271    }
272
273    let result: Value = response
274        .json()
275        .await
276        .map_err(|e| format!("Failed to parse response: {}", e))?;
277
278    Ok(result)
279}
280
281/// Receive messages from a topic via the Mycelium node
282///
283/// # Arguments
284///
285/// * `api_url` - The URL of the Mycelium API
286/// * `topic` - The message topic
287/// * `wait_deadline` - Time we wait for receiving a message
288///
289/// # Returns
290///
291/// * `Result<Value, String>` - The received messages as a JSON value, or an error message
292pub async fn receive_messages(
293    api_url: &str,
294    topic: &str,
295    wait_deadline: Option<Duration>,
296) -> Result<Value, String> {
297    let client = Client::new();
298    let url = format!("{}/api/v1/messages", api_url);
299
300    let mut request = client.get(&url);
301
302    if let Some(deadline) = wait_deadline {
303        request = request.query(&[
304            ("topic", general_purpose::STANDARD.encode(topic)),
305            ("timeout", deadline.as_secs().to_string()),
306        ])
307    } else {
308        request = request.query(&[("topic", general_purpose::STANDARD.encode(topic))])
309    };
310
311    let response = request
312        .send()
313        .await
314        .map_err(|e| format!("Failed to send request: {}", e))?;
315
316    let status = response.status();
317    if !status.is_success() {
318        return Err(format!("Request failed with status: {}", status));
319    }
320
321    let result: Value = response
322        .json()
323        .await
324        .map_err(|e| format!("Failed to parse response: {}", e))?;
325
326    Ok(result)
327}