From c6e044cc8c5f82160db2dd1de7f423d30dc9564b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Wed, 26 Nov 2025 18:47:01 -0300 Subject: [PATCH 1/3] feat: trix build specifying all types as openapi spec --- src/commands/build.rs | 179 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 174 insertions(+), 5 deletions(-) diff --git a/src/commands/build.rs b/src/commands/build.rs index 07a406d..c0fe21c 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -69,9 +69,41 @@ pub fn run(args: Args, config: &Config, profile: &ProfileConfig) -> miette::Resu let hex = hex::encode(prototx.ir_bytes()); + let mut custom_types = serde_json::Map::new(); + // include normalized builtin type schemas so callers can reference them + custom_types.insert( + "utxo_ref".to_string(), + Tx3Type::Primitive(ir::Type::UtxoRef).json_schema(), + ); + custom_types.insert( + "any_asset".to_string(), + Tx3Type::Primitive(ir::Type::AnyAsset).json_schema(), + ); + custom_types.insert( + "utxo".to_string(), + Tx3Type::Primitive(ir::Type::Utxo).json_schema(), + ); + custom_types.insert( + "bytes".to_string(), + Tx3Type::Primitive(ir::Type::Bytes).json_schema(), + ); let mut args = serde_json::Map::new(); for (key, kind) in prototx.find_params().iter() { - let tx3_type = Tx3Type(kind.clone()); + let tx3_type = match kind { + tx3_lang::ir::Type::Custom(name) => { + let type_def = protocol + .ast() + .types + .iter() + .find(|x| x.name.value == *name) + .unwrap(); + Tx3Type::Custom(CustomTx3Type { + r#type: kind.clone(), + ctx: type_def.clone(), + }) + } + _ => Tx3Type::Primitive(kind.clone()), + }; if let Some(envs) = envs.as_ref() { if let Some((_, value)) = envs.iter().find(|(k, _)| k.eq_ignore_ascii_case(key)) @@ -81,6 +113,10 @@ pub fn run(args: Args, config: &Config, profile: &ProfileConfig) -> miette::Resu } } + if let ir::Type::Custom(_) = kind { + custom_types.insert(tx3_type.to_string(), tx3_type.json_schema()); + } + args.insert(key.clone(), serde_json::Value::String(tx3_type.to_string())); } let args_value = serde_json::Value::Object(args); @@ -91,7 +127,8 @@ pub fn run(args: Args, config: &Config, profile: &ProfileConfig) -> miette::Resu "encoding": "hex", "version": tx3_lang::ir::IR_VERSION }, - "args": args_value + "args": args_value, + "types": custom_types, }) }) .collect::>(); @@ -111,10 +148,27 @@ pub fn run(args: Args, config: &Config, profile: &ProfileConfig) -> miette::Resu Ok(()) } -struct Tx3Type(ir::Type); +struct CustomTx3Type { + pub r#type: ir::Type, + pub ctx: tx3_lang::ast::TypeDef, +} + +enum Tx3Type { + Primitive(ir::Type), + Custom(CustomTx3Type), +} + impl Tx3Type { + pub fn ir_type(&self) -> &ir::Type { + match self { + Tx3Type::Primitive(t) => t, + Tx3Type::Custom(custom) => &custom.r#type, + } + } pub fn env_to_value(&self, value: &str) -> serde_json::Value { - match &self.0 { + let ir_type = self.ir_type(); + + match ir_type { ir::Type::Undefined => serde_json::Value::Null, ir::Type::Unit => serde_json::Value::String(String::from(value)), ir::Type::Int => match serde_json::Number::from_str(value) { @@ -145,11 +199,126 @@ impl Tx3Type { _ => serde_json::Value::String(self.to_string()), } } + + pub fn json_schema(&self) -> serde_json::Value { + fn ast_type_to_schema(ty: &tx3_lang::ast::Type) -> serde_json::Value { + match ty { + tx3_lang::ast::Type::Undefined => serde_json::json!({"type": "null"}), + tx3_lang::ast::Type::Unit => serde_json::json!({}), + tx3_lang::ast::Type::Int => serde_json::json!({"type": "integer"}), + tx3_lang::ast::Type::Bool => serde_json::json!({"type": "boolean"}), + tx3_lang::ast::Type::Bytes => { + serde_json::json!({"type": "string", "pattern": "^0x[0-9a-fA-F]+$"}) + } + tx3_lang::ast::Type::Address => serde_json::json!({"type": "string"}), + tx3_lang::ast::Type::UtxoRef + | tx3_lang::ast::Type::AnyAsset + | tx3_lang::ast::Type::Utxo => { + serde_json::json!({"type": "object"}) + } + tx3_lang::ast::Type::List(inner) => { + serde_json::json!({"type": "array", "items": ast_type_to_schema(inner)}) + } + tx3_lang::ast::Type::Map(_, value_ty) => { + serde_json::json!({"type": "object", "additionalProperties": ast_type_to_schema(value_ty)}) + } + tx3_lang::ast::Type::Custom(id) => { + serde_json::json!({"$ref": format!("#/definitions/{}", id.value)}) + } + } + } + + match self { + Tx3Type::Primitive(ir_type) => match ir_type { + ir::Type::Undefined => serde_json::json!({"type": "null"}), + ir::Type::Unit => serde_json::json!({}), + ir::Type::Int => serde_json::json!({"type": "integer"}), + ir::Type::Bool => serde_json::json!({"type": "boolean"}), + ir::Type::Bytes => { + serde_json::json!({"type": "string", "pattern": "^0x[0-9a-fA-F]+$"}) + } + ir::Type::Address => serde_json::json!({"type": "string"}), + ir::Type::UtxoRef => { + // Represent utxo_ref as a single string with pattern: optional 0x hex (64 chars) # index + serde_json::json!({ + "type": "string", + "pattern": "^(0x)?[0-9a-fA-F]{64}#[0-9]+$", + "description": "UTxO reference: # (txid is 32-byte hex)" + }) + } + ir::Type::AnyAsset => { + serde_json::json!({ + "type": "object", + "properties": { + "amount": {"type": "integer"}, + "policy": {"type": "string", "pattern": "^0x[0-9a-fA-F]+$"}, + "asset_name": {"type": "string"} + }, + "required": ["amount", "policy", "asset_name"] + }) + } + // TODO: make sure this is factual + ir::Type::Utxo => { + serde_json::json!({ + "type": "object", + "properties": { + "tx_hash": {"type": "string", "pattern": "^(0x)?[0-9a-fA-F]{64}$"}, + "output_index": {"type": "integer", "minimum": 0} + }, + "required": ["tx_hash", "output_index"] + }) + } + // TODO: add type parameters here if possible + ir::Type::List => serde_json::json!({"type": "array", "items": {}}), + ir::Type::Map => serde_json::json!({"type": "object", "additionalProperties": {}}), + _ => panic!("Custom types are not primitive types"), + }, + Tx3Type::Custom(custom) => { + let def = &custom.ctx; + let variants = def + .cases + .iter() + .enumerate() + .map(|(constructor, case)| { + let mut props = serde_json::Map::new(); + let mut req = vec![]; + for field in case.fields.iter() { + props.insert( + field.name.value.clone(), + ast_type_to_schema(&field.r#type), + ); + req.push(field.name.value.clone()); + } + let mut obj = serde_json::Map::new(); + obj.insert( + "type".to_string(), + serde_json::Value::String("object".to_string()), + ); + obj.insert( + "constructor".to_string(), + serde_json::Value::Number(constructor.into()), + ); + obj.insert("properties".to_string(), serde_json::Value::Object(props)); + obj.insert( + "required".to_string(), + serde_json::Value::Array( + req.into_iter().map(serde_json::Value::String).collect(), + ), + ); + serde_json::Value::Object(obj) + }) + .collect::>(); + + serde_json::json!({"oneOf": variants}) + } + } + } } impl Display for Tx3Type { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.0 { + let ir_type = self.ir_type(); + match ir_type { ir::Type::Undefined => write!(f, "undefined"), ir::Type::Unit => write!(f, "unit"), ir::Type::Int => write!(f, "int"), From d544a75af95f886fe958a79680c7ae3e695295a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Tue, 2 Dec 2025 00:12:18 -0300 Subject: [PATCH 2/3] feat: bindgen functional for custom type parameters --- src/commands/bindgen.rs | 216 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 204 insertions(+), 12 deletions(-) diff --git a/src/commands/bindgen.rs b/src/commands/bindgen.rs index d48ba98..534eab0 100644 --- a/src/commands/bindgen.rs +++ b/src/commands/bindgen.rs @@ -1,7 +1,7 @@ use std::io::Read; use std::{collections::HashMap, path::PathBuf}; -use crate::config::{BindingsTemplateConfig, Config, KnownChain, ProfileConfig, TrpConfig}; +use crate::config::{BindingsTemplateConfig, Config, ProfileConfig}; use clap::Args as ClapArgs; use miette::IntoDiagnostic; use serde::{Serialize, Serializer}; @@ -51,11 +51,27 @@ fn parse_type_from_string(type_str: &str) -> Result "Utxo" => Ok(tx3_lang::ir::Type::Utxo), "Undefined" => Ok(tx3_lang::ir::Type::Undefined), "List" => Ok(tx3_lang::ir::Type::List), - _ => { - // You would need to create a CustomId here. This depends on how CustomId is defined - // Ok(tx3_lang::ir::Type::Custom(CustomId { value: type_str.to_string() })) - Ok(tx3_lang::ir::Type::Custom(type_str.to_string())) // Keep your current implementation if CustomId is not accessible - } + _ => Ok(tx3_lang::ir::Type::Custom(type_str.to_string())), + } +} + +fn get_argvalue_type_for_language(type_: &tx3_lang::ir::Type, language: &str) -> String { + match language { + "typescript" => match &type_ { + tx3_lang::ir::Type::Int => "ArgValueInt | number | bigint".to_string(), + tx3_lang::ir::Type::Bool => "ArgValueBool | bool".to_string(), + tx3_lang::ir::Type::Bytes => "ArgValueBytes | Uint8Array".to_string(), + tx3_lang::ir::Type::Unit => "string".to_string(), + tx3_lang::ir::Type::Address => "ArgValueAddress | Uint8Array | string".to_string(), + tx3_lang::ir::Type::UtxoRef => "ArgValueUtxoRef".to_string(), + tx3_lang::ir::Type::List => "any[]".to_string(), + tx3_lang::ir::Type::Custom(name) => name.clone(), + tx3_lang::ir::Type::AnyAsset => "any".to_string(), + tx3_lang::ir::Type::Utxo => "any".to_string(), + tx3_lang::ir::Type::Undefined => "any".to_string(), + tx3_lang::ir::Type::Map => "any".to_string(), + }, + _ => "ArgValue".to_string(), } } @@ -132,7 +148,7 @@ fn register_handlebars_helpers(handlebars: &mut Handlebars<'_>) { } // Add more helpers as needed - // Register helper to convert ir types to language types. + // helper to convert ir types to language types handlebars.register_helper( "typeFor", Box::new( @@ -148,24 +164,111 @@ fn register_handlebars_helpers(handlebars: &mut Handlebars<'_>) { .param(1) .ok_or_else(|| RenderErrorReason::ParamNotFoundForIndex("typeFor", 1))?; - let type_str = type_param.value().as_str().ok_or_else(|| { - RenderErrorReason::InvalidParamType("Expected type as string") + let type_ = if let Some(type_str) = type_param.value().as_str() { + parse_type_from_string(type_str) + .map_err(|_| RenderErrorReason::InvalidParamType("Failed to parse type"))? + } else if let Some(obj) = type_param.value().as_object() { + // Handle {"Custom": "TypeName"} format + if let Some(custom_name) = obj.get("Custom").and_then(|v| v.as_str()) { + tx3_lang::ir::Type::Custom(custom_name.to_string()) + } else { + return Err(RenderErrorReason::InvalidParamType( + "Unknown type object format", + ) + .into()); + } + } else { + return Err(RenderErrorReason::InvalidParamType( + "Expected type as string or object", + ) + .into()); + }; + + let language = lang_param.value().as_str().ok_or_else(|| { + RenderErrorReason::InvalidParamType("Expected language as string") })?; - let type_ = parse_type_from_string(type_str) - .map_err(|_| RenderErrorReason::InvalidParamType("Failed to parse type"))?; + let output_type = get_type_for_language(&type_, language); + + out.write(&output_type)?; + Ok(()) + }, + ), + ); + + // helper to convert types to ArgValue type names (Int -> ArgValueInt) + handlebars.register_helper( + "argValueType", + Box::new( + |h: &Helper, + _: &Handlebars, + _: &Context, + _: &mut RenderContext, + out: &mut dyn Output| { + let type_param = h + .param(0) + .ok_or_else(|| RenderErrorReason::ParamNotFoundForIndex("argValueType", 0))?; + let lang_param = h + .param(1) + .ok_or_else(|| RenderErrorReason::ParamNotFoundForIndex("argValueType", 1))?; + + let type_ = if let Some(type_str) = type_param.value().as_str() { + parse_type_from_string(type_str) + .map_err(|_| RenderErrorReason::InvalidParamType("Failed to parse type"))? + } else if let Some(obj) = type_param.value().as_object() { + if let Some(custom_name) = obj.get("Custom").and_then(|v| v.as_str()) { + tx3_lang::ir::Type::Custom(custom_name.to_string()) + } else { + return Err(RenderErrorReason::InvalidParamType( + "Unknown type object format", + ) + .into()); + } + } else { + return Err(RenderErrorReason::InvalidParamType( + "Expected type as string or object", + ) + .into()); + }; let language = lang_param.value().as_str().ok_or_else(|| { RenderErrorReason::InvalidParamType("Expected language as string") })?; - let output_type = get_type_for_language(&type_, language); + let output_type = get_argvalue_type_for_language(&type_, language); out.write(&output_type)?; Ok(()) }, ), ); + + // helper to check if a type is a custom type + handlebars.register_helper( + "isCustomType", + Box::new( + |h: &Helper, + _: &Handlebars, + _: &Context, + _: &mut RenderContext, + out: &mut dyn Output| { + let type_param = h + .param(0) + .ok_or_else(|| RenderErrorReason::ParamNotFoundForIndex("isCustomType", 0))?; + + let is_custom = if type_param.value().as_str().is_some() { + false // Simple string types are not custom + } else if let Some(obj) = type_param.value().as_object() { + obj.contains_key("Custom") + } else { + false + }; + + out.write(if is_custom { "true" } else { "false" })?; + Ok(()) + }, + ), + ); } /// Loads Handlebars templates from a GitHub repository ZIP archive. @@ -322,9 +425,34 @@ impl Serialize for BytesHex { } #[derive(Serialize)] +#[serde(rename_all = "camelCase")] struct TxParameter { name: String, type_name: tx3_lang::ir::Type, + is_custom: bool, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct TypeField { + name: String, + type_name: String, + is_custom: bool, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct TypeVariant { + name: String, + index: usize, + fields: Vec, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct CustomTypeDef { + name: String, + variants: Vec, } #[derive(Serialize)] @@ -348,6 +476,7 @@ struct HandlebarsData { headers: HashMap, env_vars: HashMap, options: HashMap, + custom_types: Vec, } struct Job { @@ -360,7 +489,68 @@ struct Job { options: HashMap, } +fn ast_type_to_string(ty: &tx3_lang::ast::Type) -> String { + match ty { + tx3_lang::ast::Type::Undefined => "Undefined".to_string(), + tx3_lang::ast::Type::Unit => "Unit".to_string(), + tx3_lang::ast::Type::Int => "Int".to_string(), + tx3_lang::ast::Type::Bool => "Bool".to_string(), + tx3_lang::ast::Type::Bytes => "Bytes".to_string(), + tx3_lang::ast::Type::Address => "Address".to_string(), + tx3_lang::ast::Type::UtxoRef => "UtxoRef".to_string(), + tx3_lang::ast::Type::AnyAsset => "AnyAsset".to_string(), + tx3_lang::ast::Type::Utxo => "Utxo".to_string(), + tx3_lang::ast::Type::List(inner) => format!("List<{}>", ast_type_to_string(inner)), + tx3_lang::ast::Type::Map(k, v) => { + format!("Map<{}, {}>", ast_type_to_string(k), ast_type_to_string(v)) + } + tx3_lang::ast::Type::Custom(id) => id.value.clone(), + } +} + +fn is_custom_type(ty: &tx3_lang::ir::Type) -> bool { + matches!(ty, tx3_lang::ir::Type::Custom(_)) +} + fn generate_arguments(job: &Job, version: &str) -> miette::Result { + let custom_types: Vec = job + .protocol + .ast() + .types + .iter() + .map(|type_def| { + let variants: Vec = type_def + .cases + .iter() + .enumerate() + .map(|(index, variant)| { + let fields: Vec = variant + .fields + .iter() + .map(|field| { + let type_name = ast_type_to_string(&field.r#type); + let is_custom = matches!(&field.r#type, tx3_lang::ast::Type::Custom(_)); + TypeField { + name: field.name.value.clone(), + type_name, + is_custom, + } + }) + .collect(); + TypeVariant { + name: variant.name.value.clone(), + index, + fields, + } + }) + .collect(); + CustomTypeDef { + name: type_def.name.value.clone(), + variants, + } + }) + .collect(); + let transactions = job .protocol .txs() @@ -373,6 +563,7 @@ fn generate_arguments(job: &Job, version: &str) -> miette::Result miette::Result Date: Tue, 2 Dec 2025 14:57:56 -0300 Subject: [PATCH 3/3] chore: remove TODOs --- src/commands/build.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/commands/build.rs b/src/commands/build.rs index c0fe21c..ec39c01 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -257,7 +257,6 @@ impl Tx3Type { "required": ["amount", "policy", "asset_name"] }) } - // TODO: make sure this is factual ir::Type::Utxo => { serde_json::json!({ "type": "object", @@ -268,7 +267,6 @@ impl Tx3Type { "required": ["tx_hash", "output_index"] }) } - // TODO: add type parameters here if possible ir::Type::List => serde_json::json!({"type": "array", "items": {}}), ir::Type::Map => serde_json::json!({"type": "object", "additionalProperties": {}}), _ => panic!("Custom types are not primitive types"),