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