mod decrypt;
pub use decrypt::*;
mod deploy;
pub use deploy::*;
mod execute;
pub use execute::*;
mod scan;
pub use scan::*;
mod transfer_private;
pub use transfer_private::*;
use crate::helpers::{args::network_id_parser, logger::initialize_terminal_logger};
use snarkos_node_rest::{API_VERSION_V1, API_VERSION_V2};
use snarkvm::{package::Package, prelude::*};
use anyhow::{Context, Result, anyhow, bail, ensure};
use clap::{Parser, ValueEnum};
use colored::Colorize;
use serde::{Serialize, de::DeserializeOwned};
use std::{
path::PathBuf,
str::FromStr,
thread,
time::{Duration, Instant},
};
use tracing::debug;
use ureq::http::{self, Uri};
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum StoreFormat {
String,
Bytes,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ApiVersion {
V1,
V2,
}
#[derive(Debug, Parser)]
pub enum DeveloperCommand {
Decrypt(Decrypt),
Deploy(Deploy),
Execute(Execute),
Scan(Scan),
TransferPrivate(TransferPrivate),
}
const DEFAULT_ENDPOINT: &str = "https://api.explorer.provable.com/v2";
#[derive(Debug, Parser)]
pub struct Developer {
#[clap(subcommand)]
command: DeveloperCommand,
#[clap(long, default_value_t=MainnetV0::ID, long, global=true, value_parser = network_id_parser())]
network: u16,
#[clap(long, global = true)]
verbosity: Option<u8>,
}
#[derive(Debug, Deserialize)]
struct RestError {
error_type: String,
message: String,
#[serde(default)]
chain: Vec<String>,
}
impl RestError {
pub fn parse(self) -> anyhow::Error {
let mut error: Option<anyhow::Error> = None;
for next in self.chain.into_iter() {
if let Some(previous) = error {
error = Some(previous.context(next));
} else {
error = Some(anyhow!(next));
}
}
let toplevel = format!("{}: {}", self.error_type, self.message);
if let Some(error) = error { error.context(toplevel) } else { anyhow!(toplevel) }
}
}
impl Developer {
pub fn parse(self) -> Result<String> {
if let Some(verbosity) = self.verbosity {
initialize_terminal_logger(verbosity).with_context(|| "Failed to initialize terminal logger")?
}
match self.network {
MainnetV0::ID => self.parse_inner::<MainnetV0>(),
TestnetV0::ID => self.parse_inner::<TestnetV0>(),
CanaryV0::ID => self.parse_inner::<CanaryV0>(),
unknown_id => bail!("Unknown network ID ({unknown_id})"),
}
}
fn parse_inner<N: Network>(self) -> Result<String> {
use DeveloperCommand::*;
match self.command {
Decrypt(decrypt) => decrypt.parse::<N>(),
Deploy(deploy) => deploy.parse::<N>(),
Execute(execute) => execute.parse::<N>(),
Scan(scan) => scan.parse::<N>(),
TransferPrivate(transfer_private) => transfer_private.parse::<N>(),
}
}
fn parse_package<N: Network>(program_id: ProgramID<N>, path: &Option<String>) -> Result<Package<N>> {
let directory = match path {
Some(path) => PathBuf::from_str(path)?,
None => std::env::current_dir()?,
};
let package = Package::open(&directory)?;
ensure!(
package.program_id() == &program_id,
"The program name in the package does not match the specified program name"
);
Ok(package)
}
fn parse_record<N: Network>(private_key: &PrivateKey<N>, record: &str) -> Result<Record<N, Plaintext<N>>> {
match record.starts_with("record1") {
true => {
let ciphertext = Record::<N, Ciphertext<N>>::from_str(record)?;
let view_key = ViewKey::try_from(private_key)?;
ciphertext.decrypt(&view_key)
}
false => Record::<N, Plaintext<N>>::from_str(record),
}
}
fn build_endpoint<N: Network>(base_url: &http::Uri, route: &str) -> Result<(String, ApiVersion)> {
ensure!(!route.starts_with('/'), "path cannot start with a slash");
let api_version = {
let r = base_url.path().trim_end_matches('/');
if r.ends_with(API_VERSION_V1) {
ApiVersion::V1
} else if r.ends_with(API_VERSION_V2) {
ApiVersion::V2
} else {
ApiVersion::V1
}
};
let sep = if base_url.path().ends_with('/') { "" } else { "/" };
let full_uri = format!("{base_url}{sep}{network}/{route}", network = N::SHORT_NAME);
Ok((full_uri, api_version))
}
fn handle_ureq_result(result: Result<http::Response<ureq::Body>>) -> Result<Option<ureq::Body>> {
let response = result?;
if response.status().is_success() {
Ok(Some(response.into_body()))
} else if response.status() == http::StatusCode::NOT_FOUND {
Ok(None)
} else {
let is_json = response
.headers()
.get(http::header::CONTENT_TYPE)
.and_then(|h| h.to_str().ok())
.map(|ct| ct.contains("json"))
.unwrap_or(false);
if is_json {
let rest_error: RestError =
response.into_body().read_json().with_context(|| "Failed to parse error JSON")?;
Err(rest_error.parse())
} else {
let err_msg = response.into_body().read_to_string()?;
Err(anyhow!(err_msg))
}
}
}
fn parse_custom_endpoint<N: Network>(url: &Uri) -> (String, ApiVersion) {
if let Some(pq) = url.path_and_query()
&& pq.path().ends_with(&format!("{API_VERSION_V2}/{}/transaction/broadcast", N::SHORT_NAME))
{
(url.to_string(), ApiVersion::V2)
} else {
(url.to_string(), ApiVersion::V1)
}
}
fn http_post_json<I: Serialize, O: DeserializeOwned>(path: &str, arg: &I) -> Result<Option<O>> {
debug!("Issuing POST request to \"{path}\"");
let result =
ureq::post(path).config().http_status_as_error(false).build().send_json(arg).map_err(|err| err.into());
match Self::handle_ureq_result(result).with_context(|| format!("HTTP POST request to {path} failed"))? {
Some(mut body) => {
let json = body.read_json().with_context(|| format!("Failed to parse JSON response from {path}"))?;
Ok(Some(json))
}
None => Ok(None),
}
}
fn http_get_json<N: Network, O: DeserializeOwned>(base_url: &http::Uri, route: &str) -> Result<Option<O>> {
let (endpoint, _api_version) = Self::build_endpoint::<N>(base_url, route)?;
debug!("Issuing GET request to \"{endpoint}\"");
let result = ureq::get(&endpoint).config().http_status_as_error(false).build().call().map_err(|err| err.into());
match Self::handle_ureq_result(result).with_context(|| format!("HTTP GET request to {endpoint} failed"))? {
Some(mut body) => {
let json =
body.read_json().with_context(|| format!("Failed to parse JSON response from {endpoint}"))?;
Ok(Some(json))
}
None => Ok(None),
}
}
fn http_get<N: Network>(base_url: &http::Uri, route: &str) -> Result<Option<ureq::Body>> {
let (endpoint, _api_version) = Self::build_endpoint::<N>(base_url, route)?;
debug!("Issuing GET request to \"{endpoint}\"");
let result = ureq::get(&endpoint).config().http_status_as_error(false).build().call().map_err(|err| err.into());
Self::handle_ureq_result(result).with_context(|| format!("HTTP GET request to {endpoint} failed"))
}
fn wait_for_transaction_confirmation<N: Network>(
endpoint: &Uri,
transaction_id: &N::TransactionID,
timeout_seconds: u64,
api_version: ApiVersion,
) -> Result<()> {
let start_time = Instant::now();
let timeout_duration = Duration::from_secs(timeout_seconds);
let poll_interval = Duration::from_secs(1);
while start_time.elapsed() < timeout_duration {
let result = Self::http_get::<N>(endpoint, &format!("transaction/{transaction_id}"));
match api_version {
ApiVersion::V1 => match result {
Ok(Some(_)) => return Ok(()),
Ok(None) => {
}
Err(err) => {
eprintln!("Got error when fetching transaction ({err}). Will retry...");
}
},
ApiVersion::V2 => {
match result.with_context(|| "Failed to check transaction status")? {
Some(_) => return Ok(()),
None => {
}
}
}
}
thread::sleep(poll_interval);
}
bail!("❌ Transaction {transaction_id} was not confirmed within {timeout_seconds} seconds");
}
fn get_latest_edition<N: Network>(endpoint: &Uri, program_id: &ProgramID<N>) -> Result<u16> {
match Self::http_get_json::<N, _>(endpoint, &format!("program/{program_id}/latest_edition"))? {
Some(edition) => Ok(edition),
None => bail!("Got unexpected 404 response"),
}
}
fn get_public_balance<N: Network>(endpoint: &Uri, address: &Address<N>) -> Result<Option<u64>> {
let account_mapping = Identifier::<N>::from_str("account")?;
let credits = ProgramID::<N>::from_str("credits.aleo")?;
let result: Option<Value<N>> =
Self::http_get_json::<N, _>(endpoint, &format!("program/{credits}/mapping/{account_mapping}/{address}"))?
.ok_or_else(|| anyhow!("Got unexpected 404 error when fetching public balance"))?;
match result {
Some(Value::Plaintext(Plaintext::Literal(Literal::<N>::U64(amount), _))) => Ok(Some(*amount)),
Some(..) => bail!("Failed to deserialize balance for {address}"),
None => Ok(None),
}
}
#[allow(clippy::too_many_arguments)]
fn handle_transaction<N: Network>(
endpoint: &Uri,
broadcast: &Option<Option<Uri>>,
dry_run: bool,
store: &Option<String>,
store_format: StoreFormat,
wait: bool,
timeout: u64,
transaction: Transaction<N>,
operation: String,
) -> Result<String> {
let transaction_id = transaction.id();
ensure!(!transaction.is_fee(), "The transaction is a fee transaction and cannot be broadcast");
if let Some(path) = store {
match PathBuf::from_str(path) {
Ok(file_path) => {
match store_format {
StoreFormat::Bytes => {
let transaction_bytes = transaction.to_bytes_le()?;
std::fs::write(&file_path, transaction_bytes)?;
}
StoreFormat::String => {
let transaction_string = transaction.to_string();
std::fs::write(&file_path, transaction_string)?;
}
}
println!(
"Transaction {transaction_id} was stored to {} as {:?}",
file_path.display(),
store_format
);
}
Err(err) => {
println!("The transaction was unable to be stored due to: {err}");
}
}
};
if let Some(broadcast_value) = broadcast {
let (broadcast_endpoint, api_version) = if let Some(url) = broadcast_value {
debug!("Using custom endpoint for broadcasting: {url}");
Self::parse_custom_endpoint::<N>(url)
} else {
Self::build_endpoint::<N>(endpoint, "transaction/broadcast")?
};
let result: Result<String> = match Self::http_post_json(&broadcast_endpoint, &transaction) {
Ok(Some(s)) => Ok(s),
Ok(None) => Err(anyhow!("Got unexpected 404 error")),
Err(err) => Err(err),
};
match result {
Ok(response_string) => {
ensure!(
response_string == transaction_id.to_string(),
"The response does not match the transaction id. ({response_string} != {transaction_id})"
);
match transaction {
Transaction::Deploy(..) => {
println!(
"⌛ Deployment {transaction_id} ('{}') has been broadcast to {}.",
operation.bold(),
broadcast_endpoint
)
}
Transaction::Execute(..) => {
println!(
"⌛ Execution {transaction_id} ('{}') has been broadcast to {}.",
operation.bold(),
broadcast_endpoint
)
}
_ => unreachable!(),
}
if wait {
println!("⏳ Waiting for transaction confirmation (timeout: {timeout}s)...");
Self::wait_for_transaction_confirmation::<N>(endpoint, &transaction_id, timeout, api_version)?;
match transaction {
Transaction::Deploy(..) => {
println!("✅ Deployment {transaction_id} ('{}') confirmed!", operation.bold())
}
Transaction::Execute(..) => {
println!("✅ Execution {transaction_id} ('{}') confirmed!", operation.bold())
}
Transaction::Fee(..) => unreachable!(),
}
}
}
Err(error) => match transaction {
Transaction::Deploy(..) => {
return Err(error.context(anyhow!(
"Failed to deploy '{op}' to {broadcast_endpoint}",
op = operation.bold()
)));
}
Transaction::Execute(..) => {
return Err(error.context(anyhow!(
"Failed to broadcast execution '{op}' to {broadcast_endpoint}",
op = operation.bold()
)));
}
Transaction::Fee(..) => unreachable!(),
},
};
Ok(transaction_id.to_string())
} else if dry_run {
Ok(transaction.to_string())
} else {
Ok("".to_string())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use snarkvm::ledger::test_helpers::CurrentNetwork;
#[test]
fn test_build_endpoint_default_v1() {
let base_uri_str = "http://localhost:3030";
let base_uri = Uri::try_from(base_uri_str).unwrap();
let (endpoint, api_version) =
Developer::build_endpoint::<CurrentNetwork>(&base_uri, "transaction/broadcast").unwrap();
assert_eq!(endpoint, format!("{base_uri_str}/{}/transaction/broadcast", CurrentNetwork::SHORT_NAME));
assert_eq!(api_version, ApiVersion::V1);
}
#[test]
fn test_build_endpoint_v1() {
let base_uri_str = "http://localhost:3030/v1";
let base_uri = Uri::try_from(base_uri_str).unwrap();
let (endpoint, api_version) =
Developer::build_endpoint::<CurrentNetwork>(&base_uri, "transaction/broadcast").unwrap();
assert_eq!(endpoint, format!("{base_uri_str}/{}/transaction/broadcast", CurrentNetwork::SHORT_NAME));
assert_eq!(api_version, ApiVersion::V1);
}
#[test]
fn test_build_endpoint_v2() {
let base_uri_str = "http://localhost:3030/v2";
let base_uri = Uri::try_from(base_uri_str).unwrap();
let (endpoint, api_version) =
Developer::build_endpoint::<CurrentNetwork>(&base_uri, "transaction/broadcast").unwrap();
assert_eq!(endpoint, format!("{base_uri_str}/{}/transaction/broadcast", CurrentNetwork::SHORT_NAME));
assert_eq!(api_version, ApiVersion::V2);
}
#[test]
fn test_custom_endpoint_v1() {
let endpoint_str = "http://localhost:3030/v1/mainnet/transaction/broadcast";
let endpoint = Uri::try_from(endpoint_str).unwrap();
let (parsed, api_version) = Developer::parse_custom_endpoint::<CurrentNetwork>(&endpoint);
assert_eq!(parsed, endpoint_str);
assert_eq!(api_version, ApiVersion::V1);
}
#[test]
fn test_custom_endpoint_v2() {
let endpoint_str = "http://localhost:3030/v2/mainnet/transaction/broadcast";
let endpoint = Uri::try_from(endpoint_str).unwrap();
let (parsed, api_version) = Developer::parse_custom_endpoint::<CurrentNetwork>(&endpoint);
assert_eq!(parsed, endpoint_str);
assert_eq!(api_version, ApiVersion::V2);
}
}