This commit is contained in:
evan-schott 2024-05-06 14:46:13 -07:00
parent 764f116afd
commit 46774633f6
15 changed files with 582 additions and 4 deletions

1
Cargo.lock generated
View File

@ -1972,6 +1972,7 @@ dependencies = [
"toml 0.8.12",
"tracing",
"tracing-subscriber",
"ureq",
"walkdir",
"zip",
]

View File

@ -64,6 +64,9 @@ default = [ ]
ci_skip = [ "leo-compiler/ci_skip" ]
noconfig = [ ]
[dependencies]
ureq = "2.9.7"
[dependencies.leo-ast]
path = "./compiler/ast"
version = "=1.11.0"

View File

@ -383,4 +383,18 @@ create_messages!(
msg: format!("The dependency program `{name}` was not found among the manifest's dependencies."),
help: None,
}
@backtraced
conflicting_on_chain_program_name {
args: (first: impl Display, second: impl Display),
msg: format!("Conflicting program names given to execute on chain: `{first}` and `{second}`."),
help: Some("Either set `--local` to execute the local program on chain, or set `--program <PROGRAM>`.".to_string()),
}
@backtraced
missing_on_chain_program_name {
args: (),
msg: "The name of the program to execute on-chain is missing.".to_string(),
help: Some("Either set `--local` to execute the local program on chain, or set `--program <PROGRAM>`.".to_string()),
}
);

View File

@ -140,8 +140,8 @@ create_messages!(
@formatted
failed_to_retrieve_from_endpoint {
args: (endpoint: impl Display, error: impl ErrorArg),
msg: format!("Failed to retrieve from endpoint `{endpoint}`. Error: {error}"),
args: (error: impl ErrorArg),
msg: format!("{error}"),
help: None,
}
@ -151,4 +151,46 @@ create_messages!(
msg: format!("Compiled file at `{path}` does not exist, cannot compile parent."),
help: Some("If you were using the `--non-recursive` flag, remove it and try again.".to_string()),
}
@backtraced
invalid_input_id_len {
args: (input: impl Display, expected_type: impl Display),
msg: format!("Invalid input: {input}."),
help: Some(format!("Type `{expected_type}` must contain exactly 61 lowercase characters or numbers.")),
}
@backtraced
invalid_input_id {
args: (input: impl Display, expected_type: impl Display, expected_preface: impl Display),
msg: format!("Invalid input: {input}."),
help: Some(format!("Type `{expected_type}` must start with \"{expected_preface}\".")),
}
@backtraced
invalid_numerical_input {
args: (input: impl Display),
msg: format!("Invalid numerical input: {input}."),
help: Some("Input must be a valid u32.".to_string()),
}
@backtraced
invalid_range {
args: (),
msg: "The range must be less than or equal to 50 blocks.".to_string(),
help: None,
}
@backtraced
invalid_height_or_hash {
args: (input: impl Display),
msg: format!("Invalid input: {input}."),
help: Some("Input must be a valid height or hash. Valid hashes are 61 characters long, composed of only numbers and lower case letters, and be prefaced with \"ab1\".".to_string()),
}
@backtraced
invalid_field {
args: (field: impl Display),
msg: format!("Invalid field: {field}."),
help: Some("Field element must be numerical string with optional \"field\" suffix.".to_string()),
}
);

View File

@ -36,7 +36,7 @@ pub struct CLI {
#[clap(long, global = true, help = "Path to Leo program root folder")]
path: Option<PathBuf>,
#[clap(long, global = true, help = "Path to aleo program registry.")]
#[clap(long, global = true, help = "Path to aleo program registry")]
pub home: Option<PathBuf>,
}
@ -73,6 +73,11 @@ enum Commands {
#[clap(flatten)]
command: Deploy,
},
#[clap(about = "Query live data from the Aleo network")]
Query {
#[clap(flatten)]
command: Query,
},
#[clap(about = "Compile the current package as a program")]
Build {
#[clap(flatten)]
@ -144,6 +149,7 @@ pub fn run_with_args(cli: CLI) -> Result<()> {
command.try_execute(context)
}
Commands::Query { command } => command.try_execute(context),
Commands::Clean { command } => command.try_execute(context),
Commands::Deploy { command } => command.try_execute(context),
Commands::Example { command } => command.try_execute(context),

View File

@ -35,6 +35,9 @@ pub use example::Example;
pub mod execute;
pub use execute::Execute;
pub mod query;
pub use query::Query;
pub mod new;
pub use new::New;

102
leo/cli/commands/query.rs Normal file
View File

@ -0,0 +1,102 @@
// Copyright (C) 2019-2023 Aleo Systems Inc.
// This file is part of the Leo library.
// The Leo library is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// The Leo library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
use super::*;
use leo_errors::UtilError;
/// Query live data from the Aleo network.
#[derive(Parser, Debug)]
pub struct Query {
#[clap(
short,
long,
global = true,
help = "Endpoint to retrieve network state from. Defaults to http://api.explorer.aleo.org/v1.",
default_value = "http://api.explorer.aleo.org/v1"
)]
pub endpoint: String,
#[clap(short, long, global = true, help = "Network to use. Defaults to testnet3.", default_value = "testnet3")]
pub(crate) network: String,
#[clap(subcommand)]
command: QueryCommands,
}
impl Command for Query {
type Input = ();
type Output = ();
fn log_span(&self) -> Span {
tracing::span!(tracing::Level::INFO, "Leo")
}
fn prelude(&self, _context: Context) -> Result<Self::Input> {
Ok(())
}
fn apply(self, context: Context, _: Self::Input) -> Result<Self::Output> {
let output = match self.command {
QueryCommands::Block { command } => command.apply(context, ())?,
QueryCommands::Transaction { command } => command.apply(context, ())?,
QueryCommands::Program { command } => command.apply(context, ())?,
QueryCommands::Stateroot { command } => command.apply(context, ())?,
QueryCommands::Committee { command } => command.apply(context, ())?,
};
// Make GET request to retrieve on-chain state.
let url = format!("{}/{}/{}", self.endpoint, self.network, output);
let response = ureq::get(&url.clone())
.call()
.map_err(|err| UtilError::failed_to_retrieve_from_endpoint(err, Default::default()))?;
if response.status() == 200 {
tracing::info!("✅ Successfully retrieved data from '{url}'.");
// Unescape the newlines.
println!("{}", response.into_string().unwrap().replace("\\n", "\n"));
Ok(())
} else {
Err(UtilError::network_error(url, response.status(), Default::default()).into())
}
}
}
#[derive(Parser, Debug)]
enum QueryCommands {
#[clap(about = "Query block information")]
Block {
#[clap(flatten)]
command: Block,
},
#[clap(about = "Query transaction information")]
Transaction {
#[clap(flatten)]
command: Transaction,
},
#[clap(about = "Query program source code and live mapping values")]
Program {
#[clap(flatten)]
command: Program,
},
#[clap(about = "Query the latest stateroot")]
Stateroot {
#[clap(flatten)]
command: StateRoot,
},
#[clap(about = "Query the current committee")]
Committee {
#[clap(flatten)]
command: Committee,
},
}

View File

@ -23,6 +23,9 @@ pub use commands::*;
mod helpers;
pub use helpers::*;
mod query_commands;
pub use query_commands::*;
pub(crate) type CurrentNetwork = snarkvm::prelude::MainnetV0;
pub(crate) const SNARKVM_COMMAND: &str = "snarkvm";

View File

@ -0,0 +1,93 @@
// Copyright (C) 2019-2023 Aleo Systems Inc.
// This file is part of the Leo library.
// The Leo library is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// The Leo library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
use super::*;
use crate::cli::context::Context;
use clap::Parser;
// Query on-chain information related to blocks.
#[derive(Parser, Debug)]
pub struct Block {
#[clap(help = "Fetch a block by specifying its height or hash", required_unless_present_any = &["latest", "latest_hash", "latest_height", "range"])]
pub(crate) id: Option<String>,
#[arg(short, long, help = "Get the latest block", default_value = "false", conflicts_with_all(["latest_hash", "latest_height", "range", "transactions", "to_height"]))]
pub(crate) latest: bool,
#[arg(short, long, help = "Get the latest block hash", default_value = "false", conflicts_with_all(["latest", "latest_height", "range", "transactions", "to_height"]))]
pub(crate) latest_hash: bool,
#[arg(short, long, help = "Get the latest block height", default_value = "false", conflicts_with_all(["latest", "latest_hash", "range", "transactions", "to_height"]))]
pub(crate) latest_height: bool,
#[arg(short, long, help = "Get up to 50 consecutive blocks", number_of_values = 2, value_names = &["START_HEIGHT", "END_HEIGHT"], conflicts_with_all(["latest", "latest_hash", "latest_height", "transactions", "to_height"]))]
pub(crate) range: Option<Vec<String>>,
#[arg(
short,
long,
help = "Get all transactions at the specified block height",
conflicts_with("to_height"),
default_value = "false"
)]
pub(crate) transactions: bool,
#[arg(short, long, help = "Lookup the block height corresponding to a hash value", default_value = "false")]
pub(crate) to_height: bool,
}
impl Command for Block {
type Input = ();
type Output = String;
fn log_span(&self) -> Span {
tracing::span!(tracing::Level::INFO, "Leo")
}
fn prelude(&self, _context: Context) -> Result<Self::Input> {
Ok(())
}
fn apply(self, _context: Context, _input: Self::Input) -> Result<Self::Output> {
// Build custom url to fetch from based on the flags and user's input.
let url = if self.latest_height {
"block/height/latest".to_string()
} else if self.latest_hash {
"block/hash/latest".to_string()
} else if self.latest {
"block/latest".to_string()
} else if let Some(range) = self.range {
// Make sure the range is composed of valid numbers.
is_valid_numerical_input(&range[0])?;
is_valid_numerical_input(&range[1])?;
// Make sure the range is not too large.
if range[1].parse::<u32>().unwrap() - range[0].parse::<u32>().unwrap() > 50 {
return Err(UtilError::invalid_range().into());
}
format!("blocks?start={}&end={}", range[0], range[1])
} else if self.transactions {
is_valid_numerical_input(&self.id.clone().unwrap())?;
format!("block/{}/transactions", self.id.unwrap()).to_string()
} else if self.to_height {
let id = self.id.unwrap();
is_valid_hash(&id)?;
format!("height/{}", id).to_string()
} else if let Some(id) = self.id {
is_valid_height_or_hash(&id)?;
format!("block/{}", id)
} else {
unreachable!("All cases are covered")
};
Ok(url)
}
}

View File

@ -0,0 +1,40 @@
// Copyright (C) 2019-2023 Aleo Systems Inc.
// This file is part of the Leo library.
// The Leo library is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// The Leo library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
use super::*;
use clap::Parser;
/// Query the committee.
#[derive(Parser, Debug)]
pub struct Committee {}
impl Command for Committee {
type Input = ();
type Output = String;
fn log_span(&self) -> Span {
tracing::span!(tracing::Level::INFO, "Leo")
}
fn prelude(&self, _context: Context) -> Result<Self::Input> {
Ok(())
}
fn apply(self, _context: Context, _: Self::Input) -> Result<Self::Output> {
Ok("/committee/latest".to_string())
}
}

View File

@ -0,0 +1,99 @@
// Copyright (C) 2019-2023 Aleo Systems Inc.
// This file is part of the Leo library.
// The Leo library is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// The Leo library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
pub use super::*;
pub mod block;
pub use block::Block;
pub mod program;
pub use program::Program;
pub mod state_root;
pub use state_root::StateRoot;
pub mod committee;
pub mod transaction;
pub use committee::Committee;
pub use transaction::Transaction;
use crate::cli::helpers::context::*;
use leo_errors::{LeoError, Result, UtilError};
use tracing::span::Span;
// A valid hash is 61 characters long, with preface "ab1" and all characters lowercase or numbers.
pub fn is_valid_hash(hash: &str) -> Result<(), LeoError> {
if hash.len() != 61 {
Err(UtilError::invalid_input_id_len(hash, "hash").into())
} else if !hash.starts_with("ab1") && hash.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()) {
Err(UtilError::invalid_input_id(hash, "hash", "ab1").into())
} else {
Ok(())
}
}
// A valid transaction id is 61 characters long, with preface "at1" and all characters lowercase or numbers.
pub fn is_valid_transaction_id(transaction: &str) -> Result<(), LeoError> {
if transaction.len() != 61 {
Err(UtilError::invalid_input_id_len(transaction, "transaction").into())
} else if !transaction.starts_with("at1")
&& transaction.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
{
Err(UtilError::invalid_input_id(transaction, "transaction", "at1").into())
} else {
Ok(())
}
}
// A valid transition id is 61 characters long, with preface "au1" and all characters lowercase or numbers.
pub fn is_valid_transition_id(transition: &str) -> Result<(), LeoError> {
if transition.len() != 61 {
Err(UtilError::invalid_input_id_len(transition, "transition").into())
} else if !transition.starts_with("au1") && transition.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
{
Err(UtilError::invalid_input_id(transition, "transition", "au1").into())
} else {
Ok(())
}
}
// A valid numerical input is a u32.
pub fn is_valid_numerical_input(num: &str) -> Result<(), LeoError> {
if num.parse::<u32>().is_err() { Err(UtilError::invalid_numerical_input(num).into()) } else { Ok(()) }
}
// A valid height or hash.
pub fn is_valid_height_or_hash(input: &str) -> Result<(), LeoError> {
match (is_valid_hash(input), is_valid_numerical_input(input)) {
(Ok(_), _) | (_, Ok(_)) => Ok(()),
_ => Err(UtilError::invalid_height_or_hash(input).into()),
}
}
// Checks if the string is a valid field, allowing for optional `field` suffix.
pub fn is_valid_field(field: &str) -> Result<String, LeoError> {
let split = field.split("field").collect::<Vec<&str>>();
if split.len() == 1 && split[0].chars().all(|c| c.is_numeric()) {
Ok(format!("{}field", field))
} else if split.len() == 2 && split[0].chars().all(|c| c.is_numeric()) && split[1].is_empty() {
Ok(field.to_string())
} else {
Err(UtilError::invalid_field(field).into())
}
}

View File

@ -0,0 +1,64 @@
// Copyright (C) 2019-2023 Aleo Systems Inc.
// This file is part of the Leo library.
// The Leo library is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// The Leo library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
use super::*;
use clap::Parser;
/// Query program source code and live mapping values.
#[derive(Parser, Debug)]
pub struct Program {
#[clap(name = "NAME", help = "The name of the program to fetch")]
pub(crate) name: String,
#[arg(
short,
long,
help = "Get all mappings defined in the program",
default_value = "false",
conflicts_with = "mapping_value"
)]
pub(crate) mappings: bool,
#[arg(short, long, help = "Get the value corresponding to the specified mapping and key.", number_of_values = 2, value_names = &["MAPPING", "KEY"], conflicts_with = "mappings")]
pub(crate) mapping_value: Option<Vec<String>>,
}
impl Command for Program {
type Input = ();
type Output = String;
fn log_span(&self) -> Span {
tracing::span!(tracing::Level::INFO, "Leo")
}
fn prelude(&self, _context: Context) -> Result<Self::Input> {
Ok(())
}
fn apply(self, _context: Context, _: Self::Input) -> Result<Self::Output> {
// TODO: Validate program name.
// Build custom url to fetch from based on the flags and user's input.
let url = if let Some(mapping_info) = self.mapping_value {
// TODO: Validate mapping name.
format!("program/{}/mapping/{}/{}", self.name, mapping_info[0], mapping_info[1])
} else if self.mappings {
format!("program/{}/mappings", self.name)
} else {
format!("program/{}", self.name)
};
Ok(url)
}
}

View File

@ -0,0 +1,40 @@
// Copyright (C) 2019-2023 Aleo Systems Inc.
// This file is part of the Leo library.
// The Leo library is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// The Leo library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
use super::*;
use clap::Parser;
/// Query the latest stateroot.
#[derive(Parser, Debug)]
pub struct StateRoot {}
impl Command for StateRoot {
type Input = ();
type Output = String;
fn log_span(&self) -> Span {
tracing::span!(tracing::Level::INFO, "Leo")
}
fn prelude(&self, _context: Context) -> Result<Self::Input> {
Ok(())
}
fn apply(self, _context: Context, _: Self::Input) -> Result<Self::Output> {
Ok("/stateRoot/latest".to_string())
}
}

View File

@ -0,0 +1,68 @@
// Copyright (C) 2019-2023 Aleo Systems Inc.
// This file is part of the Leo library.
// The Leo library is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// The Leo library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
use super::*;
use clap::Parser;
/// Query transaction information.
#[derive(Parser, Debug)]
pub struct Transaction {
#[clap(name = "ID", help = "The id of the transaction to fetch", required_unless_present_any = &["from_program", "from_transition", "from_io", "range"])]
pub(crate) id: Option<String>,
#[arg(short, long, help = "Get the transaction only if it has been confirmed", default_value = "false", conflicts_with_all(["from_io", "from_transition", "from_program"]))]
pub(crate) confirmed: bool,
#[arg(value_name = "INPUT_OR_OUTPUT_ID", short, long, help = "Get the transition id that an input or output id occurred in", conflicts_with_all(["from_program", "from_transition", "confirmed", "id"]))]
pub(crate) from_io: Option<String>,
#[arg(value_name = "TRANSITION_ID", short, long, help = "Get the id of the transaction containing the specified transition", conflicts_with_all(["from_io", "from_program", "confirmed", "id"]))]
pub(crate) from_transition: Option<String>,
#[arg(value_name = "PROGRAM", short, long, help = "Get the id of the transaction id that the specified program was deployed in", conflicts_with_all(["from_io", "from_transition", "confirmed", "id"]))]
pub(crate) from_program: Option<String>,
}
impl Command for Transaction {
type Input = ();
type Output = String;
fn log_span(&self) -> Span {
tracing::span!(tracing::Level::INFO, "Leo")
}
fn prelude(&self, _context: Context) -> Result<Self::Input> {
Ok(())
}
fn apply(self, _context: Context, _: Self::Input) -> Result<Self::Output> {
// Build custom url to fetch from based on the flags and user's input.
let url = if let Some(io_id) = self.from_io {
let field = is_valid_field(&io_id)?;
format!("find/transitionID/{field}")
} else if let Some(transition) = self.from_transition {
is_valid_transition_id(&transition)?;
format!("find/transactionID/{transition}")
} else if let Some(program) = self.from_program {
// TODO: Validate program name.
format!("find/transactionID/deployment/{program}")
} else if let Some(id) = self.id {
is_valid_transaction_id(&id)?;
if self.confirmed { format!("transaction/confirmed/{}", id) } else { format!("transaction/{}", id) }
} else {
unreachable!("All command paths covered.")
};
Ok(url)
}
}

View File

@ -508,7 +508,7 @@ fn fetch_from_network(endpoint: &String, program: &String, network: Network) ->
let url = format!("{}/{}/program/{}", endpoint, network.clone(), program);
let response = ureq::get(&url.clone())
.call()
.map_err(|err| UtilError::failed_to_retrieve_from_endpoint(url.clone(), err, Default::default()))?;
.map_err(|err| UtilError::failed_to_retrieve_from_endpoint(err, Default::default()))?;
if response.status() == 200 {
Ok(response.into_string().unwrap())
} else {