diff --git a/Cargo.lock b/Cargo.lock index 5ea5fdb6..a80c4da9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,6 +66,7 @@ dependencies = [ "pallas-primitives", "pallas-traverse", "regex", + "serde_json", "thiserror", "uplc", ] diff --git a/crates/aiken-project/src/blueprint/error.rs b/crates/aiken-project/src/blueprint/error.rs index d7a612d9..f9acdb29 100644 --- a/crates/aiken-project/src/blueprint/error.rs +++ b/crates/aiken-project/src/blueprint/error.rs @@ -27,7 +27,7 @@ pub enum Error { source_code: NamedSource, return_type: Arc, }, - #[error("A {} validator requires at least {at_least} arguments", name.purple().bold())] + #[error("A {} validator requires at least {} arguments.", name.purple().bold(), at_least.to_string().purple().bold())] #[diagnostic(code("aiken::blueprint::invalid::arity"))] WrongValidatorArity { name: String, @@ -47,6 +47,11 @@ pub enum Error { #[source_code] source_code: NamedSource, }, + + #[error("Invalid or missing project's blueprint file.")] + #[diagnostic(code("aiken::blueprint::missing"))] + #[diagnostic(help("Did you forget to {build} the project?", build = "build".purple().bold()))] + InvalidOrMissingFile, } pub fn assert_return_bool(module: &CheckedModule, def: &TypedFunction) -> Result<(), Error> { diff --git a/crates/aiken-project/src/error.rs b/crates/aiken-project/src/error.rs index 94678712..9def30f8 100644 --- a/crates/aiken-project/src/error.rs +++ b/crates/aiken-project/src/error.rs @@ -1,5 +1,8 @@ use crate::{ - blueprint::error as blueprint, deps::manifest::Package, package_name::PackageName, pretty, + blueprint::{error as blueprint, validator}, + deps::manifest::Package, + package_name::PackageName, + pretty, script::EvalHint, }; use aiken_lang::{ @@ -10,6 +13,7 @@ use aiken_lang::{ use miette::{ Diagnostic, EyreContext, LabeledSpan, MietteHandlerOpts, NamedSource, RgbColors, SourceCode, }; +use owo_colors::OwoColorize; use std::{ fmt::{Debug, Display}, io, @@ -49,6 +53,9 @@ pub enum Error { #[error(transparent)] JoinError(#[from] tokio::task::JoinError), + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error("{help}")] TomlLoading { path: PathBuf, @@ -120,6 +127,21 @@ pub enum Error { package.name.repo )] UnknownPackageVersion { package: Package }, + + #[error("I couldn't parse the provided stake address.")] + MalformedStakeAddress { + error: Option, + }, + + #[error("I didn't find any validator matching your criteria.")] + NoValidatorNotFound { + known_validators: Vec<(String, validator::Purpose)>, + }, + + #[error("I found multiple suitable validators and I need you to tell me which one to pick.")] + MoreThanOneValidatorFound { + known_validators: Vec<(String, validator::Purpose)>, + }, } impl Error { @@ -203,6 +225,10 @@ impl Error { Error::ZipExtract(_) => None, Error::JoinError(_) => None, Error::UnknownPackageVersion { .. } => None, + Error::Json { .. } => None, + Error::MalformedStakeAddress { .. } => None, + Error::NoValidatorNotFound { .. } => None, + Error::MoreThanOneValidatorFound { .. } => None, } } @@ -226,6 +252,10 @@ impl Error { Error::ZipExtract(_) => None, Error::JoinError(_) => None, Error::UnknownPackageVersion { .. } => None, + Error::Json { .. } => None, + Error::MalformedStakeAddress { .. } => None, + Error::NoValidatorNotFound { .. } => None, + Error::MoreThanOneValidatorFound { .. } => None, } } } @@ -277,6 +307,10 @@ impl Diagnostic for Error { Error::ZipExtract(_) => None, Error::JoinError(_) => None, Error::UnknownPackageVersion { .. } => Some(Box::new("aiken::packages::resolve")), + Error::Json { .. } => None, + Error::MalformedStakeAddress { .. } => None, + Error::NoValidatorNotFound { .. } => None, + Error::MoreThanOneValidatorFound { .. } => None, } } @@ -287,7 +321,7 @@ impl Diagnostic for Error { first.display(), second.display() ))), - Error::FileIo { .. } => None, + Error::FileIo { error, .. } => Some(Box::new(format!("{error}"))), Error::Blueprint(e) => e.help(), Error::ImportCycle { modules } => Some(Box::new(format!( "Try moving the shared code to a separate module that the others can depend on\n- {}", @@ -334,6 +368,23 @@ impl Diagnostic for Error { Error::ZipExtract(_) => None, Error::JoinError(_) => None, Error::UnknownPackageVersion{..} => Some(Box::new("Perhaps, double-check the package repository and version?")), + Error::Json(error) => Some(Box::new(format!("{error}"))), + Error::MalformedStakeAddress { error } => Some(Box::new(format!("A stake address must be provided either as a base16-encoded string, or as a bech32-encoded string with the 'stake' or 'stake_test' prefix.{hint}", hint = match error { + Some(error) => format!("\n\nHere's the error I encountered: {error}"), + None => String::new(), + }))), + Error::NoValidatorNotFound { known_validators } => { + Some(Box::new(format!( + "Here's a list of all validators (and their purpose) I've found in your project. Please double-check this list against the options that you've provided:\n\n{}", + known_validators.iter().map(|(name, purpose)| format!("→ {name} (purpose = {purpose})", name = name.purple().bold(), purpose = purpose.bright_blue())).collect::>().join("\n") + ))) + }, + Error::MoreThanOneValidatorFound { known_validators } => { + Some(Box::new(format!( + "Here's a list of all validators (and their purpose) I've found in your project. Select one of them using the appropriate options:\n\n{}", + known_validators.iter().map(|(name, purpose)| format!("→ {name} (purpose = {purpose})", name = name.purple().bold(), purpose = purpose.bright_blue())).collect::>().join("\n") + ))) + }, } } @@ -369,6 +420,10 @@ impl Diagnostic for Error { Error::ZipExtract(_) => None, Error::JoinError(_) => None, Error::UnknownPackageVersion { .. } => None, + Error::Json { .. } => None, + Error::MalformedStakeAddress { .. } => None, + Error::NoValidatorNotFound { .. } => None, + Error::MoreThanOneValidatorFound { .. } => None, } } @@ -392,6 +447,10 @@ impl Diagnostic for Error { Error::ZipExtract(_) => None, Error::JoinError(_) => None, Error::UnknownPackageVersion { .. } => None, + Error::Json { .. } => None, + Error::MalformedStakeAddress { .. } => None, + Error::NoValidatorNotFound { .. } => None, + Error::MoreThanOneValidatorFound { .. } => None, } } @@ -415,6 +474,10 @@ impl Diagnostic for Error { Error::ZipExtract { .. } => None, Error::JoinError { .. } => None, Error::UnknownPackageVersion { .. } => None, + Error::Json { .. } => None, + Error::MalformedStakeAddress { .. } => None, + Error::NoValidatorNotFound { .. } => None, + Error::MoreThanOneValidatorFound { .. } => None, } } @@ -438,6 +501,10 @@ impl Diagnostic for Error { Error::ZipExtract { .. } => None, Error::JoinError { .. } => None, Error::UnknownPackageVersion { .. } => None, + Error::Json { .. } => None, + Error::MalformedStakeAddress { .. } => None, + Error::NoValidatorNotFound { .. } => None, + Error::MoreThanOneValidatorFound { .. } => None, } } } diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index 10b0fc47..00ecc293 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -12,7 +12,7 @@ pub mod pretty; pub mod script; pub mod telemetry; -use crate::blueprint::Blueprint; +use crate::blueprint::{schema::Schema, validator, Blueprint}; use aiken_lang::{ ast::{Definition, Function, ModuleKind, TypedDataType, TypedFunction}, builder::{DataTypeKey, FunctionAccessKey}, @@ -25,10 +25,14 @@ use indexmap::IndexMap; use miette::NamedSource; use options::{CodeGenMode, Options}; use package_name::PackageName; +use pallas::ledger::addresses::{ + Address, Network, ShelleyAddress, ShelleyDelegationPart, StakePayload, +}; use script::{EvalHint, EvalInfo, Script}; use std::{ collections::HashMap, - fs, + fs::{self, File}, + io::BufReader, path::{Path, PathBuf}, }; use telemetry::EventListener; @@ -184,6 +188,10 @@ where Ok(()) } + pub fn blueprint_path(&self) -> PathBuf { + self.root.join("plutus.json") + } + pub fn compile(&mut self, options: Options) -> Result<(), Error> { self.compile_deps()?; @@ -202,10 +210,9 @@ where match options.code_gen_mode { CodeGenMode::Build(uplc_dump) => { - let blueprint_path = self.root.join("plutus.json"); self.event_listener .handle_event(Event::GeneratingBlueprint { - path: blueprint_path.clone(), + path: self.blueprint_path(), }); let mut generator = self.checked_modules.new_generator( @@ -226,9 +233,9 @@ where } let json = serde_json::to_string_pretty(&blueprint).unwrap(); - fs::write(&blueprint_path, json).map_err(|error| Error::FileIo { + fs::write(self.blueprint_path(), json).map_err(|error| Error::FileIo { error, - path: blueprint_path, + path: self.blueprint_path(), }) } CodeGenMode::Test { @@ -274,6 +281,71 @@ where } } + pub fn address( + &self, + with_title: Option<&String>, + with_purpose: Option<&validator::Purpose>, + stake_address: Option<&String>, + ) -> Result { + // Parse stake address + let stake_address = stake_address + .map(|s| { + Address::from_hex(s) + .or_else(|_| Address::from_bech32(s)) + .map_err(|error| Error::MalformedStakeAddress { error: Some(error) }) + .and_then(|addr| match addr { + Address::Stake(addr) => Ok(addr), + _ => Err(Error::MalformedStakeAddress { error: None }), + }) + }) + .transpose()?; + let delegation_part = match stake_address.map(|addr| addr.payload().to_owned()) { + None => ShelleyDelegationPart::Null, + Some(StakePayload::Stake(key)) => ShelleyDelegationPart::Key(key), + Some(StakePayload::Script(script)) => ShelleyDelegationPart::Script(script), + }; + + // Read blueprint + let filepath = self.blueprint_path(); + let blueprint = + File::open(filepath).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(), + }) + }) + } + } + + // 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(), + }), + } + } + fn compile_deps(&mut self) -> Result<(), Error> { let manifest = deps::download( &self.event_listener, diff --git a/crates/aiken/Cargo.toml b/crates/aiken/Cargo.toml index 921732bf..c925a682 100644 --- a/crates/aiken/Cargo.toml +++ b/crates/aiken/Cargo.toml @@ -28,3 +28,4 @@ aiken-lang = { path = "../aiken-lang", version = "0.0.28" } aiken-lsp = { path = "../aiken-lsp", version = "0.0.28" } aiken-project = { path = '../aiken-project', version = "0.0.28" } uplc = { path = '../uplc', version = "0.0.28" } +serde_json = "1.0.91" diff --git a/crates/aiken/src/cmd/address.rs b/crates/aiken/src/cmd/address.rs new file mode 100644 index 00000000..467e9c7b --- /dev/null +++ b/crates/aiken/src/cmd/address.rs @@ -0,0 +1,53 @@ +use crate::with_project; +use aiken_lang::VALIDATOR_NAMES; +use std::path::PathBuf; + +#[derive(clap::Args)] +#[clap(setting(clap::AppSettings::DeriveDisplayOrder))] +/// Compute a validator's address. +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, + + /// Stake address to attach, if any. + #[clap(long)] + delegated_to: Option, + + /// Force the project to be rebuilt, otherwise relies on existing artifacts (i.e. plutus.json). + #[clap(long)] + rebuild: bool, +} + +pub fn exec( + Args { + directory, + validator, + purpose, + delegated_to, + rebuild, + }: Args, +) -> miette::Result<()> { + with_project(directory, |p| { + if rebuild { + p.build(false)?; + } + let address = p.address( + validator.as_ref(), + purpose + .as_ref() + .map(|p| p.clone().try_into().unwrap()) + .as_ref(), + delegated_to.as_ref(), + )?; + println!("{}", address.to_bech32().unwrap()); + Ok(()) + }) +} diff --git a/crates/aiken/src/cmd/mod.rs b/crates/aiken/src/cmd/mod.rs index 49240999..1f69dae9 100644 --- a/crates/aiken/src/cmd/mod.rs +++ b/crates/aiken/src/cmd/mod.rs @@ -1,3 +1,4 @@ +pub mod address; pub mod build; pub mod check; pub mod docs; diff --git a/crates/aiken/src/main.rs b/crates/aiken/src/main.rs index f66b8825..597f134a 100644 --- a/crates/aiken/src/main.rs +++ b/crates/aiken/src/main.rs @@ -1,5 +1,5 @@ use aiken::cmd::{ - build, check, docs, fmt, lsp, new, + address, build, check, docs, fmt, lsp, new, packages::{self, add}, tx, uplc, }; @@ -14,6 +14,7 @@ pub enum Cmd { New(new::Args), Fmt(fmt::Args), Build(build::Args), + Address(address::Args), Check(check::Args), Docs(docs::Args), Add(add::Args), @@ -43,6 +44,7 @@ fn main() -> miette::Result<()> { Cmd::New(args) => new::exec(args), Cmd::Fmt(args) => fmt::exec(args), Cmd::Build(args) => build::exec(args), + Cmd::Address(args) => address::exec(args), Cmd::Check(args) => check::exec(args), Cmd::Docs(args) => docs::exec(args), Cmd::Add(args) => add::exec(args),