diff --git a/crates/aiken/Cargo.toml b/crates/aiken/Cargo.toml index f6fc64f8..6d438c91 100644 --- a/crates/aiken/Cargo.toml +++ b/crates/aiken/Cargo.toml @@ -32,6 +32,9 @@ aiken-lsp = { path = "../aiken-lsp", version = "1.0.14-alpha" } aiken-project = { path = '../aiken-project', version = "1.0.14-alpha" } uplc = { path = '../uplc', version = "1.0.14-alpha" } clap_complete = "4.3.2" +inquire = "0.6.2" +num-bigint = "0.4.3" +ordinal = "0.3.2" [build-dependencies] built = { version = "0.6.0", features = ["git2"] } diff --git a/crates/aiken/src/cmd/blueprint/apply.rs b/crates/aiken/src/cmd/blueprint/apply.rs index 2823f23e..56881538 100644 --- a/crates/aiken/src/cmd/blueprint/apply.rs +++ b/crates/aiken/src/cmd/blueprint/apply.rs @@ -1,8 +1,21 @@ use crate::with_project; -use aiken_project::{blueprint, error::Error}; +use aiken_project::{ + blueprint::{ + self, + definitions::Definitions, + schema::{Annotated, Constructor, Data, Declaration, Items, Schema}, + }, + error::Error, + pretty::multiline, +}; +use inquire; +use num_bigint::BigInt; +use ordinal::Ordinal; use owo_colors::{OwoColorize, Stream::Stderr}; +use pallas_primitives::alonzo::PlutusData; +use std::str::FromStr; use std::{fs, path::PathBuf, process, rc::Rc}; -use uplc::ast::{Constant, DeBruijn, Term}; +use uplc::ast::{Constant, Data as UplcData, DeBruijn, Term}; /// Apply a parameter to a parameterized validator. #[derive(clap::Args)] @@ -12,10 +25,7 @@ pub struct Args { /// For example, `182A` designates an integer of value 42. If you're unsure about the shape of /// the parameter, look at the schema specified in the project's blueprint (i.e. /// `plutus.json`), or use the `cbor.serialise` function from the Aiken standard library. - parameter: String, - - /// Path to project - directory: Option, + parameter: Option, /// Output file. Optional, print on stdout when omitted. #[clap(short, long)] @@ -33,55 +43,12 @@ pub struct Args { pub fn exec( Args { parameter, - directory, out, module, validator, }: Args, ) -> miette::Result<()> { - eprintln!( - "{} inputs", - " Parsing" - .if_supports_color(Stderr, |s| s.purple()) - .if_supports_color(Stderr, |s| s.bold()), - ); - - let bytes = hex::decode(parameter) - .map_err::(|e| { - blueprint::error::Error::MalformedParameter { - hint: format!("Invalid hex-encoded string: {e}"), - } - .into() - }) - .unwrap_or_else(|e| { - println!(); - e.report(); - process::exit(1) - }); - - let data = uplc::plutus_data(&bytes) - .map_err::(|e| { - blueprint::error::Error::MalformedParameter { - hint: format!("Invalid Plutus data; malformed CBOR encoding: {e}"), - } - .into() - }) - .unwrap_or_else(|e| { - println!(); - e.report(); - process::exit(1) - }); - - let term: Term = Term::Constant(Rc::new(Constant::Data(data))); - - eprintln!( - "{} blueprint", - " Analyzing" - .if_supports_color(Stderr, |s| s.purple()) - .if_supports_color(Stderr, |s| s.bold()), - ); - - with_project(directory, |p| { + with_project(None, |p| { let title = module.as_ref().map(|m| { format!( "{m}{}", @@ -95,10 +62,65 @@ pub fn exec( let title = title.as_ref().or(validator.as_ref()); eprintln!( - "{} parameter", + "{} blueprint", + " Analyzing" + .if_supports_color(Stderr, |s| s.purple()) + .if_supports_color(Stderr, |s| s.bold()), + ); + + let term: Term = match ¶meter { + Some(param) => { + eprintln!( + "{} inputs", + " Parsing" + .if_supports_color(Stderr, |s| s.purple()) + .if_supports_color(Stderr, |s| s.bold()), + ); + + let bytes = hex::decode(param) + .map_err::(|e| { + blueprint::error::Error::MalformedParameter { + hint: format!("Invalid hex-encoded string: {e}"), + } + .into() + }) + .unwrap_or_else(|e| { + println!(); + e.report(); + process::exit(1) + }); + + let data = uplc::plutus_data(&bytes) + .map_err::(|e| { + blueprint::error::Error::MalformedParameter { + hint: format!("Invalid Plutus data; malformed CBOR encoding: {e}"), + } + .into() + }) + .unwrap_or_else(|e| { + println!(); + e.report(); + process::exit(1) + }); + + Term::Constant(Rc::new(Constant::Data(data))) + } + + None => p.construct_parameter_incrementally(title, ask_schema)?, + }; + + eprintln!( + "{} {}", " Applying" .if_supports_color(Stderr, |s| s.purple()) .if_supports_color(Stderr, |s| s.bold()), + match TryInto::::try_into(term.clone()) { + Ok(data) => { + let padding = "\n "; + multiline(48, UplcData::to_hex(data)).join(padding) + } + Err(_) => term.to_pretty(), + } ); let blueprint = p.apply_parameter(title, &term)?; @@ -126,3 +148,262 @@ pub fn exec( Ok(()) }) } + +fn ask_schema( + schema: &Annotated, + definitions: &Definitions>, +) -> Result { + match schema.annotated { + Schema::Data(Data::Integer) => { + let input = prompt_primitive("an integer", schema)?; + + let n = BigInt::from_str(input.as_str()).map_err(|e| { + blueprint::error::Error::MalformedParameter { + hint: format!("Unable to convert input to integer: {e}"), + } + })?; + + Ok(UplcData::integer(n)) + } + + Schema::Data(Data::Bytes) => { + let input = prompt_primitive("a byte-array", schema)?; + + let bytes = + hex::decode(input).map_err(|e| blueprint::error::Error::MalformedParameter { + hint: format!("Invalid hex-encoded string: {e}"), + })?; + + Ok(UplcData::bytestring(bytes)) + } + + Schema::Data(Data::List(Items::Many(ref decls))) => { + eprintln!(" {}", asking(schema, "Found", &format!("a {}-tuple", decls.len()))); + + let mut elems = vec![]; + + for (ix, decl) in decls.iter().enumerate() { + eprintln!( + " {} Tuple's {}{} element", + "Asking".if_supports_color(Stderr, |s| s.purple()).if_supports_color(Stderr, |s| s.bold()), + ix+1, + Ordinal::(ix+1).suffix() + ); + let inner_schema = lookup_declaration(&decl.clone().into(), definitions); + elems.push(ask_schema(&inner_schema, definitions)?); + } + + Ok(UplcData::list(elems)) + } + + Schema::Data(Data::List(Items::One(ref decl))) => { + eprintln!(" {}", asking(schema, "Found", "a list")); + + let inner_schema = lookup_declaration(&decl.clone().into(), definitions); + + let mut elems = vec![]; + while prompt_iterable(schema, "item")? { + elems.push(ask_schema(&inner_schema, definitions)?); + } + + Ok(UplcData::list(elems)) + } + + Schema::Data(Data::Map(ref key_decl, ref value_decl)) => { + eprintln!(" {}", asking(schema, "Found", "an associative map")); + + let key_schema = lookup_declaration(&key_decl.clone().into(), definitions); + let value_schema = lookup_declaration(&value_decl.clone().into(), definitions); + + let mut elems = vec![]; + while prompt_iterable(schema, "key/value entry")? { + elems.push(( + ask_schema(&key_schema, definitions)?, + ask_schema(&value_schema, definitions)?, + )); + } + + Ok(UplcData::map(elems)) + } + + Schema::Data(Data::AnyOf(ref constructors)) => { + eprintln!( + " {}", + asking( + schema, + "Found", + if constructors.len() == 1 { + "a record" + } else { + "a data-type" + } + ) + ); + + let ix = prompt_constructor(constructors, schema)?; + + let mut fields = Vec::new(); + for field in &constructors[ix].annotated.fields { + let inner_schema = lookup_declaration(field, definitions); + fields.push(ask_schema(&inner_schema, definitions)?); + } + + Ok(UplcData::constr(ix.try_into().unwrap(), fields)) + } + + _ => unimplemented!("Hey! You've found a case that we haven't implemented yet. Yes, we've been a bit lazy on that one... If that use-case is important to you, please let us know on Discord or on Github."), + } +} + +fn lookup_declaration( + decl: &Annotated>, + definitions: &Definitions>, +) -> Annotated { + match decl.annotated { + Declaration::Inline(ref data) => Annotated { + title: decl.title.clone(), + description: decl.description.clone(), + annotated: Schema::Data(*(*data).clone()), + }, + Declaration::Referenced(ref reference) => { + let schema = definitions + .lookup(reference) + .expect("reference to unknown type in blueprint?"); + Annotated { + title: decl.title.clone().or_else(|| schema.title.clone()), + description: decl + .description + .clone() + .or_else(|| schema.description.clone()), + annotated: schema.annotated.clone(), + } + } + } +} + +fn asking(schema: &Annotated, verb: &str, type_name: &str) -> String { + let subject = get_subject(schema, type_name); + format!( + "{} {subject}", + verb.if_supports_color(Stderr, |s| s.purple()) + .if_supports_color(Stderr, |s| s.bold()), + subject = subject, + ) +} + +fn prompt_primitive( + type_name: &str, + schema: &Annotated, +) -> Result { + inquire::Text::new(&format!(" {}:", asking(schema, "Asking", type_name))) + .with_description(schema.description.as_ref()) + .prompt() + .map_err(|e| blueprint::error::Error::MalformedParameter { + hint: format!("Invalid input received from prompt: {e}"), + }) +} + +fn prompt_iterable( + schema: &Annotated, + elem_name: &str, +) -> Result { + inquire::Confirm::new(&format!( + " {} one more {elem_name}?", + "Adding" + .if_supports_color(Stderr, |s| s.purple()) + .if_supports_color(Stderr, |s| s.bold()) + )) + .with_description(schema.description.as_ref()) + .with_default(true) + .prompt() + .map_err(|e| blueprint::error::Error::MalformedParameter { + hint: format!("Invalid input received from prompt: {e}"), + }) +} + +fn prompt_constructor( + constructors: &[Annotated], + schema: &Annotated, +) -> Result { + let mut choices = Vec::new(); + for c in constructors { + let name = c + .title + .as_ref() + .cloned() + .unwrap_or_else(|| format!("{}", c.annotated.index)); + choices.push(name); + } + + let mut choice = choices + .first() + .expect("Data-type with no constructor?") + .to_string(); + + if choices.len() > 1 { + choice = inquire::Select::new( + &format!( + " {} constructor", + "Selecting" + .if_supports_color(Stderr, |s| s.purple()) + .if_supports_color(Stderr, |s| s.bold()) + ), + choices.clone(), + ) + .with_description(schema.description.as_ref()) + .prompt() + .map_err(|e| blueprint::error::Error::MalformedParameter { + hint: format!("Invalid input received from prompt: {e}"), + })?; + } + + Ok(choices.into_iter().position(|c| c == choice).unwrap()) +} + +fn get_subject(schema: &Annotated, type_name: &str) -> String { + schema + .title + .as_ref() + .map(|title| format!("{title} ({type_name})")) + .unwrap_or_else(|| type_name.to_string()) +} + +trait WithDescription<'a> { + fn with_description(self, opt: Option<&'a String>) -> Self; +} + +impl<'a> WithDescription<'a> for inquire::Confirm<'a> { + fn with_description( + self: inquire::Confirm<'a>, + opt: Option<&'a String>, + ) -> inquire::Confirm<'a> { + match opt { + Some(description) => self.with_help_message(description), + None => self, + } + } +} + +impl<'a> WithDescription<'a> for inquire::Text<'a> { + fn with_description(self: inquire::Text<'a>, opt: Option<&'a String>) -> inquire::Text<'a> { + match opt { + Some(description) => self.with_help_message(description), + None => self, + } + } +} + +impl<'a, T> WithDescription<'a> for inquire::Select<'a, T> +where + T: std::fmt::Display, +{ + fn with_description( + self: inquire::Select<'a, T>, + opt: Option<&'a String>, + ) -> inquire::Select<'a, T> { + match opt { + Some(description) => self.with_help_message(description), + None => self, + } + } +}