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