1use phf::phf_map;
2use sha2::{Digest, Sha256};
3use stellar_strkey::ed25519::PrivateKey;
4
5use crate::{
6 print::Print,
7 xdr::{
8 self, Asset, ContractIdPreimage, Hash, HashIdPreimage, HashIdPreimageContractId, Limits,
9 ScMap, ScMapEntry, ScVal, Transaction, TransactionEnvelope, TransactionSignaturePayload,
10 TransactionSignaturePayloadTaggedTransaction, WriteXdr,
11 },
12};
13
14pub use soroban_spec_tools::contract as contract_spec;
15
16use crate::config::network::Network;
17
18pub fn contract_hash(contract: &[u8]) -> Result<Hash, xdr::Error> {
22 Ok(Hash(Sha256::digest(contract).into()))
23}
24
25pub fn transaction_env_hash(
32 tx_env: &TransactionEnvelope,
33 network_passphrase: &str,
34) -> Result<[u8; 32], xdr::Error> {
35 match tx_env {
36 TransactionEnvelope::Tx(ref v1_env) => transaction_hash(&v1_env.tx, network_passphrase),
37 TransactionEnvelope::TxFeeBump(ref fee_bump_env) => {
38 fee_bump_transaction_hash(&fee_bump_env.tx, network_passphrase)
39 }
40 TransactionEnvelope::TxV0(_) => Err(xdr::Error::Unsupported),
41 }
42}
43
44pub fn transaction_hash(
48 tx: &Transaction,
49 network_passphrase: &str,
50) -> Result<[u8; 32], xdr::Error> {
51 let signature_payload = TransactionSignaturePayload {
52 network_id: Hash(Sha256::digest(network_passphrase).into()),
53 tagged_transaction: TransactionSignaturePayloadTaggedTransaction::Tx(tx.clone()),
54 };
55 Ok(Sha256::digest(signature_payload.to_xdr(Limits::none())?).into())
56}
57
58pub fn fee_bump_transaction_hash(
62 fee_bump_tx: &xdr::FeeBumpTransaction,
63 network_passphrase: &str,
64) -> Result<[u8; 32], xdr::Error> {
65 let signature_payload = TransactionSignaturePayload {
66 network_id: Hash(Sha256::digest(network_passphrase).into()),
67 tagged_transaction: TransactionSignaturePayloadTaggedTransaction::TxFeeBump(
68 fee_bump_tx.clone(),
69 ),
70 };
71 Ok(Sha256::digest(signature_payload.to_xdr(Limits::none())?).into())
72}
73
74static EXPLORERS: phf::Map<&'static str, &'static str> = phf_map! {
75 "Test SDF Network ; September 2015" => "https://stellar.expert/explorer/testnet",
76 "Public Global Stellar Network ; September 2015" => "https://stellar.expert/explorer/public",
77};
78
79static LAB_CONTRACT_URLS: phf::Map<&'static str, &'static str> = phf_map! {
80 "Test SDF Network ; September 2015" => "https://lab.stellar.org/r/testnet/contract/{contract_id}",
81 "Public Global Stellar Network ; September 2015" => "https://lab.stellar.org/r/mainnet/contract/{contract_id}",
82};
83
84pub fn explorer_url_for_transaction(network: &Network, tx_hash: &str) -> Option<String> {
85 EXPLORERS
86 .get(&network.network_passphrase)
87 .map(|base_url| format!("{base_url}/tx/{tx_hash}"))
88}
89
90pub fn lab_url_for_contract(
91 network: &Network,
92 contract_id: &stellar_strkey::Contract,
93) -> Option<String> {
94 LAB_CONTRACT_URLS
95 .get(&network.network_passphrase)
96 .map(|base_url| base_url.replace("{contract_id}", &contract_id.to_string()))
97}
98
99pub fn contract_id_from_str(
103 contract_id: &str,
104) -> Result<stellar_strkey::Contract, stellar_strkey::DecodeError> {
105 Ok(
106 if let Ok(strkey) = stellar_strkey::Contract::from_string(contract_id) {
107 strkey
108 } else {
109 stellar_strkey::Contract(
111 soroban_spec_tools::utils::padded_hex_from_str(contract_id, 32)
112 .map_err(|_| stellar_strkey::DecodeError::Invalid)?
113 .try_into()
114 .map_err(|_| stellar_strkey::DecodeError::Invalid)?,
115 )
116 },
117 )
118}
119
120pub fn find_config_dir(mut pwd: std::path::PathBuf) -> std::io::Result<std::path::PathBuf> {
123 loop {
124 let stellar_dir = pwd.join(".stellar");
125 let stellar_exists = stellar_dir.exists();
126
127 let soroban_dir = pwd.join(".soroban");
128 let soroban_exists = soroban_dir.exists();
129
130 if stellar_exists && soroban_exists {
131 tracing::warn!("the .stellar and .soroban config directories exist at path {pwd:?}, using the .stellar");
132 }
133
134 if stellar_exists {
135 return Ok(stellar_dir);
136 }
137
138 if soroban_exists {
139 return Ok(soroban_dir);
140 }
141
142 if !pwd.pop() {
143 break;
144 }
145 }
146
147 Err(std::io::Error::other("stellar directory not found"))
148}
149
150pub(crate) fn into_signing_key(key: &PrivateKey) -> ed25519_dalek::SigningKey {
151 let secret: ed25519_dalek::SecretKey = key.0;
152 ed25519_dalek::SigningKey::from_bytes(&secret)
153}
154
155pub fn deprecate_message(print: Print, arg: &str, hint: &str) {
156 print.warnln(
157 format!("`{arg}` is deprecated and will be removed in future versions of the CLI. {hint}")
158 .trim(),
159 );
160}
161
162#[allow(unused)]
164pub(crate) fn parse_secret_key(
165 s: &str,
166) -> Result<ed25519_dalek::SigningKey, stellar_strkey::DecodeError> {
167 Ok(into_signing_key(&PrivateKey::from_string(s)?))
168}
169
170pub fn is_hex_string(s: &str) -> bool {
171 s.chars().all(|s| s.is_ascii_hexdigit())
172}
173
174pub fn contract_id_hash_from_asset(
175 asset: &Asset,
176 network_passphrase: &str,
177) -> stellar_strkey::Contract {
178 let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into());
179 let preimage = HashIdPreimage::ContractId(HashIdPreimageContractId {
180 network_id,
181 contract_id_preimage: ContractIdPreimage::Asset(asset.clone()),
182 });
183 let preimage_xdr = preimage
184 .to_xdr(Limits::none())
185 .expect("HashIdPreimage should not fail encoding to xdr");
186 stellar_strkey::Contract(Sha256::digest(preimage_xdr).into())
187}
188
189pub fn get_name_from_stellar_asset_contract_storage(storage: &ScMap) -> Option<String> {
190 if let Some(ScMapEntry {
191 val: ScVal::Map(Some(map)),
192 ..
193 }) = storage
194 .iter()
195 .find(|ScMapEntry { key, .. }| key == &ScVal::Symbol("METADATA".try_into().unwrap()))
196 {
197 if let Some(ScMapEntry {
198 val: ScVal::String(name),
199 ..
200 }) = map
201 .iter()
202 .find(|ScMapEntry { key, .. }| key == &ScVal::Symbol("name".try_into().unwrap()))
203 {
204 Some(name.to_string())
205 } else {
206 None
207 }
208 } else {
209 None
210 }
211}
212
213pub mod http {
214 use crate::commands::version;
215 fn user_agent() -> String {
216 format!("{}/{}", env!("CARGO_PKG_NAME"), version::pkg())
217 }
218
219 pub fn client() -> reqwest::Client {
225 reqwest::Client::builder()
230 .user_agent(user_agent())
231 .build()
232 .expect("Failed to build reqwest client")
233 }
234
235 pub fn blocking_client() -> reqwest::blocking::Client {
241 reqwest::blocking::Client::builder()
242 .user_agent(user_agent())
243 .build()
244 .expect("Failed to build reqwest blocking client")
245 }
246}
247
248pub mod args {
249 #[derive(thiserror::Error, Debug)]
250 pub enum DeprecatedError<'a> {
251 #[error("This argument has been removed and will be not be recognized by the future versions of CLI: {0}"
252 )]
253 RemovedArgument(&'a str),
254 }
255
256 #[macro_export]
257 macro_rules! error_on_use_of_removed_arg {
259 ($_type:ident, $message: expr) => {
260 |a: &str| {
261 Err::<$_type, utils::args::DeprecatedError>(
262 utils::args::DeprecatedError::RemovedArgument($message),
263 )
264 }
265 };
266 }
267
268 #[macro_export]
270 macro_rules! deprecated_arg {
271 (bool, $message: expr) => {
272 <_ as clap::builder::TypedValueParser>::map(
273 clap::builder::BoolValueParser::new(),
274 |x| {
275 if (x) {
276 $crate::print::Print::new(false).warnln($message);
277 }
278 x
279 },
280 )
281 };
282 }
283}
284
285pub mod rpc {
286 use crate::xdr;
287 use soroban_rpc::{Client, Error};
288 use stellar_xdr::curr::{Hash, LedgerEntryData, LedgerKey, Limits, ReadXdr};
289
290 pub async fn get_remote_wasm_from_hash(client: &Client, hash: &Hash) -> Result<Vec<u8>, Error> {
291 let code_key = LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() });
292 let contract_data = client.get_ledger_entries(&[code_key]).await?;
293 let entries = contract_data.entries.unwrap_or_default();
294 if entries.is_empty() {
295 return Err(Error::NotFound(
296 "Contract Code".to_string(),
297 hex::encode(hash),
298 ));
299 }
300 let contract_data_entry = &entries[0];
301 let code = match LedgerEntryData::from_xdr_base64(&contract_data_entry.xdr, Limits::none())?
302 {
303 LedgerEntryData::ContractCode(xdr::ContractCodeEntry { code, .. }) => Vec::from(code),
304 scval => return Err(Error::UnexpectedContractCodeDataType(scval)),
305 };
306 super::verify_wasm_hash(&code, hash)?;
307 Ok(code)
308 }
309}
310
311fn verify_wasm_hash(code: &[u8], expected_hash: &Hash) -> Result<(), soroban_rpc::Error> {
314 let computed_hash = Hash(Sha256::digest(code).into());
315 if computed_hash != *expected_hash {
316 return Err(soroban_rpc::Error::NotFound(
317 "WASM hash mismatch".to_string(),
318 format!(
319 "expected {}, got {}",
320 hex::encode(expected_hash.0),
321 hex::encode(computed_hash.0),
322 ),
323 ));
324 }
325 Ok(())
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn test_contract_id_from_str() {
334 match contract_id_from_str("CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE") {
336 Ok(contract_id) => assert_eq!(
337 contract_id.0,
338 [
339 0x36, 0x3e, 0xaa, 0x38, 0x67, 0x84, 0x1f, 0xba, 0xd0, 0xf4, 0xed, 0x88, 0xc7,
340 0x79, 0xe4, 0xfe, 0x66, 0xe5, 0x6a, 0x24, 0x70, 0xdc, 0x98, 0xc0, 0xec, 0x9c,
341 0x07, 0x3d, 0x05, 0xc7, 0xb1, 0x03,
342 ]
343 ),
344 Err(err) => panic!("Failed to parse contract id: {err}"),
345 }
346 }
347
348 #[test]
349 fn test_verify_wasm_hash_matching() {
350 use sha2::{Digest, Sha256};
351 use stellar_xdr::curr::Hash;
352
353 let wasm_bytes = b"\0asm fake wasm content";
354 let correct_hash = Hash(Sha256::digest(wasm_bytes).into());
355 assert!(verify_wasm_hash(wasm_bytes, &correct_hash).is_ok());
356 }
357
358 #[test]
359 fn test_verify_wasm_hash_mismatch() {
360 use stellar_xdr::curr::Hash;
361
362 let wasm_bytes = b"\0asm fake wasm content";
363 let wrong_hash = Hash([0xAB; 32]);
364 let err = verify_wasm_hash(wasm_bytes, &wrong_hash).unwrap_err();
365 let err_msg = err.to_string();
366 assert!(
367 err_msg.contains("WASM hash mismatch"),
368 "expected 'WASM hash mismatch' in error: {err_msg}"
369 );
370 assert!(
371 err_msg.contains("abababababababababababababababababababababababababababababababab"),
372 "expected expected-hash in error: {err_msg}"
373 );
374 assert!(
375 err_msg.contains("501dc4e05f47c4713c4a27e89a5b07ed769bb2cc858bcf46de9bed13ae65af29"),
376 "expected computed-hash in error: {err_msg}"
377 );
378 }
379}