Skip to content

Commit

Permalink
feat(ts-bindings): allow generating from wasm hash
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
chadoh committed Dec 6, 2024
1 parent 7c29f05 commit 12be08e
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 111 deletions.
2 changes: 1 addition & 1 deletion cmd/crates/soroban-spec-typescript/ts-tests/initialize.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion cmd/soroban-cli/src/commands/contract/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub enum Cmd {
Rust(rust::Cmd),

/// Generate a TypeScript / JavaScript package
Typescript(typescript::Cmd),
Typescript(Box<typescript::Cmd>),

/// Generate Python bindings
Python(python::Cmd),
Expand Down
97 changes: 19 additions & 78 deletions cmd/soroban-cli/src/commands/contract/bindings/typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::path::PathBuf>,
/// 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<String>,
#[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),

Expand All @@ -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]
Expand All @@ -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()));
}
Expand All @@ -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!(
Expand Down
12 changes: 7 additions & 5 deletions cmd/soroban-cli/src/commands/contract/info.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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(())
Expand Down
15 changes: 10 additions & 5 deletions cmd/soroban-cli/src/commands/contract/info/env_meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};

Expand Down Expand Up @@ -37,8 +41,9 @@ pub enum Error {
}

impl Cmd {
pub async fn run(&self) -> Result<String, Error> {
let bytes = fetch_wasm(&self.common).await?;
pub async fn run(&self, global_args: &global::Args) -> Result<String, Error> {
let print = Print::new(global_args.quiet);
let (bytes, ..) = fetch_wasm(&self.common, &print).await?;

let Some(bytes) = bytes else {
return Err(NoSACEnvMeta());
Expand Down
7 changes: 5 additions & 2 deletions cmd/soroban-cli/src/commands/contract/info/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -43,8 +45,9 @@ pub enum Error {
}

impl Cmd {
pub async fn run(&self) -> Result<String, Error> {
let bytes = fetch_wasm(&self.common).await?;
pub async fn run(&self, global_args: &global::Args) -> Result<String, Error> {
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())?
Expand Down
7 changes: 5 additions & 2 deletions cmd/soroban-cli/src/commands/contract/info/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -32,8 +34,9 @@ pub enum Error {
}

impl Cmd {
pub async fn run(&self) -> Result<String, Error> {
let bytes = fetch_wasm(&self.common).await?;
pub async fn run(&self, global_args: &global::Args) -> Result<String, Error> {
let print = Print::new(global_args.quiet);
let (bytes, ..) = fetch_wasm(&self.common, &print).await?;

let Some(bytes) = bytes else {
return Err(NoSACMeta());
Expand Down
68 changes: 52 additions & 16 deletions cmd/soroban-cli/src/commands/contract/info/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -18,13 +22,29 @@ 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<PathBuf>,
/// 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<String>,
/// Contract id or contract alias to get the data for
/// Contract ID/alias on a network. Provide this OR `--wasm-hash` OR `--wasm`.
#[arg(
long,
visible_alias = "id",
conflicts_with = "wasm",
conflicts_with = "wasm_hash"
)]
#[arg(long = "id", env = "STELLAR_CONTRACT_ID", group = "Source")]
pub contract_id: Option<config::ContractAddress>,
#[command(flatten)]
Expand Down Expand Up @@ -54,23 +74,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<Option<Vec<u8>>, Error> {
pub async fn fetch_wasm(
args: &Args,
print: &Print,
) -> Result<(Option<Vec<u8>>, Option<String>, Option<Network>), 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()
Expand All @@ -84,18 +112,26 @@ pub async fn fetch_wasm(args: &Args) -> Result<Option<Vec<u8>>, 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);
}
}
2 changes: 1 addition & 1 deletion cmd/soroban-cli/src/commands/contract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand Down

0 comments on commit 12be08e

Please sign in to comment.