implement unix and ws using jsonrpsee
This commit is contained in:
		
							
								
								
									
										42
									
								
								interfaces/openrpc/client/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								interfaces/openrpc/client/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "hero-openrpc-client"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
[[bin]]
 | 
			
		||||
name = "hero-openrpc-client"
 | 
			
		||||
path = "cmd/main.rs"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
# Core dependencies
 | 
			
		||||
tokio = { version = "1.0", features = ["full"] }
 | 
			
		||||
serde = { version = "1.0", features = ["derive"] }
 | 
			
		||||
serde_json = "1.0"
 | 
			
		||||
anyhow = "1.0"
 | 
			
		||||
thiserror = "1.0"
 | 
			
		||||
tracing = "0.1"
 | 
			
		||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
 | 
			
		||||
clap = { version = "4.0", features = ["derive"] }
 | 
			
		||||
 | 
			
		||||
# JSON-RPC dependencies
 | 
			
		||||
jsonrpsee = { version = "0.21", features = [
 | 
			
		||||
    "client",
 | 
			
		||||
    "macros"
 | 
			
		||||
] }
 | 
			
		||||
async-trait = "0.1"
 | 
			
		||||
 | 
			
		||||
# Hero dependencies
 | 
			
		||||
hero_job = { path = "../../../core/job" }
 | 
			
		||||
 | 
			
		||||
# Authentication and crypto
 | 
			
		||||
secp256k1 = { version = "0.28", features = ["rand", "recovery"] }
 | 
			
		||||
hex = "0.4"
 | 
			
		||||
sha2 = "0.10"
 | 
			
		||||
rand = "0.8"
 | 
			
		||||
 | 
			
		||||
# CLI utilities
 | 
			
		||||
dialoguer = "0.11"
 | 
			
		||||
colored = "2.0"
 | 
			
		||||
 | 
			
		||||
# Async utilities
 | 
			
		||||
futures = "0.3"
 | 
			
		||||
							
								
								
									
										472
									
								
								interfaces/openrpc/client/cmd/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										472
									
								
								interfaces/openrpc/client/cmd/main.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,472 @@
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use clap::{Parser, Subcommand};
 | 
			
		||||
use colored::*;
 | 
			
		||||
use dialoguer::{Input, Select, Confirm, MultiSelect};
 | 
			
		||||
use hero_job::ScriptType;
 | 
			
		||||
use hero_openrpc_client::{
 | 
			
		||||
    AuthHelper, ClientTransport, HeroOpenRpcClient, JobParams,
 | 
			
		||||
};
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
use tracing::{error, info, Level};
 | 
			
		||||
use tracing_subscriber;
 | 
			
		||||
 | 
			
		||||
#[derive(Parser)]
 | 
			
		||||
#[command(name = "hero-openrpc-client")]
 | 
			
		||||
#[command(about = "Hero OpenRPC Client - Interactive JSON-RPC client")]
 | 
			
		||||
struct Cli {
 | 
			
		||||
    #[command(subcommand)]
 | 
			
		||||
    command: Commands,
 | 
			
		||||
 | 
			
		||||
    /// Private key for authentication (hex format)
 | 
			
		||||
    #[arg(long)]
 | 
			
		||||
    private_key: Option<String>,
 | 
			
		||||
 | 
			
		||||
    /// Generate a new private key and exit
 | 
			
		||||
    #[arg(long)]
 | 
			
		||||
    generate_key: bool,
 | 
			
		||||
 | 
			
		||||
    /// Log level
 | 
			
		||||
    #[arg(long, default_value = "info")]
 | 
			
		||||
    log_level: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Subcommand)]
 | 
			
		||||
enum Commands {
 | 
			
		||||
    /// Connect to WebSocket server
 | 
			
		||||
    Websocket {
 | 
			
		||||
        /// Server URL
 | 
			
		||||
        #[arg(long, default_value = "ws://127.0.0.1:9944")]
 | 
			
		||||
        url: String,
 | 
			
		||||
    },
 | 
			
		||||
    /// Connect to Unix socket server
 | 
			
		||||
    Unix {
 | 
			
		||||
        /// Unix socket path
 | 
			
		||||
        #[arg(long, default_value = "/tmp/hero-openrpc.sock")]
 | 
			
		||||
        socket_path: PathBuf,
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Available RPC methods with descriptions
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
struct RpcMethod {
 | 
			
		||||
    name: &'static str,
 | 
			
		||||
    description: &'static str,
 | 
			
		||||
    requires_auth: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const RPC_METHODS: &[RpcMethod] = &[
 | 
			
		||||
    RpcMethod {
 | 
			
		||||
        name: "fetch_nonce",
 | 
			
		||||
        description: "Fetch a nonce for authentication",
 | 
			
		||||
        requires_auth: false,
 | 
			
		||||
    },
 | 
			
		||||
    RpcMethod {
 | 
			
		||||
        name: "authenticate",
 | 
			
		||||
        description: "Authenticate with public key and signature",
 | 
			
		||||
        requires_auth: false,
 | 
			
		||||
    },
 | 
			
		||||
    RpcMethod {
 | 
			
		||||
        name: "whoami",
 | 
			
		||||
        description: "Get authentication status and user information",
 | 
			
		||||
        requires_auth: true,
 | 
			
		||||
    },
 | 
			
		||||
    RpcMethod {
 | 
			
		||||
        name: "play",
 | 
			
		||||
        description: "Execute a Rhai script immediately",
 | 
			
		||||
        requires_auth: true,
 | 
			
		||||
    },
 | 
			
		||||
    RpcMethod {
 | 
			
		||||
        name: "create_job",
 | 
			
		||||
        description: "Create a new job without starting it",
 | 
			
		||||
        requires_auth: true,
 | 
			
		||||
    },
 | 
			
		||||
    RpcMethod {
 | 
			
		||||
        name: "start_job",
 | 
			
		||||
        description: "Start a previously created job",
 | 
			
		||||
        requires_auth: true,
 | 
			
		||||
    },
 | 
			
		||||
    RpcMethod {
 | 
			
		||||
        name: "run_job",
 | 
			
		||||
        description: "Create and run a job, returning result when complete",
 | 
			
		||||
        requires_auth: true,
 | 
			
		||||
    },
 | 
			
		||||
    RpcMethod {
 | 
			
		||||
        name: "get_job_status",
 | 
			
		||||
        description: "Get the current status of a job",
 | 
			
		||||
        requires_auth: true,
 | 
			
		||||
    },
 | 
			
		||||
    RpcMethod {
 | 
			
		||||
        name: "get_job_output",
 | 
			
		||||
        description: "Get the output of a completed job",
 | 
			
		||||
        requires_auth: true,
 | 
			
		||||
    },
 | 
			
		||||
    RpcMethod {
 | 
			
		||||
        name: "get_job_logs",
 | 
			
		||||
        description: "Get the logs of a job",
 | 
			
		||||
        requires_auth: true,
 | 
			
		||||
    },
 | 
			
		||||
    RpcMethod {
 | 
			
		||||
        name: "list_jobs",
 | 
			
		||||
        description: "List all jobs in the system",
 | 
			
		||||
        requires_auth: true,
 | 
			
		||||
    },
 | 
			
		||||
    RpcMethod {
 | 
			
		||||
        name: "stop_job",
 | 
			
		||||
        description: "Stop a running job",
 | 
			
		||||
        requires_auth: true,
 | 
			
		||||
    },
 | 
			
		||||
    RpcMethod {
 | 
			
		||||
        name: "delete_job",
 | 
			
		||||
        description: "Delete a job from the system",
 | 
			
		||||
        requires_auth: true,
 | 
			
		||||
    },
 | 
			
		||||
    RpcMethod {
 | 
			
		||||
        name: "clear_all_jobs",
 | 
			
		||||
        description: "Clear all jobs from the system",
 | 
			
		||||
        requires_auth: true,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
#[tokio::main]
 | 
			
		||||
async fn main() -> Result<()> {
 | 
			
		||||
    let cli = Cli::parse();
 | 
			
		||||
 | 
			
		||||
    // Initialize tracing
 | 
			
		||||
    let log_level = match cli.log_level.to_lowercase().as_str() {
 | 
			
		||||
        "trace" => Level::TRACE,
 | 
			
		||||
        "debug" => Level::DEBUG,
 | 
			
		||||
        "info" => Level::INFO,
 | 
			
		||||
        "warn" => Level::WARN,
 | 
			
		||||
        "error" => Level::ERROR,
 | 
			
		||||
        _ => Level::INFO,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    tracing_subscriber::fmt()
 | 
			
		||||
        .with_max_level(log_level)
 | 
			
		||||
        .init();
 | 
			
		||||
 | 
			
		||||
    // Handle key generation
 | 
			
		||||
    if cli.generate_key {
 | 
			
		||||
        let auth_helper = AuthHelper::generate()?;
 | 
			
		||||
        println!("{}", "Generated new private key:".green().bold());
 | 
			
		||||
        println!("Private Key: {}", auth_helper.private_key_hex().yellow());
 | 
			
		||||
        println!("Public Key:  {}", auth_helper.public_key_hex().cyan());
 | 
			
		||||
        println!();
 | 
			
		||||
        println!("{}", "Save the private key securely and use it with --private-key".bright_yellow());
 | 
			
		||||
        return Ok(());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let transport = match cli.command {
 | 
			
		||||
        Commands::Websocket { url } => {
 | 
			
		||||
            println!("{} {}", "Connecting to WebSocket server:".green(), url.cyan());
 | 
			
		||||
            ClientTransport::WebSocket(url)
 | 
			
		||||
        }
 | 
			
		||||
        Commands::Unix { socket_path } => {
 | 
			
		||||
            println!("{} {:?}", "Connecting to Unix socket server:".green(), socket_path);
 | 
			
		||||
            ClientTransport::Unix(socket_path)
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Connect to the server
 | 
			
		||||
    let client = HeroOpenRpcClient::connect(transport).await?;
 | 
			
		||||
    println!("{}", "Connected successfully!".green().bold());
 | 
			
		||||
 | 
			
		||||
    // Handle authentication if private key is provided
 | 
			
		||||
    let mut authenticated = false;
 | 
			
		||||
    if let Some(private_key) = cli.private_key {
 | 
			
		||||
        println!("{}", "Authenticating...".yellow());
 | 
			
		||||
        match client.authenticate_with_key(&private_key).await {
 | 
			
		||||
            Ok(true) => {
 | 
			
		||||
                println!("{}", "Authentication successful!".green().bold());
 | 
			
		||||
                authenticated = true;
 | 
			
		||||
            }
 | 
			
		||||
            Ok(false) => {
 | 
			
		||||
                println!("{}", "Authentication failed!".red().bold());
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                error!("Authentication error: {}", e);
 | 
			
		||||
                println!("{} {}", "Authentication error:".red().bold(), e);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        println!("{}", "No private key provided. Some methods will require authentication.".yellow());
 | 
			
		||||
        println!("{}", "Use --generate-key to create a new key or --private-key to use an existing one.".bright_yellow());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    println!();
 | 
			
		||||
 | 
			
		||||
    // Interactive loop
 | 
			
		||||
    loop {
 | 
			
		||||
        // Filter methods based on authentication status
 | 
			
		||||
        let available_methods: Vec<&RpcMethod> = RPC_METHODS
 | 
			
		||||
            .iter()
 | 
			
		||||
            .filter(|method| !method.requires_auth || authenticated)
 | 
			
		||||
            .collect();
 | 
			
		||||
 | 
			
		||||
        if available_methods.is_empty() {
 | 
			
		||||
            println!("{}", "No methods available. Please authenticate first.".red());
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Display method selection
 | 
			
		||||
        let method_names: Vec<String> = available_methods
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(|method| {
 | 
			
		||||
                if method.requires_auth && !authenticated {
 | 
			
		||||
                    format!("{} {} (requires auth)", method.name.red(), method.description)
 | 
			
		||||
                } else {
 | 
			
		||||
                    format!("{} {}", method.name.green(), method.description)
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .collect();
 | 
			
		||||
 | 
			
		||||
        let selection = Select::new()
 | 
			
		||||
            .with_prompt("Select an RPC method to call")
 | 
			
		||||
            .items(&method_names)
 | 
			
		||||
            .default(0)
 | 
			
		||||
            .interact_opt()?;
 | 
			
		||||
 | 
			
		||||
        let Some(selection) = selection else {
 | 
			
		||||
            println!("{}", "Goodbye!".cyan());
 | 
			
		||||
            break;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let selected_method = available_methods[selection];
 | 
			
		||||
        println!();
 | 
			
		||||
        println!("{} {}", "Selected method:".bold(), selected_method.name.green());
 | 
			
		||||
 | 
			
		||||
        // Handle method-specific parameter collection and execution
 | 
			
		||||
        match execute_method(&client, selected_method.name).await {
 | 
			
		||||
            Ok(_) => {}
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                error!("Method execution failed: {}", e);
 | 
			
		||||
                println!("{} {}", "Error:".red().bold(), e);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        println!();
 | 
			
		||||
 | 
			
		||||
        // Ask if user wants to continue
 | 
			
		||||
        if !Confirm::new()
 | 
			
		||||
            .with_prompt("Do you want to call another method?")
 | 
			
		||||
            .default(true)
 | 
			
		||||
            .interact()?
 | 
			
		||||
        {
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        println!();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    println!("{}", "Goodbye!".cyan().bold());
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn execute_method(client: &HeroOpenRpcClient, method_name: &str) -> Result<()> {
 | 
			
		||||
    match method_name {
 | 
			
		||||
        "fetch_nonce" => {
 | 
			
		||||
            let pubkey: String = Input::new()
 | 
			
		||||
                .with_prompt("Public key (hex)")
 | 
			
		||||
                .interact_text()?;
 | 
			
		||||
 | 
			
		||||
            let result = client.fetch_nonce(pubkey).await?;
 | 
			
		||||
            println!("{} {}", "Nonce:".green().bold(), result.yellow());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        "authenticate" => {
 | 
			
		||||
            let pubkey: String = Input::new()
 | 
			
		||||
                .with_prompt("Public key (hex)")
 | 
			
		||||
                .interact_text()?;
 | 
			
		||||
 | 
			
		||||
            let signature: String = Input::new()
 | 
			
		||||
                .with_prompt("Signature (hex)")
 | 
			
		||||
                .interact_text()?;
 | 
			
		||||
 | 
			
		||||
            let result = client.authenticate(pubkey, signature, nonce).await?;
 | 
			
		||||
            println!("{} {}", "Authentication result:".green().bold(), 
 | 
			
		||||
                if result { "Success".green() } else { "Failed".red() });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        "whoami" => {
 | 
			
		||||
            let result = client.whoami().await?;
 | 
			
		||||
            println!("{} {}", "User info:".green().bold(), 
 | 
			
		||||
                serde_json::to_string_pretty(&result)?.cyan());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        "play" => {
 | 
			
		||||
            let script: String = Input::new()
 | 
			
		||||
                .with_prompt("Rhai script to execute")
 | 
			
		||||
                .interact_text()?;
 | 
			
		||||
 | 
			
		||||
            let result = client.play(script).await?;
 | 
			
		||||
            println!("{} {}", "Script output:".green().bold(), result.output.cyan());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        "create_job" => {
 | 
			
		||||
            let script: String = Input::new()
 | 
			
		||||
                .with_prompt("Script content")
 | 
			
		||||
                .interact_text()?;
 | 
			
		||||
 | 
			
		||||
            let script_types = ["HeroScript", "RhaiSAL", "RhaiDSL"];
 | 
			
		||||
            let script_type_selection = Select::new()
 | 
			
		||||
                .with_prompt("Script type")
 | 
			
		||||
                .items(&script_types)
 | 
			
		||||
                .default(0)
 | 
			
		||||
                .interact()?;
 | 
			
		||||
 | 
			
		||||
            let script_type = match script_type_selection {
 | 
			
		||||
                0 => ScriptType::HeroScript,
 | 
			
		||||
                1 => ScriptType::RhaiSAL,
 | 
			
		||||
                2 => ScriptType::RhaiDSL,
 | 
			
		||||
                _ => ScriptType::HeroScript,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let add_prerequisites = Confirm::new()
 | 
			
		||||
                .with_prompt("Add prerequisites?")
 | 
			
		||||
                .default(false)
 | 
			
		||||
                .interact()?;
 | 
			
		||||
 | 
			
		||||
            let prerequisites = if add_prerequisites {
 | 
			
		||||
                let prereq_input: String = Input::new()
 | 
			
		||||
                    .with_prompt("Prerequisites (comma-separated job IDs)")
 | 
			
		||||
                    .interact_text()?;
 | 
			
		||||
                Some(prereq_input.split(',').map(|s| s.trim().to_string()).collect())
 | 
			
		||||
            } else {
 | 
			
		||||
                None
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let job_params = JobParams {
 | 
			
		||||
                script,
 | 
			
		||||
                script_type,
 | 
			
		||||
                prerequisites,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let result = client.create_job(job_params).await?;
 | 
			
		||||
            println!("{} {}", "Created job ID:".green().bold(), result.yellow());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        "start_job" => {
 | 
			
		||||
            let job_id: String = Input::new()
 | 
			
		||||
                .with_prompt("Job ID to start")
 | 
			
		||||
                .interact_text()?;
 | 
			
		||||
 | 
			
		||||
            let result = client.start_job(job_id).await?;
 | 
			
		||||
            println!("{} {}", "Start result:".green().bold(), 
 | 
			
		||||
                if result.success { "Success".green() } else { "Failed".red() });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        "run_job" => {
 | 
			
		||||
            let script: String = Input::new()
 | 
			
		||||
                .with_prompt("Script content")
 | 
			
		||||
                .interact_text()?;
 | 
			
		||||
 | 
			
		||||
            let script_types = ["HeroScript", "RhaiSAL", "RhaiDSL"];
 | 
			
		||||
            let script_type_selection = Select::new()
 | 
			
		||||
                .with_prompt("Script type")
 | 
			
		||||
                .items(&script_types)
 | 
			
		||||
                .default(0)
 | 
			
		||||
                .interact()?;
 | 
			
		||||
 | 
			
		||||
            let script_type = match script_type_selection {
 | 
			
		||||
                0 => ScriptType::HeroScript,
 | 
			
		||||
                1 => ScriptType::RhaiSAL,
 | 
			
		||||
                2 => ScriptType::RhaiDSL,
 | 
			
		||||
                _ => ScriptType::HeroScript,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let add_prerequisites = Confirm::new()
 | 
			
		||||
                .with_prompt("Add prerequisites?")
 | 
			
		||||
                .default(false)
 | 
			
		||||
                .interact()?;
 | 
			
		||||
 | 
			
		||||
            let prerequisites = if add_prerequisites {
 | 
			
		||||
                let prereq_input: String = Input::new()
 | 
			
		||||
                    .with_prompt("Prerequisites (comma-separated job IDs)")
 | 
			
		||||
                    .interact_text()?;
 | 
			
		||||
                Some(prereq_input.split(',').map(|s| s.trim().to_string()).collect())
 | 
			
		||||
            } else {
 | 
			
		||||
                None
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let result = client.run_job(script, script_type, prerequisites).await?;
 | 
			
		||||
            println!("{} {}", "Job result:".green().bold(), result.cyan());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        "get_job_status" => {
 | 
			
		||||
            let job_id: String = Input::new()
 | 
			
		||||
                .with_prompt("Job ID")
 | 
			
		||||
                .interact_text()?;
 | 
			
		||||
 | 
			
		||||
            let result = client.get_job_status(job_id).await?;
 | 
			
		||||
            println!("{} {:?}", "Job status:".green().bold(), result);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        "get_job_output" => {
 | 
			
		||||
            let job_id: String = Input::new()
 | 
			
		||||
                .with_prompt("Job ID")
 | 
			
		||||
                .interact_text()?;
 | 
			
		||||
 | 
			
		||||
            let result = client.get_job_output(job_id).await?;
 | 
			
		||||
            println!("{} {}", "Job output:".green().bold(), result.cyan());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        "get_job_logs" => {
 | 
			
		||||
            let job_id: String = Input::new()
 | 
			
		||||
                .with_prompt("Job ID")
 | 
			
		||||
                .interact_text()?;
 | 
			
		||||
 | 
			
		||||
            let result = client.get_job_logs(job_id).await?;
 | 
			
		||||
            println!("{} {}", "Job logs:".green().bold(), result.logs.cyan());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        "list_jobs" => {
 | 
			
		||||
            let result = client.list_jobs().await?;
 | 
			
		||||
            println!("{}", "Jobs:".green().bold());
 | 
			
		||||
            for job in result {
 | 
			
		||||
                println!("  {} - {} ({:?})", 
 | 
			
		||||
                    job.id().yellow(),
 | 
			
		||||
                    job.script_type(),
 | 
			
		||||
                    job.status()
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        "stop_job" => {
 | 
			
		||||
            let job_id: String = Input::new()
 | 
			
		||||
                .with_prompt("Job ID to stop")
 | 
			
		||||
                .interact_text()?;
 | 
			
		||||
 | 
			
		||||
            client.stop_job(job_id.clone()).await?;
 | 
			
		||||
            println!("{} {}", "Stopped job:".green().bold(), job_id.yellow());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        "delete_job" => {
 | 
			
		||||
            let job_id: String = Input::new()
 | 
			
		||||
                .with_prompt("Job ID to delete")
 | 
			
		||||
                .interact_text()?;
 | 
			
		||||
 | 
			
		||||
            client.delete_job(job_id.clone()).await?;
 | 
			
		||||
            println!("{} {}", "Deleted job:".green().bold(), job_id.yellow());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        "clear_all_jobs" => {
 | 
			
		||||
            let confirm = Confirm::new()
 | 
			
		||||
                .with_prompt("Are you sure you want to clear ALL jobs?")
 | 
			
		||||
                .default(false)
 | 
			
		||||
                .interact()?;
 | 
			
		||||
 | 
			
		||||
            if confirm {
 | 
			
		||||
                client.clear_all_jobs().await?;
 | 
			
		||||
                println!("{}", "Cleared all jobs".green().bold());
 | 
			
		||||
            } else {
 | 
			
		||||
                println!("{}", "Operation cancelled".yellow());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _ => {
 | 
			
		||||
            println!("{} {}", "Unknown method:".red().bold(), method_name);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										81
									
								
								interfaces/openrpc/client/src/auth.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								interfaces/openrpc/client/src/auth.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
use anyhow::{anyhow, Result};
 | 
			
		||||
use secp256k1::{Message, PublicKey, ecdsa::Signature, Secp256k1, SecretKey};
 | 
			
		||||
use sha2::{Digest, Sha256};
 | 
			
		||||
 | 
			
		||||
/// Helper for authentication operations
 | 
			
		||||
pub struct AuthHelper {
 | 
			
		||||
    secret_key: SecretKey,
 | 
			
		||||
    public_key: PublicKey,
 | 
			
		||||
    secp: Secp256k1<secp256k1::All>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AuthHelper {
 | 
			
		||||
    /// Create a new auth helper from a private key hex string
 | 
			
		||||
    pub fn new(private_key_hex: &str) -> Result<Self> {
 | 
			
		||||
        let secp = Secp256k1::new();
 | 
			
		||||
        
 | 
			
		||||
        let secret_key_bytes = hex::decode(private_key_hex)
 | 
			
		||||
            .map_err(|_| anyhow!("Invalid private key hex format"))?;
 | 
			
		||||
        
 | 
			
		||||
        let secret_key = SecretKey::from_slice(&secret_key_bytes)
 | 
			
		||||
            .map_err(|_| anyhow!("Invalid private key"))?;
 | 
			
		||||
        
 | 
			
		||||
        let public_key = PublicKey::from_secret_key(&secp, &secret_key);
 | 
			
		||||
        
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            secret_key,
 | 
			
		||||
            public_key,
 | 
			
		||||
            secp,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Generate a new random private key
 | 
			
		||||
    pub fn generate() -> Result<Self> {
 | 
			
		||||
        let secp = Secp256k1::new();
 | 
			
		||||
        let (secret_key, public_key) = secp.generate_keypair(&mut rand::thread_rng());
 | 
			
		||||
        
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            secret_key,
 | 
			
		||||
            public_key,
 | 
			
		||||
            secp,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the public key as a hex string
 | 
			
		||||
    pub fn public_key_hex(&self) -> String {
 | 
			
		||||
        hex::encode(self.public_key.serialize())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the private key as a hex string
 | 
			
		||||
    pub fn private_key_hex(&self) -> String {
 | 
			
		||||
        hex::encode(self.secret_key.secret_bytes())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Sign a message and return the signature as hex
 | 
			
		||||
    pub fn sign_message(&self, message: &str) -> Result<String> {
 | 
			
		||||
        let message_hash = Sha256::digest(message.as_bytes());
 | 
			
		||||
        let message = Message::from_slice(&message_hash)
 | 
			
		||||
            .map_err(|_| anyhow!("Failed to create message from hash"))?;
 | 
			
		||||
        
 | 
			
		||||
        let signature = self.secp.sign_ecdsa(&message, &self.secret_key);
 | 
			
		||||
        Ok(hex::encode(signature.serialize_compact()))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Verify a signature against a message
 | 
			
		||||
    pub fn verify_signature(&self, message: &str, signature_hex: &str) -> Result<bool> {
 | 
			
		||||
        let message_hash = Sha256::digest(message.as_bytes());
 | 
			
		||||
        let message = Message::from_slice(&message_hash)
 | 
			
		||||
            .map_err(|_| anyhow!("Failed to create message from hash"))?;
 | 
			
		||||
        
 | 
			
		||||
        let signature_bytes = hex::decode(signature_hex)
 | 
			
		||||
            .map_err(|_| anyhow!("Invalid signature hex format"))?;
 | 
			
		||||
        
 | 
			
		||||
        let signature = Signature::from_compact(&signature_bytes)
 | 
			
		||||
            .map_err(|_| anyhow!("Invalid signature format"))?;
 | 
			
		||||
        
 | 
			
		||||
        match self.secp.verify_ecdsa(&message, &signature, &self.public_key) {
 | 
			
		||||
            Ok(_) => Ok(true),
 | 
			
		||||
            Err(_) => Ok(false),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										212
									
								
								interfaces/openrpc/client/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								interfaces/openrpc/client/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,212 @@
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use async_trait::async_trait;
 | 
			
		||||
use hero_job::{Job, JobStatus, ScriptType};
 | 
			
		||||
use jsonrpsee::core::client::ClientT;
 | 
			
		||||
use jsonrpsee::core::ClientError;
 | 
			
		||||
use jsonrpsee::proc_macros::rpc;
 | 
			
		||||
use jsonrpsee::rpc_params;
 | 
			
		||||
use jsonrpsee::ws_client::{WsClient, WsClientBuilder};
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
use tracing::{error, info};
 | 
			
		||||
 | 
			
		||||
mod auth;
 | 
			
		||||
mod types;
 | 
			
		||||
 | 
			
		||||
pub use auth::*;
 | 
			
		||||
pub use types::*;
 | 
			
		||||
 | 
			
		||||
/// Transport configuration for the client
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
pub enum ClientTransport {
 | 
			
		||||
    WebSocket(String),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// OpenRPC client trait defining all available methods
 | 
			
		||||
#[rpc(client)]
 | 
			
		||||
pub trait OpenRpcClient {
 | 
			
		||||
    // Authentication methods
 | 
			
		||||
    #[method(name = "fetch_nonce")]
 | 
			
		||||
    async fn fetch_nonce(&self, pubkey: String) -> Result<String, ClientError>;
 | 
			
		||||
 | 
			
		||||
    #[method(name = "authenticate")]
 | 
			
		||||
    async fn authenticate(
 | 
			
		||||
        &self,
 | 
			
		||||
        pubkey: String,
 | 
			
		||||
        signature: String,
 | 
			
		||||
        nonce: String,
 | 
			
		||||
    ) -> Result<bool, ClientError>;
 | 
			
		||||
 | 
			
		||||
    #[method(name = "whoami")]
 | 
			
		||||
    async fn whoami(&self) -> Result<serde_json::Value, ClientError>;
 | 
			
		||||
 | 
			
		||||
    // Script execution
 | 
			
		||||
    #[method(name = "play")]
 | 
			
		||||
    async fn play(&self, script: String) -> Result<PlayResult, ClientError>;
 | 
			
		||||
 | 
			
		||||
    // Job management
 | 
			
		||||
    #[method(name = "create_job")]
 | 
			
		||||
    async fn create_job(&self, job: JobParams) -> Result<String, ClientError>;
 | 
			
		||||
 | 
			
		||||
    #[method(name = "start_job")]
 | 
			
		||||
    async fn start_job(&self, job_id: String) -> Result<StartJobResult, ClientError>;
 | 
			
		||||
 | 
			
		||||
    #[method(name = "run_job")]
 | 
			
		||||
    async fn run_job(
 | 
			
		||||
        &self,
 | 
			
		||||
        script: String,
 | 
			
		||||
        script_type: ScriptType,
 | 
			
		||||
        prerequisites: Option<Vec<String>>,
 | 
			
		||||
    ) -> Result<String, ClientError>;
 | 
			
		||||
 | 
			
		||||
    #[method(name = "get_job_status")]
 | 
			
		||||
    async fn get_job_status(&self, job_id: String) -> Result<JobStatus, ClientError>;
 | 
			
		||||
 | 
			
		||||
    #[method(name = "get_job_output")]
 | 
			
		||||
    async fn get_job_output(&self, job_id: String) -> Result<String, ClientError>;
 | 
			
		||||
 | 
			
		||||
    #[method(name = "get_job_logs")]
 | 
			
		||||
    async fn get_job_logs(&self, job_id: String) -> Result<JobLogsResult, ClientError>;
 | 
			
		||||
 | 
			
		||||
    #[method(name = "list_jobs")]
 | 
			
		||||
    async fn list_jobs(&self) -> Result<Vec<Job>, ClientError>;
 | 
			
		||||
 | 
			
		||||
    #[method(name = "stop_job")]
 | 
			
		||||
    async fn stop_job(&self, job_id: String) -> Result<(), ClientError>;
 | 
			
		||||
 | 
			
		||||
    #[method(name = "delete_job")]
 | 
			
		||||
    async fn delete_job(&self, job_id: String) -> Result<(), ClientError>;
 | 
			
		||||
 | 
			
		||||
    #[method(name = "clear_all_jobs")]
 | 
			
		||||
    async fn clear_all_jobs(&self) -> Result<(), ClientError>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Wrapper client that can use WebSocket transport
 | 
			
		||||
pub struct HeroOpenRpcClient {
 | 
			
		||||
    client: WsClient,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl HeroOpenRpcClient {
 | 
			
		||||
    /// Connect to the OpenRPC server using the specified transport
 | 
			
		||||
    pub async fn connect(transport: ClientTransport) -> Result<Self> {
 | 
			
		||||
        match transport {
 | 
			
		||||
            ClientTransport::WebSocket(url) => {
 | 
			
		||||
                info!("Connecting to WebSocket server at {}", url);
 | 
			
		||||
                let client = WsClientBuilder::default()
 | 
			
		||||
                    .build(&url)
 | 
			
		||||
                    .await?;
 | 
			
		||||
                Ok(Self { client })
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the underlying client for making RPC calls
 | 
			
		||||
    pub fn client(&self) -> &WsClient {
 | 
			
		||||
        &self.client
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Authenticate with the server using a private key
 | 
			
		||||
    pub async fn authenticate_with_key(&self, private_key: &str) -> Result<bool> {
 | 
			
		||||
        let auth_helper = AuthHelper::new(private_key)?;
 | 
			
		||||
        
 | 
			
		||||
        // Get nonce
 | 
			
		||||
        let pubkey = auth_helper.public_key_hex();
 | 
			
		||||
        let nonce: String = self.client.fetch_nonce(pubkey.clone()).await?;
 | 
			
		||||
        
 | 
			
		||||
        // Sign nonce
 | 
			
		||||
        let signature = auth_helper.sign_message(&nonce)?;
 | 
			
		||||
        
 | 
			
		||||
        // Authenticate  
 | 
			
		||||
        let result = self.client.authenticate(pubkey, signature, nonce).await?;
 | 
			
		||||
        
 | 
			
		||||
        if result {
 | 
			
		||||
            info!("Authentication successful");
 | 
			
		||||
        } else {
 | 
			
		||||
            error!("Authentication failed");
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Ok(result)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Implement delegation methods on HeroOpenRpcClient to use the generated trait methods
 | 
			
		||||
impl HeroOpenRpcClient {
 | 
			
		||||
    /// Delegate to fetch_nonce on the underlying client
 | 
			
		||||
    pub async fn fetch_nonce(&self, pubkey: String) -> Result<String, ClientError> {
 | 
			
		||||
        self.client.fetch_nonce(pubkey).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Delegate to authenticate on the underlying client
 | 
			
		||||
    pub async fn authenticate(
 | 
			
		||||
        &self,
 | 
			
		||||
        pubkey: String,
 | 
			
		||||
        signature: String,
 | 
			
		||||
        nonce: String,
 | 
			
		||||
    ) -> Result<bool, ClientError> {
 | 
			
		||||
        self.client.authenticate(pubkey, signature, nonce).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Delegate to whoami on the underlying client
 | 
			
		||||
    pub async fn whoami(&self) -> Result<serde_json::Value, ClientError> {
 | 
			
		||||
        self.client.whoami().await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Delegate to play on the underlying client
 | 
			
		||||
    pub async fn play(&self, script: String) -> Result<PlayResult, ClientError> {
 | 
			
		||||
        self.client.play(script).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Delegate to create_job on the underlying client
 | 
			
		||||
    pub async fn create_job(&self, job: JobParams) -> Result<String, ClientError> {
 | 
			
		||||
        self.client.create_job(job).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Delegate to start_job on the underlying client
 | 
			
		||||
    pub async fn start_job(&self, job_id: String) -> Result<StartJobResult, ClientError> {
 | 
			
		||||
        self.client.start_job(job_id).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Delegate to run_job on the underlying client
 | 
			
		||||
    pub async fn run_job(
 | 
			
		||||
        &self,
 | 
			
		||||
        script: String,
 | 
			
		||||
        script_type: ScriptType,
 | 
			
		||||
        prerequisites: Option<Vec<String>>,
 | 
			
		||||
    ) -> Result<String, ClientError> {
 | 
			
		||||
        self.client.run_job(script, script_type, prerequisites).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Delegate to get_job_status on the underlying client
 | 
			
		||||
    pub async fn get_job_status(&self, job_id: String) -> Result<JobStatus, ClientError> {
 | 
			
		||||
        self.client.get_job_status(job_id).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Delegate to get_job_output on the underlying client
 | 
			
		||||
    pub async fn get_job_output(&self, job_id: String) -> Result<String, ClientError> {
 | 
			
		||||
        self.client.get_job_output(job_id).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Delegate to get_job_logs on the underlying client
 | 
			
		||||
    pub async fn get_job_logs(&self, job_id: String) -> Result<JobLogsResult, ClientError> {
 | 
			
		||||
        self.client.get_job_logs(job_id).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Delegate to list_jobs on the underlying client
 | 
			
		||||
    pub async fn list_jobs(&self) -> Result<Vec<Job>, ClientError> {
 | 
			
		||||
        self.client.list_jobs().await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Delegate to stop_job on the underlying client
 | 
			
		||||
    pub async fn stop_job(&self, job_id: String) -> Result<(), ClientError> {
 | 
			
		||||
        self.client.stop_job(job_id).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Delegate to delete_job on the underlying client
 | 
			
		||||
    pub async fn delete_job(&self, job_id: String) -> Result<(), ClientError> {
 | 
			
		||||
        self.client.delete_job(job_id).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Delegate to clear_all_jobs on the underlying client
 | 
			
		||||
    pub async fn clear_all_jobs(&self) -> Result<(), ClientError> {
 | 
			
		||||
        self.client.clear_all_jobs().await
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								interfaces/openrpc/client/src/types.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								interfaces/openrpc/client/src/types.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
use hero_job::ScriptType;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
/// Parameters for creating a job
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
pub struct JobParams {
 | 
			
		||||
    pub script: String,
 | 
			
		||||
    pub script_type: ScriptType,
 | 
			
		||||
    pub prerequisites: Option<Vec<String>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Result of script execution
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
pub struct PlayResult {
 | 
			
		||||
    pub output: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Result of starting a job
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
pub struct StartJobResult {
 | 
			
		||||
    pub success: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Result of getting job logs
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
pub struct JobLogsResult {
 | 
			
		||||
    pub logs: String,
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user