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

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.