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
18pub fn contract_hash(contract: &[u8]) -> Result<Hash, xdr::Error> {
22 Ok(Hash(Sha256::digest(contract).into()))
23}
24
25pub 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
64pub 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 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
85pub 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#[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 pub fn client() -> reqwest::Client {
190 reqwest::Client::builder()
195 .user_agent(user_agent())
196 .build()
197 .expect("Failed to build reqwest client")
198 }
199
200 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 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 #[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 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}