Skip to main content

tx3_resolver/
interop.rs

1use base64::Engine as _;
2use serde::{Deserialize, Serialize};
3use serde_json::{Map, Number, Value};
4use thiserror::Error;
5
6use tx3_tir::model::assets::{AssetClass, CanonicalAssets};
7use tx3_tir::model::core::{Type, Utxo, UtxoRef};
8pub use tx3_tir::reduce::ArgValue;
9
10#[derive(Debug, Error)]
11pub enum Error {
12    #[error("invalid base64: {0}")]
13    InvalidBase64(#[from] base64::DecodeError),
14
15    #[error("invalid hex: {0}")]
16    InvalidHex(#[from] hex::FromHexError),
17
18    #[error("invalid bech32: {0}")]
19    InvalidBech32(#[from] bech32::DecodeError),
20
21    #[error("value is not a valid number: {0}")]
22    InvalidBytesForNumber(String),
23
24    #[error("value is null")]
25    ValueIsNull,
26
27    #[error("can't infer type for value: {0}")]
28    CantInferTypeForValue(Value),
29
30    #[error("value is not a number: {0}")]
31    ValueIsNotANumber(Value),
32
33    #[error("value can't fit: {0}")]
34    NumberCantFit(Number),
35
36    #[error("value is not a bool: {0}")]
37    ValueIsNotABool(Value),
38
39    #[error("value is not a string")]
40    ValueIsNotAString,
41
42    #[error("value is not bytes: {0}")]
43    ValueIsNotBytes(Value),
44
45    #[error("value is not a utxo ref: {0}")]
46    ValueIsNotUtxoRef(Value),
47
48    #[error("invalid bytes envelope: {0}")]
49    InvalidBytesEnvelope(serde_json::Error),
50
51    #[error("value is not an address: {0}")]
52    ValueIsNotAnAddress(Value),
53
54    #[error("invalid utxo ref: {0}")]
55    InvalidUtxoRef(String),
56
57    #[error("target type not supported: {0:?}")]
58    TargetTypeNotSupported(Type),
59
60    #[error("unknown tag: {0}")]
61    UnknownTag(String),
62
63    #[error("malformed tagged value (expected a single-key object): {0}")]
64    MalformedTaggedArg(Value),
65
66    #[error("malformed tagged map pair (expected a [key, value] array): {0}")]
67    MalformedMapPair(Value),
68
69    #[error("malformed tagged struct (expected {{ constructor, fields }}): {0}")]
70    MalformedStruct(Value),
71}
72
73#[derive(Debug, Deserialize, Serialize, Clone)]
74#[serde(rename_all = "lowercase")]
75pub enum BytesEncoding {
76    Base64,
77    Hex,
78}
79
80#[derive(Debug, Deserialize, Serialize, Clone)]
81pub struct BytesEnvelope {
82    // Aliases for backward compatibility
83    #[serde(alias = "bytecode", alias = "payload")]
84    pub content: String,
85    #[serde(rename = "contentType", alias = "encoding")]
86    pub content_type: BytesEncoding,
87}
88
89/// Envelope for serialized TIR (Transaction Intermediate Representation) bytes.
90/// Used for serialization/deserialization of TIR across dump mechanism, TRP, etc.
91#[derive(Debug, Deserialize, Serialize, Clone)]
92pub struct TirEnvelope {
93    // Aliases for backward compatibility
94    #[serde(alias = "bytecode", alias = "payload")]
95    pub content: String,
96    pub encoding: BytesEncoding,
97    pub version: String,
98}
99
100impl BytesEnvelope {
101    pub fn from_hex(hex: &str) -> Result<Self, Error> {
102        Ok(Self {
103            content: hex.to_string(),
104            content_type: BytesEncoding::Hex,
105        })
106    }
107}
108
109impl From<BytesEnvelope> for Vec<u8> {
110    fn from(envelope: BytesEnvelope) -> Self {
111        match envelope.content_type {
112            BytesEncoding::Base64 => base64_to_bytes(&envelope.content).unwrap(),
113            BytesEncoding::Hex => hex_to_bytes(&envelope.content).unwrap(),
114        }
115    }
116}
117
118impl From<TirEnvelope> for Vec<u8> {
119    fn from(envelope: TirEnvelope) -> Self {
120        match envelope.encoding {
121            BytesEncoding::Base64 => base64_to_bytes(&envelope.content).unwrap(),
122            BytesEncoding::Hex => hex_to_bytes(&envelope.content).unwrap(),
123        }
124    }
125}
126
127impl TryFrom<TirEnvelope> for tx3_tir::encoding::AnyTir {
128    type Error = crate::Error;
129
130    fn try_from(envelope: TirEnvelope) -> Result<Self, Self::Error> {
131        let version = tx3_tir::encoding::TirVersion::try_from(envelope.version.as_str())?;
132        let bytes: Vec<u8> = envelope.into();
133        let tir = tx3_tir::encoding::from_bytes(&bytes, version)?;
134        Ok(tir)
135    }
136}
137
138impl From<tx3_tir::encoding::AnyTir> for TirEnvelope {
139    fn from(tir: tx3_tir::encoding::AnyTir) -> Self {
140        let (bytes, version) = match tir {
141            tx3_tir::encoding::AnyTir::V1Beta0(tx) => tx3_tir::encoding::to_bytes(&tx),
142        };
143        Self {
144            content: hex::encode(bytes),
145            encoding: BytesEncoding::Hex,
146            version: version.to_string(),
147        }
148    }
149}
150
151fn has_hex_prefix(s: &str) -> bool {
152    s.starts_with("0x")
153}
154
155pub fn string_to_bigint(s: String) -> Result<i128, Error> {
156    if has_hex_prefix(&s) {
157        let bytes = hex_to_bytes(&s)?;
158        let bytes = <[u8; 16]>::try_from(bytes)
159            .map_err(|x| Error::InvalidBytesForNumber(hex::encode(x)))?;
160        Ok(i128::from_be_bytes(bytes))
161    } else {
162        let i = s
163            .parse::<i128>()
164            .map_err(|x| Error::InvalidBytesForNumber(x.to_string()))?;
165        Ok(i)
166    }
167}
168
169pub fn hex_to_bytes(s: &str) -> Result<Vec<u8>, Error> {
170    let s = if has_hex_prefix(s) {
171        s.trim_start_matches("0x")
172    } else {
173        s
174    };
175
176    let out = hex::decode(s)?;
177
178    Ok(out)
179}
180
181pub fn base64_to_bytes(s: &str) -> Result<Vec<u8>, Error> {
182    let out = base64::engine::general_purpose::STANDARD.decode(s)?;
183
184    Ok(out)
185}
186
187pub fn bech32_to_bytes(s: &str) -> Result<Vec<u8>, Error> {
188    let (_, data) = bech32::decode(s)?;
189
190    Ok(data)
191}
192
193fn number_to_bigint(x: Number) -> Result<i128, Error> {
194    x.as_i128().ok_or(Error::NumberCantFit(x))
195}
196
197fn value_to_bigint(value: Value) -> Result<i128, Error> {
198    let out = match value {
199        Value::Number(n) => number_to_bigint(n)?,
200        Value::String(s) => string_to_bigint(s)?,
201        Value::Null => return Err(Error::ValueIsNull),
202        x => return Err(Error::ValueIsNotANumber(x)),
203    };
204
205    Ok(out)
206}
207
208fn value_to_bool(value: Value) -> Result<bool, Error> {
209    match value {
210        Value::Bool(b) => Ok(b),
211        Value::Number(n) if n == Number::from(0) => Ok(false),
212        Value::Number(n) if n == Number::from(1) => Ok(true),
213        Value::String(s) if s == "true" => Ok(true),
214        Value::String(s) if s == "false" => Ok(false),
215        x => Err(Error::ValueIsNotABool(x)),
216    }
217}
218
219fn value_to_bytes(value: Value) -> Result<Vec<u8>, Error> {
220    let out = match value {
221        Value::String(s) => hex_to_bytes(&s)?,
222        Value::Object(_) => {
223            let envelope: BytesEnvelope =
224                serde_json::from_value(value).map_err(Error::InvalidBytesEnvelope)?;
225
226            match envelope.content_type {
227                BytesEncoding::Base64 => base64_to_bytes(&envelope.content)?,
228                BytesEncoding::Hex => hex_to_bytes(&envelope.content)?,
229            }
230        }
231        x => return Err(Error::ValueIsNotBytes(x)),
232    };
233
234    Ok(out)
235}
236
237fn value_to_address(value: Value) -> Result<Vec<u8>, Error> {
238    let out = match value {
239        Value::String(s) => match bech32_to_bytes(&s) {
240            Ok(data) => data,
241            Err(_) => hex_to_bytes(&s)?,
242        },
243        x => return Err(Error::ValueIsNotAnAddress(x)),
244    };
245
246    Ok(out)
247}
248
249fn value_to_underfined(value: Value) -> Result<ArgValue, Error> {
250    match value {
251        Value::Bool(b) => Ok(ArgValue::Bool(b)),
252        Value::Number(x) => Ok(ArgValue::Int(number_to_bigint(x)?)),
253        Value::String(s) => Ok(ArgValue::String(s)),
254        x => Err(Error::CantInferTypeForValue(x)),
255    }
256}
257
258pub fn string_to_utxo_ref(s: &str) -> Result<UtxoRef, Error> {
259    let (txid, index) = s
260        .split_once('#')
261        .ok_or(Error::InvalidUtxoRef(s.to_string()))?;
262
263    let txid = hex::decode(txid).map_err(|_| Error::InvalidUtxoRef(s.to_string()))?;
264    let index = index
265        .parse()
266        .map_err(|_| Error::InvalidUtxoRef(s.to_string()))?;
267
268    Ok(UtxoRef { txid, index })
269}
270
271fn value_to_utxo_ref(value: Value) -> Result<UtxoRef, Error> {
272    match value {
273        Value::String(s) => string_to_utxo_ref(&s),
274        x => Err(Error::ValueIsNotUtxoRef(x)),
275    }
276}
277
278/// Returns the single `(key, value)` of a one-key object, else `None`.
279fn single_key(value: &Value) -> Option<(&str, &Value)> {
280    let obj = value.as_object()?;
281    if obj.len() != 1 {
282        return None;
283    }
284    obj.iter().next().map(|(k, v)| (k.as_str(), v))
285}
286
287/// Whether `value` is a tagged node: a single-key object whose key
288/// is one of the wire tags. A tagged value is self-describing and decodes the
289/// same way whatever its kind; anything else is a bare (legacy) scalar.
290fn is_tagged(value: &Value) -> bool {
291    matches!(
292        single_key(value),
293        Some((
294            "int"
295                | "bool"
296                | "string"
297                | "bytes"
298                | "address"
299                | "utxoRef"
300                | "list"
301                | "tuple"
302                | "map"
303                | "struct",
304            _,
305        ))
306    )
307}
308
309/// Binds a JSON argument value to its [`ArgValue`].
310///
311/// One path for every argument: a tagged value decodes from its tags via
312/// [`decode_tagged`] regardless of kind, while a bare value (no tag) is coerced
313/// via the param's flat TIR [`Type`] — the legacy form a top-level scalar may
314/// still take.
315pub fn from_json(value: Value, target: &Type) -> Result<ArgValue, Error> {
316    if is_tagged(&value) {
317        decode_tagged(value)
318    } else {
319        coerce_bare(value, target)
320    }
321}
322
323/// Coerces a bare (untagged) value via the param's flat [`Type`]. Only scalars
324/// may be sent bare; an aggregate must arrive tagged.
325fn coerce_bare(value: Value, target: &Type) -> Result<ArgValue, Error> {
326    match target {
327        Type::Int => Ok(ArgValue::Int(value_to_bigint(value)?)),
328        Type::Bool => Ok(ArgValue::Bool(value_to_bool(value)?)),
329        Type::Bytes => Ok(ArgValue::Bytes(value_to_bytes(value)?)),
330        Type::Address => Ok(ArgValue::Address(value_to_address(value)?)),
331        Type::UtxoRef => Ok(ArgValue::UtxoRef(value_to_utxo_ref(value)?)),
332        Type::Undefined => value_to_underfined(value),
333        x => Err(Error::TargetTypeNotSupported(x.clone())),
334    }
335}
336
337/// Decodes a tagged JSON value into an [`ArgValue`], recursively.
338/// Every node carries its own tag and struct fields are positional, so no schema
339/// is needed; scalar leaves reuse the per-scalar coercions.
340fn decode_tagged(value: Value) -> Result<ArgValue, Error> {
341    let Some((tag, _)) = single_key(&value) else {
342        return Err(Error::MalformedTaggedArg(value));
343    };
344    let tag = tag.to_string();
345    // Take ownership of the inner value out of the one-key object.
346    let inner = match value {
347        Value::Object(mut m) => m.remove(&tag).expect("single key present"),
348        _ => unreachable!("single_key guaranteed an object"),
349    };
350
351    match tag.as_str() {
352        "int" => Ok(ArgValue::Int(value_to_bigint(inner)?)),
353        "bool" => Ok(ArgValue::Bool(value_to_bool(inner)?)),
354        "string" => match inner {
355            Value::String(s) => Ok(ArgValue::String(s)),
356            _ => Err(Error::ValueIsNotAString),
357        },
358        "bytes" => Ok(ArgValue::Bytes(value_to_bytes(inner)?)),
359        "address" => Ok(ArgValue::Address(value_to_address(inner)?)),
360        "utxoRef" => Ok(ArgValue::UtxoRef(value_to_utxo_ref(inner)?)),
361        "list" => Ok(ArgValue::List(decode_seq(inner)?)),
362        "tuple" => Ok(ArgValue::Tuple(decode_seq(inner)?)),
363        "map" => Ok(ArgValue::Map(decode_map(inner)?)),
364        "struct" => decode_struct(inner),
365        other => Err(Error::UnknownTag(other.to_string())),
366    }
367}
368
369/// Decodes an array of tagged values (the body of a `list`/`tuple`/struct fields).
370fn decode_seq(value: Value) -> Result<Vec<ArgValue>, Error> {
371    match value {
372        Value::Array(items) => items.into_iter().map(decode_tagged).collect(),
373        x => Err(Error::MalformedTaggedArg(x)),
374    }
375}
376
377/// Decodes a `map` body: an array of `[key, value]` tagged pairs.
378fn decode_map(value: Value) -> Result<Vec<(ArgValue, ArgValue)>, Error> {
379    let arr = match value {
380        Value::Array(a) => a,
381        x => return Err(Error::MalformedTaggedArg(x)),
382    };
383
384    arr.into_iter()
385        .map(|pair| match pair {
386            Value::Array(mut kv) if kv.len() == 2 => {
387                let v = kv.pop().unwrap();
388                let k = kv.pop().unwrap();
389                Ok((decode_tagged(k)?, decode_tagged(v)?))
390            }
391            x => Err(Error::MalformedMapPair(x)),
392        })
393        .collect()
394}
395
396/// Decodes a `struct` body: `{ "constructor": <usize>, "fields": [TaggedArg, …] }`.
397fn decode_struct(value: Value) -> Result<ArgValue, Error> {
398    let mut obj = match value {
399        Value::Object(o) => o,
400        x => return Err(Error::MalformedStruct(x)),
401    };
402
403    let constructor =
404        obj.get("constructor")
405            .and_then(Value::as_u64)
406            .ok_or_else(|| Error::MalformedStruct(Value::Object(obj.clone())))? as usize;
407
408    let fields = obj
409        .remove("fields")
410        .ok_or_else(|| Error::MalformedStruct(Value::Object(obj.clone())))?;
411
412    Ok(ArgValue::Struct {
413        constructor,
414        fields: decode_seq(fields)?,
415    })
416}
417
418// ---------------------------------------------------------------------------
419// Rust → JSON marshalling
420// ---------------------------------------------------------------------------
421
422pub fn utxo_ref_to_json(r: &UtxoRef) -> Value {
423    Value::String(format!("{}#{}", hex::encode(&r.txid), r.index))
424}
425
426pub fn arg_to_json(arg: &ArgValue) -> Value {
427    match arg {
428        ArgValue::Int(i) => serde_json::json!(i),
429        ArgValue::Bool(b) => Value::Bool(*b),
430        ArgValue::String(s) => Value::String(s.clone()),
431        ArgValue::Bytes(v) => Value::String(hex::encode(v)),
432        ArgValue::Address(v) => Value::String(hex::encode(v)),
433        ArgValue::UtxoRef(r) => utxo_ref_to_json(r),
434        ArgValue::UtxoSet(_) => Value::Null,
435        // Aggregates round-trip through the tagged form (re-decodable by
436        // `decode_tagged`); scalars stay bare above.
437        ArgValue::List(_) | ArgValue::Tuple(_) | ArgValue::Map(_) | ArgValue::Struct { .. } => {
438            tagged_arg_to_json(arg)
439        }
440    }
441}
442
443/// Serializes any [`ArgValue`] into its fully-tagged JSON form — the
444/// inverse of [`decode_tagged`], with every leaf tagged.
445pub fn tagged_arg_to_json(arg: &ArgValue) -> Value {
446    match arg {
447        ArgValue::Int(i) => serde_json::json!({ "int": i }),
448        ArgValue::Bool(b) => serde_json::json!({ "bool": b }),
449        ArgValue::String(s) => serde_json::json!({ "string": s }),
450        ArgValue::Bytes(v) => serde_json::json!({ "bytes": hex::encode(v) }),
451        ArgValue::Address(v) => serde_json::json!({ "address": hex::encode(v) }),
452        ArgValue::UtxoRef(r) => {
453            serde_json::json!({ "utxoRef": format!("{}#{}", hex::encode(&r.txid), r.index) })
454        }
455        // A resolved UTxO set is not a tagged leaf; surface it as null.
456        ArgValue::UtxoSet(_) => Value::Null,
457        ArgValue::List(xs) => {
458            serde_json::json!({ "list": xs.iter().map(tagged_arg_to_json).collect::<Vec<_>>() })
459        }
460        ArgValue::Tuple(xs) => {
461            serde_json::json!({ "tuple": xs.iter().map(tagged_arg_to_json).collect::<Vec<_>>() })
462        }
463        ArgValue::Map(pairs) => serde_json::json!({
464            "map": pairs
465                .iter()
466                .map(|(k, v)| vec![tagged_arg_to_json(k), tagged_arg_to_json(v)])
467                .collect::<Vec<_>>()
468        }),
469        ArgValue::Struct {
470            constructor,
471            fields,
472        } => serde_json::json!({
473            "struct": {
474                "constructor": constructor,
475                "fields": fields.iter().map(tagged_arg_to_json).collect::<Vec<_>>(),
476            }
477        }),
478    }
479}
480
481pub fn utxo_to_json(utxo: &Utxo) -> Value {
482    let assets: Map<String, Value> = utxo
483        .assets
484        .iter()
485        .map(|(class, amount)| (class.to_string(), serde_json::json!(amount)))
486        .collect();
487
488    serde_json::json!({
489        "ref": utxo_ref_to_json(&utxo.r#ref),
490        "address": hex::encode(&utxo.address),
491        "assets": assets,
492        "datum": utxo.datum,
493        "script": utxo.script,
494    })
495}
496
497fn parse_asset_class(key: &str) -> AssetClass {
498    if key == "naked" {
499        AssetClass::Naked
500    } else if let Some((policy, name)) = key.split_once('.') {
501        let policy_bytes = hex::decode(policy).unwrap_or_default();
502        let name_bytes = hex::decode(name).unwrap_or_default();
503        AssetClass::Defined(policy_bytes, name_bytes)
504    } else {
505        let name_bytes = hex::decode(key).unwrap_or_default();
506        AssetClass::Named(name_bytes)
507    }
508}
509
510fn assets_from_json(value: &Value) -> Result<CanonicalAssets, Error> {
511    let obj = value.as_object().ok_or(Error::ValueIsNotAString)?;
512
513    let mut assets = CanonicalAssets::empty();
514    for (key, amount_val) in obj {
515        let class = parse_asset_class(key);
516        let amount = value_to_bigint(amount_val.clone())?;
517        assets = assets + CanonicalAssets::from_class_and_amount(class, amount);
518    }
519
520    Ok(assets)
521}
522
523pub fn utxo_from_json(value: &Value) -> Result<Utxo, Error> {
524    let ref_str = value["ref"].as_str().ok_or(Error::ValueIsNotAString)?;
525    let utxo_ref = string_to_utxo_ref(ref_str)?;
526
527    let address = hex_to_bytes(value["address"].as_str().ok_or(Error::ValueIsNotAString)?)?;
528
529    let assets = assets_from_json(&value["assets"])?;
530
531    let datum = value
532        .get("datum")
533        .filter(|v| !v.is_null())
534        .map(|v| serde_json::from_value(v.clone()))
535        .transpose()
536        .map_err(Error::InvalidBytesEnvelope)?;
537
538    let script = value
539        .get("script")
540        .filter(|v| !v.is_null())
541        .map(|v| serde_json::from_value(v.clone()))
542        .transpose()
543        .map_err(Error::InvalidBytesEnvelope)?;
544
545    Ok(Utxo {
546        r#ref: utxo_ref,
547        address,
548        assets,
549        datum,
550        script,
551    })
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557    use serde_json::json;
558
559    fn assert_from_json(provided: Value, target: Type, expected: ArgValue) {
560        let value = from_json(provided, &target).unwrap();
561        assert_eq!(value, expected);
562    }
563
564    // -----------------------------------------------------------------------
565    // JSON → Rust (from_json)
566    // -----------------------------------------------------------------------
567
568    #[test]
569    fn from_json_small_int() {
570        assert_from_json(json!(123456789), Type::Int, ArgValue::Int(123456789));
571    }
572
573    #[test]
574    fn from_json_negative_int() {
575        assert_from_json(json!(-123456789), Type::Int, ArgValue::Int(-123456789));
576    }
577
578    #[test]
579    fn from_json_big_int() {
580        assert_from_json(
581            json!("12345678901234567890"),
582            Type::Int,
583            ArgValue::Int(12345678901234567890),
584        );
585    }
586
587    #[test]
588    fn from_json_int_overflow() {
589        assert_from_json(
590            json!(i128::MIN.to_string()),
591            Type::Int,
592            ArgValue::Int(i128::MIN),
593        );
594        assert_from_json(
595            json!(i128::MAX.to_string()),
596            Type::Int,
597            ArgValue::Int(i128::MAX),
598        );
599    }
600
601    #[test]
602    fn from_json_bool() {
603        assert_from_json(json!(true), Type::Bool, ArgValue::Bool(true));
604        assert_from_json(json!(false), Type::Bool, ArgValue::Bool(false));
605    }
606
607    #[test]
608    fn from_json_bool_number() {
609        assert_from_json(json!(1), Type::Bool, ArgValue::Bool(true));
610        assert_from_json(json!(0), Type::Bool, ArgValue::Bool(false));
611    }
612
613    #[test]
614    fn from_json_bool_string() {
615        assert_from_json(json!("true"), Type::Bool, ArgValue::Bool(true));
616        assert_from_json(json!("false"), Type::Bool, ArgValue::Bool(false));
617    }
618
619    #[test]
620    fn from_json_bytes() {
621        assert_from_json(
622            json!(hex::encode("hello")),
623            Type::Bytes,
624            ArgValue::Bytes(b"hello".to_vec()),
625        );
626
627        assert_from_json(
628            json!(format!("0x{}", hex::encode("hello"))),
629            Type::Bytes,
630            ArgValue::Bytes(b"hello".to_vec()),
631        );
632    }
633
634    #[test]
635    fn from_json_bytes_base64() {
636        let json = json!(BytesEnvelope {
637            content: "aGVsbG8=".to_string(),
638            content_type: BytesEncoding::Base64,
639        });
640
641        assert_from_json(json, Type::Bytes, ArgValue::Bytes(b"hello".to_vec()));
642    }
643
644    #[test]
645    fn from_json_bytes_hex() {
646        let json = json!(BytesEnvelope {
647            content: "68656c6c6f".to_string(),
648            content_type: BytesEncoding::Hex,
649        });
650
651        assert_from_json(json, Type::Bytes, ArgValue::Bytes(b"hello".to_vec()));
652    }
653
654    #[test]
655    fn from_json_address() {
656        assert_from_json(
657            json!(hex::encode("abc123")),
658            Type::Address,
659            ArgValue::Address(b"abc123".to_vec()),
660        );
661    }
662
663    #[test]
664    fn from_json_address_bech32() {
665        let json = json!("addr1vx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzers66hrl8");
666        let bytes =
667            hex::decode("619493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e").unwrap();
668        assert_from_json(json, Type::Address, ArgValue::Address(bytes));
669    }
670
671    #[test]
672    fn from_json_utxo_ref() {
673        let json = json!("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef#0");
674
675        let utxo_ref = UtxoRef {
676            txid: hex::decode("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
677                .unwrap(),
678            index: 0,
679        };
680
681        assert_from_json(json, Type::UtxoRef, ArgValue::UtxoRef(utxo_ref));
682    }
683
684    #[test]
685    fn from_json_undefined_infers_from_shape() {
686        // The untyped fallback: a bare value binds by its JSON shape.
687        assert_from_json(json!(true), Type::Undefined, ArgValue::Bool(true));
688        assert_from_json(json!(42), Type::Undefined, ArgValue::Int(42));
689        assert_from_json(json!("hi"), Type::Undefined, ArgValue::String("hi".into()));
690    }
691
692    #[test]
693    fn bytes_envelope_object_is_not_mistaken_for_a_tag() {
694        // A two-key BytesEnvelope (the only object-shaped legacy scalar input)
695        // must route to bare coercion, never to the tagged decoder.
696        let envelope = json!({ "content": "aGVsbG8=", "contentType": "base64" });
697        assert_from_json(envelope, Type::Bytes, ArgValue::Bytes(b"hello".to_vec()));
698    }
699
700    #[test]
701    fn decode_list_of_int() {
702        assert_from_json(
703            json!({ "list": [{ "int": 1 }, { "int": 2 }, { "int": 3 }] }),
704            Type::List,
705            ArgValue::List(vec![ArgValue::Int(1), ArgValue::Int(2), ArgValue::Int(3)]),
706        );
707    }
708
709    #[test]
710    fn decode_tuple_int_bytes() {
711        assert_from_json(
712            json!({ "tuple": [{ "int": 42 }, { "bytes": "cafe" }] }),
713            Type::Tuple,
714            ArgValue::Tuple(vec![ArgValue::Int(42), ArgValue::Bytes(vec![0xca, 0xfe])]),
715        );
716    }
717
718    #[test]
719    fn decode_map_string_keys() {
720        assert_from_json(
721            json!({ "map": [[{ "string": "1" }, { "int": 100 }], [{ "string": "2" }, { "int": 200 }]] }),
722            Type::Map,
723            ArgValue::Map(vec![
724                (ArgValue::String("1".into()), ArgValue::Int(100)),
725                (ArgValue::String("2".into()), ArgValue::Int(200)),
726            ]),
727        );
728    }
729
730    #[test]
731    fn decode_struct_record() {
732        // AssetClass { policy: Bytes, name: Bytes } in declared order.
733        assert_from_json(
734            json!({ "struct": { "constructor": 0, "fields": [{ "bytes": "aabb" }, { "bytes": "0011" }] } }),
735            Type::Custom("AssetClass".into()),
736            ArgValue::Struct {
737                constructor: 0,
738                fields: vec![
739                    ArgValue::Bytes(vec![0xaa, 0xbb]),
740                    ArgValue::Bytes(vec![0x00, 0x11]),
741                ],
742            },
743        );
744    }
745
746    #[test]
747    fn decode_nested_struct_05_invoke() {
748        // The journey-critical shape: Meta { tags: List<Int>, level: Int }.
749        assert_from_json(
750            json!({
751                "struct": {
752                    "constructor": 0,
753                    "fields": [
754                        { "list": [{ "int": 1 }, { "int": 2 }, { "int": 3 }] },
755                        { "int": 7 }
756                    ]
757                }
758            }),
759            Type::Custom("Meta".into()),
760            ArgValue::Struct {
761                constructor: 0,
762                fields: vec![
763                    ArgValue::List(vec![ArgValue::Int(1), ArgValue::Int(2), ArgValue::Int(3)]),
764                    ArgValue::Int(7),
765                ],
766            },
767        );
768    }
769
770    #[test]
771    fn decode_accepts_tagged_scalar_leniently() {
772        assert_from_json(json!({ "int": 5 }), Type::Int, ArgValue::Int(5));
773        assert_from_json(
774            json!({ "bytes": "cafe" }),
775            Type::Bytes,
776            ArgValue::Bytes(vec![0xca, 0xfe]),
777        );
778    }
779
780    #[test]
781    fn decode_round_trips_through_encoded_json() {
782        let encoded = json!({
783            "struct": {
784                "constructor": 0,
785                "fields": [{ "list": [{ "int": 1 }, { "int": 2 }] }, { "int": 7 }]
786            }
787        });
788        let arg = from_json(encoded.clone(), &Type::Custom("Meta".into())).unwrap();
789        assert_eq!(tagged_arg_to_json(&arg), encoded);
790    }
791
792    #[test]
793    fn decode_rejects_unknown_tag() {
794        // A single-key object whose key is a known tag but unknown nested tag.
795        let err = from_json(json!({ "list": [{ "bogus": 1 }] }), &Type::List).unwrap_err();
796        assert!(matches!(err, Error::UnknownTag(_)), "got {err:?}");
797    }
798
799    #[test]
800    fn bare_aggregate_is_rejected() {
801        // A bare array carries no tag, so it can't bind to an aggregate param.
802        let err = from_json(json!([1, 2, 3]), &Type::List).unwrap_err();
803        assert!(
804            matches!(err, Error::TargetTypeNotSupported(_)),
805            "got {err:?}"
806        );
807    }
808
809    #[test]
810    fn decode_rejects_malformed_struct() {
811        // Missing `fields`.
812        let err = from_json(
813            json!({ "struct": { "constructor": 0 } }),
814            &Type::Custom("Meta".into()),
815        )
816        .unwrap_err();
817        assert!(matches!(err, Error::MalformedStruct(_)), "got {err:?}");
818    }
819
820    #[test]
821    fn decode_rejects_malformed_map_pair() {
822        let err = from_json(json!({ "map": [[{ "int": 1 }]] }), &Type::Map).unwrap_err();
823        assert!(matches!(err, Error::MalformedMapPair(_)), "got {err:?}");
824    }
825
826    #[test]
827    fn decode_rejects_untagged_nested_value() {
828        // A bare element inside a tagged list has no tag to decode by.
829        let err = from_json(json!({ "list": [5] }), &Type::List).unwrap_err();
830        assert!(matches!(err, Error::MalformedTaggedArg(_)), "got {err:?}");
831    }
832
833    // -----------------------------------------------------------------------
834    // Rust → JSON (to_json)
835    // -----------------------------------------------------------------------
836
837    use tx3_tir::model::assets::{AssetClass, CanonicalAssets};
838    use tx3_tir::model::core::Utxo;
839
840    fn sample_utxo_ref() -> UtxoRef {
841        UtxoRef {
842            txid: hex::decode("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
843                .unwrap(),
844            index: 2,
845        }
846    }
847
848    #[test]
849    fn utxo_ref_to_json_format() {
850        let r = sample_utxo_ref();
851        let v = utxo_ref_to_json(&r);
852        assert_eq!(
853            v,
854            json!("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef#2")
855        );
856    }
857
858    #[test]
859    fn arg_to_json_int() {
860        assert_eq!(arg_to_json(&ArgValue::Int(42)), json!(42));
861    }
862
863    #[test]
864    fn arg_to_json_bool() {
865        assert_eq!(arg_to_json(&ArgValue::Bool(true)), json!(true));
866    }
867
868    #[test]
869    fn arg_to_json_string() {
870        assert_eq!(
871            arg_to_json(&ArgValue::String("hello".into())),
872            json!("hello")
873        );
874    }
875
876    #[test]
877    fn arg_to_json_bytes() {
878        assert_eq!(
879            arg_to_json(&ArgValue::Bytes(b"hello".to_vec())),
880            json!("68656c6c6f")
881        );
882    }
883
884    #[test]
885    fn arg_to_json_address() {
886        assert_eq!(
887            arg_to_json(&ArgValue::Address(b"\x01\x02".to_vec())),
888            json!("0102")
889        );
890    }
891
892    #[test]
893    fn arg_to_json_utxo_ref() {
894        let r = sample_utxo_ref();
895        assert_eq!(
896            arg_to_json(&ArgValue::UtxoRef(r.clone())),
897            utxo_ref_to_json(&r)
898        );
899    }
900
901    #[test]
902    fn utxo_to_json_empty_assets() {
903        let utxo = Utxo {
904            r#ref: sample_utxo_ref(),
905            address: b"\xab\xcd".to_vec(),
906            assets: CanonicalAssets::empty(),
907            datum: None,
908            script: None,
909        };
910
911        let v = utxo_to_json(&utxo);
912        assert_eq!(v["assets"], json!({}));
913        assert_eq!(v["address"], json!("abcd"));
914        assert!(v["datum"].is_null());
915        assert!(v["script"].is_null());
916    }
917
918    #[test]
919    fn utxo_to_json_naked_assets() {
920        let utxo = Utxo {
921            r#ref: sample_utxo_ref(),
922            address: b"\x01".to_vec(),
923            assets: CanonicalAssets::from_naked_amount(5_000_000),
924            datum: None,
925            script: None,
926        };
927
928        let v = utxo_to_json(&utxo);
929        assert_eq!(v["assets"]["naked"], json!(5_000_000));
930    }
931
932    #[test]
933    fn utxo_to_json_defined_assets() {
934        let assets = CanonicalAssets::from_class_and_amount(
935            AssetClass::Defined(b"policy1".to_vec(), b"token1".to_vec()),
936            100,
937        );
938
939        let utxo = Utxo {
940            r#ref: sample_utxo_ref(),
941            address: b"\x01".to_vec(),
942            assets,
943            datum: None,
944            script: None,
945        };
946
947        let v = utxo_to_json(&utxo);
948        let key = format!("{}.{}", hex::encode(b"policy1"), hex::encode(b"token1"));
949        assert_eq!(v["assets"][&key], json!(100));
950    }
951}