tx3_sdk/trp/
args.rs

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