diff --git a/Cargo.lock b/Cargo.lock index a44af436f..09d548fd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -828,13 +828,22 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys 0.4.1", +] + [[package]] name = "dirs" version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" dependencies = [ - "dirs-sys", + "dirs-sys 0.3.7", ] [[package]] @@ -848,6 +857,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -2727,6 +2748,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "overload" version = "0.1.1" @@ -3648,6 +3675,7 @@ dependencies = [ "clap_complete", "crate-git-revision 0.0.4", "csv", + "directories", "dirs", "dotenvy", "ed25519-dalek 2.0.0", @@ -3701,6 +3729,7 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", + "ulid", "ureq", "wasm-opt", "wasmparser 0.90.0", @@ -3946,6 +3975,7 @@ dependencies = [ "thiserror", "tokio", "toml 0.8.10", + "ulid", "walkdir", "which", ] @@ -4580,6 +4610,18 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ulid" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34778c17965aa2a08913b57e1f34db9b4a63f5de31768b55bf20d2795f921259" +dependencies = [ + "getrandom", + "rand", + "serde", + "web-time", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -4875,6 +4917,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.25.4" diff --git a/Cargo.toml b/Cargo.toml index b2ea0177b..0f91b7153 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,6 +85,8 @@ tracing-subscriber = "0.3.16" tracing-appender = "0.2.2" which = "4.4.0" wasmparser = "0.90.0" +directories = "5.0.1" +ulid = { version = "1.1" } termcolor = "1.1.3" termcolor_output = "1.0.1" ed25519-dalek = "2.0.0" diff --git a/cmd/crates/soroban-test/Cargo.toml b/cmd/crates/soroban-test/Cargo.toml index cba4dae74..b6388b292 100644 --- a/cmd/crates/soroban-test/Cargo.toml +++ b/cmd/crates/soroban-test/Cargo.toml @@ -41,6 +41,7 @@ serde_json = "1.0.93" which = { workspace = true } tokio = "1.28.1" walkdir = "2.4.0" +ulid.workspace = true [features] it = [] diff --git a/cmd/crates/soroban-test/src/lib.rs b/cmd/crates/soroban-test/src/lib.rs index 9ac884193..e4d7410ce 100644 --- a/cmd/crates/soroban-test/src/lib.rs +++ b/cmd/crates/soroban-test/src/lib.rs @@ -126,7 +126,8 @@ impl TestEnv { .env("SOROBAN_ACCOUNT", TEST_ACCOUNT) .env("SOROBAN_RPC_URL", &self.rpc_url) .env("SOROBAN_NETWORK_PASSPHRASE", LOCAL_NETWORK_PASSPHRASE) - .env("XDG_CONFIG_HOME", self.temp_dir.as_os_str()) + .env("XDG_CONFIG_HOME", self.temp_dir.join("config").as_os_str()) + .env("XDG_DATA_HOME", self.temp_dir.join("data").as_os_str()) .current_dir(&self.temp_dir); cmd } @@ -246,6 +247,7 @@ impl TestEnv { verbose: false, very_verbose: false, list: false, + no_cache: false, }), Some(&config), ) diff --git a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs index 671d2edff..be03afa8d 100644 --- a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -10,6 +10,7 @@ use crate::integration::util::extend_contract; use super::util::{deploy_hello, extend, HELLO_WORLD}; +#[allow(clippy::too_many_lines)] #[tokio::test] async fn invoke() { let sandbox = &TestEnv::new(); @@ -63,6 +64,13 @@ async fn invoke() { }; let id = &deploy_hello(sandbox).await; extend_contract(sandbox, id).await; + let uid = sandbox + .new_assert_cmd("cache") + .arg("actionlog") + .arg("ls") + .assert() + .stdout_as_str(); + ulid::Ulid::from_string(&uid).expect("invalid ulid"); // Note that all functions tested here have no state invoke_hello_world(sandbox, id); diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index 8c0aa3e8c..85895ef02 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -95,6 +95,8 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] } cargo_metadata = "0.15.4" pathdiff = "0.2.1" dotenvy = "0.15.7" +directories = { workspace = true } +ulid = { workspace = true, features = ["serde"] } strum = "0.17.1" strum_macros = "0.17.1" gix = { version = "0.58.0", default-features = false, features = [ diff --git a/cmd/soroban-cli/src/commands/cache/actionlog/ls.rs b/cmd/soroban-cli/src/commands/cache/actionlog/ls.rs new file mode 100644 index 000000000..cb7a958c6 --- /dev/null +++ b/cmd/soroban-cli/src/commands/cache/actionlog/ls.rs @@ -0,0 +1,44 @@ +use clap::command; + +use super::super::super::config::locator; +use crate::commands::config::data; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Config(#[from] locator::Error), + #[error(transparent)] + Data(#[from] data::Error), +} + +#[derive(Debug, clap::Parser, Clone)] +#[group(skip)] +pub struct Cmd { + #[command(flatten)] + pub config_locator: locator::Args, + + #[arg(long, short = 'l')] + pub long: bool, +} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + let res = if self.long { self.ls_l() } else { self.ls() }?.join("\n"); + println!("{res}"); + Ok(()) + } + + pub fn ls(&self) -> Result, Error> { + Ok(data::list_ulids()? + .iter() + .map(ToString::to_string) + .collect()) + } + + pub fn ls_l(&self) -> Result, Error> { + Ok(data::list_actions()? + .iter() + .map(ToString::to_string) + .collect()) + } +} diff --git a/cmd/soroban-cli/src/commands/cache/actionlog/mod.rs b/cmd/soroban-cli/src/commands/cache/actionlog/mod.rs new file mode 100644 index 000000000..cb7549609 --- /dev/null +++ b/cmd/soroban-cli/src/commands/cache/actionlog/mod.rs @@ -0,0 +1,30 @@ +use clap::Parser; + +pub mod ls; +pub mod read; + +#[derive(Debug, Parser)] +pub enum Cmd { + /// List cached actions (transactions, simulations) + Ls(ls::Cmd), + /// Read cached action + Read(read::Cmd), +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Ls(#[from] ls::Error), + #[error(transparent)] + Read(#[from] read::Error), +} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + match self { + Cmd::Ls(cmd) => cmd.run()?, + Cmd::Read(cmd) => cmd.run()?, + }; + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/commands/cache/actionlog/read.rs b/cmd/soroban-cli/src/commands/cache/actionlog/read.rs new file mode 100644 index 000000000..67b4358f1 --- /dev/null +++ b/cmd/soroban-cli/src/commands/cache/actionlog/read.rs @@ -0,0 +1,39 @@ +use std::{fs, io, path::PathBuf}; + +use super::super::super::config::locator; +use crate::commands::config::data; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Config(#[from] locator::Error), + #[error(transparent)] + Data(#[from] data::Error), + #[error("failed to find cache entry {0}")] + NotFound(String), + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), +} + +#[derive(Debug, clap::Parser, Clone)] +#[group(skip)] +pub struct Cmd { + /// ID of the cache entry + #[arg(long)] + pub id: String, +} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + let file = self.file()?; + tracing::debug!("reading file {}", file.display()); + let mut file = fs::File::open(file).map_err(|_| Error::NotFound(self.id.clone()))?; + let mut stdout = io::stdout(); + let _ = io::copy(&mut file, &mut stdout); + Ok(()) + } + + pub fn file(&self) -> Result { + Ok(data::actions_dir()?.join(&self.id).with_extension("json")) + } +} diff --git a/cmd/soroban-cli/src/commands/cache/clean.rs b/cmd/soroban-cli/src/commands/cache/clean.rs new file mode 100644 index 000000000..bea0a43d4 --- /dev/null +++ b/cmd/soroban-cli/src/commands/cache/clean.rs @@ -0,0 +1,30 @@ +use std::{fs, io::ErrorKind}; + +use super::super::config::locator; +use crate::commands::config::data; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Config(#[from] locator::Error), + #[error(transparent)] + Data(#[from] data::Error), + #[error(transparent)] + Io(#[from] std::io::Error), +} + +#[derive(Debug, clap::Parser, Clone)] +#[group(skip)] +pub struct Cmd {} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + let binding = data::project_dir()?; + let dir = binding.data_dir(); + match fs::remove_dir_all(dir) { + Err(err) if err.kind() == ErrorKind::NotFound => (), + r => r?, + } + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/commands/cache/mod.rs b/cmd/soroban-cli/src/commands/cache/mod.rs new file mode 100644 index 000000000..87ee351ae --- /dev/null +++ b/cmd/soroban-cli/src/commands/cache/mod.rs @@ -0,0 +1,38 @@ +use clap::Parser; + +pub mod actionlog; +pub mod clean; +pub mod path; + +#[derive(Debug, Parser)] +pub enum Cmd { + /// Delete the cache + Clean(clean::Cmd), + /// Show the location of the cache + Path(path::Cmd), + /// Access details about cached actions like transactions, and simulations. + /// (Experimental. May see breaking changes at any time.) + #[command(subcommand)] + Actionlog(actionlog::Cmd), +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Clean(#[from] clean::Error), + #[error(transparent)] + Path(#[from] path::Error), + #[error(transparent)] + Actionlog(#[from] actionlog::Error), +} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + match self { + Cmd::Clean(cmd) => cmd.run()?, + Cmd::Path(cmd) => cmd.run()?, + Cmd::Actionlog(cmd) => cmd.run()?, + }; + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/commands/cache/path.rs b/cmd/soroban-cli/src/commands/cache/path.rs new file mode 100644 index 000000000..337608735 --- /dev/null +++ b/cmd/soroban-cli/src/commands/cache/path.rs @@ -0,0 +1,21 @@ +use super::super::config::locator; +use crate::commands::config::data; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Config(#[from] locator::Error), + #[error(transparent)] + Data(#[from] data::Error), +} + +#[derive(Debug, clap::Parser, Clone)] +#[group(skip)] +pub struct Cmd {} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + println!("{}", data::data_local_dir()?.to_string_lossy()); + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/commands/config/data.rs b/cmd/soroban-cli/src/commands/config/data.rs new file mode 100644 index 000000000..409ef2227 --- /dev/null +++ b/cmd/soroban-cli/src/commands/config/data.rs @@ -0,0 +1,211 @@ +use crate::rpc::{GetTransactionResponse, GetTransactionResponseRaw, SimulateTransactionResponse}; +use directories::ProjectDirs; +use http::Uri; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +use crate::xdr::{self, WriteXdr}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Failed to find project directories")] + FailedToFindProjectDirs, + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), + #[error(transparent)] + Http(#[from] http::uri::InvalidUri), + #[error(transparent)] + Ulid(#[from] ulid::DecodeError), + #[error(transparent)] + Xdr(#[from] xdr::Error), +} + +pub const XDG_DATA_HOME: &str = "XDG_DATA_HOME"; + +pub fn project_dir() -> Result { + std::env::var(XDG_DATA_HOME) + .map_or_else( + |_| ProjectDirs::from("org", "stellar", "stellar-cli"), + |data_home| ProjectDirs::from_path(std::path::PathBuf::from(data_home)), + ) + .ok_or(Error::FailedToFindProjectDirs) +} + +#[allow(clippy::module_name_repetitions)] +pub fn data_local_dir() -> Result { + Ok(project_dir()?.data_local_dir().to_path_buf()) +} + +pub fn actions_dir() -> Result { + let dir = data_local_dir()?.join("actions"); + std::fs::create_dir_all(&dir)?; + Ok(dir) +} + +pub fn spec_dir() -> Result { + let dir = data_local_dir()?.join("spec"); + std::fs::create_dir_all(&dir)?; + Ok(dir) +} + +pub fn write(action: Action, rpc_url: &Uri) -> Result { + let data = Data { + action, + rpc_url: rpc_url.to_string(), + }; + let id = ulid::Ulid::new(); + let file = actions_dir()?.join(id.to_string()).with_extension("json"); + std::fs::write(file, serde_json::to_string(&data)?)?; + Ok(id) +} + +pub fn read(id: &ulid::Ulid) -> Result<(Action, Uri), Error> { + let file = actions_dir()?.join(id.to_string()).with_extension("json"); + let data: Data = serde_json::from_str(&std::fs::read_to_string(file)?)?; + Ok((data.action, http::Uri::from_str(&data.rpc_url)?)) +} + +pub fn write_spec(hash: &str, spec_entries: &[xdr::ScSpecEntry]) -> Result<(), Error> { + let file = spec_dir()?.join(hash); + tracing::trace!("writing spec to {:?}", file); + let mut contents: Vec = Vec::new(); + for entry in spec_entries { + contents.extend(entry.to_xdr(xdr::Limits::none())?); + } + std::fs::write(file, contents)?; + Ok(()) +} + +pub fn read_spec(hash: &str) -> Result, Error> { + let file = spec_dir()?.join(hash); + tracing::trace!("reading spec from {:?}", file); + Ok(soroban_spec::read::parse_raw(&std::fs::read(file)?)?) +} + +pub fn list_ulids() -> Result, Error> { + let dir = actions_dir()?; + let mut list = std::fs::read_dir(dir)? + .map(|entry| { + entry + .map(|e| e.file_name().into_string().unwrap()) + .map_err(Error::from) + }) + .collect::, Error>>()?; + list.sort(); + Ok(list + .iter() + .map(|s| ulid::Ulid::from_str(s.trim_end_matches(".json"))) + .collect::, _>>()?) +} + +pub fn list_actions() -> Result, Error> { + list_ulids()? + .into_iter() + .rev() + .map(|id| { + let (action, uri) = read(&id)?; + Ok(DatedAction(id, action, uri)) + }) + .collect::, Error>>() +} + +pub struct DatedAction(ulid::Ulid, Action, Uri); + +impl std::fmt::Display for DatedAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let (id, a, uri) = (&self.0, &self.1, &self.2); + let datetime = to_datatime(id).format("%b %d %H:%M"); + let status = match a { + Action::Simulate { response } => response + .error + .as_ref() + .map_or_else(|| "SUCCESS".to_string(), |_| "ERROR".to_string()), + Action::Send { response } => response.status.to_string(), + }; + write!(f, "{id} {} {status} {datetime} {uri} ", a.type_str(),) + } +} + +impl DatedAction {} + +fn to_datatime(id: &ulid::Ulid) -> chrono::DateTime { + chrono::DateTime::from_timestamp_millis(id.timestamp_ms().try_into().unwrap()).unwrap() +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +struct Data { + action: Action, + rpc_url: String, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum Action { + Simulate { + response: SimulateTransactionResponse, + }, + Send { + response: GetTransactionResponseRaw, + }, +} + +impl Action { + pub fn type_str(&self) -> String { + match self { + Action::Simulate { .. } => "Simulate", + Action::Send { .. } => "Send ", + } + .to_string() + } +} + +impl From for Action { + fn from(response: SimulateTransactionResponse) -> Self { + Self::Simulate { response } + } +} + +impl TryFrom for Action { + type Error = xdr::Error; + fn try_from(res: GetTransactionResponse) -> Result { + Ok(Self::Send { + response: GetTransactionResponseRaw { + status: res.status, + envelope_xdr: res.envelope.as_ref().map(to_xdr).transpose()?, + result_xdr: res.result.as_ref().map(to_xdr).transpose()?, + result_meta_xdr: res.result_meta.as_ref().map(to_xdr).transpose()?, + }, + }) + } +} + +fn to_xdr(data: &impl WriteXdr) -> Result { + data.to_xdr_base64(xdr::Limits::none()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_write_read() { + let t = assert_fs::TempDir::new().unwrap(); + std::env::set_var(XDG_DATA_HOME, t.path().to_str().unwrap()); + let rpc_uri = http::uri::Uri::from_str("http://localhost:8000").unwrap(); + let sim = SimulateTransactionResponse::default(); + let original_action: Action = sim.into(); + + let id = write(original_action.clone(), &rpc_uri.clone()).unwrap(); + let (action, new_rpc_uri) = read(&id).unwrap(); + assert_eq!(rpc_uri, new_rpc_uri); + match (action, original_action) { + (Action::Simulate { response: a }, Action::Simulate { response: b }) => { + assert_eq!(a.cost.cpu_insns, b.cost.cpu_insns); + } + _ => panic!("Action mismatch"), + } + } +} diff --git a/cmd/soroban-cli/src/commands/config/mod.rs b/cmd/soroban-cli/src/commands/config/mod.rs index efe99f0d8..4d835685b 100644 --- a/cmd/soroban-cli/src/commands/config/mod.rs +++ b/cmd/soroban-cli/src/commands/config/mod.rs @@ -9,6 +9,7 @@ use self::{network::Network, secret::Secret}; use super::{keys, network}; +pub mod data; pub mod locator; pub mod secret; diff --git a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index dbca1940c..41e7367dc 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -12,7 +12,10 @@ use std::convert::Infallible; use std::{array::TryFromSliceError, fmt::Debug, num::ParseIntError}; use crate::{ - commands::{config, global, NetworkRunnable}, + commands::{ + config::{self, data}, + global, network, NetworkRunnable, + }, rpc::{Client, Error as SorobanRpcError}, utils::{contract_id_hash_from_asset, parsing::parse_asset}, }; @@ -35,6 +38,10 @@ pub enum Error { Config(#[from] config::Error), #[error(transparent)] ParseAssetError(#[from] crate::utils::parsing::Error), + #[error(transparent)] + Data(#[from] data::Error), + #[error(transparent)] + Network(#[from] network::Error), } impl From for Error { @@ -71,7 +78,7 @@ impl NetworkRunnable for Cmd { async fn run_against_rpc_server( &self, - _: Option<&global::Args>, + args: Option<&global::Args>, config: Option<&config::Args>, ) -> Result { let config = config.unwrap_or(&self.config); @@ -103,9 +110,13 @@ impl NetworkRunnable for Cmd { )?; let txn = client.create_assembled_transaction(&tx).await?; let txn = self.fee.apply_to_assembled_txn(txn); - client + let get_txn_resp = client .send_assembled_transaction(txn, &key, &[], network_passphrase, None, None) - .await?; + .await? + .try_into()?; + if args.map_or(true, |a| !a.no_cache) { + data::write(get_txn_resp, &network.rpc_uri()?)?; + } Ok(stellar_strkey::Contract(contract_id.0).to_string()) } diff --git a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs index bac7fe2f0..40369b230 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -15,8 +15,9 @@ use soroban_env_host::{ }; use crate::commands::{ + config::data, contract::{self, id::wasm::get_contract_id}, - global, NetworkRunnable, + global, network, NetworkRunnable, }; use crate::{ commands::{config, contract::install, HEADING_RPC}, @@ -91,6 +92,10 @@ pub enum Error { Infallible(#[from] std::convert::Infallible), #[error(transparent)] WasmId(#[from] contract::id::wasm::Error), + #[error(transparent)] + Data(#[from] data::Error), + #[error(transparent)] + Network(#[from] network::Error), } impl Cmd { @@ -166,9 +171,13 @@ impl NetworkRunnable for Cmd { )?; let txn = client.create_assembled_transaction(&txn).await?; let txn = self.fee.apply_to_assembled_txn(txn); - client + let get_txn_resp = client .send_assembled_transaction(txn, &key, &[], &network.network_passphrase, None, None) - .await?; + .await? + .try_into()?; + if global_args.map_or(true, |a| !a.no_cache) { + data::write(get_txn_resp, &network.rpc_uri()?)?; + } Ok(stellar_strkey::Contract(contract_id.0).to_string()) } } diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index bcf8a90d5..ab7834959 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -9,7 +9,10 @@ use soroban_env_host::xdr::{ }; use crate::{ - commands::{config, global, NetworkRunnable}, + commands::{ + config::{self, data}, + global, network, NetworkRunnable, + }, key, rpc::{self, Client}, wasm, Pwd, @@ -75,6 +78,10 @@ pub enum Error { Wasm(#[from] wasm::Error), #[error(transparent)] Key(#[from] key::Error), + #[error(transparent)] + Data(#[from] data::Error), + #[error(transparent)] + Network(#[from] network::Error), } impl Cmd { @@ -108,7 +115,7 @@ impl NetworkRunnable for Cmd { async fn run_against_rpc_server( &self, - _args: Option<&global::Args>, + args: Option<&global::Args>, config: Option<&config::Args>, ) -> Result { let config = config.unwrap_or(&self.config); @@ -158,6 +165,9 @@ impl NetworkRunnable for Cmd { let res = client .prepare_and_send_transaction(&tx, &key, &[], &network.network_passphrase, None, None) .await?; + if args.map_or(true, |a| !a.no_cache) { + data::write(res.clone().try_into()?, &network.rpc_uri()?)?; + } let events = res.events()?; if !events.is_empty() { diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 47a47e1d2..d4a0d541f 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -10,7 +10,8 @@ use soroban_env_host::xdr::{ }; use super::restore; -use crate::commands::{global, NetworkRunnable}; +use crate::commands::network; +use crate::commands::{config::data, global, NetworkRunnable}; use crate::key; use crate::rpc::{self, Client}; use crate::{commands::config, utils, wasm}; @@ -62,6 +63,10 @@ pub enum Error { wasm: std::path::PathBuf, version: String, }, + #[error(transparent)] + Network(#[from] network::Error), + #[error(transparent)] + Data(#[from] data::Error), } impl Cmd { @@ -130,16 +135,17 @@ impl NetworkRunnable for Cmd { .create_assembled_transaction(&tx_without_preflight) .await?; let txn = self.fee.apply_to_assembled_txn(txn); - + let txn_resp = client + .send_assembled_transaction(txn, &key, &[], &network.network_passphrase, None, None) + .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, .. - }) = client - .send_assembled_transaction(txn, &key, &[], &network.network_passphrase, None, None) - .await? - .result - .as_ref() + }) = txn_resp.result.as_ref() { // Now just need to restore it and don't have to install again restore::Cmd { @@ -159,7 +165,9 @@ 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(hash) } } diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index cd42d818f..d40f9c46f 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -12,11 +12,11 @@ use heck::ToKebabCase; use soroban_env_host::{ xdr::{ - self, Error as XdrError, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, - LedgerEntryData, LedgerFootprint, Memo, MuxedAccount, Operation, OperationBody, - Preconditions, ScAddress, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, ScVal, ScVec, - SequenceNumber, SorobanAuthorizationEntry, SorobanResources, Transaction, TransactionExt, - Uint256, VecM, + self, ContractDataEntry, Error as XdrError, Hash, HostFunction, InvokeContractArgs, + InvokeHostFunctionOp, LedgerEntryData, LedgerFootprint, Memo, MuxedAccount, Operation, + OperationBody, Preconditions, ScAddress, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, + ScVal, ScVec, SequenceNumber, SorobanAuthorizationEntry, SorobanResources, Transaction, + TransactionExt, Uint256, VecM, }, HostError, }; @@ -29,7 +29,10 @@ use super::super::{ events, }; use crate::commands::NetworkRunnable; -use crate::{commands::global, rpc, Pwd}; +use crate::{ + commands::{config::data, global, network}, + rpc, Pwd, +}; use soroban_spec_tools::{contract, Spec}; #[derive(Parser, Debug, Default, Clone)] @@ -139,6 +142,12 @@ pub enum Error { ContractSpec(#[from] contract::Error), #[error("")] MissingFileArg(PathBuf), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Data(#[from] data::Error), + #[error(transparent)] + Network(#[from] network::Error), } impl From for Error { @@ -322,8 +331,33 @@ impl NetworkRunnable for Cmd { let account_details = client.get_account(&public_strkey).await?; let sequence: i64 = account_details.seq_num.into(); + let r = client.get_contract_data(&contract_id).await?; + tracing::trace!("{r:?}"); + let ContractDataEntry { + val: xdr::ScVal::ContractInstance(xdr::ScContractInstance { executable, .. }), + .. + } = r + else { + return Err(Error::MissingResult); + }; // Get the contract - let spec_entries = client.get_remote_contract_spec(&contract_id).await?; + let spec_entries = match executable { + xdr::ContractExecutable::Wasm(hash) => { + let hash = hash.to_string(); + if let Ok(entries) = data::read_spec(&hash) { + entries + } else { + let res = client.get_remote_contract_spec(&contract_id).await?; + if global_args.map_or(true, |a| !a.no_cache) { + data::write_spec(&hash, &res)?; + } + res + } + } + xdr::ContractExecutable::StellarAsset => { + soroban_spec::read::parse_raw(&soroban_sdk::token::StellarAssetSpec::spec_xdr())? + } + }; // Get the ledger footprint let (function, spec, host_function_params, signers) = @@ -336,15 +370,17 @@ impl NetworkRunnable for Cmd { )?; let txn = client.create_assembled_transaction(&tx).await?; let txn = self.fee.apply_to_assembled_txn(txn); + let sim_res = txn.sim_response(); + if global_args.map_or(true, |a| !a.no_cache) { + data::write(sim_res.clone().into(), &network.rpc_uri()?)?; + } let (return_value, events) = if self.is_view() { - ( - txn.sim_response().results()?[0].xdr.clone(), - txn.sim_response().events()?, - ) + (sim_res.results()?[0].xdr.clone(), sim_res.events()?) } else { let global::Args { verbose, very_verbose, + no_cache, .. } = global_args.map(Clone::clone).unwrap_or_default(); let res = client @@ -357,6 +393,9 @@ impl NetworkRunnable for Cmd { (verbose || very_verbose || self.fee.cost).then_some(log_resources), ) .await?; + if !no_cache { + data::write(res.clone().try_into()?, &network.rpc_uri()?)?; + } (res.return_value()?, res.contract_events()?) }; diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index 869d26015..8b5921a1a 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -11,9 +11,9 @@ use stellar_strkey::DecodeError; use crate::{ commands::{ - config::{self, locator}, + config::{self, data, locator}, contract::extend, - global, NetworkRunnable, + global, network, NetworkRunnable, }, key, rpc::{self, Client}, @@ -83,6 +83,10 @@ pub enum Error { Key(#[from] key::Error), #[error(transparent)] Extend(#[from] extend::Error), + #[error(transparent)] + Data(#[from] data::Error), + #[error(transparent)] + Network(#[from] network::Error), } impl Cmd { @@ -115,7 +119,7 @@ impl NetworkRunnable for Cmd { async fn run_against_rpc_server( &self, - _: Option<&global::Args>, + args: Option<&global::Args>, config: Option<&config::Args>, ) -> Result { let config = config.unwrap_or(&self.config); @@ -162,7 +166,9 @@ impl NetworkRunnable for Cmd { let res = client .prepare_and_send_transaction(&tx, &key, &[], &network.network_passphrase, None, None) .await?; - + if args.map_or(true, |a| !a.no_cache) { + data::write(res.clone().try_into()?, &network.rpc_uri()?)?; + } let meta = res .result_meta .as_ref() diff --git a/cmd/soroban-cli/src/commands/global.rs b/cmd/soroban-cli/src/commands/global.rs index c606bd1bd..bb530f043 100644 --- a/cmd/soroban-cli/src/commands/global.rs +++ b/cmd/soroban-cli/src/commands/global.rs @@ -29,6 +29,10 @@ pub struct Args { /// List installed plugins. E.g. `soroban-hello` #[arg(long)] pub list: bool, + + /// Do not cache your simulations and transactions + #[arg(long, env = "SOROBAN_NO_CACHE")] + pub no_cache: bool, } #[derive(thiserror::Error, Debug)] diff --git a/cmd/soroban-cli/src/commands/mod.rs b/cmd/soroban-cli/src/commands/mod.rs index cd0aeadae..4bb5dcb53 100644 --- a/cmd/soroban-cli/src/commands/mod.rs +++ b/cmd/soroban-cli/src/commands/mod.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use async_trait::async_trait; use clap::{command, error::ErrorKind, CommandFactory, FromArgMatches, Parser}; +pub mod cache; pub mod completion; pub mod config; pub mod contract; @@ -98,6 +99,7 @@ impl Root { Cmd::Network(network) => network.run().await?, Cmd::Version(version) => version.run(), Cmd::Keys(id) => id.run().await?, + Cmd::Cache(data) => data.run()?, }; Ok(()) } @@ -131,6 +133,9 @@ pub enum Cmd { Network(network::Cmd), /// Print version information Version(version::Cmd), + /// Cache for tranasctions and contract specs + #[command(subcommand)] + Cache(cache::Cmd), } #[derive(thiserror::Error, Debug)] @@ -150,6 +155,8 @@ pub enum Error { Plugin(#[from] plugin::Error), #[error(transparent)] Network(#[from] network::Error), + #[error(transparent)] + Cache(#[from] cache::Error), } #[async_trait] diff --git a/cmd/soroban-cli/src/commands/network/mod.rs b/cmd/soroban-cli/src/commands/network/mod.rs index 8e0059596..e56d99068 100644 --- a/cmd/soroban-cli/src/commands/network/mod.rs +++ b/cmd/soroban-cli/src/commands/network/mod.rs @@ -179,7 +179,10 @@ impl Network { .build()?) } else { let client = Client::new(&self.rpc_url)?; + let network = client.get_network().await?; + tracing::debug!("network {network:?}"); let uri = client.friendbot_url().await?; + tracing::debug!("URI {uri:?}"); Uri::from_str(&format!("{uri}?addr={addr}")).map_err(|e| { tracing::error!("{e}"); Error::InvalidUrl(uri.to_string()) @@ -217,6 +220,10 @@ impl Network { } Ok(()) } + + pub fn rpc_uri(&self) -> Result { + http::Uri::from_str(&self.rpc_url).map_err(|_| Error::InvalidUrl(self.rpc_url.to_string())) + } } impl Network { diff --git a/cmd/soroban-cli/src/lib.rs b/cmd/soroban-cli/src/lib.rs index ef443853b..d4118a6be 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -3,6 +3,7 @@ clippy::must_use_candidate, clippy::missing_panics_doc )] +pub(crate) use soroban_env_host::xdr; pub(crate) use soroban_rpc as rpc; use std::path::Path; diff --git a/docs/soroban-cli-full-docs.md b/docs/soroban-cli-full-docs.md index 40d63624e..766a7798e 100644 --- a/docs/soroban-cli-full-docs.md +++ b/docs/soroban-cli-full-docs.md @@ -51,6 +51,12 @@ This document contains the help content for the `soroban` command-line program. * [`soroban network start`↴](#soroban-network-start) * [`soroban network stop`↴](#soroban-network-stop) * [`soroban version`↴](#soroban-version) +* [`soroban cache`↴](#soroban-cache) +* [`soroban cache clean`↴](#soroban-cache-clean) +* [`soroban cache path`↴](#soroban-cache-path) +* [`soroban cache actionlog`↴](#soroban-cache-actionlog) +* [`soroban cache actionlog ls`↴](#soroban-cache-actionlog-ls) +* [`soroban cache actionlog read`↴](#soroban-cache-actionlog-read) ## `soroban` @@ -90,6 +96,7 @@ Full CLI reference: https://github.com/stellar/soroban-tools/tree/main/docs/soro * `xdr` — Decode and encode XDR * `network` — Start and configure networks * `version` — Print version information +* `cache` — Cache for tranasctions and contract specs ###### **Options:** @@ -115,6 +122,10 @@ Full CLI reference: https://github.com/stellar/soroban-tools/tree/main/docs/soro Possible values: `true`, `false` +* `--no-cache` — Do not cache your simulations and transactions + + Possible values: `true`, `false` + @@ -1255,6 +1266,81 @@ Print version information +## `soroban cache` + +Cache for tranasctions and contract specs + +**Usage:** `soroban cache ` + +###### **Subcommands:** + +* `clean` — Delete the cache +* `path` — Show the location of the cache +* `actionlog` — Access details about cached actions like transactions, and simulations. (Experimental. May see breaking changes at any time.) + + + +## `soroban cache clean` + +Delete the cache + +**Usage:** `soroban cache clean` + + + +## `soroban cache path` + +Show the location of the cache + +**Usage:** `soroban cache path` + + + +## `soroban cache actionlog` + +Access details about cached actions like transactions, and simulations. (Experimental. May see breaking changes at any time.) + +**Usage:** `soroban cache actionlog ` + +###### **Subcommands:** + +* `ls` — List cached actions (transactions, simulations) +* `read` — Read cached action + + + +## `soroban cache actionlog ls` + +List cached actions (transactions, simulations) + +**Usage:** `soroban cache actionlog ls [OPTIONS]` + +###### **Options:** + +* `--global` — Use global config + + Possible values: `true`, `false` + +* `--config-dir ` — Location of config directory, default is "." +* `-l`, `--long` + + Possible values: `true`, `false` + + + + +## `soroban cache actionlog read` + +Read cached action + +**Usage:** `soroban cache actionlog read --id ` + +###### **Options:** + +* `--id ` — ID of the cache entry + + +