//! XDR decoding engine for Stellar and Soroban structures.
//!
//! Handles conversion of raw Base64, Hex, or raw-byte XDR payloads into
//! standardized JSON formats (compact or pretty-printed).
//!
//! # Supported Types
//!
//! - `ScVal`
//! - `TransactionEnvelope`
//! - `TransactionResult`
//! - `TransactionMeta`
//! - `LedgerKey`
//! - `LedgerEntry`
//! - `ContractEvent` (auto + explicit)
//!
//! # Example
//!
//! ```rust
//! use sdkt_xdr::decode;
//!
//! let b64 = "AAAAAQAAAAoAAAAA"; // ScVal::I32(1)
//! let json = decode(b64, Some("scval"), sdkt_xdr::OutputFormat::Json).unwrap();
//! println!("{}", json);
//! ```
pub mod builder;
pub mod envelope;
pub mod sign;
pub mod typed;
pub use builder::{
build_create_contract_tx, build_create_contract_tx_with_data,
build_create_contract_tx_with_data_and_auth, build_create_contract_v2_tx,
build_create_contract_v2_tx_with_data, build_create_contract_v2_tx_with_data_and_auth,
build_extend_footprint_tx, build_extend_footprint_tx_with_data, build_invoke_transaction,
build_invoke_transaction_with_data, build_restore_footprint_tx, build_upload_wasm_tx,
build_upload_wasm_tx_with_data, decode_account_id, decode_contract_id, decode_ledger_key,
derive_contract_id, memo_id, memo_text, merge_footprint_keys, parse_scval_args,
parse_soroban_transaction_data, CreateContractParams, CreateContractV2Params,
ExtendFootprintParams, InvokeTransactionParams, RestoreFootprintParams, UploadWasmParams,
};
pub use envelope::{decode_envelope, render_scval, view_envelope, EnvelopeView};
pub use sign::{
sign_envelope_with, sign_transaction, verify_signature, Ed25519Signer, Network, Signer,
SigningError, SigningOptions,
};
pub use typed::{
decode_scvals, decode_scvals_ref, encode_scvals, json_args_to_base64, json_to_scval,
scval_from_base64, scval_to_base64, Address, FromScVal, IntoScVal, ScValError,
};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use serde_json::Value;
pub use sdkt_core::OutputFormat;
use stellar_xdr::{
ContractDataDurability, ContractEvent, ContractExecutable, ContractId, Hash, LedgerEntry,
LedgerEntryData, LedgerKey, LedgerKeyContractCode, LedgerKeyContractData, Limited, Limits,
ReadXdr, ScAddress, ScSymbol, ScVal, ScVec, TransactionEnvelope, TransactionMeta,
TransactionResult, VecM, WriteXdr,
};
use thiserror::Error;
/// Errors returned by the decoder.
#[derive(Error, Debug)]
pub enum DecodeError {
#[error("Base64 decode failed: {0}")]
Base64(#[from] base64::DecodeError),
#[error("Hex decode failed: {0}")]
Hex(#[from] hex::FromHexError),
#[error("XDR parse failed for type '{0}': {1}")]
XdrParse(String, stellar_xdr::Error),
#[error("XDR write failed: {0}")]
XdrWrite(stellar_xdr::Error),
#[error("Unknown XDR type: {0}")]
TypeUnknown(String),
#[error("Invalid input: empty payload")]
EmptyPayload,
#[error("JSON serialization failed: {0}")]
Json(#[from] serde_json::Error),
#[error("Extraction error: {0}")]
Extraction(String),
}
/// Parameters for constructing a `LedgerKey`.
pub enum LedgerKeyParams {
/// A contract's instance data key. Takes the contract ID as a hex string.
ContractData(String),
/// A contract's WASM code key. Takes the WASM hash as a hex string.
ContractCode(String),
/// An arbitrary contract-data entry: the contract (`C...` StrKey or 32-byte
/// hex), the entry's key `ScVal`, and its durability. This is the general
/// form behind typed storage-key construction (e.g. a persistent map entry
/// keyed by `ScVec[symbol, address]`).
ContractDataEntry {
contract: String,
key: ScVal,
durability: ContractDataDurability,
},
}
/// Parse a contract identifier supplied as either a `C...` StrKey or a 32-byte
/// hex string into its raw 32-byte contract ID.
fn parse_contract_id_bytes(contract: &str) -> Result<[u8; 32], DecodeError> {
let trimmed = contract.trim();
if let Ok(stellar_strkey::Strkey::Contract(c)) = stellar_strkey::Strkey::from_string(trimmed) {
return Ok(c.0);
}
let bytes = hex::decode(trimmed).map_err(DecodeError::Hex)?;
if bytes.len() != 32 {
return Err(DecodeError::Extraction(
"contract must be a C... StrKey or 32-byte hex".to_string(),
));
}
let mut id = [0u8; 32];
id.copy_from_slice(&bytes);
Ok(id)
}
/// Build a validated `ScVal::Symbol`.
///
/// Soroban `Symbol`s are restricted to <=32 chars of [a-zA-Z0-9_]. `ScSymbol`
/// itself only enforces the length bound, so validate the charset here — an
/// out-of-charset symbol would silently build a value no contract ever emits.
pub fn symbol_scval(symbol: &str) -> Result<ScVal, DecodeError> {
if symbol.is_empty()
|| symbol.len() > 32
|| !symbol
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_')
{
return Err(DecodeError::Extraction(format!(
"invalid symbol {symbol:?}: must be 1..=32 chars of [a-zA-Z0-9_]"
)));
}
let sym: ScSymbol = symbol.try_into().map_err(|_| {
DecodeError::Extraction(format!(
"invalid symbol {symbol:?}: must be <=32 chars of [a-zA-Z0-9_]"
))
})?;
Ok(ScVal::Symbol(sym))
}
/// Encode an event-name symbol as the base64 XDR `ScVal` the RPC `getEvents`
/// topic matcher compares against (e.g. `topic[0]` of a contract event).
pub fn symbol_topic_base64(symbol: &str) -> Result<String, DecodeError> {
scval_to_base64(&symbol_scval(symbol)?)
.map_err(|e| DecodeError::Extraction(format!("failed to encode symbol {symbol:?}: {e}")))
}
/// Build a Soroban "enum/map"-style storage key `ScVal`: an `ScVec` whose first
/// element is `symbol` and whose remaining elements are the decoded typed key
/// arguments (each supplied as a Base64 `ScVal`).
///
/// This mirrors how the Soroban SDK encodes `DataKey`-style enum keys, e.g.
/// `DataKey::Balance(addr)` → `ScVec[symbol("Balance"), address]`, so it also
/// covers the single-symbol case (`ScVec[symbol("...")]`) when no args are given.
pub fn build_map_key(symbol: &str, arg_scvals_b64: &[String]) -> Result<ScVal, DecodeError> {
let mut elems: Vec<ScVal> = Vec::with_capacity(1 + arg_scvals_b64.len());
elems.push(symbol_scval(symbol)?);
for b64 in arg_scvals_b64 {
let scval = scval_from_base64(b64).ok_or_else(|| {
DecodeError::Extraction(format!("invalid Base64 ScVal key argument: {b64}"))
})?;
elems.push(scval);
}
let vec: VecM<ScVal> = elems
.try_into()
.map_err(|_| DecodeError::Extraction("too many key arguments".to_string()))?;
Ok(ScVal::Vec(Some(ScVec(vec))))
}
/// Encodes `LedgerKeyParams` into a Base64 XDR `LedgerKey`.
pub fn encode_ledger_key(params: &LedgerKeyParams) -> Result<String, DecodeError> {
let key = match params {
LedgerKeyParams::ContractData(contract_id_hex) => {
let hash_bytes = hex::decode(contract_id_hex).map_err(DecodeError::Hex)?;
if hash_bytes.len() != 32 {
return Err(DecodeError::Extraction(
"Contract ID must be 32 bytes".to_string(),
));
}
let mut contract_id = [0u8; 32];
contract_id.copy_from_slice(&hash_bytes);
LedgerKey::ContractData(LedgerKeyContractData {
contract: ScAddress::Contract(ContractId(Hash(contract_id))),
key: ScVal::LedgerKeyContractInstance,
durability: stellar_xdr::ContractDataDurability::Persistent,
})
}
LedgerKeyParams::ContractCode(wasm_hash_hex) => {
let hash_bytes = hex::decode(wasm_hash_hex).map_err(DecodeError::Hex)?;
if hash_bytes.len() != 32 {
return Err(DecodeError::Extraction(
"WASM hash must be 32 bytes".to_string(),
));
}
let mut wasm_hash = [0u8; 32];
wasm_hash.copy_from_slice(&hash_bytes);
LedgerKey::ContractCode(LedgerKeyContractCode {
hash: Hash(wasm_hash),
})
}
LedgerKeyParams::ContractDataEntry {
contract,
key,
durability,
} => {
let contract_id = parse_contract_id_bytes(contract)?;
LedgerKey::ContractData(LedgerKeyContractData {
contract: ScAddress::Contract(ContractId(Hash(contract_id))),
key: key.clone(),
durability: *durability,
})
}
};
let mut buf = Vec::new();
let mut l = Limited::new(&mut buf, Limits::none());
key.write_xdr(&mut l).map_err(DecodeError::XdrWrite)?;
Ok(STANDARD.encode(&buf))
}
/// Extracts the WASM hash from a Base64 encoded `LedgerEntry`.
/// Traverses: LedgerEntry -> LedgerEntryData::ContractData -> ScVal::ContractInstance -> ContractExecutable::Wasm -> Hash
///
/// Compatibility bridge: some live Stellar/Soroban RPC endpoints (e.g. the current
/// testnet) serialize `LedgerEntry` with the `data` union FIRST, whereas stellar-xdr
/// 28.0.0 decodes `lastModifiedLedgerSeq` first. When the standard decode fails we
/// fall back to [`extract_wasm_hash_from_live_ledger_entry`], which reads the
/// `LedgerEntryData` union first and ignores the trailing ledger-seq/ext fields. This
/// is intentionally scoped to ContractData/Wasm extraction only — it does NOT replace
/// stellar-xdr's generic `LedgerEntry` decoder used elsewhere.
pub fn extract_wasm_hash(base64_ledger_entry: &str) -> Result<String, DecodeError> {
let raw = detect_and_decode(base64_ledger_entry)?;
// Fast path: standard stellar-xdr 28.0.0 layout.
match extract_wasm_hash_standard(&raw) {
Ok(hash) => Ok(hash),
// Compatibility bridge: live data-first `LedgerEntry` wire layout.
// On failure, keep the original (standard) error so callers still see the
// expected `Extraction`/`XdrParse` classification for non-Wasm/non-contract
// entries.
Err(e) => match e {
DecodeError::XdrParse(..) => extract_wasm_hash_from_live_ledger_entry(&raw),
_ => Err(e),
},
}
}
/// Standard (stellar-xdr 28.0.0) `LedgerEntry` decode: lastModifiedLedgerSeq first.
fn extract_wasm_hash_standard(raw: &[u8]) -> Result<String, DecodeError> {
let mut cursor = std::io::Cursor::new(raw);
let mut l = Limited::new(&mut cursor, Limits::none());
let entry = LedgerEntry::read_xdr(&mut l)
.map_err(|e| DecodeError::XdrParse("LedgerEntry".to_string(), e))?;
let data = match entry.data {
LedgerEntryData::ContractData(d) => d,
_ => return Err(DecodeError::Extraction("Not a ContractData entry".into())),
};
extract_hash_from_contract_data(data)
}
/// Live-wire compatibility decoder: reads the `LedgerEntryData` union FIRST (the order
/// the live testnet RPC uses), then ignores any trailing `lastModifiedLedgerSeq`/ext
/// bytes. Scope is strictly ContractData/Wasm hash extraction.
pub fn extract_wasm_hash_from_live_ledger_entry(raw: &[u8]) -> Result<String, DecodeError> {
let mut cursor = std::io::Cursor::new(raw);
let mut l = Limited::new(&mut cursor, Limits::none());
let data = LedgerEntryData::read_xdr(&mut l)
.map_err(|e| DecodeError::XdrParse("LedgerEntryData(live)".to_string(), e))?;
let cd = match data {
LedgerEntryData::ContractData(d) => d,
_ => return Err(DecodeError::Extraction("Not a ContractData entry".into())),
};
extract_hash_from_contract_data(cd)
}
/// A contract's executable as recorded in its `ContractInstance` ledger entry.
///
/// Protocol 28 (CAP-0085) widened the `ContractExecutable` union beyond an
/// inline Wasm hash, so "the contract's code" is no longer always a single
/// `Hash`. This mirrors the XDR union faithfully; callers that need the actual
/// code resolve the referenced entry through the RPC layer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContractExecutableRef {
/// `CONTRACT_EXECUTABLE_WASM`: the instance's own Wasm hash.
Wasm(Hash),
/// `CONTRACT_EXECUTABLE_STELLAR_ASSET`: a Stellar Asset Contract. This is
/// protocol-defined code with no Wasm artifact of its own — there is no hash
/// to report and nothing to download.
StellarAsset,
/// `CONTRACT_EXECUTABLE_EXTERNAL_REF` (CAP-0085): the instance holds no
/// code; it references an entry owned by another contract.
ExternalRef(ExternalExecutableRef),
}
/// The owner + tag pair of a CAP-85 `CONTRACT_EXECUTABLE_EXTERNAL_REF`.
///
/// Per CAP-0085 the referenced Wasm hash lives in a **persistent contract-data
/// entry of `executable_owner`, keyed by `tag`**, with the value being the 32
/// raw hash bytes. The owner contract is not invoked for the lookup.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExternalExecutableRef {
/// Owner contract holding the executable entry (CAP-85 `executable_owner`).
pub owner: ScAddress,
/// The tag keying the owner's entry (CAP-85 `tag`, an `ScString`).
pub tag: String,
}
impl ContractExecutableRef {
/// Stable lowercase label (`wasm`, `stellar_asset`, `external_ref`).
pub fn kind(&self) -> &'static str {
match self {
ContractExecutableRef::Wasm(_) => "wasm",
ContractExecutableRef::StellarAsset => "stellar_asset",
ContractExecutableRef::ExternalRef(_) => "external_ref",
}
}
/// The ledger key of the owner's executable entry, for resolution.
///
/// CAP-0085 keys this entry with `SCV_EXECUTABLE_TAG` and stores it as
/// `Persistent` contract data (the host rejects the `Temporary` and
/// `Instance` durabilities for such entries).
pub fn executable_tag_key(&self) -> Option<&ExternalExecutableRef> {
match self {
ContractExecutableRef::ExternalRef(r) => Some(r),
_ => None,
}
}
}
/// Decode a contract instance's executable out of a Base64 XDR `LedgerEntry`.
///
/// Accepts both wire layouts SDKT already tolerates (standard
/// `lastModifiedLedgerSeq`-first, and the live data-first layout) and returns
/// the union variant actually present. Non-`ContractInstance` payloads and
/// undecodable data keep returning the existing [`DecodeError`] variants.
pub fn extract_contract_executable(
base64_ledger_entry: &str,
) -> Result<ContractExecutableRef, DecodeError> {
let raw = detect_and_decode(base64_ledger_entry)?;
match extract_executable_standard(&raw) {
Ok(exec) => Ok(exec),
Err(DecodeError::XdrParse(..)) => extract_executable_from_live_ledger_entry(&raw),
Err(e) => Err(e),
}
}
fn map_executable(executable: ContractExecutable) -> Result<ContractExecutableRef, DecodeError> {
Ok(match executable {
ContractExecutable::Wasm(h) => ContractExecutableRef::Wasm(h),
ContractExecutable::StellarAsset => ContractExecutableRef::StellarAsset,
ContractExecutable::ExternalRef(r) => {
ContractExecutableRef::ExternalRef(ExternalExecutableRef {
owner: r.executable_owner,
// `SCString` payload is not required to be valid UTF-8 (it is an
// opaque key); surface it losslessly rather than dropping bytes.
tag: String::from_utf8_lossy(r.tag.as_slice()).into_owned(),
})
}
})
}
fn executable_from_contract_data(
data: stellar_xdr::ContractDataEntry,
) -> Result<ContractExecutableRef, DecodeError> {
let instance = match data.val {
ScVal::ContractInstance(i) => i,
_ => return Err(DecodeError::Extraction("Not a ContractInstance".into())),
};
map_executable(instance.executable)
}
fn extract_executable_standard(raw: &[u8]) -> Result<ContractExecutableRef, DecodeError> {
let mut cursor = std::io::Cursor::new(raw);
let mut l = Limited::new(&mut cursor, Limits::none());
let entry = LedgerEntry::read_xdr(&mut l)
.map_err(|e| DecodeError::XdrParse("LedgerEntry".to_string(), e))?;
let data = match entry.data {
LedgerEntryData::ContractData(d) => d,
_ => return Err(DecodeError::Extraction("Not a ContractData entry".into())),
};
executable_from_contract_data(data)
}
fn extract_executable_from_live_ledger_entry(
raw: &[u8],
) -> Result<ContractExecutableRef, DecodeError> {
let mut cursor = std::io::Cursor::new(raw);
let mut l = Limited::new(&mut cursor, Limits::none());
let data = LedgerEntryData::read_xdr(&mut l)
.map_err(|e| DecodeError::XdrParse("LedgerEntryData(live)".to_string(), e))?;
let cd = match data {
LedgerEntryData::ContractData(d) => d,
_ => return Err(DecodeError::Extraction("Not a ContractData entry".into())),
};
executable_from_contract_data(cd)
}
/// Read the 32-byte Wasm hash stored in a CAP-85 executable-tag entry.
///
/// CAP-0085 constrains such an entry to a `Bytes` value holding the raw hash
/// of an already-uploaded Wasm (the host rejects strings, integers, and any
/// 32-byte value that is not a live Wasm hash). Anything else is rejected here
/// rather than coerced.
pub fn parse_external_executable_hash(value: &ScVal) -> Result<String, DecodeError> {
let ScVal::Bytes(bytes) = value else {
return Err(DecodeError::Extraction(format!(
"CAP-85 executable tag entry holds {}, expected a 32-byte Bytes Wasm hash",
value.name()
)));
};
let slice = bytes.as_slice();
if slice.len() != 32 {
return Err(DecodeError::Extraction(format!(
"CAP-85 executable tag entry holds {} bytes, expected 32",
slice.len()
)));
}
Ok(hex::encode(slice))
}
/// Shared tail of both decoders: ContractData -> ContractInstance -> Wasm hash.
fn extract_hash_from_contract_data(
data: stellar_xdr::ContractDataEntry,
) -> Result<String, DecodeError> {
let instance = match data.val {
ScVal::ContractInstance(i) => i,
_ => return Err(DecodeError::Extraction("Not a ContractInstance".into())),
};
let hash = match instance.executable {
ContractExecutable::Wasm(h) => h,
_ => return Err(DecodeError::Extraction("Not a Wasm executable".into())),
};
Ok(hex::encode(hash.0))
}
/// Extracts the generic `val: ScVal` and durability from a `LedgerEntry`
/// containing a `ContractData` entry.
///
/// Uses the same live-wire compatibility bridge as [`extract_wasm_hash`].
pub fn extract_contract_data_value(
base64_ledger_entry: &str,
) -> Result<stellar_xdr::ContractDataEntry, DecodeError> {
let raw = detect_and_decode(base64_ledger_entry)?;
match extract_contract_data_value_standard(&raw) {
Ok(data) => Ok(data),
Err(e) => match e {
DecodeError::XdrParse(..) => extract_contract_data_value_from_live_ledger_entry(&raw),
_ => Err(e),
},
}
}
fn extract_contract_data_value_standard(
raw: &[u8],
) -> Result<stellar_xdr::ContractDataEntry, DecodeError> {
let mut cursor = std::io::Cursor::new(raw);
let mut l = Limited::new(&mut cursor, Limits::none());
let entry = LedgerEntry::read_xdr(&mut l)
.map_err(|e| DecodeError::XdrParse("LedgerEntry".to_string(), e))?;
match entry.data {
LedgerEntryData::ContractData(d) => Ok(d),
_ => Err(DecodeError::Extraction("Not a ContractData entry".into())),
}
}
fn extract_contract_data_value_from_live_ledger_entry(
raw: &[u8],
) -> Result<stellar_xdr::ContractDataEntry, DecodeError> {
let mut cursor = std::io::Cursor::new(raw);
let mut l = Limited::new(&mut cursor, Limits::none());
let data = LedgerEntryData::read_xdr(&mut l)
.map_err(|e| DecodeError::XdrParse("LedgerEntryData(live)".to_string(), e))?;
match data {
LedgerEntryData::ContractData(d) => Ok(d),
_ => Err(DecodeError::Extraction("Not a ContractData entry".into())),
}
}
/// Extracts the raw WASM bytecode from a Base64 encoded `LedgerEntry` containing a `ContractCode` entry.
///
/// Same compatibility bridge as [`extract_wasm_hash`]: tries the standard
/// stellar-xdr 28.0.0 layout first, then falls back to the live data-first wire layout.
pub fn extract_wasm_bytecode(base64_ledger_entry: &str) -> Result<Vec<u8>, DecodeError> {
let raw = detect_and_decode(base64_ledger_entry)?;
match extract_wasm_bytecode_standard(&raw) {
Ok(code) => Ok(code),
Err(e) => match e {
DecodeError::XdrParse(..) => extract_wasm_bytecode_from_live_ledger_entry(&raw),
_ => Err(e),
},
}
}
fn extract_wasm_bytecode_standard(raw: &[u8]) -> Result<Vec<u8>, DecodeError> {
let mut cursor = std::io::Cursor::new(raw);
let mut l = Limited::new(&mut cursor, Limits::none());
let entry = LedgerEntry::read_xdr(&mut l)
.map_err(|e| DecodeError::XdrParse("LedgerEntry".to_string(), e))?;
let code = match entry.data {
LedgerEntryData::ContractCode(c) => c,
_ => return Err(DecodeError::Extraction("Not a ContractCode entry".into())),
};
Ok(code.code.to_vec())
}
/// Live-wire compatibility decoder for `ContractCode` entries (data-first layout).
pub fn extract_wasm_bytecode_from_live_ledger_entry(raw: &[u8]) -> Result<Vec<u8>, DecodeError> {
let mut cursor = std::io::Cursor::new(raw);
let mut l = Limited::new(&mut cursor, Limits::none());
let data = LedgerEntryData::read_xdr(&mut l)
.map_err(|e| DecodeError::XdrParse("LedgerEntryData(live)".to_string(), e))?;
let code = match data {
LedgerEntryData::ContractCode(c) => c,
_ => return Err(DecodeError::Extraction("Not a ContractCode entry".into())),
};
Ok(code.code.to_vec())
}
/// Decode a base64- or hex-encoded XDR payload to JSON.
///
/// `payload` is tried as base64 first; if that fails and the string is valid
/// hex, it is decoded as hex. Raw-byte callers should use [`decode_bytes`].
///
/// # Arguments
///
/// * `payload` – base64 or hex encoded string
/// * `type_hint` – explicit type (`"scval"`, etc.) or `None` for auto-detection
/// * `format` – [`OutputFormat::Json`] or [`OutputFormat::Pretty`]
///
/// # Returns
///
/// JSON string representation of the decoded XDR.
///
/// # Errors
///
/// Returns [`DecodeError`] if the input is invalid or the XDR cannot be parsed.
pub fn decode(
payload: &str,
type_hint: Option<&str>,
format: OutputFormat,
) -> Result<String, DecodeError> {
let raw = detect_and_decode(payload)?;
let value = decode_bytes(&raw, type_hint)?;
format_json(&value, format)
}
/// Decode raw bytes (no base64/hex pre-processing).
pub fn decode_bytes(raw: &[u8], type_hint: Option<&str>) -> Result<Value, DecodeError> {
if raw.is_empty() {
return Err(DecodeError::EmptyPayload);
}
let type_name = type_hint.unwrap_or("auto");
match type_name.to_lowercase().as_str() {
"scval" => decode_single::<ScVal>(raw, "ScVal"),
"transactionenvelope" => decode_single::<TransactionEnvelope>(raw, "TransactionEnvelope"),
"transactionresult" => decode_single::<TransactionResult>(raw, "TransactionResult"),
"transactionmeta" => decode_single::<TransactionMeta>(raw, "TransactionMeta"),
"ledgerkey" => decode_single::<LedgerKey>(raw, "LedgerKey"),
"ledgerentry" => decode_single::<LedgerEntry>(raw, "LedgerEntry"),
"contractevent" => decode_single::<ContractEvent>(raw, "ContractEvent"),
"auto" => auto_detect(raw),
other => Err(DecodeError::TypeUnknown(other.to_string())),
}
}
fn decode_single<T: ReadXdr + serde::Serialize>(
raw: &[u8],
name: &str,
) -> Result<Value, DecodeError> {
let mut cursor = std::io::Cursor::new(raw);
let mut l = Limited::new(&mut cursor, Limits::none());
T::read_xdr(&mut l)
.map_err(|e| DecodeError::XdrParse(name.to_string(), e))
.and_then(|v| serde_json::to_value(&v).map_err(DecodeError::Json))
}
fn decode_single_strict<T: ReadXdr + serde::Serialize>(
raw: &[u8],
name: &str,
) -> Result<Value, DecodeError> {
let mut cursor = std::io::Cursor::new(raw);
let mut l = Limited::new(&mut cursor, Limits::none());
T::read_xdr_to_end(&mut l)
.map_err(|e| DecodeError::XdrParse(name.to_string(), e))
.and_then(|v| serde_json::to_value(&v).map_err(DecodeError::Json))
}
fn auto_detect(raw: &[u8]) -> Result<Value, DecodeError> {
if let Ok(v) = decode_single_strict::<ScVal>(raw, "ScVal") {
return Ok(v);
}
if let Ok(v) = decode_single_strict::<TransactionEnvelope>(raw, "TransactionEnvelope") {
return Ok(v);
}
if let Ok(v) = decode_single_strict::<ContractEvent>(raw, "ContractEvent") {
return Ok(v);
}
if let Ok(v) = decode_single_strict::<TransactionResult>(raw, "TransactionResult") {
return Ok(v);
}
Err(DecodeError::TypeUnknown(
"auto-detection failed for all known types".to_string(),
))
}
fn detect_and_decode(payload: &str) -> Result<Vec<u8>, DecodeError> {
if payload.is_empty() {
return Err(DecodeError::EmptyPayload);
}
let trimmed = payload.trim();
let base64_likely = !trimmed.is_empty()
&& trimmed.len().is_multiple_of(4)
&& trimmed
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=');
if base64_likely {
if let Ok(bytes) = STANDARD.decode(trimmed) {
return Ok(bytes);
}
}
if trimmed.len().is_multiple_of(2) && trimmed.chars().all(|c| c.is_ascii_hexdigit()) {
if let Ok(bytes) = hex::decode(trimmed) {
return Ok(bytes);
}
}
if base64_likely {
Err(DecodeError::Base64(base64::DecodeError::InvalidLength(0)))
} else {
Err(DecodeError::Hex(hex::FromHexError::InvalidHexCharacter {
c: trimmed
.chars()
.find(|c| !c.is_ascii_hexdigit())
.unwrap_or('?'),
index: 0,
}))
}
}
pub fn format_json(value: &Value, format: OutputFormat) -> Result<String, DecodeError> {
match format {
OutputFormat::Json => Ok(serde_json::to_string(value)?),
OutputFormat::Pretty => Ok(serde_json::to_string_pretty(value)?),
}
}
/// Estimate the serialized XDR size (in bytes) of a `WriteXdr` value.
///
/// Reuses the existing `WriteXdr` machinery — no duplicate parser. Returns the
/// exact number of bytes the payload would occupy on the wire.
pub fn estimate_xdr_size<T: WriteXdr>(value: &T) -> usize {
let mut buf = Vec::new();
let mut l = Limited::new(&mut buf, Limits::none());
// Best-effort: if serialization fails (e.g. value too large), report the
// buffer length so far. Callers use this only for pre-flight checks.
let _ = value.write_xdr(&mut l);
buf.len()
}
/// Validate that a raw byte payload is a well-formed XDR `TransactionEnvelope`.
///
/// Returns `Ok(size)` when the payload parses, or `Err` with a message when it
/// does not. This is a pure structural check (no RPC).
pub fn validate_xdr(raw: &[u8]) -> Result<usize, String> {
if raw.is_empty() {
return Err("empty payload".into());
}
let mut cursor = std::io::Cursor::new(raw);
let mut l = Limited::new(&mut cursor, Limits::none());
TransactionEnvelope::read_xdr(&mut l)
.map_err(|e| format!("malformed transaction envelope: {e}"))
.map(|_| raw.len())
}
#[cfg(test)]
mod tests {
use super::*;
use stellar_xdr::{
ContractDataDurability, ContractDataEntry, ContractExecutable, ExtensionPoint, Hash,
LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerKey, ScAddress, ScContractInstance,
ScVal, TransactionResult, TransactionResultExt, TransactionResultResult, WriteXdr,
};
#[test]
fn test_invalid_base64() {
let result = decode("!!! ???", None, OutputFormat::default());
assert!(matches!(result, Err(DecodeError::Hex(_))));
}
#[test]
fn test_valid_base64_but_invalid_xdr() {
// Base64 for "hello world" which is not valid XDR
let result = decode("aGVsbG8gd29ybGQ=", None, OutputFormat::default());
assert!(matches!(result, Err(DecodeError::TypeUnknown(_))));
}
#[test]
fn test_valid_scval_integer_base64() {
let payload = "AAAABAAAAAE=";
let json = decode(payload, Some("scval"), OutputFormat::Json).unwrap();
let v: Value = serde_json::from_str(&json).unwrap();
assert!(v.is_object());
assert_eq!(v["i32"], 1);
}
#[test]
fn test_auto_decode_scval() {
let payload = "AAAABAAAAAE=";
let json = decode(payload, None, OutputFormat::Json).unwrap();
let v: Value = serde_json::from_str(&json).unwrap();
assert!(v.is_object());
assert_eq!(v["i32"], 1);
}
#[test]
fn test_auto_decode_transaction_result() {
let fee_charged = 9_000_000_000_000_i64;
let result = TransactionResult {
fee_charged,
result: TransactionResultResult::TxSuccess(vec![].try_into().unwrap()),
ext: TransactionResultExt::V0,
};
let bytes = result.to_xdr(Limits::none()).unwrap();
let payload = STANDARD.encode(bytes);
let json = decode(&payload, None, OutputFormat::Json).unwrap();
let v: Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["fee_charged"], fee_charged.to_string());
assert!(v["result"]["tx_success"].is_array());
}
#[test]
fn test_auto_decode_small_transaction_result_not_scval() {
let fee_charged = 100_i64;
let result = TransactionResult {
fee_charged,
result: TransactionResultResult::TxSuccess(vec![].try_into().unwrap()),
ext: TransactionResultExt::V0,
};
let bytes = result.to_xdr(Limits::none()).unwrap();
let payload = STANDARD.encode(bytes);
let json = decode(&payload, None, OutputFormat::Json).unwrap();
let v: Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["fee_charged"], fee_charged.to_string());
assert!(v["result"]["tx_success"].is_array());
assert!(v.get("i32").is_none());
}
#[test]
fn test_auto_decode_unknown_payload_still_returns_type_unknown() {
let payload = "aGVsbG8gd29ybGQ=";
let result = decode(payload, None, OutputFormat::default());
assert!(matches!(result, Err(DecodeError::TypeUnknown(_))));
}
#[test]
fn test_empty_payload() {
let result = decode("", None, OutputFormat::default());
assert!(matches!(result, Err(DecodeError::EmptyPayload)));
}
#[test]
fn test_unknown_type() {
let payload = "AAAABAAAAAE=";
let result = decode(payload, Some("nonexistent"), OutputFormat::default());
assert!(matches!(result, Err(DecodeError::TypeUnknown(_))));
}
#[test]
fn test_json_vs_pretty() {
let payload = "AAAABAAAAAE=";
let compact = decode(payload, Some("scval"), OutputFormat::Json).unwrap();
let pretty = decode(payload, Some("scval"), OutputFormat::Pretty).unwrap();
assert!(!compact.contains('\n'));
assert!(pretty.contains('\n'));
}
#[test]
fn test_build_map_key_symbol_and_u32() {
let u32_b64 = scval_to_base64(&ScVal::U32(100)).unwrap();
let key = build_map_key("balances", &[u32_b64]).unwrap();
match &key {
ScVal::Vec(Some(v)) => {
assert_eq!(v.len(), 2);
assert_eq!(v[0], ScVal::Symbol("balances".try_into().unwrap()));
assert_eq!(v[1], ScVal::U32(100));
}
other => panic!("expected ScVec, got {other:?}"),
}
}
#[test]
fn test_symbol_topic_base64_encodes_scval_symbol() {
// SCV_SYMBOL (15) + length 8 + "Transfer" (already 4-byte aligned).
let b64 = symbol_topic_base64("Transfer").unwrap();
assert_eq!(b64, "AAAADwAAAAhUcmFuc2Zlcg==");
assert_eq!(
scval_from_base64(&b64),
Some(ScVal::Symbol("Transfer".try_into().unwrap()))
);
// Symbols are case-sensitive on the wire.
assert_eq!(
symbol_topic_base64("transfer").unwrap(),
"AAAADwAAAAh0cmFuc2Zlcg=="
);
}
#[test]
fn test_symbol_topic_base64_rejects_invalid_symbols() {
for bad in ["", "has space", "dash-ed", "é", &"a".repeat(33)] {
let err = symbol_topic_base64(bad).unwrap_err();
assert!(
err.to_string().contains("invalid symbol"),
"unexpected error for {bad:?}: {err}"
);
}
assert!(symbol_topic_base64(&"a".repeat(32)).is_ok());
}
#[test]
fn test_build_map_key_symbol_only_and_composite() {
// Single symbol -> ScVec[symbol]
let single = build_map_key("admin", &[]).unwrap();
match &single {
ScVal::Vec(Some(v)) => {
assert_eq!(v.len(), 1);
assert_eq!(v[0], ScVal::Symbol("admin".try_into().unwrap()));
}
other => panic!("expected ScVec, got {other:?}"),
}
// Composite symbol + two typed args
let a = scval_to_base64(&ScVal::U32(1)).unwrap();
let b = scval_to_base64(&ScVal::U32(2)).unwrap();
let composite = build_map_key("pair", &[a, b]).unwrap();
match &composite {
ScVal::Vec(Some(v)) => assert_eq!(v.len(), 3),
other => panic!("expected ScVec, got {other:?}"),
}
}
#[test]
fn test_encode_contract_data_entry_durability_changes_key() {
let contract = "0000000000000000000000000000000000000000000000000000000000000000";
let key = build_map_key("k", &[scval_to_base64(&ScVal::U32(7)).unwrap()]).unwrap();
let persistent = encode_ledger_key(&LedgerKeyParams::ContractDataEntry {
contract: contract.to_string(),
key: key.clone(),
durability: ContractDataDurability::Persistent,
})
.unwrap();
let temporary = encode_ledger_key(&LedgerKeyParams::ContractDataEntry {
contract: contract.to_string(),
key: key.clone(),
durability: ContractDataDurability::Temporary,
})
.unwrap();
assert_ne!(
persistent, temporary,
"durability must produce a different LedgerKey"
);
let decoded = decode_ledger_key(&persistent).unwrap();
match decoded {
LedgerKey::ContractData(d) => {
assert_eq!(d.durability, ContractDataDurability::Persistent);
assert_eq!(d.key, key);
}
other => panic!("expected ContractData, got {other:?}"),
}
}
#[test]
fn test_encode_contract_data_entry_accepts_strkey_and_hex() {
let id = [3u8; 32];
let hex_id = hex::encode(id);
let strkey = format!("{}", stellar_strkey::Contract(id));
let from_hex = encode_ledger_key(&LedgerKeyParams::ContractDataEntry {
contract: hex_id,
key: ScVal::LedgerKeyContractInstance,
durability: ContractDataDurability::Persistent,
})
.unwrap();
let from_strkey = encode_ledger_key(&LedgerKeyParams::ContractDataEntry {
contract: strkey,
key: ScVal::LedgerKeyContractInstance,
durability: ContractDataDurability::Persistent,
})
.unwrap();
assert_eq!(from_hex, from_strkey);
}
// ---- CAP-85 (Protocol 28) executable variants ----
//
// `ContractExecutable` is a Protocol 28 XDR union. These fixtures are built
// with the real `stellar-xdr` types and round-tripped through the same
// decoders the RPC inspection path uses, so they exercise the wire
// representation rather than a hand-written enum value.
/// Owner address used by the external-ref fixtures.
fn external_ref_fixture() -> (ContractExecutable, ScAddress) {
let owner = ScAddress::Contract(ContractId(Hash([7u8; 32])));
let tag =
stellar_xdr::ScString(stellar_xdr::StringM::try_from(b"oracle-v1".to_vec()).unwrap());
(
ContractExecutable::ExternalRef(stellar_xdr::ContractExecutableExternalRef {
executable_owner: owner.clone(),
tag,
}),
owner,
)
}
#[test]
fn test_extract_contract_executable_wasm() {
let hash = [9u8; 32];
let b64 = create_test_ledger_entry(ContractExecutable::Wasm(Hash(hash)));
let exec = extract_contract_executable(&b64).unwrap();
assert_eq!(exec.kind(), "wasm");
assert_eq!(exec, ContractExecutableRef::Wasm(Hash(hash)));
// The pre-existing Wasm-only extraction must keep reporting the same hash.
assert_eq!(extract_wasm_hash(&b64).unwrap(), hex::encode(hash));
}
#[test]
fn test_extract_contract_executable_stellar_asset_is_not_wasm() {
let b64 = create_test_ledger_entry(ContractExecutable::StellarAsset);
let exec = extract_contract_executable(&b64).unwrap();
assert_eq!(exec, ContractExecutableRef::StellarAsset);
assert_eq!(exec.kind(), "stellar_asset");
// A SAC must never be reported as a Wasm contract.
assert!(!matches!(exec, ContractExecutableRef::Wasm(_)));
assert!(exec.executable_tag_key().is_none());
}
#[test]
fn test_extract_contract_executable_external_ref() {
let (executable, owner) = external_ref_fixture();
let b64 = create_test_ledger_entry(executable);
let exec = extract_contract_executable(&b64).unwrap();
assert_eq!(exec.kind(), "external_ref");
let reference = exec.executable_tag_key().expect("external ref key");
assert_eq!(reference.owner, owner);
assert_eq!(reference.tag, "oracle-v1");
// An external ref has no inline hash: the legacy Wasm-only extractor must
// keep failing loudly rather than inventing one.
assert!(extract_wasm_hash(&b64).is_err());
}
#[test]
fn test_external_ref_survives_live_wire_layout() {
// The live RPC wire layout serialises `LedgerEntryData` first (with the
// remaining `LedgerEntry` fields trailing) — the layout the compatibility
// bridge exists for. Build exactly that: `LedgerEntryData` only.
let (executable, owner) = external_ref_fixture();
let data = LedgerEntryData::ContractData(ContractDataEntry {
ext: ExtensionPoint::V0,
contract: ScAddress::Contract(ContractId(Hash([0; 32]))),
key: ScVal::LedgerKeyContractInstance,
durability: ContractDataDurability::Persistent,
val: ScVal::ContractInstance(ScContractInstance {
executable,
storage: None,
}),
});
let mut buf = Vec::new();
let mut cursor = std::io::Cursor::new(&mut buf);
let mut l = Limited::new(&mut cursor, Limits::none());
data.write_xdr(&mut l).unwrap();
let exec = extract_executable_from_live_ledger_entry(&buf).unwrap();
assert_eq!(exec.kind(), "external_ref");
assert_eq!(exec.executable_tag_key().unwrap().owner, owner);
assert_eq!(exec.executable_tag_key().unwrap().tag, "oracle-v1");
}
#[test]
fn test_parse_external_executable_hash_accepts_32_bytes() {
let hash = [3u8; 32];
let val = ScVal::Bytes(stellar_xdr::ScBytes(
stellar_xdr::BytesM::try_from(hash.to_vec()).unwrap(),
));
assert_eq!(
parse_external_executable_hash(&val).unwrap(),
hex::encode(hash)
);
}
#[test]
fn test_parse_external_executable_hash_rejects_non_hash_values() {
// CAP-0085 constrains the entry to raw 32-byte hash bytes; the host
// rejects these shapes, and so must we (no silent coercion).
let short = ScVal::Bytes(stellar_xdr::ScBytes(
stellar_xdr::BytesM::try_from(vec![1u8; 31]).unwrap(),
));
assert!(parse_external_executable_hash(&short).is_err());
let as_string = ScVal::String(stellar_xdr::ScString(
stellar_xdr::StringM::try_from(vec![1u8; 32]).unwrap(),
));
assert!(parse_external_executable_hash(&as_string).is_err());
assert!(parse_external_executable_hash(&ScVal::U32(5)).is_err());
}
#[test]
fn test_extract_contract_executable_rejects_non_contract_data() {
let err = extract_contract_executable("AAAABAAAAAE=").unwrap_err();
assert!(matches!(err, DecodeError::XdrParse(_, _)));
}
#[test]
fn test_build_map_key_rejects_invalid_symbol_and_arg() {
assert!(build_map_key("has spaces", &[]).is_err());
assert!(build_map_key("k", &["!!!not-base64!!!".to_string()]).is_err());
}
#[test]
fn test_encode_contract_data_entry_rejects_bad_contract() {
let err = encode_ledger_key(&LedgerKeyParams::ContractDataEntry {
contract: "not-a-contract".to_string(),
key: ScVal::LedgerKeyContractInstance,
durability: ContractDataDurability::Persistent,
});
assert!(err.is_err());
}
#[test]
fn test_encode_ledger_key() {
let contract_id = "0000000000000000000000000000000000000000000000000000000000000000";
let res =
encode_ledger_key(&LedgerKeyParams::ContractData(contract_id.to_string())).unwrap();
// Decode it back to verify it's a LedgerKey::ContractData with ScVal::LedgerKeyContractInstance
let decoded = detect_and_decode(&res).unwrap();
let mut cursor = std::io::Cursor::new(&decoded);
let mut l = Limited::new(&mut cursor, Limits::none());
let lk = LedgerKey::read_xdr(&mut l).unwrap();
match lk {
LedgerKey::ContractData(d) => {
assert_eq!(d.key, ScVal::LedgerKeyContractInstance);
}
_ => panic!("Expected ContractData"),
}
}
fn create_test_ledger_entry(executable: ContractExecutable) -> String {
let entry = LedgerEntry {
last_modified_ledger_seq: 1,
data: LedgerEntryData::ContractData(ContractDataEntry {
ext: ExtensionPoint::V0,
contract: ScAddress::Contract(ContractId(Hash([0; 32]))),
key: ScVal::LedgerKeyContractInstance,
durability: ContractDataDurability::Persistent,
val: ScVal::ContractInstance(ScContractInstance {
executable,
storage: None,
}),
}),
ext: LedgerEntryExt::V0,
};
let mut buf = Vec::new();
let mut cursor = std::io::Cursor::new(&mut buf);
let mut l = Limited::new(&mut cursor, Limits::none());
entry.write_xdr(&mut l).unwrap();
STANDARD.encode(&buf)
}
#[test]
fn test_extract_wasm_hash_valid() {
let hash = [1u8; 32];
let b64 = create_test_ledger_entry(ContractExecutable::Wasm(Hash(hash)));
let extracted = extract_wasm_hash(&b64).unwrap();
assert_eq!(extracted, hex::encode(hash));
}
#[test]
fn test_extract_wasm_hash_non_wasm() {
let b64 = create_test_ledger_entry(ContractExecutable::StellarAsset);
let err = extract_wasm_hash(&b64).unwrap_err();
assert!(matches!(err, DecodeError::Extraction(e) if e.contains("Not a Wasm")));
}
#[test]
fn test_extract_wasm_hash_non_ledger_entry() {
// Just an ScVal
let b64 = "AAAABAAAAAE=";
let err = extract_wasm_hash(b64).unwrap_err();
assert!(matches!(err, DecodeError::XdrParse(_, _)));
}
#[test]
fn test_extract_wasm_hash_malformed_base64() {
let err = extract_wasm_hash("invalid base64!!!").unwrap_err();
assert!(matches!(err, DecodeError::Hex(_))); // detect_and_decode falls back to Hex and fails there
}
// Captured real `LedgerEntry` XDR from the live Stellar/Soroban testnet.
//
// CAE3U7JKESRWZHPEQ72DVNGOQ6WPA7HSPQZL5YV46NPCE4TMUPAGYMEC — a deployed Wasm
// (Soroban) contract. Its `LedgerEntry` uses the LIVE wire layout where
// `LedgerEntryData` is serialized FIRST (data union discriminant 6 =
// CONTRACT_DATA), unlike stellar-xdr 28.0.0's lastModifiedLedgerSeq-first
// `LedgerEntry`. The `val` is a `ContractInstance` whose `executable` is
// `ContractExecutable::Wasm`, so the compatibility bridge must extract the Wasm
// hash `60cddae6...`. Used to prove `extract_wasm_hash_from_live_ledger_entry`
// works without network access.
const LIVE_WASM_LEDGER_ENTRY_B64: &str = "AAAABgAAAAAAAAABCbp9KiSjbJ3kh/Q6tM6HrPB88nwyvuK8814icmyjwGwAAAAUAAAAAQAAABMAAAAAYM3a5n8gLBnuewAMiU/RKqi0TeCatlL14Yi8DGOmzwIAAAABAAAABwAAABAAAAABAAAAAQAAAA8AAAAFQWRtaW4AAAAAAAASAAAAAAAAAACUijpq22w97c0/5wJEUlkFddoMBv/zyTHhNMr9T4FBBQAAABAAAAABAAAAAQAAAA8AAAAGQ29uZmlnAAAAAAARAAAAAQAAAAsAAAAPAAAAFWJhc2VfZnVuZGluZ19yYXRlX2JwcwAAAAAAAAMAAAABAAAADwAAABJiYXNlX21ha2VyX2ZlZV9icHMAAAAAAAMAAAACAAAADwAAABJiYXNlX3Rha2VyX2ZlZV9icHMAAAAAAAMAAAAFAAAADwAAABNsaXF1aWRhdGlvbl9mZWVfYnBzAAAAAAMAAAH0AAAADwAAABZtYWludGVuYW5jZV9tYXJnaW5fYnBzAAAAAAADAAAAZAAAAA8AAAAMbWF4X2xldmVyYWdlAAAAAwAAAAoAAAAPAAAAGG1heF9vcmFjbGVfZGV2aWF0aW9uX2JwcwAAAAMAAABkAAAADwAAABFtYXhfcG9zaXRpb25fc2l6ZQAAAAAAAAoAAAAAAAAAAAAAAOjUpRAAAAAADwAAABNtYXhfcHJpY2Vfc3RhbGVuZXNzAAAAAAUAAAAAAAAAPAAAAA8AAAAObWluX2NvbGxhdGVyYWwAAAAAAAoAAAAAAAAAAAAAAAAF9eEAAAAADwAAAA90cmFkaW5nX2ZlZV9icHMAAAAAAwAAAAoAAAAQAAAAAQAAAAEAAAAPAAAAC0luaXRpYWxpemVkAAAAAAAAAAABAAAAEAAAAAEAAAABAAAADwAAAA1PcmFjbGVBZGFwdGVyAAAAAAAAEgAAAAFxzC34I3pmV4llcmFKnN7k7qZkMKXvGRPZMuobQm6naQAAABAAAAABAAAAAQAAAA8AAAAGUGF1c2VkAAAAAAAAAAAAAQAAABAAAAABAAAAAQAAAA8AAAAJVXNkY1Rva2VuAAAAAAAAEgAAAAE9sj2cIS9K0A0viwokid/jaOEXECNi3xRj3/ivNitXOgAAABAAAAABAAAAAQAAAA8AAAAFVmF1bHQAAAAAAAASAAAAAVdc5734Fh3zRgwQwWibRi6egIv95VY++/CqtURwMM+t";
// Captured real `LedgerEntry` XDR for a Stellar Asset Contract (SAC) on testnet
// (CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC). It uses the same
// live data-first wire layout, but its `executable` is `ContractExecutable::
// StellarAsset`, NOT `Wasm`. Proves the bridge correctly identifies a non-Wasm
// contract and returns a controlled `Extraction` error (not an XDR panic).
const LIVE_SAC_LEDGER_ENTRY_B64: &str = "AAAABgAAAAAAAAAB15KLcsJwPM/q9+uf9O9NUEpVqLl5/JtFDqLIQrTRzmEAAAAUAAAAAQAAABMAAAABAAAAAQAAAAIAAAAPAAAACE1FVEFEQVRBAAAAEQAAAAEAAAADAAAADwAAAAdkZWNpbWFsAAAAAAMAAAAHAAAADwAAAARuYW1lAAAADgAAAAZuYXRpdmUAAAAAAA8AAAAGc3ltYm9sAAAAAAAOAAAABm5hdGl2ZQAAAAAAEAAAAAEAAAABAAAADwAAAAlBc3NldEluZm8AAAAAAAAQAAAAAQAAAAEAAAAPAAAABk5hdGl2ZQAA";
#[test]
fn test_extract_wasm_hash_live_wasm_wire_layout() {
// The live entry must NOT decode via the standard (lastModifiedLedgerSeq-first)
// path, proving the captured wire layout genuinely differs.
let raw = detect_and_decode(LIVE_WASM_LEDGER_ENTRY_B64).unwrap();
assert!(extract_wasm_hash_standard(&raw).is_err());
// The compatibility bridge decodes it and extracts the correct Wasm hash.
let hash = extract_wasm_hash_from_live_ledger_entry(&raw).unwrap();
assert_eq!(
hash,
"60cddae67f202c19ee7b000c894fd12aa8b44de09ab652f5e188bc0c63a6cf02"
);
// And the public entry point falls back to the bridge automatically.
let via_public = extract_wasm_hash(LIVE_WASM_LEDGER_ENTRY_B64).unwrap();
assert_eq!(via_public, hash);
}
#[test]
fn test_extract_wasm_hash_live_sac_is_controlled_error() {
// A live SAC (StellarAsset executable) decodes structurally but yields a
// controlled `Extraction` error — never an XDR panic or a wrong hash.
let raw = detect_and_decode(LIVE_SAC_LEDGER_ENTRY_B64).unwrap();
let err = extract_wasm_hash_from_live_ledger_entry(&raw).unwrap_err();
assert!(matches!(err, DecodeError::Extraction(e) if e.contains("Not a Wasm")));
// The public entry point returns the same controlled error.
let err = extract_wasm_hash(LIVE_SAC_LEDGER_ENTRY_B64).unwrap_err();
assert!(matches!(err, DecodeError::Extraction(e) if e.contains("Not a Wasm")));
}
#[test]
fn test_extract_wasm_hash_live_wire_truncated_fails_controlled() {
// Drop the last 4 base64 chars -> partial trailing bytes -> controlled XdrParse,
// never a panic.
let truncated = &LIVE_WASM_LEDGER_ENTRY_B64[..LIVE_WASM_LEDGER_ENTRY_B64.len() - 4];
let err = extract_wasm_hash(truncated).unwrap_err();
assert!(matches!(err, DecodeError::XdrParse(_, _)));
}
#[test]
fn test_estimate_xdr_size() {
let val = ScVal::U32(42);
// Tag(4) + U32(4) = 8 bytes
assert_eq!(estimate_xdr_size(&val), 8);
}
}
pub mod abi_decode;