soroban_cli/
utils.rs

1use phf::phf_map;
2use sha2::{Digest, Sha256};
3use stellar_strkey::ed25519::PrivateKey;
4
5use crate::xdr::{
6    self, Asset, ContractIdPreimage, Hash, HashIdPreimage, HashIdPreimageContractId, Limits, ScMap,
7    ScMapEntry, ScVal, Transaction, TransactionSignaturePayload,
8    TransactionSignaturePayloadTaggedTransaction, WriteXdr,
9};
10
11pub use soroban_spec_tools::contract as contract_spec;
12
13use crate::config::network::Network;
14
15/// # Errors
16///
17/// Might return an error
18pub fn contract_hash(contract: &[u8]) -> Result<Hash, xdr::Error> {
19    Ok(Hash(Sha256::digest(contract).into()))
20}
21
22/// # Errors
23///
24/// Might return an error
25pub fn transaction_hash(
26    tx: &Transaction,
27    network_passphrase: &str,
28) -> Result<[u8; 32], xdr::Error> {
29    let signature_payload = TransactionSignaturePayload {
30        network_id: Hash(Sha256::digest(network_passphrase).into()),
31        tagged_transaction: TransactionSignaturePayloadTaggedTransaction::Tx(tx.clone()),
32    };
33    Ok(Sha256::digest(signature_payload.to_xdr(Limits::none())?).into())
34}
35
36static EXPLORERS: phf::Map<&'static str, &'static str> = phf_map! {
37    "Test SDF Network ; September 2015" => "https://stellar.expert/explorer/testnet",
38    "Public Global Stellar Network ; September 2015" => "https://stellar.expert/explorer/public",
39};
40
41pub fn explorer_url_for_transaction(network: &Network, tx_hash: &str) -> Option<String> {
42    EXPLORERS
43        .get(&network.network_passphrase)
44        .map(|base_url| format!("{base_url}/tx/{tx_hash}"))
45}
46
47pub fn explorer_url_for_contract(
48    network: &Network,
49    contract_id: &stellar_strkey::Contract,
50) -> Option<String> {
51    EXPLORERS
52        .get(&network.network_passphrase)
53        .map(|base_url| format!("{base_url}/contract/{contract_id}"))
54}
55
56/// # Errors
57///
58/// Might return an error
59pub fn contract_id_from_str(
60    contract_id: &str,
61) -> Result<stellar_strkey::Contract, stellar_strkey::DecodeError> {
62    Ok(
63        if let Ok(strkey) = stellar_strkey::Contract::from_string(contract_id) {
64            strkey
65        } else {
66            // strkey failed, try to parse it as a hex string, for backwards compatibility.
67            stellar_strkey::Contract(
68                soroban_spec_tools::utils::padded_hex_from_str(contract_id, 32)
69                    .map_err(|_| stellar_strkey::DecodeError::Invalid)?
70                    .try_into()
71                    .map_err(|_| stellar_strkey::DecodeError::Invalid)?,
72            )
73        },
74    )
75}
76
77/// # Errors
78/// May not find a config dir
79pub fn find_config_dir(mut pwd: std::path::PathBuf) -> std::io::Result<std::path::PathBuf> {
80    loop {
81        let stellar_dir = pwd.join(".stellar");
82        let stellar_exists = stellar_dir.exists();
83
84        let soroban_dir = pwd.join(".soroban");
85        let soroban_exists = soroban_dir.exists();
86
87        if stellar_exists && soroban_exists {
88            tracing::warn!("the .stellar and .soroban config directories exist at path {pwd:?}, using the .stellar");
89        }
90
91        if stellar_exists {
92            return Ok(stellar_dir);
93        }
94
95        if soroban_exists {
96            return Ok(soroban_dir);
97        }
98
99        if !pwd.pop() {
100            break;
101        }
102    }
103
104    Err(std::io::Error::other("stellar directory not found"))
105}
106
107pub(crate) fn into_signing_key(key: &PrivateKey) -> ed25519_dalek::SigningKey {
108    let secret: ed25519_dalek::SecretKey = key.0;
109    ed25519_dalek::SigningKey::from_bytes(&secret)
110}
111
112/// Used in tests
113#[allow(unused)]
114pub(crate) fn parse_secret_key(
115    s: &str,
116) -> Result<ed25519_dalek::SigningKey, stellar_strkey::DecodeError> {
117    Ok(into_signing_key(&PrivateKey::from_string(s)?))
118}
119
120pub fn is_hex_string(s: &str) -> bool {
121    s.chars().all(|s| s.is_ascii_hexdigit())
122}
123
124pub fn contract_id_hash_from_asset(
125    asset: &Asset,
126    network_passphrase: &str,
127) -> stellar_strkey::Contract {
128    let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into());
129    let preimage = HashIdPreimage::ContractId(HashIdPreimageContractId {
130        network_id,
131        contract_id_preimage: ContractIdPreimage::Asset(asset.clone()),
132    });
133    let preimage_xdr = preimage
134        .to_xdr(Limits::none())
135        .expect("HashIdPreimage should not fail encoding to xdr");
136    stellar_strkey::Contract(Sha256::digest(preimage_xdr).into())
137}
138
139pub fn get_name_from_stellar_asset_contract_storage(storage: &ScMap) -> Option<String> {
140    if let Some(ScMapEntry {
141        val: ScVal::Map(Some(map)),
142        ..
143    }) = storage
144        .iter()
145        .find(|ScMapEntry { key, .. }| key == &ScVal::Symbol("METADATA".try_into().unwrap()))
146    {
147        if let Some(ScMapEntry {
148            val: ScVal::String(name),
149            ..
150        }) = map
151            .iter()
152            .find(|ScMapEntry { key, .. }| key == &ScVal::Symbol("name".try_into().unwrap()))
153        {
154            Some(name.to_string())
155        } else {
156            None
157        }
158    } else {
159        None
160    }
161}
162
163pub mod http {
164    use crate::commands::version;
165    fn user_agent() -> String {
166        format!("{}/{}", env!("CARGO_PKG_NAME"), version::pkg())
167    }
168
169    /// Creates and returns a configured `reqwest::Client`.
170    ///
171    /// # Panics
172    ///
173    /// Panics if the Client initialization fails.
174    pub fn client() -> reqwest::Client {
175        // Why we panic here:
176        // 1. Client initialization failures are rare and usually indicate serious issues.
177        // 2. The application cannot function properly without a working HTTP client.
178        // 3. This simplifies error handling for callers, as they can assume a valid client.
179        reqwest::Client::builder()
180            .user_agent(user_agent())
181            .build()
182            .expect("Failed to build reqwest client")
183    }
184
185    /// Creates and returns a configured `reqwest::blocking::Client`.
186    ///
187    /// # Panics
188    ///
189    /// Panics if the Client initialization fails.
190    pub fn blocking_client() -> reqwest::blocking::Client {
191        reqwest::blocking::Client::builder()
192            .user_agent(user_agent())
193            .build()
194            .expect("Failed to build reqwest blocking client")
195    }
196}
197
198pub mod args {
199    #[derive(thiserror::Error, Debug)]
200    pub enum DeprecatedError<'a> {
201        #[error("This argument has been removed and will be not be recognized by the future versions of CLI: {0}"
202        )]
203        RemovedArgument(&'a str),
204    }
205
206    #[macro_export]
207    /// Mark argument as removed with an error to be printed when it's used.
208    macro_rules! error_on_use_of_removed_arg {
209        ($_type:ident, $message: expr) => {
210            |a: &str| {
211                Err::<$_type, utils::args::DeprecatedError>(
212                    utils::args::DeprecatedError::RemovedArgument($message),
213                )
214            }
215        };
216    }
217
218    /// Mark argument as deprecated with warning to be printed when it's used.
219    #[macro_export]
220    macro_rules! deprecated_arg {
221        (bool, $message: expr) => {
222            <_ as clap::builder::TypedValueParser>::map(
223                clap::builder::BoolValueParser::new(),
224                |x| {
225                    if (x) {
226                        $crate::print::Print::new(false).warnln($message);
227                    }
228                    x
229                },
230            )
231        };
232    }
233}
234
235pub mod rpc {
236    use crate::xdr;
237    use soroban_rpc::{Client, Error};
238    use stellar_xdr::curr::{Hash, LedgerEntryData, LedgerKey, Limits, ReadXdr};
239
240    pub async fn get_remote_wasm_from_hash(client: &Client, hash: &Hash) -> Result<Vec<u8>, Error> {
241        let code_key = LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() });
242        let contract_data = client.get_ledger_entries(&[code_key]).await?;
243        let entries = contract_data.entries.unwrap_or_default();
244        if entries.is_empty() {
245            return Err(Error::NotFound(
246                "Contract Code".to_string(),
247                hex::encode(hash),
248            ));
249        }
250        let contract_data_entry = &entries[0];
251        match LedgerEntryData::from_xdr_base64(&contract_data_entry.xdr, Limits::none())? {
252            LedgerEntryData::ContractCode(xdr::ContractCodeEntry { code, .. }) => Ok(code.into()),
253            scval => Err(Error::UnexpectedContractCodeDataType(scval)),
254        }
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_contract_id_from_str() {
264        // strkey
265        match contract_id_from_str("CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE") {
266            Ok(contract_id) => assert_eq!(
267                contract_id.0,
268                [
269                    0x36, 0x3e, 0xaa, 0x38, 0x67, 0x84, 0x1f, 0xba, 0xd0, 0xf4, 0xed, 0x88, 0xc7,
270                    0x79, 0xe4, 0xfe, 0x66, 0xe5, 0x6a, 0x24, 0x70, 0xdc, 0x98, 0xc0, 0xec, 0x9c,
271                    0x07, 0x3d, 0x05, 0xc7, 0xb1, 0x03,
272                ]
273            ),
274            Err(err) => panic!("Failed to parse contract id: {err}"),
275        }
276    }
277}