Skip to main content

soroban_cli/
utils.rs

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