use super::{ error::Error, schema::{Annotated, Schema}, }; use crate::module::{CheckedModule, CheckedModules}; use aiken_lang::{ast::TypedValidator, uplc::CodeGenerator}; use miette::NamedSource; use serde; use std::{ collections::HashMap, fmt::{self, Display}, }; use uplc::ast::{DeBruijn, Program, Term}; #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct Validator { pub title: String, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] pub datum: Option>, pub redeemer: Annotated, #[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default)] pub parameters: Vec>, #[serde(flatten)] pub program: Program, } impl Display for Validator { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = serde_json::to_string_pretty(self).map_err(|_| fmt::Error)?; f.write_str(&s) } } impl Validator { pub fn from_checked_module( modules: &CheckedModules, generator: &mut CodeGenerator, module: &CheckedModule, def: &TypedValidator, ) -> Result, Error> { let mut args = def.fun.arguments.iter().rev(); let (_, redeemer, datum) = (args.next(), args.next().unwrap(), args.next()); let mut arguments = Vec::with_capacity(def.params.len() + def.fun.arguments.len()); arguments.extend(def.params.clone()); arguments.extend(def.fun.arguments.clone()); Ok(Validator { title: format!("{}.{}", &module.name, &def.fun.name), description: None, parameters: def .params .iter() .map(|param| { let annotation = Annotated::from_type(modules.into(), ¶m.tipo, &HashMap::new()).map_err( |error| Error::Schema { error, location: param.location, source_code: NamedSource::new( module.input_path.display().to_string(), module.code.clone(), ), }, ); annotation.map(|mut annotation| { annotation.title = annotation .title .or_else(|| Some(param.arg_name.get_label())); annotation }) }) .collect::>()?, datum: datum .map(|datum| { Annotated::from_type(modules.into(), &datum.tipo, &HashMap::new()).map_err( |error| Error::Schema { error, location: datum.location, source_code: NamedSource::new( module.input_path.display().to_string(), module.code.clone(), ), }, ) }) .transpose()?, redeemer: Annotated::from_type(modules.into(), &redeemer.tipo, &HashMap::new()) .map_err(|error| Error::Schema { error, location: redeemer.location, source_code: NamedSource::new( module.input_path.display().to_string(), module.code.clone(), ), })?, program: generator .generate(&def.fun.body, &arguments, true) .try_into() .unwrap(), }) } } impl Validator where T: Clone, { pub fn apply(self, arg: &Term) -> Result { match self.parameters.split_first() { None => Err(Error::NoParametersToApply), Some((_, tail)) => { // TODO: Ideally, we should control that the applied term matches its schema. Ok(Self { program: self.program.apply_term(arg), parameters: tail.to_vec(), ..self }) } } } } #[cfg(test)] mod test { use super::*; use crate::{module::ParsedModule, PackageName}; use aiken_lang::{ self, ast::{ModuleKind, Tracing, TypedDataType, TypedFunction}, builder::{DataTypeKey, FunctionAccessKey}, builtins, parser, tipo::TypeInfo, IdGenerator, }; use assert_json_diff::assert_json_eq; use indexmap::IndexMap; use serde_json::{self, json}; use std::{collections::HashMap, path::PathBuf}; // TODO: Possible refactor this out of the module and have it used by `Project`. The idea would // be to make this struct below the actual project, and wrap it in another metadata struct // which contains all the config and I/O stuff regarding the project. struct TestProject { package: PackageName, id_gen: IdGenerator, module_types: HashMap, functions: IndexMap, data_types: IndexMap, } impl TestProject { fn new() -> Self { let id_gen = IdGenerator::new(); let package = PackageName { owner: "test".to_owned(), repo: "project".to_owned(), }; let mut module_types = HashMap::new(); module_types.insert("aiken".to_string(), builtins::prelude(&id_gen)); module_types.insert("aiken/builtin".to_string(), builtins::plutus(&id_gen)); let functions = builtins::prelude_functions(&id_gen); let data_types = builtins::prelude_data_types(&id_gen); TestProject { package, id_gen, module_types, functions, data_types, } } fn parse(&self, source_code: &str) -> ParsedModule { let kind = ModuleKind::Validator; let name = "test_module".to_owned(); let (mut ast, extra) = parser::module(source_code, kind).expect("Failed to parse module"); ast.name = name.clone(); ParsedModule { kind, ast, code: source_code.to_string(), name, path: PathBuf::new(), extra, package: self.package.to_string(), } } fn check(&mut self, module: ParsedModule) -> CheckedModule { let mut warnings = vec![]; let ast = module .ast .infer( &self.id_gen, module.kind, &self.package.to_string(), &self.module_types, Tracing::NoTraces, &mut warnings, ) .expect("Failed to type-check module"); self.module_types .insert(module.name.clone(), ast.type_info.clone()); let mut checked_module = CheckedModule { kind: module.kind, extra: module.extra, name: module.name, code: module.code, package: module.package, input_path: module.path, ast, }; checked_module.attach_doc_and_module_comments(); checked_module } } fn assert_validator(source_code: &str, json: serde_json::Value) { let mut project = TestProject::new(); let modules = CheckedModules::singleton(project.check(project.parse(source_code))); let mut generator = modules.new_generator( &project.functions, &project.data_types, &project.module_types, ); let (validator, def) = modules .validators() .next() .expect("source code did no yield any validator"); let validator = Validator::from_checked_module(&modules, &mut generator, validator, def) .expect("Failed to create validator blueprint"); assert_json_eq!(serde_json::to_value(&validator).unwrap(), json); } #[test] fn validator_mint_basic() { assert_validator( r#" validator mint { fn(redeemer: Data, ctx: Data) { True } } "#, json!({ "title": "test_module.mint", "hash": "afddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72", "redeemer": { "title": "Data", "description": "Any Plutus data." }, "compiledCode": "583b010000323232323232322253330054a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae881" }), ); } #[test] fn validator_mint_parameterized() { assert_validator( r#" validator mint(utxo_ref: Int) { fn(redeemer: Data, ctx: Data) { True } } "#, json!({ "title": "test_module.mint", "hash": "a82df717fd39f5b273c4eb89ae5252e11cc272ac59d815419bf2e4c3", "parameters": [{ "title": "utxo_ref", "dataType": "integer" }], "redeemer": { "title": "Data", "description": "Any Plutus data." }, "compiledCode": "5840010000323232323232322322253330074a22930b1bad0013001001222533300600214984cc014c004c01c008ccc00c00cc0200080055cd2b9b5573eae855d101" }), ); } #[test] fn validator_spend() { assert_validator( r#" /// On-chain state type State { /// The contestation period as a number of seconds contestationPeriod: ContestationPeriod, /// List of public key hashes of all participants parties: List, utxoHash: Hash, } /// A Hash digest for a given algorithm. type Hash = ByteArray type Blake2b_256 { Blake2b_256 } /// Whatever type ContestationPeriod { /// A positive, non-zero number of seconds. ContestationPeriod(Int) } type Party = ByteArray type Input { CollectCom Close /// Abort a transaction Abort } validator spend { fn(datum: State, redeemer: Input, ctx: Data) { True } } "#, json!({ "title": "test_module.spend", "hash": "e37db487fbd58c45d059bcbf5cd6b1604d3bec16cf888f1395a4ebc4", "datum": { "title": "State", "description": "On-chain state", "anyOf": [ { "title": "State", "dataType": "constructor", "index": 0, "fields": [ { "title": "contestationPeriod", "description": "The contestation period as a number of seconds", "anyOf": [ { "title": "ContestationPeriod", "description": "A positive, non-zero number of seconds.", "dataType": "constructor", "index": 0, "fields": [ { "dataType": "integer" } ] } ] }, { "title": "parties", "description": "List of public key hashes of all participants", "dataType": "list", "items": { "dataType": "bytes" } }, { "title": "utxoHash", "dataType": "bytes" } ] } ] }, "redeemer": { "title": "Input", "anyOf": [ { "title": "CollectCom", "dataType": "constructor", "index": 0, "fields": [] }, { "title": "Close", "dataType": "constructor", "index": 1, "fields": [] }, { "title": "Abort", "description": "Abort a transaction", "dataType": "constructor", "index": 2, "fields": [] } ] }, "compiledCode": "583b0100003232323232323222253330064a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae89" }), ); } #[test] fn validator_spend_2tuple() { assert_validator( r#" validator spend { fn(datum: (Int, ByteArray), redeemer: String, ctx: Void) { True } } "#, json!({ "title": "test_module.spend", "hash": "3c6766e7a36df2aa13c0e9e6e071317ed39d05f405771c4f1a81c6cc", "datum": { "dataType": "list", "items": [ { "dataType": "integer" }, { "dataType": "bytes" } ] }, "redeemer": { "dataType": "#string" }, "compiledCode": "585501000032323232323232232232253330084a22930b1b99375c002646466ec0c024008c024004c024004dd6000980080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae881" }), ) } #[test] fn validator_spend_tuples() { assert_validator( r#" validator spend { fn(datum: (Int, Int, Int), redeemer: Data, ctx: Void) { True } } "#, json!({ "title": "test_module.spend", "hash": "f335ce0436fd7df56e727a66ada7298534a27b98f887bc3b7947ee48", "datum": { "title": "Tuple", "dataType": "list", "items": [ { "dataType": "integer" }, { "dataType": "integer" }, { "dataType": "integer" } ] }, "redeemer": { "title": "Data", "description": "Any Plutus data." }, "compiledCode": "5840010000323232323232322322253330074a22930b1bac0013001001222533300600214984cc014c004c01c008ccc00c00cc0200080055cd2b9b5573eae855d101" }), ) } #[test] fn validator_generics() { assert_validator( r#" type Either { Left(left) Right(right) } type Interval { Finite(a) Infinite } validator withdraw { fn(redeemer: Either>, ctx: Void) { True } } "#, json!( { "title": "test_module.withdraw", "hash": "afddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72", "redeemer": { "title": "Either", "anyOf": [ { "title": "Left", "dataType": "constructor", "index": 0, "fields": [ { "dataType": "bytes" } ] }, { "title": "Right", "dataType": "constructor", "index": 1, "fields": [ { "title": "Interval", "anyOf": [ { "title": "Finite", "dataType": "constructor", "index": 0, "fields": [ { "dataType": "integer" } ] }, { "title": "Infinite", "dataType": "constructor", "index": 1, "fields": [] } ] } ] } ] }, "compiledCode": "583b010000323232323232322253330054a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae881" } ), ) } #[test] fn validator_phantom_types() { assert_validator( r#" type Dict { inner: List<(ByteArray, value)> } type UUID { UUID } validator mint { fn(redeemer: Dict, ctx: Void) { True } } "#, json!( { "title": "test_module.mint", "hash": "afddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72", "redeemer": { "title": "Dict", "anyOf": [ { "title": "Dict", "dataType": "constructor", "index": 0, "fields": [ { "title": "inner", "dataType": "map", "keys": { "dataType": "bytes" }, "values": { "dataType": "integer" } } ] } ] }, "compiledCode": "583b010000323232323232322253330054a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae881" } ), ); } #[test] fn validator_opaque_types() { assert_validator( r#" pub opaque type Dict { inner: List<(ByteArray, value)> } type UUID { UUID } validator mint { fn(redeemer: Dict, ctx: Void) { True } } "#, json!( { "title": "test_module.mint", "hash": "afddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72", "redeemer": { "title": "Dict", "dataType": "map", "keys": { "dataType": "bytes" }, "values": { "dataType": "integer" } }, "compiledCode": "583b010000323232323232322253330054a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae881" } ), ); } #[test] fn exported_data() { assert_validator( r#" pub type Foo { foo: Data } validator spend { fn(datum: Data, redeemer: Int, ctx: Void) { True } } "#, json!( { "title": "test_module.spend", "hash": "a3dbab684d90d19e6bab3a0b00a7290ff59fe637d14428859bf74376", "datum": { "title": "Data", "description": "Any Plutus data.", }, "redeemer": { "dataType": "integer", }, "compiledCode": "5840010000323232323232322232253330074a22930b1bad0013001001222533300600214984cc014c004c01c008ccc00c00cc0200080055cd2b9b5573eae855d101" } ), ); } }