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::new(
105        std::io::ErrorKind::Other,
106        "stellar directory not found",
107    ))
108}
109
110pub(crate) fn into_signing_key(key: &PrivateKey) -> ed25519_dalek::SigningKey {
111    let secret: ed25519_dalek::SecretKey = key.0;
112    ed25519_dalek::SigningKey::from_bytes(&secret)
113}
114
115/// Used in tests
116#[allow(unused)]
117pub(crate) fn parse_secret_key(
118    s: &str,
119) -> Result<ed25519_dalek::SigningKey, stellar_strkey::DecodeError> {
120    Ok(into_signing_key(&PrivateKey::from_string(s)?))
121}
122
123pub fn is_hex_string(s: &str) -> bool {
124    s.chars().all(|s| s.is_ascii_hexdigit())
125}
126
127pub fn contract_id_hash_from_asset(
128    asset: &Asset,
129    network_passphrase: &str,
130) -> stellar_strkey::Contract {
131    let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into());
132    let preimage = HashIdPreimage::ContractId(HashIdPreimageContractId {
133        network_id,
134        contract_id_preimage: ContractIdPreimage::Asset(asset.clone()),
135    });
136    let preimage_xdr = preimage
137        .to_xdr(Limits::none())
138        .expect("HashIdPreimage should not fail encoding to xdr");
139    stellar_strkey::Contract(Sha256::digest(preimage_xdr).into())
140}
141
142pub fn get_name_from_stellar_asset_contract_storage(storage: &ScMap) -> Option<String> {
143    if let Some(ScMapEntry {
144        val: ScVal::Map(Some(map)),
145        ..
146    }) = storage
147        .iter()
148        .find(|ScMapEntry { key, .. }| key == &ScVal::Symbol("METADATA".try_into().unwrap()))
149    {
150        if let Some(ScMapEntry {
151            val: ScVal::String(name),
152            ..
153        }) = map
154            .iter()
155            .find(|ScMapEntry { key, .. }| key == &ScVal::Symbol("name".try_into().unwrap()))
156        {
157            Some(name.to_string())
158        } else {
159            None
160        }
161    } else {
162        None
163    }
164}
165
166pub mod http {
167    use crate::commands::version;
168    fn user_agent() -> String {
169        format!("{}/{}", env!("CARGO_PKG_NAME"), version::pkg())
170    }
171
172    /// Creates and returns a configured `reqwest::Client`.
173    ///
174    /// # Panics
175    ///
176    /// Panics if the Client initialization fails.
177    pub fn client() -> reqwest::Client {
178        // Why we panic here:
179        // 1. Client initialization failures are rare and usually indicate serious issues.
180        // 2. The application cannot function properly without a working HTTP client.
181        // 3. This simplifies error handling for callers, as they can assume a valid client.
182        reqwest::Client::builder()
183            .user_agent(user_agent())
184            .build()
185            .expect("Failed to build reqwest client")
186    }
187
188    /// Creates and returns a configured `reqwest::blocking::Client`.
189    ///
190    /// # Panics
191    ///
192    /// Panics if the Client initialization fails.
193    pub fn blocking_client() -> reqwest::blocking::Client {
194        reqwest::blocking::Client::builder()
195            .user_agent(user_agent())
196            .build()
197            .expect("Failed to build reqwest blocking client")
198    }
199}
200
201pub mod args {
202    #[derive(thiserror::Error, Debug)]
203    pub enum DeprecatedError<'a> {
204        #[error("This argument has been removed and will be not be recognized by the future versions of CLI: {0}"
205        )]
206        RemovedArgument(&'a str),
207    }
208
209    #[macro_export]
210    /// Mark argument as removed with an error to be printed when it's used.
211    macro_rules! error_on_use_of_removed_arg {
212        ($_type:ident, $message: expr) => {
213            |a: &str| {
214                Err::<$_type, utils::args::DeprecatedError>(
215                    utils::args::DeprecatedError::RemovedArgument($message),
216                )
217            }
218        };
219    }
220
221    /// Mark argument as deprecated with warning to be printed when it's used.
222    #[macro_export]
223    macro_rules! deprecated_arg {
224        (bool, $message: expr) => {
225            <_ as clap::builder::TypedValueParser>::map(
226                clap::builder::BoolValueParser::new(),
227                |x| {
228                    if (x) {
229                        $crate::print::Print::new(false).warnln($message);
230                    }
231                    x
232                },
233            )
234        };
235    }
236}
237
238pub mod rpc {
239    use crate::xdr;
240    use soroban_rpc::{Client, Error};
241    use stellar_xdr::curr::{Hash, LedgerEntryData, LedgerKey, Limits, ReadXdr};
242
243    pub async fn get_remote_wasm_from_hash(client: &Client, hash: &Hash) -> Result<Vec<u8>, Error> {
244        let code_key = LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() });
245        let contract_data = client.get_ledger_entries(&[code_key]).await?;
246        let entries = contract_data.entries.unwrap_or_default();
247        if entries.is_empty() {
248            return Err(Error::NotFound(
249                "Contract Code".to_string(),
250                hex::encode(hash),
251            ));
252        }
253        let contract_data_entry = &entries[0];
254        match LedgerEntryData::from_xdr_base64(&contract_data_entry.xdr, Limits::none())? {
255            LedgerEntryData::ContractCode(xdr::ContractCodeEntry { code, .. }) => Ok(code.into()),
256            scval => Err(Error::UnexpectedContractCodeDataType(scval)),
257        }
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_contract_id_from_str() {
267        // strkey
268        match contract_id_from_str("CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE") {
269            Ok(contract_id) => assert_eq!(
270                contract_id.0,
271                [
272                    0x36, 0x3e, 0xaa, 0x38, 0x67, 0x84, 0x1f, 0xba, 0xd0, 0xf4, 0xed, 0x88, 0xc7,
273                    0x79, 0xe4, 0xfe, 0x66, 0xe5, 0x6a, 0x24, 0x70, 0xdc, 0x98, 0xc0, 0xec, 0x9c,
274                    0x07, 0x3d, 0x05, 0xc7, 0xb1, 0x03,
275                ]
276            ),
277            Err(err) => panic!("Failed to parse contract id: {err}"),
278        }
279    }
280}