Skip to content

Commit

Permalink
feat(ts-bindings): allow generating from wasm hash
Browse files Browse the repository at this point in the history
  • Loading branch information
chadoh committed Dec 4, 2024
1 parent 7c29f05 commit 0c507ea
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 51 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
119 changes: 87 additions & 32 deletions cmd/soroban-cli/src/commands/contract/bindings/typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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<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")]
/// 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<String>,
/// 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<String>,
/// Where to place generated project
#[arg(long)]
Expand Down Expand Up @@ -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)]
Expand All @@ -80,23 +100,29 @@ 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>,
config: Option<&config::Args>,
) -> 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")
Expand All @@ -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()));
Expand Down
44 changes: 26 additions & 18 deletions cmd/soroban-cli/src/get_spec.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<Vec<ScSpecEntry>, 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
})
}

0 comments on commit 0c507ea

Please sign in to comment.