1pub mod builder;
27pub mod envelope;
28pub mod sign;
29pub mod typed;
30pub use builder::{
31 build_create_contract_tx, build_create_contract_tx_with_data,
32 build_create_contract_tx_with_data_and_auth, build_create_contract_v2_tx,
33 build_create_contract_v2_tx_with_data, build_create_contract_v2_tx_with_data_and_auth,
34 build_extend_footprint_tx, build_extend_footprint_tx_with_data, build_invoke_transaction,
35 build_invoke_transaction_with_data, build_restore_footprint_tx, build_upload_wasm_tx,
36 build_upload_wasm_tx_with_data, decode_account_id, decode_contract_id, decode_ledger_key,
37 derive_contract_id, memo_id, memo_text, merge_footprint_keys, parse_scval_args,
38 parse_soroban_transaction_data, CreateContractParams, CreateContractV2Params,
39 ExtendFootprintParams, InvokeTransactionParams, RestoreFootprintParams, UploadWasmParams,
40};
41pub use envelope::{decode_envelope, render_scval, view_envelope, EnvelopeView};
42pub use sign::{
43 sign_envelope_with, sign_transaction, verify_signature, Ed25519Signer, Network, Signer,
44 SigningError, SigningOptions,
45};
46pub use typed::{
47 decode_scvals, decode_scvals_ref, encode_scvals, json_args_to_base64, json_to_scval,
48 scval_from_base64, scval_to_base64, Address, FromScVal, IntoScVal, ScValError,
49};
50
51use base64::engine::general_purpose::STANDARD;
52use base64::Engine;
53use serde_json::Value;
54
55pub use sdkt_core::OutputFormat;
56
57use stellar_xdr::{
58 ContractDataDurability, ContractEvent, ContractExecutable, ContractId, Hash, LedgerEntry,
59 LedgerEntryData, LedgerKey, LedgerKeyContractCode, LedgerKeyContractData, Limited, Limits,
60 ReadXdr, ScAddress, ScSymbol, ScVal, ScVec, TransactionEnvelope, TransactionMeta,
61 TransactionResult, VecM, WriteXdr,
62};
63use thiserror::Error;
64
65#[derive(Error, Debug)]
67pub enum DecodeError {
68 #[error("Base64 decode failed: {0}")]
69 Base64(#[from] base64::DecodeError),
70 #[error("Hex decode failed: {0}")]
71 Hex(#[from] hex::FromHexError),
72 #[error("XDR parse failed for type '{0}': {1}")]
73 XdrParse(String, stellar_xdr::Error),
74 #[error("XDR write failed: {0}")]
75 XdrWrite(stellar_xdr::Error),
76 #[error("Unknown XDR type: {0}")]
77 TypeUnknown(String),
78 #[error("Invalid input: empty payload")]
79 EmptyPayload,
80 #[error("JSON serialization failed: {0}")]
81 Json(#[from] serde_json::Error),
82 #[error("Extraction error: {0}")]
83 Extraction(String),
84}
85
86pub enum LedgerKeyParams {
88 ContractData(String),
90 ContractCode(String),
92 ContractDataEntry {
97 contract: String,
98 key: ScVal,
99 durability: ContractDataDurability,
100 },
101}
102
103fn parse_contract_id_bytes(contract: &str) -> Result<[u8; 32], DecodeError> {
106 let trimmed = contract.trim();
107 if let Ok(stellar_strkey::Strkey::Contract(c)) = stellar_strkey::Strkey::from_string(trimmed) {
108 return Ok(c.0);
109 }
110 let bytes = hex::decode(trimmed).map_err(DecodeError::Hex)?;
111 if bytes.len() != 32 {
112 return Err(DecodeError::Extraction(
113 "contract must be a C... StrKey or 32-byte hex".to_string(),
114 ));
115 }
116 let mut id = [0u8; 32];
117 id.copy_from_slice(&bytes);
118 Ok(id)
119}
120
121pub fn symbol_scval(symbol: &str) -> Result<ScVal, DecodeError> {
127 if symbol.is_empty()
128 || symbol.len() > 32
129 || !symbol
130 .bytes()
131 .all(|b| b.is_ascii_alphanumeric() || b == b'_')
132 {
133 return Err(DecodeError::Extraction(format!(
134 "invalid symbol {symbol:?}: must be 1..=32 chars of [a-zA-Z0-9_]"
135 )));
136 }
137 let sym: ScSymbol = symbol.try_into().map_err(|_| {
138 DecodeError::Extraction(format!(
139 "invalid symbol {symbol:?}: must be <=32 chars of [a-zA-Z0-9_]"
140 ))
141 })?;
142 Ok(ScVal::Symbol(sym))
143}
144
145pub fn symbol_topic_base64(symbol: &str) -> Result<String, DecodeError> {
148 scval_to_base64(&symbol_scval(symbol)?)
149 .map_err(|e| DecodeError::Extraction(format!("failed to encode symbol {symbol:?}: {e}")))
150}
151
152pub fn build_map_key(symbol: &str, arg_scvals_b64: &[String]) -> Result<ScVal, DecodeError> {
160 let mut elems: Vec<ScVal> = Vec::with_capacity(1 + arg_scvals_b64.len());
161 elems.push(symbol_scval(symbol)?);
162 for b64 in arg_scvals_b64 {
163 let scval = scval_from_base64(b64).ok_or_else(|| {
164 DecodeError::Extraction(format!("invalid Base64 ScVal key argument: {b64}"))
165 })?;
166 elems.push(scval);
167 }
168 let vec: VecM<ScVal> = elems
169 .try_into()
170 .map_err(|_| DecodeError::Extraction("too many key arguments".to_string()))?;
171 Ok(ScVal::Vec(Some(ScVec(vec))))
172}
173
174pub fn encode_ledger_key(params: &LedgerKeyParams) -> Result<String, DecodeError> {
176 let key = match params {
177 LedgerKeyParams::ContractData(contract_id_hex) => {
178 let hash_bytes = hex::decode(contract_id_hex).map_err(DecodeError::Hex)?;
179 if hash_bytes.len() != 32 {
180 return Err(DecodeError::Extraction(
181 "Contract ID must be 32 bytes".to_string(),
182 ));
183 }
184 let mut contract_id = [0u8; 32];
185 contract_id.copy_from_slice(&hash_bytes);
186
187 LedgerKey::ContractData(LedgerKeyContractData {
188 contract: ScAddress::Contract(ContractId(Hash(contract_id))),
189 key: ScVal::LedgerKeyContractInstance,
190 durability: stellar_xdr::ContractDataDurability::Persistent,
191 })
192 }
193 LedgerKeyParams::ContractCode(wasm_hash_hex) => {
194 let hash_bytes = hex::decode(wasm_hash_hex).map_err(DecodeError::Hex)?;
195 if hash_bytes.len() != 32 {
196 return Err(DecodeError::Extraction(
197 "WASM hash must be 32 bytes".to_string(),
198 ));
199 }
200 let mut wasm_hash = [0u8; 32];
201 wasm_hash.copy_from_slice(&hash_bytes);
202
203 LedgerKey::ContractCode(LedgerKeyContractCode {
204 hash: Hash(wasm_hash),
205 })
206 }
207 LedgerKeyParams::ContractDataEntry {
208 contract,
209 key,
210 durability,
211 } => {
212 let contract_id = parse_contract_id_bytes(contract)?;
213 LedgerKey::ContractData(LedgerKeyContractData {
214 contract: ScAddress::Contract(ContractId(Hash(contract_id))),
215 key: key.clone(),
216 durability: *durability,
217 })
218 }
219 };
220
221 let mut buf = Vec::new();
222 let mut l = Limited::new(&mut buf, Limits::none());
223 key.write_xdr(&mut l).map_err(DecodeError::XdrWrite)?;
224 Ok(STANDARD.encode(&buf))
225}
226
227pub fn extract_wasm_hash(base64_ledger_entry: &str) -> Result<String, DecodeError> {
238 let raw = detect_and_decode(base64_ledger_entry)?;
239
240 match extract_wasm_hash_standard(&raw) {
242 Ok(hash) => Ok(hash),
243 Err(e) => match e {
248 DecodeError::XdrParse(..) => extract_wasm_hash_from_live_ledger_entry(&raw),
249 _ => Err(e),
250 },
251 }
252}
253
254fn extract_wasm_hash_standard(raw: &[u8]) -> Result<String, DecodeError> {
256 let mut cursor = std::io::Cursor::new(raw);
257 let mut l = Limited::new(&mut cursor, Limits::none());
258 let entry = LedgerEntry::read_xdr(&mut l)
259 .map_err(|e| DecodeError::XdrParse("LedgerEntry".to_string(), e))?;
260
261 let data = match entry.data {
262 LedgerEntryData::ContractData(d) => d,
263 _ => return Err(DecodeError::Extraction("Not a ContractData entry".into())),
264 };
265 extract_hash_from_contract_data(data)
266}
267
268pub fn extract_wasm_hash_from_live_ledger_entry(raw: &[u8]) -> Result<String, DecodeError> {
272 let mut cursor = std::io::Cursor::new(raw);
273 let mut l = Limited::new(&mut cursor, Limits::none());
274 let data = LedgerEntryData::read_xdr(&mut l)
275 .map_err(|e| DecodeError::XdrParse("LedgerEntryData(live)".to_string(), e))?;
276
277 let cd = match data {
278 LedgerEntryData::ContractData(d) => d,
279 _ => return Err(DecodeError::Extraction("Not a ContractData entry".into())),
280 };
281 extract_hash_from_contract_data(cd)
282}
283
284#[derive(Debug, Clone, PartialEq, Eq)]
291pub enum ContractExecutableRef {
292 Wasm(Hash),
294 StellarAsset,
298 ExternalRef(ExternalExecutableRef),
301}
302
303#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct ExternalExecutableRef {
310 pub owner: ScAddress,
312 pub tag: String,
314}
315
316impl ContractExecutableRef {
317 pub fn kind(&self) -> &'static str {
319 match self {
320 ContractExecutableRef::Wasm(_) => "wasm",
321 ContractExecutableRef::StellarAsset => "stellar_asset",
322 ContractExecutableRef::ExternalRef(_) => "external_ref",
323 }
324 }
325
326 pub fn executable_tag_key(&self) -> Option<&ExternalExecutableRef> {
332 match self {
333 ContractExecutableRef::ExternalRef(r) => Some(r),
334 _ => None,
335 }
336 }
337}
338
339pub fn extract_contract_executable(
346 base64_ledger_entry: &str,
347) -> Result<ContractExecutableRef, DecodeError> {
348 let raw = detect_and_decode(base64_ledger_entry)?;
349 match extract_executable_standard(&raw) {
350 Ok(exec) => Ok(exec),
351 Err(DecodeError::XdrParse(..)) => extract_executable_from_live_ledger_entry(&raw),
352 Err(e) => Err(e),
353 }
354}
355
356fn map_executable(executable: ContractExecutable) -> Result<ContractExecutableRef, DecodeError> {
357 Ok(match executable {
358 ContractExecutable::Wasm(h) => ContractExecutableRef::Wasm(h),
359 ContractExecutable::StellarAsset => ContractExecutableRef::StellarAsset,
360 ContractExecutable::ExternalRef(r) => {
361 ContractExecutableRef::ExternalRef(ExternalExecutableRef {
362 owner: r.executable_owner,
363 tag: String::from_utf8_lossy(r.tag.as_slice()).into_owned(),
366 })
367 }
368 })
369}
370
371fn executable_from_contract_data(
372 data: stellar_xdr::ContractDataEntry,
373) -> Result<ContractExecutableRef, DecodeError> {
374 let instance = match data.val {
375 ScVal::ContractInstance(i) => i,
376 _ => return Err(DecodeError::Extraction("Not a ContractInstance".into())),
377 };
378 map_executable(instance.executable)
379}
380
381fn extract_executable_standard(raw: &[u8]) -> Result<ContractExecutableRef, DecodeError> {
382 let mut cursor = std::io::Cursor::new(raw);
383 let mut l = Limited::new(&mut cursor, Limits::none());
384 let entry = LedgerEntry::read_xdr(&mut l)
385 .map_err(|e| DecodeError::XdrParse("LedgerEntry".to_string(), e))?;
386 let data = match entry.data {
387 LedgerEntryData::ContractData(d) => d,
388 _ => return Err(DecodeError::Extraction("Not a ContractData entry".into())),
389 };
390 executable_from_contract_data(data)
391}
392
393fn extract_executable_from_live_ledger_entry(
394 raw: &[u8],
395) -> Result<ContractExecutableRef, DecodeError> {
396 let mut cursor = std::io::Cursor::new(raw);
397 let mut l = Limited::new(&mut cursor, Limits::none());
398 let data = LedgerEntryData::read_xdr(&mut l)
399 .map_err(|e| DecodeError::XdrParse("LedgerEntryData(live)".to_string(), e))?;
400 let cd = match data {
401 LedgerEntryData::ContractData(d) => d,
402 _ => return Err(DecodeError::Extraction("Not a ContractData entry".into())),
403 };
404 executable_from_contract_data(cd)
405}
406
407pub fn parse_external_executable_hash(value: &ScVal) -> Result<String, DecodeError> {
414 let ScVal::Bytes(bytes) = value else {
415 return Err(DecodeError::Extraction(format!(
416 "CAP-85 executable tag entry holds {}, expected a 32-byte Bytes Wasm hash",
417 value.name()
418 )));
419 };
420 let slice = bytes.as_slice();
421 if slice.len() != 32 {
422 return Err(DecodeError::Extraction(format!(
423 "CAP-85 executable tag entry holds {} bytes, expected 32",
424 slice.len()
425 )));
426 }
427 Ok(hex::encode(slice))
428}
429
430fn extract_hash_from_contract_data(
432 data: stellar_xdr::ContractDataEntry,
433) -> Result<String, DecodeError> {
434 let instance = match data.val {
435 ScVal::ContractInstance(i) => i,
436 _ => return Err(DecodeError::Extraction("Not a ContractInstance".into())),
437 };
438
439 let hash = match instance.executable {
440 ContractExecutable::Wasm(h) => h,
441 _ => return Err(DecodeError::Extraction("Not a Wasm executable".into())),
442 };
443
444 Ok(hex::encode(hash.0))
445}
446
447pub fn extract_contract_data_value(
452 base64_ledger_entry: &str,
453) -> Result<stellar_xdr::ContractDataEntry, DecodeError> {
454 let raw = detect_and_decode(base64_ledger_entry)?;
455 match extract_contract_data_value_standard(&raw) {
456 Ok(data) => Ok(data),
457 Err(e) => match e {
458 DecodeError::XdrParse(..) => extract_contract_data_value_from_live_ledger_entry(&raw),
459 _ => Err(e),
460 },
461 }
462}
463
464fn extract_contract_data_value_standard(
465 raw: &[u8],
466) -> Result<stellar_xdr::ContractDataEntry, DecodeError> {
467 let mut cursor = std::io::Cursor::new(raw);
468 let mut l = Limited::new(&mut cursor, Limits::none());
469 let entry = LedgerEntry::read_xdr(&mut l)
470 .map_err(|e| DecodeError::XdrParse("LedgerEntry".to_string(), e))?;
471 match entry.data {
472 LedgerEntryData::ContractData(d) => Ok(d),
473 _ => Err(DecodeError::Extraction("Not a ContractData entry".into())),
474 }
475}
476
477fn extract_contract_data_value_from_live_ledger_entry(
478 raw: &[u8],
479) -> Result<stellar_xdr::ContractDataEntry, DecodeError> {
480 let mut cursor = std::io::Cursor::new(raw);
481 let mut l = Limited::new(&mut cursor, Limits::none());
482 let data = LedgerEntryData::read_xdr(&mut l)
483 .map_err(|e| DecodeError::XdrParse("LedgerEntryData(live)".to_string(), e))?;
484 match data {
485 LedgerEntryData::ContractData(d) => Ok(d),
486 _ => Err(DecodeError::Extraction("Not a ContractData entry".into())),
487 }
488}
489
490pub fn extract_wasm_bytecode(base64_ledger_entry: &str) -> Result<Vec<u8>, DecodeError> {
495 let raw = detect_and_decode(base64_ledger_entry)?;
496
497 match extract_wasm_bytecode_standard(&raw) {
498 Ok(code) => Ok(code),
499 Err(e) => match e {
500 DecodeError::XdrParse(..) => extract_wasm_bytecode_from_live_ledger_entry(&raw),
501 _ => Err(e),
502 },
503 }
504}
505
506fn extract_wasm_bytecode_standard(raw: &[u8]) -> Result<Vec<u8>, DecodeError> {
507 let mut cursor = std::io::Cursor::new(raw);
508 let mut l = Limited::new(&mut cursor, Limits::none());
509 let entry = LedgerEntry::read_xdr(&mut l)
510 .map_err(|e| DecodeError::XdrParse("LedgerEntry".to_string(), e))?;
511
512 let code = match entry.data {
513 LedgerEntryData::ContractCode(c) => c,
514 _ => return Err(DecodeError::Extraction("Not a ContractCode entry".into())),
515 };
516 Ok(code.code.to_vec())
517}
518
519pub fn extract_wasm_bytecode_from_live_ledger_entry(raw: &[u8]) -> Result<Vec<u8>, DecodeError> {
521 let mut cursor = std::io::Cursor::new(raw);
522 let mut l = Limited::new(&mut cursor, Limits::none());
523 let data = LedgerEntryData::read_xdr(&mut l)
524 .map_err(|e| DecodeError::XdrParse("LedgerEntryData(live)".to_string(), e))?;
525
526 let code = match data {
527 LedgerEntryData::ContractCode(c) => c,
528 _ => return Err(DecodeError::Extraction("Not a ContractCode entry".into())),
529 };
530 Ok(code.code.to_vec())
531}
532
533pub fn decode(
552 payload: &str,
553 type_hint: Option<&str>,
554 format: OutputFormat,
555) -> Result<String, DecodeError> {
556 let raw = detect_and_decode(payload)?;
557 let value = decode_bytes(&raw, type_hint)?;
558 format_json(&value, format)
559}
560
561pub fn decode_bytes(raw: &[u8], type_hint: Option<&str>) -> Result<Value, DecodeError> {
563 if raw.is_empty() {
564 return Err(DecodeError::EmptyPayload);
565 }
566
567 let type_name = type_hint.unwrap_or("auto");
568
569 match type_name.to_lowercase().as_str() {
570 "scval" => decode_single::<ScVal>(raw, "ScVal"),
571 "transactionenvelope" => decode_single::<TransactionEnvelope>(raw, "TransactionEnvelope"),
572 "transactionresult" => decode_single::<TransactionResult>(raw, "TransactionResult"),
573 "transactionmeta" => decode_single::<TransactionMeta>(raw, "TransactionMeta"),
574 "ledgerkey" => decode_single::<LedgerKey>(raw, "LedgerKey"),
575 "ledgerentry" => decode_single::<LedgerEntry>(raw, "LedgerEntry"),
576 "contractevent" => decode_single::<ContractEvent>(raw, "ContractEvent"),
577 "auto" => auto_detect(raw),
578 other => Err(DecodeError::TypeUnknown(other.to_string())),
579 }
580}
581
582fn decode_single<T: ReadXdr + serde::Serialize>(
583 raw: &[u8],
584 name: &str,
585) -> Result<Value, DecodeError> {
586 let mut cursor = std::io::Cursor::new(raw);
587 let mut l = Limited::new(&mut cursor, Limits::none());
588 T::read_xdr(&mut l)
589 .map_err(|e| DecodeError::XdrParse(name.to_string(), e))
590 .and_then(|v| serde_json::to_value(&v).map_err(DecodeError::Json))
591}
592
593fn decode_single_strict<T: ReadXdr + serde::Serialize>(
594 raw: &[u8],
595 name: &str,
596) -> Result<Value, DecodeError> {
597 let mut cursor = std::io::Cursor::new(raw);
598 let mut l = Limited::new(&mut cursor, Limits::none());
599 T::read_xdr_to_end(&mut l)
600 .map_err(|e| DecodeError::XdrParse(name.to_string(), e))
601 .and_then(|v| serde_json::to_value(&v).map_err(DecodeError::Json))
602}
603
604fn auto_detect(raw: &[u8]) -> Result<Value, DecodeError> {
605 if let Ok(v) = decode_single_strict::<ScVal>(raw, "ScVal") {
606 return Ok(v);
607 }
608 if let Ok(v) = decode_single_strict::<TransactionEnvelope>(raw, "TransactionEnvelope") {
609 return Ok(v);
610 }
611 if let Ok(v) = decode_single_strict::<ContractEvent>(raw, "ContractEvent") {
612 return Ok(v);
613 }
614 if let Ok(v) = decode_single_strict::<TransactionResult>(raw, "TransactionResult") {
615 return Ok(v);
616 }
617 Err(DecodeError::TypeUnknown(
618 "auto-detection failed for all known types".to_string(),
619 ))
620}
621
622fn detect_and_decode(payload: &str) -> Result<Vec<u8>, DecodeError> {
623 if payload.is_empty() {
624 return Err(DecodeError::EmptyPayload);
625 }
626
627 let trimmed = payload.trim();
628
629 let base64_likely = !trimmed.is_empty()
630 && trimmed.len().is_multiple_of(4)
631 && trimmed
632 .chars()
633 .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=');
634
635 if base64_likely {
636 if let Ok(bytes) = STANDARD.decode(trimmed) {
637 return Ok(bytes);
638 }
639 }
640
641 if trimmed.len().is_multiple_of(2) && trimmed.chars().all(|c| c.is_ascii_hexdigit()) {
642 if let Ok(bytes) = hex::decode(trimmed) {
643 return Ok(bytes);
644 }
645 }
646
647 if base64_likely {
648 Err(DecodeError::Base64(base64::DecodeError::InvalidLength(0)))
649 } else {
650 Err(DecodeError::Hex(hex::FromHexError::InvalidHexCharacter {
651 c: trimmed
652 .chars()
653 .find(|c| !c.is_ascii_hexdigit())
654 .unwrap_or('?'),
655 index: 0,
656 }))
657 }
658}
659
660pub fn format_json(value: &Value, format: OutputFormat) -> Result<String, DecodeError> {
661 match format {
662 OutputFormat::Json => Ok(serde_json::to_string(value)?),
663 OutputFormat::Pretty => Ok(serde_json::to_string_pretty(value)?),
664 }
665}
666
667pub fn estimate_xdr_size<T: WriteXdr>(value: &T) -> usize {
672 let mut buf = Vec::new();
673 let mut l = Limited::new(&mut buf, Limits::none());
674 let _ = value.write_xdr(&mut l);
677 buf.len()
678}
679
680pub fn validate_xdr(raw: &[u8]) -> Result<usize, String> {
685 if raw.is_empty() {
686 return Err("empty payload".into());
687 }
688 let mut cursor = std::io::Cursor::new(raw);
689 let mut l = Limited::new(&mut cursor, Limits::none());
690 TransactionEnvelope::read_xdr(&mut l)
691 .map_err(|e| format!("malformed transaction envelope: {e}"))
692 .map(|_| raw.len())
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698 use stellar_xdr::{
699 ContractDataDurability, ContractDataEntry, ContractExecutable, ExtensionPoint, Hash,
700 LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerKey, ScAddress, ScContractInstance,
701 ScVal, TransactionResult, TransactionResultExt, TransactionResultResult, WriteXdr,
702 };
703
704 #[test]
705 fn test_invalid_base64() {
706 let result = decode("!!! ???", None, OutputFormat::default());
707 assert!(matches!(result, Err(DecodeError::Hex(_))));
708 }
709
710 #[test]
711 fn test_valid_base64_but_invalid_xdr() {
712 let result = decode("aGVsbG8gd29ybGQ=", None, OutputFormat::default());
714 assert!(matches!(result, Err(DecodeError::TypeUnknown(_))));
715 }
716
717 #[test]
718 fn test_valid_scval_integer_base64() {
719 let payload = "AAAABAAAAAE=";
720 let json = decode(payload, Some("scval"), OutputFormat::Json).unwrap();
721 let v: Value = serde_json::from_str(&json).unwrap();
722 assert!(v.is_object());
723 assert_eq!(v["i32"], 1);
724 }
725
726 #[test]
727 fn test_auto_decode_scval() {
728 let payload = "AAAABAAAAAE=";
729 let json = decode(payload, None, OutputFormat::Json).unwrap();
730 let v: Value = serde_json::from_str(&json).unwrap();
731 assert!(v.is_object());
732 assert_eq!(v["i32"], 1);
733 }
734
735 #[test]
736 fn test_auto_decode_transaction_result() {
737 let fee_charged = 9_000_000_000_000_i64;
738 let result = TransactionResult {
739 fee_charged,
740 result: TransactionResultResult::TxSuccess(vec![].try_into().unwrap()),
741 ext: TransactionResultExt::V0,
742 };
743 let bytes = result.to_xdr(Limits::none()).unwrap();
744 let payload = STANDARD.encode(bytes);
745 let json = decode(&payload, None, OutputFormat::Json).unwrap();
746 let v: Value = serde_json::from_str(&json).unwrap();
747
748 assert_eq!(v["fee_charged"], fee_charged.to_string());
749 assert!(v["result"]["tx_success"].is_array());
750 }
751
752 #[test]
753 fn test_auto_decode_small_transaction_result_not_scval() {
754 let fee_charged = 100_i64;
755 let result = TransactionResult {
756 fee_charged,
757 result: TransactionResultResult::TxSuccess(vec![].try_into().unwrap()),
758 ext: TransactionResultExt::V0,
759 };
760 let bytes = result.to_xdr(Limits::none()).unwrap();
761 let payload = STANDARD.encode(bytes);
762 let json = decode(&payload, None, OutputFormat::Json).unwrap();
763 let v: Value = serde_json::from_str(&json).unwrap();
764
765 assert_eq!(v["fee_charged"], fee_charged.to_string());
766 assert!(v["result"]["tx_success"].is_array());
767 assert!(v.get("i32").is_none());
768 }
769
770 #[test]
771 fn test_auto_decode_unknown_payload_still_returns_type_unknown() {
772 let payload = "aGVsbG8gd29ybGQ=";
773 let result = decode(payload, None, OutputFormat::default());
774 assert!(matches!(result, Err(DecodeError::TypeUnknown(_))));
775 }
776
777 #[test]
778 fn test_empty_payload() {
779 let result = decode("", None, OutputFormat::default());
780 assert!(matches!(result, Err(DecodeError::EmptyPayload)));
781 }
782
783 #[test]
784 fn test_unknown_type() {
785 let payload = "AAAABAAAAAE=";
786 let result = decode(payload, Some("nonexistent"), OutputFormat::default());
787 assert!(matches!(result, Err(DecodeError::TypeUnknown(_))));
788 }
789
790 #[test]
791 fn test_json_vs_pretty() {
792 let payload = "AAAABAAAAAE=";
793 let compact = decode(payload, Some("scval"), OutputFormat::Json).unwrap();
794 let pretty = decode(payload, Some("scval"), OutputFormat::Pretty).unwrap();
795 assert!(!compact.contains('\n'));
796 assert!(pretty.contains('\n'));
797 }
798
799 #[test]
800 fn test_build_map_key_symbol_and_u32() {
801 let u32_b64 = scval_to_base64(&ScVal::U32(100)).unwrap();
802 let key = build_map_key("balances", &[u32_b64]).unwrap();
803 match &key {
804 ScVal::Vec(Some(v)) => {
805 assert_eq!(v.len(), 2);
806 assert_eq!(v[0], ScVal::Symbol("balances".try_into().unwrap()));
807 assert_eq!(v[1], ScVal::U32(100));
808 }
809 other => panic!("expected ScVec, got {other:?}"),
810 }
811 }
812
813 #[test]
814 fn test_symbol_topic_base64_encodes_scval_symbol() {
815 let b64 = symbol_topic_base64("Transfer").unwrap();
817 assert_eq!(b64, "AAAADwAAAAhUcmFuc2Zlcg==");
818 assert_eq!(
819 scval_from_base64(&b64),
820 Some(ScVal::Symbol("Transfer".try_into().unwrap()))
821 );
822 assert_eq!(
824 symbol_topic_base64("transfer").unwrap(),
825 "AAAADwAAAAh0cmFuc2Zlcg=="
826 );
827 }
828
829 #[test]
830 fn test_symbol_topic_base64_rejects_invalid_symbols() {
831 for bad in ["", "has space", "dash-ed", "é", &"a".repeat(33)] {
832 let err = symbol_topic_base64(bad).unwrap_err();
833 assert!(
834 err.to_string().contains("invalid symbol"),
835 "unexpected error for {bad:?}: {err}"
836 );
837 }
838 assert!(symbol_topic_base64(&"a".repeat(32)).is_ok());
839 }
840
841 #[test]
842 fn test_build_map_key_symbol_only_and_composite() {
843 let single = build_map_key("admin", &[]).unwrap();
845 match &single {
846 ScVal::Vec(Some(v)) => {
847 assert_eq!(v.len(), 1);
848 assert_eq!(v[0], ScVal::Symbol("admin".try_into().unwrap()));
849 }
850 other => panic!("expected ScVec, got {other:?}"),
851 }
852 let a = scval_to_base64(&ScVal::U32(1)).unwrap();
854 let b = scval_to_base64(&ScVal::U32(2)).unwrap();
855 let composite = build_map_key("pair", &[a, b]).unwrap();
856 match &composite {
857 ScVal::Vec(Some(v)) => assert_eq!(v.len(), 3),
858 other => panic!("expected ScVec, got {other:?}"),
859 }
860 }
861
862 #[test]
863 fn test_encode_contract_data_entry_durability_changes_key() {
864 let contract = "0000000000000000000000000000000000000000000000000000000000000000";
865 let key = build_map_key("k", &[scval_to_base64(&ScVal::U32(7)).unwrap()]).unwrap();
866 let persistent = encode_ledger_key(&LedgerKeyParams::ContractDataEntry {
867 contract: contract.to_string(),
868 key: key.clone(),
869 durability: ContractDataDurability::Persistent,
870 })
871 .unwrap();
872 let temporary = encode_ledger_key(&LedgerKeyParams::ContractDataEntry {
873 contract: contract.to_string(),
874 key: key.clone(),
875 durability: ContractDataDurability::Temporary,
876 })
877 .unwrap();
878 assert_ne!(
879 persistent, temporary,
880 "durability must produce a different LedgerKey"
881 );
882
883 let decoded = decode_ledger_key(&persistent).unwrap();
884 match decoded {
885 LedgerKey::ContractData(d) => {
886 assert_eq!(d.durability, ContractDataDurability::Persistent);
887 assert_eq!(d.key, key);
888 }
889 other => panic!("expected ContractData, got {other:?}"),
890 }
891 }
892
893 #[test]
894 fn test_encode_contract_data_entry_accepts_strkey_and_hex() {
895 let id = [3u8; 32];
896 let hex_id = hex::encode(id);
897 let strkey = format!("{}", stellar_strkey::Contract(id));
898 let from_hex = encode_ledger_key(&LedgerKeyParams::ContractDataEntry {
899 contract: hex_id,
900 key: ScVal::LedgerKeyContractInstance,
901 durability: ContractDataDurability::Persistent,
902 })
903 .unwrap();
904 let from_strkey = encode_ledger_key(&LedgerKeyParams::ContractDataEntry {
905 contract: strkey,
906 key: ScVal::LedgerKeyContractInstance,
907 durability: ContractDataDurability::Persistent,
908 })
909 .unwrap();
910 assert_eq!(from_hex, from_strkey);
911 }
912
913 fn external_ref_fixture() -> (ContractExecutable, ScAddress) {
922 let owner = ScAddress::Contract(ContractId(Hash([7u8; 32])));
923 let tag =
924 stellar_xdr::ScString(stellar_xdr::StringM::try_from(b"oracle-v1".to_vec()).unwrap());
925 (
926 ContractExecutable::ExternalRef(stellar_xdr::ContractExecutableExternalRef {
927 executable_owner: owner.clone(),
928 tag,
929 }),
930 owner,
931 )
932 }
933
934 #[test]
935 fn test_extract_contract_executable_wasm() {
936 let hash = [9u8; 32];
937 let b64 = create_test_ledger_entry(ContractExecutable::Wasm(Hash(hash)));
938 let exec = extract_contract_executable(&b64).unwrap();
939 assert_eq!(exec.kind(), "wasm");
940 assert_eq!(exec, ContractExecutableRef::Wasm(Hash(hash)));
941 assert_eq!(extract_wasm_hash(&b64).unwrap(), hex::encode(hash));
943 }
944
945 #[test]
946 fn test_extract_contract_executable_stellar_asset_is_not_wasm() {
947 let b64 = create_test_ledger_entry(ContractExecutable::StellarAsset);
948 let exec = extract_contract_executable(&b64).unwrap();
949 assert_eq!(exec, ContractExecutableRef::StellarAsset);
950 assert_eq!(exec.kind(), "stellar_asset");
951 assert!(!matches!(exec, ContractExecutableRef::Wasm(_)));
953 assert!(exec.executable_tag_key().is_none());
954 }
955
956 #[test]
957 fn test_extract_contract_executable_external_ref() {
958 let (executable, owner) = external_ref_fixture();
959 let b64 = create_test_ledger_entry(executable);
960 let exec = extract_contract_executable(&b64).unwrap();
961 assert_eq!(exec.kind(), "external_ref");
962 let reference = exec.executable_tag_key().expect("external ref key");
963 assert_eq!(reference.owner, owner);
964 assert_eq!(reference.tag, "oracle-v1");
965 assert!(extract_wasm_hash(&b64).is_err());
968 }
969
970 #[test]
971 fn test_external_ref_survives_live_wire_layout() {
972 let (executable, owner) = external_ref_fixture();
976 let data = LedgerEntryData::ContractData(ContractDataEntry {
977 ext: ExtensionPoint::V0,
978 contract: ScAddress::Contract(ContractId(Hash([0; 32]))),
979 key: ScVal::LedgerKeyContractInstance,
980 durability: ContractDataDurability::Persistent,
981 val: ScVal::ContractInstance(ScContractInstance {
982 executable,
983 storage: None,
984 }),
985 });
986 let mut buf = Vec::new();
987 let mut cursor = std::io::Cursor::new(&mut buf);
988 let mut l = Limited::new(&mut cursor, Limits::none());
989 data.write_xdr(&mut l).unwrap();
990
991 let exec = extract_executable_from_live_ledger_entry(&buf).unwrap();
992 assert_eq!(exec.kind(), "external_ref");
993 assert_eq!(exec.executable_tag_key().unwrap().owner, owner);
994 assert_eq!(exec.executable_tag_key().unwrap().tag, "oracle-v1");
995 }
996
997 #[test]
998 fn test_parse_external_executable_hash_accepts_32_bytes() {
999 let hash = [3u8; 32];
1000 let val = ScVal::Bytes(stellar_xdr::ScBytes(
1001 stellar_xdr::BytesM::try_from(hash.to_vec()).unwrap(),
1002 ));
1003 assert_eq!(
1004 parse_external_executable_hash(&val).unwrap(),
1005 hex::encode(hash)
1006 );
1007 }
1008
1009 #[test]
1010 fn test_parse_external_executable_hash_rejects_non_hash_values() {
1011 let short = ScVal::Bytes(stellar_xdr::ScBytes(
1014 stellar_xdr::BytesM::try_from(vec![1u8; 31]).unwrap(),
1015 ));
1016 assert!(parse_external_executable_hash(&short).is_err());
1017
1018 let as_string = ScVal::String(stellar_xdr::ScString(
1019 stellar_xdr::StringM::try_from(vec![1u8; 32]).unwrap(),
1020 ));
1021 assert!(parse_external_executable_hash(&as_string).is_err());
1022
1023 assert!(parse_external_executable_hash(&ScVal::U32(5)).is_err());
1024 }
1025
1026 #[test]
1027 fn test_extract_contract_executable_rejects_non_contract_data() {
1028 let err = extract_contract_executable("AAAABAAAAAE=").unwrap_err();
1029 assert!(matches!(err, DecodeError::XdrParse(_, _)));
1030 }
1031
1032 #[test]
1033 fn test_build_map_key_rejects_invalid_symbol_and_arg() {
1034 assert!(build_map_key("has spaces", &[]).is_err());
1035 assert!(build_map_key("k", &["!!!not-base64!!!".to_string()]).is_err());
1036 }
1037
1038 #[test]
1039 fn test_encode_contract_data_entry_rejects_bad_contract() {
1040 let err = encode_ledger_key(&LedgerKeyParams::ContractDataEntry {
1041 contract: "not-a-contract".to_string(),
1042 key: ScVal::LedgerKeyContractInstance,
1043 durability: ContractDataDurability::Persistent,
1044 });
1045 assert!(err.is_err());
1046 }
1047
1048 #[test]
1049 fn test_encode_ledger_key() {
1050 let contract_id = "0000000000000000000000000000000000000000000000000000000000000000";
1051 let res =
1052 encode_ledger_key(&LedgerKeyParams::ContractData(contract_id.to_string())).unwrap();
1053 let decoded = detect_and_decode(&res).unwrap();
1055 let mut cursor = std::io::Cursor::new(&decoded);
1056 let mut l = Limited::new(&mut cursor, Limits::none());
1057 let lk = LedgerKey::read_xdr(&mut l).unwrap();
1058 match lk {
1059 LedgerKey::ContractData(d) => {
1060 assert_eq!(d.key, ScVal::LedgerKeyContractInstance);
1061 }
1062 _ => panic!("Expected ContractData"),
1063 }
1064 }
1065
1066 fn create_test_ledger_entry(executable: ContractExecutable) -> String {
1067 let entry = LedgerEntry {
1068 last_modified_ledger_seq: 1,
1069 data: LedgerEntryData::ContractData(ContractDataEntry {
1070 ext: ExtensionPoint::V0,
1071 contract: ScAddress::Contract(ContractId(Hash([0; 32]))),
1072 key: ScVal::LedgerKeyContractInstance,
1073 durability: ContractDataDurability::Persistent,
1074 val: ScVal::ContractInstance(ScContractInstance {
1075 executable,
1076 storage: None,
1077 }),
1078 }),
1079 ext: LedgerEntryExt::V0,
1080 };
1081 let mut buf = Vec::new();
1082 let mut cursor = std::io::Cursor::new(&mut buf);
1083 let mut l = Limited::new(&mut cursor, Limits::none());
1084 entry.write_xdr(&mut l).unwrap();
1085 STANDARD.encode(&buf)
1086 }
1087
1088 #[test]
1089 fn test_extract_wasm_hash_valid() {
1090 let hash = [1u8; 32];
1091 let b64 = create_test_ledger_entry(ContractExecutable::Wasm(Hash(hash)));
1092 let extracted = extract_wasm_hash(&b64).unwrap();
1093 assert_eq!(extracted, hex::encode(hash));
1094 }
1095
1096 #[test]
1097 fn test_extract_wasm_hash_non_wasm() {
1098 let b64 = create_test_ledger_entry(ContractExecutable::StellarAsset);
1099 let err = extract_wasm_hash(&b64).unwrap_err();
1100 assert!(matches!(err, DecodeError::Extraction(e) if e.contains("Not a Wasm")));
1101 }
1102
1103 #[test]
1104 fn test_extract_wasm_hash_non_ledger_entry() {
1105 let b64 = "AAAABAAAAAE=";
1107 let err = extract_wasm_hash(b64).unwrap_err();
1108 assert!(matches!(err, DecodeError::XdrParse(_, _)));
1109 }
1110
1111 #[test]
1112 fn test_extract_wasm_hash_malformed_base64() {
1113 let err = extract_wasm_hash("invalid base64!!!").unwrap_err();
1114 assert!(matches!(err, DecodeError::Hex(_))); }
1116
1117 const LIVE_WASM_LEDGER_ENTRY_B64: &str = "AAAABgAAAAAAAAABCbp9KiSjbJ3kh/Q6tM6HrPB88nwyvuK8814icmyjwGwAAAAUAAAAAQAAABMAAAAAYM3a5n8gLBnuewAMiU/RKqi0TeCatlL14Yi8DGOmzwIAAAABAAAABwAAABAAAAABAAAAAQAAAA8AAAAFQWRtaW4AAAAAAAASAAAAAAAAAACUijpq22w97c0/5wJEUlkFddoMBv/zyTHhNMr9T4FBBQAAABAAAAABAAAAAQAAAA8AAAAGQ29uZmlnAAAAAAARAAAAAQAAAAsAAAAPAAAAFWJhc2VfZnVuZGluZ19yYXRlX2JwcwAAAAAAAAMAAAABAAAADwAAABJiYXNlX21ha2VyX2ZlZV9icHMAAAAAAAMAAAACAAAADwAAABJiYXNlX3Rha2VyX2ZlZV9icHMAAAAAAAMAAAAFAAAADwAAABNsaXF1aWRhdGlvbl9mZWVfYnBzAAAAAAMAAAH0AAAADwAAABZtYWludGVuYW5jZV9tYXJnaW5fYnBzAAAAAAADAAAAZAAAAA8AAAAMbWF4X2xldmVyYWdlAAAAAwAAAAoAAAAPAAAAGG1heF9vcmFjbGVfZGV2aWF0aW9uX2JwcwAAAAMAAABkAAAADwAAABFtYXhfcG9zaXRpb25fc2l6ZQAAAAAAAAoAAAAAAAAAAAAAAOjUpRAAAAAADwAAABNtYXhfcHJpY2Vfc3RhbGVuZXNzAAAAAAUAAAAAAAAAPAAAAA8AAAAObWluX2NvbGxhdGVyYWwAAAAAAAoAAAAAAAAAAAAAAAAF9eEAAAAADwAAAA90cmFkaW5nX2ZlZV9icHMAAAAAAwAAAAoAAAAQAAAAAQAAAAEAAAAPAAAAC0luaXRpYWxpemVkAAAAAAAAAAABAAAAEAAAAAEAAAABAAAADwAAAA1PcmFjbGVBZGFwdGVyAAAAAAAAEgAAAAFxzC34I3pmV4llcmFKnN7k7qZkMKXvGRPZMuobQm6naQAAABAAAAABAAAAAQAAAA8AAAAGUGF1c2VkAAAAAAAAAAAAAQAAABAAAAABAAAAAQAAAA8AAAAJVXNkY1Rva2VuAAAAAAAAEgAAAAE9sj2cIS9K0A0viwokid/jaOEXECNi3xRj3/ivNitXOgAAABAAAAABAAAAAQAAAA8AAAAFVmF1bHQAAAAAAAASAAAAAVdc5734Fh3zRgwQwWibRi6egIv95VY++/CqtURwMM+t";
1128
1129 const LIVE_SAC_LEDGER_ENTRY_B64: &str = "AAAABgAAAAAAAAAB15KLcsJwPM/q9+uf9O9NUEpVqLl5/JtFDqLIQrTRzmEAAAAUAAAAAQAAABMAAAABAAAAAQAAAAIAAAAPAAAACE1FVEFEQVRBAAAAEQAAAAEAAAADAAAADwAAAAdkZWNpbWFsAAAAAAMAAAAHAAAADwAAAARuYW1lAAAADgAAAAZuYXRpdmUAAAAAAA8AAAAGc3ltYm9sAAAAAAAOAAAABm5hdGl2ZQAAAAAAEAAAAAEAAAABAAAADwAAAAlBc3NldEluZm8AAAAAAAAQAAAAAQAAAAEAAAAPAAAABk5hdGl2ZQAA";
1135
1136 #[test]
1137 fn test_extract_wasm_hash_live_wasm_wire_layout() {
1138 let raw = detect_and_decode(LIVE_WASM_LEDGER_ENTRY_B64).unwrap();
1141 assert!(extract_wasm_hash_standard(&raw).is_err());
1142
1143 let hash = extract_wasm_hash_from_live_ledger_entry(&raw).unwrap();
1145 assert_eq!(
1146 hash,
1147 "60cddae67f202c19ee7b000c894fd12aa8b44de09ab652f5e188bc0c63a6cf02"
1148 );
1149
1150 let via_public = extract_wasm_hash(LIVE_WASM_LEDGER_ENTRY_B64).unwrap();
1152 assert_eq!(via_public, hash);
1153 }
1154
1155 #[test]
1156 fn test_extract_wasm_hash_live_sac_is_controlled_error() {
1157 let raw = detect_and_decode(LIVE_SAC_LEDGER_ENTRY_B64).unwrap();
1160 let err = extract_wasm_hash_from_live_ledger_entry(&raw).unwrap_err();
1161 assert!(matches!(err, DecodeError::Extraction(e) if e.contains("Not a Wasm")));
1162
1163 let err = extract_wasm_hash(LIVE_SAC_LEDGER_ENTRY_B64).unwrap_err();
1165 assert!(matches!(err, DecodeError::Extraction(e) if e.contains("Not a Wasm")));
1166 }
1167
1168 #[test]
1169 fn test_extract_wasm_hash_live_wire_truncated_fails_controlled() {
1170 let truncated = &LIVE_WASM_LEDGER_ENTRY_B64[..LIVE_WASM_LEDGER_ENTRY_B64.len() - 4];
1173 let err = extract_wasm_hash(truncated).unwrap_err();
1174 assert!(matches!(err, DecodeError::XdrParse(_, _)));
1175 }
1176
1177 #[test]
1178 fn test_estimate_xdr_size() {
1179 let val = ScVal::U32(42);
1180 assert_eq!(estimate_xdr_size(&val), 8);
1182 }
1183}
1184
1185pub mod abi_decode;