diff --git a/cmd/crates/soroban-spec-typescript/ts-tests/initialize.sh b/cmd/crates/soroban-spec-typescript/ts-tests/initialize.sh index e872923c6..804820abb 100755 --- a/cmd/crates/soroban-spec-typescript/ts-tests/initialize.sh +++ b/cmd/crates/soroban-spec-typescript/ts-tests/initialize.sh @@ -43,7 +43,7 @@ function bind() { } function bind_all() { bind --contract-id $(cat contract-id-custom-types.txt) test-custom-types - bind --wasm ../../../../target/wasm32-unknown-unknown/test-wasms/test_constructor.wasm test-constructor + bind --wasm-hash $(cat contract-wasm-hash-constructor.txt) test-constructor } fund_all diff --git a/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs b/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs index ea1f63eed..b22d28cf9 100644 --- a/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs +++ b/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs @@ -5,7 +5,10 @@ use soroban_spec_tools::contract as contract_spec; use soroban_spec_typescript::{self as typescript, boilerplate::Project}; use stellar_strkey::DecodeError; +use crate::get_spec::get_spec_from_hash; use crate::print::Print; +use crate::rpc; +use crate::utils::contract_id_from_str; use crate::wasm; use crate::{ commands::{contract::fetch, global, NetworkRunnable}, @@ -17,11 +20,19 @@ use crate::{ #[derive(Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { - /// Path to wasm file on local filesystem. You must either include this OR `--contract-id`. - #[arg(long)] + /// Path to wasm file on local filesystem. Provide this OR `--wasm-hash` OR `--contract-id`. + #[arg(long, conflicts_with = "contract_id", conflicts_with = "wasm_hash")] pub wasm: Option, - /// A contract ID/address on a network (if no network settings provided, Testnet will be assumed). You must either include this OR `--wasm`. - #[arg(long, visible_alias = "id")] + /// Hash of Wasm blob on a network (default: testnet). Provide this OR `--wasm` OR `--contract-id`. + #[arg(long, conflicts_with = "contract_id", conflicts_with = "wasm")] + pub wasm_hash: Option, + /// A contract ID/address on a network (default: testnet). Provide this OR `--wasm-hash` OR `--wasm`. + #[arg( + long, + visible_alias = "id", + conflicts_with = "wasm", + conflicts_with = "wasm_hash" + )] pub contract_id: Option, /// Where to place generated project #[arg(long)] @@ -51,12 +62,21 @@ pub enum Error { #[error("--output-dir filepath not representable as utf-8: {0:?}")] NotUtf8(OsString), - #[error("must include either --wasm or --contract-id")] - MissingWasmOrContractId, + #[error("must provide one of --wasm, --wasm-hash, or --contract-id")] + MalformedWasmOrWasmHashOrContractId, + + #[error("cannot parse WASM hash {wasm_hash}: {error}")] + CannotParseWasmHash { + wasm_hash: String, + error: stellar_strkey::DecodeError, + }, #[error(transparent)] Network(#[from] network::Error), + #[error(transparent)] + RpcClient(#[from] rpc::Error), + #[error(transparent)] Locator(#[from] locator::Error), #[error(transparent)] @@ -80,6 +100,7 @@ impl NetworkRunnable for Cmd { type Error = Error; type Result = (); + #[allow(clippy::too_many_lines)] async fn run_against_rpc_server( &self, global_args: Option<&global::Args>, @@ -87,16 +108,21 @@ impl NetworkRunnable for Cmd { ) -> Result<(), Error> { let print = Print::new(global_args.is_some_and(|a| a.quiet)); + if (None, None, None) + == ( + self.wasm.as_ref(), + self.wasm_hash.as_ref(), + self.contract_id.as_ref(), + ) + { + return Err(Error::MalformedWasmOrWasmHashOrContractId); + } + let (spec, contract_address, rpc_url, network_passphrase) = if let Some(wasm) = &self.wasm { print.infoln("Loading contract spec from file..."); let wasm: wasm::Args = wasm.into(); (wasm.parse()?.spec, None, None, None) } else { - let contract_id = self - .contract_id - .as_ref() - .ok_or(Error::MissingWasmOrContractId)?; - let network = self.network.get(&self.locator).ok().unwrap_or_else(|| { network::DEFAULTS .get("testnet") @@ -105,28 +131,57 @@ impl NetworkRunnable for Cmd { }); print.infoln(format!("Network: {}", network.network_passphrase)); - let contract_id = self - .locator - .resolve_contract_id(contract_id, &network.network_passphrase)? - .0; - - let contract_address = ScAddress::Contract(Hash(contract_id)).to_string(); - print.globeln(format!("Downloading contract spec: {contract_address}")); - - ( - get_remote_contract_spec( - &contract_id, - &self.locator, - &self.network, - global_args, - config, + if let Some(wasm_hash) = &self.wasm_hash { + let wasm_hash = Hash( + contract_id_from_str(wasm_hash) + .map_err(|e| Error::CannotParseWasmHash { + wasm_hash: wasm_hash.clone(), + error: e, + })? + .0, + ); + print.globeln(format!( + "Downloading contract spec for wasm hash: {wasm_hash}" + )); + + let client = rpc::Client::new(&network.rpc_url)?; + ( + get_spec_from_hash(wasm_hash, &client, global_args) + .await + .map_err(Error::from)?, + None, + Some(network.rpc_url), + Some(network.network_passphrase), ) - .await - .map_err(Error::from)?, - Some(contract_address), - Some(network.rpc_url), - Some(network.network_passphrase), - ) + } else { + let contract_id = self + .contract_id + .as_ref() + .ok_or(Error::MalformedWasmOrWasmHashOrContractId)?; + + let contract_id = self + .locator + .resolve_contract_id(contract_id, &network.network_passphrase)? + .0; + + let contract_address = ScAddress::Contract(Hash(contract_id)).to_string(); + print.globeln(format!("Downloading contract spec: {contract_address}")); + + ( + get_remote_contract_spec( + &contract_id, + &self.locator, + &self.network, + global_args, + config, + ) + .await + .map_err(Error::from)?, + Some(contract_address), + Some(network.rpc_url), + Some(network.network_passphrase), + ) + } }; if self.output_dir.is_file() { return Err(Error::IsFile(self.output_dir.clone())); diff --git a/cmd/soroban-cli/src/get_spec.rs b/cmd/soroban-cli/src/get_spec.rs index 26e609543..244426cbf 100644 --- a/cmd/soroban-cli/src/get_spec.rs +++ b/cmd/soroban-cli/src/get_spec.rs @@ -1,6 +1,8 @@ use crate::xdr; -use crate::xdr::{ContractDataEntry, ContractExecutable, ScContractInstance, ScSpecEntry, ScVal}; +use crate::xdr::{ + ContractDataEntry, ContractExecutable, Hash, ScContractInstance, ScSpecEntry, ScVal, +}; use soroban_spec::read::FromWasmError; pub use soroban_spec_tools::contract as contract_spec; @@ -46,35 +48,41 @@ pub async fn get_remote_contract_spec( tracing::trace!(?network); let client = rpc::Client::new(&network.rpc_url)?; // Get contract data - let r = client.get_contract_data(contract_id).await?; - tracing::trace!("{r:?}"); + let entry = client.get_contract_data(contract_id).await?; + tracing::trace!("{entry:?}"); let ContractDataEntry { val: ScVal::ContractInstance(ScContractInstance { executable, .. }), .. - } = r + } = entry else { return Err(Error::MissingResult); }; // Get the contract spec entries based on the executable type Ok(match executable { - ContractExecutable::Wasm(hash) => { - let hash_str = hash.to_string(); - if let Ok(entries) = data::read_spec(&hash_str) { - entries - } else { - let raw_wasm = get_remote_wasm_from_hash(&client, &hash).await?; - let res = contract_spec::Spec::new(&raw_wasm)?; - let res = res.spec; - if global_args.map_or(true, |a| !a.no_cache) { - data::write_spec(&hash_str, &res)?; - } - res - } - } + ContractExecutable::Wasm(hash) => get_spec_from_hash(hash, &client, global_args).await?, ContractExecutable::StellarAsset => { soroban_spec::read::parse_raw(&soroban_sdk::token::StellarAssetSpec::spec_xdr())? } }) } + +pub async fn get_spec_from_hash( + hash: Hash, + client: &rpc::Client, + global_args: Option<&global::Args>, +) -> Result, Error> { + let hash_str = hash.to_string(); + Ok(if let Ok(entries) = data::read_spec(&hash_str) { + entries + } else { + let raw_wasm = get_remote_wasm_from_hash(&client, &hash).await?; + let res = contract_spec::Spec::new(&raw_wasm)?; + let res = res.spec; + if global_args.map_or(true, |a| !a.no_cache) { + data::write_spec(&hash_str, &res)?; + } + res + }) +}