use crate::client::SorobanRpcClient;
use crate::error::RpcError;
use crate::wasm::get_wasm_bytecode;
use sdkt_wasm::{parse_contract_spec, ContractSpec};
use sdkt_xdr::{decode_contract_id, encode_ledger_key, extract_wasm_hash, LedgerKeyParams};
use serde::Deserialize;
use serde::Serialize;
pub(crate) fn contract_id_to_hex(contract_id: &str) -> Result<String, RpcError> {
if let Ok(bytes) = hex::decode(contract_id) {
if bytes.len() == 32 {
return Ok(contract_id.to_string());
}
}
let hash = decode_contract_id(contract_id)
.map_err(|e| RpcError::Rpc(format!("Invalid contract ID '{contract_id}': {e}")))?;
Ok(hex::encode(hash.0))
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct ContractAbiSummary {
pub functions: Vec<String>,
pub events: Vec<String>,
pub types: Vec<String>,
}
impl ContractAbiSummary {
pub fn from_spec(spec: &ContractSpec) -> Self {
Self {
functions: spec.functions.iter().map(|f| f.name.clone()).collect(),
events: spec.events.iter().map(|e| e.name.clone()).collect(),
types: spec.custom_types.iter().map(|t| t.name.clone()).collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContractInspection {
pub contract_id: String,
pub wasm_hash: String,
pub wasm_size: Option<usize>,
pub abi: Option<ContractAbiSummary>,
pub storage_summary: StorageSummary,
pub ttl_info: Option<TtlInfoSummary>,
pub storage_keys: Vec<StorageKeyInfo>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct TtlInfoSummary {
pub minimum_ttl: u32,
pub maximum_ttl: u32,
pub average_ttl: u32,
pub expiring_entries_count: usize,
pub estimated_rent_cost: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct StorageSummary {
pub instance_entries: usize,
pub persistent_entries: usize,
pub temporary_entries: usize,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StorageKeyInfo {
pub key: String,
pub key_type: String,
pub permissions: String,
}
pub async fn inspect_contract(
client: &SorobanRpcClient,
contract_id: &str,
) -> Result<ContractInspection, RpcError> {
let contract_id_hex = contract_id_to_hex(contract_id)?;
let encoded_key = encode_ledger_key(&LedgerKeyParams::ContractData(contract_id_hex.clone()))
.map_err(|e| RpcError::Rpc(format!("Failed to encode ledger key: {e}")))?;
let response = client
.get_contract_storage(contract_id, &[encoded_key])
.await?;
if response.entries.is_empty() {
return Err(RpcError::ContractNotFound);
}
let first_entry = &response.entries[0];
let wasm_hash = extract_wasm_hash(&first_entry.xdr)
.map_err(|e| RpcError::Rpc(format!("Failed to extract WASM hash: {e}")))?;
let mut wasm_size = None;
let mut abi = None;
if let Ok(bytes) = get_wasm_bytecode(client, &wasm_hash).await {
wasm_size = Some(bytes.len());
if let Ok(spec) = parse_contract_spec(&bytes) {
abi = Some(ContractAbiSummary::from_spec(&spec));
}
}
Ok(ContractInspection {
contract_id: contract_id.to_string(),
wasm_hash,
wasm_size,
abi,
storage_summary: StorageSummary::default(),
ttl_info: None,
storage_keys: Vec::new(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn abi_summary_from_spec_lists_names() {
let spec = ContractSpec {
env_meta: None,
functions: vec![sdkt_wasm::ContractFunction {
name: "transfer".into(),
doc: String::new(),
parameters: vec![],
outputs: vec![],
}],
custom_types: vec![sdkt_wasm::ContractType {
name: "Asset".into(),
kind: "Struct".into(),
doc: String::new(),
members: vec![],
}],
events: vec![sdkt_wasm::ContractEvent {
name: "transfer".into(),
doc: String::new(),
}],
};
let summary = ContractAbiSummary::from_spec(&spec);
assert_eq!(summary.functions, vec!["transfer".to_string()]);
assert_eq!(summary.events, vec!["transfer".to_string()]);
assert_eq!(summary.types, vec!["Asset".to_string()]);
}
#[test]
fn abi_summary_default_is_empty() {
let s = ContractAbiSummary::default();
assert!(s.functions.is_empty());
assert!(s.events.is_empty());
assert!(s.types.is_empty());
}
#[test]
fn contract_id_to_hex_accepts_strkey_c_address() {
let c = "CAE3U7JKESRWZHPEQ72DVNGOQ6WPA7HSPQZL5YV46NPCE4TMUPAGYMEC";
let hex = contract_id_to_hex(c).expect("StrKey C... should decode");
assert_eq!(hex.len(), 64);
assert_eq!(contract_id_to_hex(&hex).unwrap(), hex);
assert_eq!(
hex,
"09ba7d2a24a36c9de487f43ab4ce87acf07cf27c32bee2bcf35e22726ca3c06c"
);
}
#[test]
fn contract_id_to_hex_rejects_garbage() {
assert!(contract_id_to_hex("not-a-contract-id").is_err());
assert!(
contract_id_to_hex("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF").is_err()
);
}
}