diff --git a/crates/server/src/handlers/blocks/decode/args.rs b/crates/server/src/handlers/blocks/decode/args.rs index 8a4d947b..2c0591d3 100644 --- a/crates/server/src/handlers/blocks/decode/args.rs +++ b/crates/server/src/handlers/blocks/decode/args.rs @@ -10,10 +10,11 @@ //! Type aliases: //! - `JsonVisitor`: Uses camelCase for field names (general API responses) //! - `CallArgsVisitor`: Keeps snake_case for field names (extrinsic call args, matching sidecar) +//! - `EventJsonVisitor`: Uses camelCase, preserves basic enum variant casing (event fields) //! //! For enum types, it distinguishes between "basic" enums (all variants have no data) //! and "non-basic" enums (any variant has data): -//! - Basic enums serialize as strings: `"Normal"`, `"Yes"` +//! - Basic enums serialize as strings: `"normal"` or `"Normal"` (depending on `PRESERVE_ENUM_CASE`) //! - Non-basic enums serialize as objects: `{"unlimited": null}`, `{"limited": {...}}` use heck::ToLowerCamelCase; @@ -24,10 +25,13 @@ use serde_json::Value; use sp_core::crypto::{AccountId32, Ss58Codec}; /// Type alias for the visitor that converts field names to camelCase. -pub type JsonVisitor<'r> = ScaleVisitor<'r, true>; +pub type JsonVisitor<'r> = ScaleVisitor<'r, true, false>; /// Type alias for the visitor that keeps field names in snake_case. -pub type CallArgsVisitor<'r> = ScaleVisitor<'r, false>; +pub type CallArgsVisitor<'r> = ScaleVisitor<'r, false, false>; + +/// Type alias for event field decoding: camelCase keys, preserved basic enum variant casing. +pub type EventJsonVisitor<'r> = ScaleVisitor<'r, true, true>; /// Check if an enum type is "basic" (all variants have no associated data). fn is_basic_enum(resolver: &PortableRegistry, type_id: u32) -> bool { @@ -95,12 +99,16 @@ fn try_items_to_hex(items: &[Value]) -> Option { /// /// - `CAMEL_CASE = true`: Convert field names to camelCase (use `JsonVisitor` alias) /// - `CAMEL_CASE = false`: Keep field names in snake_case (use `CallArgsVisitor` alias) -pub struct ScaleVisitor<'r, const CAMEL_CASE: bool> { +/// - `PRESERVE_ENUM_CASE = true`: Keep basic enum variant names as-is (use `EventJsonVisitor`) +/// - `PRESERVE_ENUM_CASE = false`: Lowercase first char of basic enum variants (default) +pub struct ScaleVisitor<'r, const CAMEL_CASE: bool, const PRESERVE_ENUM_CASE: bool> { ss58_prefix: u16, resolver: &'r PortableRegistry, } -impl<'r, const CAMEL_CASE: bool> ScaleVisitor<'r, CAMEL_CASE> { +impl<'r, const CAMEL_CASE: bool, const PRESERVE_ENUM_CASE: bool> + ScaleVisitor<'r, CAMEL_CASE, PRESERVE_ENUM_CASE> +{ pub fn new(ss58_prefix: u16, resolver: &'r PortableRegistry) -> Self { Self { ss58_prefix, @@ -113,7 +121,9 @@ impl<'r, const CAMEL_CASE: bool> ScaleVisitor<'r, CAMEL_CASE> { } } -impl<'r, const CAMEL_CASE: bool> scale_decode::Visitor for ScaleVisitor<'r, CAMEL_CASE> { +impl<'r, const CAMEL_CASE: bool, const PRESERVE_ENUM_CASE: bool> scale_decode::Visitor + for ScaleVisitor<'r, CAMEL_CASE, PRESERVE_ENUM_CASE> +{ type Value<'scale, 'resolver> = Value; type Error = scale_decode::Error; type TypeResolver = PortableRegistry; @@ -346,15 +356,21 @@ impl<'r, const CAMEL_CASE: bool> scale_decode::Visitor for ScaleVisitor<'r, CAME return self.decode_call_variant(value); } - let variant_name = crate::utils::lowercase_first_char(name); - if is_basic_enum(self.resolver, type_id) { for field in value.fields() { field?.decode_with_visitor(SkipVisitor)?; } + let variant_name = if PRESERVE_ENUM_CASE { + name.to_string() + } else { + crate::utils::lowercase_first_char(name) + }; return Ok(Value::String(variant_name)); } + // Non-basic enums: always lowercase the key + let variant_name = crate::utils::lowercase_first_char(name); + let is_junction = is_junction_variant(name); let fields: Vec<_> = value.fields().collect::, _>>()?; @@ -454,7 +470,9 @@ impl<'r, const CAMEL_CASE: bool> scale_decode::Visitor for ScaleVisitor<'r, CAME } } -impl<'r, const CAMEL_CASE: bool> ScaleVisitor<'r, CAMEL_CASE> { +impl<'r, const CAMEL_CASE: bool, const PRESERVE_ENUM_CASE: bool> + ScaleVisitor<'r, CAMEL_CASE, PRESERVE_ENUM_CASE> +{ fn try_extract_single_byte( &self, value: &mut visitor::types::Composite<'_, '_, PortableRegistry>, diff --git a/crates/server/src/handlers/blocks/decode/events.rs b/crates/server/src/handlers/blocks/decode/events.rs index 4a023c08..8b3f1fa7 100644 --- a/crates/server/src/handlers/blocks/decode/events.rs +++ b/crates/server/src/handlers/blocks/decode/events.rs @@ -1,15 +1,13 @@ // Copyright (C) 2026 Parity Technologies (UK) Ltd. // SPDX-License-Identifier: GPL-3.0-or-later -//! Event decoding and transformation for block data. +//! Event decoding for block data. //! -//! This module provides: -//! - `EventsVisitor` for extracting event information from System.Events storage -//! - Post-processing functions for transforming decoded event data to JSON -//! -//! Updated for subxt 0.50.0 which uses PortableRegistry. +//! This module provides `EventsVisitor` for extracting event information from +//! System.Events storage. Event fields are decoded using `EventJsonVisitor` +//! (from `args.rs`) which handles all type-aware transformations at decode time: +//! AccountId32 → SS58, numbers → strings, camelCase keys, byte arrays → hex. -use heck::ToLowerCamelCase; use scale_decode::{ Visitor, visitor::{ @@ -18,54 +16,17 @@ use scale_decode::{ }, }; use scale_info::PortableRegistry; -use scale_type_resolver::TypeResolver; use serde_json::Value as JsonValue; -use sp_core::crypto::{AccountId32, Ss58Codec}; - -/// Check if an enum type is "basic" (all variants have no associated data). -fn is_basic_enum(resolver: &PortableRegistry, type_id: u32) -> bool { - let type_visitor = - scale_type_resolver::visitor::new((), |_, _| false).visit_variant(|_, _path, variants| { - for variant in variants { - if variant.fields.len() > 0 { - return false; - } - } - true - }); - - resolver - .resolve_type(type_id, type_visitor) - .unwrap_or(false) -} -// ================================================================================================ -// Event Visitor Types -// ================================================================================================ +use super::args::EventJsonVisitor; -/// Lowercase the first character only, preserving the rest -fn lowercase_first_char(s: &str) -> String { - let mut chars = s.chars(); - match chars.next() { - None => String::new(), - Some(first) => first.to_lowercase().chain(chars).collect(), - } -} - -/// Represents a single event field with its type name and value -#[derive(Debug, Clone)] -pub struct EventField { - pub type_name: Option, - pub value: JsonValue, -} - -/// Represents a single event with its metadata and field type information +/// Represents a single event with its metadata and decoded fields #[derive(Debug, Clone)] pub struct EventInfo { pub phase: EventPhase, pub pallet_name: String, pub event_name: String, - pub fields: Vec, + pub fields: Vec, } /// Event phase extracted from EventRecord @@ -76,15 +37,23 @@ pub enum EventPhase { Finalization, } -/// Visitor that collects all events with their field type information -/// Specialized for PortableRegistry. +// ================================================================================================ +// Event Visitor Types +// ================================================================================================ + +/// Visitor that collects all events with their decoded field data. +/// Uses `EventJsonVisitor` for type-aware field decoding. pub struct EventsVisitor<'r> { + ss58_prefix: u16, resolver: &'r PortableRegistry, } impl<'r> EventsVisitor<'r> { - pub fn new(resolver: &'r PortableRegistry) -> Self { - Self { resolver } + pub fn new(ss58_prefix: u16, resolver: &'r PortableRegistry) -> Self { + Self { + ss58_prefix, + resolver, + } } } @@ -101,7 +70,7 @@ impl<'r> Visitor for EventsVisitor<'r> { let mut events = Vec::new(); while let Some(event_record_result) = - value.decode_item(EventRecordVisitor::new(self.resolver)) + value.decode_item(EventRecordVisitor::new(self.ss58_prefix, self.resolver)) { match event_record_result { Ok(Some(event_info)) => events.push(event_info), @@ -127,12 +96,16 @@ impl<'r> Visitor for EventsVisitor<'r> { /// Visitor for a single EventRecord struct EventRecordVisitor<'r> { + ss58_prefix: u16, resolver: &'r PortableRegistry, } impl<'r> EventRecordVisitor<'r> { - fn new(resolver: &'r PortableRegistry) -> Self { - Self { resolver } + fn new(ss58_prefix: u16, resolver: &'r PortableRegistry) -> Self { + Self { + ss58_prefix, + resolver, + } } } @@ -152,8 +125,11 @@ impl<'r> Visitor for EventRecordVisitor<'r> { EventPhase::Finalization }; - if let Some(event_result) = value.decode_item(PalletEventVisitor::new(phase, self.resolver)) - { + if let Some(event_result) = value.decode_item(PalletEventVisitor::new( + phase, + self.ss58_prefix, + self.resolver, + )) { return event_result; } @@ -248,12 +224,17 @@ impl Visitor for U32Extractor { /// Visitor for the pallet-level variant struct PalletEventVisitor<'r> { phase: EventPhase, + ss58_prefix: u16, resolver: &'r PortableRegistry, } impl<'r> PalletEventVisitor<'r> { - fn new(phase: EventPhase, resolver: &'r PortableRegistry) -> Self { - Self { phase, resolver } + fn new(phase: EventPhase, ss58_prefix: u16, resolver: &'r PortableRegistry) -> Self { + Self { + phase, + ss58_prefix, + resolver, + } } } @@ -267,12 +248,13 @@ impl<'r> Visitor for PalletEventVisitor<'r> { value: &mut Variant<'scale, 'resolver, Self::TypeResolver>, _type_id: TypeIdFor, ) -> Result, Self::Error> { - let pallet_name = lowercase_first_char(value.name()); + let pallet_name = crate::utils::lowercase_first_char(value.name()); let fields_composite = value.fields(); if let Some(inner_event_result) = fields_composite.decode_item(ActualEventVisitor::new( self.phase, pallet_name, + self.ss58_prefix, self.resolver, )) { return inner_event_result; @@ -289,18 +271,26 @@ impl<'r> Visitor for PalletEventVisitor<'r> { } } -/// Visitor for the actual event variant +/// Visitor for the actual event variant. +/// Decodes event fields using `EventJsonVisitor` for type-aware JSON serialization. struct ActualEventVisitor<'r> { phase: EventPhase, pallet_name: String, + ss58_prefix: u16, resolver: &'r PortableRegistry, } impl<'r> ActualEventVisitor<'r> { - fn new(phase: EventPhase, pallet_name: String, resolver: &'r PortableRegistry) -> Self { + fn new( + phase: EventPhase, + pallet_name: String, + ss58_prefix: u16, + resolver: &'r PortableRegistry, + ) -> Self { Self { phase, pallet_name, + ss58_prefix, resolver, } } @@ -322,14 +312,11 @@ impl<'r> Visitor for ActualEventVisitor<'r> { let fields_composite = value.fields(); while let Some(field_result) = - fields_composite.decode_item(FieldWithTypeExtractor::new(self.resolver)) + fields_composite.decode_item(EventJsonVisitor::new(self.ss58_prefix, self.resolver)) { match field_result { - Ok((type_name, json_value)) => { - event_fields.push(EventField { - type_name, - value: json_value, - }); + Ok(json_value) => { + event_fields.push(json_value); } Err(e) => { tracing::warn!("Failed to decode field: {:?}", e); @@ -352,644 +339,3 @@ impl<'r> Visitor for ActualEventVisitor<'r> { Ok(None) } } - -/// Visitor that extracts both the type name and JSON value for a field -struct FieldWithTypeExtractor<'r> { - resolver: &'r PortableRegistry, -} - -impl<'r> FieldWithTypeExtractor<'r> { - fn new(resolver: &'r PortableRegistry) -> Self { - Self { resolver } - } -} - -impl<'r> Visitor for FieldWithTypeExtractor<'r> { - type Value<'scale, 'resolver> = (Option, JsonValue); - type Error = scale_decode::Error; - type TypeResolver = PortableRegistry; - - fn visit_composite<'scale, 'resolver>( - self, - value: &mut Composite<'scale, 'resolver, Self::TypeResolver>, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - let type_name = value.path().last().map(|s| s.to_string()); - - if type_name.as_deref() == Some("AccountId32") || type_name.as_deref() == Some("AccountId") - { - let bytes = value.bytes_from_start(); - if bytes.len() == 32 { - let hex_string = format!("0x{}", hex::encode(bytes)); - return Ok((type_name, JsonValue::String(hex_string))); - } - } - - let field_names: Vec> = value - .fields() - .iter() - .map(|f| f.name.map(|s| s.to_lower_camel_case())) - .collect(); - let has_named_fields = field_names.iter().any(|n| n.is_some()); - - let mut field_values = Vec::new(); - while let Some(field_result) = value.decode_item(ValueExtractor::new(self.resolver)) { - match field_result { - Ok(json_val) => field_values.push(json_val), - Err(e) => tracing::warn!("Failed to decode composite field: {:?}", e), - } - } - - let json_value = if has_named_fields && field_names.len() == field_values.len() { - let obj: serde_json::Map = field_names - .into_iter() - .zip(field_values) - .filter_map(|(name, val)| name.map(|n| (n, val))) - .collect(); - JsonValue::Object(obj) - } else { - JsonValue::Array(field_values) - }; - - Ok((type_name, json_value)) - } - - fn visit_variant<'scale, 'resolver>( - self, - value: &mut Variant<'scale, 'resolver, Self::TypeResolver>, - type_id: TypeIdFor, - ) -> Result, Self::Error> { - let type_name = value.path().last().map(|s| s.to_string()); - let variant_name = value.name(); - - if variant_name == "None" { - let fields_composite = value.fields(); - while let Some(field_result) = - fields_composite.decode_item(ValueExtractor::new(self.resolver)) - { - let _ = field_result; - } - return Ok((type_name, JsonValue::Null)); - } - - if variant_name == "Some" { - let fields_composite = value.fields(); - if let Some(field_result) = - fields_composite.decode_item(ValueExtractor::new(self.resolver)) - { - match field_result { - Ok(inner_value) => return Ok((type_name, inner_value)), - Err(e) => { - tracing::debug!( - "Failed to decode Option::Some inner value (FieldWithTypeExtractor): {e:?}" - ); - return Ok((type_name, JsonValue::Null)); - } - } - } - return Ok((type_name, JsonValue::Null)); - } - - if type_name.as_deref() == Some("MultiAddress") && variant_name == "Id" { - let bytes = value.bytes_from_start(); - if bytes.len() >= 33 { - let hex_string = format!("0x{}", hex::encode(&bytes[1..33])); - return Ok((type_name, JsonValue::String(hex_string))); - } - } - - let is_basic = is_basic_enum(self.resolver, type_id); - - if is_basic { - let fields_composite = value.fields(); - while let Some(field_result) = - fields_composite.decode_item(ValueExtractor::new(self.resolver)) - { - let _ = field_result; - } - return Ok((type_name, JsonValue::String(variant_name.to_string()))); - } - - let mut fields = Vec::new(); - let fields_composite = value.fields(); - while let Some(field_result) = - fields_composite.decode_item(ValueExtractor::new(self.resolver)) - { - match field_result { - Ok(json_val) => fields.push(json_val), - Err(e) => tracing::warn!("Failed to decode variant field: {:?}", e), - } - } - - let key = lowercase_first_char(variant_name); - let json_value = if fields.is_empty() { - serde_json::json!({ key: JsonValue::Null }) - } else if fields.len() == 1 { - serde_json::json!({ key: fields[0].clone() }) - } else { - serde_json::json!({ key: fields }) - }; - - Ok((type_name, json_value)) - } - - fn visit_sequence<'scale, 'resolver>( - self, - value: &mut Sequence<'scale, 'resolver, Self::TypeResolver>, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - let mut items = Vec::new(); - while let Some(item_result) = value.decode_item(ValueExtractor::new(self.resolver)) { - match item_result { - Ok(json_val) => items.push(json_val), - Err(e) => tracing::warn!("Failed to decode sequence item: {:?}", e), - } - } - Ok((None, JsonValue::Array(items))) - } - - fn visit_array<'scale, 'resolver>( - self, - value: &mut scale_decode::visitor::types::Array<'scale, 'resolver, Self::TypeResolver>, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - let mut items = Vec::new(); - while let Some(item_result) = value.decode_item(ValueExtractor::new(self.resolver)) { - match item_result { - Ok(json_val) => items.push(json_val), - Err(e) => tracing::warn!("Failed to decode array item: {:?}", e), - } - } - - if items.len() >= 2 { - let mut is_byte_array = true; - let mut bytes = Vec::with_capacity(items.len()); - for item in &items { - if let JsonValue::Number(n) = item - && let Some(byte) = n.as_u64() - && byte <= 255 - { - bytes.push(byte as u8); - continue; - } - is_byte_array = false; - break; - } - if is_byte_array && bytes.len() == items.len() { - return Ok(( - None, - JsonValue::String(format!("0x{}", hex::encode(&bytes))), - )); - } - } - - Ok((None, JsonValue::Array(items))) - } - - fn visit_u8<'scale, 'resolver>( - self, - value: u8, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - Ok((None, serde_json::json!(value))) - } - - fn visit_u16<'scale, 'resolver>( - self, - value: u16, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - Ok((None, serde_json::json!(value))) - } - - fn visit_u32<'scale, 'resolver>( - self, - value: u32, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - Ok((None, serde_json::json!(value))) - } - - fn visit_u64<'scale, 'resolver>( - self, - value: u64, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - Ok((None, serde_json::json!(value))) - } - - fn visit_u128<'scale, 'resolver>( - self, - value: u128, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - Ok((None, serde_json::json!(value.to_string()))) - } - - fn visit_bool<'scale, 'resolver>( - self, - value: bool, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - Ok((None, serde_json::json!(value))) - } - - fn visit_unexpected<'scale, 'resolver>( - self, - _unexpected: Unexpected, - ) -> Result, Self::Error> { - Ok((None, JsonValue::Null)) - } -} - -/// Visitor that extracts just the JSON value without type information -struct ValueExtractor<'r> { - resolver: &'r PortableRegistry, -} - -impl<'r> ValueExtractor<'r> { - fn new(resolver: &'r PortableRegistry) -> Self { - Self { resolver } - } -} - -impl<'r> Visitor for ValueExtractor<'r> { - type Value<'scale, 'resolver> = JsonValue; - type Error = scale_decode::Error; - type TypeResolver = PortableRegistry; - - fn visit_composite<'scale, 'resolver>( - self, - value: &mut Composite<'scale, 'resolver, Self::TypeResolver>, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - let field_names: Vec> = value - .fields() - .iter() - .map(|f| f.name.map(|s| s.to_lower_camel_case())) - .collect(); - let has_named_fields = field_names.iter().any(|n| n.is_some()); - - let mut field_values = Vec::new(); - while let Some(field_result) = value.decode_item(ValueExtractor::new(self.resolver)) { - match field_result { - Ok(json_val) => field_values.push(json_val), - Err(e) => tracing::warn!("Failed to decode composite field: {:?}", e), - } - } - - if has_named_fields && field_names.len() == field_values.len() { - let obj: serde_json::Map = field_names - .into_iter() - .zip(field_values) - .filter_map(|(name, val)| name.map(|n| (n, val))) - .collect(); - Ok(JsonValue::Object(obj)) - } else { - Ok(JsonValue::Array(field_values)) - } - } - - fn visit_variant<'scale, 'resolver>( - self, - value: &mut Variant<'scale, 'resolver, Self::TypeResolver>, - type_id: TypeIdFor, - ) -> Result, Self::Error> { - let variant_name = value.name(); - - if variant_name == "None" { - let fields_composite = value.fields(); - while let Some(field_result) = - fields_composite.decode_item(ValueExtractor::new(self.resolver)) - { - let _ = field_result; - } - return Ok(JsonValue::Null); - } - - if variant_name == "Some" { - let fields_composite = value.fields(); - if let Some(field_result) = - fields_composite.decode_item(ValueExtractor::new(self.resolver)) - { - match field_result { - Ok(inner_value) => return Ok(inner_value), - Err(e) => { - tracing::debug!( - "Failed to decode Option::Some inner value(ValueExtractor): {e:?}" - ); - return Ok(JsonValue::Null); - } - } - } - return Ok(JsonValue::Null); - } - - let is_basic = is_basic_enum(self.resolver, type_id); - - if is_basic { - let fields_composite = value.fields(); - while let Some(field_result) = - fields_composite.decode_item(ValueExtractor::new(self.resolver)) - { - let _ = field_result; - } - return Ok(JsonValue::String(variant_name.to_string())); - } - - let mut fields = Vec::new(); - let fields_composite = value.fields(); - while let Some(field_result) = - fields_composite.decode_item(ValueExtractor::new(self.resolver)) - { - match field_result { - Ok(json_val) => fields.push(json_val), - Err(e) => tracing::warn!("Failed to decode variant field: {:?}", e), - } - } - - let key = lowercase_first_char(variant_name); - if fields.is_empty() { - Ok(serde_json::json!({ key: JsonValue::Null })) - } else if fields.len() == 1 { - Ok(serde_json::json!({ key: fields[0].clone() })) - } else { - Ok(serde_json::json!({ key: fields })) - } - } - - fn visit_sequence<'scale, 'resolver>( - self, - value: &mut Sequence<'scale, 'resolver, Self::TypeResolver>, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - let mut items = Vec::new(); - while let Some(item_result) = value.decode_item(ValueExtractor::new(self.resolver)) { - match item_result { - Ok(json_val) => items.push(json_val), - Err(e) => tracing::warn!("Failed to decode sequence item: {:?}", e), - } - } - Ok(JsonValue::Array(items)) - } - - fn visit_array<'scale, 'resolver>( - self, - value: &mut scale_decode::visitor::types::Array<'scale, 'resolver, Self::TypeResolver>, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - let mut items = Vec::new(); - while let Some(item_result) = value.decode_item(ValueExtractor::new(self.resolver)) { - match item_result { - Ok(json_val) => items.push(json_val), - Err(e) => tracing::warn!("Failed to decode array item: {:?}", e), - } - } - - if items.len() >= 2 { - let mut is_byte_array = true; - let mut bytes = Vec::with_capacity(items.len()); - for item in &items { - if let JsonValue::Number(n) = item - && let Some(byte) = n.as_u64() - && byte <= 255 - { - bytes.push(byte as u8); - continue; - } - is_byte_array = false; - break; - } - if is_byte_array && bytes.len() == items.len() { - return Ok(JsonValue::String(format!("0x{}", hex::encode(&bytes)))); - } - } - - Ok(JsonValue::Array(items)) - } - - fn visit_u8<'scale, 'resolver>( - self, - value: u8, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - Ok(serde_json::json!(value)) - } - - fn visit_u16<'scale, 'resolver>( - self, - value: u16, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - Ok(serde_json::json!(value)) - } - - fn visit_u32<'scale, 'resolver>( - self, - value: u32, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - Ok(serde_json::json!(value)) - } - - fn visit_u64<'scale, 'resolver>( - self, - value: u64, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - Ok(serde_json::json!(value)) - } - - fn visit_u128<'scale, 'resolver>( - self, - value: u128, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - Ok(serde_json::json!(value.to_string())) - } - - fn visit_bool<'scale, 'resolver>( - self, - value: bool, - _type_id: TypeIdFor, - ) -> Result, Self::Error> { - Ok(serde_json::json!(value)) - } - - fn visit_unexpected<'scale, 'resolver>( - self, - _unexpected: Unexpected, - ) -> Result, Self::Error> { - Ok(JsonValue::Null) - } -} - -// ================================================================================================ -// Post-Processing Functions -// ================================================================================================ - -/// Convert JSON value, replacing byte arrays with hex strings and all numbers with strings recursively -pub fn convert_bytes_to_hex(value: JsonValue) -> JsonValue { - match value { - JsonValue::Number(n) => JsonValue::String(n.to_string()), - JsonValue::Array(arr) => { - let is_byte_array = !arr.is_empty() - && arr.iter().all(|v| match v { - JsonValue::Number(n) => n.as_u64().is_some_and(|val| val <= 255), - _ => false, - }); - - if is_byte_array { - let bytes: Vec = arr - .iter() - .filter_map(|v| v.as_u64().map(|n| n as u8)) - .collect(); - JsonValue::String(format!("0x{}", hex::encode(&bytes))) - } else { - let converted: Vec = arr.into_iter().map(convert_bytes_to_hex).collect(); - match converted.len() { - 1 => match converted.into_iter().next() { - Some(v) => v, - None => JsonValue::Array(vec![]), - }, - _ => JsonValue::Array(converted), - } - } - } - JsonValue::Object(mut map) => { - if let Some(JsonValue::Array(bits)) = map.get("__bitvec__values__") { - let mut bytes = Vec::new(); - let mut current_byte = 0u8; - for (i, bit) in bits.iter().enumerate() { - if let Some(true) = bit.as_bool() { - current_byte |= 1 << (i % 8); - } - if (i + 1) % 8 == 0 { - bytes.push(current_byte); - current_byte = 0; - } - } - if bits.len() % 8 != 0 { - bytes.push(current_byte); - } - return JsonValue::String(format!("0x{}", hex::encode(&bytes))); - } - - for (_, v) in map.iter_mut() { - *v = convert_bytes_to_hex(std::mem::take(v)); - } - JsonValue::Object(map) - } - other => other, - } -} - -/// Unified transformation function -pub fn transform_json_unified(value: JsonValue, ss58_prefix: Option) -> JsonValue { - match value { - JsonValue::Number(n) => JsonValue::String(n.to_string()), - JsonValue::Array(arr) => { - let is_byte_array = arr.len() > 1 - && arr.iter().all(|v| match v { - JsonValue::Number(n) => n.as_u64().is_some_and(|val| val <= 255), - _ => false, - }); - - if is_byte_array { - let bytes: Vec = arr - .iter() - .filter_map(|v| v.as_u64().map(|n| n as u8)) - .collect(); - JsonValue::String(format!("0x{}", hex::encode(&bytes))) - } else { - let converted: Vec = arr - .into_iter() - .map(|v| transform_json_unified(v, ss58_prefix)) - .collect(); - match converted.len() { - 1 => match converted.into_iter().next() { - Some(v) => v, - None => JsonValue::Array(vec![]), - }, - _ => JsonValue::Array(converted), - } - } - } - JsonValue::Object(map) => { - if let Some(JsonValue::Array(bits)) = map.get("__bitvec__values__") { - let mut bytes = Vec::new(); - let mut current_byte = 0u8; - for (i, bit) in bits.iter().enumerate() { - if let Some(true) = bit.as_bool() { - current_byte |= 1 << (i % 8); - } - if (i + 1) % 8 == 0 { - bytes.push(current_byte); - current_byte = 0; - } - } - if bits.len() % 8 != 0 { - bytes.push(current_byte); - } - return JsonValue::String(format!("0x{}", hex::encode(&bytes))); - } - - let transformed: serde_json::Map = map - .into_iter() - .map(|(key, val)| { - let camel_key = key.to_lower_camel_case(); - (camel_key, transform_json_unified(val, ss58_prefix)) - }) - .collect(); - JsonValue::Object(transformed) - } - JsonValue::String(s) => { - if let Some(prefix) = ss58_prefix - && s.starts_with("0x") - && (s.len() == 66 || s.len() == 68) - && let Some(ss58_addr) = crate::utils::decode_address_to_ss58(&s, prefix) - { - return JsonValue::String(ss58_addr); - } - JsonValue::String(s) - } - other => other, - } -} - -/// Convert AccountId32 (as hex or array) to SS58 format -pub fn try_convert_accountid_to_ss58(value: &JsonValue, ss58_prefix: u16) -> Option { - if let Some(hex_str) = value.as_str() - && hex_str.starts_with("0x") - && hex_str.len() == 66 - { - match hex::decode(&hex_str[2..]) { - Ok(bytes) if bytes.len() == 32 => { - let mut arr = [0u8; 32]; - arr.copy_from_slice(&bytes); - let account_id = AccountId32::from(arr); - let ss58 = account_id.to_ss58check_with_version(ss58_prefix.into()); - return Some(JsonValue::String(ss58)); - } - _ => {} - } - } - - if let Some(arr) = value.as_array() - && arr.len() == 32 - { - let mut bytes = [0u8; 32]; - for (i, val) in arr.iter().enumerate() { - if let Some(byte) = val.as_u64() { - bytes[i] = byte as u8; - } else { - return None; - } - } - let account_id = AccountId32::from(bytes); - let ss58 = account_id.to_ss58check_with_version(ss58_prefix.into()); - return Some(JsonValue::String(ss58)); - } - - None -} diff --git a/crates/server/src/handlers/blocks/decode/mod.rs b/crates/server/src/handlers/blocks/decode/mod.rs index 50acd1b2..7bfa2678 100644 --- a/crates/server/src/handlers/blocks/decode/mod.rs +++ b/crates/server/src/handlers/blocks/decode/mod.rs @@ -9,12 +9,12 @@ //! into JSON. It is separate from `processing/` because decoding requires specialized //! visitor patterns and type-aware logic that differs based on the data source: //! -//! - **Extrinsic args** use `JsonVisitor` (type-aware at decode time) -//! - **Events** use `EventsVisitor` + post-processing transforms (different JSON format) +//! - **Extrinsic args** use `JsonVisitor`/`CallArgsVisitor` (type-aware at decode time) +//! - **Events** use `EventsVisitor` + `EventJsonVisitor` (type-aware at decode time) //! - **XCM messages** use `scale_value` + registry-aware conversion (different decode path) //! -//! Each decoder produces different JSON output formats to match substrate-api-sidecar's -//! API compatibility requirements. +//! All decoders use `ScaleVisitor` from `args.rs` for type-aware JSON serialization, +//! with different const generic parameters for field casing and enum variant handling. pub mod args; pub mod events; @@ -22,10 +22,7 @@ pub mod type_name; pub mod xcm; // Re-export commonly used types -pub use args::JsonVisitor; -pub use events::{ - EventField, EventInfo, EventPhase, EventsVisitor, convert_bytes_to_hex, transform_json_unified, - try_convert_accountid_to_ss58, -}; +pub use args::{EventJsonVisitor, JsonVisitor}; +pub use events::{EventInfo, EventPhase, EventsVisitor}; pub use type_name::GetTypeName; pub use xcm::XcmDecoder; diff --git a/crates/server/src/handlers/blocks/processing/events.rs b/crates/server/src/handlers/blocks/processing/events.rs index a4ec4709..4a0ee918 100644 --- a/crates/server/src/handlers/blocks/processing/events.rs +++ b/crates/server/src/handlers/blocks/processing/events.rs @@ -17,16 +17,31 @@ use crate::state::AppState; use serde_json::Value; use super::super::common::BlockClient; -use super::super::decode::{ - EventPhase as VisitorEventPhase, EventsVisitor, convert_bytes_to_hex, transform_json_unified, - try_convert_accountid_to_ss58, -}; +use super::super::decode::{EventPhase as VisitorEventPhase, EventsVisitor}; use super::super::types::{ ActualWeight, Event, EventPhase, ExtrinsicOutcome, GetBlockError, MethodInfo, OnFinalize, OnInitialize, ParsedEvent, }; use super::super::utils::extract_number_as_string; +/// Recursively unwrap single-element arrays in a JSON value +fn unwrap_single_element_arrays(value: Value) -> Value { + match value { + Value::Array(arr) if arr.len() == 1 => { + unwrap_single_element_arrays(arr.into_iter().next().unwrap_or(Value::Array(vec![]))) + } + Value::Array(arr) => { + Value::Array(arr.into_iter().map(unwrap_single_element_arrays).collect()) + } + Value::Object(map) => Value::Object( + map.into_iter() + .map(|(k, v)| (k, unwrap_single_element_arrays(v))) + .collect(), + ), + other => other, + } +} + /// Extract `paysFee` value from DispatchInfo in event data /// /// DispatchInfo contains: { weight, class, paysFee } @@ -232,10 +247,11 @@ async fn fetch_block_events_impl( let addr = subxt::dynamic::storage::<(), ()>("System", "Events"); let events_value = client_at_block.storage().fetch(addr, ()).await?; - // Decode events once using the visitor pattern which provides all needed data: - // phase, pallet_name, event_name, and typed fields + // Decode events using the visitor pattern with type-aware field decoding. + // EventJsonVisitor handles all transformations at decode time: + // AccountId32 → SS58, numbers → strings, camelCase keys, byte arrays → hex. let events_with_types = events_value - .visit(EventsVisitor::new(resolver)) + .visit(EventsVisitor::new(ss58_prefix, resolver)) .map_err(|e| { tracing::warn!( "Failed to decode events for block {}: {:?}", @@ -256,40 +272,10 @@ async fn fetch_block_events_impl( VisitorEventPhase::Finalization => EventPhase::Finalization, }; - // Use the visitor's field values which have proper type-level enum serialization - // (basic enums as strings, non-basic enums as objects) let event_data: Vec = event_info .fields .into_iter() - .map(|event_field| { - let json_value = event_field.value; - let type_name = event_field.type_name; - let type_name_ref = type_name.as_deref(); - - if let Some(tn) = type_name_ref { - if tn == "AccountId32" || tn == "MultiAddress" || tn == "AccountId" { - let with_hex = convert_bytes_to_hex(json_value.clone()); - if let Some(ss58_value) = - try_convert_accountid_to_ss58(&with_hex, ss58_prefix) - { - return ss58_value; - } - } else if tn == "RewardDestination" - && let Some(account_value) = json_value.get("account") - { - let with_hex = convert_bytes_to_hex(account_value.clone()); - if let Some(ss58_value) = - try_convert_accountid_to_ss58(&with_hex, ss58_prefix) - { - return serde_json::json!({ - "account": ss58_value - }); - } - } - } - // Apply remaining transformations (bytes to hex, numbers to strings, camelCase keys) - transform_json_unified(json_value, None) - }) + .map(unwrap_single_element_arrays) .collect(); parsed_events.push(ParsedEvent { diff --git a/crates/server/src/handlers/blocks/processing/extrinsics.rs b/crates/server/src/handlers/blocks/processing/extrinsics.rs index f7461574..e54344aa 100644 --- a/crates/server/src/handlers/blocks/processing/extrinsics.rs +++ b/crates/server/src/handlers/blocks/processing/extrinsics.rs @@ -128,7 +128,7 @@ async fn extract_extrinsics_impl( for field in extrinsic.iter_call_data_fields() { let field_name = field.name(); // Keep field names as-is (snake_case from SCALE metadata) - // Only nested object keys are transformed to camelCase via transform_json_unified + // Only nested object keys are transformed to camelCase via ScaleVisitor let field_key = field_name.to_string(); // Use the visitor pattern to get type information