diff --git a/examples/ore-server/Cargo.lock b/examples/ore-server/Cargo.lock index 4a61a85d..389a3550 100644 --- a/examples/ore-server/Cargo.lock +++ b/examples/ore-server/Cargo.lock @@ -1448,7 +1448,7 @@ dependencies = [ [[package]] name = "hyperstack" -version = "0.5.5" +version = "0.5.8" dependencies = [ "anyhow", "bs58", @@ -1475,17 +1475,18 @@ dependencies = [ [[package]] name = "hyperstack-idl" -version = "0.1.2" +version = "0.1.5" dependencies = [ "serde", "serde_json", "sha2 0.10.9", "strsim", + "tracing", ] [[package]] name = "hyperstack-interpreter" -version = "0.5.5" +version = "0.5.8" dependencies = [ "bs58", "dashmap", @@ -1512,7 +1513,7 @@ dependencies = [ [[package]] name = "hyperstack-macros" -version = "0.5.5" +version = "0.5.8" dependencies = [ "bs58", "hex", @@ -1527,7 +1528,7 @@ dependencies = [ [[package]] name = "hyperstack-sdk" -version = "0.5.5" +version = "0.5.6" dependencies = [ "anyhow", "flate2", @@ -1544,7 +1545,7 @@ dependencies = [ [[package]] name = "hyperstack-server" -version = "0.5.5" +version = "0.5.8" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/hyperstack-macros/src/stream_spec/entity.rs b/hyperstack-macros/src/stream_spec/entity.rs index 80da9d67..470a7610 100644 --- a/hyperstack-macros/src/stream_spec/entity.rs +++ b/hyperstack-macros/src/stream_spec/entity.rs @@ -1053,6 +1053,7 @@ fn generate_computed_fields_hook( }).collect(); quote! { + #[allow(clippy::too_many_arguments)] fn #eval_fn_name( section_obj: &mut hyperstack::runtime::serde_json::Map, section_parent_state: &hyperstack::runtime::serde_json::Value, @@ -1060,31 +1061,6 @@ fn generate_computed_fields_hook( __context_timestamp: i64, #(#cross_section_params),* ) -> Result<(), Box> { - // Create local bindings for all fields in the current section - // Helper macro to get field values with proper type inference - macro_rules! extract_field { - ($name:ident, $ty:ty) => { - let $name: Option<$ty> = section_obj - .get(stringify!($name)) - .and_then(|v| hyperstack::runtime::serde_json::from_value(v.clone()).ok()); - }; - } - - // Extract all numeric/common fields that might be referenced - extract_field!(total_buy_volume, u64); - extract_field!(total_sell_volume, u64); - extract_field!(total_trades, u64); - extract_field!(total_volume, u64); - extract_field!(buy_count, u64); - extract_field!(sell_count, u64); - extract_field!(unique_traders, u64); - extract_field!(largest_trade, u64); - extract_field!(smallest_trade, u64); - extract_field!(last_trade_timestamp, i64); - extract_field!(last_trade_price, f64); - extract_field!(whale_trade_count, u64); - extract_field!(average_trade_size, f64); - // Initialize cache with current section values for intra-section computed field dependencies let mut computed_cache: std::collections::HashMap = std::collections::HashMap::new(); for (key, value) in section_obj.iter() { diff --git a/interpreter/src/resolvers.rs b/interpreter/src/resolvers.rs index d3667bd3..adf3d828 100644 --- a/interpreter/src/resolvers.rs +++ b/interpreter/src/resolvers.rs @@ -759,7 +759,12 @@ impl SlotHashResolver { return Ok(Value::Null); } - let slot_hash = Self::json_array_to_bytes(&args[0], 32); + // slot_hash() returns { bytes: [...] }, so extract the bytes array + let slot_hash_bytes = match &args[0] { + Value::Object(obj) => obj.get("bytes").cloned().unwrap_or(Value::Null), + _ => args[0].clone(), + }; + let slot_hash = Self::json_array_to_bytes(&slot_hash_bytes, 32); let seed = Self::json_array_to_bytes(&args[1], 32); let samples = match &args[2] { Value::Number(n) => n.as_u64(), @@ -788,7 +793,7 @@ impl SlotHashResolver { let r4 = u64::from_le_bytes(hash[24..32].try_into()?); let rng = r1 ^ r2 ^ r3 ^ r4; - Ok(Value::String(rng.to_string())) + Ok(Value::Number(serde_json::Number::from(rng))) } /// Extract a byte array of expected length from a JSON array value. diff --git a/interpreter/src/rust.rs b/interpreter/src/rust.rs index 37e50fdd..851346d7 100644 --- a/interpreter/src/rust.rs +++ b/interpreter/src/rust.rs @@ -139,7 +139,8 @@ pub use hyperstack_sdk::{{ConnectionState, HyperStack, Stack, Update, Views}}; fn generate_types_rs(&self) -> String { let mut output = String::new(); - output.push_str("use serde::{Deserialize, Serialize};\n\n"); + output.push_str("use serde::{Deserialize, Serialize};\n"); + output.push_str("use hyperstack_sdk::serde_utils;\n\n"); let mut generated = HashSet::new(); @@ -170,10 +171,11 @@ pub use hyperstack_sdk::{{ConnectionState, HyperStack, Stack, Update, Views}}; } let field_name = to_snake_case(&field.field_name); let rust_type = self.field_type_to_rust(field); + let serde_attr = self.serde_attr_for_field(field); fields.push(format!( - " #[serde(default)]\n pub {}: {},", - field_name, rust_type + " {}\n pub {}: {},", + serde_attr, field_name, rust_type )); } @@ -212,9 +214,10 @@ pub use hyperstack_sdk::{{ConnectionState, HyperStack, Stack, Update, Views}}; } let field_name = to_snake_case(&field.field_name); let rust_type = self.field_type_to_rust(field); + let serde_attr = self.serde_attr_for_field(field); fields.push(format!( - " #[serde(default)]\n pub {}: {},", - field_name, rust_type + " {}\n pub {}: {},", + serde_attr, field_name, rust_type )); } } @@ -266,8 +269,10 @@ pub use hyperstack_sdk::{{ConnectionState, HyperStack, Stack, Update, Views}}; .iter() .map(|f| { let rust_type = self.resolved_field_to_rust(f); + let serde_attr = self.serde_attr_for_resolved_field(f); format!( - " #[serde(default)]\n pub {}: {},", + " {}\n pub {}: {},", + serde_attr, to_snake_case(&f.field_name), rust_type ) @@ -287,7 +292,7 @@ pub use hyperstack_sdk::{{ConnectionState, HyperStack, Stack, Update, Views}}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EventWrapper { - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_i64")] pub timestamp: i64, pub data: T, #[serde(default)] @@ -508,6 +513,72 @@ impl {entity_name}EntityViews {{ } } + /// Return the `#[serde(...)]` attribute for a field. + /// Integer fields get a `deserialize_with` pointing to the appropriate + /// `serde_utils` function so that string-encoded big integers are handled. + fn serde_attr_for_field(&self, field: &FieldTypeInfo) -> String { + if let Some(deser_fn) = self.deserialize_with_for_type( + &field.base_type, + field.is_optional, + field.is_array && !matches!(field.base_type, BaseType::Array), + &field.rust_type_name, + ) { + format!("#[serde(default, deserialize_with = \"{}\")]", deser_fn) + } else { + "#[serde(default)]".to_string() + } + } + + /// Same as `serde_attr_for_field` but for resolved struct fields. + fn serde_attr_for_resolved_field(&self, field: &ResolvedField) -> String { + if let Some(deser_fn) = self.deserialize_with_for_type( + &field.base_type, + field.is_optional, + field.is_array, + &field.field_type, + ) { + format!("#[serde(default, deserialize_with = \"{}\")]", deser_fn) + } else { + "#[serde(default)]".to_string() + } + } + + /// Determine the appropriate `serde_utils::deserialize_*` function for a + /// given type combination, or `None` if no custom deserializer is needed. + fn deserialize_with_for_type( + &self, + base_type: &BaseType, + is_optional: bool, + is_array: bool, + rust_type_name: &str, + ) -> Option { + // Only integer and timestamp types need the string-or-number treatment + let int_kind = match base_type { + BaseType::Integer => { + if rust_type_name.contains("i64") { + "i64" + } else if rust_type_name.contains("i32") { + "i32" + } else if rust_type_name.contains("u32") { + "u32" + } else { + "u64" + } + } + BaseType::Timestamp => "i64", + _ => return None, + }; + + let fn_name = match (is_optional, is_array) { + (false, false) => format!("serde_utils::deserialize_option_{}", int_kind), + (true, false) => format!("serde_utils::deserialize_option_option_{}", int_kind), + (false, true) => format!("serde_utils::deserialize_option_vec_{}", int_kind), + (true, true) => format!("serde_utils::deserialize_option_option_vec_{}", int_kind), + }; + + Some(fn_name) + } + fn resolved_field_to_rust(&self, field: &ResolvedField) -> String { let base = self.base_type_to_rust(&field.base_type, &field.field_type); @@ -638,7 +709,8 @@ fn generate_stack_types_rs( entity_names: &[String], ) -> String { let mut output = String::new(); - output.push_str("use serde::{Deserialize, Serialize};\n\n"); + output.push_str("use serde::{Deserialize, Serialize};\n"); + output.push_str("use hyperstack_sdk::serde_utils;\n\n"); let mut generated = HashSet::new(); @@ -673,7 +745,7 @@ fn generate_stack_types_rs( r#" #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EventWrapper { - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_i64")] pub timestamp: i64, pub data: T, #[serde(default)] diff --git a/interpreter/src/vm.rs b/interpreter/src/vm.rs index fcc4d67c..59bbd2d8 100644 --- a/interpreter/src/vm.rs +++ b/interpreter/src/vm.rs @@ -4386,7 +4386,9 @@ impl VmContext { let arr: [u8; 8] = byte_vec[..8] .try_into() .map_err(|_| "Failed to convert to [u8; 8]")?; - Ok(Value::String(u64::from_le_bytes(arr).to_string())) + Ok(Value::Number(serde_json::Number::from(u64::from_le_bytes( + arr, + )))) } ComputedExpr::U64FromBeBytes { bytes } => { @@ -4402,7 +4404,9 @@ impl VmContext { let arr: [u8; 8] = byte_vec[..8] .try_into() .map_err(|_| "Failed to convert to [u8; 8]")?; - Ok(Value::String(u64::from_be_bytes(arr).to_string())) + Ok(Value::Number(serde_json::Number::from(u64::from_be_bytes( + arr, + )))) } ComputedExpr::ByteArray { bytes } => { diff --git a/rust/hyperstack-sdk/src/lib.rs b/rust/hyperstack-sdk/src/lib.rs index 5b5eca56..c44ecafd 100644 --- a/rust/hyperstack-sdk/src/lib.rs +++ b/rust/hyperstack-sdk/src/lib.rs @@ -23,6 +23,7 @@ mod entity; mod error; mod frame; pub mod prelude; +pub mod serde_utils; mod store; mod stream; mod subscription; diff --git a/rust/hyperstack-sdk/src/serde_utils.rs b/rust/hyperstack-sdk/src/serde_utils.rs new file mode 100644 index 00000000..b4f95372 --- /dev/null +++ b/rust/hyperstack-sdk/src/serde_utils.rs @@ -0,0 +1,811 @@ +//! Serde helpers for deserializing integers that may arrive as JSON strings. +//! +//! The HyperStack server converts u64 values exceeding JavaScript's +//! `Number.MAX_SAFE_INTEGER` (2^53 - 1) to strings for JSON transport. +//! These helpers allow the Rust SDK to transparently parse both formats. +//! +//! Each function is designed for use with `#[serde(deserialize_with = "...")]`. + +use serde::de::{self, Deserializer, SeqAccess, Visitor}; +use std::fmt; + +// ─── Core visitors ────────────────────────────────────────────────────────── + +struct U64OrStringVisitor; + +impl<'de> Visitor<'de> for U64OrStringVisitor { + type Value = u64; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("u64 or string-encoded u64") + } + + fn visit_u64(self, v: u64) -> Result { + Ok(v) + } + + fn visit_i64(self, v: i64) -> Result { + u64::try_from(v).map_err(|_| E::custom(format!("negative value {v} cannot be u64"))) + } + + fn visit_f64(self, v: f64) -> Result { + if v >= 0.0 && v <= u64::MAX as f64 { + Ok(v as u64) + } else { + Err(E::custom(format!("f64 {v} out of u64 range"))) + } + } + + fn visit_str(self, v: &str) -> Result { + v.parse().map_err(E::custom) + } +} + +struct I64OrStringVisitor; + +impl<'de> Visitor<'de> for I64OrStringVisitor { + type Value = i64; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("i64 or string-encoded i64") + } + + fn visit_u64(self, v: u64) -> Result { + i64::try_from(v).map_err(|_| E::custom(format!("u64 {v} overflows i64"))) + } + + fn visit_i64(self, v: i64) -> Result { + Ok(v) + } + + fn visit_f64(self, v: f64) -> Result { + if v >= i64::MIN as f64 && v <= i64::MAX as f64 { + Ok(v as i64) + } else { + Err(E::custom(format!("f64 {v} out of i64 range"))) + } + } + + fn visit_str(self, v: &str) -> Result { + v.parse().map_err(E::custom) + } +} + +// ─── Bare types ───────────────────────────────────────────────────────────── + +/// Deserialize a bare `u64` from a JSON number or string. +pub fn deserialize_u64<'de, D: Deserializer<'de>>(d: D) -> Result { + d.deserialize_any(U64OrStringVisitor) +} + +/// Deserialize a bare `i64` from a JSON number or string. +pub fn deserialize_i64<'de, D: Deserializer<'de>>(d: D) -> Result { + d.deserialize_any(I64OrStringVisitor) +} + +// ─── Option ────────────────────────────────────────────────────────────── +// Used for non-optional spec fields. `None` = not yet received in any patch. +// With `#[serde(default)]`, missing fields → None. This function is only +// called when the field IS present in the JSON (null, number, or string). + +/// Deserialize `Option` from null / number / string. +pub fn deserialize_option_u64<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + struct V; + impl<'de> Visitor<'de> for V { + type Value = Option; + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("null, u64, or string-encoded u64") + } + fn visit_unit(self) -> Result, E> { + Ok(None) + } + fn visit_none(self) -> Result, E> { + Ok(None) + } + fn visit_some>(self, d: D2) -> Result, D2::Error> { + deserialize_u64(d).map(Some) + } + } + d.deserialize_option(V) +} + +/// Deserialize `Option` from null / number / string. +pub fn deserialize_option_i64<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + struct V; + impl<'de> Visitor<'de> for V { + type Value = Option; + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("null, i64, or string-encoded i64") + } + fn visit_unit(self) -> Result, E> { + Ok(None) + } + fn visit_none(self) -> Result, E> { + Ok(None) + } + fn visit_some>(self, d: D2) -> Result, D2::Error> { + deserialize_i64(d).map(Some) + } + } + d.deserialize_option(V) +} + +// ─── Option> ───────────────────────────────────────────────────── +// Used for optional spec fields (patch semantics): +// None = field not present in patch (handled by #[serde(default)]) +// Some(None) = field explicitly set to null +// Some(Some(v))= field has value +// +// This function is only called when the field IS present, so: +// JSON null → Some(None) +// JSON number → Some(Some(n)) +// JSON string → Some(Some(parse(s))) + +/// Deserialize `Option>` for patch semantics. +pub fn deserialize_option_option_u64<'de, D: Deserializer<'de>>( + d: D, +) -> Result>, D::Error> { + struct V; + impl<'de> Visitor<'de> for V { + type Value = Option>; + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("null, u64, or string-encoded u64") + } + fn visit_unit(self) -> Result>, E> { + Ok(Some(None)) + } + fn visit_u64(self, v: u64) -> Result>, E> { + Ok(Some(Some(v))) + } + fn visit_i64(self, v: i64) -> Result>, E> { + u64::try_from(v) + .map(|v| Some(Some(v))) + .map_err(|_| E::custom(format!("negative value {v} cannot be u64"))) + } + fn visit_f64(self, v: f64) -> Result>, E> { + if v >= 0.0 && v < (u64::MAX as f64) { + Ok(Some(Some(v as u64))) + } else { + Err(E::custom(format!("f64 {v} out of u64 range"))) + } + } + fn visit_str(self, v: &str) -> Result>, E> { + v.parse().map(|v| Some(Some(v))).map_err(E::custom) + } + } + d.deserialize_any(V) +} + +/// Deserialize `Option>` for patch semantics. +pub fn deserialize_option_option_i64<'de, D: Deserializer<'de>>( + d: D, +) -> Result>, D::Error> { + struct V; + impl<'de> Visitor<'de> for V { + type Value = Option>; + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("null, i64, or string-encoded i64") + } + fn visit_unit(self) -> Result>, E> { + Ok(Some(None)) + } + fn visit_u64(self, v: u64) -> Result>, E> { + i64::try_from(v) + .map(|v| Some(Some(v))) + .map_err(|_| E::custom(format!("u64 {v} overflows i64"))) + } + fn visit_i64(self, v: i64) -> Result>, E> { + Ok(Some(Some(v))) + } + fn visit_f64(self, v: f64) -> Result>, E> { + Ok(Some(Some(v as i64))) + } + fn visit_str(self, v: &str) -> Result>, E> { + v.parse().map(|v| Some(Some(v))).map_err(E::custom) + } + } + d.deserialize_any(V) +} + +// ─── Vec variants ──────────────────────────────────────────────────────── +// For array fields where elements may be numbers or strings. + +/// Deserialize `Option>` where each element may be a number or string. +pub fn deserialize_option_vec_u64<'de, D: Deserializer<'de>>( + d: D, +) -> Result>, D::Error> { + struct V; + impl<'de> Visitor<'de> for V { + type Value = Option>; + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("null or array of u64/string-encoded u64") + } + fn visit_unit(self) -> Result>, E> { + Ok(None) + } + fn visit_none(self) -> Result>, E> { + Ok(None) + } + fn visit_some>(self, d: D2) -> Result>, D2::Error> { + struct SeqV; + impl<'de> Visitor<'de> for SeqV { + type Value = Vec; + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("array of u64/string-encoded u64") + } + fn visit_seq>(self, mut seq: A) -> Result, A::Error> { + let mut vec = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(elem) = seq.next_element::()? { + let n = match &elem { + serde_json::Value::Number(n) => n + .as_u64() + .or_else(|| n.as_i64().and_then(|i| u64::try_from(i).ok())) + .ok_or_else(|| { + de::Error::custom(format!("cannot convert {n} to u64")) + })?, + serde_json::Value::String(s) => { + s.parse::().map_err(de::Error::custom)? + } + other => { + return Err(de::Error::custom(format!( + "expected number or string in array, got {other}" + ))); + } + }; + vec.push(n); + } + Ok(vec) + } + } + d.deserialize_seq(SeqV).map(Some) + } + } + d.deserialize_option(V) +} + +/// Deserialize `Option>>` for optional array fields (patch semantics). +pub fn deserialize_option_option_vec_u64<'de, D: Deserializer<'de>>( + d: D, +) -> Result>>, D::Error> { + struct V; + impl<'de> Visitor<'de> for V { + type Value = Option>>; + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("null or array of u64/string-encoded u64") + } + fn visit_unit(self) -> Result>>, E> { + Ok(Some(None)) + } + fn visit_seq>( + self, + mut seq: A, + ) -> Result>>, A::Error> { + let mut vec = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(elem) = seq.next_element::()? { + let n = match &elem { + serde_json::Value::Number(n) => n + .as_u64() + .or_else(|| n.as_i64().and_then(|i| u64::try_from(i).ok())) + .ok_or_else(|| de::Error::custom(format!("cannot convert {n} to u64")))?, + serde_json::Value::String(s) => s.parse::().map_err(de::Error::custom)?, + other => { + return Err(de::Error::custom(format!( + "expected number or string in array, got {other}" + ))); + } + }; + vec.push(n); + } + Ok(Some(Some(vec))) + } + } + d.deserialize_any(V) +} + +/// Deserialize `Option>` where each element may be a number or string. +pub fn deserialize_option_vec_i64<'de, D: Deserializer<'de>>( + d: D, +) -> Result>, D::Error> { + struct V; + impl<'de> Visitor<'de> for V { + type Value = Option>; + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("null or array of i64/string-encoded i64") + } + fn visit_unit(self) -> Result>, E> { + Ok(None) + } + fn visit_none(self) -> Result>, E> { + Ok(None) + } + fn visit_some>(self, d: D2) -> Result>, D2::Error> { + struct SeqV; + impl<'de> Visitor<'de> for SeqV { + type Value = Vec; + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("array of i64/string-encoded i64") + } + fn visit_seq>(self, mut seq: A) -> Result, A::Error> { + let mut vec = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(elem) = seq.next_element::()? { + let n = match &elem { + serde_json::Value::Number(n) => n.as_i64().ok_or_else(|| { + de::Error::custom(format!("cannot convert {n} to i64")) + })?, + serde_json::Value::String(s) => { + s.parse::().map_err(de::Error::custom)? + } + other => { + return Err(de::Error::custom(format!( + "expected number or string in array, got {other}" + ))); + } + }; + vec.push(n); + } + Ok(vec) + } + } + d.deserialize_seq(SeqV).map(Some) + } + } + d.deserialize_option(V) +} + +/// Deserialize `Option>>` for optional array fields (patch semantics). +pub fn deserialize_option_option_vec_i64<'de, D: Deserializer<'de>>( + d: D, +) -> Result>>, D::Error> { + struct V; + impl<'de> Visitor<'de> for V { + type Value = Option>>; + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("null or array of i64/string-encoded i64") + } + fn visit_unit(self) -> Result>>, E> { + Ok(Some(None)) + } + fn visit_seq>( + self, + mut seq: A, + ) -> Result>>, A::Error> { + let mut vec = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(elem) = seq.next_element::()? { + let n = match &elem { + serde_json::Value::Number(n) => n + .as_i64() + .ok_or_else(|| de::Error::custom(format!("cannot convert {n} to i64")))?, + serde_json::Value::String(s) => s.parse::().map_err(de::Error::custom)?, + other => { + return Err(de::Error::custom(format!( + "expected number or string in array, got {other}" + ))); + } + }; + vec.push(n); + } + Ok(Some(Some(vec))) + } + } + d.deserialize_any(V) +} + +// ─── 32-bit narrowing helpers ─────────────────────────────────────────────── +// Delegate to the 64-bit deserializers above, then narrow via TryFrom. +// This avoids duplicating all the visitor boilerplate for i32/u32. + +fn narrow_opt(opt: Option) -> Result, E> +where + N: TryFrom, + N::Error: fmt::Display, +{ + opt.map(|v| N::try_from(v).map_err(E::custom)).transpose() +} + +fn narrow_opt_opt(opt: Option>) -> Result>, E> +where + N: TryFrom, + N::Error: fmt::Display, +{ + match opt { + None => Ok(None), + Some(None) => Ok(Some(None)), + Some(Some(v)) => N::try_from(v).map(|n| Some(Some(n))).map_err(E::custom), + } +} + +fn narrow_opt_vec(opt: Option>) -> Result>, E> +where + N: TryFrom, + N::Error: fmt::Display, +{ + opt.map(|vec| { + vec.into_iter() + .map(|v| N::try_from(v).map_err(E::custom)) + .collect() + }) + .transpose() +} + +fn narrow_opt_opt_vec( + opt: Option>>, +) -> Result>>, E> +where + N: TryFrom, + N::Error: fmt::Display, +{ + match opt { + None => Ok(None), + Some(None) => Ok(Some(None)), + Some(Some(vec)) => vec + .into_iter() + .map(|v| N::try_from(v).map_err(E::custom)) + .collect::, E>>() + .map(|v| Some(Some(v))), + } +} + +// ─── Option ──────────────────────────────────────────────────────── + +pub fn deserialize_option_u32<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + narrow_opt(deserialize_option_u64(d)?) +} + +pub fn deserialize_option_i32<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + narrow_opt(deserialize_option_i64(d)?) +} + +// ─── Option> ──────────────────────────────────────────────── + +pub fn deserialize_option_option_u32<'de, D: Deserializer<'de>>( + d: D, +) -> Result>, D::Error> { + narrow_opt_opt(deserialize_option_option_u64(d)?) +} + +pub fn deserialize_option_option_i32<'de, D: Deserializer<'de>>( + d: D, +) -> Result>, D::Error> { + narrow_opt_opt(deserialize_option_option_i64(d)?) +} + +// ─── Option> ─────────────────────────────────────────────────── + +pub fn deserialize_option_vec_u32<'de, D: Deserializer<'de>>( + d: D, +) -> Result>, D::Error> { + narrow_opt_vec(deserialize_option_vec_u64(d)?) +} + +pub fn deserialize_option_vec_i32<'de, D: Deserializer<'de>>( + d: D, +) -> Result>, D::Error> { + narrow_opt_vec(deserialize_option_vec_i64(d)?) +} + +// ─── Option>> ─────────────────────────────────────────── + +pub fn deserialize_option_option_vec_u32<'de, D: Deserializer<'de>>( + d: D, +) -> Result>>, D::Error> { + narrow_opt_opt_vec(deserialize_option_option_vec_u64(d)?) +} + +pub fn deserialize_option_option_vec_i32<'de, D: Deserializer<'de>>( + d: D, +) -> Result>>, D::Error> { + narrow_opt_opt_vec(deserialize_option_option_vec_i64(d)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::Deserialize; + + #[derive(Deserialize, Debug, PartialEq)] + struct TestBare { + #[serde(deserialize_with = "deserialize_u64")] + balance: u64, + #[serde(deserialize_with = "deserialize_i64")] + timestamp: i64, + } + + #[derive(Deserialize, Debug, PartialEq)] + struct TestOption { + #[serde(default, deserialize_with = "deserialize_option_u64")] + balance: Option, + #[serde(default, deserialize_with = "deserialize_option_i64")] + timestamp: Option, + } + + #[derive(Deserialize, Debug, PartialEq)] + struct TestOptionOption { + #[serde(default, deserialize_with = "deserialize_option_option_u64")] + balance: Option>, + #[serde(default, deserialize_with = "deserialize_option_option_i64")] + timestamp: Option>, + } + + #[derive(Deserialize, Debug, PartialEq)] + struct TestVec { + #[serde(default, deserialize_with = "deserialize_option_vec_u64")] + values: Option>, + } + + #[derive(Deserialize, Debug, PartialEq)] + struct TestOptionOptionVec { + #[serde(default, deserialize_with = "deserialize_option_option_vec_u64")] + values: Option>>, + } + + #[derive(Deserialize, Debug, PartialEq)] + struct TestVecI64 { + #[serde(default, deserialize_with = "deserialize_option_vec_i64")] + values: Option>, + } + + #[derive(Deserialize, Debug, PartialEq)] + struct TestOptionOptionVecI64 { + #[serde(default, deserialize_with = "deserialize_option_option_vec_i64")] + values: Option>>, + } + + // ── Bare types ── + + #[test] + fn bare_u64_from_number() { + let v: TestBare = serde_json::from_str(r#"{"balance": 42, "timestamp": -100}"#).unwrap(); + assert_eq!(v.balance, 42); + assert_eq!(v.timestamp, -100); + } + + #[test] + fn bare_u64_from_string() { + let v: TestBare = + serde_json::from_str(r#"{"balance": "9007199254740992", "timestamp": "-100"}"#) + .unwrap(); + assert_eq!(v.balance, 9007199254740992); + assert_eq!(v.timestamp, -100); + } + + // ── Option ── + + #[test] + fn option_from_number() { + let v: TestOption = serde_json::from_str(r#"{"balance": 42, "timestamp": -100}"#).unwrap(); + assert_eq!(v.balance, Some(42)); + assert_eq!(v.timestamp, Some(-100)); + } + + #[test] + fn option_from_string() { + let v: TestOption = + serde_json::from_str(r#"{"balance": "9007199254740992", "timestamp": "123"}"#).unwrap(); + assert_eq!(v.balance, Some(9007199254740992)); + assert_eq!(v.timestamp, Some(123)); + } + + #[test] + fn option_from_null() { + let v: TestOption = + serde_json::from_str(r#"{"balance": null, "timestamp": null}"#).unwrap(); + assert_eq!(v.balance, None); + assert_eq!(v.timestamp, None); + } + + #[test] + fn option_missing_field() { + let v: TestOption = serde_json::from_str(r#"{}"#).unwrap(); + assert_eq!(v.balance, None); + assert_eq!(v.timestamp, None); + } + + // ── Option> ── + + #[test] + fn option_option_from_number() { + let v: TestOptionOption = + serde_json::from_str(r#"{"balance": 42, "timestamp": -100}"#).unwrap(); + assert_eq!(v.balance, Some(Some(42))); + assert_eq!(v.timestamp, Some(Some(-100))); + } + + #[test] + fn option_option_from_string() { + let v: TestOptionOption = + serde_json::from_str(r#"{"balance": "9007199254740992", "timestamp": "123"}"#).unwrap(); + assert_eq!(v.balance, Some(Some(9007199254740992))); + assert_eq!(v.timestamp, Some(Some(123))); + } + + #[test] + fn option_option_null_means_explicit_null() { + let v: TestOptionOption = + serde_json::from_str(r#"{"balance": null, "timestamp": null}"#).unwrap(); + assert_eq!(v.balance, Some(None)); // explicitly null + assert_eq!(v.timestamp, Some(None)); + } + + #[test] + fn option_option_missing_means_not_received() { + let v: TestOptionOption = serde_json::from_str(r#"{}"#).unwrap(); + assert_eq!(v.balance, None); // not in patch + assert_eq!(v.timestamp, None); + } + + // ── Vec variants ── + + #[test] + fn vec_mixed_numbers_and_strings() { + let v: TestVec = serde_json::from_str(r#"{"values": [1, "9007199254740992", 3]}"#).unwrap(); + assert_eq!(v.values, Some(vec![1, 9007199254740992, 3])); + } + + #[test] + fn vec_null() { + let v: TestVec = serde_json::from_str(r#"{"values": null}"#).unwrap(); + assert_eq!(v.values, None); + } + + #[test] + fn vec_missing() { + let v: TestVec = serde_json::from_str(r#"{}"#).unwrap(); + assert_eq!(v.values, None); + } + + #[test] + fn option_option_vec_from_array() { + let v: TestOptionOptionVec = + serde_json::from_str(r#"{"values": [1, "9007199254740992"]}"#).unwrap(); + assert_eq!(v.values, Some(Some(vec![1, 9007199254740992]))); + } + + #[test] + fn option_option_vec_null() { + let v: TestOptionOptionVec = serde_json::from_str(r#"{"values": null}"#).unwrap(); + assert_eq!(v.values, Some(None)); + } + + #[test] + fn option_option_vec_missing() { + let v: TestOptionOptionVec = serde_json::from_str(r#"{}"#).unwrap(); + assert_eq!(v.values, None); + } + + // ── Vec variants ── + + #[test] + fn vec_i64_mixed_numbers_and_strings() { + let v: TestVecI64 = + serde_json::from_str(r#"{"values": [-1, "9007199254740992", 3]}"#).unwrap(); + assert_eq!(v.values, Some(vec![-1, 9007199254740992, 3])); + } + + #[test] + fn vec_i64_null() { + let v: TestVecI64 = serde_json::from_str(r#"{"values": null}"#).unwrap(); + assert_eq!(v.values, None); + } + + #[test] + fn vec_i64_missing() { + let v: TestVecI64 = serde_json::from_str(r#"{}"#).unwrap(); + assert_eq!(v.values, None); + } + + #[test] + fn option_option_vec_i64_from_array() { + let v: TestOptionOptionVecI64 = + serde_json::from_str(r#"{"values": [-1, "9007199254740992"]}"#).unwrap(); + assert_eq!(v.values, Some(Some(vec![-1, 9007199254740992]))); + } + + #[test] + fn option_option_vec_i64_null() { + let v: TestOptionOptionVecI64 = serde_json::from_str(r#"{"values": null}"#).unwrap(); + assert_eq!(v.values, Some(None)); + } + + #[test] + fn option_option_vec_i64_missing() { + let v: TestOptionOptionVecI64 = serde_json::from_str(r#"{}"#).unwrap(); + assert_eq!(v.values, None); + } + + // ── Edge cases ── + + #[test] + fn large_u64_from_string() { + let v: TestOption = serde_json::from_str(r#"{"balance": "18446744073709551615"}"#).unwrap(); + assert_eq!(v.balance, Some(u64::MAX)); + } + + #[test] + fn u64_from_float() { + let v: TestOption = serde_json::from_str(r#"{"balance": 42.0}"#).unwrap(); + assert_eq!(v.balance, Some(42)); + } + + // ── 32-bit narrowing ── + + #[derive(Deserialize, Debug, PartialEq)] + struct TestOption32 { + #[serde(default, deserialize_with = "deserialize_option_u32")] + balance: Option, + #[serde(default, deserialize_with = "deserialize_option_i32")] + timestamp: Option, + } + + #[test] + fn option_u32_from_number() { + let v: TestOption32 = + serde_json::from_str(r#"{"balance": 42, "timestamp": -100}"#).unwrap(); + assert_eq!(v.balance, Some(42)); + assert_eq!(v.timestamp, Some(-100)); + } + + #[test] + fn option_u32_from_string() { + let v: TestOption32 = + serde_json::from_str(r#"{"balance": "1000", "timestamp": "-50"}"#).unwrap(); + assert_eq!(v.balance, Some(1000)); + assert_eq!(v.timestamp, Some(-50)); + } + + #[test] + fn option_u32_overflow_rejected() { + let r = serde_json::from_str::(r#"{"balance": 4294967296}"#); + assert!(r.is_err(), "u32 overflow should be rejected"); + } + + #[test] + fn option_i32_overflow_rejected() { + let r = serde_json::from_str::(r#"{"timestamp": 2147483648}"#); + assert!(r.is_err(), "i32 overflow should be rejected"); + } + + #[test] + fn option_u32_null_and_missing() { + let v: TestOption32 = + serde_json::from_str(r#"{"balance": null, "timestamp": null}"#).unwrap(); + assert_eq!(v.balance, None); + assert_eq!(v.timestamp, None); + let v: TestOption32 = serde_json::from_str(r#"{}"#).unwrap(); + assert_eq!(v.balance, None); + assert_eq!(v.timestamp, None); + } + + #[derive(Deserialize, Debug, PartialEq)] + struct TestOptionOption32 { + #[serde(default, deserialize_with = "deserialize_option_option_u32")] + balance: Option>, + } + + #[test] + fn option_option_u32_patch_semantics() { + let v: TestOptionOption32 = serde_json::from_str(r#"{"balance": 42}"#).unwrap(); + assert_eq!(v.balance, Some(Some(42))); + let v: TestOptionOption32 = serde_json::from_str(r#"{"balance": null}"#).unwrap(); + assert_eq!(v.balance, Some(None)); + let v: TestOptionOption32 = serde_json::from_str(r#"{}"#).unwrap(); + assert_eq!(v.balance, None); + } + + #[derive(Deserialize, Debug, PartialEq)] + struct TestVec32 { + #[serde(default, deserialize_with = "deserialize_option_vec_u32")] + values: Option>, + } + + #[test] + fn vec_u32_mixed() { + let v: TestVec32 = serde_json::from_str(r#"{"values": [1, "2", 3]}"#).unwrap(); + assert_eq!(v.values, Some(vec![1, 2, 3])); + } + + #[test] + fn vec_u32_overflow_rejected() { + let r = serde_json::from_str::(r#"{"values": [1, 4294967296]}"#); + assert!(r.is_err()); + } +} diff --git a/rust/hyperstack-server/src/projector.rs b/rust/hyperstack-server/src/projector.rs index 4d465c90..0bf0d6de 100644 --- a/rust/hyperstack-server/src/projector.rs +++ b/rust/hyperstack-server/src/projector.rs @@ -2,7 +2,7 @@ use crate::bus::{BusManager, BusMessage}; use crate::cache::EntityCache; use crate::mutation_batch::{MutationBatch, SlotContext}; use crate::view::{ViewIndex, ViewSpec}; -use crate::websocket::frame::{Frame, Mode}; +use crate::websocket::frame::{transform_large_u64_to_strings, Frame, Mode}; use bytes::Bytes; use hyperstack_interpreter::CanonicalLog; use serde_json::Value; @@ -165,7 +165,8 @@ impl Projector { patch.clone() }; - let projected = spec.projection.apply(patch_data); + let mut projected = spec.projection.apply(patch_data); + transform_large_u64_to_strings(&mut projected); let frame = Frame { mode: spec.mode, diff --git a/rust/hyperstack-server/src/websocket/frame.rs b/rust/hyperstack-server/src/websocket/frame.rs index b2c99b24..8196a6c7 100644 --- a/rust/hyperstack-server/src/websocket/frame.rs +++ b/rust/hyperstack-server/src/websocket/frame.rs @@ -93,6 +93,39 @@ fn default_complete() -> bool { true } +/// Transform large u64 values to strings for JavaScript compatibility. +/// JavaScript's Number.MAX_SAFE_INTEGER is 2^53 - 1 (9007199254740991). +/// Values larger than this will lose precision in JavaScript. +pub fn transform_large_u64_to_strings(value: &mut serde_json::Value) { + const MAX_SAFE_INTEGER: u64 = 9007199254740991; // 2^53 - 1 + + match value { + serde_json::Value::Object(map) => { + for (_, v) in map.iter_mut() { + transform_large_u64_to_strings(v); + } + } + serde_json::Value::Array(arr) => { + for v in arr.iter_mut() { + transform_large_u64_to_strings(v); + } + } + serde_json::Value::Number(n) => { + if let Some(n_u64) = n.as_u64() { + if n_u64 > MAX_SAFE_INTEGER { + *value = serde_json::Value::String(n_u64.to_string()); + } + } else if let Some(n_i64) = n.as_i64() { + const MIN_SAFE_INTEGER: i64 = -(MAX_SAFE_INTEGER as i64); + if n_i64 < MIN_SAFE_INTEGER { + *value = serde_json::Value::String(n_i64.to_string()); + } + } + } + _ => {} + } +} + impl Frame { pub fn entity(&self) -> &str { &self.export diff --git a/rust/hyperstack-server/src/websocket/server.rs b/rust/hyperstack-server/src/websocket/server.rs index a3fce0dc..39f9efda 100644 --- a/rust/hyperstack-server/src/websocket/server.rs +++ b/rust/hyperstack-server/src/websocket/server.rs @@ -4,7 +4,8 @@ use crate::compression::maybe_compress; use crate::view::{ViewIndex, ViewSpec}; use crate::websocket::client_manager::ClientManager; use crate::websocket::frame::{ - Frame, Mode, SnapshotEntity, SnapshotFrame, SortConfig, SortOrder, SubscribedFrame, + transform_large_u64_to_strings, Frame, Mode, SnapshotEntity, SnapshotFrame, SortConfig, + SortOrder, SubscribedFrame, }; use crate::websocket::subscription::{ClientMessage, Subscription}; use anyhow::Result; @@ -580,7 +581,8 @@ async fn attach_client_to_bus( let mut rx = ctx.bus_manager.get_or_create_state_bus(view_id, key).await; - if let Some(cached_entity) = ctx.entity_cache.get(view_id, key).await { + if let Some(mut cached_entity) = ctx.entity_cache.get(view_id, key).await { + transform_large_u64_to_strings(&mut cached_entity); let snapshot_entities = vec![SnapshotEntity { key: key.to_string(), data: cached_entity, @@ -641,7 +643,10 @@ async fn attach_client_to_bus( let snapshot_entities: Vec = snapshots .into_iter() .filter(|(key, _)| subscription.matches_key(key)) - .map(|(key, data)| SnapshotEntity { key, data }) + .map(|(key, mut data)| { + transform_large_u64_to_strings(&mut data); + SnapshotEntity { key, data } + }) .collect(); if !snapshot_entities.is_empty() { @@ -750,7 +755,10 @@ async fn attach_derived_view_subscription_otel( if !initial_window.is_empty() { let snapshot_entities: Vec = initial_window .into_iter() - .map(|(key, data)| SnapshotEntity { key, data }) + .map(|(key, mut data)| { + transform_large_u64_to_strings(&mut data); + SnapshotEntity { key, data } + }) .collect(); let batch_config = ctx.entity_cache.snapshot_config(); @@ -830,12 +838,14 @@ async fn attach_derived_view_subscription_otel( } } + let mut transformed_data = data.clone(); + transform_large_u64_to_strings(&mut transformed_data); let frame = Frame { mode: frame_mode, export: view_id_clone.clone(), op: "upsert", key: new_key.clone(), - data: data.clone(), + data: transformed_data, append: vec![], }; @@ -871,12 +881,14 @@ async fn attach_derived_view_subscription_otel( } for (key, data) in &new_window { + let mut transformed_data = data.clone(); + transform_large_u64_to_strings(&mut transformed_data); let frame = Frame { mode: frame_mode, export: view_id_clone.clone(), op: "upsert", key: key.clone(), - data: data.clone(), + data: transformed_data, append: vec![], }; if let Ok(json) = serde_json::to_vec(&frame) { @@ -947,7 +959,8 @@ async fn attach_client_to_bus( let mut rx = ctx.bus_manager.get_or_create_state_bus(view_id, key).await; - if let Some(cached_entity) = ctx.entity_cache.get(view_id, key).await { + if let Some(mut cached_entity) = ctx.entity_cache.get(view_id, key).await { + transform_large_u64_to_strings(&mut cached_entity); let snapshot_entities = vec![SnapshotEntity { key: key.to_string(), data: cached_entity, @@ -1002,7 +1015,10 @@ async fn attach_client_to_bus( let snapshot_entities: Vec = snapshots .into_iter() .filter(|(key, _)| subscription.matches_key(key)) - .map(|(key, data)| SnapshotEntity { key, data }) + .map(|(key, mut data)| { + transform_large_u64_to_strings(&mut data); + SnapshotEntity { key, data } + }) .collect(); if !snapshot_entities.is_empty() { @@ -1104,7 +1120,10 @@ async fn attach_derived_view_subscription( if !initial_window.is_empty() { let snapshot_entities: Vec = initial_window .into_iter() - .map(|(key, data)| SnapshotEntity { key, data }) + .map(|(key, mut data)| { + transform_large_u64_to_strings(&mut data); + SnapshotEntity { key, data } + }) .collect(); let batch_config = ctx.entity_cache.snapshot_config(); @@ -1179,12 +1198,14 @@ async fn attach_derived_view_subscription( } } + let mut transformed_data = data.clone(); + transform_large_u64_to_strings(&mut transformed_data); let frame = Frame { mode: frame_mode, export: view_id_clone.clone(), op: "upsert", key: new_key.clone(), - data: data.clone(), + data: transformed_data, append: vec![], }; if let Ok(json) = serde_json::to_vec(&frame) { @@ -1213,12 +1234,14 @@ async fn attach_derived_view_subscription( } for (key, data) in &new_window { + let mut transformed_data = data.clone(); + transform_large_u64_to_strings(&mut transformed_data); let frame = Frame { mode: frame_mode, export: view_id_clone.clone(), op: "upsert", key: key.clone(), - data: data.clone(), + data: transformed_data, append: vec![], }; if let Ok(json) = serde_json::to_vec(&frame) { diff --git a/stacks/ore/Cargo.lock b/stacks/ore/Cargo.lock index 1a8cc4b4..085f1c24 100644 --- a/stacks/ore/Cargo.lock +++ b/stacks/ore/Cargo.lock @@ -1436,7 +1436,7 @@ dependencies = [ [[package]] name = "hyperstack" -version = "0.5.5" +version = "0.5.8" dependencies = [ "anyhow", "bs58", @@ -1463,7 +1463,7 @@ dependencies = [ [[package]] name = "hyperstack-idl" -version = "0.1.2" +version = "0.1.5" dependencies = [ "serde", "serde_json", @@ -1474,7 +1474,7 @@ dependencies = [ [[package]] name = "hyperstack-interpreter" -version = "0.5.5" +version = "0.5.8" dependencies = [ "bs58", "dashmap", @@ -1501,7 +1501,7 @@ dependencies = [ [[package]] name = "hyperstack-macros" -version = "0.5.5" +version = "0.5.8" dependencies = [ "bs58", "hex", @@ -1516,7 +1516,7 @@ dependencies = [ [[package]] name = "hyperstack-sdk" -version = "0.5.5" +version = "0.5.6" dependencies = [ "anyhow", "flate2", @@ -1533,7 +1533,7 @@ dependencies = [ [[package]] name = "hyperstack-server" -version = "0.5.5" +version = "0.5.8" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/stacks/sdk/rust/src/ore/types.rs b/stacks/sdk/rust/src/ore/types.rs index b97fa4f2..1b2561d8 100644 --- a/stacks/sdk/rust/src/ore/types.rs +++ b/stacks/sdk/rust/src/ore/types.rs @@ -1,8 +1,9 @@ +use hyperstack_sdk::serde_utils; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct OreRoundId { - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub round_id: Option, #[serde(default)] pub round_address: Option, @@ -10,9 +11,15 @@ pub struct OreRoundId { #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct OreRoundState { - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_i64" + )] pub expires_at: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_i64" + )] pub estimated_expires_at_unix: Option>, #[serde(default)] pub motherlode: Option>, @@ -22,7 +29,10 @@ pub struct OreRoundState { pub total_vaulted: Option>, #[serde(default)] pub total_winnings: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub total_miners: Option>, #[serde(default)] pub deployed_per_square: Option>>, @@ -42,9 +52,15 @@ pub struct OreRoundResults { pub rent_payer: Option>, #[serde(default)] pub slot_hash: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub rng: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub winning_square: Option>, #[serde(default)] pub did_hit_motherlode: Option>, @@ -52,9 +68,15 @@ pub struct OreRoundResults { #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct OreRoundMetrics { - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub deploy_count: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub checkpoint_count: Option>, } @@ -72,11 +94,20 @@ pub struct OreRoundEntropy { pub entropy_seed: Option>, #[serde(default)] pub entropy_slot_hash: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_i64" + )] pub entropy_start_at: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_i64" + )] pub entropy_end_at: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub entropy_samples: Option>, #[serde(default)] pub entropy_var_address: Option>, @@ -110,7 +141,10 @@ pub struct OreTreasuryId { #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct OreTreasuryState { - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub balance: Option>, #[serde(default)] pub motherlode: Option>, @@ -132,27 +166,25 @@ pub struct OreTreasury { pub treasury_snapshot: Option>, } - - #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Treasury { - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub balance: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub buffer_a: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub motherlode: Option, #[serde(default)] pub miner_rewards_factor: Option, #[serde(default)] pub stake_rewards_factor: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub buffer_b: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub total_refined: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub total_staked: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub total_unclaimed: Option, } @@ -168,49 +200,100 @@ pub struct OreMinerId { #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct OreMinerRewards { - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub rewards_sol: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub rewards_ore: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub refined_ore: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub lifetime_rewards_sol: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub lifetime_rewards_ore: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub lifetime_deployed: Option>, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct OreMinerState { - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub round_id: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub checkpoint_id: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub checkpoint_fee: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_i64" + )] pub last_claim_ore_at: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_i64" + )] pub last_claim_sol_at: Option>, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct OreMinerAutomation { - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub amount: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub balance: Option>, #[serde(default)] pub executor: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub fee: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub strategy: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub mask: Option>, - #[serde(default)] + #[serde( + default, + deserialize_with = "serde_utils::deserialize_option_option_u64" + )] pub reload: Option>, } @@ -230,66 +313,63 @@ pub struct OreMiner { pub automation_snapshot: Option>, } - - #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Miner { #[serde(default)] pub authority: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_vec_u64")] pub deployed: Option>, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_vec_u64")] pub cumulative: Option>, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub checkpoint_fee: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub checkpoint_id: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_i64")] pub last_claim_ore_at: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_i64")] pub last_claim_sol_at: Option, #[serde(default)] pub rewards_factor: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub rewards_sol: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub rewards_ore: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub refined_ore: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub round_id: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub lifetime_rewards_sol: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub lifetime_rewards_ore: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub lifetime_deployed: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Automation { - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub amount: Option, #[serde(default)] pub authority: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub balance: Option, #[serde(default)] pub executor: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub fee: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub strategy: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub mask: Option, - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")] pub reload: Option, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EventWrapper { - #[serde(default)] + #[serde(default, deserialize_with = "serde_utils::deserialize_i64")] pub timestamp: i64, pub data: T, #[serde(default)]