diff --git a/Cargo.lock b/Cargo.lock index be011f45..c71a791d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -378,6 +378,14 @@ dependencies = [ "half", ] +[[package]] +name = "cip-57" +version = "0.14.3" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "clap" version = "4.5.53" @@ -2350,6 +2358,7 @@ version = "0.15.1" dependencies = [ "assert-json-diff", "ciborium", + "cip-57", "hex", "miette", "paste", diff --git a/Cargo.toml b/Cargo.toml index fe487f58..9725280b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,13 @@ [workspace] resolver = "2" -members = ["bin/tx3c", - "crates/tx3-cardano", - "crates/tx3-lang", - "crates/tx3-resolver", - "crates/tx3-tir", + +members = [ + "bin/tx3c", + "crates/tx3-cardano", + "crates/tx3-lang", + "crates/tx3-resolver", + "crates/tx3-tir", + "crates/cip-57", ] [workspace.package] diff --git a/crates/cip-57/Cargo.toml b/crates/cip-57/Cargo.toml new file mode 100644 index 00000000..a339ae1f --- /dev/null +++ b/crates/cip-57/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "cip-57" +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.0.137" 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/lib.rs b/crates/cip-57/src/lib.rs new file mode 100644 index 00000000..d46fc5f8 --- /dev/null +++ b/crates/cip-57/src/lib.rs @@ -0,0 +1,186 @@ +//! 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 which can be either a single purpose or an object with oneOf. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(untagged)] +pub enum PurposeArray { + Single(Purpose), + 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. +#[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, +} + +#[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 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"); + + assert!( + !bp.preamble.title.is_empty(), + "preamble.title should not be empty" + ); + assert!(!bp.validators.is_empty(), "expected at least one validator"); + } +} diff --git a/crates/tx3-lang/Cargo.toml b/crates/tx3-lang/Cargo.toml index b7389151..22821d3c 100644 --- a/crates/tx3-lang/Cargo.toml +++ b/crates/tx3-lang/Cargo.toml @@ -19,6 +19,9 @@ hex = { workspace = true } serde = { workspace = true } tx3-tir = { version = "0.15.1", path = "../tx3-tir" } +cip-57 = { path = "../cip-57" } +serde_json = "1.0.137" + miette = { version = "7.4.0", features = ["fancy"] } pest = { version = "2.7.15", features = ["miette-error", "pretty-print"] } diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index 54578928..7d172b00 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -173,6 +173,8 @@ impl AsRef for Identifier { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] pub struct Program { + #[serde(default)] + pub imports: Vec, pub env: Option, pub txs: Vec, pub types: Vec, @@ -187,6 +189,13 @@ pub struct Program { pub(crate) scope: Option>, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ImportDef { + pub path: StringLiteral, + pub alias: Option, + pub span: Span, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct EnvField { pub name: String, @@ -934,6 +943,28 @@ impl TypeDef { pub(crate) fn find_case(&self, case: &str) -> Option<&VariantCase> { self.cases.iter().find(|x| x.name.value == case) } + + pub fn to_tx3_source(&self) -> String { + let name = &self.name.value; + // Implicit cases don't have an explicit constructor on its usage + if self.cases.len() == 1 && self.cases[0].name.value == "Default" { + let fields = &self.cases[0].fields; + let fields_str = fields + .iter() + .map(|f| format!("{}: {}", f.name.value, f.r#type)) + .collect::>() + .join(", "); + format!("type {} {{ {} }}", name, fields_str) + } else { + let cases_str = self + .cases + .iter() + .map(VariantCase::to_tx3_source) + .collect::>() + .join(", "); + format!("type {} {{ {} }}", name, cases_str) + } + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -953,6 +984,21 @@ impl VariantCase { pub(crate) fn find_field(&self, field: &str) -> Option<&RecordField> { self.fields.iter().find(|x| x.name.value == field) } + + fn to_tx3_source(&self) -> String { + let name = &self.name.value; + if self.fields.is_empty() { + name.clone() + } else { + let fields_str = self + .fields + .iter() + .map(|f| format!("{}: {}", f.name.value, f.r#type)) + .collect::>() + .join(", "); + format!("{} {{ {} }}", name, fields_str) + } + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] diff --git a/crates/tx3-lang/src/cardano.rs b/crates/tx3-lang/src/cardano.rs index 036ab79f..985db0d9 100644 --- a/crates/tx3-lang/src/cardano.rs +++ b/crates/tx3-lang/src/cardano.rs @@ -845,10 +845,7 @@ impl IntoLower for CardanoBlock { #[cfg(test)] mod tests { use super::*; - use crate::{ - analyzing::analyze, - ast::{self, *}, - }; + use crate::{analyzing::analyze, ast::*}; use pest::Parser; macro_rules! input_to_ast_check { diff --git a/crates/tx3-lang/src/facade.rs b/crates/tx3-lang/src/facade.rs index 39f507d3..13d34eb7 100644 --- a/crates/tx3-lang/src/facade.rs +++ b/crates/tx3-lang/src/facade.rs @@ -1,8 +1,9 @@ use std::collections::{BTreeMap, HashMap}; +use std::path::PathBuf; use tx3_tir::reduce::{Apply, ArgValue}; -use crate::{analyzing, ast, lowering, parsing}; +use crate::{analyzing, ast, importing, lowering, parsing}; #[derive(Debug, thiserror::Error, miette::Diagnostic)] pub enum Error { @@ -16,6 +17,10 @@ pub enum Error { #[diagnostic(transparent)] Parsing(#[from] parsing::Error), + #[error("Import error: {0}")] + #[diagnostic(transparent)] + Importing(#[from] importing::Error), + #[error("Analyzing error")] Analyzing(#[from] analyzing::AnalyzeReport), @@ -27,6 +32,7 @@ pub type Code = String; pub struct Workspace { main: Option, + root: Option, ast: Option, analisis: Option, tir: HashMap, @@ -34,10 +40,13 @@ pub struct Workspace { impl Workspace { pub fn from_file(main: impl AsRef) -> Result { - let main = std::fs::read_to_string(main.as_ref())?; + let path = main.as_ref(); + let main = std::fs::read_to_string(path)?; + let root = path.parent().map(PathBuf::from); Ok(Self { main: Some(main), + root, ast: None, analisis: None, tir: HashMap::new(), @@ -47,6 +56,7 @@ impl Workspace { pub fn from_string(main: Code) -> Self { Self { main: Some(main), + root: None, ast: None, analisis: None, tir: HashMap::new(), @@ -63,7 +73,9 @@ impl Workspace { pub fn parse(&mut self) -> Result<(), Error> { let main = self.ensure_main()?; - let ast = parsing::parse_string(main)?; + let mut ast = parsing::parse_string(main)?; + let loader = self.root.clone().map(importing::FsLoader::new); + importing::resolve_imports(&mut ast, loader.as_ref())?; self.ast = Some(ast); Ok(()) } diff --git a/crates/tx3-lang/src/importing.rs b/crates/tx3-lang/src/importing.rs new file mode 100644 index 00000000..7fc94ced --- /dev/null +++ b/crates/tx3-lang/src/importing.rs @@ -0,0 +1,547 @@ +//! Resolves plutus.json (CIP-57) imports and maps blueprint definitions to tx3 types. + +use std::collections::HashSet; +use std::path::PathBuf; + +use cip_57::{Blueprint, DataType, Definition, Definitions, Field, ReferencesArray, Schema}; + +use crate::ast::{Identifier, Program, RecordField, Span, Type, TypeDef, VariantCase}; + +#[derive(Debug, thiserror::Error, miette::Diagnostic)] +pub enum Error { + #[error("cannot resolve imports without a root path (use Workspace::from_file instead of from_string)")] + #[diagnostic(code(tx3::importing::missing_root))] + MissingRoot, + + #[error("I/O error reading import file: {0}")] + #[diagnostic(code(tx3::importing::io))] + Io(#[from] std::io::Error), + + #[error("invalid JSON in plutus file: {0}")] + #[diagnostic(code(tx3::importing::json))] + Json(#[from] serde_json::Error), + + #[error("duplicate type name: {name}")] + #[diagnostic(code(tx3::importing::duplicate_type))] + DuplicateType { + name: String, + + #[source_code] + src: Option, + + #[label("type already defined here or from another import")] + span: Span, + }, + + #[error("plutus schema error: {message}")] + #[diagnostic(code(tx3::importing::schema))] + Schema { message: String }, +} + +impl Error { + fn duplicate_type(name: String, span: Span) -> Self { + Error::DuplicateType { + name, + src: None, + span, + } + } +} + +pub trait ImportLoader { + fn load_source(&self, path: &str) -> std::io::Result; +} + +pub struct FsLoader { + root: PathBuf, +} + +impl FsLoader { + pub fn new(root: impl Into) -> Self { + Self { root: root.into() } + } +} + +impl ImportLoader for FsLoader { + fn load_source(&self, path: &str) -> std::io::Result { + let full_path = self.root.join(path); + std::fs::read_to_string(full_path) + } +} + +/// Resolves `#/definitions/cardano~1address~1Address` to definition key `cardano/address/Address`. +/// JSON pointer uses ~1 for / and ~0 for ~. +fn ref_to_key_owned(r: &str) -> String { + let prefix = "#/definitions/"; + let s = r + .strip_prefix(prefix) + .unwrap_or(r) + .replace("~1", "/") + .replace("~0", "~"); + s +} + +/// Normalize definition key to a single identifier: replace `/` and `$` with `_`. +fn key_to_normalized_name(key: &str) -> String { + key.replace('/', "_").replace('$', "_") +} + +fn import_type_name(key: &str, alias: Option<&str>) -> String { + let base = key_to_normalized_name(key); + match alias { + Some(a) => format!("{}_{}", a, base), + None => base, + } +} + +fn resolve_ref_to_type( + ref_str: &str, + definitions: &Definitions, + alias: Option<&str>, +) -> Result { + let key = ref_to_key_owned(ref_str); + let def = definitions.inner.get(&key).ok_or_else(|| Error::Schema { + message: format!("definition not found: {}", key), + })?; + + if let Some(dt) = &def.data_type { + match dt { + DataType::Integer => return Ok(Type::Int), + DataType::Bytes => return Ok(Type::Bytes), + DataType::List => { + let inner = match &def.items { + Some(ReferencesArray::Single(r)) => r + .reference + .as_ref() + .map(|s| resolve_ref_to_type(s, definitions, alias)), + Some(ReferencesArray::Array(arr)) => arr.first().and_then(|r| { + r.reference + .as_ref() + .map(|s| resolve_ref_to_type(s, definitions, alias)) + }), + None => None, + }; + let inner = inner.ok_or_else(|| Error::Schema { + message: "list without items".to_string(), + })?; + return Ok(Type::List(Box::new(inner?))); + } + DataType::Map => { + let k = def + .keys + .as_ref() + .and_then(|r| r.reference.as_ref()) + .ok_or_else(|| Error::Schema { + message: "map without keys".to_string(), + })?; + let v = def + .values + .as_ref() + .and_then(|r| r.reference.as_ref()) + .ok_or_else(|| Error::Schema { + message: "map without values".to_string(), + })?; + let key_ty = resolve_ref_to_type(k, definitions, alias)?; + let val_ty = resolve_ref_to_type(v, definitions, alias)?; + return Ok(Type::Map(Box::new(key_ty), Box::new(val_ty))); + } + DataType::Constructor => {} + } + } + + Ok(Type::Custom(Identifier::new(import_type_name(&key, alias)))) +} + +fn field_to_record_field( + f: &Field, + definitions: &Definitions, + alias: Option<&str>, +) -> Result { + let name = f.title.as_deref().unwrap_or("field").to_string(); + let r#type = resolve_ref_to_type(&f.reference, definitions, alias)?; + Ok(RecordField { + name: Identifier::new(name), + r#type, + span: Span::DUMMY, + }) +} + +fn schema_to_variant_case( + schema: &Schema, + definitions: &Definitions, + alias: Option<&str>, +) -> Result { + let name = schema.title.as_deref().unwrap_or("Variant").to_string(); + let fields: Vec = schema + .fields + .iter() + .map(|f| field_to_record_field(f, definitions, alias)) + .collect::, _>>()?; + Ok(VariantCase { + name: Identifier::new(name), + fields, + span: Span::DUMMY, + }) +} + +fn definition_to_type_def( + key: &str, + def: &Definition, + definitions: &Definitions, + alias: Option<&str>, +) -> Result, Error> { + if let Some(dt) = &def.data_type { + match dt { + DataType::Integer | DataType::Bytes => return Ok(None), + DataType::List | DataType::Map => return Ok(None), + DataType::Constructor => {} + } + } + + let cases = if let Some(any_of) = &def.any_of { + any_of + .iter() + .map(|s| schema_to_variant_case(s, definitions, alias)) + .collect::, _>>()? + } else { + // Data type has no structure but should still be imported. + // TODO: There is no Any for our type system, so for now we'll just import it as a single empty variant case. + if key == "Data" { + return Ok(Some(TypeDef { + name: Identifier::new(import_type_name(key, alias)), + cases: vec![VariantCase { + name: Identifier::new("Default"), + fields: vec![], + span: Span::DUMMY, + }], + span: Span::DUMMY, + })); + } + return Ok(None); + }; + + if cases.is_empty() { + return Ok(None); + } + + let mut cases = cases; + if cases.len() == 1 { + cases[0].name = Identifier::new("Default"); + } + + let type_name = import_type_name(key, alias); + Ok(Some(TypeDef { + name: Identifier::new(type_name), + cases, + span: Span::DUMMY, + })) +} + +pub fn type_defs_from_blueprint( + blueprint: &Blueprint, + alias: Option<&str>, +) -> Result, Error> { + let definitions = match &blueprint.definitions { + Some(d) => d, + None => return Ok(vec![]), + }; + + let mut type_defs = Vec::new(); + for (key, def) in &definitions.inner { + if let Some(type_def) = definition_to_type_def(key, def, definitions, alias)? { + type_defs.push(type_def); + } + } + Ok(type_defs) +} + +pub fn types_from_plutus( + path: &str, + alias: Option<&str>, + loader: &impl ImportLoader, +) -> Result, Error> { + let json = loader.load_source(path)?; + let blueprint: Blueprint = serde_json::from_str(&json)?; + type_defs_from_blueprint(&blueprint, alias) +} + +pub fn resolve_imports( + program: &mut Program, + loader: Option<&impl ImportLoader>, +) -> Result<(), Error> { + if program.imports.is_empty() { + return Ok(()); + } + let loader = loader.ok_or(Error::MissingRoot)?; + + let existing_names: HashSet = program + .types + .iter() + .map(|t| t.name.value.clone()) + .chain(program.aliases.iter().map(|a| a.name.value.clone())) + .collect(); + let mut added_names = HashSet::::new(); + + for import in &program.imports { + let json = loader.load_source(import.path.value.as_str())?; + let blueprint: Blueprint = serde_json::from_str(&json)?; + let alias = import.alias.as_ref().map(|a| a.value.as_str()); + let type_defs = type_defs_from_blueprint(&blueprint, alias)?; + + for type_def in type_defs { + let name = type_def.name.value.clone(); + if existing_names.contains(&name) || added_names.contains(&name) { + return Err(Error::duplicate_type(name, import.span.clone())); + } + added_names.insert(name.clone()); + program.types.push(type_def); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{Identifier, ImportDef, Program, Span, StringLiteral}; + + #[derive(Default, Clone)] + pub struct InMemoryLoader { + map: std::collections::HashMap, + } + + impl InMemoryLoader { + pub fn new() -> Self { + Self { + map: std::collections::HashMap::new(), + } + } + + pub fn add(&mut self, path: impl Into, contents: impl Into) -> &mut Self { + self.map.insert(path.into(), contents.into()); + self + } + } + + impl ImportLoader for InMemoryLoader { + fn load_source(&self, path: &str) -> std::io::Result { + self.map.get(path).cloned().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("import not found: {}", path), + ) + }) + } + } + + #[test] + fn resolve_imports_with_in_memory_loader() { + let mut program = Program { + imports: vec![ImportDef { + path: StringLiteral::new("test.json"), + alias: Some(Identifier::new("types")), + span: Span::DUMMY, + }], + env: None, + txs: vec![], + types: vec![], + aliases: vec![], + assets: vec![], + parties: vec![], + policies: vec![], + span: Span::DUMMY, + scope: None, + }; + + let json = r#"{ + "preamble": { "title": "test", "version": "0", "plutusVersion": "v3" }, + "validators": [], + "definitions": { + "Bool": { + "title": "Bool", + "anyOf": [ + { "title": "False", "dataType": "constructor", "index": 0, "fields": [] }, + { "title": "True", "dataType": "constructor", "index": 1, "fields": [] } + ] + } + } + }"#; + + let mut loader = InMemoryLoader::new(); + loader.add("test.json", json); + resolve_imports(&mut program, Some(&loader)).unwrap(); + + let type_names: Vec = program.types.iter().map(|t| t.name.value.clone()).collect(); + assert!( + type_names.contains(&"types_Bool".to_string()), + "expected types_Bool in program.types, got: {:?}", + type_names + ); + } + + #[test] + fn import_with_alias_adds_types() { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let root = std::path::Path::new(manifest_dir); + + let mut program = + crate::parsing::parse_string(r#"import "../cip-57/examples/plutus.json" as types;"#) + .unwrap(); + + let loader = FsLoader::new(root); + resolve_imports(&mut program, Some(&loader)).unwrap(); + + let type_names: Vec = program.types.iter().map(|t| t.name.value.clone()).collect(); + assert!( + type_names.iter().any(|n| n.starts_with("types_")), + "expected at least one type prefixed with 'types_', got: {:?}", + type_names + ); + } + + #[test] + fn import_without_root_errors() { + let src = r#" + import "some/file.json"; + party X; + tx dummy() {} + "#; + let mut program = crate::parsing::parse_string(src).unwrap(); + let res = resolve_imports(&mut program, None::<&InMemoryLoader>); + + assert!(res.is_err(), "expected error when importing without root"); + let err = res.unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("root") || msg.contains("import"), + "expected error about root or import, got: {}", + msg + ); + } + + #[test] + fn duplicate_type_name_from_imports_errors() { + let json = r#"{ + "preamble": { "title": "test", "version": "0", "plutusVersion": "v3" }, + "validators": [], + "definitions": { + "One": { + "title": "One", + "anyOf": [ { "title": "A", "dataType": "constructor", "index": 0, "fields": [] } ] + } + } + }"#; + + let src = r#" + import "schema1.json" as types; + import "schema2.json" as types; + "#; + + let mut program = crate::parsing::parse_string(src).unwrap(); + let mut loader = InMemoryLoader::new(); + loader.add("schema1.json", json); + loader.add("schema2.json", json); + let res = resolve_imports(&mut program, Some(&loader)); + + assert!( + res.is_err(), + "expected error for duplicate type names from two imports" + ); + let err = res.unwrap_err(); + assert!( + err.to_string().contains("duplicate") || err.to_string().contains("type"), + "expected duplicate/type error, got: {}", + err + ); + } + + #[test] + fn invalid_import_path_errors() { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let root = std::path::Path::new(manifest_dir); + let src = r#" + import "nonexistent/plutus.json" as types; + "#; + let mut program = crate::parsing::parse_string(src).unwrap(); + let loader = FsLoader::new(root); + + let res = resolve_imports(&mut program, Some(&loader)); + assert!(res.is_err(), "expected error for missing import file"); + } + + #[test] + fn single_variant_type_import_with_analyze() { + let dollar = "$"; + let hash = "#"; + let json = format!( + r#"{{ + "preamble": {{ "title": "test", "version": "0", "plutusVersion": "v3" }}, + "validators": [], + "definitions": {{ + "OutputReference": {{ + "title": "OutputReference", + "anyOf": [ + {{ + "title": "OutputReference", + "dataType": "constructor", + "index": 0, + "fields": [ + {{ + "title": "transaction_id", + "{}ref": "{}/definitions/ByteArray" + }}, + {{ + "title": "output_index", + "{}ref": "{}/definitions/Int" + }} + ] + }} + ] + }}, + "ByteArray": {{ + "title": "ByteArray", + "dataType": "bytes" + }}, + "Int": {{ + "title": "Int", + "dataType": "integer" + }} + }} + }}"#, + dollar, hash, dollar, hash + ); + + let src = r#" + import "test.json" as types; + party Alice; + tx test(outref: types_OutputReference) { + output my_output { + to: Alice, + amount: 1000000, + } + } + "#; + + let mut program = crate::parsing::parse_string(src).unwrap(); + let mut loader = InMemoryLoader::new(); + loader.add("test.json", json); + + resolve_imports(&mut program, Some(&loader)).unwrap(); + + let output_ref_type = program + .types + .iter() + .find(|t| t.name.value == "types_OutputReference") + .expect("types_OutputReference should be imported"); + assert_eq!(output_ref_type.cases.len(), 1); + assert_eq!(output_ref_type.cases[0].name.value, "Default"); + + let report = crate::analyzing::analyze(&mut program); + assert!( + report.errors.is_empty(), + "expected no analysis errors, got: {:?}", + report.errors + ); + } +} diff --git a/crates/tx3-lang/src/lib.rs b/crates/tx3-lang/src/lib.rs index 9d044437..ec6686c3 100644 --- a/crates/tx3-lang/src/lib.rs +++ b/crates/tx3-lang/src/lib.rs @@ -26,6 +26,7 @@ pub mod analyzing; pub mod ast; +pub mod importing; pub mod lowering; pub mod parsing; diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 2b34fc1c..116ede21 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -12,10 +12,7 @@ use pest::{ }; use pest_derive::Parser; -use crate::{ - ast::*, - cardano::{PlutusWitnessBlock, PlutusWitnessField}, -}; +use crate::ast::*; #[derive(Parser)] #[grammar = "tx3.pest"] pub(crate) struct Tx3Grammar; @@ -87,6 +84,7 @@ impl AstNode for Program { let inner = pair.into_inner(); let mut program = Self { + imports: Vec::new(), env: None, txs: Vec::new(), assets: Vec::new(), @@ -100,6 +98,7 @@ impl AstNode for Program { for pair in inner { match pair.as_rule() { + Rule::import_def => program.imports.push(ImportDef::parse(pair)?), Rule::env_def => program.env = Some(EnvDef::parse(pair)?), Rule::tx_def => program.txs.push(TxDef::parse(pair)?), Rule::asset_def => program.assets.push(AssetDef::parse(pair)?), @@ -121,6 +120,22 @@ impl AstNode for Program { } } +impl AstNode for ImportDef { + const RULE: Rule = Rule::import_def; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + let mut inner = pair.into_inner(); + let path = StringLiteral::parse(inner.next().unwrap())?; + let alias = inner.next().map(Identifier::parse).transpose()?; + Ok(ImportDef { path, alias, span }) + } + + fn span(&self) -> &Span { + &self.span + } +} + impl AstNode for EnvField { const RULE: Rule = Rule::env_field; @@ -2587,6 +2602,7 @@ mod tests { "basic", "party Abc; tx my_tx() {}", Program { + imports: vec![], parties: vec![PartyDef { name: Identifier::new("Abc"), span: Span::DUMMY, @@ -2817,4 +2833,6 @@ mod tests { test_parsing!(list_concat); test_parsing!(buidler_fest_2026); + + test_parsing!(imported_datum); } diff --git a/crates/tx3-lang/src/tx3.pest b/crates/tx3-lang/src/tx3.pest index a01471ff..a1fe6987 100644 --- a/crates/tx3-lang/src/tx3.pest +++ b/crates/tx3-lang/src/tx3.pest @@ -439,6 +439,10 @@ tx_body_block = _{ validity_block } +import_def = { + "import" ~ string ~ ("as" ~ identifier)? ~ ";" +} + env_field = { identifier ~ ":" ~ type } env_def = { @@ -453,6 +457,6 @@ tx_def = { // Program program = { SOI ~ - (env_def | asset_def | party_def | policy_def | type_def | tx_def)* ~ + (import_def | env_def | asset_def | party_def | policy_def | type_def | tx_def)* ~ EOI } diff --git a/examples/asteria.ast b/examples/asteria.ast index 4b556cb1..6e284ff7 100644 --- a/examples/asteria.ast +++ b/examples/asteria.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/buidler_fest_2026.ast b/examples/buidler_fest_2026.ast index c0c0f570..96b13316 100644 --- a/examples/buidler_fest_2026.ast +++ b/examples/buidler_fest_2026.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": { "fields": [ { diff --git a/examples/burn.ast b/examples/burn.ast index 6eafd087..1ed27f63 100644 --- a/examples/burn.ast +++ b/examples/burn.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/cardano_witness.ast b/examples/cardano_witness.ast index 9136d18d..4fe2ba60 100644 --- a/examples/cardano_witness.ast +++ b/examples/cardano_witness.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/cardano_witness.mint_from_plutus.tir b/examples/cardano_witness.mint_from_plutus.tir index c0f4530f..8ff6e2e2 100644 --- a/examples/cardano_witness.mint_from_plutus.tir +++ b/examples/cardano_witness.mint_from_plutus.tir @@ -202,9 +202,6 @@ { "name": "plutus_witness", "data": { - "version": { - "Number": 3 - }, "script": { "Bytes": [ 81, @@ -226,6 +223,9 @@ 174, 105 ] + }, + "version": { + "Number": 3 } } } diff --git a/examples/disordered.ast b/examples/disordered.ast index de9c48f3..0983c7f2 100644 --- a/examples/disordered.ast +++ b/examples/disordered.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/donation.ast b/examples/donation.ast index f82e5c24..0660f4f4 100644 --- a/examples/donation.ast +++ b/examples/donation.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/env_vars.ast b/examples/env_vars.ast index cef2b474..373b6ebc 100644 --- a/examples/env_vars.ast +++ b/examples/env_vars.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": { "fields": [ { diff --git a/examples/faucet.ast b/examples/faucet.ast index e961c1d4..f82b8247 100644 --- a/examples/faucet.ast +++ b/examples/faucet.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/imported_datum.ast b/examples/imported_datum.ast new file mode 100644 index 00000000..5b2abd96 --- /dev/null +++ b/examples/imported_datum.ast @@ -0,0 +1,414 @@ +{ + "imports": [ + { + "path": { + "value": "../crates/cip-57/examples/plutus.json", + "span": { + "dummy": false, + "start": 7, + "end": 46 + } + }, + "alias": { + "value": "plutus", + "span": { + "dummy": false, + "start": 50, + "end": 56 + } + }, + "span": { + "dummy": false, + "start": 0, + "end": 57 + } + } + ], + "env": null, + "txs": [ + { + "name": { + "value": "create_settings", + "span": { + "dummy": false, + "start": 76, + "end": 91 + } + }, + "parameters": { + "parameters": [ + { + "name": { + "value": "now", + "span": { + "dummy": false, + "start": 97, + "end": 100 + } + }, + "type": "Int" + } + ], + "span": { + "dummy": false, + "start": 91, + "end": 108 + } + }, + "locals": null, + "references": [], + "inputs": [ + { + "name": "fee_source", + "many": false, + "fields": [ + { + "From": { + "Identifier": { + "value": "Admin", + "span": { + "dummy": false, + "start": 148, + "end": 153 + } + } + } + }, + { + "MinAmount": { + "Number": 1000000 + } + } + ], + "span": { + "dummy": false, + "start": 115, + "end": 189 + } + } + ], + "outputs": [ + { + "name": { + "value": "settings", + "span": { + "dummy": false, + "start": 202, + "end": 210 + } + }, + "optional": false, + "fields": [ + { + "To": { + "Identifier": { + "value": "Admin", + "span": { + "dummy": false, + "start": 225, + "end": 230 + } + } + } + }, + { + "Datum": { + "StructConstructor": { + "type": { + "value": "plutus_types_SettingsDatum", + "span": { + "dummy": false, + "start": 247, + "end": 273 + } + }, + "case": { + "name": { + "value": "Default", + "span": { + "dummy": true, + "start": 0, + "end": 0 + } + }, + "fields": [ + { + "name": { + "value": "githoney_address", + "span": { + "dummy": false, + "start": 288, + "end": 304 + } + }, + "value": { + "StructConstructor": { + "type": { + "value": "plutus_cardano_address_Address", + "span": { + "dummy": false, + "start": 306, + "end": 336 + } + }, + "case": { + "name": { + "value": "Default", + "span": { + "dummy": true, + "start": 0, + "end": 0 + } + }, + "fields": [ + { + "name": { + "value": "payment_credential", + "span": { + "dummy": false, + "start": 355, + "end": 373 + } + }, + "value": { + "StructConstructor": { + "type": { + "value": "plutus_cardano_address_PaymentCredential", + "span": { + "dummy": false, + "start": 375, + "end": 415 + } + }, + "case": { + "name": { + "value": "VerificationKey", + "span": { + "dummy": false, + "start": 417, + "end": 432 + } + }, + "fields": [ + { + "name": { + "value": "field", + "span": { + "dummy": false, + "start": 455, + "end": 460 + } + }, + "value": { + "HexString": { + "value": "12345678901234567890123456789012345678901234567890123456", + "span": { + "dummy": false, + "start": 462, + "end": 520 + } + } + }, + "span": { + "dummy": false, + "start": 455, + "end": 520 + } + } + ], + "spread": null, + "span": { + "dummy": false, + "start": 415, + "end": 539 + } + }, + "span": { + "dummy": false, + "start": 375, + "end": 539 + } + } + }, + "span": { + "dummy": false, + "start": 355, + "end": 539 + } + }, + { + "name": { + "value": "stake_credential", + "span": { + "dummy": false, + "start": 557, + "end": 573 + } + }, + "value": { + "StructConstructor": { + "type": { + "value": "plutus_Option_cardano_address_StakeCredential", + "span": { + "dummy": false, + "start": 575, + "end": 620 + } + }, + "case": { + "name": { + "value": "None", + "span": { + "dummy": false, + "start": 622, + "end": 626 + } + }, + "fields": [], + "spread": null, + "span": { + "dummy": false, + "start": 620, + "end": 629 + } + }, + "span": { + "dummy": false, + "start": 575, + "end": 629 + } + } + }, + "span": { + "dummy": false, + "start": 557, + "end": 629 + } + } + ], + "spread": null, + "span": { + "dummy": false, + "start": 337, + "end": 644 + } + }, + "span": { + "dummy": false, + "start": 306, + "end": 644 + } + } + }, + "span": { + "dummy": false, + "start": 288, + "end": 644 + } + }, + { + "name": { + "value": "bounty_creation_fee", + "span": { + "dummy": false, + "start": 658, + "end": 677 + } + }, + "value": { + "Number": 500 + }, + "span": { + "dummy": false, + "start": 658, + "end": 682 + } + }, + { + "name": { + "value": "bounty_reward_fee", + "span": { + "dummy": false, + "start": 696, + "end": 713 + } + }, + "value": { + "Number": 100 + }, + "span": { + "dummy": false, + "start": 696, + "end": 718 + } + } + ], + "spread": null, + "span": { + "dummy": false, + "start": 274, + "end": 729 + } + }, + "span": { + "dummy": false, + "start": 247, + "end": 729 + } + } + } + }, + { + "Amount": { + "Number": 2000000 + } + } + ], + "span": { + "dummy": false, + "start": 195, + "end": 761 + } + } + ], + "validity": null, + "mints": [], + "burns": [], + "signers": null, + "adhoc": [], + "span": { + "dummy": false, + "start": 73, + "end": 763 + }, + "collateral": [], + "metadata": null + } + ], + "types": [], + "aliases": [], + "assets": [], + "parties": [ + { + "name": { + "value": "Admin", + "span": { + "dummy": false, + "start": 65, + "end": 70 + } + }, + "span": { + "dummy": false, + "start": 59, + "end": 71 + } + } + ], + "policies": [], + "span": { + "dummy": false, + "start": 0, + "end": 764 + } +} \ No newline at end of file diff --git a/examples/imported_datum.tx3 b/examples/imported_datum.tx3 new file mode 100644 index 00000000..339f3aea --- /dev/null +++ b/examples/imported_datum.tx3 @@ -0,0 +1,27 @@ +import "../crates/cip-57/examples/plutus.json" as plutus; + +party Admin; + +tx create_settings( + now: Int, +) { + input fee_source { + from: Admin, + min_amount: 1000000, + } + + output settings { + to: Admin, + datum: plutus_types_SettingsDatum { + githoney_address: plutus_cardano_address_Address { + payment_credential: plutus_cardano_address_PaymentCredential::VerificationKey { + field: 0x12345678901234567890123456789012345678901234567890123456, + }, + stake_credential: plutus_Option_cardano_address_StakeCredential::None {}, + }, + bounty_creation_fee: 500, + bounty_reward_fee: 100, + }, + amount: 2000000, + } +} diff --git a/examples/input_datum.ast b/examples/input_datum.ast index c0cb2672..55b334ae 100644 --- a/examples/input_datum.ast +++ b/examples/input_datum.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/lang_tour.ast b/examples/lang_tour.ast index cfe0e4cd..2a3b452b 100644 --- a/examples/lang_tour.ast +++ b/examples/lang_tour.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": { "fields": [ { diff --git a/examples/lang_tour.my_tx.tir b/examples/lang_tour.my_tx.tir index 525dcb75..f60eca2f 100644 --- a/examples/lang_tour.my_tx.tir +++ b/examples/lang_tour.my_tx.tir @@ -645,14 +645,6 @@ { "name": "vote_delegation_certificate", "data": { - "stake": { - "Bytes": [ - 135, - 101, - 67, - 33 - ] - }, "drep": { "Bytes": [ 18, @@ -660,6 +652,14 @@ 86, 120 ] + }, + "stake": { + "Bytes": [ + 135, + 101, + 67, + 33 + ] } } }, @@ -688,6 +688,9 @@ { "name": "plutus_witness", "data": { + "version": { + "Number": 2 + }, "script": { "Bytes": [ 171, @@ -696,9 +699,6 @@ 18, 52 ] - }, - "version": { - "Number": 2 } } }, diff --git a/examples/list_concat.ast b/examples/list_concat.ast index f8b6c17f..2708bb0d 100644 --- a/examples/list_concat.ast +++ b/examples/list_concat.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/local_vars.ast b/examples/local_vars.ast index 432bb4ff..8f52defe 100644 --- a/examples/local_vars.ast +++ b/examples/local_vars.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/map.ast b/examples/map.ast index 148694e0..224baad4 100644 --- a/examples/map.ast +++ b/examples/map.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/reference_script.ast b/examples/reference_script.ast index ccb5f6db..3764b1ab 100644 --- a/examples/reference_script.ast +++ b/examples/reference_script.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/reference_script.publish_native.tir b/examples/reference_script.publish_native.tir index 90fdd3b9..644738e7 100644 --- a/examples/reference_script.publish_native.tir +++ b/examples/reference_script.publish_native.tir @@ -147,6 +147,17 @@ 0 ] }, + "to": { + "EvalParam": { + "ExpectValue": [ + "receiver", + "Address" + ] + } + }, + "version": { + "Number": 0 + }, "amount": { "Assets": [ { @@ -162,17 +173,6 @@ } } ] - }, - "to": { - "EvalParam": { - "ExpectValue": [ - "receiver", - "Address" - ] - } - }, - "version": { - "Number": 0 } } } diff --git a/examples/reference_script.publish_plutus.tir b/examples/reference_script.publish_plutus.tir index 7506c0f5..7a17ad71 100644 --- a/examples/reference_script.publish_plutus.tir +++ b/examples/reference_script.publish_plutus.tir @@ -137,25 +137,6 @@ { "name": "cardano_publish", "data": { - "version": { - "Number": 3 - }, - "amount": { - "Assets": [ - { - "policy": "None", - "asset_name": "None", - "amount": { - "EvalParam": { - "ExpectValue": [ - "quantity", - "Int" - ] - } - } - } - ] - }, "script": { "Bytes": [ 81, @@ -178,6 +159,22 @@ 105 ] }, + "amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + } + ] + }, "to": { "EvalParam": { "ExpectValue": [ @@ -185,6 +182,9 @@ "Address" ] } + }, + "version": { + "Number": 3 } } } diff --git a/examples/swap.ast b/examples/swap.ast index 8d915d0f..db090bc7 100644 --- a/examples/swap.ast +++ b/examples/swap.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/transfer.ast b/examples/transfer.ast index a21a5413..0fce4dc3 100644 --- a/examples/transfer.ast +++ b/examples/transfer.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/vesting.ast b/examples/vesting.ast index dac2dfa9..1ae0698a 100644 --- a/examples/vesting.ast +++ b/examples/vesting.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/withdrawal.ast b/examples/withdrawal.ast index 0f0e7382..ce7cc4d1 100644 --- a/examples/withdrawal.ast +++ b/examples/withdrawal.ast @@ -1,4 +1,5 @@ { + "imports": [], "env": null, "txs": [ { diff --git a/examples/withdrawal.transfer.tir b/examples/withdrawal.transfer.tir index facdbb3c..48a914f8 100644 --- a/examples/withdrawal.transfer.tir +++ b/examples/withdrawal.transfer.tir @@ -165,6 +165,12 @@ { "name": "withdrawal", "data": { + "redeemer": { + "Struct": { + "constructor": 0, + "fields": [] + } + }, "credential": { "EvalParam": { "ExpectValue": [ @@ -175,12 +181,6 @@ }, "amount": { "Number": 0 - }, - "redeemer": { - "Struct": { - "constructor": 0, - "fields": [] - } } } }