synor/apps/synord/src/services/rpc.rs
Gulshan Yadav 48949ebb3f Initial commit: Synor blockchain monorepo
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
2026-01-08 05:22:17 +05:30

975 lines
32 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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(&params.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(&params.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,
&params.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(&params.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(&params.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,
&params.method,
args,
&params.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(&params.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(&params.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,
&params.method,
args,
&params.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(&params.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(&params.contract_id) {
Ok(id) => id,
Err(e) => return serde_json::json!({"error": format!("Invalid contract_id: {}", e)}),
};
let key = match hex_to_hash(&params.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(&params.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
}