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 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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)]