use crate::bus::EventBus;
use crate::types::{EventEnvelope, EventFilter, EventType, SubscriptionId, TenzroEvent, VmType};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tracing::{debug, error, info, warn};
#[derive(Debug, Clone)]
pub struct WebSocketConfig {
pub max_subscriptions_per_connection: usize,
pub ping_interval_secs: u64,
pub max_message_size: usize,
pub rate_limit_per_second: u32,
}
impl Default for WebSocketConfig {
fn default() -> Self {
Self {
max_subscriptions_per_connection: 100,
ping_interval_secs: 30,
max_message_size: 1_048_576, rate_limit_per_second: 100,
}
}
}
impl WebSocketConfig {
pub fn with_max_subscriptions(mut self, max: usize) -> Self {
self.max_subscriptions_per_connection = max;
self
}
pub fn with_ping_interval_secs(mut self, secs: u64) -> Self {
self.ping_interval_secs = secs;
self
}
pub fn with_max_message_size(mut self, size: usize) -> Self {
self.max_message_size = size;
self
}
pub fn with_rate_limit(mut self, rate: u32) -> Self {
self.rate_limit_per_second = rate;
self
}
}
#[derive(Debug)]
pub struct WebSocketStats {
pub active_connections: AtomicU64,
pub total_subscriptions: AtomicU64,
pub total_events_sent: AtomicU64,
pub total_errors: AtomicU64,
}
impl WebSocketStats {
fn new() -> Self {
Self {
active_connections: AtomicU64::new(0),
total_subscriptions: AtomicU64::new(0),
total_events_sent: AtomicU64::new(0),
total_errors: AtomicU64::new(0),
}
}
pub fn snapshot(&self) -> WebSocketStatsSnapshot {
WebSocketStatsSnapshot {
active_connections: self.active_connections.load(Ordering::Relaxed),
total_subscriptions: self.total_subscriptions.load(Ordering::Relaxed),
total_events_sent: self.total_events_sent.load(Ordering::Relaxed),
total_errors: self.total_errors.load(Ordering::Relaxed),
}
}
}
impl Default for WebSocketStats {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WebSocketStatsSnapshot {
pub active_connections: u64,
pub total_subscriptions: u64,
pub total_events_sent: u64,
pub total_errors: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SubscriptionType {
NewHeads,
Logs,
NewPendingTransactions,
Syncing,
TenzroEvents,
}
impl SubscriptionType {
pub fn from_str_param(s: &str) -> Option<Self> {
match s {
"newHeads" => Some(SubscriptionType::NewHeads),
"logs" => Some(SubscriptionType::Logs),
"newPendingTransactions" => Some(SubscriptionType::NewPendingTransactions),
"syncing" => Some(SubscriptionType::Syncing),
"tenzroEvents" => Some(SubscriptionType::TenzroEvents),
_ => None,
}
}
fn matching_event_types(&self) -> Vec<EventType> {
match self {
SubscriptionType::NewHeads => vec![EventType::NewBlock],
SubscriptionType::Logs => vec![EventType::Log],
SubscriptionType::NewPendingTransactions => vec![EventType::NewPendingTransaction],
SubscriptionType::Syncing => vec![EventType::SyncProgress],
SubscriptionType::TenzroEvents => vec![], }
}
}
#[derive(Debug, Clone)]
pub struct SubscriptionInfo {
pub id: u64,
pub subscription_type: SubscriptionType,
pub filter: EventFilter,
pub created_at: i64,
}
pub struct WebSocketServer {
event_bus: Arc<EventBus>,
subscriptions: Arc<DashMap<u64, SubscriptionInfo>>,
next_subscription_id: AtomicU64,
config: WebSocketConfig,
stats: Arc<WebSocketStats>,
}
impl WebSocketServer {
pub fn new(event_bus: Arc<EventBus>, config: WebSocketConfig) -> Self {
Self {
event_bus,
subscriptions: Arc::new(DashMap::new()),
next_subscription_id: AtomicU64::new(1),
config,
stats: Arc::new(WebSocketStats::new()),
}
}
pub fn stats(&self) -> &Arc<WebSocketStats> {
&self.stats
}
pub fn subscription_count(&self) -> usize {
self.subscriptions.len()
}
pub fn event_bus(&self) -> &Arc<EventBus> {
&self.event_bus
}
pub fn handle_subscribe(
&self,
params: &serde_json::Value,
) -> Result<(u64, SubscriptionType, EventFilter), String> {
if self.subscriptions.len() >= self.config.max_subscriptions_per_connection {
return Err(format!(
"maximum subscriptions ({}) reached",
self.config.max_subscriptions_per_connection
));
}
let type_str = params
.get(0)
.and_then(|v| v.as_str())
.or_else(|| params.get("type").and_then(|v| v.as_str()))
.ok_or_else(|| "missing subscription type parameter".to_string())?;
let sub_type = SubscriptionType::from_str_param(type_str)
.ok_or_else(|| format!("unknown subscription type: {}", type_str))?;
let filter = match &sub_type {
SubscriptionType::Logs => {
let filter_param = params.get(1).or_else(|| params.get("filter"));
match filter_param {
Some(fp) => parse_log_filter(fp),
None => EventFilter::new().with_event_types(vec![EventType::Log]),
}
}
SubscriptionType::TenzroEvents => {
let filter_param = params.get(1).or_else(|| params.get("filter"));
match filter_param {
Some(fp) => parse_tenzro_filter(fp),
None => EventFilter::new(),
}
}
_ => {
let types = sub_type.matching_event_types();
if types.is_empty() {
EventFilter::new()
} else {
EventFilter::new().with_event_types(types)
}
}
};
let id = self.next_subscription_id.fetch_add(1, Ordering::SeqCst);
let info = SubscriptionInfo {
id,
subscription_type: sub_type.clone(),
filter: filter.clone(),
created_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64,
};
self.subscriptions.insert(id, info);
self.stats.total_subscriptions.fetch_add(1, Ordering::Relaxed);
info!(subscription_id = id, sub_type = ?sub_type, "subscription created");
Ok((id, sub_type, filter))
}
pub fn handle_unsubscribe(&self, subscription_id: u64) -> bool {
let removed = self.subscriptions.remove(&subscription_id).is_some();
if removed {
self.stats.total_subscriptions.fetch_sub(1, Ordering::Relaxed);
info!(subscription_id, "subscription removed");
} else {
warn!(subscription_id, "unsubscribe: subscription not found");
}
removed
}
pub fn format_event_for_subscription(
envelope: &EventEnvelope,
sub_type: &SubscriptionType,
sub_id: u64,
) -> Option<serde_json::Value> {
match sub_type {
SubscriptionType::NewHeads => {
if let TenzroEvent::NewBlock {
block_hash,
parent_hash,
height,
tx_count,
proposer,
} = &envelope.event
{
let result = json!({
"number": format!("0x{:x}", height),
"hash": format!("0x{}", hex::encode(block_hash)),
"parentHash": format!("0x{}", hex::encode(parent_hash)),
"timestamp": format!("0x{:x}", envelope.timestamp / 1000),
"gasUsed": "0x0",
"gasLimit": "0x1c9c380",
"baseFeePerGas": "0x0",
"miner": format!("0x{}", hex::encode(proposer)),
"transactionsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
"stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
"receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
});
Some(wrap_subscription_notification(sub_id, result))
} else {
None
}
}
SubscriptionType::Logs => {
if let TenzroEvent::Log {
address,
topics,
data,
block_height,
tx_hash,
log_index,
removed,
} = &envelope.event
{
let topics_hex: Vec<String> = topics
.iter()
.map(|t| format!("0x{}", hex::encode(t)))
.collect();
let result = json!({
"address": format!("0x{}", hex::encode(address)),
"topics": topics_hex,
"data": format!("0x{}", hex::encode(data)),
"blockNumber": format!("0x{:x}", block_height),
"transactionHash": format!("0x{}", hex::encode(tx_hash)),
"logIndex": format!("0x{:x}", log_index),
"blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"transactionIndex": "0x0",
"removed": removed,
});
Some(wrap_subscription_notification(sub_id, result))
} else {
None
}
}
SubscriptionType::NewPendingTransactions => {
if let TenzroEvent::NewPendingTransaction { tx_hash, .. } = &envelope.event {
let hash_hex = format!("0x{}", hex::encode(tx_hash));
Some(wrap_subscription_notification(sub_id, json!(hash_hex)))
} else {
None
}
}
SubscriptionType::Syncing => {
if let TenzroEvent::SyncProgress {
current_block,
highest_block,
percent,
} = &envelope.event
{
if *current_block < *highest_block {
let result = json!({
"syncing": true,
"status": {
"startingBlock": "0x0",
"currentBlock": format!("0x{:x}", current_block),
"highestBlock": format!("0x{:x}", highest_block),
}
});
Some(wrap_subscription_notification(sub_id, result))
} else {
Some(wrap_subscription_notification(sub_id, json!(false)))
}
} else {
None
}
}
SubscriptionType::TenzroEvents => {
let result = match serde_json::to_value(envelope) {
Ok(v) => v,
Err(_) => return None,
};
Some(wrap_subscription_notification(sub_id, result))
}
}
}
pub fn process_message(&self, message: &str) -> serde_json::Value {
let parsed: serde_json::Value = match serde_json::from_str(message) {
Ok(v) => v,
Err(e) => {
self.stats.total_errors.fetch_add(1, Ordering::Relaxed);
return json!({
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32700,
"message": format!("Parse error: {}", e)
}
});
}
};
let id = parsed.get("id").cloned().unwrap_or(json!(null));
let method = parsed.get("method").and_then(|v| v.as_str()).unwrap_or("");
let params = parsed.get("params").cloned().unwrap_or(json!([]));
match method {
"eth_subscribe" => match self.handle_subscribe(¶ms) {
Ok((sub_id, sub_type, _filter)) => {
debug!(subscription_id = sub_id, sub_type = ?sub_type, "eth_subscribe");
json!({
"jsonrpc": "2.0",
"id": id,
"result": format!("0x{:x}", sub_id)
})
}
Err(err) => {
self.stats.total_errors.fetch_add(1, Ordering::Relaxed);
json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": -32602,
"message": err
}
})
}
},
"eth_unsubscribe" => {
let sub_id = params
.get(0)
.and_then(|v| {
if let Some(s) = v.as_str() {
let s = s.strip_prefix("0x").unwrap_or(s);
u64::from_str_radix(s, 16).ok()
} else {
v.as_u64()
}
})
.unwrap_or(0);
let result = self.handle_unsubscribe(sub_id);
json!({
"jsonrpc": "2.0",
"id": id,
"result": result
})
}
"tenzro_subscribe" => {
let mut effective_params = vec![json!("tenzroEvents")];
if let Some(filter) = params.get(0) {
effective_params.push(filter.clone());
}
let effective_params = serde_json::Value::Array(effective_params);
match self.handle_subscribe(&effective_params) {
Ok((sub_id, sub_type, _filter)) => {
debug!(subscription_id = sub_id, sub_type = ?sub_type, "tenzro_subscribe");
json!({
"jsonrpc": "2.0",
"id": id,
"result": format!("0x{:x}", sub_id)
})
}
Err(err) => {
self.stats.total_errors.fetch_add(1, Ordering::Relaxed);
json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": -32602,
"message": err
}
})
}
}
}
_ => {
self.stats.total_errors.fetch_add(1, Ordering::Relaxed);
json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": -32601,
"message": format!("Method not found: {}", method)
}
})
}
}
}
pub fn match_event(&self, envelope: &EventEnvelope) -> Vec<(u64, serde_json::Value)> {
let mut results = Vec::new();
for entry in self.subscriptions.iter() {
let info = entry.value();
if !info.filter.matches(envelope) {
continue;
}
if let Some(formatted) = Self::format_event_for_subscription(
envelope,
&info.subscription_type,
info.id,
) {
results.push((info.id, formatted));
self.stats.total_events_sent.fetch_add(1, Ordering::Relaxed);
}
}
results
}
}
pub fn parse_log_filter(params: &serde_json::Value) -> EventFilter {
let mut filter = EventFilter::new().with_event_types(vec![EventType::Log]);
if let Some(addr) = params.get("address") {
let mut addrs = Vec::new();
if let Some(s) = addr.as_str() {
if let Some(a) = parse_hex_address(s) {
addrs.push(a);
}
} else if let Some(arr) = addr.as_array() {
for v in arr {
if let Some(s) = v.as_str() {
if let Some(a) = parse_hex_address(s) {
addrs.push(a);
}
}
}
}
if !addrs.is_empty() {
filter = filter.with_addresses(addrs);
}
}
if let Some(topics_val) = params.get("topics").and_then(|v| v.as_array()) {
let mut parsed_topics: Vec<Option<Vec<[u8; 32]>>> = Vec::new();
for slot in topics_val {
if slot.is_null() {
parsed_topics.push(None);
} else if let Some(s) = slot.as_str() {
if let Some(h) = parse_hex_hash(s) {
parsed_topics.push(Some(vec![h]));
} else {
parsed_topics.push(None);
}
} else if let Some(arr) = slot.as_array() {
let hashes: Vec<[u8; 32]> = arr
.iter()
.filter_map(|v| v.as_str().and_then(parse_hex_hash))
.collect();
if hashes.is_empty() {
parsed_topics.push(None);
} else {
parsed_topics.push(Some(hashes));
}
} else {
parsed_topics.push(None);
}
}
filter.topics = parsed_topics;
}
if let Some(from) = params.get("fromBlock") {
filter.from_block = parse_block_number(from);
}
if let Some(to) = params.get("toBlock") {
filter.to_block = parse_block_number(to);
}
filter
}
pub fn parse_tenzro_filter(params: &serde_json::Value) -> EventFilter {
let mut filter = EventFilter::new();
if let Some(types) = params.get("eventTypes").and_then(|v| v.as_array()) {
let mut event_types = Vec::new();
for t in types {
if let Some(s) = t.as_str() {
if let Some(et) = parse_event_type_name(s) {
event_types.push(et);
}
}
}
if !event_types.is_empty() {
filter = filter.with_event_types(event_types);
}
}
if let Some(addrs) = params.get("addresses").and_then(|v| v.as_array()) {
let parsed: Vec<[u8; 20]> = addrs
.iter()
.filter_map(|v| v.as_str().and_then(parse_hex_address))
.collect();
if !parsed.is_empty() {
filter = filter.with_addresses(parsed);
}
}
if let Some(vms) = params.get("vmTypes").and_then(|v| v.as_array()) {
let mut vm_types = Vec::new();
for v in vms {
if let Some(s) = v.as_str() {
match s {
"native" | "Native" => vm_types.push(VmType::Native),
"evm" | "Evm" | "EVM" => vm_types.push(VmType::Evm),
"svm" | "Svm" | "SVM" => vm_types.push(VmType::Svm),
"daml" | "Daml" | "DAML" => vm_types.push(VmType::Daml),
_ => {}
}
}
}
filter.vm_types = vm_types;
}
if let Some(from) = params.get("fromBlock") {
filter.from_block = parse_block_number(from);
}
if let Some(to) = params.get("toBlock") {
filter.to_block = parse_block_number(to);
}
filter
}
fn parse_hex_address(s: &str) -> Option<[u8; 20]> {
let s = s.strip_prefix("0x").unwrap_or(s);
let bytes = hex::decode(s).ok()?;
if bytes.len() == 20 {
let mut arr = [0u8; 20];
arr.copy_from_slice(&bytes);
Some(arr)
} else {
None
}
}
fn parse_hex_hash(s: &str) -> Option<[u8; 32]> {
let s = s.strip_prefix("0x").unwrap_or(s);
let bytes = hex::decode(s).ok()?;
if bytes.len() == 32 {
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Some(arr)
} else {
None
}
}
fn parse_block_number(value: &serde_json::Value) -> Option<u64> {
if let Some(s) = value.as_str() {
let s = s.strip_prefix("0x").unwrap_or(s);
u64::from_str_radix(s, 16).ok()
} else {
value.as_u64()
}
}
fn parse_event_type_name(name: &str) -> Option<EventType> {
match name {
"NewBlock" | "newBlock" | "newHeads" => Some(EventType::NewBlock),
"BlockFinalized" | "blockFinalized" => Some(EventType::BlockFinalized),
"BlockReorged" | "blockReorged" => Some(EventType::BlockReorged),
"NewPendingTransaction" | "newPendingTransactions" => {
Some(EventType::NewPendingTransaction)
}
"TransactionIncluded" | "transactionIncluded" => Some(EventType::TransactionIncluded),
"TransactionFinalized" | "transactionFinalized" => Some(EventType::TransactionFinalized),
"Log" | "log" | "logs" => Some(EventType::Log),
"Transfer" | "transfer" => Some(EventType::Transfer),
"NftTransfer" | "nftTransfer" => Some(EventType::NftTransfer),
"CrosschainMint" | "crosschainMint" => Some(EventType::CrosschainMint),
"CrosschainBurn" | "crosschainBurn" => Some(EventType::CrosschainBurn),
"IdentityRegistered" | "identityRegistered" => Some(EventType::IdentityRegistered),
"CredentialIssued" | "credentialIssued" => Some(EventType::CredentialIssued),
"ComplianceViolation" | "complianceViolation" => Some(EventType::ComplianceViolation),
"ModelRegistered" | "modelRegistered" => Some(EventType::ModelRegistered),
"InferenceCompleted" | "inferenceCompleted" => Some(EventType::InferenceCompleted),
"AgentMessage" | "agentMessage" => Some(EventType::AgentMessage),
"SettlementCompleted" | "settlementCompleted" => Some(EventType::SettlementCompleted),
"PaymentChannelUpdate" | "paymentChannelUpdate" => Some(EventType::PaymentChannelUpdate),
"StakeDeposited" | "stakeDeposited" => Some(EventType::StakeDeposited),
"StakeWithdrawn" | "stakeWithdrawn" => Some(EventType::StakeWithdrawn),
"ValidatorSlashed" | "validatorSlashed" => Some(EventType::ValidatorSlashed),
"ProposalCreated" | "proposalCreated" => Some(EventType::ProposalCreated),
"VoteCast" | "voteCast" => Some(EventType::VoteCast),
"BridgeTransferInitiated" | "bridgeTransferInitiated" => {
Some(EventType::BridgeTransferInitiated)
}
"BridgeTransferCompleted" | "bridgeTransferCompleted" => {
Some(EventType::BridgeTransferCompleted)
}
"SyncProgress" | "syncProgress" | "syncing" => Some(EventType::SyncProgress),
_ => None,
}
}
fn wrap_subscription_notification(sub_id: u64, result: serde_json::Value) -> serde_json::Value {
json!({
"jsonrpc": "2.0",
"method": "eth_subscription",
"params": {
"subscription": format!("0x{:x}", sub_id),
"result": result
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bus::{EventBus, EventBusConfig};
use crate::types::{EventEnvelope, EventType, TenzroEvent, VmType};
use std::sync::Arc;
fn make_bus() -> Arc<EventBus> {
Arc::new(EventBus::new(EventBusConfig::default()))
}
fn make_server() -> WebSocketServer {
WebSocketServer::new(make_bus(), WebSocketConfig::default())
}
fn make_server_with_config(config: WebSocketConfig) -> WebSocketServer {
WebSocketServer::new(make_bus(), config)
}
fn make_block_envelope(height: u64) -> EventEnvelope {
EventEnvelope {
sequence: 0,
timestamp: 1700000000,
block_height: Some(height),
vm_type: Some(VmType::Native),
event: TenzroEvent::NewBlock {
block_hash: [0xAA; 32],
parent_hash: [0xBB; 32],
height,
tx_count: 5,
proposer: [0x01; 20],
},
}
}
fn make_log_envelope() -> EventEnvelope {
EventEnvelope {
sequence: 1,
timestamp: 1700000001,
block_height: Some(100),
vm_type: Some(VmType::Evm),
event: TenzroEvent::Log {
address: [0x42; 20],
topics: vec![[0xDD; 32]],
data: vec![0x01, 0x02, 0x03],
block_height: 100,
tx_hash: [0xCC; 32],
log_index: 0,
removed: false,
},
}
}
fn make_pending_tx_envelope() -> EventEnvelope {
EventEnvelope {
sequence: 2,
timestamp: 1700000002,
block_height: None,
vm_type: None,
event: TenzroEvent::NewPendingTransaction {
tx_hash: [0xEE; 32],
from: [0x01; 20],
to: Some([0x02; 20]),
value: 1000,
nonce: 42,
},
}
}
fn make_transfer_envelope() -> EventEnvelope {
EventEnvelope {
sequence: 3,
timestamp: 1700000003,
block_height: Some(50),
vm_type: Some(VmType::Native),
event: TenzroEvent::Transfer {
from: [0x01; 20],
to: [0x02; 20],
amount: 1_000_000_000_000_000_000,
token_id: "TNZO".into(),
tx_hash: [0xFF; 32],
},
}
}
#[test]
fn test_subscribe_new_heads() {
let server = make_server();
let params = json!(["newHeads"]);
let (id, sub_type, filter) = server.handle_subscribe(¶ms).unwrap();
assert!(id > 0);
assert_eq!(sub_type, SubscriptionType::NewHeads);
assert_eq!(filter.event_types, vec![EventType::NewBlock]);
assert_eq!(server.subscription_count(), 1);
}
#[test]
fn test_subscribe_logs_with_filter() {
let server = make_server();
let params = json!(["logs", {
"address": "0x4242424242424242424242424242424242424242",
"fromBlock": "0xa"
}]);
let (id, sub_type, filter) = server.handle_subscribe(¶ms).unwrap();
assert!(id > 0);
assert_eq!(sub_type, SubscriptionType::Logs);
assert_eq!(filter.event_types, vec![EventType::Log]);
assert_eq!(filter.addresses.len(), 1);
assert_eq!(filter.addresses[0], [0x42; 20]);
assert_eq!(filter.from_block, Some(10));
}
#[test]
fn test_subscribe_logs_with_address_array() {
let server = make_server();
let params = json!(["logs", {
"address": [
"0x0101010101010101010101010101010101010101",
"0x0202020202020202020202020202020202020202"
]
}]);
let (_id, _sub_type, filter) = server.handle_subscribe(¶ms).unwrap();
assert_eq!(filter.addresses.len(), 2);
}
#[test]
fn test_subscribe_pending_transactions() {
let server = make_server();
let params = json!(["newPendingTransactions"]);
let (_id, sub_type, filter) = server.handle_subscribe(¶ms).unwrap();
assert_eq!(sub_type, SubscriptionType::NewPendingTransactions);
assert_eq!(filter.event_types, vec![EventType::NewPendingTransaction]);
}
#[test]
fn test_subscribe_syncing() {
let server = make_server();
let params = json!(["syncing"]);
let (_id, sub_type, _filter) = server.handle_subscribe(¶ms).unwrap();
assert_eq!(sub_type, SubscriptionType::Syncing);
}
#[test]
fn test_tenzro_subscribe() {
let server = make_server();
let params = json!(["tenzroEvents", {
"eventTypes": ["NewBlock", "Transfer"]
}]);
let (_id, sub_type, filter) = server.handle_subscribe(¶ms).unwrap();
assert_eq!(sub_type, SubscriptionType::TenzroEvents);
assert_eq!(filter.event_types.len(), 2);
}
#[test]
fn test_subscribe_unknown_type_fails() {
let server = make_server();
let params = json!(["unknownType"]);
let result = server.handle_subscribe(¶ms);
assert!(result.is_err());
assert!(result.unwrap_err().contains("unknown subscription type"));
}
#[test]
fn test_unsubscribe() {
let server = make_server();
let (id, _, _) = server.handle_subscribe(&json!(["newHeads"])).unwrap();
assert_eq!(server.subscription_count(), 1);
let removed = server.handle_unsubscribe(id);
assert!(removed);
assert_eq!(server.subscription_count(), 0);
}
#[test]
fn test_unsubscribe_nonexistent() {
let server = make_server();
let removed = server.handle_unsubscribe(9999);
assert!(!removed);
}
#[test]
fn test_format_block_header() {
let envelope = make_block_envelope(42);
let formatted = WebSocketServer::format_event_for_subscription(
&envelope,
&SubscriptionType::NewHeads,
1,
);
assert!(formatted.is_some());
let value = formatted.unwrap();
let result = &value["params"]["result"];
assert_eq!(result["number"], "0x2a"); assert!(result["hash"].as_str().unwrap().starts_with("0x"));
assert!(result["miner"].as_str().unwrap().starts_with("0x"));
}
#[test]
fn test_format_block_header_skips_non_block() {
let envelope = make_transfer_envelope();
let formatted = WebSocketServer::format_event_for_subscription(
&envelope,
&SubscriptionType::NewHeads,
1,
);
assert!(formatted.is_none());
}
#[test]
fn test_format_log() {
let envelope = make_log_envelope();
let formatted = WebSocketServer::format_event_for_subscription(
&envelope,
&SubscriptionType::Logs,
1,
);
assert!(formatted.is_some());
let value = formatted.unwrap();
let result = &value["params"]["result"];
assert_eq!(result["blockNumber"], "0x64"); assert_eq!(result["logIndex"], "0x0");
assert_eq!(result["removed"], false);
assert!(result["address"].as_str().unwrap().starts_with("0x"));
assert!(result["topics"].as_array().unwrap().len() == 1);
}
#[test]
fn test_format_pending_tx() {
let envelope = make_pending_tx_envelope();
let formatted = WebSocketServer::format_event_for_subscription(
&envelope,
&SubscriptionType::NewPendingTransactions,
1,
);
assert!(formatted.is_some());
let value = formatted.unwrap();
let result = &value["params"]["result"];
assert!(result.as_str().unwrap().starts_with("0x"));
}
#[test]
fn test_format_tenzro_event() {
let envelope = make_transfer_envelope();
let formatted = WebSocketServer::format_event_for_subscription(
&envelope,
&SubscriptionType::TenzroEvents,
1,
);
assert!(formatted.is_some());
let value = formatted.unwrap();
let result = &value["params"]["result"];
assert_eq!(result["sequence"], 3);
assert_eq!(result["block_height"], 50);
}
#[test]
fn test_parse_log_filter_single_address() {
let params = json!({
"address": "0x4242424242424242424242424242424242424242",
"fromBlock": "0x10",
"toBlock": 256
});
let filter = parse_log_filter(¶ms);
assert_eq!(filter.addresses, vec![[0x42; 20]]);
assert_eq!(filter.from_block, Some(16));
assert_eq!(filter.to_block, Some(256));
}
#[test]
fn test_parse_log_filter_topics() {
let topic_hex = format!("0x{}", hex::encode([0xDD; 32]));
let params = json!({
"topics": [topic_hex, null]
});
let filter = parse_log_filter(¶ms);
assert_eq!(filter.topics.len(), 2);
assert!(filter.topics[0].is_some());
assert!(filter.topics[1].is_none());
}
#[test]
fn test_parse_tenzro_filter() {
let params = json!({
"eventTypes": ["NewBlock", "Transfer"],
"vmTypes": ["evm"]
});
let filter = parse_tenzro_filter(¶ms);
assert_eq!(filter.event_types.len(), 2);
assert_eq!(filter.vm_types, vec![VmType::Evm]);
}
#[test]
fn test_max_subscriptions_limit() {
let config = WebSocketConfig::default().with_max_subscriptions(3);
let server = make_server_with_config(config);
for _ in 0..3 {
let result = server.handle_subscribe(&json!(["newHeads"]));
assert!(result.is_ok());
}
let result = server.handle_subscribe(&json!(["newHeads"]));
assert!(result.is_err());
assert!(result.unwrap_err().contains("maximum subscriptions"));
}
#[test]
fn test_stats_tracking() {
let server = make_server();
let snap = server.stats().snapshot();
assert_eq!(snap.total_subscriptions, 0);
assert_eq!(snap.total_events_sent, 0);
let (id, _, _) = server.handle_subscribe(&json!(["newHeads"])).unwrap();
assert_eq!(server.stats().snapshot().total_subscriptions, 1);
let envelope = make_block_envelope(1);
let matches = server.match_event(&envelope);
assert_eq!(matches.len(), 1);
assert_eq!(server.stats().snapshot().total_events_sent, 1);
server.handle_unsubscribe(id);
assert_eq!(server.stats().snapshot().total_subscriptions, 0);
}
#[test]
fn test_process_message_eth_subscribe() {
let server = make_server();
let msg = r#"{"jsonrpc":"2.0","id":1,"method":"eth_subscribe","params":["newHeads"]}"#;
let response = server.process_message(msg);
assert_eq!(response["jsonrpc"], "2.0");
assert_eq!(response["id"], 1);
assert!(response["result"].as_str().unwrap().starts_with("0x"));
assert_eq!(server.subscription_count(), 1);
}
#[test]
fn test_process_message_eth_unsubscribe() {
let server = make_server();
let sub_resp = server.process_message(
r#"{"jsonrpc":"2.0","id":1,"method":"eth_subscribe","params":["newHeads"]}"#,
);
let sub_id_hex = sub_resp["result"].as_str().unwrap();
let unsub_msg = format!(
r#"{{"jsonrpc":"2.0","id":2,"method":"eth_unsubscribe","params":["{}"]}}"#,
sub_id_hex
);
let response = server.process_message(&unsub_msg);
assert_eq!(response["result"], true);
assert_eq!(server.subscription_count(), 0);
}
#[test]
fn test_process_message_tenzro_subscribe() {
let server = make_server();
let msg = r#"{"jsonrpc":"2.0","id":1,"method":"tenzro_subscribe","params":[{"eventTypes":["NewBlock"]}]}"#;
let response = server.process_message(msg);
assert_eq!(response["jsonrpc"], "2.0");
assert!(response["result"].as_str().unwrap().starts_with("0x"));
assert_eq!(server.subscription_count(), 1);
let sub_id_hex = response["result"].as_str().unwrap();
let sub_id_str = sub_id_hex.strip_prefix("0x").unwrap();
let sub_id = u64::from_str_radix(sub_id_str, 16).unwrap();
let info = server.subscriptions.get(&sub_id).unwrap();
assert_eq!(info.subscription_type, SubscriptionType::TenzroEvents);
}
#[test]
fn test_process_message_unknown_method() {
let server = make_server();
let msg = r#"{"jsonrpc":"2.0","id":1,"method":"eth_badMethod","params":[]}"#;
let response = server.process_message(msg);
assert!(response["error"]["message"]
.as_str()
.unwrap()
.contains("Method not found"));
assert_eq!(server.stats().snapshot().total_errors, 1);
}
#[test]
fn test_process_message_parse_error() {
let server = make_server();
let response = server.process_message("not json");
assert!(response["error"]["message"]
.as_str()
.unwrap()
.contains("Parse error"));
}
#[test]
fn test_match_event_multiple_subscriptions() {
let server = make_server();
server.handle_subscribe(&json!(["newHeads"])).unwrap();
server.handle_subscribe(&json!(["tenzroEvents"])).unwrap();
server.handle_subscribe(&json!(["logs"])).unwrap();
let envelope = make_block_envelope(10);
let matches = server.match_event(&envelope);
assert_eq!(matches.len(), 2);
}
#[test]
fn test_parse_event_type_name() {
assert_eq!(parse_event_type_name("NewBlock"), Some(EventType::NewBlock));
assert_eq!(parse_event_type_name("newHeads"), Some(EventType::NewBlock));
assert_eq!(parse_event_type_name("Transfer"), Some(EventType::Transfer));
assert_eq!(parse_event_type_name("logs"), Some(EventType::Log));
assert_eq!(parse_event_type_name("syncing"), Some(EventType::SyncProgress));
assert_eq!(parse_event_type_name("nonexistent"), None);
}
#[test]
fn test_subscription_notification_format() {
let notif = wrap_subscription_notification(42, json!("hello"));
assert_eq!(notif["jsonrpc"], "2.0");
assert_eq!(notif["method"], "eth_subscription");
assert_eq!(notif["params"]["subscription"], "0x2a");
assert_eq!(notif["params"]["result"], "hello");
}
}