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