Skip to content

Commit 6652ccf

Browse files
committed
Add stellar contract info hash command.
1 parent bc9282d commit 6652ccf

6 files changed

Lines changed: 247 additions & 0 deletions

File tree

FULL_HELP_DOCS.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,7 @@ Access info about contracts
602602
- `meta` — Output the metadata stored in a contract
603603
- `env-meta` — Output the env required metadata stored in a contract
604604
- `build` — Output the contract build information, if available
605+
- `hash` — Output the SHA-256 hash of a contract's Wasm
605606

606607
## `stellar contract info interface`
607608

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

746+
## `stellar contract info hash`
747+
748+
Output the SHA-256 hash of a contract's Wasm.
749+
750+
The hash can be computed from a local .wasm file (`--wasm`) or read from a deployed contract (`--id`). The two flags are mutually exclusive.
751+
752+
Stellar Asset Contracts have no Wasm and therefore no hash; using `--id` against a SAC will return an error.
753+
754+
**Usage:** `stellar contract info hash [OPTIONS] <--wasm <WASM>|--contract-id <CONTRACT_ID>>`
755+
756+
###### **Global Options:**
757+
758+
- `--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
759+
760+
###### **Options:**
761+
762+
- `--wasm <WASM>` — Path to a local .wasm file
763+
- `--contract-id <CONTRACT_ID>` [alias: `id`] — Contract ID or alias of a deployed contract
764+
765+
###### **RPC Options:**
766+
767+
- `--rpc-url <RPC_URL>` — RPC server endpoint
768+
- `--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
769+
- `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to sign the transaction sent to the rpc server
770+
- `-n`, `--network <NETWORK>` — Name of network to use from config
771+
745772
## `stellar contract init`
746773

747774
Initialize a Soroban contract project.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use crate::integration::util::{deploy_contract, test_address, DeployOptions, HELLO_WORLD};
2+
3+
use soroban_test::{AssertExt, TestEnv};
4+
5+
#[tokio::test]
6+
async fn info_hash_with_wasm_file() {
7+
let sandbox = &TestEnv::new();
8+
let expected = HELLO_WORLD.hash().unwrap().to_string();
9+
10+
let actual = sandbox
11+
.new_assert_cmd("contract")
12+
.arg("info")
13+
.arg("hash")
14+
.arg("--wasm")
15+
.arg(HELLO_WORLD.path())
16+
.assert()
17+
.success()
18+
.stdout_as_str();
19+
20+
assert_eq!(actual, expected);
21+
}
22+
23+
#[tokio::test]
24+
async fn info_hash_with_contract_id() {
25+
let sandbox = &TestEnv::new();
26+
let expected = HELLO_WORLD.hash().unwrap().to_string();
27+
let contract_id = deploy_contract(sandbox, HELLO_WORLD, DeployOptions::default()).await;
28+
29+
let actual = sandbox
30+
.new_assert_cmd("contract")
31+
.arg("info")
32+
.arg("hash")
33+
.arg("--id")
34+
.arg(&contract_id)
35+
.assert()
36+
.success()
37+
.stdout_as_str();
38+
39+
assert_eq!(actual, expected);
40+
}
41+
42+
#[tokio::test]
43+
async fn info_hash_with_contract_alias() {
44+
let sandbox = &TestEnv::new();
45+
let expected = HELLO_WORLD.hash().unwrap().to_string();
46+
let contract_id = deploy_contract(sandbox, HELLO_WORLD, DeployOptions::default()).await;
47+
48+
sandbox
49+
.new_assert_cmd("contract")
50+
.arg("alias")
51+
.arg("add")
52+
.arg("hello")
53+
.arg("--id")
54+
.arg(&contract_id)
55+
.assert()
56+
.success();
57+
58+
let actual = sandbox
59+
.new_assert_cmd("contract")
60+
.arg("info")
61+
.arg("hash")
62+
.arg("--id")
63+
.arg("hello")
64+
.assert()
65+
.success()
66+
.stdout_as_str();
67+
68+
assert_eq!(actual, expected);
69+
}
70+
71+
#[tokio::test]
72+
async fn info_hash_errors_on_stellar_asset_contract() {
73+
let sandbox = &TestEnv::new();
74+
let issuer = test_address(sandbox);
75+
let sac_id = sandbox
76+
.new_assert_cmd("contract")
77+
.arg("asset")
78+
.arg("deploy")
79+
.arg(format!("--asset=USDC:{issuer}"))
80+
.assert()
81+
.success()
82+
.stdout_as_str();
83+
84+
sandbox
85+
.new_assert_cmd("contract")
86+
.arg("info")
87+
.arg("hash")
88+
.arg("--id")
89+
.arg(&sac_id)
90+
.assert()
91+
.failure();
92+
}
93+
94+
#[tokio::test]
95+
async fn info_hash_requires_one_source() {
96+
let sandbox = &TestEnv::new();
97+
98+
sandbox
99+
.new_assert_cmd("contract")
100+
.arg("info")
101+
.arg("hash")
102+
.assert()
103+
.failure();
104+
}
105+
106+
#[tokio::test]
107+
async fn info_hash_wasm_and_id_are_mutually_exclusive() {
108+
let sandbox = &TestEnv::new();
109+
let contract_id = deploy_contract(sandbox, HELLO_WORLD, DeployOptions::default()).await;
110+
111+
sandbox
112+
.new_assert_cmd("contract")
113+
.arg("info")
114+
.arg("hash")
115+
.arg("--wasm")
116+
.arg(HELLO_WORLD.path())
117+
.arg("--id")
118+
.arg(&contract_id)
119+
.assert()
120+
.failure();
121+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
mod fetch;
2+
mod info_hash;

cmd/soroban-cli/src/commands/contract/info.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::commands::global;
44

55
pub mod build;
66
pub mod env_meta;
7+
pub mod hash;
78
pub mod interface;
89
pub mod meta;
910
pub mod shared;
@@ -56,6 +57,15 @@ pub enum Cmd {
5657
/// If the contract has a meta entry like `source_repo=github:user/repo`, this command will try
5758
/// to fetch the attestation information for the WASM file.
5859
Build(build::Cmd),
60+
61+
/// Output the SHA-256 hash of a contract's Wasm.
62+
///
63+
/// The hash can be computed from a local .wasm file (`--wasm`) or read from a deployed
64+
/// contract (`--id`). The two flags are mutually exclusive.
65+
///
66+
/// Stellar Asset Contracts have no Wasm and therefore no hash; using `--id` against a
67+
/// SAC will return an error.
68+
Hash(hash::Cmd),
5969
}
6070

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

7282
#[error(transparent)]
7383
Build(#[from] build::Error),
84+
85+
#[error(transparent)]
86+
Hash(#[from] hash::Error),
7487
}
7588

7689
impl Cmd {
@@ -80,6 +93,7 @@ impl Cmd {
8093
Cmd::Meta(meta) => meta.run(global_args).await?,
8194
Cmd::EnvMeta(env_meta) => env_meta.run(global_args).await?,
8295
Cmd::Build(build) => build.run(global_args).await?,
96+
Cmd::Hash(hash) => hash.run(global_args).await?,
8397
}
8498

8599
Ok(())
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use std::path::PathBuf;
2+
3+
use clap::Parser;
4+
5+
use crate::{
6+
commands::global,
7+
config::{
8+
self, locator,
9+
network::{self},
10+
},
11+
wasm,
12+
};
13+
14+
#[derive(Parser, Debug, Clone)]
15+
#[command(group(
16+
clap::ArgGroup::new("source")
17+
.required(true)
18+
.args(&["wasm", "contract_id"]),
19+
))]
20+
#[group(skip)]
21+
pub struct Cmd {
22+
/// Path to a local .wasm file.
23+
#[arg(long, conflicts_with = "contract_id")]
24+
pub wasm: Option<PathBuf>,
25+
/// Contract ID or alias of a deployed contract.
26+
#[arg(
27+
long,
28+
visible_alias = "id",
29+
env = "STELLAR_CONTRACT_ID",
30+
conflicts_with = "wasm"
31+
)]
32+
pub contract_id: Option<config::UnresolvedContract>,
33+
#[command(flatten)]
34+
pub network: network::Args,
35+
#[command(flatten)]
36+
pub locator: locator::Args,
37+
}
38+
39+
#[derive(thiserror::Error, Debug)]
40+
pub enum Error {
41+
#[error(transparent)]
42+
Wasm(#[from] wasm::Error),
43+
#[error(transparent)]
44+
Network(#[from] network::Error),
45+
#[error(transparent)]
46+
Locator(#[from] locator::Error),
47+
}
48+
49+
impl Cmd {
50+
pub async fn run(&self, _global_args: &global::Args) -> Result<(), Error> {
51+
let hash = if let Some(path) = &self.wasm {
52+
wasm::Args { wasm: path.clone() }.hash()?
53+
} else if let Some(contract_id) = &self.contract_id {
54+
let network = self.network.get(&self.locator)?;
55+
let resolved =
56+
contract_id.resolve_contract_id(&self.locator, &network.network_passphrase)?;
57+
wasm::fetch_wasm_hash_from_contract(&resolved, &network).await?
58+
} else {
59+
unreachable!("clap ArgGroup guarantees one of --wasm or --contract-id is set");
60+
};
61+
62+
println!("{hash}");
63+
Ok(())
64+
}
65+
}

cmd/soroban-cli/src/wasm.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,22 @@ pub async fn fetch_from_wasm_hash(hash: Hash, network: &Network) -> Result<Vec<u
143143
let client = network.rpc_client()?;
144144
Ok(get_remote_wasm_from_hash(&client, &hash).await?)
145145
}
146+
147+
pub async fn fetch_wasm_hash_from_contract(
148+
stellar_strkey::Contract(contract_id): &stellar_strkey::Contract,
149+
network: &Network,
150+
) -> Result<Hash, Error> {
151+
tracing::trace!(?network);
152+
let client = network.rpc_client()?;
153+
client
154+
.verify_network_passphrase(Some(&network.network_passphrase))
155+
.await?;
156+
let data_entry = client.get_contract_data(contract_id).await?;
157+
if let ScVal::ContractInstance(contract) = &data_entry.val {
158+
return match &contract.executable {
159+
ContractExecutable::Wasm(hash) => Ok(hash.clone()),
160+
ContractExecutable::StellarAsset => Err(ContractIsStellarAsset),
161+
};
162+
}
163+
Err(UnexpectedContractToken(Box::new(data_entry)))
164+
}

0 commit comments

Comments
 (0)