From 90e900e6bbd87c654657917bff4108eefa0d9e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Wed, 3 Dec 2025 15:16:32 -0300 Subject: [PATCH 01/13] feat: inject types from parsed plutus.json files into tx3 pipeline --- crates/tx3-lang/src/cardano.rs | 52 ++++++++++++++++++++++++++++++++++ crates/tx3-lang/src/parsing.rs | 7 ++++- crates/tx3-lang/src/tx3.pest | 11 +++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/crates/tx3-lang/src/cardano.rs b/crates/tx3-lang/src/cardano.rs index a91c511f..6954c7a9 100644 --- a/crates/tx3-lang/src/cardano.rs +++ b/crates/tx3-lang/src/cardano.rs @@ -841,6 +841,58 @@ impl IntoLower for CardanoBlock { } } +pub fn load_externals(path: &str) -> Vec { + use crate::ast::{Identifier, RecordField, Span, Type, TypeDef, VariantCase}; + + vec![TypeDef { + name: Identifier { + value: "PlutusData".to_string(), + span: Span::DUMMY, + symbol: None, + }, + cases: vec![ + VariantCase { + name: Identifier::new("Constr"), + fields: vec![ + RecordField::new("constructor", Type::Int), + RecordField::new( + "fields", + Type::List(Box::new(Type::Custom(Identifier::new("PlutusData")))), + ), + ], + span: Span::DUMMY, + }, + VariantCase { + name: Identifier::new("Map"), + fields: vec![RecordField::new( + "entries", + Type::List(Box::new(Type::Custom(Identifier::new("PlutusData")))), + )], + span: Span::DUMMY, + }, + VariantCase { + name: Identifier::new("List"), + fields: vec![RecordField::new( + "items", + Type::List(Box::new(Type::Custom(Identifier::new("PlutusData")))), + )], + span: Span::DUMMY, + }, + VariantCase { + name: Identifier::new("Integer"), + fields: vec![RecordField::new("value", Type::Int)], + span: Span::DUMMY, + }, + VariantCase { + name: Identifier::new("Bytes"), + fields: vec![RecordField::new("value", Type::Bytes)], + span: Span::DUMMY, + }, + ], + span: Span::DUMMY, + }] +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 1e67ca80..d07b1c50 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -14,7 +14,7 @@ use pest_derive::Parser; use crate::{ ast::*, - cardano::{PlutusWitnessBlock, PlutusWitnessField}, + cardano::{load_externals, PlutusWitnessBlock, PlutusWitnessField}, }; #[derive(Parser)] #[grammar = "tx3.pest"] @@ -108,6 +108,11 @@ impl AstNode for Program { Rule::alias_def => program.aliases.push(AliasDef::parse(pair)?), Rule::party_def => program.parties.push(PartyDef::parse(pair)?), Rule::policy_def => program.policies.push(PolicyDef::parse(pair)?), + Rule::cardano_import => { + let import_path = pair.into_inner().as_str(); + let external_types = load_externals(import_path); + program.types.extend(external_types); + } Rule::EOI => break, x => unreachable!("Unexpected rule in program: {:?}", x), } diff --git a/crates/tx3-lang/src/tx3.pest b/crates/tx3-lang/src/tx3.pest index 43ef45c1..4f787568 100644 --- a/crates/tx3-lang/src/tx3.pest +++ b/crates/tx3-lang/src/tx3.pest @@ -462,9 +462,20 @@ tx_def = { "tx" ~ identifier ~ parameter_list ~ "{" ~ tx_body_block* ~ "}" } +path_segment = @{ ASCII_ALPHANUMERIC+ | "." | "_" | "-" } + +file_ext = @{ "." ~ ASCII_ALPHANUMERIC+ } + +file_path = @{ path_segment ~ ("/" ~ path_segment)* ~ file_ext? } + +cardano_import = { + "cardano::import " ~ "\"" ~ file_path ~ "\"" ~ ";" +} + // Program program = { SOI ~ + cardano_import* ~ (env_def | asset_def | party_def | policy_def | type_def | tx_def)* ~ EOI } From 875e62d703e58e4fb4bc170544a091472591aef9 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Wed, 3 Dec 2025 16:18:38 -0300 Subject: [PATCH 02/13] 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 03/13] 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 04/13] 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 015a8fcdcdea67c96396070d9d300fe7ae1802fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 5 Dec 2025 18:22:16 -0300 Subject: [PATCH 05/13] 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 dd9c4274774db5cca3dfc23fa7419a925cd4dfaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 5 Dec 2025 18:23:10 -0300 Subject: [PATCH 06/13] feat: mapping cip57 files into scope --- Cargo.lock | 1 + crates/tx3-lang/Cargo.toml | 5 +- crates/tx3-lang/src/analyzing.rs | 2 +- crates/tx3-lang/src/ast.rs | 7 ++ crates/tx3-lang/src/cardano.rs | 197 ++++++++++++++++++++++--------- crates/tx3-lang/src/parsing.rs | 14 ++- crates/tx3-lang/src/tx3.pest | 8 +- 7 files changed, 169 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d7c9129a..61c4ccf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2162,6 +2162,7 @@ version = "0.13.0" 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 6e8735aa..60b8a95c 100644 --- a/crates/tx3-lang/Cargo.toml +++ b/crates/tx3-lang/Cargo.toml @@ -16,15 +16,16 @@ readme.workspace = true thiserror = { workspace = true } trait-variant = { workspace = true } hex = { workspace = true } - +cip-57 = { version = "0.13.0", 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"] } pest_derive = "2.7.15" serde = { version = "1.0.217", features = ["derive"] } ciborium = "0.2.2" + [dev-dependencies] assert-json-diff = "2.0.2" paste = "1.0.15" proptest = "1.7.0" -serde_json = "1.0.137" diff --git a/crates/tx3-lang/src/analyzing.rs b/crates/tx3-lang/src/analyzing.rs index 120d6366..2b1c1fec 100644 --- a/crates/tx3-lang/src/analyzing.rs +++ b/crates/tx3-lang/src/analyzing.rs @@ -1439,7 +1439,7 @@ impl Analyzable for Program { /// # Returns /// * `AnalyzeReport` of the analysis. Empty if no errors are found. pub fn analyze(ast: &mut Program) -> AnalyzeReport { - ast.analyze(None) + ast.analyze(ast.scope.clone()) } #[cfg(test)] diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index 1c89006a..fc11918d 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -907,6 +907,13 @@ pub struct AliasDef { } impl AliasDef { + pub fn new(name: &str, target: Type) -> Self { + Self { + name: Identifier::new(name), + alias_type: target, + span: Span::DUMMY, + } + } pub fn resolve_alias_chain(&self) -> Option<&TypeDef> { match &self.alias_type { Type::Custom(identifier) => match &identifier.symbol { diff --git a/crates/tx3-lang/src/cardano.rs b/crates/tx3-lang/src/cardano.rs index 6954c7a9..6251a53d 100644 --- a/crates/tx3-lang/src/cardano.rs +++ b/crates/tx3-lang/src/cardano.rs @@ -1,11 +1,11 @@ -use std::{collections::HashMap, rc::Rc}; +use std::{collections::HashMap, fs, rc::Rc}; use pest::iterators::Pair; use serde::{Deserialize, Serialize}; use crate::{ analyzing::{Analyzable, AnalyzeReport}, - ast::{DataExpr, Identifier, Scope, Span, Type}, + ast::{DataExpr, Identifier, RecordField, Scope, Span, Symbol, Type, TypeDef, VariantCase}, ir, lowering::IntoLower, parsing::{AstNode, Error, Rule}, @@ -841,65 +841,154 @@ impl IntoLower for CardanoBlock { } } -pub fn load_externals(path: &str) -> Vec { - use crate::ast::{Identifier, RecordField, Span, Type, TypeDef, VariantCase}; +/// Sanitizes a type name to be a valid tx3 identifier. +/// Replaces characters like `<`, `>`, `,`, `/`, `$`, `~1` with underscores. +fn sanitize_type_name(name: &str) -> String { + name.replace("~1", "_") // URL-encoded `/` in JSON references + .replace('/', "_") + .replace('$', "_") + .replace('<', "_") + .replace('>', "") + .replace(',', "_") + .replace(' ', "") +} - vec![TypeDef { - name: Identifier { - value: "PlutusData".to_string(), - span: Span::DUMMY, - symbol: None, - }, - cases: vec![ - VariantCase { - name: Identifier::new("Constr"), - fields: vec![ - RecordField::new("constructor", Type::Int), - RecordField::new( - "fields", - Type::List(Box::new(Type::Custom(Identifier::new("PlutusData")))), - ), - ], - span: Span::DUMMY, - }, - VariantCase { - name: Identifier::new("Map"), - fields: vec![RecordField::new( - "entries", - Type::List(Box::new(Type::Custom(Identifier::new("PlutusData")))), - )], - span: Span::DUMMY, - }, - VariantCase { - name: Identifier::new("List"), - fields: vec![RecordField::new( - "items", - Type::List(Box::new(Type::Custom(Identifier::new("PlutusData")))), - )], - span: Span::DUMMY, - }, - VariantCase { - name: Identifier::new("Integer"), - fields: vec![RecordField::new("value", Type::Int)], - span: Span::DUMMY, - }, - VariantCase { - name: Identifier::new("Bytes"), - fields: vec![RecordField::new("value", Type::Bytes)], - span: Span::DUMMY, +// TODO: add policies, and parameters as well +pub fn load_externals( + path: &str, +) -> Result, crate::parsing::Error> { + // TODO: add error handling + let json = fs::read_to_string(path).unwrap(); + let bp = serde_json::from_str::(&json).unwrap(); + + let ref_to_type = |r: &str| -> Type { + let sanitized = sanitize_type_name(r.strip_prefix("#/definitions/").unwrap_or(r)); + Type::Custom(Identifier::new(&sanitized)) + }; + + let mut symbols = HashMap::new(); + for (key, def) in bp + .definitions + .as_ref() + .map(|d| d.inner.iter()) + .into_iter() + .flatten() + { + let name = sanitize_type_name(key); + + let new = match def.data_type { + Some(cip_57::DataType::Integer) => Some(Symbol::AliasDef(Box::new( + crate::ast::AliasDef::new(&name, Type::Int), + ))), + Some(cip_57::DataType::Bytes) => Some(Symbol::AliasDef(Box::new( + crate::ast::AliasDef::new(&name, Type::Bytes), + ))), + Some(cip_57::DataType::Map) => { + let key_ty = def + .keys + .as_ref() + .and_then(|r| r.reference.as_ref()) + .map(|r| ref_to_type(r)); + let value = def + .values + .as_ref() + .and_then(|r| r.reference.as_ref()) + .map(|r| ref_to_type(r)); + + if let (Some(key_ty), Some(value)) = (key_ty, value) { + Some(Symbol::AliasDef(Box::new(crate::ast::AliasDef::new( + &name, + Type::Map(Box::new(key_ty), Box::new(value)), + )))) + } else { + None + } + } + Some(cip_57::DataType::List) => match &def.items { + Some(cip_57::ReferencesArray::Single(r)) => { + let name = name.clone(); + r.reference.as_ref().map(|r| { + Symbol::AliasDef(Box::new(crate::ast::AliasDef::new( + &name, + Type::List(Box::new(ref_to_type(r))), + ))) + }) + } + _ => None, }, - ], - span: Span::DUMMY, - }] + Some(cip_57::DataType::Constructor) => { + let mut cases = vec![]; + if let Some(any_of) = &def.any_of { + for schema in any_of { + let case_name = schema + .title + .clone() + .unwrap_or_else(|| format!("Constructor{}", schema.index)); + let mut fields = vec![]; + for (i, field) in schema.fields.iter().enumerate() { + let field_name = field + .title + .clone() + .unwrap_or_else(|| format!("field_{}", i)); + let field_ty = ref_to_type(&field.reference); + fields.push(RecordField::new(&field_name, field_ty)); + } + cases.push(VariantCase { + name: Identifier::new(case_name), + fields, + span: Span::default(), + }); + } + } + Some(Symbol::TypeDef(Box::new(TypeDef { + name: Identifier::new(&name), + cases, + span: Span::default(), + }))) + } + None if def.any_of.is_some() => { + let mut cases = vec![]; + if let Some(any_of) = &def.any_of { + for schema in any_of { + let case_name = schema + .title + .clone() + .unwrap_or_else(|| format!("Constructor{}", schema.index)); + let mut fields = vec![]; + for (i, field) in schema.fields.iter().enumerate() { + let field_name = field + .title + .clone() + .unwrap_or_else(|| format!("field_{}", i)); + let field_ty = ref_to_type(&field.reference); + fields.push(RecordField::new(&field_name, field_ty)); + } + cases.push(VariantCase { + name: Identifier::new(case_name), + fields, + span: Span::default(), + }); + } + } + Some(Symbol::TypeDef(Box::new(TypeDef { + name: Identifier::new(&name), + cases, + span: Span::default(), + }))) + } + None => None, + }; + if let Some(symbol) = new { + symbols.insert(name, symbol); + } + } + Ok(symbols) } #[cfg(test)] mod tests { use super::*; - use crate::{ - analyzing::analyze, - ast::{self, *}, - }; + use crate::{analyzing::analyze, ast::*}; use pest::Parser; macro_rules! input_to_ast_check { diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index d07b1c50..5c3a8e86 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -110,8 +110,18 @@ impl AstNode for Program { Rule::policy_def => program.policies.push(PolicyDef::parse(pair)?), Rule::cardano_import => { let import_path = pair.into_inner().as_str(); - let external_types = load_externals(import_path); - program.types.extend(external_types); + let external_types = load_externals(import_path)?; + dbg!(&external_types); + if let Some(ref mut scope) = program.scope { + if let Some(scope_mut) = std::rc::Rc::get_mut(scope) { + scope_mut.symbols.extend(external_types); + } + } else { + program.scope = Some(std::rc::Rc::new(Scope { + symbols: external_types, + parent: None, + })); + } } Rule::EOI => break, x => unreachable!("Unexpected rule in program: {:?}", x), diff --git a/crates/tx3-lang/src/tx3.pest b/crates/tx3-lang/src/tx3.pest index 4f787568..c1d693dd 100644 --- a/crates/tx3-lang/src/tx3.pest +++ b/crates/tx3-lang/src/tx3.pest @@ -462,11 +462,7 @@ tx_def = { "tx" ~ identifier ~ parameter_list ~ "{" ~ tx_body_block* ~ "}" } -path_segment = @{ ASCII_ALPHANUMERIC+ | "." | "_" | "-" } - -file_ext = @{ "." ~ ASCII_ALPHANUMERIC+ } - -file_path = @{ path_segment ~ ("/" ~ path_segment)* ~ file_ext? } +file_path = @{ (!"\"" ~ ANY)+ } cardano_import = { "cardano::import " ~ "\"" ~ file_path ~ "\"" ~ ";" @@ -475,7 +471,7 @@ cardano_import = { // Program program = { SOI ~ - cardano_import* ~ + (cardano_import)* ~ (env_def | asset_def | party_def | policy_def | type_def | tx_def)* ~ EOI } From e27169ae49dac2828b5a96452e50fc1d6a52078f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Tue, 9 Dec 2025 17:19:48 -0300 Subject: [PATCH 07/13] feat: testable imports of cip 57 files --- crates/tx3-lang/src/ast.rs | 1 + crates/tx3-lang/src/cardano.rs | 16 +- crates/tx3-lang/src/loading.rs | 58 +- crates/tx3-lang/src/parsing.rs | 27 +- crates/tx3-lang/src/tx3.pest | 4 +- examples/asteria.ast | 1 + examples/burn.ast | 1 + examples/cardano_witness.ast | 1 + examples/cardano_witness.mint_from_plutus.tir | 6 +- examples/cip_imports.tx3 | 34 + examples/disordered.ast | 1 + examples/donation.ast | 1 + examples/env_vars.ast | 1 + examples/faucet.ast | 1 + examples/imports/plutus.json | 634 ++++++++++++++++++ examples/input_datum.ast | 1 + examples/lang_tour.ast | 1 + 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 | 26 +- examples/reference_script.publish_plutus.tir | 32 +- examples/swap.ast | 1 + examples/transfer.ast | 1 + examples/vesting.ast | 1 + examples/withdrawal.ast | 1 + examples/withdrawal.transfer.tir | 6 +- 28 files changed, 797 insertions(+), 64 deletions(-) create mode 100644 examples/cip_imports.tx3 create mode 100644 examples/imports/plutus.json diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index fc11918d..d2fd9576 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -179,6 +179,7 @@ pub struct Program { pub assets: Vec, pub parties: Vec, pub policies: Vec, + pub imports: Vec, pub span: Span, // analysis diff --git a/crates/tx3-lang/src/cardano.rs b/crates/tx3-lang/src/cardano.rs index 6251a53d..e46f42aa 100644 --- a/crates/tx3-lang/src/cardano.rs +++ b/crates/tx3-lang/src/cardano.rs @@ -853,13 +853,21 @@ fn sanitize_type_name(name: &str) -> String { .replace(' ', "") } -// TODO: add policies, and parameters as well +// TODO: add policies, and script parameters as well pub fn load_externals( path: &str, ) -> Result, crate::parsing::Error> { - // TODO: add error handling - let json = fs::read_to_string(path).unwrap(); - let bp = serde_json::from_str::(&json).unwrap(); + let json = fs::read_to_string(path).map_err(|e| crate::parsing::Error { + message: format!("Failed to read import file: {}", e), + src: "".to_string(), // TODO: propagate source? + span: crate::ast::Span::DUMMY, + })?; + let bp = + serde_json::from_str::(&json).map_err(|e| crate::parsing::Error { + message: format!("Failed to parse blueprint JSON: {}", e), + src: "".to_string(), // TODO: should I add path here? + span: crate::ast::Span::DUMMY, + })?; let ref_to_type = |r: &str| -> Type { let sanitized = sanitize_type_name(r.strip_prefix("#/definitions/").unwrap_or(r)); diff --git a/crates/tx3-lang/src/loading.rs b/crates/tx3-lang/src/loading.rs index 8d95808d..9965f8f5 100644 --- a/crates/tx3-lang/src/loading.rs +++ b/crates/tx3-lang/src/loading.rs @@ -21,6 +21,8 @@ pub enum Error { InvalidEnvFile(String), } +use crate::cardano::load_externals; + /// Parses a Tx3 source file into a Program AST. /// /// # Arguments @@ -46,10 +48,41 @@ pub enum Error { /// ``` pub fn parse_file(path: &str) -> Result { let input = std::fs::read_to_string(path)?; - let program = parsing::parse_string(&input)?; + let mut program = parsing::parse_string(&input)?; + // Should it be configurable by trix.toml? A path for imports like "../onchain" and all imports + // would be really clean + let base_path = std::path::Path::new(path) + .parent() + .unwrap_or(std::path::Path::new(".")); + process_imports(&mut program, base_path)?; Ok(program) } +fn process_imports(program: &mut ast::Program, base_path: &Path) -> Result<(), Error> { + for import_path in &program.imports { + let full_path = base_path.join(import_path); + let path_str = full_path.to_str().ok_or_else(|| { + Error::Io(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Invalid path", + )) + })?; + let external_types = load_externals(path_str)?; + + if let Some(ref mut scope) = program.scope { + if let Some(scope_mut) = std::rc::Rc::get_mut(scope) { + scope_mut.symbols.extend(external_types); + } + } else { + program.scope = Some(std::rc::Rc::new(ast::Scope { + symbols: external_types, + parent: None, + })); + } + } + Ok(()) +} + pub type ArgMap = std::collections::HashMap; fn load_env_file(path: &Path) -> Result { @@ -132,14 +165,19 @@ impl ProtocolLoader { } pub fn load(self) -> Result { - let code = match (self.code_file, self.code_string) { + let code = match (&self.code_file, &self.code_string) { (Some(file), None) => std::fs::read_to_string(file)?, - (None, Some(code)) => code, + (None, Some(code)) => code.clone(), _ => unreachable!(), }; let mut ast = parsing::parse_string(&code)?; + if let Some(file) = &self.code_file { + let base_path = file.parent().unwrap_or(std::path::Path::new(".")); + process_imports(&mut ast, base_path)?; + } + if self.analyze { analyzing::analyze(&mut ast).ok()?; } @@ -173,4 +211,18 @@ pub mod tests { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let _ = parse_file(&format!("{}/../..//examples/transfer.tx3", manifest_dir)).unwrap(); } + + #[test] + fn test_cardano_import() { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let path = format!("{}/../../examples/cip_imports.tx3", manifest_dir); + let program = parse_file(&path).unwrap(); + + assert!(program.scope.is_some()); + let scope = program.scope.as_ref().unwrap(); + + assert!(scope.symbols.contains_key("Int")); + assert!(scope.symbols.contains_key("cardano_assets_AssetName")); + assert!(scope.symbols.contains_key("cardano_transaction_Datum")); + } } diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 5c3a8e86..b6d9ebc2 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::{load_externals, PlutusWitnessBlock, PlutusWitnessField}, -}; +use crate::{ast::*, cardano::load_externals}; #[derive(Parser)] #[grammar = "tx3.pest"] pub(crate) struct Tx3Grammar; @@ -94,6 +91,7 @@ impl AstNode for Program { aliases: Vec::new(), parties: Vec::new(), policies: Vec::new(), + imports: Vec::new(), scope: None, span, }; @@ -109,19 +107,8 @@ impl AstNode for Program { Rule::party_def => program.parties.push(PartyDef::parse(pair)?), Rule::policy_def => program.policies.push(PolicyDef::parse(pair)?), Rule::cardano_import => { - let import_path = pair.into_inner().as_str(); - let external_types = load_externals(import_path)?; - dbg!(&external_types); - if let Some(ref mut scope) = program.scope { - if let Some(scope_mut) = std::rc::Rc::get_mut(scope) { - scope_mut.symbols.extend(external_types); - } - } else { - program.scope = Some(std::rc::Rc::new(Scope { - symbols: external_types, - parent: None, - })); - } + let import_path = pair.into_inner().next().unwrap().as_str().trim_matches('"'); + program.imports.push(import_path.to_string()); } Rule::EOI => break, x => unreachable!("Unexpected rule in program: {:?}", x), @@ -1553,8 +1540,9 @@ impl AstNode for ChainSpecificBlock { /// let program = parse_string("tx swap() {}").unwrap(); /// ``` pub fn parse_string(input: &str) -> Result { - let pairs = Tx3Grammar::parse(Rule::program, input)?; - Program::parse(pairs.into_iter().next().unwrap()) + let mut pairs = Tx3Grammar::parse(Rule::program, input)?; + let program = Program::parse(pairs.next().unwrap())?; + Ok(program) } #[cfg(test)] @@ -2642,6 +2630,7 @@ mod tests { env: None, assets: vec![], policies: vec![], + imports: vec![], span: Span::DUMMY, scope: None, } diff --git a/crates/tx3-lang/src/tx3.pest b/crates/tx3-lang/src/tx3.pest index c1d693dd..4a43ce27 100644 --- a/crates/tx3-lang/src/tx3.pest +++ b/crates/tx3-lang/src/tx3.pest @@ -462,10 +462,8 @@ tx_def = { "tx" ~ identifier ~ parameter_list ~ "{" ~ tx_body_block* ~ "}" } -file_path = @{ (!"\"" ~ ANY)+ } - cardano_import = { - "cardano::import " ~ "\"" ~ file_path ~ "\"" ~ ";" + "cardano::import " ~ string ~ ";" } // Program diff --git a/examples/asteria.ast b/examples/asteria.ast index 0d231790..a8555b4f 100644 --- a/examples/asteria.ast +++ b/examples/asteria.ast @@ -1081,6 +1081,7 @@ } ], "policies": [], + "imports": [], "span": { "dummy": false, "start": 0, diff --git a/examples/burn.ast b/examples/burn.ast index 6eafd087..f5c16afb 100644 --- a/examples/burn.ast +++ b/examples/burn.ast @@ -285,6 +285,7 @@ } ], "policies": [], + "imports": [], "span": { "dummy": false, "start": 0, diff --git a/examples/cardano_witness.ast b/examples/cardano_witness.ast index 9136d18d..93ccd6c0 100644 --- a/examples/cardano_witness.ast +++ b/examples/cardano_witness.ast @@ -652,6 +652,7 @@ } ], "policies": [], + "imports": [], "span": { "dummy": false, "start": 0, 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/cip_imports.tx3 b/examples/cip_imports.tx3 new file mode 100644 index 00000000..9a88e668 --- /dev/null +++ b/examples/cip_imports.tx3 @@ -0,0 +1,34 @@ +cardano::import "imports/plutus.json"; + +party Sender; + +party Receiver; + +tx transfer_with_imports( + quantity: Int +) { + input source { + from: Sender, + min_amount: Ada(quantity), + } + + output { + to: Receiver, + amount: Ada(quantity), + } + + output { + to: Sender, + amount: source - Ada(quantity) - fees, + datum: types_SettingsDatum::SettingsDatum { + githoney_address: cardano_address_Address::Address{ + payment_credential: cardano_address_PaymentCredential::VerificationKey { + field_0: 0x123123, + }, + stake_credential: Option_cardano_address_StakeCredential::None {}, + }, + bounty_creation_fee: 0, + bounty_reward_fee: 0, + }, + } +} diff --git a/examples/disordered.ast b/examples/disordered.ast index 5dd0f5f3..5f2e9a7f 100644 --- a/examples/disordered.ast +++ b/examples/disordered.ast @@ -299,6 +299,7 @@ } ], "policies": [], + "imports": [], "span": { "dummy": false, "start": 0, diff --git a/examples/donation.ast b/examples/donation.ast index d7fd2b5b..54942850 100644 --- a/examples/donation.ast +++ b/examples/donation.ast @@ -316,6 +316,7 @@ } ], "policies": [], + "imports": [], "span": { "dummy": false, "start": 0, diff --git a/examples/env_vars.ast b/examples/env_vars.ast index cef2b474..5958754d 100644 --- a/examples/env_vars.ast +++ b/examples/env_vars.ast @@ -255,6 +255,7 @@ } ], "policies": [], + "imports": [], "span": { "dummy": false, "start": 0, diff --git a/examples/faucet.ast b/examples/faucet.ast index ef4e9653..51d509c8 100644 --- a/examples/faucet.ast +++ b/examples/faucet.ast @@ -320,6 +320,7 @@ } } ], + "imports": [], "span": { "dummy": false, "start": 0, diff --git a/examples/imports/plutus.json b/examples/imports/plutus.json new file mode 100644 index 00000000..97472029 --- /dev/null +++ b/examples/imports/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/examples/input_datum.ast b/examples/input_datum.ast index c0cb2672..dc43f438 100644 --- a/examples/input_datum.ast +++ b/examples/input_datum.ast @@ -364,6 +364,7 @@ } ], "policies": [], + "imports": [], "span": { "dummy": false, "start": 0, diff --git a/examples/lang_tour.ast b/examples/lang_tour.ast index 882ba085..71060e61 100644 --- a/examples/lang_tour.ast +++ b/examples/lang_tour.ast @@ -1582,6 +1582,7 @@ } } ], + "imports": [], "span": { "dummy": false, "start": 0, diff --git a/examples/list_concat.ast b/examples/list_concat.ast index f8b6c17f..65d268bf 100644 --- a/examples/list_concat.ast +++ b/examples/list_concat.ast @@ -305,6 +305,7 @@ } ], "policies": [], + "imports": [], "span": { "dummy": false, "start": 0, diff --git a/examples/local_vars.ast b/examples/local_vars.ast index 432bb4ff..c950a292 100644 --- a/examples/local_vars.ast +++ b/examples/local_vars.ast @@ -258,6 +258,7 @@ } ], "policies": [], + "imports": [], "span": { "dummy": false, "start": 0, diff --git a/examples/map.ast b/examples/map.ast index 7f8e3eee..abcc336c 100644 --- a/examples/map.ast +++ b/examples/map.ast @@ -439,6 +439,7 @@ } ], "policies": [], + "imports": [], "span": { "dummy": false, "start": 0, diff --git a/examples/reference_script.ast b/examples/reference_script.ast index 9c312a18..22ada355 100644 --- a/examples/reference_script.ast +++ b/examples/reference_script.ast @@ -564,6 +564,7 @@ } ], "policies": [], + "imports": [], "span": { "dummy": false, "start": 0, diff --git a/examples/reference_script.publish_native.tir b/examples/reference_script.publish_native.tir index 90fdd3b9..73821504 100644 --- a/examples/reference_script.publish_native.tir +++ b/examples/reference_script.publish_native.tir @@ -137,16 +137,6 @@ { "name": "cardano_publish", "data": { - "script": { - "Bytes": [ - 130, - 1, - 129, - 130, - 4, - 0 - ] - }, "amount": { "Assets": [ { @@ -163,6 +153,19 @@ } ] }, + "script": { + "Bytes": [ + 130, + 1, + 129, + 130, + 4, + 0 + ] + }, + "version": { + "Number": 0 + }, "to": { "EvalParam": { "ExpectValue": [ @@ -170,9 +173,6 @@ "Address" ] } - }, - "version": { - "Number": 0 } } } diff --git a/examples/reference_script.publish_plutus.tir b/examples/reference_script.publish_plutus.tir index 7506c0f5..81087f15 100644 --- a/examples/reference_script.publish_plutus.tir +++ b/examples/reference_script.publish_plutus.tir @@ -140,22 +140,6 @@ "version": { "Number": 3 }, - "amount": { - "Assets": [ - { - "policy": "None", - "asset_name": "None", - "amount": { - "EvalParam": { - "ExpectValue": [ - "quantity", - "Int" - ] - } - } - } - ] - }, "script": { "Bytes": [ 81, @@ -185,6 +169,22 @@ "Address" ] } + }, + "amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + } + ] } } } diff --git a/examples/swap.ast b/examples/swap.ast index 8d915d0f..d1ff5dd8 100644 --- a/examples/swap.ast +++ b/examples/swap.ast @@ -743,6 +743,7 @@ } ], "policies": [], + "imports": [], "span": { "dummy": false, "start": 0, diff --git a/examples/transfer.ast b/examples/transfer.ast index b100c14b..acc911e2 100644 --- a/examples/transfer.ast +++ b/examples/transfer.ast @@ -280,6 +280,7 @@ } ], "policies": [], + "imports": [], "span": { "dummy": false, "start": 0, diff --git a/examples/vesting.ast b/examples/vesting.ast index 5c40088d..692c4570 100644 --- a/examples/vesting.ast +++ b/examples/vesting.ast @@ -754,6 +754,7 @@ } } ], + "imports": [], "span": { "dummy": false, "start": 0, diff --git a/examples/withdrawal.ast b/examples/withdrawal.ast index ca893dd5..5c79f5a0 100644 --- a/examples/withdrawal.ast +++ b/examples/withdrawal.ast @@ -314,6 +314,7 @@ } ], "policies": [], + "imports": [], "span": { "dummy": false, "start": 0, diff --git a/examples/withdrawal.transfer.tir b/examples/withdrawal.transfer.tir index facdbb3c..ec19f1e1 100644 --- a/examples/withdrawal.transfer.tir +++ b/examples/withdrawal.transfer.tir @@ -165,6 +165,9 @@ { "name": "withdrawal", "data": { + "amount": { + "Number": 0 + }, "credential": { "EvalParam": { "ExpectValue": [ @@ -173,9 +176,6 @@ ] } }, - "amount": { - "Number": 0 - }, "redeemer": { "Struct": { "constructor": 0, From c5ec2ef5bc69367c0b818d1b7b4a310f27c93d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Tue, 9 Dec 2025 18:01:42 -0300 Subject: [PATCH 08/13] feat: remove unnecessary change on parsing --- crates/tx3-lang/src/parsing.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index b6d9ebc2..2d885441 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -1540,9 +1540,8 @@ impl AstNode for ChainSpecificBlock { /// let program = parse_string("tx swap() {}").unwrap(); /// ``` pub fn parse_string(input: &str) -> Result { - let mut pairs = Tx3Grammar::parse(Rule::program, input)?; - let program = Program::parse(pairs.next().unwrap())?; - Ok(program) + let pairs = Tx3Grammar::parse(Rule::program, input)?; + Program::parse(pairs.into_iter().next().unwrap()) } #[cfg(test)] From 61649b8897b20a0e742e6c163f4881c6356f513e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 12 Dec 2025 19:01:45 -0300 Subject: [PATCH 09/13] feat: generalize syntax and improve parsing --- crates/tx3-lang/src/ast.rs | 15 +++++++++++++- crates/tx3-lang/src/cardano.rs | 22 +++++++++++++++++---- crates/tx3-lang/src/loading.rs | 4 ++-- crates/tx3-lang/src/parsing.rs | 36 ++++++++++++++++++++++++++++++---- crates/tx3-lang/src/tx3.pest | 15 +++++++++++--- examples/cip_imports.tx3 | 4 ++-- 6 files changed, 80 insertions(+), 16 deletions(-) diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index d2fd9576..2898f2bc 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -170,6 +170,19 @@ impl AsRef for Identifier { } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum ImportKind { + Cip57, + Tx3, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Import { + pub span: Span, + pub path: String, + pub kind: ImportKind, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] pub struct Program { pub env: Option, @@ -179,7 +192,7 @@ pub struct Program { pub assets: Vec, pub parties: Vec, pub policies: Vec, - pub imports: Vec, + pub imports: Vec, pub span: Span, // analysis diff --git a/crates/tx3-lang/src/cardano.rs b/crates/tx3-lang/src/cardano.rs index e46f42aa..cd813e65 100644 --- a/crates/tx3-lang/src/cardano.rs +++ b/crates/tx3-lang/src/cardano.rs @@ -843,7 +843,7 @@ impl IntoLower for CardanoBlock { /// Sanitizes a type name to be a valid tx3 identifier. /// Replaces characters like `<`, `>`, `,`, `/`, `$`, `~1` with underscores. -fn sanitize_type_name(name: &str) -> String { +fn generic_sanitizer(name: &str) -> String { name.replace("~1", "_") // URL-encoded `/` in JSON references .replace('/', "_") .replace('$', "_") @@ -853,7 +853,21 @@ fn sanitize_type_name(name: &str) -> String { .replace(' ', "") } -// TODO: add policies, and script parameters as well +pub enum LoadKind { + Cip57, +} + +pub enum Compiler { + Aiken, + Unknown, +} + +pub struct ExternalLoader { + pub path: String, + pub kind: LoadKind, + pub compiler: Compiler, +} + pub fn load_externals( path: &str, ) -> Result, crate::parsing::Error> { @@ -870,7 +884,7 @@ pub fn load_externals( })?; let ref_to_type = |r: &str| -> Type { - let sanitized = sanitize_type_name(r.strip_prefix("#/definitions/").unwrap_or(r)); + let sanitized = generic_sanitizer(r.strip_prefix("#/definitions/").unwrap_or(r)); Type::Custom(Identifier::new(&sanitized)) }; @@ -882,7 +896,7 @@ pub fn load_externals( .into_iter() .flatten() { - let name = sanitize_type_name(key); + let name = generic_sanitizer(key); let new = match def.data_type { Some(cip_57::DataType::Integer) => Some(Symbol::AliasDef(Box::new( diff --git a/crates/tx3-lang/src/loading.rs b/crates/tx3-lang/src/loading.rs index 9965f8f5..ab81f7b6 100644 --- a/crates/tx3-lang/src/loading.rs +++ b/crates/tx3-lang/src/loading.rs @@ -59,8 +59,8 @@ pub fn parse_file(path: &str) -> Result { } fn process_imports(program: &mut ast::Program, base_path: &Path) -> Result<(), Error> { - for import_path in &program.imports { - let full_path = base_path.join(import_path); + for import in &program.imports { + let full_path = base_path.join(&import.path); let path_str = full_path.to_str().ok_or_else(|| { Error::Io(std::io::Error::new( std::io::ErrorKind::InvalidInput, diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 2d885441..819f8bbb 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -76,6 +76,37 @@ pub trait AstNode: Sized { fn span(&self) -> &Span; } +impl AstNode for Import { + const RULE: Rule = Rule::import; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + let mut inner = pair.into_inner(); + + let import_rule = inner.next().unwrap(); + match import_rule.as_rule() { + Rule::cip57_import => { + let path = import_rule + .into_inner() + .as_str() + .trim_matches('"') + .to_string(); + Ok(Import { + span, + path, + kind: ImportKind::Cip57, + }) + } + Rule::tx3_import => todo!(), + x => unreachable!("Unexpected rule in import: {:?}", x), + } + } + + fn span(&self) -> &Span { + &self.span + } +} + impl AstNode for Program { const RULE: Rule = Rule::program; @@ -106,10 +137,7 @@ impl AstNode for Program { Rule::alias_def => program.aliases.push(AliasDef::parse(pair)?), Rule::party_def => program.parties.push(PartyDef::parse(pair)?), Rule::policy_def => program.policies.push(PolicyDef::parse(pair)?), - Rule::cardano_import => { - let import_path = pair.into_inner().next().unwrap().as_str().trim_matches('"'); - program.imports.push(import_path.to_string()); - } + Rule::import => program.imports.push(Import::parse(pair)?), Rule::EOI => break, x => unreachable!("Unexpected rule in program: {:?}", x), } diff --git a/crates/tx3-lang/src/tx3.pest b/crates/tx3-lang/src/tx3.pest index 4a43ce27..3cccb5b4 100644 --- a/crates/tx3-lang/src/tx3.pest +++ b/crates/tx3-lang/src/tx3.pest @@ -462,14 +462,23 @@ tx_def = { "tx" ~ identifier ~ parameter_list ~ "{" ~ tx_body_block* ~ "}" } -cardano_import = { - "cardano::import " ~ string ~ ";" +cip57_import = { + "cip57::import" ~ string ~ ";" +} + +tx3_import = { + "tx3::import" ~ string ~ ";" +} + +import = { + cip57_import | + tx3_import } // Program program = { SOI ~ - (cardano_import)* ~ + import* ~ (env_def | asset_def | party_def | policy_def | type_def | tx_def)* ~ EOI } diff --git a/examples/cip_imports.tx3 b/examples/cip_imports.tx3 index 9a88e668..b8d96d75 100644 --- a/examples/cip_imports.tx3 +++ b/examples/cip_imports.tx3 @@ -1,4 +1,4 @@ -cardano::import "imports/plutus.json"; +cip57::import "imports/plutus.json"; party Sender; @@ -11,7 +11,7 @@ tx transfer_with_imports( from: Sender, min_amount: Ada(quantity), } - + output { to: Receiver, amount: Ada(quantity), From 1d6b5ca6dbd519b768ac9e9ce60ea59606006f83 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Mon, 15 Dec 2025 15:18:17 -0300 Subject: [PATCH 10/13] feat: add scope inspection methods to Scope and Program structs --- crates/tx3-lang/src/ast.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index 1c89006a..0483bc13 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -17,6 +17,16 @@ pub struct Scope { pub(crate) parent: Option>, } +impl Scope { + pub fn symbols(&self) -> &HashMap { + &self.symbols + } + + pub fn parent(&self) -> Option<&Rc> { + self.parent.as_ref() + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum Symbol { EnvVar(String, Box), @@ -186,6 +196,12 @@ pub struct Program { pub(crate) scope: Option>, } +impl Program { + pub fn scope(&self) -> Option<&Rc> { + self.scope.as_ref() + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct EnvField { pub name: String, From 3837fe6de0d46a6c88018a0bf3b98e1145db51cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Wed, 17 Dec 2025 16:34:47 -0300 Subject: [PATCH 11/13] refactor: cip57 -> blueprint & cardano God file split up into digestible parts --- crates/tx3-lang/src/ast.rs | 2 +- crates/tx3-lang/src/cardano/analyzing.rs | 271 +++++++ crates/tx3-lang/src/cardano/ast.rs | 120 +++ crates/tx3-lang/src/cardano/blueprint.rs | 247 ++++++ crates/tx3-lang/src/cardano/lowering.rs | 228 ++++++ crates/tx3-lang/src/cardano/mod.rs | 8 + .../src/{cardano.rs => cardano/parsing.rs} | 757 +++--------------- crates/tx3-lang/src/loading.rs | 5 +- crates/tx3-lang/src/parsing.rs | 6 +- crates/tx3-lang/src/tx3.pest | 6 +- examples/cip_imports.tx3 | 10 +- 11 files changed, 1022 insertions(+), 638 deletions(-) create mode 100644 crates/tx3-lang/src/cardano/analyzing.rs create mode 100644 crates/tx3-lang/src/cardano/ast.rs create mode 100644 crates/tx3-lang/src/cardano/blueprint.rs create mode 100644 crates/tx3-lang/src/cardano/lowering.rs create mode 100644 crates/tx3-lang/src/cardano/mod.rs rename crates/tx3-lang/src/{cardano.rs => cardano/parsing.rs} (51%) diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index 69da470c..acbb48e0 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -182,7 +182,7 @@ impl AsRef for Identifier { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum ImportKind { - Cip57, + Blueprint, Tx3, } diff --git a/crates/tx3-lang/src/cardano/analyzing.rs b/crates/tx3-lang/src/cardano/analyzing.rs new file mode 100644 index 00000000..c4c74f3d --- /dev/null +++ b/crates/tx3-lang/src/cardano/analyzing.rs @@ -0,0 +1,271 @@ +use std::rc::Rc; + +use crate::{ + analyzing::{Analyzable, AnalyzeReport}, + ast::{Scope, Type}, +}; + +use super::ast::*; + +impl Analyzable for WithdrawalField { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + match self { + WithdrawalField::From(x) => x.analyze(parent), + WithdrawalField::Amount(x) => { + let amount = x.analyze(parent.clone()); + let amount_type = AnalyzeReport::expect_data_expr_type(x, &Type::Int); + amount + amount_type + } + WithdrawalField::Redeemer(x) => x.analyze(parent), + } + } + + fn is_resolved(&self) -> bool { + match self { + WithdrawalField::From(x) => x.is_resolved(), + WithdrawalField::Amount(x) => x.is_resolved(), + WithdrawalField::Redeemer(x) => x.is_resolved(), + } + } +} + +impl Analyzable for WithdrawalBlock { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + self.fields.analyze(parent) + } + + fn is_resolved(&self) -> bool { + self.fields.is_resolved() + } +} + +impl Analyzable for VoteDelegationCertificate { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + let drep = self.drep.analyze(parent.clone()); + let stake = self.stake.analyze(parent.clone()); + + drep + stake + } + + fn is_resolved(&self) -> bool { + self.drep.is_resolved() && self.stake.is_resolved() + } +} + +impl Analyzable for StakeDelegationCertificate { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + let pool = self.pool.analyze(parent.clone()); + let stake = self.stake.analyze(parent.clone()); + + pool + stake + } + + fn is_resolved(&self) -> bool { + self.pool.is_resolved() && self.stake.is_resolved() + } +} + +impl Analyzable for PlutusWitnessField { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + match self { + PlutusWitnessField::Version(x, _) => x.analyze(parent), + PlutusWitnessField::Script(x, _) => x.analyze(parent), + } + } + + fn is_resolved(&self) -> bool { + match self { + PlutusWitnessField::Version(x, _) => x.is_resolved(), + PlutusWitnessField::Script(x, _) => x.is_resolved(), + } + } +} + +impl Analyzable for PlutusWitnessBlock { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + self.fields.analyze(parent) + } + + fn is_resolved(&self) -> bool { + self.fields.is_resolved() + } +} + +impl Analyzable for NativeWitnessField { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + match self { + NativeWitnessField::Script(x, _) => x.analyze(parent), + } + } + + fn is_resolved(&self) -> bool { + match self { + NativeWitnessField::Script(x, _) => x.is_resolved(), + } + } +} + +impl Analyzable for NativeWitnessBlock { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + self.fields.analyze(parent) + } + + fn is_resolved(&self) -> bool { + self.fields.is_resolved() + } +} + +impl Analyzable for TreasuryDonationBlock { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + let coin = self.coin.analyze(parent); + let coin_type = AnalyzeReport::expect_data_expr_type(&self.coin, &Type::Int); + + coin + coin_type + } + + fn is_resolved(&self) -> bool { + self.coin.is_resolved() + } +} + +impl Analyzable for CardanoPublishBlockField { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + match self { + CardanoPublishBlockField::To(x) => x.analyze(parent), + CardanoPublishBlockField::Amount(x) => x.analyze(parent), + CardanoPublishBlockField::Datum(x) => x.analyze(parent), + CardanoPublishBlockField::Version(x) => x.analyze(parent), + CardanoPublishBlockField::Script(x) => x.analyze(parent), + } + } + + fn is_resolved(&self) -> bool { + match self { + CardanoPublishBlockField::To(x) => x.is_resolved(), + CardanoPublishBlockField::Amount(x) => x.is_resolved(), + CardanoPublishBlockField::Datum(x) => x.is_resolved(), + CardanoPublishBlockField::Version(x) => x.is_resolved(), + CardanoPublishBlockField::Script(x) => x.is_resolved(), + } + } +} + +impl Analyzable for CardanoPublishBlock { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + self.fields.analyze(parent) + } + + fn is_resolved(&self) -> bool { + self.fields.is_resolved() + } +} + +impl Analyzable for CardanoBlock { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + match self { + CardanoBlock::VoteDelegationCertificate(x) => x.analyze(parent), + CardanoBlock::StakeDelegationCertificate(x) => x.analyze(parent), + CardanoBlock::Withdrawal(x) => x.analyze(parent), + CardanoBlock::PlutusWitness(x) => x.analyze(parent), + CardanoBlock::NativeWitness(x) => x.analyze(parent), + CardanoBlock::TreasuryDonation(x) => x.analyze(parent), + CardanoBlock::Publish(x) => x.analyze(parent), + } + } + + fn is_resolved(&self) -> bool { + match self { + CardanoBlock::VoteDelegationCertificate(x) => x.is_resolved(), + CardanoBlock::StakeDelegationCertificate(x) => x.is_resolved(), + CardanoBlock::Withdrawal(x) => x.is_resolved(), + CardanoBlock::PlutusWitness(x) => x.is_resolved(), + CardanoBlock::NativeWitness(x) => x.is_resolved(), + Self::TreasuryDonation(x) => x.is_resolved(), + CardanoBlock::Publish(x) => x.is_resolved(), + } + } +} + +#[cfg(test)] +mod tests { + + use crate::analyzing::analyze; + + #[test] + fn test_treasury_donation_type() { + let mut ast = crate::parsing::parse_string( + r#" + tx test(quantity: Int) { + cardano::treasury_donation { + coin: quantity, + } + } + "#, + ) + .unwrap(); + + let result = analyze(&mut ast); + assert!(result.errors.is_empty()); + } + + #[test] + fn test_treasury_donation_type_not_ok() { + let mut ast = crate::parsing::parse_string( + r#" + tx test(quantity: Bytes) { + cardano::treasury_donation { + coin: quantity, + } + } + "#, + ) + .unwrap(); + + let result = analyze(&mut ast); + assert!(!result.errors.is_empty()); + } + + #[test] + fn test_publish_type_ok() { + let mut ast = crate::parsing::parse_string( + r#" + party Receiver; + + tx test(quantity: Int) { + cardano::publish { + to: Receiver, + amount: Ada(quantity), + version: 3, + script: 0xABCDEF, + } + } + "#, + ) + .unwrap(); + + let result = analyze(&mut ast); + assert!(result.errors.is_empty()); + } + + #[test] + fn test_publish_type_with_name_ok() { + let mut ast = crate::parsing::parse_string( + r#" + party Receiver; + + tx test(quantity: Int) { + cardano::publish deploy { + to: Receiver, + amount: Ada(quantity), + version: 3, + script: 0xABCDEF, + } + } + "#, + ) + .unwrap(); + + let result = analyze(&mut ast); + assert!(result.errors.is_empty()); + } +} diff --git a/crates/tx3-lang/src/cardano/ast.rs b/crates/tx3-lang/src/cardano/ast.rs new file mode 100644 index 00000000..5cf45473 --- /dev/null +++ b/crates/tx3-lang/src/cardano/ast.rs @@ -0,0 +1,120 @@ +use serde::{Deserialize, Serialize}; + +use crate::ast::{DataExpr, Identifier, Span}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum WithdrawalField { + From(Box), + Amount(Box), + Redeemer(Box), +} + +impl WithdrawalField { + pub(crate) fn key(&self) -> &str { + match self { + WithdrawalField::From(_) => "from", + WithdrawalField::Amount(_) => "amount", + WithdrawalField::Redeemer(_) => "redeemer", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct WithdrawalBlock { + pub fields: Vec, + pub span: Span, +} + +impl WithdrawalBlock { + pub(crate) fn find(&self, key: &str) -> Option<&WithdrawalField> { + self.fields.iter().find(|x| x.key() == key) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct VoteDelegationCertificate { + pub drep: DataExpr, + pub stake: DataExpr, + pub span: Span, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct StakeDelegationCertificate { + pub pool: DataExpr, + pub stake: DataExpr, + pub span: Span, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum PlutusWitnessField { + Version(DataExpr, Span), + Script(DataExpr, Span), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PlutusWitnessBlock { + pub fields: Vec, + pub span: Span, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum NativeWitnessField { + Script(DataExpr, Span), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct NativeWitnessBlock { + pub fields: Vec, + pub span: Span, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TreasuryDonationBlock { + pub coin: DataExpr, + pub span: Span, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum CardanoPublishBlockField { + To(Box), + Amount(Box), + Datum(Box), + Version(Box), + Script(Box), +} + +impl CardanoPublishBlockField { + pub(crate) fn key(&self) -> &str { + match self { + CardanoPublishBlockField::To(_) => "to", + CardanoPublishBlockField::Amount(_) => "amount", + CardanoPublishBlockField::Datum(_) => "datum", + CardanoPublishBlockField::Version(_) => "version", + CardanoPublishBlockField::Script(_) => "script", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct CardanoPublishBlock { + pub name: Option, + pub fields: Vec, + pub span: Span, +} + +impl CardanoPublishBlock { + pub(crate) fn find(&self, key: &str) -> Option<&CardanoPublishBlockField> { + self.fields.iter().find(|x| x.key() == key) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum CardanoBlock { + VoteDelegationCertificate(VoteDelegationCertificate), + StakeDelegationCertificate(StakeDelegationCertificate), + Withdrawal(WithdrawalBlock), + PlutusWitness(PlutusWitnessBlock), + NativeWitness(NativeWitnessBlock), + TreasuryDonation(TreasuryDonationBlock), + Publish(CardanoPublishBlock), +} diff --git a/crates/tx3-lang/src/cardano/blueprint.rs b/crates/tx3-lang/src/cardano/blueprint.rs new file mode 100644 index 00000000..d9746221 --- /dev/null +++ b/crates/tx3-lang/src/cardano/blueprint.rs @@ -0,0 +1,247 @@ +use std::collections::HashMap; +use std::collections::HashSet; + +/// Decodes JSON pointer escapes (~1 -> /, ~0 -> ~). +fn decode_json_pointer_escapes(s: &str) -> String { + s.replace("~1", "/").replace("~0", "~") +} + +/// Strips known prefixes (Option, Pairs) and extracts the last segment +pub fn aiken_prettify_name(name: &str) -> String { + let decoded = decode_json_pointer_escapes(name); + + if let Some(inner) = decoded.strip_prefix("Option$") { + let inner_pretty = aiken_prettify_name(inner); + return format!("Option{}", inner_pretty); + } + + 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 parts: Vec<&str> = inner.split('_').collect(); + let prettified: Vec = parts.iter().map(|p| aiken_prettify_name(p)).collect(); + return format!("Pairs{}", prettified.join("")); + } + + if let Some(inner) = decoded.strip_prefix("Pairs<") { + let inner = inner.strip_suffix(">").unwrap_or(inner); + let parts: Vec<&str> = inner.split(',').collect(); + let prettified: Vec = parts + .iter() + .map(|p| aiken_prettify_name(p.trim())) + .collect(); + return format!("Pairs{}", prettified.join("")); + } + + extract_last_segment(&decoded) +} + +fn extract_last_segment(path: &str) -> String { + path.rsplit('/').next().unwrap_or(path).to_string() +} + +/// Converts a path to upper camel case handling nested generic types like Option and Pairs +pub fn path_to_upper_camel(name: &str) -> String { + let decoded = decode_json_pointer_escapes(name); + + if let Some(inner) = decoded.strip_prefix("Option$") { + let inner_camel = path_to_upper_camel(inner); + return format!("Option{}", inner_camel); + } + + if let Some(inner) = decoded.strip_prefix("Option<") { + let inner = inner.strip_suffix(">").unwrap_or(inner); + let inner_camel = path_to_upper_camel(inner); + return format!("Option{}", inner_camel); + } + + if let Some(inner) = decoded.strip_prefix("Pairs$") { + let parts: Vec<&str> = inner.split('_').collect(); + let prettified: Vec = parts.iter().map(|p| path_to_upper_camel(p)).collect(); + return format!("Pairs{}", prettified.join("")); + } + + if let Some(inner) = decoded.strip_prefix("Pairs<") { + let inner = inner.strip_suffix(">").unwrap_or(inner); + let parts: Vec<&str> = inner.split(',').collect(); + let prettified: Vec = parts + .iter() + .map(|p| path_to_upper_camel(p.trim())) + .collect(); + return format!("Pairs{}", prettified.join("")); + } + + path_segments_to_camel(&decoded) +} + +fn path_segments_to_camel(path: &str) -> String { + path.split('/') + .map(|segment| { + let mut chars = segment.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().to_string() + chars.as_str(), + } + }) + .collect() +} + +pub fn build_aiken_name_mapping(keys: &[&str]) -> HashMap { + // collect simplified names and detect clashes + let mut simple_to_originals: HashMap> = HashMap::new(); + + for &key in 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 keys { + let simple = aiken_prettify_name(key); + let originals = simple_to_originals.get(&simple).unwrap(); + + let final_name = if originals.len() == 1 { + simple.clone() + } else { + path_to_upper_camel(key) + }; + + // ensure uniqueness + 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); + } + + result +} + +/// Sanitizes a type name to be a valid tx3 identifier +pub fn generic_sanitizer(name: &str) -> String { + name.replace("~1", "_") + .replace('/', "_") + .replace('$', "_") + .replace('<', "_") + .replace('>', "") + .replace(',', "_") + .replace(' ', "") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_aiken_prettify_name_basic() { + assert_eq!(aiken_prettify_name("cardano/address/Address"), "Address"); + assert_eq!(aiken_prettify_name("aiken/crypto/ScriptHash"), "ScriptHash"); + assert_eq!(aiken_prettify_name("types/SettingsDatum"), "SettingsDatum"); + + assert_eq!(aiken_prettify_name("Int"), "Int"); + assert_eq!(aiken_prettify_name("Bool"), "Bool"); + assert_eq!(aiken_prettify_name("ByteArray"), "ByteArray"); + } + + #[test] + fn test_aiken_prettify_name_option_and_pairs() { + assert_eq!( + aiken_prettify_name("Option$cardano/address/Address"), + "OptionAddress" + ); + assert_eq!( + aiken_prettify_name("Option$cardano/address/StakeCredential"), + "OptionStakeCredential" + ); + + assert_eq!( + aiken_prettify_name("Pairs$cardano/assets/AssetName_Int"), + "PairsAssetNameInt" + ); + + assert_eq!( + aiken_prettify_name("Pairs$cardano/assets/PolicyId_Pairs$cardano/assets/AssetName_Int"), + "PairsPolicyIdPairsAssetNameInt" + ); + } + + #[test] + fn test_aiken_prettify_name_url_encoded() { + assert_eq!(aiken_prettify_name("cardano~1address~1Address"), "Address"); + } + + #[test] + fn test_aiken_prettify_name_generics() { + assert_eq!( + aiken_prettify_name("Option"), + "OptionStakeCredential" + ); + + assert_eq!( + aiken_prettify_name("Pairs"), + "PairsAssetNameInt" + ); + assert_eq!( + aiken_prettify_name("Pairs"), + "PairsAssetNameInt" + ); + } + + #[test] + fn test_path_to_upper_camel_generics() { + assert_eq!( + path_to_upper_camel("Option"), + "OptionCardanoAddressStakeCredential" + ); + + assert_eq!( + path_to_upper_camel("Pairs"), + "PairsCardanoAssetsAssetNameInt" + ); + } + + #[test] + fn test_build_aiken_name_mapping_no_clashes() { + let keys = vec![ + "cardano/address/Address", + "cardano/assets/AssetName", + "types/SettingsDatum", + "Int", + ]; + let mapping = build_aiken_name_mapping(&keys); + + assert_eq!(mapping.get("cardano/address/Address").unwrap(), "Address"); + assert_eq!( + mapping.get("cardano/assets/AssetName").unwrap(), + "AssetName" + ); + assert_eq!(mapping.get("types/SettingsDatum").unwrap(), "SettingsDatum"); + assert_eq!(mapping.get("Int").unwrap(), "Int"); + } + + #[test] + fn test_build_aiken_name_mapping_with_clashes() { + // Both cardano/transaction/Datum and types/Datum map into Datum + let keys = vec!["cardano/transaction/Datum", "types/Datum"]; + let mapping = build_aiken_name_mapping(&keys); + + assert_eq!( + mapping.get("cardano/transaction/Datum").unwrap(), + "CardanoTransactionDatum" + ); + assert_eq!(mapping.get("types/Datum").unwrap(), "TypesDatum"); + } +} diff --git a/crates/tx3-lang/src/cardano/lowering.rs b/crates/tx3-lang/src/cardano/lowering.rs new file mode 100644 index 00000000..ad607d5d --- /dev/null +++ b/crates/tx3-lang/src/cardano/lowering.rs @@ -0,0 +1,228 @@ +use std::collections::HashMap; + +use crate::{ir, lowering::IntoLower}; + +use super::ast::*; + +impl IntoLower for WithdrawalField { + type Output = ir::Expression; + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + match self { + WithdrawalField::From(x) => x.into_lower(ctx), + WithdrawalField::Amount(x) => x.into_lower(ctx), + WithdrawalField::Redeemer(x) => x.into_lower(ctx), + } + } +} + +impl IntoLower for WithdrawalBlock { + type Output = ir::AdHocDirective; + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + let credential = self + .find("from") + .ok_or_else(|| { + crate::lowering::Error::MissingRequiredField("from".to_string(), "WithdrawalBlock") + })? + .into_lower(ctx)?; + + let amount = self + .find("amount") + .ok_or_else(|| { + crate::lowering::Error::MissingRequiredField( + "amount".to_string(), + "WithdrawalBlock", + ) + })? + .into_lower(ctx)?; + + let redeemer = self + .find("redeemer") + .map(|r| r.into_lower(ctx)) + .transpose()? + .unwrap_or(ir::Expression::None); + + Ok(ir::AdHocDirective { + name: "withdrawal".to_string(), + data: std::collections::HashMap::from([ + ("credential".to_string(), credential), + ("amount".to_string(), amount), + ("redeemer".to_string(), redeemer), + ]), + }) + } +} + +impl IntoLower for VoteDelegationCertificate { + type Output = ir::AdHocDirective; + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + Ok(ir::AdHocDirective { + name: "vote_delegation_certificate".to_string(), + data: HashMap::from([ + ("drep".to_string(), self.drep.into_lower(ctx)?), + ("stake".to_string(), self.stake.into_lower(ctx)?), + ]), + }) + } +} + +impl IntoLower for StakeDelegationCertificate { + type Output = ir::AdHocDirective; + + fn into_lower( + &self, + _ctx: &crate::lowering::Context, + ) -> Result { + todo!("StakeDelegationCertificate lowering not implemented") + } +} + +impl IntoLower for PlutusWitnessField { + type Output = (String, ir::Expression); + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + match self { + PlutusWitnessField::Version(x, _) => Ok(("version".to_string(), x.into_lower(ctx)?)), + PlutusWitnessField::Script(x, _) => Ok(("script".to_string(), x.into_lower(ctx)?)), + } + } +} + +impl IntoLower for PlutusWitnessBlock { + type Output = ir::AdHocDirective; + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + let data = self + .fields + .iter() + .map(|x| x.into_lower(ctx)) + .collect::>()?; + + Ok(ir::AdHocDirective { + name: "plutus_witness".to_string(), + data, + }) + } +} + +impl IntoLower for NativeWitnessField { + type Output = (String, ir::Expression); + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + match self { + NativeWitnessField::Script(x, _) => Ok(("script".to_string(), x.into_lower(ctx)?)), + } + } +} + +impl IntoLower for NativeWitnessBlock { + type Output = ir::AdHocDirective; + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + let data = self + .fields + .iter() + .map(|x| x.into_lower(ctx)) + .collect::>()?; + + Ok(ir::AdHocDirective { + name: "native_witness".to_string(), + data, + }) + } +} + +impl IntoLower for TreasuryDonationBlock { + type Output = ir::AdHocDirective; + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + let coin = self.coin.into_lower(ctx)?; + + Ok(ir::AdHocDirective { + name: "treasury_donation".to_string(), + data: std::collections::HashMap::from([("coin".to_string(), coin)]), + }) + } +} + +impl IntoLower for CardanoPublishBlockField { + type Output = (String, ir::Expression); + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + match self { + CardanoPublishBlockField::To(x) => Ok(("to".to_string(), x.into_lower(ctx)?)), + CardanoPublishBlockField::Amount(x) => Ok(("amount".to_string(), x.into_lower(ctx)?)), + CardanoPublishBlockField::Datum(x) => Ok(("datum".to_string(), x.into_lower(ctx)?)), + CardanoPublishBlockField::Version(x) => Ok(("version".to_string(), x.into_lower(ctx)?)), + CardanoPublishBlockField::Script(x) => Ok(("script".to_string(), x.into_lower(ctx)?)), + } + } +} + +impl IntoLower for CardanoPublishBlock { + type Output = ir::AdHocDirective; + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + let data = self + .fields + .iter() + .map(|x| x.into_lower(ctx)) + .collect::>()?; + + Ok(ir::AdHocDirective { + name: "cardano_publish".to_string(), + data, + }) + } +} + +impl IntoLower for CardanoBlock { + type Output = ir::AdHocDirective; + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result<::Output, crate::lowering::Error> { + match self { + CardanoBlock::VoteDelegationCertificate(x) => x.into_lower(ctx), + CardanoBlock::StakeDelegationCertificate(x) => x.into_lower(ctx), + CardanoBlock::Withdrawal(x) => x.into_lower(ctx), + CardanoBlock::PlutusWitness(x) => x.into_lower(ctx), + CardanoBlock::NativeWitness(x) => x.into_lower(ctx), + CardanoBlock::TreasuryDonation(x) => x.into_lower(ctx), + CardanoBlock::Publish(x) => x.into_lower(ctx), + } + } +} diff --git a/crates/tx3-lang/src/cardano/mod.rs b/crates/tx3-lang/src/cardano/mod.rs new file mode 100644 index 00000000..5d2da74e --- /dev/null +++ b/crates/tx3-lang/src/cardano/mod.rs @@ -0,0 +1,8 @@ +pub mod analyzing; +pub mod ast; +pub mod blueprint; +pub mod lowering; +pub mod parsing; + +pub use ast::*; +pub use parsing::load_externals; diff --git a/crates/tx3-lang/src/cardano.rs b/crates/tx3-lang/src/cardano/parsing.rs similarity index 51% rename from crates/tx3-lang/src/cardano.rs rename to crates/tx3-lang/src/cardano/parsing.rs index cd813e65..34219fe2 100644 --- a/crates/tx3-lang/src/cardano.rs +++ b/crates/tx3-lang/src/cardano/parsing.rs @@ -1,44 +1,13 @@ -use std::{collections::HashMap, fs, rc::Rc}; +use std::{collections::HashMap, fs}; use pest::iterators::Pair; -use serde::{Deserialize, Serialize}; use crate::{ - analyzing::{Analyzable, AnalyzeReport}, - ast::{DataExpr, Identifier, RecordField, Scope, Span, Symbol, Type, TypeDef, VariantCase}, - ir, - lowering::IntoLower, + ast::{DataExpr, Identifier, RecordField, Span, Symbol, Type, TypeDef, VariantCase}, parsing::{AstNode, Error, Rule}, }; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum WithdrawalField { - From(Box), - Amount(Box), - Redeemer(Box), -} - -impl WithdrawalField { - fn key(&self) -> &str { - match self { - WithdrawalField::From(_) => "from", - WithdrawalField::Amount(_) => "amount", - WithdrawalField::Redeemer(_) => "redeemer", - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct WithdrawalBlock { - pub fields: Vec, - pub span: Span, -} - -impl WithdrawalBlock { - pub(crate) fn find(&self, key: &str) -> Option<&WithdrawalField> { - self.fields.iter().find(|x| x.key() == key) - } -} +use super::{ast::*, blueprint}; impl AstNode for WithdrawalField { const RULE: Rule = Rule::cardano_withdrawal_field; @@ -89,101 +58,6 @@ impl AstNode for WithdrawalBlock { } } -impl Analyzable for WithdrawalField { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - match self { - WithdrawalField::From(x) => x.analyze(parent), - WithdrawalField::Amount(x) => { - let amount = x.analyze(parent.clone()); - let amount_type = AnalyzeReport::expect_data_expr_type(x, &Type::Int); - amount + amount_type - } - WithdrawalField::Redeemer(x) => x.analyze(parent), - } - } - - fn is_resolved(&self) -> bool { - match self { - WithdrawalField::From(x) => x.is_resolved(), - WithdrawalField::Amount(x) => x.is_resolved(), - WithdrawalField::Redeemer(x) => x.is_resolved(), - } - } -} - -impl Analyzable for WithdrawalBlock { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - self.fields.analyze(parent) - } - - fn is_resolved(&self) -> bool { - self.fields.is_resolved() - } -} - -impl IntoLower for WithdrawalField { - type Output = ir::Expression; - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - match self { - WithdrawalField::From(x) => x.into_lower(ctx), - WithdrawalField::Amount(x) => x.into_lower(ctx), - WithdrawalField::Redeemer(x) => x.into_lower(ctx), - } - } -} - -impl IntoLower for WithdrawalBlock { - type Output = ir::AdHocDirective; - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - let credential = self - .find("from") - .ok_or_else(|| { - crate::lowering::Error::MissingRequiredField("from".to_string(), "WithdrawalBlock") - })? - .into_lower(ctx)?; - - let amount = self - .find("amount") - .ok_or_else(|| { - crate::lowering::Error::MissingRequiredField( - "amount".to_string(), - "WithdrawalBlock", - ) - })? - .into_lower(ctx)?; - - let redeemer = self - .find("redeemer") - .map(|r| r.into_lower(ctx)) - .transpose()? - .unwrap_or(ir::Expression::None); - - Ok(ir::AdHocDirective { - name: "withdrawal".to_string(), - data: std::collections::HashMap::from([ - ("credential".to_string(), credential), - ("amount".to_string(), amount), - ("redeemer".to_string(), redeemer), - ]), - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct VoteDelegationCertificate { - pub drep: DataExpr, - pub stake: DataExpr, - pub span: Span, -} - impl AstNode for VoteDelegationCertificate { const RULE: Rule = Rule::cardano_vote_delegation_certificate; @@ -203,43 +77,6 @@ impl AstNode for VoteDelegationCertificate { } } -impl Analyzable for VoteDelegationCertificate { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - let drep = self.drep.analyze(parent.clone()); - let stake = self.stake.analyze(parent.clone()); - - drep + stake - } - - fn is_resolved(&self) -> bool { - self.drep.is_resolved() && self.stake.is_resolved() - } -} - -impl IntoLower for VoteDelegationCertificate { - type Output = ir::AdHocDirective; - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - Ok(ir::AdHocDirective { - name: "vote_delegation_certificate".to_string(), - data: HashMap::from([ - ("drep".to_string(), self.drep.into_lower(ctx)?), - ("stake".to_string(), self.stake.into_lower(ctx)?), - ]), - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct StakeDelegationCertificate { - pub pool: DataExpr, - pub stake: DataExpr, - pub span: Span, -} - impl AstNode for StakeDelegationCertificate { const RULE: Rule = Rule::cardano_stake_delegation_certificate; @@ -259,50 +96,6 @@ impl AstNode for StakeDelegationCertificate { } } -impl Analyzable for StakeDelegationCertificate { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - let pool = self.pool.analyze(parent.clone()); - let stake = self.stake.analyze(parent.clone()); - - pool + stake - } - - fn is_resolved(&self) -> bool { - self.pool.is_resolved() && self.stake.is_resolved() - } -} - -impl IntoLower for StakeDelegationCertificate { - type Output = ir::AdHocDirective; - - fn into_lower( - &self, - _ctx: &crate::lowering::Context, - ) -> Result { - todo!("StakeDelegationCertificate lowering not implemented") - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum PlutusWitnessField { - Version(DataExpr, Span), - Script(DataExpr, Span), -} - -impl IntoLower for PlutusWitnessField { - type Output = (String, ir::Expression); - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - match self { - PlutusWitnessField::Version(x, _) => Ok(("version".to_string(), x.into_lower(ctx)?)), - PlutusWitnessField::Script(x, _) => Ok(("script".to_string(), x.into_lower(ctx)?)), - } - } -} - impl AstNode for PlutusWitnessField { const RULE: Rule = Rule::cardano_plutus_witness_field; @@ -328,28 +121,6 @@ impl AstNode for PlutusWitnessField { } } -impl Analyzable for PlutusWitnessField { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - match self { - PlutusWitnessField::Version(x, _) => x.analyze(parent), - PlutusWitnessField::Script(x, _) => x.analyze(parent), - } - } - - fn is_resolved(&self) -> bool { - match self { - PlutusWitnessField::Version(x, _) => x.is_resolved(), - PlutusWitnessField::Script(x, _) => x.is_resolved(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct PlutusWitnessBlock { - pub fields: Vec, - pub span: Span, -} - impl AstNode for PlutusWitnessBlock { const RULE: Rule = Rule::cardano_plutus_witness_block; @@ -369,54 +140,6 @@ impl AstNode for PlutusWitnessBlock { } } -impl Analyzable for PlutusWitnessBlock { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - self.fields.analyze(parent) - } - - fn is_resolved(&self) -> bool { - self.fields.is_resolved() - } -} - -impl IntoLower for PlutusWitnessBlock { - type Output = ir::AdHocDirective; - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - let data = self - .fields - .iter() - .map(|x| x.into_lower(ctx)) - .collect::>()?; - - Ok(ir::AdHocDirective { - name: "plutus_witness".to_string(), - data, - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum NativeWitnessField { - Script(DataExpr, Span), -} - -impl IntoLower for NativeWitnessField { - type Output = (String, ir::Expression); - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - match self { - NativeWitnessField::Script(x, _) => Ok(("script".to_string(), x.into_lower(ctx)?)), - } - } -} - impl AstNode for NativeWitnessField { const RULE: Rule = Rule::cardano_native_witness_field; @@ -438,26 +161,6 @@ impl AstNode for NativeWitnessField { } } -impl Analyzable for NativeWitnessField { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - match self { - NativeWitnessField::Script(x, _) => x.analyze(parent), - } - } - - fn is_resolved(&self) -> bool { - match self { - NativeWitnessField::Script(x, _) => x.is_resolved(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct NativeWitnessBlock { - pub fields: Vec, - pub span: Span, -} - impl AstNode for NativeWitnessBlock { const RULE: Rule = Rule::cardano_native_witness_block; @@ -477,42 +180,6 @@ impl AstNode for NativeWitnessBlock { } } -impl Analyzable for NativeWitnessBlock { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - self.fields.analyze(parent) - } - - fn is_resolved(&self) -> bool { - self.fields.is_resolved() - } -} - -impl IntoLower for NativeWitnessBlock { - type Output = ir::AdHocDirective; - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - let data = self - .fields - .iter() - .map(|x| x.into_lower(ctx)) - .collect::>()?; - - Ok(ir::AdHocDirective { - name: "native_witness".to_string(), - data, - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct TreasuryDonationBlock { - pub coin: DataExpr, - pub span: Span, -} - impl AstNode for TreasuryDonationBlock { const RULE: Rule = Rule::cardano_treasury_donation_block; @@ -530,69 +197,6 @@ impl AstNode for TreasuryDonationBlock { } } -impl Analyzable for TreasuryDonationBlock { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - let coin = self.coin.analyze(parent); - let coin_type = AnalyzeReport::expect_data_expr_type(&self.coin, &Type::Int); - - coin + coin_type - } - - fn is_resolved(&self) -> bool { - self.coin.is_resolved() - } -} - -impl IntoLower for TreasuryDonationBlock { - type Output = ir::AdHocDirective; - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - let coin = self.coin.into_lower(ctx)?; - - Ok(ir::AdHocDirective { - name: "treasury_donation".to_string(), - data: std::collections::HashMap::from([("coin".to_string(), coin)]), - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum CardanoPublishBlockField { - To(Box), - Amount(Box), - Datum(Box), - Version(Box), - Script(Box), -} - -impl CardanoPublishBlockField { - fn key(&self) -> &str { - match self { - CardanoPublishBlockField::To(_) => "to", - CardanoPublishBlockField::Amount(_) => "amount", - CardanoPublishBlockField::Datum(_) => "datum", - CardanoPublishBlockField::Version(_) => "version", - CardanoPublishBlockField::Script(_) => "script", - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct CardanoPublishBlock { - pub name: Option, - pub fields: Vec, - pub span: Span, -} - -impl CardanoPublishBlock { - pub(crate) fn find(&self, key: &str) -> Option<&CardanoPublishBlockField> { - self.fields.iter().find(|x| x.key() == key) - } -} - impl AstNode for CardanoPublishBlockField { const RULE: Rule = Rule::cardano_publish_block_field; @@ -670,86 +274,6 @@ impl AstNode for CardanoPublishBlock { } } -impl Analyzable for CardanoPublishBlockField { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - match self { - CardanoPublishBlockField::To(x) => x.analyze(parent), - CardanoPublishBlockField::Amount(x) => x.analyze(parent), - CardanoPublishBlockField::Datum(x) => x.analyze(parent), - CardanoPublishBlockField::Version(x) => x.analyze(parent), - CardanoPublishBlockField::Script(x) => x.analyze(parent), - } - } - - fn is_resolved(&self) -> bool { - match self { - CardanoPublishBlockField::To(x) => x.is_resolved(), - CardanoPublishBlockField::Amount(x) => x.is_resolved(), - CardanoPublishBlockField::Datum(x) => x.is_resolved(), - CardanoPublishBlockField::Version(x) => x.is_resolved(), - CardanoPublishBlockField::Script(x) => x.is_resolved(), - } - } -} - -impl Analyzable for CardanoPublishBlock { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - self.fields.analyze(parent) - } - - fn is_resolved(&self) -> bool { - self.fields.is_resolved() - } -} - -impl IntoLower for CardanoPublishBlockField { - type Output = (String, ir::Expression); - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - match self { - CardanoPublishBlockField::To(x) => Ok(("to".to_string(), x.into_lower(ctx)?)), - CardanoPublishBlockField::Amount(x) => Ok(("amount".to_string(), x.into_lower(ctx)?)), - CardanoPublishBlockField::Datum(x) => Ok(("datum".to_string(), x.into_lower(ctx)?)), - CardanoPublishBlockField::Version(x) => Ok(("version".to_string(), x.into_lower(ctx)?)), - CardanoPublishBlockField::Script(x) => Ok(("script".to_string(), x.into_lower(ctx)?)), - } - } -} - -impl IntoLower for CardanoPublishBlock { - type Output = ir::AdHocDirective; - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - let data = self - .fields - .iter() - .map(|x| x.into_lower(ctx)) - .collect::>()?; - - Ok(ir::AdHocDirective { - name: "cardano_publish".to_string(), - data, - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum CardanoBlock { - VoteDelegationCertificate(VoteDelegationCertificate), - StakeDelegationCertificate(StakeDelegationCertificate), - Withdrawal(WithdrawalBlock), - PlutusWitness(PlutusWitnessBlock), - NativeWitness(NativeWitnessBlock), - TreasuryDonation(TreasuryDonationBlock), - Publish(CardanoPublishBlock), -} - impl AstNode for CardanoBlock { const RULE: Rule = Rule::cardano_block; @@ -796,95 +320,56 @@ impl AstNode for CardanoBlock { } } -impl Analyzable for CardanoBlock { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - match self { - CardanoBlock::VoteDelegationCertificate(x) => x.analyze(parent), - CardanoBlock::StakeDelegationCertificate(x) => x.analyze(parent), - CardanoBlock::Withdrawal(x) => x.analyze(parent), - CardanoBlock::PlutusWitness(x) => x.analyze(parent), - CardanoBlock::NativeWitness(x) => x.analyze(parent), - CardanoBlock::TreasuryDonation(x) => x.analyze(parent), - CardanoBlock::Publish(x) => x.analyze(parent), - } - } - - fn is_resolved(&self) -> bool { - match self { - CardanoBlock::VoteDelegationCertificate(x) => x.is_resolved(), - CardanoBlock::StakeDelegationCertificate(x) => x.is_resolved(), - CardanoBlock::Withdrawal(x) => x.is_resolved(), - CardanoBlock::PlutusWitness(x) => x.is_resolved(), - CardanoBlock::NativeWitness(x) => x.is_resolved(), - Self::TreasuryDonation(x) => x.is_resolved(), - CardanoBlock::Publish(x) => x.is_resolved(), - } - } -} - -impl IntoLower for CardanoBlock { - type Output = ir::AdHocDirective; - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result<::Output, crate::lowering::Error> { - match self { - CardanoBlock::VoteDelegationCertificate(x) => x.into_lower(ctx), - CardanoBlock::StakeDelegationCertificate(x) => x.into_lower(ctx), - CardanoBlock::Withdrawal(x) => x.into_lower(ctx), - CardanoBlock::PlutusWitness(x) => x.into_lower(ctx), - CardanoBlock::NativeWitness(x) => x.into_lower(ctx), - CardanoBlock::TreasuryDonation(x) => x.into_lower(ctx), - CardanoBlock::Publish(x) => x.into_lower(ctx), - } - } -} - -/// Sanitizes a type name to be a valid tx3 identifier. -/// Replaces characters like `<`, `>`, `,`, `/`, `$`, `~1` with underscores. -fn generic_sanitizer(name: &str) -> String { - name.replace("~1", "_") // URL-encoded `/` in JSON references - .replace('/', "_") - .replace('$', "_") - .replace('<', "_") - .replace('>', "") - .replace(',', "_") - .replace(' ', "") -} - -pub enum LoadKind { - Cip57, -} - -pub enum Compiler { - Aiken, - Unknown, -} - -pub struct ExternalLoader { - pub path: String, - pub kind: LoadKind, - pub compiler: Compiler, -} - pub fn load_externals( path: &str, ) -> Result, crate::parsing::Error> { let json = fs::read_to_string(path).map_err(|e| crate::parsing::Error { message: format!("Failed to read import file: {}", e), - src: "".to_string(), // TODO: propagate source? + src: path.to_string(), span: crate::ast::Span::DUMMY, })?; let bp = serde_json::from_str::(&json).map_err(|e| crate::parsing::Error { message: format!("Failed to parse blueprint JSON: {}", e), - src: "".to_string(), // TODO: should I add path here? + src: "".to_string(), span: crate::ast::Span::DUMMY, })?; + let is_aiken = bp + .preamble + .compiler + .as_ref() + .is_some_and(|c| c.name.to_lowercase() == "aiken"); + + let name_mapping: HashMap = if is_aiken { + let keys: Vec<&str> = bp + .definitions + .as_ref() + .map(|d| d.inner.keys().map(|s| s.as_str()).collect()) + .unwrap_or_default(); + blueprint::build_aiken_name_mapping(&keys) + } else { + bp.definitions + .as_ref() + .map(|d| { + d.inner + .keys() + .map(|k| (k.clone(), blueprint::generic_sanitizer(k))) + .collect() + }) + .unwrap_or_default() + }; + let ref_to_type = |r: &str| -> Type { - let sanitized = generic_sanitizer(r.strip_prefix("#/definitions/").unwrap_or(r)); + let key = r.strip_prefix("#/definitions/").unwrap_or(r); + let decoded_key = key.replace("~1", "/"); + let sanitized = name_mapping.get(&decoded_key).cloned().unwrap_or_else(|| { + if is_aiken { + blueprint::aiken_prettify_name(key) + } else { + blueprint::generic_sanitizer(key) + } + }); Type::Custom(Identifier::new(&sanitized)) }; @@ -896,7 +381,13 @@ pub fn load_externals( .into_iter() .flatten() { - let name = generic_sanitizer(key); + let name = name_mapping.get(key).cloned().unwrap_or_else(|| { + if is_aiken { + blueprint::aiken_prettify_name(key) + } else { + blueprint::generic_sanitizer(key) + } + }); let new = match def.data_type { Some(cip_57::DataType::Integer) => Some(Symbol::AliasDef(Box::new( @@ -941,11 +432,17 @@ pub fn load_externals( Some(cip_57::DataType::Constructor) => { let mut cases = vec![]; if let Some(any_of) = &def.any_of { + let single = any_of.len() == 1; for schema in any_of { - let case_name = schema + let original_case_name = schema .title .clone() .unwrap_or_else(|| format!("Constructor{}", schema.index)); + let case_name = if single && original_case_name == name { + "Default".to_string() + } else { + original_case_name + }; let mut fields = vec![]; for (i, field) in schema.fields.iter().enumerate() { let field_name = field @@ -971,11 +468,17 @@ pub fn load_externals( None if def.any_of.is_some() => { let mut cases = vec![]; if let Some(any_of) = &def.any_of { + let single = any_of.len() == 1; for schema in any_of { - let case_name = schema + let original_case_name = schema .title .clone() .unwrap_or_else(|| format!("Constructor{}", schema.index)); + let case_name = if single && original_case_name == name { + "Default".to_string() + } else { + original_case_name + }; let mut fields = vec![]; for (i, field) in schema.fields.iter().enumerate() { let field_name = field @@ -1010,7 +513,7 @@ pub fn load_externals( #[cfg(test)] mod tests { use super::*; - use crate::{analyzing::analyze, ast::*}; + use crate::ast::*; use pest::Parser; macro_rules! input_to_ast_check { @@ -1137,80 +640,86 @@ mod tests { ); #[test] - fn test_treasury_donation_type() { - let mut ast = crate::parsing::parse_string( - r#" - tx test(quantity: Int) { - cardano::treasury_donation { - coin: quantity, + fn test_single_constructor_alias() { + let json = r##"{ + "preamble": { + "title": "Test", + "description": "Test", + "version": "1.0.0", + "plutusVersion": "v2", + "compiler": { + "name": "Aiken", + "version": "1.0.0" + }, + "license": "Apache-2.0" + }, + "validators": [], + "definitions": { + "ticketer/types/TicketerDatum": { + "title": "TicketerDatum", + "anyOf": [ + { + "title": "TicketerDatum", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "ticket_counter", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "ticketer/types/TicketerRedeemer": { + "title": "TicketerRedeemer", + "anyOf": [ + { + "title": "BuyTicket", + "dataType": "constructor", + "index": 0, + "fields": [] + } + ] + }, + "Int": { + "dataType": "integer" } } - "#, - ) - .unwrap(); + }"##; - let result = analyze(&mut ast); - assert!(result.errors.is_empty()); - } + use std::io::Write; - #[test] - fn test_treasury_donation_type_not_ok() { - let mut ast = crate::parsing::parse_string( - r#" - tx test(quantity: Bytes) { - cardano::treasury_donation { - coin: quantity, - } - } - "#, - ) - .unwrap(); + let mut path = std::env::temp_dir(); + path.push(format!("tx3_test_{}.json", std::process::id())); - let result = analyze(&mut ast); - assert!(!result.errors.is_empty()); - } + { + let mut file = std::fs::File::create(&path).unwrap(); + file.write_all(json.as_bytes()).unwrap(); + } - #[test] - fn test_publish_type_ok() { - let mut ast = crate::parsing::parse_string( - r#" - party Receiver; - - tx test(quantity: Int) { - cardano::publish { - to: Receiver, - amount: Ada(quantity), - version: 3, - script: 0xABCDEF, - } - } - "#, - ) - .unwrap(); + let symbols = load_externals(path.to_str().unwrap()).unwrap(); - let result = analyze(&mut ast); - assert!(result.errors.is_empty()); - } + // Cleanup + let _ = std::fs::remove_file(&path); - #[test] - fn test_publish_type_with_name_ok() { - let mut ast = crate::parsing::parse_string( - r#" - party Receiver; - - tx test(quantity: Int) { - cardano::publish deploy { - to: Receiver, - amount: Ada(quantity), - version: 3, - script: 0xABCDEF, - } - } - "#, - ) - .unwrap(); + let datum = symbols.get("TicketerDatum").unwrap(); + + if let Symbol::TypeDef(def) = datum { + assert_eq!(def.cases.len(), 1); + // Matches type name -> Default + assert_eq!(def.cases[0].name.value, "Default"); + } else { + panic!("Expected TypeDef for TicketerDatum, got {:?}", datum); + } - let result = analyze(&mut ast); - assert!(result.errors.is_empty()); + let redeemer = symbols.get("TicketerRedeemer").unwrap(); + if let Symbol::TypeDef(def) = redeemer { + assert_eq!(def.cases.len(), 1); + // Does NOT match type name -> Keep original name + assert_eq!(def.cases[0].name.value, "BuyTicket"); + } else { + panic!("Expected TypeDef for TicketerRedeemer, got {:?}", redeemer); + } } } diff --git a/crates/tx3-lang/src/loading.rs b/crates/tx3-lang/src/loading.rs index ab81f7b6..e282eba3 100644 --- a/crates/tx3-lang/src/loading.rs +++ b/crates/tx3-lang/src/loading.rs @@ -79,6 +79,7 @@ fn process_imports(program: &mut ast::Program, base_path: &Path) -> Result<(), E parent: None, })); } + dbg!(&program.scope.clone().unwrap().symbols.clone().into_keys()); } Ok(()) } @@ -222,7 +223,7 @@ pub mod tests { let scope = program.scope.as_ref().unwrap(); assert!(scope.symbols.contains_key("Int")); - assert!(scope.symbols.contains_key("cardano_assets_AssetName")); - assert!(scope.symbols.contains_key("cardano_transaction_Datum")); + assert!(scope.symbols.contains_key("AssetName")); + assert!(scope.symbols.contains_key("Datum")); } } diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 819f8bbb..48d15293 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -12,7 +12,7 @@ use pest::{ }; use pest_derive::Parser; -use crate::{ast::*, cardano::load_externals}; +use crate::ast::*; #[derive(Parser)] #[grammar = "tx3.pest"] pub(crate) struct Tx3Grammar; @@ -85,7 +85,7 @@ impl AstNode for Import { let import_rule = inner.next().unwrap(); match import_rule.as_rule() { - Rule::cip57_import => { + Rule::blueprint_import => { let path = import_rule .into_inner() .as_str() @@ -94,7 +94,7 @@ impl AstNode for Import { Ok(Import { span, path, - kind: ImportKind::Cip57, + kind: ImportKind::Blueprint, }) } Rule::tx3_import => todo!(), diff --git a/crates/tx3-lang/src/tx3.pest b/crates/tx3-lang/src/tx3.pest index 3cccb5b4..4bb62d6e 100644 --- a/crates/tx3-lang/src/tx3.pest +++ b/crates/tx3-lang/src/tx3.pest @@ -462,8 +462,8 @@ tx_def = { "tx" ~ identifier ~ parameter_list ~ "{" ~ tx_body_block* ~ "}" } -cip57_import = { - "cip57::import" ~ string ~ ";" +blueprint_import = { + "blueprint::import" ~ string ~ ";" } tx3_import = { @@ -471,7 +471,7 @@ tx3_import = { } import = { - cip57_import | + blueprint_import | tx3_import } diff --git a/examples/cip_imports.tx3 b/examples/cip_imports.tx3 index b8d96d75..eedc3608 100644 --- a/examples/cip_imports.tx3 +++ b/examples/cip_imports.tx3 @@ -1,4 +1,4 @@ -cip57::import "imports/plutus.json"; +blueprint::import "imports/plutus.json"; party Sender; @@ -20,12 +20,12 @@ tx transfer_with_imports( output { to: Sender, amount: source - Ada(quantity) - fees, - datum: types_SettingsDatum::SettingsDatum { - githoney_address: cardano_address_Address::Address{ - payment_credential: cardano_address_PaymentCredential::VerificationKey { + datum: SettingsDatum::SettingsDatum { + githoney_address: Address::Address{ + payment_credential: PaymentCredential::VerificationKey { field_0: 0x123123, }, - stake_credential: Option_cardano_address_StakeCredential::None {}, + stake_credential: OptionStakeCredential::None {}, }, bounty_creation_fee: 0, bounty_reward_fee: 0, From 227632d60b76c17e152282134412e6f95c2b15ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Wed, 17 Dec 2025 16:47:09 -0300 Subject: [PATCH 12/13] feat: Sanitize generic type names in external blueprint definitions and add corresponding tests. --- crates/tx3-lang/src/cardano/blueprint.rs | 11 +++++++ crates/tx3-lang/src/cardano/parsing.rs | 39 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/crates/tx3-lang/src/cardano/blueprint.rs b/crates/tx3-lang/src/cardano/blueprint.rs index d9746221..c97d9f62 100644 --- a/crates/tx3-lang/src/cardano/blueprint.rs +++ b/crates/tx3-lang/src/cardano/blueprint.rs @@ -244,4 +244,15 @@ mod tests { ); assert_eq!(mapping.get("types/Datum").unwrap(), "TypesDatum"); } + #[test] + fn test_generic_sanitizer() { + assert_eq!(generic_sanitizer("simple"), "simple"); + assert_eq!(generic_sanitizer("path/to/something"), "path_to_something"); + assert_eq!(generic_sanitizer("Option"), "Option_Int"); + assert_eq!(generic_sanitizer("Map"), "Map_K_V"); + assert_eq!( + generic_sanitizer("some ~1 weird ~0 thing"), + "some_weird~0thing" + ); + } } diff --git a/crates/tx3-lang/src/cardano/parsing.rs b/crates/tx3-lang/src/cardano/parsing.rs index 34219fe2..95fb28aa 100644 --- a/crates/tx3-lang/src/cardano/parsing.rs +++ b/crates/tx3-lang/src/cardano/parsing.rs @@ -722,4 +722,43 @@ mod tests { panic!("Expected TypeDef for TicketerRedeemer, got {:?}", redeemer); } } + #[test] + fn test_load_externals_generic_compiler() { + let json = r##"{ + "preamble": { + "title": "Test", + "description": "Test", + "version": "1.0.0", + "plutusVersion": "v1", + "compiler": { + "name": "Test", + "version": "0.0.0" + }, + "license": "MIT" + }, + "validators": [], + "definitions": { + "Option": { + "dataType": "integer" + } + } + }"##; + + use std::io::Write; + + let mut path = std::env::temp_dir(); + path.push(format!("tx3_test_generic_{}.json", std::process::id())); + + { + let mut file = std::fs::File::create(&path).unwrap(); + file.write_all(json.as_bytes()).unwrap(); + } + + let symbols = load_externals(path.to_str().unwrap()).unwrap(); + + let _ = std::fs::remove_file(&path); + + assert!(symbols.contains_key("Option_Int")); + assert!(!symbols.contains_key("OptionInt")); + } } From f96c768b1c0645a17bb050bae1e659ca8715bc5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Wed, 17 Dec 2025 17:08:44 -0300 Subject: [PATCH 13/13] feat: remove unnecessary log --- crates/tx3-lang/src/cardano/blueprint.rs | 4 ++++ crates/tx3-lang/src/loading.rs | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/tx3-lang/src/cardano/blueprint.rs b/crates/tx3-lang/src/cardano/blueprint.rs index c97d9f62..3aed3596 100644 --- a/crates/tx3-lang/src/cardano/blueprint.rs +++ b/crates/tx3-lang/src/cardano/blueprint.rs @@ -254,5 +254,9 @@ mod tests { generic_sanitizer("some ~1 weird ~0 thing"), "some_weird~0thing" ); + assert_eq!( + generic_sanitizer("Result, String>"), + "Result_Option_Int_String" + ); } } diff --git a/crates/tx3-lang/src/loading.rs b/crates/tx3-lang/src/loading.rs index e282eba3..77b6fd0e 100644 --- a/crates/tx3-lang/src/loading.rs +++ b/crates/tx3-lang/src/loading.rs @@ -79,7 +79,6 @@ fn process_imports(program: &mut ast::Program, base_path: &Path) -> Result<(), E parent: None, })); } - dbg!(&program.scope.clone().unwrap().symbols.clone().into_keys()); } Ok(()) }