diff --git a/crates/aiken-project/src/blueprint/error.rs b/crates/aiken-project/src/blueprint/error.rs index f9acdb29..0fceaab4 100644 --- a/crates/aiken-project/src/blueprint/error.rs +++ b/crates/aiken-project/src/blueprint/error.rs @@ -27,6 +27,7 @@ pub enum Error { source_code: NamedSource, return_type: Arc, }, + #[error("A {} validator requires at least {} arguments.", name.purple().bold(), at_least.to_string().purple().bold())] #[diagnostic(code("aiken::blueprint::invalid::arity"))] WrongValidatorArity { @@ -37,12 +38,13 @@ pub enum Error { #[source_code] source_code: NamedSource, }, + #[error("{}", error)] #[diagnostic(help("{}", error.help()))] #[diagnostic(code("aiken::blueprint::interface"))] Schema { error: schema::Error, - #[label("invalid contract's boundary")] + #[label("invalid validator's boundary")] location: Span, #[source_code] source_code: NamedSource, @@ -52,6 +54,15 @@ pub enum Error { #[diagnostic(code("aiken::blueprint::missing"))] #[diagnostic(help("Did you forget to {build} the project?", build = "build".purple().bold()))] InvalidOrMissingFile, + + #[error("I didn't find any parameters to apply in the given validator.")] + #[diagnostic(code("aiken::blueprint::apply::no_parameters"))] + NoParametersToApply, + + #[error("I couldn't compute the address of the given validator because it's parameterized by {} parameter(s)!", format!("{n}").purple())] + #[diagnostic(code("aiken::blueprint::address::parameterized"))] + #[diagnostic(help("I can only compute addresses of validators that are fully applied. For example, a {keyword_spend} validator must have exactly 3 arguments: a datum, a redeemer and a context. If it has more, they need to be provided beforehand and applied directly in the validator. Applying parameters change the validator's compiled code, and thus the address.\n\nThis is why I need you to apply parmeters first.", keyword_spend = "spend".purple()))] + ParameterizedValidator { n: usize }, } pub fn assert_return_bool(module: &CheckedModule, def: &TypedFunction) -> Result<(), Error> { diff --git a/crates/aiken-project/src/blueprint/mod.rs b/crates/aiken-project/src/blueprint/mod.rs index 0f23a14d..a420d6a1 100644 --- a/crates/aiken-project/src/blueprint/mod.rs +++ b/crates/aiken-project/src/blueprint/mod.rs @@ -7,12 +7,12 @@ use aiken_lang::uplc::CodeGenerator; use error::Error; use schema::Schema; use std::fmt::{self, Debug, Display}; -use validator::Validator; +use validator::{Purpose, Validator}; #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] -pub struct Blueprint { +pub struct Blueprint { pub preamble: Preamble, - pub validators: Vec>, + pub validators: Vec>, } #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] @@ -25,6 +25,12 @@ pub struct Preamble { pub license: Option, } +#[derive(Debug, PartialEq, Clone)] +pub enum LookupResult<'a, T> { + One(&'a T), + Many, +} + impl Blueprint { pub fn new( config: &Config, @@ -47,6 +53,59 @@ impl Blueprint { } } +impl Blueprint +where + T: Clone + Default, +{ + pub fn lookup( + &self, + title: Option<&String>, + purpose: Option<&Purpose>, + ) -> Option>> { + let mut validator = None; + for v in self.validators.iter() { + let match_title = Some(&v.title) == title.or(Some(&v.title)); + let match_purpose = Some(&v.purpose) == purpose.or(Some(&v.purpose)); + if match_title && match_purpose { + validator = Some(if validator.is_none() { + LookupResult::One(v) + } else { + LookupResult::Many + }) + } + } + validator + } + + pub fn with_validator( + &self, + title: Option<&String>, + purpose: Option<&Purpose>, + when_missing: fn(Vec<(String, Purpose)>) -> E, + when_too_many: fn(Vec<(String, Purpose)>) -> E, + action: F, + ) -> Result + where + F: Fn(Validator) -> Result, + { + match self.lookup(title, purpose) { + Some(LookupResult::One(validator)) => action(validator.to_owned()), + Some(LookupResult::Many) => Err(when_too_many( + self.validators + .iter() + .map(|v| (v.title.clone(), v.purpose.clone())) + .collect(), + )), + None => Err(when_missing( + self.validators + .iter() + .map(|v| (v.title.clone(), v.purpose.clone())) + .collect(), + )), + } + } +} + impl Display for Blueprint { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = serde_json::to_string_pretty(self).map_err(|_| fmt::Error)?; diff --git a/crates/aiken-project/src/blueprint/schema.rs b/crates/aiken-project/src/blueprint/schema.rs index f598199b..df96b9a5 100644 --- a/crates/aiken-project/src/blueprint/schema.rs +++ b/crates/aiken-project/src/blueprint/schema.rs @@ -302,6 +302,12 @@ impl Data { } } +impl Default for Schema { + fn default() -> Self { + Schema::Unit + } +} + impl Display for Schema { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = serde_json::to_string_pretty(self).map_err(|_| fmt::Error)?; diff --git a/crates/aiken-project/src/blueprint/validator.rs b/crates/aiken-project/src/blueprint/validator.rs index 62605a08..a19fa832 100644 --- a/crates/aiken-project/src/blueprint/validator.rs +++ b/crates/aiken-project/src/blueprint/validator.rs @@ -10,7 +10,7 @@ use std::{ collections::HashMap, fmt::{self, Display}, }; -use uplc::ast::{DeBruijn, Program}; +use uplc::ast::{DeBruijn, Program, Term}; #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct Validator { @@ -21,6 +21,9 @@ pub struct Validator { #[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, } @@ -58,12 +61,39 @@ impl Validator { assert_min_arity(validator, def, purpose.min_arity())?; let mut args = def.arguments.iter().rev(); - let (_, redeemer, datum) = (args.next(), args.next().unwrap(), args.next()); + let (_, redeemer) = (args.next(), args.next().unwrap()); + let datum = if purpose.min_arity() > 2 { + args.next() + } else { + None + }; Ok(Validator { title: validator.name.clone(), description: None, purpose, + parameters: args + .rev() + .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( + validator.input_path.display().to_string(), + validator.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( @@ -95,6 +125,25 @@ impl Validator { } } +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 + }) + } + } + } +} + impl Purpose { pub fn min_arity(&self) -> u8 { match self { @@ -272,6 +321,32 @@ mod test { ); } + #[test] + fn validator_mint_parameterized() { + assert_validator( + r#" + fn mint(utxo_ref: Int, redeemer: Data, ctx: Data) { + True + } + "#, + json!({ + "title": "test_module", + "purpose": "mint", + "hash": "455f24922a520c59499fdafad95e1272fab81a99452f6b9545f95337", + "parameters": [{ + "title": "utxo_ref", + "dataType": "integer" + + }], + "redeemer": { + "title": "Data", + "description": "Any Plutus data." + }, + "compiledCode": "4d01000022253335734944526161" + }), + ); + } + #[test] fn validator_spend() { assert_validator( diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index ae14e46f..da0923ef 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -37,7 +37,7 @@ use std::{ }; use telemetry::EventListener; use uplc::{ - ast::{Constant, Term}, + ast::{Constant, DeBruijn, Term}, machine::cost_model::ExBudget, }; @@ -283,8 +283,8 @@ where pub fn address( &self, - with_title: Option<&String>, - with_purpose: Option<&validator::Purpose>, + title: Option<&String>, + purpose: Option<&validator::Purpose>, stake_address: Option<&String>, ) -> Result { // Parse stake address @@ -306,44 +306,64 @@ where }; // Read blueprint - let filepath = self.blueprint_path(); - let blueprint = - File::open(filepath).map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?; + let blueprint = File::open(self.blueprint_path()) + .map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?; let blueprint: Blueprint = serde_json::from_reader(BufReader::new(blueprint))?; - // Find validator's program - let mut program = None; - for v in blueprint.validators.iter() { - if Some(&v.title) == with_title.or(Some(&v.title)) - && Some(&v.purpose) == with_purpose.or(Some(&v.purpose)) - { - program = Some(if program.is_none() { - Ok(v.program.clone()) - } else { - Err(Error::MoreThanOneValidatorFound { - known_validators: blueprint - .validators - .iter() - .map(|v| (v.title.clone(), v.purpose.clone())) - .collect(), - }) - }) + // Calculate the address + let when_too_many = + |known_validators| Error::MoreThanOneValidatorFound { known_validators }; + let when_missing = |known_validators| Error::NoValidatorNotFound { known_validators }; + blueprint.with_validator(title, purpose, when_too_many, when_missing, |validator| { + let n = validator.parameters.len(); + if n > 0 { + Err(blueprint::error::Error::ParameterizedValidator { n }.into()) + } else { + Ok(validator + .program + .address(Network::Testnet, delegation_part.to_owned())) } - } + }) + } - // Print the address - match program { - Some(Ok(program)) => Ok(program.address(Network::Testnet, delegation_part)), - Some(Err(e)) => Err(e), - None => Err(Error::NoValidatorNotFound { - known_validators: blueprint - .validators - .iter() - .map(|v| (v.title.clone(), v.purpose.clone())) - .collect(), - }), - } + pub fn apply_parameter( + &self, + title: Option<&String>, + purpose: Option<&validator::Purpose>, + param: &Term, + ) -> Result, Error> { + // Read blueprint + let blueprint = File::open(self.blueprint_path()) + .map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?; + let mut blueprint: Blueprint = + serde_json::from_reader(BufReader::new(blueprint))?; + + // Apply parameters + let when_too_many = + |known_validators| Error::MoreThanOneValidatorFound { known_validators }; + let when_missing = |known_validators| Error::NoValidatorNotFound { known_validators }; + let applied_validator = + blueprint.with_validator(title, purpose, when_too_many, when_missing, |validator| { + validator.apply(param).map_err(|e| e.into()) + })?; + + // Overwrite validator + blueprint.validators = blueprint + .validators + .into_iter() + .map(|validator| { + let same_title = validator.title == applied_validator.title; + let same_purpose = validator.purpose == applied_validator.purpose; + if same_title && same_purpose { + applied_validator.to_owned() + } else { + validator + } + }) + .collect(); + + Ok(blueprint) } fn compile_deps(&mut self) -> Result<(), Error> { diff --git a/crates/aiken/src/cmd/address.rs b/crates/aiken/src/cmd/blueprint/address.rs similarity index 100% rename from crates/aiken/src/cmd/address.rs rename to crates/aiken/src/cmd/blueprint/address.rs diff --git a/crates/aiken/src/cmd/blueprint/apply.rs b/crates/aiken/src/cmd/blueprint/apply.rs new file mode 100644 index 00000000..9fb70586 --- /dev/null +++ b/crates/aiken/src/cmd/blueprint/apply.rs @@ -0,0 +1,59 @@ +use crate::with_project; +use aiken_lang::VALIDATOR_NAMES; +use aiken_project::error::Error; +use miette::IntoDiagnostic; +use std::{fs, path::PathBuf}; +use uplc::{ + ast::{DeBruijn, Term}, + parser, +}; + +#[derive(clap::Args)] +#[clap(setting(clap::AppSettings::DeriveDisplayOrder))] +/// Apply a parameter to a parameterized validator. +pub struct Args { + /// Path to project + directory: Option, + + /// Name of the validator's module within the project. Optional if there's only one validator. + #[clap(short, long)] + validator: Option, + + /// Purpose of the validator within the module. Optional if there's only one validator. + #[clap(short, long, possible_values=&VALIDATOR_NAMES)] + purpose: Option, + + /// The parameter, using high-level UPLC-syntax + parameter: String, +} + +pub fn exec( + Args { + directory, + validator, + purpose, + parameter, + }: Args, +) -> miette::Result<()> { + let term: Term = parser::term(¶meter) + .into_diagnostic()? + .try_into() + .into_diagnostic()?; + + with_project(directory, |p| { + let blueprint = p.apply_parameter( + validator.as_ref(), + purpose + .as_ref() + .map(|p| p.clone().try_into().unwrap()) + .as_ref(), + &term, + )?; + + let json = serde_json::to_string_pretty(&blueprint).unwrap(); + fs::write(p.blueprint_path(), json).map_err(|error| Error::FileIo { + error, + path: p.blueprint_path(), + }) + }) +} diff --git a/crates/aiken/src/cmd/blueprint/mod.rs b/crates/aiken/src/cmd/blueprint/mod.rs new file mode 100644 index 00000000..0dde4d8a --- /dev/null +++ b/crates/aiken/src/cmd/blueprint/mod.rs @@ -0,0 +1,19 @@ +pub mod address; +pub mod apply; + +use clap::Subcommand; + +/// Commands for working with Plutus blueprints +#[derive(Subcommand)] +#[clap(setting(clap::AppSettings::DeriveDisplayOrder))] +pub enum Cmd { + Address(address::Args), + Apply(apply::Args), +} + +pub fn exec(cmd: Cmd) -> miette::Result<()> { + match cmd { + Cmd::Address(args) => address::exec(args), + Cmd::Apply(args) => apply::exec(args), + } +} diff --git a/crates/aiken/src/cmd/mod.rs b/crates/aiken/src/cmd/mod.rs index 1f69dae9..bfef865e 100644 --- a/crates/aiken/src/cmd/mod.rs +++ b/crates/aiken/src/cmd/mod.rs @@ -1,4 +1,4 @@ -pub mod address; +pub mod blueprint; pub mod build; pub mod check; pub mod docs; diff --git a/crates/aiken/src/main.rs b/crates/aiken/src/main.rs index 597f134a..f3bc0fc4 100644 --- a/crates/aiken/src/main.rs +++ b/crates/aiken/src/main.rs @@ -1,5 +1,6 @@ use aiken::cmd::{ - address, build, check, docs, fmt, lsp, new, + blueprint::{self, address}, + build, check, docs, fmt, lsp, new, packages::{self, add}, tx, uplc, }; @@ -19,6 +20,9 @@ pub enum Cmd { Docs(docs::Args), Add(add::Args), + #[clap(subcommand)] + Blueprint(blueprint::Cmd), + #[clap(subcommand)] Packages(packages::Cmd), @@ -48,6 +52,7 @@ fn main() -> miette::Result<()> { Cmd::Check(args) => check::exec(args), Cmd::Docs(args) => docs::exec(args), Cmd::Add(args) => add::exec(args), + Cmd::Blueprint(args) => blueprint::exec(args), Cmd::Packages(args) => packages::exec(args), Cmd::Lsp(args) => lsp::exec(args), Cmd::Tx(sub_cmd) => tx::exec(sub_cmd),