Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@ Access info about contracts
- `meta` — Output the metadata stored in a contract
- `env-meta` — Output the env required metadata stored in a contract
- `build` — Output the contract build information, if available
- `hash` — Output the SHA-256 hash of a contract's Wasm

## `stellar contract info interface`

Expand Down Expand Up @@ -742,6 +743,32 @@ If the contract has a meta entry like `source_repo=github:user/repo`, this comma
- `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to sign the transaction sent to the rpc server
- `-n`, `--network <NETWORK>` — Name of network to use from config

## `stellar contract info hash`

Output the SHA-256 hash of a contract's Wasm.

The hash can be computed from a local .wasm file (`--wasm`) or read from a deployed contract (`--id`). The two flags are mutually exclusive.

Comment thread
fnando marked this conversation as resolved.
Stellar Asset Contracts have no Wasm and therefore no hash; using `--id` against a SAC will return an error.

**Usage:** `stellar contract info hash [OPTIONS] <--wasm <WASM>|--contract-id <CONTRACT_ID>>`

###### **Global Options:**

- `--config-dir <CONFIG_DIR>` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings

###### **Options:**

- `--wasm <WASM>` — Path to a local .wasm file
- `--contract-id <CONTRACT_ID>` [alias: `id`] — Contract ID or alias of a deployed contract

###### **RPC Options:**

- `--rpc-url <RPC_URL>` — RPC server endpoint
- `--rpc-header <RPC_HEADERS>` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times
- `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to sign the transaction sent to the rpc server
- `-n`, `--network <NETWORK>` — Name of network to use from config

## `stellar contract init`

Initialize a Soroban contract project.
Expand Down
121 changes: 121 additions & 0 deletions cmd/crates/soroban-test/tests/it/integration/contract/info_hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use crate::integration::util::{deploy_contract, test_address, DeployOptions, HELLO_WORLD};

use soroban_test::{AssertExt, TestEnv};

#[tokio::test]
async fn info_hash_with_wasm_file() {
let sandbox = &TestEnv::new();
let expected = HELLO_WORLD.hash().unwrap().to_string();

let actual = sandbox
.new_assert_cmd("contract")
.arg("info")
.arg("hash")
.arg("--wasm")
.arg(HELLO_WORLD.path())
.assert()
.success()
.stdout_as_str();

assert_eq!(actual, expected);
}

#[tokio::test]
async fn info_hash_with_contract_id() {
let sandbox = &TestEnv::new();
let expected = HELLO_WORLD.hash().unwrap().to_string();
let contract_id = deploy_contract(sandbox, HELLO_WORLD, DeployOptions::default()).await;

let actual = sandbox
.new_assert_cmd("contract")
.arg("info")
.arg("hash")
.arg("--id")
.arg(&contract_id)
.assert()
.success()
.stdout_as_str();

assert_eq!(actual, expected);
}

#[tokio::test]
async fn info_hash_with_contract_alias() {
let sandbox = &TestEnv::new();
let expected = HELLO_WORLD.hash().unwrap().to_string();
let contract_id = deploy_contract(sandbox, HELLO_WORLD, DeployOptions::default()).await;

sandbox
.new_assert_cmd("contract")
.arg("alias")
.arg("add")
.arg("hello")
.arg("--id")
.arg(&contract_id)
.assert()
.success();

let actual = sandbox
.new_assert_cmd("contract")
.arg("info")
.arg("hash")
.arg("--id")
.arg("hello")
.assert()
.success()
.stdout_as_str();

assert_eq!(actual, expected);
}

#[tokio::test]
async fn info_hash_errors_on_stellar_asset_contract() {
let sandbox = &TestEnv::new();
let issuer = test_address(sandbox);
let sac_id = sandbox
.new_assert_cmd("contract")
.arg("asset")
.arg("deploy")
.arg(format!("--asset=USDC:{issuer}"))
.assert()
.success()
.stdout_as_str();

sandbox
.new_assert_cmd("contract")
.arg("info")
.arg("hash")
.arg("--id")
.arg(&sac_id)
.assert()
.failure();
}

#[tokio::test]
async fn info_hash_requires_one_source() {
let sandbox = &TestEnv::new();

sandbox
.new_assert_cmd("contract")
.arg("info")
.arg("hash")
.assert()
.failure();
}

#[tokio::test]
async fn info_hash_wasm_and_id_are_mutually_exclusive() {
let sandbox = &TestEnv::new();
let contract_id = deploy_contract(sandbox, HELLO_WORLD, DeployOptions::default()).await;

Comment thread
fnando marked this conversation as resolved.
sandbox
.new_assert_cmd("contract")
.arg("info")
.arg("hash")
.arg("--wasm")
.arg(HELLO_WORLD.path())
.arg("--id")
.arg(&contract_id)
.assert()
.failure();
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mod fetch;
mod info_hash;
14 changes: 14 additions & 0 deletions cmd/soroban-cli/src/commands/contract/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::commands::global;

pub mod build;
pub mod env_meta;
pub mod hash;
pub mod interface;
pub mod meta;
pub mod shared;
Expand Down Expand Up @@ -56,6 +57,15 @@ pub enum Cmd {
/// If the contract has a meta entry like `source_repo=github:user/repo`, this command will try
/// to fetch the attestation information for the WASM file.
Build(build::Cmd),

/// Output the SHA-256 hash of a contract's Wasm.
///
/// The hash can be computed from a local .wasm file (`--wasm`) or read from a deployed
/// contract (`--id`). The two flags are mutually exclusive.
///
/// Stellar Asset Contracts have no Wasm and therefore no hash; using `--id` against a
/// SAC will return an error.
Hash(hash::Cmd),
}

#[derive(thiserror::Error, Debug)]
Expand All @@ -71,6 +81,9 @@ pub enum Error {

#[error(transparent)]
Build(#[from] build::Error),

#[error(transparent)]
Hash(#[from] hash::Error),
}

impl Cmd {
Expand All @@ -80,6 +93,7 @@ impl Cmd {
Cmd::Meta(meta) => meta.run(global_args).await?,
Cmd::EnvMeta(env_meta) => env_meta.run(global_args).await?,
Cmd::Build(build) => build.run(global_args).await?,
Cmd::Hash(hash) => hash.run(global_args).await?,
}

Ok(())
Expand Down
65 changes: 65 additions & 0 deletions cmd/soroban-cli/src/commands/contract/info/hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use std::path::PathBuf;

use clap::Parser;

use crate::{
commands::global,
config::{
self, locator,
network::{self},
},
wasm,
};

#[derive(Parser, Debug, Clone)]
#[command(group(
clap::ArgGroup::new("source")
.required(true)
.args(&["wasm", "contract_id"]),
))]
#[group(skip)]
pub struct Cmd {
/// Path to a local .wasm file.
#[arg(long, conflicts_with = "contract_id")]
pub wasm: Option<PathBuf>,
/// Contract ID or alias of a deployed contract.
#[arg(
long,
visible_alias = "id",
env = "STELLAR_CONTRACT_ID",
conflicts_with = "wasm"
)]
pub contract_id: Option<config::UnresolvedContract>,
#[command(flatten)]
pub network: network::Args,
#[command(flatten)]
pub locator: locator::Args,
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Wasm(#[from] wasm::Error),
#[error(transparent)]
Network(#[from] network::Error),
#[error(transparent)]
Locator(#[from] locator::Error),
}

impl Cmd {
pub async fn run(&self, _global_args: &global::Args) -> Result<(), Error> {
let hash = if let Some(path) = &self.wasm {
wasm::Args { wasm: path.clone() }.hash()?
} else if let Some(contract_id) = &self.contract_id {
let network = self.network.get(&self.locator)?;
let resolved =
contract_id.resolve_contract_id(&self.locator, &network.network_passphrase)?;
wasm::fetch_wasm_hash_from_contract(&resolved, &network).await?
} else {
unreachable!("clap ArgGroup guarantees one of --wasm or --contract-id is set");
};

println!("{hash}");
Ok(())
}
}
19 changes: 19 additions & 0 deletions cmd/soroban-cli/src/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,22 @@ pub async fn fetch_from_wasm_hash(hash: Hash, network: &Network) -> Result<Vec<u
let client = network.rpc_client()?;
Ok(get_remote_wasm_from_hash(&client, &hash).await?)
}

pub async fn fetch_wasm_hash_from_contract(
stellar_strkey::Contract(contract_id): &stellar_strkey::Contract,
network: &Network,
) -> Result<Hash, Error> {
tracing::trace!(?network);
let client = network.rpc_client()?;
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(hash.clone()),
ContractExecutable::StellarAsset => Err(ContractIsStellarAsset),
};
Comment thread
fnando marked this conversation as resolved.
}
Err(UnexpectedContractToken(Box::new(data_entry)))
}
Loading