use stellar_xdr::{
Duration, Int128Parts, ReadXdr, ScAddress, ScBytes, ScMap, ScMapEntry, ScString, ScVal, ScVec,
TimePoint, UInt128Parts, VecM,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScValError {
TooLong,
TypeMismatch(&'static str),
OutOfRange,
Json(String),
}
impl std::fmt::Display for ScValError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ScValError::TooLong => write!(f, "value exceeds Soroban length limit"),
ScValError::TypeMismatch(t) => write!(f, "expected {t}, got a different ScVal kind"),
ScValError::OutOfRange => write!(f, "numeric value out of range"),
ScValError::Json(message) => write!(f, "invalid JSON argument: {message}"),
}
}
}
pub fn json_to_scval(value: &serde_json::Value) -> Result<ScVal, ScValError> {
match value {
serde_json::Value::Null => Ok(ScVal::Void),
serde_json::Value::Bool(value) => Ok(ScVal::Bool(*value)),
serde_json::Value::String(value) => value.clone().into_scval(),
serde_json::Value::Number(number) => {
if let Some(value) = number.as_u64() {
if value <= u32::MAX as u64 {
Ok(ScVal::U32(value as u32))
} else {
Ok(ScVal::U64(value))
}
} else if let Some(value) = number.as_i64() {
if value >= i32::MIN as i64 && value <= i32::MAX as i64 {
Ok(ScVal::I32(value as i32))
} else {
Ok(ScVal::I64(value))
}
} else {
Err(ScValError::Json(number.to_string()))
}
}
serde_json::Value::Array(values) => {
let values = values
.iter()
.map(json_to_scval)
.collect::<Result<Vec<_>, _>>()?;
Ok(ScVal::Vec(Some(ScVec(
VecM::try_from(values).map_err(|_| ScValError::TooLong)?,
))))
}
serde_json::Value::Object(values) => {
let entries = values
.iter()
.map(|(key, value)| {
Ok(ScMapEntry {
key: key.clone().into_scval()?,
val: json_to_scval(value)?,
})
})
.collect::<Result<Vec<_>, ScValError>>()?;
Ok(ScVal::Map(Some(ScMap(
VecM::try_from(entries).map_err(|_| ScValError::TooLong)?,
))))
}
}
}
pub fn json_args_to_base64(input: &str) -> Result<Vec<String>, ScValError> {
let value: serde_json::Value = serde_json::from_str(input)
.map_err(|error| ScValError::Json(format!("{error}: {input}")))?;
let values = value
.as_array()
.ok_or_else(|| ScValError::Json(format!("expected a JSON array of arguments: {input}")))?;
values
.iter()
.map(|value| {
let scval = json_to_scval(value)?;
scval_to_base64(&scval).map_err(|error| ScValError::Json(error.to_string()))
})
.collect()
}
impl std::error::Error for ScValError {}
pub trait IntoScVal {
fn into_scval(self) -> Result<ScVal, ScValError>;
}
pub trait FromScVal: Sized {
fn from_scval(v: &ScVal) -> Result<Self, ScValError>;
}
impl IntoScVal for bool {
fn into_scval(self) -> Result<ScVal, ScValError> {
Ok(ScVal::Bool(self))
}
}
impl FromScVal for bool {
fn from_scval(v: &ScVal) -> Result<Self, ScValError> {
match v {
ScVal::Bool(b) => Ok(*b),
_ => Err(ScValError::TypeMismatch("bool")),
}
}
}
impl IntoScVal for u32 {
fn into_scval(self) -> Result<ScVal, ScValError> {
Ok(ScVal::U32(self))
}
}
impl FromScVal for u32 {
fn from_scval(v: &ScVal) -> Result<Self, ScValError> {
match v {
ScVal::U32(n) => Ok(*n),
_ => Err(ScValError::TypeMismatch("u32")),
}
}
}
impl IntoScVal for i32 {
fn into_scval(self) -> Result<ScVal, ScValError> {
Ok(ScVal::I32(self))
}
}
impl FromScVal for i32 {
fn from_scval(v: &ScVal) -> Result<Self, ScValError> {
match v {
ScVal::I32(n) => Ok(*n),
_ => Err(ScValError::TypeMismatch("i32")),
}
}
}
impl IntoScVal for u64 {
fn into_scval(self) -> Result<ScVal, ScValError> {
Ok(ScVal::U64(self))
}
}
impl FromScVal for u64 {
fn from_scval(v: &ScVal) -> Result<Self, ScValError> {
match v {
ScVal::U64(n) => Ok(*n),
_ => Err(ScValError::TypeMismatch("u64")),
}
}
}
impl IntoScVal for TimePoint {
fn into_scval(self) -> Result<ScVal, ScValError> {
Ok(ScVal::Timepoint(self))
}
}
impl FromScVal for TimePoint {
fn from_scval(v: &ScVal) -> Result<Self, ScValError> {
match v {
ScVal::Timepoint(value) => Ok(value.clone()),
_ => Err(ScValError::TypeMismatch("timepoint")),
}
}
}
impl IntoScVal for Duration {
fn into_scval(self) -> Result<ScVal, ScValError> {
Ok(ScVal::Duration(self))
}
}
impl FromScVal for Duration {
fn from_scval(v: &ScVal) -> Result<Self, ScValError> {
match v {
ScVal::Duration(value) => Ok(value.clone()),
_ => Err(ScValError::TypeMismatch("duration")),
}
}
}
impl IntoScVal for i64 {
fn into_scval(self) -> Result<ScVal, ScValError> {
Ok(ScVal::I64(self))
}
}
impl FromScVal for i64 {
fn from_scval(v: &ScVal) -> Result<Self, ScValError> {
match v {
ScVal::I64(n) => Ok(*n),
_ => Err(ScValError::TypeMismatch("i64")),
}
}
}
impl IntoScVal for u128 {
fn into_scval(self) -> Result<ScVal, ScValError> {
Ok(ScVal::U128(UInt128Parts {
hi: (self >> 64) as u64,
lo: self as u64,
}))
}
}
impl FromScVal for u128 {
fn from_scval(v: &ScVal) -> Result<Self, ScValError> {
match v {
ScVal::U128(p) => Ok(((p.hi as u128) << 64) | p.lo as u128),
_ => Err(ScValError::TypeMismatch("u128")),
}
}
}
impl IntoScVal for i128 {
fn into_scval(self) -> Result<ScVal, ScValError> {
Ok(ScVal::I128(Int128Parts {
hi: (self >> 64) as i64,
lo: self as u64,
}))
}
}
impl FromScVal for i128 {
fn from_scval(v: &ScVal) -> Result<Self, ScValError> {
match v {
ScVal::I128(p) => Ok(((p.hi as i128) << 64) | p.lo as i128),
_ => Err(ScValError::TypeMismatch("i128")),
}
}
}
impl IntoScVal for String {
fn into_scval(self) -> Result<ScVal, ScValError> {
let str_m = self
.into_bytes()
.try_into()
.map_err(|_| ScValError::TooLong)?;
Ok(ScVal::String(ScString(str_m)))
}
}
impl FromScVal for String {
fn from_scval(v: &ScVal) -> Result<Self, ScValError> {
match v {
ScVal::String(s) => Ok(s.to_utf8_string_lossy()),
_ => Err(ScValError::TypeMismatch("String")),
}
}
}
impl IntoScVal for &str {
fn into_scval(self) -> Result<ScVal, ScValError> {
self.to_string().into_scval()
}
}
impl IntoScVal for Vec<u8> {
fn into_scval(self) -> Result<ScVal, ScValError> {
let bytes = self.try_into().map_err(|_| ScValError::TooLong)?;
Ok(ScVal::Bytes(ScBytes(bytes)))
}
}
impl FromScVal for Vec<u8> {
fn from_scval(v: &ScVal) -> Result<Self, ScValError> {
match v {
ScVal::Bytes(b) => Ok(b.to_vec()),
_ => Err(ScValError::TypeMismatch("Vec<u8>")),
}
}
}
impl IntoScVal for &[u8] {
fn into_scval(self) -> Result<ScVal, ScValError> {
self.to_vec().into_scval()
}
}
impl IntoScVal for [u8; 32] {
fn into_scval(self) -> Result<ScVal, ScValError> {
self.to_vec().into_scval()
}
}
impl FromScVal for [u8; 32] {
fn from_scval(v: &ScVal) -> Result<Self, ScValError> {
let bytes = Vec::<u8>::from_scval(v)?;
bytes
.try_into()
.map_err(|_| ScValError::TypeMismatch("[u8; 32]"))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Address(pub ScAddress);
impl IntoScVal for Address {
fn into_scval(self) -> Result<ScVal, ScValError> {
Ok(ScVal::Address(self.0))
}
}
impl FromScVal for Address {
fn from_scval(v: &ScVal) -> Result<Self, ScValError> {
match v {
ScVal::Address(a) => Ok(Address(a.clone())),
_ => Err(ScValError::TypeMismatch("Address")),
}
}
}
impl Address {
pub fn from_strkey(key: &str) -> Result<Self, ScValError> {
let parsed = stellar_strkey::Strkey::from_string(key)
.map_err(|_| ScValError::TypeMismatch("address strkey"))?;
let sc = match parsed {
stellar_strkey::Strkey::PublicKeyEd25519(pk) => {
ScAddress::Account(stellar_xdr::AccountId(
stellar_xdr::PublicKey::PublicKeyTypeEd25519(stellar_xdr::Uint256(pk.0)),
))
}
stellar_strkey::Strkey::Contract(c) => {
ScAddress::Contract(stellar_xdr::ContractId(stellar_xdr::Hash(c.0)))
}
_ => return Err(ScValError::TypeMismatch("unsupported strkey")),
};
Ok(Address(sc))
}
}
pub fn encode_scvals<I, T>(values: I) -> Result<Vec<ScVal>, ScValError>
where
I: IntoIterator<Item = T>,
T: IntoScVal,
{
values.into_iter().map(|v| v.into_scval()).collect()
}
pub fn scval_to_base64(v: &ScVal) -> Result<String, ScValError> {
use base64::Engine;
let mut buf = Vec::new();
use stellar_xdr::WriteXdr;
let mut limited = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
v.write_xdr(&mut limited).map_err(|_| ScValError::TooLong)?;
Ok(base64::engine::general_purpose::STANDARD.encode(&buf))
}
pub fn scval_from_base64(raw: &str) -> Option<ScVal> {
use base64::Engine;
let buf = base64::engine::general_purpose::STANDARD
.decode(raw.trim())
.ok()?;
let mut cursor = std::io::Cursor::new(buf);
let mut limited = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
stellar_xdr::ScVal::read_xdr(&mut limited).ok()
}
pub fn estimate_xdr_size<T: stellar_xdr::WriteXdr>(val: &T) -> Result<usize, ScValError> {
let mut buf = Vec::new();
let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
val.write_xdr(&mut l).map_err(|_| ScValError::TooLong)?;
Ok(buf.len())
}
pub fn decode_scvals<T, I>(vals: I) -> Result<Vec<T>, ScValError>
where
I: IntoIterator<Item = ScVal>,
T: FromScVal,
{
vals.into_iter().map(|v| T::from_scval(&v)).collect()
}
pub fn decode_scvals_ref<T, I>(vals: I) -> Result<Vec<T>, ScValError>
where
I: IntoIterator<Item = ScVal>,
T: FromScVal,
{
decode_scvals(vals)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bool_roundtrip() {
let sv = true.into_scval().unwrap();
assert!(bool::from_scval(&sv).unwrap());
}
#[test]
fn integer_roundtrip() {
assert_eq!(42u32.into_scval().unwrap(), ScVal::U32(42));
assert_eq!((-7i32).into_scval().unwrap(), ScVal::I32(-7));
assert_eq!(u32::MAX.into_scval().unwrap(), ScVal::U32(u32::MAX));
assert_eq!(u32::MIN.into_scval().unwrap(), ScVal::U32(u32::MIN));
assert_eq!(i32::MAX.into_scval().unwrap(), ScVal::I32(i32::MAX));
assert_eq!(i32::MIN.into_scval().unwrap(), ScVal::I32(i32::MIN));
assert_eq!(
9_000_000_000u64.into_scval().unwrap(),
ScVal::U64(9_000_000_000)
);
assert_eq!((-5i64).into_scval().unwrap(), ScVal::I64(-5));
let u = 0x_0123_4567_89ab_cdef_0000_0000_0000_0001u128;
let sv = u.into_scval().unwrap();
assert_eq!(u128::from_scval(&sv).unwrap(), u);
let i = -(0x_0123_4567_89ab_cdef_0000_0000_0000_0001i128);
let sv = i.into_scval().unwrap();
assert_eq!(i128::from_scval(&sv).unwrap(), i);
}
#[test]
fn timepoint_and_duration_roundtrip_as_distinct_scvals() {
for value in [0, 1, u64::MAX] {
let timepoint = TimePoint(value).into_scval().unwrap();
assert_eq!(timepoint, ScVal::Timepoint(TimePoint(value)));
assert_eq!(TimePoint::from_scval(&timepoint).unwrap(), TimePoint(value));
let duration = Duration(value).into_scval().unwrap();
assert_eq!(duration, ScVal::Duration(Duration(value)));
assert_eq!(Duration::from_scval(&duration).unwrap(), Duration(value));
for encoded in [
scval_to_base64(&timepoint).unwrap(),
scval_to_base64(&duration).unwrap(),
] {
assert!(scval_from_base64(&encoded).is_some());
}
}
}
#[test]
fn string_roundtrip() {
let sv = "hello".into_scval().unwrap();
assert_eq!(String::from_scval(&sv).unwrap(), "hello");
let own = String::from("world");
let sv = own.into_scval().unwrap();
assert_eq!(String::from_scval(&sv).unwrap(), "world");
}
#[test]
fn bytes_roundtrip() {
let data = vec![1u8, 2, 3, 255];
let sv = data.clone().into_scval().unwrap();
assert_eq!(Vec::<u8>::from_scval(&sv).unwrap(), data);
let arr = [7u8; 32];
let sv = arr.into_scval().unwrap();
assert_eq!(<[u8; 32]>::from_scval(&sv).unwrap(), arr);
}
#[test]
fn invalid_typed_value() {
let sv = ScVal::I32(5);
assert_eq!(u32::from_scval(&sv), Err(ScValError::TypeMismatch("u32")));
}
#[test]
fn encode_decode_batch() {
let scs = encode_scvals([1u32, 2, 3]).unwrap();
assert_eq!(scs.len(), 3);
let back = decode_scvals::<u32, _>(scs).unwrap();
assert_eq!(back, vec![1, 2, 3]);
}
#[test]
fn json_arguments_encode_nested_values() {
let encoded = json_args_to_base64(r#"[{"name":"alice","amount":100},null,true]"#).unwrap();
assert_eq!(encoded.len(), 3);
assert!(matches!(scval_from_base64(&encoded[1]), Some(ScVal::Void)));
assert!(matches!(
scval_from_base64(&encoded[2]),
Some(ScVal::Bool(true))
));
assert!(matches!(
scval_from_base64(&encoded[0]),
Some(ScVal::Map(_))
));
}
#[test]
fn json_arguments_reject_non_array_and_invalid_json() {
assert!(json_args_to_base64(r#"{"value":1}"#).is_err());
assert!(json_args_to_base64("not-json")
.unwrap_err()
.to_string()
.contains("not-json"));
}
#[test]
fn address_from_strkey() {
let addr = Address::from_strkey("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
.unwrap();
assert!(matches!(addr.0, ScAddress::Account(_)));
let addr2 =
Address::from_strkey("CAAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQC526")
.unwrap();
assert!(matches!(addr2.0, ScAddress::Contract(_)));
}
}