diff --git a/Cargo.lock b/Cargo.lock index aaa1f87a48..a4224833e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1972,6 +1972,7 @@ dependencies = [ "toml 0.8.12", "tracing", "tracing-subscriber", + "ureq", "walkdir", "zip", ] diff --git a/Cargo.toml b/Cargo.toml index 59ac10119d..670c706ded 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/errors/src/errors/package/package_errors.rs b/errors/src/errors/package/package_errors.rs index b1d044cd17..f328648a9c 100644 --- a/errors/src/errors/package/package_errors.rs +++ b/errors/src/errors/package/package_errors.rs @@ -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 `.".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 `.".to_string()), + } ); diff --git a/errors/src/errors/utils/util_errors.rs b/errors/src/errors/utils/util_errors.rs index 903a721945..b5dacb4286 100644 --- a/errors/src/errors/utils/util_errors.rs +++ b/errors/src/errors/utils/util_errors.rs @@ -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()), + } ); diff --git a/leo/cli/cli.rs b/leo/cli/cli.rs index c6dc0d9f82..631ee5063e 100644 --- a/leo/cli/cli.rs +++ b/leo/cli/cli.rs @@ -36,7 +36,7 @@ pub struct CLI { #[clap(long, global = true, help = "Path to Leo program root folder")] path: Option, - #[clap(long, global = true, help = "Path to aleo program registry.")] + #[clap(long, global = true, help = "Path to aleo program registry")] pub home: Option, } @@ -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), diff --git a/leo/cli/commands/mod.rs b/leo/cli/commands/mod.rs index 26c963d121..b4d7dc92c4 100644 --- a/leo/cli/commands/mod.rs +++ b/leo/cli/commands/mod.rs @@ -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; diff --git a/leo/cli/commands/query.rs b/leo/cli/commands/query.rs new file mode 100644 index 0000000000..10be77850b --- /dev/null +++ b/leo/cli/commands/query.rs @@ -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 . + +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 { + Ok(()) + } + + fn apply(self, context: Context, _: Self::Input) -> Result { + 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, + }, +} diff --git a/leo/cli/mod.rs b/leo/cli/mod.rs index 4dcfa0a3d7..19e10b6a91 100644 --- a/leo/cli/mod.rs +++ b/leo/cli/mod.rs @@ -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"; diff --git a/leo/cli/query_commands/block.rs b/leo/cli/query_commands/block.rs new file mode 100644 index 0000000000..84ef81f9e8 --- /dev/null +++ b/leo/cli/query_commands/block.rs @@ -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 . + +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, + #[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>, + #[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 { + Ok(()) + } + + fn apply(self, _context: Context, _input: Self::Input) -> Result { + // 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::().unwrap() - range[0].parse::().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) + } +} diff --git a/leo/cli/query_commands/committee.rs b/leo/cli/query_commands/committee.rs new file mode 100644 index 0000000000..86bf67b74f --- /dev/null +++ b/leo/cli/query_commands/committee.rs @@ -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 . + +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 { + Ok(()) + } + + fn apply(self, _context: Context, _: Self::Input) -> Result { + Ok("/committee/latest".to_string()) + } +} diff --git a/leo/cli/query_commands/mod.rs b/leo/cli/query_commands/mod.rs new file mode 100644 index 0000000000..2ce9097f06 --- /dev/null +++ b/leo/cli/query_commands/mod.rs @@ -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 . + +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::().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 { + let split = field.split("field").collect::>(); + + 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()) + } +} diff --git a/leo/cli/query_commands/program.rs b/leo/cli/query_commands/program.rs new file mode 100644 index 0000000000..9c3f4378fd --- /dev/null +++ b/leo/cli/query_commands/program.rs @@ -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 . + +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>, +} + +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 { + Ok(()) + } + + fn apply(self, _context: Context, _: Self::Input) -> Result { + // 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) + } +} diff --git a/leo/cli/query_commands/state_root.rs b/leo/cli/query_commands/state_root.rs new file mode 100644 index 0000000000..b53a9e6e27 --- /dev/null +++ b/leo/cli/query_commands/state_root.rs @@ -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 . + +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 { + Ok(()) + } + + fn apply(self, _context: Context, _: Self::Input) -> Result { + Ok("/stateRoot/latest".to_string()) + } +} diff --git a/leo/cli/query_commands/transaction.rs b/leo/cli/query_commands/transaction.rs new file mode 100644 index 0000000000..9a8ae9589b --- /dev/null +++ b/leo/cli/query_commands/transaction.rs @@ -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 . + +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, + #[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, + #[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, + #[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, +} + +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 { + Ok(()) + } + + fn apply(self, _context: Context, _: Self::Input) -> Result { + // 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) + } +} diff --git a/utils/retriever/src/retriever/mod.rs b/utils/retriever/src/retriever/mod.rs index f67444d1b5..28ecc0639f 100644 --- a/utils/retriever/src/retriever/mod.rs +++ b/utils/retriever/src/retriever/mod.rs @@ -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 {