From 3417e2c1ff39190ad638be3e08a3a632cb1aaba6 Mon Sep 17 00:00:00 2001 From: Maxime Van Hees Date: Tue, 13 May 2025 10:20:08 +0200 Subject: [PATCH 01/10] fixed merge conflict --- Cargo.toml | 2 + examples/mycelium/mycelium_basic.rhai | 129 ++++++++++++ src/lib.rs | 1 + src/mycelium/mod.rs | 280 ++++++++++++++++++++++++++ src/rhai/mod.rs | 7 + src/rhai/mycelium.rs | 235 +++++++++++++++++++++ 6 files changed, 654 insertions(+) create mode 100644 examples/mycelium/mycelium_basic.rhai create mode 100644 src/mycelium/mod.rs create mode 100644 src/rhai/mycelium.rs diff --git a/Cargo.toml b/Cargo.toml index 8d362cb..31bba39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,8 @@ tokio-postgres = "0.7.8" # Async PostgreSQL client tokio-test = "0.4.4" uuid = { version = "1.16.0", features = ["v4"] } zinit-client = { git = "https://github.com/threefoldtech/zinit", branch = "json_rpc", package = "zinit-client" } +reqwest = { version = "0.12.15", features = ["json"] } + # Optional features for specific OS functionality [target.'cfg(unix)'.dependencies] nix = "0.30.1" # Unix-specific functionality diff --git a/examples/mycelium/mycelium_basic.rhai b/examples/mycelium/mycelium_basic.rhai new file mode 100644 index 0000000..a6b79fe --- /dev/null +++ b/examples/mycelium/mycelium_basic.rhai @@ -0,0 +1,129 @@ +// Basic example of using the Mycelium client in Rhai + +// API URL for Mycelium +let api_url = "http://localhost:8989"; + +// Get node information +print("Getting node information:"); +try { + let node_info = mycelium_get_node_info(api_url); + print(`Node subnet: ${node_info.subnet}`); + print(`Node public key: ${node_info.publicKey}`); +} catch(err) { + print(`Error getting node info: ${err}`); +} + +// List all peers +print("\nListing all peers:"); +try { + let peers = mycelium_list_peers(api_url); + + if peers.is_empty() { + print("No peers connected."); + } else { + for peer in peers { + print(`Peer ID: ${peer.id}`); + print(` Address: ${peer.address}`); + print(` Connected: ${peer.connected}`); + print(` Bytes sent: ${peer.txBytes}`); + print(` Bytes received: ${peer.rxBytes}`); + } + } +} catch(err) { + print(`Error listing peers: ${err}`); +} + +// Add a new peer +print("\nAdding a new peer:"); +let new_peer_address = "tcp://185.69.166.8:9651"; +try { + let result = mycelium_add_peer(api_url, new_peer_address); + print(`Peer added: ${result.success}`); +} catch(err) { + print(`Error adding peer: ${err}`); +} + +// List selected routes +print("\nListing selected routes:"); +try { + let routes = mycelium_list_selected_routes(api_url); + + if routes.is_empty() { + print("No selected routes."); + } else { + for route in routes { + print(`Subnet: ${route.subnet}`); + print(` Next hop: ${route.nextHop}`); + print(` Metric: ${route.metric}`); + } + } +} catch(err) { + print(`Error listing routes: ${err}`); +} + +// List fallback routes +print("\nListing fallback routes:"); +try { + let routes = mycelium_list_fallback_routes(api_url); + + if routes.is_empty() { + print("No fallback routes."); + } else { + for route in routes { + print(`Subnet: ${route.subnet}`); + print(` Next hop: ${route.nextHop}`); + print(` Metric: ${route.metric}`); + } + } +} catch(err) { + print(`Error listing fallback routes: ${err}`); +} + +// Send a message +print("\nSending a message:"); +let destination = "400:1234:5678:9abc:def0:1234:5678:9abc"; +let topic = "test"; +let message = "Hello from Rhai!"; +let deadline_secs = 60; + +try { + let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs); + print(`Message sent: ${result.success}`); + if result.id { + print(`Message ID: ${result.id}`); + } +} catch(err) { + print(`Error sending message: ${err}`); +} + +// Receive messages +print("\nReceiving messages:"); +let receive_topic = "test"; +let count = 5; + +try { + let messages = mycelium_receive_messages(api_url, receive_topic, count); + + if messages.is_empty() { + print("No messages received."); + } else { + for msg in messages { + print(`Message from: ${msg.source}`); + print(` Topic: ${msg.topic}`); + print(` Content: ${msg.content}`); + print(` Timestamp: ${msg.timestamp}`); + } + } +} catch(err) { + print(`Error receiving messages: ${err}`); +} + +// Remove a peer +print("\nRemoving a peer:"); +let peer_id = "some-peer-id"; // Replace with an actual peer ID +try { + let result = mycelium_remove_peer(api_url, peer_id); + print(`Peer removed: ${result.success}`); +} catch(err) { + print(`Error removing peer: ${err}`); +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index bc8cbdf..91162b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,7 @@ pub mod text; pub mod virt; pub mod vault; pub mod zinit_client; +pub mod mycelium; // Version information /// Returns the version of the SAL library diff --git a/src/mycelium/mod.rs b/src/mycelium/mod.rs new file mode 100644 index 0000000..adbbc98 --- /dev/null +++ b/src/mycelium/mod.rs @@ -0,0 +1,280 @@ +use std::time::Duration; +use serde_json::Value; +use reqwest::Client; + +/// Get information about the Mycelium node +/// +/// # Arguments +/// +/// * `api_url` - The URL of the Mycelium API +/// +/// # Returns +/// +/// * `Result` - The node information as a JSON value, or an error message +pub async fn get_node_info(api_url: &str) -> Result { + let client = Client::new(); + let url = format!("{}/api/v1/admin", api_url); + + let response = client + .get(&url) + .send() + .await + .map_err(|e| format!("Failed to send request: {}", e))?; + + let status = response.status(); + if !status.is_success() { + return Err(format!("Request failed with status: {}", status)); + } + + let result: Value = response + .json() + .await + .map_err(|e| format!("Failed to parse response: {}", e))?; + + Ok(result) +} + +/// List all peers connected to the Mycelium node +/// +/// # Arguments +/// +/// * `api_url` - The URL of the Mycelium API +/// +/// # Returns +/// +/// * `Result` - The list of peers as a JSON value, or an error message +pub async fn list_peers(api_url: &str) -> Result { + let client = Client::new(); + let url = format!("{}/api/v1/peers", api_url); + + let response = client + .get(&url) + .send() + .await + .map_err(|e| format!("Failed to send request: {}", e))?; + + let status = response.status(); + if !status.is_success() { + return Err(format!("Request failed with status: {}", status)); + } + + let result: Value = response + .json() + .await + .map_err(|e| format!("Failed to parse response: {}", e))?; + + Ok(result) +} + +/// Add a new peer to the Mycelium node +/// +/// # Arguments +/// +/// * `api_url` - The URL of the Mycelium API +/// * `peer_address` - The address of the peer to add +/// +/// # Returns +/// +/// * `Result` - The result of the operation as a JSON value, or an error message +pub async fn add_peer(api_url: &str, peer_address: &str) -> Result { + let client = Client::new(); + let url = format!("{}/api/v1/peers", api_url); + + let response = client + .post(&url) + .json(&serde_json::json!({ + "address": peer_address + })) + .send() + .await + .map_err(|e| format!("Failed to send request: {}", e))?; + + let status = response.status(); + if !status.is_success() { + return Err(format!("Request failed with status: {}", status)); + } + + let result: Value = response + .json() + .await + .map_err(|e| format!("Failed to parse response: {}", e))?; + + Ok(result) +} + +/// Remove a peer from the Mycelium node +/// +/// # Arguments +/// +/// * `api_url` - The URL of the Mycelium API +/// * `peer_id` - The ID of the peer to remove +/// +/// # Returns +/// +/// * `Result` - The result of the operation as a JSON value, or an error message +pub async fn remove_peer(api_url: &str, peer_id: &str) -> Result { + let client = Client::new(); + let url = format!("{}/api/v1/peers/{}", api_url, peer_id); + + let response = client + .delete(&url) + .send() + .await + .map_err(|e| format!("Failed to send request: {}", e))?; + + let status = response.status(); + if !status.is_success() { + return Err(format!("Request failed with status: {}", status)); + } + + let result: Value = response + .json() + .await + .map_err(|e| format!("Failed to parse response: {}", e))?; + + Ok(result) +} + +/// List all selected routes in the Mycelium node +/// +/// # Arguments +/// +/// * `api_url` - The URL of the Mycelium API +/// +/// # Returns +/// +/// * `Result` - The list of selected routes as a JSON value, or an error message +pub async fn list_selected_routes(api_url: &str) -> Result { + let client = Client::new(); + let url = format!("{}/api/v1/routes/selected", api_url); + + let response = client + .get(&url) + .send() + .await + .map_err(|e| format!("Failed to send request: {}", e))?; + + let status = response.status(); + if !status.is_success() { + return Err(format!("Request failed with status: {}", status)); + } + + let result: Value = response + .json() + .await + .map_err(|e| format!("Failed to parse response: {}", e))?; + + Ok(result) +} + +/// List all fallback routes in the Mycelium node +/// +/// # Arguments +/// +/// * `api_url` - The URL of the Mycelium API +/// +/// # Returns +/// +/// * `Result` - The list of fallback routes as a JSON value, or an error message +pub async fn list_fallback_routes(api_url: &str) -> Result { + let client = Client::new(); + let url = format!("{}/api/v1/routes/fallback", api_url); + + let response = client + .get(&url) + .send() + .await + .map_err(|e| format!("Failed to send request: {}", e))?; + + let status = response.status(); + if !status.is_success() { + return Err(format!("Request failed with status: {}", status)); + } + + let result: Value = response + .json() + .await + .map_err(|e| format!("Failed to parse response: {}", e))?; + + Ok(result) +} + +/// Send a message to a destination via the Mycelium node +/// +/// # Arguments +/// +/// * `api_url` - The URL of the Mycelium API +/// * `destination` - The destination address +/// * `topic` - The message topic +/// * `message` - The message content +/// * `deadline_secs` - The deadline in seconds +/// +/// # Returns +/// +/// * `Result` - The result of the operation as a JSON value, or an error message +pub async fn send_message(api_url: &str, destination: &str, topic: &str, message: &str, deadline_secs: u64) -> Result { + let client = Client::new(); + let url = format!("{}/api/v1/messages", api_url); + + // Convert deadline to seconds + let deadline = Duration::from_secs(deadline_secs).as_secs(); + + let response = client + .post(&url) + .json(&serde_json::json!({ + "destination": destination, + "topic": topic, + "content": message, + "deadline": deadline + })) + .send() + .await + .map_err(|e| format!("Failed to send request: {}", e))?; + + let status = response.status(); + if !status.is_success() { + return Err(format!("Request failed with status: {}", status)); + } + + let result: Value = response + .json() + .await + .map_err(|e| format!("Failed to parse response: {}", e))?; + + Ok(result) +} + +/// Receive messages from a topic via the Mycelium node +/// +/// # Arguments +/// +/// * `api_url` - The URL of the Mycelium API +/// * `topic` - The message topic +/// * `count` - The maximum number of messages to receive +/// +/// # Returns +/// +/// * `Result` - The received messages as a JSON value, or an error message +pub async fn receive_messages(api_url: &str, topic: &str, count: u32) -> Result { + let client = Client::new(); + let url = format!("{}/api/v1/messages/{}", api_url, topic); + + let response = client + .get(&url) + .query(&[("count", count)]) + .send() + .await + .map_err(|e| format!("Failed to send request: {}", e))?; + + let status = response.status(); + if !status.is_success() { + return Err(format!("Request failed with status: {}", status)); + } + + let result: Value = response + .json() + .await + .map_err(|e| format!("Failed to parse response: {}", e))?; + + Ok(result) +} \ No newline at end of file diff --git a/src/rhai/mod.rs b/src/rhai/mod.rs index f3e380f..801864c 100644 --- a/src/rhai/mod.rs +++ b/src/rhai/mod.rs @@ -15,6 +15,7 @@ mod rfs; mod vault; mod text; mod zinit; +mod mycelium; #[cfg(test)] mod tests; @@ -95,6 +96,9 @@ pub use git::register_git_module; // Re-export zinit module pub use zinit::register_zinit_module; +// Re-export mycelium module +pub use mycelium::register_mycelium_module; + // Re-export text module pub use text::register_text_module; // Re-export text functions directly from text module @@ -155,6 +159,9 @@ pub fn register(engine: &mut Engine) -> Result<(), Box> { // Register Zinit module functions zinit::register_zinit_module(engine)?; + // Register Mycelium module functions + mycelium::register_mycelium_module(engine)?; + // Register Text module functions text::register_text_module(engine)?; diff --git a/src/rhai/mycelium.rs b/src/rhai/mycelium.rs new file mode 100644 index 0000000..4a9b342 --- /dev/null +++ b/src/rhai/mycelium.rs @@ -0,0 +1,235 @@ +//! Rhai wrappers for Mycelium client module functions +//! +//! This module provides Rhai wrappers for the functions in the Mycelium client module. + +use rhai::{Engine, EvalAltResult, Array, Dynamic, Map}; +use crate::mycelium as client; +use tokio::runtime::Runtime; +use serde_json::Value; +use crate::rhai::error::ToRhaiError; + +/// Register Mycelium module functions with the Rhai engine +/// +/// # Arguments +/// +/// * `engine` - The Rhai engine to register the functions with +/// +/// # Returns +/// +/// * `Result<(), Box>` - Ok if registration was successful, Err otherwise +pub fn register_mycelium_module(engine: &mut Engine) -> Result<(), Box> { + // Register Mycelium client functions + engine.register_fn("mycelium_get_node_info", mycelium_get_node_info); + engine.register_fn("mycelium_list_peers", mycelium_list_peers); + engine.register_fn("mycelium_add_peer", mycelium_add_peer); + engine.register_fn("mycelium_remove_peer", mycelium_remove_peer); + engine.register_fn("mycelium_list_selected_routes", mycelium_list_selected_routes); + engine.register_fn("mycelium_list_fallback_routes", mycelium_list_fallback_routes); + engine.register_fn("mycelium_send_message", mycelium_send_message); + engine.register_fn("mycelium_receive_messages", mycelium_receive_messages); + + Ok(()) +} + +// Helper function to get a runtime +fn get_runtime() -> Result> { + tokio::runtime::Runtime::new().map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to create Tokio runtime: {}", e).into(), + rhai::Position::NONE + )) + }) +} + +// Helper function to convert serde_json::Value to rhai::Dynamic +fn value_to_dynamic(value: Value) -> Dynamic { + match value { + Value::Null => Dynamic::UNIT, + Value::Bool(b) => Dynamic::from(b), + Value::Number(n) => { + if let Some(i) = n.as_i64() { + Dynamic::from(i) + } else if let Some(f) = n.as_f64() { + Dynamic::from(f) + } else { + Dynamic::from(n.to_string()) + } + }, + Value::String(s) => Dynamic::from(s), + Value::Array(arr) => { + let mut rhai_arr = Array::new(); + for item in arr { + rhai_arr.push(value_to_dynamic(item)); + } + Dynamic::from(rhai_arr) + }, + Value::Object(map) => { + let mut rhai_map = Map::new(); + for (k, v) in map { + rhai_map.insert(k.into(), value_to_dynamic(v)); + } + Dynamic::from_map(rhai_map) + } + } +} + +// Helper trait to convert String errors to Rhai errors +impl ToRhaiError for Result { + fn to_rhai_error(self) -> Result> { + self.map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Mycelium error: {}", e).into(), + rhai::Position::NONE + )) + }) + } +} + +// +// Mycelium Client Function Wrappers +// + +/// Wrapper for mycelium::get_node_info +/// +/// Gets information about the Mycelium node. +pub fn mycelium_get_node_info(api_url: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::get_node_info(api_url).await + }); + + let node_info = result.to_rhai_error()?; + + Ok(value_to_dynamic(node_info)) +} + +/// Wrapper for mycelium::list_peers +/// +/// Lists all peers connected to the Mycelium node. +pub fn mycelium_list_peers(api_url: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::list_peers(api_url).await + }); + + let peers = result.to_rhai_error()?; + + Ok(value_to_dynamic(peers)) +} + +/// Wrapper for mycelium::add_peer +/// +/// Adds a new peer to the Mycelium node. +pub fn mycelium_add_peer(api_url: &str, peer_address: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::add_peer(api_url, peer_address).await + }); + + let response = result.to_rhai_error()?; + + Ok(value_to_dynamic(response)) +} + +/// Wrapper for mycelium::remove_peer +/// +/// Removes a peer from the Mycelium node. +pub fn mycelium_remove_peer(api_url: &str, peer_id: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::remove_peer(api_url, peer_id).await + }); + + let response = result.to_rhai_error()?; + + Ok(value_to_dynamic(response)) +} + +/// Wrapper for mycelium::list_selected_routes +/// +/// Lists all selected routes in the Mycelium node. +pub fn mycelium_list_selected_routes(api_url: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::list_selected_routes(api_url).await + }); + + let routes = result.to_rhai_error()?; + + Ok(value_to_dynamic(routes)) +} + +/// Wrapper for mycelium::list_fallback_routes +/// +/// Lists all fallback routes in the Mycelium node. +pub fn mycelium_list_fallback_routes(api_url: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::list_fallback_routes(api_url).await + }); + + let routes = result.to_rhai_error()?; + + Ok(value_to_dynamic(routes)) +} + +/// Wrapper for mycelium::send_message +/// +/// Sends a message to a destination via the Mycelium node. +pub fn mycelium_send_message(api_url: &str, destination: &str, topic: &str, message: &str, deadline_secs: i64) -> Result> { + let rt = get_runtime()?; + + // Convert deadline to u64 + let deadline = if deadline_secs < 0 { + return Err(Box::new(EvalAltResult::ErrorRuntime( + "Deadline cannot be negative".into(), + rhai::Position::NONE + ))); + } else { + deadline_secs as u64 + }; + + let result = rt.block_on(async { + client::send_message(api_url, destination, topic, message, deadline).await + }); + + let response = result.to_rhai_error()?; + + Ok(value_to_dynamic(response)) +} + +/// Wrapper for mycelium::receive_messages +/// +/// Receives messages from a topic via the Mycelium node. +pub fn mycelium_receive_messages(api_url: &str, topic: &str, count: i64) -> Result> { + let rt = get_runtime()?; + + // Convert count to u32 + let count = if count < 0 { + return Err(Box::new(EvalAltResult::ErrorRuntime( + "Count cannot be negative".into(), + rhai::Position::NONE + ))); + } else if count > u32::MAX as i64 { + return Err(Box::new(EvalAltResult::ErrorRuntime( + format!("Count too large, maximum is {}", u32::MAX).into(), + rhai::Position::NONE + ))); + } else { + count as u32 + }; + + let result = rt.block_on(async { + client::receive_messages(api_url, topic, count).await + }); + + let messages = result.to_rhai_error()?; + + Ok(value_to_dynamic(messages)) +} \ No newline at end of file -- 2.40.1 From 3225b3f029597edf63f9f21eda6b57d7491a0092 Mon Sep 17 00:00:00 2001 From: Maxime Van Hees Date: Tue, 13 May 2025 10:40:03 +0200 Subject: [PATCH 02/10] corrected response mapping from API requests --- examples/mycelium/mycelium_basic.rhai | 4 ++-- src/mycelium/mod.rs | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/examples/mycelium/mycelium_basic.rhai b/examples/mycelium/mycelium_basic.rhai index a6b79fe..6460b24 100644 --- a/examples/mycelium/mycelium_basic.rhai +++ b/examples/mycelium/mycelium_basic.rhai @@ -7,8 +7,8 @@ let api_url = "http://localhost:8989"; print("Getting node information:"); try { let node_info = mycelium_get_node_info(api_url); - print(`Node subnet: ${node_info.subnet}`); - print(`Node public key: ${node_info.publicKey}`); + print(`Node subnet: ${node_info.nodeSubnet}`); + print(`Node public key: ${node_info.nodePubkey}`); } catch(err) { print(`Error getting node info: ${err}`); } diff --git a/src/mycelium/mod.rs b/src/mycelium/mod.rs index adbbc98..10279ad 100644 --- a/src/mycelium/mod.rs +++ b/src/mycelium/mod.rs @@ -1,6 +1,7 @@ use std::time::Duration; use serde_json::Value; use reqwest::Client; +use base64::encode; /// Get information about the Mycelium node /// @@ -45,7 +46,7 @@ pub async fn get_node_info(api_url: &str) -> Result { /// * `Result` - The list of peers as a JSON value, or an error message pub async fn list_peers(api_url: &str) -> Result { let client = Client::new(); - let url = format!("{}/api/v1/peers", api_url); + let url = format!("{}/api/v1/admin/peers", api_url); let response = client .get(&url) @@ -78,7 +79,7 @@ pub async fn list_peers(api_url: &str) -> Result { /// * `Result` - The result of the operation as a JSON value, or an error message pub async fn add_peer(api_url: &str, peer_address: &str) -> Result { let client = Client::new(); - let url = format!("{}/api/v1/peers", api_url); + let url = format!("{}/api/v1/admin/peers", api_url); let response = client .post(&url) @@ -114,7 +115,7 @@ pub async fn add_peer(api_url: &str, peer_address: &str) -> Result` - The result of the operation as a JSON value, or an error message pub async fn remove_peer(api_url: &str, peer_id: &str) -> Result { let client = Client::new(); - let url = format!("{}/api/v1/peers/{}", api_url, peer_id); + let url = format!("{}/api/v1/admin/peers/{}", api_url, peer_id); let response = client .delete(&url) @@ -146,7 +147,7 @@ pub async fn remove_peer(api_url: &str, peer_id: &str) -> Result /// * `Result` - The list of selected routes as a JSON value, or an error message pub async fn list_selected_routes(api_url: &str) -> Result { let client = Client::new(); - let url = format!("{}/api/v1/routes/selected", api_url); + let url = format!("{}/api/v1/admin/routes/selected", api_url); let response = client .get(&url) @@ -178,7 +179,7 @@ pub async fn list_selected_routes(api_url: &str) -> Result { /// * `Result` - The list of fallback routes as a JSON value, or an error message pub async fn list_fallback_routes(api_url: &str) -> Result { let client = Client::new(); - let url = format!("{}/api/v1/routes/fallback", api_url); + let url = format!("{}/api/v1/admin/routes/fallback", api_url); let response = client .get(&url) @@ -257,11 +258,11 @@ pub async fn send_message(api_url: &str, destination: &str, topic: &str, message /// * `Result` - The received messages as a JSON value, or an error message pub async fn receive_messages(api_url: &str, topic: &str, count: u32) -> Result { let client = Client::new(); - let url = format!("{}/api/v1/messages/{}", api_url, topic); + let url = format!("{}/api/v1/messages", api_url); let response = client .get(&url) - .query(&[("count", count)]) + .query(&[("topic", encode(topic)), ("count", count.to_string())]) .send() .await .map_err(|e| format!("Failed to send request: {}", e))?; -- 2.40.1 From 3e64a53a83d7a04380af26458a8d7f5100f2e3ba Mon Sep 17 00:00:00 2001 From: Maxime Van Hees Date: Tue, 13 May 2025 11:52:31 +0200 Subject: [PATCH 03/10] working example for mycelium --- Cargo.toml | 1 + examples/mycelium/mycelium_basic.rhai | 78 +++++++++--------- src/mycelium/mod.rs | 109 +++++++++++++++----------- 3 files changed, 106 insertions(+), 82 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 31bba39..99268e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ tokio-test = "0.4.4" uuid = { version = "1.16.0", features = ["v4"] } zinit-client = { git = "https://github.com/threefoldtech/zinit", branch = "json_rpc", package = "zinit-client" } reqwest = { version = "0.12.15", features = ["json"] } +urlencoding = "2.1.3" # Optional features for specific OS functionality [target.'cfg(unix)'.dependencies] diff --git a/examples/mycelium/mycelium_basic.rhai b/examples/mycelium/mycelium_basic.rhai index 6460b24..1297fa4 100644 --- a/examples/mycelium/mycelium_basic.rhai +++ b/examples/mycelium/mycelium_basic.rhai @@ -22,9 +22,9 @@ try { print("No peers connected."); } else { for peer in peers { - print(`Peer ID: ${peer.id}`); - print(` Address: ${peer.address}`); - print(` Connected: ${peer.connected}`); + print(`Peer Endpoint: ${peer.endpoint.proto}://${peer.endpoint.socketAddr}`); + print(` Type: ${peer.type}`); + print(` Connection State: ${peer.connectionState}`); print(` Bytes sent: ${peer.txBytes}`); print(` Bytes received: ${peer.rxBytes}`); } @@ -35,7 +35,7 @@ try { // Add a new peer print("\nAdding a new peer:"); -let new_peer_address = "tcp://185.69.166.8:9651"; +let new_peer_address = "tcp://65.21.231.58:9651"; try { let result = mycelium_add_peer(api_url, new_peer_address); print(`Peer added: ${result.success}`); @@ -80,47 +80,51 @@ try { } // Send a message -print("\nSending a message:"); -let destination = "400:1234:5678:9abc:def0:1234:5678:9abc"; -let topic = "test"; -let message = "Hello from Rhai!"; -let deadline_secs = 60; +// TO SEND A MESSAGE FILL IN THE DESTINATION IP ADDRESS +// -----------------------------------------------------// +// print("\nSending a message:"); +// let destination = < FILL IN CORRECT DEST IP > +// let topic = "test"; +// let message = "Hello from Rhai!"; +// let deadline_secs = 60; -try { - let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs); - print(`Message sent: ${result.success}`); - if result.id { - print(`Message ID: ${result.id}`); - } -} catch(err) { - print(`Error sending message: ${err}`); -} +// try { +// let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs); +// print(`Message sent: ${result.success}`); +// if result.id { +// print(`Message ID: ${result.id}`); +// } +// } catch(err) { +// print(`Error sending message: ${err}`); +// } // Receive messages -print("\nReceiving messages:"); -let receive_topic = "test"; -let count = 5; +// RECEIVING MESSAGES SHOULD BE DONE ON THE DESTINATION NODE FROM THE CALL ABOVE +// -----------------------------------------------------------------------------// +// print("\nReceiving messages:"); +// let receive_topic = "test"; +// let count = 5; -try { - let messages = mycelium_receive_messages(api_url, receive_topic, count); +// try { +// let messages = mycelium_receive_messages(api_url, receive_topic, count); - if messages.is_empty() { - print("No messages received."); - } else { - for msg in messages { - print(`Message from: ${msg.source}`); - print(` Topic: ${msg.topic}`); - print(` Content: ${msg.content}`); - print(` Timestamp: ${msg.timestamp}`); - } - } -} catch(err) { - print(`Error receiving messages: ${err}`); -} +// if messages.is_empty() { +// print("No messages received."); +// } else { +// for msg in messages { +// print(`Message from: ${msg.source}`); +// print(` Topic: ${msg.topic}`); +// print(` Content: ${msg.content}`); +// print(` Timestamp: ${msg.timestamp}`); +// } +// } +// } catch(err) { +// print(`Error receiving messages: ${err}`); +// } // Remove a peer print("\nRemoving a peer:"); -let peer_id = "some-peer-id"; // Replace with an actual peer ID +let peer_id = "tcp://65.21.231.58:9651"; // This is the peer we added earlier try { let result = mycelium_remove_peer(api_url, peer_id); print(`Peer removed: ${result.success}`); diff --git a/src/mycelium/mod.rs b/src/mycelium/mod.rs index 10279ad..00847c2 100644 --- a/src/mycelium/mod.rs +++ b/src/mycelium/mod.rs @@ -1,7 +1,11 @@ -use std::time::Duration; -use serde_json::Value; +use base64::{ + alphabet, + engine::{self, general_purpose}, + Engine as _, +}; use reqwest::Client; -use base64::encode; +use serde_json::Value; +use std::time::Duration; /// Get information about the Mycelium node /// @@ -15,23 +19,23 @@ use base64::encode; pub async fn get_node_info(api_url: &str) -> Result { let client = Client::new(); let url = format!("{}/api/v1/admin", api_url); - + let response = client .get(&url) .send() .await .map_err(|e| format!("Failed to send request: {}", e))?; - + let status = response.status(); if !status.is_success() { return Err(format!("Request failed with status: {}", status)); } - + let result: Value = response .json() .await .map_err(|e| format!("Failed to parse response: {}", e))?; - + Ok(result) } @@ -47,23 +51,23 @@ pub async fn get_node_info(api_url: &str) -> Result { pub async fn list_peers(api_url: &str) -> Result { let client = Client::new(); let url = format!("{}/api/v1/admin/peers", api_url); - + let response = client .get(&url) .send() .await .map_err(|e| format!("Failed to send request: {}", e))?; - + let status = response.status(); if !status.is_success() { return Err(format!("Request failed with status: {}", status)); } - + let result: Value = response .json() .await .map_err(|e| format!("Failed to parse response: {}", e))?; - + Ok(result) } @@ -80,26 +84,31 @@ pub async fn list_peers(api_url: &str) -> Result { pub async fn add_peer(api_url: &str, peer_address: &str) -> Result { let client = Client::new(); let url = format!("{}/api/v1/admin/peers", api_url); - + let response = client .post(&url) .json(&serde_json::json!({ - "address": peer_address + "endpoint": peer_address })) .send() .await .map_err(|e| format!("Failed to send request: {}", e))?; - + let status = response.status(); + if status == reqwest::StatusCode::NO_CONTENT { + // Successfully added, but no content to parse + return Ok(serde_json::json!({"success": true})); + } if !status.is_success() { return Err(format!("Request failed with status: {}", status)); } - + + // For other success statuses that might have a body let result: Value = response .json() .await .map_err(|e| format!("Failed to parse response: {}", e))?; - + Ok(result) } @@ -115,24 +124,29 @@ pub async fn add_peer(api_url: &str, peer_address: &str) -> Result` - The result of the operation as a JSON value, or an error message pub async fn remove_peer(api_url: &str, peer_id: &str) -> Result { let client = Client::new(); - let url = format!("{}/api/v1/admin/peers/{}", api_url, peer_id); - + let peer_id_url_encoded = urlencoding::encode(peer_id); + let url = format!("{}/api/v1/admin/peers/{}", api_url, peer_id_url_encoded); + let response = client .delete(&url) .send() .await .map_err(|e| format!("Failed to send request: {}", e))?; - + let status = response.status(); + if status == reqwest::StatusCode::NO_CONTENT { + // Successfully removed, but no content to parse + return Ok(serde_json::json!({"success": true})); + } if !status.is_success() { return Err(format!("Request failed with status: {}", status)); } - + let result: Value = response .json() .await .map_err(|e| format!("Failed to parse response: {}", e))?; - + Ok(result) } @@ -148,23 +162,23 @@ pub async fn remove_peer(api_url: &str, peer_id: &str) -> Result pub async fn list_selected_routes(api_url: &str) -> Result { let client = Client::new(); let url = format!("{}/api/v1/admin/routes/selected", api_url); - + let response = client .get(&url) .send() .await .map_err(|e| format!("Failed to send request: {}", e))?; - + let status = response.status(); if !status.is_success() { return Err(format!("Request failed with status: {}", status)); } - + let result: Value = response .json() .await .map_err(|e| format!("Failed to parse response: {}", e))?; - + Ok(result) } @@ -180,23 +194,23 @@ pub async fn list_selected_routes(api_url: &str) -> Result { pub async fn list_fallback_routes(api_url: &str) -> Result { let client = Client::new(); let url = format!("{}/api/v1/admin/routes/fallback", api_url); - + let response = client .get(&url) .send() .await .map_err(|e| format!("Failed to send request: {}", e))?; - + let status = response.status(); if !status.is_success() { return Err(format!("Request failed with status: {}", status)); } - + let result: Value = response .json() .await .map_err(|e| format!("Failed to parse response: {}", e))?; - + Ok(result) } @@ -213,35 +227,40 @@ pub async fn list_fallback_routes(api_url: &str) -> Result { /// # Returns /// /// * `Result` - The result of the operation as a JSON value, or an error message -pub async fn send_message(api_url: &str, destination: &str, topic: &str, message: &str, deadline_secs: u64) -> Result { +pub async fn send_message( + api_url: &str, + destination: &str, + topic: &str, + message: &str, + deadline_secs: u64, +) -> Result { let client = Client::new(); let url = format!("{}/api/v1/messages", api_url); - + // Convert deadline to seconds let deadline = Duration::from_secs(deadline_secs).as_secs(); - + let response = client .post(&url) .json(&serde_json::json!({ - "destination": destination, - "topic": topic, - "content": message, - "deadline": deadline + "dst": { "ip": destination }, + "topic": general_purpose::STANDARD.encode(topic), + "payload": general_purpose::STANDARD.encode(message) })) .send() .await .map_err(|e| format!("Failed to send request: {}", e))?; - + let status = response.status(); if !status.is_success() { return Err(format!("Request failed with status: {}", status)); } - + let result: Value = response .json() .await .map_err(|e| format!("Failed to parse response: {}", e))?; - + Ok(result) } @@ -259,23 +278,23 @@ pub async fn send_message(api_url: &str, destination: &str, topic: &str, message pub async fn receive_messages(api_url: &str, topic: &str, count: u32) -> Result { let client = Client::new(); let url = format!("{}/api/v1/messages", api_url); - + let response = client .get(&url) - .query(&[("topic", encode(topic)), ("count", count.to_string())]) + .query(&[("topic", general_purpose::STANDARD.encode(topic))]) .send() .await .map_err(|e| format!("Failed to send request: {}", e))?; - + let status = response.status(); if !status.is_success() { return Err(format!("Request failed with status: {}", status)); } - + let result: Value = response .json() .await .map_err(|e| format!("Failed to parse response: {}", e))?; - + Ok(result) -} \ No newline at end of file +} -- 2.40.1 From 9a23c4cc09983bfb1515dcfaf5d7beeae0133a46 Mon Sep 17 00:00:00 2001 From: Maxime Van Hees Date: Thu, 15 May 2025 17:30:20 +0200 Subject: [PATCH 04/10] sending and receiving message via Rhai script + added examples --- .../mycelium/mycelium_receive_message.rhai | 31 ++++++++++++++ examples/mycelium/mycelium_send_message.rhai | 25 +++++++++++ src/mycelium/mod.rs | 42 ++++++++++++------- src/rhai/mycelium.rs | 37 ++++++---------- 4 files changed, 97 insertions(+), 38 deletions(-) create mode 100644 examples/mycelium/mycelium_receive_message.rhai create mode 100644 examples/mycelium/mycelium_send_message.rhai diff --git a/examples/mycelium/mycelium_receive_message.rhai b/examples/mycelium/mycelium_receive_message.rhai new file mode 100644 index 0000000..80aef19 --- /dev/null +++ b/examples/mycelium/mycelium_receive_message.rhai @@ -0,0 +1,31 @@ +// Script to receive Mycelium messages + +// API URL for Mycelium +let api_url = "http://localhost:2222"; + +// Receive messages +// This script will listen for messages on a specific topic. +// Ensure the sender script is using the same topic. +// -----------------------------------------------------------------------------// +print("\nReceiving messages:"); +let receive_topic = "test_topic"; +let wait_deadline_secs = 100; + +print(`Listening for messages on topic '${receive_topic}'...`); +try { + let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs); + + if messages.is_empty() { + // print("No new messages received in this poll."); + } else { + print("Received a message:"); + print(` Message id: ${messages.id}`); + print(` Message from: ${messages.srcIp}`); + print(` Topic: ${messages.topic}`); + print(` Payload: ${messages.payload}`); + } +} catch(err) { + print(`Error receiving messages: ${err}`); +} + +print("Finished attempting to receive messages."); \ No newline at end of file diff --git a/examples/mycelium/mycelium_send_message.rhai b/examples/mycelium/mycelium_send_message.rhai new file mode 100644 index 0000000..9be348c --- /dev/null +++ b/examples/mycelium/mycelium_send_message.rhai @@ -0,0 +1,25 @@ +// Script to send a Mycelium message + +// API URL for Mycelium +let api_url = "http://localhost:1111"; + +// Send a message +// TO SEND A MESSAGE FILL IN THE DESTINATION IP ADDRESS +// -----------------------------------------------------// +print("\nSending a message:"); +let destination = "5af:ae6b:dcd8:ffdb:b71:7dde:d3:1033"; // IMPORTANT: Replace with the actual destination IP address +let topic = "test_topic"; +let message = "Hello from Rhai sender!"; +let deadline_secs = -10; // Seconds we wait for a reply + +try { + print(`Attempting to send message to ${destination} on topic '${topic}'`); + let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs); + print(`result: ${result}`); + print(`Message sent: ${result.success}`); + if result.id != "" { + print(`Message ID: ${result.id}`); + } +} catch(err) { + print(`Error sending message: ${err}`); +} \ No newline at end of file diff --git a/src/mycelium/mod.rs b/src/mycelium/mod.rs index 00847c2..76b76f7 100644 --- a/src/mycelium/mod.rs +++ b/src/mycelium/mod.rs @@ -222,7 +222,7 @@ pub async fn list_fallback_routes(api_url: &str) -> Result { /// * `destination` - The destination address /// * `topic` - The message topic /// * `message` - The message content -/// * `deadline_secs` - The deadline in seconds +/// * `reply_deadline` - The deadline in seconds; pass `-1` to indicate we do not want to wait on a reply /// /// # Returns /// @@ -232,20 +232,21 @@ pub async fn send_message( destination: &str, topic: &str, message: &str, - deadline_secs: u64, + reply_deadline: Option, // This is passed in URL query ) -> Result { let client = Client::new(); let url = format!("{}/api/v1/messages", api_url); - // Convert deadline to seconds - let deadline = Duration::from_secs(deadline_secs).as_secs(); + let mut request = client.post(&url); + if let Some(deadline) = reply_deadline { + request = request.query(&[("reply_timeout", deadline.as_secs())]); + } - let response = client - .post(&url) + let response = request .json(&serde_json::json!({ - "dst": { "ip": destination }, - "topic": general_purpose::STANDARD.encode(topic), - "payload": general_purpose::STANDARD.encode(message) + "dst": { "ip": destination }, + "topic": general_purpose::STANDARD.encode(topic), + "payload": general_purpose::STANDARD.encode(message) })) .send() .await @@ -270,18 +271,31 @@ pub async fn send_message( /// /// * `api_url` - The URL of the Mycelium API /// * `topic` - The message topic -/// * `count` - The maximum number of messages to receive +/// * `wait_deadline` - Time we wait for receiving a message /// /// # Returns /// /// * `Result` - The received messages as a JSON value, or an error message -pub async fn receive_messages(api_url: &str, topic: &str, count: u32) -> Result { +pub async fn receive_messages( + api_url: &str, + topic: &str, + wait_deadline: Option, +) -> Result { let client = Client::new(); let url = format!("{}/api/v1/messages", api_url); - let response = client - .get(&url) - .query(&[("topic", general_purpose::STANDARD.encode(topic))]) + let mut request = client.get(&url); + + if let Some(deadline) = wait_deadline { + request = request.query(&[ + ("topic", general_purpose::STANDARD.encode(topic)), + ("timeout", deadline.as_secs().to_string()), + ]) + } else { + request = request.query(&[("topic", general_purpose::STANDARD.encode(topic))]) + }; + + let response = request .send() .await .map_err(|e| format!("Failed to send request: {}", e))?; diff --git a/src/rhai/mycelium.rs b/src/rhai/mycelium.rs index 4a9b342..03cc68c 100644 --- a/src/rhai/mycelium.rs +++ b/src/rhai/mycelium.rs @@ -2,6 +2,8 @@ //! //! This module provides Rhai wrappers for the functions in the Mycelium client module. +use std::time::Duration; + use rhai::{Engine, EvalAltResult, Array, Dynamic, Map}; use crate::mycelium as client; use tokio::runtime::Runtime; @@ -182,17 +184,13 @@ pub fn mycelium_list_fallback_routes(api_url: &str) -> Result Result> { +pub fn mycelium_send_message(api_url: &str, destination: &str, topic: &str, message: &str, reply_deadline_secs: i64) -> Result> { let rt = get_runtime()?; - - // Convert deadline to u64 - let deadline = if deadline_secs < 0 { - return Err(Box::new(EvalAltResult::ErrorRuntime( - "Deadline cannot be negative".into(), - rhai::Position::NONE - ))); + + let deadline = if reply_deadline_secs < 0 { + None } else { - deadline_secs as u64 + Some(Duration::from_secs(reply_deadline_secs as u64)) }; let result = rt.block_on(async { @@ -207,26 +205,17 @@ pub fn mycelium_send_message(api_url: &str, destination: &str, topic: &str, mess /// Wrapper for mycelium::receive_messages /// /// Receives messages from a topic via the Mycelium node. -pub fn mycelium_receive_messages(api_url: &str, topic: &str, count: i64) -> Result> { +pub fn mycelium_receive_messages(api_url: &str, topic: &str, wait_deadline_secs: i64) -> Result> { let rt = get_runtime()?; - - // Convert count to u32 - let count = if count < 0 { - return Err(Box::new(EvalAltResult::ErrorRuntime( - "Count cannot be negative".into(), - rhai::Position::NONE - ))); - } else if count > u32::MAX as i64 { - return Err(Box::new(EvalAltResult::ErrorRuntime( - format!("Count too large, maximum is {}", u32::MAX).into(), - rhai::Position::NONE - ))); + + let deadline = if wait_deadline_secs < 0 { + None } else { - count as u32 + Some(Duration::from_secs(wait_deadline_secs as u64)) }; let result = rt.block_on(async { - client::receive_messages(api_url, topic, count).await + client::receive_messages(api_url, topic, deadline).await }); let messages = result.to_rhai_error()?; -- 2.40.1 From 771df07c25ed91d69d2f873907e3492d6f32fe15 Mon Sep 17 00:00:00 2001 From: Maxime Van Hees Date: Fri, 16 May 2025 10:25:51 +0200 Subject: [PATCH 05/10] Add tutorial explain how to use Mycelium in Rhai scripts --- docs/docs/rhai/mycelium_tutorial.md | 332 ++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 docs/docs/rhai/mycelium_tutorial.md diff --git a/docs/docs/rhai/mycelium_tutorial.md b/docs/docs/rhai/mycelium_tutorial.md new file mode 100644 index 0000000..883ab4f --- /dev/null +++ b/docs/docs/rhai/mycelium_tutorial.md @@ -0,0 +1,332 @@ +# Mycelium Tutorial for Rhai + +This tutorial explains how to use the Mycelium networking functionality in Rhai scripts. Mycelium is a peer-to-peer networking system that allows nodes to communicate with each other, and the Rhai bindings provide an easy way to interact with Mycelium from your scripts. + +## Introduction + +The Mycelium module for Rhai provides the following capabilities: + +- Getting node information +- Managing peers (listing, adding, removing) +- Viewing routing information +- Sending and receiving messages between nodes + +This tutorial will walk you through using these features with example scripts. + +## Prerequisites + +Before using the Mycelium functionality in Rhai, you need: + +1. A running Mycelium node accessible via HTTP + > See https://github.com/threefoldtech/mycelium +2. The Rhai runtime with Mycelium module enabled + +## Basic Mycelium Operations + +Let's start by exploring the basic operations available in Mycelium using the `mycelium_basic.rhai` example. + +### Getting Node Information + +To get information about your Mycelium node: + +```rhai +// API URL for Mycelium +let api_url = "http://localhost:8989"; + +// Get node information +print("Getting node information:"); +try { + let node_info = mycelium_get_node_info(api_url); + print(`Node subnet: ${node_info.nodeSubnet}`); + print(`Node public key: ${node_info.nodePubkey}`); +} catch(err) { + print(`Error getting node info: ${err}`); +} +``` + +This code: +1. Sets the API URL for your Mycelium node +2. Calls `mycelium_get_node_info()` to retrieve information about the node +3. Prints the node's subnet and public key + +### Managing Peers + +#### Listing Peers + +To list all peers connected to your Mycelium node: + +```rhai +// List all peers +print("\nListing all peers:"); +try { + let peers = mycelium_list_peers(api_url); + + if peers.is_empty() { + print("No peers connected."); + } else { + for peer in peers { + print(`Peer Endpoint: ${peer.endpoint.proto}://${peer.endpoint.socketAddr}`); + print(` Type: ${peer.type}`); + print(` Connection State: ${peer.connectionState}`); + print(` Bytes sent: ${peer.txBytes}`); + print(` Bytes received: ${peer.rxBytes}`); + } + } +} catch(err) { + print(`Error listing peers: ${err}`); +} +``` + +This code: +1. Calls `mycelium_list_peers()` to get all connected peers +2. Iterates through the peers and prints their details + +#### Adding a Peer + +To add a new peer to your Mycelium node: + +```rhai +// Add a new peer +print("\nAdding a new peer:"); +let new_peer_address = "tcp://65.21.231.58:9651"; +try { + let result = mycelium_add_peer(api_url, new_peer_address); + print(`Peer added: ${result.success}`); +} catch(err) { + print(`Error adding peer: ${err}`); +} +``` + +This code: +1. Specifies a peer address to add +2. Calls `mycelium_add_peer()` to add the peer to your node +3. Prints whether the operation was successful + +#### Removing a Peer + +To remove a peer from your Mycelium node: + +```rhai +// Remove a peer +print("\nRemoving a peer:"); +let peer_id = "tcp://65.21.231.58:9651"; // This is the peer we added earlier +try { + let result = mycelium_remove_peer(api_url, peer_id); + print(`Peer removed: ${result.success}`); +} catch(err) { + print(`Error removing peer: ${err}`); +} +``` + +This code: +1. Specifies the peer ID to remove +2. Calls `mycelium_remove_peer()` to remove the peer +3. Prints whether the operation was successful + +### Viewing Routing Information + +#### Listing Selected Routes + +To list the selected routes in your Mycelium node: + +```rhai +// List selected routes +print("\nListing selected routes:"); +try { + let routes = mycelium_list_selected_routes(api_url); + + if routes.is_empty() { + print("No selected routes."); + } else { + for route in routes { + print(`Subnet: ${route.subnet}`); + print(` Next hop: ${route.nextHop}`); + print(` Metric: ${route.metric}`); + } + } +} catch(err) { + print(`Error listing routes: ${err}`); +} +``` + +This code: +1. Calls `mycelium_list_selected_routes()` to get all selected routes +2. Iterates through the routes and prints their details + +#### Listing Fallback Routes + +To list the fallback routes in your Mycelium node: + +```rhai +// List fallback routes +print("\nListing fallback routes:"); +try { + let routes = mycelium_list_fallback_routes(api_url); + + if routes.is_empty() { + print("No fallback routes."); + } else { + for route in routes { + print(`Subnet: ${route.subnet}`); + print(` Next hop: ${route.nextHop}`); + print(` Metric: ${route.metric}`); + } + } +} catch(err) { + print(`Error listing fallback routes: ${err}`); +} +``` + +This code: +1. Calls `mycelium_list_fallback_routes()` to get all fallback routes +2. Iterates through the routes and prints their details + +## Sending Messages + +Now let's look at how to send messages using the `mycelium_send_message.rhai` example. + +```rhai +// API URL for Mycelium +let api_url = "http://localhost:1111"; + +// Send a message +print("\nSending a message:"); +let destination = "5af:ae6b:dcd8:ffdb:b71:7dde:d3:1033"; // Replace with the actual destination IP address +let topic = "test_topic"; +let message = "Hello from Rhai sender!"; +let deadline_secs = -10; // Seconds we wait for a reply + +try { + print(`Attempting to send message to ${destination} on topic '${topic}'`); + let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs); + print(`result: ${result}`); + print(`Message sent: ${result.success}`); + if result.id != "" { + print(`Message ID: ${result.id}`); + } +} catch(err) { + print(`Error sending message: ${err}`); +} +``` + +This code: +1. Sets the API URL for your Mycelium node +2. Specifies the destination IP address, topic, message content, and deadline +3. Calls `mycelium_send_message()` to send the message +4. Prints the result, including the message ID if successful + +### Important Parameters for Sending Messages + +- `api_url`: The URL of your Mycelium node's API +- `destination`: The IP address of the destination node +- `topic`: The topic to send the message on (must match what the receiver is listening for) +- `message`: The content of the message +- `deadline_secs`: Time in seconds to wait for a reply. Use a negative value if you don't want to wait for a reply. + +## Receiving Messages + +Now let's look at how to receive messages using the `mycelium_receive_message.rhai` example. + +```rhai +// API URL for Mycelium +let api_url = "http://localhost:2222"; + +// Receive messages +print("\nReceiving messages:"); +let receive_topic = "test_topic"; +let wait_deadline_secs = 100; + +print(`Listening for messages on topic '${receive_topic}'...`); +try { + let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs); + + if messages.is_empty() { + // print("No new messages received in this poll."); + } else { + print("Received a message:"); + print(` Message id: ${messages.id}`); + print(` Message from: ${messages.srcIp}`); + print(` Topic: ${messages.topic}`); + print(` Payload: ${messages.payload}`); + } +} catch(err) { + print(`Error receiving messages: ${err}`); +} + +print("Finished attempting to receive messages."); +``` + +This code: +1. Sets the API URL for your Mycelium node +2. Specifies the topic to listen on and how long to wait for messages +3. Calls `mycelium_receive_messages()` to receive messages +4. Processes and prints any received messages + +### Important Parameters for Receiving Messages + +- `api_url`: The URL of your Mycelium node's API +- `receive_topic`: The topic to listen for messages on (must match what the sender is using) +- `wait_deadline_secs`: Time in seconds to wait for messages to arrive. The function will block for this duration if no messages are immediately available. + +## Complete Messaging Example + +To set up a complete messaging system, you would typically run two instances of Mycelium (node A sender, node B receiver). + +1. Run the `mycelium_receive_message.rhai` script to listen for messages. **Fill in the API address of node B**. +2. Run the `mycelium_send_message.rhai` script to send messages. **Fill in the API address of node A, and fill in the overlay address of node B as destination**. + +### Setting Up the Receiver + +First, start a Mycelium node and run the receiver script: + +```rhai +// API URL for Mycelium +let api_url = "http://localhost:2222"; // Your receiver node's API URL + +// Receive messages +let receive_topic = "test_topic"; +let wait_deadline_secs = 100; // Wait up to 100 seconds for messages + +print(`Listening for messages on topic '${receive_topic}'...`); +try { + let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs); + + if messages.is_empty() { + print("No new messages received in this poll."); + } else { + print("Received a message:"); + print(` Message id: ${messages.id}`); + print(` Message from: ${messages.srcIp}`); + print(` Topic: ${messages.topic}`); + print(` Payload: ${messages.payload}`); + } +} catch(err) { + print(`Error receiving messages: ${err}`); +} +``` + +### Setting Up the Sender + +Then, on another Mycelium node, run the sender script: + +```rhai +// API URL for Mycelium +let api_url = "http://localhost:1111"; // Your sender node's API URL + +// Send a message +let destination = "5af:ae6b:dcd8:ffdb:b71:7dde:d3:1033"; // The receiver node's IP address +let topic = "test_topic"; // Must match the receiver's topic +let message = "Hello from Rhai sender!"; +let deadline_secs = -10; // Don't wait for a reply + +try { + print(`Attempting to send message to ${destination} on topic '${topic}'`); + let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs); + print(`Message sent: ${result.success}`); + if result.id != "" { + print(`Message ID: ${result.id}`); + } +} catch(err) { + print(`Error sending message: ${err}`); +} +``` \ No newline at end of file -- 2.40.1 From 7b8b8c662e2b3bf4071b9e0513b44b0703c36374 Mon Sep 17 00:00:00 2001 From: Maxime Van Hees Date: Fri, 16 May 2025 14:28:49 +0200 Subject: [PATCH 06/10] Added tutorial on how to send/receive message with 2 mycelium peers running the same host --- docs/docs/rhai/mycelium_tutorial.md | 58 +++++++++++++++++++- examples/mycelium/mycelium_send_message.rhai | 2 +- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/docs/docs/rhai/mycelium_tutorial.md b/docs/docs/rhai/mycelium_tutorial.md index 883ab4f..7ed83de 100644 --- a/docs/docs/rhai/mycelium_tutorial.md +++ b/docs/docs/rhai/mycelium_tutorial.md @@ -329,4 +329,60 @@ try { } catch(err) { print(`Error sending message: ${err}`); } -``` \ No newline at end of file +``` + +### Example: setting up 2 different Mycelium peers on same the host and sending/receiving a message + +#### Obtain Mycelium + +- Download the latest Mycelium binary from https://github.com/threefoldtech/mycelium/releases/ +- Or compile from source + +#### Setup +> `cd myceliumd && cargo build` + +- Create two different private key files. Each key file should contain exactely 32 bytes. In this example we'll save these files as `sender.bin` and `receiver.bin`. Note: generate your own 32-byte key files, the values below are just used as examples. +> `echo '9f3d72c1a84be6f027bba94cde015ee839cedb2ac4f2822bfc94449e3e2a1c6a' > sender.bin` + +> `echo 'e81c5a76f42bd9a3c73fe0bb2196acdfb6348e99d0b01763a2e57ce3a4e8f5dd' > receiver.bin` + +#### Start the nodes +- **Sender**: this node will have the API server hosted on `127.0.0.1:1111` and the JSON-RPC server on `127.0.0.1:8991`. +> `sudo ./mycelium --key-file sender.bin --disable-peer-discovery --disable-quic --no-tun --api-addr 127.0.0.1:1111 --jsonrpc-addr 127.0.0.1:8991` + +- **Receiver**: this node will have the API server hosted on `127.0.0.1:2222` and the JSON-RPC server on `127.0.0.1:8992`. +> `sudo ./mycelium --key-file receiver.bin --disable-peer-discovery --disable-quic --no-tun --api-addr 127.0.0.1:2222 --jsonrpc-addr 127.0.0.1:8992 --peers tcp://:9651` +- Obtain the Mycelium overlay IP by running `./mycelium --key-file receiver.bin --api-addr 127.0.0.1:2222 inspect`. **Replace this IP as destination in the [mycelium_receive_message.rhai](../../../examples/mycelium/mycelium_receive_message.rhai) example**. + +#### Execute the examples +- First build by executing `./build_herdo.sh` from the SAL root directory +- `cd target/debug` + +- Run the sender script: `sudo ./herodo --path ../../examples/mycelium/mycelium_send_message.rhai` +``` +Executing: ../../examples/mycelium/mycelium_send_message.rhai + +Sending a message: +Attempting to send message to 50e:6d75:4568:366e:f75:2ac3:bbb1:3fdd on topic 'test_topic' +result: #{"id": "bfd47dc689a7b826"} +Message sent: +Message ID: bfd47dc689a7b826 +Script executed successfull +``` + +- Run the receiver script: `sudo ./herodo --path ../../examples/mycelium/mycelium_receive_message.rhai` +``` +Executing: ../../examples/mycelium/mycelium_receive_message.rhai + +Receiving messages: +Listening for messages on topic 'test_topic'... +Received a message: + Message id: bfd47dc689a7b826 + Message from: 45d:26e1:a413:9d08:80ce:71c6:a931:4315 + Topic: dGVzdF90b3BpYw== + Payload: SGVsbG8gZnJvbSBSaGFpIHNlbmRlciE= +Finished attempting to receive messages. +Script executed successfully +``` +> Decoding the payload `SGVsbG8gZnJvbSBSaGFpIHNlbmRlciE=` results in the expected `Hello from Rhai sender!` message. Mission succesful! + diff --git a/examples/mycelium/mycelium_send_message.rhai b/examples/mycelium/mycelium_send_message.rhai index 9be348c..628a21e 100644 --- a/examples/mycelium/mycelium_send_message.rhai +++ b/examples/mycelium/mycelium_send_message.rhai @@ -7,7 +7,7 @@ let api_url = "http://localhost:1111"; // TO SEND A MESSAGE FILL IN THE DESTINATION IP ADDRESS // -----------------------------------------------------// print("\nSending a message:"); -let destination = "5af:ae6b:dcd8:ffdb:b71:7dde:d3:1033"; // IMPORTANT: Replace with the actual destination IP address +let destination = "50e:6d75:4568:366e:f75:2ac3:bbb1:3fdd"; // IMPORTANT: Replace with the actual destination IP address let topic = "test_topic"; let message = "Hello from Rhai sender!"; let deadline_secs = -10; // Seconds we wait for a reply -- 2.40.1 From dd84ce3f482c6ffa904ec754bda1a84c7e007fd7 Mon Sep 17 00:00:00 2001 From: Maxime Van Hees Date: Fri, 16 May 2025 14:30:46 +0200 Subject: [PATCH 07/10] remove command from tutorial --- docs/docs/rhai/mycelium_tutorial.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/docs/rhai/mycelium_tutorial.md b/docs/docs/rhai/mycelium_tutorial.md index 7ed83de..2b80a05 100644 --- a/docs/docs/rhai/mycelium_tutorial.md +++ b/docs/docs/rhai/mycelium_tutorial.md @@ -339,8 +339,6 @@ try { - Or compile from source #### Setup -> `cd myceliumd && cargo build` - - Create two different private key files. Each key file should contain exactely 32 bytes. In this example we'll save these files as `sender.bin` and `receiver.bin`. Note: generate your own 32-byte key files, the values below are just used as examples. > `echo '9f3d72c1a84be6f027bba94cde015ee839cedb2ac4f2822bfc94449e3e2a1c6a' > sender.bin` -- 2.40.1 From 229fef217fb1dcff268b4bda0bbb5620c8539eb5 Mon Sep 17 00:00:00 2001 From: Maxime Van Hees Date: Fri, 16 May 2025 14:32:03 +0200 Subject: [PATCH 08/10] fixed wrong link to file --- docs/docs/rhai/mycelium_tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/rhai/mycelium_tutorial.md b/docs/docs/rhai/mycelium_tutorial.md index 2b80a05..0c8715a 100644 --- a/docs/docs/rhai/mycelium_tutorial.md +++ b/docs/docs/rhai/mycelium_tutorial.md @@ -350,7 +350,7 @@ try { - **Receiver**: this node will have the API server hosted on `127.0.0.1:2222` and the JSON-RPC server on `127.0.0.1:8992`. > `sudo ./mycelium --key-file receiver.bin --disable-peer-discovery --disable-quic --no-tun --api-addr 127.0.0.1:2222 --jsonrpc-addr 127.0.0.1:8992 --peers tcp://:9651` -- Obtain the Mycelium overlay IP by running `./mycelium --key-file receiver.bin --api-addr 127.0.0.1:2222 inspect`. **Replace this IP as destination in the [mycelium_receive_message.rhai](../../../examples/mycelium/mycelium_receive_message.rhai) example**. +- Obtain the Mycelium overlay IP by running `./mycelium --key-file receiver.bin --api-addr 127.0.0.1:2222 inspect`. **Replace this IP as destination in the [mycelium_send_message.rhai](../../../examples/mycelium/mycelium_receive_message.rhai) example**. #### Execute the examples - First build by executing `./build_herdo.sh` from the SAL root directory -- 2.40.1 From cc4e087f1daccd49ce55e1812578f45e5ad986de Mon Sep 17 00:00:00 2001 From: Maxime Van Hees Date: Fri, 16 May 2025 14:32:35 +0200 Subject: [PATCH 09/10] fixed wrong link to file, again --- docs/docs/rhai/mycelium_tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/rhai/mycelium_tutorial.md b/docs/docs/rhai/mycelium_tutorial.md index 0c8715a..12a17e4 100644 --- a/docs/docs/rhai/mycelium_tutorial.md +++ b/docs/docs/rhai/mycelium_tutorial.md @@ -350,7 +350,7 @@ try { - **Receiver**: this node will have the API server hosted on `127.0.0.1:2222` and the JSON-RPC server on `127.0.0.1:8992`. > `sudo ./mycelium --key-file receiver.bin --disable-peer-discovery --disable-quic --no-tun --api-addr 127.0.0.1:2222 --jsonrpc-addr 127.0.0.1:8992 --peers tcp://:9651` -- Obtain the Mycelium overlay IP by running `./mycelium --key-file receiver.bin --api-addr 127.0.0.1:2222 inspect`. **Replace this IP as destination in the [mycelium_send_message.rhai](../../../examples/mycelium/mycelium_receive_message.rhai) example**. +- Obtain the Mycelium overlay IP by running `./mycelium --key-file receiver.bin --api-addr 127.0.0.1:2222 inspect`. **Replace this IP as destination in the [mycelium_send_message.rhai](../../../examples/mycelium/mycelium_send_message.rhai) example**. #### Execute the examples - First build by executing `./build_herdo.sh` from the SAL root directory -- 2.40.1 From c26e0e5ad8f931e810eacc9983ede046e11c7e49 Mon Sep 17 00:00:00 2001 From: Maxime Van Hees Date: Fri, 16 May 2025 15:04:00 +0200 Subject: [PATCH 10/10] removed unused imports --- src/mycelium/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/mycelium/mod.rs b/src/mycelium/mod.rs index 76b76f7..89f9b5d 100644 --- a/src/mycelium/mod.rs +++ b/src/mycelium/mod.rs @@ -1,6 +1,5 @@ use base64::{ - alphabet, - engine::{self, general_purpose}, + engine::general_purpose, Engine as _, }; use reqwest::Client; -- 2.40.1