From 46f4aa58ff2e57e7a52edb542a9c2da7fa1b0e0d Mon Sep 17 00:00:00 2001 From: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:22:15 -0500 Subject: [PATCH] feat(ts-bindings): allow generating from wasm hash Refactor spec-fetching logic from `contract::info::shared` so that it can be used for `contract::bindings::typescript` as well. This means that `contract::info` commands now have more logging than they did before, and support global args for quieting these print statements. --- FULL_HELP_DOCS.md | 35 +++---- .../ts-tests/initialize.sh | 2 +- .../src/commands/contract/bindings.rs | 2 +- .../commands/contract/bindings/typescript.rs | 97 ++++--------------- cmd/soroban-cli/src/commands/contract/info.rs | 12 ++- .../src/commands/contract/info/env_meta.rs | 15 ++- .../src/commands/contract/info/interface.rs | 7 +- .../src/commands/contract/info/meta.rs | 7 +- .../src/commands/contract/info/shared.rs | 71 ++++++++++---- cmd/soroban-cli/src/commands/contract/mod.rs | 2 +- 10 files changed, 121 insertions(+), 129 deletions(-) diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index 991872281..b08865bb6 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -290,20 +290,21 @@ Generate Rust bindings Generate a TypeScript / JavaScript package -**Usage:** `stellar contract bindings typescript [OPTIONS] --output-dir ` +**Usage:** `stellar contract bindings typescript [OPTIONS] --output-dir <--wasm |--wasm-hash |--contract-id >` ###### **Options:** -* `--wasm ` — Path to wasm file on local filesystem. You must either include this OR `--contract-id` -* `--contract-id ` — A contract ID/address on a network (if no network settings provided, Testnet will be assumed). You must either include this OR `--wasm` -* `--output-dir ` — Where to place generated project -* `--overwrite` — Whether to overwrite output directory if it already exists +* `--wasm ` — Wasm file path on local filesystem. Provide this OR `--wasm-hash` OR `--contract-id` +* `--wasm-hash ` — Hash of Wasm blob on a network. Provide this OR `--wasm` OR `--contract-id` +* `--contract-id ` — Contract ID/alias on a network. Provide this OR `--wasm-hash` OR `--wasm` * `--rpc-url ` — RPC server endpoint * `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider * `--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 "." +* `--output-dir ` — Where to place generated project +* `--overwrite` — Whether to overwrite output directory if it already exists @@ -525,13 +526,13 @@ The data outputted by this command is a stream of `SCSpecEntry` XDR values. See Outputs no data when no data is present in the contract. -**Usage:** `stellar contract info interface [OPTIONS] <--wasm |--wasm-hash |--id >` +**Usage:** `stellar contract info interface [OPTIONS] <--wasm |--wasm-hash |--contract-id >` ###### **Options:** -* `--wasm ` — Wasm file to extract the data from -* `--wasm-hash ` — Wasm hash to get the data for -* `--id ` — Contract id or contract alias to get the data for +* `--wasm ` — Wasm file path on local filesystem. Provide this OR `--wasm-hash` OR `--contract-id` +* `--wasm-hash ` — Hash of Wasm blob on a network. Provide this OR `--wasm` OR `--contract-id` +* `--contract-id ` — Contract ID/alias on a network. Provide this OR `--wasm-hash` OR `--wasm` * `--rpc-url ` — RPC server endpoint * `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider * `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server @@ -565,13 +566,13 @@ The data outputted by this command is a stream of `SCMetaEntry` XDR values. See Outputs no data when no data is present in the contract. -**Usage:** `stellar contract info meta [OPTIONS] <--wasm |--wasm-hash |--id >` +**Usage:** `stellar contract info meta [OPTIONS] <--wasm |--wasm-hash |--contract-id >` ###### **Options:** -* `--wasm ` — Wasm file to extract the data from -* `--wasm-hash ` — Wasm hash to get the data for -* `--id ` — Contract id or contract alias to get the data for +* `--wasm ` — Wasm file path on local filesystem. Provide this OR `--wasm-hash` OR `--contract-id` +* `--wasm-hash ` — Hash of Wasm blob on a network. Provide this OR `--wasm` OR `--contract-id` +* `--contract-id ` — Contract ID/alias on a network. Provide this OR `--wasm-hash` OR `--wasm` * `--rpc-url ` — RPC server endpoint * `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider * `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server @@ -605,13 +606,13 @@ The data outputted by this command is a stream of `SCEnvMetaEntry` XDR values. S Outputs no data when no data is present in the contract. -**Usage:** `stellar contract info env-meta [OPTIONS] <--wasm |--wasm-hash |--id >` +**Usage:** `stellar contract info env-meta [OPTIONS] <--wasm |--wasm-hash |--contract-id >` ###### **Options:** -* `--wasm ` — Wasm file to extract the data from -* `--wasm-hash ` — Wasm hash to get the data for -* `--id ` — Contract id or contract alias to get the data for +* `--wasm ` — Wasm file path on local filesystem. Provide this OR `--wasm-hash` OR `--contract-id` +* `--wasm-hash ` — Hash of Wasm blob on a network. Provide this OR `--wasm` OR `--contract-id` +* `--contract-id ` — Contract ID/alias on a network. Provide this OR `--wasm-hash` OR `--wasm` * `--rpc-url ` — RPC server endpoint * `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider * `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server 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.rs b/cmd/soroban-cli/src/commands/contract/bindings.rs index a91c966aa..c0e1f288a 100644 --- a/cmd/soroban-cli/src/commands/contract/bindings.rs +++ b/cmd/soroban-cli/src/commands/contract/bindings.rs @@ -12,7 +12,7 @@ pub enum Cmd { Rust(rust::Cmd), /// Generate a TypeScript / JavaScript package - Typescript(typescript::Cmd), + Typescript(Box), /// Generate Python bindings Python(python::Cmd), diff --git a/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs b/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs index ea1f63eed..6ce78f2fd 100644 --- a/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs +++ b/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs @@ -2,43 +2,30 @@ use std::{ffi::OsString, fmt::Debug, path::PathBuf}; use clap::{command, Parser}; use soroban_spec_tools::contract as contract_spec; -use soroban_spec_typescript::{self as typescript, boilerplate::Project}; -use stellar_strkey::DecodeError; +use soroban_spec_typescript::boilerplate::Project; use crate::print::Print; -use crate::wasm; use crate::{ - commands::{contract::fetch, global, NetworkRunnable}, - config::{self, locator, network}, - get_spec::{self, get_remote_contract_spec}, - xdr::{Hash, ScAddress}, + commands::{contract::info::shared as wasm_or_contract, global, NetworkRunnable}, + config, }; +use soroban_spec_tools::contract::Spec; #[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)] - 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")] - pub contract_id: Option, + #[command(flatten)] + pub wasm_or_hash_or_contract_id: wasm_or_contract::Args, /// Where to place generated project #[arg(long)] pub output_dir: PathBuf, /// Whether to overwrite output directory if it already exists #[arg(long)] pub overwrite: bool, - #[command(flatten)] - pub network: network::Args, - #[command(flatten)] - pub locator: locator::Args, } #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("failed generate TS from file: {0}")] - GenerateTSFromFile(typescript::GenerateFromFileError), #[error(transparent)] Io(#[from] std::io::Error), @@ -51,28 +38,15 @@ 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("cannot yet generate bindings for Stellar Asset Contract")] + CannotGenerateBindingsForStellarAsset, - #[error(transparent)] - Network(#[from] network::Error), - - #[error(transparent)] - Locator(#[from] locator::Error), - #[error(transparent)] - Fetch(#[from] fetch::Error), #[error(transparent)] Spec(#[from] contract_spec::Error), - #[error(transparent)] - Wasm(#[from] wasm::Error), #[error("Failed to get file name from path: {0:?}")] FailedToGetFileName(PathBuf), - #[error("cannot parse contract ID {0}: {1}")] - CannotParseContractId(String, DecodeError), - #[error(transparent)] - UtilsError(#[from] get_spec::Error), #[error(transparent)] - Config(#[from] config::Error), + WasmOrContract(#[from] wasm_or_contract::Error), } #[async_trait::async_trait] @@ -83,51 +57,18 @@ impl NetworkRunnable for Cmd { async fn run_against_rpc_server( &self, global_args: Option<&global::Args>, - config: Option<&config::Args>, + _config: Option<&config::Args>, ) -> Result<(), Error> { let print = Print::new(global_args.is_some_and(|a| a.quiet)); - 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 (spec, contract_address, network) = + wasm_or_contract::fetch_wasm(&self.wasm_or_hash_or_contract_id, &print).await?; - let network = self.network.get(&self.locator).ok().unwrap_or_else(|| { - network::DEFAULTS - .get("testnet") - .expect("no network specified and testnet network not found") - .into() - }); - 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}")); + if spec.is_none() { + return Err(Error::CannotGenerateBindingsForStellarAsset); + } + let spec = spec.expect("spec is some"); - ( - 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())); } @@ -153,9 +94,9 @@ impl NetworkRunnable for Cmd { p.init( contract_name, contract_address.as_deref(), - rpc_url.as_deref(), - network_passphrase.as_deref(), - &spec, + network.as_ref().map(|n| n.rpc_url.as_ref()), + network.as_ref().map(|n| n.network_passphrase.as_ref()), + &Spec::new(&spec)?.spec, )?; print.checkln("Generated!"); print.infoln(format!( diff --git a/cmd/soroban-cli/src/commands/contract/info.rs b/cmd/soroban-cli/src/commands/contract/info.rs index 5ca03ab2c..6192c1c61 100644 --- a/cmd/soroban-cli/src/commands/contract/info.rs +++ b/cmd/soroban-cli/src/commands/contract/info.rs @@ -1,9 +1,11 @@ use std::fmt::Debug; +use crate::commands::global; + pub mod env_meta; pub mod interface; pub mod meta; -mod shared; +pub mod shared; #[derive(Debug, clap::Subcommand)] pub enum Cmd { @@ -60,11 +62,11 @@ pub enum Error { } impl Cmd { - pub async fn run(&self) -> Result<(), Error> { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { let result = match &self { - Cmd::Interface(interface) => interface.run().await?, - Cmd::Meta(meta) => meta.run().await?, - Cmd::EnvMeta(env_meta) => env_meta.run().await?, + Cmd::Interface(interface) => interface.run(global_args).await?, + Cmd::Meta(meta) => meta.run(global_args).await?, + Cmd::EnvMeta(env_meta) => env_meta.run(global_args).await?, }; println!("{result}"); Ok(()) 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 bc2d03bc6..f882d2737 100644 --- a/cmd/soroban-cli/src/commands/contract/info/env_meta.rs +++ b/cmd/soroban-cli/src/commands/contract/info/env_meta.rs @@ -6,10 +6,14 @@ use soroban_spec_tools::contract; use soroban_spec_tools::contract::Spec; use crate::{ - commands::contract::info::{ - env_meta::Error::{NoEnvMetaPresent, NoSACEnvMeta}, - shared::{self, fetch_wasm, MetasInfoOutput}, + commands::{ + contract::info::{ + env_meta::Error::{NoEnvMetaPresent, NoSACEnvMeta}, + shared::{self, fetch_wasm, MetasInfoOutput}, + }, + global, }, + print::Print, xdr::{ScEnvMetaEntry, ScEnvMetaEntryInterfaceVersion}, }; @@ -37,8 +41,9 @@ pub enum Error { } impl Cmd { - pub async fn run(&self) -> Result { - let bytes = fetch_wasm(&self.common).await?; + pub async fn run(&self, global_args: &global::Args) -> Result { + let print = Print::new(global_args.quiet); + let (bytes, ..) = fetch_wasm(&self.common, &print).await?; let Some(bytes) = bytes else { return Err(NoSACEnvMeta()); diff --git a/cmd/soroban-cli/src/commands/contract/info/interface.rs b/cmd/soroban-cli/src/commands/contract/info/interface.rs index cf96fd700..dd71150fa 100644 --- a/cmd/soroban-cli/src/commands/contract/info/interface.rs +++ b/cmd/soroban-cli/src/commands/contract/info/interface.rs @@ -3,6 +3,8 @@ use std::fmt::Debug; use crate::commands::contract::info::interface::Error::NoInterfacePresent; use crate::commands::contract::info::shared; use crate::commands::contract::info::shared::fetch_wasm; +use crate::commands::global; +use crate::print::Print; use clap::{command, Parser}; use soroban_spec_rust::ToFormattedString; use soroban_spec_tools::contract; @@ -43,8 +45,9 @@ pub enum Error { } impl Cmd { - pub async fn run(&self) -> Result { - let bytes = fetch_wasm(&self.common).await?; + pub async fn run(&self, global_args: &global::Args) -> Result { + let print = Print::new(global_args.quiet); + let (bytes, ..) = fetch_wasm(&self.common, &print).await?; let (base64, spec) = if bytes.is_none() { Spec::spec_to_base64(&soroban_sdk::token::StellarAssetSpec::spec_xdr())? diff --git a/cmd/soroban-cli/src/commands/contract/info/meta.rs b/cmd/soroban-cli/src/commands/contract/info/meta.rs index e3e2122c8..4b6fdee3e 100644 --- a/cmd/soroban-cli/src/commands/contract/info/meta.rs +++ b/cmd/soroban-cli/src/commands/contract/info/meta.rs @@ -3,6 +3,8 @@ use std::fmt::Debug; use crate::commands::contract::info::meta::Error::{NoMetaPresent, NoSACMeta}; use crate::commands::contract::info::shared; use crate::commands::contract::info::shared::{fetch_wasm, MetasInfoOutput}; +use crate::commands::global; +use crate::print::Print; use clap::{command, Parser}; use soroban_spec_tools::contract; use soroban_spec_tools::contract::Spec; @@ -32,8 +34,9 @@ pub enum Error { } impl Cmd { - pub async fn run(&self) -> Result { - let bytes = fetch_wasm(&self.common).await?; + pub async fn run(&self, global_args: &global::Args) -> Result { + let print = Print::new(global_args.quiet); + let (bytes, ..) = fetch_wasm(&self.common, &print).await?; let Some(bytes) = bytes else { return Err(NoSACMeta()); diff --git a/cmd/soroban-cli/src/commands/contract/info/shared.rs b/cmd/soroban-cli/src/commands/contract/info/shared.rs index 33a95b607..0e178a28f 100644 --- a/cmd/soroban-cli/src/commands/contract/info/shared.rs +++ b/cmd/soroban-cli/src/commands/contract/info/shared.rs @@ -4,7 +4,11 @@ use clap::arg; use crate::{ commands::contract::info::shared::Error::InvalidWasmHash, - config::{self, locator, network}, + config::{ + self, locator, + network::{self, Network}, + }, + print::Print, utils::rpc::get_remote_wasm_from_hash, wasm::{self, Error::ContractIsStellarAsset}, xdr, @@ -18,14 +22,31 @@ use crate::{ ))] #[group(skip)] pub struct Args { - /// Wasm file to extract the data from - #[arg(long, group = "Source")] + /// Wasm file path on local filesystem. Provide this OR `--wasm-hash` OR `--contract-id`. + #[arg( + long, + group = "Source", + conflicts_with = "contract_id", + conflicts_with = "wasm_hash" + )] pub wasm: Option, - /// Wasm hash to get the data for - #[arg(long = "wasm-hash", group = "Source")] + /// Hash of Wasm blob on a network. Provide this OR `--wasm` OR `--contract-id`. + #[arg( + long = "wasm-hash", + group = "Source", + conflicts_with = "contract_id", + conflicts_with = "wasm" + )] pub wasm_hash: Option, - /// Contract id or contract alias to get the data for - #[arg(long = "id", env = "STELLAR_CONTRACT_ID", group = "Source")] + /// Contract ID/alias on a network. Provide this OR `--wasm-hash` OR `--wasm`. + #[arg( + long, + env = "STELLAR_CONTRACT_ID", + group = "Source", + visible_alias = "id", + conflicts_with = "wasm", + conflicts_with = "wasm_hash" + )] pub contract_id: Option, #[command(flatten)] pub network: network::Args, @@ -54,23 +75,31 @@ pub enum Error { Wasm(#[from] wasm::Error), #[error("provided wasm hash is invalid {0:?}")] InvalidWasmHash(String), + #[error("must provide one of --wasm, --wasm-hash, or --contract-id")] + MalformedWasmOrWasmHashOrContractId, #[error(transparent)] Rpc(#[from] soroban_rpc::Error), #[error(transparent)] Locator(#[from] locator::Error), } -pub async fn fetch_wasm(args: &Args) -> Result>, Error> { +pub async fn fetch_wasm( + args: &Args, + print: &Print, +) -> Result<(Option>, Option, Option), Error> { // Check if a local WASM file path is provided if let Some(path) = &args.wasm { // Read the WASM file and return its contents + print.infoln("Loading contract spec from file..."); let wasm_bytes = wasm::Args { wasm: path.clone() }.read()?; - return Ok(Some(wasm_bytes)); + return Ok((Some(wasm_bytes), None, None)); } // If no local wasm, then check for wasm_hash and fetch from the network let network = &args.network.get(&args.locator)?; - let wasm = if let Some(wasm_hash) = &args.wasm_hash { + print.infoln(format!("Network: {}", network.network_passphrase)); + + if let Some(wasm_hash) = &args.wasm_hash { let hash = hex::decode(wasm_hash) .map_err(|_| InvalidWasmHash(wasm_hash.clone()))? .try_into() @@ -84,18 +113,26 @@ pub async fn fetch_wasm(args: &Args) -> Result>, Error> { .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - get_remote_wasm_from_hash(&client, &hash).await? + print.globeln(format!( + "Downloading contract spec for wasm hash: {wasm_hash}" + )); + Ok(( + Some(get_remote_wasm_from_hash(&client, &hash).await?), + None, + Some(network.clone()), + )) } else if let Some(contract_id) = &args.contract_id { let contract_id = contract_id.resolve_contract_id(&args.locator, &network.network_passphrase)?; + let contract_address = xdr::ScAddress::Contract(xdr::Hash(contract_id.0)).to_string(); + print.globeln(format!("Downloading contract spec: {contract_address}")); let res = wasm::fetch_from_contract(&contract_id, network).await; if let Some(ContractIsStellarAsset) = res.as_ref().err() { - return Ok(None); + print.globeln("Contract is a Stellar asset; it has no spec"); + return Ok((None, Some(contract_address), Some(network.clone()))); } - res? + Ok((Some(res?), Some(contract_address), Some(network.clone()))) } else { - unreachable!("One of contract location arguments must be passed"); - }; - - Ok(Some(wasm)) + return Err(Error::MalformedWasmOrWasmHashOrContractId); + } } diff --git a/cmd/soroban-cli/src/commands/contract/mod.rs b/cmd/soroban-cli/src/commands/contract/mod.rs index d72ce62b6..42792a70d 100644 --- a/cmd/soroban-cli/src/commands/contract/mod.rs +++ b/cmd/soroban-cli/src/commands/contract/mod.rs @@ -151,7 +151,7 @@ impl Cmd { Cmd::Alias(alias) => alias.run(global_args)?, Cmd::Deploy(deploy) => deploy.run(global_args).await?, Cmd::Id(id) => id.run()?, - Cmd::Info(info) => info.run().await?, + Cmd::Info(info) => info.run(global_args).await?, Cmd::Init(init) => init.run(global_args)?, Cmd::Inspect(inspect) => inspect.run(global_args)?, Cmd::Install(install) => install.run(global_args).await?,