Skip to main content

tx3_resolver/
interop.rs

1use base64::Engine as _;
2use serde::{Deserialize, Serialize};
3use serde_json::{Number, Value};
4use thiserror::Error;
5
6use tx3_tir::model::core::{Type, UtxoRef};
7pub use tx3_tir::reduce::ArgValue;
8
9#[derive(Debug, Error)]
10pub enum Error {
11    #[error("invalid base64: {0}")]
12    InvalidBase64(#[from] base64::DecodeError),
13
14    #[error("invalid hex: {0}")]
15    InvalidHex(#[from] hex::FromHexError),
16
17    #[error("invalid bech32: {0}")]
18    InvalidBech32(#[from] bech32::DecodeError),
19
20    #[error("value is not a valid number: {0}")]
21    InvalidBytesForNumber(String),
22
23    #[error("value is null")]
24    ValueIsNull,
25
26    #[error("can't infer type for value: {0}")]
27    CantInferTypeForValue(Value),
28
29    #[error("value is not a number: {0}")]
30    ValueIsNotANumber(Value),
31
32    #[error("value can't fit: {0}")]
33    NumberCantFit(Number),
34
35    #[error("value is not a bool: {0}")]
36    ValueIsNotABool(Value),
37
38    #[error("value is not a string")]
39    ValueIsNotAString,
40
41    #[error("value is not bytes: {0}")]
42    ValueIsNotBytes(Value),
43
44    #[error("value is not a utxo ref: {0}")]
45    ValueIsNotUtxoRef(Value),
46
47    #[error("invalid bytes envelope: {0}")]
48    InvalidBytesEnvelope(serde_json::Error),
49
50    #[error("value is not an address: {0}")]
51    ValueIsNotAnAddress(Value),
52
53    #[error("invalid utxo ref: {0}")]
54    InvalidUtxoRef(String),
55
56    #[error("target type not supported: {0:?}")]
57    TargetTypeNotSupported(Type),
58}
59
60#[derive(Debug, Deserialize, Serialize, Clone)]
61#[serde(rename_all = "lowercase")]
62pub enum BytesEncoding {
63    Base64,
64    Hex,
65}
66
67#[derive(Debug, Deserialize, Serialize, Clone)]
68pub struct BytesEnvelope {
69    // Aliases for backward compatibility
70    #[serde(alias = "bytecode", alias = "payload")]
71    pub content: String,
72    pub encoding: BytesEncoding,
73}
74
75impl BytesEnvelope {
76    pub fn from_hex(hex: &str) -> Result<Self, Error> {
77        Ok(Self {
78            content: hex.to_string(),
79            encoding: BytesEncoding::Hex,
80        })
81    }
82}
83
84impl From<BytesEnvelope> for Vec<u8> {
85    fn from(envelope: BytesEnvelope) -> Self {
86        match envelope.encoding {
87            BytesEncoding::Base64 => base64_to_bytes(&envelope.content).unwrap(),
88            BytesEncoding::Hex => hex_to_bytes(&envelope.content).unwrap(),
89        }
90    }
91}
92
93fn has_hex_prefix(s: &str) -> bool {
94    s.starts_with("0x")
95}
96
97pub fn string_to_bigint(s: String) -> Result<i128, Error> {
98    if has_hex_prefix(&s) {
99        let bytes = hex_to_bytes(&s)?;
100        let bytes = <[u8; 16]>::try_from(bytes)
101            .map_err(|x| Error::InvalidBytesForNumber(hex::encode(x)))?;
102        Ok(i128::from_be_bytes(bytes))
103    } else {
104        let i = i128::from_str_radix(&s, 10)
105            .map_err(|x| Error::InvalidBytesForNumber(x.to_string()))?;
106        Ok(i)
107    }
108}
109
110pub fn hex_to_bytes(s: &str) -> Result<Vec<u8>, Error> {
111    let s = if has_hex_prefix(s) {
112        s.trim_start_matches("0x")
113    } else {
114        s
115    };
116
117    let out = hex::decode(s)?;
118
119    Ok(out)
120}
121
122pub fn base64_to_bytes(s: &str) -> Result<Vec<u8>, Error> {
123    let out = base64::engine::general_purpose::STANDARD.decode(s)?;
124
125    Ok(out)
126}
127
128pub fn bech32_to_bytes(s: &str) -> Result<Vec<u8>, Error> {
129    let (_, data) = bech32::decode(s)?;
130
131    Ok(data)
132}
133
134fn number_to_bigint(x: Number) -> Result<i128, Error> {
135    x.as_i128().ok_or(Error::NumberCantFit(x))
136}
137
138fn value_to_bigint(value: Value) -> Result<i128, Error> {
139    let out = match value {
140        Value::Number(n) => number_to_bigint(n)?,
141        Value::String(s) => string_to_bigint(s)?,
142        Value::Null => return Err(Error::ValueIsNull),
143        x => return Err(Error::ValueIsNotANumber(x)),
144    };
145
146    Ok(out)
147}
148
149fn value_to_bool(value: Value) -> Result<bool, Error> {
150    match value {
151        Value::Bool(b) => Ok(b),
152        Value::Number(n) if n == Number::from(0) => Ok(false),
153        Value::Number(n) if n == Number::from(1) => Ok(true),
154        Value::String(s) if s == "true" => Ok(true),
155        Value::String(s) if s == "false" => Ok(false),
156        x => Err(Error::ValueIsNotABool(x)),
157    }
158}
159
160fn value_to_bytes(value: Value) -> Result<Vec<u8>, Error> {
161    let out = match value {
162        Value::String(s) => hex_to_bytes(&s)?,
163        Value::Object(_) => {
164            let envelope: BytesEnvelope =
165                serde_json::from_value(value).map_err(Error::InvalidBytesEnvelope)?;
166
167            match envelope.encoding {
168                BytesEncoding::Base64 => base64_to_bytes(&envelope.content)?,
169                BytesEncoding::Hex => hex_to_bytes(&envelope.content)?,
170            }
171        }
172        x => return Err(Error::ValueIsNotBytes(x)),
173    };
174
175    Ok(out)
176}
177
178fn value_to_address(value: Value) -> Result<Vec<u8>, Error> {
179    let out = match value {
180        Value::String(s) => match bech32_to_bytes(&s) {
181            Ok(data) => data,
182            Err(_) => hex_to_bytes(&s)?,
183        },
184        x => return Err(Error::ValueIsNotAnAddress(x)),
185    };
186
187    Ok(out)
188}
189
190fn value_to_underfined(value: Value) -> Result<ArgValue, Error> {
191    match value {
192        Value::Bool(b) => Ok(ArgValue::Bool(b)),
193        Value::Number(x) => Ok(ArgValue::Int(number_to_bigint(x)?)),
194        Value::String(s) => Ok(ArgValue::String(s)),
195        x => Err(Error::CantInferTypeForValue(x)),
196    }
197}
198
199fn string_to_utxo_ref(s: &str) -> Result<UtxoRef, Error> {
200    let (txid, index) = s
201        .split_once('#')
202        .ok_or(Error::InvalidUtxoRef(s.to_string()))?;
203
204    let txid = hex::decode(txid).map_err(|_| Error::InvalidUtxoRef(s.to_string()))?;
205    let index = index
206        .parse()
207        .map_err(|_| Error::InvalidUtxoRef(s.to_string()))?;
208
209    Ok(UtxoRef { txid, index })
210}
211
212fn value_to_utxo_ref(value: Value) -> Result<UtxoRef, Error> {
213    match value {
214        Value::String(s) => string_to_utxo_ref(&s),
215        x => Err(Error::ValueIsNotUtxoRef(x)),
216    }
217}
218
219pub fn from_json(value: Value, target: &Type) -> Result<ArgValue, Error> {
220    match target {
221        Type::Int => {
222            let i = value_to_bigint(value)?;
223            Ok(ArgValue::Int(i))
224        }
225        Type::Bool => {
226            let b = value_to_bool(value)?;
227            Ok(ArgValue::Bool(b))
228        }
229        Type::Bytes => {
230            let b = value_to_bytes(value)?;
231            Ok(ArgValue::Bytes(b))
232        }
233        Type::Address => {
234            let a = value_to_address(value)?;
235            Ok(ArgValue::Address(a))
236        }
237        Type::UtxoRef => {
238            let x = value_to_utxo_ref(value)?;
239            Ok(ArgValue::UtxoRef(x))
240        }
241        Type::Undefined => value_to_underfined(value),
242        x => Err(Error::TargetTypeNotSupported(x.clone())),
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use serde_json::json;
250
251    // TODO: derive PartialEq in upstream tx3-lang
252    fn partial_eq(a: ArgValue, b: ArgValue) -> bool {
253        match a {
254            ArgValue::Int(a) => match b {
255                ArgValue::Int(b) => dbg!(a) == dbg!(b),
256                _ => false,
257            },
258            ArgValue::Bool(a) => match b {
259                ArgValue::Bool(b) => a == b,
260                _ => false,
261            },
262            ArgValue::String(a) => match b {
263                ArgValue::String(b) => a == b,
264                _ => false,
265            },
266            ArgValue::Bytes(a) => match b {
267                ArgValue::Bytes(b) => a == b,
268                _ => false,
269            },
270            ArgValue::Address(a) => match b {
271                ArgValue::Address(b) => a == b,
272                _ => false,
273            },
274            ArgValue::UtxoSet(hash_set) => match b {
275                ArgValue::UtxoSet(b) => hash_set == b,
276                _ => false,
277            },
278            ArgValue::UtxoRef(utxo_ref) => match b {
279                ArgValue::UtxoRef(b) => utxo_ref == b,
280                _ => false,
281            },
282        }
283    }
284
285    fn json_to_value_test(provided: Value, target: Type, expected: ArgValue) {
286        let value = from_json(provided, &target).unwrap();
287        assert!(partial_eq(value, expected));
288    }
289
290    #[test]
291    fn test_round_trip_small_int() {
292        json_to_value_test(json!(123456789), Type::Int, ArgValue::Int(123456789));
293    }
294
295    #[test]
296    fn test_round_trip_negative_int() {
297        json_to_value_test(json!(-123456789), Type::Int, ArgValue::Int(-123456789));
298    }
299
300    #[test]
301    fn test_round_trip_big_int() {
302        json_to_value_test(
303            json!("12345678901234567890"),
304            Type::Int,
305            ArgValue::Int(12345678901234567890),
306        );
307    }
308
309    #[test]
310    fn test_round_trip_int_overflow() {
311        json_to_value_test(
312            json!(i128::MIN.to_string()),
313            Type::Int,
314            ArgValue::Int(i128::MIN),
315        );
316        json_to_value_test(
317            json!(i128::MAX.to_string()),
318            Type::Int,
319            ArgValue::Int(i128::MAX),
320        );
321    }
322
323    #[test]
324    fn test_round_trip_bool() {
325        json_to_value_test(json!(true), Type::Bool, ArgValue::Bool(true));
326        json_to_value_test(json!(false), Type::Bool, ArgValue::Bool(false));
327    }
328
329    #[test]
330    fn test_round_trip_bool_number() {
331        json_to_value_test(json!(1), Type::Bool, ArgValue::Bool(true));
332        json_to_value_test(json!(0), Type::Bool, ArgValue::Bool(false));
333    }
334
335    #[test]
336    fn test_round_trip_bool_string() {
337        json_to_value_test(json!("true"), Type::Bool, ArgValue::Bool(true));
338        json_to_value_test(json!("false"), Type::Bool, ArgValue::Bool(false));
339    }
340
341    #[test]
342    fn test_round_trip_bytes() {
343        json_to_value_test(
344            json!(hex::encode("hello")),
345            Type::Bytes,
346            ArgValue::Bytes(b"hello".to_vec()),
347        );
348
349        json_to_value_test(
350            json!(format!("0x{}", hex::encode("hello"))),
351            Type::Bytes,
352            ArgValue::Bytes(b"hello".to_vec()),
353        );
354    }
355
356    #[test]
357    fn test_round_trip_bytes_base64() {
358        let json = json!(BytesEnvelope {
359            content: "aGVsbG8=".to_string(),
360            encoding: BytesEncoding::Base64,
361        });
362
363        json_to_value_test(json, Type::Bytes, ArgValue::Bytes(b"hello".to_vec()));
364    }
365
366    #[test]
367    fn test_round_trip_bytes_hex() {
368        let json = json!(BytesEnvelope {
369            content: "68656c6c6f".to_string(),
370            encoding: BytesEncoding::Hex,
371        });
372
373        json_to_value_test(json, Type::Bytes, ArgValue::Bytes(b"hello".to_vec()));
374    }
375
376    #[test]
377    fn test_round_trip_address() {
378        json_to_value_test(
379            json!(hex::encode("abc123")),
380            Type::Address,
381            ArgValue::Address(b"abc123".to_vec()),
382        );
383    }
384
385    #[test]
386    fn test_round_trip_address_bech32() {
387        let json = json!("addr1vx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzers66hrl8");
388        let bytes =
389            hex::decode("619493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e").unwrap();
390        json_to_value_test(json, Type::Address, ArgValue::Address(bytes));
391    }
392
393    #[test]
394    fn test_round_trip_utxo_ref() {
395        let json = json!("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef#0");
396
397        let utxo_ref = UtxoRef {
398            txid: hex::decode("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
399                .unwrap(),
400            index: 0,
401        };
402
403        json_to_value_test(json, Type::UtxoRef, ArgValue::UtxoRef(utxo_ref));
404    }
405}