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::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#[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 pub fn client() -> reqwest::Client {
175 reqwest::Client::builder()
180 .user_agent(user_agent())
181 .build()
182 .expect("Failed to build reqwest client")
183 }
184
185 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 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 #[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 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}