diff --git a/Cargo.lock b/Cargo.lock index 5ced0941..df83f067 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -328,6 +328,14 @@ dependencies = [ "half", ] +[[package]] +name = "cip-57" +version = "0.13.0" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "core-foundation" version = "0.10.0" @@ -2171,6 +2179,7 @@ dependencies = [ "base64 0.22.1", "bech32 0.11.1", "ciborium", + "cip-57", "hex", "miette", "paste", diff --git a/Cargo.toml b/Cargo.toml index 42a07143..2e313c3d 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..d70aa572 --- /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" 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 db7db7c5..a86afc94 100644 --- a/crates/tx3-lang/Cargo.toml +++ b/crates/tx3-lang/Cargo.toml @@ -16,7 +16,7 @@ readme.workspace = true thiserror = { workspace = true } trait-variant = { workspace = true } hex = { workspace = true } - +cip-57 = { version = "0.13.0", path = "../cip-57" } miette = { version = "7.4.0", features = ["fancy"] } pest = { version = "2.7.15", features = ["miette-error", "pretty-print"] } pest_derive = "2.7.15" @@ -26,6 +26,7 @@ serde_json = { version = "1.0.137", optional = true } base64 = { version = "0.22.1", optional = true } bech32 = { version = "0.11.1", optional = true } + [dev-dependencies] assert-json-diff = "2.0.2" paste = "1.0.15" 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..acbb48e0 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -17,6 +17,16 @@ pub struct Scope { pub(crate) parent: Option>, } +impl Scope { + pub fn symbols(&self) -> &HashMap { + &self.symbols + } + + pub fn parent(&self) -> Option<&Rc> { + self.parent.as_ref() + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum Symbol { EnvVar(String, Box), @@ -170,6 +180,19 @@ impl AsRef for Identifier { } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum ImportKind { + Blueprint, + Tx3, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Import { + pub span: Span, + pub path: String, + pub kind: ImportKind, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] pub struct Program { pub env: Option, @@ -179,6 +202,7 @@ pub struct Program { pub assets: Vec, pub parties: Vec, pub policies: Vec, + pub imports: Vec, pub span: Span, // analysis @@ -186,6 +210,12 @@ pub struct Program { pub(crate) scope: Option>, } +impl Program { + pub fn scope(&self) -> Option<&Rc> { + self.scope.as_ref() + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct EnvField { pub name: String, @@ -907,6 +937,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 deleted file mode 100644 index a91c511f..00000000 --- a/crates/tx3-lang/src/cardano.rs +++ /dev/null @@ -1,1053 +0,0 @@ -use std::{collections::HashMap, rc::Rc}; - -use pest::iterators::Pair; -use serde::{Deserialize, Serialize}; - -use crate::{ - analyzing::{Analyzable, AnalyzeReport}, - ast::{DataExpr, Identifier, Scope, Span, Type}, - ir, - lowering::IntoLower, - parsing::{AstNode, Error, Rule}, -}; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum WithdrawalField { - From(Box), - Amount(Box), - Redeemer(Box), -} - -impl WithdrawalField { - fn key(&self) -> &str { - match self { - WithdrawalField::From(_) => "from", - WithdrawalField::Amount(_) => "amount", - WithdrawalField::Redeemer(_) => "redeemer", - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct WithdrawalBlock { - pub fields: Vec, - pub span: Span, -} - -impl WithdrawalBlock { - pub(crate) fn find(&self, key: &str) -> Option<&WithdrawalField> { - self.fields.iter().find(|x| x.key() == key) - } -} - -impl AstNode for WithdrawalField { - const RULE: Rule = Rule::cardano_withdrawal_field; - - fn parse(pair: Pair) -> Result { - match pair.as_rule() { - Rule::cardano_withdrawal_from => { - let pair = pair.into_inner().next().unwrap(); - Ok(WithdrawalField::From(DataExpr::parse(pair)?.into())) - } - Rule::cardano_withdrawal_amount => { - let pair = pair.into_inner().next().unwrap(); - Ok(WithdrawalField::Amount(DataExpr::parse(pair)?.into())) - } - Rule::cardano_withdrawal_redeemer => { - let pair = pair.into_inner().next().unwrap(); - Ok(WithdrawalField::Redeemer(DataExpr::parse(pair)?.into())) - } - x => unreachable!("Unexpected rule in cardano_withdrawal_field: {:?}", x), - } - } - - fn span(&self) -> &Span { - match self { - Self::From(x) => x.span(), - Self::Amount(x) => x.span(), - Self::Redeemer(x) => x.span(), - } - } -} - -impl AstNode for WithdrawalBlock { - const RULE: Rule = Rule::cardano_withdrawal_block; - - fn parse(pair: Pair) -> Result { - let span = pair.as_span().into(); - let inner = pair.into_inner(); - - let fields = inner - .map(|x| WithdrawalField::parse(x)) - .collect::, _>>()?; - - Ok(WithdrawalBlock { fields, span }) - } - - fn span(&self) -> &Span { - &self.span - } -} - -impl Analyzable for WithdrawalField { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - match self { - WithdrawalField::From(x) => x.analyze(parent), - WithdrawalField::Amount(x) => { - let amount = x.analyze(parent.clone()); - let amount_type = AnalyzeReport::expect_data_expr_type(x, &Type::Int); - amount + amount_type - } - WithdrawalField::Redeemer(x) => x.analyze(parent), - } - } - - fn is_resolved(&self) -> bool { - match self { - WithdrawalField::From(x) => x.is_resolved(), - WithdrawalField::Amount(x) => x.is_resolved(), - WithdrawalField::Redeemer(x) => x.is_resolved(), - } - } -} - -impl Analyzable for WithdrawalBlock { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - self.fields.analyze(parent) - } - - fn is_resolved(&self) -> bool { - self.fields.is_resolved() - } -} - -impl IntoLower for WithdrawalField { - type Output = ir::Expression; - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - match self { - WithdrawalField::From(x) => x.into_lower(ctx), - WithdrawalField::Amount(x) => x.into_lower(ctx), - WithdrawalField::Redeemer(x) => x.into_lower(ctx), - } - } -} - -impl IntoLower for WithdrawalBlock { - type Output = ir::AdHocDirective; - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - let credential = self - .find("from") - .ok_or_else(|| { - crate::lowering::Error::MissingRequiredField("from".to_string(), "WithdrawalBlock") - })? - .into_lower(ctx)?; - - let amount = self - .find("amount") - .ok_or_else(|| { - crate::lowering::Error::MissingRequiredField( - "amount".to_string(), - "WithdrawalBlock", - ) - })? - .into_lower(ctx)?; - - let redeemer = self - .find("redeemer") - .map(|r| r.into_lower(ctx)) - .transpose()? - .unwrap_or(ir::Expression::None); - - Ok(ir::AdHocDirective { - name: "withdrawal".to_string(), - data: std::collections::HashMap::from([ - ("credential".to_string(), credential), - ("amount".to_string(), amount), - ("redeemer".to_string(), redeemer), - ]), - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct VoteDelegationCertificate { - pub drep: DataExpr, - pub stake: DataExpr, - pub span: Span, -} - -impl AstNode for VoteDelegationCertificate { - const RULE: Rule = Rule::cardano_vote_delegation_certificate; - - fn parse(pair: Pair) -> Result { - let span = pair.as_span().into(); - let mut inner = pair.into_inner(); - - Ok(VoteDelegationCertificate { - drep: DataExpr::parse(inner.next().unwrap())?, - stake: DataExpr::parse(inner.next().unwrap())?, - span, - }) - } - - fn span(&self) -> &Span { - &self.span - } -} - -impl Analyzable for VoteDelegationCertificate { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - let drep = self.drep.analyze(parent.clone()); - let stake = self.stake.analyze(parent.clone()); - - drep + stake - } - - fn is_resolved(&self) -> bool { - self.drep.is_resolved() && self.stake.is_resolved() - } -} - -impl IntoLower for VoteDelegationCertificate { - type Output = ir::AdHocDirective; - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - Ok(ir::AdHocDirective { - name: "vote_delegation_certificate".to_string(), - data: HashMap::from([ - ("drep".to_string(), self.drep.into_lower(ctx)?), - ("stake".to_string(), self.stake.into_lower(ctx)?), - ]), - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct StakeDelegationCertificate { - pub pool: DataExpr, - pub stake: DataExpr, - pub span: Span, -} - -impl AstNode for StakeDelegationCertificate { - const RULE: Rule = Rule::cardano_stake_delegation_certificate; - - fn parse(pair: Pair) -> Result { - let span = pair.as_span().into(); - let mut inner = pair.into_inner(); - - Ok(StakeDelegationCertificate { - pool: DataExpr::parse(inner.next().unwrap())?, - stake: DataExpr::parse(inner.next().unwrap())?, - span, - }) - } - - fn span(&self) -> &Span { - &self.span - } -} - -impl Analyzable for StakeDelegationCertificate { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - let pool = self.pool.analyze(parent.clone()); - let stake = self.stake.analyze(parent.clone()); - - pool + stake - } - - fn is_resolved(&self) -> bool { - self.pool.is_resolved() && self.stake.is_resolved() - } -} - -impl IntoLower for StakeDelegationCertificate { - type Output = ir::AdHocDirective; - - fn into_lower( - &self, - _ctx: &crate::lowering::Context, - ) -> Result { - todo!("StakeDelegationCertificate lowering not implemented") - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum PlutusWitnessField { - Version(DataExpr, Span), - Script(DataExpr, Span), -} - -impl IntoLower for PlutusWitnessField { - type Output = (String, ir::Expression); - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - match self { - PlutusWitnessField::Version(x, _) => Ok(("version".to_string(), x.into_lower(ctx)?)), - PlutusWitnessField::Script(x, _) => Ok(("script".to_string(), x.into_lower(ctx)?)), - } - } -} - -impl AstNode for PlutusWitnessField { - const RULE: Rule = Rule::cardano_plutus_witness_field; - - fn parse(pair: Pair) -> Result { - let span = pair.as_span().into(); - - match pair.as_rule() { - Rule::cardano_plutus_witness_version => { - Ok(PlutusWitnessField::Version(DataExpr::parse(pair)?, span)) - } - Rule::cardano_plutus_witness_script => { - Ok(PlutusWitnessField::Script(DataExpr::parse(pair)?, span)) - } - x => unreachable!("Unexpected rule in cardano_plutus_witness_field: {:?}", x), - } - } - - fn span(&self) -> &Span { - match self { - PlutusWitnessField::Version(_, span) => span, - PlutusWitnessField::Script(_, span) => span, - } - } -} - -impl Analyzable for PlutusWitnessField { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - match self { - PlutusWitnessField::Version(x, _) => x.analyze(parent), - PlutusWitnessField::Script(x, _) => x.analyze(parent), - } - } - - fn is_resolved(&self) -> bool { - match self { - PlutusWitnessField::Version(x, _) => x.is_resolved(), - PlutusWitnessField::Script(x, _) => x.is_resolved(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct PlutusWitnessBlock { - pub fields: Vec, - pub span: Span, -} - -impl AstNode for PlutusWitnessBlock { - const RULE: Rule = Rule::cardano_plutus_witness_block; - - fn parse(pair: Pair) -> Result { - let span = pair.as_span().into(); - let inner = pair.into_inner(); - - let fields = inner - .map(|x| PlutusWitnessField::parse(x)) - .collect::, _>>()?; - - Ok(PlutusWitnessBlock { fields, span }) - } - - fn span(&self) -> &Span { - &self.span - } -} - -impl Analyzable for PlutusWitnessBlock { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - self.fields.analyze(parent) - } - - fn is_resolved(&self) -> bool { - self.fields.is_resolved() - } -} - -impl IntoLower for PlutusWitnessBlock { - type Output = ir::AdHocDirective; - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - let data = self - .fields - .iter() - .map(|x| x.into_lower(ctx)) - .collect::>()?; - - Ok(ir::AdHocDirective { - name: "plutus_witness".to_string(), - data, - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum NativeWitnessField { - Script(DataExpr, Span), -} - -impl IntoLower for NativeWitnessField { - type Output = (String, ir::Expression); - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - match self { - NativeWitnessField::Script(x, _) => Ok(("script".to_string(), x.into_lower(ctx)?)), - } - } -} - -impl AstNode for NativeWitnessField { - const RULE: Rule = Rule::cardano_native_witness_field; - - fn parse(pair: Pair) -> Result { - let span = pair.as_span().into(); - - match pair.as_rule() { - Rule::cardano_native_witness_script => { - Ok(NativeWitnessField::Script(DataExpr::parse(pair)?, span)) - } - x => unreachable!("Unexpected rule in cardano_native_witness_field: {:?}", x), - } - } - - fn span(&self) -> &Span { - match self { - NativeWitnessField::Script(_, span) => span, - } - } -} - -impl Analyzable for NativeWitnessField { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - match self { - NativeWitnessField::Script(x, _) => x.analyze(parent), - } - } - - fn is_resolved(&self) -> bool { - match self { - NativeWitnessField::Script(x, _) => x.is_resolved(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct NativeWitnessBlock { - pub fields: Vec, - pub span: Span, -} - -impl AstNode for NativeWitnessBlock { - const RULE: Rule = Rule::cardano_native_witness_block; - - fn parse(pair: Pair) -> Result { - let span = pair.as_span().into(); - let inner = pair.into_inner(); - - let fields = inner - .map(|x| NativeWitnessField::parse(x)) - .collect::, _>>()?; - - Ok(NativeWitnessBlock { fields, span }) - } - - fn span(&self) -> &Span { - &self.span - } -} - -impl Analyzable for NativeWitnessBlock { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - self.fields.analyze(parent) - } - - fn is_resolved(&self) -> bool { - self.fields.is_resolved() - } -} - -impl IntoLower for NativeWitnessBlock { - type Output = ir::AdHocDirective; - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - let data = self - .fields - .iter() - .map(|x| x.into_lower(ctx)) - .collect::>()?; - - Ok(ir::AdHocDirective { - name: "native_witness".to_string(), - data, - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct TreasuryDonationBlock { - pub coin: DataExpr, - pub span: Span, -} - -impl AstNode for TreasuryDonationBlock { - const RULE: Rule = Rule::cardano_treasury_donation_block; - - fn parse(pair: Pair) -> Result { - let span = pair.as_span().into(); - - let mut inner = pair.into_inner(); - let coin = DataExpr::parse(inner.next().unwrap())?; - - Ok(TreasuryDonationBlock { coin, span }) - } - - fn span(&self) -> &Span { - &self.span - } -} - -impl Analyzable for TreasuryDonationBlock { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - let coin = self.coin.analyze(parent); - let coin_type = AnalyzeReport::expect_data_expr_type(&self.coin, &Type::Int); - - coin + coin_type - } - - fn is_resolved(&self) -> bool { - self.coin.is_resolved() - } -} - -impl IntoLower for TreasuryDonationBlock { - type Output = ir::AdHocDirective; - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - let coin = self.coin.into_lower(ctx)?; - - Ok(ir::AdHocDirective { - name: "treasury_donation".to_string(), - data: std::collections::HashMap::from([("coin".to_string(), coin)]), - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum CardanoPublishBlockField { - To(Box), - Amount(Box), - Datum(Box), - Version(Box), - Script(Box), -} - -impl CardanoPublishBlockField { - fn key(&self) -> &str { - match self { - CardanoPublishBlockField::To(_) => "to", - CardanoPublishBlockField::Amount(_) => "amount", - CardanoPublishBlockField::Datum(_) => "datum", - CardanoPublishBlockField::Version(_) => "version", - CardanoPublishBlockField::Script(_) => "script", - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct CardanoPublishBlock { - pub name: Option, - pub fields: Vec, - pub span: Span, -} - -impl CardanoPublishBlock { - pub(crate) fn find(&self, key: &str) -> Option<&CardanoPublishBlockField> { - self.fields.iter().find(|x| x.key() == key) - } -} - -impl AstNode for CardanoPublishBlockField { - const RULE: Rule = Rule::cardano_publish_block_field; - - fn parse(pair: Pair) -> Result { - match pair.as_rule() { - Rule::cardano_publish_block_to => { - let pair = pair.into_inner().next().unwrap(); - Ok(CardanoPublishBlockField::To(DataExpr::parse(pair)?.into())) - } - Rule::cardano_publish_block_amount => { - let pair = pair.into_inner().next().unwrap(); - Ok(CardanoPublishBlockField::Amount( - DataExpr::parse(pair)?.into(), - )) - } - Rule::cardano_publish_block_datum => { - let pair = pair.into_inner().next().unwrap(); - Ok(CardanoPublishBlockField::Datum( - DataExpr::parse(pair)?.into(), - )) - } - Rule::cardano_publish_block_version => { - let pair = pair.into_inner().next().unwrap(); - Ok(CardanoPublishBlockField::Version( - DataExpr::parse(pair)?.into(), - )) - } - Rule::cardano_publish_block_script => { - let pair = pair.into_inner().next().unwrap(); - Ok(CardanoPublishBlockField::Script( - DataExpr::parse(pair)?.into(), - )) - } - x => unreachable!("Unexpected rule in cardano_publish_block_field: {:?}", x), - } - } - - fn span(&self) -> &Span { - match self { - Self::To(x) => x.span(), - Self::Amount(x) => x.span(), - Self::Datum(x) => x.span(), - Self::Version(x) => x.span(), - Self::Script(x) => x.span(), - } - } -} - -impl AstNode for CardanoPublishBlock { - const RULE: Rule = Rule::cardano_publish_block; - - fn parse(pair: Pair) -> Result { - let span = pair.as_span().into(); - let mut inner = pair.into_inner(); - let has_name = inner - .peek() - .map(|x| x.as_rule() == Rule::identifier) - .unwrap_or_default(); - - let name = if has_name { - Some(Identifier::parse(inner.next().unwrap())?) - } else { - None - }; - - let fields = inner - .map(|x| CardanoPublishBlockField::parse(x)) - .collect::, _>>()?; - - Ok(CardanoPublishBlock { name, fields, span }) - } - - fn span(&self) -> &Span { - &self.span - } -} - -impl Analyzable for CardanoPublishBlockField { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - match self { - CardanoPublishBlockField::To(x) => x.analyze(parent), - CardanoPublishBlockField::Amount(x) => x.analyze(parent), - CardanoPublishBlockField::Datum(x) => x.analyze(parent), - CardanoPublishBlockField::Version(x) => x.analyze(parent), - CardanoPublishBlockField::Script(x) => x.analyze(parent), - } - } - - fn is_resolved(&self) -> bool { - match self { - CardanoPublishBlockField::To(x) => x.is_resolved(), - CardanoPublishBlockField::Amount(x) => x.is_resolved(), - CardanoPublishBlockField::Datum(x) => x.is_resolved(), - CardanoPublishBlockField::Version(x) => x.is_resolved(), - CardanoPublishBlockField::Script(x) => x.is_resolved(), - } - } -} - -impl Analyzable for CardanoPublishBlock { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - self.fields.analyze(parent) - } - - fn is_resolved(&self) -> bool { - self.fields.is_resolved() - } -} - -impl IntoLower for CardanoPublishBlockField { - type Output = (String, ir::Expression); - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - match self { - CardanoPublishBlockField::To(x) => Ok(("to".to_string(), x.into_lower(ctx)?)), - CardanoPublishBlockField::Amount(x) => Ok(("amount".to_string(), x.into_lower(ctx)?)), - CardanoPublishBlockField::Datum(x) => Ok(("datum".to_string(), x.into_lower(ctx)?)), - CardanoPublishBlockField::Version(x) => Ok(("version".to_string(), x.into_lower(ctx)?)), - CardanoPublishBlockField::Script(x) => Ok(("script".to_string(), x.into_lower(ctx)?)), - } - } -} - -impl IntoLower for CardanoPublishBlock { - type Output = ir::AdHocDirective; - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result { - let data = self - .fields - .iter() - .map(|x| x.into_lower(ctx)) - .collect::>()?; - - Ok(ir::AdHocDirective { - name: "cardano_publish".to_string(), - data, - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum CardanoBlock { - VoteDelegationCertificate(VoteDelegationCertificate), - StakeDelegationCertificate(StakeDelegationCertificate), - Withdrawal(WithdrawalBlock), - PlutusWitness(PlutusWitnessBlock), - NativeWitness(NativeWitnessBlock), - TreasuryDonation(TreasuryDonationBlock), - Publish(CardanoPublishBlock), -} - -impl AstNode for CardanoBlock { - const RULE: Rule = Rule::cardano_block; - - fn parse(pair: Pair) -> Result { - let mut inner = pair.into_inner(); - let item = inner.next().unwrap(); - - match item.as_rule() { - Rule::cardano_vote_delegation_certificate => Ok( - CardanoBlock::VoteDelegationCertificate(VoteDelegationCertificate::parse(item)?), - ), - Rule::cardano_stake_delegation_certificate => Ok( - CardanoBlock::StakeDelegationCertificate(StakeDelegationCertificate::parse(item)?), - ), - Rule::cardano_withdrawal_block => { - Ok(CardanoBlock::Withdrawal(WithdrawalBlock::parse(item)?)) - } - Rule::cardano_plutus_witness_block => Ok(CardanoBlock::PlutusWitness( - PlutusWitnessBlock::parse(item)?, - )), - Rule::cardano_native_witness_block => Ok(CardanoBlock::NativeWitness( - NativeWitnessBlock::parse(item)?, - )), - Rule::cardano_treasury_donation_block => Ok(CardanoBlock::TreasuryDonation( - TreasuryDonationBlock::parse(item)?, - )), - Rule::cardano_publish_block => { - Ok(CardanoBlock::Publish(CardanoPublishBlock::parse(item)?)) - } - x => unreachable!("Unexpected rule in cardano_block: {:?}", x), - } - } - - fn span(&self) -> &Span { - match self { - CardanoBlock::VoteDelegationCertificate(x) => x.span(), - CardanoBlock::StakeDelegationCertificate(x) => x.span(), - CardanoBlock::Withdrawal(x) => x.span(), - CardanoBlock::PlutusWitness(x) => x.span(), - CardanoBlock::NativeWitness(x) => x.span(), - CardanoBlock::TreasuryDonation(x) => x.span(), - CardanoBlock::Publish(x) => x.span(), - } - } -} - -impl Analyzable for CardanoBlock { - fn analyze(&mut self, parent: Option>) -> AnalyzeReport { - match self { - CardanoBlock::VoteDelegationCertificate(x) => x.analyze(parent), - CardanoBlock::StakeDelegationCertificate(x) => x.analyze(parent), - CardanoBlock::Withdrawal(x) => x.analyze(parent), - CardanoBlock::PlutusWitness(x) => x.analyze(parent), - CardanoBlock::NativeWitness(x) => x.analyze(parent), - CardanoBlock::TreasuryDonation(x) => x.analyze(parent), - CardanoBlock::Publish(x) => x.analyze(parent), - } - } - - fn is_resolved(&self) -> bool { - match self { - CardanoBlock::VoteDelegationCertificate(x) => x.is_resolved(), - CardanoBlock::StakeDelegationCertificate(x) => x.is_resolved(), - CardanoBlock::Withdrawal(x) => x.is_resolved(), - CardanoBlock::PlutusWitness(x) => x.is_resolved(), - CardanoBlock::NativeWitness(x) => x.is_resolved(), - Self::TreasuryDonation(x) => x.is_resolved(), - CardanoBlock::Publish(x) => x.is_resolved(), - } - } -} - -impl IntoLower for CardanoBlock { - type Output = ir::AdHocDirective; - - fn into_lower( - &self, - ctx: &crate::lowering::Context, - ) -> Result<::Output, crate::lowering::Error> { - match self { - CardanoBlock::VoteDelegationCertificate(x) => x.into_lower(ctx), - CardanoBlock::StakeDelegationCertificate(x) => x.into_lower(ctx), - CardanoBlock::Withdrawal(x) => x.into_lower(ctx), - CardanoBlock::PlutusWitness(x) => x.into_lower(ctx), - CardanoBlock::NativeWitness(x) => x.into_lower(ctx), - CardanoBlock::TreasuryDonation(x) => x.into_lower(ctx), - CardanoBlock::Publish(x) => x.into_lower(ctx), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - analyzing::analyze, - ast::{self, *}, - }; - use pest::Parser; - - macro_rules! input_to_ast_check { - ($ast:ty, $name:expr, $input:expr, $expected:expr) => { - paste::paste! { - #[test] - fn []() { - let pairs = crate::parsing::Tx3Grammar::parse(<$ast>::RULE, $input).unwrap(); - let single_match = pairs.into_iter().next().unwrap(); - let result = <$ast>::parse(single_match).unwrap(); - - assert_eq!(result, $expected); - } - } - }; - } - - input_to_ast_check!( - PlutusWitnessBlock, - "basic", - "plutus_witness { - version: 3, - script: 0xABCDEF, - }", - PlutusWitnessBlock { - fields: vec![ - PlutusWitnessField::Version(DataExpr::Number(3), Span::DUMMY), - PlutusWitnessField::Script( - DataExpr::HexString(HexStringLiteral::new("ABCDEF".to_string())), - Span::DUMMY - ) - ], - span: Span::DUMMY, - } - ); - - input_to_ast_check!( - NativeWitnessBlock, - "basic", - "native_witness { - script: 0xABCDEF, - }", - NativeWitnessBlock { - fields: vec![NativeWitnessField::Script( - DataExpr::HexString(HexStringLiteral::new("ABCDEF".to_string())), - Span::DUMMY - )], - span: Span::DUMMY, - } - ); - - input_to_ast_check!( - TreasuryDonationBlock, - "basic", - "treasury_donation { - coin: 2020, - }", - TreasuryDonationBlock { - coin: DataExpr::Number(2020), - span: Span::DUMMY, - } - ); - - input_to_ast_check!( - CardanoPublishBlock, - "basic", - "publish { - to: Receiver, - amount: Ada(quantity), - version: 3, - script: 0xABCDEF, - }", - CardanoPublishBlock { - name: None, - fields: vec![ - CardanoPublishBlockField::To(Box::new(DataExpr::Identifier(Identifier::new( - "Receiver" - )))), - CardanoPublishBlockField::Amount(Box::new(DataExpr::StaticAssetConstructor( - StaticAssetConstructor { - r#type: Identifier::new("Ada"), - amount: Box::new(DataExpr::Identifier(Identifier::new("quantity"))), - span: Span::DUMMY, - } - ))), - CardanoPublishBlockField::Version(Box::new(DataExpr::Number(3))), - CardanoPublishBlockField::Script(Box::new(DataExpr::HexString( - HexStringLiteral::new("ABCDEF".to_string()) - ))), - ], - span: Span::DUMMY, - } - ); - - input_to_ast_check!( - CardanoPublishBlock, - "basic_with_name", - "publish test_publish { - to: Receiver, - amount: Ada(quantity), - version: 3, - script: 0xABCDEF, - }", - CardanoPublishBlock { - name: Some(Identifier::new("test_publish")), - fields: vec![ - CardanoPublishBlockField::To(Box::new(DataExpr::Identifier(Identifier::new( - "Receiver" - )))), - CardanoPublishBlockField::Amount(Box::new(DataExpr::StaticAssetConstructor( - StaticAssetConstructor { - r#type: Identifier::new("Ada"), - amount: Box::new(DataExpr::Identifier(Identifier::new("quantity"))), - span: Span::DUMMY, - } - ))), - CardanoPublishBlockField::Version(Box::new(DataExpr::Number(3))), - CardanoPublishBlockField::Script(Box::new(DataExpr::HexString( - HexStringLiteral::new("ABCDEF".to_string()) - ))), - ], - span: Span::DUMMY, - } - ); - - #[test] - fn test_treasury_donation_type() { - let mut ast = crate::parsing::parse_string( - r#" - tx test(quantity: Int) { - cardano::treasury_donation { - coin: quantity, - } - } - "#, - ) - .unwrap(); - - let result = analyze(&mut ast); - assert!(result.errors.is_empty()); - } - - #[test] - fn test_treasury_donation_type_not_ok() { - let mut ast = crate::parsing::parse_string( - r#" - tx test(quantity: Bytes) { - cardano::treasury_donation { - coin: quantity, - } - } - "#, - ) - .unwrap(); - - let result = analyze(&mut ast); - assert!(!result.errors.is_empty()); - } - - #[test] - fn test_publish_type_ok() { - let mut ast = crate::parsing::parse_string( - r#" - party Receiver; - - tx test(quantity: Int) { - cardano::publish { - to: Receiver, - amount: Ada(quantity), - version: 3, - script: 0xABCDEF, - } - } - "#, - ) - .unwrap(); - - let result = analyze(&mut ast); - assert!(result.errors.is_empty()); - } - - #[test] - fn test_publish_type_with_name_ok() { - let mut ast = crate::parsing::parse_string( - r#" - party Receiver; - - tx test(quantity: Int) { - cardano::publish deploy { - to: Receiver, - amount: Ada(quantity), - version: 3, - script: 0xABCDEF, - } - } - "#, - ) - .unwrap(); - - let result = analyze(&mut ast); - assert!(result.errors.is_empty()); - } -} diff --git a/crates/tx3-lang/src/cardano/analyzing.rs b/crates/tx3-lang/src/cardano/analyzing.rs new file mode 100644 index 00000000..c4c74f3d --- /dev/null +++ b/crates/tx3-lang/src/cardano/analyzing.rs @@ -0,0 +1,271 @@ +use std::rc::Rc; + +use crate::{ + analyzing::{Analyzable, AnalyzeReport}, + ast::{Scope, Type}, +}; + +use super::ast::*; + +impl Analyzable for WithdrawalField { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + match self { + WithdrawalField::From(x) => x.analyze(parent), + WithdrawalField::Amount(x) => { + let amount = x.analyze(parent.clone()); + let amount_type = AnalyzeReport::expect_data_expr_type(x, &Type::Int); + amount + amount_type + } + WithdrawalField::Redeemer(x) => x.analyze(parent), + } + } + + fn is_resolved(&self) -> bool { + match self { + WithdrawalField::From(x) => x.is_resolved(), + WithdrawalField::Amount(x) => x.is_resolved(), + WithdrawalField::Redeemer(x) => x.is_resolved(), + } + } +} + +impl Analyzable for WithdrawalBlock { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + self.fields.analyze(parent) + } + + fn is_resolved(&self) -> bool { + self.fields.is_resolved() + } +} + +impl Analyzable for VoteDelegationCertificate { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + let drep = self.drep.analyze(parent.clone()); + let stake = self.stake.analyze(parent.clone()); + + drep + stake + } + + fn is_resolved(&self) -> bool { + self.drep.is_resolved() && self.stake.is_resolved() + } +} + +impl Analyzable for StakeDelegationCertificate { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + let pool = self.pool.analyze(parent.clone()); + let stake = self.stake.analyze(parent.clone()); + + pool + stake + } + + fn is_resolved(&self) -> bool { + self.pool.is_resolved() && self.stake.is_resolved() + } +} + +impl Analyzable for PlutusWitnessField { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + match self { + PlutusWitnessField::Version(x, _) => x.analyze(parent), + PlutusWitnessField::Script(x, _) => x.analyze(parent), + } + } + + fn is_resolved(&self) -> bool { + match self { + PlutusWitnessField::Version(x, _) => x.is_resolved(), + PlutusWitnessField::Script(x, _) => x.is_resolved(), + } + } +} + +impl Analyzable for PlutusWitnessBlock { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + self.fields.analyze(parent) + } + + fn is_resolved(&self) -> bool { + self.fields.is_resolved() + } +} + +impl Analyzable for NativeWitnessField { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + match self { + NativeWitnessField::Script(x, _) => x.analyze(parent), + } + } + + fn is_resolved(&self) -> bool { + match self { + NativeWitnessField::Script(x, _) => x.is_resolved(), + } + } +} + +impl Analyzable for NativeWitnessBlock { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + self.fields.analyze(parent) + } + + fn is_resolved(&self) -> bool { + self.fields.is_resolved() + } +} + +impl Analyzable for TreasuryDonationBlock { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + let coin = self.coin.analyze(parent); + let coin_type = AnalyzeReport::expect_data_expr_type(&self.coin, &Type::Int); + + coin + coin_type + } + + fn is_resolved(&self) -> bool { + self.coin.is_resolved() + } +} + +impl Analyzable for CardanoPublishBlockField { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + match self { + CardanoPublishBlockField::To(x) => x.analyze(parent), + CardanoPublishBlockField::Amount(x) => x.analyze(parent), + CardanoPublishBlockField::Datum(x) => x.analyze(parent), + CardanoPublishBlockField::Version(x) => x.analyze(parent), + CardanoPublishBlockField::Script(x) => x.analyze(parent), + } + } + + fn is_resolved(&self) -> bool { + match self { + CardanoPublishBlockField::To(x) => x.is_resolved(), + CardanoPublishBlockField::Amount(x) => x.is_resolved(), + CardanoPublishBlockField::Datum(x) => x.is_resolved(), + CardanoPublishBlockField::Version(x) => x.is_resolved(), + CardanoPublishBlockField::Script(x) => x.is_resolved(), + } + } +} + +impl Analyzable for CardanoPublishBlock { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + self.fields.analyze(parent) + } + + fn is_resolved(&self) -> bool { + self.fields.is_resolved() + } +} + +impl Analyzable for CardanoBlock { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + match self { + CardanoBlock::VoteDelegationCertificate(x) => x.analyze(parent), + CardanoBlock::StakeDelegationCertificate(x) => x.analyze(parent), + CardanoBlock::Withdrawal(x) => x.analyze(parent), + CardanoBlock::PlutusWitness(x) => x.analyze(parent), + CardanoBlock::NativeWitness(x) => x.analyze(parent), + CardanoBlock::TreasuryDonation(x) => x.analyze(parent), + CardanoBlock::Publish(x) => x.analyze(parent), + } + } + + fn is_resolved(&self) -> bool { + match self { + CardanoBlock::VoteDelegationCertificate(x) => x.is_resolved(), + CardanoBlock::StakeDelegationCertificate(x) => x.is_resolved(), + CardanoBlock::Withdrawal(x) => x.is_resolved(), + CardanoBlock::PlutusWitness(x) => x.is_resolved(), + CardanoBlock::NativeWitness(x) => x.is_resolved(), + Self::TreasuryDonation(x) => x.is_resolved(), + CardanoBlock::Publish(x) => x.is_resolved(), + } + } +} + +#[cfg(test)] +mod tests { + + use crate::analyzing::analyze; + + #[test] + fn test_treasury_donation_type() { + let mut ast = crate::parsing::parse_string( + r#" + tx test(quantity: Int) { + cardano::treasury_donation { + coin: quantity, + } + } + "#, + ) + .unwrap(); + + let result = analyze(&mut ast); + assert!(result.errors.is_empty()); + } + + #[test] + fn test_treasury_donation_type_not_ok() { + let mut ast = crate::parsing::parse_string( + r#" + tx test(quantity: Bytes) { + cardano::treasury_donation { + coin: quantity, + } + } + "#, + ) + .unwrap(); + + let result = analyze(&mut ast); + assert!(!result.errors.is_empty()); + } + + #[test] + fn test_publish_type_ok() { + let mut ast = crate::parsing::parse_string( + r#" + party Receiver; + + tx test(quantity: Int) { + cardano::publish { + to: Receiver, + amount: Ada(quantity), + version: 3, + script: 0xABCDEF, + } + } + "#, + ) + .unwrap(); + + let result = analyze(&mut ast); + assert!(result.errors.is_empty()); + } + + #[test] + fn test_publish_type_with_name_ok() { + let mut ast = crate::parsing::parse_string( + r#" + party Receiver; + + tx test(quantity: Int) { + cardano::publish deploy { + to: Receiver, + amount: Ada(quantity), + version: 3, + script: 0xABCDEF, + } + } + "#, + ) + .unwrap(); + + let result = analyze(&mut ast); + assert!(result.errors.is_empty()); + } +} diff --git a/crates/tx3-lang/src/cardano/ast.rs b/crates/tx3-lang/src/cardano/ast.rs new file mode 100644 index 00000000..5cf45473 --- /dev/null +++ b/crates/tx3-lang/src/cardano/ast.rs @@ -0,0 +1,120 @@ +use serde::{Deserialize, Serialize}; + +use crate::ast::{DataExpr, Identifier, Span}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum WithdrawalField { + From(Box), + Amount(Box), + Redeemer(Box), +} + +impl WithdrawalField { + pub(crate) fn key(&self) -> &str { + match self { + WithdrawalField::From(_) => "from", + WithdrawalField::Amount(_) => "amount", + WithdrawalField::Redeemer(_) => "redeemer", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct WithdrawalBlock { + pub fields: Vec, + pub span: Span, +} + +impl WithdrawalBlock { + pub(crate) fn find(&self, key: &str) -> Option<&WithdrawalField> { + self.fields.iter().find(|x| x.key() == key) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct VoteDelegationCertificate { + pub drep: DataExpr, + pub stake: DataExpr, + pub span: Span, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct StakeDelegationCertificate { + pub pool: DataExpr, + pub stake: DataExpr, + pub span: Span, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum PlutusWitnessField { + Version(DataExpr, Span), + Script(DataExpr, Span), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PlutusWitnessBlock { + pub fields: Vec, + pub span: Span, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum NativeWitnessField { + Script(DataExpr, Span), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct NativeWitnessBlock { + pub fields: Vec, + pub span: Span, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TreasuryDonationBlock { + pub coin: DataExpr, + pub span: Span, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum CardanoPublishBlockField { + To(Box), + Amount(Box), + Datum(Box), + Version(Box), + Script(Box), +} + +impl CardanoPublishBlockField { + pub(crate) fn key(&self) -> &str { + match self { + CardanoPublishBlockField::To(_) => "to", + CardanoPublishBlockField::Amount(_) => "amount", + CardanoPublishBlockField::Datum(_) => "datum", + CardanoPublishBlockField::Version(_) => "version", + CardanoPublishBlockField::Script(_) => "script", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct CardanoPublishBlock { + pub name: Option, + pub fields: Vec, + pub span: Span, +} + +impl CardanoPublishBlock { + pub(crate) fn find(&self, key: &str) -> Option<&CardanoPublishBlockField> { + self.fields.iter().find(|x| x.key() == key) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum CardanoBlock { + VoteDelegationCertificate(VoteDelegationCertificate), + StakeDelegationCertificate(StakeDelegationCertificate), + Withdrawal(WithdrawalBlock), + PlutusWitness(PlutusWitnessBlock), + NativeWitness(NativeWitnessBlock), + TreasuryDonation(TreasuryDonationBlock), + Publish(CardanoPublishBlock), +} diff --git a/crates/tx3-lang/src/cardano/blueprint.rs b/crates/tx3-lang/src/cardano/blueprint.rs new file mode 100644 index 00000000..3aed3596 --- /dev/null +++ b/crates/tx3-lang/src/cardano/blueprint.rs @@ -0,0 +1,262 @@ +use std::collections::HashMap; +use std::collections::HashSet; + +/// Decodes JSON pointer escapes (~1 -> /, ~0 -> ~). +fn decode_json_pointer_escapes(s: &str) -> String { + s.replace("~1", "/").replace("~0", "~") +} + +/// Strips known prefixes (Option, Pairs) and extracts the last segment +pub fn aiken_prettify_name(name: &str) -> String { + let decoded = decode_json_pointer_escapes(name); + + if let Some(inner) = decoded.strip_prefix("Option$") { + let inner_pretty = aiken_prettify_name(inner); + return format!("Option{}", inner_pretty); + } + + if let Some(inner) = decoded.strip_prefix("Option<") { + let inner = inner.strip_suffix(">").unwrap_or(inner); + let inner_pretty = aiken_prettify_name(inner); + return format!("Option{}", inner_pretty); + } + + if let Some(inner) = decoded.strip_prefix("Pairs$") { + let parts: Vec<&str> = inner.split('_').collect(); + let prettified: Vec = parts.iter().map(|p| aiken_prettify_name(p)).collect(); + return format!("Pairs{}", prettified.join("")); + } + + if let Some(inner) = decoded.strip_prefix("Pairs<") { + let inner = inner.strip_suffix(">").unwrap_or(inner); + let parts: Vec<&str> = inner.split(',').collect(); + let prettified: Vec = parts + .iter() + .map(|p| aiken_prettify_name(p.trim())) + .collect(); + return format!("Pairs{}", prettified.join("")); + } + + extract_last_segment(&decoded) +} + +fn extract_last_segment(path: &str) -> String { + path.rsplit('/').next().unwrap_or(path).to_string() +} + +/// Converts a path to upper camel case handling nested generic types like Option and Pairs +pub fn path_to_upper_camel(name: &str) -> String { + let decoded = decode_json_pointer_escapes(name); + + if let Some(inner) = decoded.strip_prefix("Option$") { + let inner_camel = path_to_upper_camel(inner); + return format!("Option{}", inner_camel); + } + + if let Some(inner) = decoded.strip_prefix("Option<") { + let inner = inner.strip_suffix(">").unwrap_or(inner); + let inner_camel = path_to_upper_camel(inner); + return format!("Option{}", inner_camel); + } + + if let Some(inner) = decoded.strip_prefix("Pairs$") { + let parts: Vec<&str> = inner.split('_').collect(); + let prettified: Vec = parts.iter().map(|p| path_to_upper_camel(p)).collect(); + return format!("Pairs{}", prettified.join("")); + } + + if let Some(inner) = decoded.strip_prefix("Pairs<") { + let inner = inner.strip_suffix(">").unwrap_or(inner); + let parts: Vec<&str> = inner.split(',').collect(); + let prettified: Vec = parts + .iter() + .map(|p| path_to_upper_camel(p.trim())) + .collect(); + return format!("Pairs{}", prettified.join("")); + } + + path_segments_to_camel(&decoded) +} + +fn path_segments_to_camel(path: &str) -> String { + path.split('/') + .map(|segment| { + let mut chars = segment.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().to_string() + chars.as_str(), + } + }) + .collect() +} + +pub fn build_aiken_name_mapping(keys: &[&str]) -> HashMap { + // collect simplified names and detect clashes + let mut simple_to_originals: HashMap> = HashMap::new(); + + for &key in keys { + let simple = aiken_prettify_name(key); + simple_to_originals + .entry(simple) + .or_default() + .push(key.to_string()); + } + + let mut result = HashMap::new(); + let mut used_names: HashSet = HashSet::new(); + + for &key in keys { + let simple = aiken_prettify_name(key); + let originals = simple_to_originals.get(&simple).unwrap(); + + let final_name = if originals.len() == 1 { + simple.clone() + } else { + path_to_upper_camel(key) + }; + + // ensure uniqueness + let mut unique_name = final_name.clone(); + let mut counter = 1; + while used_names.contains(&unique_name) { + unique_name = format!("{}{}", final_name, counter); + counter += 1; + } + + used_names.insert(unique_name.clone()); + result.insert(key.to_string(), unique_name); + } + + result +} + +/// Sanitizes a type name to be a valid tx3 identifier +pub fn generic_sanitizer(name: &str) -> String { + name.replace("~1", "_") + .replace('/', "_") + .replace('$', "_") + .replace('<', "_") + .replace('>', "") + .replace(',', "_") + .replace(' ', "") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_aiken_prettify_name_basic() { + assert_eq!(aiken_prettify_name("cardano/address/Address"), "Address"); + assert_eq!(aiken_prettify_name("aiken/crypto/ScriptHash"), "ScriptHash"); + assert_eq!(aiken_prettify_name("types/SettingsDatum"), "SettingsDatum"); + + assert_eq!(aiken_prettify_name("Int"), "Int"); + assert_eq!(aiken_prettify_name("Bool"), "Bool"); + assert_eq!(aiken_prettify_name("ByteArray"), "ByteArray"); + } + + #[test] + fn test_aiken_prettify_name_option_and_pairs() { + assert_eq!( + aiken_prettify_name("Option$cardano/address/Address"), + "OptionAddress" + ); + assert_eq!( + aiken_prettify_name("Option$cardano/address/StakeCredential"), + "OptionStakeCredential" + ); + + assert_eq!( + aiken_prettify_name("Pairs$cardano/assets/AssetName_Int"), + "PairsAssetNameInt" + ); + + assert_eq!( + aiken_prettify_name("Pairs$cardano/assets/PolicyId_Pairs$cardano/assets/AssetName_Int"), + "PairsPolicyIdPairsAssetNameInt" + ); + } + + #[test] + fn test_aiken_prettify_name_url_encoded() { + assert_eq!(aiken_prettify_name("cardano~1address~1Address"), "Address"); + } + + #[test] + fn test_aiken_prettify_name_generics() { + assert_eq!( + aiken_prettify_name("Option"), + "OptionStakeCredential" + ); + + assert_eq!( + aiken_prettify_name("Pairs"), + "PairsAssetNameInt" + ); + assert_eq!( + aiken_prettify_name("Pairs"), + "PairsAssetNameInt" + ); + } + + #[test] + fn test_path_to_upper_camel_generics() { + assert_eq!( + path_to_upper_camel("Option"), + "OptionCardanoAddressStakeCredential" + ); + + assert_eq!( + path_to_upper_camel("Pairs"), + "PairsCardanoAssetsAssetNameInt" + ); + } + + #[test] + fn test_build_aiken_name_mapping_no_clashes() { + let keys = vec![ + "cardano/address/Address", + "cardano/assets/AssetName", + "types/SettingsDatum", + "Int", + ]; + let mapping = build_aiken_name_mapping(&keys); + + assert_eq!(mapping.get("cardano/address/Address").unwrap(), "Address"); + assert_eq!( + mapping.get("cardano/assets/AssetName").unwrap(), + "AssetName" + ); + assert_eq!(mapping.get("types/SettingsDatum").unwrap(), "SettingsDatum"); + assert_eq!(mapping.get("Int").unwrap(), "Int"); + } + + #[test] + fn test_build_aiken_name_mapping_with_clashes() { + // Both cardano/transaction/Datum and types/Datum map into Datum + let keys = vec!["cardano/transaction/Datum", "types/Datum"]; + let mapping = build_aiken_name_mapping(&keys); + + assert_eq!( + mapping.get("cardano/transaction/Datum").unwrap(), + "CardanoTransactionDatum" + ); + assert_eq!(mapping.get("types/Datum").unwrap(), "TypesDatum"); + } + #[test] + fn test_generic_sanitizer() { + assert_eq!(generic_sanitizer("simple"), "simple"); + assert_eq!(generic_sanitizer("path/to/something"), "path_to_something"); + assert_eq!(generic_sanitizer("Option"), "Option_Int"); + assert_eq!(generic_sanitizer("Map"), "Map_K_V"); + assert_eq!( + generic_sanitizer("some ~1 weird ~0 thing"), + "some_weird~0thing" + ); + assert_eq!( + generic_sanitizer("Result, String>"), + "Result_Option_Int_String" + ); + } +} diff --git a/crates/tx3-lang/src/cardano/lowering.rs b/crates/tx3-lang/src/cardano/lowering.rs new file mode 100644 index 00000000..ad607d5d --- /dev/null +++ b/crates/tx3-lang/src/cardano/lowering.rs @@ -0,0 +1,228 @@ +use std::collections::HashMap; + +use crate::{ir, lowering::IntoLower}; + +use super::ast::*; + +impl IntoLower for WithdrawalField { + type Output = ir::Expression; + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + match self { + WithdrawalField::From(x) => x.into_lower(ctx), + WithdrawalField::Amount(x) => x.into_lower(ctx), + WithdrawalField::Redeemer(x) => x.into_lower(ctx), + } + } +} + +impl IntoLower for WithdrawalBlock { + type Output = ir::AdHocDirective; + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + let credential = self + .find("from") + .ok_or_else(|| { + crate::lowering::Error::MissingRequiredField("from".to_string(), "WithdrawalBlock") + })? + .into_lower(ctx)?; + + let amount = self + .find("amount") + .ok_or_else(|| { + crate::lowering::Error::MissingRequiredField( + "amount".to_string(), + "WithdrawalBlock", + ) + })? + .into_lower(ctx)?; + + let redeemer = self + .find("redeemer") + .map(|r| r.into_lower(ctx)) + .transpose()? + .unwrap_or(ir::Expression::None); + + Ok(ir::AdHocDirective { + name: "withdrawal".to_string(), + data: std::collections::HashMap::from([ + ("credential".to_string(), credential), + ("amount".to_string(), amount), + ("redeemer".to_string(), redeemer), + ]), + }) + } +} + +impl IntoLower for VoteDelegationCertificate { + type Output = ir::AdHocDirective; + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + Ok(ir::AdHocDirective { + name: "vote_delegation_certificate".to_string(), + data: HashMap::from([ + ("drep".to_string(), self.drep.into_lower(ctx)?), + ("stake".to_string(), self.stake.into_lower(ctx)?), + ]), + }) + } +} + +impl IntoLower for StakeDelegationCertificate { + type Output = ir::AdHocDirective; + + fn into_lower( + &self, + _ctx: &crate::lowering::Context, + ) -> Result { + todo!("StakeDelegationCertificate lowering not implemented") + } +} + +impl IntoLower for PlutusWitnessField { + type Output = (String, ir::Expression); + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + match self { + PlutusWitnessField::Version(x, _) => Ok(("version".to_string(), x.into_lower(ctx)?)), + PlutusWitnessField::Script(x, _) => Ok(("script".to_string(), x.into_lower(ctx)?)), + } + } +} + +impl IntoLower for PlutusWitnessBlock { + type Output = ir::AdHocDirective; + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + let data = self + .fields + .iter() + .map(|x| x.into_lower(ctx)) + .collect::>()?; + + Ok(ir::AdHocDirective { + name: "plutus_witness".to_string(), + data, + }) + } +} + +impl IntoLower for NativeWitnessField { + type Output = (String, ir::Expression); + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + match self { + NativeWitnessField::Script(x, _) => Ok(("script".to_string(), x.into_lower(ctx)?)), + } + } +} + +impl IntoLower for NativeWitnessBlock { + type Output = ir::AdHocDirective; + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + let data = self + .fields + .iter() + .map(|x| x.into_lower(ctx)) + .collect::>()?; + + Ok(ir::AdHocDirective { + name: "native_witness".to_string(), + data, + }) + } +} + +impl IntoLower for TreasuryDonationBlock { + type Output = ir::AdHocDirective; + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + let coin = self.coin.into_lower(ctx)?; + + Ok(ir::AdHocDirective { + name: "treasury_donation".to_string(), + data: std::collections::HashMap::from([("coin".to_string(), coin)]), + }) + } +} + +impl IntoLower for CardanoPublishBlockField { + type Output = (String, ir::Expression); + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + match self { + CardanoPublishBlockField::To(x) => Ok(("to".to_string(), x.into_lower(ctx)?)), + CardanoPublishBlockField::Amount(x) => Ok(("amount".to_string(), x.into_lower(ctx)?)), + CardanoPublishBlockField::Datum(x) => Ok(("datum".to_string(), x.into_lower(ctx)?)), + CardanoPublishBlockField::Version(x) => Ok(("version".to_string(), x.into_lower(ctx)?)), + CardanoPublishBlockField::Script(x) => Ok(("script".to_string(), x.into_lower(ctx)?)), + } + } +} + +impl IntoLower for CardanoPublishBlock { + type Output = ir::AdHocDirective; + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result { + let data = self + .fields + .iter() + .map(|x| x.into_lower(ctx)) + .collect::>()?; + + Ok(ir::AdHocDirective { + name: "cardano_publish".to_string(), + data, + }) + } +} + +impl IntoLower for CardanoBlock { + type Output = ir::AdHocDirective; + + fn into_lower( + &self, + ctx: &crate::lowering::Context, + ) -> Result<::Output, crate::lowering::Error> { + match self { + CardanoBlock::VoteDelegationCertificate(x) => x.into_lower(ctx), + CardanoBlock::StakeDelegationCertificate(x) => x.into_lower(ctx), + CardanoBlock::Withdrawal(x) => x.into_lower(ctx), + CardanoBlock::PlutusWitness(x) => x.into_lower(ctx), + CardanoBlock::NativeWitness(x) => x.into_lower(ctx), + CardanoBlock::TreasuryDonation(x) => x.into_lower(ctx), + CardanoBlock::Publish(x) => x.into_lower(ctx), + } + } +} diff --git a/crates/tx3-lang/src/cardano/mod.rs b/crates/tx3-lang/src/cardano/mod.rs new file mode 100644 index 00000000..5d2da74e --- /dev/null +++ b/crates/tx3-lang/src/cardano/mod.rs @@ -0,0 +1,8 @@ +pub mod analyzing; +pub mod ast; +pub mod blueprint; +pub mod lowering; +pub mod parsing; + +pub use ast::*; +pub use parsing::load_externals; diff --git a/crates/tx3-lang/src/cardano/parsing.rs b/crates/tx3-lang/src/cardano/parsing.rs new file mode 100644 index 00000000..95fb28aa --- /dev/null +++ b/crates/tx3-lang/src/cardano/parsing.rs @@ -0,0 +1,764 @@ +use std::{collections::HashMap, fs}; + +use pest::iterators::Pair; + +use crate::{ + ast::{DataExpr, Identifier, RecordField, Span, Symbol, Type, TypeDef, VariantCase}, + parsing::{AstNode, Error, Rule}, +}; + +use super::{ast::*, blueprint}; + +impl AstNode for WithdrawalField { + const RULE: Rule = Rule::cardano_withdrawal_field; + + fn parse(pair: Pair) -> Result { + match pair.as_rule() { + Rule::cardano_withdrawal_from => { + let pair = pair.into_inner().next().unwrap(); + Ok(WithdrawalField::From(DataExpr::parse(pair)?.into())) + } + Rule::cardano_withdrawal_amount => { + let pair = pair.into_inner().next().unwrap(); + Ok(WithdrawalField::Amount(DataExpr::parse(pair)?.into())) + } + Rule::cardano_withdrawal_redeemer => { + let pair = pair.into_inner().next().unwrap(); + Ok(WithdrawalField::Redeemer(DataExpr::parse(pair)?.into())) + } + x => unreachable!("Unexpected rule in cardano_withdrawal_field: {:?}", x), + } + } + + fn span(&self) -> &Span { + match self { + Self::From(x) => x.span(), + Self::Amount(x) => x.span(), + Self::Redeemer(x) => x.span(), + } + } +} + +impl AstNode for WithdrawalBlock { + const RULE: Rule = Rule::cardano_withdrawal_block; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + let inner = pair.into_inner(); + + let fields = inner + .map(|x| WithdrawalField::parse(x)) + .collect::, _>>()?; + + Ok(WithdrawalBlock { fields, span }) + } + + fn span(&self) -> &Span { + &self.span + } +} + +impl AstNode for VoteDelegationCertificate { + const RULE: Rule = Rule::cardano_vote_delegation_certificate; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + let mut inner = pair.into_inner(); + + Ok(VoteDelegationCertificate { + drep: DataExpr::parse(inner.next().unwrap())?, + stake: DataExpr::parse(inner.next().unwrap())?, + span, + }) + } + + fn span(&self) -> &Span { + &self.span + } +} + +impl AstNode for StakeDelegationCertificate { + const RULE: Rule = Rule::cardano_stake_delegation_certificate; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + let mut inner = pair.into_inner(); + + Ok(StakeDelegationCertificate { + pool: DataExpr::parse(inner.next().unwrap())?, + stake: DataExpr::parse(inner.next().unwrap())?, + span, + }) + } + + fn span(&self) -> &Span { + &self.span + } +} + +impl AstNode for PlutusWitnessField { + const RULE: Rule = Rule::cardano_plutus_witness_field; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + + match pair.as_rule() { + Rule::cardano_plutus_witness_version => { + Ok(PlutusWitnessField::Version(DataExpr::parse(pair)?, span)) + } + Rule::cardano_plutus_witness_script => { + Ok(PlutusWitnessField::Script(DataExpr::parse(pair)?, span)) + } + x => unreachable!("Unexpected rule in cardano_plutus_witness_field: {:?}", x), + } + } + + fn span(&self) -> &Span { + match self { + PlutusWitnessField::Version(_, span) => span, + PlutusWitnessField::Script(_, span) => span, + } + } +} + +impl AstNode for PlutusWitnessBlock { + const RULE: Rule = Rule::cardano_plutus_witness_block; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + let inner = pair.into_inner(); + + let fields = inner + .map(|x| PlutusWitnessField::parse(x)) + .collect::, _>>()?; + + Ok(PlutusWitnessBlock { fields, span }) + } + + fn span(&self) -> &Span { + &self.span + } +} + +impl AstNode for NativeWitnessField { + const RULE: Rule = Rule::cardano_native_witness_field; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + + match pair.as_rule() { + Rule::cardano_native_witness_script => { + Ok(NativeWitnessField::Script(DataExpr::parse(pair)?, span)) + } + x => unreachable!("Unexpected rule in cardano_native_witness_field: {:?}", x), + } + } + + fn span(&self) -> &Span { + match self { + NativeWitnessField::Script(_, span) => span, + } + } +} + +impl AstNode for NativeWitnessBlock { + const RULE: Rule = Rule::cardano_native_witness_block; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + let inner = pair.into_inner(); + + let fields = inner + .map(|x| NativeWitnessField::parse(x)) + .collect::, _>>()?; + + Ok(NativeWitnessBlock { fields, span }) + } + + fn span(&self) -> &Span { + &self.span + } +} + +impl AstNode for TreasuryDonationBlock { + const RULE: Rule = Rule::cardano_treasury_donation_block; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + + let mut inner = pair.into_inner(); + let coin = DataExpr::parse(inner.next().unwrap())?; + + Ok(TreasuryDonationBlock { coin, span }) + } + + fn span(&self) -> &Span { + &self.span + } +} + +impl AstNode for CardanoPublishBlockField { + const RULE: Rule = Rule::cardano_publish_block_field; + + fn parse(pair: Pair) -> Result { + match pair.as_rule() { + Rule::cardano_publish_block_to => { + let pair = pair.into_inner().next().unwrap(); + Ok(CardanoPublishBlockField::To(DataExpr::parse(pair)?.into())) + } + Rule::cardano_publish_block_amount => { + let pair = pair.into_inner().next().unwrap(); + Ok(CardanoPublishBlockField::Amount( + DataExpr::parse(pair)?.into(), + )) + } + Rule::cardano_publish_block_datum => { + let pair = pair.into_inner().next().unwrap(); + Ok(CardanoPublishBlockField::Datum( + DataExpr::parse(pair)?.into(), + )) + } + Rule::cardano_publish_block_version => { + let pair = pair.into_inner().next().unwrap(); + Ok(CardanoPublishBlockField::Version( + DataExpr::parse(pair)?.into(), + )) + } + Rule::cardano_publish_block_script => { + let pair = pair.into_inner().next().unwrap(); + Ok(CardanoPublishBlockField::Script( + DataExpr::parse(pair)?.into(), + )) + } + x => unreachable!("Unexpected rule in cardano_publish_block_field: {:?}", x), + } + } + + fn span(&self) -> &Span { + match self { + Self::To(x) => x.span(), + Self::Amount(x) => x.span(), + Self::Datum(x) => x.span(), + Self::Version(x) => x.span(), + Self::Script(x) => x.span(), + } + } +} + +impl AstNode for CardanoPublishBlock { + const RULE: Rule = Rule::cardano_publish_block; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + let mut inner = pair.into_inner(); + let has_name = inner + .peek() + .map(|x| x.as_rule() == Rule::identifier) + .unwrap_or_default(); + + let name = if has_name { + Some(Identifier::parse(inner.next().unwrap())?) + } else { + None + }; + + let fields = inner + .map(|x| CardanoPublishBlockField::parse(x)) + .collect::, _>>()?; + + Ok(CardanoPublishBlock { name, fields, span }) + } + + fn span(&self) -> &Span { + &self.span + } +} + +impl AstNode for CardanoBlock { + const RULE: Rule = Rule::cardano_block; + + fn parse(pair: Pair) -> Result { + let mut inner = pair.into_inner(); + let item = inner.next().unwrap(); + + match item.as_rule() { + Rule::cardano_vote_delegation_certificate => Ok( + CardanoBlock::VoteDelegationCertificate(VoteDelegationCertificate::parse(item)?), + ), + Rule::cardano_stake_delegation_certificate => Ok( + CardanoBlock::StakeDelegationCertificate(StakeDelegationCertificate::parse(item)?), + ), + Rule::cardano_withdrawal_block => { + Ok(CardanoBlock::Withdrawal(WithdrawalBlock::parse(item)?)) + } + Rule::cardano_plutus_witness_block => Ok(CardanoBlock::PlutusWitness( + PlutusWitnessBlock::parse(item)?, + )), + Rule::cardano_native_witness_block => Ok(CardanoBlock::NativeWitness( + NativeWitnessBlock::parse(item)?, + )), + Rule::cardano_treasury_donation_block => Ok(CardanoBlock::TreasuryDonation( + TreasuryDonationBlock::parse(item)?, + )), + Rule::cardano_publish_block => { + Ok(CardanoBlock::Publish(CardanoPublishBlock::parse(item)?)) + } + x => unreachable!("Unexpected rule in cardano_block: {:?}", x), + } + } + + fn span(&self) -> &Span { + match self { + CardanoBlock::VoteDelegationCertificate(x) => x.span(), + CardanoBlock::StakeDelegationCertificate(x) => x.span(), + CardanoBlock::Withdrawal(x) => x.span(), + CardanoBlock::PlutusWitness(x) => x.span(), + CardanoBlock::NativeWitness(x) => x.span(), + CardanoBlock::TreasuryDonation(x) => x.span(), + CardanoBlock::Publish(x) => x.span(), + } + } +} + +pub fn load_externals( + path: &str, +) -> Result, crate::parsing::Error> { + let json = fs::read_to_string(path).map_err(|e| crate::parsing::Error { + message: format!("Failed to read import file: {}", e), + src: path.to_string(), + span: crate::ast::Span::DUMMY, + })?; + let bp = + serde_json::from_str::(&json).map_err(|e| crate::parsing::Error { + message: format!("Failed to parse blueprint JSON: {}", e), + src: "".to_string(), + span: crate::ast::Span::DUMMY, + })?; + + let is_aiken = bp + .preamble + .compiler + .as_ref() + .is_some_and(|c| c.name.to_lowercase() == "aiken"); + + let name_mapping: HashMap = if is_aiken { + let keys: Vec<&str> = bp + .definitions + .as_ref() + .map(|d| d.inner.keys().map(|s| s.as_str()).collect()) + .unwrap_or_default(); + blueprint::build_aiken_name_mapping(&keys) + } else { + bp.definitions + .as_ref() + .map(|d| { + d.inner + .keys() + .map(|k| (k.clone(), blueprint::generic_sanitizer(k))) + .collect() + }) + .unwrap_or_default() + }; + + let ref_to_type = |r: &str| -> Type { + let key = r.strip_prefix("#/definitions/").unwrap_or(r); + let decoded_key = key.replace("~1", "/"); + let sanitized = name_mapping.get(&decoded_key).cloned().unwrap_or_else(|| { + if is_aiken { + blueprint::aiken_prettify_name(key) + } else { + blueprint::generic_sanitizer(key) + } + }); + Type::Custom(Identifier::new(&sanitized)) + }; + + let mut symbols = HashMap::new(); + for (key, def) in bp + .definitions + .as_ref() + .map(|d| d.inner.iter()) + .into_iter() + .flatten() + { + let name = name_mapping.get(key).cloned().unwrap_or_else(|| { + if is_aiken { + blueprint::aiken_prettify_name(key) + } else { + blueprint::generic_sanitizer(key) + } + }); + + let new = match def.data_type { + Some(cip_57::DataType::Integer) => Some(Symbol::AliasDef(Box::new( + 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, + }, + Some(cip_57::DataType::Constructor) => { + let mut cases = vec![]; + if let Some(any_of) = &def.any_of { + let single = any_of.len() == 1; + for schema in any_of { + let original_case_name = schema + .title + .clone() + .unwrap_or_else(|| format!("Constructor{}", schema.index)); + let case_name = if single && original_case_name == name { + "Default".to_string() + } else { + original_case_name + }; + let mut fields = vec![]; + for (i, field) in schema.fields.iter().enumerate() { + let field_name = field + .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 { + let single = any_of.len() == 1; + for schema in any_of { + let original_case_name = schema + .title + .clone() + .unwrap_or_else(|| format!("Constructor{}", schema.index)); + let case_name = if single && original_case_name == name { + "Default".to_string() + } else { + original_case_name + }; + let mut fields = vec![]; + for (i, field) in schema.fields.iter().enumerate() { + let field_name = field + .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::ast::*; + use pest::Parser; + + macro_rules! input_to_ast_check { + ($ast:ty, $name:expr, $input:expr, $expected:expr) => { + paste::paste! { + #[test] + fn []() { + let pairs = crate::parsing::Tx3Grammar::parse(<$ast>::RULE, $input).unwrap(); + let single_match = pairs.into_iter().next().unwrap(); + let result = <$ast>::parse(single_match).unwrap(); + + assert_eq!(result, $expected); + } + } + }; + } + + input_to_ast_check!( + PlutusWitnessBlock, + "basic", + "plutus_witness { + version: 3, + script: 0xABCDEF, + }", + PlutusWitnessBlock { + fields: vec![ + PlutusWitnessField::Version(DataExpr::Number(3), Span::DUMMY), + PlutusWitnessField::Script( + DataExpr::HexString(HexStringLiteral::new("ABCDEF".to_string())), + Span::DUMMY + ) + ], + span: Span::DUMMY, + } + ); + + input_to_ast_check!( + NativeWitnessBlock, + "basic", + "native_witness { + script: 0xABCDEF, + }", + NativeWitnessBlock { + fields: vec![NativeWitnessField::Script( + DataExpr::HexString(HexStringLiteral::new("ABCDEF".to_string())), + Span::DUMMY + )], + span: Span::DUMMY, + } + ); + + input_to_ast_check!( + TreasuryDonationBlock, + "basic", + "treasury_donation { + coin: 2020, + }", + TreasuryDonationBlock { + coin: DataExpr::Number(2020), + span: Span::DUMMY, + } + ); + + input_to_ast_check!( + CardanoPublishBlock, + "basic", + "publish { + to: Receiver, + amount: Ada(quantity), + version: 3, + script: 0xABCDEF, + }", + CardanoPublishBlock { + name: None, + fields: vec![ + CardanoPublishBlockField::To(Box::new(DataExpr::Identifier(Identifier::new( + "Receiver" + )))), + CardanoPublishBlockField::Amount(Box::new(DataExpr::StaticAssetConstructor( + StaticAssetConstructor { + r#type: Identifier::new("Ada"), + amount: Box::new(DataExpr::Identifier(Identifier::new("quantity"))), + span: Span::DUMMY, + } + ))), + CardanoPublishBlockField::Version(Box::new(DataExpr::Number(3))), + CardanoPublishBlockField::Script(Box::new(DataExpr::HexString( + HexStringLiteral::new("ABCDEF".to_string()) + ))), + ], + span: Span::DUMMY, + } + ); + + input_to_ast_check!( + CardanoPublishBlock, + "basic_with_name", + "publish test_publish { + to: Receiver, + amount: Ada(quantity), + version: 3, + script: 0xABCDEF, + }", + CardanoPublishBlock { + name: Some(Identifier::new("test_publish")), + fields: vec![ + CardanoPublishBlockField::To(Box::new(DataExpr::Identifier(Identifier::new( + "Receiver" + )))), + CardanoPublishBlockField::Amount(Box::new(DataExpr::StaticAssetConstructor( + StaticAssetConstructor { + r#type: Identifier::new("Ada"), + amount: Box::new(DataExpr::Identifier(Identifier::new("quantity"))), + span: Span::DUMMY, + } + ))), + CardanoPublishBlockField::Version(Box::new(DataExpr::Number(3))), + CardanoPublishBlockField::Script(Box::new(DataExpr::HexString( + HexStringLiteral::new("ABCDEF".to_string()) + ))), + ], + span: Span::DUMMY, + } + ); + + #[test] + fn test_single_constructor_alias() { + let json = r##"{ + "preamble": { + "title": "Test", + "description": "Test", + "version": "1.0.0", + "plutusVersion": "v2", + "compiler": { + "name": "Aiken", + "version": "1.0.0" + }, + "license": "Apache-2.0" + }, + "validators": [], + "definitions": { + "ticketer/types/TicketerDatum": { + "title": "TicketerDatum", + "anyOf": [ + { + "title": "TicketerDatum", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "ticket_counter", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "ticketer/types/TicketerRedeemer": { + "title": "TicketerRedeemer", + "anyOf": [ + { + "title": "BuyTicket", + "dataType": "constructor", + "index": 0, + "fields": [] + } + ] + }, + "Int": { + "dataType": "integer" + } + } + }"##; + + use std::io::Write; + + let mut path = std::env::temp_dir(); + path.push(format!("tx3_test_{}.json", std::process::id())); + + { + let mut file = std::fs::File::create(&path).unwrap(); + file.write_all(json.as_bytes()).unwrap(); + } + + let symbols = load_externals(path.to_str().unwrap()).unwrap(); + + // Cleanup + let _ = std::fs::remove_file(&path); + + let datum = symbols.get("TicketerDatum").unwrap(); + + if let Symbol::TypeDef(def) = datum { + assert_eq!(def.cases.len(), 1); + // Matches type name -> Default + assert_eq!(def.cases[0].name.value, "Default"); + } else { + panic!("Expected TypeDef for TicketerDatum, got {:?}", datum); + } + + let redeemer = symbols.get("TicketerRedeemer").unwrap(); + if let Symbol::TypeDef(def) = redeemer { + assert_eq!(def.cases.len(), 1); + // Does NOT match type name -> Keep original name + assert_eq!(def.cases[0].name.value, "BuyTicket"); + } else { + panic!("Expected TypeDef for TicketerRedeemer, got {:?}", redeemer); + } + } + #[test] + fn test_load_externals_generic_compiler() { + let json = r##"{ + "preamble": { + "title": "Test", + "description": "Test", + "version": "1.0.0", + "plutusVersion": "v1", + "compiler": { + "name": "Test", + "version": "0.0.0" + }, + "license": "MIT" + }, + "validators": [], + "definitions": { + "Option": { + "dataType": "integer" + } + } + }"##; + + use std::io::Write; + + let mut path = std::env::temp_dir(); + path.push(format!("tx3_test_generic_{}.json", std::process::id())); + + { + let mut file = std::fs::File::create(&path).unwrap(); + file.write_all(json.as_bytes()).unwrap(); + } + + let symbols = load_externals(path.to_str().unwrap()).unwrap(); + + let _ = std::fs::remove_file(&path); + + assert!(symbols.contains_key("Option_Int")); + assert!(!symbols.contains_key("OptionInt")); + } +} diff --git a/crates/tx3-lang/src/loading.rs b/crates/tx3-lang/src/loading.rs index 8d95808d..77b6fd0e 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 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("AssetName")); + assert!(scope.symbols.contains_key("Datum")); + } } diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 1e67ca80..48d15293 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; @@ -79,6 +76,37 @@ pub trait AstNode: Sized { fn span(&self) -> &Span; } +impl AstNode for Import { + const RULE: Rule = Rule::import; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + let mut inner = pair.into_inner(); + + let import_rule = inner.next().unwrap(); + match import_rule.as_rule() { + Rule::blueprint_import => { + let path = import_rule + .into_inner() + .as_str() + .trim_matches('"') + .to_string(); + Ok(Import { + span, + path, + kind: ImportKind::Blueprint, + }) + } + Rule::tx3_import => todo!(), + x => unreachable!("Unexpected rule in import: {:?}", x), + } + } + + fn span(&self) -> &Span { + &self.span + } +} + impl AstNode for Program { const RULE: Rule = Rule::program; @@ -94,6 +122,7 @@ impl AstNode for Program { aliases: Vec::new(), parties: Vec::new(), policies: Vec::new(), + imports: Vec::new(), scope: None, span, }; @@ -108,6 +137,7 @@ impl AstNode for Program { Rule::alias_def => program.aliases.push(AliasDef::parse(pair)?), Rule::party_def => program.parties.push(PartyDef::parse(pair)?), Rule::policy_def => program.policies.push(PolicyDef::parse(pair)?), + Rule::import => program.imports.push(Import::parse(pair)?), Rule::EOI => break, x => unreachable!("Unexpected rule in program: {:?}", x), } @@ -2627,6 +2657,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 43ef45c1..4bb62d6e 100644 --- a/crates/tx3-lang/src/tx3.pest +++ b/crates/tx3-lang/src/tx3.pest @@ -462,9 +462,23 @@ tx_def = { "tx" ~ identifier ~ parameter_list ~ "{" ~ tx_body_block* ~ "}" } +blueprint_import = { + "blueprint::import" ~ string ~ ";" +} + +tx3_import = { + "tx3::import" ~ string ~ ";" +} + +import = { + blueprint_import | + tx3_import +} + // Program program = { SOI ~ + import* ~ (env_def | asset_def | party_def | policy_def | type_def | tx_def)* ~ EOI } 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..eedc3608 --- /dev/null +++ b/examples/cip_imports.tx3 @@ -0,0 +1,34 @@ +blueprint::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: SettingsDatum::SettingsDatum { + githoney_address: Address::Address{ + payment_credential: PaymentCredential::VerificationKey { + field_0: 0x123123, + }, + stake_credential: OptionStakeCredential::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,