From f1422c3f5c6da87a554c22385afc3ba260125f7f Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Thu, 11 Dec 2025 13:01:59 -0500 Subject: [PATCH 01/20] fix: logs as Vec instead of String --- .../models/block/block_app_eval_delta.rs.j2 | 4 +- .../block/block_application_eval_delta.rs.j2 | 57 ------------------- .../src/models/block_app_eval_delta.rs | 4 +- 3 files changed, 4 insertions(+), 61 deletions(-) delete mode 100644 api/oas_generator/rust_oas_generator/templates/models/block/block_application_eval_delta.rs.j2 diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 index 8fb08890f..dee9f8a0f 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 @@ -41,9 +41,9 @@ pub struct BlockAppEvalDelta { #[serde_as(as = "Option>")] #[serde(rename = "sa", skip_serializing_if = "Option::is_none")] pub shared_accounts: Option>>, - /// [lg] Application log outputs as strings (msgpack strings). + /// [lg] Application log outputs. #[serde(rename = "lg", skip_serializing_if = "Option::is_none")] - pub logs: Option>, + pub logs: Option>>, } impl AlgorandMsgpack for BlockAppEvalDelta { diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_application_eval_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_application_eval_delta.rs.j2 deleted file mode 100644 index ae175315a..000000000 --- a/api/oas_generator/rust_oas_generator/templates/models/block/block_application_eval_delta.rs.j2 +++ /dev/null @@ -1,57 +0,0 @@ -/* - * {{ spec.info.title }} - * - * {{ spec.info.description or "API client generated from OpenAPI specification" }} - * - * The version of the OpenAPI document: {{ spec.info.version }} - {% if spec.info.contact and spec.info.contact.email %} * Contact: {{ spec.info.contact.email }} - {% endif %} * Generated by: Rust OpenAPI Generator - */ - -use crate::models; -#[cfg(not(feature = "ffi_uniffi"))] -use algokit_transact::SignedTransaction as AlgokitSignedTransaction; -use serde::{Deserialize, Serialize}; -use serde_with::{Bytes, serde_as}; -use std::collections::HashMap; - -#[cfg(feature = "ffi_uniffi")] -use algokit_transact_ffi::SignedTransaction as AlgokitSignedTransaction; - -use algokit_transact::AlgorandMsgpack; - -use crate::models::SignedTxnInBlock; -use crate::models::BlockStateDelta; - -/// BlockApplicationEvalDelta matches msgpack wire for blocks; uses BlockStateDelta maps. -#[serde_as] -#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] -pub struct BlockApplicationEvalDelta { - /// [gd] Global state delta for the application. - #[serde(rename = "gd", skip_serializing_if = "Option::is_none")] - pub global_delta: Option, - /// [ld] Local state deltas keyed by integer account index. - #[serde(rename = "ld", skip_serializing_if = "Option::is_none")] - pub local_deltas: Option>, - /// [itx] Inner transactions produced by this application execution. - #[serde(rename = "itx", skip_serializing_if = "Option::is_none")] - pub inner_txns: Option>, - /// [sa] Shared accounts referenced by local deltas. - #[serde_as(as = "Option>")] - #[serde(rename = "sa", skip_serializing_if = "Option::is_none")] - pub shared_accounts: Option>>, - /// [lg] Application log outputs as strings (msgpack strings). - #[serde(rename = "lg", skip_serializing_if = "Option::is_none")] - pub logs: Option>, -} - -impl AlgorandMsgpack for BlockApplicationEvalDelta { - const PREFIX: &'static [u8] = b""; -} - -impl BlockApplicationEvalDelta { - pub fn new() -> BlockApplicationEvalDelta { BlockApplicationEvalDelta::default() } -} - - diff --git a/crates/algod_client/src/models/block_app_eval_delta.rs b/crates/algod_client/src/models/block_app_eval_delta.rs index 5243d53d2..49c292fce 100644 --- a/crates/algod_client/src/models/block_app_eval_delta.rs +++ b/crates/algod_client/src/models/block_app_eval_delta.rs @@ -41,9 +41,9 @@ pub struct BlockAppEvalDelta { #[serde_as(as = "Option>")] #[serde(rename = "sa", skip_serializing_if = "Option::is_none")] pub shared_accounts: Option>>, - /// [lg] Application log outputs as strings (msgpack strings). + /// [lg] Application log outputs. #[serde(rename = "lg", skip_serializing_if = "Option::is_none")] - pub logs: Option>, + pub logs: Option>>, } impl AlgorandMsgpack for BlockAppEvalDelta { From 59f0c1b2b39eddba5c1bf84792d8510a6954e0da Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Thu, 11 Dec 2025 13:11:02 -0500 Subject: [PATCH 02/20] fix: delta values as Vec --- .../templates/models/block/block_eval_delta.rs.j2 | 2 +- .../templates/models/block/block_state_delta.rs.j2 | 2 +- crates/algod_client/src/models/block_eval_delta.rs | 2 +- crates/algod_client/src/models/block_state_delta.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_eval_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_eval_delta.rs.j2 index 64049ae23..0ea03126f 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/block_eval_delta.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_eval_delta.rs.j2 @@ -22,7 +22,7 @@ pub struct BlockEvalDelta { pub action: u32, /// [bs] bytes value. #[serde(rename = "bs", skip_serializing_if = "Option::is_none")] - pub bytes: Option, + pub bytes: Option>, /// [ui] uint value. #[serde(rename = "ui", skip_serializing_if = "Option::is_none")] pub uint: Option, diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 index a7af21e30..1df76b4b8 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 @@ -14,6 +14,6 @@ use std::collections::HashMap; use crate::models::BlockEvalDelta; /// BlockStateDelta is a map keyed by state key to BlockEvalDelta. -pub type BlockStateDelta = HashMap; +pub type BlockStateDelta = HashMap, BlockEvalDelta>; diff --git a/crates/algod_client/src/models/block_eval_delta.rs b/crates/algod_client/src/models/block_eval_delta.rs index 3dc01a80f..872acdfe8 100644 --- a/crates/algod_client/src/models/block_eval_delta.rs +++ b/crates/algod_client/src/models/block_eval_delta.rs @@ -22,7 +22,7 @@ pub struct BlockEvalDelta { pub action: u32, /// [bs] bytes value. #[serde(rename = "bs", skip_serializing_if = "Option::is_none")] - pub bytes: Option, + pub bytes: Option>, /// [ui] uint value. #[serde(rename = "ui", skip_serializing_if = "Option::is_none")] pub uint: Option, diff --git a/crates/algod_client/src/models/block_state_delta.rs b/crates/algod_client/src/models/block_state_delta.rs index 506bd9626..a0add63c2 100644 --- a/crates/algod_client/src/models/block_state_delta.rs +++ b/crates/algod_client/src/models/block_state_delta.rs @@ -14,4 +14,4 @@ use std::collections::HashMap; use crate::models::BlockEvalDelta; /// BlockStateDelta is a map keyed by state key to BlockEvalDelta. -pub type BlockStateDelta = HashMap; +pub type BlockStateDelta = HashMap, BlockEvalDelta>; From 6f7ce6bf975657c9bd51aebec2030c64ee14b9bf Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Thu, 11 Dec 2025 13:17:58 -0500 Subject: [PATCH 03/20] wip: test --- crates/algod_client/src/lib.rs | 1 + .../src/models/block_eval_delta.rs | 7 +- .../src/models/block_state_delta.rs | 86 ++++++++++++++++++- .../algod_client/src/msgpack_string_bytes.rs | 38 ++++++++ 4 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 crates/algod_client/src/msgpack_string_bytes.rs diff --git a/crates/algod_client/src/lib.rs b/crates/algod_client/src/lib.rs index 89b21599a..667594ff5 100644 --- a/crates/algod_client/src/lib.rs +++ b/crates/algod_client/src/lib.rs @@ -6,6 +6,7 @@ uniffi::setup_scaffolding!(); pub mod apis; pub mod models; +pub mod msgpack_string_bytes; pub mod msgpack_value_bytes; // Re-export the main client for convenience diff --git a/crates/algod_client/src/models/block_eval_delta.rs b/crates/algod_client/src/models/block_eval_delta.rs index 872acdfe8..6b2a9fba2 100644 --- a/crates/algod_client/src/models/block_eval_delta.rs +++ b/crates/algod_client/src/models/block_eval_delta.rs @@ -21,7 +21,12 @@ pub struct BlockEvalDelta { #[serde(rename = "at")] pub action: u32, /// [bs] bytes value. - #[serde(rename = "bs", skip_serializing_if = "Option::is_none")] + #[serde( + with = "crate::msgpack_string_bytes", + default, + rename = "bs", + skip_serializing_if = "Option::is_none" + )] pub bytes: Option>, /// [ui] uint value. #[serde(rename = "ui", skip_serializing_if = "Option::is_none")] diff --git a/crates/algod_client/src/models/block_state_delta.rs b/crates/algod_client/src/models/block_state_delta.rs index a0add63c2..88a0da7e8 100644 --- a/crates/algod_client/src/models/block_state_delta.rs +++ b/crates/algod_client/src/models/block_state_delta.rs @@ -8,10 +8,88 @@ * Generated by: Rust OpenAPI Generator */ -use crate::models; -use std::collections::HashMap; - use crate::models::BlockEvalDelta; +use serde::de::{MapAccess, Visitor}; +use serde::ser::SerializeMap; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::collections::HashMap; +use std::fmt; +use std::ops::{Deref, DerefMut}; /// BlockStateDelta is a map keyed by state key to BlockEvalDelta. -pub type BlockStateDelta = HashMap, BlockEvalDelta>; +/// Keys are raw bytes that may come as msgpack strings (which can contain non-UTF8 data). +#[derive(Clone, Default, Debug, PartialEq)] +#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] +pub struct BlockStateDelta { + pub entries: HashMap, BlockEvalDelta>, +} + +impl Deref for BlockStateDelta { + type Target = HashMap, BlockEvalDelta>; + + fn deref(&self) -> &Self::Target { + &self.entries + } +} + +impl DerefMut for BlockStateDelta { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.entries + } +} + +struct BlockStateDeltaVisitor; + +impl<'de> Visitor<'de> for BlockStateDeltaVisitor { + type Value = BlockStateDelta; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map with string or binary keys") + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut entries = HashMap::with_capacity(access.size_hint().unwrap_or(0)); + + // Use rmpv::Value for keys to handle both string and binary + while let Some((key, value)) = access.next_entry::()? { + let key_bytes = match key { + rmpv::Value::String(s) => s.into_bytes(), + rmpv::Value::Binary(b) => b, + _ => { + return Err(serde::de::Error::custom( + "expected string or binary key in BlockStateDelta", + )) + } + }; + entries.insert(key_bytes, value); + } + + Ok(BlockStateDelta { entries }) + } +} + +impl<'de> Deserialize<'de> for BlockStateDelta { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_map(BlockStateDeltaVisitor) + } +} + +impl Serialize for BlockStateDelta { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(self.entries.len()))?; + for (k, v) in &self.entries { + // Serialize keys as bytes + map.serialize_entry(&serde_bytes::Bytes::new(k), v)?; + } + map.end() + } +} diff --git a/crates/algod_client/src/msgpack_string_bytes.rs b/crates/algod_client/src/msgpack_string_bytes.rs new file mode 100644 index 000000000..6d36377c7 --- /dev/null +++ b/crates/algod_client/src/msgpack_string_bytes.rs @@ -0,0 +1,38 @@ +/// Custom serde module for deserializing msgpack strings as raw bytes. +/// +/// Msgpack strings may contain arbitrary bytes that aren't valid UTF-8. +/// This module deserializes the raw string bytes into Vec without +/// requiring UTF-8 validity. +use serde::{Deserialize, Deserializer, Serializer}; + +/// Deserialize a msgpack string as raw bytes +pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + // Use rmpv::Value to capture the raw msgpack value + let value: Option = Option::deserialize(deserializer)?; + + match value { + Some(rmpv::Value::String(s)) => { + // rmpv::Utf8String gives us access to raw bytes even if not valid UTF-8 + Ok(Some(s.into_bytes())) + } + Some(rmpv::Value::Binary(b)) => Ok(Some(b)), + Some(_) => Err(serde::de::Error::custom( + "expected string or binary, got other type", + )), + None => Ok(None), + } +} + +/// Serialize bytes as a msgpack binary +pub fn serialize(value: &Option>, serializer: S) -> Result +where + S: Serializer, +{ + match value { + Some(bytes) => serializer.serialize_bytes(bytes), + None => serializer.serialize_none(), + } +} From afa4c9eb5842972a0fbaf5f7aa64f6a58b5bf763 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Thu, 11 Dec 2025 13:34:43 -0500 Subject: [PATCH 04/20] wip: t1 --- .../generator/template_engine.py | 4 + .../templates/base/lib.rs.j2 | 1 + .../templates/base/msgpack_string_bytes.rs.j2 | 39 +++++++++ .../models/block/block_eval_delta.rs.j2 | 7 +- .../models/block/block_state_delta.rs.j2 | 86 ++++++++++++++++++- .../src/models/block_state_delta.rs | 2 +- 6 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes.rs.j2 diff --git a/api/oas_generator/rust_oas_generator/generator/template_engine.py b/api/oas_generator/rust_oas_generator/generator/template_engine.py index e0771e209..ca0083489 100644 --- a/api/oas_generator/rust_oas_generator/generator/template_engine.py +++ b/api/oas_generator/rust_oas_generator/generator/template_engine.py @@ -490,6 +490,10 @@ def _generate_base_files(self, context: dict[str, Any], output_dir: Path) -> dic files[src_dir / "msgpack_value_bytes.rs"] = self.template_engine.render_template( "base/msgpack_value_bytes.rs.j2", context ) + # Provide msgpack helper to deserialize msgpack strings as raw bytes + files[src_dir / "msgpack_string_bytes.rs"] = self.template_engine.render_template( + "base/msgpack_string_bytes.rs.j2", context + ) return files diff --git a/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 b/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 index 486e93502..ebca25605 100644 --- a/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 @@ -8,6 +8,7 @@ uniffi::setup_scaffolding!(); pub mod apis; pub mod models; {% if client_type == "Algod" %} +pub mod msgpack_string_bytes; pub mod msgpack_value_bytes; {% endif %} diff --git a/api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes.rs.j2 b/api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes.rs.j2 new file mode 100644 index 000000000..8a309dde5 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes.rs.j2 @@ -0,0 +1,39 @@ +/// Custom serde module for deserializing msgpack strings as raw bytes. +/// +/// Msgpack strings may contain arbitrary bytes that aren't valid UTF-8. +/// This module deserializes the raw string bytes into Vec without +/// requiring UTF-8 validity. +use serde::{Deserialize, Deserializer, Serializer}; + +/// Deserialize a msgpack string as raw bytes +pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + // Use rmpv::Value to capture the raw msgpack value + let value: Option = Option::deserialize(deserializer)?; + + match value { + Some(rmpv::Value::String(s)) => { + // rmpv::Utf8String gives us access to raw bytes even if not valid UTF-8 + Ok(Some(s.into_bytes())) + } + Some(rmpv::Value::Binary(b)) => Ok(Some(b)), + Some(_) => Err(serde::de::Error::custom( + "expected string or binary, got other type", + )), + None => Ok(None), + } +} + +/// Serialize bytes as a msgpack binary +pub fn serialize(value: &Option>, serializer: S) -> Result +where + S: Serializer, +{ + match value { + Some(bytes) => serializer.serialize_bytes(bytes), + None => serializer.serialize_none(), + } +} + diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_eval_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_eval_delta.rs.j2 index 0ea03126f..c12aece86 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/block_eval_delta.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_eval_delta.rs.j2 @@ -21,7 +21,12 @@ pub struct BlockEvalDelta { #[serde(rename = "at")] pub action: u32, /// [bs] bytes value. - #[serde(rename = "bs", skip_serializing_if = "Option::is_none")] + #[serde( + with = "crate::msgpack_string_bytes", + default, + rename = "bs", + skip_serializing_if = "Option::is_none" + )] pub bytes: Option>, /// [ui] uint value. #[serde(rename = "ui", skip_serializing_if = "Option::is_none")] diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 index 1df76b4b8..bc0e20ef0 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 @@ -8,12 +8,90 @@ {% endif %} * Generated by: Rust OpenAPI Generator */ -use crate::models; -use std::collections::HashMap; - use crate::models::BlockEvalDelta; +use serde::de::{MapAccess, Visitor}; +use serde::ser::SerializeMap; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::collections::HashMap; +use std::fmt; +use std::ops::{Deref, DerefMut}; /// BlockStateDelta is a map keyed by state key to BlockEvalDelta. -pub type BlockStateDelta = HashMap, BlockEvalDelta>; +/// Keys are raw bytes that may come as msgpack strings (which can contain non-UTF8 data). +#[derive(Clone, Default, Debug, PartialEq)] +#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] +pub struct BlockStateDelta { + pub entries: HashMap, BlockEvalDelta>, +} + +impl Deref for BlockStateDelta { + type Target = HashMap, BlockEvalDelta>; + + fn deref(&self) -> &Self::Target { + &self.entries + } +} + +impl DerefMut for BlockStateDelta { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.entries + } +} + +struct BlockStateDeltaVisitor; + +impl<'de> Visitor<'de> for BlockStateDeltaVisitor { + type Value = BlockStateDelta; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map with string or binary keys") + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut entries = HashMap::with_capacity(access.size_hint().unwrap_or(0)); + + // Use rmpv::Value for keys to handle both string and binary + while let Some((key, value)) = access.next_entry::()? { + let key_bytes = match key { + rmpv::Value::String(s) => s.into_bytes(), + rmpv::Value::Binary(b) => b, + _ => { + return Err(serde::de::Error::custom( + "expected string or binary key in BlockStateDelta", + )) + } + }; + entries.insert(key_bytes, value); + } + + Ok(BlockStateDelta { entries }) + } +} + +impl<'de> Deserialize<'de> for BlockStateDelta { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_map(BlockStateDeltaVisitor) + } +} + +impl Serialize for BlockStateDelta { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(self.entries.len()))?; + for (k, v) in &self.entries { + // Serialize keys as bytes + map.serialize_entry(&serde_bytes::Bytes::new(k), v)?; + } + map.end() + } +} diff --git a/crates/algod_client/src/models/block_state_delta.rs b/crates/algod_client/src/models/block_state_delta.rs index 88a0da7e8..ca57fd761 100644 --- a/crates/algod_client/src/models/block_state_delta.rs +++ b/crates/algod_client/src/models/block_state_delta.rs @@ -61,7 +61,7 @@ impl<'de> Visitor<'de> for BlockStateDeltaVisitor { _ => { return Err(serde::de::Error::custom( "expected string or binary key in BlockStateDelta", - )) + )); } }; entries.insert(key_bytes, value); From 85a56531ac4b6e64e1d7040170380a7dc79eac77 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Thu, 11 Dec 2025 13:43:40 -0500 Subject: [PATCH 05/20] wip: lg --- .../templates/models/block/block_app_eval_delta.rs.j2 | 1 + crates/algod_client/src/models/block_app_eval_delta.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 index dee9f8a0f..8c5697913 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 @@ -42,6 +42,7 @@ pub struct BlockAppEvalDelta { #[serde(rename = "sa", skip_serializing_if = "Option::is_none")] pub shared_accounts: Option>>, /// [lg] Application log outputs. + #[serde_as(as = "Option>")] #[serde(rename = "lg", skip_serializing_if = "Option::is_none")] pub logs: Option>>, } diff --git a/crates/algod_client/src/models/block_app_eval_delta.rs b/crates/algod_client/src/models/block_app_eval_delta.rs index 49c292fce..3dc0abd77 100644 --- a/crates/algod_client/src/models/block_app_eval_delta.rs +++ b/crates/algod_client/src/models/block_app_eval_delta.rs @@ -42,6 +42,7 @@ pub struct BlockAppEvalDelta { #[serde(rename = "sa", skip_serializing_if = "Option::is_none")] pub shared_accounts: Option>>, /// [lg] Application log outputs. + #[serde_as(as = "Option>")] #[serde(rename = "lg", skip_serializing_if = "Option::is_none")] pub logs: Option>>, } From b3801444ab6d5cb59db4185be66169b080fd27d2 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sat, 31 Jan 2026 16:37:18 -0500 Subject: [PATCH 06/20] wip: try unsafe str --- .../templates/base/msgpack_string_bytes.rs.j2 | 2 +- crates/algod_client/src/msgpack_string_bytes.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes.rs.j2 b/api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes.rs.j2 index 8a309dde5..3245e4c8a 100644 --- a/api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes.rs.j2 @@ -32,7 +32,7 @@ where S: Serializer, { match value { - Some(bytes) => serializer.serialize_bytes(bytes), + Some(bytes) => serializer.serialize_str(unsafe { std::str::from_utf8_unchecked(bytes) }), None => serializer.serialize_none(), } } diff --git a/crates/algod_client/src/msgpack_string_bytes.rs b/crates/algod_client/src/msgpack_string_bytes.rs index 6d36377c7..ebbb34442 100644 --- a/crates/algod_client/src/msgpack_string_bytes.rs +++ b/crates/algod_client/src/msgpack_string_bytes.rs @@ -32,7 +32,7 @@ where S: Serializer, { match value { - Some(bytes) => serializer.serialize_bytes(bytes), + Some(bytes) => serializer.serialize_str(unsafe { std::str::from_utf8_unchecked(bytes) }), None => serializer.serialize_none(), } } From 5939ebf85d060b13a7a4a1d3369bca98cf5101d0 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sat, 31 Jan 2026 16:43:31 -0500 Subject: [PATCH 07/20] wip: msgpack_string_bytes for logs --- .../templates/models/block/block_app_eval_delta.rs.j2 | 3 +-- crates/algod_client/src/models/block_app_eval_delta.rs | 7 +++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 index 8c5697913..7cdffe801 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 @@ -42,8 +42,7 @@ pub struct BlockAppEvalDelta { #[serde(rename = "sa", skip_serializing_if = "Option::is_none")] pub shared_accounts: Option>>, /// [lg] Application log outputs. - #[serde_as(as = "Option>")] - #[serde(rename = "lg", skip_serializing_if = "Option::is_none")] + #[serde(rename = "lg", skip_serializing_if = "Option::is_none", with = "crate::msgpack_string_bytes")] pub logs: Option>>, } diff --git a/crates/algod_client/src/models/block_app_eval_delta.rs b/crates/algod_client/src/models/block_app_eval_delta.rs index 3dc0abd77..034bb7c20 100644 --- a/crates/algod_client/src/models/block_app_eval_delta.rs +++ b/crates/algod_client/src/models/block_app_eval_delta.rs @@ -42,8 +42,11 @@ pub struct BlockAppEvalDelta { #[serde(rename = "sa", skip_serializing_if = "Option::is_none")] pub shared_accounts: Option>>, /// [lg] Application log outputs. - #[serde_as(as = "Option>")] - #[serde(rename = "lg", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "lg", + skip_serializing_if = "Option::is_none", + with = "crate::msgpack_string_bytes" + )] pub logs: Option>>, } From 287558eb634ca76cf80599edc827a20348c7c622 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sat, 31 Jan 2026 16:55:45 -0500 Subject: [PATCH 08/20] wip: string_bytes_vec --- .../generator/template_engine.py | 5 ++ .../templates/base/lib.rs.j2 | 1 + .../base/msgpack_string_bytes_vec.rs.j2 | 59 +++++++++++++++++++ .../models/block/block_app_eval_delta.rs.j2 | 2 +- crates/algod_client/src/lib.rs | 1 + .../src/models/block_app_eval_delta.rs | 2 +- .../src/msgpack_string_bytes_vec.rs | 58 ++++++++++++++++++ 7 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes_vec.rs.j2 create mode 100644 crates/algod_client/src/msgpack_string_bytes_vec.rs diff --git a/api/oas_generator/rust_oas_generator/generator/template_engine.py b/api/oas_generator/rust_oas_generator/generator/template_engine.py index ca0083489..53ac126f4 100644 --- a/api/oas_generator/rust_oas_generator/generator/template_engine.py +++ b/api/oas_generator/rust_oas_generator/generator/template_engine.py @@ -495,6 +495,11 @@ def _generate_base_files(self, context: dict[str, Any], output_dir: Path) -> dic "base/msgpack_string_bytes.rs.j2", context ) + files[src_dir / "msgpack_string_bytes_vec.rs"] = self.template_engine.render_template( + "base/msgpack_string_bytes_vec.rs.j2", context + ) + + return files def _generate_model_files( diff --git a/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 b/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 index ebca25605..98ede0496 100644 --- a/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 @@ -9,6 +9,7 @@ pub mod apis; pub mod models; {% if client_type == "Algod" %} pub mod msgpack_string_bytes; +pub mod msgpack_string_bytes_vec; pub mod msgpack_value_bytes; {% endif %} diff --git a/api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes_vec.rs.j2 b/api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes_vec.rs.j2 new file mode 100644 index 000000000..3aadcca03 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes_vec.rs.j2 @@ -0,0 +1,59 @@ +/// Custom serde module for deserializing a vector of msgpack strings as raw bytes. +/// +/// Msgpack strings may contain arbitrary bytes that aren't valid UTF-8. +/// This module deserializes the raw string bytes into Vec> without +/// requiring UTF-8 validity. +use serde::{Deserialize, Deserializer, Serializer}; + +/// Deserialize a vector of msgpack strings as raw bytes +pub fn deserialize<'de, D>(deserializer: D) -> Result>>, D::Error> +where + D: Deserializer<'de>, +{ + // Use rmpv::Value to capture the raw msgpack value + let value: Option = Option::deserialize(deserializer)?; + + match value { + Some(rmpv::Value::Array(arr)) => { + let mut result = Vec::with_capacity(arr.len()); + for item in arr { + match item { + rmpv::Value::String(s) => { + // rmpv::Utf8String gives us access to raw bytes even if not valid UTF-8 + result.push(s.into_bytes()); + } + rmpv::Value::Binary(b) => result.push(b), + _ => { + return Err(serde::de::Error::custom( + "expected string or binary in array, got other type", + )); + } + } + } + Ok(Some(result)) + } + Some(_) => Err(serde::de::Error::custom("expected array, got other type")), + None => Ok(None), + } +} + +/// Serialize a vector of bytes as msgpack strings +pub fn serialize(value: &Option>>, serializer: S) -> Result +where + S: Serializer, +{ + match value { + Some(vec) => { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(vec.len()))?; + for bytes in vec { + // Serialize each byte vector as a string (not binary) + let s = unsafe { std::str::from_utf8_unchecked(bytes) }; + seq.serialize_element(s)?; + } + seq.end() + } + None => serializer.serialize_none(), + } +} + diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 index 7cdffe801..d83e45a85 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 @@ -42,7 +42,7 @@ pub struct BlockAppEvalDelta { #[serde(rename = "sa", skip_serializing_if = "Option::is_none")] pub shared_accounts: Option>>, /// [lg] Application log outputs. - #[serde(rename = "lg", skip_serializing_if = "Option::is_none", with = "crate::msgpack_string_bytes")] + #[serde(rename = "lg", skip_serializing_if = "Option::is_none", with = "crate::msgpack_string_bytes_vec")] pub logs: Option>>, } diff --git a/crates/algod_client/src/lib.rs b/crates/algod_client/src/lib.rs index 667594ff5..d93ad507f 100644 --- a/crates/algod_client/src/lib.rs +++ b/crates/algod_client/src/lib.rs @@ -7,6 +7,7 @@ uniffi::setup_scaffolding!(); pub mod apis; pub mod models; pub mod msgpack_string_bytes; +pub mod msgpack_string_bytes_vec; pub mod msgpack_value_bytes; // Re-export the main client for convenience diff --git a/crates/algod_client/src/models/block_app_eval_delta.rs b/crates/algod_client/src/models/block_app_eval_delta.rs index 034bb7c20..37c6bf8e5 100644 --- a/crates/algod_client/src/models/block_app_eval_delta.rs +++ b/crates/algod_client/src/models/block_app_eval_delta.rs @@ -45,7 +45,7 @@ pub struct BlockAppEvalDelta { #[serde( rename = "lg", skip_serializing_if = "Option::is_none", - with = "crate::msgpack_string_bytes" + with = "crate::msgpack_string_bytes_vec" )] pub logs: Option>>, } diff --git a/crates/algod_client/src/msgpack_string_bytes_vec.rs b/crates/algod_client/src/msgpack_string_bytes_vec.rs new file mode 100644 index 000000000..851174e32 --- /dev/null +++ b/crates/algod_client/src/msgpack_string_bytes_vec.rs @@ -0,0 +1,58 @@ +/// Custom serde module for deserializing a vector of msgpack strings as raw bytes. +/// +/// Msgpack strings may contain arbitrary bytes that aren't valid UTF-8. +/// This module deserializes the raw string bytes into Vec> without +/// requiring UTF-8 validity. +use serde::{Deserialize, Deserializer, Serializer}; + +/// Deserialize a vector of msgpack strings as raw bytes +pub fn deserialize<'de, D>(deserializer: D) -> Result>>, D::Error> +where + D: Deserializer<'de>, +{ + // Use rmpv::Value to capture the raw msgpack value + let value: Option = Option::deserialize(deserializer)?; + + match value { + Some(rmpv::Value::Array(arr)) => { + let mut result = Vec::with_capacity(arr.len()); + for item in arr { + match item { + rmpv::Value::String(s) => { + // rmpv::Utf8String gives us access to raw bytes even if not valid UTF-8 + result.push(s.into_bytes()); + } + rmpv::Value::Binary(b) => result.push(b), + _ => { + return Err(serde::de::Error::custom( + "expected string or binary in array, got other type", + )); + } + } + } + Ok(Some(result)) + } + Some(_) => Err(serde::de::Error::custom("expected array, got other type")), + None => Ok(None), + } +} + +/// Serialize a vector of bytes as msgpack strings +pub fn serialize(value: &Option>>, serializer: S) -> Result +where + S: Serializer, +{ + match value { + Some(vec) => { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(vec.len()))?; + for bytes in vec { + // Serialize each byte vector as a string (not binary) + let s = unsafe { std::str::from_utf8_unchecked(bytes) }; + seq.serialize_element(s)?; + } + seq.end() + } + None => serializer.serialize_none(), + } +} From 0cda72dfe075fb99f6bc75a1d9cb7be1d32ba48b Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sat, 31 Jan 2026 17:07:48 -0500 Subject: [PATCH 09/20] wip: default logs --- .../templates/models/block/block_app_eval_delta.rs.j2 | 7 ++++++- crates/algod_client/src/models/block_app_eval_delta.rs | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 index d83e45a85..215444472 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 @@ -42,7 +42,12 @@ pub struct BlockAppEvalDelta { #[serde(rename = "sa", skip_serializing_if = "Option::is_none")] pub shared_accounts: Option>>, /// [lg] Application log outputs. - #[serde(rename = "lg", skip_serializing_if = "Option::is_none", with = "crate::msgpack_string_bytes_vec")] + #[serde( + rename = "lg", + skip_serializing_if = "Option::is_none", + default, + with = "crate::msgpack_string_bytes_vec" + )] pub logs: Option>>, } diff --git a/crates/algod_client/src/models/block_app_eval_delta.rs b/crates/algod_client/src/models/block_app_eval_delta.rs index 37c6bf8e5..e733ecbde 100644 --- a/crates/algod_client/src/models/block_app_eval_delta.rs +++ b/crates/algod_client/src/models/block_app_eval_delta.rs @@ -45,6 +45,7 @@ pub struct BlockAppEvalDelta { #[serde( rename = "lg", skip_serializing_if = "Option::is_none", + default, with = "crate::msgpack_string_bytes_vec" )] pub logs: Option>>, From 3b1f25dff9f7aa6fea814a54731d62dfad6485c2 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sat, 31 Jan 2026 17:20:34 -0500 Subject: [PATCH 10/20] wip: block state delta keys --- .../templates/models/block/block_state_delta.rs.j2 | 3 ++- crates/algod_client/src/models/block_state_delta.rs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 index bc0e20ef0..0a07b4f99 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 @@ -87,8 +87,9 @@ impl Serialize for BlockStateDelta { { let mut map = serializer.serialize_map(Some(self.entries.len()))?; for (k, v) in &self.entries { + let str = unsafe { std::str::from_utf8_unchecked(k) }; // Serialize keys as bytes - map.serialize_entry(&serde_bytes::Bytes::new(k), v)?; + map.serialize_entry(&str, v)?; } map.end() } diff --git a/crates/algod_client/src/models/block_state_delta.rs b/crates/algod_client/src/models/block_state_delta.rs index ca57fd761..e87488002 100644 --- a/crates/algod_client/src/models/block_state_delta.rs +++ b/crates/algod_client/src/models/block_state_delta.rs @@ -87,8 +87,9 @@ impl Serialize for BlockStateDelta { { let mut map = serializer.serialize_map(Some(self.entries.len()))?; for (k, v) in &self.entries { + let str = unsafe { std::str::from_utf8_unchecked(k) }; // Serialize keys as bytes - map.serialize_entry(&serde_bytes::Bytes::new(k), v)?; + map.serialize_entry(&str, v)?; } map.end() } From 8746939ee9629bdfe12a2b2300475d713431d7de Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sat, 31 Jan 2026 17:36:27 -0500 Subject: [PATCH 11/20] wip: NonAsciiString --- .../generator/template_engine.py | 13 +-- .../templates/base/lib.rs.j2 | 2 - .../templates/base/msgpack_string_bytes.rs.j2 | 39 -------- .../base/msgpack_string_bytes_vec.rs.j2 | 59 ------------ .../models/block/block_app_eval_delta.rs.j2 | 10 +-- .../models/block/block_eval_delta.rs.j2 | 10 +-- .../models/block/non_ascii_string.rs.j2 | 89 +++++++++++++++++++ .../templates/models/mod.rs.j2 | 2 + crates/algod_client/src/lib.rs | 2 - .../src/models/block_app_eval_delta.rs | 10 +-- .../src/models/block_eval_delta.rs | 10 +-- crates/algod_client/src/models/mod.rs | 2 + .../src/models/non_ascii_string.rs | 89 +++++++++++++++++++ .../algod_client/src/msgpack_string_bytes.rs | 38 -------- .../src/msgpack_string_bytes_vec.rs | 58 ------------ 15 files changed, 198 insertions(+), 235 deletions(-) delete mode 100644 api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes.rs.j2 delete mode 100644 api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes_vec.rs.j2 create mode 100644 api/oas_generator/rust_oas_generator/templates/models/block/non_ascii_string.rs.j2 create mode 100644 crates/algod_client/src/models/non_ascii_string.rs delete mode 100644 crates/algod_client/src/msgpack_string_bytes.rs delete mode 100644 crates/algod_client/src/msgpack_string_bytes_vec.rs diff --git a/api/oas_generator/rust_oas_generator/generator/template_engine.py b/api/oas_generator/rust_oas_generator/generator/template_engine.py index 53ac126f4..a9a934a61 100644 --- a/api/oas_generator/rust_oas_generator/generator/template_engine.py +++ b/api/oas_generator/rust_oas_generator/generator/template_engine.py @@ -490,15 +490,6 @@ def _generate_base_files(self, context: dict[str, Any], output_dir: Path) -> dic files[src_dir / "msgpack_value_bytes.rs"] = self.template_engine.render_template( "base/msgpack_value_bytes.rs.j2", context ) - # Provide msgpack helper to deserialize msgpack strings as raw bytes - files[src_dir / "msgpack_string_bytes.rs"] = self.template_engine.render_template( - "base/msgpack_string_bytes.rs.j2", context - ) - - files[src_dir / "msgpack_string_bytes_vec.rs"] = self.template_engine.render_template( - "base/msgpack_string_bytes_vec.rs.j2", context - ) - return files @@ -554,6 +545,10 @@ def _generate_model_files( files[models_dir / "get_block.rs"] = self.template_engine.render_template( "models/block/get_block.rs.j2", context ) + # Generate NonAsciiString helper type for msgpack strings + files[models_dir / "non_ascii_string.rs"] = self.template_engine.render_template( + "models/block/non_ascii_string.rs.j2", context + ) return files diff --git a/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 b/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 index 98ede0496..486e93502 100644 --- a/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 @@ -8,8 +8,6 @@ uniffi::setup_scaffolding!(); pub mod apis; pub mod models; {% if client_type == "Algod" %} -pub mod msgpack_string_bytes; -pub mod msgpack_string_bytes_vec; pub mod msgpack_value_bytes; {% endif %} diff --git a/api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes.rs.j2 b/api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes.rs.j2 deleted file mode 100644 index 3245e4c8a..000000000 --- a/api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes.rs.j2 +++ /dev/null @@ -1,39 +0,0 @@ -/// Custom serde module for deserializing msgpack strings as raw bytes. -/// -/// Msgpack strings may contain arbitrary bytes that aren't valid UTF-8. -/// This module deserializes the raw string bytes into Vec without -/// requiring UTF-8 validity. -use serde::{Deserialize, Deserializer, Serializer}; - -/// Deserialize a msgpack string as raw bytes -pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - // Use rmpv::Value to capture the raw msgpack value - let value: Option = Option::deserialize(deserializer)?; - - match value { - Some(rmpv::Value::String(s)) => { - // rmpv::Utf8String gives us access to raw bytes even if not valid UTF-8 - Ok(Some(s.into_bytes())) - } - Some(rmpv::Value::Binary(b)) => Ok(Some(b)), - Some(_) => Err(serde::de::Error::custom( - "expected string or binary, got other type", - )), - None => Ok(None), - } -} - -/// Serialize bytes as a msgpack binary -pub fn serialize(value: &Option>, serializer: S) -> Result -where - S: Serializer, -{ - match value { - Some(bytes) => serializer.serialize_str(unsafe { std::str::from_utf8_unchecked(bytes) }), - None => serializer.serialize_none(), - } -} - diff --git a/api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes_vec.rs.j2 b/api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes_vec.rs.j2 deleted file mode 100644 index 3aadcca03..000000000 --- a/api/oas_generator/rust_oas_generator/templates/base/msgpack_string_bytes_vec.rs.j2 +++ /dev/null @@ -1,59 +0,0 @@ -/// Custom serde module for deserializing a vector of msgpack strings as raw bytes. -/// -/// Msgpack strings may contain arbitrary bytes that aren't valid UTF-8. -/// This module deserializes the raw string bytes into Vec> without -/// requiring UTF-8 validity. -use serde::{Deserialize, Deserializer, Serializer}; - -/// Deserialize a vector of msgpack strings as raw bytes -pub fn deserialize<'de, D>(deserializer: D) -> Result>>, D::Error> -where - D: Deserializer<'de>, -{ - // Use rmpv::Value to capture the raw msgpack value - let value: Option = Option::deserialize(deserializer)?; - - match value { - Some(rmpv::Value::Array(arr)) => { - let mut result = Vec::with_capacity(arr.len()); - for item in arr { - match item { - rmpv::Value::String(s) => { - // rmpv::Utf8String gives us access to raw bytes even if not valid UTF-8 - result.push(s.into_bytes()); - } - rmpv::Value::Binary(b) => result.push(b), - _ => { - return Err(serde::de::Error::custom( - "expected string or binary in array, got other type", - )); - } - } - } - Ok(Some(result)) - } - Some(_) => Err(serde::de::Error::custom("expected array, got other type")), - None => Ok(None), - } -} - -/// Serialize a vector of bytes as msgpack strings -pub fn serialize(value: &Option>>, serializer: S) -> Result -where - S: Serializer, -{ - match value { - Some(vec) => { - use serde::ser::SerializeSeq; - let mut seq = serializer.serialize_seq(Some(vec.len()))?; - for bytes in vec { - // Serialize each byte vector as a string (not binary) - let s = unsafe { std::str::from_utf8_unchecked(bytes) }; - seq.serialize_element(s)?; - } - seq.end() - } - None => serializer.serialize_none(), - } -} - diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 index 215444472..07acc1db7 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 @@ -9,6 +9,7 @@ */ use crate::models; +use crate::models::NonAsciiString; #[cfg(not(feature = "ffi_uniffi"))] use algokit_transact::SignedTransaction as AlgokitSignedTransaction; use serde::{Deserialize, Serialize}; @@ -42,13 +43,8 @@ pub struct BlockAppEvalDelta { #[serde(rename = "sa", skip_serializing_if = "Option::is_none")] pub shared_accounts: Option>>, /// [lg] Application log outputs. - #[serde( - rename = "lg", - skip_serializing_if = "Option::is_none", - default, - with = "crate::msgpack_string_bytes_vec" - )] - pub logs: Option>>, + #[serde(rename = "lg", skip_serializing_if = "Option::is_none", default)] + pub logs: Option>, } impl AlgorandMsgpack for BlockAppEvalDelta { diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_eval_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_eval_delta.rs.j2 index c12aece86..ab6daff47 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/block_eval_delta.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_eval_delta.rs.j2 @@ -9,6 +9,7 @@ */ use crate::models; +use crate::models::NonAsciiString; use serde::{Deserialize, Serialize}; use algokit_transact::AlgorandMsgpack; @@ -21,13 +22,8 @@ pub struct BlockEvalDelta { #[serde(rename = "at")] pub action: u32, /// [bs] bytes value. - #[serde( - with = "crate::msgpack_string_bytes", - default, - rename = "bs", - skip_serializing_if = "Option::is_none" - )] - pub bytes: Option>, + #[serde(default, rename = "bs", skip_serializing_if = "Option::is_none")] + pub bytes: Option, /// [ui] uint value. #[serde(rename = "ui", skip_serializing_if = "Option::is_none")] pub uint: Option, diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/non_ascii_string.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/non_ascii_string.rs.j2 new file mode 100644 index 000000000..ff563d8a6 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/models/block/non_ascii_string.rs.j2 @@ -0,0 +1,89 @@ +/// A wrapper type for msgpack strings that may contain non-UTF8 bytes. +/// +/// Msgpack strings can contain arbitrary bytes that aren't valid UTF-8. +/// This type handles both string and binary msgpack values, storing them +/// internally as raw bytes. +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::ops::{Deref, DerefMut}; + +#[derive(Clone, Default, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] +pub struct NonAsciiString(pub Vec); + +impl NonAsciiString { + pub fn new(bytes: Vec) -> Self { + Self(bytes) + } + + pub fn into_bytes(self) -> Vec { + self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +impl From> for NonAsciiString { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +impl From for Vec { + fn from(s: NonAsciiString) -> Self { + s.0 + } +} + +impl AsRef<[u8]> for NonAsciiString { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl Deref for NonAsciiString { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for NonAsciiString { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'de> Deserialize<'de> for NonAsciiString { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // Use rmpv::Value to capture the raw msgpack value + let value = rmpv::Value::deserialize(deserializer)?; + + match value { + rmpv::Value::String(s) => { + // rmpv::Utf8String gives us access to raw bytes even if not valid UTF-8 + Ok(NonAsciiString(s.into_bytes())) + } + rmpv::Value::Binary(b) => Ok(NonAsciiString(b)), + _ => Err(serde::de::Error::custom( + "expected string or binary, got other type", + )), + } + } +} + +impl Serialize for NonAsciiString { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Serialize as a msgpack string (not binary) + let s = unsafe { std::str::from_utf8_unchecked(&self.0) }; + serializer.serialize_str(s) + } +} diff --git a/api/oas_generator/rust_oas_generator/templates/models/mod.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/mod.rs.j2 index a0338c124..2092c446e 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/mod.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/mod.rs.j2 @@ -38,5 +38,7 @@ pub mod block_state_proof_tracking_data; pub use self::block_state_proof_tracking_data::BlockStateProofTrackingData; pub mod block_state_proof_tracking; pub use self::block_state_proof_tracking::BlockStateProofTracking; +pub mod non_ascii_string; +pub use self::non_ascii_string::NonAsciiString; {% endif %} diff --git a/crates/algod_client/src/lib.rs b/crates/algod_client/src/lib.rs index d93ad507f..89b21599a 100644 --- a/crates/algod_client/src/lib.rs +++ b/crates/algod_client/src/lib.rs @@ -6,8 +6,6 @@ uniffi::setup_scaffolding!(); pub mod apis; pub mod models; -pub mod msgpack_string_bytes; -pub mod msgpack_string_bytes_vec; pub mod msgpack_value_bytes; // Re-export the main client for convenience diff --git a/crates/algod_client/src/models/block_app_eval_delta.rs b/crates/algod_client/src/models/block_app_eval_delta.rs index e733ecbde..f738a1fc8 100644 --- a/crates/algod_client/src/models/block_app_eval_delta.rs +++ b/crates/algod_client/src/models/block_app_eval_delta.rs @@ -9,6 +9,7 @@ */ use crate::models; +use crate::models::NonAsciiString; #[cfg(not(feature = "ffi_uniffi"))] use algokit_transact::SignedTransaction as AlgokitSignedTransaction; use serde::{Deserialize, Serialize}; @@ -42,13 +43,8 @@ pub struct BlockAppEvalDelta { #[serde(rename = "sa", skip_serializing_if = "Option::is_none")] pub shared_accounts: Option>>, /// [lg] Application log outputs. - #[serde( - rename = "lg", - skip_serializing_if = "Option::is_none", - default, - with = "crate::msgpack_string_bytes_vec" - )] - pub logs: Option>>, + #[serde(rename = "lg", skip_serializing_if = "Option::is_none", default)] + pub logs: Option>, } impl AlgorandMsgpack for BlockAppEvalDelta { diff --git a/crates/algod_client/src/models/block_eval_delta.rs b/crates/algod_client/src/models/block_eval_delta.rs index 6b2a9fba2..c9971e068 100644 --- a/crates/algod_client/src/models/block_eval_delta.rs +++ b/crates/algod_client/src/models/block_eval_delta.rs @@ -9,6 +9,7 @@ */ use crate::models; +use crate::models::NonAsciiString; use serde::{Deserialize, Serialize}; use algokit_transact::AlgorandMsgpack; @@ -21,13 +22,8 @@ pub struct BlockEvalDelta { #[serde(rename = "at")] pub action: u32, /// [bs] bytes value. - #[serde( - with = "crate::msgpack_string_bytes", - default, - rename = "bs", - skip_serializing_if = "Option::is_none" - )] - pub bytes: Option>, + #[serde(default, rename = "bs", skip_serializing_if = "Option::is_none")] + pub bytes: Option, /// [ui] uint value. #[serde(rename = "ui", skip_serializing_if = "Option::is_none")] pub uint: Option, diff --git a/crates/algod_client/src/models/mod.rs b/crates/algod_client/src/models/mod.rs index 51cac1d74..2dd2bb02b 100644 --- a/crates/algod_client/src/models/mod.rs +++ b/crates/algod_client/src/models/mod.rs @@ -196,3 +196,5 @@ pub mod block_state_proof_tracking_data; pub use self::block_state_proof_tracking_data::BlockStateProofTrackingData; pub mod block_state_proof_tracking; pub use self::block_state_proof_tracking::BlockStateProofTracking; +pub mod non_ascii_string; +pub use self::non_ascii_string::NonAsciiString; diff --git a/crates/algod_client/src/models/non_ascii_string.rs b/crates/algod_client/src/models/non_ascii_string.rs new file mode 100644 index 000000000..ff563d8a6 --- /dev/null +++ b/crates/algod_client/src/models/non_ascii_string.rs @@ -0,0 +1,89 @@ +/// A wrapper type for msgpack strings that may contain non-UTF8 bytes. +/// +/// Msgpack strings can contain arbitrary bytes that aren't valid UTF-8. +/// This type handles both string and binary msgpack values, storing them +/// internally as raw bytes. +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::ops::{Deref, DerefMut}; + +#[derive(Clone, Default, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] +pub struct NonAsciiString(pub Vec); + +impl NonAsciiString { + pub fn new(bytes: Vec) -> Self { + Self(bytes) + } + + pub fn into_bytes(self) -> Vec { + self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +impl From> for NonAsciiString { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +impl From for Vec { + fn from(s: NonAsciiString) -> Self { + s.0 + } +} + +impl AsRef<[u8]> for NonAsciiString { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl Deref for NonAsciiString { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for NonAsciiString { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'de> Deserialize<'de> for NonAsciiString { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // Use rmpv::Value to capture the raw msgpack value + let value = rmpv::Value::deserialize(deserializer)?; + + match value { + rmpv::Value::String(s) => { + // rmpv::Utf8String gives us access to raw bytes even if not valid UTF-8 + Ok(NonAsciiString(s.into_bytes())) + } + rmpv::Value::Binary(b) => Ok(NonAsciiString(b)), + _ => Err(serde::de::Error::custom( + "expected string or binary, got other type", + )), + } + } +} + +impl Serialize for NonAsciiString { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Serialize as a msgpack string (not binary) + let s = unsafe { std::str::from_utf8_unchecked(&self.0) }; + serializer.serialize_str(s) + } +} diff --git a/crates/algod_client/src/msgpack_string_bytes.rs b/crates/algod_client/src/msgpack_string_bytes.rs deleted file mode 100644 index ebbb34442..000000000 --- a/crates/algod_client/src/msgpack_string_bytes.rs +++ /dev/null @@ -1,38 +0,0 @@ -/// Custom serde module for deserializing msgpack strings as raw bytes. -/// -/// Msgpack strings may contain arbitrary bytes that aren't valid UTF-8. -/// This module deserializes the raw string bytes into Vec without -/// requiring UTF-8 validity. -use serde::{Deserialize, Deserializer, Serializer}; - -/// Deserialize a msgpack string as raw bytes -pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - // Use rmpv::Value to capture the raw msgpack value - let value: Option = Option::deserialize(deserializer)?; - - match value { - Some(rmpv::Value::String(s)) => { - // rmpv::Utf8String gives us access to raw bytes even if not valid UTF-8 - Ok(Some(s.into_bytes())) - } - Some(rmpv::Value::Binary(b)) => Ok(Some(b)), - Some(_) => Err(serde::de::Error::custom( - "expected string or binary, got other type", - )), - None => Ok(None), - } -} - -/// Serialize bytes as a msgpack binary -pub fn serialize(value: &Option>, serializer: S) -> Result -where - S: Serializer, -{ - match value { - Some(bytes) => serializer.serialize_str(unsafe { std::str::from_utf8_unchecked(bytes) }), - None => serializer.serialize_none(), - } -} diff --git a/crates/algod_client/src/msgpack_string_bytes_vec.rs b/crates/algod_client/src/msgpack_string_bytes_vec.rs deleted file mode 100644 index 851174e32..000000000 --- a/crates/algod_client/src/msgpack_string_bytes_vec.rs +++ /dev/null @@ -1,58 +0,0 @@ -/// Custom serde module for deserializing a vector of msgpack strings as raw bytes. -/// -/// Msgpack strings may contain arbitrary bytes that aren't valid UTF-8. -/// This module deserializes the raw string bytes into Vec> without -/// requiring UTF-8 validity. -use serde::{Deserialize, Deserializer, Serializer}; - -/// Deserialize a vector of msgpack strings as raw bytes -pub fn deserialize<'de, D>(deserializer: D) -> Result>>, D::Error> -where - D: Deserializer<'de>, -{ - // Use rmpv::Value to capture the raw msgpack value - let value: Option = Option::deserialize(deserializer)?; - - match value { - Some(rmpv::Value::Array(arr)) => { - let mut result = Vec::with_capacity(arr.len()); - for item in arr { - match item { - rmpv::Value::String(s) => { - // rmpv::Utf8String gives us access to raw bytes even if not valid UTF-8 - result.push(s.into_bytes()); - } - rmpv::Value::Binary(b) => result.push(b), - _ => { - return Err(serde::de::Error::custom( - "expected string or binary in array, got other type", - )); - } - } - } - Ok(Some(result)) - } - Some(_) => Err(serde::de::Error::custom("expected array, got other type")), - None => Ok(None), - } -} - -/// Serialize a vector of bytes as msgpack strings -pub fn serialize(value: &Option>>, serializer: S) -> Result -where - S: Serializer, -{ - match value { - Some(vec) => { - use serde::ser::SerializeSeq; - let mut seq = serializer.serialize_seq(Some(vec.len()))?; - for bytes in vec { - // Serialize each byte vector as a string (not binary) - let s = unsafe { std::str::from_utf8_unchecked(bytes) }; - seq.serialize_element(s)?; - } - seq.end() - } - None => serializer.serialize_none(), - } -} From 81a39882b493dd74927833ce6309cbd580dda720 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sat, 31 Jan 2026 17:42:49 -0500 Subject: [PATCH 12/20] wip: use NonAsciiString --- .../generator/template_engine.py | 5 -- .../templates/base/lib.rs.j2 | 3 -- .../templates/models/block/get_block.rs.j2 | 5 +- .../models/block/signed_txn_in_block.rs.j2 | 5 +- crates/algod_client/src/lib.rs | 1 - crates/algod_client/src/models/get_block.rs | 11 ++--- .../src/models/signed_txn_in_block.rs | 11 ++--- .../algod_client/src/msgpack_value_bytes.rs | 48 ------------------- 8 files changed, 14 insertions(+), 75 deletions(-) delete mode 100644 crates/algod_client/src/msgpack_value_bytes.rs diff --git a/api/oas_generator/rust_oas_generator/generator/template_engine.py b/api/oas_generator/rust_oas_generator/generator/template_engine.py index a9a934a61..80646819e 100644 --- a/api/oas_generator/rust_oas_generator/generator/template_engine.py +++ b/api/oas_generator/rust_oas_generator/generator/template_engine.py @@ -485,11 +485,6 @@ def _generate_base_files(self, context: dict[str, Any], output_dir: Path) -> dic # Detect client type client_type_fn = self.template_engine.env.globals.get("get_client_type") client_type = client_type_fn(context["spec"]) if callable(client_type_fn) else "Api" - if client_type == "Algod": - # Provide msgpack helper to encode/decode arbitrary msgpack values as bytes - files[src_dir / "msgpack_value_bytes.rs"] = self.template_engine.render_template( - "base/msgpack_value_bytes.rs.j2", context - ) return files diff --git a/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 b/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 index 486e93502..3a865ec78 100644 --- a/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 @@ -7,9 +7,6 @@ uniffi::setup_scaffolding!(); pub mod apis; pub mod models; -{% if client_type == "Algod" %} -pub mod msgpack_value_bytes; -{% endif %} // Re-export the main client for convenience pub use apis::{{ client_type }}Client; diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 index aee113d70..5532d7e31 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 @@ -20,6 +20,8 @@ use algokit_transact::AlgorandMsgpack; use crate::models::Block; +use crate::models::NonAsciiString; + /// Encoded block object. #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] @@ -29,12 +31,11 @@ pub struct GetBlock { pub block: Block, /// Block certificate (msgpack only). #[serde( - with = "crate::msgpack_value_bytes", default, rename = "cert", skip_serializing_if = "Option::is_none" )] - pub cert: Option>, + pub cert: Option, } impl AlgorandMsgpack for GetBlock { diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/signed_txn_in_block.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/signed_txn_in_block.rs.j2 index 24b03ef00..71db06808 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/signed_txn_in_block.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/signed_txn_in_block.rs.j2 @@ -20,6 +20,8 @@ use algokit_transact::AlgorandMsgpack; use crate::models::BlockAppEvalDelta; +use crate::models::NonAsciiString; + /// SignedTxnInBlock is a SignedTransaction with additional ApplyData and block-specific metadata. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] @@ -29,12 +31,11 @@ pub struct SignedTxnInBlock { pub signed_transaction: AlgokitSignedTransaction, /// [lsig] Logic signature (program signature). #[serde( - with = "crate::msgpack_value_bytes", default, rename = "lsig", skip_serializing_if = "Option::is_none" )] - pub logic_signature: Option>, + pub logic_signature: Option, /// [ca] Rewards applied to close-remainder-to account. #[serde(rename = "ca", skip_serializing_if = "Option::is_none")] pub closing_amount: Option, diff --git a/crates/algod_client/src/lib.rs b/crates/algod_client/src/lib.rs index 89b21599a..9dc8eed92 100644 --- a/crates/algod_client/src/lib.rs +++ b/crates/algod_client/src/lib.rs @@ -6,7 +6,6 @@ uniffi::setup_scaffolding!(); pub mod apis; pub mod models; -pub mod msgpack_value_bytes; // Re-export the main client for convenience pub use apis::AlgodClient; diff --git a/crates/algod_client/src/models/get_block.rs b/crates/algod_client/src/models/get_block.rs index aee113d70..03b09c053 100644 --- a/crates/algod_client/src/models/get_block.rs +++ b/crates/algod_client/src/models/get_block.rs @@ -20,6 +20,8 @@ use algokit_transact::AlgorandMsgpack; use crate::models::Block; +use crate::models::NonAsciiString; + /// Encoded block object. #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] @@ -28,13 +30,8 @@ pub struct GetBlock { #[serde(rename = "block")] pub block: Block, /// Block certificate (msgpack only). - #[serde( - with = "crate::msgpack_value_bytes", - default, - rename = "cert", - skip_serializing_if = "Option::is_none" - )] - pub cert: Option>, + #[serde(default, rename = "cert", skip_serializing_if = "Option::is_none")] + pub cert: Option, } impl AlgorandMsgpack for GetBlock { diff --git a/crates/algod_client/src/models/signed_txn_in_block.rs b/crates/algod_client/src/models/signed_txn_in_block.rs index 24b03ef00..847b56d3a 100644 --- a/crates/algod_client/src/models/signed_txn_in_block.rs +++ b/crates/algod_client/src/models/signed_txn_in_block.rs @@ -20,6 +20,8 @@ use algokit_transact::AlgorandMsgpack; use crate::models::BlockAppEvalDelta; +use crate::models::NonAsciiString; + /// SignedTxnInBlock is a SignedTransaction with additional ApplyData and block-specific metadata. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] @@ -28,13 +30,8 @@ pub struct SignedTxnInBlock { #[serde(flatten)] pub signed_transaction: AlgokitSignedTransaction, /// [lsig] Logic signature (program signature). - #[serde( - with = "crate::msgpack_value_bytes", - default, - rename = "lsig", - skip_serializing_if = "Option::is_none" - )] - pub logic_signature: Option>, + #[serde(default, rename = "lsig", skip_serializing_if = "Option::is_none")] + pub logic_signature: Option, /// [ca] Rewards applied to close-remainder-to account. #[serde(rename = "ca", skip_serializing_if = "Option::is_none")] pub closing_amount: Option, diff --git a/crates/algod_client/src/msgpack_value_bytes.rs b/crates/algod_client/src/msgpack_value_bytes.rs deleted file mode 100644 index 525d6f852..000000000 --- a/crates/algod_client/src/msgpack_value_bytes.rs +++ /dev/null @@ -1,48 +0,0 @@ -/// Custom serde module for handling msgpack-only fields as bytes. -/// -/// This module provides serialization/deserialization for fields that: -/// 1. Contain complex msgpack structures (maps with integer keys, nested data, etc.) -/// 2. Need to be stored as Vec for uniffi compatibility -/// 3. Should preserve the exact msgpack encoding -/// -/// When deserializing, it accepts any msgpack value and re-encodes it to bytes. -/// When serializing, it decodes the bytes back to a msgpack value. -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - -/// Deserialize a msgpack value and re-encode it as bytes -pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - // Deserialize as an optional rmpv::Value (accepts any msgpack structure) - let value: Option = Option::deserialize(deserializer)?; - - // If present, re-encode the value to msgpack bytes - match value { - Some(v) => { - let mut bytes = Vec::new(); - rmpv::encode::write_value(&mut bytes, &v).map_err(|e| { - serde::de::Error::custom(format!("Failed to encode msgpack value: {}", e)) - })?; - Ok(Some(bytes)) - } - None => Ok(None), - } -} - -/// Serialize bytes back to a msgpack value -pub fn serialize(value: &Option>, serializer: S) -> Result -where - S: Serializer, -{ - match value { - Some(bytes) => { - // Decode the bytes back to a msgpack value - let value = rmpv::decode::read_value(&mut bytes.as_slice()).map_err(|e| { - serde::ser::Error::custom(format!("Failed to decode msgpack bytes: {}", e)) - })?; - value.serialize(serializer) - } - None => serializer.serialize_none(), - } -} From 6798592aeb2ebe3280581260a89e22bc5b87182c Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sat, 31 Jan 2026 17:47:45 -0500 Subject: [PATCH 13/20] wip: NonAsciiString for block state delta --- .../models/block/block_state_delta.rs.j2 | 87 ++----------------- .../src/models/block_state_delta.rs | 87 ++----------------- 2 files changed, 10 insertions(+), 164 deletions(-) diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 index 0a07b4f99..f2ae020cb 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 @@ -8,91 +8,14 @@ {% endif %} * Generated by: Rust OpenAPI Generator */ -use crate::models::BlockEvalDelta; -use serde::de::{MapAccess, Visitor}; -use serde::ser::SerializeMap; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use crate::models; use std::collections::HashMap; -use std::fmt; -use std::ops::{Deref, DerefMut}; - -/// BlockStateDelta is a map keyed by state key to BlockEvalDelta. -/// Keys are raw bytes that may come as msgpack strings (which can contain non-UTF8 data). -#[derive(Clone, Default, Debug, PartialEq)] -#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] -pub struct BlockStateDelta { - pub entries: HashMap, BlockEvalDelta>, -} - -impl Deref for BlockStateDelta { - type Target = HashMap, BlockEvalDelta>; - - fn deref(&self) -> &Self::Target { - &self.entries - } -} - -impl DerefMut for BlockStateDelta { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.entries - } -} - -struct BlockStateDeltaVisitor; -impl<'de> Visitor<'de> for BlockStateDeltaVisitor { - type Value = BlockStateDelta; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a map with string or binary keys") - } - - fn visit_map(self, mut access: M) -> Result - where - M: MapAccess<'de>, - { - let mut entries = HashMap::with_capacity(access.size_hint().unwrap_or(0)); - - // Use rmpv::Value for keys to handle both string and binary - while let Some((key, value)) = access.next_entry::()? { - let key_bytes = match key { - rmpv::Value::String(s) => s.into_bytes(), - rmpv::Value::Binary(b) => b, - _ => { - return Err(serde::de::Error::custom( - "expected string or binary key in BlockStateDelta", - )) - } - }; - entries.insert(key_bytes, value); - } - - Ok(BlockStateDelta { entries }) - } -} +use crate::models::BlockEvalDelta; -impl<'de> Deserialize<'de> for BlockStateDelta { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_map(BlockStateDeltaVisitor) - } -} +use crate::models::NonAsciiString; -impl Serialize for BlockStateDelta { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut map = serializer.serialize_map(Some(self.entries.len()))?; - for (k, v) in &self.entries { - let str = unsafe { std::str::from_utf8_unchecked(k) }; - // Serialize keys as bytes - map.serialize_entry(&str, v)?; - } - map.end() - } -} +/// BlockStateDelta is a map keyed by state key to BlockEvalDelta. +pub type BlockStateDelta = HashMap; diff --git a/crates/algod_client/src/models/block_state_delta.rs b/crates/algod_client/src/models/block_state_delta.rs index e87488002..d16eb60f3 100644 --- a/crates/algod_client/src/models/block_state_delta.rs +++ b/crates/algod_client/src/models/block_state_delta.rs @@ -8,89 +8,12 @@ * Generated by: Rust OpenAPI Generator */ -use crate::models::BlockEvalDelta; -use serde::de::{MapAccess, Visitor}; -use serde::ser::SerializeMap; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use crate::models; use std::collections::HashMap; -use std::fmt; -use std::ops::{Deref, DerefMut}; - -/// BlockStateDelta is a map keyed by state key to BlockEvalDelta. -/// Keys are raw bytes that may come as msgpack strings (which can contain non-UTF8 data). -#[derive(Clone, Default, Debug, PartialEq)] -#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] -pub struct BlockStateDelta { - pub entries: HashMap, BlockEvalDelta>, -} - -impl Deref for BlockStateDelta { - type Target = HashMap, BlockEvalDelta>; - - fn deref(&self) -> &Self::Target { - &self.entries - } -} - -impl DerefMut for BlockStateDelta { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.entries - } -} - -struct BlockStateDeltaVisitor; -impl<'de> Visitor<'de> for BlockStateDeltaVisitor { - type Value = BlockStateDelta; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a map with string or binary keys") - } - - fn visit_map(self, mut access: M) -> Result - where - M: MapAccess<'de>, - { - let mut entries = HashMap::with_capacity(access.size_hint().unwrap_or(0)); - - // Use rmpv::Value for keys to handle both string and binary - while let Some((key, value)) = access.next_entry::()? { - let key_bytes = match key { - rmpv::Value::String(s) => s.into_bytes(), - rmpv::Value::Binary(b) => b, - _ => { - return Err(serde::de::Error::custom( - "expected string or binary key in BlockStateDelta", - )); - } - }; - entries.insert(key_bytes, value); - } - - Ok(BlockStateDelta { entries }) - } -} +use crate::models::BlockEvalDelta; -impl<'de> Deserialize<'de> for BlockStateDelta { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_map(BlockStateDeltaVisitor) - } -} +use crate::models::NonAsciiString; -impl Serialize for BlockStateDelta { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut map = serializer.serialize_map(Some(self.entries.len()))?; - for (k, v) in &self.entries { - let str = unsafe { std::str::from_utf8_unchecked(k) }; - // Serialize keys as bytes - map.serialize_entry(&str, v)?; - } - map.end() - } -} +/// BlockStateDelta is a map keyed by state key to BlockEvalDelta. +pub type BlockStateDelta = HashMap; From 15e44b307d3da550113c16670f8e9dd2fef8b70c Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sat, 31 Jan 2026 18:05:30 -0500 Subject: [PATCH 14/20] wip: block cert --- .../templates/models/block/get_block.rs.j2 | 104 +++++++++++++++++- crates/algod_client/src/models/get_block.rs | 103 ++++++++++++++++- 2 files changed, 203 insertions(+), 4 deletions(-) diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 index 5532d7e31..1ea7e0276 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 @@ -20,7 +20,107 @@ use algokit_transact::AlgorandMsgpack; use crate::models::Block; -use crate::models::NonAsciiString; +// Type aliases for clarity +pub type Round = u64; +pub type Period = u64; +pub type Step = u64; +pub type Digest = Vec; // DigestSize = sha512.Size256 = 32 bytes +pub type Address = Digest; +pub type VrfProof = Vec; // VrfProofSize = 80 bytes + +pub type Ed25519Signature = Vec; // Ed25519SignatureSize = 64 bytes +pub type Ed25519PublicKey = Vec; // Ed25519PublicKeySize = 32 bytes + +/// OneTimeSignature is a cryptographic signature that is produced a limited +/// number of times and provides forward integrity. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct OneTimeSignature { + /// Sig is a signature of msg under the key PK. + #[serde(rename = "s")] + pub sig: Ed25519Signature, + #[serde(rename = "p")] + pub pk: Ed25519PublicKey, + /// Old-style signature that does not use proper domain separation. + /// PKSigOld is unused; however, unfortunately we forgot to mark it + /// `codec:omitempty` and so it appears (with zero value) in certs. + #[serde(rename = "ps")] + pub pk_sig_old: Ed25519Signature, + /// Used to verify a new-style two-level ephemeral signature. + /// PK1Sig is a signature of OneTimeSignatureSubkeyOffsetID(PK, Batch, Offset) under the key PK2. + /// PK2Sig is a signature of OneTimeSignatureSubkeyBatchID(PK2, Batch) under the master key (OneTimeSignatureVerifier). + #[serde(rename = "p2")] + pub pk2: Ed25519PublicKey, + #[serde(rename = "p1s")] + pub pk1_sig: Ed25519Signature, + #[serde(rename = "p2s")] + pub pk2_sig: Ed25519Signature, +} + +/// An UnauthenticatedCredential is a Credential which has not yet been +/// authenticated. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct UnauthenticatedCredential { + #[serde(rename = "pf")] + pub proof: VrfProof, +} + +/// A proposalValue is a triplet of a block hashes (the contents themselves and the encoding of the block), +/// its proposer, and the period in which it was proposed. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProposalValue { + #[serde(rename = "oper")] + pub original_period: Period, + #[serde(rename = "oprop")] + pub original_proposer: Address, + /// BlockDigest = proposal.Block.Digest() + #[serde(rename = "dig")] + pub block_digest: Digest, + /// EncodingDigest = crypto.HashObj(proposal) + #[serde(rename = "encdig")] + pub encoding_digest: Digest, +} + +/// voteAuthenticator omits the Round, Period, Step, and Proposal for compression +/// and to simplify checking logic. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VoteAuthenticator { + #[serde(rename = "snd")] + pub sender: Address, + #[serde(rename = "cred")] + pub cred: UnauthenticatedCredential, + #[serde(rename = "sig")] + pub sig: OneTimeSignature, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EquivocationVoteAuthenticator { + #[serde(rename = "snd")] + pub sender: Address, + #[serde(rename = "cred")] + pub cred: UnauthenticatedCredential, + #[serde(rename = "sig")] + pub sigs: [OneTimeSignature; 2], + #[serde(rename = "props")] + pub proposals: [ProposalValue; 2], +} + +/// unauthenticatedBundle is a bundle which has not yet been verified. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct UnauthenticatedBundle { + #[serde(rename = "rnd")] + pub round: Round, + #[serde(rename = "per")] + pub period: Period, + #[serde(rename = "step")] + pub step: Step, + #[serde(rename = "prop")] + pub proposal: ProposalValue, + #[serde(rename = "vote")] + pub votes: Vec, + #[serde(rename = "eqv")] + pub equivocation_votes: Vec, +} + /// Encoded block object. #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] @@ -35,7 +135,7 @@ pub struct GetBlock { rename = "cert", skip_serializing_if = "Option::is_none" )] - pub cert: Option, + pub cert: Option, } impl AlgorandMsgpack for GetBlock { diff --git a/crates/algod_client/src/models/get_block.rs b/crates/algod_client/src/models/get_block.rs index 03b09c053..d9bd2f19b 100644 --- a/crates/algod_client/src/models/get_block.rs +++ b/crates/algod_client/src/models/get_block.rs @@ -20,7 +20,106 @@ use algokit_transact::AlgorandMsgpack; use crate::models::Block; -use crate::models::NonAsciiString; +// Type aliases for clarity +pub type Round = u64; +pub type Period = u64; +pub type Step = u64; +pub type Digest = Vec; // DigestSize = sha512.Size256 = 32 bytes +pub type Address = Digest; +pub type VrfProof = Vec; // VrfProofSize = 80 bytes + +pub type Ed25519Signature = Vec; // Ed25519SignatureSize = 64 bytes +pub type Ed25519PublicKey = Vec; // Ed25519PublicKeySize = 32 bytes + +/// OneTimeSignature is a cryptographic signature that is produced a limited +/// number of times and provides forward integrity. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct OneTimeSignature { + /// Sig is a signature of msg under the key PK. + #[serde(rename = "s")] + pub sig: Ed25519Signature, + #[serde(rename = "p")] + pub pk: Ed25519PublicKey, + /// Old-style signature that does not use proper domain separation. + /// PKSigOld is unused; however, unfortunately we forgot to mark it + /// `codec:omitempty` and so it appears (with zero value) in certs. + #[serde(rename = "ps")] + pub pk_sig_old: Ed25519Signature, + /// Used to verify a new-style two-level ephemeral signature. + /// PK1Sig is a signature of OneTimeSignatureSubkeyOffsetID(PK, Batch, Offset) under the key PK2. + /// PK2Sig is a signature of OneTimeSignatureSubkeyBatchID(PK2, Batch) under the master key (OneTimeSignatureVerifier). + #[serde(rename = "p2")] + pub pk2: Ed25519PublicKey, + #[serde(rename = "p1s")] + pub pk1_sig: Ed25519Signature, + #[serde(rename = "p2s")] + pub pk2_sig: Ed25519Signature, +} + +/// An UnauthenticatedCredential is a Credential which has not yet been +/// authenticated. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct UnauthenticatedCredential { + #[serde(rename = "pf")] + pub proof: VrfProof, +} + +/// A proposalValue is a triplet of a block hashes (the contents themselves and the encoding of the block), +/// its proposer, and the period in which it was proposed. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProposalValue { + #[serde(rename = "oper")] + pub original_period: Period, + #[serde(rename = "oprop")] + pub original_proposer: Address, + /// BlockDigest = proposal.Block.Digest() + #[serde(rename = "dig")] + pub block_digest: Digest, + /// EncodingDigest = crypto.HashObj(proposal) + #[serde(rename = "encdig")] + pub encoding_digest: Digest, +} + +/// voteAuthenticator omits the Round, Period, Step, and Proposal for compression +/// and to simplify checking logic. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VoteAuthenticator { + #[serde(rename = "snd")] + pub sender: Address, + #[serde(rename = "cred")] + pub cred: UnauthenticatedCredential, + #[serde(rename = "sig")] + pub sig: OneTimeSignature, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EquivocationVoteAuthenticator { + #[serde(rename = "snd")] + pub sender: Address, + #[serde(rename = "cred")] + pub cred: UnauthenticatedCredential, + #[serde(rename = "sig")] + pub sigs: [OneTimeSignature; 2], + #[serde(rename = "props")] + pub proposals: [ProposalValue; 2], +} + +/// unauthenticatedBundle is a bundle which has not yet been verified. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct UnauthenticatedBundle { + #[serde(rename = "rnd")] + pub round: Round, + #[serde(rename = "per")] + pub period: Period, + #[serde(rename = "step")] + pub step: Step, + #[serde(rename = "prop")] + pub proposal: ProposalValue, + #[serde(rename = "vote")] + pub votes: Vec, + #[serde(rename = "eqv")] + pub equivocation_votes: Vec, +} /// Encoded block object. #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] @@ -31,7 +130,7 @@ pub struct GetBlock { pub block: Block, /// Block certificate (msgpack only). #[serde(default, rename = "cert", skip_serializing_if = "Option::is_none")] - pub cert: Option, + pub cert: Option, } impl AlgorandMsgpack for GetBlock { From 3ecebb6ccf42999424c23483d314426f87faa841 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sat, 31 Jan 2026 18:12:39 -0500 Subject: [PATCH 15/20] wip: rm lsig, optional oper --- .../templates/models/block/get_block.rs.j2 | 4 ++-- .../templates/models/block/signed_txn_in_block.rs.j2 | 7 ------- crates/algod_client/src/models/get_block.rs | 4 ++-- crates/algod_client/src/models/signed_txn_in_block.rs | 3 --- 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 index 1ea7e0276..ef913d063 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 @@ -68,8 +68,8 @@ pub struct UnauthenticatedCredential { /// its proposer, and the period in which it was proposed. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ProposalValue { - #[serde(rename = "oper")] - pub original_period: Period, + #[serde(rename = "oper", default, skip_serializing_if = "Option::is_none")] + pub original_period: Option, #[serde(rename = "oprop")] pub original_proposer: Address, /// BlockDigest = proposal.Block.Digest() diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/signed_txn_in_block.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/signed_txn_in_block.rs.j2 index 71db06808..d53c37cdf 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/signed_txn_in_block.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/signed_txn_in_block.rs.j2 @@ -29,13 +29,6 @@ pub struct SignedTxnInBlock { /// SignedTransaction fields (flattened from algokit_transact) #[serde(flatten)] pub signed_transaction: AlgokitSignedTransaction, - /// [lsig] Logic signature (program signature). - #[serde( - default, - rename = "lsig", - skip_serializing_if = "Option::is_none" - )] - pub logic_signature: Option, /// [ca] Rewards applied to close-remainder-to account. #[serde(rename = "ca", skip_serializing_if = "Option::is_none")] pub closing_amount: Option, diff --git a/crates/algod_client/src/models/get_block.rs b/crates/algod_client/src/models/get_block.rs index d9bd2f19b..468bd2bef 100644 --- a/crates/algod_client/src/models/get_block.rs +++ b/crates/algod_client/src/models/get_block.rs @@ -68,8 +68,8 @@ pub struct UnauthenticatedCredential { /// its proposer, and the period in which it was proposed. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ProposalValue { - #[serde(rename = "oper")] - pub original_period: Period, + #[serde(rename = "oper", default, skip_serializing_if = "Option::is_none")] + pub original_period: Option, #[serde(rename = "oprop")] pub original_proposer: Address, /// BlockDigest = proposal.Block.Digest() diff --git a/crates/algod_client/src/models/signed_txn_in_block.rs b/crates/algod_client/src/models/signed_txn_in_block.rs index 847b56d3a..d53c37cdf 100644 --- a/crates/algod_client/src/models/signed_txn_in_block.rs +++ b/crates/algod_client/src/models/signed_txn_in_block.rs @@ -29,9 +29,6 @@ pub struct SignedTxnInBlock { /// SignedTransaction fields (flattened from algokit_transact) #[serde(flatten)] pub signed_transaction: AlgokitSignedTransaction, - /// [lsig] Logic signature (program signature). - #[serde(default, rename = "lsig", skip_serializing_if = "Option::is_none")] - pub logic_signature: Option, /// [ca] Rewards applied to close-remainder-to account. #[serde(rename = "ca", skip_serializing_if = "Option::is_none")] pub closing_amount: Option, From fd41c5a21e8afd83a3f9d9353fd0c8b806255ed5 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sat, 31 Jan 2026 18:15:32 -0500 Subject: [PATCH 16/20] wip: rm lsig from struct --- .../templates/models/block/signed_txn_in_block.rs.j2 | 1 - crates/algod_client/src/models/signed_txn_in_block.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/signed_txn_in_block.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/signed_txn_in_block.rs.j2 index d53c37cdf..d4e7da35a 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/signed_txn_in_block.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/signed_txn_in_block.rs.j2 @@ -90,7 +90,6 @@ impl Default for SignedTxnInBlock { auth_address: None, multisignature: None, }, - logic_signature: None, closing_amount: None, asset_closing_amount: None, sender_rewards: None, diff --git a/crates/algod_client/src/models/signed_txn_in_block.rs b/crates/algod_client/src/models/signed_txn_in_block.rs index d53c37cdf..d4e7da35a 100644 --- a/crates/algod_client/src/models/signed_txn_in_block.rs +++ b/crates/algod_client/src/models/signed_txn_in_block.rs @@ -90,7 +90,6 @@ impl Default for SignedTxnInBlock { auth_address: None, multisignature: None, }, - logic_signature: None, closing_amount: None, asset_closing_amount: None, sender_rewards: None, From 9ada1ffeaf5b0c9e8abdccb0a167cf5a03a83e08 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sat, 31 Jan 2026 18:24:52 -0500 Subject: [PATCH 17/20] wip: default u64 values --- .../templates/models/block/get_block.rs.j2 | 19 +++++++---------- crates/algod_client/src/models/get_block.rs | 21 ++++++++----------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 index ef913d063..d3a85354c 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 @@ -21,9 +21,6 @@ use algokit_transact::AlgorandMsgpack; use crate::models::Block; // Type aliases for clarity -pub type Round = u64; -pub type Period = u64; -pub type Step = u64; pub type Digest = Vec; // DigestSize = sha512.Size256 = 32 bytes pub type Address = Digest; pub type VrfProof = Vec; // VrfProofSize = 80 bytes @@ -68,8 +65,8 @@ pub struct UnauthenticatedCredential { /// its proposer, and the period in which it was proposed. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ProposalValue { - #[serde(rename = "oper", default, skip_serializing_if = "Option::is_none")] - pub original_period: Option, + #[serde(rename = "oper", default)] + pub original_period: u64, #[serde(rename = "oprop")] pub original_proposer: Address, /// BlockDigest = proposal.Block.Digest() @@ -107,12 +104,12 @@ pub struct EquivocationVoteAuthenticator { /// unauthenticatedBundle is a bundle which has not yet been verified. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct UnauthenticatedBundle { - #[serde(rename = "rnd")] - pub round: Round, - #[serde(rename = "per")] - pub period: Period, - #[serde(rename = "step")] - pub step: Step, + #[serde(rename = "rnd", default)] + pub round: u64, + #[serde(rename = "per", default)] + pub period: u64, + #[serde(rename = "step", default)] + pub step: u64, #[serde(rename = "prop")] pub proposal: ProposalValue, #[serde(rename = "vote")] diff --git a/crates/algod_client/src/models/get_block.rs b/crates/algod_client/src/models/get_block.rs index 468bd2bef..ce28bdc72 100644 --- a/crates/algod_client/src/models/get_block.rs +++ b/crates/algod_client/src/models/get_block.rs @@ -21,9 +21,6 @@ use algokit_transact::AlgorandMsgpack; use crate::models::Block; // Type aliases for clarity -pub type Round = u64; -pub type Period = u64; -pub type Step = u64; pub type Digest = Vec; // DigestSize = sha512.Size256 = 32 bytes pub type Address = Digest; pub type VrfProof = Vec; // VrfProofSize = 80 bytes @@ -68,9 +65,9 @@ pub struct UnauthenticatedCredential { /// its proposer, and the period in which it was proposed. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ProposalValue { - #[serde(rename = "oper", default, skip_serializing_if = "Option::is_none")] - pub original_period: Option, - #[serde(rename = "oprop")] + #[serde(rename = "oper", default)] + pub original_period: u64, + #[serde(rename = "oprop", default)] pub original_proposer: Address, /// BlockDigest = proposal.Block.Digest() #[serde(rename = "dig")] @@ -107,12 +104,12 @@ pub struct EquivocationVoteAuthenticator { /// unauthenticatedBundle is a bundle which has not yet been verified. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct UnauthenticatedBundle { - #[serde(rename = "rnd")] - pub round: Round, - #[serde(rename = "per")] - pub period: Period, - #[serde(rename = "step")] - pub step: Step, + #[serde(rename = "rnd", default)] + pub round: u64, + #[serde(rename = "per", default)] + pub period: u64, + #[serde(rename = "step", default)] + pub step: u64, #[serde(rename = "prop")] pub proposal: ProposalValue, #[serde(rename = "vote")] From 2bcd30db4cd550744a4dc875797488f2b3a81537 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sat, 31 Jan 2026 18:27:10 -0500 Subject: [PATCH 18/20] wip: handle empty vecs --- .../templates/models/block/get_block.rs.j2 | 4 ++-- crates/algod_client/src/models/get_block.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 index d3a85354c..5a6aec628 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 @@ -112,9 +112,9 @@ pub struct UnauthenticatedBundle { pub step: u64, #[serde(rename = "prop")] pub proposal: ProposalValue, - #[serde(rename = "vote")] + #[serde(rename = "vote", default, skip_serializing_if = "Vec::is_empty")] pub votes: Vec, - #[serde(rename = "eqv")] + #[serde(rename = "eqv", default, skip_serializing_if = "Vec::is_empty")] pub equivocation_votes: Vec, } diff --git a/crates/algod_client/src/models/get_block.rs b/crates/algod_client/src/models/get_block.rs index ce28bdc72..d9d2cfea6 100644 --- a/crates/algod_client/src/models/get_block.rs +++ b/crates/algod_client/src/models/get_block.rs @@ -67,7 +67,7 @@ pub struct UnauthenticatedCredential { pub struct ProposalValue { #[serde(rename = "oper", default)] pub original_period: u64, - #[serde(rename = "oprop", default)] + #[serde(rename = "oprop")] pub original_proposer: Address, /// BlockDigest = proposal.Block.Digest() #[serde(rename = "dig")] @@ -112,9 +112,9 @@ pub struct UnauthenticatedBundle { pub step: u64, #[serde(rename = "prop")] pub proposal: ProposalValue, - #[serde(rename = "vote")] + #[serde(rename = "vote", default, skip_serializing_if = "Vec::is_empty")] pub votes: Vec, - #[serde(rename = "eqv")] + #[serde(rename = "eqv", default, skip_serializing_if = "Vec::is_empty")] pub equivocation_votes: Vec, } From 5e028d18d0c90deb620a87a332d6ffa768c03850 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sat, 31 Jan 2026 18:57:38 -0500 Subject: [PATCH 19/20] wip: serialize as str if possible --- .../templates/models/block/non_ascii_string.rs.j2 | 11 ++++++++--- crates/algod_client/src/models/non_ascii_string.rs | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/non_ascii_string.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/non_ascii_string.rs.j2 index ff563d8a6..18cd75779 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/non_ascii_string.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/non_ascii_string.rs.j2 @@ -82,8 +82,13 @@ impl Serialize for NonAsciiString { where S: Serializer, { - // Serialize as a msgpack string (not binary) - let s = unsafe { std::str::from_utf8_unchecked(&self.0) }; - serializer.serialize_str(s) + // Try to serialize as UTF-8 string if valid, otherwise as binary + // This matches how the Algorand protocol handles state keys + // TODO: this works for state keys but may not be appropriate for all uses + if let Ok(s) = std::str::from_utf8(&self.0) { + serializer.serialize_str(s) + } else { + serializer.serialize_bytes(&self.0) + } } } diff --git a/crates/algod_client/src/models/non_ascii_string.rs b/crates/algod_client/src/models/non_ascii_string.rs index ff563d8a6..18cd75779 100644 --- a/crates/algod_client/src/models/non_ascii_string.rs +++ b/crates/algod_client/src/models/non_ascii_string.rs @@ -82,8 +82,13 @@ impl Serialize for NonAsciiString { where S: Serializer, { - // Serialize as a msgpack string (not binary) - let s = unsafe { std::str::from_utf8_unchecked(&self.0) }; - serializer.serialize_str(s) + // Try to serialize as UTF-8 string if valid, otherwise as binary + // This matches how the Algorand protocol handles state keys + // TODO: this works for state keys but may not be appropriate for all uses + if let Ok(s) = std::str::from_utf8(&self.0) { + serializer.serialize_str(s) + } else { + serializer.serialize_bytes(&self.0) + } } } From f46c1b6c6346fc96d4dbff354668ff36e4759ddb Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sat, 31 Jan 2026 20:26:32 -0500 Subject: [PATCH 20/20] wip: use rmp_serde::Raw --- .../templates/models/block/non_ascii_string.rs.j2 | 14 ++++++-------- crates/algod_client/src/models/non_ascii_string.rs | 13 +++++-------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/non_ascii_string.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/non_ascii_string.rs.j2 index 18cd75779..e8a1e95cc 100644 --- a/api/oas_generator/rust_oas_generator/templates/models/block/non_ascii_string.rs.j2 +++ b/api/oas_generator/rust_oas_generator/templates/models/block/non_ascii_string.rs.j2 @@ -82,13 +82,11 @@ impl Serialize for NonAsciiString { where S: Serializer, { - // Try to serialize as UTF-8 string if valid, otherwise as binary - // This matches how the Algorand protocol handles state keys - // TODO: this works for state keys but may not be appropriate for all uses - if let Ok(s) = std::str::from_utf8(&self.0) { - serializer.serialize_str(s) - } else { - serializer.serialize_bytes(&self.0) - } + // NOTE: We are using this deprecated function because algod incorrectly encodes non-utf8 + // data as string + #[allow(deprecated)] + let raw = rmp_serde::Raw::from_utf8(self.0.clone()); + raw.serialize(serializer) + } } diff --git a/crates/algod_client/src/models/non_ascii_string.rs b/crates/algod_client/src/models/non_ascii_string.rs index 18cd75779..5e69fac94 100644 --- a/crates/algod_client/src/models/non_ascii_string.rs +++ b/crates/algod_client/src/models/non_ascii_string.rs @@ -82,13 +82,10 @@ impl Serialize for NonAsciiString { where S: Serializer, { - // Try to serialize as UTF-8 string if valid, otherwise as binary - // This matches how the Algorand protocol handles state keys - // TODO: this works for state keys but may not be appropriate for all uses - if let Ok(s) = std::str::from_utf8(&self.0) { - serializer.serialize_str(s) - } else { - serializer.serialize_bytes(&self.0) - } + // NOTE: We are using this deprecated function because algod incorrectly encodes non-utf8 + // data as string + #[allow(deprecated)] + let raw = rmp_serde::Raw::from_utf8(self.0.clone()); + raw.serialize(serializer) } }