From 70d56cafee74430071fa4dbbd5c69f6bdd951a0a Mon Sep 17 00:00:00 2001 From: Nando Vieira Date: Tue, 6 Aug 2024 09:46:05 -0300 Subject: [PATCH] Add initial implementation of rich output with emojis. (#1509) Co-authored-by: Willem Wyndham Co-authored-by: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> --- .../src/commands/contract/deploy.rs | 6 +- .../src/commands/contract/deploy/wasm.rs | 50 +++++++++++---- .../src/commands/contract/install.rs | 28 ++++++++- cmd/soroban-cli/src/commands/contract/mod.rs | 4 +- cmd/soroban-cli/src/lib.rs | 1 + cmd/soroban-cli/src/output.rs | 63 +++++++++++++++++++ cmd/soroban-cli/src/utils.rs | 20 ++++++ 7 files changed, 155 insertions(+), 17 deletions(-) create mode 100644 cmd/soroban-cli/src/output.rs diff --git a/cmd/soroban-cli/src/commands/contract/deploy.rs b/cmd/soroban-cli/src/commands/contract/deploy.rs index 9baf44590..812fb4c77 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy.rs @@ -1,3 +1,5 @@ +use crate::commands::global; + pub mod asset; pub mod wasm; @@ -18,10 +20,10 @@ pub enum Error { } impl Cmd { - pub async fn run(&self) -> Result<(), Error> { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { match &self { Cmd::Asset(asset) => asset.run().await?, - Cmd::Wasm(wasm) => wasm.run().await?, + Cmd::Wasm(wasm) => wasm.run(global_args).await?, } Ok(()) } diff --git a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs index 4c1aa5d55..ec73a2322 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -15,18 +15,21 @@ use soroban_env_host::{ HostError, }; -use crate::commands::{ - contract::{self, id::wasm::get_contract_id}, - global, - txn_result::{TxnEnvelopeResult, TxnResult}, - NetworkRunnable, -}; use crate::{ commands::{contract::install, HEADING_RPC}, config::{self, data, locator, network}, rpc::{self, Client}, utils, wasm, }; +use crate::{ + commands::{ + contract::{self, id::wasm::get_contract_id}, + global, + txn_result::{TxnEnvelopeResult, TxnResult}, + NetworkRunnable, + }, + output::Output, +}; #[derive(Parser, Debug, Clone)] #[command(group( @@ -115,8 +118,11 @@ pub enum Error { } impl Cmd { - pub async fn run(&self) -> Result<(), Error> { - let res = self.run_against_rpc_server(None, None).await?.to_envelope(); + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let res = self + .run_against_rpc_server(Some(global_args), None) + .await? + .to_envelope(); match res { TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?), TxnEnvelopeResult::Res(contract) => { @@ -159,6 +165,7 @@ impl NetworkRunnable for Cmd { global_args: Option<&global::Args>, config: Option<&config::Args>, ) -> Result, Error> { + let output = Output::new(global_args.map_or(false, |a| a.quiet)); let config = config.unwrap_or(&self.config); let wasm_hash = if let Some(wasm) = &self.wasm { let hash = if self.fee.build_only || self.fee.sim_only { @@ -189,6 +196,9 @@ impl NetworkRunnable for Cmd { error: e, } })?); + + output.info(format!("Using wasm hash {wasm_hash}").as_str()); + let network = config.get_network()?; let salt: [u8; 32] = match &self.salt { Some(h) => soroban_spec_tools::utils::padded_hex_from_str(h, 32) @@ -218,25 +228,43 @@ impl NetworkRunnable for Cmd { salt, &key, )?; + if self.fee.build_only { + output.check("Transaction built!"); return Ok(TxnResult::Txn(txn)); } + output.info("Simulating deploy transaction…"); + let txn = client.simulate_and_assemble_transaction(&txn).await?; let txn = self.fee.apply_to_assembled_txn(txn).transaction().clone(); + if self.fee.sim_only { + output.check("Done!"); return Ok(TxnResult::Txn(txn)); } + + output.globe("Submitting deploy transaction…"); + output.log_transaction(&txn, &network, true)?; + let get_txn_resp = client .send_transaction_polling(&config.sign_with_local_key(txn).await?) .await? .try_into()?; + if global_args.map_or(true, |a| !a.no_cache) { data::write(get_txn_resp, &network.rpc_uri()?)?; } - Ok(TxnResult::Res( - stellar_strkey::Contract(contract_id.0).to_string(), - )) + + let contract_id = stellar_strkey::Contract(contract_id.0).to_string(); + + if let Some(url) = utils::explorer_url_for_contract(&network, &contract_id) { + output.link(url); + } + + output.check("Deployed!"); + + Ok(TxnResult::Res(contract_id)) } } diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 9d2e474a3..23e4e0913 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -15,6 +15,7 @@ use crate::commands::txn_result::{TxnEnvelopeResult, TxnResult}; use crate::commands::{global, NetworkRunnable}; use crate::config::{self, data, network}; use crate::key; +use crate::output::Output; use crate::rpc::{self, Client}; use crate::{utils, wasm}; @@ -72,8 +73,11 @@ pub enum Error { } impl Cmd { - pub async fn run(&self) -> Result<(), Error> { - let res = self.run_against_rpc_server(None, None).await?.to_envelope(); + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let res = self + .run_against_rpc_server(Some(global_args), None) + .await? + .to_envelope(); match res { TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?), TxnEnvelopeResult::Res(hash) => println!("{}", hex::encode(hash)), @@ -86,11 +90,13 @@ impl Cmd { impl NetworkRunnable for Cmd { type Error = Error; type Result = TxnResult; + async fn run_against_rpc_server( &self, args: Option<&global::Args>, config: Option<&config::Args>, ) -> Result, Error> { + let output = Output::new(args.map_or(false, |a| a.quiet)); let config = config.unwrap_or(&self.config); let contract = self.wasm.read()?; let network = config.get_network()?; @@ -102,6 +108,7 @@ impl NetworkRunnable for Cmd { wasm: self.wasm.wasm.clone(), error: e, })?; + // Check Rust SDK version if using the public network. if let Some(rs_sdk_ver) = get_contract_meta_sdk_version(wasm_spec) { if rs_sdk_ver.contains("rc") @@ -118,6 +125,7 @@ impl NetworkRunnable for Cmd { tracing::warn!("the deployed smart contract {path} was built with Soroban Rust SDK v{rs_sdk_ver}, a release candidate version not intended for use with the Stellar Public Network", path = self.wasm.wasm.display()); } } + let key = config.key_pair()?; // Get the account sequence number @@ -132,6 +140,7 @@ impl NetworkRunnable for Cmd { if self.fee.build_only { return Ok(TxnResult::Txn(tx_without_preflight)); } + // Don't check whether the contract is already installed when the user // has requested to perform simulation only and is hoping to get a // transaction back. @@ -139,6 +148,7 @@ impl NetworkRunnable for Cmd { let code_key = xdr::LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() }); let contract_data = client.get_ledger_entries(&[code_key]).await?; + // Skip install if the contract is already installed, and the contract has an extension version that isn't V0. // In protocol 21 extension V1 was added that stores additional information about a contract making execution // of the contract cheaper. So if folks want to reinstall we should let them which is why the install will still @@ -153,6 +163,7 @@ impl NetworkRunnable for Cmd { // Skip reupload if this isn't V0 because V1 extension already // exists. if code.ext.ne(&ContractCodeEntryExt::V0) { + output.info("Skipping install because wasm already installed"); return Ok(TxnResult::Res(hash)); } } @@ -163,19 +174,28 @@ impl NetworkRunnable for Cmd { } } } + + output.info("Simulating install transaction…"); + let txn = client .simulate_and_assemble_transaction(&tx_without_preflight) .await?; let txn = self.fee.apply_to_assembled_txn(txn).transaction().clone(); + if self.fee.sim_only { return Ok(TxnResult::Txn(txn)); } + + output.globe("Submitting install transaction…"); + let txn_resp = client .send_transaction_polling(&self.config.sign_with_local_key(txn).await?) .await?; + if args.map_or(true, |a| !a.no_cache) { data::write(txn_resp.clone().try_into().unwrap(), &network.rpc_uri()?)?; } + // Currently internal errors are not returned if the contract code is expired if let Some(TransactionResult { result: TransactionResultResult::TxInternalError, @@ -200,9 +220,11 @@ impl NetworkRunnable for Cmd { .run_against_rpc_server(args, None) .await?; } + if args.map_or(true, |a| !a.no_cache) { data::write_spec(&hash.to_string(), &wasm_spec.spec)?; } + Ok(TxnResult::Res(hash)) } } @@ -217,6 +239,7 @@ fn get_contract_meta_sdk_version(wasm_spec: &soroban_spec_tools::contract::Spec) } else { None }; + if let Some(rs_sdk_version_entry) = &rs_sdk_version_option { match rs_sdk_version_entry { ScMetaEntry::ScMetaV0(ScMetaV0 { val, .. }) => { @@ -224,6 +247,7 @@ fn get_contract_meta_sdk_version(wasm_spec: &soroban_spec_tools::contract::Spec) } } } + None } diff --git a/cmd/soroban-cli/src/commands/contract/mod.rs b/cmd/soroban-cli/src/commands/contract/mod.rs index f83cad37f..4a2f8dcfe 100644 --- a/cmd/soroban-cli/src/commands/contract/mod.rs +++ b/cmd/soroban-cli/src/commands/contract/mod.rs @@ -124,11 +124,11 @@ impl Cmd { Cmd::Bindings(bindings) => bindings.run().await?, Cmd::Build(build) => build.run()?, Cmd::Extend(extend) => extend.run().await?, - Cmd::Deploy(deploy) => deploy.run().await?, + Cmd::Deploy(deploy) => deploy.run(global_args).await?, Cmd::Id(id) => id.run()?, Cmd::Init(init) => init.run()?, Cmd::Inspect(inspect) => inspect.run()?, - Cmd::Install(install) => install.run().await?, + Cmd::Install(install) => install.run(global_args).await?, Cmd::Invoke(invoke) => invoke.run(global_args).await?, Cmd::Optimize(optimize) => optimize.run()?, Cmd::Fetch(fetch) => fetch.run().await?, diff --git a/cmd/soroban-cli/src/lib.rs b/cmd/soroban-cli/src/lib.rs index 0268a40e0..afb0607ca 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -17,6 +17,7 @@ pub mod fee; pub mod get_spec; pub mod key; pub mod log; +pub mod output; pub mod signer; pub mod toid; pub mod utils; diff --git a/cmd/soroban-cli/src/output.rs b/cmd/soroban-cli/src/output.rs new file mode 100644 index 000000000..a86b56d65 --- /dev/null +++ b/cmd/soroban-cli/src/output.rs @@ -0,0 +1,63 @@ +use std::fmt::Display; + +use soroban_env_host::xdr::{Error as XdrError, Transaction}; + +use crate::{ + config::network::Network, + utils::{explorer_url_for_transaction, transaction_hash}, +}; + +pub struct Output { + pub quiet: bool, +} + +impl Output { + pub fn new(quiet: bool) -> Output { + Output { quiet } + } + + fn print(&self, icon: &str, message: T) { + if !self.quiet { + eprintln!("{icon} {message}"); + } + } + + pub fn check(&self, message: T) { + self.print("✅", message); + } + + pub fn info(&self, message: T) { + self.print("ℹ️", message); + } + + pub fn globe(&self, message: T) { + self.print("🌎", message); + } + + pub fn link(&self, message: T) { + self.print("🔗", message); + } + + /// # Errors + /// + /// Might return an error + pub fn log_transaction( + &self, + tx: &Transaction, + network: &Network, + show_link: bool, + ) -> Result<(), XdrError> { + let tx_hash = transaction_hash(tx, &network.network_passphrase)?; + let hash = hex::encode(tx_hash); + + self.info(format!("Transaction hash is {hash}").as_str()); + + if show_link { + if let Some(url) = explorer_url_for_transaction(network, &hash) { + self.link(url); + } + } + + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/utils.rs b/cmd/soroban-cli/src/utils.rs index 24b76b24f..f8ebb8b37 100644 --- a/cmd/soroban-cli/src/utils.rs +++ b/cmd/soroban-cli/src/utils.rs @@ -1,4 +1,5 @@ use ed25519_dalek::Signer; +use phf::phf_map; use sha2::{Digest, Sha256}; use stellar_strkey::ed25519::PrivateKey; @@ -11,6 +12,8 @@ use soroban_env_host::xdr::{ pub use soroban_spec_tools::contract as contract_spec; +use crate::config::network::Network; + /// # Errors /// /// Might return an error @@ -29,6 +32,23 @@ pub fn transaction_hash(tx: &Transaction, network_passphrase: &str) -> Result<[u Ok(Sha256::digest(signature_payload.to_xdr(Limits::none())?).into()) } +static EXPLORERS: phf::Map<&'static str, &'static str> = phf_map! { + "Test SDF Network ; September 2015" => "https://stellar.expert/explorer/testnet", + "Public Global Stellar Network ; September 2015" => "https://stellar.expert/explorer/public", +}; + +pub fn explorer_url_for_transaction(network: &Network, tx_hash: &str) -> Option { + EXPLORERS + .get(&network.network_passphrase) + .map(|base_url| format!("{base_url}/tx/{tx_hash}")) +} + +pub fn explorer_url_for_contract(network: &Network, contract_id: &str) -> Option { + EXPLORERS + .get(&network.network_passphrase) + .map(|base_url| format!("{base_url}/contract/{contract_id}")) +} + /// # Errors /// /// Might return an error