diff --git a/Cargo.lock b/Cargo.lock index 541deda6a..a9db36c50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,6 +35,7 @@ dependencies = [ "algokit_transact_ffi", "base64 0.22.1", "rmp-serde", + "rmpv", "serde", "serde_bytes", "serde_json", 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 9fb2565dd..e0771e209 100644 --- a/api/oas_generator/rust_oas_generator/generator/template_engine.py +++ b/api/oas_generator/rust_oas_generator/generator/template_engine.py @@ -477,10 +477,22 @@ def generate_client( def _generate_base_files(self, context: dict[str, Any], output_dir: Path) -> dict[Path, str]: """Generate base library files.""" src_dir = output_dir / "src" - return { + files: dict[Path, str] = { src_dir / "lib.rs": self.template_engine.render_template("base/lib.rs.j2", context), } + # Inject additional base files for specific clients + # 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 + def _generate_model_files( self, schemas: dict[str, Schema], @@ -501,6 +513,39 @@ def _generate_model_files( models_context = {**context, "schemas": schemas} files[models_dir / "mod.rs"] = self.template_engine.render_template("models/mod.rs.j2", models_context) + # Inject custom, hand-authored models for specific clients + # 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": + # Always generate/override the typed block models + files[models_dir / "block_eval_delta.rs"] = self.template_engine.render_template( + "models/block/block_eval_delta.rs.j2", context + ) + files[models_dir / "block_state_delta.rs"] = self.template_engine.render_template( + "models/block/block_state_delta.rs.j2", context + ) + files[models_dir / "block_account_state_delta.rs"] = self.template_engine.render_template( + "models/block/block_account_state_delta.rs.j2", context + ) + files[models_dir / "block_app_eval_delta.rs"] = self.template_engine.render_template( + "models/block/block_app_eval_delta.rs.j2", context + ) + files[models_dir / "block_state_proof_tracking_data.rs"] = self.template_engine.render_template( + "models/block/block_state_proof_tracking_data.rs.j2", context + ) + files[models_dir / "block_state_proof_tracking.rs"] = self.template_engine.render_template( + "models/block/block_state_proof_tracking.rs.j2", context + ) + files[models_dir / "signed_txn_in_block.rs"] = self.template_engine.render_template( + "models/block/signed_txn_in_block.rs.j2", context + ) + files[models_dir / "block.rs"] = self.template_engine.render_template("models/block/block.rs.j2", context) + # Override GetBlock with a typed model that references Block + files[models_dir / "get_block.rs"] = self.template_engine.render_template( + "models/block/get_block.rs.j2", context + ) + return files def _generate_parameter_enums( diff --git a/api/oas_generator/rust_oas_generator/templates/base/Cargo.toml.j2 b/api/oas_generator/rust_oas_generator/templates/base/Cargo.toml.j2 index c2f6ee95f..6c56dbf5d 100644 --- a/api/oas_generator/rust_oas_generator/templates/base/Cargo.toml.j2 +++ b/api/oas_generator/rust_oas_generator/templates/base/Cargo.toml.j2 @@ -36,6 +36,7 @@ algokit_transact_ffi = { optional = true, version = "0.1.0", path = "../algokit_ algokit_transact = { path = "../algokit_transact" } # MessagePack serialization rmp-serde = "^1.1" +rmpv = { version = "1.3.0", features = ["with-serde"] } {% endif %} # Error handling 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 3a865ec78..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 @@ -7,6 +7,9 @@ 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/base/msgpack_value_bytes.rs.j2 b/api/oas_generator/rust_oas_generator/templates/base/msgpack_value_bytes.rs.j2 new file mode 100644 index 000000000..3c7175f07 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/base/msgpack_value_bytes.rs.j2 @@ -0,0 +1,50 @@ +/// 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(), + } +} + + diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block.rs.j2 new file mode 100644 index 000000000..b910dadf0 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block.rs.j2 @@ -0,0 +1,162 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * 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}; + +#[cfg(feature = "ffi_uniffi")] +use algokit_transact_ffi::SignedTransaction as AlgokitSignedTransaction; + +use algokit_transact::AlgorandMsgpack; + +use crate::models::SignedTxnInBlock; +use crate::models::BlockStateProofTracking; + +/// Block contains the BlockHeader and the list of transactions (Payset). +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] +pub struct Block { + /// [rnd] Round number. + #[serde(rename = "rnd", skip_serializing_if = "Option::is_none")] + pub round: Option, + /// [prev] Previous block hash. + #[serde_as(as = "Option")] + #[serde(rename = "prev", skip_serializing_if = "Option::is_none")] + pub previous_block_hash: Option>, + /// [prev512] Previous block hash using SHA-512. + #[serde_as(as = "Option")] + #[serde(rename = "prev512", skip_serializing_if = "Option::is_none")] + pub previous_block_hash_512: Option>, + /// [seed] Sortition seed. + #[serde_as(as = "Option")] + #[serde(rename = "seed", skip_serializing_if = "Option::is_none")] + pub seed: Option>, + /// [txn] Root of transaction merkle tree using SHA512_256. + #[serde_as(as = "Option")] + #[serde(rename = "txn", skip_serializing_if = "Option::is_none")] + pub transactions_root: Option>, + /// [txn256] Root of transaction vector commitment using SHA256. + #[serde_as(as = "Option")] + #[serde(rename = "txn256", skip_serializing_if = "Option::is_none")] + pub transactions_root_sha256: Option>, + /// [txn512] Root of transaction vector commitment using SHA512. + #[serde_as(as = "Option")] + #[serde(rename = "txn512", skip_serializing_if = "Option::is_none")] + pub transactions_root_sha512: Option>, + /// [ts] Block timestamp in seconds since epoch. + #[serde(rename = "ts", skip_serializing_if = "Option::is_none")] + pub timestamp: Option, + /// [gen] Genesis ID. + #[serde(rename = "gen", skip_serializing_if = "Option::is_none")] + pub genesis_id: Option, + /// [gh] Genesis hash. + #[serde_as(as = "Option")] + #[serde(rename = "gh", skip_serializing_if = "Option::is_none")] + pub genesis_hash: Option>, + /// [prp] Proposer address. + #[serde_as(as = "Option")] + #[serde(rename = "prp", skip_serializing_if = "Option::is_none")] + pub proposer: Option>, + /// [fc] Fees collected in this block. + #[serde(rename = "fc", skip_serializing_if = "Option::is_none")] + pub fees_collected: Option, + /// [bi] Bonus incentive for block proposal. + #[serde(rename = "bi", skip_serializing_if = "Option::is_none")] + pub bonus: Option, + /// [pp] Proposer payout. + #[serde(rename = "pp", skip_serializing_if = "Option::is_none")] + pub proposer_payout: Option, + /// [fees] FeeSink address. + #[serde_as(as = "Option")] + #[serde(rename = "fees", skip_serializing_if = "Option::is_none")] + pub fee_sink: Option>, + /// [rwd] RewardsPool address. + #[serde_as(as = "Option")] + #[serde(rename = "rwd", skip_serializing_if = "Option::is_none")] + pub rewards_pool: Option>, + /// [earn] Rewards level. + #[serde(rename = "earn", skip_serializing_if = "Option::is_none")] + pub rewards_level: Option, + /// [rate] Rewards rate. + #[serde(rename = "rate", skip_serializing_if = "Option::is_none")] + pub rewards_rate: Option, + /// [frac] Rewards residue. + #[serde(rename = "frac", skip_serializing_if = "Option::is_none")] + pub rewards_residue: Option, + /// [rwcalr] Rewards recalculation round. + #[serde(rename = "rwcalr", skip_serializing_if = "Option::is_none")] + pub rewards_recalculation_round: Option, + /// [proto] Current consensus protocol. + #[serde(rename = "proto", skip_serializing_if = "Option::is_none")] + pub current_protocol: Option, + /// [nextproto] Next proposed protocol. + #[serde(rename = "nextproto", skip_serializing_if = "Option::is_none")] + pub next_protocol: Option, + /// [nextyes] Next protocol approvals. + #[serde(rename = "nextyes", skip_serializing_if = "Option::is_none")] + pub next_protocol_approvals: Option, + /// [nextbefore] Next protocol vote deadline. + #[serde(rename = "nextbefore", skip_serializing_if = "Option::is_none")] + pub next_protocol_vote_before: Option, + /// [nextswitch] Next protocol switch round. + #[serde(rename = "nextswitch", skip_serializing_if = "Option::is_none")] + pub next_protocol_switch_on: Option, + /// [upgradeprop] Upgrade proposal. + #[serde(rename = "upgradeprop", skip_serializing_if = "Option::is_none")] + pub upgrade_propose: Option, + /// [upgradedelay] Upgrade delay in rounds. + #[serde(rename = "upgradedelay", skip_serializing_if = "Option::is_none")] + pub upgrade_delay: Option, + /// [upgradeyes] Upgrade approval flag. + #[serde(rename = "upgradeyes", skip_serializing_if = "Option::is_none")] + pub upgrade_approve: Option, + /// [tc] Transaction counter. + #[serde(rename = "tc", skip_serializing_if = "Option::is_none")] + pub txn_counter: Option, + /// [spt] State proof tracking data keyed by state proof type. + #[serde(rename = "spt", skip_serializing_if = "Option::is_none", default)] + pub state_proof_tracking: Option, + /// [partupdrmv] Expired participation accounts. + #[serde_as(as = "Option>")] + #[serde(rename = "partupdrmv", skip_serializing_if = "Option::is_none")] + pub expired_participation_accounts: Option>>, + /// [partupdabs] Absent participation accounts. + #[serde_as(as = "Option>")] + #[serde(rename = "partupdabs", skip_serializing_if = "Option::is_none")] + pub absent_participation_accounts: Option>>, + /// [txns] Block transactions (Payset). + #[serde(rename = "txns", skip_serializing_if = "Option::is_none")] + pub transactions: Option>, +} + +impl AlgorandMsgpack for Block { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl Block { + /// Default constructor for Block + pub fn new() -> Block { + Block::default() + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_account_state_delta.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_account_state_delta.rs.j2 new file mode 100644 index 000000000..cb14f7730 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_account_state_delta.rs.j2 @@ -0,0 +1,29 @@ +/* + * {{ 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; +use serde::{Deserialize, Serialize}; +use crate::models::BlockStateDelta; + +/// BlockAccountStateDelta pairs an address with a BlockStateDelta map. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] +pub struct BlockAccountStateDelta { + #[serde(rename = "address")] + pub address: String, + #[serde(rename = "delta")] + pub delta: BlockStateDelta, +} + +impl BlockAccountStateDelta { + pub fn new(address: String, delta: BlockStateDelta) -> Self { Self { address, delta } } +} + + 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 new file mode 100644 index 000000000..8fb08890f --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_app_eval_delta.rs.j2 @@ -0,0 +1,57 @@ +/* + * {{ 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; + +/// BlockAppEvalDelta 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 BlockAppEvalDelta { + /// [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 BlockAppEvalDelta { + const PREFIX: &'static [u8] = b""; +} + +impl BlockAppEvalDelta { + pub fn new() -> BlockAppEvalDelta { BlockAppEvalDelta::default() } +} + + 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 new file mode 100644 index 000000000..ae175315a --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_application_eval_delta.rs.j2 @@ -0,0 +1,57 @@ +/* + * {{ 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 new file mode 100644 index 000000000..64049ae23 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_eval_delta.rs.j2 @@ -0,0 +1,56 @@ +/* + * {{ 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; +use serde::{Deserialize, Serialize}; + +use algokit_transact::AlgorandMsgpack; + +/// BlockEvalDelta represents a TEAL value delta (block/msgpack wire keys). +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] +pub struct BlockEvalDelta { + /// [at] delta action. + #[serde(rename = "at")] + pub action: u32, + /// [bs] bytes value. + #[serde(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, +} + +impl AlgorandMsgpack for BlockEvalDelta { + const PREFIX: &'static [u8] = b""; +} + +impl BlockEvalDelta { + /// Constructor for BlockEvalDelta + pub fn new(action: u32) -> BlockEvalDelta { + BlockEvalDelta { + action, + bytes: None, + uint: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} + + 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 new file mode 100644 index 000000000..a7af21e30 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_state_delta.rs.j2 @@ -0,0 +1,19 @@ +/* + * {{ 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; +use std::collections::HashMap; + +use crate::models::BlockEvalDelta; + +/// BlockStateDelta is a map keyed by state key to BlockEvalDelta. +pub type BlockStateDelta = HashMap; + + diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_state_proof_tracking.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_state_proof_tracking.rs.j2 new file mode 100644 index 000000000..791b72b3f --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_state_proof_tracking.rs.j2 @@ -0,0 +1,20 @@ +/* + * {{ 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; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use crate::models::BlockStateProofTrackingData; + +/// Tracks state proof metadata by state proof type. +pub type BlockStateProofTracking = HashMap; + + diff --git a/api/oas_generator/rust_oas_generator/templates/models/block/block_state_proof_tracking_data.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/block/block_state_proof_tracking_data.rs.j2 new file mode 100644 index 000000000..a8e3aeeed --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/models/block/block_state_proof_tracking_data.rs.j2 @@ -0,0 +1,44 @@ +/* + * {{ 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; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, Bytes}; + +use algokit_transact::AlgorandMsgpack; + +/// Tracking metadata for a specific StateProofType. +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] +pub struct BlockStateProofTrackingData { + /// [v] Vector commitment root of state proof voters (may be absent when not applicable). + #[serde_as(as = "Option")] + #[serde(rename = "v", skip_serializing_if = "Option::is_none", default)] + pub state_proof_voters_commitment: Option>, + /// [t] Online total weight during state proof round. + #[serde(rename = "t", skip_serializing_if = "Option::is_none", default)] + pub state_proof_online_total_weight: Option, + /// [n] Next round for which state proofs are accepted. + #[serde(rename = "n", skip_serializing_if = "Option::is_none", default)] + pub state_proof_next_round: Option, +} + +impl AlgorandMsgpack for BlockStateProofTrackingData { + const PREFIX: &'static [u8] = b""; +} + +impl BlockStateProofTrackingData { + pub fn new() -> BlockStateProofTrackingData { + BlockStateProofTrackingData::default() + } +} + + 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 new file mode 100644 index 000000000..aee113d70 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/models/block/get_block.rs.j2 @@ -0,0 +1,59 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +#[cfg(not(feature = "ffi_uniffi"))] +use algokit_transact::SignedTransaction as AlgokitSignedTransaction; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "ffi_uniffi")] +use algokit_transact_ffi::SignedTransaction as AlgokitSignedTransaction; + +use algokit_transact::AlgorandMsgpack; + +use crate::models::Block; + +/// Encoded block object. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] +pub struct GetBlock { + /// Block data including header and transactions. + #[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>, +} + +impl AlgorandMsgpack for GetBlock { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl GetBlock { + /// Constructor for GetBlock + pub fn new(block: Block) -> GetBlock { + GetBlock { block, cert: None } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} 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 new file mode 100644 index 000000000..24b03ef00 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/models/block/signed_txn_in_block.rs.j2 @@ -0,0 +1,133 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +#[cfg(not(feature = "ffi_uniffi"))] +use algokit_transact::SignedTransaction as AlgokitSignedTransaction; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "ffi_uniffi")] +use algokit_transact_ffi::SignedTransaction as AlgokitSignedTransaction; + +use algokit_transact::AlgorandMsgpack; + +use crate::models::BlockAppEvalDelta; + +/// 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))] +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, + /// [aca] Asset closing amount. + #[serde(rename = "aca", skip_serializing_if = "Option::is_none")] + pub asset_closing_amount: Option, + /// [rs] Sender rewards. + #[serde(rename = "rs", skip_serializing_if = "Option::is_none")] + pub sender_rewards: Option, + /// [rr] Receiver rewards. + #[serde(rename = "rr", skip_serializing_if = "Option::is_none")] + pub receiver_rewards: Option, + /// [rc] Close rewards. + #[serde(rename = "rc", skip_serializing_if = "Option::is_none")] + pub close_rewards: Option, + /// [dt] State changes from app execution. + #[serde(rename = "dt", skip_serializing_if = "Option::is_none")] + pub eval_delta: Option, + /// [caid] Asset ID if created. + #[serde(rename = "caid", skip_serializing_if = "Option::is_none")] + pub config_asset: Option, + /// [apid] App ID if created. + #[serde(rename = "apid", skip_serializing_if = "Option::is_none")] + pub application_id: Option, + /// [hgi] Has genesis ID flag. + #[serde(rename = "hgi", skip_serializing_if = "Option::is_none")] + pub has_genesis_id: Option, + /// [hgh] Has genesis hash flag. + #[serde(rename = "hgh", skip_serializing_if = "Option::is_none")] + pub has_genesis_hash: Option, +} + +impl Default for SignedTxnInBlock { + fn default() -> Self { + Self { + signed_transaction: AlgokitSignedTransaction { + #[allow(clippy::useless_conversion)] + transaction: algokit_transact::Transaction::Payment( + algokit_transact::PaymentTransactionFields { + header: algokit_transact::TransactionHeader { + sender: Default::default(), + fee: None, + first_valid: 0, + last_valid: 0, + genesis_hash: None, + genesis_id: None, + note: None, + rekey_to: None, + lease: None, + group: None, + }, + receiver: Default::default(), + amount: 0, + close_remainder_to: None, + }, + ) + .into(), + signature: None, + auth_address: None, + multisignature: None, + }, + logic_signature: None, + closing_amount: None, + asset_closing_amount: None, + sender_rewards: None, + receiver_rewards: None, + close_rewards: None, + eval_delta: None, + config_asset: None, + application_id: None, + has_genesis_id: None, + has_genesis_hash: None, + } + } +} + +impl AlgorandMsgpack for SignedTxnInBlock { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl SignedTxnInBlock { + /// Default constructor for SignedTxnInBlock + pub fn new() -> SignedTxnInBlock { + SignedTxnInBlock::default() + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} 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 c8e395b81..a0338c124 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 @@ -19,3 +19,24 @@ pub mod {{ schema.rust_file_name }}; pub use self::{{ schema.rust_file_name }}::{{ schema.rust_struct_name }}; {% endfor %} +{% set client_type = get_client_type(spec) %} +{% if client_type == "Algod" %} +// Custom Algod typed block models (Block* to avoid shape collisions) +pub mod block; +pub use self::block::Block; +pub mod signed_txn_in_block; +pub use self::signed_txn_in_block::SignedTxnInBlock; +pub mod block_eval_delta; +pub use self::block_eval_delta::BlockEvalDelta; +pub mod block_state_delta; +pub use self::block_state_delta::BlockStateDelta; +pub mod block_account_state_delta; +pub use self::block_account_state_delta::BlockAccountStateDelta; +pub mod block_app_eval_delta; +pub use self::block_app_eval_delta::BlockAppEvalDelta; +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; +{% endif %} + diff --git a/api/oas_generator/ts_oas_generator/generator/template_engine.py b/api/oas_generator/ts_oas_generator/generator/template_engine.py index ea5082c33..02c0bb62c 100644 --- a/api/oas_generator/ts_oas_generator/generator/template_engine.py +++ b/api/oas_generator/ts_oas_generator/generator/template_engine.py @@ -684,11 +684,99 @@ def generate( # Generate components files.update(self.schema_processor.generate_models(output_dir, all_schemas)) + + # Inject custom Algod models if this spec targets Algod + client_type = self._detect_client_type(spec) + if client_type == "Algod": + models_dir = output_dir / constants.DirectoryName.SRC / constants.DirectoryName.MODELS + # Custom typed block models + # Block-specific models (prefixed to avoid shape collisions) + files[models_dir / "block-eval-delta.ts"] = self.renderer.render( + "models/block/block-eval-delta.ts.j2", + {"spec": spec}, + ) + files[models_dir / "block-state-delta.ts"] = self.renderer.render( + "models/block/block-state-delta.ts.j2", + {"spec": spec}, + ) + files[models_dir / "block-account-state-delta.ts"] = self.renderer.render( + "models/block/block-account-state-delta.ts.j2", + {"spec": spec}, + ) + # BlockAppEvalDelta is implemented by repurposing application-eval-delta.ts.j2 to new name + files[models_dir / "block-app-eval-delta.ts"] = self.renderer.render( + "models/block/application-eval-delta.ts.j2", + {"spec": spec}, + ) + files[models_dir / "block_state_proof_tracking_data.ts"] = self.renderer.render( + "models/block/block-state-proof-tracking-data.ts.j2", + {"spec": spec}, + ) + files[models_dir / "block_state_proof_tracking.ts"] = self.renderer.render( + "models/block/block-state-proof-tracking.ts.j2", + {"spec": spec}, + ) + files[models_dir / "signed-txn-in-block.ts"] = self.renderer.render( + "models/block/signed-txn-in-block.ts.j2", + {"spec": spec}, + ) + files[models_dir / "block.ts"] = self.renderer.render( + "models/block/block.ts.j2", + {"spec": spec}, + ) + files[models_dir / "get-block.ts"] = self.renderer.render( + "models/block/get-block.ts.j2", + {"spec": spec}, + ) + + # Ensure index exports include the custom models + index_path = models_dir / constants.INDEX_FILE + base_index = self.renderer.render(constants.MODELS_INDEX_TEMPLATE, {"schemas": all_schemas}) + extras = ( + "\n" + "export type { BlockEvalDelta } from './block-eval-delta';\n" + "export { BlockEvalDeltaMeta } from './block-eval-delta';\n" + "export type { BlockStateDelta } from './block-state-delta';\n" + "export { BlockStateDeltaMeta } from './block-state-delta';\n" + "export type { BlockAccountStateDelta } from './block-account-state-delta';\n" + "export { BlockAccountStateDeltaMeta } from './block-account-state-delta';\n" + "export type { BlockAppEvalDelta } from './block-app-eval-delta';\n" + "export { BlockAppEvalDeltaMeta } from './block-app-eval-delta';\n" + "export type { BlockStateProofTrackingData } from './block_state_proof_tracking_data';\n" + "export { BlockStateProofTrackingDataMeta } from './block_state_proof_tracking_data';\n" + "export type { BlockStateProofTracking } from './block_state_proof_tracking';\n" + "export { BlockStateProofTrackingMeta } from './block_state_proof_tracking';\n" + "export type { Block } from './block';\n" + "export { BlockMeta } from './block';\n" + "export type { SignedTxnInBlock } from './signed-txn-in-block';\n" + "export { SignedTxnInBlockMeta } from './signed-txn-in-block';\n" + "export type { GetBlock } from './get-block';\n" + "export { GetBlockMeta } from './get-block';\n" + ) + files[index_path] = base_index + extras files.update(self.operation_processor.generate_service(output_dir, ops_by_tag, tags, service_class)) files.update(self._generate_client_files(output_dir, client_class, service_class)) return files + @staticmethod + def _detect_client_type(spec: Schema) -> str: + """Detect client type from the OpenAPI spec title.""" + try: + title = (spec.get("info", {}) or {}).get("title", "") + if not isinstance(title, str): + return "Api" + tl = title.lower() + if "algod" in tl: + return "Algod" + if "indexer" in tl: + return "Indexer" + if "kmd" in tl: + return "Kmd" + return (title.split()[0] or "Api").title() + except Exception: + return "Api" + def _generate_runtime( self, output_dir: Path, diff --git a/api/oas_generator/ts_oas_generator/templates/base/src/core/model-runtime.ts.j2 b/api/oas_generator/ts_oas_generator/templates/base/src/core/model-runtime.ts.j2 index 14061fe01..72d16545d 100644 --- a/api/oas_generator/ts_oas_generator/templates/base/src/core/model-runtime.ts.j2 +++ b/api/oas_generator/ts_oas_generator/templates/base/src/core/model-runtime.ts.j2 @@ -38,10 +38,15 @@ export type FieldType = ScalarFieldType | CodecFieldType | ModelFieldType | Arra export interface FieldMetadata { readonly name: string; - readonly wireKey: string; + readonly wireKey?: string; readonly optional: boolean; readonly nullable: boolean; readonly type: FieldType; + /** + * If true and the field is a SignedTransaction codec, its encoded map entries + * are merged into the parent object (no own wire key). + */ + readonly flattened?: boolean; } export type ModelKind = 'object' | 'array' | 'passthrough'; @@ -56,6 +61,19 @@ export interface ModelMetadata { readonly passThrough?: FieldType; } +// Registry for model metadata to avoid direct circular imports between model files +const modelMetaRegistry = new Map(); + +export function registerModelMeta(name: string, meta: ModelMetadata): void { + modelMetaRegistry.set(name, meta); +} + +export function getModelMeta(name: string): ModelMetadata { + const meta = modelMetaRegistry.get(name); + if (!meta) throw new Error(`Model metadata not registered: ${name}`); + return meta; +} + export interface TypeCodec { encode(value: TValue, format: BodyFormat): unknown; decode(value: unknown, format: BodyFormat): TValue; @@ -114,6 +132,9 @@ export class AlgorandSerializer { private static transformObject(value: unknown, meta: ModelMetadata, ctx: TransformContext): unknown { const fields = meta.fields ?? []; + const hasFlattenedSignedTxn = fields.some( + (f) => f.flattened && f.type.kind === 'codec' && f.type.codecKey === 'SignedTransaction', + ); if (ctx.direction === 'encode') { const src = value as Record; const out: Record = {}; @@ -122,7 +143,13 @@ export class AlgorandSerializer { if (fieldValue === undefined) continue; const encoded = this.transformType(fieldValue, field.type, ctx); if (encoded === undefined && fieldValue === undefined) continue; - out[field.wireKey] = encoded; + if (field.flattened && field.type.kind === 'codec' && field.type.codecKey === 'SignedTransaction') { + // Merge signed transaction map into parent + const mapValue = encoded as Record; + for (const [k, v] of Object.entries(mapValue ?? {})) out[k] = v; + continue; + } + if (field.wireKey) out[field.wireKey] = encoded; } if (meta.additionalProperties) { for (const [key, val] of Object.entries(src)) { @@ -135,7 +162,7 @@ export class AlgorandSerializer { const src = value as Record; const out: Record = {}; - const fieldByWire = new Map(fields.map((field) => [field.wireKey, field])); + const fieldByWire = new Map(fields.filter((f) => !!f.wireKey).map((field) => [field.wireKey as string, field])); for (const [wireKey, wireValue] of Object.entries(src)) { const field = fieldByWire.get(wireKey); @@ -148,7 +175,19 @@ export class AlgorandSerializer { out[wireKey] = this.transformType(wireValue, meta.additionalProperties, ctx); continue; } - out[wireKey] = wireValue; + // If we have a flattened SignedTransaction, skip unknown keys (e.g., 'sig', 'txn') + if (!hasFlattenedSignedTxn) { + out[wireKey] = wireValue; + } + } + + // If there are flattened fields, attempt to reconstruct them from remaining keys by decoding + for (const field of fields) { + if (out[field.name] !== undefined) continue; + if (field.flattened && field.type.kind === 'codec' && field.type.codecKey === 'SignedTransaction') { + // Reconstruct from entire object map + out[field.name] = this.applyCodec(src, 'SignedTransaction', ctx); + } } return out; diff --git a/api/oas_generator/ts_oas_generator/templates/models/block/application-eval-delta.ts.j2 b/api/oas_generator/ts_oas_generator/templates/models/block/application-eval-delta.ts.j2 new file mode 100644 index 000000000..849ec0f21 --- /dev/null +++ b/api/oas_generator/ts_oas_generator/templates/models/block/application-eval-delta.ts.j2 @@ -0,0 +1,37 @@ +import type { ModelMetadata } from '../core/model-runtime'; +import { getModelMeta, registerModelMeta } from '../core/model-runtime'; +import type { SignedTxnInBlock } from './signed-txn-in-block'; +import type { BlockStateDelta } from './block-state-delta'; +import { BlockStateDeltaMeta } from './block-state-delta'; + +/** + * State changes from application execution, including inner transactions and logs. + */ +export interface BlockAppEvalDelta { + /** [gd] Global state delta for the application. */ + globalDelta?: BlockStateDelta; + /** [ld] Local state deltas keyed by address index. */ + localDeltas?: Record; + /** [itx] Inner transactions produced by this application execution. */ + innerTxns?: SignedTxnInBlock[]; + /** [sa] Shared accounts referenced by local deltas. */ + sharedAccounts?: Uint8Array[]; + /** [lg] Application log outputs. */ + logs?: Uint8Array[]; +} + +export const BlockAppEvalDeltaMeta: ModelMetadata = { + name: 'BlockAppEvalDelta', + kind: 'object', + fields: [ + { name: 'globalDelta', wireKey: 'gd', optional: true, nullable: false, type: { kind: 'model', meta: () => BlockStateDeltaMeta } }, + { name: 'localDeltas', wireKey: 'ld', optional: true, nullable: false, type: { kind: 'record', value: { kind: 'model', meta: () => BlockStateDeltaMeta } } }, + { name: 'innerTxns', wireKey: 'itx', optional: true, nullable: false, type: { kind: 'array', item: { kind: 'model', meta: () => getModelMeta('SignedTxnInBlock') } } }, + { name: 'sharedAccounts', wireKey: 'sa', optional: true, nullable: false, type: { kind: 'array', item: { kind: 'scalar', isBytes: true } } }, + { name: 'logs', wireKey: 'lg', optional: true, nullable: false, type: { kind: 'array', item: { kind: 'scalar', isBytes: true } } }, + ], +}; + +registerModelMeta('BlockAppEvalDelta', BlockAppEvalDeltaMeta); + + diff --git a/api/oas_generator/ts_oas_generator/templates/models/block/block-account-state-delta.ts.j2 b/api/oas_generator/ts_oas_generator/templates/models/block/block-account-state-delta.ts.j2 new file mode 100644 index 000000000..d49bb97a1 --- /dev/null +++ b/api/oas_generator/ts_oas_generator/templates/models/block/block-account-state-delta.ts.j2 @@ -0,0 +1,22 @@ +import type { ModelMetadata } from '../core/model-runtime'; +import { registerModelMeta } from '../core/model-runtime'; +import { BlockStateDeltaMeta } from './block-state-delta'; + +/** BlockAccountStateDelta pairs an address with a BlockStateDelta map. */ +export interface BlockAccountStateDelta { + address: string; + delta: import('./block-state-delta').BlockStateDelta; +} + +export const BlockAccountStateDeltaMeta: ModelMetadata = { + name: 'BlockAccountStateDelta', + kind: 'object', + fields: [ + { name: 'address', wireKey: 'address', optional: false, nullable: false, type: { kind: 'scalar' } }, + { name: 'delta', wireKey: 'delta', optional: false, nullable: false, type: { kind: 'model', meta: () => BlockStateDeltaMeta } }, + ], +}; + +registerModelMeta('BlockAccountStateDelta', BlockAccountStateDeltaMeta); + + diff --git a/api/oas_generator/ts_oas_generator/templates/models/block/block-eval-delta.ts.j2 b/api/oas_generator/ts_oas_generator/templates/models/block/block-eval-delta.ts.j2 new file mode 100644 index 000000000..93d9eb020 --- /dev/null +++ b/api/oas_generator/ts_oas_generator/templates/models/block/block-eval-delta.ts.j2 @@ -0,0 +1,26 @@ +import type { ModelMetadata } from '../core/model-runtime'; +import { registerModelMeta } from '../core/model-runtime'; + +/** BlockEvalDelta represents a TEAL value delta (block/msgpack wire keys). */ +export interface BlockEvalDelta { + /** [at] delta action. */ + action: number; + /** [bs] bytes value. */ + bytes?: string; + /** [ui] uint value. */ + uint?: bigint; +} + +export const BlockEvalDeltaMeta: ModelMetadata = { + name: 'BlockEvalDelta', + kind: 'object', + fields: [ + { name: 'action', wireKey: 'at', optional: false, nullable: false, type: { kind: 'scalar' } }, + { name: 'bytes', wireKey: 'bs', optional: true, nullable: false, type: { kind: 'scalar' } }, + { name: 'uint', wireKey: 'ui', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + ], +}; + +registerModelMeta('BlockEvalDelta', BlockEvalDeltaMeta); + + diff --git a/api/oas_generator/ts_oas_generator/templates/models/block/block-state-delta.ts.j2 b/api/oas_generator/ts_oas_generator/templates/models/block/block-state-delta.ts.j2 new file mode 100644 index 000000000..945a2f890 --- /dev/null +++ b/api/oas_generator/ts_oas_generator/templates/models/block/block-state-delta.ts.j2 @@ -0,0 +1,16 @@ +import type { ModelMetadata } from '../core/model-runtime'; +import { registerModelMeta } from '../core/model-runtime'; +import { BlockEvalDeltaMeta } from './block-eval-delta'; + +/** BlockStateDelta is a map keyed by state key to BlockEvalDelta. */ +export type BlockStateDelta = Record; + +export const BlockStateDeltaMeta: ModelMetadata = { + name: 'BlockStateDelta', + kind: 'object', + additionalProperties: { kind: 'model', meta: () => BlockEvalDeltaMeta }, +}; + +registerModelMeta('BlockStateDelta', BlockStateDeltaMeta); + + diff --git a/api/oas_generator/ts_oas_generator/templates/models/block/block-state-proof-tracking-data.ts.j2 b/api/oas_generator/ts_oas_generator/templates/models/block/block-state-proof-tracking-data.ts.j2 new file mode 100644 index 000000000..6306e924f --- /dev/null +++ b/api/oas_generator/ts_oas_generator/templates/models/block/block-state-proof-tracking-data.ts.j2 @@ -0,0 +1,26 @@ +import type { ModelMetadata } from '../core/model-runtime'; +import { registerModelMeta } from '../core/model-runtime'; + +/** Tracking metadata for a specific StateProofType. */ +export interface BlockStateProofTrackingData { + /** [v] Vector commitment root of state proof voters. */ + stateProofVotersCommitment?: Uint8Array; + /** [t] Online total weight during state proof round. */ + stateProofOnlineTotalWeight?: bigint; + /** [n] Next round for which state proofs are accepted. */ + stateProofNextRound?: bigint; +} + +export const BlockStateProofTrackingDataMeta: ModelMetadata = { + name: 'BlockStateProofTrackingData', + kind: 'object', + fields: [ + { name: 'stateProofVotersCommitment', wireKey: 'v', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'stateProofOnlineTotalWeight', wireKey: 't', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'stateProofNextRound', wireKey: 'n', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + ], +}; + +registerModelMeta('BlockStateProofTrackingData', BlockStateProofTrackingDataMeta); + + diff --git a/api/oas_generator/ts_oas_generator/templates/models/block/block-state-proof-tracking.ts.j2 b/api/oas_generator/ts_oas_generator/templates/models/block/block-state-proof-tracking.ts.j2 new file mode 100644 index 000000000..c2ccaef96 --- /dev/null +++ b/api/oas_generator/ts_oas_generator/templates/models/block/block-state-proof-tracking.ts.j2 @@ -0,0 +1,17 @@ +import type { ModelMetadata } from '../core/model-runtime'; +import { registerModelMeta } from '../core/model-runtime'; +import type { BlockStateProofTrackingData } from './block_state_proof_tracking_data'; +import { BlockStateProofTrackingDataMeta } from './block_state_proof_tracking_data'; + +/** Tracks state proof metadata by state proof type. */ +export type BlockStateProofTracking = Record; + +export const BlockStateProofTrackingMeta: ModelMetadata = { + name: 'BlockStateProofTracking', + kind: 'object', + additionalProperties: { kind: 'model', meta: () => BlockStateProofTrackingDataMeta }, +}; + +registerModelMeta('BlockStateProofTracking', BlockStateProofTrackingMeta); + + diff --git a/api/oas_generator/ts_oas_generator/templates/models/block/block.ts.j2 b/api/oas_generator/ts_oas_generator/templates/models/block/block.ts.j2 new file mode 100644 index 000000000..465ebd0df --- /dev/null +++ b/api/oas_generator/ts_oas_generator/templates/models/block/block.ts.j2 @@ -0,0 +1,120 @@ +import type { ModelMetadata } from '../core/model-runtime'; +import type { SignedTxnInBlock } from './signed-txn-in-block'; +import { SignedTxnInBlockMeta } from './signed-txn-in-block'; +import type { BlockStateProofTracking } from './block_state_proof_tracking'; +import { BlockStateProofTrackingMeta } from './block_state_proof_tracking'; + +/** + * Block contains the BlockHeader and the list of transactions (Payset). + */ +export interface Block { + /** [rnd] Round number. */ + round?: bigint; + /** [prev] Previous block hash. */ + previousBlockHash?: Uint8Array; + /** [prev512] Previous block hash using SHA-512. */ + previousBlockHash512?: Uint8Array; + /** [seed] Sortition seed. */ + seed?: Uint8Array; + /** [txn] Root of transaction merkle tree using SHA512_256. */ + transactionsRoot?: Uint8Array; + /** [txn256] Root of transaction vector commitment using SHA256. */ + transactionsRootSha256?: Uint8Array; + /** [txn512] Root of transaction vector commitment using SHA512. */ + transactionsRootSha512?: Uint8Array; + /** [ts] Block timestamp in seconds since epoch. */ + timestamp?: bigint; + /** [gen] Genesis ID. */ + genesisId?: string; + /** [gh] Genesis hash. */ + genesisHash?: Uint8Array; + /** [prp] Proposer address. */ + proposer?: Uint8Array; + /** [fc] Fees collected in this block. */ + feesCollected?: bigint; + /** [bi] Bonus incentive for block proposal. */ + bonus?: bigint; + /** [pp] Proposer payout. */ + proposerPayout?: bigint; + /** [fees] FeeSink address. */ + feeSink?: Uint8Array; + /** [rwd] RewardsPool address. */ + rewardsPool?: Uint8Array; + /** [earn] Rewards level. */ + rewardsLevel?: bigint; + /** [rate] Rewards rate. */ + rewardsRate?: bigint; + /** [frac] Rewards residue. */ + rewardsResidue?: bigint; + /** [rwcalr] Rewards recalculation round. */ + rewardsRecalculationRound?: bigint; + /** [proto] Current consensus protocol. */ + currentProtocol?: string; + /** [nextproto] Next proposed protocol. */ + nextProtocol?: string; + /** [nextyes] Next protocol approvals. */ + nextProtocolApprovals?: bigint; + /** [nextbefore] Next protocol vote deadline. */ + nextProtocolVoteBefore?: bigint; + /** [nextswitch] Next protocol switch round. */ + nextProtocolSwitchOn?: bigint; + /** [upgradeprop] Upgrade proposal. */ + upgradePropose?: string; + /** [upgradedelay] Upgrade delay in rounds. */ + upgradeDelay?: bigint; + /** [upgradeyes] Upgrade approval flag. */ + upgradeApprove?: boolean; + /** [tc] Transaction counter. */ + txnCounter?: bigint; + /** [spt] State proof tracking data keyed by state proof type. */ + stateProofTracking?: BlockStateProofTracking; + /** [partupdrmv] Expired participation accounts. */ + expiredParticipationAccounts?: Uint8Array[]; + /** [partupdabs] Absent participation accounts. */ + absentParticipationAccounts?: Uint8Array[]; + /** [txns] Block transactions (Payset). */ + transactions?: SignedTxnInBlock[]; +} + +export const BlockMeta: ModelMetadata = { + name: 'Block', + kind: 'object', + fields: [ + { name: 'round', wireKey: 'rnd', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'previousBlockHash', wireKey: 'prev', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'previousBlockHash512', wireKey: 'prev512', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'seed', wireKey: 'seed', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'transactionsRoot', wireKey: 'txn', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'transactionsRootSha256', wireKey: 'txn256', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'transactionsRootSha512', wireKey: 'txn512', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'timestamp', wireKey: 'ts', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'genesisId', wireKey: 'gen', optional: true, nullable: false, type: { kind: 'scalar' } }, + { name: 'genesisHash', wireKey: 'gh', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'proposer', wireKey: 'prp', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'feesCollected', wireKey: 'fc', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'bonus', wireKey: 'bi', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'proposerPayout', wireKey: 'pp', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'feeSink', wireKey: 'fees', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'rewardsPool', wireKey: 'rwd', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'rewardsLevel', wireKey: 'earn', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'rewardsRate', wireKey: 'rate', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'rewardsResidue', wireKey: 'frac', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'rewardsRecalculationRound', wireKey: 'rwcalr', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'currentProtocol', wireKey: 'proto', optional: true, nullable: false, type: { kind: 'scalar' } }, + { name: 'nextProtocol', wireKey: 'nextproto', optional: true, nullable: false, type: { kind: 'scalar' } }, + { name: 'nextProtocolApprovals', wireKey: 'nextyes', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'nextProtocolVoteBefore', wireKey: 'nextbefore', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'nextProtocolSwitchOn', wireKey: 'nextswitch', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'upgradePropose', wireKey: 'upgradeprop', optional: true, nullable: false, type: { kind: 'scalar' } }, + { name: 'upgradeDelay', wireKey: 'upgradedelay', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'upgradeApprove', wireKey: 'upgradeyes', optional: true, nullable: false, type: { kind: 'scalar' } }, + { name: 'txnCounter', wireKey: 'tc', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'stateProofTracking', wireKey: 'spt', optional: true, nullable: false, type: { kind: 'model', meta: () => BlockStateProofTrackingMeta } }, + { name: 'expiredParticipationAccounts', wireKey: 'partupdrmv', optional: true, nullable: false, type: { kind: 'array', item: { kind: 'scalar', isBytes: true } } }, + { name: 'absentParticipationAccounts', wireKey: 'partupdabs', optional: true, nullable: false, type: { kind: 'array', item: { kind: 'scalar', isBytes: true } } }, + { name: 'transactions', wireKey: 'txns', optional: true, nullable: false, type: { kind: 'array', item: { kind: 'model', meta: () => SignedTxnInBlockMeta } } }, + ], +}; + + + diff --git a/api/oas_generator/ts_oas_generator/templates/models/block/get-block.ts.j2 b/api/oas_generator/ts_oas_generator/templates/models/block/get-block.ts.j2 new file mode 100644 index 000000000..04816e656 --- /dev/null +++ b/api/oas_generator/ts_oas_generator/templates/models/block/get-block.ts.j2 @@ -0,0 +1,22 @@ +import type { ModelMetadata } from '../core/model-runtime'; +import type { Block } from './block'; +import { BlockMeta } from './block'; + +export type GetBlock = { + /** Block data including header and transactions. */ + block: Block; + /** Block certificate (msgpack only). */ + cert?: Record; +}; + +export const GetBlockMeta: ModelMetadata = { + name: 'GetBlock', + kind: 'object', + fields: [ + { name: 'block', wireKey: 'block', optional: false, nullable: false, type: { kind: 'model', meta: () => BlockMeta } }, + { name: 'cert', wireKey: 'cert', optional: true, nullable: false, type: { kind: 'scalar' } }, + ], +}; + + + diff --git a/api/oas_generator/ts_oas_generator/templates/models/block/signed-txn-in-block.ts.j2 b/api/oas_generator/ts_oas_generator/templates/models/block/signed-txn-in-block.ts.j2 new file mode 100644 index 000000000..cabf849da --- /dev/null +++ b/api/oas_generator/ts_oas_generator/templates/models/block/signed-txn-in-block.ts.j2 @@ -0,0 +1,63 @@ +/* + * {{ 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 + */ + +import type { ModelMetadata } from '../core/model-runtime'; +import type { SignedTransaction } from '@algorandfoundation/algokit-transact'; +import type { BlockAppEvalDelta } from './block-app-eval-delta'; +import { getModelMeta, registerModelMeta } from '../core/model-runtime'; + +/** + * SignedTxnInBlock is a SignedTransaction with additional ApplyData and block-specific metadata. + */ +export interface SignedTxnInBlock { + signedTransaction: SignedTransaction; + logicSignature?: Record; + closingAmount?: bigint; + assetClosingAmount?: bigint; + senderRewards?: bigint; + receiverRewards?: bigint; + closeRewards?: bigint; + evalDelta?: BlockAppEvalDelta; + configAsset?: bigint; + applicationId?: bigint; + hasGenesisId?: boolean; + hasGenesisHash?: boolean; +} + +export const SignedTxnInBlockMeta: ModelMetadata = { + name: 'SignedTxnInBlock', + kind: 'object', + fields: [ + { + name: 'signedTransaction', + // flatten signed transaction fields into parent + flattened: true, + optional: false, + nullable: false, + type: { kind: 'codec', codecKey: 'SignedTransaction' }, + }, + { name: 'logicSignature', wireKey: 'lsig', optional: true, nullable: false, type: { kind: 'scalar' } }, + { name: 'closingAmount', wireKey: 'ca', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'assetClosingAmount', wireKey: 'aca', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'senderRewards', wireKey: 'rs', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'receiverRewards', wireKey: 'rr', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'closeRewards', wireKey: 'rc', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'evalDelta', wireKey: 'dt', optional: true, nullable: false, type: { kind: 'model', meta: () => getModelMeta('BlockAppEvalDelta') } }, + { name: 'configAsset', wireKey: 'caid', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'applicationId', wireKey: 'apid', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'hasGenesisId', wireKey: 'hgi', optional: true, nullable: false, type: { kind: 'scalar' } }, + { name: 'hasGenesisHash', wireKey: 'hgh', optional: true, nullable: false, type: { kind: 'scalar' } }, + ], +}; + +registerModelMeta('SignedTxnInBlock', SignedTxnInBlockMeta); + + + diff --git a/crates/algod_client/Cargo.toml b/crates/algod_client/Cargo.toml index 506d84e2a..7e95b2b31 100644 --- a/crates/algod_client/Cargo.toml +++ b/crates/algod_client/Cargo.toml @@ -35,6 +35,7 @@ algokit_transact_ffi = { optional = true, version = "0.1.0", path = "../algokit_ algokit_transact = { path = "../algokit_transact" } # MessagePack serialization rmp-serde = "^1.1" +rmpv = { version = "1.3.0", features = ["with-serde"] } # Error handling snafu = { workspace = true } diff --git a/crates/algod_client/src/lib.rs b/crates/algod_client/src/lib.rs index 9dc8eed92..89b21599a 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_value_bytes; // Re-export the main client for convenience pub use apis::AlgodClient; diff --git a/crates/algod_client/src/models/block.rs b/crates/algod_client/src/models/block.rs new file mode 100644 index 000000000..b265ad405 --- /dev/null +++ b/crates/algod_client/src/models/block.rs @@ -0,0 +1,162 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * 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}; + +#[cfg(feature = "ffi_uniffi")] +use algokit_transact_ffi::SignedTransaction as AlgokitSignedTransaction; + +use algokit_transact::AlgorandMsgpack; + +use crate::models::BlockStateProofTracking; +use crate::models::SignedTxnInBlock; + +/// Block contains the BlockHeader and the list of transactions (Payset). +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] +pub struct Block { + /// [rnd] Round number. + #[serde(rename = "rnd", skip_serializing_if = "Option::is_none")] + pub round: Option, + /// [prev] Previous block hash. + #[serde_as(as = "Option")] + #[serde(rename = "prev", skip_serializing_if = "Option::is_none")] + pub previous_block_hash: Option>, + /// [prev512] Previous block hash using SHA-512. + #[serde_as(as = "Option")] + #[serde(rename = "prev512", skip_serializing_if = "Option::is_none")] + pub previous_block_hash_512: Option>, + /// [seed] Sortition seed. + #[serde_as(as = "Option")] + #[serde(rename = "seed", skip_serializing_if = "Option::is_none")] + pub seed: Option>, + /// [txn] Root of transaction merkle tree using SHA512_256. + #[serde_as(as = "Option")] + #[serde(rename = "txn", skip_serializing_if = "Option::is_none")] + pub transactions_root: Option>, + /// [txn256] Root of transaction vector commitment using SHA256. + #[serde_as(as = "Option")] + #[serde(rename = "txn256", skip_serializing_if = "Option::is_none")] + pub transactions_root_sha256: Option>, + /// [txn512] Root of transaction vector commitment using SHA512. + #[serde_as(as = "Option")] + #[serde(rename = "txn512", skip_serializing_if = "Option::is_none")] + pub transactions_root_sha512: Option>, + /// [ts] Block timestamp in seconds since epoch. + #[serde(rename = "ts", skip_serializing_if = "Option::is_none")] + pub timestamp: Option, + /// [gen] Genesis ID. + #[serde(rename = "gen", skip_serializing_if = "Option::is_none")] + pub genesis_id: Option, + /// [gh] Genesis hash. + #[serde_as(as = "Option")] + #[serde(rename = "gh", skip_serializing_if = "Option::is_none")] + pub genesis_hash: Option>, + /// [prp] Proposer address. + #[serde_as(as = "Option")] + #[serde(rename = "prp", skip_serializing_if = "Option::is_none")] + pub proposer: Option>, + /// [fc] Fees collected in this block. + #[serde(rename = "fc", skip_serializing_if = "Option::is_none")] + pub fees_collected: Option, + /// [bi] Bonus incentive for block proposal. + #[serde(rename = "bi", skip_serializing_if = "Option::is_none")] + pub bonus: Option, + /// [pp] Proposer payout. + #[serde(rename = "pp", skip_serializing_if = "Option::is_none")] + pub proposer_payout: Option, + /// [fees] FeeSink address. + #[serde_as(as = "Option")] + #[serde(rename = "fees", skip_serializing_if = "Option::is_none")] + pub fee_sink: Option>, + /// [rwd] RewardsPool address. + #[serde_as(as = "Option")] + #[serde(rename = "rwd", skip_serializing_if = "Option::is_none")] + pub rewards_pool: Option>, + /// [earn] Rewards level. + #[serde(rename = "earn", skip_serializing_if = "Option::is_none")] + pub rewards_level: Option, + /// [rate] Rewards rate. + #[serde(rename = "rate", skip_serializing_if = "Option::is_none")] + pub rewards_rate: Option, + /// [frac] Rewards residue. + #[serde(rename = "frac", skip_serializing_if = "Option::is_none")] + pub rewards_residue: Option, + /// [rwcalr] Rewards recalculation round. + #[serde(rename = "rwcalr", skip_serializing_if = "Option::is_none")] + pub rewards_recalculation_round: Option, + /// [proto] Current consensus protocol. + #[serde(rename = "proto", skip_serializing_if = "Option::is_none")] + pub current_protocol: Option, + /// [nextproto] Next proposed protocol. + #[serde(rename = "nextproto", skip_serializing_if = "Option::is_none")] + pub next_protocol: Option, + /// [nextyes] Next protocol approvals. + #[serde(rename = "nextyes", skip_serializing_if = "Option::is_none")] + pub next_protocol_approvals: Option, + /// [nextbefore] Next protocol vote deadline. + #[serde(rename = "nextbefore", skip_serializing_if = "Option::is_none")] + pub next_protocol_vote_before: Option, + /// [nextswitch] Next protocol switch round. + #[serde(rename = "nextswitch", skip_serializing_if = "Option::is_none")] + pub next_protocol_switch_on: Option, + /// [upgradeprop] Upgrade proposal. + #[serde(rename = "upgradeprop", skip_serializing_if = "Option::is_none")] + pub upgrade_propose: Option, + /// [upgradedelay] Upgrade delay in rounds. + #[serde(rename = "upgradedelay", skip_serializing_if = "Option::is_none")] + pub upgrade_delay: Option, + /// [upgradeyes] Upgrade approval flag. + #[serde(rename = "upgradeyes", skip_serializing_if = "Option::is_none")] + pub upgrade_approve: Option, + /// [tc] Transaction counter. + #[serde(rename = "tc", skip_serializing_if = "Option::is_none")] + pub txn_counter: Option, + /// [spt] State proof tracking data keyed by state proof type. + #[serde(rename = "spt", skip_serializing_if = "Option::is_none", default)] + pub state_proof_tracking: Option, + /// [partupdrmv] Expired participation accounts. + #[serde_as(as = "Option>")] + #[serde(rename = "partupdrmv", skip_serializing_if = "Option::is_none")] + pub expired_participation_accounts: Option>>, + /// [partupdabs] Absent participation accounts. + #[serde_as(as = "Option>")] + #[serde(rename = "partupdabs", skip_serializing_if = "Option::is_none")] + pub absent_participation_accounts: Option>>, + /// [txns] Block transactions (Payset). + #[serde(rename = "txns", skip_serializing_if = "Option::is_none")] + pub transactions: Option>, +} + +impl AlgorandMsgpack for Block { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl Block { + /// Default constructor for Block + pub fn new() -> Block { + Block::default() + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/block_account_state_delta.rs b/crates/algod_client/src/models/block_account_state_delta.rs new file mode 100644 index 000000000..32f350e11 --- /dev/null +++ b/crates/algod_client/src/models/block_account_state_delta.rs @@ -0,0 +1,29 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use crate::models::BlockStateDelta; +use serde::{Deserialize, Serialize}; + +/// BlockAccountStateDelta pairs an address with a BlockStateDelta map. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] +pub struct BlockAccountStateDelta { + #[serde(rename = "address")] + pub address: String, + #[serde(rename = "delta")] + pub delta: BlockStateDelta, +} + +impl BlockAccountStateDelta { + pub fn new(address: String, delta: BlockStateDelta) -> Self { + Self { address, delta } + } +} diff --git a/crates/algod_client/src/models/block_app_eval_delta.rs b/crates/algod_client/src/models/block_app_eval_delta.rs new file mode 100644 index 000000000..5243d53d2 --- /dev/null +++ b/crates/algod_client/src/models/block_app_eval_delta.rs @@ -0,0 +1,57 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * 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::BlockStateDelta; +use crate::models::SignedTxnInBlock; + +/// BlockAppEvalDelta 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 BlockAppEvalDelta { + /// [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 BlockAppEvalDelta { + const PREFIX: &'static [u8] = b""; +} + +impl BlockAppEvalDelta { + pub fn new() -> BlockAppEvalDelta { + BlockAppEvalDelta::default() + } +} diff --git a/crates/algod_client/src/models/block_eval_delta.rs b/crates/algod_client/src/models/block_eval_delta.rs new file mode 100644 index 000000000..3dc01a80f --- /dev/null +++ b/crates/algod_client/src/models/block_eval_delta.rs @@ -0,0 +1,54 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +use algokit_transact::AlgorandMsgpack; + +/// BlockEvalDelta represents a TEAL value delta (block/msgpack wire keys). +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] +pub struct BlockEvalDelta { + /// [at] delta action. + #[serde(rename = "at")] + pub action: u32, + /// [bs] bytes value. + #[serde(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, +} + +impl AlgorandMsgpack for BlockEvalDelta { + const PREFIX: &'static [u8] = b""; +} + +impl BlockEvalDelta { + /// Constructor for BlockEvalDelta + pub fn new(action: u32) -> BlockEvalDelta { + BlockEvalDelta { + action, + bytes: None, + uint: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/block_state_delta.rs b/crates/algod_client/src/models/block_state_delta.rs new file mode 100644 index 000000000..506bd9626 --- /dev/null +++ b/crates/algod_client/src/models/block_state_delta.rs @@ -0,0 +1,17 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use std::collections::HashMap; + +use crate::models::BlockEvalDelta; + +/// BlockStateDelta is a map keyed by state key to BlockEvalDelta. +pub type BlockStateDelta = HashMap; diff --git a/crates/algod_client/src/models/block_state_proof_tracking.rs b/crates/algod_client/src/models/block_state_proof_tracking.rs new file mode 100644 index 000000000..286508282 --- /dev/null +++ b/crates/algod_client/src/models/block_state_proof_tracking.rs @@ -0,0 +1,18 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use crate::models::BlockStateProofTrackingData; + +/// Tracks state proof metadata by state proof type. +pub type BlockStateProofTracking = HashMap; diff --git a/crates/algod_client/src/models/block_state_proof_tracking_data.rs b/crates/algod_client/src/models/block_state_proof_tracking_data.rs new file mode 100644 index 000000000..276c6539d --- /dev/null +++ b/crates/algod_client/src/models/block_state_proof_tracking_data.rs @@ -0,0 +1,42 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_with::{Bytes, serde_as}; + +use algokit_transact::AlgorandMsgpack; + +/// Tracking metadata for a specific StateProofType. +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] +pub struct BlockStateProofTrackingData { + /// [v] Vector commitment root of state proof voters (may be absent when not applicable). + #[serde_as(as = "Option")] + #[serde(rename = "v", skip_serializing_if = "Option::is_none", default)] + pub state_proof_voters_commitment: Option>, + /// [t] Online total weight during state proof round. + #[serde(rename = "t", skip_serializing_if = "Option::is_none", default)] + pub state_proof_online_total_weight: Option, + /// [n] Next round for which state proofs are accepted. + #[serde(rename = "n", skip_serializing_if = "Option::is_none", default)] + pub state_proof_next_round: Option, +} + +impl AlgorandMsgpack for BlockStateProofTrackingData { + const PREFIX: &'static [u8] = b""; +} + +impl BlockStateProofTrackingData { + pub fn new() -> BlockStateProofTrackingData { + BlockStateProofTrackingData::default() + } +} diff --git a/crates/algod_client/src/models/get_block.rs b/crates/algod_client/src/models/get_block.rs index af6005b0e..aee113d70 100644 --- a/crates/algod_client/src/models/get_block.rs +++ b/crates/algod_client/src/models/get_block.rs @@ -18,18 +18,23 @@ use algokit_transact_ffi::SignedTransaction as AlgokitSignedTransaction; use algokit_transact::AlgorandMsgpack; -use crate::models::UnknownJsonValue; +use crate::models::Block; /// Encoded block object. #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] pub struct GetBlock { - /// Block header data. + /// Block data including header and transactions. #[serde(rename = "block")] - pub block: UnknownJsonValue, - /// Optional certificate object. This is only included when the format is set to message pack. - #[serde(rename = "cert", skip_serializing_if = "Option::is_none")] - pub cert: Option, + 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>, } impl AlgorandMsgpack for GetBlock { @@ -38,7 +43,7 @@ impl AlgorandMsgpack for GetBlock { impl GetBlock { /// Constructor for GetBlock - pub fn new(block: UnknownJsonValue) -> GetBlock { + pub fn new(block: Block) -> GetBlock { GetBlock { block, cert: None } } diff --git a/crates/algod_client/src/models/mod.rs b/crates/algod_client/src/models/mod.rs index 549a1c51d..51cac1d74 100644 --- a/crates/algod_client/src/models/mod.rs +++ b/crates/algod_client/src/models/mod.rs @@ -178,3 +178,21 @@ pub mod teal_dryrun; pub use self::teal_dryrun::TealDryrun; pub mod get_block_time_stamp_offset; pub use self::get_block_time_stamp_offset::GetBlockTimeStampOffset; + +// Custom Algod typed block models (Block* to avoid shape collisions) +pub mod block; +pub use self::block::Block; +pub mod signed_txn_in_block; +pub use self::signed_txn_in_block::SignedTxnInBlock; +pub mod block_eval_delta; +pub use self::block_eval_delta::BlockEvalDelta; +pub mod block_state_delta; +pub use self::block_state_delta::BlockStateDelta; +pub mod block_account_state_delta; +pub use self::block_account_state_delta::BlockAccountStateDelta; +pub mod block_app_eval_delta; +pub use self::block_app_eval_delta::BlockAppEvalDelta; +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; diff --git a/crates/algod_client/src/models/signed_txn_in_block.rs b/crates/algod_client/src/models/signed_txn_in_block.rs new file mode 100644 index 000000000..24b03ef00 --- /dev/null +++ b/crates/algod_client/src/models/signed_txn_in_block.rs @@ -0,0 +1,133 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +#[cfg(not(feature = "ffi_uniffi"))] +use algokit_transact::SignedTransaction as AlgokitSignedTransaction; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "ffi_uniffi")] +use algokit_transact_ffi::SignedTransaction as AlgokitSignedTransaction; + +use algokit_transact::AlgorandMsgpack; + +use crate::models::BlockAppEvalDelta; + +/// 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))] +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, + /// [aca] Asset closing amount. + #[serde(rename = "aca", skip_serializing_if = "Option::is_none")] + pub asset_closing_amount: Option, + /// [rs] Sender rewards. + #[serde(rename = "rs", skip_serializing_if = "Option::is_none")] + pub sender_rewards: Option, + /// [rr] Receiver rewards. + #[serde(rename = "rr", skip_serializing_if = "Option::is_none")] + pub receiver_rewards: Option, + /// [rc] Close rewards. + #[serde(rename = "rc", skip_serializing_if = "Option::is_none")] + pub close_rewards: Option, + /// [dt] State changes from app execution. + #[serde(rename = "dt", skip_serializing_if = "Option::is_none")] + pub eval_delta: Option, + /// [caid] Asset ID if created. + #[serde(rename = "caid", skip_serializing_if = "Option::is_none")] + pub config_asset: Option, + /// [apid] App ID if created. + #[serde(rename = "apid", skip_serializing_if = "Option::is_none")] + pub application_id: Option, + /// [hgi] Has genesis ID flag. + #[serde(rename = "hgi", skip_serializing_if = "Option::is_none")] + pub has_genesis_id: Option, + /// [hgh] Has genesis hash flag. + #[serde(rename = "hgh", skip_serializing_if = "Option::is_none")] + pub has_genesis_hash: Option, +} + +impl Default for SignedTxnInBlock { + fn default() -> Self { + Self { + signed_transaction: AlgokitSignedTransaction { + #[allow(clippy::useless_conversion)] + transaction: algokit_transact::Transaction::Payment( + algokit_transact::PaymentTransactionFields { + header: algokit_transact::TransactionHeader { + sender: Default::default(), + fee: None, + first_valid: 0, + last_valid: 0, + genesis_hash: None, + genesis_id: None, + note: None, + rekey_to: None, + lease: None, + group: None, + }, + receiver: Default::default(), + amount: 0, + close_remainder_to: None, + }, + ) + .into(), + signature: None, + auth_address: None, + multisignature: None, + }, + logic_signature: None, + closing_amount: None, + asset_closing_amount: None, + sender_rewards: None, + receiver_rewards: None, + close_rewards: None, + eval_delta: None, + config_asset: None, + application_id: None, + has_genesis_id: None, + has_genesis_hash: None, + } + } +} + +impl AlgorandMsgpack for SignedTxnInBlock { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl SignedTxnInBlock { + /// Default constructor for SignedTxnInBlock + pub fn new() -> SignedTxnInBlock { + SignedTxnInBlock::default() + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/msgpack_value_bytes.rs b/crates/algod_client/src/msgpack_value_bytes.rs new file mode 100644 index 000000000..525d6f852 --- /dev/null +++ b/crates/algod_client/src/msgpack_value_bytes.rs @@ -0,0 +1,48 @@ +/// 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(), + } +} diff --git a/crates/algokit_utils/tests/algod/block.rs b/crates/algokit_utils/tests/algod/block.rs new file mode 100644 index 000000000..a613ebbf2 --- /dev/null +++ b/crates/algokit_utils/tests/algod/block.rs @@ -0,0 +1,41 @@ +// Block tests +// These tests demonstrate the integration test structure and API communication + +use algokit_utils::ClientManager; + +use crate::common::logging::init_test_logging; + +#[tokio::test] +async fn test_block_endpoint() { + init_test_logging(); + + let config = + ClientManager::get_algonode_config("testnet", algokit_utils::AlgorandService::Algod); + let algod_client = ClientManager::get_algod_client(&config).unwrap(); + let large_block_with_state_proof_txns = 24098947; + let block_response = algod_client + .get_block(large_block_with_state_proof_txns, Some(false)) + .await + .unwrap(); + + assert!(block_response.cert.is_some()); + assert!(block_response.block.state_proof_tracking.is_some()); + assert!(block_response.block.transactions.is_some()); + + // Validate deeply nested signed transaction fields are present and + // leverage transact crate model + let transactions = block_response + .block + .transactions + .as_ref() + .expect("expected transactions"); + assert!(!transactions.is_empty()); + assert_eq!( + transactions[0] + .signed_transaction + .transaction + .sender() + .as_str(), + "XM6FEYVJ2XDU2IBH4OT6VZGW75YM63CM4TC6AV6BD3JZXFJUIICYTVB5EU" + ); +} diff --git a/crates/algokit_utils/tests/algod/mod.rs b/crates/algokit_utils/tests/algod/mod.rs index cde41a74e..064e0e769 100644 --- a/crates/algokit_utils/tests/algod/mod.rs +++ b/crates/algokit_utils/tests/algod/mod.rs @@ -1,3 +1,4 @@ +pub mod block; pub mod pending_transaction_information; pub mod raw_transaction; pub mod simulate_transactions; diff --git a/packages/typescript/algod_client/src/core/model-runtime.ts b/packages/typescript/algod_client/src/core/model-runtime.ts index 87c8bfaf9..eecaf8bb8 100644 --- a/packages/typescript/algod_client/src/core/model-runtime.ts +++ b/packages/typescript/algod_client/src/core/model-runtime.ts @@ -38,10 +38,15 @@ export type FieldType = ScalarFieldType | CodecFieldType | ModelFieldType | Arra export interface FieldMetadata { readonly name: string - readonly wireKey: string + readonly wireKey?: string readonly optional: boolean readonly nullable: boolean readonly type: FieldType + /** + * If true and the field is a SignedTransaction codec, its encoded map entries + * are merged into the parent object (no own wire key). + */ + readonly flattened?: boolean } export type ModelKind = 'object' | 'array' | 'passthrough' @@ -56,6 +61,19 @@ export interface ModelMetadata { readonly passThrough?: FieldType } +// Registry for model metadata to avoid direct circular imports between model files +const modelMetaRegistry = new Map() + +export function registerModelMeta(name: string, meta: ModelMetadata): void { + modelMetaRegistry.set(name, meta) +} + +export function getModelMeta(name: string): ModelMetadata { + const meta = modelMetaRegistry.get(name) + if (!meta) throw new Error(`Model metadata not registered: ${name}`) + return meta +} + export interface TypeCodec { encode(value: TValue, format: BodyFormat): unknown decode(value: unknown, format: BodyFormat): TValue @@ -114,6 +132,7 @@ export class AlgorandSerializer { private static transformObject(value: unknown, meta: ModelMetadata, ctx: TransformContext): unknown { const fields = meta.fields ?? [] + const hasFlattenedSignedTxn = fields.some((f) => f.flattened && f.type.kind === 'codec' && f.type.codecKey === 'SignedTransaction') if (ctx.direction === 'encode') { const src = value as Record const out: Record = {} @@ -122,7 +141,13 @@ export class AlgorandSerializer { if (fieldValue === undefined) continue const encoded = this.transformType(fieldValue, field.type, ctx) if (encoded === undefined && fieldValue === undefined) continue - out[field.wireKey] = encoded + if (field.flattened && field.type.kind === 'codec' && field.type.codecKey === 'SignedTransaction') { + // Merge signed transaction map into parent + const mapValue = encoded as Record + for (const [k, v] of Object.entries(mapValue ?? {})) out[k] = v + continue + } + if (field.wireKey) out[field.wireKey] = encoded } if (meta.additionalProperties) { for (const [key, val] of Object.entries(src)) { @@ -135,7 +160,7 @@ export class AlgorandSerializer { const src = value as Record const out: Record = {} - const fieldByWire = new Map(fields.map((field) => [field.wireKey, field])) + const fieldByWire = new Map(fields.filter((f) => !!f.wireKey).map((field) => [field.wireKey as string, field])) for (const [wireKey, wireValue] of Object.entries(src)) { const field = fieldByWire.get(wireKey) @@ -148,7 +173,19 @@ export class AlgorandSerializer { out[wireKey] = this.transformType(wireValue, meta.additionalProperties, ctx) continue } - out[wireKey] = wireValue + // If we have a flattened SignedTransaction, skip unknown keys (e.g., 'sig', 'txn') + if (!hasFlattenedSignedTxn) { + out[wireKey] = wireValue + } + } + + // If there are flattened fields, attempt to reconstruct them from remaining keys by decoding + for (const field of fields) { + if (out[field.name] !== undefined) continue + if (field.flattened && field.type.kind === 'codec' && field.type.codecKey === 'SignedTransaction') { + // Reconstruct from entire object map + out[field.name] = this.applyCodec(src, 'SignedTransaction', ctx) + } } return out diff --git a/packages/typescript/algod_client/src/models/block-account-state-delta.ts b/packages/typescript/algod_client/src/models/block-account-state-delta.ts new file mode 100644 index 000000000..f40192cad --- /dev/null +++ b/packages/typescript/algod_client/src/models/block-account-state-delta.ts @@ -0,0 +1,20 @@ +import type { ModelMetadata } from '../core/model-runtime' +import { registerModelMeta } from '../core/model-runtime' +import { BlockStateDeltaMeta } from './block-state-delta' + +/** BlockAccountStateDelta pairs an address with a BlockStateDelta map. */ +export interface BlockAccountStateDelta { + address: string + delta: import('./block-state-delta').BlockStateDelta +} + +export const BlockAccountStateDeltaMeta: ModelMetadata = { + name: 'BlockAccountStateDelta', + kind: 'object', + fields: [ + { name: 'address', wireKey: 'address', optional: false, nullable: false, type: { kind: 'scalar' } }, + { name: 'delta', wireKey: 'delta', optional: false, nullable: false, type: { kind: 'model', meta: () => BlockStateDeltaMeta } }, + ], +} + +registerModelMeta('BlockAccountStateDelta', BlockAccountStateDeltaMeta) diff --git a/packages/typescript/algod_client/src/models/block-app-eval-delta.ts b/packages/typescript/algod_client/src/models/block-app-eval-delta.ts new file mode 100644 index 000000000..bc3b0d806 --- /dev/null +++ b/packages/typescript/algod_client/src/models/block-app-eval-delta.ts @@ -0,0 +1,53 @@ +import type { ModelMetadata } from '../core/model-runtime' +import { getModelMeta, registerModelMeta } from '../core/model-runtime' +import type { SignedTxnInBlock } from './signed-txn-in-block' +import type { BlockStateDelta } from './block-state-delta' +import { BlockStateDeltaMeta } from './block-state-delta' + +/** + * State changes from application execution, including inner transactions and logs. + */ +export interface BlockAppEvalDelta { + /** [gd] Global state delta for the application. */ + globalDelta?: BlockStateDelta + /** [ld] Local state deltas keyed by address index. */ + localDeltas?: Record + /** [itx] Inner transactions produced by this application execution. */ + innerTxns?: SignedTxnInBlock[] + /** [sa] Shared accounts referenced by local deltas. */ + sharedAccounts?: Uint8Array[] + /** [lg] Application log outputs. */ + logs?: Uint8Array[] +} + +export const BlockAppEvalDeltaMeta: ModelMetadata = { + name: 'BlockAppEvalDelta', + kind: 'object', + fields: [ + { name: 'globalDelta', wireKey: 'gd', optional: true, nullable: false, type: { kind: 'model', meta: () => BlockStateDeltaMeta } }, + { + name: 'localDeltas', + wireKey: 'ld', + optional: true, + nullable: false, + type: { kind: 'record', value: { kind: 'model', meta: () => BlockStateDeltaMeta } }, + }, + { + name: 'innerTxns', + wireKey: 'itx', + optional: true, + nullable: false, + type: { kind: 'array', item: { kind: 'model', meta: () => getModelMeta('SignedTxnInBlock') } }, + }, + { + name: 'sharedAccounts', + wireKey: 'sa', + optional: true, + nullable: false, + type: { kind: 'array', item: { kind: 'scalar', isBytes: true } }, + }, + { name: 'logs', wireKey: 'lg', optional: true, nullable: false, type: { kind: 'array', item: { kind: 'scalar', isBytes: true } } }, + ], +} + +registerModelMeta('BlockAppEvalDelta', BlockAppEvalDeltaMeta) diff --git a/packages/typescript/algod_client/src/models/block-eval-delta.ts b/packages/typescript/algod_client/src/models/block-eval-delta.ts new file mode 100644 index 000000000..faab05dde --- /dev/null +++ b/packages/typescript/algod_client/src/models/block-eval-delta.ts @@ -0,0 +1,24 @@ +import type { ModelMetadata } from '../core/model-runtime' +import { registerModelMeta } from '../core/model-runtime' + +/** BlockEvalDelta represents a TEAL value delta (block/msgpack wire keys). */ +export interface BlockEvalDelta { + /** [at] delta action. */ + action: number + /** [bs] bytes value. */ + bytes?: string + /** [ui] uint value. */ + uint?: bigint +} + +export const BlockEvalDeltaMeta: ModelMetadata = { + name: 'BlockEvalDelta', + kind: 'object', + fields: [ + { name: 'action', wireKey: 'at', optional: false, nullable: false, type: { kind: 'scalar' } }, + { name: 'bytes', wireKey: 'bs', optional: true, nullable: false, type: { kind: 'scalar' } }, + { name: 'uint', wireKey: 'ui', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + ], +} + +registerModelMeta('BlockEvalDelta', BlockEvalDeltaMeta) diff --git a/packages/typescript/algod_client/src/models/block-state-delta.ts b/packages/typescript/algod_client/src/models/block-state-delta.ts new file mode 100644 index 000000000..b3ebdc36f --- /dev/null +++ b/packages/typescript/algod_client/src/models/block-state-delta.ts @@ -0,0 +1,14 @@ +import type { ModelMetadata } from '../core/model-runtime' +import { registerModelMeta } from '../core/model-runtime' +import { BlockEvalDeltaMeta } from './block-eval-delta' + +/** BlockStateDelta is a map keyed by state key to BlockEvalDelta. */ +export type BlockStateDelta = Record + +export const BlockStateDeltaMeta: ModelMetadata = { + name: 'BlockStateDelta', + kind: 'object', + additionalProperties: { kind: 'model', meta: () => BlockEvalDeltaMeta }, +} + +registerModelMeta('BlockStateDelta', BlockStateDeltaMeta) diff --git a/packages/typescript/algod_client/src/models/block.ts b/packages/typescript/algod_client/src/models/block.ts new file mode 100644 index 000000000..e64b8a251 --- /dev/null +++ b/packages/typescript/algod_client/src/models/block.ts @@ -0,0 +1,141 @@ +import type { ModelMetadata } from '../core/model-runtime' +import type { SignedTxnInBlock } from './signed-txn-in-block' +import { SignedTxnInBlockMeta } from './signed-txn-in-block' +import type { BlockStateProofTracking } from './block_state_proof_tracking' +import { BlockStateProofTrackingMeta } from './block_state_proof_tracking' + +/** + * Block contains the BlockHeader and the list of transactions (Payset). + */ +export interface Block { + /** [rnd] Round number. */ + round?: bigint + /** [prev] Previous block hash. */ + previousBlockHash?: Uint8Array + /** [prev512] Previous block hash using SHA-512. */ + previousBlockHash512?: Uint8Array + /** [seed] Sortition seed. */ + seed?: Uint8Array + /** [txn] Root of transaction merkle tree using SHA512_256. */ + transactionsRoot?: Uint8Array + /** [txn256] Root of transaction vector commitment using SHA256. */ + transactionsRootSha256?: Uint8Array + /** [txn512] Root of transaction vector commitment using SHA512. */ + transactionsRootSha512?: Uint8Array + /** [ts] Block timestamp in seconds since epoch. */ + timestamp?: bigint + /** [gen] Genesis ID. */ + genesisId?: string + /** [gh] Genesis hash. */ + genesisHash?: Uint8Array + /** [prp] Proposer address. */ + proposer?: Uint8Array + /** [fc] Fees collected in this block. */ + feesCollected?: bigint + /** [bi] Bonus incentive for block proposal. */ + bonus?: bigint + /** [pp] Proposer payout. */ + proposerPayout?: bigint + /** [fees] FeeSink address. */ + feeSink?: Uint8Array + /** [rwd] RewardsPool address. */ + rewardsPool?: Uint8Array + /** [earn] Rewards level. */ + rewardsLevel?: bigint + /** [rate] Rewards rate. */ + rewardsRate?: bigint + /** [frac] Rewards residue. */ + rewardsResidue?: bigint + /** [rwcalr] Rewards recalculation round. */ + rewardsRecalculationRound?: bigint + /** [proto] Current consensus protocol. */ + currentProtocol?: string + /** [nextproto] Next proposed protocol. */ + nextProtocol?: string + /** [nextyes] Next protocol approvals. */ + nextProtocolApprovals?: bigint + /** [nextbefore] Next protocol vote deadline. */ + nextProtocolVoteBefore?: bigint + /** [nextswitch] Next protocol switch round. */ + nextProtocolSwitchOn?: bigint + /** [upgradeprop] Upgrade proposal. */ + upgradePropose?: string + /** [upgradedelay] Upgrade delay in rounds. */ + upgradeDelay?: bigint + /** [upgradeyes] Upgrade approval flag. */ + upgradeApprove?: boolean + /** [tc] Transaction counter. */ + txnCounter?: bigint + /** [spt] State proof tracking data keyed by state proof type. */ + stateProofTracking?: BlockStateProofTracking + /** [partupdrmv] Expired participation accounts. */ + expiredParticipationAccounts?: Uint8Array[] + /** [partupdabs] Absent participation accounts. */ + absentParticipationAccounts?: Uint8Array[] + /** [txns] Block transactions (Payset). */ + transactions?: SignedTxnInBlock[] +} + +export const BlockMeta: ModelMetadata = { + name: 'Block', + kind: 'object', + fields: [ + { name: 'round', wireKey: 'rnd', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'previousBlockHash', wireKey: 'prev', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'previousBlockHash512', wireKey: 'prev512', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'seed', wireKey: 'seed', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'transactionsRoot', wireKey: 'txn', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'transactionsRootSha256', wireKey: 'txn256', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'transactionsRootSha512', wireKey: 'txn512', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'timestamp', wireKey: 'ts', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'genesisId', wireKey: 'gen', optional: true, nullable: false, type: { kind: 'scalar' } }, + { name: 'genesisHash', wireKey: 'gh', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'proposer', wireKey: 'prp', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'feesCollected', wireKey: 'fc', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'bonus', wireKey: 'bi', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'proposerPayout', wireKey: 'pp', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'feeSink', wireKey: 'fees', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'rewardsPool', wireKey: 'rwd', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'rewardsLevel', wireKey: 'earn', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'rewardsRate', wireKey: 'rate', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'rewardsResidue', wireKey: 'frac', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'rewardsRecalculationRound', wireKey: 'rwcalr', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'currentProtocol', wireKey: 'proto', optional: true, nullable: false, type: { kind: 'scalar' } }, + { name: 'nextProtocol', wireKey: 'nextproto', optional: true, nullable: false, type: { kind: 'scalar' } }, + { name: 'nextProtocolApprovals', wireKey: 'nextyes', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'nextProtocolVoteBefore', wireKey: 'nextbefore', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'nextProtocolSwitchOn', wireKey: 'nextswitch', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'upgradePropose', wireKey: 'upgradeprop', optional: true, nullable: false, type: { kind: 'scalar' } }, + { name: 'upgradeDelay', wireKey: 'upgradedelay', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'upgradeApprove', wireKey: 'upgradeyes', optional: true, nullable: false, type: { kind: 'scalar' } }, + { name: 'txnCounter', wireKey: 'tc', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { + name: 'stateProofTracking', + wireKey: 'spt', + optional: true, + nullable: false, + type: { kind: 'model', meta: () => BlockStateProofTrackingMeta }, + }, + { + name: 'expiredParticipationAccounts', + wireKey: 'partupdrmv', + optional: true, + nullable: false, + type: { kind: 'array', item: { kind: 'scalar', isBytes: true } }, + }, + { + name: 'absentParticipationAccounts', + wireKey: 'partupdabs', + optional: true, + nullable: false, + type: { kind: 'array', item: { kind: 'scalar', isBytes: true } }, + }, + { + name: 'transactions', + wireKey: 'txns', + optional: true, + nullable: false, + type: { kind: 'array', item: { kind: 'model', meta: () => SignedTxnInBlockMeta } }, + }, + ], +} diff --git a/packages/typescript/algod_client/src/models/block_state_proof_tracking.ts b/packages/typescript/algod_client/src/models/block_state_proof_tracking.ts new file mode 100644 index 000000000..8fba4555f --- /dev/null +++ b/packages/typescript/algod_client/src/models/block_state_proof_tracking.ts @@ -0,0 +1,15 @@ +import type { ModelMetadata } from '../core/model-runtime' +import { registerModelMeta } from '../core/model-runtime' +import type { BlockStateProofTrackingData } from './block_state_proof_tracking_data' +import { BlockStateProofTrackingDataMeta } from './block_state_proof_tracking_data' + +/** Tracks state proof metadata by state proof type. */ +export type BlockStateProofTracking = Record + +export const BlockStateProofTrackingMeta: ModelMetadata = { + name: 'BlockStateProofTracking', + kind: 'object', + additionalProperties: { kind: 'model', meta: () => BlockStateProofTrackingDataMeta }, +} + +registerModelMeta('BlockStateProofTracking', BlockStateProofTrackingMeta) diff --git a/packages/typescript/algod_client/src/models/block_state_proof_tracking_data.ts b/packages/typescript/algod_client/src/models/block_state_proof_tracking_data.ts new file mode 100644 index 000000000..db995ca25 --- /dev/null +++ b/packages/typescript/algod_client/src/models/block_state_proof_tracking_data.ts @@ -0,0 +1,24 @@ +import type { ModelMetadata } from '../core/model-runtime' +import { registerModelMeta } from '../core/model-runtime' + +/** Tracking metadata for a specific StateProofType. */ +export interface BlockStateProofTrackingData { + /** [v] Vector commitment root of state proof voters. */ + stateProofVotersCommitment?: Uint8Array + /** [t] Online total weight during state proof round. */ + stateProofOnlineTotalWeight?: bigint + /** [n] Next round for which state proofs are accepted. */ + stateProofNextRound?: bigint +} + +export const BlockStateProofTrackingDataMeta: ModelMetadata = { + name: 'BlockStateProofTrackingData', + kind: 'object', + fields: [ + { name: 'stateProofVotersCommitment', wireKey: 'v', optional: true, nullable: false, type: { kind: 'scalar', isBytes: true } }, + { name: 'stateProofOnlineTotalWeight', wireKey: 't', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'stateProofNextRound', wireKey: 'n', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + ], +} + +registerModelMeta('BlockStateProofTrackingData', BlockStateProofTrackingDataMeta) diff --git a/packages/typescript/algod_client/src/models/get-block.ts b/packages/typescript/algod_client/src/models/get-block.ts index 7a9bdf84f..cf45466af 100644 --- a/packages/typescript/algod_client/src/models/get-block.ts +++ b/packages/typescript/algod_client/src/models/get-block.ts @@ -1,14 +1,11 @@ import type { ModelMetadata } from '../core/model-runtime' +import type { Block } from './block' +import { BlockMeta } from './block' export type GetBlock = { - /** - * Block header data. - */ - block: Record - - /** - * Optional certificate object. This is only included when the format is set to message pack. - */ + /** Block data including header and transactions. */ + block: Block + /** Block certificate (msgpack only). */ cert?: Record } @@ -16,19 +13,7 @@ export const GetBlockMeta: ModelMetadata = { name: 'GetBlock', kind: 'object', fields: [ - { - name: 'block', - wireKey: 'block', - optional: false, - nullable: false, - type: { kind: 'scalar' }, - }, - { - name: 'cert', - wireKey: 'cert', - optional: true, - nullable: false, - type: { kind: 'scalar' }, - }, + { name: 'block', wireKey: 'block', optional: false, nullable: false, type: { kind: 'model', meta: () => BlockMeta } }, + { name: 'cert', wireKey: 'cert', optional: true, nullable: false, type: { kind: 'scalar' } }, ], } diff --git a/packages/typescript/algod_client/src/models/index.ts b/packages/typescript/algod_client/src/models/index.ts index 0900ce210..91600ee47 100644 --- a/packages/typescript/algod_client/src/models/index.ts +++ b/packages/typescript/algod_client/src/models/index.ts @@ -164,3 +164,22 @@ export type { TealDryrun } from './teal-dryrun' export { TealDryrunMeta } from './teal-dryrun' export type { GetBlockTimeStampOffset } from './get-block-time-stamp-offset' export { GetBlockTimeStampOffsetMeta } from './get-block-time-stamp-offset' + +export type { BlockEvalDelta } from './block-eval-delta' +export { BlockEvalDeltaMeta } from './block-eval-delta' +export type { BlockStateDelta } from './block-state-delta' +export { BlockStateDeltaMeta } from './block-state-delta' +export type { BlockAccountStateDelta } from './block-account-state-delta' +export { BlockAccountStateDeltaMeta } from './block-account-state-delta' +export type { BlockAppEvalDelta } from './block-app-eval-delta' +export { BlockAppEvalDeltaMeta } from './block-app-eval-delta' +export type { BlockStateProofTrackingData } from './block_state_proof_tracking_data' +export { BlockStateProofTrackingDataMeta } from './block_state_proof_tracking_data' +export type { BlockStateProofTracking } from './block_state_proof_tracking' +export { BlockStateProofTrackingMeta } from './block_state_proof_tracking' +export type { Block } from './block' +export { BlockMeta } from './block' +export type { SignedTxnInBlock } from './signed-txn-in-block' +export { SignedTxnInBlockMeta } from './signed-txn-in-block' +export type { GetBlock } from './get-block' +export { GetBlockMeta } from './get-block' diff --git a/packages/typescript/algod_client/src/models/signed-txn-in-block.ts b/packages/typescript/algod_client/src/models/signed-txn-in-block.ts new file mode 100644 index 000000000..2ac4b30e6 --- /dev/null +++ b/packages/typescript/algod_client/src/models/signed-txn-in-block.ts @@ -0,0 +1,66 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +import type { ModelMetadata } from '../core/model-runtime' +import type { SignedTransaction } from '@algorandfoundation/algokit-transact' +import type { BlockAppEvalDelta } from './block-app-eval-delta' +import { getModelMeta, registerModelMeta } from '../core/model-runtime' + +/** + * SignedTxnInBlock is a SignedTransaction with additional ApplyData and block-specific metadata. + */ +export interface SignedTxnInBlock { + signedTransaction: SignedTransaction + logicSignature?: Record + closingAmount?: bigint + assetClosingAmount?: bigint + senderRewards?: bigint + receiverRewards?: bigint + closeRewards?: bigint + evalDelta?: BlockAppEvalDelta + configAsset?: bigint + applicationId?: bigint + hasGenesisId?: boolean + hasGenesisHash?: boolean +} + +export const SignedTxnInBlockMeta: ModelMetadata = { + name: 'SignedTxnInBlock', + kind: 'object', + fields: [ + { + name: 'signedTransaction', + // flatten signed transaction fields into parent + flattened: true, + optional: false, + nullable: false, + type: { kind: 'codec', codecKey: 'SignedTransaction' }, + }, + { name: 'logicSignature', wireKey: 'lsig', optional: true, nullable: false, type: { kind: 'scalar' } }, + { name: 'closingAmount', wireKey: 'ca', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'assetClosingAmount', wireKey: 'aca', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'senderRewards', wireKey: 'rs', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'receiverRewards', wireKey: 'rr', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'closeRewards', wireKey: 'rc', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { + name: 'evalDelta', + wireKey: 'dt', + optional: true, + nullable: false, + type: { kind: 'model', meta: () => getModelMeta('BlockAppEvalDelta') }, + }, + { name: 'configAsset', wireKey: 'caid', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'applicationId', wireKey: 'apid', optional: true, nullable: false, type: { kind: 'scalar', isBigint: true } }, + { name: 'hasGenesisId', wireKey: 'hgi', optional: true, nullable: false, type: { kind: 'scalar' } }, + { name: 'hasGenesisHash', wireKey: 'hgh', optional: true, nullable: false, type: { kind: 'scalar' } }, + ], +} + +registerModelMeta('SignedTxnInBlock', SignedTxnInBlockMeta) diff --git a/packages/typescript/algokit_utils/tests/algod/block.test.ts b/packages/typescript/algokit_utils/tests/algod/block.test.ts new file mode 100644 index 000000000..64ca4b8ce --- /dev/null +++ b/packages/typescript/algokit_utils/tests/algod/block.test.ts @@ -0,0 +1,26 @@ +import { expect, it, describe } from 'vitest' +import { AlgodClient } from '@algorandfoundation/algod-client' + +const ALGONODE_TESTNET_URL = 'https://testnet-api.algonode.cloud' + +describe('Algod get block', () => { + it('gets a block from the network', async () => { + const client = new AlgodClient({ + baseUrl: ALGONODE_TESTNET_URL, + apiToken: undefined, + }) + const largeBlockWithStateProofTxns = 24098947 + const blockResponse = await client.getBlock(largeBlockWithStateProofTxns, { headerOnly: false }) + expect(blockResponse).toBeDefined() + expect(blockResponse.cert).toBeDefined() + expect(blockResponse.block.stateProofTracking).toBeDefined() + expect(blockResponse.block.transactions?.length).toBeGreaterThan(0) + + // Validate deeply nested signed transaction fields are present and + // leverage transact crate model + const transactions = blockResponse.block.transactions + expect(transactions).toBeDefined() + expect(transactions.length).toBeGreaterThan(0) + expect(transactions?.[0].signedTransaction.transaction.sender).toBe('XM6FEYVJ2XDU2IBH4OT6VZGW75YM63CM4TC6AV6BD3JZXFJUIICYTVB5EU') + }, 30_000) +}) diff --git a/packages/typescript/indexer_client/src/core/model-runtime.ts b/packages/typescript/indexer_client/src/core/model-runtime.ts index 87c8bfaf9..eecaf8bb8 100644 --- a/packages/typescript/indexer_client/src/core/model-runtime.ts +++ b/packages/typescript/indexer_client/src/core/model-runtime.ts @@ -38,10 +38,15 @@ export type FieldType = ScalarFieldType | CodecFieldType | ModelFieldType | Arra export interface FieldMetadata { readonly name: string - readonly wireKey: string + readonly wireKey?: string readonly optional: boolean readonly nullable: boolean readonly type: FieldType + /** + * If true and the field is a SignedTransaction codec, its encoded map entries + * are merged into the parent object (no own wire key). + */ + readonly flattened?: boolean } export type ModelKind = 'object' | 'array' | 'passthrough' @@ -56,6 +61,19 @@ export interface ModelMetadata { readonly passThrough?: FieldType } +// Registry for model metadata to avoid direct circular imports between model files +const modelMetaRegistry = new Map() + +export function registerModelMeta(name: string, meta: ModelMetadata): void { + modelMetaRegistry.set(name, meta) +} + +export function getModelMeta(name: string): ModelMetadata { + const meta = modelMetaRegistry.get(name) + if (!meta) throw new Error(`Model metadata not registered: ${name}`) + return meta +} + export interface TypeCodec { encode(value: TValue, format: BodyFormat): unknown decode(value: unknown, format: BodyFormat): TValue @@ -114,6 +132,7 @@ export class AlgorandSerializer { private static transformObject(value: unknown, meta: ModelMetadata, ctx: TransformContext): unknown { const fields = meta.fields ?? [] + const hasFlattenedSignedTxn = fields.some((f) => f.flattened && f.type.kind === 'codec' && f.type.codecKey === 'SignedTransaction') if (ctx.direction === 'encode') { const src = value as Record const out: Record = {} @@ -122,7 +141,13 @@ export class AlgorandSerializer { if (fieldValue === undefined) continue const encoded = this.transformType(fieldValue, field.type, ctx) if (encoded === undefined && fieldValue === undefined) continue - out[field.wireKey] = encoded + if (field.flattened && field.type.kind === 'codec' && field.type.codecKey === 'SignedTransaction') { + // Merge signed transaction map into parent + const mapValue = encoded as Record + for (const [k, v] of Object.entries(mapValue ?? {})) out[k] = v + continue + } + if (field.wireKey) out[field.wireKey] = encoded } if (meta.additionalProperties) { for (const [key, val] of Object.entries(src)) { @@ -135,7 +160,7 @@ export class AlgorandSerializer { const src = value as Record const out: Record = {} - const fieldByWire = new Map(fields.map((field) => [field.wireKey, field])) + const fieldByWire = new Map(fields.filter((f) => !!f.wireKey).map((field) => [field.wireKey as string, field])) for (const [wireKey, wireValue] of Object.entries(src)) { const field = fieldByWire.get(wireKey) @@ -148,7 +173,19 @@ export class AlgorandSerializer { out[wireKey] = this.transformType(wireValue, meta.additionalProperties, ctx) continue } - out[wireKey] = wireValue + // If we have a flattened SignedTransaction, skip unknown keys (e.g., 'sig', 'txn') + if (!hasFlattenedSignedTxn) { + out[wireKey] = wireValue + } + } + + // If there are flattened fields, attempt to reconstruct them from remaining keys by decoding + for (const field of fields) { + if (out[field.name] !== undefined) continue + if (field.flattened && field.type.kind === 'codec' && field.type.codecKey === 'SignedTransaction') { + // Reconstruct from entire object map + out[field.name] = this.applyCodec(src, 'SignedTransaction', ctx) + } } return out diff --git a/packages/typescript/kmd_client/src/core/model-runtime.ts b/packages/typescript/kmd_client/src/core/model-runtime.ts index 87c8bfaf9..eecaf8bb8 100644 --- a/packages/typescript/kmd_client/src/core/model-runtime.ts +++ b/packages/typescript/kmd_client/src/core/model-runtime.ts @@ -38,10 +38,15 @@ export type FieldType = ScalarFieldType | CodecFieldType | ModelFieldType | Arra export interface FieldMetadata { readonly name: string - readonly wireKey: string + readonly wireKey?: string readonly optional: boolean readonly nullable: boolean readonly type: FieldType + /** + * If true and the field is a SignedTransaction codec, its encoded map entries + * are merged into the parent object (no own wire key). + */ + readonly flattened?: boolean } export type ModelKind = 'object' | 'array' | 'passthrough' @@ -56,6 +61,19 @@ export interface ModelMetadata { readonly passThrough?: FieldType } +// Registry for model metadata to avoid direct circular imports between model files +const modelMetaRegistry = new Map() + +export function registerModelMeta(name: string, meta: ModelMetadata): void { + modelMetaRegistry.set(name, meta) +} + +export function getModelMeta(name: string): ModelMetadata { + const meta = modelMetaRegistry.get(name) + if (!meta) throw new Error(`Model metadata not registered: ${name}`) + return meta +} + export interface TypeCodec { encode(value: TValue, format: BodyFormat): unknown decode(value: unknown, format: BodyFormat): TValue @@ -114,6 +132,7 @@ export class AlgorandSerializer { private static transformObject(value: unknown, meta: ModelMetadata, ctx: TransformContext): unknown { const fields = meta.fields ?? [] + const hasFlattenedSignedTxn = fields.some((f) => f.flattened && f.type.kind === 'codec' && f.type.codecKey === 'SignedTransaction') if (ctx.direction === 'encode') { const src = value as Record const out: Record = {} @@ -122,7 +141,13 @@ export class AlgorandSerializer { if (fieldValue === undefined) continue const encoded = this.transformType(fieldValue, field.type, ctx) if (encoded === undefined && fieldValue === undefined) continue - out[field.wireKey] = encoded + if (field.flattened && field.type.kind === 'codec' && field.type.codecKey === 'SignedTransaction') { + // Merge signed transaction map into parent + const mapValue = encoded as Record + for (const [k, v] of Object.entries(mapValue ?? {})) out[k] = v + continue + } + if (field.wireKey) out[field.wireKey] = encoded } if (meta.additionalProperties) { for (const [key, val] of Object.entries(src)) { @@ -135,7 +160,7 @@ export class AlgorandSerializer { const src = value as Record const out: Record = {} - const fieldByWire = new Map(fields.map((field) => [field.wireKey, field])) + const fieldByWire = new Map(fields.filter((f) => !!f.wireKey).map((field) => [field.wireKey as string, field])) for (const [wireKey, wireValue] of Object.entries(src)) { const field = fieldByWire.get(wireKey) @@ -148,7 +173,19 @@ export class AlgorandSerializer { out[wireKey] = this.transformType(wireValue, meta.additionalProperties, ctx) continue } - out[wireKey] = wireValue + // If we have a flattened SignedTransaction, skip unknown keys (e.g., 'sig', 'txn') + if (!hasFlattenedSignedTxn) { + out[wireKey] = wireValue + } + } + + // If there are flattened fields, attempt to reconstruct them from remaining keys by decoding + for (const field of fields) { + if (out[field.name] !== undefined) continue + if (field.flattened && field.type.kind === 'codec' && field.type.codecKey === 'SignedTransaction') { + // Reconstruct from entire object map + out[field.name] = this.applyCodec(src, 'SignedTransaction', ctx) + } } return out