Skip to content

Commit

Permalink
Add initial implementation of rich output with emojis. (#1509)
Browse files Browse the repository at this point in the history
Co-authored-by: Willem Wyndham <[email protected]>
Co-authored-by: Leigh McCulloch <[email protected]>
  • Loading branch information
3 people authored Aug 6, 2024
1 parent 2c936d5 commit 70d56ca
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 17 deletions.
6 changes: 4 additions & 2 deletions cmd/soroban-cli/src/commands/contract/deploy.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::commands::global;

pub mod asset;
pub mod wasm;

Expand All @@ -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(())
}
Expand Down
50 changes: 39 additions & 11 deletions cmd/soroban-cli/src/commands/contract/deploy/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -159,6 +165,7 @@ impl NetworkRunnable for Cmd {
global_args: Option<&global::Args>,
config: Option<&config::Args>,
) -> Result<TxnResult<String>, 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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
}
}

Expand Down
28 changes: 26 additions & 2 deletions cmd/soroban-cli/src/commands/contract/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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)),
Expand All @@ -86,11 +90,13 @@ impl Cmd {
impl NetworkRunnable for Cmd {
type Error = Error;
type Result = TxnResult<Hash>;

async fn run_against_rpc_server(
&self,
args: Option<&global::Args>,
config: Option<&config::Args>,
) -> Result<TxnResult<Hash>, 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()?;
Expand All @@ -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")
Expand All @@ -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
Expand All @@ -132,13 +140,15 @@ 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.
if !self.fee.sim_only {
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
Expand All @@ -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));
}
}
Expand All @@ -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,
Expand All @@ -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))
}
}
Expand All @@ -217,13 +239,15 @@ 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, .. }) => {
return Some(val.to_utf8_string_lossy());
}
}
}

None
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/soroban-cli/src/commands/contract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand Down
1 change: 1 addition & 0 deletions cmd/soroban-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
63 changes: 63 additions & 0 deletions cmd/soroban-cli/src/output.rs
Original file line number Diff line number Diff line change
@@ -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<T: Display>(&self, icon: &str, message: T) {
if !self.quiet {
eprintln!("{icon} {message}");
}
}

pub fn check<T: Display>(&self, message: T) {
self.print("✅", message);
}

pub fn info<T: Display>(&self, message: T) {
self.print("ℹ️", message);
}

pub fn globe<T: Display>(&self, message: T) {
self.print("🌎", message);
}

pub fn link<T: Display>(&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(())
}
}
20 changes: 20 additions & 0 deletions cmd/soroban-cli/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use ed25519_dalek::Signer;
use phf::phf_map;
use sha2::{Digest, Sha256};
use stellar_strkey::ed25519::PrivateKey;

Expand All @@ -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
Expand All @@ -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<String> {
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<String> {
EXPLORERS
.get(&network.network_passphrase)
.map(|base_url| format!("{base_url}/contract/{contract_id}"))
}

/// # Errors
///
/// Might return an error
Expand Down

0 comments on commit 70d56ca

Please sign in to comment.