diff --git a/examples/clients/mycelium_rpc.vsh b/examples/clients/mycelium_rpc.vsh new file mode 100755 index 00000000..7b4e3f31 --- /dev/null +++ b/examples/clients/mycelium_rpc.vsh @@ -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.') +} diff --git a/lib/clients/mycelium_rpc/.heroscript b/lib/clients/mycelium_rpc/.heroscript new file mode 100644 index 00000000..3a475016 --- /dev/null +++ b/lib/clients/mycelium_rpc/.heroscript @@ -0,0 +1,8 @@ + +!!hero_code.generate_client + name:'mycelium_rpc' + classname:'MyceliumRPC' + singleton:1 + default:0 + hasconfig:1 + reset:0 \ No newline at end of file diff --git a/lib/clients/mycelium_rpc/mycelium_rpc.v b/lib/clients/mycelium_rpc/mycelium_rpc.v new file mode 100644 index 00000000..c9ca4822 --- /dev/null +++ b/lib/clients/mycelium_rpc/mycelium_rpc.v @@ -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)! +} diff --git a/lib/clients/mycelium_rpc/mycelium_rpc_factory_.v b/lib/clients/mycelium_rpc/mycelium_rpc_factory_.v new file mode 100644 index 00000000..91980310 --- /dev/null +++ b/lib/clients/mycelium_rpc/mycelium_rpc_factory_.v @@ -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' +} diff --git a/lib/clients/mycelium_rpc/mycelium_rpc_model.v b/lib/clients/mycelium_rpc/mycelium_rpc_model.v new file mode 100644 index 00000000..c24a8c0e --- /dev/null +++ b/lib/clients/mycelium_rpc/mycelium_rpc_model.v @@ -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 +} diff --git a/lib/clients/mycelium_rpc/openrpc.json b/lib/clients/mycelium_rpc/openrpc.json new file mode 100644 index 00000000..92369f30 --- /dev/null +++ b/lib/clients/mycelium_rpc/openrpc.json @@ -0,0 +1,1190 @@ +{ + "openrpc": "1.2.6", + "info": { + "version": "1.0.0", + "title": "Mycelium JSON-RPC API", + "description": "This is the specification of the Mycelium JSON-RPC API. It is used to perform admin tasks on the system, and to perform administrative duties.", + "contact": { + "url": "https://github.com/threefoldtech/mycelium" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/threefoldtech/mycelium/blob/master/LICENSE" + } + }, + "servers": [ + { + "url": "http://localhost:8990", + "name": "Mycelium JSON-RPC API" + } + ], + "methods": [ + { + "name": "getInfo", + "summary": "Get general info about the node", + "description": "Get general info about the node, which is not related to other more specific functionality", + "tags": [ + { + "name": "Admin" + } + ], + "params": [], + "result": { + "name": "info", + "description": "General information about the node", + "schema": { + "$ref": "#/components/schemas/Info" + } + } + }, + { + "name": "getPeers", + "summary": "List known peers", + "description": "List all peers known in the system, and info about their connection. This includes the endpoint, how we know about the peer, the connection state, and if the connection is alive the amount of bytes we've sent to and received from the peer.", + "tags": [ + { + "name": "Admin" + }, + { + "name": "Peer" + } + ], + "params": [], + "result": { + "name": "peers", + "description": "List of peers", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PeerStats" + } + } + } + }, + { + "name": "addPeer", + "summary": "Add a new peer", + "description": "Add a new peer identified by the provided endpoint. The peer is added to the list of known peers. It will eventually be connected to by the standard connection loop of the peer manager. This means that a peer which can't be connected to will stay in the system, as it might be reachable later on.", + "tags": [ + { + "name": "Admin" + }, + { + "name": "Peer" + } + ], + "params": [ + { + "name": "endpoint", + "description": "The endpoint of the peer to add", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "success", + "description": "Whether the peer was added successfully", + "schema": { + "type": "boolean" + } + }, + "errors": [ + { + "code": 400, + "message": "Malformed endpoint" + }, + { + "code": 409, + "message": "Peer already exists" + } + ] + }, + { + "name": "deletePeer", + "summary": "Remove an existing peer", + "description": "Remove an existing peer identified by the provided endpoint. The peer is removed from the list of known peers. If a connection to it is currently active, it will be closed.", + "tags": [ + { + "name": "Admin" + }, + { + "name": "Peer" + } + ], + "params": [ + { + "name": "endpoint", + "description": "The endpoint of the peer to remove", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "success", + "description": "Whether the peer was removed successfully", + "schema": { + "type": "boolean" + } + }, + "errors": [ + { + "code": 400, + "message": "Malformed endpoint" + }, + { + "code": 404, + "message": "Peer doesn't exist" + } + ] + }, + { + "name": "getSelectedRoutes", + "summary": "List all selected routes", + "description": "List all selected routes in the system, and their next hop identifier, metric and sequence number. It is possible for a route to be selected and have an infinite metric. This route will however not forward packets.", + "tags": [ + { + "name": "Admin" + }, + { + "name": "Route" + } + ], + "params": [], + "result": { + "name": "routes", + "description": "List of selected routes", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Route" + } + } + } + }, + { + "name": "getFallbackRoutes", + "summary": "List all active fallback routes", + "description": "List all fallback routes in the system, and their next hop identifier, metric and sequence number. These routes are available to be selected in case the selected route for a destination suddenly fails, or gets retracted.", + "tags": [ + { + "name": "Admin" + }, + { + "name": "Route" + } + ], + "params": [], + "result": { + "name": "routes", + "description": "List of fallback routes", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Route" + } + } + } + }, + { + "name": "getQueriedSubnets", + "summary": "List all currently queried subnets", + "description": "List all currently queried subnets in the system, and the amount of seconds until the query expires. These subnets are actively being probed in the network. If no route to them is discovered before the query expires, they will be marked as not reachable temporarily.", + "tags": [ + { + "name": "Admin" + }, + { + "name": "Route" + } + ], + "params": [], + "result": { + "name": "subnets", + "description": "List of queried subnets", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueriedSubnet" + } + } + } + }, + { + "name": "getNoRouteEntries", + "summary": "List all subnets which are explicitly marked as no route", + "description": "List all subnets in the system which are marked no route, and the amount of seconds until the query expires. These subnets have recently been probed in the network, and no route for them was discovered in time. No more route requests will be send for these subnets until the entry expires.", + "tags": [ + { + "name": "Admin" + }, + { + "name": "Route" + } + ], + "params": [], + "result": { + "name": "subnets", + "description": "List of no route subnets", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NoRouteSubnet" + } + } + } + }, + { + "name": "popMessage", + "summary": "Get a message from the inbound message queue", + "description": "Get a message from the inbound message queue. By default, the message is removed from the queue and won't be shown again. If the peek parameter is set to true, the message will be peeked, and the next call to this method will show the same message. This method returns immediately by default: a message is returned if one is ready, and if there isn't nothing is returned. If the timeout parameter is set, this call won't return for the given amount of seconds, unless a message is received.", + "tags": [ + { + "name": "Message" + } + ], + "params": [ + { + "name": "peek", + "description": "Whether to peek the message or not. If this is true, the message won't be removed from the inbound queue when it is read", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "timeout", + "description": "Amount of seconds to wait for a message to arrive if one is not available. Setting this to 0 is valid and will return a message if present, or return immediately if there isn't", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "name": "topic", + "description": "Optional filter for loading messages. If set, the system checks if the message has the given string at the start. This way a topic can be encoded.", + "required": false, + "schema": { + "type": "string", + "format": "byte", + "minLength": 0, + "maxLength": 340 + } + } + ], + "result": { + "name": "message", + "description": "The retrieved message", + "schema": { + "$ref": "#/components/schemas/InboundMessage" + } + }, + "errors": [ + { + "code": 204, + "message": "No message ready" + } + ] + }, + { + "name": "pushMessage", + "summary": "Submit a new message to the system", + "description": "Push a new message to the systems outbound message queue. The system will continuously attempt to send the message until it is either fully transmitted, or the send deadline is expired.", + "tags": [ + { + "name": "Message" + } + ], + "params": [ + { + "name": "message", + "description": "The message to send", + "required": true, + "schema": { + "$ref": "#/components/schemas/PushMessageBody" + } + }, + { + "name": "reply_timeout", + "description": "Amount of seconds to wait for a reply to this message to come in. If not set, the system won't wait for a reply and return the ID of the message, which can be used later. If set, the system will wait for at most the given amount of seconds for a reply to come in. If a reply arrives, it is returned to the client. If not, the message ID is returned for later use.", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + } + ], + "result": { + "name": "response", + "description": "The response to the message push", + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InboundMessage" + }, + { + "$ref": "#/components/schemas/PushMessageResponseId" + } + ] + } + }, + "errors": [ + { + "code": 408, + "message": "The system timed out waiting for a reply to the message" + } + ] + }, + { + "name": "pushMessageReply", + "summary": "Reply to a message with the given ID", + "description": "Submits a reply message to the system, where ID is an id of a previously received message. If the sender is waiting for a reply, it will bypass the queue of open messages.", + "tags": [ + { + "name": "Message" + } + ], + "params": [ + { + "name": "id", + "description": "The ID of the message to reply to", + "required": true, + "schema": { + "type": "string", + "format": "hex", + "minLength": 16, + "maxLength": 16 + } + }, + { + "name": "message", + "description": "The reply message", + "required": true, + "schema": { + "$ref": "#/components/schemas/PushMessageBody" + } + } + ], + "result": { + "name": "success", + "description": "Whether the reply was submitted successfully", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "getMessageInfo", + "summary": "Get the status of an outbound message", + "description": "Get information about the current state of an outbound message. This can be used to check the transmission state, size and destination of the message.", + "tags": [ + { + "name": "Message" + } + ], + "params": [ + { + "name": "id", + "description": "The ID of the message to get info for", + "required": true, + "schema": { + "type": "string", + "format": "hex", + "minLength": 16, + "maxLength": 16 + } + } + ], + "result": { + "name": "info", + "description": "Information about the message", + "schema": { + "$ref": "#/components/schemas/MessageStatusResponse" + } + }, + "errors": [ + { + "code": 404, + "message": "Message not found" + } + ] + }, + { + "name": "getPublicKeyFromIp", + "summary": "Get the pubkey from node ip", + "description": "Get the node's public key from it's IP address.", + "tags": [ + { + "name": "Admin" + } + ], + "params": [ + { + "name": "mycelium_ip", + "description": "The IP address to get the public key for", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + } + ], + "result": { + "name": "pubkey", + "description": "The public key of the node", + "schema": { + "$ref": "#/components/schemas/PublicKeyResponse" + } + }, + "errors": [ + { + "code": 404, + "message": "Public key not found" + } + ] + }, + { + "name": "getDefaultTopicAction", + "summary": "Get the default topic action", + "description": "Get the default action for topics that are not explicitly configured (accept or reject).", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [], + "result": { + "name": "accept", + "description": "Whether unconfigured topics are accepted by default", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "setDefaultTopicAction", + "summary": "Set the default topic action", + "description": "Set the default action for topics that are not explicitly configured (accept or reject).", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "accept", + "description": "Whether to accept unconfigured topics by default", + "required": true, + "schema": { + "type": "boolean" + } + } + ], + "result": { + "name": "success", + "description": "Whether the default topic action was set successfully", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "getTopics", + "summary": "Get all configured topics", + "description": "Get all topics that are explicitly configured in the system.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [], + "result": { + "name": "topics", + "description": "List of configured topics", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte", + "description": "Base64 encoded topic identifier" + } + } + } + }, + { + "name": "addTopic", + "summary": "Add a new topic", + "description": "Add a new topic to the system's whitelist.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "topic", + "description": "The topic to add", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "success", + "description": "Whether the topic was added successfully", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "removeTopic", + "summary": "Remove a topic", + "description": "Remove a topic from the system's whitelist.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "topic", + "description": "The topic to remove", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "success", + "description": "Whether the topic was removed successfully", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "getTopicSources", + "summary": "Get sources for a topic", + "description": "Get all sources (subnets) that are allowed to send messages for a specific topic.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "topic", + "description": "The topic to get sources for", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "sources", + "description": "List of sources (subnets) for the topic", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + { + "name": "addTopicSource", + "summary": "Add a source to a topic", + "description": "Add a source (subnet) that is allowed to send messages for a specific topic.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "topic", + "description": "The topic to add a source to", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "subnet", + "description": "The subnet to add as a source", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "success", + "description": "Whether the source was added successfully", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "removeTopicSource", + "summary": "Remove a source from a topic", + "description": "Remove a source (subnet) that is allowed to send messages for a specific topic.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "topic", + "description": "The topic to remove a source from", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "subnet", + "description": "The subnet to remove as a source", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "success", + "description": "Whether the source was removed successfully", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "getTopicForwardSocket", + "summary": "Get the forward socket for a topic", + "description": "Get the socket path where messages for a specific topic are forwarded to.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "topic", + "description": "The topic to get the forward socket for", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "socket_path", + "description": "The socket path where messages are forwarded to", + "schema": { + "type": ["string", "null"] + } + } + }, + { + "name": "setTopicForwardSocket", + "summary": "Set the forward socket for a topic", + "description": "Set the socket path where messages for a specific topic should be forwarded to.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "topic", + "description": "The topic to set the forward socket for", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "socket_path", + "description": "The socket path where messages should be forwarded to", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "success", + "description": "Whether the forward socket was set successfully", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "removeTopicForwardSocket", + "summary": "Remove the forward socket for a topic", + "description": "Remove the socket path where messages for a specific topic are forwarded to.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "topic", + "description": "The topic to remove the forward socket for", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "success", + "description": "Whether the forward socket was removed successfully", + "schema": { + "type": "boolean" + } + } + } + ], + "components": { + "schemas": { + "Info": { + "description": "General information about a node", + "type": "object", + "properties": { + "nodeSubnet": { + "description": "The subnet owned by the node and advertised to peers", + "type": "string", + "example": "54f:b680:ba6e:7ced::/64" + }, + "nodePubkey": { + "description": "The public key of the node", + "type": "string", + "format": "hex", + "minLength": 64, + "maxLength": 64, + "example": "02468ace13579bdf02468ace13579bdf02468ace13579bdf02468ace13579bdf" + } + } + }, + "Endpoint": { + "description": "Identification to connect to a peer", + "type": "object", + "properties": { + "proto": { + "description": "Protocol used", + "type": "string", + "enum": [ + "tcp", + "quic" + ], + "example": "tcp" + }, + "socketAddr": { + "description": "The socket address used", + "type": "string", + "example": "192.0.2.6:9651" + } + } + }, + "TopicInfo": { + "description": "Information about a configured topic", + "type": "object", + "properties": { + "topic": { + "description": "The topic identifier", + "type": "string", + "format": "byte", + "example": "example.topic" + }, + "sources": { + "description": "List of subnets that are allowed to send messages for this topic", + "type": "array", + "items": { + "type": "string", + "example": "503:5478:df06:d79a::/64" + } + }, + "forward_socket": { + "description": "Optional socket path where messages for this topic are forwarded to", + "type": ["string", "null"], + "example": "/var/run/mycelium/topic_socket" + } + } + }, + "PeerStats": { + "description": "Info about a peer", + "type": "object", + "properties": { + "endpoint": { + "$ref": "#/components/schemas/Endpoint" + }, + "type": { + "description": "How we know about this peer", + "type": "string", + "enum": [ + "static", + "inbound", + "linkLocalDiscovery" + ], + "example": "static" + }, + "connectionState": { + "description": "The current state of the connection to the peer", + "type": "string", + "enum": [ + "alive", + "connecting", + "dead" + ], + "example": "alive" + }, + "txBytes": { + "description": "The amount of bytes transmitted to this peer", + "type": "integer", + "format": "int64", + "minimum": 0, + "example": 464531564 + }, + "rxBytes": { + "description": "The amount of bytes received from this peer", + "type": "integer", + "format": "int64", + "minimum": 0, + "example": 64645089 + } + } + }, + "Route": { + "description": "Information about a route", + "type": "object", + "properties": { + "subnet": { + "description": "The overlay subnet for which this is the route", + "type": "string", + "example": "469:1348:ab0c:a1d8::/64" + }, + "nextHop": { + "description": "A way to identify the next hop of the route, where forwarded packets will be sent", + "type": "string", + "example": "TCP 203.0.113.2:60128 <-> 198.51.100.27:9651" + }, + "metric": { + "description": "The metric of the route, an estimation of how long the packet will take to arrive at its final destination", + "oneOf": [ + { + "description": "A finite metric value", + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 65534, + "example": 13 + }, + { + "description": "An infinite (unreachable) metric. This is always `infinite`", + "type": "string", + "example": "infinite" + } + ] + }, + "seqno": { + "description": "the sequence number advertised with this route by the source", + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 65535, + "example": 1 + } + } + }, + "QueriedSubnet": { + "description": "Information about a subnet currently being queried", + "type": "object", + "properties": { + "subnet": { + "description": "The overlay subnet which we are currently querying", + "type": "string", + "example": "503:5478:df06:d79a::/64" + }, + "expiration": { + "description": "The amount of seconds until the query expires", + "type": "string", + "example": "37" + } + } + }, + "NoRouteSubnet": { + "description": "Information about a subnet which is marked as no route", + "type": "object", + "properties": { + "subnet": { + "description": "The overlay subnet which is marked", + "type": "string", + "example": "503:5478:df06:d79a::/64" + }, + "expiration": { + "description": "The amount of seconds until the entry expires", + "type": "string", + "example": "37" + } + } + }, + "InboundMessage": { + "description": "A message received by the system", + "type": "object", + "properties": { + "id": { + "description": "Id of the message, hex encoded", + "type": "string", + "format": "hex", + "minLength": 16, + "maxLength": 16, + "example": "0123456789abcdef" + }, + "srcIp": { + "description": "Sender overlay IP address", + "type": "string", + "format": "ipv6", + "example": "449:abcd:0123:defa::1" + }, + "srcPk": { + "description": "Sender public key, hex encoded", + "type": "string", + "format": "hex", + "minLength": 64, + "maxLength": 64, + "example": "fedbca9876543210fedbca9876543210fedbca9876543210fedbca9876543210" + }, + "dstIp": { + "description": "Receiver overlay IP address", + "type": "string", + "format": "ipv6", + "example": "34f:b680:ba6e:7ced:355f:346f:d97b:eecb" + }, + "dstPk": { + "description": "Receiver public key, hex encoded. This is the public key of the system", + "type": "string", + "format": "hex", + "minLength": 64, + "maxLength": 64, + "example": "02468ace13579bdf02468ace13579bdf02468ace13579bdf02468ace13579bdf" + }, + "topic": { + "description": "An optional message topic", + "type": "string", + "format": "byte", + "minLength": 0, + "maxLength": 340, + "example": "hpV+" + }, + "payload": { + "description": "The message payload, encoded in standard alphabet base64", + "type": "string", + "format": "byte", + "example": "xuV+" + } + } + }, + "PushMessageBody": { + "description": "A message to send to a given receiver", + "type": "object", + "properties": { + "dst": { + "$ref": "#/components/schemas/MessageDestination" + }, + "topic": { + "description": "An optional message topic", + "type": "string", + "format": "byte", + "minLength": 0, + "maxLength": 340, + "example": "hpV+" + }, + "payload": { + "description": "The message to send, base64 encoded", + "type": "string", + "format": "byte", + "example": "xuV+" + } + } + }, + "MessageDestination": { + "oneOf": [ + { + "description": "An IP in the subnet of the receiver node", + "type": "object", + "properties": { + "ip": { + "description": "The target IP of the message", + "type": "string", + "format": "ipv6", + "example": "449:abcd:0123:defa::1" + } + } + }, + { + "description": "The hex encoded public key of the receiver node", + "type": "object", + "properties": { + "pk": { + "description": "The hex encoded public key of the target node", + "type": "string", + "minLength": 64, + "maxLength": 64, + "example": "bb39b4a3a4efd70f3e05e37887677e02efbda14681d0acd3882bc0f754792c32" + } + } + } + ] + }, + "PushMessageResponseId": { + "description": "The ID generated for a message after pushing it to the system", + "type": "object", + "properties": { + "id": { + "description": "Id of the message, hex encoded", + "type": "string", + "format": "hex", + "minLength": 16, + "maxLength": 16, + "example": "0123456789abcdef" + } + } + }, + "MessageStatusResponse": { + "description": "Information about an outbound message", + "type": "object", + "properties": { + "dst": { + "description": "IP address of the receiving node", + "type": "string", + "format": "ipv6", + "example": "449:abcd:0123:defa::1" + }, + "state": { + "$ref": "#/components/schemas/TransmissionState" + }, + "created": { + "description": "Unix timestamp of when this message was created", + "type": "integer", + "format": "int64", + "example": 1649512789 + }, + "deadline": { + "description": "Unix timestamp of when this message will expire. If the message is not received before this, the system will give up", + "type": "integer", + "format": "int64", + "example": 1649513089 + }, + "msgLen": { + "description": "Length of the message in bytes", + "type": "integer", + "minimum": 0, + "example": 27 + } + } + }, + "TransmissionState": { + "description": "The state of an outbound message in it's lifetime", + "oneOf": [ + { + "type": "string", + "enum": [ + "pending", + "received", + "read", + "aborted" + ], + "example": "received" + }, + { + "type": "object", + "properties": { + "sending": { + "type": "object", + "properties": { + "pending": { + "type": "integer", + "minimum": 0, + "example": 5 + }, + "sent": { + "type": "integer", + "minimum": 0, + "example": 17 + }, + "acked": { + "type": "integer", + "minimum": 0, + "example": 3 + } + } + } + } + } + ] + }, + "PublicKeyResponse": { + "description": "Public key requested based on a node's IP", + "type": "object", + "properties": { + "NodePubKey": { + "type": "string", + "format": "hex", + "minLength": 64, + "maxLength": 64, + "example": "02468ace13579bdf02468ace13579bdf02468ace13579bdf02468ace13579bdf" + } + } + } + } + } +} \ No newline at end of file diff --git a/lib/clients/mycelium_rpc/readme.md b/lib/clients/mycelium_rpc/readme.md new file mode 100644 index 00000000..c117f654 --- /dev/null +++ b/lib/clients/mycelium_rpc/readme.md @@ -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. + + diff --git a/lib/installers/net/mycelium_installer/mycelium_installer_actions.v b/lib/installers/net/mycelium_installer/mycelium_installer_actions.v index 91cf7f82..999f11ca 100644 --- a/lib/installers/net/mycelium_installer/mycelium_installer_actions.v +++ b/lib/installers/net/mycelium_installer/mycelium_installer_actions.v @@ -21,9 +21,10 @@ fn startupcmd() ![]zinit.ZProcessNewArgs { mut tun_name := 'tun${installer.tun_nr}' res << zinit.ZProcessNewArgs{ - name: 'mycelium' - cmd: 'mycelium --key-file ${osal.hero_path()!}/cfg/priv_key.bin --peers ${peers_str} --tun-name ${tun_name}' - env: { + 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' diff --git a/lib/installers/net/mycelium_installer/mycelium_installer_model.v b/lib/installers/net/mycelium_installer/mycelium_installer_model.v index d0420667..5dcfd8c6 100644 --- a/lib/installers/net/mycelium_installer/mycelium_installer_model.v +++ b/lib/installers/net/mycelium_installer/mycelium_installer_model.v @@ -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 } diff --git a/lib/schemas/jsonrpc/model_error.v b/lib/schemas/jsonrpc/model_error.v index cb586418..20dedf9b 100644 --- a/lib/schemas/jsonrpc/model_error.v +++ b/lib/schemas/jsonrpc/model_error.v @@ -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.