diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index ace89c45e..cf0af281a 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -414,9 +414,9 @@ Outputs no data when no data is present in the contract. ###### **Options:** -* `--wasm ` — Wasm file to extract the meta from -* `--wasm-hash ` — Wasm hash to get the meta for -* `--id ` — Contract ID to get the meta for +* `--wasm ` — Wasm file to extract the data from +* `--wasm-hash ` — Wasm hash to get the data for +* `--id ` — Contract id to get the data for * `--output ` — Format of the output Default value: `xdr-base64` @@ -429,6 +429,11 @@ Outputs no data when no data is present in the contract. - `json-formatted`: Formatted JSON output of the info entry +* `--rpc-url ` — RPC server endpoint +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `--network ` — Name of network to use from config +* `--global` — Use global config +* `--config-dir ` — Location of config directory, default is "." @@ -446,9 +451,9 @@ Outputs no data when no data is present in the contract. ###### **Options:** -* `--wasm ` — Wasm file to extract the interface from -* `--wasm-hash ` — Wasm hash to get the interface for -* `--id ` — Format of the output +* `--wasm ` — Wasm file to extract the data from +* `--wasm-hash ` — Wasm hash to get the data for +* `--id ` — Contract id to get the data for * `--output ` — Format of the output Default value: `xdr-base64` @@ -461,6 +466,11 @@ Outputs no data when no data is present in the contract. - `json-formatted`: Formatted JSON output of the info entry +* `--rpc-url ` — RPC server endpoint +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `--network ` — Name of network to use from config +* `--global` — Use global config +* `--config-dir ` — Location of config directory, default is "." @@ -478,9 +488,9 @@ Outputs no data when no data is present in the contract. ###### **Options:** -* `--wasm ` — Wasm file to extract the meta from -* `--wasm-hash ` — Wasm hash to get the meta for -* `--id ` — Format of the output +* `--wasm ` — Wasm file to extract the data from +* `--wasm-hash ` — Wasm hash to get the data for +* `--id ` — Contract id to get the data for * `--output ` — Format of the output Default value: `xdr-base64` @@ -493,6 +503,11 @@ Outputs no data when no data is present in the contract. - `json-formatted`: Formatted JSON output of the info entry +* `--rpc-url ` — RPC server endpoint +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `--network ` — Name of network to use from config +* `--global` — Use global config +* `--config-dir ` — Location of config directory, default is "." diff --git a/cmd/crates/soroban-spec-tools/src/contract.rs b/cmd/crates/soroban-spec-tools/src/contract.rs index bca72f533..334b2425f 100644 --- a/cmd/crates/soroban-spec-tools/src/contract.rs +++ b/cmd/crates/soroban-spec-tools/src/contract.rs @@ -77,14 +77,11 @@ impl Spec { vec![] }; - let mut spec_base64 = None; - let spec = if let Some(spec) = spec { - spec_base64 = Some(base64.encode(spec)); - let cursor = Cursor::new(spec); - let mut read = Limited::new(cursor, Limits::none()); - ScSpecEntry::read_xdr_iter(&mut read).collect::, xdr::Error>>()? + let (spec_base64, spec) = if let Some(spec) = spec { + let (spec_base64, spec) = Spec::spec_to_base64(spec)?; + (Some(spec_base64), spec) } else { - vec![] + (None, vec![]) }; Ok(Spec { @@ -106,6 +103,16 @@ impl Spec { .join(",\n"); Ok(format!("[{spec}]")) } + + pub fn spec_to_base64(spec: &[u8]) -> Result<(String, Vec), Error> { + let spec_base64 = base64.encode(spec); + let cursor = Cursor::new(spec); + let mut read = Limited::new(cursor, Limits::none()); + Ok(( + spec_base64, + ScSpecEntry::read_xdr_iter(&mut read).collect::, xdr::Error>>()?, + )) + } } impl Display for Spec { diff --git a/cmd/soroban-cli/src/commands/contract/fetch.rs b/cmd/soroban-cli/src/commands/contract/fetch.rs index 830db5cd4..fd8070898 100644 --- a/cmd/soroban-cli/src/commands/contract/fetch.rs +++ b/cmd/soroban-cli/src/commands/contract/fetch.rs @@ -6,19 +6,13 @@ use std::str::FromStr; use std::{fmt::Debug, fs, io}; use clap::{arg, command, Parser}; -use stellar_xdr::curr::{ContractDataEntry, ContractExecutable, ScVal}; -use crate::commands::contract::fetch::Error::{ContractIsStellarAsset, UnexpectedContractToken}; use crate::commands::{global, NetworkRunnable}; use crate::config::{ self, locator, network::{self, Network}, }; -use crate::utils::rpc::get_remote_wasm_from_hash; -use crate::{ - rpc::{self, Client}, - Pwd, -}; +use crate::{wasm, Pwd}; #[derive(Parser, Debug, Default, Clone)] #[allow(clippy::struct_excessive_bools)] @@ -53,8 +47,6 @@ impl Pwd for Cmd { #[derive(thiserror::Error, Debug)] pub enum Error { - #[error(transparent)] - Rpc(#[from] rpc::Error), #[error(transparent)] Config(#[from] config::Error), #[error(transparent)] @@ -67,13 +59,8 @@ pub enum Error { Network(#[from] network::Error), #[error("cannot create contract directory for {0:?}")] CannotCreateContractDir(PathBuf), - #[error("unexpected contract data {0:?}")] - UnexpectedContractToken(ContractDataEntry), - #[error( - "cannot fetch wasm for contract because the contract is \ - a network built-in asset contract that does not have a downloadable code binary" - )] - ContractIsStellarAsset(), + #[error(transparent)] + Wasm(#[from] wasm::Error), } impl From for Error { @@ -110,14 +97,6 @@ impl Cmd { pub fn network(&self) -> Result { Ok(self.network.get(&self.locator)?) } - - fn contract_id(&self) -> Result<[u8; 32], Error> { - let network = self.network()?; - Ok(self - .locator - .resolve_contract_id(&self.contract_id, &network.network_passphrase)? - .0) - } } #[async_trait::async_trait] @@ -130,21 +109,6 @@ impl NetworkRunnable for Cmd { config: Option<&config::Args>, ) -> Result, Error> { let network = config.map_or_else(|| self.network(), |c| Ok(c.get_network()?))?; - tracing::trace!(?network); - let contract_id = self.contract_id()?; - let client = Client::new(&network.rpc_url)?; - client - .verify_network_passphrase(Some(&network.network_passphrase)) - .await?; - let data_entry = client.get_contract_data(&contract_id).await?; - if let ScVal::ContractInstance(contract) = &data_entry.val { - return match &contract.executable { - ContractExecutable::Wasm(hash) => { - Ok(get_remote_wasm_from_hash(&client, hash).await?) - } - ContractExecutable::StellarAsset => Err(ContractIsStellarAsset()), - }; - } - return Err(UnexpectedContractToken(data_entry)); + return Ok(wasm::fetch_from_contract(&self.contract_id, &network, &self.locator).await?); } } diff --git a/cmd/soroban-cli/src/commands/contract/info.rs b/cmd/soroban-cli/src/commands/contract/info.rs index cb9f67e6c..a6606716c 100644 --- a/cmd/soroban-cli/src/commands/contract/info.rs +++ b/cmd/soroban-cli/src/commands/contract/info.rs @@ -3,6 +3,7 @@ use std::fmt::Debug; pub mod env_meta; pub mod interface; pub mod meta; +mod shared; #[derive(Debug, clap::Subcommand)] pub enum Cmd { diff --git a/cmd/soroban-cli/src/commands/contract/info/env_meta.rs b/cmd/soroban-cli/src/commands/contract/info/env_meta.rs index 828b15034..b34610767 100644 --- a/cmd/soroban-cli/src/commands/contract/info/env_meta.rs +++ b/cmd/soroban-cli/src/commands/contract/info/env_meta.rs @@ -1,41 +1,55 @@ -use std::{fmt::Debug, path::PathBuf}; +use std::fmt::Debug; +use crate::commands::contract::info::env_meta::Error::{NoEnvMetaPresent, NoSACEnvMeta}; +use crate::commands::contract::info::shared; +use crate::commands::contract::info::shared::fetch_wasm; use clap::{command, Parser}; +use soroban_spec_tools::contract; +use soroban_spec_tools::contract::Spec; use crate::commands::contract::InfoOutput; #[derive(Parser, Debug, Clone)] -#[command(group( - clap::ArgGroup::new("src") - .required(true) - .args(& ["wasm", "wasm_hash", "contract_id"]), -))] -#[group(skip)] pub struct Cmd { - /// Wasm file to extract the meta from - #[arg( - long, - conflicts_with = "wasm_hash", - conflicts_with = "contract_id", - group = "src" - )] - pub wasm: Option, - /// Wasm hash to get the meta for - #[arg(long = "wasm-hash", group = "src")] - pub wasm_hash: Option, - /// Contract ID to get the meta for - #[arg(long = "id", env = "STELLAR_CONTRACT_ID", group = "src")] - pub contract_id: Option, - /// Format of the output - #[arg(long, default_value = "xdr-base64")] - output: InfoOutput, + #[command(flatten)] + pub common: shared::Args, } #[derive(thiserror::Error, Debug)] -pub enum Error {} +pub enum Error { + #[error(transparent)] + Wasm(#[from] shared::Error), + #[error(transparent)] + Spec(#[from] contract::Error), + #[error("Stellar asset contract doesn't contain meta information")] + NoSACEnvMeta(), + #[error("no meta present in provided WASM file")] + NoEnvMetaPresent(), +} impl Cmd { pub async fn run(&self) -> Result { - Ok("env_meta".to_string()) // TODO + let bytes = fetch_wasm(&self.common).await?; + + if bytes.is_none() { + return Err(NoSACEnvMeta()); + } + let spec = Spec::new(&bytes.unwrap())?; + + if spec.env_meta_base64.is_none() { + return Err(NoEnvMetaPresent()); + } + + let res = match self.common.output { + InfoOutput::XdrBase64 => spec.env_meta_base64.unwrap(), + InfoOutput::Json => { + unreachable!("TODO") + } + InfoOutput::JsonFormatted => { + unreachable!("TODO") + } + }; + + Ok(res) } } diff --git a/cmd/soroban-cli/src/commands/contract/info/interface.rs b/cmd/soroban-cli/src/commands/contract/info/interface.rs index 4d50b0247..8aaf9db72 100644 --- a/cmd/soroban-cli/src/commands/contract/info/interface.rs +++ b/cmd/soroban-cli/src/commands/contract/info/interface.rs @@ -1,42 +1,58 @@ use std::fmt::Debug; -use std::path::PathBuf; +use crate::commands::contract::info::interface::Error::NoInterfacePresent; +use crate::commands::contract::info::shared; +use crate::commands::contract::info::shared::fetch_wasm; use clap::{command, Parser}; +use soroban_spec_tools::contract; +use soroban_spec_tools::contract::Spec; use crate::commands::contract::InfoOutput; #[derive(Parser, Debug, Clone)] -#[command(group( - clap::ArgGroup::new("src") - .required(true) - .args(& ["wasm", "wasm_hash", "contract_id"]), -))] -#[group(skip)] pub struct Cmd { - /// Wasm file to extract the interface from - #[arg( - long, - conflicts_with = "wasm_hash", - conflicts_with = "contract_id", - group = "src" - )] - pub wasm: Option, - /// Wasm hash to get the interface for - #[arg(long = "wasm-hash", group = "src")] - pub wasm_hash: Option, - /// Format of the output - #[arg(long = "id", env = "STELLAR_CONTRACT_ID", group = "src")] - pub contract_id: Option, - /// Format of the output - #[arg(long, default_value = "xdr-base64")] - output: InfoOutput, + #[command(flatten)] + pub common: shared::Args, } #[derive(thiserror::Error, Debug)] -pub enum Error {} +pub enum Error { + #[error(transparent)] + Wasm(#[from] shared::Error), + #[error(transparent)] + Spec(#[from] contract::Error), + #[error("no interface present in provided WASM file")] + NoInterfacePresent(), +} impl Cmd { pub async fn run(&self) -> Result { - Ok("interface".to_string()) // TODO + let bytes = fetch_wasm(&self.common).await?; + + let base64 = if bytes.is_none() { + let res = Spec::spec_to_base64(&soroban_sdk::token::StellarAssetSpec::spec_xdr())?; + + res.0 + } else { + let spec = Spec::new(&bytes.unwrap())?; + + if spec.env_meta_base64.is_none() { + return Err(NoInterfacePresent()); + } + + spec.spec_base64.unwrap() + }; + + let res = match self.common.output { + InfoOutput::XdrBase64 => base64, + InfoOutput::Json => { + unreachable!("TODO") + } + InfoOutput::JsonFormatted => { + unreachable!("TODO") + } + }; + + Ok(res) } } diff --git a/cmd/soroban-cli/src/commands/contract/info/meta.rs b/cmd/soroban-cli/src/commands/contract/info/meta.rs index 45dbe6796..e6f5b5f02 100644 --- a/cmd/soroban-cli/src/commands/contract/info/meta.rs +++ b/cmd/soroban-cli/src/commands/contract/info/meta.rs @@ -1,42 +1,56 @@ use std::fmt::Debug; -use std::path::PathBuf; +use crate::commands::contract::info::meta::Error::{NoMetaPresent, NoSACMeta}; +use crate::commands::contract::info::shared; +use crate::commands::contract::info::shared::fetch_wasm; use clap::{command, Parser}; +use soroban_spec_tools::contract; +use soroban_spec_tools::contract::Spec; +// use crate::commands::contract::info::shared::fetch_wasm; use crate::commands::contract::InfoOutput; #[derive(Parser, Debug, Clone)] -#[command(group( - clap::ArgGroup::new("src") - .required(true) - .args(& ["wasm", "wasm_hash", "contract_id"]), -))] -#[group(skip)] pub struct Cmd { - /// Wasm file to extract the meta from - #[arg( - long, - conflicts_with = "wasm_hash", - conflicts_with = "contract_id", - group = "src" - )] - pub wasm: Option, - /// Wasm hash to get the meta for - #[arg(long = "wasm-hash", group = "src")] - pub wasm_hash: Option, - /// Format of the output - #[arg(long = "id", env = "STELLAR_CONTRACT_ID", group = "src")] - pub contract_id: Option, - /// Format of the output - #[arg(long, default_value = "xdr-base64")] - output: InfoOutput, + #[command(flatten)] + pub common: shared::Args, } #[derive(thiserror::Error, Debug)] -pub enum Error {} +pub enum Error { + #[error(transparent)] + Wasm(#[from] shared::Error), + #[error(transparent)] + Spec(#[from] contract::Error), + #[error("Stellar asset contract doesn't contain meta information")] + NoSACMeta(), + #[error("no meta present in provided WASM file")] + NoMetaPresent(), +} impl Cmd { pub async fn run(&self) -> Result { - Ok("meta".to_string()) // TODO + let bytes = fetch_wasm(&self.common).await?; + + if bytes.is_none() { + return Err(NoSACMeta()); + } + let spec = Spec::new(&bytes.unwrap())?; + + if spec.meta_base64.is_none() { + return Err(NoMetaPresent()); + } + + let res = match self.common.output { + InfoOutput::XdrBase64 => spec.meta_base64.unwrap(), + InfoOutput::Json => { + unreachable!("TODO") + } + InfoOutput::JsonFormatted => { + unreachable!("TODO") + } + }; + + Ok(res) } } diff --git a/cmd/soroban-cli/src/commands/contract/info/shared.rs b/cmd/soroban-cli/src/commands/contract/info/shared.rs new file mode 100644 index 000000000..2c9f9b9aa --- /dev/null +++ b/cmd/soroban-cli/src/commands/contract/info/shared.rs @@ -0,0 +1,82 @@ +use std::path::PathBuf; + +use clap::arg; +use soroban_env_host::xdr; +use soroban_rpc::Client; + +use crate::commands::contract::info::shared::Error::InvalidWasmHash; +use crate::commands::contract::InfoOutput; +use crate::config::{locator, network}; +use crate::utils::rpc::get_remote_wasm_from_hash; +use crate::wasm; +use crate::wasm::Error::ContractIsStellarAsset; + +#[derive(Debug, clap::Args, Clone, Default)] +#[command(group( + clap::ArgGroup::new("src") + .required(true) + .args(& ["wasm", "wasm_hash", "contract_id"]), +))] +#[group(skip)] +pub struct Args { + /// Wasm file to extract the data from + #[arg(long, group = "src")] + pub wasm: Option, + /// Wasm hash to get the data for + #[arg(long = "wasm-hash", group = "src")] + pub wasm_hash: Option, + /// Contract id to get the data for + #[arg(long = "id", env = "STELLAR_CONTRACT_ID", group = "src")] + pub contract_id: Option, + /// Format of the output + #[arg(long, default_value = "xdr-base64")] + pub output: InfoOutput, + #[command(flatten)] + pub network: network::Args, + #[command(flatten)] + pub locator: locator::Args, +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Network(#[from] network::Error), + #[error(transparent)] + Wasm(#[from] wasm::Error), + #[error("provided wasm hash is invalid {0:?}")] + InvalidWasmHash(String), + #[error(transparent)] + Rpc(#[from] soroban_rpc::Error), +} + +pub async fn fetch_wasm(args: &Args) -> Result>, Error> { + let network = &args.network.get(&args.locator)?; + + let wasm = if let Some(path) = &args.wasm { + wasm::Args { wasm: path.clone() }.read()? + } else if let Some(wasm_hash) = &args.wasm_hash { + let hash = hex::decode(wasm_hash) + .map_err(|_| InvalidWasmHash(wasm_hash.clone()))? + .try_into() + .map_err(|_| InvalidWasmHash(wasm_hash.clone()))?; + + let hash = xdr::Hash(hash); + + let client = Client::new(&network.rpc_url)?; + client + .verify_network_passphrase(Some(&network.network_passphrase)) + .await?; + + get_remote_wasm_from_hash(&client, &hash).await? + } else if let Some(contract_id) = &args.contract_id { + let res = wasm::fetch_from_contract(contract_id, network, &args.locator).await; + if let Some(ContractIsStellarAsset()) = res.as_ref().err() { + return Ok(None); + } + res? + } else { + unreachable!("One of contract location arguments must be passed"); + }; + + Ok(Some(wasm)) +} diff --git a/cmd/soroban-cli/src/commands/contract/mod.rs b/cmd/soroban-cli/src/commands/contract/mod.rs index b327ece9a..54d83926d 100644 --- a/cmd/soroban-cli/src/commands/contract/mod.rs +++ b/cmd/soroban-cli/src/commands/contract/mod.rs @@ -175,9 +175,10 @@ pub enum SpecOutput { Docs, } -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum, Default)] pub enum InfoOutput { /// XDR output of the info entry + #[default] XdrBase64, /// JSON output of the info entry (not formatted) Json, diff --git a/cmd/soroban-cli/src/config/network.rs b/cmd/soroban-cli/src/config/network.rs index 8599c0a3d..9e1eabee3 100644 --- a/cmd/soroban-cli/src/config/network.rs +++ b/cmd/soroban-cli/src/config/network.rs @@ -60,6 +60,7 @@ pub struct Args { #[arg( long, required_unless_present = "rpc_url", + required_unless_present = "network_passphrase", env = "STELLAR_NETWORK", help_heading = HEADING_RPC, )] diff --git a/cmd/soroban-cli/src/wasm.rs b/cmd/soroban-cli/src/wasm.rs index 6f6daf462..8ae897598 100644 --- a/cmd/soroban-cli/src/wasm.rs +++ b/cmd/soroban-cli/src/wasm.rs @@ -1,14 +1,21 @@ +use crate::config::locator; +use crate::config::network::Network; use clap::arg; use sha2::{Digest, Sha256}; use soroban_env_host::xdr::{self, Hash, LedgerKey, LedgerKeyContractCode}; +use soroban_rpc::Client; use soroban_spec_tools::contract::{self, Spec}; use std::{ fs, io, path::{Path, PathBuf}, }; +use stellar_xdr::curr::{ContractDataEntry, ContractExecutable, ScVal}; +use crate::utils::rpc::get_remote_wasm_from_hash; use crate::utils::{self}; +use crate::wasm::Error::{ContractIsStellarAsset, UnexpectedContractToken}; + #[derive(thiserror::Error, Debug)] pub enum Error { #[error("reading file {filepath}: {error}")] @@ -28,6 +35,18 @@ pub enum Error { Parser(#[from] wasmparser::BinaryReaderError), #[error(transparent)] ContractSpec(#[from] contract::Error), + + #[error(transparent)] + Locator(#[from] locator::Error), + #[error(transparent)] + Rpc(#[from] soroban_rpc::Error), + #[error("unexpected contract data {0:?}")] + UnexpectedContractToken(ContractDataEntry), + #[error( + "cannot fetch wasm for contract because the contract is \ + a network built-in asset contract that does not have a downloadable code binary" + )] + ContractIsStellarAsset(), } #[derive(Debug, clap::Args, Clone)] @@ -97,3 +116,28 @@ pub fn len(p: &Path) -> Result { })? .len()) } + +pub async fn fetch_from_contract( + contract_id: &str, + network: &Network, + locator: &locator::Args, +) -> Result, Error> { + tracing::trace!(?network); + + let contract_id = &locator + .resolve_contract_id(contract_id, &network.network_passphrase)? + .0; + + let client = Client::new(&network.rpc_url)?; + client + .verify_network_passphrase(Some(&network.network_passphrase)) + .await?; + let data_entry = client.get_contract_data(contract_id).await?; + if let ScVal::ContractInstance(contract) = &data_entry.val { + return match &contract.executable { + ContractExecutable::Wasm(hash) => Ok(get_remote_wasm_from_hash(&client, hash).await?), + ContractExecutable::StellarAsset => Err(ContractIsStellarAsset()), + }; + } + Err(UnexpectedContractToken(data_entry)) +}