//! RPC service. use std::net::SocketAddr; use std::sync::Arc; use jsonrpsee::server::{ServerBuilder, ServerHandle}; use jsonrpsee::RpcModule; use tokio::sync::RwLock; use tracing::{info, warn}; use synor_network::SyncState; use synor_types::{BlockHeader, block::BlockBody}; use crate::config::NodeConfig; use crate::services::{ ConsensusService, ContractService, MempoolService, NetworkService, StorageService, }; /// RPC service context for handlers. #[derive(Clone)] pub struct RpcContext { pub storage: Arc, pub network: Arc, pub consensus: Arc, pub mempool: Arc, pub contract: Arc, } /// RPC service manages the JSON-RPC server. pub struct RpcService { /// Storage reference. storage: Arc, /// Network reference. network: Arc, /// Consensus reference. consensus: Arc, /// Mempool reference. mempool: Arc, /// Contract service reference. contract: Arc, /// HTTP bind address. http_addr: String, /// WebSocket bind address. ws_addr: String, /// Enable HTTP. http_enabled: bool, /// Enable WebSocket. ws_enabled: bool, /// Is running. running: RwLock, /// HTTP server handle. http_handle: RwLock>, /// WebSocket server handle. ws_handle: RwLock>, } impl RpcService { /// Creates a new RPC service. pub fn new( storage: Arc, network: Arc, consensus: Arc, mempool: Arc, contract: Arc, config: &NodeConfig, ) -> anyhow::Result { Ok(RpcService { storage, network, consensus, mempool, contract, http_addr: config.rpc.http_addr.clone(), ws_addr: config.rpc.ws_addr.clone(), http_enabled: config.rpc.http_enabled, ws_enabled: config.rpc.ws_enabled, running: RwLock::new(false), http_handle: RwLock::new(None), ws_handle: RwLock::new(None), }) } /// Starts the RPC service. pub async fn start(&self) -> anyhow::Result<()> { info!("Starting RPC service"); // Create RPC context for handlers let context = RpcContext { storage: self.storage.clone(), network: self.network.clone(), consensus: self.consensus.clone(), mempool: self.mempool.clone(), contract: self.contract.clone(), }; // Build RPC module with all methods let module = self.build_module(context)?; // Start HTTP server if self.http_enabled { let http_addr: SocketAddr = self.http_addr.parse() .map_err(|e| anyhow::anyhow!("Invalid HTTP address: {}", e))?; info!(addr = %http_addr, "Starting HTTP RPC server"); let server = ServerBuilder::default() .build(http_addr) .await .map_err(|e| anyhow::anyhow!("Failed to start HTTP server: {}", e))?; let local_addr = server.local_addr() .map_err(|e| anyhow::anyhow!("Failed to get local address: {}", e))?; info!(addr = %local_addr, "HTTP RPC server started"); let handle = server.start(module.clone()); *self.http_handle.write().await = Some(handle); } // Start WebSocket server if self.ws_enabled { let ws_addr: SocketAddr = self.ws_addr.parse() .map_err(|e| anyhow::anyhow!("Invalid WebSocket address: {}", e))?; info!(addr = %ws_addr, "Starting WebSocket RPC server"); let server = ServerBuilder::default() .build(ws_addr) .await .map_err(|e| anyhow::anyhow!("Failed to start WebSocket server: {}", e))?; let local_addr = server.local_addr() .map_err(|e| anyhow::anyhow!("Failed to get local address: {}", e))?; info!(addr = %local_addr, "WebSocket RPC server started"); let handle = server.start(module); *self.ws_handle.write().await = Some(handle); } *self.running.write().await = true; Ok(()) } /// Stops the RPC service. pub async fn stop(&self) -> anyhow::Result<()> { info!("Stopping RPC service"); // Stop HTTP server if let Some(handle) = self.http_handle.write().await.take() { if let Err(e) = handle.stop() { warn!("Error stopping HTTP server: {:?}", e); } info!("HTTP RPC server stopped"); } // Stop WebSocket server if let Some(handle) = self.ws_handle.write().await.take() { if let Err(e) = handle.stop() { warn!("Error stopping WebSocket server: {:?}", e); } info!("WebSocket RPC server stopped"); } *self.running.write().await = false; Ok(()) } /// Builds the RPC module with all methods. fn build_module(&self, ctx: RpcContext) -> anyhow::Result> { let mut module = RpcModule::new(ctx); // Register base methods self.register_base_methods(&mut module)?; // Register block methods self.register_block_methods(&mut module)?; // Register transaction methods self.register_tx_methods(&mut module)?; // Register network methods self.register_network_methods(&mut module)?; // Register mining methods self.register_mining_methods(&mut module)?; // Register contract methods self.register_contract_methods(&mut module)?; Ok(module) } /// Registers base methods. fn register_base_methods(&self, module: &mut RpcModule) -> anyhow::Result<()> { // synor_getServerVersion module.register_method("synor_getServerVersion", |_, _| { serde_json::json!({ "version": env!("CARGO_PKG_VERSION"), "name": "synord" }) })?; // synor_echo - for testing module.register_method("synor_echo", |params, _| { let message: String = params.one().unwrap_or_default(); message })?; Ok(()) } /// Registers block-related methods. fn register_block_methods(&self, module: &mut RpcModule) -> anyhow::Result<()> { // synor_getBlockCount module.register_async_method("synor_getBlockCount", |_, ctx| async move { let count = ctx.consensus.current_height().await; serde_json::json!({"blockCount": count}) })?; // synor_getBlueScore module.register_async_method("synor_getBlueScore", |_, ctx| async move { let score = ctx.consensus.current_blue_score().await; serde_json::json!({"blueScore": score}) })?; // synor_getTips module.register_async_method("synor_getTips", |_, ctx| async move { let tips = ctx.consensus.tips().await; let tip_strings: Vec = tips.iter().map(|t| hex::encode(t)).collect(); serde_json::json!({"tips": tip_strings}) })?; // synor_getBlocksByBlueScore module.register_async_method("synor_getBlocksByBlueScore", |params, ctx| async move { let parsed: (u64, Option) = match params.parse() { Ok(p) => p, Err(_) => return serde_json::json!([]), }; let (blue_score, include_txs) = parsed; let include_txs = include_txs.unwrap_or(false); let block_hashes = ctx.consensus.get_blocks_by_blue_score(blue_score).await; let mut blocks = Vec::new(); for hash in block_hashes { if let Ok(Some(block_data)) = ctx.storage.get_block(&hash).await { // Deserialize header and body from raw bytes let header: BlockHeader = match borsh::from_slice(&block_data.header) { Ok(h) => h, Err(_) => continue, }; let body: BlockBody = match borsh::from_slice(&block_data.body) { Ok(b) => b, Err(_) => continue, }; let block_json = serde_json::json!({ "hash": hex::encode(&hash), "header": { "version": header.version, "parents": header.parents.iter().map(|p| hex::encode(p.as_bytes())).collect::>(), "hashMerkleRoot": hex::encode(header.merkle_root.as_bytes()), "utxoCommitment": hex::encode(header.utxo_commitment.as_bytes()), "timestamp": header.timestamp.as_millis(), "bits": header.bits, "nonce": header.nonce, "blueScore": blue_score }, "transactions": if include_txs { body.transactions.iter().map(|tx| { serde_json::json!({ "hash": hex::encode(tx.txid().as_bytes()), "inputs": tx.inputs.len(), "outputs": tx.outputs.len() }) }).collect::>() } else { vec![] } }); blocks.push(block_json); } } serde_json::json!(blocks) })?; Ok(()) } /// Registers transaction methods. fn register_tx_methods(&self, module: &mut RpcModule) -> anyhow::Result<()> { // synor_getMempoolSize module.register_async_method("synor_getMempoolSize", |_, ctx| async move { let size = ctx.mempool.count().await; serde_json::json!({"size": size}) })?; Ok(()) } /// Registers network methods. fn register_network_methods(&self, module: &mut RpcModule) -> anyhow::Result<()> { // synor_getInfo module.register_async_method("synor_getInfo", |_, ctx| async move { let peer_count = ctx.network.peer_count().await; let block_count = ctx.consensus.current_height().await; let blue_score = ctx.consensus.current_blue_score().await; let mempool_size = ctx.mempool.count().await; // Check actual sync status from network service let synced = ctx.network.sync_status().await .map(|status| matches!(status.state, SyncState::Synced | SyncState::Idle)) .unwrap_or(false); serde_json::json!({ "version": env!("CARGO_PKG_VERSION"), "protocolVersion": 1, "peerCount": peer_count, "blockCount": block_count, "blueScore": blue_score, "mempoolSize": mempool_size, "synced": synced }) })?; // synor_getPeerCount module.register_async_method("synor_getPeerCount", |_, ctx| async move { let count = ctx.network.peer_count().await; serde_json::json!({"peerCount": count}) })?; // synor_getPeerInfo module.register_async_method("synor_getPeerInfo", |_, ctx| async move { let peers = ctx.network.peers().await; let peer_info: Vec = peers.iter().map(|p| { serde_json::json!({ "id": p.id, "address": p.address.map(|a| a.to_string()).unwrap_or_default(), "isInbound": p.inbound, "version": p.version, "userAgent": p.user_agent, "latencyMs": p.latency_ms }) }).collect(); serde_json::json!({"peers": peer_info}) })?; Ok(()) } /// Registers mining methods. fn register_mining_methods(&self, module: &mut RpcModule) -> anyhow::Result<()> { // synor_getMiningInfo module.register_async_method("synor_getMiningInfo", |_, ctx| async move { let block_count = ctx.consensus.current_height().await; let difficulty_bits = ctx.consensus.current_difficulty().await; // Convert compact difficulty bits to difficulty value // difficulty = max_target / current_target // For simplified calculation, use the exponent and mantissa from compact bits let exponent = (difficulty_bits >> 24) as u64; let mantissa = (difficulty_bits & 0x00FFFFFF) as u64; let difficulty = if exponent <= 3 { (mantissa >> (8 * (3 - exponent))) as f64 } else { (mantissa as f64) * (256.0_f64).powi((exponent - 3) as i32) }; // Estimate network hashrate based on difficulty // hashrate ≈ difficulty × 2^32 / block_time_seconds // With 100ms (0.1s) block time target: let block_time_seconds = 0.1_f64; let network_hashrate = if difficulty > 0.0 { (difficulty * 4_294_967_296.0 / block_time_seconds) as u64 } else { 0 }; serde_json::json!({ "blocks": block_count, "difficulty": difficulty, "networkhashps": network_hashrate }) })?; Ok(()) } /// Registers smart contract methods. fn register_contract_methods(&self, module: &mut RpcModule) -> anyhow::Result<()> { // synor_deployContract - Deploy a new contract module.register_async_method("synor_deployContract", |params, ctx| async move { #[derive(serde::Deserialize)] struct DeployParams { bytecode: String, #[serde(default)] init_args: String, deployer: synor_types::Address, #[serde(default)] gas_limit: Option, } let params: DeployParams = match params.parse() { Ok(p) => p, Err(e) => return serde_json::json!({"error": format!("Invalid params: {}", e)}), }; let bytecode = match hex::decode(¶ms.bytecode) { Ok(b) => b, Err(e) => return serde_json::json!({"error": format!("Invalid bytecode hex: {}", e)}), }; let init_args = if params.init_args.is_empty() { Vec::new() } else { match hex::decode(¶ms.init_args) { Ok(a) => a, Err(e) => return serde_json::json!({"error": format!("Invalid init_args hex: {}", e)}), } }; let block_height = ctx.consensus.current_height().await; let timestamp = current_timestamp(); match ctx.contract.deploy( bytecode, init_args, ¶ms.deployer, params.gas_limit, block_height, timestamp, ).await { Ok(result) => serde_json::json!({ "contractId": hex::encode(&result.contract_id), "address": hex::encode(&result.address), "gasUsed": result.gas_used }), Err(e) => serde_json::json!({ "error": e.to_string() }) } })?; // synor_callContract - Call a contract method module.register_async_method("synor_callContract", |params, ctx| async move { #[derive(serde::Deserialize)] struct CallParams { contract_id: String, method: String, #[serde(default)] args: String, caller: synor_types::Address, #[serde(default)] value: u64, #[serde(default)] gas_limit: Option, } let params: CallParams = match params.parse() { Ok(p) => p, Err(e) => return serde_json::json!({"error": format!("Invalid params: {}", e)}), }; let contract_id = match hex_to_hash(¶ms.contract_id) { Ok(id) => id, Err(e) => return serde_json::json!({"error": format!("Invalid contract_id: {}", e)}), }; let args = if params.args.is_empty() { Vec::new() } else { match hex::decode(¶ms.args) { Ok(a) => a, Err(e) => return serde_json::json!({"error": format!("Invalid args hex: {}", e)}), } }; let block_height = ctx.consensus.current_height().await; let timestamp = current_timestamp(); match ctx.contract.call( &contract_id, ¶ms.method, args, ¶ms.caller, params.value, params.gas_limit, block_height, timestamp, ).await { Ok(result) => { let logs: Vec = result.logs.iter().map(|log| { serde_json::json!({ "contractId": hex::encode(&log.contract_id), "topics": log.topics.iter().map(|t| hex::encode(t)).collect::>(), "data": hex::encode(&log.data) }) }).collect(); serde_json::json!({ "success": result.success, "data": hex::encode(&result.data), "gasUsed": result.gas_used, "logs": logs }) }, Err(e) => serde_json::json!({ "error": e.to_string() }) } })?; // synor_estimateGas - Estimate gas for a contract call module.register_async_method("synor_estimateGas", |params, ctx| async move { #[derive(serde::Deserialize)] struct EstimateParams { contract_id: String, method: String, #[serde(default)] args: String, caller: synor_types::Address, #[serde(default)] value: u64, } let params: EstimateParams = match params.parse() { Ok(p) => p, Err(e) => return serde_json::json!({"error": format!("Invalid params: {}", e)}), }; let contract_id = match hex_to_hash(¶ms.contract_id) { Ok(id) => id, Err(e) => return serde_json::json!({"error": format!("Invalid contract_id: {}", e)}), }; let args = if params.args.is_empty() { Vec::new() } else { match hex::decode(¶ms.args) { Ok(a) => a, Err(e) => return serde_json::json!({"error": format!("Invalid args hex: {}", e)}), } }; let block_height = ctx.consensus.current_height().await; let timestamp = current_timestamp(); match ctx.contract.estimate_gas( &contract_id, ¶ms.method, args, ¶ms.caller, params.value, block_height, timestamp, ).await { Ok(gas) => serde_json::json!({ "estimatedGas": gas }), Err(e) => serde_json::json!({ "error": e.to_string() }) } })?; // synor_getCode - Get contract bytecode module.register_async_method("synor_getCode", |params, ctx| async move { #[derive(serde::Deserialize)] struct GetCodeParams { contract_id: String, } let params: GetCodeParams = match params.parse() { Ok(p) => p, Err(e) => return serde_json::json!({"error": format!("Invalid params: {}", e)}), }; let contract_id = match hex_to_hash(¶ms.contract_id) { Ok(id) => id, Err(e) => return serde_json::json!({"error": format!("Invalid contract_id: {}", e)}), }; match ctx.contract.get_code(&contract_id).await { Ok(Some(code)) => serde_json::json!({ "code": hex::encode(&code) }), Ok(None) => serde_json::json!({ "code": null }), Err(e) => serde_json::json!({ "error": e.to_string() }) } })?; // synor_getStorageAt - Get contract storage value module.register_async_method("synor_getStorageAt", |params, ctx| async move { #[derive(serde::Deserialize)] struct GetStorageParams { contract_id: String, key: String, } let params: GetStorageParams = match params.parse() { Ok(p) => p, Err(e) => return serde_json::json!({"error": format!("Invalid params: {}", e)}), }; let contract_id = match hex_to_hash(¶ms.contract_id) { Ok(id) => id, Err(e) => return serde_json::json!({"error": format!("Invalid contract_id: {}", e)}), }; let key = match hex_to_hash(¶ms.key) { Ok(k) => k, Err(e) => return serde_json::json!({"error": format!("Invalid key: {}", e)}), }; match ctx.contract.get_storage_at(&contract_id, &key).await { Ok(Some(value)) => serde_json::json!({ "value": hex::encode(&value) }), Ok(None) => serde_json::json!({ "value": null }), Err(e) => serde_json::json!({ "error": e.to_string() }) } })?; // synor_getContract - Get contract metadata module.register_async_method("synor_getContract", |params, ctx| async move { #[derive(serde::Deserialize)] struct GetContractParams { contract_id: String, } let params: GetContractParams = match params.parse() { Ok(p) => p, Err(e) => return serde_json::json!({"error": format!("Invalid params: {}", e)}), }; let contract_id = match hex_to_hash(¶ms.contract_id) { Ok(id) => id, Err(e) => return serde_json::json!({"error": format!("Invalid contract_id: {}", e)}), }; match ctx.contract.get_contract(&contract_id).await { Ok(Some(contract)) => serde_json::json!({ "codeHash": hex::encode(&contract.code_hash), "deployer": hex::encode(&contract.deployer), "deployedAt": contract.deployed_at, "deployedHeight": contract.deployed_height }), Ok(None) => serde_json::json!({ "contract": null }), Err(e) => serde_json::json!({ "error": e.to_string() }) } })?; Ok(()) } } /// RPC handlers implementation. impl RpcService { // ==================== Block Methods ==================== /// Gets a block by hash. pub async fn get_block( &self, hash: &str, include_txs: bool, ) -> anyhow::Result> { let hash_bytes = hex_to_hash(hash)?; let block_data = self.storage.get_block(&hash_bytes).await?; if let Some(_data) = block_data { Ok(Some(RpcBlock { hash: hash.to_string(), header: RpcBlockHeader { version: 1, parents: vec![], hash_merkle_root: String::new(), utxo_commitment: String::new(), timestamp: 0, bits: 0, nonce: 0, blue_score: 0, blue_work: String::new(), pruning_point: None, }, transactions: if include_txs { vec![] } else { vec![] }, verbose_data: None, })) } else { Ok(None) } } /// Gets the current block count. pub async fn get_block_count(&self) -> u64 { self.consensus.current_height().await } /// Gets current tips. pub async fn get_tips(&self) -> Vec { self.consensus .tips() .await .iter() .map(|h| hex::encode(h)) .collect() } // ==================== Transaction Methods ==================== /// Submits a transaction. pub async fn submit_transaction(&self, tx_hex: &str) -> anyhow::Result { let tx_bytes = hex::decode(tx_hex)?; // Validate let validation = self.consensus.validate_tx(&tx_bytes).await; match validation { crate::services::consensus::TxValidation::Valid => { // Add to mempool let hash = compute_tx_hash(&tx_bytes); let tx = crate::services::mempool::MempoolTx { hash, data: tx_bytes, mass: 100, // TODO: Calculate fee: 0, // TODO: Calculate fee_rate: 0.0, timestamp: current_timestamp(), dependencies: vec![], high_priority: false, }; self.mempool.add_transaction(tx).await?; // Announce to network self.network.announce_tx(hash).await; Ok(hex::encode(&hash)) } crate::services::consensus::TxValidation::Invalid { reason } => { anyhow::bail!("Invalid transaction: {}", reason) } crate::services::consensus::TxValidation::Duplicate => { anyhow::bail!("Transaction already exists") } crate::services::consensus::TxValidation::Conflict => { anyhow::bail!("Transaction conflicts with existing") } } } /// Gets transaction from mempool or chain. pub async fn get_transaction(&self, hash: &str) -> anyhow::Result> { let hash_bytes = hex_to_hash(hash)?; // Check mempool first if let Some(mempool_tx) = self.mempool.get_transaction(&hash_bytes).await { return Ok(Some(RpcTransaction { hash: hash.to_string(), inputs: vec![], outputs: vec![], mass: mempool_tx.mass, fee: mempool_tx.fee, verbose_data: None, })); } // TODO: Check chain Ok(None) } // ==================== Network Methods ==================== /// Gets node info. pub async fn get_info(&self) -> RpcNodeInfo { RpcNodeInfo { version: env!("CARGO_PKG_VERSION").to_string(), protocol_version: 1, network: "mainnet".to_string(), // TODO: From config peer_count: self.network.peer_count().await, synced: true, // TODO: Check sync state block_count: self.consensus.current_height().await, blue_score: self.consensus.current_blue_score().await, mempool_size: self.mempool.count().await, } } /// Gets connected peers. pub async fn get_peer_info(&self) -> Vec { self.network .peers() .await .into_iter() .map(|p| RpcPeerInfo { id: p.id, address: p.address.map(|a| a.to_string()).unwrap_or_default(), is_inbound: p.inbound, version: p.version, user_agent: p.user_agent, latency_ms: p.latency_ms, }) .collect() } // ==================== Mining Methods ==================== /// Gets block template for mining. pub async fn get_block_template(&self, _pay_address: &str) -> anyhow::Result { // TODO: Get template from miner service Ok(RpcBlockTemplate { header: RpcBlockHeader { version: 1, parents: self.get_tips().await, hash_merkle_root: String::new(), utxo_commitment: String::new(), timestamp: current_timestamp(), bits: 0x1e0fffff, nonce: 0, blue_score: self.consensus.current_blue_score().await, blue_work: String::new(), pruning_point: None, }, transactions: vec![], target: "00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff" .to_string(), is_synced: true, }) } } // ==================== RPC Types ==================== #[derive(Clone, Debug)] pub struct RpcBlock { pub hash: String, pub header: RpcBlockHeader, pub transactions: Vec, pub verbose_data: Option, } #[derive(Clone, Debug)] pub struct RpcBlockHeader { pub version: u32, pub parents: Vec, pub hash_merkle_root: String, pub utxo_commitment: String, pub timestamp: u64, pub bits: u32, pub nonce: u64, pub blue_score: u64, pub blue_work: String, pub pruning_point: Option, } #[derive(Clone, Debug)] pub struct RpcBlockVerboseData { pub hash: String, pub blue_score: u64, pub is_chain_block: bool, pub selected_parent: Option, pub children: Vec, } #[derive(Clone, Debug)] pub struct RpcTransaction { pub hash: String, pub inputs: Vec, pub outputs: Vec, pub mass: u64, pub fee: u64, pub verbose_data: Option, } #[derive(Clone, Debug)] pub struct RpcTxInput { pub previous_outpoint: RpcOutpoint, pub signature_script: String, pub sig_op_count: u32, } #[derive(Clone, Debug)] pub struct RpcOutpoint { pub transaction_id: String, pub index: u32, } #[derive(Clone, Debug)] pub struct RpcTxOutput { pub value: u64, pub script_public_key: String, } #[derive(Clone, Debug)] pub struct RpcTxVerboseData { pub block_hash: Option, pub confirmations: u64, pub accepting_block_hash: Option, } #[derive(Clone, Debug)] pub struct RpcNodeInfo { pub version: String, pub protocol_version: u32, pub network: String, pub peer_count: usize, pub synced: bool, pub block_count: u64, pub blue_score: u64, pub mempool_size: usize, } #[derive(Clone, Debug)] pub struct RpcPeerInfo { pub id: String, pub address: String, pub is_inbound: bool, pub version: u32, pub user_agent: String, pub latency_ms: u32, } #[derive(Clone, Debug)] pub struct RpcBlockTemplate { pub header: RpcBlockHeader, pub transactions: Vec, pub target: String, pub is_synced: bool, } // ==================== Helpers ==================== fn hex_to_hash(hex: &str) -> anyhow::Result<[u8; 32]> { let bytes = hex::decode(hex)?; if bytes.len() != 32 { anyhow::bail!("Invalid hash length"); } let mut arr = [0u8; 32]; arr.copy_from_slice(&bytes); Ok(arr) } fn compute_tx_hash(tx: &[u8]) -> [u8; 32] { blake3::hash(tx).into() } fn current_timestamp() -> u64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() as u64 }