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
61#[derive(Debug, Deserialize, Serialize, Clone)]
62#[serde(rename_all = "lowercase")]
63pub enum BytesEncoding {
64    Base64,
65    Hex,
66}
67
68#[derive(Debug, Deserialize, Serialize, Clone)]
69pub struct BytesEnvelope {
70    // Aliases for backward compatibility
71    #[serde(alias = "bytecode", alias = "payload")]
72    pub content: String,
73    #[serde(rename = "contentType", alias = "encoding")]
74    pub content_type: BytesEncoding,
75}
76
77/// Envelope for serialized TIR (Transaction Intermediate Representation) bytes.
78/// Used for serialization/deserialization of TIR across dump mechanism, TRP, etc.
79#[derive(Debug, Deserialize, Serialize, Clone)]
80pub struct TirEnvelope {
81    // Aliases for backward compatibility
82    #[serde(alias = "bytecode", alias = "payload")]
83    pub content: String,
84    pub encoding: BytesEncoding,
85    pub version: String,
86}
87
88impl BytesEnvelope {
89    pub fn from_hex(hex: &str) -> Result<Self, Error> {
90        Ok(Self {
91            content: hex.to_string(),
92            content_type: BytesEncoding::Hex,
93        })
94    }
95}
96
97impl From<BytesEnvelope> for Vec<u8> {
98    fn from(envelope: BytesEnvelope) -> Self {
99        match envelope.content_type {
100            BytesEncoding::Base64 => base64_to_bytes(&envelope.content).unwrap(),
101            BytesEncoding::Hex => hex_to_bytes(&envelope.content).unwrap(),
102        }
103    }
104}
105
106impl From<TirEnvelope> for Vec<u8> {
107    fn from(envelope: TirEnvelope) -> Self {
108        match envelope.encoding {
109            BytesEncoding::Base64 => base64_to_bytes(&envelope.content).unwrap(),
110            BytesEncoding::Hex => hex_to_bytes(&envelope.content).unwrap(),
111        }
112    }
113}
114
115impl TryFrom<TirEnvelope> for tx3_tir::encoding::AnyTir {
116    type Error = crate::Error;
117
118    fn try_from(envelope: TirEnvelope) -> Result<Self, Self::Error> {
119        let version = tx3_tir::encoding::TirVersion::try_from(envelope.version.as_str())?;
120        let bytes: Vec<u8> = envelope.into();
121        let tir = tx3_tir::encoding::from_bytes(&bytes, version)?;
122        Ok(tir)
123    }
124}
125
126impl From<tx3_tir::encoding::AnyTir> for TirEnvelope {
127    fn from(tir: tx3_tir::encoding::AnyTir) -> Self {
128        let (bytes, version) = match tir {
129            tx3_tir::encoding::AnyTir::V1Beta0(tx) => tx3_tir::encoding::to_bytes(&tx),
130        };
131        Self {
132            content: hex::encode(bytes),
133            encoding: BytesEncoding::Hex,
134            version: version.to_string(),
135        }
136    }
137}
138
139fn has_hex_prefix(s: &str) -> bool {
140    s.starts_with("0x")
141}
142
143pub fn string_to_bigint(s: String) -> Result<i128, Error> {
144    if has_hex_prefix(&s) {
145        let bytes = hex_to_bytes(&s)?;
146        let bytes = <[u8; 16]>::try_from(bytes)
147            .map_err(|x| Error::InvalidBytesForNumber(hex::encode(x)))?;
148        Ok(i128::from_be_bytes(bytes))
149    } else {
150        let i = i128::from_str_radix(&s, 10)
151            .map_err(|x| Error::InvalidBytesForNumber(x.to_string()))?;
152        Ok(i)
153    }
154}
155
156pub fn hex_to_bytes(s: &str) -> Result<Vec<u8>, Error> {
157    let s = if has_hex_prefix(s) {
158        s.trim_start_matches("0x")
159    } else {
160        s
161    };
162
163    let out = hex::decode(s)?;
164
165    Ok(out)
166}
167
168pub fn base64_to_bytes(s: &str) -> Result<Vec<u8>, Error> {
169    let out = base64::engine::general_purpose::STANDARD.decode(s)?;
170
171    Ok(out)
172}
173
174pub fn bech32_to_bytes(s: &str) -> Result<Vec<u8>, Error> {
175    let (_, data) = bech32::decode(s)?;
176
177    Ok(data)
178}
179
180fn number_to_bigint(x: Number) -> Result<i128, Error> {
181    x.as_i128().ok_or(Error::NumberCantFit(x))
182}
183
184fn value_to_bigint(value: Value) -> Result<i128, Error> {
185    let out = match value {
186        Value::Number(n) => number_to_bigint(n)?,
187        Value::String(s) => string_to_bigint(s)?,
188        Value::Null => return Err(Error::ValueIsNull),
189        x => return Err(Error::ValueIsNotANumber(x)),
190    };
191
192    Ok(out)
193}
194
195fn value_to_bool(value: Value) -> Result<bool, Error> {
196    match value {
197        Value::Bool(b) => Ok(b),
198        Value::Number(n) if n == Number::from(0) => Ok(false),
199        Value::Number(n) if n == Number::from(1) => Ok(true),
200        Value::String(s) if s == "true" => Ok(true),
201        Value::String(s) if s == "false" => Ok(false),
202        x => Err(Error::ValueIsNotABool(x)),
203    }
204}
205
206fn value_to_bytes(value: Value) -> Result<Vec<u8>, Error> {
207    let out = match value {
208        Value::String(s) => hex_to_bytes(&s)?,
209        Value::Object(_) => {
210            let envelope: BytesEnvelope =
211                serde_json::from_value(value).map_err(Error::InvalidBytesEnvelope)?;
212
213            match envelope.content_type {
214                BytesEncoding::Base64 => base64_to_bytes(&envelope.content)?,
215                BytesEncoding::Hex => hex_to_bytes(&envelope.content)?,
216            }
217        }
218        x => return Err(Error::ValueIsNotBytes(x)),
219    };
220
221    Ok(out)
222}
223
224fn value_to_address(value: Value) -> Result<Vec<u8>, Error> {
225    let out = match value {
226        Value::String(s) => match bech32_to_bytes(&s) {
227            Ok(data) => data,
228            Err(_) => hex_to_bytes(&s)?,
229        },
230        x => return Err(Error::ValueIsNotAnAddress(x)),
231    };
232
233    Ok(out)
234}
235
236fn value_to_underfined(value: Value) -> Result<ArgValue, Error> {
237    match value {
238        Value::Bool(b) => Ok(ArgValue::Bool(b)),
239        Value::Number(x) => Ok(ArgValue::Int(number_to_bigint(x)?)),
240        Value::String(s) => Ok(ArgValue::String(s)),
241        x => Err(Error::CantInferTypeForValue(x)),
242    }
243}
244
245pub fn string_to_utxo_ref(s: &str) -> Result<UtxoRef, Error> {
246    let (txid, index) = s
247        .split_once('#')
248        .ok_or(Error::InvalidUtxoRef(s.to_string()))?;
249
250    let txid = hex::decode(txid).map_err(|_| Error::InvalidUtxoRef(s.to_string()))?;
251    let index = index
252        .parse()
253        .map_err(|_| Error::InvalidUtxoRef(s.to_string()))?;
254
255    Ok(UtxoRef { txid, index })
256}
257
258fn value_to_utxo_ref(value: Value) -> Result<UtxoRef, Error> {
259    match value {
260        Value::String(s) => string_to_utxo_ref(&s),
261        x => Err(Error::ValueIsNotUtxoRef(x)),
262    }
263}
264
265pub fn from_json(value: Value, target: &Type) -> Result<ArgValue, Error> {
266    match target {
267        Type::Int => {
268            let i = value_to_bigint(value)?;
269            Ok(ArgValue::Int(i))
270        }
271        Type::Bool => {
272            let b = value_to_bool(value)?;
273            Ok(ArgValue::Bool(b))
274        }
275        Type::Bytes => {
276            let b = value_to_bytes(value)?;
277            Ok(ArgValue::Bytes(b))
278        }
279        Type::Address => {
280            let a = value_to_address(value)?;
281            Ok(ArgValue::Address(a))
282        }
283        Type::UtxoRef => {
284            let x = value_to_utxo_ref(value)?;
285            Ok(ArgValue::UtxoRef(x))
286        }
287        Type::Undefined => value_to_underfined(value),
288        x => Err(Error::TargetTypeNotSupported(x.clone())),
289    }
290}
291
292// ---------------------------------------------------------------------------
293// Rust → JSON marshalling
294// ---------------------------------------------------------------------------
295
296pub fn utxo_ref_to_json(r: &UtxoRef) -> Value {
297    Value::String(format!("{}#{}", hex::encode(&r.txid), r.index))
298}
299
300pub fn arg_to_json(arg: &ArgValue) -> Value {
301    match arg {
302        ArgValue::Int(i) => serde_json::json!(i),
303        ArgValue::Bool(b) => Value::Bool(*b),
304        ArgValue::String(s) => Value::String(s.clone()),
305        ArgValue::Bytes(v) => Value::String(hex::encode(v)),
306        ArgValue::Address(v) => Value::String(hex::encode(v)),
307        ArgValue::UtxoRef(r) => utxo_ref_to_json(r),
308        ArgValue::UtxoSet(_) => Value::Null,
309    }
310}
311
312pub fn utxo_to_json(utxo: &Utxo) -> Value {
313    let assets: Map<String, Value> = utxo
314        .assets
315        .iter()
316        .map(|(class, amount)| (class.to_string(), serde_json::json!(amount)))
317        .collect();
318
319    serde_json::json!({
320        "ref": utxo_ref_to_json(&utxo.r#ref),
321        "address": hex::encode(&utxo.address),
322        "assets": assets,
323        "datum": utxo.datum,
324        "script": utxo.script,
325    })
326}
327
328fn parse_asset_class(key: &str) -> AssetClass {
329    if key == "naked" {
330        AssetClass::Naked
331    } else if let Some((policy, name)) = key.split_once('.') {
332        let policy_bytes = hex::decode(policy).unwrap_or_default();
333        let name_bytes = hex::decode(name).unwrap_or_default();
334        AssetClass::Defined(policy_bytes, name_bytes)
335    } else {
336        let name_bytes = hex::decode(key).unwrap_or_default();
337        AssetClass::Named(name_bytes)
338    }
339}
340
341fn assets_from_json(value: &Value) -> Result<CanonicalAssets, Error> {
342    let obj = value.as_object().ok_or(Error::ValueIsNotAString)?;
343
344    let mut assets = CanonicalAssets::empty();
345    for (key, amount_val) in obj {
346        let class = parse_asset_class(key);
347        let amount = value_to_bigint(amount_val.clone())?;
348        assets = assets + CanonicalAssets::from_class_and_amount(class, amount);
349    }
350
351    Ok(assets)
352}
353
354pub fn utxo_from_json(value: &Value) -> Result<Utxo, Error> {
355    let ref_str = value["ref"].as_str().ok_or(Error::ValueIsNotAString)?;
356    let utxo_ref = string_to_utxo_ref(ref_str)?;
357
358    let address = hex_to_bytes(value["address"].as_str().ok_or(Error::ValueIsNotAString)?)?;
359
360    let assets = assets_from_json(&value["assets"])?;
361
362    let datum = value
363        .get("datum")
364        .filter(|v| !v.is_null())
365        .map(|v| serde_json::from_value(v.clone()))
366        .transpose()
367        .map_err(|e| Error::InvalidBytesEnvelope(e))?;
368
369    let script = value
370        .get("script")
371        .filter(|v| !v.is_null())
372        .map(|v| serde_json::from_value(v.clone()))
373        .transpose()
374        .map_err(|e| Error::InvalidBytesEnvelope(e))?;
375
376    Ok(Utxo {
377        r#ref: utxo_ref,
378        address,
379        assets,
380        datum,
381        script,
382    })
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388    use serde_json::json;
389
390    // TODO: derive PartialEq in upstream tx3-lang
391    fn partial_eq(a: ArgValue, b: ArgValue) -> bool {
392        match a {
393            ArgValue::Int(a) => match b {
394                ArgValue::Int(b) => dbg!(a) == dbg!(b),
395                _ => false,
396            },
397            ArgValue::Bool(a) => match b {
398                ArgValue::Bool(b) => a == b,
399                _ => false,
400            },
401            ArgValue::String(a) => match b {
402                ArgValue::String(b) => a == b,
403                _ => false,
404            },
405            ArgValue::Bytes(a) => match b {
406                ArgValue::Bytes(b) => a == b,
407                _ => false,
408            },
409            ArgValue::Address(a) => match b {
410                ArgValue::Address(b) => a == b,
411                _ => false,
412            },
413            ArgValue::UtxoSet(hash_set) => match b {
414                ArgValue::UtxoSet(b) => hash_set == b,
415                _ => false,
416            },
417            ArgValue::UtxoRef(utxo_ref) => match b {
418                ArgValue::UtxoRef(b) => utxo_ref == b,
419                _ => false,
420            },
421        }
422    }
423
424    fn assert_from_json(provided: Value, target: Type, expected: ArgValue) {
425        let value = from_json(provided, &target).unwrap();
426        assert!(partial_eq(value, expected));
427    }
428
429    // -----------------------------------------------------------------------
430    // JSON → Rust (from_json)
431    // -----------------------------------------------------------------------
432
433    #[test]
434    fn from_json_small_int() {
435        assert_from_json(json!(123456789), Type::Int, ArgValue::Int(123456789));
436    }
437
438    #[test]
439    fn from_json_negative_int() {
440        assert_from_json(json!(-123456789), Type::Int, ArgValue::Int(-123456789));
441    }
442
443    #[test]
444    fn from_json_big_int() {
445        assert_from_json(
446            json!("12345678901234567890"),
447            Type::Int,
448            ArgValue::Int(12345678901234567890),
449        );
450    }
451
452    #[test]
453    fn from_json_int_overflow() {
454        assert_from_json(
455            json!(i128::MIN.to_string()),
456            Type::Int,
457            ArgValue::Int(i128::MIN),
458        );
459        assert_from_json(
460            json!(i128::MAX.to_string()),
461            Type::Int,
462            ArgValue::Int(i128::MAX),
463        );
464    }
465
466    #[test]
467    fn from_json_bool() {
468        assert_from_json(json!(true), Type::Bool, ArgValue::Bool(true));
469        assert_from_json(json!(false), Type::Bool, ArgValue::Bool(false));
470    }
471
472    #[test]
473    fn from_json_bool_number() {
474        assert_from_json(json!(1), Type::Bool, ArgValue::Bool(true));
475        assert_from_json(json!(0), Type::Bool, ArgValue::Bool(false));
476    }
477
478    #[test]
479    fn from_json_bool_string() {
480        assert_from_json(json!("true"), Type::Bool, ArgValue::Bool(true));
481        assert_from_json(json!("false"), Type::Bool, ArgValue::Bool(false));
482    }
483
484    #[test]
485    fn from_json_bytes() {
486        assert_from_json(
487            json!(hex::encode("hello")),
488            Type::Bytes,
489            ArgValue::Bytes(b"hello".to_vec()),
490        );
491
492        assert_from_json(
493            json!(format!("0x{}", hex::encode("hello"))),
494            Type::Bytes,
495            ArgValue::Bytes(b"hello".to_vec()),
496        );
497    }
498
499    #[test]
500    fn from_json_bytes_base64() {
501        let json = json!(BytesEnvelope {
502            content: "aGVsbG8=".to_string(),
503            content_type: BytesEncoding::Base64,
504        });
505
506        assert_from_json(json, Type::Bytes, ArgValue::Bytes(b"hello".to_vec()));
507    }
508
509    #[test]
510    fn from_json_bytes_hex() {
511        let json = json!(BytesEnvelope {
512            content: "68656c6c6f".to_string(),
513            content_type: BytesEncoding::Hex,
514        });
515
516        assert_from_json(json, Type::Bytes, ArgValue::Bytes(b"hello".to_vec()));
517    }
518
519    #[test]
520    fn from_json_address() {
521        assert_from_json(
522            json!(hex::encode("abc123")),
523            Type::Address,
524            ArgValue::Address(b"abc123".to_vec()),
525        );
526    }
527
528    #[test]
529    fn from_json_address_bech32() {
530        let json = json!("addr1vx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzers66hrl8");
531        let bytes =
532            hex::decode("619493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e").unwrap();
533        assert_from_json(json, Type::Address, ArgValue::Address(bytes));
534    }
535
536    #[test]
537    fn from_json_utxo_ref() {
538        let json = json!("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef#0");
539
540        let utxo_ref = UtxoRef {
541            txid: hex::decode("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
542                .unwrap(),
543            index: 0,
544        };
545
546        assert_from_json(json, Type::UtxoRef, ArgValue::UtxoRef(utxo_ref));
547    }
548
549    // -----------------------------------------------------------------------
550    // Rust → JSON (to_json)
551    // -----------------------------------------------------------------------
552
553    use tx3_tir::model::assets::{AssetClass, CanonicalAssets};
554    use tx3_tir::model::core::Utxo;
555
556    fn sample_utxo_ref() -> UtxoRef {
557        UtxoRef {
558            txid: hex::decode("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
559                .unwrap(),
560            index: 2,
561        }
562    }
563
564    #[test]
565    fn utxo_ref_to_json_format() {
566        let r = sample_utxo_ref();
567        let v = utxo_ref_to_json(&r);
568        assert_eq!(
569            v,
570            json!("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef#2")
571        );
572    }
573
574    #[test]
575    fn arg_to_json_int() {
576        assert_eq!(arg_to_json(&ArgValue::Int(42)), json!(42));
577    }
578
579    #[test]
580    fn arg_to_json_bool() {
581        assert_eq!(arg_to_json(&ArgValue::Bool(true)), json!(true));
582    }
583
584    #[test]
585    fn arg_to_json_string() {
586        assert_eq!(
587            arg_to_json(&ArgValue::String("hello".into())),
588            json!("hello")
589        );
590    }
591
592    #[test]
593    fn arg_to_json_bytes() {
594        assert_eq!(
595            arg_to_json(&ArgValue::Bytes(b"hello".to_vec())),
596            json!("68656c6c6f")
597        );
598    }
599
600    #[test]
601    fn arg_to_json_address() {
602        assert_eq!(
603            arg_to_json(&ArgValue::Address(b"\x01\x02".to_vec())),
604            json!("0102")
605        );
606    }
607
608    #[test]
609    fn arg_to_json_utxo_ref() {
610        let r = sample_utxo_ref();
611        assert_eq!(
612            arg_to_json(&ArgValue::UtxoRef(r.clone())),
613            utxo_ref_to_json(&r)
614        );
615    }
616
617    #[test]
618    fn utxo_to_json_empty_assets() {
619        let utxo = Utxo {
620            r#ref: sample_utxo_ref(),
621            address: b"\xab\xcd".to_vec(),
622            assets: CanonicalAssets::empty(),
623            datum: None,
624            script: None,
625        };
626
627        let v = utxo_to_json(&utxo);
628        assert_eq!(v["assets"], json!({}));
629        assert_eq!(v["address"], json!("abcd"));
630        assert!(v["datum"].is_null());
631        assert!(v["script"].is_null());
632    }
633
634    #[test]
635    fn utxo_to_json_naked_assets() {
636        let utxo = Utxo {
637            r#ref: sample_utxo_ref(),
638            address: b"\x01".to_vec(),
639            assets: CanonicalAssets::from_naked_amount(5_000_000),
640            datum: None,
641            script: None,
642        };
643
644        let v = utxo_to_json(&utxo);
645        assert_eq!(v["assets"]["naked"], json!(5_000_000));
646    }
647
648    #[test]
649    fn utxo_to_json_defined_assets() {
650        let assets = CanonicalAssets::from_class_and_amount(
651            AssetClass::Defined(b"policy1".to_vec(), b"token1".to_vec()),
652            100,
653        );
654
655        let utxo = Utxo {
656            r#ref: sample_utxo_ref(),
657            address: b"\x01".to_vec(),
658            assets,
659            datum: None,
660            script: None,
661        };
662
663        let v = utxo_to_json(&utxo);
664        let key = format!("{}.{}", hex::encode(b"policy1"), hex::encode(b"token1"));
665        assert_eq!(v["assets"][&key], json!(100));
666    }
667}