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

Add initial implementation of rich output with emojis. #1509

Merged
merged 10 commits into from
Aug 6, 2024
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!");
fnando marked this conversation as resolved.
Show resolved Hide resolved
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()?)?;
}
willemneal marked this conversation as resolved.
Show resolved Hide resolved
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
Loading