A complete blockchain implementation featuring: - synord: Full node with GHOSTDAG consensus - explorer-web: Modern React blockchain explorer with 3D DAG visualization - CLI wallet and tools - Smart contract SDK and example contracts (DEX, NFT, token) - WASM crypto library for browser/mobile
975 lines
32 KiB
Rust
975 lines
32 KiB
Rust
//! 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<StorageService>,
|
||
pub network: Arc<NetworkService>,
|
||
pub consensus: Arc<ConsensusService>,
|
||
pub mempool: Arc<MempoolService>,
|
||
pub contract: Arc<ContractService>,
|
||
}
|
||
|
||
/// RPC service manages the JSON-RPC server.
|
||
pub struct RpcService {
|
||
/// Storage reference.
|
||
storage: Arc<StorageService>,
|
||
|
||
/// Network reference.
|
||
network: Arc<NetworkService>,
|
||
|
||
/// Consensus reference.
|
||
consensus: Arc<ConsensusService>,
|
||
|
||
/// Mempool reference.
|
||
mempool: Arc<MempoolService>,
|
||
|
||
/// Contract service reference.
|
||
contract: Arc<ContractService>,
|
||
|
||
/// 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<bool>,
|
||
|
||
/// HTTP server handle.
|
||
http_handle: RwLock<Option<ServerHandle>>,
|
||
|
||
/// WebSocket server handle.
|
||
ws_handle: RwLock<Option<ServerHandle>>,
|
||
}
|
||
|
||
impl RpcService {
|
||
/// Creates a new RPC service.
|
||
pub fn new(
|
||
storage: Arc<StorageService>,
|
||
network: Arc<NetworkService>,
|
||
consensus: Arc<ConsensusService>,
|
||
mempool: Arc<MempoolService>,
|
||
contract: Arc<ContractService>,
|
||
config: &NodeConfig,
|
||
) -> anyhow::Result<Self> {
|
||
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<RpcModule<RpcContext>> {
|
||
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<RpcContext>) -> 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<RpcContext>) -> 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<String> = 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<bool>) = 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::<Vec<_>>(),
|
||
"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::<Vec<_>>()
|
||
} else {
|
||
vec![]
|
||
}
|
||
});
|
||
blocks.push(block_json);
|
||
}
|
||
}
|
||
|
||
serde_json::json!(blocks)
|
||
})?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Registers transaction methods.
|
||
fn register_tx_methods(&self, module: &mut RpcModule<RpcContext>) -> 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<RpcContext>) -> 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<serde_json::Value> = 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<RpcContext>) -> 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<RpcContext>) -> 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<u64>,
|
||
}
|
||
|
||
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<u64>,
|
||
}
|
||
|
||
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<serde_json::Value> = result.logs.iter().map(|log| {
|
||
serde_json::json!({
|
||
"contractId": hex::encode(&log.contract_id),
|
||
"topics": log.topics.iter().map(|t| hex::encode(t)).collect::<Vec<_>>(),
|
||
"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<Option<RpcBlock>> {
|
||
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<String> {
|
||
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<String> {
|
||
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<Option<RpcTransaction>> {
|
||
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<RpcPeerInfo> {
|
||
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<RpcBlockTemplate> {
|
||
// 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<RpcTransaction>,
|
||
pub verbose_data: Option<RpcBlockVerboseData>,
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
pub struct RpcBlockHeader {
|
||
pub version: u32,
|
||
pub parents: Vec<String>,
|
||
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<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
pub struct RpcBlockVerboseData {
|
||
pub hash: String,
|
||
pub blue_score: u64,
|
||
pub is_chain_block: bool,
|
||
pub selected_parent: Option<String>,
|
||
pub children: Vec<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
pub struct RpcTransaction {
|
||
pub hash: String,
|
||
pub inputs: Vec<RpcTxInput>,
|
||
pub outputs: Vec<RpcTxOutput>,
|
||
pub mass: u64,
|
||
pub fee: u64,
|
||
pub verbose_data: Option<RpcTxVerboseData>,
|
||
}
|
||
|
||
#[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<String>,
|
||
pub confirmations: u64,
|
||
pub accepting_block_hash: Option<String>,
|
||
}
|
||
|
||
#[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<RpcTransaction>,
|
||
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
|
||
}
|