From daee8e39d69ff10fa7543d071834292df257276e Mon Sep 17 00:00:00 2001 From: KtorZ Date: Tue, 31 Jan 2023 15:30:52 +0100 Subject: [PATCH] Implement new command: address This calculates a validator's address from validators found in a blueprint. It also provides a convenient way to attach a delegation part to the validator if needs be. The command is meant to provide a nice user experience and works 'out of the box' for projects that have only a single validator. Just call 'aiken address' to get the validator's address. Note that the command-line doesn't provide any option to configure the target network. This automatically assumes testnet, and will until we deem the project ready for mainnet. Those brave enough to run an Aiken's program on mainnet will find a way anyway. --- Cargo.lock | 1 + crates/aiken-project/src/blueprint/error.rs | 7 +- crates/aiken-project/src/error.rs | 71 ++++++++++++++++- crates/aiken-project/src/lib.rs | 84 +++++++++++++++++++-- crates/aiken/Cargo.toml | 1 + crates/aiken/src/cmd/address.rs | 53 +++++++++++++ crates/aiken/src/cmd/mod.rs | 1 + crates/aiken/src/main.rs | 4 +- 8 files changed, 212 insertions(+), 10 deletions(-) create mode 100644 crates/aiken/src/cmd/address.rs 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),