Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] leo query CLI tool #27935

Merged
merged 7 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 14 additions & 0 deletions errors/src/errors/package/package_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
}
);
46 changes: 44 additions & 2 deletions errors/src/errors/utils/util_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand All @@ -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()),
}
);
8 changes: 7 additions & 1 deletion leo/cli/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
}

Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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),
Expand Down
4 changes: 2 additions & 2 deletions leo/cli/commands/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ impl Command for Add {
// Allow both `credits.aleo` and `credits` syntax.
let name: String = match &self.name {
name if name.ends_with(".aleo")
&& Package::<CurrentNetwork>::is_program_name_valid(&name[0..self.name.len() - 5]) =>
&& Package::<CurrentNetwork>::is_aleo_name_valid(&name[0..self.name.len() - 5]) =>
{
name.clone()
}
name if Package::<CurrentNetwork>::is_program_name_valid(name) => format!("{name}.aleo"),
name if Package::<CurrentNetwork>::is_aleo_name_valid(name) => format!("{name}.aleo"),
name => return Err(PackageError::invalid_file_name_dependency(name).into()),
};

Expand Down
3 changes: 3 additions & 0 deletions leo/cli/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
129 changes: 129 additions & 0 deletions leo/cli/commands/query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// 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, ())?,
QueryCommands::Mempool { command } => {
if self.endpoint == "http://api.explorer.aleo.org/v1" {
tracing::warn!(
"⚠️ `leo query mempool` is only valid when using a custom endpoint. Specify one using `--endpoint`."
);
}
command.apply(context, ())?
}
QueryCommands::Peers { command } => {
if self.endpoint == "http://api.explorer.aleo.org/v1" {
tracing::warn!(
"⚠️ `leo query peers` is only valid when using a custom endpoint. Specify one using `--endpoint`."
);
}
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())
.set(&format!("X-Aleo-Leo-{}", env!("CARGO_PKG_VERSION")), "true")
.call()
.map_err(|err| UtilError::failed_to_retrieve_from_endpoint(err, Default::default()))?;
if response.status() == 200 {
tracing::info!("✅ Successfully retrieved data from '{url}'.\n");
// Unescape the newlines.
println!("{}\n", 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,
},
#[clap(about = "Query transactions and transmissions from the memory pool")]
Mempool {
#[clap(flatten)]
command: Mempool,
},
#[clap(about = "Query peer information")]
Peers {
#[clap(flatten)]
command: Peers,
},
}
4 changes: 2 additions & 2 deletions leo/cli/commands/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ impl Command for Remove {
let name: String = match &self.name {
Some(name)
if name.ends_with(".aleo")
&& Package::<CurrentNetwork>::is_program_name_valid(&name[0..name.len() - 5]) =>
&& Package::<CurrentNetwork>::is_aleo_name_valid(&name[0..name.len() - 5]) =>
{
name.clone()
}
Some(name) if Package::<CurrentNetwork>::is_program_name_valid(name) => format!("{name}.aleo"),
Some(name) if Package::<CurrentNetwork>::is_aleo_name_valid(name) => format!("{name}.aleo"),
name => return Err(PackageError::invalid_file_name_dependency(name.clone().unwrap()).into()),
};

Expand Down
3 changes: 3 additions & 0 deletions leo/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
93 changes: 93 additions & 0 deletions leo/cli/query_commands/block.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}