diff --git a/Cargo.lock b/Cargo.lock index c126a7fac6..0023793d5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -816,13 +816,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]] @@ -836,6 +845,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" @@ -2704,6 +2725,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" @@ -3604,6 +3631,7 @@ dependencies = [ "clap_complete", "crate-git-revision 0.0.4", "csv", + "directories", "dirs", "dotenvy", "ed25519-dalek 2.0.0", @@ -3640,7 +3668,7 @@ dependencies = [ "soroban-spec", "soroban-spec-json", "soroban-spec-rust", - "soroban-spec-tools 20.3.1", + "soroban-spec-tools", "soroban-spec-typescript", "stellar-strkey 0.0.7", "stellar-xdr", @@ -3657,6 +3685,7 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", + "ulid", "ureq", "wasm-opt", "wasmparser 0.90.0", @@ -3754,8 +3783,7 @@ dependencies = [ [[package]] name = "soroban-rpc" version = "20.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06a502143406eb8ca6d9e98b6ea50928a5eb47a216a94769bfa13d19c03d41c4" +source = "git+https://github.com/stellar/soroban-rpc?rev=e84d0154d8ad416d64ac3a2c16d81f962595b5ea#e84d0154d8ad416d64ac3a2c16d81f962595b5ea" dependencies = [ "base64 0.21.7", "clap", @@ -3773,7 +3801,6 @@ dependencies = [ "soroban-env-host", "soroban-sdk", "soroban-spec", - "soroban-spec-tools 20.3.3", "stellar-strkey 0.0.7", "stellar-xdr", "termcolor", @@ -3885,25 +3912,6 @@ dependencies = [ "which", ] -[[package]] -name = "soroban-spec-tools" -version = "20.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5caac0698a57c2e1c5faaec5dfaa2ce511b822e863500ef225cc34203d0b6a4" -dependencies = [ - "base64 0.21.7", - "ethnum", - "hex", - "itertools 0.10.5", - "serde_json", - "soroban-env-host", - "soroban-spec", - "stellar-strkey 0.0.7", - "stellar-xdr", - "thiserror", - "wasmparser 0.90.0", -] - [[package]] name = "soroban-spec-typescript" version = "20.3.1" @@ -3942,7 +3950,7 @@ dependencies = [ "soroban-rpc", "soroban-sdk", "soroban-spec", - "soroban-spec-tools 20.3.1", + "soroban-spec-tools", "stellar-strkey 0.0.7", "thiserror", "tokio", @@ -4552,6 +4560,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" @@ -4846,6 +4866,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 aad28acebe..41a149b63e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,7 +70,8 @@ path = "cmd/soroban-cli" [workspace.dependencies.soroban-rpc] version = "=20.3.3" -# git = "https://github.com/stellar/soroban-rpc" +git = "https://github.com/stellar/soroban-rpc" +rev = "e84d0154d8ad416d64ac3a2c16d81f962595b5ea" [workspace.dependencies.stellar-xdr] version = "=20.1.0" @@ -103,6 +104,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/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index f5ef2b1b01..460d43fef1 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -95,6 +95,11 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] } cargo_metadata = "0.15.4" pathdiff = "0.2.1" dotenvy = "0.15.7" +directories = { workspace = true } +# For unique identifiers +ulid.workspace = true +# For unique identifiers +ulid.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/config/data.rs b/cmd/soroban-cli/src/commands/config/data.rs new file mode 100644 index 0000000000..121b9fd38b --- /dev/null +++ b/cmd/soroban-cli/src/commands/config/data.rs @@ -0,0 +1,138 @@ +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")] + FiledToFindProjectDirs, + #[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), +} + +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("com", "stellar", "soroban-cli"), + |data_home| ProjectDirs::from_path(std::path::PathBuf::from(data_home)), + ) + .ok_or(Error::FiledToFindProjectDirs) +} + +pub fn actions_dir() -> Result { + let dir = project_dir()?.data_local_dir().join("actions"); + 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 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)) + .collect::, _>>()?) +} + +pub fn list_actions() -> Result, Error> { + list_ulids()?.into_iter() + .map(|id| { + let (action, uri) = read(&id)?; + Ok((id, action, uri)) + }) + .collect::,Error>>() +} + +#[derive(Serialize, Deserialize)] +struct Data { + action: Action, + rpc_url: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub enum Action { + Simulation(SimulateTransactionResponse), + Transaction(GetTransactionResponseRaw), +} + +impl From for Action { + fn from(res: SimulateTransactionResponse) -> Self { + Self::Simulation(res) + } +} + +impl TryFrom for Action { + type Error = xdr::Error; + fn try_from(res: GetTransactionResponse) -> Result { + Ok(Self::Transaction(GetTransactionResponseRaw { + status: res.status, + envelope_xdr: res.envelope.map(to_xdr).transpose()?, + result_xdr: res.result.map(to_xdr).transpose()?, + result_meta_xdr: res.result_meta.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::Simulation(a), Action::Simulation(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 3c5f4cca45..0125ff115d 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/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index cd42d818f8..c68a4f9c14 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -29,7 +29,7 @@ use super::super::{ events, }; use crate::commands::NetworkRunnable; -use crate::{commands::global, rpc, Pwd}; +use crate::{commands::{global, config::data, network}, rpc, Pwd}; use soroban_spec_tools::{contract, Spec}; #[derive(Parser, Debug, Default, Clone)] @@ -139,6 +139,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 { @@ -336,10 +342,13 @@ 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(); + 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 { @@ -357,6 +366,7 @@ impl NetworkRunnable for Cmd { (verbose || very_verbose || self.fee.cost).then_some(log_resources), ) .await?; + data::write(res.clone().try_into()?, network.rpc_uri()?)?; (res.return_value()?, res.contract_events()?) }; diff --git a/cmd/soroban-cli/src/commands/data/ls.rs b/cmd/soroban-cli/src/commands/data/ls.rs new file mode 100644 index 0000000000..da2a3960a5 --- /dev/null +++ b/cmd/soroban-cli/src/commands/data/ls.rs @@ -0,0 +1,60 @@ +use clap::command; + +use crate::commands::config::data::{self, Action}; + +use super::super::config::locator; + +#[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> { + data::list_actions()? + .iter() + .map(|(id, action, uri)| { + Ok(format!( + "{} {} {uri}\n", + to_datatime(id), + action_type(action) + )) + }) + .collect() + } + + pub fn ls_l(&self) -> Result, Error> { + todo!() + } +} + +fn to_datatime(id: &ulid::Ulid) -> chrono::DateTime { + chrono::DateTime::from_timestamp_millis(id.timestamp_ms().try_into().unwrap()).unwrap() +} + +fn action_type(a: &Action) -> String { + match a { + Action::Simulation(_) => "Simulation", + Action::Transaction(_) => "Transaction", + } + .to_string() +} diff --git a/cmd/soroban-cli/src/commands/data/mod.rs b/cmd/soroban-cli/src/commands/data/mod.rs new file mode 100644 index 0000000000..923426f429 --- /dev/null +++ b/cmd/soroban-cli/src/commands/data/mod.rs @@ -0,0 +1 @@ +pub mod ls; \ No newline at end of file diff --git a/cmd/soroban-cli/src/commands/mod.rs b/cmd/soroban-cli/src/commands/mod.rs index 4924ef5ee0..4b14b45bfe 100644 --- a/cmd/soroban-cli/src/commands/mod.rs +++ b/cmd/soroban-cli/src/commands/mod.rs @@ -6,6 +6,7 @@ use clap::{command, error::ErrorKind, CommandFactory, FromArgMatches, Parser}; pub mod completion; pub mod config; pub mod contract; +pub mod data; pub mod events; pub mod global; pub mod keys; diff --git a/cmd/soroban-cli/src/commands/network/mod.rs b/cmd/soroban-cli/src/commands/network/mod.rs index b4a145f131..3e8216dfab 100644 --- a/cmd/soroban-cli/src/commands/network/mod.rs +++ b/cmd/soroban-cli/src/commands/network/mod.rs @@ -180,7 +180,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()) @@ -225,6 +228,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 ef443853bc..1a47e79670 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -4,6 +4,7 @@ clippy::missing_panics_doc )] pub(crate) use soroban_rpc as rpc; +pub(crate) use soroban_env_host::xdr; use std::path::Path; pub mod commands;