ergo_lib_wasm/ast/
js_conv.rs

1//! Arbitrary JS type to Ergo data type conversion.
2
3#![allow(clippy::wildcard_enum_match_arm)]
4
5use std::convert::TryFrom;
6
7use ergo_lib::ergotree_ir::bigint256::BigInt256;
8use ergo_lib::ergotree_ir::mir::constant::Constant;
9use ergo_lib::ergotree_ir::mir::constant::Literal;
10use ergo_lib::ergotree_ir::mir::constant::TryExtractFromError;
11use ergo_lib::ergotree_ir::mir::constant::TryExtractInto;
12use ergo_lib::ergotree_ir::mir::value::CollKind;
13use ergo_lib::ergotree_ir::mir::value::NativeColl;
14use ergo_lib::ergotree_ir::types::stuple::STuple;
15use ergo_lib::ergotree_ir::types::stuple::TupleItems;
16use ergo_lib::ergotree_ir::types::stype::SType;
17use js_sys::Array;
18use js_sys::JsString;
19use js_sys::Number;
20use js_sys::Uint8Array;
21use num_traits::Num;
22use sigma_util::AsVecI8;
23use sigma_util::AsVecU8;
24use thiserror::Error;
25use wasm_bindgen::prelude::wasm_bindgen;
26use wasm_bindgen::JsCast;
27use wasm_bindgen::JsValue;
28
29const TUPLE_TOKEN: &str = "Tuple";
30
31/// Encode a JS array as an Ergo tuple.
32#[wasm_bindgen]
33pub fn array_as_tuple(items: Vec<JsValue>) -> JsValue {
34    let arr = Array::new();
35    arr.push(&JsValue::from_str(TUPLE_TOKEN));
36    for item in items {
37        arr.push(&item);
38    }
39    arr.into()
40}
41
42#[allow(missing_docs)]
43#[derive(Debug, Error)]
44pub enum ConvError {
45    #[error("not supported: {0:?}")]
46    NotSupported(JsValue),
47    #[error("unexpected constant: {0:?}")]
48    UnexpectedConst(Constant),
49    #[error("unexpected js value: {0:?}, expected {1}")]
50    UnexpectedJs(JsValue, &'static str),
51    #[error("IO error: {0}")]
52    TryExtractFromError(#[from] TryExtractFromError),
53    #[error("Failed to parse Long from string: {0}")]
54    FailedToParseLongFromString(String),
55    #[error("Failed to convert JS BigInt to Ergo BigInt: {0}")]
56    FailedToConvertJsBigInt(js_sys::BigInt),
57    #[error(
58        "Invalid tuple encoding, expected the first array item to be '{}''",
59        TUPLE_TOKEN
60    )]
61    InvalidTupleEncoding,
62}
63
64pub(crate) fn constant_from_js(val: &JsValue) -> Result<Constant, ConvError> {
65    if let Ok(bytes) = val.clone().dyn_into::<Uint8Array>() {
66        Ok(Constant {
67            tpe: SType::SColl(SType::SByte.into()),
68            v: Literal::Coll(CollKind::NativeColl(NativeColl::CollByte(
69                bytes.to_vec().as_vec_i8().into(),
70            ))),
71        })
72    } else if let Ok(arr) = val.clone().dyn_into::<Array>() {
73        if let Ok(str) = arr.get(0).dyn_into::<JsString>() {
74            if str == TUPLE_TOKEN {
75                tuple_from_js(&arr)
76            } else if let Ok(coll_longs) = coll_long_from_js(&arr) {
77                Ok(coll_longs)
78            } else {
79                return Err(ConvError::NotSupported(val.clone()));
80            }
81        } else {
82            // regular array
83            let mut cs: Vec<Constant> = Vec::new();
84            for i in 0..arr.length() {
85                let elem_const = constant_from_js(&arr.get(i))?;
86                cs.push(elem_const);
87            }
88            let elem_tpe = cs[0].tpe.clone();
89            Ok(Constant {
90                tpe: SType::SColl(elem_tpe.clone().into()),
91                v: Literal::Coll(CollKind::WrappedColl {
92                    elem_tpe,
93                    items: cs.into_iter().map(|c| c.v).collect(),
94                }),
95            })
96        }
97    } else if let Ok(num) = val.clone().dyn_into::<Number>() {
98        let c: Constant = (num.value_of() as i32).into();
99        Ok(c)
100    } else if let Ok(long_js_str) = val.clone().dyn_into::<JsString>() {
101        let long_str: String = long_js_str.into();
102        let long: i64 = long_str
103            .parse::<i64>()
104            .map_err(|_| ConvError::FailedToParseLongFromString(long_str))?;
105        let c: Constant = long.into();
106        Ok(c)
107    } else if let Ok(bigint_js) = val.clone().dyn_into::<js_sys::BigInt>() {
108        let c: Constant = js_bigint_to_ergo_bigint(bigint_js)?.into();
109        Ok(c)
110    } else {
111        Err(ConvError::NotSupported(val.clone()))
112    }
113}
114
115fn tuple_from_js(arr: &Array) -> Result<Constant, ConvError> {
116    // tuple
117    let mut v: Vec<Constant> = Vec::new();
118    for i in 1..arr.length() {
119        let elem_const = constant_from_js(&arr.get(i))?;
120        v.push(elem_const);
121    }
122    #[allow(clippy::unwrap_used)]
123    Ok(Constant {
124        tpe: SType::STuple(STuple {
125            items: TupleItems::try_from(
126                v.clone().into_iter().map(|c| c.tpe).collect::<Vec<SType>>(),
127            )
128            .unwrap(),
129        }),
130        v: Literal::Tup(
131            TupleItems::try_from(v.into_iter().map(|c| c.v).collect::<Vec<Literal>>()).unwrap(),
132        ),
133    })
134}
135
136fn coll_long_from_js(arr: &Array) -> Result<Constant, ConvError> {
137    // try array of longs
138    let mut longs: Vec<i64> = Vec::new();
139    for i in 0..arr.length() {
140        let arr_elem = arr.get(i);
141        let js_string = arr_elem
142            .clone()
143            .dyn_into::<JsString>()
144            .map_err(|_| ConvError::UnexpectedJs(arr_elem, "JsString"))?;
145        let parsed_i64 = String::from(js_string.clone())
146            .parse::<i64>()
147            .map_err(|e| ConvError::FailedToParseLongFromString(js_string.into()))?;
148        longs.push(parsed_i64);
149    }
150    Ok(longs.into())
151}
152
153pub(crate) fn constant_to_js(c: Constant) -> Result<JsValue, ConvError> {
154    Ok(match c.tpe {
155        SType::SBoolean => c.v.try_extract_into::<bool>()?.into(),
156        SType::SShort => c.v.try_extract_into::<i16>()?.into(),
157        SType::SByte => c.v.try_extract_into::<i8>()?.into(),
158        SType::SInt => c.v.try_extract_into::<i32>()?.into(),
159        SType::SLong => c.v.try_extract_into::<i64>()?.to_string().into(),
160        SType::SBigInt => ergo_bigint_to_js_bigint(c.v.try_extract_into::<BigInt256>()?).into(),
161        SType::SColl(_) => match c.v {
162            Literal::Coll(CollKind::NativeColl(NativeColl::CollByte(v))) => {
163                Uint8Array::from(v.as_vec_u8().as_slice()).into()
164            }
165            Literal::Coll(CollKind::WrappedColl { elem_tpe, items }) => {
166                let arr = Array::new();
167                for item in items.iter() {
168                    arr.push(&constant_to_js(Constant {
169                        tpe: elem_tpe.clone(),
170                        v: item.clone(),
171                    })?);
172                }
173                arr.into()
174            }
175            _ => return Err(ConvError::UnexpectedConst(c)),
176        },
177        SType::STuple(ref item_tpes) => {
178            let vec: Vec<JsValue> = match c.v {
179                Literal::Tup(v) => v
180                    .into_iter()
181                    .zip(item_tpes.clone().items.into_iter())
182                    .map(|(v, tpe)| constant_to_js(Constant { tpe, v }))
183                    .collect::<Result<Vec<JsValue>, _>>()?,
184                _ => return Err(ConvError::UnexpectedConst(c.clone())),
185            };
186            let arr = Array::new();
187            for item in vec {
188                arr.push(&item);
189            }
190            arr.into()
191        }
192        _ => return Err(ConvError::UnexpectedConst(c.clone())),
193    })
194}
195
196fn ergo_bigint_to_js_bigint(bigint: BigInt256) -> js_sys::BigInt {
197    let bigint_str = bigint.to_string();
198    #[allow(clippy::unwrap_used)]
199    // since BigInt256 bounds are less the JS BigInt it should not fail
200    js_sys::BigInt::new(&bigint_str.into()).unwrap()
201}
202
203fn js_bigint_to_ergo_bigint(bigint: js_sys::BigInt) -> Result<BigInt256, ConvError> {
204    #[allow(clippy::unwrap_used)]
205    // safe, because it can only return an error on invalid radix
206    let bigint_js_str: String = bigint.to_string(10).unwrap().into();
207    BigInt256::from_str_radix(bigint_js_str.as_str(), 10)
208        .map_err(|_| ConvError::FailedToConvertJsBigInt(bigint))
209}