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..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 @@ -545,6 +540,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 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/block_app_eval_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 index 8fb08890f..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}; @@ -41,9 +42,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). - #[serde(rename = "lg", skip_serializing_if = "Option::is_none")] - pub logs: Option>, + /// [lg] Application log outputs. + #[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_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/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..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,8 +22,8 @@ pub struct BlockEvalDelta { #[serde(rename = "at")] pub action: u32, /// [bs] bytes value. - #[serde(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/block_state_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 index a7af21e30..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 @@ -13,7 +13,9 @@ use std::collections::HashMap; use crate::models::BlockEvalDelta; +use crate::models::NonAsciiString; + /// BlockStateDelta is a map keyed by state key to BlockEvalDelta. -pub type BlockStateDelta = HashMap; +pub type BlockStateDelta = HashMap; 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..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 @@ -20,6 +20,105 @@ use algokit_transact::AlgorandMsgpack; use crate::models::Block; +// Type aliases for clarity +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", default)] + pub original_period: u64, + #[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", 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", default, skip_serializing_if = "Vec::is_empty")] + pub votes: Vec, + #[serde(rename = "eqv", default, skip_serializing_if = "Vec::is_empty")] + pub equivocation_votes: Vec, +} + + /// Encoded block object. #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] @@ -29,12 +128,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/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..e8a1e95cc --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/models/block/non_ascii_string.rs.j2 @@ -0,0 +1,92 @@ +/// 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, + { + // 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/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..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 @@ -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))] @@ -27,14 +29,6 @@ pub struct SignedTxnInBlock { /// SignedTransaction fields (flattened from algokit_transact) #[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>, /// [ca] Rewards applied to close-remainder-to account. #[serde(rename = "ca", skip_serializing_if = "Option::is_none")] pub closing_amount: Option, @@ -96,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/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 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/block_app_eval_delta.rs b/crates/algod_client/src/models/block_app_eval_delta.rs index 5243d53d2..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}; @@ -41,9 +42,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). - #[serde(rename = "lg", skip_serializing_if = "Option::is_none")] - pub logs: Option>, + /// [lg] Application log outputs. + #[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 3dc01a80f..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,8 +22,8 @@ pub struct BlockEvalDelta { #[serde(rename = "at")] pub action: u32, /// [bs] bytes value. - #[serde(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/block_state_delta.rs b/crates/algod_client/src/models/block_state_delta.rs index 506bd9626..d16eb60f3 100644 --- a/crates/algod_client/src/models/block_state_delta.rs +++ b/crates/algod_client/src/models/block_state_delta.rs @@ -13,5 +13,7 @@ use std::collections::HashMap; use crate::models::BlockEvalDelta; +use crate::models::NonAsciiString; + /// BlockStateDelta is a map keyed by state key to BlockEvalDelta. -pub type BlockStateDelta = HashMap; +pub type BlockStateDelta = HashMap; diff --git a/crates/algod_client/src/models/get_block.rs b/crates/algod_client/src/models/get_block.rs index aee113d70..d9d2cfea6 100644 --- a/crates/algod_client/src/models/get_block.rs +++ b/crates/algod_client/src/models/get_block.rs @@ -20,6 +20,104 @@ use algokit_transact::AlgorandMsgpack; use crate::models::Block; +// Type aliases for clarity +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", default)] + pub original_period: u64, + #[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", 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", default, skip_serializing_if = "Vec::is_empty")] + pub votes: Vec, + #[serde(rename = "eqv", default, skip_serializing_if = "Vec::is_empty")] + pub equivocation_votes: Vec, +} + /// Encoded block object. #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] @@ -28,13 +126,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/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..5e69fac94 --- /dev/null +++ b/crates/algod_client/src/models/non_ascii_string.rs @@ -0,0 +1,91 @@ +/// 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, + { + // 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/signed_txn_in_block.rs b/crates/algod_client/src/models/signed_txn_in_block.rs index 24b03ef00..d4e7da35a 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))] @@ -27,14 +29,6 @@ pub struct SignedTxnInBlock { /// SignedTransaction fields (flattened from algokit_transact) #[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>, /// [ca] Rewards applied to close-remainder-to account. #[serde(rename = "ca", skip_serializing_if = "Option::is_none")] pub closing_amount: Option, @@ -96,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/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(), - } -}