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
15pub fn contract_hash(contract: &[u8]) -> Result<Hash, xdr::Error> {
19 Ok(Hash(Sha256::digest(contract).into()))
20}
21
22pub 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
56pub 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 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
77pub 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#[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 pub fn client() -> reqwest::Client {
178 reqwest::Client::builder()
183 .user_agent(user_agent())
184 .build()
185 .expect("Failed to build reqwest client")
186 }
187
188 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 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 #[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 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}