From 875e62d703e58e4fb4bc170544a091472591aef9 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Wed, 3 Dec 2025 16:18:38 -0300 Subject: [PATCH 01/27] feat: add CIP-57 parsing crate with blueprint and schema structures --- Cargo.lock | 9 + Cargo.toml | 7 +- crates/CIP-57/Cargo.toml | 19 ++ crates/CIP-57/src/blueprint.rs | 156 +++++++++++ crates/CIP-57/src/lib.rs | 482 +++++++++++++++++++++++++++++++++ crates/CIP-57/src/schema.rs | 267 ++++++++++++++++++ 6 files changed, 939 insertions(+), 1 deletion(-) create mode 100644 crates/CIP-57/Cargo.toml create mode 100644 crates/CIP-57/src/blueprint.rs create mode 100644 crates/CIP-57/src/lib.rs create mode 100644 crates/CIP-57/src/schema.rs diff --git a/Cargo.lock b/Cargo.lock index be1edebf..3a334efe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -313,6 +313,15 @@ dependencies = [ "half", ] +[[package]] +name = "cip57" +version = "0.13.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", +] + [[package]] name = "core-foundation" version = "0.10.0" diff --git a/Cargo.toml b/Cargo.toml index 42a07143..6cecbe46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,11 @@ [workspace] resolver = "2" -members = ["crates/tx3-cardano", "crates/tx3-lang", "crates/tx3-resolver"] +members = [ + "crates/tx3-cardano", + "crates/tx3-lang", + "crates/tx3-resolver", + "crates/CIP-57", +] [workspace.package] publish = true diff --git a/crates/CIP-57/Cargo.toml b/crates/CIP-57/Cargo.toml new file mode 100644 index 00000000..e7345fd9 --- /dev/null +++ b/crates/CIP-57/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "cip57" +description = "CIP-57 compatibility (JSON parsing and serialization)" +publish.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true +keywords.workspace = true +documentation.workspace = true +homepage.workspace = true +readme.workspace = true + + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +anyhow = "1" diff --git a/crates/CIP-57/src/blueprint.rs b/crates/CIP-57/src/blueprint.rs new file mode 100644 index 00000000..09b4147c --- /dev/null +++ b/crates/CIP-57/src/blueprint.rs @@ -0,0 +1,156 @@ +//! This module defines the structures for a Blueprint, including its preamble, validators, and definitions. + +use serde::{Deserialize, Serialize}; +use serde_json::Number; +use std::collections::BTreeMap; + +/// Represents a blueprint containing preamble, validators, and optional definitions. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Blueprint { + pub preamble: Preamble, + pub validators: Vec, + pub definitions: Option, +} + +/// Represents the preamble of a blueprint, including metadata such as title, description, version, and compiler information. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Preamble { + pub title: String, + pub description: Option, + pub version: String, + pub plutus_version: String, + pub compiler: Option, + pub license: Option, +} + +/// Represents the compiler information in the preamble. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Compiler { + pub name: String, + pub version: Option, +} + +/// Represents a validator in the blueprint, including its title, description, compiled code, hash, datum, redeemer, and parameters. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Validator { + pub title: String, + pub description: Option, + pub compiled_code: Option, + pub hash: Option, + pub datum: Option, + pub redeemer: Option, + pub parameters: Option>, +} + +/// Represents an argument in a validator, including its title, description, purpose, and schema reference. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Argument { + pub title: Option, + pub description: Option, + pub purpose: Option, + pub schema: Reference, +} + +/// Represents a purpose array which can be either a single purpose or an array of purposes. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(untagged)] +pub enum PurposeArray { + Single(Purpose), + Array(Vec), +} + +/// Represents the purpose of an argument, which can be spend, mint, withdraw, or publish. +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum Purpose { + Spend, + Mint, + Withdraw, + Publish, +} + +/// Represents a reference to a schema. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Reference { + #[serde(rename = "$ref")] + pub reference: Option, +} + +/// Represents a parameter in a validator, including its title, description, purpose, and schema reference. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Parameter { + pub title: Option, + pub description: Option, + pub purpose: Option, + pub schema: Reference, +} + +/// Represents the definitions in a blueprint, which is a map of definition names to their corresponding definitions. +#[derive(Debug, Default, Deserialize, Serialize, Clone)] +pub struct Definitions { + #[serde(flatten, default)] + pub inner: BTreeMap, +} + +/// Represents a definition in the blueprint, including its title, description, data type, any_of schemas, items, keys, and values. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Definition { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub data_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub any_of: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub items: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub keys: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub values: Option, +} + +/// Represents an array of references which can be either a single reference or an array of references. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(untagged)] +pub enum ReferencesArray { + Single(Reference), + Array(Vec), +} + +/// Represents a schema in a definition, including its title, description, data type, index, and fields. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Schema { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + pub data_type: DataType, + pub index: Number, + pub fields: Vec, +} + +/// Represents the data type of a schema, which can be integer, bytes, list, map, or constructor. +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum DataType { + Integer, + Bytes, + List, + Map, + Constructor, +} + +/// Represents a field in a schema, including its title and reference. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Field { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(rename = "$ref")] + pub reference: String, +} diff --git a/crates/CIP-57/src/lib.rs b/crates/CIP-57/src/lib.rs new file mode 100644 index 00000000..1fe7651a --- /dev/null +++ b/crates/CIP-57/src/lib.rs @@ -0,0 +1,482 @@ +//! This module provides functions to work with blueprints and schemas, including +//! parsing JSON, reading from files, and JSON-to-JSON conversion helpers. + +use anyhow::Result; +use schema::TypeName; +use serde_json; +use std::fs; +use tx3_lang::ir::{Expression, StructExpr}; + +pub mod blueprint; +pub mod schema; +// Templates are intentionally removed for a pure JSON-to-JSON crate. + +pub struct Codegen {} + +impl Codegen { + pub fn new() -> Codegen { + Codegen {} + } + + fn get_schema_name(&self, key: String) -> String { + // Keep a simple normalization without external deps (heck) + let normalized = key + .replace("#/definitions/", "") + .replace("~1", " ") + .replace("/", " ") + .replace("_", " ") + .replace("$", " "); + // Basic PascalCase conversion + normalized + .split_whitespace() + .map(|w| { + let mut chars = w.chars(); + match chars.next() { + Some(first) => first.to_uppercase().collect::() + chars.as_str(), + None => String::new(), + } + }) + .collect::>() + .join("") + } + + fn parse_bytes_string(s: &str) -> Vec { + if let Some(hex) = s.strip_prefix("0x") { + return hex::decode(hex).unwrap_or_default(); + } + s.as_bytes().to_vec() + } + + /// Parses a JSON string into a `Blueprint`. + /// + /// # Arguments + /// + /// * `json` - The JSON data from a `plutus.json` file + /// + /// # Returns + /// + /// A `Blueprint` instance or an error. + pub fn get_blueprint_from_json(&self, json: String) -> Result { + let bp = serde_json::from_str(&json)?; + Ok(bp) + } + + /// Reads a JSON file from a specified path and parses it into a `Blueprint`. + /// + /// # Arguments + /// + /// * `path` - The `plutus.json` file path in the filesystem + /// + /// # Returns + /// + /// A `Blueprint` instance or an error. + pub fn get_blueprint_from_path(&self, path: String) -> Result { + let json = fs::read_to_string(path)?; + self.get_blueprint_from_json(json) + } + + /// Obtains the list of schemas from a given `Blueprint`. + /// + /// # Arguments + /// + /// * `blueprint` - A `Blueprint` from which to obtain the schemas. + /// + /// # Returns + /// + /// A vector of `Schema` from the blueprint. + pub fn get_schemas_from_blueprint( + &self, + blueprint: blueprint::Blueprint, + ) -> Vec { + let mut schemas: Vec = vec![]; + if blueprint.definitions.is_some() { + for definition in blueprint.definitions.unwrap().inner.iter() { + let definition_name = self.get_schema_name(definition.0.clone()); + let definition_json = serde_json::to_string(&definition.1).unwrap(); + if definition.1.data_type.is_some() { + match definition.1.data_type.unwrap() { + blueprint::DataType::Integer => { + schemas.push(schema::Schema::new_integer( + definition_name.clone(), + definition_json.clone(), + )); + } + blueprint::DataType::Bytes => { + schemas.push(schema::Schema::new_bytes( + definition_name.clone(), + definition_json.clone(), + )); + } + blueprint::DataType::List => { + if definition.1.items.is_some() { + match definition.1.items.as_ref().unwrap() { + blueprint::ReferencesArray::Single(reference) => { + if reference.reference.is_some() { + schemas.push(schema::Schema::new_list( + definition_name.clone(), + schema::Reference { + name: None, + schema_name: self.get_schema_name( + reference + .reference + .as_ref() + .unwrap() + .clone(), + ), + }, + definition_json.clone(), + )); + } + } + blueprint::ReferencesArray::Array(references) => { + let mut properties: Vec = vec![]; + for reference in references { + if reference.reference.is_some() { + properties.push(schema::Reference { + name: None, + schema_name: self.get_schema_name( + reference + .reference + .as_ref() + .unwrap() + .clone(), + ), + }); + } + } + schemas.push(schema::Schema::new_tuple( + definition_name.clone(), + properties, + definition_json.clone(), + )); + } + } + } + } + _ => {} + } + } + if definition.1.title.is_some() { + if definition.1.title.as_ref().unwrap() == "Data" && definition_name == "Data" { + schemas.push(schema::Schema::new_anydata(definition_json.clone())); + } + } + if definition.1.any_of.is_some() { + let mut internal_schemas: Vec = vec![]; + for (index, parameter) in + definition.1.any_of.as_ref().unwrap().iter().enumerate() + { + match parameter.data_type { + blueprint::DataType::Constructor => { + let schema_name = format!( + "{}{}", + definition_name, + parameter.title.clone().unwrap_or((index + 1).to_string()) + ); + let mut properties: Vec = vec![]; + for property in ¶meter.fields { + let mut schema_name = + self.get_schema_name(property.reference.clone()); + if schema_name == "Data" { + schema_name = "AnyData".to_string(); + } + properties.push(schema::Reference { + name: property.title.clone(), + schema_name, + }); + } + let schema: schema::Schema; + if properties.len().gt(&0) || parameter.title.is_none() { + if properties.iter().any(|p| p.name.is_none()) { + schema = schema::Schema::new_tuple( + schema_name, + properties, + definition_json.clone(), + ); + } else { + schema = schema::Schema::new_object( + schema_name, + properties, + definition_json.clone(), + ); + } + } else { + schema = schema::Schema::new_literal( + schema_name, + parameter.title.clone().unwrap(), + definition_json.clone(), + ); + } + internal_schemas.push(schema); + } + _ => {} + } + } + if internal_schemas.len().eq(&1) { + let mut schema = internal_schemas.first().unwrap().clone(); + schema.name = definition_name.clone(); + schemas.push(schema); + } + if internal_schemas.len().gt(&1) { + if internal_schemas.len().eq(&2) + && internal_schemas + .iter() + .any(|s| s.type_name.eq(&TypeName::Literal)) + && !internal_schemas + .iter() + .all(|s| s.type_name.eq(&TypeName::Literal)) + { + let reference = internal_schemas + .iter() + .find(|s| s.type_name.ne(&TypeName::Literal)); + schemas.push(reference.unwrap().clone()); + schemas.push(schema::Schema::new_nullable( + definition_name.clone(), + reference.unwrap().name.clone(), + definition_json.clone(), + )); + } else { + for schema in &internal_schemas { + schemas.push(schema.clone()); + } + schemas.push(schema::Schema::new_enum( + definition_name.clone(), + &internal_schemas, + definition_json.clone(), + )); + } + } + } + } + } + + schemas + } + + /// Obtains the list of validators from a given `Blueprint`. + /// + /// # Arguments + /// + /// * `blueprint` - A `Blueprint` from which to obtain the validators. + /// + /// # Returns + /// + /// A vector of `Validator` from the blueprint. + pub fn get_validators_from_blueprint( + &self, + blueprint: blueprint::Blueprint, + ) -> Vec { + let mut validators: Vec = vec![]; + for validator in blueprint.validators.iter() { + let mut datum: Option = None; + if validator.datum.is_some() + && validator.datum.as_ref().unwrap().schema.reference.is_some() + { + datum = Some(schema::Reference { + name: validator.datum.as_ref().unwrap().title.clone(), + schema_name: self.get_schema_name( + validator + .datum + .as_ref() + .unwrap() + .schema + .reference + .as_ref() + .unwrap() + .clone(), + ), + }); + } + let mut redeemer: Option = None; + if validator.redeemer.is_some() + && validator + .redeemer + .as_ref() + .unwrap() + .schema + .reference + .is_some() + { + redeemer = Some(schema::Reference { + name: validator.redeemer.as_ref().unwrap().title.clone(), + schema_name: self.get_schema_name( + validator + .redeemer + .as_ref() + .unwrap() + .schema + .reference + .as_ref() + .unwrap() + .clone(), + ), + }); + } + let mut parameters: Vec = vec![]; + if let Some(p) = &validator.parameters { + for parameter in p { + if parameter.schema.reference.is_some() { + parameters.push(schema::Reference { + name: parameter.title.clone(), + schema_name: self.get_schema_name( + parameter.schema.reference.as_ref().unwrap().clone(), + ), + }) + } + } + } + validators.push(schema::Validator { + name: validator.title.clone(), + datum: datum, + redeemer: redeemer, + parameters: parameters, + }); + } + validators + } + + /// Converts a `Blueprint` into a JSON value containing extracted schemas and validators. + + + /// Convert a JSON value according to a schema name into a tx3 IR Expression. + /// This expects `value` to match the referenced schema (by name) present in the blueprint definitions. + pub fn convert_value_by_schema_name( + &self, + blueprint: &blueprint::Blueprint, + schema_name: &str, + value: &serde_json::Value, + ) -> Result { + // Build a quick lookup of definitions by normalized schema name + let mut defs = std::collections::BTreeMap::new(); + if let Some(all) = &blueprint.definitions { + for (raw_name, def) in &all.inner { + let name = self.get_schema_name(raw_name.clone()); + defs.insert(name, def); + } + } + + let def = defs + .get(schema_name) + .ok_or_else(|| anyhow::anyhow!("unknown schema: {}", schema_name))?; + + // Primitive types + if let Some(dt) = def.data_type { + match dt { + blueprint::DataType::Integer => { + let num = match value { + serde_json::Value::Number(n) => n.as_i64().unwrap_or(0) as i128, + serde_json::Value::String(s) => s.parse::().unwrap_or(0), + _ => 0, + }; + return Ok(Expression::Number(num)); + } + blueprint::DataType::Bytes => { + let bytes = match value { + serde_json::Value::String(s) => Self::parse_bytes_string(s), + serde_json::Value::Array(arr) => arr + .iter() + .filter_map(|v| v.as_u64().map(|x| x as u8)) + .collect(), + _ => Vec::new(), + }; + return Ok(Expression::Bytes(bytes)); + } + blueprint::DataType::List => { + // For lists, expect a single reference in items (or tuple in Array case already handled elsewhere) + if let Some(items) = &def.items { + match items { + blueprint::ReferencesArray::Single(r) => { + let inner_name = r + .reference + .as_ref() + .map(|s| self.get_schema_name(s.clone())) + .ok_or_else(|| anyhow::anyhow!("list items missing $ref"))?; + let list = match value { + serde_json::Value::Array(arr) => { + let mut out = Vec::new(); + for v in arr { + out.push(self.convert_value_by_schema_name( + blueprint, + &inner_name, + v, + )?); + } + out + } + _ => Vec::new(), + }; + return Ok(Expression::List(list)); + } + blueprint::ReferencesArray::Array(_) => { + // Treat as tuple + let arr = value.as_array().cloned().unwrap_or_default(); + let mut fields = Vec::new(); + if let blueprint::ReferencesArray::Array(refs) = items { + for (i, r) in refs.iter().enumerate() { + let inner_name = r + .reference + .as_ref() + .map(|s| self.get_schema_name(s.clone())) + .ok_or_else(|| { + anyhow::anyhow!("tuple item missing $ref") + })?; + let v = + arr.get(i).cloned().unwrap_or(serde_json::Value::Null); + fields.push(self.convert_value_by_schema_name( + blueprint, + &inner_name, + &v, + )?); + } + } + return Ok(Expression::Struct(StructExpr { + constructor: 0, + fields, + })); + } + } + } + } + _ => {} + } + } + + // Constructors (anyOf with data_type = Constructor). We choose variant by matching expected shape or `index`. + if let Some(any_of) = &def.any_of { + // Strategy: if `value` is an object with fields, pick matching constructor by field count; else use first. + let chosen = any_of + .first() + .ok_or_else(|| anyhow::anyhow!("empty anyOf"))?; + let index = chosen.index.as_i64().unwrap_or(0) as usize; + let mut fields_expr = Vec::new(); + for f in &chosen.fields { + let ref_name = self.get_schema_name(f.reference.clone()); + // Try to fetch field by title from value object; fallback to Null + let field_json = match value { + serde_json::Value::Object(map) => { + if let Some(title) = &f.title { + map.get(title).cloned().unwrap_or(serde_json::Value::Null) + } else { + serde_json::Value::Null + } + } + _ => serde_json::Value::Null, + }; + fields_expr.push(self.convert_value_by_schema_name( + blueprint, + &ref_name, + &field_json, + )?); + } + return Ok(Expression::Struct(StructExpr { + constructor: index, + fields: fields_expr, + })); + } + + // Fallback + Ok(Expression::None) + } + +// (no free functions) +} diff --git a/crates/CIP-57/src/schema.rs b/crates/CIP-57/src/schema.rs new file mode 100644 index 00000000..8680e523 --- /dev/null +++ b/crates/CIP-57/src/schema.rs @@ -0,0 +1,267 @@ +//! This module defines the structures and implementations for schemas, including types, references, and validators. + +use serde::{Deserialize, Serialize}; +use std::str; + +/// Represents the different types a schema can have. +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum TypeName { + AnyData, + Integer, + Bytes, + Literal, + Nullable, + Object, + Enum, + Tuple, + List, +} + +/// Represents a schema with a name, type, optional properties, and JSON representation. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Schema { + pub name: String, + pub type_name: TypeName, + pub properties: Option>, + pub json: String, +} + +/// Represents a reference to another schema, including an optional name and schema name. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Reference { + pub name: Option, + pub schema_name: String, +} + +/// Represents a validator with a name, optional datum and redeemer, and parameters. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Validator { + pub name: String, + pub datum: Option, + pub redeemer: Option, + pub parameters: Vec, +} + +impl Schema { + /// Creates a new any data schema. + /// + /// # Arguments + /// + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `AnyData`. + pub fn new_anydata(json: String) -> Self { + Self { + name: "AnyData".to_string(), + type_name: TypeName::AnyData, + properties: None, + json, + } + } + + /// Creates a new integer schema. + /// + /// # Arguments + /// + /// * `name` - The name of the schema. + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `Integer`. + pub fn new_integer(name: String, json: String) -> Self { + Self { + name, + type_name: TypeName::Integer, + properties: None, + json, + } + } + + /// Creates a new bytes schema. + /// + /// # Arguments + /// + /// * `name` - The name of the schema. + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `Bytes`. + pub fn new_bytes(name: String, json: String) -> Self { + Self { + name, + type_name: TypeName::Bytes, + properties: None, + json, + } + } + + /// Creates a new literal schema. + /// + /// # Arguments + /// + /// * `name` - The name of the schema. + /// * `value` - The literal value of the schema. + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `Literal`. + pub fn new_literal(name: String, value: String, json: String) -> Self { + Self { + name, + type_name: TypeName::Literal, + properties: Some(vec![Reference { + name: None, + schema_name: value, + }]), + json, + } + } + + /// Creates a new nullable schema. + /// + /// # Arguments + /// + /// * `name` - The name of the schema. + /// * `reference` - The reference schema name. + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `Nullable`. + pub fn new_nullable(name: String, reference: String, json: String) -> Self { + Self { + name, + type_name: TypeName::Nullable, + properties: Some(vec![Reference { + name: None, + schema_name: reference, + }]), + json, + } + } + + /// Creates a new object schema. + /// + /// # Arguments + /// + /// * `name` - The name of the schema. + /// * `properties` - The properties of the object schema. + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `Object`. + pub fn new_object(name: String, properties: Vec, json: String) -> Self { + Self { + name, + type_name: TypeName::Object, + properties: Some(properties), + json, + } + } + + /// Creates a new enum schema. + /// + /// # Arguments + /// + /// * `name` - The name of the schema. + /// * `schemas` - The schemas that make up the enum. + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `Enum`. + pub fn new_enum(name: String, schemas: &Vec, json: String) -> Self { + Self { + name, + type_name: TypeName::Enum, + properties: Some( + schemas + .iter() + .map(|s| Reference { + name: None, + schema_name: s.name.clone(), + }) + .collect(), + ), + json, + } + } + + /// Creates a new tuple schema. + /// + /// # Arguments + /// + /// * `name` - The name of the schema. + /// * `properties` - The properties of the tuple schema. + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `Tuple`. + pub fn new_tuple(name: String, properties: Vec, json: String) -> Self { + Self { + name, + type_name: TypeName::Tuple, + properties: Some(properties), + json, + } + } + + /// Creates a new list schema. + /// + /// # Arguments + /// + /// * `name` - The name of the schema. + /// * `reference` - The reference schema. + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `List`. + pub fn new_list(name: String, reference: Reference, json: String) -> Self { + Self { + name, + type_name: TypeName::List, + properties: Some(vec![reference]), + json, + } + } +} + +impl str::FromStr for TypeName { + type Err = (); + + /// Converts a string to a `TypeName`. + /// + /// # Arguments + /// + /// * `input` - The string representation of the type name. + /// + /// # Returns + /// + /// A `Result` containing the `TypeName` or an error. + fn from_str(input: &str) -> Result { + match input { + "AnyData" => Ok(TypeName::AnyData), + "Integer" => Ok(TypeName::Integer), + "Bytes" => Ok(TypeName::Bytes), + "Literal" => Ok(TypeName::Literal), + "Nullable" => Ok(TypeName::Nullable), + "Object" => Ok(TypeName::Object), + "Enum" => Ok(TypeName::Enum), + "Tuple" => Ok(TypeName::Tuple), + "List" => Ok(TypeName::List), + _ => Err(()), + } + } +} From e8d4749080d2419c06c488667b9b07289de4114f Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Thu, 4 Dec 2025 13:35:20 -0300 Subject: [PATCH 02/27] Refactor blueprint and schema modules: consolidate structures and remove unused code --- crates/CIP-57/examples/plutus.json | 634 +++++++++++++++++++++++++++++ crates/CIP-57/src/blueprint.rs | 156 ------- crates/CIP-57/src/lib.rs | 620 ++++++++-------------------- crates/CIP-57/src/schema.rs | 267 ------------ 4 files changed, 793 insertions(+), 884 deletions(-) create mode 100644 crates/CIP-57/examples/plutus.json delete mode 100644 crates/CIP-57/src/blueprint.rs delete mode 100644 crates/CIP-57/src/schema.rs diff --git a/crates/CIP-57/examples/plutus.json b/crates/CIP-57/examples/plutus.json new file mode 100644 index 00000000..97472029 --- /dev/null +++ b/crates/CIP-57/examples/plutus.json @@ -0,0 +1,634 @@ +{ + "preamble": { + "title": "txpipe/contract", + "description": "Aiken contracts for project 'txpipe/contract'", + "version": "0.0.0", + "plutusVersion": "v3", + "compiler": { + "name": "Aiken", + "version": "v1.1.17+c3a7fba" + }, + "license": "Apache-2.0" + }, + "validators": [ + { + "title": "githoney_contract.badges_contract.spend", + "datum": { + "title": "_datum", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1Datum" + } + }, + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1Redeemer" + } + }, + "parameters": [ + { + "title": "settings_policy_id", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "eedaa957c60268de", + "hash": "24b9b1964ce02550db270a1d6270b505b9c0342625ee766d77fab1f9" + }, + { + "title": "githoney_contract.badges_contract.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "settings_policy_id", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "3fe9763bc5ea0108", + "hash": "24b9b1964ce02550db270a1d6270b505b9c0342625ee766d77fab1f9" + }, + { + "title": "githoney_contract.badges_policy.mint", + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1Redeemer" + } + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + }, + { + "title": "nonce", + "schema": { + "$ref": "#/definitions/Int" + } + } + ], + "compiledCode": "c8db1de3fbbc8921", + "hash": "87d6bd0b40f204d49803dad8e0d70611918d22354125c8f226a42670" + }, + { + "title": "githoney_contract.badges_policy.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + }, + { + "title": "nonce", + "schema": { + "$ref": "#/definitions/Int" + } + } + ], + "compiledCode": "f143b98f13d5b12b", + "hash": "87d6bd0b40f204d49803dad8e0d70611918d22354125c8f226a42670" + }, + { + "title": "githoney_contract.githoney.spend", + "datum": { + "title": "datum", + "schema": { + "$ref": "#/definitions/types~1GithoneyDatum" + } + }, + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/types~1GithoneyContractRedeemers" + } + }, + "parameters": [ + { + "title": "settings_policy_id", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "f1a9d1de401b5004", + "hash": "7577273f99e7f3c9211d6342338d6316afc91689333c4d5099e7b2d1" + }, + { + "title": "githoney_contract.githoney.mint", + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1Redeemer" + } + }, + "parameters": [ + { + "title": "settings_policy_id", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "f1f665b7b5ec44d6", + "hash": "7577273f99e7f3c9211d6342338d6316afc91689333c4d5099e7b2d1" + }, + { + "title": "githoney_contract.githoney.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "settings_policy_id", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "8dc98413cbd1b43f", + "hash": "7577273f99e7f3c9211d6342338d6316afc91689333c4d5099e7b2d1" + }, + { + "title": "githoney_contract.settings.spend", + "datum": { + "title": "datum", + "schema": { + "$ref": "#/definitions/types~1SettingsDatum" + } + }, + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/types~1SettingsRedeemers" + } + }, + "compiledCode": "5eb78784a02ee1a0", + "hash": "049f1a09fb535089fdd9df98b4b0975d1081f9afc2d6f59ad2f9c208" + }, + { + "title": "githoney_contract.settings.else", + "redeemer": { + "schema": {} + }, + "compiledCode": "9813b3c286674b27", + "hash": "049f1a09fb535089fdd9df98b4b0975d1081f9afc2d6f59ad2f9c208" + }, + { + "title": "githoney_contract.settings_minting.mint", + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1Redeemer" + } + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + }, + { + "title": "settings_script_addr", + "schema": { + "$ref": "#/definitions/cardano~1address~1Address" + } + } + ], + "compiledCode": "6396e66bd8b6ac88", + "hash": "15721f473d73adf918aa7541871739c2e8df0a60abe023411cf9f1b0" + }, + { + "title": "githoney_contract.settings_minting.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + }, + { + "title": "settings_script_addr", + "schema": { + "$ref": "#/definitions/cardano~1address~1Address" + } + } + ], + "compiledCode": "fa2dba3b69a8d00c", + "hash": "15721f473d73adf918aa7541871739c2e8df0a60abe023411cf9f1b0" + } + ], + "definitions": { + "Bool": { + "title": "Bool", + "anyOf": [ + { + "title": "False", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "True", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "ByteArray": { + "title": "ByteArray", + "dataType": "bytes" + }, + "Data": { + "title": "Data", + "description": "Any Plutus data." + }, + "Int": { + "dataType": "integer" + }, + "Option$cardano/address/Address": { + "title": "Option", + "anyOf": [ + { + "title": "Some", + "description": "An optional value.", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/cardano~1address~1Address" + } + ] + }, + { + "title": "None", + "description": "Nothing.", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "Option$cardano/address/StakeCredential": { + "title": "Option", + "anyOf": [ + { + "title": "Some", + "description": "An optional value.", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/cardano~1address~1StakeCredential" + } + ] + }, + { + "title": "None", + "description": "Nothing.", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "Pairs$cardano/assets/AssetName_Int": { + "title": "Pairs", + "dataType": "map", + "keys": { + "$ref": "#/definitions/cardano~1assets~1AssetName" + }, + "values": { + "$ref": "#/definitions/Int" + } + }, + "Pairs$cardano/assets/PolicyId_Pairs$cardano/assets/AssetName_Int": { + "title": "Pairs>", + "dataType": "map", + "keys": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + }, + "values": { + "$ref": "#/definitions/Pairs$cardano~1assets~1AssetName_Int" + } + }, + "aiken/crypto/DataHash": { + "title": "DataHash", + "dataType": "bytes" + }, + "aiken/crypto/ScriptHash": { + "title": "ScriptHash", + "dataType": "bytes" + }, + "aiken/crypto/VerificationKeyHash": { + "title": "VerificationKeyHash", + "dataType": "bytes" + }, + "cardano/address/Address": { + "title": "Address", + "description": "A Cardano `Address` typically holding one or two credential references.\n\n Note that legacy bootstrap addresses (a.k.a. 'Byron addresses') are\n completely excluded from Plutus contexts. Thus, from an on-chain\n perspective only exists addresses of type 00, 01, ..., 07 as detailed\n in [CIP-0019 :: Shelley Addresses](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0019/#shelley-addresses).", + "anyOf": [ + { + "title": "Address", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "payment_credential", + "$ref": "#/definitions/cardano~1address~1PaymentCredential" + }, + { + "title": "stake_credential", + "$ref": "#/definitions/Option$cardano~1address~1StakeCredential" + } + ] + } + ] + }, + "cardano/address/Credential": { + "title": "Credential", + "description": "A general structure for representing an on-chain `Credential`.\n\n Credentials are always one of two kinds: a direct public/private key\n pair, or a script (native or Plutus).", + "anyOf": [ + { + "title": "VerificationKey", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + } + ] + }, + { + "title": "Script", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1ScriptHash" + } + ] + } + ] + }, + "cardano/address/PaymentCredential": { + "title": "PaymentCredential", + "description": "A general structure for representing an on-chain `Credential`.\n\n Credentials are always one of two kinds: a direct public/private key\n pair, or a script (native or Plutus).", + "anyOf": [ + { + "title": "VerificationKey", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + } + ] + }, + { + "title": "Script", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1ScriptHash" + } + ] + } + ] + }, + "cardano/address/StakeCredential": { + "title": "StakeCredential", + "description": "Represent a type of object that can be represented either inline (by hash)\n or via a reference (i.e. a pointer to an on-chain location).\n\n This is mainly use for capturing pointers to a stake credential\n registration certificate in the case of so-called pointer addresses.", + "anyOf": [ + { + "title": "Inline", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/cardano~1address~1Credential" + } + ] + }, + { + "title": "Pointer", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "title": "slot_number", + "$ref": "#/definitions/Int" + }, + { + "title": "transaction_index", + "$ref": "#/definitions/Int" + }, + { + "title": "certificate_index", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "cardano/assets/AssetName": { + "title": "AssetName", + "dataType": "bytes" + }, + "cardano/assets/PolicyId": { + "title": "PolicyId", + "dataType": "bytes" + }, + "cardano/transaction/Datum": { + "title": "Datum", + "description": "An output `Datum`.", + "anyOf": [ + { + "title": "NoDatum", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "DatumHash", + "description": "A datum referenced by its hash digest.", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1DataHash" + } + ] + }, + { + "title": "InlineDatum", + "description": "A datum completely inlined in the output.", + "dataType": "constructor", + "index": 2, + "fields": [ + { + "$ref": "#/definitions/Data" + } + ] + } + ] + }, + "cardano/transaction/OutputReference": { + "title": "OutputReference", + "description": "An `OutputReference` is a unique reference to an output on-chain. The `output_index`\n corresponds to the position in the output list of the transaction (identified by its id)\n that produced that output", + "anyOf": [ + { + "title": "OutputReference", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "transaction_id", + "$ref": "#/definitions/ByteArray" + }, + { + "title": "output_index", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "cardano/transaction/Redeemer": { + "title": "Redeemer", + "description": "Any Plutus data." + }, + "types/GithoneyContractRedeemers": { + "title": "GithoneyContractRedeemers", + "anyOf": [ + { + "title": "AddRewards", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "Assign", + "dataType": "constructor", + "index": 1, + "fields": [] + }, + { + "title": "Merge", + "dataType": "constructor", + "index": 2, + "fields": [] + }, + { + "title": "Close", + "dataType": "constructor", + "index": 3, + "fields": [] + }, + { + "title": "Claim", + "dataType": "constructor", + "index": 4, + "fields": [] + } + ] + }, + "types/GithoneyDatum": { + "title": "GithoneyDatum", + "anyOf": [ + { + "title": "GithoneyDatum", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "admin_payment_credential", + "$ref": "#/definitions/cardano~1address~1Credential" + }, + { + "title": "maintainer_address", + "$ref": "#/definitions/cardano~1address~1Address" + }, + { + "title": "contributor_address", + "$ref": "#/definitions/Option$cardano~1address~1Address" + }, + { + "title": "bounty_reward_fee", + "$ref": "#/definitions/Int" + }, + { + "title": "deadline", + "$ref": "#/definitions/Int" + }, + { + "title": "merged", + "$ref": "#/definitions/Bool" + }, + { + "title": "initial_value", + "$ref": "#/definitions/Pairs$cardano~1assets~1PolicyId_Pairs$cardano~1assets~1AssetName_Int" + } + ] + } + ] + }, + "types/SettingsDatum": { + "title": "SettingsDatum", + "anyOf": [ + { + "title": "SettingsDatum", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "githoney_address", + "$ref": "#/definitions/cardano~1address~1Address" + }, + { + "title": "bounty_creation_fee", + "$ref": "#/definitions/Int" + }, + { + "title": "bounty_reward_fee", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "types/SettingsRedeemers": { + "title": "SettingsRedeemers", + "anyOf": [ + { + "title": "UpdateSettings", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "CloseSettings", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + } + } +} diff --git a/crates/CIP-57/src/blueprint.rs b/crates/CIP-57/src/blueprint.rs deleted file mode 100644 index 09b4147c..00000000 --- a/crates/CIP-57/src/blueprint.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! This module defines the structures for a Blueprint, including its preamble, validators, and definitions. - -use serde::{Deserialize, Serialize}; -use serde_json::Number; -use std::collections::BTreeMap; - -/// Represents a blueprint containing preamble, validators, and optional definitions. -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Blueprint { - pub preamble: Preamble, - pub validators: Vec, - pub definitions: Option, -} - -/// Represents the preamble of a blueprint, including metadata such as title, description, version, and compiler information. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Preamble { - pub title: String, - pub description: Option, - pub version: String, - pub plutus_version: String, - pub compiler: Option, - pub license: Option, -} - -/// Represents the compiler information in the preamble. -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Compiler { - pub name: String, - pub version: Option, -} - -/// Represents a validator in the blueprint, including its title, description, compiled code, hash, datum, redeemer, and parameters. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Validator { - pub title: String, - pub description: Option, - pub compiled_code: Option, - pub hash: Option, - pub datum: Option, - pub redeemer: Option, - pub parameters: Option>, -} - -/// Represents an argument in a validator, including its title, description, purpose, and schema reference. -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Argument { - pub title: Option, - pub description: Option, - pub purpose: Option, - pub schema: Reference, -} - -/// Represents a purpose array which can be either a single purpose or an array of purposes. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(untagged)] -pub enum PurposeArray { - Single(Purpose), - Array(Vec), -} - -/// Represents the purpose of an argument, which can be spend, mint, withdraw, or publish. -#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] -#[serde(rename_all = "camelCase")] -pub enum Purpose { - Spend, - Mint, - Withdraw, - Publish, -} - -/// Represents a reference to a schema. -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Reference { - #[serde(rename = "$ref")] - pub reference: Option, -} - -/// Represents a parameter in a validator, including its title, description, purpose, and schema reference. -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Parameter { - pub title: Option, - pub description: Option, - pub purpose: Option, - pub schema: Reference, -} - -/// Represents the definitions in a blueprint, which is a map of definition names to their corresponding definitions. -#[derive(Debug, Default, Deserialize, Serialize, Clone)] -pub struct Definitions { - #[serde(flatten, default)] - pub inner: BTreeMap, -} - -/// Represents a definition in the blueprint, including its title, description, data type, any_of schemas, items, keys, and values. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Definition { - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub data_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub any_of: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub items: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub keys: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub values: Option, -} - -/// Represents an array of references which can be either a single reference or an array of references. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(untagged)] -pub enum ReferencesArray { - Single(Reference), - Array(Vec), -} - -/// Represents a schema in a definition, including its title, description, data type, index, and fields. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Schema { - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - pub data_type: DataType, - pub index: Number, - pub fields: Vec, -} - -/// Represents the data type of a schema, which can be integer, bytes, list, map, or constructor. -#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] -#[serde(rename_all = "camelCase")] -pub enum DataType { - Integer, - Bytes, - List, - Map, - Constructor, -} - -/// Represents a field in a schema, including its title and reference. -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Field { - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - #[serde(rename = "$ref")] - pub reference: String, -} diff --git a/crates/CIP-57/src/lib.rs b/crates/CIP-57/src/lib.rs index 1fe7651a..0e368fab 100644 --- a/crates/CIP-57/src/lib.rs +++ b/crates/CIP-57/src/lib.rs @@ -1,482 +1,180 @@ -//! This module provides functions to work with blueprints and schemas, including -//! parsing JSON, reading from files, and JSON-to-JSON conversion helpers. +//! This module defines the structures for a Blueprint, including its preamble, validators, and definitions. -use anyhow::Result; -use schema::TypeName; -use serde_json; +use serde::{Deserialize, Serialize}; +use serde_json::Number; +use std::collections::BTreeMap; use std::fs; -use tx3_lang::ir::{Expression, StructExpr}; +use std::path::PathBuf; + +/// Represents a blueprint containing preamble, validators, and optional definitions. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Blueprint { + pub preamble: Preamble, + pub validators: Vec, + pub definitions: Option, +} -pub mod blueprint; -pub mod schema; -// Templates are intentionally removed for a pure JSON-to-JSON crate. +/// Represents the preamble of a blueprint, including metadata such as title, description, version, and compiler information. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Preamble { + pub title: String, + pub description: Option, + pub version: String, + pub plutus_version: String, + pub compiler: Option, + pub license: Option, +} -pub struct Codegen {} +/// Represents the compiler information in the preamble. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Compiler { + pub name: String, + pub version: Option, +} -impl Codegen { - pub fn new() -> Codegen { - Codegen {} - } +/// Represents a validator in the blueprint, including its title, description, compiled code, hash, datum, redeemer, and parameters. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Validator { + pub title: String, + pub description: Option, + pub compiled_code: Option, + pub hash: Option, + pub datum: Option, + pub redeemer: Option, + pub parameters: Option>, +} - fn get_schema_name(&self, key: String) -> String { - // Keep a simple normalization without external deps (heck) - let normalized = key - .replace("#/definitions/", "") - .replace("~1", " ") - .replace("/", " ") - .replace("_", " ") - .replace("$", " "); - // Basic PascalCase conversion - normalized - .split_whitespace() - .map(|w| { - let mut chars = w.chars(); - match chars.next() { - Some(first) => first.to_uppercase().collect::() + chars.as_str(), - None => String::new(), - } - }) - .collect::>() - .join("") - } +/// Represents an argument in a validator, including its title, description, purpose, and schema reference. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Argument { + pub title: Option, + pub description: Option, + pub purpose: Option, + pub schema: Reference, +} - fn parse_bytes_string(s: &str) -> Vec { - if let Some(hex) = s.strip_prefix("0x") { - return hex::decode(hex).unwrap_or_default(); - } - s.as_bytes().to_vec() - } +/// Represents a purpose array which can be either a single purpose or an array of purposes. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(untagged)] +pub enum PurposeArray { + Single(Purpose), + Array(Vec), +} - /// Parses a JSON string into a `Blueprint`. - /// - /// # Arguments - /// - /// * `json` - The JSON data from a `plutus.json` file - /// - /// # Returns - /// - /// A `Blueprint` instance or an error. - pub fn get_blueprint_from_json(&self, json: String) -> Result { - let bp = serde_json::from_str(&json)?; - Ok(bp) - } +/// Represents the purpose of an argument, which can be spend, mint, withdraw, or publish. +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum Purpose { + Spend, + Mint, + Withdraw, + Publish, +} - /// Reads a JSON file from a specified path and parses it into a `Blueprint`. - /// - /// # Arguments - /// - /// * `path` - The `plutus.json` file path in the filesystem - /// - /// # Returns - /// - /// A `Blueprint` instance or an error. - pub fn get_blueprint_from_path(&self, path: String) -> Result { - let json = fs::read_to_string(path)?; - self.get_blueprint_from_json(json) - } +/// Represents a reference to a schema. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Reference { + #[serde(rename = "$ref")] + pub reference: Option, +} - /// Obtains the list of schemas from a given `Blueprint`. - /// - /// # Arguments - /// - /// * `blueprint` - A `Blueprint` from which to obtain the schemas. - /// - /// # Returns - /// - /// A vector of `Schema` from the blueprint. - pub fn get_schemas_from_blueprint( - &self, - blueprint: blueprint::Blueprint, - ) -> Vec { - let mut schemas: Vec = vec![]; - if blueprint.definitions.is_some() { - for definition in blueprint.definitions.unwrap().inner.iter() { - let definition_name = self.get_schema_name(definition.0.clone()); - let definition_json = serde_json::to_string(&definition.1).unwrap(); - if definition.1.data_type.is_some() { - match definition.1.data_type.unwrap() { - blueprint::DataType::Integer => { - schemas.push(schema::Schema::new_integer( - definition_name.clone(), - definition_json.clone(), - )); - } - blueprint::DataType::Bytes => { - schemas.push(schema::Schema::new_bytes( - definition_name.clone(), - definition_json.clone(), - )); - } - blueprint::DataType::List => { - if definition.1.items.is_some() { - match definition.1.items.as_ref().unwrap() { - blueprint::ReferencesArray::Single(reference) => { - if reference.reference.is_some() { - schemas.push(schema::Schema::new_list( - definition_name.clone(), - schema::Reference { - name: None, - schema_name: self.get_schema_name( - reference - .reference - .as_ref() - .unwrap() - .clone(), - ), - }, - definition_json.clone(), - )); - } - } - blueprint::ReferencesArray::Array(references) => { - let mut properties: Vec = vec![]; - for reference in references { - if reference.reference.is_some() { - properties.push(schema::Reference { - name: None, - schema_name: self.get_schema_name( - reference - .reference - .as_ref() - .unwrap() - .clone(), - ), - }); - } - } - schemas.push(schema::Schema::new_tuple( - definition_name.clone(), - properties, - definition_json.clone(), - )); - } - } - } - } - _ => {} - } - } - if definition.1.title.is_some() { - if definition.1.title.as_ref().unwrap() == "Data" && definition_name == "Data" { - schemas.push(schema::Schema::new_anydata(definition_json.clone())); - } - } - if definition.1.any_of.is_some() { - let mut internal_schemas: Vec = vec![]; - for (index, parameter) in - definition.1.any_of.as_ref().unwrap().iter().enumerate() - { - match parameter.data_type { - blueprint::DataType::Constructor => { - let schema_name = format!( - "{}{}", - definition_name, - parameter.title.clone().unwrap_or((index + 1).to_string()) - ); - let mut properties: Vec = vec![]; - for property in ¶meter.fields { - let mut schema_name = - self.get_schema_name(property.reference.clone()); - if schema_name == "Data" { - schema_name = "AnyData".to_string(); - } - properties.push(schema::Reference { - name: property.title.clone(), - schema_name, - }); - } - let schema: schema::Schema; - if properties.len().gt(&0) || parameter.title.is_none() { - if properties.iter().any(|p| p.name.is_none()) { - schema = schema::Schema::new_tuple( - schema_name, - properties, - definition_json.clone(), - ); - } else { - schema = schema::Schema::new_object( - schema_name, - properties, - definition_json.clone(), - ); - } - } else { - schema = schema::Schema::new_literal( - schema_name, - parameter.title.clone().unwrap(), - definition_json.clone(), - ); - } - internal_schemas.push(schema); - } - _ => {} - } - } - if internal_schemas.len().eq(&1) { - let mut schema = internal_schemas.first().unwrap().clone(); - schema.name = definition_name.clone(); - schemas.push(schema); - } - if internal_schemas.len().gt(&1) { - if internal_schemas.len().eq(&2) - && internal_schemas - .iter() - .any(|s| s.type_name.eq(&TypeName::Literal)) - && !internal_schemas - .iter() - .all(|s| s.type_name.eq(&TypeName::Literal)) - { - let reference = internal_schemas - .iter() - .find(|s| s.type_name.ne(&TypeName::Literal)); - schemas.push(reference.unwrap().clone()); - schemas.push(schema::Schema::new_nullable( - definition_name.clone(), - reference.unwrap().name.clone(), - definition_json.clone(), - )); - } else { - for schema in &internal_schemas { - schemas.push(schema.clone()); - } - schemas.push(schema::Schema::new_enum( - definition_name.clone(), - &internal_schemas, - definition_json.clone(), - )); - } - } - } - } - } +/// Represents a parameter in a validator, including its title, description, purpose, and schema reference. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Parameter { + pub title: Option, + pub description: Option, + pub purpose: Option, + pub schema: Reference, +} - schemas - } +/// Represents the definitions in a blueprint, which is a map of definition names to their corresponding definitions. +#[derive(Debug, Default, Deserialize, Serialize, Clone)] +pub struct Definitions { + #[serde(flatten, default)] + pub inner: BTreeMap, +} - /// Obtains the list of validators from a given `Blueprint`. - /// - /// # Arguments - /// - /// * `blueprint` - A `Blueprint` from which to obtain the validators. - /// - /// # Returns - /// - /// A vector of `Validator` from the blueprint. - pub fn get_validators_from_blueprint( - &self, - blueprint: blueprint::Blueprint, - ) -> Vec { - let mut validators: Vec = vec![]; - for validator in blueprint.validators.iter() { - let mut datum: Option = None; - if validator.datum.is_some() - && validator.datum.as_ref().unwrap().schema.reference.is_some() - { - datum = Some(schema::Reference { - name: validator.datum.as_ref().unwrap().title.clone(), - schema_name: self.get_schema_name( - validator - .datum - .as_ref() - .unwrap() - .schema - .reference - .as_ref() - .unwrap() - .clone(), - ), - }); - } - let mut redeemer: Option = None; - if validator.redeemer.is_some() - && validator - .redeemer - .as_ref() - .unwrap() - .schema - .reference - .is_some() - { - redeemer = Some(schema::Reference { - name: validator.redeemer.as_ref().unwrap().title.clone(), - schema_name: self.get_schema_name( - validator - .redeemer - .as_ref() - .unwrap() - .schema - .reference - .as_ref() - .unwrap() - .clone(), - ), - }); - } - let mut parameters: Vec = vec![]; - if let Some(p) = &validator.parameters { - for parameter in p { - if parameter.schema.reference.is_some() { - parameters.push(schema::Reference { - name: parameter.title.clone(), - schema_name: self.get_schema_name( - parameter.schema.reference.as_ref().unwrap().clone(), - ), - }) - } - } - } - validators.push(schema::Validator { - name: validator.title.clone(), - datum: datum, - redeemer: redeemer, - parameters: parameters, - }); - } - validators - } +/// Represents a definition in the blueprint, including its title, description, data type, any_of schemas, items, keys, and values. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Definition { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub data_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub any_of: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub items: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub keys: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub values: Option, +} + +/// Represents an array of references which can be either a single reference or an array of references. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(untagged)] +pub enum ReferencesArray { + Single(Reference), + Array(Vec), +} + +/// Represents a schema in a definition, including its title, description, data type, index, and fields. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Schema { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + pub data_type: DataType, + pub index: Number, + pub fields: Vec, +} - /// Converts a `Blueprint` into a JSON value containing extracted schemas and validators. +/// Represents the data type of a schema, which can be integer, bytes, list, map, or constructor. +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum DataType { + Integer, + Bytes, + List, + Map, + Constructor, +} +/// Represents a field in a schema, including its title and reference. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Field { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(rename = "$ref")] + pub reference: String, +} - /// Convert a JSON value according to a schema name into a tx3 IR Expression. - /// This expects `value` to match the referenced schema (by name) present in the blueprint definitions. - pub fn convert_value_by_schema_name( - &self, - blueprint: &blueprint::Blueprint, - schema_name: &str, - value: &serde_json::Value, - ) -> Result { - // Build a quick lookup of definitions by normalized schema name - let mut defs = std::collections::BTreeMap::new(); - if let Some(all) = &blueprint.definitions { - for (raw_name, def) in &all.inner { - let name = self.get_schema_name(raw_name.clone()); - defs.insert(name, def); - } - } +#[cfg(test)] +mod tests { + use super::*; - let def = defs - .get(schema_name) - .ok_or_else(|| anyhow::anyhow!("unknown schema: {}", schema_name))?; + #[test] + fn deserialize_plutus_json_into_blueprint() { + let manifest = env!("CARGO_MANIFEST_DIR"); + let path = PathBuf::from(manifest).join("examples").join("plutus.json"); - // Primitive types - if let Some(dt) = def.data_type { - match dt { - blueprint::DataType::Integer => { - let num = match value { - serde_json::Value::Number(n) => n.as_i64().unwrap_or(0) as i128, - serde_json::Value::String(s) => s.parse::().unwrap_or(0), - _ => 0, - }; - return Ok(Expression::Number(num)); - } - blueprint::DataType::Bytes => { - let bytes = match value { - serde_json::Value::String(s) => Self::parse_bytes_string(s), - serde_json::Value::Array(arr) => arr - .iter() - .filter_map(|v| v.as_u64().map(|x| x as u8)) - .collect(), - _ => Vec::new(), - }; - return Ok(Expression::Bytes(bytes)); - } - blueprint::DataType::List => { - // For lists, expect a single reference in items (or tuple in Array case already handled elsewhere) - if let Some(items) = &def.items { - match items { - blueprint::ReferencesArray::Single(r) => { - let inner_name = r - .reference - .as_ref() - .map(|s| self.get_schema_name(s.clone())) - .ok_or_else(|| anyhow::anyhow!("list items missing $ref"))?; - let list = match value { - serde_json::Value::Array(arr) => { - let mut out = Vec::new(); - for v in arr { - out.push(self.convert_value_by_schema_name( - blueprint, - &inner_name, - v, - )?); - } - out - } - _ => Vec::new(), - }; - return Ok(Expression::List(list)); - } - blueprint::ReferencesArray::Array(_) => { - // Treat as tuple - let arr = value.as_array().cloned().unwrap_or_default(); - let mut fields = Vec::new(); - if let blueprint::ReferencesArray::Array(refs) = items { - for (i, r) in refs.iter().enumerate() { - let inner_name = r - .reference - .as_ref() - .map(|s| self.get_schema_name(s.clone())) - .ok_or_else(|| { - anyhow::anyhow!("tuple item missing $ref") - })?; - let v = - arr.get(i).cloned().unwrap_or(serde_json::Value::Null); - fields.push(self.convert_value_by_schema_name( - blueprint, - &inner_name, - &v, - )?); - } - } - return Ok(Expression::Struct(StructExpr { - constructor: 0, - fields, - })); - } - } - } - } - _ => {} - } - } + let json = fs::read_to_string(&path) + .expect(&format!("failed to read example file: {}", path.display())); - // Constructors (anyOf with data_type = Constructor). We choose variant by matching expected shape or `index`. - if let Some(any_of) = &def.any_of { - // Strategy: if `value` is an object with fields, pick matching constructor by field count; else use first. - let chosen = any_of - .first() - .ok_or_else(|| anyhow::anyhow!("empty anyOf"))?; - let index = chosen.index.as_i64().unwrap_or(0) as usize; - let mut fields_expr = Vec::new(); - for f in &chosen.fields { - let ref_name = self.get_schema_name(f.reference.clone()); - // Try to fetch field by title from value object; fallback to Null - let field_json = match value { - serde_json::Value::Object(map) => { - if let Some(title) = &f.title { - map.get(title).cloned().unwrap_or(serde_json::Value::Null) - } else { - serde_json::Value::Null - } - } - _ => serde_json::Value::Null, - }; - fields_expr.push(self.convert_value_by_schema_name( - blueprint, - &ref_name, - &field_json, - )?); - } - return Ok(Expression::Struct(StructExpr { - constructor: index, - fields: fields_expr, - })); - } + let bp: Blueprint = serde_json::from_str(&json).expect("failed to deserialize blueprint"); - // Fallback - Ok(Expression::None) + assert!( + !bp.preamble.title.is_empty(), + "preamble.title should not be empty" + ); + assert!(!bp.validators.is_empty(), "expected at least one validator"); } - -// (no free functions) } diff --git a/crates/CIP-57/src/schema.rs b/crates/CIP-57/src/schema.rs deleted file mode 100644 index 8680e523..00000000 --- a/crates/CIP-57/src/schema.rs +++ /dev/null @@ -1,267 +0,0 @@ -//! This module defines the structures and implementations for schemas, including types, references, and validators. - -use serde::{Deserialize, Serialize}; -use std::str; - -/// Represents the different types a schema can have. -#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] -#[serde(rename_all = "camelCase")] -pub enum TypeName { - AnyData, - Integer, - Bytes, - Literal, - Nullable, - Object, - Enum, - Tuple, - List, -} - -/// Represents a schema with a name, type, optional properties, and JSON representation. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Schema { - pub name: String, - pub type_name: TypeName, - pub properties: Option>, - pub json: String, -} - -/// Represents a reference to another schema, including an optional name and schema name. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Reference { - pub name: Option, - pub schema_name: String, -} - -/// Represents a validator with a name, optional datum and redeemer, and parameters. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Validator { - pub name: String, - pub datum: Option, - pub redeemer: Option, - pub parameters: Vec, -} - -impl Schema { - /// Creates a new any data schema. - /// - /// # Arguments - /// - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `AnyData`. - pub fn new_anydata(json: String) -> Self { - Self { - name: "AnyData".to_string(), - type_name: TypeName::AnyData, - properties: None, - json, - } - } - - /// Creates a new integer schema. - /// - /// # Arguments - /// - /// * `name` - The name of the schema. - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `Integer`. - pub fn new_integer(name: String, json: String) -> Self { - Self { - name, - type_name: TypeName::Integer, - properties: None, - json, - } - } - - /// Creates a new bytes schema. - /// - /// # Arguments - /// - /// * `name` - The name of the schema. - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `Bytes`. - pub fn new_bytes(name: String, json: String) -> Self { - Self { - name, - type_name: TypeName::Bytes, - properties: None, - json, - } - } - - /// Creates a new literal schema. - /// - /// # Arguments - /// - /// * `name` - The name of the schema. - /// * `value` - The literal value of the schema. - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `Literal`. - pub fn new_literal(name: String, value: String, json: String) -> Self { - Self { - name, - type_name: TypeName::Literal, - properties: Some(vec![Reference { - name: None, - schema_name: value, - }]), - json, - } - } - - /// Creates a new nullable schema. - /// - /// # Arguments - /// - /// * `name` - The name of the schema. - /// * `reference` - The reference schema name. - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `Nullable`. - pub fn new_nullable(name: String, reference: String, json: String) -> Self { - Self { - name, - type_name: TypeName::Nullable, - properties: Some(vec![Reference { - name: None, - schema_name: reference, - }]), - json, - } - } - - /// Creates a new object schema. - /// - /// # Arguments - /// - /// * `name` - The name of the schema. - /// * `properties` - The properties of the object schema. - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `Object`. - pub fn new_object(name: String, properties: Vec, json: String) -> Self { - Self { - name, - type_name: TypeName::Object, - properties: Some(properties), - json, - } - } - - /// Creates a new enum schema. - /// - /// # Arguments - /// - /// * `name` - The name of the schema. - /// * `schemas` - The schemas that make up the enum. - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `Enum`. - pub fn new_enum(name: String, schemas: &Vec, json: String) -> Self { - Self { - name, - type_name: TypeName::Enum, - properties: Some( - schemas - .iter() - .map(|s| Reference { - name: None, - schema_name: s.name.clone(), - }) - .collect(), - ), - json, - } - } - - /// Creates a new tuple schema. - /// - /// # Arguments - /// - /// * `name` - The name of the schema. - /// * `properties` - The properties of the tuple schema. - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `Tuple`. - pub fn new_tuple(name: String, properties: Vec, json: String) -> Self { - Self { - name, - type_name: TypeName::Tuple, - properties: Some(properties), - json, - } - } - - /// Creates a new list schema. - /// - /// # Arguments - /// - /// * `name` - The name of the schema. - /// * `reference` - The reference schema. - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `List`. - pub fn new_list(name: String, reference: Reference, json: String) -> Self { - Self { - name, - type_name: TypeName::List, - properties: Some(vec![reference]), - json, - } - } -} - -impl str::FromStr for TypeName { - type Err = (); - - /// Converts a string to a `TypeName`. - /// - /// # Arguments - /// - /// * `input` - The string representation of the type name. - /// - /// # Returns - /// - /// A `Result` containing the `TypeName` or an error. - fn from_str(input: &str) -> Result { - match input { - "AnyData" => Ok(TypeName::AnyData), - "Integer" => Ok(TypeName::Integer), - "Bytes" => Ok(TypeName::Bytes), - "Literal" => Ok(TypeName::Literal), - "Nullable" => Ok(TypeName::Nullable), - "Object" => Ok(TypeName::Object), - "Enum" => Ok(TypeName::Enum), - "Tuple" => Ok(TypeName::Tuple), - "List" => Ok(TypeName::List), - _ => Err(()), - } - } -} From cec9b3149bcb4d1feddda46f959d0b3fc7ce6734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Thu, 4 Dec 2025 14:10:34 -0300 Subject: [PATCH 03/27] chore: renaming, removing unnecessary deps & fixing warnings --- Cargo.lock | 3 +-- Cargo.toml | 8 ++++---- crates/{CIP-57 => cip-57}/Cargo.toml | 3 +-- crates/{CIP-57 => cip-57}/examples/plutus.json | 0 crates/{CIP-57 => cip-57}/src/lib.rs | 9 ++++----- 5 files changed, 10 insertions(+), 13 deletions(-) rename crates/{CIP-57 => cip-57}/Cargo.toml (93%) rename crates/{CIP-57 => cip-57}/examples/plutus.json (100%) rename crates/{CIP-57 => cip-57}/src/lib.rs (96%) diff --git a/Cargo.lock b/Cargo.lock index 3a334efe..d7c9129a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,10 +314,9 @@ dependencies = [ ] [[package]] -name = "cip57" +name = "cip-57" version = "0.13.0" dependencies = [ - "anyhow", "serde", "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index 6cecbe46..2e313c3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [workspace] resolver = "2" members = [ - "crates/tx3-cardano", - "crates/tx3-lang", - "crates/tx3-resolver", - "crates/CIP-57", + "crates/tx3-cardano", + "crates/tx3-lang", + "crates/tx3-resolver", + "crates/cip-57", ] [workspace.package] diff --git a/crates/CIP-57/Cargo.toml b/crates/cip-57/Cargo.toml similarity index 93% rename from crates/CIP-57/Cargo.toml rename to crates/cip-57/Cargo.toml index e7345fd9..d70aa572 100644 --- a/crates/CIP-57/Cargo.toml +++ b/crates/cip-57/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "cip57" +name = "cip-57" description = "CIP-57 compatibility (JSON parsing and serialization)" publish.workspace = true authors.workspace = true @@ -16,4 +16,3 @@ readme.workspace = true [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" -anyhow = "1" diff --git a/crates/CIP-57/examples/plutus.json b/crates/cip-57/examples/plutus.json similarity index 100% rename from crates/CIP-57/examples/plutus.json rename to crates/cip-57/examples/plutus.json diff --git a/crates/CIP-57/src/lib.rs b/crates/cip-57/src/lib.rs similarity index 96% rename from crates/CIP-57/src/lib.rs rename to crates/cip-57/src/lib.rs index 0e368fab..b1a4d97d 100644 --- a/crates/CIP-57/src/lib.rs +++ b/crates/cip-57/src/lib.rs @@ -3,9 +3,6 @@ use serde::{Deserialize, Serialize}; use serde_json::Number; use std::collections::BTreeMap; -use std::fs; -use std::path::PathBuf; - /// Represents a blueprint containing preamble, validators, and optional definitions. #[derive(Debug, Deserialize, Serialize, Clone)] pub struct Blueprint { @@ -160,14 +157,16 @@ pub struct Field { #[cfg(test)] mod tests { use super::*; + use std::fs; + use std::path::PathBuf; #[test] fn deserialize_plutus_json_into_blueprint() { let manifest = env!("CARGO_MANIFEST_DIR"); let path = PathBuf::from(manifest).join("examples").join("plutus.json"); - let json = fs::read_to_string(&path) - .expect(&format!("failed to read example file: {}", path.display())); + let failure_msg = format!("failed to read example file: {}", path.display()); + let json = fs::read_to_string(&path).expect(&failure_msg); let bp: Blueprint = serde_json::from_str(&json).expect("failed to deserialize blueprint"); From 82b199b4536e2d80fd5b8cc3587c73b3c1299c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Thu, 5 Feb 2026 23:52:15 -0300 Subject: [PATCH 04/27] feat: first draft at aiken imports --- Cargo.lock | 1 + crates/tx3-lang/Cargo.toml | 2 + crates/tx3-lang/src/ast.rs | 9 + crates/tx3-lang/src/facade.rs | 17 +- crates/tx3-lang/src/interop.rs | 255 ++++++++++++++++++ crates/tx3-lang/src/lib.rs | 1 + crates/tx3-lang/src/parsing.rs | 23 ++ crates/tx3-lang/src/tx3.pest | 6 +- .../tx3-lang/tests/fixtures/import_test.tx3 | 6 + crates/tx3-lang/tests/imports.rs | 80 ++++++ plutus_import_plan.md | 74 +++++ 11 files changed, 470 insertions(+), 4 deletions(-) create mode 100644 crates/tx3-lang/src/interop.rs create mode 100644 crates/tx3-lang/tests/fixtures/import_test.tx3 create mode 100644 crates/tx3-lang/tests/imports.rs create mode 100644 plutus_import_plan.md diff --git a/Cargo.lock b/Cargo.lock index c46551a3..aa1d25e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2358,6 +2358,7 @@ version = "0.14.3" dependencies = [ "assert-json-diff", "ciborium", + "cip-57", "hex", "miette", "paste", diff --git a/crates/tx3-lang/Cargo.toml b/crates/tx3-lang/Cargo.toml index 7aa4f807..b5d423dd 100644 --- a/crates/tx3-lang/Cargo.toml +++ b/crates/tx3-lang/Cargo.toml @@ -19,6 +19,8 @@ hex = { workspace = true } serde = { workspace = true } tx3-tir = { version = "0.14.3", path = "../tx3-tir" } +cip-57 = { path = "../cip-57" } +serde_json = "1.0.137" miette = { version = "7.4.0", features = ["fancy"] } pest = { version = "2.7.15", features = ["miette-error", "pretty-print"] } diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index 54578928..c34e43fe 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -173,6 +173,8 @@ impl AsRef for Identifier { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] pub struct Program { + #[serde(default)] + pub imports: Vec, pub env: Option, pub txs: Vec, pub types: Vec, @@ -187,6 +189,13 @@ pub struct Program { pub(crate) scope: Option>, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ImportDef { + pub path: StringLiteral, + pub alias: Option, + pub span: Span, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct EnvField { pub name: String, diff --git a/crates/tx3-lang/src/facade.rs b/crates/tx3-lang/src/facade.rs index 39f507d3..d56a62d0 100644 --- a/crates/tx3-lang/src/facade.rs +++ b/crates/tx3-lang/src/facade.rs @@ -1,8 +1,9 @@ use std::collections::{BTreeMap, HashMap}; +use std::path::PathBuf; use tx3_tir::reduce::{Apply, ArgValue}; -use crate::{analyzing, ast, lowering, parsing}; +use crate::{analyzing, ast, interop, lowering, parsing}; #[derive(Debug, thiserror::Error, miette::Diagnostic)] pub enum Error { @@ -16,6 +17,10 @@ pub enum Error { #[diagnostic(transparent)] Parsing(#[from] parsing::Error), + #[error("Import error: {0}")] + #[diagnostic(transparent)] + Interop(#[from] interop::Error), + #[error("Analyzing error")] Analyzing(#[from] analyzing::AnalyzeReport), @@ -27,6 +32,7 @@ pub type Code = String; pub struct Workspace { main: Option, + root: Option, ast: Option, analisis: Option, tir: HashMap, @@ -34,10 +40,13 @@ pub struct Workspace { impl Workspace { pub fn from_file(main: impl AsRef) -> Result { - let main = std::fs::read_to_string(main.as_ref())?; + let path = main.as_ref(); + let main = std::fs::read_to_string(path)?; + let root = path.parent().map(PathBuf::from); Ok(Self { main: Some(main), + root, ast: None, analisis: None, tir: HashMap::new(), @@ -47,6 +56,7 @@ impl Workspace { pub fn from_string(main: Code) -> Self { Self { main: Some(main), + root: None, ast: None, analisis: None, tir: HashMap::new(), @@ -63,7 +73,8 @@ impl Workspace { pub fn parse(&mut self) -> Result<(), Error> { let main = self.ensure_main()?; - let ast = parsing::parse_string(main)?; + let mut ast = parsing::parse_string(main)?; + interop::resolve_imports(&mut ast, self.root.as_deref())?; self.ast = Some(ast); Ok(()) } diff --git a/crates/tx3-lang/src/interop.rs b/crates/tx3-lang/src/interop.rs new file mode 100644 index 00000000..79e2c6da --- /dev/null +++ b/crates/tx3-lang/src/interop.rs @@ -0,0 +1,255 @@ +//! Resolves plutus.json (CIP-57) imports and maps blueprint definitions to tx3 types. + +use std::collections::HashSet; +use std::path::Path; + +use cip_57::{Blueprint, DataType, Definition, Definitions, Field, ReferencesArray, Schema}; + +use crate::ast::{Identifier, Program, RecordField, Span, Type, TypeDef, VariantCase}; + +#[derive(Debug, thiserror::Error, miette::Diagnostic)] +pub enum Error { + #[error("cannot resolve imports without a root path (use Workspace::from_file instead of from_string)")] + #[diagnostic(code(tx3::interop::missing_root))] + MissingRoot, + + #[error("I/O error reading import file: {0}")] + #[diagnostic(code(tx3::interop::io))] + Io(#[from] std::io::Error), + + #[error("invalid JSON in plutus file: {0}")] + #[diagnostic(code(tx3::interop::json))] + Json(#[from] serde_json::Error), + + #[error("duplicate type name: {name}")] + #[diagnostic(code(tx3::interop::duplicate_type))] + DuplicateType { + name: String, + + #[source_code] + src: Option, + + #[label("type already defined here or from another import")] + span: Span, + }, + + #[error("plutus schema error: {message}")] + #[diagnostic(code(tx3::interop::schema))] + Schema { message: String }, +} + +impl Error { + fn duplicate_type(name: String, span: Span) -> Self { + Error::DuplicateType { + name, + src: None, + span, + } + } +} + +/// Resolves `#/definitions/cardano~1address~1Address` to definition key `cardano/address/Address`. +/// JSON pointer uses ~1 for / and ~0 for ~. +fn ref_to_key_owned(r: &str) -> String { + let prefix = "#/definitions/"; + let s = r + .strip_prefix(prefix) + .unwrap_or(r) + .replace("~1", "/") + .replace("~0", "~"); + s +} + +/// Normalize definition key to a single identifier: replace `/` and `$` with `_`. +fn key_to_normalized_name(key: &str) -> String { + key.replace('/', "_").replace('$', "_") +} + +/// Full type name for an imported definition: with alias use `Alias_NormalizedKey`, else `NormalizedKey`. +fn import_type_name(key: &str, alias: Option<&str>) -> String { + let base = key_to_normalized_name(key); + match alias { + Some(a) => format!("{}_{}", a, base), + None => base, + } +} + +/// Resolve a $ref to a tx3 Type using definitions and the current import alias. +fn resolve_ref_to_type( + ref_str: &str, + definitions: &Definitions, + alias: Option<&str>, +) -> Result { + let key = ref_to_key_owned(ref_str); + let def = definitions + .inner + .get(&key) + .ok_or_else(|| Error::Schema { + message: format!("definition not found: {}", key), + })?; + + if let Some(dt) = &def.data_type { + match dt { + DataType::Integer => return Ok(Type::Int), + DataType::Bytes => return Ok(Type::Bytes), + DataType::List => { + let inner = match &def.items { + Some(ReferencesArray::Single(r)) => { + r.reference.as_ref().map(|s| resolve_ref_to_type(s, definitions, alias)) + } + Some(ReferencesArray::Array(arr)) => arr.first().and_then(|r| { + r.reference.as_ref().map(|s| resolve_ref_to_type(s, definitions, alias)) + }), + None => None, + }; + let inner = inner.ok_or_else(|| Error::Schema { + message: "list without items".to_string(), + })?; + return Ok(Type::List(Box::new(inner?))); + } + DataType::Map => { + let k = def + .keys + .as_ref() + .and_then(|r| r.reference.as_ref()) + .ok_or_else(|| Error::Schema { + message: "map without keys".to_string(), + })?; + let v = def + .values + .as_ref() + .and_then(|r| r.reference.as_ref()) + .ok_or_else(|| Error::Schema { + message: "map without values".to_string(), + })?; + let key_ty = resolve_ref_to_type(k, definitions, alias)?; + let val_ty = resolve_ref_to_type(v, definitions, alias)?; + return Ok(Type::Map(Box::new(key_ty), Box::new(val_ty))); + } + DataType::Constructor => {} + } + } + + if def.any_of.is_some() { + return Ok(Type::Custom(Identifier::new(import_type_name(&key, alias)))); + } + + if def.data_type == Some(DataType::Integer) { + return Ok(Type::Int); + } + if def.data_type == Some(DataType::Bytes) { + return Ok(Type::Bytes); + } + + Ok(Type::Custom(Identifier::new(import_type_name(&key, alias)))) +} + +fn field_to_record_field(f: &Field, definitions: &Definitions, alias: Option<&str>) -> Result { + let name = f + .title + .as_deref() + .unwrap_or("field") + .to_string(); + let r#type = resolve_ref_to_type(&f.reference, definitions, alias)?; + Ok(RecordField { + name: Identifier::new(name), + r#type, + span: Span::DUMMY, + }) +} + +fn schema_to_variant_case(schema: &Schema, definitions: &Definitions, alias: Option<&str>) -> Result { + let name = schema + .title + .as_deref() + .unwrap_or("Variant") + .to_string(); + let fields: Vec = schema + .fields + .iter() + .map(|f| field_to_record_field(f, definitions, alias)) + .collect::, _>>()?; + Ok(VariantCase { + name: Identifier::new(name), + fields, + span: Span::DUMMY, + }) +} + +/// Convert a CIP-57 Definition to a tx3 TypeDef if it is a product or sum type. Primitives and raw List/Map return None. +fn definition_to_type_def( + key: &str, + def: &Definition, + definitions: &Definitions, + alias: Option<&str>, +) -> Result, Error> { + if let Some(dt) = &def.data_type { + match dt { + DataType::Integer | DataType::Bytes => return Ok(None), + DataType::List | DataType::Map => return Ok(None), + DataType::Constructor => {} + } + } + + let cases = if let Some(any_of) = &def.any_of { + any_of + .iter() + .map(|s| schema_to_variant_case(s, definitions, alias)) + .collect::, _>>()? + } else { + return Ok(None); + }; + + if cases.is_empty() { + return Ok(None); + } + + let type_name = import_type_name(key, alias); + Ok(Some(TypeDef { + name: Identifier::new(type_name), + cases, + span: Span::DUMMY, + })) +} + +/// Resolve all plutus.json imports: read files, parse blueprints, map definitions to TypeDefs, append to program.types with collision checks. +pub fn resolve_imports(program: &mut Program, root: Option<&Path>) -> Result<(), Error> { + if program.imports.is_empty() { + return Ok(()); + } + let root = root.ok_or(Error::MissingRoot)?; + + let existing_names: HashSet = program + .types + .iter() + .map(|t| t.name.value.clone()) + .chain(program.aliases.iter().map(|a| a.name.value.clone())) + .collect(); + let mut added_names = HashSet::::new(); + + for import in &program.imports { + let path = root.join(import.path.value.as_str()); + let json = std::fs::read_to_string(&path)?; + let blueprint: Blueprint = serde_json::from_str(&json)?; + + let definitions = match &blueprint.definitions { + Some(d) => d, + None => continue, + }; + + let alias = import.alias.as_ref().map(|a| a.value.as_str()); + + for (key, def) in &definitions.inner { + if let Some(type_def) = definition_to_type_def(key, def, definitions, alias)? { + let name = type_def.name.value.clone(); + if existing_names.contains(&name) || added_names.contains(&name) { + return Err(Error::duplicate_type(name, import.span.clone())); + } + added_names.insert(name.clone()); + program.types.push(type_def); + } + } + } + + Ok(()) +} diff --git a/crates/tx3-lang/src/lib.rs b/crates/tx3-lang/src/lib.rs index 9d044437..3fbd5699 100644 --- a/crates/tx3-lang/src/lib.rs +++ b/crates/tx3-lang/src/lib.rs @@ -26,6 +26,7 @@ pub mod analyzing; pub mod ast; +pub mod interop; pub mod lowering; pub mod parsing; diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 2b34fc1c..9244ff32 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -87,6 +87,7 @@ impl AstNode for Program { let inner = pair.into_inner(); let mut program = Self { + imports: Vec::new(), env: None, txs: Vec::new(), assets: Vec::new(), @@ -100,6 +101,7 @@ impl AstNode for Program { for pair in inner { match pair.as_rule() { + Rule::import_def => program.imports.push(ImportDef::parse(pair)?), Rule::env_def => program.env = Some(EnvDef::parse(pair)?), Rule::tx_def => program.txs.push(TxDef::parse(pair)?), Rule::asset_def => program.assets.push(AssetDef::parse(pair)?), @@ -121,6 +123,26 @@ impl AstNode for Program { } } +impl AstNode for ImportDef { + const RULE: Rule = Rule::import_def; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + let mut inner = pair.into_inner(); + let path = StringLiteral::parse(inner.next().unwrap())?; + let alias = inner.next().map(Identifier::parse).transpose()?; + Ok(ImportDef { + path, + alias, + span, + }) + } + + fn span(&self) -> &Span { + &self.span + } +} + impl AstNode for EnvField { const RULE: Rule = Rule::env_field; @@ -2587,6 +2609,7 @@ mod tests { "basic", "party Abc; tx my_tx() {}", Program { + imports: vec![], parties: vec![PartyDef { name: Identifier::new("Abc"), span: Span::DUMMY, diff --git a/crates/tx3-lang/src/tx3.pest b/crates/tx3-lang/src/tx3.pest index a01471ff..18ca5af4 100644 --- a/crates/tx3-lang/src/tx3.pest +++ b/crates/tx3-lang/src/tx3.pest @@ -439,6 +439,10 @@ tx_body_block = _{ validity_block } +import_def = { + "cardano" ~ "::" ~ "import" ~ string ~ ("as" ~ identifier)? ~ ";" +} + env_field = { identifier ~ ":" ~ type } env_def = { @@ -453,6 +457,6 @@ tx_def = { // Program program = { SOI ~ - (env_def | asset_def | party_def | policy_def | type_def | tx_def)* ~ + (import_def | env_def | asset_def | party_def | policy_def | type_def | tx_def)* ~ EOI } diff --git a/crates/tx3-lang/tests/fixtures/import_test.tx3 b/crates/tx3-lang/tests/fixtures/import_test.tx3 new file mode 100644 index 00000000..fd4aeb85 --- /dev/null +++ b/crates/tx3-lang/tests/fixtures/import_test.tx3 @@ -0,0 +1,6 @@ +cardano::import "../../../cip-57/examples/plutus.json" as types; + +party Sender; +party Receiver; + +tx dummy() {} diff --git a/crates/tx3-lang/tests/imports.rs b/crates/tx3-lang/tests/imports.rs new file mode 100644 index 00000000..8186173b --- /dev/null +++ b/crates/tx3-lang/tests/imports.rs @@ -0,0 +1,80 @@ +//! Tests for plutus.json import support. + +use std::path::Path; + +use tx3_lang::Workspace; + +#[test] +fn import_with_alias_adds_types() { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let path = Path::new(manifest_dir).join("tests/fixtures/import_test.tx3"); + let mut workspace = Workspace::from_file(&path).unwrap(); + workspace.parse().unwrap(); + let ast = workspace.ast().unwrap(); + + let type_names: Vec = ast.types.iter().map(|t| t.name.value.clone()).collect(); + assert!( + type_names.iter().any(|n| n.starts_with("types_")), + "expected at least one type prefixed with 'types_', got: {:?}", + type_names + ); +} + +#[test] +fn import_without_root_errors() { + let src = r#" +cardano::import "some/file.json"; +party X; +tx dummy() {} +"#; + let mut workspace = Workspace::from_string(src.to_string()); + let res = workspace.parse(); + assert!(res.is_err(), "expected error when importing without root"); + let err = res.unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("root") || msg.contains("import"), + "expected error about root or import, got: {}", + msg + ); +} + +#[test] +fn duplicate_type_name_from_imports_errors() { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let path = Path::new(manifest_dir).join("tests/fixtures/import_test.tx3"); + let content = std::fs::read_to_string(&path).unwrap(); + let content_twice = format!( + "{}\ncardano::import \"../../../cip-57/examples/plutus.json\" as types;\n{}", + content, + "tx dummy2() {}" + ); + let fixtures = Path::new(manifest_dir).join("tests/fixtures"); + let temp_tx3 = fixtures.join("temp_duplicate_import.tx3"); + std::fs::write(&temp_tx3, content_twice).unwrap(); + let res = Workspace::from_file(&temp_tx3).and_then(|mut w| w.parse()); + let _ = std::fs::remove_file(&temp_tx3); + assert!(res.is_err(), "expected error for duplicate type names from two imports"); + let err = res.unwrap_err(); + assert!( + err.to_string().contains("duplicate") || err.to_string().contains("type"), + "expected duplicate/type error, got: {}", + err + ); +} + +#[test] +fn invalid_import_path_errors() { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let fixtures = Path::new(manifest_dir).join("tests/fixtures"); + let temp_tx3 = fixtures.join("temp_nonexistent_import.tx3"); + let content = r#" +cardano::import "nonexistent/plutus.json" as types; +party X; +tx dummy() {} +"#; + std::fs::write(&temp_tx3, content).unwrap(); + let res = Workspace::from_file(&temp_tx3).and_then(|mut w| w.parse()); + let _ = std::fs::remove_file(&temp_tx3); + assert!(res.is_err(), "expected error for missing import file"); +} diff --git a/plutus_import_plan.md b/plutus_import_plan.md new file mode 100644 index 00000000..e4ba54d0 --- /dev/null +++ b/plutus_import_plan.md @@ -0,0 +1,74 @@ +# Implementation Plan - Support Plutus JSON Imports + +The goal is to allow importing types from `plutus.json` files (CIP-57) into the Tx3 DSL using the `cardano::import` statement. + +## User Review Approved + +> [!NOTE] +> Coupling `tx3-lang` to the `cip-57` implementation is confirmed as acceptable. +> +> Types imported from `plutus.json` will be mapped to `tx3` `TypeDef` and `VariantCase`. +> - Product types (single constructor) -> `TypeDef` with one `VariantCase`. +> - Sum types (anyOf) -> `TypeDef` with multiple `VariantCase`s. +> - Primitive types map to `Int`, `Bytes`, `List`, `Map`. +> +> Naming and Collision Handling: +> - If `as Alias` is provided, imported types are prefixed: `Alias_TypeName`. +> - If no alias is provided, types are imported as is. +> - **CRITICAL: Any naming collision (with existing types or other imports) MUST throw an error.** + +## Proposed Changes + +### `crates/tx3-lang` + +#### [MODIFY] `Cargo.toml` +- Add `cip-57` to `[dependencies]`. + +#### [MODIFY] `tx3.pest` +- Add `import_def` rule: `import_def = { "cardano" ~ "::" ~ "import" ~ string ~ ("as" ~ identifier)? ~ ";" }` +- Add `import_def` to `program` rule. + +#### [MODIFY] `ast.rs` +- Add `ImportDef` struct. +- Add `imports: Vec` to `Program` struct. + +#### [MODIFY] `parsing.rs` +- Implement `AstNode` for `ImportDef`. +- Update `Program::parse` to parse imports. + +#### [NEW] `interop.rs` +- Create `resolve_imports(program: &mut Program, root: &Path) -> Result<(), Error>`. +- Logic: + - Iterate `program.imports`. + - Resolve path relative to `root`. + - Read file (IO). + - Parse `cip_57::Blueprint`. + - Iterate `blueprint.definitions`. + - Map `Definition` to `TypeDef` (flattening `Schema`s). + - Apply alias prefix if present. + - Append to `program.types`. + - **Verify no collisions** with existing types or other imports. + +#### [MODIFY] `facade.rs` +- Update `Workspace` struct to store `root: Option`. +- Update `Workspace::from_file` to store the path. +- Update `Workspace::parse` (or a helper) to call `interop::resolve_imports` after parsing. + +#### [MODIFY] `lib.rs` +- Export `interop` module. + +## Verification Plan + +### Automated Tests +- Create `crates/tx3-lang/tests/imports.rs`. +- Test cases: + 1. Successful import with and without alias. + 2. Import with alias prefixing. + 3. Collision detection between two imports. + 4. Collision detection between import and local type. + 5. Invalid JSON file handling. + +### Manual Verification +- Use `bin/tx3c` to build a sample project. + - Create `examples/import_test.tx3`. + - Run `cargo run -p tx3-cli -- build examples/import_test.tx3`. From 84eb7d4d6aaefd67c140969b98c22693c2a05cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 00:46:58 -0300 Subject: [PATCH 05/27] refactor: decouple loading with from parser --- crates/tx3-lang/src/facade.rs | 3 +- crates/tx3-lang/src/interop.rs | 161 +++++++++++++++++++++++++------ crates/tx3-lang/src/parsing.rs | 6 +- crates/tx3-lang/tests/imports.rs | 8 +- 4 files changed, 138 insertions(+), 40 deletions(-) diff --git a/crates/tx3-lang/src/facade.rs b/crates/tx3-lang/src/facade.rs index d56a62d0..195d8b7d 100644 --- a/crates/tx3-lang/src/facade.rs +++ b/crates/tx3-lang/src/facade.rs @@ -74,7 +74,8 @@ impl Workspace { pub fn parse(&mut self) -> Result<(), Error> { let main = self.ensure_main()?; let mut ast = parsing::parse_string(main)?; - interop::resolve_imports(&mut ast, self.root.as_deref())?; + let loader = self.root.clone().map(interop::FsLoader::new); + interop::resolve_imports(&mut ast, loader.as_ref())?; self.ast = Some(ast); Ok(()) } diff --git a/crates/tx3-lang/src/interop.rs b/crates/tx3-lang/src/interop.rs index 79e2c6da..0e5badb1 100644 --- a/crates/tx3-lang/src/interop.rs +++ b/crates/tx3-lang/src/interop.rs @@ -1,7 +1,7 @@ //! Resolves plutus.json (CIP-57) imports and maps blueprint definitions to tx3 types. use std::collections::HashSet; -use std::path::Path; +use std::path::PathBuf; use cip_57::{Blueprint, DataType, Definition, Definitions, Field, ReferencesArray, Schema}; @@ -48,6 +48,27 @@ impl Error { } } +pub trait ImportLoader { + fn load_source(&self, path: &str) -> std::io::Result; +} + +pub struct FsLoader { + root: PathBuf, +} + +impl FsLoader { + pub fn new(root: impl Into) -> Self { + Self { root: root.into() } + } +} + +impl ImportLoader for FsLoader { + fn load_source(&self, path: &str) -> std::io::Result { + let full_path = self.root.join(path); + std::fs::read_to_string(full_path) + } +} + /// Resolves `#/definitions/cardano~1address~1Address` to definition key `cardano/address/Address`. /// JSON pointer uses ~1 for / and ~0 for ~. fn ref_to_key_owned(r: &str) -> String { @@ -65,7 +86,6 @@ fn key_to_normalized_name(key: &str) -> String { key.replace('/', "_").replace('$', "_") } -/// Full type name for an imported definition: with alias use `Alias_NormalizedKey`, else `NormalizedKey`. fn import_type_name(key: &str, alias: Option<&str>) -> String { let base = key_to_normalized_name(key); match alias { @@ -74,19 +94,15 @@ fn import_type_name(key: &str, alias: Option<&str>) -> String { } } -/// Resolve a $ref to a tx3 Type using definitions and the current import alias. fn resolve_ref_to_type( ref_str: &str, definitions: &Definitions, alias: Option<&str>, ) -> Result { let key = ref_to_key_owned(ref_str); - let def = definitions - .inner - .get(&key) - .ok_or_else(|| Error::Schema { - message: format!("definition not found: {}", key), - })?; + let def = definitions.inner.get(&key).ok_or_else(|| Error::Schema { + message: format!("definition not found: {}", key), + })?; if let Some(dt) = &def.data_type { match dt { @@ -94,11 +110,14 @@ fn resolve_ref_to_type( DataType::Bytes => return Ok(Type::Bytes), DataType::List => { let inner = match &def.items { - Some(ReferencesArray::Single(r)) => { - r.reference.as_ref().map(|s| resolve_ref_to_type(s, definitions, alias)) - } + Some(ReferencesArray::Single(r)) => r + .reference + .as_ref() + .map(|s| resolve_ref_to_type(s, definitions, alias)), Some(ReferencesArray::Array(arr)) => arr.first().and_then(|r| { - r.reference.as_ref().map(|s| resolve_ref_to_type(s, definitions, alias)) + r.reference + .as_ref() + .map(|s| resolve_ref_to_type(s, definitions, alias)) }), None => None, }; @@ -144,12 +163,12 @@ fn resolve_ref_to_type( Ok(Type::Custom(Identifier::new(import_type_name(&key, alias)))) } -fn field_to_record_field(f: &Field, definitions: &Definitions, alias: Option<&str>) -> Result { - let name = f - .title - .as_deref() - .unwrap_or("field") - .to_string(); +fn field_to_record_field( + f: &Field, + definitions: &Definitions, + alias: Option<&str>, +) -> Result { + let name = f.title.as_deref().unwrap_or("field").to_string(); let r#type = resolve_ref_to_type(&f.reference, definitions, alias)?; Ok(RecordField { name: Identifier::new(name), @@ -158,12 +177,12 @@ fn field_to_record_field(f: &Field, definitions: &Definitions, alias: Option<&st }) } -fn schema_to_variant_case(schema: &Schema, definitions: &Definitions, alias: Option<&str>) -> Result { - let name = schema - .title - .as_deref() - .unwrap_or("Variant") - .to_string(); +fn schema_to_variant_case( + schema: &Schema, + definitions: &Definitions, + alias: Option<&str>, +) -> Result { + let name = schema.title.as_deref().unwrap_or("Variant").to_string(); let fields: Vec = schema .fields .iter() @@ -176,7 +195,6 @@ fn schema_to_variant_case(schema: &Schema, definitions: &Definitions, alias: Opt }) } -/// Convert a CIP-57 Definition to a tx3 TypeDef if it is a product or sum type. Primitives and raw List/Map return None. fn definition_to_type_def( key: &str, def: &Definition, @@ -212,12 +230,14 @@ fn definition_to_type_def( })) } -/// Resolve all plutus.json imports: read files, parse blueprints, map definitions to TypeDefs, append to program.types with collision checks. -pub fn resolve_imports(program: &mut Program, root: Option<&Path>) -> Result<(), Error> { +pub fn resolve_imports( + program: &mut Program, + loader: Option<&impl ImportLoader>, +) -> Result<(), Error> { if program.imports.is_empty() { return Ok(()); } - let root = root.ok_or(Error::MissingRoot)?; + let loader = loader.ok_or(Error::MissingRoot)?; let existing_names: HashSet = program .types @@ -228,8 +248,7 @@ pub fn resolve_imports(program: &mut Program, root: Option<&Path>) -> Result<(), let mut added_names = HashSet::::new(); for import in &program.imports { - let path = root.join(import.path.value.as_str()); - let json = std::fs::read_to_string(&path)?; + let json = loader.load_source(import.path.value.as_str())?; let blueprint: Blueprint = serde_json::from_str(&json)?; let definitions = match &blueprint.definitions { @@ -253,3 +272,83 @@ pub fn resolve_imports(program: &mut Program, root: Option<&Path>) -> Result<(), Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{Identifier, ImportDef, Program, Span, StringLiteral}; + + #[derive(Default, Clone)] + pub struct InMemoryLoader { + map: std::collections::HashMap, + } + + impl InMemoryLoader { + pub fn new() -> Self { + Self { + map: std::collections::HashMap::new(), + } + } + + pub fn add(&mut self, path: impl Into, contents: impl Into) -> &mut Self { + self.map.insert(path.into(), contents.into()); + self + } + } + + impl ImportLoader for InMemoryLoader { + fn load_source(&self, path: &str) -> std::io::Result { + self.map.get(path).cloned().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("import not found: {}", path), + ) + }) + } + } + + #[test] + fn resolve_imports_with_in_memory_loader() { + let mut program = Program { + imports: vec![ImportDef { + path: StringLiteral::new("test.json"), + alias: Some(Identifier::new("types")), + span: Span::DUMMY, + }], + env: None, + txs: vec![], + types: vec![], + aliases: vec![], + assets: vec![], + parties: vec![], + policies: vec![], + span: Span::DUMMY, + scope: None, + }; + + let json = r#"{ + "preamble": { "title": "test", "version": "0", "plutusVersion": "v3" }, + "validators": [], + "definitions": { + "Bool": { + "title": "Bool", + "anyOf": [ + { "title": "False", "dataType": "constructor", "index": 0, "fields": [] }, + { "title": "True", "dataType": "constructor", "index": 1, "fields": [] } + ] + } + } + }"#; + + let mut loader = InMemoryLoader::new(); + loader.add("test.json", json); + resolve_imports(&mut program, Some(&loader)).unwrap(); + + let type_names: Vec = program.types.iter().map(|t| t.name.value.clone()).collect(); + assert!( + type_names.contains(&"types_Bool".to_string()), + "expected types_Bool in program.types, got: {:?}", + type_names + ); + } +} diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 9244ff32..3b9c4276 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -131,11 +131,7 @@ impl AstNode for ImportDef { let mut inner = pair.into_inner(); let path = StringLiteral::parse(inner.next().unwrap())?; let alias = inner.next().map(Identifier::parse).transpose()?; - Ok(ImportDef { - path, - alias, - span, - }) + Ok(ImportDef { path, alias, span }) } fn span(&self) -> &Span { diff --git a/crates/tx3-lang/tests/imports.rs b/crates/tx3-lang/tests/imports.rs index 8186173b..6e4bb05d 100644 --- a/crates/tx3-lang/tests/imports.rs +++ b/crates/tx3-lang/tests/imports.rs @@ -46,15 +46,17 @@ fn duplicate_type_name_from_imports_errors() { let content = std::fs::read_to_string(&path).unwrap(); let content_twice = format!( "{}\ncardano::import \"../../../cip-57/examples/plutus.json\" as types;\n{}", - content, - "tx dummy2() {}" + content, "tx dummy2() {}" ); let fixtures = Path::new(manifest_dir).join("tests/fixtures"); let temp_tx3 = fixtures.join("temp_duplicate_import.tx3"); std::fs::write(&temp_tx3, content_twice).unwrap(); let res = Workspace::from_file(&temp_tx3).and_then(|mut w| w.parse()); let _ = std::fs::remove_file(&temp_tx3); - assert!(res.is_err(), "expected error for duplicate type names from two imports"); + assert!( + res.is_err(), + "expected error for duplicate type names from two imports" + ); let err = res.unwrap_err(); assert!( err.to_string().contains("duplicate") || err.to_string().contains("type"), From 5ec5bd3aec7ea7d4f4d9f2715ab8567dba06a8fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 01:16:08 -0300 Subject: [PATCH 06/27] fix: cip57 purpose is oneOf object instead of array --- crates/cip-57/src/lib.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/cip-57/src/lib.rs b/crates/cip-57/src/lib.rs index b1a4d97d..d46fc5f8 100644 --- a/crates/cip-57/src/lib.rs +++ b/crates/cip-57/src/lib.rs @@ -52,12 +52,19 @@ pub struct Argument { pub schema: Reference, } -/// Represents a purpose array which can be either a single purpose or an array of purposes. +/// Represents a purpose which can be either a single purpose or an object with oneOf. #[derive(Debug, Deserialize, Serialize, Clone)] #[serde(untagged)] pub enum PurposeArray { Single(Purpose), - Array(Vec), + OneOf(PurposeOneOf), +} + +/// Represents a purpose object with a oneOf field containing an array of purposes. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PurposeOneOf { + pub one_of: Vec, } /// Represents the purpose of an argument, which can be spend, mint, withdraw, or publish. From a2fb44fc656af4b4b20088fd5fbe50c8949dc043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 01:19:17 -0300 Subject: [PATCH 07/27] chore: remove import plan --- plutus_import_plan.md | 74 ------------------------------------------- 1 file changed, 74 deletions(-) delete mode 100644 plutus_import_plan.md diff --git a/plutus_import_plan.md b/plutus_import_plan.md deleted file mode 100644 index e4ba54d0..00000000 --- a/plutus_import_plan.md +++ /dev/null @@ -1,74 +0,0 @@ -# Implementation Plan - Support Plutus JSON Imports - -The goal is to allow importing types from `plutus.json` files (CIP-57) into the Tx3 DSL using the `cardano::import` statement. - -## User Review Approved - -> [!NOTE] -> Coupling `tx3-lang` to the `cip-57` implementation is confirmed as acceptable. -> -> Types imported from `plutus.json` will be mapped to `tx3` `TypeDef` and `VariantCase`. -> - Product types (single constructor) -> `TypeDef` with one `VariantCase`. -> - Sum types (anyOf) -> `TypeDef` with multiple `VariantCase`s. -> - Primitive types map to `Int`, `Bytes`, `List`, `Map`. -> -> Naming and Collision Handling: -> - If `as Alias` is provided, imported types are prefixed: `Alias_TypeName`. -> - If no alias is provided, types are imported as is. -> - **CRITICAL: Any naming collision (with existing types or other imports) MUST throw an error.** - -## Proposed Changes - -### `crates/tx3-lang` - -#### [MODIFY] `Cargo.toml` -- Add `cip-57` to `[dependencies]`. - -#### [MODIFY] `tx3.pest` -- Add `import_def` rule: `import_def = { "cardano" ~ "::" ~ "import" ~ string ~ ("as" ~ identifier)? ~ ";" }` -- Add `import_def` to `program` rule. - -#### [MODIFY] `ast.rs` -- Add `ImportDef` struct. -- Add `imports: Vec` to `Program` struct. - -#### [MODIFY] `parsing.rs` -- Implement `AstNode` for `ImportDef`. -- Update `Program::parse` to parse imports. - -#### [NEW] `interop.rs` -- Create `resolve_imports(program: &mut Program, root: &Path) -> Result<(), Error>`. -- Logic: - - Iterate `program.imports`. - - Resolve path relative to `root`. - - Read file (IO). - - Parse `cip_57::Blueprint`. - - Iterate `blueprint.definitions`. - - Map `Definition` to `TypeDef` (flattening `Schema`s). - - Apply alias prefix if present. - - Append to `program.types`. - - **Verify no collisions** with existing types or other imports. - -#### [MODIFY] `facade.rs` -- Update `Workspace` struct to store `root: Option`. -- Update `Workspace::from_file` to store the path. -- Update `Workspace::parse` (or a helper) to call `interop::resolve_imports` after parsing. - -#### [MODIFY] `lib.rs` -- Export `interop` module. - -## Verification Plan - -### Automated Tests -- Create `crates/tx3-lang/tests/imports.rs`. -- Test cases: - 1. Successful import with and without alias. - 2. Import with alias prefixing. - 3. Collision detection between two imports. - 4. Collision detection between import and local type. - 5. Invalid JSON file handling. - -### Manual Verification -- Use `bin/tx3c` to build a sample project. - - Create `examples/import_test.tx3`. - - Run `cargo run -p tx3-cli -- build examples/import_test.tx3`. From 7174b1491557945871e6768963137a8a9990cf9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 01:32:19 -0300 Subject: [PATCH 08/27] fix: remove redundant branches of code --- crates/tx3-lang/src/interop.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crates/tx3-lang/src/interop.rs b/crates/tx3-lang/src/interop.rs index 0e5badb1..8a2c617d 100644 --- a/crates/tx3-lang/src/interop.rs +++ b/crates/tx3-lang/src/interop.rs @@ -153,13 +153,6 @@ fn resolve_ref_to_type( return Ok(Type::Custom(Identifier::new(import_type_name(&key, alias)))); } - if def.data_type == Some(DataType::Integer) { - return Ok(Type::Int); - } - if def.data_type == Some(DataType::Bytes) { - return Ok(Type::Bytes); - } - Ok(Type::Custom(Identifier::new(import_type_name(&key, alias)))) } From c2b4dca5a52148a70007373e22073628031f1406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 01:33:40 -0300 Subject: [PATCH 09/27] fix: some warnings are gone --- crates/tx3-lang/src/cardano.rs | 2 +- crates/tx3-lang/src/parsing.rs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/tx3-lang/src/cardano.rs b/crates/tx3-lang/src/cardano.rs index 036ab79f..ac11eb39 100644 --- a/crates/tx3-lang/src/cardano.rs +++ b/crates/tx3-lang/src/cardano.rs @@ -847,7 +847,7 @@ mod tests { use super::*; use crate::{ analyzing::analyze, - ast::{self, *}, + ast::*, }; use pest::Parser; diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 3b9c4276..b92190fe 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -12,10 +12,7 @@ use pest::{ }; use pest_derive::Parser; -use crate::{ - ast::*, - cardano::{PlutusWitnessBlock, PlutusWitnessField}, -}; +use crate::ast::*; #[derive(Parser)] #[grammar = "tx3.pest"] pub(crate) struct Tx3Grammar; From c20187aedabf6c9b2428753663e8ff84455a2d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 11:33:46 -0300 Subject: [PATCH 10/27] refactor: rename `interop` module to `importing` --- crates/tx3-lang/src/facade.rs | 8 ++++---- crates/tx3-lang/src/{interop.rs => importing.rs} | 10 +++++----- crates/tx3-lang/src/lib.rs | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) rename crates/tx3-lang/src/{interop.rs => importing.rs} (97%) diff --git a/crates/tx3-lang/src/facade.rs b/crates/tx3-lang/src/facade.rs index 195d8b7d..13d34eb7 100644 --- a/crates/tx3-lang/src/facade.rs +++ b/crates/tx3-lang/src/facade.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use tx3_tir::reduce::{Apply, ArgValue}; -use crate::{analyzing, ast, interop, lowering, parsing}; +use crate::{analyzing, ast, importing, lowering, parsing}; #[derive(Debug, thiserror::Error, miette::Diagnostic)] pub enum Error { @@ -19,7 +19,7 @@ pub enum Error { #[error("Import error: {0}")] #[diagnostic(transparent)] - Interop(#[from] interop::Error), + Importing(#[from] importing::Error), #[error("Analyzing error")] Analyzing(#[from] analyzing::AnalyzeReport), @@ -74,8 +74,8 @@ impl Workspace { pub fn parse(&mut self) -> Result<(), Error> { let main = self.ensure_main()?; let mut ast = parsing::parse_string(main)?; - let loader = self.root.clone().map(interop::FsLoader::new); - interop::resolve_imports(&mut ast, loader.as_ref())?; + let loader = self.root.clone().map(importing::FsLoader::new); + importing::resolve_imports(&mut ast, loader.as_ref())?; self.ast = Some(ast); Ok(()) } diff --git a/crates/tx3-lang/src/interop.rs b/crates/tx3-lang/src/importing.rs similarity index 97% rename from crates/tx3-lang/src/interop.rs rename to crates/tx3-lang/src/importing.rs index 8a2c617d..5abfb6a1 100644 --- a/crates/tx3-lang/src/interop.rs +++ b/crates/tx3-lang/src/importing.rs @@ -10,19 +10,19 @@ use crate::ast::{Identifier, Program, RecordField, Span, Type, TypeDef, VariantC #[derive(Debug, thiserror::Error, miette::Diagnostic)] pub enum Error { #[error("cannot resolve imports without a root path (use Workspace::from_file instead of from_string)")] - #[diagnostic(code(tx3::interop::missing_root))] + #[diagnostic(code(tx3::importing::missing_root))] MissingRoot, #[error("I/O error reading import file: {0}")] - #[diagnostic(code(tx3::interop::io))] + #[diagnostic(code(tx3::importing::io))] Io(#[from] std::io::Error), #[error("invalid JSON in plutus file: {0}")] - #[diagnostic(code(tx3::interop::json))] + #[diagnostic(code(tx3::importing::json))] Json(#[from] serde_json::Error), #[error("duplicate type name: {name}")] - #[diagnostic(code(tx3::interop::duplicate_type))] + #[diagnostic(code(tx3::importing::duplicate_type))] DuplicateType { name: String, @@ -34,7 +34,7 @@ pub enum Error { }, #[error("plutus schema error: {message}")] - #[diagnostic(code(tx3::interop::schema))] + #[diagnostic(code(tx3::importing::schema))] Schema { message: String }, } diff --git a/crates/tx3-lang/src/lib.rs b/crates/tx3-lang/src/lib.rs index 3fbd5699..ec6686c3 100644 --- a/crates/tx3-lang/src/lib.rs +++ b/crates/tx3-lang/src/lib.rs @@ -26,7 +26,7 @@ pub mod analyzing; pub mod ast; -pub mod interop; +pub mod importing; pub mod lowering; pub mod parsing; From 2f44dd83e455017928804c4a11167a73f9c3f661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 11:43:41 -0300 Subject: [PATCH 11/27] chore: remove cardano prefix on imports --- crates/tx3-lang/src/tx3.pest | 2 +- crates/tx3-lang/tests/fixtures/import_test.tx3 | 2 +- crates/tx3-lang/tests/imports.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/tx3-lang/src/tx3.pest b/crates/tx3-lang/src/tx3.pest index 18ca5af4..a1fe6987 100644 --- a/crates/tx3-lang/src/tx3.pest +++ b/crates/tx3-lang/src/tx3.pest @@ -440,7 +440,7 @@ tx_body_block = _{ } import_def = { - "cardano" ~ "::" ~ "import" ~ string ~ ("as" ~ identifier)? ~ ";" + "import" ~ string ~ ("as" ~ identifier)? ~ ";" } env_field = { identifier ~ ":" ~ type } diff --git a/crates/tx3-lang/tests/fixtures/import_test.tx3 b/crates/tx3-lang/tests/fixtures/import_test.tx3 index fd4aeb85..6c26249e 100644 --- a/crates/tx3-lang/tests/fixtures/import_test.tx3 +++ b/crates/tx3-lang/tests/fixtures/import_test.tx3 @@ -1,4 +1,4 @@ -cardano::import "../../../cip-57/examples/plutus.json" as types; +import "../../../cip-57/examples/plutus.json" as types; party Sender; party Receiver; diff --git a/crates/tx3-lang/tests/imports.rs b/crates/tx3-lang/tests/imports.rs index 6e4bb05d..a432a55e 100644 --- a/crates/tx3-lang/tests/imports.rs +++ b/crates/tx3-lang/tests/imports.rs @@ -23,7 +23,7 @@ fn import_with_alias_adds_types() { #[test] fn import_without_root_errors() { let src = r#" -cardano::import "some/file.json"; +import "some/file.json"; party X; tx dummy() {} "#; @@ -45,7 +45,7 @@ fn duplicate_type_name_from_imports_errors() { let path = Path::new(manifest_dir).join("tests/fixtures/import_test.tx3"); let content = std::fs::read_to_string(&path).unwrap(); let content_twice = format!( - "{}\ncardano::import \"../../../cip-57/examples/plutus.json\" as types;\n{}", + "{}\nimport \"../../../cip-57/examples/plutus.json\" as types;\n{}", content, "tx dummy2() {}" ); let fixtures = Path::new(manifest_dir).join("tests/fixtures"); @@ -71,7 +71,7 @@ fn invalid_import_path_errors() { let fixtures = Path::new(manifest_dir).join("tests/fixtures"); let temp_tx3 = fixtures.join("temp_nonexistent_import.tx3"); let content = r#" -cardano::import "nonexistent/plutus.json" as types; +import "nonexistent/plutus.json" as types; party X; tx dummy() {} "#; From 8ebf3181f09f28dbc01df9f232162b3a6ac7c260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 12:47:01 -0300 Subject: [PATCH 12/27] refactor: move import tests with the rest of tests --- crates/tx3-lang/src/cardano.rs | 5 +- crates/tx3-lang/src/importing.rs | 90 ++++ crates/tx3-lang/src/parsing.rs | 2 + .../tx3-lang/tests/fixtures/import_test.tx3 | 6 - crates/tx3-lang/tests/imports.rs | 82 ---- examples/imported_datum.ast | 414 ++++++++++++++++++ examples/imported_datum.tx3 | 27 ++ 7 files changed, 534 insertions(+), 92 deletions(-) delete mode 100644 crates/tx3-lang/tests/fixtures/import_test.tx3 delete mode 100644 crates/tx3-lang/tests/imports.rs create mode 100644 examples/imported_datum.ast create mode 100644 examples/imported_datum.tx3 diff --git a/crates/tx3-lang/src/cardano.rs b/crates/tx3-lang/src/cardano.rs index ac11eb39..985db0d9 100644 --- a/crates/tx3-lang/src/cardano.rs +++ b/crates/tx3-lang/src/cardano.rs @@ -845,10 +845,7 @@ impl IntoLower for CardanoBlock { #[cfg(test)] mod tests { use super::*; - use crate::{ - analyzing::analyze, - ast::*, - }; + use crate::{analyzing::analyze, ast::*}; use pest::Parser; macro_rules! input_to_ast_check { diff --git a/crates/tx3-lang/src/importing.rs b/crates/tx3-lang/src/importing.rs index 5abfb6a1..d8504db0 100644 --- a/crates/tx3-lang/src/importing.rs +++ b/crates/tx3-lang/src/importing.rs @@ -344,4 +344,94 @@ mod tests { type_names ); } + + #[test] + fn import_with_alias_adds_types() { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let root = std::path::Path::new(manifest_dir); + + let mut program = + crate::parsing::parse_string(r#"import "../cip-57/examples/plutus.json" as types;"#) + .unwrap(); + + let loader = FsLoader::new(root); + resolve_imports(&mut program, Some(&loader)).unwrap(); + + let type_names: Vec = program.types.iter().map(|t| t.name.value.clone()).collect(); + assert!( + type_names.iter().any(|n| n.starts_with("types_")), + "expected at least one type prefixed with 'types_', got: {:?}", + type_names + ); + } + + #[test] + fn import_without_root_errors() { + let src = r#" + import "some/file.json"; + party X; + tx dummy() {} + "#; + let mut program = crate::parsing::parse_string(src).unwrap(); + let res = resolve_imports(&mut program, None::<&InMemoryLoader>); + + assert!(res.is_err(), "expected error when importing without root"); + let err = res.unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("root") || msg.contains("import"), + "expected error about root or import, got: {}", + msg + ); + } + + #[test] + fn duplicate_type_name_from_imports_errors() { + let json = r#"{ + "preamble": { "title": "test", "version": "0", "plutusVersion": "v3" }, + "validators": [], + "definitions": { + "One": { + "title": "One", + "anyOf": [ { "title": "A", "dataType": "constructor", "index": 0, "fields": [] } ] + } + } + }"#; + + let src = r#" + import "schema1.json" as types; + import "schema2.json" as types; + "#; + + let mut program = crate::parsing::parse_string(src).unwrap(); + let mut loader = InMemoryLoader::new(); + loader.add("schema1.json", json); + loader.add("schema2.json", json); + let res = resolve_imports(&mut program, Some(&loader)); + + assert!( + res.is_err(), + "expected error for duplicate type names from two imports" + ); + let err = res.unwrap_err(); + assert!( + err.to_string().contains("duplicate") || err.to_string().contains("type"), + "expected duplicate/type error, got: {}", + err + ); + } + + #[test] + fn invalid_import_path_errors() { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let root = std::path::Path::new(manifest_dir); + let src = r#" + import "nonexistent/plutus.json" as types; + "#; + let mut program = crate::parsing::parse_string(src).unwrap(); + let loader = FsLoader::new(root); + + let res = resolve_imports(&mut program, Some(&loader)); + assert!(res.is_err(), "expected error for missing import file"); + } } diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index b92190fe..116ede21 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -2833,4 +2833,6 @@ mod tests { test_parsing!(list_concat); test_parsing!(buidler_fest_2026); + + test_parsing!(imported_datum); } diff --git a/crates/tx3-lang/tests/fixtures/import_test.tx3 b/crates/tx3-lang/tests/fixtures/import_test.tx3 deleted file mode 100644 index 6c26249e..00000000 --- a/crates/tx3-lang/tests/fixtures/import_test.tx3 +++ /dev/null @@ -1,6 +0,0 @@ -import "../../../cip-57/examples/plutus.json" as types; - -party Sender; -party Receiver; - -tx dummy() {} diff --git a/crates/tx3-lang/tests/imports.rs b/crates/tx3-lang/tests/imports.rs deleted file mode 100644 index a432a55e..00000000 --- a/crates/tx3-lang/tests/imports.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Tests for plutus.json import support. - -use std::path::Path; - -use tx3_lang::Workspace; - -#[test] -fn import_with_alias_adds_types() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let path = Path::new(manifest_dir).join("tests/fixtures/import_test.tx3"); - let mut workspace = Workspace::from_file(&path).unwrap(); - workspace.parse().unwrap(); - let ast = workspace.ast().unwrap(); - - let type_names: Vec = ast.types.iter().map(|t| t.name.value.clone()).collect(); - assert!( - type_names.iter().any(|n| n.starts_with("types_")), - "expected at least one type prefixed with 'types_', got: {:?}", - type_names - ); -} - -#[test] -fn import_without_root_errors() { - let src = r#" -import "some/file.json"; -party X; -tx dummy() {} -"#; - let mut workspace = Workspace::from_string(src.to_string()); - let res = workspace.parse(); - assert!(res.is_err(), "expected error when importing without root"); - let err = res.unwrap_err(); - let msg = err.to_string(); - assert!( - msg.contains("root") || msg.contains("import"), - "expected error about root or import, got: {}", - msg - ); -} - -#[test] -fn duplicate_type_name_from_imports_errors() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let path = Path::new(manifest_dir).join("tests/fixtures/import_test.tx3"); - let content = std::fs::read_to_string(&path).unwrap(); - let content_twice = format!( - "{}\nimport \"../../../cip-57/examples/plutus.json\" as types;\n{}", - content, "tx dummy2() {}" - ); - let fixtures = Path::new(manifest_dir).join("tests/fixtures"); - let temp_tx3 = fixtures.join("temp_duplicate_import.tx3"); - std::fs::write(&temp_tx3, content_twice).unwrap(); - let res = Workspace::from_file(&temp_tx3).and_then(|mut w| w.parse()); - let _ = std::fs::remove_file(&temp_tx3); - assert!( - res.is_err(), - "expected error for duplicate type names from two imports" - ); - let err = res.unwrap_err(); - assert!( - err.to_string().contains("duplicate") || err.to_string().contains("type"), - "expected duplicate/type error, got: {}", - err - ); -} - -#[test] -fn invalid_import_path_errors() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let fixtures = Path::new(manifest_dir).join("tests/fixtures"); - let temp_tx3 = fixtures.join("temp_nonexistent_import.tx3"); - let content = r#" -import "nonexistent/plutus.json" as types; -party X; -tx dummy() {} -"#; - std::fs::write(&temp_tx3, content).unwrap(); - let res = Workspace::from_file(&temp_tx3).and_then(|mut w| w.parse()); - let _ = std::fs::remove_file(&temp_tx3); - assert!(res.is_err(), "expected error for missing import file"); -} diff --git a/examples/imported_datum.ast b/examples/imported_datum.ast new file mode 100644 index 00000000..4a5c6fd5 --- /dev/null +++ b/examples/imported_datum.ast @@ -0,0 +1,414 @@ +{ + "imports": [ + { + "path": { + "value": "../crates/cip-57/examples/plutus.json", + "span": { + "dummy": false, + "start": 7, + "end": 46 + } + }, + "alias": { + "value": "plutus", + "span": { + "dummy": false, + "start": 50, + "end": 56 + } + }, + "span": { + "dummy": false, + "start": 0, + "end": 57 + } + } + ], + "env": null, + "txs": [ + { + "name": { + "value": "create_settings", + "span": { + "dummy": false, + "start": 76, + "end": 91 + } + }, + "parameters": { + "parameters": [ + { + "name": { + "value": "now", + "span": { + "dummy": false, + "start": 97, + "end": 100 + } + }, + "type": "Int" + } + ], + "span": { + "dummy": false, + "start": 91, + "end": 108 + } + }, + "locals": null, + "references": [], + "inputs": [ + { + "name": "fee_source", + "many": false, + "fields": [ + { + "From": { + "Identifier": { + "value": "Admin", + "span": { + "dummy": false, + "start": 148, + "end": 153 + } + } + } + }, + { + "MinAmount": { + "Number": 1000000 + } + } + ], + "span": { + "dummy": false, + "start": 115, + "end": 189 + } + } + ], + "outputs": [ + { + "name": { + "value": "settings", + "span": { + "dummy": false, + "start": 202, + "end": 210 + } + }, + "optional": false, + "fields": [ + { + "To": { + "Identifier": { + "value": "Admin", + "span": { + "dummy": false, + "start": 225, + "end": 230 + } + } + } + }, + { + "Datum": { + "StructConstructor": { + "type": { + "value": "plutus_types_SettingsDatum", + "span": { + "dummy": false, + "start": 247, + "end": 273 + } + }, + "case": { + "name": { + "value": "SettingsDatum", + "span": { + "dummy": false, + "start": 275, + "end": 288 + } + }, + "fields": [ + { + "name": { + "value": "githoney_address", + "span": { + "dummy": false, + "start": 303, + "end": 319 + } + }, + "value": { + "StructConstructor": { + "type": { + "value": "plutus_cardano_address_Address", + "span": { + "dummy": false, + "start": 321, + "end": 351 + } + }, + "case": { + "name": { + "value": "Address", + "span": { + "dummy": false, + "start": 353, + "end": 360 + } + }, + "fields": [ + { + "name": { + "value": "payment_credential", + "span": { + "dummy": false, + "start": 379, + "end": 397 + } + }, + "value": { + "StructConstructor": { + "type": { + "value": "plutus_cardano_address_PaymentCredential", + "span": { + "dummy": false, + "start": 399, + "end": 439 + } + }, + "case": { + "name": { + "value": "VerificationKey", + "span": { + "dummy": false, + "start": 441, + "end": 456 + } + }, + "fields": [ + { + "name": { + "value": "field", + "span": { + "dummy": false, + "start": 479, + "end": 484 + } + }, + "value": { + "HexString": { + "value": "12345678901234567890123456789012345678901234567890123456", + "span": { + "dummy": false, + "start": 486, + "end": 544 + } + } + }, + "span": { + "dummy": false, + "start": 479, + "end": 544 + } + } + ], + "spread": null, + "span": { + "dummy": false, + "start": 439, + "end": 563 + } + }, + "span": { + "dummy": false, + "start": 399, + "end": 563 + } + } + }, + "span": { + "dummy": false, + "start": 379, + "end": 563 + } + }, + { + "name": { + "value": "stake_credential", + "span": { + "dummy": false, + "start": 581, + "end": 597 + } + }, + "value": { + "StructConstructor": { + "type": { + "value": "plutus_Option_cardano_address_StakeCredential", + "span": { + "dummy": false, + "start": 599, + "end": 644 + } + }, + "case": { + "name": { + "value": "None", + "span": { + "dummy": false, + "start": 646, + "end": 650 + } + }, + "fields": [], + "spread": null, + "span": { + "dummy": false, + "start": 644, + "end": 653 + } + }, + "span": { + "dummy": false, + "start": 599, + "end": 653 + } + } + }, + "span": { + "dummy": false, + "start": 581, + "end": 653 + } + } + ], + "spread": null, + "span": { + "dummy": false, + "start": 351, + "end": 668 + } + }, + "span": { + "dummy": false, + "start": 321, + "end": 668 + } + } + }, + "span": { + "dummy": false, + "start": 303, + "end": 668 + } + }, + { + "name": { + "value": "bounty_creation_fee", + "span": { + "dummy": false, + "start": 682, + "end": 701 + } + }, + "value": { + "Number": 500 + }, + "span": { + "dummy": false, + "start": 682, + "end": 706 + } + }, + { + "name": { + "value": "bounty_reward_fee", + "span": { + "dummy": false, + "start": 720, + "end": 737 + } + }, + "value": { + "Number": 100 + }, + "span": { + "dummy": false, + "start": 720, + "end": 742 + } + } + ], + "spread": null, + "span": { + "dummy": false, + "start": 273, + "end": 753 + } + }, + "span": { + "dummy": false, + "start": 247, + "end": 753 + } + } + } + }, + { + "Amount": { + "Number": 2000000 + } + } + ], + "span": { + "dummy": false, + "start": 195, + "end": 785 + } + } + ], + "validity": null, + "mints": [], + "burns": [], + "signers": null, + "adhoc": [], + "span": { + "dummy": false, + "start": 73, + "end": 787 + }, + "collateral": [], + "metadata": null + } + ], + "types": [], + "aliases": [], + "assets": [], + "parties": [ + { + "name": { + "value": "Admin", + "span": { + "dummy": false, + "start": 65, + "end": 70 + } + }, + "span": { + "dummy": false, + "start": 59, + "end": 71 + } + } + ], + "policies": [], + "span": { + "dummy": false, + "start": 0, + "end": 788 + } +} \ No newline at end of file diff --git a/examples/imported_datum.tx3 b/examples/imported_datum.tx3 new file mode 100644 index 00000000..5d0637c1 --- /dev/null +++ b/examples/imported_datum.tx3 @@ -0,0 +1,27 @@ +import "../crates/cip-57/examples/plutus.json" as plutus; + +party Admin; + +tx create_settings( + now: Int, +) { + input fee_source { + from: Admin, + min_amount: 1000000, + } + + output settings { + to: Admin, + datum: plutus_types_SettingsDatum::SettingsDatum { + githoney_address: plutus_cardano_address_Address::Address { + payment_credential: plutus_cardano_address_PaymentCredential::VerificationKey { + field: 0x12345678901234567890123456789012345678901234567890123456, + }, + stake_credential: plutus_Option_cardano_address_StakeCredential::None {}, + }, + bounty_creation_fee: 500, + bounty_reward_fee: 100, + }, + amount: 2000000, + } +} From 8fbf3c936be5a01af9acf0d3bdc9595894043f21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 14:15:56 -0300 Subject: [PATCH 13/27] feat: single variant types have no explicit constructor --- crates/tx3-lang/src/importing.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/tx3-lang/src/importing.rs b/crates/tx3-lang/src/importing.rs index d8504db0..36bf4f60 100644 --- a/crates/tx3-lang/src/importing.rs +++ b/crates/tx3-lang/src/importing.rs @@ -215,6 +215,11 @@ fn definition_to_type_def( return Ok(None); } + let mut cases = cases; + if cases.len() == 1 { + cases[0].name = Identifier::new("Default"); + } + let type_name = import_type_name(key, alias); Ok(Some(TypeDef { name: Identifier::new(type_name), From 912952c74d15e0078f55da8e27cc23107ccf5cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 14:16:53 -0300 Subject: [PATCH 14/27] fix: Data has no internal structure Workaround turn it into an empty struct basically --- crates/tx3-lang/src/importing.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/tx3-lang/src/importing.rs b/crates/tx3-lang/src/importing.rs index 36bf4f60..421aeaaf 100644 --- a/crates/tx3-lang/src/importing.rs +++ b/crates/tx3-lang/src/importing.rs @@ -208,6 +208,19 @@ fn definition_to_type_def( .map(|s| schema_to_variant_case(s, definitions, alias)) .collect::, _>>()? } else { + // Data type has no structure but should still be imported. + // TODO: There is no Any for our type system, so for now we'll just import it as a single empty variant case. + if key == "Data" { + return Ok(Some(TypeDef { + name: Identifier::new(import_type_name(key, alias)), + cases: vec![VariantCase { + name: Identifier::new("Default"), + fields: vec![], + span: Span::DUMMY, + }], + span: Span::DUMMY, + })); + } return Ok(None); }; From 1601a1c2523376dfdf886a5fd69e3fb03752670a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 14:17:20 -0300 Subject: [PATCH 15/27] feat: parsing import example with analyze phase to make sure that it works great e2e --- crates/tx3-lang/src/importing.rs | 70 ++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/crates/tx3-lang/src/importing.rs b/crates/tx3-lang/src/importing.rs index 421aeaaf..a13ed7fb 100644 --- a/crates/tx3-lang/src/importing.rs +++ b/crates/tx3-lang/src/importing.rs @@ -452,4 +452,74 @@ mod tests { let res = resolve_imports(&mut program, Some(&loader)); assert!(res.is_err(), "expected error for missing import file"); } + + #[test] + fn single_variant_type_import_with_analyze() { + let dollar = "$"; + let hash = "#"; + let json = format!(r#"{{ + "preamble": {{ "title": "test", "version": "0", "plutusVersion": "v3" }}, + "validators": [], + "definitions": {{ + "OutputReference": {{ + "title": "OutputReference", + "anyOf": [ + {{ + "title": "OutputReference", + "dataType": "constructor", + "index": 0, + "fields": [ + {{ + "title": "transaction_id", + "{}ref": "{}/definitions/ByteArray" + }}, + {{ + "title": "output_index", + "{}ref": "{}/definitions/Int" + }} + ] + }} + ] + }}, + "ByteArray": {{ + "title": "ByteArray", + "dataType": "bytes" + }}, + "Int": {{ + "title": "Int", + "dataType": "integer" + }} + }} + }}"#, dollar, hash, dollar, hash); + + let src = r#" + import "test.json" as types; + party Alice; + tx test(outref: types_OutputReference) { + output my_output { + to: Alice, + amount: 1000000, + } + } + "#; + + let mut program = crate::parsing::parse_string(src).unwrap(); + let mut loader = InMemoryLoader::new(); + loader.add("test.json", json); + + resolve_imports(&mut program, Some(&loader)).unwrap(); + + let output_ref_type = program.types.iter() + .find(|t| t.name.value == "types_OutputReference") + .expect("types_OutputReference should be imported"); + assert_eq!(output_ref_type.cases.len(), 1); + assert_eq!(output_ref_type.cases[0].name.value, "Default"); + + let report = crate::analyzing::analyze(&mut program); + assert!( + report.errors.is_empty(), + "expected no analysis errors, got: {:?}", + report.errors + ); + } } From 20ce92b43ffefe32cf2babe34523476c3a08a93a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 14:52:22 -0300 Subject: [PATCH 16/27] chore: formating --- crates/tx3-lang/src/importing.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/tx3-lang/src/importing.rs b/crates/tx3-lang/src/importing.rs index a13ed7fb..7836dd16 100644 --- a/crates/tx3-lang/src/importing.rs +++ b/crates/tx3-lang/src/importing.rs @@ -457,7 +457,8 @@ mod tests { fn single_variant_type_import_with_analyze() { let dollar = "$"; let hash = "#"; - let json = format!(r#"{{ + let json = format!( + r#"{{ "preamble": {{ "title": "test", "version": "0", "plutusVersion": "v3" }}, "validators": [], "definitions": {{ @@ -490,7 +491,9 @@ mod tests { "dataType": "integer" }} }} - }}"#, dollar, hash, dollar, hash); + }}"#, + dollar, hash, dollar, hash + ); let src = r#" import "test.json" as types; @@ -506,10 +509,12 @@ mod tests { let mut program = crate::parsing::parse_string(src).unwrap(); let mut loader = InMemoryLoader::new(); loader.add("test.json", json); - + resolve_imports(&mut program, Some(&loader)).unwrap(); - let output_ref_type = program.types.iter() + let output_ref_type = program + .types + .iter() .find(|t| t.name.value == "types_OutputReference") .expect("types_OutputReference should be imported"); assert_eq!(output_ref_type.cases.len(), 1); From 0e79d5089935eab6a6e22c4a7733b06442640165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 14:57:38 -0300 Subject: [PATCH 17/27] fix: use the same serde_json lib as everywhere else --- crates/cip-57/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cip-57/Cargo.toml b/crates/cip-57/Cargo.toml index d70aa572..a339ae1f 100644 --- a/crates/cip-57/Cargo.toml +++ b/crates/cip-57/Cargo.toml @@ -15,4 +15,4 @@ readme.workspace = true [dependencies] serde = { version = "1", features = ["derive"] } -serde_json = "1" +serde_json = "1.0.137" From e42d42a4928752b1a99839d03fa31021f5363a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 15:02:53 -0300 Subject: [PATCH 18/27] chore: remove unnecessary code branch --- crates/tx3-lang/src/importing.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/tx3-lang/src/importing.rs b/crates/tx3-lang/src/importing.rs index 7836dd16..f56b3a59 100644 --- a/crates/tx3-lang/src/importing.rs +++ b/crates/tx3-lang/src/importing.rs @@ -149,10 +149,6 @@ fn resolve_ref_to_type( } } - if def.any_of.is_some() { - return Ok(Type::Custom(Identifier::new(import_type_name(&key, alias)))); - } - Ok(Type::Custom(Identifier::new(import_type_name(&key, alias)))) } From 5be14b80009ccbfb12f756960cf962774dd019b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 15:25:28 -0300 Subject: [PATCH 19/27] fix: ast & tir files up-to-date --- examples/asteria.ast | 1 + examples/buidler_fest_2026.ast | 1 + examples/burn.ast | 1 + examples/cardano_witness.ast | 1 + examples/cardano_witness.mint_from_plutus.tir | 6 +- examples/disordered.ast | 1 + examples/donation.ast | 1 + examples/env_vars.ast | 1 + examples/faucet.ast | 1 + examples/imported_datum.ast | 124 +++++++++--------- examples/imported_datum.tx3 | 4 +- examples/input_datum.ast | 1 + examples/lang_tour.ast | 1 + examples/lang_tour.my_tx.tir | 22 ++-- examples/list_concat.ast | 1 + examples/local_vars.ast | 1 + examples/map.ast | 1 + examples/reference_script.ast | 1 + examples/reference_script.publish_native.tir | 22 ++-- examples/reference_script.publish_plutus.tir | 38 +++--- examples/swap.ast | 1 + examples/transfer.ast | 1 + examples/vesting.ast | 1 + examples/withdrawal.ast | 1 + examples/withdrawal.transfer.tir | 12 +- 25 files changed, 132 insertions(+), 114 deletions(-) diff --git a/examples/asteria.ast b/examples/asteria.ast index 4b556cb1..6e284ff7 100644 --- a/examples/asteria.ast +++ b/examples/asteria.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/buidler_fest_2026.ast b/examples/buidler_fest_2026.ast index c0c0f570..96b13316 100644 --- a/examples/buidler_fest_2026.ast +++ b/examples/buidler_fest_2026.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": { "fields": [ { diff --git a/examples/burn.ast b/examples/burn.ast index 6eafd087..1ed27f63 100644 --- a/examples/burn.ast +++ b/examples/burn.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/cardano_witness.ast b/examples/cardano_witness.ast index 9136d18d..4fe2ba60 100644 --- a/examples/cardano_witness.ast +++ b/examples/cardano_witness.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/cardano_witness.mint_from_plutus.tir b/examples/cardano_witness.mint_from_plutus.tir index c0f4530f..8ff6e2e2 100644 --- a/examples/cardano_witness.mint_from_plutus.tir +++ b/examples/cardano_witness.mint_from_plutus.tir @@ -202,9 +202,6 @@ { "name": "plutus_witness", "data": { - "version": { - "Number": 3 - }, "script": { "Bytes": [ 81, @@ -226,6 +223,9 @@ 174, 105 ] + }, + "version": { + "Number": 3 } } } diff --git a/examples/disordered.ast b/examples/disordered.ast index de9c48f3..0983c7f2 100644 --- a/examples/disordered.ast +++ b/examples/disordered.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/donation.ast b/examples/donation.ast index f82e5c24..0660f4f4 100644 --- a/examples/donation.ast +++ b/examples/donation.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/env_vars.ast b/examples/env_vars.ast index cef2b474..373b6ebc 100644 --- a/examples/env_vars.ast +++ b/examples/env_vars.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": { "fields": [ { diff --git a/examples/faucet.ast b/examples/faucet.ast index e961c1d4..f82b8247 100644 --- a/examples/faucet.ast +++ b/examples/faucet.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/imported_datum.ast b/examples/imported_datum.ast index 4a5c6fd5..5b2abd96 100644 --- a/examples/imported_datum.ast +++ b/examples/imported_datum.ast @@ -124,11 +124,11 @@ }, "case": { "name": { - "value": "SettingsDatum", + "value": "Default", "span": { - "dummy": false, - "start": 275, - "end": 288 + "dummy": true, + "start": 0, + "end": 0 } }, "fields": [ @@ -137,8 +137,8 @@ "value": "githoney_address", "span": { "dummy": false, - "start": 303, - "end": 319 + "start": 288, + "end": 304 } }, "value": { @@ -147,17 +147,17 @@ "value": "plutus_cardano_address_Address", "span": { "dummy": false, - "start": 321, - "end": 351 + "start": 306, + "end": 336 } }, "case": { "name": { - "value": "Address", + "value": "Default", "span": { - "dummy": false, - "start": 353, - "end": 360 + "dummy": true, + "start": 0, + "end": 0 } }, "fields": [ @@ -166,8 +166,8 @@ "value": "payment_credential", "span": { "dummy": false, - "start": 379, - "end": 397 + "start": 355, + "end": 373 } }, "value": { @@ -176,8 +176,8 @@ "value": "plutus_cardano_address_PaymentCredential", "span": { "dummy": false, - "start": 399, - "end": 439 + "start": 375, + "end": 415 } }, "case": { @@ -185,8 +185,8 @@ "value": "VerificationKey", "span": { "dummy": false, - "start": 441, - "end": 456 + "start": 417, + "end": 432 } }, "fields": [ @@ -195,8 +195,8 @@ "value": "field", "span": { "dummy": false, - "start": 479, - "end": 484 + "start": 455, + "end": 460 } }, "value": { @@ -204,36 +204,36 @@ "value": "12345678901234567890123456789012345678901234567890123456", "span": { "dummy": false, - "start": 486, - "end": 544 + "start": 462, + "end": 520 } } }, "span": { "dummy": false, - "start": 479, - "end": 544 + "start": 455, + "end": 520 } } ], "spread": null, "span": { "dummy": false, - "start": 439, - "end": 563 + "start": 415, + "end": 539 } }, "span": { "dummy": false, - "start": 399, - "end": 563 + "start": 375, + "end": 539 } } }, "span": { "dummy": false, - "start": 379, - "end": 563 + "start": 355, + "end": 539 } }, { @@ -241,8 +241,8 @@ "value": "stake_credential", "span": { "dummy": false, - "start": 581, - "end": 597 + "start": 557, + "end": 573 } }, "value": { @@ -251,8 +251,8 @@ "value": "plutus_Option_cardano_address_StakeCredential", "span": { "dummy": false, - "start": 599, - "end": 644 + "start": 575, + "end": 620 } }, "case": { @@ -260,50 +260,50 @@ "value": "None", "span": { "dummy": false, - "start": 646, - "end": 650 + "start": 622, + "end": 626 } }, "fields": [], "spread": null, "span": { "dummy": false, - "start": 644, - "end": 653 + "start": 620, + "end": 629 } }, "span": { "dummy": false, - "start": 599, - "end": 653 + "start": 575, + "end": 629 } } }, "span": { "dummy": false, - "start": 581, - "end": 653 + "start": 557, + "end": 629 } } ], "spread": null, "span": { "dummy": false, - "start": 351, - "end": 668 + "start": 337, + "end": 644 } }, "span": { "dummy": false, - "start": 321, - "end": 668 + "start": 306, + "end": 644 } } }, "span": { "dummy": false, - "start": 303, - "end": 668 + "start": 288, + "end": 644 } }, { @@ -311,8 +311,8 @@ "value": "bounty_creation_fee", "span": { "dummy": false, - "start": 682, - "end": 701 + "start": 658, + "end": 677 } }, "value": { @@ -320,8 +320,8 @@ }, "span": { "dummy": false, - "start": 682, - "end": 706 + "start": 658, + "end": 682 } }, { @@ -329,8 +329,8 @@ "value": "bounty_reward_fee", "span": { "dummy": false, - "start": 720, - "end": 737 + "start": 696, + "end": 713 } }, "value": { @@ -338,22 +338,22 @@ }, "span": { "dummy": false, - "start": 720, - "end": 742 + "start": 696, + "end": 718 } } ], "spread": null, "span": { "dummy": false, - "start": 273, - "end": 753 + "start": 274, + "end": 729 } }, "span": { "dummy": false, "start": 247, - "end": 753 + "end": 729 } } } @@ -367,7 +367,7 @@ "span": { "dummy": false, "start": 195, - "end": 785 + "end": 761 } } ], @@ -379,7 +379,7 @@ "span": { "dummy": false, "start": 73, - "end": 787 + "end": 763 }, "collateral": [], "metadata": null @@ -409,6 +409,6 @@ "span": { "dummy": false, "start": 0, - "end": 788 + "end": 764 } } \ No newline at end of file diff --git a/examples/imported_datum.tx3 b/examples/imported_datum.tx3 index 5d0637c1..339f3aea 100644 --- a/examples/imported_datum.tx3 +++ b/examples/imported_datum.tx3 @@ -12,8 +12,8 @@ tx create_settings( output settings { to: Admin, - datum: plutus_types_SettingsDatum::SettingsDatum { - githoney_address: plutus_cardano_address_Address::Address { + datum: plutus_types_SettingsDatum { + githoney_address: plutus_cardano_address_Address { payment_credential: plutus_cardano_address_PaymentCredential::VerificationKey { field: 0x12345678901234567890123456789012345678901234567890123456, }, diff --git a/examples/input_datum.ast b/examples/input_datum.ast index c0cb2672..55b334ae 100644 --- a/examples/input_datum.ast +++ b/examples/input_datum.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/lang_tour.ast b/examples/lang_tour.ast index cfe0e4cd..2a3b452b 100644 --- a/examples/lang_tour.ast +++ b/examples/lang_tour.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": { "fields": [ { diff --git a/examples/lang_tour.my_tx.tir b/examples/lang_tour.my_tx.tir index 525dcb75..f60eca2f 100644 --- a/examples/lang_tour.my_tx.tir +++ b/examples/lang_tour.my_tx.tir @@ -645,14 +645,6 @@ { "name": "vote_delegation_certificate", "data": { - "stake": { - "Bytes": [ - 135, - 101, - 67, - 33 - ] - }, "drep": { "Bytes": [ 18, @@ -660,6 +652,14 @@ 86, 120 ] + }, + "stake": { + "Bytes": [ + 135, + 101, + 67, + 33 + ] } } }, @@ -688,6 +688,9 @@ { "name": "plutus_witness", "data": { + "version": { + "Number": 2 + }, "script": { "Bytes": [ 171, @@ -696,9 +699,6 @@ 18, 52 ] - }, - "version": { - "Number": 2 } } }, diff --git a/examples/list_concat.ast b/examples/list_concat.ast index f8b6c17f..2708bb0d 100644 --- a/examples/list_concat.ast +++ b/examples/list_concat.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/local_vars.ast b/examples/local_vars.ast index 432bb4ff..8f52defe 100644 --- a/examples/local_vars.ast +++ b/examples/local_vars.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/map.ast b/examples/map.ast index 148694e0..224baad4 100644 --- a/examples/map.ast +++ b/examples/map.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/reference_script.ast b/examples/reference_script.ast index ccb5f6db..3764b1ab 100644 --- a/examples/reference_script.ast +++ b/examples/reference_script.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/reference_script.publish_native.tir b/examples/reference_script.publish_native.tir index 90fdd3b9..644738e7 100644 --- a/examples/reference_script.publish_native.tir +++ b/examples/reference_script.publish_native.tir @@ -147,6 +147,17 @@ 0 ] }, + "to": { + "EvalParam": { + "ExpectValue": [ + "receiver", + "Address" + ] + } + }, + "version": { + "Number": 0 + }, "amount": { "Assets": [ { @@ -162,17 +173,6 @@ } } ] - }, - "to": { - "EvalParam": { - "ExpectValue": [ - "receiver", - "Address" - ] - } - }, - "version": { - "Number": 0 } } } diff --git a/examples/reference_script.publish_plutus.tir b/examples/reference_script.publish_plutus.tir index 7506c0f5..7a17ad71 100644 --- a/examples/reference_script.publish_plutus.tir +++ b/examples/reference_script.publish_plutus.tir @@ -137,25 +137,6 @@ { "name": "cardano_publish", "data": { - "version": { - "Number": 3 - }, - "amount": { - "Assets": [ - { - "policy": "None", - "asset_name": "None", - "amount": { - "EvalParam": { - "ExpectValue": [ - "quantity", - "Int" - ] - } - } - } - ] - }, "script": { "Bytes": [ 81, @@ -178,6 +159,22 @@ 105 ] }, + "amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + } + ] + }, "to": { "EvalParam": { "ExpectValue": [ @@ -185,6 +182,9 @@ "Address" ] } + }, + "version": { + "Number": 3 } } } diff --git a/examples/swap.ast b/examples/swap.ast index 8d915d0f..db090bc7 100644 --- a/examples/swap.ast +++ b/examples/swap.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/transfer.ast b/examples/transfer.ast index a21a5413..0fce4dc3 100644 --- a/examples/transfer.ast +++ b/examples/transfer.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/vesting.ast b/examples/vesting.ast index dac2dfa9..1ae0698a 100644 --- a/examples/vesting.ast +++ b/examples/vesting.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/withdrawal.ast b/examples/withdrawal.ast index 0f0e7382..ce7cc4d1 100644 --- a/examples/withdrawal.ast +++ b/examples/withdrawal.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/withdrawal.transfer.tir b/examples/withdrawal.transfer.tir index facdbb3c..48a914f8 100644 --- a/examples/withdrawal.transfer.tir +++ b/examples/withdrawal.transfer.tir @@ -165,6 +165,12 @@ { "name": "withdrawal", "data": { + "redeemer": { + "Struct": { + "constructor": 0, + "fields": [] + } + }, "credential": { "EvalParam": { "ExpectValue": [ @@ -175,12 +181,6 @@ }, "amount": { "Number": 0 - }, - "redeemer": { - "Struct": { - "constructor": 0, - "fields": [] - } } } } From 23a74299e1d1a9ada36e538ef38f8727dc3e01b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 17:52:14 -0300 Subject: [PATCH 20/27] feat: TxDef to tx3 source code --- crates/tx3-lang/src/ast.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index c34e43fe..7d172b00 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -943,6 +943,28 @@ impl TypeDef { pub(crate) fn find_case(&self, case: &str) -> Option<&VariantCase> { self.cases.iter().find(|x| x.name.value == case) } + + pub fn to_tx3_source(&self) -> String { + let name = &self.name.value; + // Implicit cases don't have an explicit constructor on its usage + if self.cases.len() == 1 && self.cases[0].name.value == "Default" { + let fields = &self.cases[0].fields; + let fields_str = fields + .iter() + .map(|f| format!("{}: {}", f.name.value, f.r#type)) + .collect::>() + .join(", "); + format!("type {} {{ {} }}", name, fields_str) + } else { + let cases_str = self + .cases + .iter() + .map(VariantCase::to_tx3_source) + .collect::>() + .join(", "); + format!("type {} {{ {} }}", name, cases_str) + } + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -962,6 +984,21 @@ impl VariantCase { pub(crate) fn find_field(&self, field: &str) -> Option<&RecordField> { self.fields.iter().find(|x| x.name.value == field) } + + fn to_tx3_source(&self) -> String { + let name = &self.name.value; + if self.fields.is_empty() { + name.clone() + } else { + let fields_str = self + .fields + .iter() + .map(|f| format!("{}: {}", f.name.value, f.r#type)) + .collect::>() + .join(", "); + format!("{} {{ {} }}", name, fields_str) + } + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] From 13bada89949ffb27d303e283edaff077c046fff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 17:54:14 -0300 Subject: [PATCH 21/27] refactor: resolve types from blueprint logic extracted into function --- crates/tx3-lang/src/importing.rs | 38 ++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/crates/tx3-lang/src/importing.rs b/crates/tx3-lang/src/importing.rs index f56b3a59..6f1369b7 100644 --- a/crates/tx3-lang/src/importing.rs +++ b/crates/tx3-lang/src/importing.rs @@ -237,6 +237,23 @@ fn definition_to_type_def( })) } +pub fn type_defs_from_blueprint( + blueprint: &Blueprint, + alias: Option<&str>, +) -> Result, Error> { + let definitions = match &blueprint.definitions { + Some(d) => d, + None => return Ok(vec![]), + }; + + let mut type_defs = Vec::new(); + for (key, def) in &definitions.inner { + if let Some(type_def) = definition_to_type_def(key, def, definitions, alias)? { + type_defs.push(type_def); + } + } + Ok(type_defs) +} pub fn resolve_imports( program: &mut Program, loader: Option<&impl ImportLoader>, @@ -257,23 +274,16 @@ pub fn resolve_imports( for import in &program.imports { let json = loader.load_source(import.path.value.as_str())?; let blueprint: Blueprint = serde_json::from_str(&json)?; - - let definitions = match &blueprint.definitions { - Some(d) => d, - None => continue, - }; - let alias = import.alias.as_ref().map(|a| a.value.as_str()); + let type_defs = type_defs_from_blueprint(&blueprint, alias)?; - for (key, def) in &definitions.inner { - if let Some(type_def) = definition_to_type_def(key, def, definitions, alias)? { - let name = type_def.name.value.clone(); - if existing_names.contains(&name) || added_names.contains(&name) { - return Err(Error::duplicate_type(name, import.span.clone())); - } - added_names.insert(name.clone()); - program.types.push(type_def); + for type_def in type_defs { + let name = type_def.name.value.clone(); + if existing_names.contains(&name) || added_names.contains(&name) { + return Err(Error::duplicate_type(name, import.span.clone())); } + added_names.insert(name.clone()); + program.types.push(type_def); } } From 726b4571f3650eff384b18e3c4a9e26bb8a0364b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 6 Feb 2026 17:54:55 -0300 Subject: [PATCH 22/27] feat: types from plutus public on importing crate --- crates/tx3-lang/src/importing.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/tx3-lang/src/importing.rs b/crates/tx3-lang/src/importing.rs index 6f1369b7..7fc94ced 100644 --- a/crates/tx3-lang/src/importing.rs +++ b/crates/tx3-lang/src/importing.rs @@ -254,6 +254,17 @@ pub fn type_defs_from_blueprint( } Ok(type_defs) } + +pub fn types_from_plutus( + path: &str, + alias: Option<&str>, + loader: &impl ImportLoader, +) -> Result, Error> { + let json = loader.load_source(path)?; + let blueprint: Blueprint = serde_json::from_str(&json)?; + type_defs_from_blueprint(&blueprint, alias) +} + pub fn resolve_imports( program: &mut Program, loader: Option<&impl ImportLoader>, From 98d41cd9810a8a0665e24c8fed723f6c09fed5b0 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Wed, 25 Feb 2026 14:03:08 -0300 Subject: [PATCH 23/27] feat: enhance type import handling and update example datum structure --- crates/tx3-lang/src/analyzing.rs | 9 +++++ crates/tx3-lang/src/importing.rs | 67 ++++++++++++++++++++++++-------- examples/imported_datum.tx3 | 16 ++++---- 3 files changed, 67 insertions(+), 25 deletions(-) diff --git a/crates/tx3-lang/src/analyzing.rs b/crates/tx3-lang/src/analyzing.rs index 55e50b02..812595b0 100644 --- a/crates/tx3-lang/src/analyzing.rs +++ b/crates/tx3-lang/src/analyzing.rs @@ -645,6 +645,15 @@ impl Analyzable for StructConstructor { _ => unreachable!(), }; + if type_def.cases.len() == 1 { + let only_case_name = type_def.cases[0].name.value.clone(); + let requested_case = self.case.name.value.clone(); + + if requested_case == "Default" || requested_case == self.r#type.value { + self.case.name.value = only_case_name; + } + } + for case in type_def.cases.iter() { scope.track_variant_case(case); } diff --git a/crates/tx3-lang/src/importing.rs b/crates/tx3-lang/src/importing.rs index 7fc94ced..1a8b867f 100644 --- a/crates/tx3-lang/src/importing.rs +++ b/crates/tx3-lang/src/importing.rs @@ -86,12 +86,39 @@ fn key_to_normalized_name(key: &str) -> String { key.replace('/', "_").replace('$', "_") } -fn import_type_name(key: &str, alias: Option<&str>) -> String { - let base = key_to_normalized_name(key); - match alias { - Some(a) => format!("{}_{}", a, base), - None => base, +fn import_type_name(key: &str) -> String { + friendly_import_type_alias_name(key).unwrap_or_else(|| key_to_normalized_name(key)) +} + +fn sanitize_identifier_part(raw: &str) -> String { + raw.chars() + .filter(|c| c.is_ascii_alphanumeric() || *c == '_') + .collect() +} + +fn friendly_import_type_alias_name(key: &str) -> Option { + let (base_key, generic_key) = match key.split_once('$') { + Some((base, generic)) => (base, Some(generic)), + None => (key, None), + }; + + let base = base_key.rsplit('/').next().unwrap_or(base_key); + let base = sanitize_identifier_part(base); + + if base.is_empty() || !base.chars().next()?.is_ascii_alphabetic() { + return None; + } + + let mut suffix = String::new(); + + if let Some(generic_key) = generic_key { + for generic_part in generic_key.split('_').filter(|x| !x.is_empty()) { + let segment = generic_part.rsplit('/').next().unwrap_or(generic_part); + suffix.push_str(&sanitize_identifier_part(segment)); + } } + + Some(format!("{}{}", base, suffix)) } fn resolve_ref_to_type( @@ -149,15 +176,20 @@ fn resolve_ref_to_type( } } - Ok(Type::Custom(Identifier::new(import_type_name(&key, alias)))) + Ok(Type::Custom(Identifier::new(import_type_name(&key)))) } fn field_to_record_field( f: &Field, + index: usize, definitions: &Definitions, alias: Option<&str>, ) -> Result { - let name = f.title.as_deref().unwrap_or("field").to_string(); + let name = f + .title + .as_deref() + .map(ToString::to_string) + .unwrap_or_else(|| format!("field_{}", index)); let r#type = resolve_ref_to_type(&f.reference, definitions, alias)?; Ok(RecordField { name: Identifier::new(name), @@ -175,7 +207,8 @@ fn schema_to_variant_case( let fields: Vec = schema .fields .iter() - .map(|f| field_to_record_field(f, definitions, alias)) + .enumerate() + .map(|(i, f)| field_to_record_field(f, i, definitions, alias)) .collect::, _>>()?; Ok(VariantCase { name: Identifier::new(name), @@ -208,7 +241,7 @@ fn definition_to_type_def( // TODO: There is no Any for our type system, so for now we'll just import it as a single empty variant case. if key == "Data" { return Ok(Some(TypeDef { - name: Identifier::new(import_type_name(key, alias)), + name: Identifier::new(import_type_name(key)), cases: vec![VariantCase { name: Identifier::new("Default"), fields: vec![], @@ -229,7 +262,7 @@ fn definition_to_type_def( cases[0].name = Identifier::new("Default"); } - let type_name = import_type_name(key, alias); + let type_name = import_type_name(key); Ok(Some(TypeDef { name: Identifier::new(type_name), cases, @@ -374,8 +407,8 @@ mod tests { let type_names: Vec = program.types.iter().map(|t| t.name.value.clone()).collect(); assert!( - type_names.contains(&"types_Bool".to_string()), - "expected types_Bool in program.types, got: {:?}", + type_names.contains(&"Bool".to_string()), + "expected Bool in program.types, got: {:?}", type_names ); } @@ -394,8 +427,8 @@ mod tests { let type_names: Vec = program.types.iter().map(|t| t.name.value.clone()).collect(); assert!( - type_names.iter().any(|n| n.starts_with("types_")), - "expected at least one type prefixed with 'types_', got: {:?}", + type_names.contains(&"SettingsDatum".to_string()), + "expected friendly imported type names (e.g. SettingsDatum), got: {:?}", type_names ); } @@ -515,7 +548,7 @@ mod tests { let src = r#" import "test.json" as types; party Alice; - tx test(outref: types_OutputReference) { + tx test(outref: OutputReference) { output my_output { to: Alice, amount: 1000000, @@ -532,8 +565,8 @@ mod tests { let output_ref_type = program .types .iter() - .find(|t| t.name.value == "types_OutputReference") - .expect("types_OutputReference should be imported"); + .find(|t| t.name.value == "OutputReference") + .expect("OutputReference should be imported"); assert_eq!(output_ref_type.cases.len(), 1); assert_eq!(output_ref_type.cases[0].name.value, "Default"); diff --git a/examples/imported_datum.tx3 b/examples/imported_datum.tx3 index 339f3aea..a4550328 100644 --- a/examples/imported_datum.tx3 +++ b/examples/imported_datum.tx3 @@ -12,15 +12,15 @@ tx create_settings( output settings { to: Admin, - datum: plutus_types_SettingsDatum { - githoney_address: plutus_cardano_address_Address { - payment_credential: plutus_cardano_address_PaymentCredential::VerificationKey { - field: 0x12345678901234567890123456789012345678901234567890123456, - }, - stake_credential: plutus_Option_cardano_address_StakeCredential::None {}, + datum: SettingsDatum::SettingsDatum { + githoney_address: Address::Address{ + payment_credential: PaymentCredential::VerificationKey { + field: 0x12345678901234567890123456789012345678901234567890123456, }, - bounty_creation_fee: 500, - bounty_reward_fee: 100, + stake_credential: OptionStakeCredential::None {}, + }, + bounty_creation_fee: 500, + bounty_reward_fee: 100, }, amount: 2000000, } From ac8d93b27686d43d24a5ec1be06e4858ec063b23 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Thu, 26 Feb 2026 13:07:29 -0300 Subject: [PATCH 24/27] feat: add handling for empty default variant cases in TypeDef --- crates/tx3-lang/src/ast.rs | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index 7d172b00..b6d5ea53 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -949,6 +949,9 @@ impl TypeDef { // Implicit cases don't have an explicit constructor on its usage if self.cases.len() == 1 && self.cases[0].name.value == "Default" { let fields = &self.cases[0].fields; + if fields.is_empty() { + return format!("type {}", name); + } let fields_str = fields .iter() .map(|f| format!("{}: {}", f.name.value, f.r#type)) @@ -1001,6 +1004,44 @@ impl VariantCase { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn to_tx3_source_omits_braces_for_empty_default_case() { + let type_def = TypeDef { + name: Identifier::new("Data"), + cases: vec![VariantCase { + name: Identifier::new("Default"), + fields: vec![], + span: Span::DUMMY, + }], + span: Span::DUMMY, + }; + + assert_eq!(type_def.to_tx3_source(), "type Data"); + } + + #[test] + fn to_tx3_source_keeps_braces_for_default_with_fields() { + let type_def = TypeDef { + name: Identifier::new("OutputReference"), + cases: vec![VariantCase { + name: Identifier::new("Default"), + fields: vec![RecordField::new("transaction_id", Type::Bytes)], + span: Span::DUMMY, + }], + span: Span::DUMMY, + }; + + assert_eq!( + type_def.to_tx3_source(), + "type OutputReference { transaction_id: Bytes }" + ); + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct AssetDef { pub name: Identifier, From 7bdbdc922bda5ef9ff83d7016cec600a92501295 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Thu, 26 Feb 2026 13:57:35 -0300 Subject: [PATCH 25/27] update plutus.json example to the lastest aiken version --- crates/cip-57/examples/plutus.json | 62 +++++++++++++++--------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/crates/cip-57/examples/plutus.json b/crates/cip-57/examples/plutus.json index 97472029..1a6de029 100644 --- a/crates/cip-57/examples/plutus.json +++ b/crates/cip-57/examples/plutus.json @@ -6,7 +6,7 @@ "plutusVersion": "v3", "compiler": { "name": "Aiken", - "version": "v1.1.17+c3a7fba" + "version": "v1.1.21+42babe5" }, "license": "Apache-2.0" }, @@ -33,8 +33,8 @@ } } ], - "compiledCode": "eedaa957c60268de", - "hash": "24b9b1964ce02550db270a1d6270b505b9c0342625ee766d77fab1f9" + "compiledCode": "590389010100229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cdc3a400530090024888966002600460126ea800e266453001300f002980798080014c03cc030dd5180798061baa32332259800980980144c966003300130083259800980498081baa0018a40011375a602860226ea800500f192cc004c024c040dd5000c5300103d87a8000899198008009bab30153012375400444b30010018a6103d87a8000899192cc004cdc8a450b73657474696e67734e4654000018acc004cdc7a4410b73657474696e67734e465400001899ba548000cc05cc0540092f5c114c0103d87a8000404d1330040043019003404c6eb8c04c004c058005014201e32330010013756600a60226ea8c014c044dd5001112cc004006298103d87a8000899192cc004cdc8808800c56600266e3c044006266e9520003301630140024bd7045300103d87a80004049133004004301800340486eb8c048004c054005013528528a01c8b44c96600266e1d20043010375400313259800980b000c4c8c966002601260266ea800a26464646644b3001301d0038992cc004c03cc064dd5000c4c8c8c966002604200513300f3020003132598009809800c4c96600260460031323259800980b000c4c966002604c003133014302500100f8b204630213754005159800980c800c4c8c8ca60026eb4c09c0066eb4c09c00e6eb4c09c00922259800981580240522c8140604e002604c00260426ea800a2c80f901f180f9baa00130220018b2040301e3754005159800980b000c566002603c6ea800a01716407d16407080e0c070dd5000c5901e180f800980f800980d1baa0018b2030301c0058b2034375a60340026eb4c068008c068004c064004c050dd50014590120800980a800c5901318089baa0018b201e30133014301430103754600860206ea800500e18090014590101bac3001300d37540084464b30013005001899192cc004c05800a00916404c6eb8c050004c040dd5001c56600260100031323259800980b00140122c8098dd7180a00098081baa0038b201c4038601c6ea80088c040c0440066eb0c03cc040c040c040c040c040c040c040c040c030dd5001a4444b30013005300f375400513232330010010032259800800c528456600266e3cdd7180b000801c528c4cc008008c05c0050112028375c602660206ea800a2c807060146ea800cdc3a4001164020300900130043754013149a26cac80101", + "hash": "691561f28bb3eb65568768048df9233a0b4cf87a5d2a0ce63da90ae3" }, { "title": "githoney_contract.badges_contract.else", @@ -49,8 +49,8 @@ } } ], - "compiledCode": "3fe9763bc5ea0108", - "hash": "24b9b1964ce02550db270a1d6270b505b9c0342625ee766d77fab1f9" + "compiledCode": "590389010100229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cdc3a400530090024888966002600460126ea800e266453001300f002980798080014c03cc030dd5180798061baa32332259800980980144c966003300130083259800980498081baa0018a40011375a602860226ea800500f192cc004c024c040dd5000c5300103d87a8000899198008009bab30153012375400444b30010018a6103d87a8000899192cc004cdc8a450b73657474696e67734e4654000018acc004cdc7a4410b73657474696e67734e465400001899ba548000cc05cc0540092f5c114c0103d87a8000404d1330040043019003404c6eb8c04c004c058005014201e32330010013756600a60226ea8c014c044dd5001112cc004006298103d87a8000899192cc004cdc8808800c56600266e3c044006266e9520003301630140024bd7045300103d87a80004049133004004301800340486eb8c048004c054005013528528a01c8b44c96600266e1d20043010375400313259800980b000c4c8c966002601260266ea800a26464646644b3001301d0038992cc004c03cc064dd5000c4c8c8c966002604200513300f3020003132598009809800c4c96600260460031323259800980b000c4c966002604c003133014302500100f8b204630213754005159800980c800c4c8c8ca60026eb4c09c0066eb4c09c00e6eb4c09c00922259800981580240522c8140604e002604c00260426ea800a2c80f901f180f9baa00130220018b2040301e3754005159800980b000c566002603c6ea800a01716407d16407080e0c070dd5000c5901e180f800980f800980d1baa0018b2030301c0058b2034375a60340026eb4c068008c068004c064004c050dd50014590120800980a800c5901318089baa0018b201e30133014301430103754600860206ea800500e18090014590101bac3001300d37540084464b30013005001899192cc004c05800a00916404c6eb8c050004c040dd5001c56600260100031323259800980b00140122c8098dd7180a00098081baa0038b201c4038601c6ea80088c040c0440066eb0c03cc040c040c040c040c040c040c040c040c030dd5001a4444b30013005300f375400513232330010010032259800800c528456600266e3cdd7180b000801c528c4cc008008c05c0050112028375c602660206ea800a2c807060146ea800cdc3a4001164020300900130043754013149a26cac80101", + "hash": "691561f28bb3eb65568768048df9233a0b4cf87a5d2a0ce63da90ae3" }, { "title": "githoney_contract.badges_policy.mint", @@ -74,8 +74,8 @@ } } ], - "compiledCode": "c8db1de3fbbc8921", - "hash": "87d6bd0b40f204d49803dad8e0d70611918d22354125c8f226a42670" + "compiledCode": "58980101002229800aba2aba1aab9faab9eaab9dab9a9bad002488888896600264646644b30013370e900018039baa0018992cc004c8cc004004dd6180618051baa300c0062259800800c528456600266ebcc034c02cdd51806800808c528c4cc008008c0380050092018899b894800001a29410071bae300a30083754003164018601000260106012002601000260086ea802229344d9590021", + "hash": "96b8e7848e41c82226a894a633bcdfd8f7fe73b24849288d1f0e47aa" }, { "title": "githoney_contract.badges_policy.else", @@ -96,8 +96,8 @@ } } ], - "compiledCode": "f143b98f13d5b12b", - "hash": "87d6bd0b40f204d49803dad8e0d70611918d22354125c8f226a42670" + "compiledCode": "58980101002229800aba2aba1aab9faab9eaab9dab9a9bad002488888896600264646644b30013370e900018039baa0018992cc004c8cc004004dd6180618051baa300c0062259800800c528456600266ebcc034c02cdd51806800808c528c4cc008008c0380050092018899b894800001a29410071bae300a30083754003164018601000260106012002601000260086ea802229344d9590021", + "hash": "96b8e7848e41c82226a894a633bcdfd8f7fe73b24849288d1f0e47aa" }, { "title": "githoney_contract.githoney.spend", @@ -121,8 +121,8 @@ } } ], - "compiledCode": "f1a9d1de401b5004", - "hash": "7577273f99e7f3c9211d6342338d6316afc91689333c4d5099e7b2d1" + "compiledCode": "5914b7010100229800aba4aba2aba1aba0aab9faab9eaab9dab9a9bae002488888888966003300130043754015230083009300930093009300930090019b874800246010601260126012003370e90014dd2a40012300830090019180418049804800cdc3a40092300830093009300930093009300930090019119198008008019119801800980100148c020c024c024c024c024c02400644b30010018a4d13259800800c5268992cc004cdc81bae3007300b003375c600e0031330040043300a001300c0028b200c300a00140206014002803a4b30010018a518a50401922232330010010042259800800c40122653001375c6012003375a601400333003003300e002401060180028052444646600200200844b3001001880244ca60026eb8c0240066eacc02800666006006601c0048020c03000500a48c020c024c024c024c0240066e1d2001488888888888888888a60026034025301901291111919800800802912cc00400626603c66ec0dd48029ba60044bd6f7b63044ca60026eb8c0700066eacc07400660420049112cc004cdc8004801c4cc088cdd81ba9009374c01000b15980099b8f0090038992cc004c06cc080dd5000c4cc08ccdd81ba900a3024302137540020051002407d3001009804400900744cc088cdd81ba9003374c0046600c00c00280f101e0c07c00501d488c96600260260031323259800980f80140122c80e0dd7180e800980c9baa0038acc004c04400626464b3001301f00280245901c1bae301d0013019375400716405c80b8c05cdd5001488c8cc00400400c896600200314c103d87a8000899192cc004cdc8802800c56600266e3c014006260266603c603800497ae08a60103d87a80004069133004004302000340686eb8c068004c07400501b488966002602600314bd6f7b63044c8c8cc0040052f5bded8c044b300100189980f19bb0375200c6e9800d2f5bded8c113298009bae301c0019bab301d00198108012444b300133720014007133022337606ea4028dd3003802c56600266e3c02800e26604466ec0dd48051ba600700189981119bb037520066e98008cc01801800501e203c180f800a03a32330010014bd6f7b630112cc00400626603a66ec0dd48021ba80034bd6f7b63044ca60026eb8c06c0066eb4c07000660400049112cc004cdc8004001c4cc084cdd81ba9008375000e00b15980099b8f00800389981099bb037520106ea001c00626604266ec0dd48019ba800233006006001407480e8603c00280e101749660026022602c6ea80062942294501548896600200514a11329800992cc004c054c068dd5000c4dd6980d980f1bab301e301b3754003148001019180e800cdd7980e980f000cdd5801a444b30010018acc004c00801a26600a0069000452820368992cc004cdd7980e800a610140008acc004cc018010dd6980f18109bab301e001898019ba630220028a5040711598009980300224001130030078a50407080e0c08000501e0ca6002003004911980f0011980f1ba60014bd7020022225980080144cc005300103d87a80004bd6f7b63044ca60026eb8c0700066eacc07400660420069112cc004cdc8a441000038acc004cdc7a44100003899802980b998111ba60024bd70000c4cc015300103d87a8000006407919800803c006446600e0046604866ec0dd48029ba6004001401c80f0603e00480ea29422942294101e488c966002602260306ea8006266e24dd6980e180c9baa0010028a50405c603660306ea8c038c060dd500148888c8cc004004014896600200313301e337606ea4014dd400225eb7bdb1822653001375c6038003375a603a003302100248896600266e4002400e26604466ec0dd48049ba80080058acc004cdc7804801c4c966002603660406ea800626604666ec0dd4805181218109baa0010028801203e9800804c022004803a26604466ec0dd48019ba800233006006001407880f0603e00280ea600c00d29800800d2f5c1222980080140064446603e6e9ccc07cdd48031980f9ba90033301f375000497ae000140208022444b30010028a6103d87a80008acc004c04c0062602066036603800497ae08cc00400e603a005337000029000a006405c80d1222222222222298009813006cc098c09c03644453001004801c00a4453001003800c009004201e912cc004c07cc090dd500144c8c8c966002605800513300f302b003132598009811800c4c966002605c00313232598009813000c4c966002606200313301430300010098b205c302c37540051598009812000c4c8c8ca60026eb4c0c80066eb4c0c800e6eb4c0c800922259800981b002403a2c81986064002606200260586ea800a2c815102a18151baa001302d0018b2056302937540051598009810800c56600260526ea800a00b1640a916409c8138c09cdd5000c590291815000981500098129baa0028b2046911192cc004c07cc098dd5000c5200089bad302a302737540028128c966002603e604c6ea8006298103d87a8000899198008009bab302b3028375400444b30010018a6103d87a8000899192cc004cdc8803000c56600266e3c018006260446605a605600497ae08a60103d87a800040a5133004004302f00340a46eb8c0a4004c0b000502a204a3300c00300294c005220100a44100800a0129192cc004c0a400626eb0c0a00062c8130c00cdd5980818121baa0019800800a444444453001302a375400f232330010010022259800800c52f5bded8c113298009bae302e0019bab302f00199801801981980124453001001801cc8c8010c8cc0040040108966002003149a264b30010018acc004c010dd6981b181c80145268b2068899912cc004cdc81bae3037002375c606e00315980098031bad30380028998028029981d000981e001c59036459036181c801181c800a06e303900140d853001302d001a50a5140c522264034303100140bd2232598009819000c4c9660033001302798009bab3025302f3754604a605e6ea800600748810b73657474696e67734e46540040214a14a2816a2d13259800981218179baa0018992cc004c0d4006264b3001302c303137540031323232332259800981d801c4cc044c0e8014401a2c81c0dd6981c0009bad303800230380013037001303237540031640c060680031640c860606ea80062c8170c090c0bcdd5181298179baa00140b460620031640bc6eb0c08cc0b4dd5001488c8cc004004008896600200314a313233223322330020020012259800800c400e2653001375c6068003375a606a00333003003303900248896600266e22600201b008801a0220028acc00400629422941036456600200314a314a081b10360c0dc0050351bab3032003375c605e002660060066068004606400281824b30013020302b3754003132598009818800c4c9660026050605a6ea80062646464646464653001375660700033038006981c002cdd6981c0024dd6981c001cc966002606c00315980099b8948010c0d40062d1302e303500140d11640dc6ea8c0e000922222259800981f803c4cc088c0f80344cc0540144cc8966002606e003132598009821000c4cc060c10400400e2c81f8c0f4dd50034566002606a003159800981e9baa00680145903e45903b207613302d00622598008014404226464660626eacc0fc00889660020051300530450068991991180218240029bae3041001375a608400260880048210dd7181e8009820001207c303a37540091640f0303800130370013036001303500130340013033001302e37540031640b060600031640b860586ea80062c81524444b300130290018802466002009003991900118080009981899bb037520046ea00052f5bded8c122232598009811000c530103d87a8000898151981a9ba60014bd7020629800801401600922232598009818000c530103d87a8000898169981c1ba80014bd70206833700002004809900a205a911192cc004c0a4c0b8dd5000c4c96600266ebcc0ccc0c0dd5000802440062c8170c0c8c0bcdd5000c5902d19802001800a444444464b3001302b008899194c004566002605e60686ea80062646644b30013030303737540051323259800981f000c4c8c96600260686eb4c0f000a264b300130370018992cc0066002606c607a6ea8c0bcc0f8dd5003d28528a0788acc004cdd780398079819981f1baa0018acc004cc080c0c4c0f8dd50139bad302a303e375400f159800998061bab3034303e37540026eacc0d0c0f8dd5004c4cdc49919800800980f1bab3035303f375400444b30010018a40011337009001198010011822000a0824807a294103c452820788a5040f114a081e260026eb0c0c8c0f4dd50134c100c0f4dd50045200040311332259800981b801c4c96600264b30013375e002646460746608a608c0046608a608c0026608a607060866ea800cc11cc11c004c118004c104dd500544c00cc0d8c104dd5000c528207e3011303530403754003159800cc004dd5981b18201baa0019ba6330033756606c60806ea802cc06120809bee029119b87002001408d15980099811181998201baa029375a605860806ea802626048606a60806ea8026294103e4528207c8a5040f9300137586068607e6ea80a26084607e6ea802a9000201c899912cc004c0d401626644b30019800981d98211baa3034304337540194a14a2820a2b3001330033758600860866ea80b0c118c10cdd5006456600266ebcc050c0e0c10cdd50011919191919181f998251825802998251825802198251825801998251825801198251825800998254c00528d30103d87a8000a60103d8798000411860986098002609600260940026092002609000260866ea80322b300198009bab30393043375400530460019119b87002001409915980099812981b18219baa02c375a605e60866ea80322b300130053038304337540191598009980a9bab30393043375530013758607060866ea80b2607260866ea803290022024301b482026fb80a26602a6eacc0e4c10cdd54c004dd6181c18219baa02c982318219baa3301602c040a40048090dd598231823800c52820828a50410514a0820a2941041452820828a50410514a0820a60026eb0c0d8c104dd50154c110c104dd500652000404064646608a608c0026608a608c608e00297ae09800998029bab30383042375401a6034907fdaee02ccc110dd319802cc00402a00f48009027180d2410137dc0466088980101a0004bd70488a60020050019112cc004cdc780300844006264660966e98cc030dd598260014c00401e009337020060028170cc12cdd3198061bab304c304d0029800803c01200281712f5c066e0ccdc1001003a414138028231033205e375a607460826ea802a2b30013370e9003002c4c8cc89660033001303c30433754606a60886ea80369429450424566002660086eb0c014c110dd5016982398221baa00d8acc004c966002607e60886ea80062b30013301737566076608a6eaa60026eb0c0e8c114dd50174c120c114dd5000d20024050603a90404df7014400a294104344009043181c98221baa00d8acc004c0bcdd698239824001456600266e3cdd71823801004c4cdc79bae304700300c8a50410914a082122941042452820848a504108608c002660286eacc0e0c108dd54c004dd6181b98211baa02b981c18211baa00ba40008088c058dd5981e98211baa00b301802a8992cc004c0f0c104dd5000c4c8c966002607860866ea8c0d4c110dd5006c566002b3001302f375a608e609000315980099b8f375c608e00201313371e6eb8c11c00803229410424528208489980b1bab303a3044375530013758607260886ea80b6608e60886ea800e9000202698009bab303a3044375401f00c804d2001405114a0821229410421823800980c815c59040181b18209baa00a40fc81f88c10cc110c110c110c110c110c110c110c1100048966002607660806ea8006264646600200200844b30010018a508acc004cdc79bae30470010038a518998010011824000a08441146eb8c110c104dd5000c5903f207a22329800800c00e00480088896600200510018994c004012608e00798008014dd71821000cdd59821800c888c966002606c00314c103d87a80008981f198249ba60014bd70208a329800800c00e00480088896600200510018994c004012609c00798008014dd71824800cdd69825000c888c966002609000314c103d87a800089822998281ba80014bd7020983370000400281590041826001209440ac8020c11400904312cc004c0e0c0f4dd5000c528c528207840ec60766ea80662c81d0dd7181d000981e800c5903b192cc004c0c8c0e4dd5000c52f5bded8c113756607a60746ea80050381980f9bab302f303937540080026eb8c0ecc0e0dd5001459036181c981b1baa3039303637540026072606c6ea8008c0acc0d4dd5192cc004c0e8006260720031640dc64660020026eb0c0e4c0d8dd500f912cc004006297ae0899912cc00566002606260706ea8c0f0c0e4dd5181e181c9baa302f3039375400514a314a081ba2660760046600800800313300400400140dc6074002607600281c22c819a60686ea804a60700049112cc004c0c800a2b30013038375402b0038b20728acc004c0c000a2b30013038375402b0038b20728acc004c0b000a2b30013038375402b0038b20728acc004cdc3a400c005159800981c1baa015801c59039456600266e1d20080028acc004c0e0dd500ac00e2c81ca2c81b1036206c40d881b0606e607000260666ea803e2b3001302d008899194c004dd7181c000cdd6981c181c800cdd7181c181a9baa0119bae30380024888966002604800714a313233225980099b89375a606460786ea800660026eacc0c8c0f0dd54c004dd61818981e1baa025981f981e1baa001a4004805a910100a441004055159800981a0034566002b300159800981a181d9baa302d303c375400514a119800a50a50a5140e881d22b30013301e302f303c375404a6eb4c0a0c0f0dd500145660026040606260786ea800a266e1cdd6981a981e1baa002375a606260786ea8006294103a452820748a5040e9159800998051bab3032303c375400730013014482026fb80a009007a400480623300130103756606e60786ea800a6e9a60026eacc0c8c0f0dd5001c01200f4800500c488cdc3801000a03e8a5040e914a081d2294103a45282074300b302f303a37540026601a04606f30013758605c60726ea808a60606607666e9520023303b375200297ae03303b4c103d87a80004bd7052000402081b8607000260140391640c4818889660026600c00400319800cc00400a6e980064466e1c008005017528528a0648a5040c81149a26cac8011", + "hash": "72f5c8da799d628b55f237451ae44b4311b8f4b2b06ab57b3fef5947" }, { "title": "githoney_contract.githoney.mint", @@ -140,8 +140,8 @@ } } ], - "compiledCode": "f1f665b7b5ec44d6", - "hash": "7577273f99e7f3c9211d6342338d6316afc91689333c4d5099e7b2d1" + "compiledCode": "5914b7010100229800aba4aba2aba1aba0aab9faab9eaab9dab9a9bae002488888888966003300130043754015230083009300930093009300930090019b874800246010601260126012003370e90014dd2a40012300830090019180418049804800cdc3a40092300830093009300930093009300930090019119198008008019119801800980100148c020c024c024c024c024c02400644b30010018a4d13259800800c5268992cc004cdc81bae3007300b003375c600e0031330040043300a001300c0028b200c300a00140206014002803a4b30010018a518a50401922232330010010042259800800c40122653001375c6012003375a601400333003003300e002401060180028052444646600200200844b3001001880244ca60026eb8c0240066eacc02800666006006601c0048020c03000500a48c020c024c024c024c0240066e1d2001488888888888888888a60026034025301901291111919800800802912cc00400626603c66ec0dd48029ba60044bd6f7b63044ca60026eb8c0700066eacc07400660420049112cc004cdc8004801c4cc088cdd81ba9009374c01000b15980099b8f0090038992cc004c06cc080dd5000c4cc08ccdd81ba900a3024302137540020051002407d3001009804400900744cc088cdd81ba9003374c0046600c00c00280f101e0c07c00501d488c96600260260031323259800980f80140122c80e0dd7180e800980c9baa0038acc004c04400626464b3001301f00280245901c1bae301d0013019375400716405c80b8c05cdd5001488c8cc00400400c896600200314c103d87a8000899192cc004cdc8802800c56600266e3c014006260266603c603800497ae08a60103d87a80004069133004004302000340686eb8c068004c07400501b488966002602600314bd6f7b63044c8c8cc0040052f5bded8c044b300100189980f19bb0375200c6e9800d2f5bded8c113298009bae301c0019bab301d00198108012444b300133720014007133022337606ea4028dd3003802c56600266e3c02800e26604466ec0dd48051ba600700189981119bb037520066e98008cc01801800501e203c180f800a03a32330010014bd6f7b630112cc00400626603a66ec0dd48021ba80034bd6f7b63044ca60026eb8c06c0066eb4c07000660400049112cc004cdc8004001c4cc084cdd81ba9008375000e00b15980099b8f00800389981099bb037520106ea001c00626604266ec0dd48019ba800233006006001407480e8603c00280e101749660026022602c6ea80062942294501548896600200514a11329800992cc004c054c068dd5000c4dd6980d980f1bab301e301b3754003148001019180e800cdd7980e980f000cdd5801a444b30010018acc004c00801a26600a0069000452820368992cc004cdd7980e800a610140008acc004cc018010dd6980f18109bab301e001898019ba630220028a5040711598009980300224001130030078a50407080e0c08000501e0ca6002003004911980f0011980f1ba60014bd7020022225980080144cc005300103d87a80004bd6f7b63044ca60026eb8c0700066eacc07400660420069112cc004cdc8a441000038acc004cdc7a44100003899802980b998111ba60024bd70000c4cc015300103d87a8000006407919800803c006446600e0046604866ec0dd48029ba6004001401c80f0603e00480ea29422942294101e488c966002602260306ea8006266e24dd6980e180c9baa0010028a50405c603660306ea8c038c060dd500148888c8cc004004014896600200313301e337606ea4014dd400225eb7bdb1822653001375c6038003375a603a003302100248896600266e4002400e26604466ec0dd48049ba80080058acc004cdc7804801c4c966002603660406ea800626604666ec0dd4805181218109baa0010028801203e9800804c022004803a26604466ec0dd48019ba800233006006001407880f0603e00280ea600c00d29800800d2f5c1222980080140064446603e6e9ccc07cdd48031980f9ba90033301f375000497ae000140208022444b30010028a6103d87a80008acc004c04c0062602066036603800497ae08cc00400e603a005337000029000a006405c80d1222222222222298009813006cc098c09c03644453001004801c00a4453001003800c009004201e912cc004c07cc090dd500144c8c8c966002605800513300f302b003132598009811800c4c966002605c00313232598009813000c4c966002606200313301430300010098b205c302c37540051598009812000c4c8c8ca60026eb4c0c80066eb4c0c800e6eb4c0c800922259800981b002403a2c81986064002606200260586ea800a2c815102a18151baa001302d0018b2056302937540051598009810800c56600260526ea800a00b1640a916409c8138c09cdd5000c590291815000981500098129baa0028b2046911192cc004c07cc098dd5000c5200089bad302a302737540028128c966002603e604c6ea8006298103d87a8000899198008009bab302b3028375400444b30010018a6103d87a8000899192cc004cdc8803000c56600266e3c018006260446605a605600497ae08a60103d87a800040a5133004004302f00340a46eb8c0a4004c0b000502a204a3300c00300294c005220100a44100800a0129192cc004c0a400626eb0c0a00062c8130c00cdd5980818121baa0019800800a444444453001302a375400f232330010010022259800800c52f5bded8c113298009bae302e0019bab302f00199801801981980124453001001801cc8c8010c8cc0040040108966002003149a264b30010018acc004c010dd6981b181c80145268b2068899912cc004cdc81bae3037002375c606e00315980098031bad30380028998028029981d000981e001c59036459036181c801181c800a06e303900140d853001302d001a50a5140c522264034303100140bd2232598009819000c4c9660033001302798009bab3025302f3754604a605e6ea800600748810b73657474696e67734e46540040214a14a2816a2d13259800981218179baa0018992cc004c0d4006264b3001302c303137540031323232332259800981d801c4cc044c0e8014401a2c81c0dd6981c0009bad303800230380013037001303237540031640c060680031640c860606ea80062c8170c090c0bcdd5181298179baa00140b460620031640bc6eb0c08cc0b4dd5001488c8cc004004008896600200314a313233223322330020020012259800800c400e2653001375c6068003375a606a00333003003303900248896600266e22600201b008801a0220028acc00400629422941036456600200314a314a081b10360c0dc0050351bab3032003375c605e002660060066068004606400281824b30013020302b3754003132598009818800c4c9660026050605a6ea80062646464646464653001375660700033038006981c002cdd6981c0024dd6981c001cc966002606c00315980099b8948010c0d40062d1302e303500140d11640dc6ea8c0e000922222259800981f803c4cc088c0f80344cc0540144cc8966002606e003132598009821000c4cc060c10400400e2c81f8c0f4dd50034566002606a003159800981e9baa00680145903e45903b207613302d00622598008014404226464660626eacc0fc00889660020051300530450068991991180218240029bae3041001375a608400260880048210dd7181e8009820001207c303a37540091640f0303800130370013036001303500130340013033001302e37540031640b060600031640b860586ea80062c81524444b300130290018802466002009003991900118080009981899bb037520046ea00052f5bded8c122232598009811000c530103d87a8000898151981a9ba60014bd7020629800801401600922232598009818000c530103d87a8000898169981c1ba80014bd70206833700002004809900a205a911192cc004c0a4c0b8dd5000c4c96600266ebcc0ccc0c0dd5000802440062c8170c0c8c0bcdd5000c5902d19802001800a444444464b3001302b008899194c004566002605e60686ea80062646644b30013030303737540051323259800981f000c4c8c96600260686eb4c0f000a264b300130370018992cc0066002606c607a6ea8c0bcc0f8dd5003d28528a0788acc004cdd780398079819981f1baa0018acc004cc080c0c4c0f8dd50139bad302a303e375400f159800998061bab3034303e37540026eacc0d0c0f8dd5004c4cdc49919800800980f1bab3035303f375400444b30010018a40011337009001198010011822000a0824807a294103c452820788a5040f114a081e260026eb0c0c8c0f4dd50134c100c0f4dd50045200040311332259800981b801c4c96600264b30013375e002646460746608a608c0046608a608c0026608a607060866ea800cc11cc11c004c118004c104dd500544c00cc0d8c104dd5000c528207e3011303530403754003159800cc004dd5981b18201baa0019ba6330033756606c60806ea802cc06120809bee029119b87002001408d15980099811181998201baa029375a605860806ea802626048606a60806ea8026294103e4528207c8a5040f9300137586068607e6ea80a26084607e6ea802a9000201c899912cc004c0d401626644b30019800981d98211baa3034304337540194a14a2820a2b3001330033758600860866ea80b0c118c10cdd5006456600266ebcc050c0e0c10cdd50011919191919181f998251825802998251825802198251825801998251825801198251825800998254c00528d30103d87a8000a60103d8798000411860986098002609600260940026092002609000260866ea80322b300198009bab30393043375400530460019119b87002001409915980099812981b18219baa02c375a605e60866ea80322b300130053038304337540191598009980a9bab30393043375530013758607060866ea80b2607260866ea803290022024301b482026fb80a26602a6eacc0e4c10cdd54c004dd6181c18219baa02c982318219baa3301602c040a40048090dd598231823800c52820828a50410514a0820a2941041452820828a50410514a0820a60026eb0c0d8c104dd50154c110c104dd500652000404064646608a608c0026608a608c608e00297ae09800998029bab30383042375401a6034907fdaee02ccc110dd319802cc00402a00f48009027180d2410137dc0466088980101a0004bd70488a60020050019112cc004cdc780300844006264660966e98cc030dd598260014c00401e009337020060028170cc12cdd3198061bab304c304d0029800803c01200281712f5c066e0ccdc1001003a414138028231033205e375a607460826ea802a2b30013370e9003002c4c8cc89660033001303c30433754606a60886ea80369429450424566002660086eb0c014c110dd5016982398221baa00d8acc004c966002607e60886ea80062b30013301737566076608a6eaa60026eb0c0e8c114dd50174c120c114dd5000d20024050603a90404df7014400a294104344009043181c98221baa00d8acc004c0bcdd698239824001456600266e3cdd71823801004c4cdc79bae304700300c8a50410914a082122941042452820848a504108608c002660286eacc0e0c108dd54c004dd6181b98211baa02b981c18211baa00ba40008088c058dd5981e98211baa00b301802a8992cc004c0f0c104dd5000c4c8c966002607860866ea8c0d4c110dd5006c566002b3001302f375a608e609000315980099b8f375c608e00201313371e6eb8c11c00803229410424528208489980b1bab303a3044375530013758607260886ea80b6608e60886ea800e9000202698009bab303a3044375401f00c804d2001405114a0821229410421823800980c815c59040181b18209baa00a40fc81f88c10cc110c110c110c110c110c110c110c1100048966002607660806ea8006264646600200200844b30010018a508acc004cdc79bae30470010038a518998010011824000a08441146eb8c110c104dd5000c5903f207a22329800800c00e00480088896600200510018994c004012608e00798008014dd71821000cdd59821800c888c966002606c00314c103d87a80008981f198249ba60014bd70208a329800800c00e00480088896600200510018994c004012609c00798008014dd71824800cdd69825000c888c966002609000314c103d87a800089822998281ba80014bd7020983370000400281590041826001209440ac8020c11400904312cc004c0e0c0f4dd5000c528c528207840ec60766ea80662c81d0dd7181d000981e800c5903b192cc004c0c8c0e4dd5000c52f5bded8c113756607a60746ea80050381980f9bab302f303937540080026eb8c0ecc0e0dd5001459036181c981b1baa3039303637540026072606c6ea8008c0acc0d4dd5192cc004c0e8006260720031640dc64660020026eb0c0e4c0d8dd500f912cc004006297ae0899912cc00566002606260706ea8c0f0c0e4dd5181e181c9baa302f3039375400514a314a081ba2660760046600800800313300400400140dc6074002607600281c22c819a60686ea804a60700049112cc004c0c800a2b30013038375402b0038b20728acc004c0c000a2b30013038375402b0038b20728acc004c0b000a2b30013038375402b0038b20728acc004cdc3a400c005159800981c1baa015801c59039456600266e1d20080028acc004c0e0dd500ac00e2c81ca2c81b1036206c40d881b0606e607000260666ea803e2b3001302d008899194c004dd7181c000cdd6981c181c800cdd7181c181a9baa0119bae30380024888966002604800714a313233225980099b89375a606460786ea800660026eacc0c8c0f0dd54c004dd61818981e1baa025981f981e1baa001a4004805a910100a441004055159800981a0034566002b300159800981a181d9baa302d303c375400514a119800a50a50a5140e881d22b30013301e302f303c375404a6eb4c0a0c0f0dd500145660026040606260786ea800a266e1cdd6981a981e1baa002375a606260786ea8006294103a452820748a5040e9159800998051bab3032303c375400730013014482026fb80a009007a400480623300130103756606e60786ea800a6e9a60026eacc0c8c0f0dd5001c01200f4800500c488cdc3801000a03e8a5040e914a081d2294103a45282074300b302f303a37540026601a04606f30013758605c60726ea808a60606607666e9520023303b375200297ae03303b4c103d87a80004bd7052000402081b8607000260140391640c4818889660026600c00400319800cc00400a6e980064466e1c008005017528528a0648a5040c81149a26cac8011", + "hash": "72f5c8da799d628b55f237451ae44b4311b8f4b2b06ab57b3fef5947" }, { "title": "githoney_contract.githoney.else", @@ -156,8 +156,8 @@ } } ], - "compiledCode": "8dc98413cbd1b43f", - "hash": "7577273f99e7f3c9211d6342338d6316afc91689333c4d5099e7b2d1" + "compiledCode": "5914b7010100229800aba4aba2aba1aba0aab9faab9eaab9dab9a9bae002488888888966003300130043754015230083009300930093009300930090019b874800246010601260126012003370e90014dd2a40012300830090019180418049804800cdc3a40092300830093009300930093009300930090019119198008008019119801800980100148c020c024c024c024c024c02400644b30010018a4d13259800800c5268992cc004cdc81bae3007300b003375c600e0031330040043300a001300c0028b200c300a00140206014002803a4b30010018a518a50401922232330010010042259800800c40122653001375c6012003375a601400333003003300e002401060180028052444646600200200844b3001001880244ca60026eb8c0240066eacc02800666006006601c0048020c03000500a48c020c024c024c024c0240066e1d2001488888888888888888a60026034025301901291111919800800802912cc00400626603c66ec0dd48029ba60044bd6f7b63044ca60026eb8c0700066eacc07400660420049112cc004cdc8004801c4cc088cdd81ba9009374c01000b15980099b8f0090038992cc004c06cc080dd5000c4cc08ccdd81ba900a3024302137540020051002407d3001009804400900744cc088cdd81ba9003374c0046600c00c00280f101e0c07c00501d488c96600260260031323259800980f80140122c80e0dd7180e800980c9baa0038acc004c04400626464b3001301f00280245901c1bae301d0013019375400716405c80b8c05cdd5001488c8cc00400400c896600200314c103d87a8000899192cc004cdc8802800c56600266e3c014006260266603c603800497ae08a60103d87a80004069133004004302000340686eb8c068004c07400501b488966002602600314bd6f7b63044c8c8cc0040052f5bded8c044b300100189980f19bb0375200c6e9800d2f5bded8c113298009bae301c0019bab301d00198108012444b300133720014007133022337606ea4028dd3003802c56600266e3c02800e26604466ec0dd48051ba600700189981119bb037520066e98008cc01801800501e203c180f800a03a32330010014bd6f7b630112cc00400626603a66ec0dd48021ba80034bd6f7b63044ca60026eb8c06c0066eb4c07000660400049112cc004cdc8004001c4cc084cdd81ba9008375000e00b15980099b8f00800389981099bb037520106ea001c00626604266ec0dd48019ba800233006006001407480e8603c00280e101749660026022602c6ea80062942294501548896600200514a11329800992cc004c054c068dd5000c4dd6980d980f1bab301e301b3754003148001019180e800cdd7980e980f000cdd5801a444b30010018acc004c00801a26600a0069000452820368992cc004cdd7980e800a610140008acc004cc018010dd6980f18109bab301e001898019ba630220028a5040711598009980300224001130030078a50407080e0c08000501e0ca6002003004911980f0011980f1ba60014bd7020022225980080144cc005300103d87a80004bd6f7b63044ca60026eb8c0700066eacc07400660420069112cc004cdc8a441000038acc004cdc7a44100003899802980b998111ba60024bd70000c4cc015300103d87a8000006407919800803c006446600e0046604866ec0dd48029ba6004001401c80f0603e00480ea29422942294101e488c966002602260306ea8006266e24dd6980e180c9baa0010028a50405c603660306ea8c038c060dd500148888c8cc004004014896600200313301e337606ea4014dd400225eb7bdb1822653001375c6038003375a603a003302100248896600266e4002400e26604466ec0dd48049ba80080058acc004cdc7804801c4c966002603660406ea800626604666ec0dd4805181218109baa0010028801203e9800804c022004803a26604466ec0dd48019ba800233006006001407880f0603e00280ea600c00d29800800d2f5c1222980080140064446603e6e9ccc07cdd48031980f9ba90033301f375000497ae000140208022444b30010028a6103d87a80008acc004c04c0062602066036603800497ae08cc00400e603a005337000029000a006405c80d1222222222222298009813006cc098c09c03644453001004801c00a4453001003800c009004201e912cc004c07cc090dd500144c8c8c966002605800513300f302b003132598009811800c4c966002605c00313232598009813000c4c966002606200313301430300010098b205c302c37540051598009812000c4c8c8ca60026eb4c0c80066eb4c0c800e6eb4c0c800922259800981b002403a2c81986064002606200260586ea800a2c815102a18151baa001302d0018b2056302937540051598009810800c56600260526ea800a00b1640a916409c8138c09cdd5000c590291815000981500098129baa0028b2046911192cc004c07cc098dd5000c5200089bad302a302737540028128c966002603e604c6ea8006298103d87a8000899198008009bab302b3028375400444b30010018a6103d87a8000899192cc004cdc8803000c56600266e3c018006260446605a605600497ae08a60103d87a800040a5133004004302f00340a46eb8c0a4004c0b000502a204a3300c00300294c005220100a44100800a0129192cc004c0a400626eb0c0a00062c8130c00cdd5980818121baa0019800800a444444453001302a375400f232330010010022259800800c52f5bded8c113298009bae302e0019bab302f00199801801981980124453001001801cc8c8010c8cc0040040108966002003149a264b30010018acc004c010dd6981b181c80145268b2068899912cc004cdc81bae3037002375c606e00315980098031bad30380028998028029981d000981e001c59036459036181c801181c800a06e303900140d853001302d001a50a5140c522264034303100140bd2232598009819000c4c9660033001302798009bab3025302f3754604a605e6ea800600748810b73657474696e67734e46540040214a14a2816a2d13259800981218179baa0018992cc004c0d4006264b3001302c303137540031323232332259800981d801c4cc044c0e8014401a2c81c0dd6981c0009bad303800230380013037001303237540031640c060680031640c860606ea80062c8170c090c0bcdd5181298179baa00140b460620031640bc6eb0c08cc0b4dd5001488c8cc004004008896600200314a313233223322330020020012259800800c400e2653001375c6068003375a606a00333003003303900248896600266e22600201b008801a0220028acc00400629422941036456600200314a314a081b10360c0dc0050351bab3032003375c605e002660060066068004606400281824b30013020302b3754003132598009818800c4c9660026050605a6ea80062646464646464653001375660700033038006981c002cdd6981c0024dd6981c001cc966002606c00315980099b8948010c0d40062d1302e303500140d11640dc6ea8c0e000922222259800981f803c4cc088c0f80344cc0540144cc8966002606e003132598009821000c4cc060c10400400e2c81f8c0f4dd50034566002606a003159800981e9baa00680145903e45903b207613302d00622598008014404226464660626eacc0fc00889660020051300530450068991991180218240029bae3041001375a608400260880048210dd7181e8009820001207c303a37540091640f0303800130370013036001303500130340013033001302e37540031640b060600031640b860586ea80062c81524444b300130290018802466002009003991900118080009981899bb037520046ea00052f5bded8c122232598009811000c530103d87a8000898151981a9ba60014bd7020629800801401600922232598009818000c530103d87a8000898169981c1ba80014bd70206833700002004809900a205a911192cc004c0a4c0b8dd5000c4c96600266ebcc0ccc0c0dd5000802440062c8170c0c8c0bcdd5000c5902d19802001800a444444464b3001302b008899194c004566002605e60686ea80062646644b30013030303737540051323259800981f000c4c8c96600260686eb4c0f000a264b300130370018992cc0066002606c607a6ea8c0bcc0f8dd5003d28528a0788acc004cdd780398079819981f1baa0018acc004cc080c0c4c0f8dd50139bad302a303e375400f159800998061bab3034303e37540026eacc0d0c0f8dd5004c4cdc49919800800980f1bab3035303f375400444b30010018a40011337009001198010011822000a0824807a294103c452820788a5040f114a081e260026eb0c0c8c0f4dd50134c100c0f4dd50045200040311332259800981b801c4c96600264b30013375e002646460746608a608c0046608a608c0026608a607060866ea800cc11cc11c004c118004c104dd500544c00cc0d8c104dd5000c528207e3011303530403754003159800cc004dd5981b18201baa0019ba6330033756606c60806ea802cc06120809bee029119b87002001408d15980099811181998201baa029375a605860806ea802626048606a60806ea8026294103e4528207c8a5040f9300137586068607e6ea80a26084607e6ea802a9000201c899912cc004c0d401626644b30019800981d98211baa3034304337540194a14a2820a2b3001330033758600860866ea80b0c118c10cdd5006456600266ebcc050c0e0c10cdd50011919191919181f998251825802998251825802198251825801998251825801198251825800998254c00528d30103d87a8000a60103d8798000411860986098002609600260940026092002609000260866ea80322b300198009bab30393043375400530460019119b87002001409915980099812981b18219baa02c375a605e60866ea80322b300130053038304337540191598009980a9bab30393043375530013758607060866ea80b2607260866ea803290022024301b482026fb80a26602a6eacc0e4c10cdd54c004dd6181c18219baa02c982318219baa3301602c040a40048090dd598231823800c52820828a50410514a0820a2941041452820828a50410514a0820a60026eb0c0d8c104dd50154c110c104dd500652000404064646608a608c0026608a608c608e00297ae09800998029bab30383042375401a6034907fdaee02ccc110dd319802cc00402a00f48009027180d2410137dc0466088980101a0004bd70488a60020050019112cc004cdc780300844006264660966e98cc030dd598260014c00401e009337020060028170cc12cdd3198061bab304c304d0029800803c01200281712f5c066e0ccdc1001003a414138028231033205e375a607460826ea802a2b30013370e9003002c4c8cc89660033001303c30433754606a60886ea80369429450424566002660086eb0c014c110dd5016982398221baa00d8acc004c966002607e60886ea80062b30013301737566076608a6eaa60026eb0c0e8c114dd50174c120c114dd5000d20024050603a90404df7014400a294104344009043181c98221baa00d8acc004c0bcdd698239824001456600266e3cdd71823801004c4cdc79bae304700300c8a50410914a082122941042452820848a504108608c002660286eacc0e0c108dd54c004dd6181b98211baa02b981c18211baa00ba40008088c058dd5981e98211baa00b301802a8992cc004c0f0c104dd5000c4c8c966002607860866ea8c0d4c110dd5006c566002b3001302f375a608e609000315980099b8f375c608e00201313371e6eb8c11c00803229410424528208489980b1bab303a3044375530013758607260886ea80b6608e60886ea800e9000202698009bab303a3044375401f00c804d2001405114a0821229410421823800980c815c59040181b18209baa00a40fc81f88c10cc110c110c110c110c110c110c110c1100048966002607660806ea8006264646600200200844b30010018a508acc004cdc79bae30470010038a518998010011824000a08441146eb8c110c104dd5000c5903f207a22329800800c00e00480088896600200510018994c004012608e00798008014dd71821000cdd59821800c888c966002606c00314c103d87a80008981f198249ba60014bd70208a329800800c00e00480088896600200510018994c004012609c00798008014dd71824800cdd69825000c888c966002609000314c103d87a800089822998281ba80014bd7020983370000400281590041826001209440ac8020c11400904312cc004c0e0c0f4dd5000c528c528207840ec60766ea80662c81d0dd7181d000981e800c5903b192cc004c0c8c0e4dd5000c52f5bded8c113756607a60746ea80050381980f9bab302f303937540080026eb8c0ecc0e0dd5001459036181c981b1baa3039303637540026072606c6ea8008c0acc0d4dd5192cc004c0e8006260720031640dc64660020026eb0c0e4c0d8dd500f912cc004006297ae0899912cc00566002606260706ea8c0f0c0e4dd5181e181c9baa302f3039375400514a314a081ba2660760046600800800313300400400140dc6074002607600281c22c819a60686ea804a60700049112cc004c0c800a2b30013038375402b0038b20728acc004c0c000a2b30013038375402b0038b20728acc004c0b000a2b30013038375402b0038b20728acc004cdc3a400c005159800981c1baa015801c59039456600266e1d20080028acc004c0e0dd500ac00e2c81ca2c81b1036206c40d881b0606e607000260666ea803e2b3001302d008899194c004dd7181c000cdd6981c181c800cdd7181c181a9baa0119bae30380024888966002604800714a313233225980099b89375a606460786ea800660026eacc0c8c0f0dd54c004dd61818981e1baa025981f981e1baa001a4004805a910100a441004055159800981a0034566002b300159800981a181d9baa302d303c375400514a119800a50a50a5140e881d22b30013301e302f303c375404a6eb4c0a0c0f0dd500145660026040606260786ea800a266e1cdd6981a981e1baa002375a606260786ea8006294103a452820748a5040e9159800998051bab3032303c375400730013014482026fb80a009007a400480623300130103756606e60786ea800a6e9a60026eacc0c8c0f0dd5001c01200f4800500c488cdc3801000a03e8a5040e914a081d2294103a45282074300b302f303a37540026601a04606f30013758605c60726ea808a60606607666e9520023303b375200297ae03303b4c103d87a80004bd7052000402081b8607000260140391640c4818889660026600c00400319800cc00400a6e980064466e1c008005017528528a0648a5040c81149a26cac8011", + "hash": "72f5c8da799d628b55f237451ae44b4311b8f4b2b06ab57b3fef5947" }, { "title": "githoney_contract.settings.spend", @@ -173,16 +173,16 @@ "$ref": "#/definitions/types~1SettingsRedeemers" } }, - "compiledCode": "5eb78784a02ee1a0", - "hash": "049f1a09fb535089fdd9df98b4b0975d1081f9afc2d6f59ad2f9c208" + "compiledCode": "59052a01010029800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cdc3a400530080024888966002600460106ea800e26466453001159800980098059baa0028994c004c004c034dd5192cc004c0480062602200316403c64660020026eb0c044c038dd5003112cc004006297ae0899912cc00566002601460206ea8c050c044dd5180a18089baa30053011375400514a314a0807a26602600466008008003133004004001403c6024002602600280824646600200200444b30010018a5eb7bdb1822653001375c602000337566022003301500248896600266e45221000038acc004cdc7a441000038800c401501244cc058cdd81ba9003374c0046600c00c00280906026002808a4646600200200444b30010018a5eb82264664466446600400400244b30010018801c4c8cc060dd39980c1ba90053301830150013301830160014bd7019801801980d001180c000a02c375660260066eb8c040004cc00c00cc054008c04c005011244464b30013015001899192cc004c030dd6980b180b980b80144c966002601260266ea803e2646464b3001301b0018992cc004c044dd6980d980e180e000c4c96600266e1d2004301837540031980091192cc004c04400626464b3001302200280245901f1bae3020001301c3754007159800980a800c4c8c96600260440050048b203e375c604000260386ea800e2c80d101a180d1baa002980e180c9baa0018acc00401a266e3cdd7180e001003c528202e488966002602260366ea800a26464646644b300130250038992cc004c05cc084dd5000c4c8c8c966002605200513300c302800313259800980d800c4c96600260560031323259800980f000c4c966002605c003133011302d00100f8b2056302937540051598009811000c4c8c8ca60026eb4c0bc0066eb4c0bc00e6eb4c0bc00922259800981980240522c8180605e002605c00260526ea800a2c813902718139baa001302a0018b205030263754005159800980f800c566002604c6ea800a01716409d1640908120c090dd5000c590261813800981380098111baa0018b204030240058b2044375a60440026eb4c088008c088004c084004c070dd500145901a22c80b8c010c060dd5001c590161bac301a0018b20303007300837566014602c6ea8004c8c9660026018602c6ea8006264b30013375e603660306ea8004c06cc060dd5005c40062c80b0c068c05cdd5000c590154c0040066eb0c008c058dd5007520004004444b30010028a60103d87a80008acc004c034006266e9520003301a301b0024bd70466002007301c00299b8000148005003202c406446030603260320031323259800acc004cdc39bad3019301a001480062b30013371e6eb8c0640052210b73657474696e67734e465400899b8f375c603200400914a080a229410144400e2941014180c800992cc004c06400626eb0c0600062c80b0c014dd5980b980c180c180c180c180a1baa00c40486644b3001300a3014375400313232330010010042259800800c528456600266e3cdd7180d800801c528c4cc008008c0700050162032375c6030602a6ea80062c8098dd6180b180b980b980b980b980b980b980b980b98099baa00b301630133754602c60266ea8c058c04cdd5004c590111bae301500137586028003164048600260046eacc010c040dd500188c040c0440062c805260166ea801e601e0069112cc004c01000a2b3001300f37540150038b20208acc004c02000a2b3001300f37540150038b20208b201a4034300d300e001370e900018049baa0038b200e180400098019baa0088a4d1365640041", + "hash": "e44c3a699837b5ff631a5aaaf9f32209d6194c8075b8d7dee62f3420" }, { "title": "githoney_contract.settings.else", "redeemer": { "schema": {} }, - "compiledCode": "9813b3c286674b27", - "hash": "049f1a09fb535089fdd9df98b4b0975d1081f9afc2d6f59ad2f9c208" + "compiledCode": "59052a01010029800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cdc3a400530080024888966002600460106ea800e26466453001159800980098059baa0028994c004c004c034dd5192cc004c0480062602200316403c64660020026eb0c044c038dd5003112cc004006297ae0899912cc00566002601460206ea8c050c044dd5180a18089baa30053011375400514a314a0807a26602600466008008003133004004001403c6024002602600280824646600200200444b30010018a5eb7bdb1822653001375c602000337566022003301500248896600266e45221000038acc004cdc7a441000038800c401501244cc058cdd81ba9003374c0046600c00c00280906026002808a4646600200200444b30010018a5eb82264664466446600400400244b30010018801c4c8cc060dd39980c1ba90053301830150013301830160014bd7019801801980d001180c000a02c375660260066eb8c040004cc00c00cc054008c04c005011244464b30013015001899192cc004c030dd6980b180b980b80144c966002601260266ea803e2646464b3001301b0018992cc004c044dd6980d980e180e000c4c96600266e1d2004301837540031980091192cc004c04400626464b3001302200280245901f1bae3020001301c3754007159800980a800c4c8c96600260440050048b203e375c604000260386ea800e2c80d101a180d1baa002980e180c9baa0018acc00401a266e3cdd7180e001003c528202e488966002602260366ea800a26464646644b300130250038992cc004c05cc084dd5000c4c8c8c966002605200513300c302800313259800980d800c4c96600260560031323259800980f000c4c966002605c003133011302d00100f8b2056302937540051598009811000c4c8c8ca60026eb4c0bc0066eb4c0bc00e6eb4c0bc00922259800981980240522c8180605e002605c00260526ea800a2c813902718139baa001302a0018b205030263754005159800980f800c566002604c6ea800a01716409d1640908120c090dd5000c590261813800981380098111baa0018b204030240058b2044375a60440026eb4c088008c088004c084004c070dd500145901a22c80b8c010c060dd5001c590161bac301a0018b20303007300837566014602c6ea8004c8c9660026018602c6ea8006264b30013375e603660306ea8004c06cc060dd5005c40062c80b0c068c05cdd5000c590154c0040066eb0c008c058dd5007520004004444b30010028a60103d87a80008acc004c034006266e9520003301a301b0024bd70466002007301c00299b8000148005003202c406446030603260320031323259800acc004cdc39bad3019301a001480062b30013371e6eb8c0640052210b73657474696e67734e465400899b8f375c603200400914a080a229410144400e2941014180c800992cc004c06400626eb0c0600062c80b0c014dd5980b980c180c180c180c180a1baa00c40486644b3001300a3014375400313232330010010042259800800c528456600266e3cdd7180d800801c528c4cc008008c0700050162032375c6030602a6ea80062c8098dd6180b180b980b980b980b980b980b980b980b98099baa00b301630133754602c60266ea8c058c04cdd5004c590111bae301500137586028003164048600260046eacc010c040dd500188c040c0440062c805260166ea801e601e0069112cc004c01000a2b3001300f37540150038b20208acc004c02000a2b3001300f37540150038b20208b201a4034300d300e001370e900018049baa0038b200e180400098019baa0088a4d1365640041", + "hash": "e44c3a699837b5ff631a5aaaf9f32209d6194c8075b8d7dee62f3420" }, { "title": "githoney_contract.settings_minting.mint", @@ -206,8 +206,8 @@ } } ], - "compiledCode": "6396e66bd8b6ac88", - "hash": "15721f473d73adf918aa7541871739c2e8df0a60abe023411cf9f1b0" + "compiledCode": "5904230101002229800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cdc3a400130080024888966002600460106ea800e26644b30013370e0029000c528c4cc88c8c96600266e1d2004300e3754003132980091192cc004c03000626464b300130190028024590161bae3017001301337540071598009802000c4c8c96600260320050048b202c375c602e00260266ea800e2c808901118089baa0028acc004c00401a2b300132330010013758602860226ea8024896600200314a115980099baf301530123754602a00203314a313300200230160014040809a2600264b300130023010375400314800226eb4c050c044dd5000a01e3259800980118081baa0018a60103d87a8000899198008009bab30153012375400444b30010018a6103d87a8000899192cc004cdc8a450b73657474696e67734e4654000018acc004cdc7a4410b73657474696e67734e465400001898049980b980a80125eb82298103d87a8000404d1330040043019003404c6eb8c04c004c058005014201e323300100137566028602a60226ea8010896600200314c103d87a8000899192cc004cdc8805800c56600266e3c02c006260106602c602800497ae08a60103d87a80004049133004004301800340486eb8c048004c0540050134528201c8a5040393013301037540049112cc004c030c048dd5000c4c8c8c8cc89660026038007132598009809180c1baa00189919192cc004c08000a266018603e006264b300130160018992cc004c08800626464b300130190018992cc004c094006266022604800202116408860406ea800a2b3001301100189919194c004dd69813000cdd69813001cdd698130012444b3001302a00480ac590270c098004c094004c080dd500145901e203c301e3754002604200316407c603a6ea800a2b3001300e0018acc004c074dd500140322c80f22c80d901b180d9baa0018b203a301e001301e0013019375400316405c603600b1640646eb4c064004dd6980c801180c800980c00098099baa0018b20221b874800a2c8068c00cc038dd500099192cc004c020c038dd5000c4c96600266ebcc04cc040dd500080b440062c8070c048c03cdd5000c5900d4c0040066eb0c00cc038dd5003520004004444b30010028a60103d87a80008acc004c0240062600866024602600497ae08cc00400e6028005337000029000a006403880888c03cc040c040004dd2a40008048dd7180618049baa003375a6018601a601a64b3001300e00189bac300d0018b201632330010013756601a601c601c601c601c60146ea8008896600200314bd7044c8cc88cc88cc00800800489660020031003899198099ba733013375200a66026602000266026602200297ae0330030033015002301300140446eacc03800cdd718058009980180198080011807000a0188b200e180400098019baa0088a4d1365640041", + "hash": "0cafde8c3dbac448e070c99beec59ac748faadb0dd75e456172602d7" }, { "title": "githoney_contract.settings_minting.else", @@ -228,8 +228,8 @@ } } ], - "compiledCode": "fa2dba3b69a8d00c", - "hash": "15721f473d73adf918aa7541871739c2e8df0a60abe023411cf9f1b0" + "compiledCode": "5904230101002229800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cdc3a400130080024888966002600460106ea800e26644b30013370e0029000c528c4cc88c8c96600266e1d2004300e3754003132980091192cc004c03000626464b300130190028024590161bae3017001301337540071598009802000c4c8c96600260320050048b202c375c602e00260266ea800e2c808901118089baa0028acc004c00401a2b300132330010013758602860226ea8024896600200314a115980099baf301530123754602a00203314a313300200230160014040809a2600264b300130023010375400314800226eb4c050c044dd5000a01e3259800980118081baa0018a60103d87a8000899198008009bab30153012375400444b30010018a6103d87a8000899192cc004cdc8a450b73657474696e67734e4654000018acc004cdc7a4410b73657474696e67734e465400001898049980b980a80125eb82298103d87a8000404d1330040043019003404c6eb8c04c004c058005014201e323300100137566028602a60226ea8010896600200314c103d87a8000899192cc004cdc8805800c56600266e3c02c006260106602c602800497ae08a60103d87a80004049133004004301800340486eb8c048004c0540050134528201c8a5040393013301037540049112cc004c030c048dd5000c4c8c8c8cc89660026038007132598009809180c1baa00189919192cc004c08000a266018603e006264b300130160018992cc004c08800626464b300130190018992cc004c094006266022604800202116408860406ea800a2b3001301100189919194c004dd69813000cdd69813001cdd698130012444b3001302a00480ac590270c098004c094004c080dd500145901e203c301e3754002604200316407c603a6ea800a2b3001300e0018acc004c074dd500140322c80f22c80d901b180d9baa0018b203a301e001301e0013019375400316405c603600b1640646eb4c064004dd6980c801180c800980c00098099baa0018b20221b874800a2c8068c00cc038dd500099192cc004c020c038dd5000c4c96600266ebcc04cc040dd500080b440062c8070c048c03cdd5000c5900d4c0040066eb0c00cc038dd5003520004004444b30010028a60103d87a80008acc004c0240062600866024602600497ae08cc00400e6028005337000029000a006403880888c03cc040c040004dd2a40008048dd7180618049baa003375a6018601a601a64b3001300e00189bac300d0018b201632330010013756601a601c601c601c601c60146ea8008896600200314bd7044c8cc88cc88cc00800800489660020031003899198099ba733013375200a66026602000266026602200297ae0330030033015002301300140446eacc03800cdd718058009980180198080011807000a0188b200e180400098019baa0088a4d1365640041", + "hash": "0cafde8c3dbac448e070c99beec59ac748faadb0dd75e456172602d7" } ], "definitions": { @@ -261,7 +261,7 @@ "Int": { "dataType": "integer" }, - "Option$cardano/address/Address": { + "Option": { "title": "Option", "anyOf": [ { @@ -284,7 +284,7 @@ } ] }, - "Option$cardano/address/StakeCredential": { + "Option": { "title": "Option", "anyOf": [ { @@ -307,7 +307,7 @@ } ] }, - "Pairs$cardano/assets/AssetName_Int": { + "Pairs": { "title": "Pairs", "dataType": "map", "keys": { @@ -317,14 +317,14 @@ "$ref": "#/definitions/Int" } }, - "Pairs$cardano/assets/PolicyId_Pairs$cardano/assets/AssetName_Int": { + "Pairs>": { "title": "Pairs>", "dataType": "map", "keys": { "$ref": "#/definitions/cardano~1assets~1PolicyId" }, "values": { - "$ref": "#/definitions/Pairs$cardano~1assets~1AssetName_Int" + "$ref": "#/definitions/Pairs" } }, "aiken/crypto/DataHash": { @@ -354,7 +354,7 @@ }, { "title": "stake_credential", - "$ref": "#/definitions/Option$cardano~1address~1StakeCredential" + "$ref": "#/definitions/Option" } ] } @@ -567,7 +567,7 @@ }, { "title": "contributor_address", - "$ref": "#/definitions/Option$cardano~1address~1Address" + "$ref": "#/definitions/Option" }, { "title": "bounty_reward_fee", @@ -583,7 +583,7 @@ }, { "title": "initial_value", - "$ref": "#/definitions/Pairs$cardano~1assets~1PolicyId_Pairs$cardano~1assets~1AssetName_Int" + "$ref": "#/definitions/Pairs>" } ] } From 80ada80012ee1f2d6599172ba25a833ff5545629 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Thu, 26 Feb 2026 13:57:45 -0300 Subject: [PATCH 26/27] feat: enhance type name handling and improve Aiken name mapping logic --- crates/tx3-lang/src/importing.rs | 150 +++++++++++++++++++++++++------ 1 file changed, 121 insertions(+), 29 deletions(-) diff --git a/crates/tx3-lang/src/importing.rs b/crates/tx3-lang/src/importing.rs index 1a8b867f..563cc9dc 100644 --- a/crates/tx3-lang/src/importing.rs +++ b/crates/tx3-lang/src/importing.rs @@ -1,6 +1,6 @@ //! Resolves plutus.json (CIP-57) imports and maps blueprint definitions to tx3 types. -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use cip_57::{Blueprint, DataType, Definition, Definitions, Field, ReferencesArray, Schema}; @@ -86,8 +86,11 @@ fn key_to_normalized_name(key: &str) -> String { key.replace('/', "_").replace('$', "_") } -fn import_type_name(key: &str) -> String { - friendly_import_type_alias_name(key).unwrap_or_else(|| key_to_normalized_name(key)) +fn import_type_name(key: &str, type_name_mapping: &HashMap) -> String { + type_name_mapping + .get(key) + .cloned() + .unwrap_or_else(|| key_to_normalized_name(key)) } fn sanitize_identifier_part(raw: &str) -> String { @@ -96,35 +99,113 @@ fn sanitize_identifier_part(raw: &str) -> String { .collect() } -fn friendly_import_type_alias_name(key: &str) -> Option { - let (base_key, generic_key) = match key.split_once('$') { - Some((base, generic)) => (base, Some(generic)), - None => (key, None), - }; +fn decode_json_pointer_escapes(name: &str) -> String { + name.replace("~1", "/").replace("~0", "~") +} + +fn extract_last_segment(name: &str) -> String { + let segment = name.rsplit('/').next().unwrap_or(name); + sanitize_identifier_part(segment) +} + +fn path_to_upper_camel(key: &str) -> String { + let mut output = String::new(); + for part in key + .split(|c: char| !c.is_ascii_alphanumeric()) + .filter(|part| !part.is_empty()) + { + let mut chars = part.chars(); + if let Some(first) = chars.next() { + output.push(first.to_ascii_uppercase()); + output.extend(chars); + } + } - let base = base_key.rsplit('/').next().unwrap_or(base_key); - let base = sanitize_identifier_part(base); + if output.is_empty() { + sanitize_identifier_part(key) + } else { + output + } +} + +fn aiken_prettify_name(name: &str) -> String { + let decoded = decode_json_pointer_escapes(name); - if base.is_empty() || !base.chars().next()?.is_ascii_alphabetic() { - return None; + if let Some(inner) = decoded.strip_prefix("Option$") { + let inner_pretty = aiken_prettify_name(inner); + return format!("Option{}", inner_pretty); } - let mut suffix = String::new(); + if let Some(inner) = decoded.strip_prefix("Option<") { + let inner = inner.strip_suffix('>').unwrap_or(inner); + let inner_pretty = aiken_prettify_name(inner); + return format!("Option{}", inner_pretty); + } + + if let Some(inner) = decoded.strip_prefix("Pairs$") { + let prettified: Vec = inner + .split('_') + .filter(|x| !x.is_empty()) + .map(aiken_prettify_name) + .collect(); + return format!("Pairs{}", prettified.join("")); + } - if let Some(generic_key) = generic_key { - for generic_part in generic_key.split('_').filter(|x| !x.is_empty()) { - let segment = generic_part.rsplit('/').next().unwrap_or(generic_part); - suffix.push_str(&sanitize_identifier_part(segment)); + if let Some(inner) = decoded.strip_prefix("Pairs<") { + let inner = inner.strip_suffix('>').unwrap_or(inner); + let prettified: Vec = inner + .split(',') + .map(|p| aiken_prettify_name(p.trim())) + .collect(); + return format!("Pairs{}", prettified.join("")); + } + + extract_last_segment(&decoded) +} + +fn build_aiken_name_mapping(definition_keys: &[&str]) -> HashMap { + let mut simple_to_originals: HashMap> = HashMap::new(); + + for &key in definition_keys { + let simple = aiken_prettify_name(key); + simple_to_originals + .entry(simple) + .or_default() + .push(key.to_string()); + } + + let mut result = HashMap::new(); + let mut used_names: HashSet = HashSet::new(); + + for &key in definition_keys { + let simple = aiken_prettify_name(key); + let originals = simple_to_originals.get(&simple).unwrap(); + + let final_name = if originals.len() == 1 { + simple + } else { + path_to_upper_camel(key) + }; + + let mut unique_name = final_name.clone(); + let mut counter = 1; + while used_names.contains(&unique_name) { + unique_name = format!("{}{}", final_name, counter); + counter += 1; } + + used_names.insert(unique_name.clone()); + result.insert(key.to_string(), unique_name); } - Some(format!("{}{}", base, suffix)) + result } fn resolve_ref_to_type( ref_str: &str, definitions: &Definitions, alias: Option<&str>, + type_name_mapping: &HashMap, ) -> Result { let key = ref_to_key_owned(ref_str); let def = definitions.inner.get(&key).ok_or_else(|| Error::Schema { @@ -140,11 +221,11 @@ fn resolve_ref_to_type( Some(ReferencesArray::Single(r)) => r .reference .as_ref() - .map(|s| resolve_ref_to_type(s, definitions, alias)), + .map(|s| resolve_ref_to_type(s, definitions, alias, type_name_mapping)), Some(ReferencesArray::Array(arr)) => arr.first().and_then(|r| { r.reference .as_ref() - .map(|s| resolve_ref_to_type(s, definitions, alias)) + .map(|s| resolve_ref_to_type(s, definitions, alias, type_name_mapping)) }), None => None, }; @@ -168,15 +249,18 @@ fn resolve_ref_to_type( .ok_or_else(|| Error::Schema { message: "map without values".to_string(), })?; - let key_ty = resolve_ref_to_type(k, definitions, alias)?; - let val_ty = resolve_ref_to_type(v, definitions, alias)?; + let key_ty = resolve_ref_to_type(k, definitions, alias, type_name_mapping)?; + let val_ty = resolve_ref_to_type(v, definitions, alias, type_name_mapping)?; return Ok(Type::Map(Box::new(key_ty), Box::new(val_ty))); } DataType::Constructor => {} } } - Ok(Type::Custom(Identifier::new(import_type_name(&key)))) + Ok(Type::Custom(Identifier::new(import_type_name( + &key, + type_name_mapping, + )))) } fn field_to_record_field( @@ -184,13 +268,14 @@ fn field_to_record_field( index: usize, definitions: &Definitions, alias: Option<&str>, + type_name_mapping: &HashMap, ) -> Result { let name = f .title .as_deref() .map(ToString::to_string) .unwrap_or_else(|| format!("field_{}", index)); - let r#type = resolve_ref_to_type(&f.reference, definitions, alias)?; + let r#type = resolve_ref_to_type(&f.reference, definitions, alias, type_name_mapping)?; Ok(RecordField { name: Identifier::new(name), r#type, @@ -202,13 +287,14 @@ fn schema_to_variant_case( schema: &Schema, definitions: &Definitions, alias: Option<&str>, + type_name_mapping: &HashMap, ) -> Result { let name = schema.title.as_deref().unwrap_or("Variant").to_string(); let fields: Vec = schema .fields .iter() .enumerate() - .map(|(i, f)| field_to_record_field(f, i, definitions, alias)) + .map(|(i, f)| field_to_record_field(f, i, definitions, alias, type_name_mapping)) .collect::, _>>()?; Ok(VariantCase { name: Identifier::new(name), @@ -222,6 +308,7 @@ fn definition_to_type_def( def: &Definition, definitions: &Definitions, alias: Option<&str>, + type_name_mapping: &HashMap, ) -> Result, Error> { if let Some(dt) = &def.data_type { match dt { @@ -234,14 +321,14 @@ fn definition_to_type_def( let cases = if let Some(any_of) = &def.any_of { any_of .iter() - .map(|s| schema_to_variant_case(s, definitions, alias)) + .map(|s| schema_to_variant_case(s, definitions, alias, type_name_mapping)) .collect::, _>>()? } else { // Data type has no structure but should still be imported. // TODO: There is no Any for our type system, so for now we'll just import it as a single empty variant case. if key == "Data" { return Ok(Some(TypeDef { - name: Identifier::new(import_type_name(key)), + name: Identifier::new(import_type_name(key, type_name_mapping)), cases: vec![VariantCase { name: Identifier::new("Default"), fields: vec![], @@ -262,7 +349,7 @@ fn definition_to_type_def( cases[0].name = Identifier::new("Default"); } - let type_name = import_type_name(key); + let type_name = import_type_name(key, type_name_mapping); Ok(Some(TypeDef { name: Identifier::new(type_name), cases, @@ -279,9 +366,14 @@ pub fn type_defs_from_blueprint( None => return Ok(vec![]), }; + let definition_keys: Vec<&str> = definitions.inner.keys().map(String::as_str).collect(); + let type_name_mapping = build_aiken_name_mapping(&definition_keys); + let mut type_defs = Vec::new(); for (key, def) in &definitions.inner { - if let Some(type_def) = definition_to_type_def(key, def, definitions, alias)? { + if let Some(type_def) = + definition_to_type_def(key, def, definitions, alias, &type_name_mapping)? + { type_defs.push(type_def); } } From 2d24bac667d02df9b57d174779b22e6b0c15e9eb Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Thu, 26 Feb 2026 14:16:00 -0300 Subject: [PATCH 27/27] refactor: remove redundant check for empty fields --- crates/tx3-lang/src/ast.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index b6d5ea53..85ad4280 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -949,9 +949,6 @@ impl TypeDef { // Implicit cases don't have an explicit constructor on its usage if self.cases.len() == 1 && self.cases[0].name.value == "Default" { let fields = &self.cases[0].fields; - if fields.is_empty() { - return format!("type {}", name); - } let fields_str = fields .iter() .map(|f| format!("{}: {}", f.name.value, f.r#type))