use sdkt_wasm::spec::{ContractSpec, ContractType};
use stellar_xdr::ScVal;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AbiDecodedValue {
pub raw: String,
pub label: String,
pub matched_type: Option<String>,
pub fields: Option<Vec<(String, String)>>,
}
pub fn decode_with_abi(
spec: &ContractSpec,
val: &ScVal,
event_hint: Option<&str>,
) -> AbiDecodedValue {
let raw = format!("{:?}", val);
if let Some(event_name) = event_hint {
if spec.events.iter().any(|e| e.name == event_name) {
return AbiDecodedValue {
raw: raw.clone(),
label: format!(
"event[{}] -> {}",
event_name,
decode_scval_to_string(val, spec)
),
matched_type: Some(format!("event:{}", event_name)),
fields: extract_scval_fields(val, spec),
};
}
}
if let Some(fields) = extract_scval_fields(val, spec) {
let matches = spec
.custom_types
.iter()
.filter(|custom_type| matches_custom_type(val, &fields, custom_type))
.collect::<Vec<_>>();
if matches.len() == 1 {
let custom_type = matches[0];
return AbiDecodedValue {
raw: raw.clone(),
label: format!(
"{} {{ {} }}",
custom_type.name,
fields
.iter()
.map(|(k, v)| format!("{}: {}", k, v))
.collect::<Vec<_>>()
.join(", ")
),
matched_type: Some(format!("udt:{}", custom_type.name)),
fields: Some(fields),
};
}
}
AbiDecodedValue {
raw: raw.clone(),
label: decode_scval_to_string(val, spec),
matched_type: None,
fields: extract_scval_fields(val, spec),
}
}
fn matches_custom_type(
val: &ScVal,
fields: &[(String, String)],
custom_type: &ContractType,
) -> bool {
let ScVal::Map(Some(entries)) = val else {
return false;
};
if custom_type.kind != "struct"
|| custom_type.members.is_empty()
|| fields.len() != custom_type.members.len()
|| entries
.0
.iter()
.any(|entry| !matches!(entry.key, ScVal::Symbol(_)))
{
return false;
}
let actual = fields
.iter()
.map(|(name, _)| {
name.trim_matches('"')
.trim_start_matches("sym(\"")
.trim_end_matches("\")")
.to_ascii_lowercase()
})
.collect::<std::collections::BTreeSet<_>>();
let expected = custom_type
.members
.iter()
.map(|member| member.name.to_ascii_lowercase())
.collect::<std::collections::BTreeSet<_>>();
actual == expected
}
fn decode_scval_to_string(val: &ScVal, _spec: &ContractSpec) -> String {
match val {
ScVal::Bool(b) => format!("bool({})", b),
ScVal::U32(n) => format!("u32({})", n),
ScVal::I32(n) => format!("i32({})", n),
ScVal::U64(n) => format!("u64({})", n),
ScVal::Timepoint(value) => format!("timepoint({})", value.0),
ScVal::Duration(value) => format!("duration({})", value.0),
ScVal::I64(n) => format!("i64({})", n),
ScVal::U128(p) => format!(
"u128({} hi={}, lo={})",
((p.hi as u128) << 64) | p.lo as u128,
p.hi,
p.lo
),
ScVal::I128(p) => format!(
"i128({} hi={}, lo={})",
((p.hi as i128) << 64) | (p.lo as i128),
p.hi,
p.lo
),
ScVal::U256(_) => "u256(...)".to_string(),
ScVal::I256(_) => "i256(...)".to_string(),
ScVal::String(s) => format!("\"{}\"", s.to_utf8_string_lossy()),
ScVal::Symbol(s) => format!("sym(\"{}\")", s.to_utf8_string_lossy()),
ScVal::Bytes(b) => format!("bytes({} bytes)", b.len()),
ScVal::Vec(v) => {
let len = v.as_ref().map(|items| items.len()).unwrap_or(0);
format!("vec(len={})", len)
}
_ => format!("scval({:?})", val),
}
}
fn extract_scval_fields(val: &ScVal, _spec: &ContractSpec) -> Option<Vec<(String, String)>> {
match val {
ScVal::Vec(v) => {
let mut fields = Vec::new();
if let Some(items) = v.as_ref() {
for item in items.iter() {
if let ScVal::Vec(pair) = item {
if let Some(pair_items) = pair.as_ref() {
if pair_items.len() == 2 {
let key = decode_scval_to_string(&pair_items[0], _spec);
let value = decode_scval_to_string(&pair_items[1], _spec);
fields.push((key, value));
}
}
}
}
}
if fields.is_empty() {
None
} else {
Some(fields)
}
}
ScVal::Map(m) => {
let mut fields = Vec::new();
if let Some(items) = m.as_ref() {
for entry in items.iter() {
let key = decode_scval_to_string(&entry.key, _spec);
let value = decode_scval_to_string(&entry.val, _spec);
fields.push((key, value));
}
}
if fields.is_empty() {
None
} else {
Some(fields)
}
}
_ => None,
}
}
pub fn decode_event_topics(
spec: &ContractSpec,
topics: &[ScVal],
data: &[ScVal],
) -> Vec<AbiDecodedValue> {
let mut results = Vec::new();
let event_name = topics.first().and_then(|t| match t {
ScVal::Symbol(s) => Some(s.to_utf8_string_lossy()),
_ => None,
});
for (i, topic) in topics.iter().enumerate() {
let hint = if i == 0 { None } else { event_name.as_deref() };
results.push(decode_with_abi(spec, topic, hint));
}
for val in data {
results.push(decode_with_abi(spec, val, event_name.as_deref()));
}
results
}
#[cfg(test)]
mod tests {
use super::*;
use sdkt_wasm::spec::{ContractEvent, ContractType, TypeMember};
use stellar_xdr::{ScMap, ScMapEntry, ScSymbol, ScVal, VecM};
fn dummy_spec() -> ContractSpec {
ContractSpec {
env_meta: None,
functions: vec![],
custom_types: vec![],
events: vec![ContractEvent {
name: "transfer".to_string(),
doc: "Transfer event".to_string(),
params: vec![],
prefix_topics: vec![],
data_format: "single_value".to_string(),
}],
}
}
#[test]
fn decode_simple_primitives() {
let spec = dummy_spec();
let val = ScVal::U32(42);
let decoded = decode_with_abi(&spec, &val, None);
assert!(decoded.label.contains("u32(42)"));
}
#[test]
fn decode_timepoint_and_duration_primitives() {
let spec = dummy_spec();
assert_eq!(
decode_with_abi(&spec, &ScVal::Timepoint(1234_u64.into()), None).label,
"timepoint(1234)"
);
assert_eq!(
decode_with_abi(&spec, &ScVal::Duration(86400_u64.into()), None).label,
"duration(86400)"
);
}
#[test]
fn decode_string() {
use stellar_xdr::ScString;
let spec = dummy_spec();
let val = ScVal::String(ScString(
"hello".to_string().into_bytes().try_into().unwrap(),
));
let decoded = decode_with_abi(&spec, &val, None);
assert!(decoded.label.contains("hello"));
}
#[test]
fn decode_event_with_hint() {
let spec = dummy_spec();
let val = ScVal::U64(100);
let decoded = decode_with_abi(&spec, &val, Some("transfer"));
assert!(decoded.label.contains("event[transfer]"));
assert_eq!(decoded.matched_type, Some("event:transfer".to_string()));
}
#[test]
fn decode_event_topics_single() {
let spec = dummy_spec();
let topics = vec![ScVal::Symbol(ScSymbol("transfer".try_into().unwrap()))];
let data = vec![ScVal::U64(100)];
let decoded = decode_event_topics(&spec, &topics, &data);
assert_eq!(decoded.len(), 2);
assert!(decoded[0].label.contains("sym"));
assert!(decoded[1].label.contains("event[transfer]"));
}
#[test]
fn abi_decodes_primitive_with_hint() {
let spec = dummy_spec();
let val = ScVal::U32(42);
let decoded = decode_with_abi(&spec, &val, Some("transfer"));
assert!(decoded.label.contains("event[transfer]"));
assert_eq!(decoded.matched_type.as_deref(), Some("event:transfer"));
assert!(decoded.fields.is_none());
}
#[test]
fn matches_udt_by_field_names_not_declaration_order() {
let mut spec = dummy_spec();
spec.custom_types = vec![
ContractType {
name: "Point".into(),
kind: "struct".into(),
doc: String::new(),
members: vec![
TypeMember {
name: "x".into(),
doc: String::new(),
types: vec![],
value: None,
},
TypeMember {
name: "y".into(),
doc: String::new(),
types: vec![],
value: None,
},
],
type_args: vec![],
bytes_n: None,
},
ContractType {
name: "Rect".into(),
kind: "struct".into(),
doc: String::new(),
members: vec![
TypeMember {
name: "width".into(),
doc: String::new(),
types: vec![],
value: None,
},
TypeMember {
name: "height".into(),
doc: String::new(),
types: vec![],
value: None,
},
],
type_args: vec![],
bytes_n: None,
},
];
let val = ScVal::Map(Some(ScMap(
VecM::try_from(vec![
ScMapEntry {
key: ScVal::Symbol(ScSymbol("width".try_into().unwrap())),
val: ScVal::U64(100),
},
ScMapEntry {
key: ScVal::Symbol(ScSymbol("height".try_into().unwrap())),
val: ScVal::U64(200),
},
])
.unwrap(),
)));
let decoded = decode_with_abi(&spec, &val, None);
assert_eq!(decoded.matched_type.as_deref(), Some("udt:Rect"));
}
#[test]
fn unmatched_shape_does_not_receive_arbitrary_udt() {
let mut spec = dummy_spec();
spec.custom_types.push(ContractType {
name: "Point".into(),
kind: "struct".into(),
doc: String::new(),
members: vec![
TypeMember {
name: "x".into(),
doc: String::new(),
types: vec![],
value: None,
},
TypeMember {
name: "y".into(),
doc: String::new(),
types: vec![],
value: None,
},
],
type_args: vec![],
bytes_n: None,
});
let val = ScVal::Map(Some(ScMap(
VecM::try_from(vec![
ScMapEntry {
key: ScVal::Symbol(ScSymbol("width".try_into().unwrap())),
val: ScVal::U64(100),
},
ScMapEntry {
key: ScVal::Symbol(ScSymbol("height".try_into().unwrap())),
val: ScVal::U64(200),
},
])
.unwrap(),
)));
assert!(decode_with_abi(&spec, &val, None).matched_type.is_none());
}
#[test]
fn same_shape_structs_are_not_selected_by_declaration_order() {
let mut spec = dummy_spec();
let members = vec![
TypeMember {
name: "x".into(),
doc: String::new(),
types: vec![],
value: None,
},
TypeMember {
name: "y".into(),
doc: String::new(),
types: vec![],
value: None,
},
];
spec.custom_types = vec![
ContractType {
name: "First".into(),
kind: "struct".into(),
doc: String::new(),
members: members.clone(),
type_args: vec![],
bytes_n: None,
},
ContractType {
name: "Second".into(),
kind: "struct".into(),
doc: String::new(),
members,
type_args: vec![],
bytes_n: None,
},
];
let val = ScVal::Map(Some(ScMap(
VecM::try_from(vec![
ScMapEntry {
key: ScVal::Symbol(ScSymbol("x".try_into().unwrap())),
val: ScVal::U64(1),
},
ScMapEntry {
key: ScVal::Symbol(ScSymbol("y".try_into().unwrap())),
val: ScVal::U64(2),
},
])
.unwrap(),
)));
assert!(decode_with_abi(&spec, &val, None).matched_type.is_none());
}
#[test]
fn enum_shape_does_not_receive_map_derived_label() {
let mut spec = dummy_spec();
spec.custom_types.push(ContractType {
name: "PointLikeEnum".into(),
kind: "enum".into(),
doc: String::new(),
members: vec![TypeMember {
name: "x".into(),
doc: String::new(),
types: vec![],
value: None,
}],
type_args: vec![],
bytes_n: None,
});
let val = ScVal::Map(Some(ScMap(
VecM::try_from(vec![ScMapEntry {
key: ScVal::Symbol(ScSymbol("x".try_into().unwrap())),
val: ScVal::U64(1),
}])
.unwrap(),
)));
assert!(decode_with_abi(&spec, &val, None).matched_type.is_none());
}
}