feat: Add Mycelium JSON-RPC client

- Adds a new V language client for interacting with the Mycelium
  JSON-RPC admin API.
- Includes comprehensive example code demonstrating all API features.
- Implements all methods defined in the Mycelium JSON-RPC spec.
- Provides type-safe API with robust error handling.
- Uses HTTP transport for communication with the Mycelium node.
This commit is contained in:
Mahmoud-Emad
2025-06-02 16:48:59 +03:00
parent d0baac83a9
commit c5759ea30e
12 changed files with 2282 additions and 41 deletions

257
examples/clients/mycelium_rpc.vsh Executable file
View File

@@ -0,0 +1,257 @@
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
// Mycelium RPC Client Example
// This example demonstrates how to use the new Mycelium JSON-RPC client
// to interact with a Mycelium node's admin API
import freeflowuniverse.herolib.clients.mycelium_rpc
import freeflowuniverse.herolib.installers.net.mycelium_installer
import time
import os
import encoding.base64
const mycelium_port = 8990
fn terminate_mycelium() ! {
// Try to find and kill any running mycelium process
res := os.execute('pkill mycelium')
if res.exit_code == 0 {
println('Terminated existing mycelium processes')
time.sleep(1 * time.second)
}
}
fn start_mycelium_node() ! {
// Start a mycelium node with JSON-RPC API enabled
println('Starting Mycelium node with JSON-RPC API on port ${mycelium_port}...')
// Create directory for mycelium data
os.execute('mkdir -p /tmp/mycelium_rpc_example')
// Start mycelium in background with both HTTP and JSON-RPC APIs enabled
spawn fn () {
cmd := 'cd /tmp/mycelium_rpc_example && mycelium --peers tcp://185.69.166.8:9651 quic://[2a02:1802:5e:0:ec4:7aff:fe51:e36b]:9651 tcp://65.109.18.113:9651 --tun-name tun_rpc_example --tcp-listen-port 9660 --quic-listen-port 9661 --api-addr 127.0.0.1:8989 --jsonrpc-addr 127.0.0.1:${mycelium_port}'
println('Executing: ${cmd}')
result := os.execute(cmd)
if result.exit_code != 0 {
println('Mycelium failed to start: ${result.output}')
}
}()
// Wait for the node to start (JSON-RPC server needs a bit more time)
println('Waiting for mycelium to start...')
time.sleep(5 * time.second)
// Check if mycelium is running
check_result := os.execute('pgrep mycelium')
if check_result.exit_code == 0 {
println('Mycelium process is running (PID: ${check_result.output.trim_space()})')
} else {
println('Warning: Mycelium process not found')
}
// Check what ports are listening
port_check := os.execute('lsof -i :${mycelium_port}')
if port_check.exit_code == 0 {
println('Port ${mycelium_port} is listening:')
println(port_check.output)
} else {
println('Warning: Port ${mycelium_port} is not listening')
}
}
fn main() {
// Install mycelium if not already installed
println('Checking Mycelium installation...')
mut installer := mycelium_installer.get()!
installer.install()!
// Clean up any existing processes
terminate_mycelium() or {}
defer {
// Clean up on exit
terminate_mycelium() or {}
os.execute('rm -rf /tmp/mycelium_rpc_example')
}
// Start mycelium node
start_mycelium_node()!
// Create RPC client
println('\n=== Creating Mycelium RPC Client ===')
mut client := mycelium_rpc.new_client(
name: 'example_client'
url: 'http://localhost:${mycelium_port}'
)!
println('Connected to Mycelium node at http://localhost:${mycelium_port}')
// Example 1: Get node information
println('\n=== Getting Node Information ===')
info := client.get_info() or {
println('Error getting node info: ${err}')
println('Make sure Mycelium node is running with API enabled')
return
}
println('Node Subnet: ${info.node_subnet}')
println('Node Public Key: ${info.node_pubkey}')
// Example 2: List peers
println('\n=== Listing Peers ===')
peers := client.get_peers() or {
println('Error getting peers: ${err}')
return
}
println('Found ${peers.len} peers:')
for i, peer in peers {
println('Peer ${i + 1}:')
println(' Endpoint: ${peer.endpoint.proto}://${peer.endpoint.socket_addr}')
println(' Type: ${peer.peer_type}')
println(' Connection State: ${peer.connection_state}')
println(' TX Bytes: ${peer.tx_bytes}')
println(' RX Bytes: ${peer.rx_bytes}')
}
// Example 3: Get routing information
println('\n=== Getting Routing Information ===')
// Get selected routes
routes := client.get_selected_routes() or {
println('Error getting selected routes: ${err}')
return
}
println('Selected Routes (${routes.len}):')
for route in routes {
println(' ${route.subnet} -> ${route.next_hop} (metric: ${route.metric}, seqno: ${route.seqno})')
}
// Get fallback routes
fallback_routes := client.get_fallback_routes() or {
println('Error getting fallback routes: ${err}')
return
}
println('Fallback Routes (${fallback_routes.len}):')
for route in fallback_routes {
println(' ${route.subnet} -> ${route.next_hop} (metric: ${route.metric}, seqno: ${route.seqno})')
}
// Example 4: Topic management
println('\n=== Topic Management ===')
// Get default topic action
default_action := client.get_default_topic_action() or {
println('Error getting default topic action: ${err}')
return
}
println('Default topic action (accept): ${default_action}')
// Get configured topics
topics := client.get_topics() or {
println('Error getting topics: ${err}')
return
}
println('Configured topics (${topics.len}):')
for topic in topics {
println(' - ${topic}')
}
// Example 5: Add a test topic (try different names)
println('\n=== Adding Test Topics ===')
test_topics := ['example_topic', 'test_with_underscore', 'hello world', 'test', 'a']
for topic in test_topics {
println('Trying to add topic: "${topic}"')
add_result := client.add_topic(topic) or {
println('Error adding topic "${topic}": ${err}')
continue
}
if add_result {
println('Successfully added topic: ${topic}')
// Try to remove it immediately
remove_result := client.remove_topic(topic) or {
println('Error removing topic "${topic}": ${err}')
continue
}
if remove_result {
println('Successfully removed topic: ${topic}')
}
break // Stop after first success
}
}
// Example 6: Message operations (demonstration only - requires another node)
println('\n=== Message Operations (Demo) ===')
println('Note: These operations require another Mycelium node to be meaningful')
// Try to pop a message with a short timeout (will likely return "No message ready" error)
message := client.pop_message(false, 1, '') or {
println('No messages available (expected): ${err}')
mycelium_rpc.InboundMessage{}
}
if message.id != '' {
println('Received message:')
println(' ID: ${message.id}')
println(' From: ${message.src_ip}')
println(' Payload: ${base64.decode_str(message.payload)}')
}
// Example 7: Peer management (demonstration)
println('\n=== Peer Management Demo ===')
// Try to add a peer (this is just for demonstration)
test_endpoint := 'tcp://127.0.0.1:9999'
add_peer_result := client.add_peer(test_endpoint) or {
println('Error adding peer (expected if endpoint is invalid): ${err}')
false
}
if add_peer_result {
println('Successfully added peer: ${test_endpoint}')
// Remove the test peer
remove_peer_result := client.delete_peer(test_endpoint) or {
println('Error removing peer: ${err}')
false
}
if remove_peer_result {
println('Successfully removed test peer')
}
}
// Example 8: Get public key from IP (demonstration)
println('\n=== Public Key Lookup Demo ===')
// This will likely fail unless we have a valid mycelium IP
if info.node_subnet != '' {
// Extract the first IP from the subnet for testing
subnet_parts := info.node_subnet.split('::')
if subnet_parts.len > 0 {
test_ip := subnet_parts[0] + '::1'
pubkey_response := client.get_public_key_from_ip(test_ip) or {
println('Could not get public key for IP ${test_ip}: ${err}')
mycelium_rpc.PublicKeyResponse{}
}
if pubkey_response.node_pub_key != '' {
println('Public key for ${test_ip}: ${pubkey_response.node_pub_key}')
}
}
}
println('\n=== Mycelium RPC Client Example Completed ===')
println('This example demonstrated:')
println('- Getting node information')
println('- Listing peers and their connection status')
println('- Retrieving routing information')
println('- Managing topics')
println('- Message operations (basic)')
println('- Peer management')
println('- Public key lookups')
println('')
println('For full message sending/receiving functionality, you would need')
println('multiple Mycelium nodes running and connected to each other.')
println('See the Mycelium documentation for more advanced usage.')
}

View File

@@ -0,0 +1,8 @@
!!hero_code.generate_client
name:'mycelium_rpc'
classname:'MyceliumRPC'
singleton:1
default:0
hasconfig:1
reset:0

View File

@@ -0,0 +1,337 @@
module mycelium_rpc
import freeflowuniverse.herolib.schemas.jsonrpc
import freeflowuniverse.herolib.core.httpconnection
import encoding.base64
// Helper function to get or create the RPC client
fn (mut c MyceliumRPC) get_client() !&jsonrpc.Client {
if client := c.rpc_client {
return client
}
// Create HTTP transport using httpconnection
mut http_conn := httpconnection.new(
name: 'mycelium_rpc_${c.name}'
url: c.url
)!
// Create a simple HTTP transport wrapper
transport := HTTPTransport{
http_conn: http_conn
}
mut client := jsonrpc.new_client(transport)
c.rpc_client = client
return client
}
// HTTPTransport implements IRPCTransportClient for HTTP connections
struct HTTPTransport {
mut:
http_conn &httpconnection.HTTPConnection
}
// send implements the IRPCTransportClient interface
fn (mut t HTTPTransport) send(request string, params jsonrpc.SendParams) !string {
req := httpconnection.Request{
method: .post
prefix: '/'
dataformat: .json
data: request
}
response := t.http_conn.post_json_str(req)!
return response
}
// Admin methods
// get_info gets general info about the node
pub fn (mut c MyceliumRPC) get_info() !Info {
mut client := c.get_client()!
request := jsonrpc.new_request_generic('getInfo', []string{})
return client.send[[]string, Info](request)!
}
// get_peers lists known peers
pub fn (mut c MyceliumRPC) get_peers() ![]PeerStats {
mut client := c.get_client()!
request := jsonrpc.new_request_generic('getPeers', []string{})
return client.send[[]string, []PeerStats](request)!
}
// add_peer adds a new peer identified by the provided endpoint
pub fn (mut c MyceliumRPC) add_peer(endpoint string) !bool {
mut client := c.get_client()!
params := {
'endpoint': endpoint
}
request := jsonrpc.new_request_generic('addPeer', params)
return client.send[map[string]string, bool](request)!
}
// delete_peer removes an existing peer identified by the provided endpoint
pub fn (mut c MyceliumRPC) delete_peer(endpoint string) !bool {
mut client := c.get_client()!
params := {
'endpoint': endpoint
}
request := jsonrpc.new_request_generic('deletePeer', params)
return client.send[map[string]string, bool](request)!
}
// Route methods
// get_selected_routes lists all selected routes
pub fn (mut c MyceliumRPC) get_selected_routes() ![]Route {
mut client := c.get_client()!
request := jsonrpc.new_request_generic('getSelectedRoutes', []string{})
return client.send[[]string, []Route](request)!
}
// get_fallback_routes lists all active fallback routes
pub fn (mut c MyceliumRPC) get_fallback_routes() ![]Route {
mut client := c.get_client()!
request := jsonrpc.new_request_generic('getFallbackRoutes', []string{})
return client.send[[]string, []Route](request)!
}
// get_queried_subnets lists all currently queried subnets
pub fn (mut c MyceliumRPC) get_queried_subnets() ![]QueriedSubnet {
mut client := c.get_client()!
request := jsonrpc.new_request_generic('getQueriedSubnets', []string{})
return client.send[[]string, []QueriedSubnet](request)!
}
// get_no_route_entries lists all subnets which are explicitly marked as no route
pub fn (mut c MyceliumRPC) get_no_route_entries() ![]NoRouteSubnet {
mut client := c.get_client()!
request := jsonrpc.new_request_generic('getNoRouteEntries', []string{})
return client.send[[]string, []NoRouteSubnet](request)!
}
// get_public_key_from_ip gets the pubkey from node ip
pub fn (mut c MyceliumRPC) get_public_key_from_ip(mycelium_ip string) !PublicKeyResponse {
mut client := c.get_client()!
params := {
'mycelium_ip': mycelium_ip
}
request := jsonrpc.new_request_generic('getPublicKeyFromIp', params)
return client.send[map[string]string, PublicKeyResponse](request)!
}
// Message methods
// PopMessageParams represents parameters for pop_message method
pub struct PopMessageParams {
pub mut:
peek ?bool // Whether to peek the message or not
timeout ?i64 // Amount of seconds to wait for a message
topic ?string // Optional filter for loading messages
}
// pop_message gets a message from the inbound message queue
pub fn (mut c MyceliumRPC) pop_message(peek bool, timeout i64, topic string) !InboundMessage {
mut client := c.get_client()!
mut params := PopMessageParams{}
if peek {
params.peek = peek
}
if timeout > 0 {
params.timeout = timeout
}
if topic != '' {
// Encode topic as base64 as required by mycelium
params.topic = base64.encode_str(topic)
}
request := jsonrpc.new_request_generic('popMessage', params)
return client.send[PopMessageParams, InboundMessage](request)!
}
// PushMessageParams represents parameters for push_message method
pub struct PushMessageParams {
pub mut:
message PushMessageBody // The message to send
reply_timeout ?i64 // Amount of seconds to wait for a reply
}
// push_message submits a new message to the system
pub fn (mut c MyceliumRPC) push_message(message PushMessageBody, reply_timeout i64) !string {
mut client := c.get_client()!
mut params := PushMessageParams{
message: message
}
if reply_timeout > 0 {
params.reply_timeout = reply_timeout
}
request := jsonrpc.new_request_generic('pushMessage', params)
// The response can be either InboundMessage or PushMessageResponseId
// For simplicity, we'll return the raw JSON response as string
return client.send[PushMessageParams, string](request)!
}
// PushMessageReplyParams represents parameters for push_message_reply method
pub struct PushMessageReplyParams {
pub mut:
id string // The ID of the message to reply to
message PushMessageBody // The reply message
}
// push_message_reply replies to a message with the given ID
pub fn (mut c MyceliumRPC) push_message_reply(id string, message PushMessageBody) !bool {
mut client := c.get_client()!
params := PushMessageReplyParams{
id: id
message: message
}
request := jsonrpc.new_request_generic('pushMessageReply', params)
return client.send[PushMessageReplyParams, bool](request)!
}
// get_message_info gets the status of an outbound message
pub fn (mut c MyceliumRPC) get_message_info(id string) !MessageStatusResponse {
mut client := c.get_client()!
params := {
'id': id
}
request := jsonrpc.new_request_generic('getMessageInfo', params)
return client.send[map[string]string, MessageStatusResponse](request)!
}
// Topic management methods
// get_default_topic_action gets the default topic action
pub fn (mut c MyceliumRPC) get_default_topic_action() !bool {
mut client := c.get_client()!
request := jsonrpc.new_request_generic('getDefaultTopicAction', []string{})
return client.send[[]string, bool](request)!
}
// SetDefaultTopicActionParams represents parameters for set_default_topic_action method
pub struct SetDefaultTopicActionParams {
pub mut:
accept bool // Whether to accept unconfigured topics by default
}
// set_default_topic_action sets the default topic action
pub fn (mut c MyceliumRPC) set_default_topic_action(accept bool) !bool {
mut client := c.get_client()!
params := SetDefaultTopicActionParams{
accept: accept
}
request := jsonrpc.new_request_generic('setDefaultTopicAction', params)
return client.send[SetDefaultTopicActionParams, bool](request)!
}
// get_topics gets all configured topics
pub fn (mut c MyceliumRPC) get_topics() ![]string {
mut client := c.get_client()!
request := jsonrpc.new_request_generic('getTopics', []string{})
encoded_topics := client.send[[]string, []string](request)!
// Decode base64-encoded topics for user convenience
mut decoded_topics := []string{}
for encoded_topic in encoded_topics {
decoded_topic := base64.decode_str(encoded_topic)
decoded_topics << decoded_topic
}
return decoded_topics
}
// add_topic adds a new topic to the system's whitelist
pub fn (mut c MyceliumRPC) add_topic(topic string) !bool {
mut client := c.get_client()!
// Encode topic as base64 as required by mycelium
encoded_topic := base64.encode_str(topic)
params := {
'topic': encoded_topic
}
request := jsonrpc.new_request_generic('addTopic', params)
return client.send[map[string]string, bool](request)!
}
// remove_topic removes a topic from the system's whitelist
pub fn (mut c MyceliumRPC) remove_topic(topic string) !bool {
mut client := c.get_client()!
// Encode topic as base64 as required by mycelium
encoded_topic := base64.encode_str(topic)
params := {
'topic': encoded_topic
}
request := jsonrpc.new_request_generic('removeTopic', params)
return client.send[map[string]string, bool](request)!
}
// get_topic_sources gets all sources (subnets) that are allowed to send messages for a specific topic
pub fn (mut c MyceliumRPC) get_topic_sources(topic string) ![]string {
mut client := c.get_client()!
// Encode topic as base64 as required by mycelium
encoded_topic := base64.encode_str(topic)
params := {
'topic': encoded_topic
}
request := jsonrpc.new_request_generic('getTopicSources', params)
return client.send[map[string]string, []string](request)!
}
// add_topic_source adds a source (subnet) that is allowed to send messages for a specific topic
pub fn (mut c MyceliumRPC) add_topic_source(topic string, subnet string) !bool {
mut client := c.get_client()!
// Encode topic as base64 as required by mycelium
encoded_topic := base64.encode_str(topic)
params := {
'topic': encoded_topic
'subnet': subnet
}
request := jsonrpc.new_request_generic('addTopicSource', params)
return client.send[map[string]string, bool](request)!
}
// remove_topic_source removes a source (subnet) that is allowed to send messages for a specific topic
pub fn (mut c MyceliumRPC) remove_topic_source(topic string, subnet string) !bool {
mut client := c.get_client()!
// Encode topic as base64 as required by mycelium
encoded_topic := base64.encode_str(topic)
params := {
'topic': encoded_topic
'subnet': subnet
}
request := jsonrpc.new_request_generic('removeTopicSource', params)
return client.send[map[string]string, bool](request)!
}
// get_topic_forward_socket gets the forward socket for a topic
pub fn (mut c MyceliumRPC) get_topic_forward_socket(topic string) !string {
mut client := c.get_client()!
// Encode topic as base64 as required by mycelium
encoded_topic := base64.encode_str(topic)
params := {
'topic': encoded_topic
}
request := jsonrpc.new_request_generic('getTopicForwardSocket', params)
return client.send[map[string]string, string](request)!
}
// set_topic_forward_socket sets the socket path where messages for a specific topic should be forwarded to
pub fn (mut c MyceliumRPC) set_topic_forward_socket(topic string, socket_path string) !bool {
mut client := c.get_client()!
// Encode topic as base64 as required by mycelium
encoded_topic := base64.encode_str(topic)
params := {
'topic': encoded_topic
'socket_path': socket_path
}
request := jsonrpc.new_request_generic('setTopicForwardSocket', params)
return client.send[map[string]string, bool](request)!
}
// remove_topic_forward_socket removes the socket path where messages for a specific topic are forwarded to
pub fn (mut c MyceliumRPC) remove_topic_forward_socket(topic string) !bool {
mut client := c.get_client()!
// Encode topic as base64 as required by mycelium
encoded_topic := base64.encode_str(topic)
params := {
'topic': encoded_topic
}
request := jsonrpc.new_request_generic('removeTopicForwardSocket', params)
return client.send[map[string]string, bool](request)!
}

View File

@@ -0,0 +1,114 @@
module mycelium_rpc
import freeflowuniverse.herolib.core.base
import freeflowuniverse.herolib.core.playbook
import freeflowuniverse.herolib.ui.console
__global (
mycelium_rpc_global map[string]&MyceliumRPC
mycelium_rpc_default string
)
/////////FACTORY
@[params]
pub struct ArgsGet {
pub mut:
name string
}
fn args_get(args_ ArgsGet) ArgsGet {
mut args := args_
if args.name == '' {
args.name = 'default'
}
return args
}
pub fn get(args_ ArgsGet) !&MyceliumRPC {
mut context := base.context()!
mut args := args_get(args_)
mut obj := MyceliumRPC{
name: args.name
}
if args.name !in mycelium_rpc_global {
if !exists(args)! {
set(obj)!
} else {
heroscript := context.hero_config_get('mycelium_rpc', args.name)!
mut obj_ := heroscript_loads(heroscript)!
set_in_mem(obj_)!
}
}
return mycelium_rpc_global[args.name] or {
println(mycelium_rpc_global)
// bug if we get here because should be in globals
panic('could not get config for mycelium_rpc with name, is bug:${args.name}')
}
}
// register the config for the future
pub fn set(o MyceliumRPC) ! {
set_in_mem(o)!
mut context := base.context()!
heroscript := heroscript_dumps(o)!
context.hero_config_set('mycelium_rpc', o.name, heroscript)!
}
// does the config exists?
pub fn exists(args_ ArgsGet) !bool {
mut context := base.context()!
mut args := args_get(args_)
return context.hero_config_exists('mycelium_rpc', args.name)
}
pub fn delete(args_ ArgsGet) ! {
mut args := args_get(args_)
mut context := base.context()!
context.hero_config_delete('mycelium_rpc', args.name)!
if args.name in mycelium_rpc_global {
// del mycelium_rpc_global[args.name]
}
}
// only sets in mem, does not set as config
fn set_in_mem(o MyceliumRPC) ! {
mut o2 := obj_init(o)!
mycelium_rpc_global[o.name] = &o2
mycelium_rpc_default = o.name
}
@[params]
pub struct PlayArgs {
pub mut:
heroscript string // if filled in then plbook will be made out of it
plbook ?playbook.PlayBook
reset bool
}
pub fn play(args_ PlayArgs) ! {
mut args := args_
mut plbook := args.plbook or { playbook.new(text: args.heroscript)! }
mut install_actions := plbook.find(filter: 'mycelium_rpc.configure')!
if install_actions.len > 0 {
for install_action in install_actions {
heroscript := install_action.heroscript()
mut obj2 := heroscript_loads(heroscript)!
set(obj2)!
}
}
}
// switch instance to be used for mycelium_rpc
pub fn switch(name string) {
mycelium_rpc_default = name
}
// helpers
@[params]
pub struct DefaultConfigArgs {
instance string = 'default'
}

View File

@@ -0,0 +1,158 @@
module mycelium_rpc
import freeflowuniverse.herolib.data.encoderhero
import freeflowuniverse.herolib.schemas.jsonrpc
pub const version = '0.0.0'
const singleton = true
const default = false
// Default configuration for Mycelium JSON-RPC API
pub const default_url = 'http://localhost:8990'
// THIS THE THE SOURCE OF THE INFORMATION OF THIS FILE, HERE WE HAVE THE CONFIG OBJECT CONFIGURED AND MODELLED
@[heap]
pub struct MyceliumRPC {
pub mut:
name string = 'default'
url string = default_url // RPC server URL
rpc_client ?&jsonrpc.Client @[skip]
}
// your checking & initialization code if needed
fn obj_init(mycfg_ MyceliumRPC) !MyceliumRPC {
mut mycfg := mycfg_
if mycfg.url == '' {
mycfg.url = default_url
}
// For now, we'll initialize the client when needed
// The actual client will be created in the factory
return mycfg
}
// Response structs based on OpenRPC specification
// Info represents general information about a node
pub struct Info {
pub mut:
node_subnet string @[json: 'nodeSubnet'] // The subnet owned by the node and advertised to peers
node_pubkey string @[json: 'nodePubkey'] // The public key of the node (hex encoded, 64 chars)
}
// Endpoint represents identification to connect to a peer
pub struct Endpoint {
pub mut:
proto string @[json: 'proto'] // Protocol used (tcp, quic)
socket_addr string @[json: 'socketAddr'] // The socket address used
}
// PeerStats represents info about a peer
pub struct PeerStats {
pub mut:
endpoint Endpoint @[json: 'endpoint'] // Peer endpoint
peer_type string @[json: 'type'] // How we know about this peer (static, inbound, linkLocalDiscovery)
connection_state string @[json: 'connectionState'] // Current state of connection (alive, connecting, dead)
tx_bytes i64 @[json: 'txBytes'] // Bytes transmitted to this peer
rx_bytes i64 @[json: 'rxBytes'] // Bytes received from this peer
}
// Route represents information about a route
pub struct Route {
pub mut:
subnet string @[json: 'subnet'] // The overlay subnet for which this is the route
next_hop string @[json: 'nextHop'] // Way to identify the next hop of the route
metric string @[json: 'metric'] // The metric of the route (can be int or "infinite")
seqno int @[json: 'seqno'] // Sequence number advertised with this route
}
// QueriedSubnet represents information about a subnet currently being queried
pub struct QueriedSubnet {
pub mut:
subnet string // The overlay subnet which we are currently querying
expiration string // Amount of seconds until the query expires
}
// NoRouteSubnet represents information about a subnet marked as no route
pub struct NoRouteSubnet {
pub mut:
subnet string // The overlay subnet which is marked
expiration string // Amount of seconds until the entry expires
}
// InboundMessage represents a message received by the system
pub struct InboundMessage {
pub mut:
id string @[json: 'id'] // Id of the message, hex encoded (16 chars)
src_ip string @[json: 'srcIp'] // Sender overlay IP address (IPv6)
src_pk string @[json: 'srcPk'] // Sender public key, hex encoded (64 chars)
dst_ip string @[json: 'dstIp'] // Receiver overlay IP address (IPv6)
dst_pk string @[json: 'dstPk'] // Receiver public key, hex encoded (64 chars)
topic string @[json: 'topic'] // Optional message topic (base64 encoded, 0-340 chars)
payload string @[json: 'payload'] // Message payload, base64 encoded
}
// MessageDestination represents the destination for a message
pub struct MessageDestination {
pub mut:
ip string // Target IP of the message (IPv6)
pk string // Hex encoded public key of the target node (64 chars)
}
// PushMessageBody represents a message to send to a given receiver
pub struct PushMessageBody {
pub mut:
dst MessageDestination // Message destination
topic string // Optional message topic (base64 encoded, 0-340 chars)
payload string // Message to send, base64 encoded
}
// PushMessageResponseId represents the ID generated for a message after pushing
pub struct PushMessageResponseId {
pub mut:
id string // Id of the message, hex encoded (16 chars)
}
// MessageStatusResponse represents information about an outbound message
pub struct MessageStatusResponse {
pub mut:
dst string @[json: 'dst'] // IP address of the receiving node (IPv6)
state string @[json: 'state'] // Transmission state
created i64 @[json: 'created'] // Unix timestamp of when this message was created
deadline i64 @[json: 'deadline'] // Unix timestamp of when this message will expire
msg_len int @[json: 'msgLen'] // Length of the message in bytes
}
// PublicKeyResponse represents public key requested based on a node's IP
pub struct PublicKeyResponse {
pub mut:
node_pub_key string @[json: 'NodePubKey'] // Public key (hex encoded, 64 chars)
}
/////////////NORMALLY NO NEED TO TOUCH
pub fn heroscript_dumps(obj MyceliumRPC) !string {
return encoderhero.encode[MyceliumRPC](obj)!
}
pub fn heroscript_loads(heroscript string) !MyceliumRPC {
mut obj := encoderhero.decode[MyceliumRPC](heroscript)!
return obj
}
// Factory function to create a new MyceliumRPC client instance
@[params]
pub struct NewClientArgs {
pub mut:
name string = 'default'
url string = default_url
}
pub fn new_client(args NewClientArgs) !&MyceliumRPC {
mut client := MyceliumRPC{
name: args.name
url: args.url
}
client = obj_init(client)!
return &client
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,180 @@
# Mycelium RPC Client
This is a V language client for the Mycelium mesh networking system, implementing the JSON-RPC API specification for administrative operations.
## Overview
Mycelium is a mesh networking system that creates secure, encrypted connections between nodes. This client provides a comprehensive API to interact with Mycelium nodes via their JSON-RPC interface for administrative tasks such as:
- Node information retrieval
- Peer management
- Routing information
- Message operations
- Topic management
## Features
- Complete implementation of all methods in the Mycelium JSON-RPC specification
- Type-safe API with proper error handling
- HTTP transport support
- Comprehensive documentation
- Example code for all operations
## Usage
### Basic Example
```v
import freeflowuniverse.herolib.clients.mycelium_rpc
// Create a new client
mut client := mycelium_rpc.new_client(
name: 'my_client'
url: 'http://localhost:8990'
)!
// Get node information
info := client.get_info()!
println('Node Subnet: ${info.node_subnet}')
println('Node Public Key: ${info.node_pubkey}')
// List peers
peers := client.get_peers()!
for peer in peers {
println('Peer: ${peer.endpoint.proto}://${peer.endpoint.socket_addr}')
println('State: ${peer.connection_state}')
}
```
### Configuration
The client can be configured with:
- `name`: Client instance name (default: 'default')
- `url`: Mycelium node API URL (default: 'http://localhost:8990')
### Available Methods
#### Admin Methods
- `get_info()` - Get general information about the node
- `get_peers()` - List known peers
- `add_peer(endpoint)` - Add a new peer
- `delete_peer(endpoint)` - Remove an existing peer
- `get_public_key_from_ip(ip)` - Get public key from node IP
#### Routing Methods
- `get_selected_routes()` - List all selected routes
- `get_fallback_routes()` - List all active fallback routes
- `get_queried_subnets()` - List currently queried subnets
- `get_no_route_entries()` - List subnets marked as no route
#### Message Methods
- `pop_message(peek, timeout, topic)` - Get message from inbound queue
- `push_message(message, reply_timeout)` - Submit new message to system
- `push_message_reply(id, message)` - Reply to a message
- `get_message_info(id)` - Get status of an outbound message
#### Topic Management Methods
- `get_default_topic_action()` - Get default topic action
- `set_default_topic_action(accept)` - Set default topic action
- `get_topics()` - Get all configured topics
- `add_topic(topic)` - Add new topic to whitelist
- `remove_topic(topic)` - Remove topic from whitelist
- `get_topic_sources(topic)` - Get sources for a topic
- `add_topic_source(topic, subnet)` - Add source to topic
- `remove_topic_source(topic, subnet)` - Remove source from topic
- `get_topic_forward_socket(topic)` - Get forward socket for topic
- `set_topic_forward_socket(topic, path)` - Set forward socket for topic
- `remove_topic_forward_socket(topic)` - Remove forward socket for topic
## Data Types
### Info
```v
struct Info {
node_subnet string // The subnet owned by the node
node_pubkey string // The public key of the node (hex encoded)
}
```
### PeerStats
```v
struct PeerStats {
endpoint Endpoint // Peer endpoint
peer_type string // How we know about this peer
connection_state string // Current state of connection
tx_bytes i64 // Bytes transmitted to this peer
rx_bytes i64 // Bytes received from this peer
}
```
### InboundMessage
```v
struct InboundMessage {
id string // Message ID (hex encoded)
src_ip string // Sender overlay IP address
src_pk string // Sender public key (hex encoded)
dst_ip string // Receiver overlay IP address
dst_pk string // Receiver public key (hex encoded)
topic string // Optional message topic (base64 encoded)
payload string // Message payload (base64 encoded)
}
```
## Examples
See `examples/clients/mycelium_rpc.vsh` for a comprehensive example that demonstrates:
- Node information retrieval
- Peer listing and management
- Routing information
- Topic management
- Message operations
- Error handling
## Requirements
- V language compiler
- Mycelium node running with API enabled
- Network connectivity to the Mycelium node
## Running the Example
```bash
# Make sure Mycelium is installed
v run examples/clients/mycelium_rpc.vsh
```
The example will:
1. Install Mycelium if needed
2. Start a Mycelium node with API enabled
3. Demonstrate various RPC operations
4. Clean up resources on exit
## Error Handling
All methods return Result types and should be handled appropriately:
```v
info := client.get_info() or {
println('Error getting node info: ${err}')
return
}
```
## Notes
- The client uses HTTP transport to communicate with the Mycelium node
- All JSON field names are properly mapped using V's `@[json: 'field_name']` attributes
- The client is thread-safe and can be used concurrently
- Message operations require multiple connected Mycelium nodes to be meaningful
## License
This client follows the same license as the HeroLib project.

View File

@@ -22,6 +22,7 @@ fn startupcmd() ![]zinit.ZProcessNewArgs {
res << zinit.ZProcessNewArgs{
name: 'mycelium'
startuptype: .zinit
cmd: 'mycelium --key-file ${osal.hero_path()!}/cfg/priv_key.bin --peers ${peers_str} --tun-name ${tun_name}'
env: {
'HOME': '/root'
@@ -86,9 +87,6 @@ fn upload() ! {
fn install() ! {
console.print_header('install mycelium')
mut z_installer := zinit_installer.get()!
z_installer.start()!
mut url := ''
if core.is_linux_arm()! {
url = 'https://github.com/threefoldtech/mycelium/releases/download/v${version}/mycelium-aarch64-unknown-linux-musl.tar.gz'

View File

@@ -3,7 +3,7 @@ module mycelium_installer
import freeflowuniverse.herolib.data.encoderhero
import freeflowuniverse.herolib.osal.tun
pub const version = '0.5.7'
pub const version = '0.6.1'
const singleton = true
const default = true
@@ -13,16 +13,17 @@ pub struct MyceliumInstaller {
pub mut:
name string = 'default'
peers []string = [
'tcp://188.40.132.242:9651',
'quic://[2a01:4f8:212:fa6::2]:9651',
'tcp://185.69.166.7:9651',
// v0.6.x public nodes
'tcp://185.69.166.8:9651',
'quic://[2a02:1802:5e:0:ec4:7aff:fe51:e36b]:9651',
'tcp://65.21.231.58:9651',
'tcp://65.109.18.113:9651',
'quic://[2a01:4f9:5a:1042::2]:9651',
'tcp://[2604:a00:50:17b:9e6b:ff:fe1f:e054]:9651',
'quic://5.78.122.16:9651',
'tcp://[2a01:4ff:2f0:3621::1]:9651',
'quic://142.93.217.194:9651',
'tcp://5.78.122.16:9651',
'quic://[2a01:4ff:1f0:8859::1]:9651',
'tcp://5.223.43.251:9651',
'quic://[2a01:4ff:2f0:3621::1]:9651',
'tcp://142.93.217.194:9651',
'quic://[2400:6180:100:d0::841:2001]:9651',
]
tun_nr int
}

View File

@@ -54,7 +54,7 @@ pub fn (mut c ZinitClient) discover() !string {
request := jsonrpc.new_request_generic('rpc.discover', []string{})
// Send the request and get the raw response
raw_response := c.rpc_client.rpc_client.transport.send(request.encode(), jsonrpc.SendParams{
raw_response := c.rpc_client.transport.send(request.encode(), jsonrpc.SendParams{
timeout: 5 // Increase timeout to 5 seconds
})!

View File

@@ -1,4 +1,5 @@
module jsonrpc
import freeflowuniverse.herolib.ui.console
// IRPCTransportClient defines the interface for transport mechanisms used by the JSON-RPC client.
@@ -27,11 +28,10 @@ pub:
retry int
}
// Client implements a JSON-RPC 2.0 client that can send requests and process responses.
// It uses a pluggable transport layer that implements the IRPCTransportClient interface.
pub struct Client {
mut:
pub mut:
// The transport implementation used to send requests and receive responses
transport IRPCTransportClient
}
@@ -44,13 +44,12 @@ mut:
// Returns:
// - A pointer to a new Client instance
pub fn new_client(transport IRPCTransportClient) &Client {
mut cl:=Client{
mut cl := Client{
transport: transport
}
return &cl
}
// send sends a JSON-RPC request with parameters of type T and expects a response with result of type D.
// This method handles the full request-response cycle including validation and error handling.
//
@@ -69,7 +68,6 @@ pub fn (mut c Client) send[T, D](request RequestGeneric[T], params SendParams) !
console.print_debug('Sending request: ${request.encode()}')
response_json := c.transport.send(request.encode(), params)!
// Decode the response JSON into a strongly-typed response object
response := decode_response_generic[D](response_json) or {
return error('Unable to decode response.\n- Response: ${response_json}\n- Error: ${err}')

View File

@@ -61,7 +61,7 @@ pub mut:
message string
// Additional information about the error (optional)
data string
data ?string
}
// new_error creates a new error response for a given request ID.