Repository: ergoplatform/sigma-rust (language: rust)

Issue:
JS <-> Constant conversion for EIP-24, EIP-34
>We need a few deserialization methods in sigma rust based on these two EIPs in js.
>https://github.com/anon-real/eips/blob/master/eip-0024.md
>https://github.com/anon-real/eips/blob/master/eip-0034.md

```
Coll[Coll[Byte]]
Coll[(Coll[Byte], Coll[Byte])]
Coll[(Coll[Byte], Int)]

(
  Coll[(  
    Coll[Byte],
    Coll[Byte] 
  )],
  ( // levels
    Coll[(
      Coll[Byte],
      (Int, Int)
    )],
    // stats
    Coll[(
      Coll[Byte],
      (Int, Int)
    )]
  )
)
```


Discussion / hints:

Fix patch:
diff --git a/bindings/ergo-lib-wasm/Cargo.toml b/bindings/ergo-lib-wasm/Cargo.toml
index 382d8c639..e7a6b0860 100644
--- a/bindings/ergo-lib-wasm/Cargo.toml
+++ b/bindings/ergo-lib-wasm/Cargo.toml
@@ -18,12 +18,14 @@ rest = ["ergo-lib/rest"]
 base16 = "0.2.1"
 serde = { version = "1.0", features = ["derive"]}
 ergo-lib = { version = "^0.19.0", path = "../../ergo-lib"}
+sigma-util = { version = "^0.7.0", path = "../../sigma-util"}
 serde_json = "1.0"
 js-sys = "0.3"
 web-sys = {version = "0.3", features = ["Url"]}
 url = "2.2"
 bounded-integer = { version = "^0.5", features = ["types"] }
 futures = "0.3"
+thiserror = "1"
 
 # used in elliptic-curve(in ergo-lib), compiled here with WASM support
 getrandom = {version = "0.2.3", features = ["js"]}
@@ -34,6 +36,7 @@ getrandom = {version = "0.2.3", features = ["js"]}
 console_error_panic_hook = { version = "0.1.6", optional = true }
 
 derive_more = "0.99"
+num-traits = "0.2.14"
 
 [dependencies.serde_with]
 version = "1.9.1"
@@ -52,7 +55,6 @@ features = ["serde"]
 
 [dev-dependencies]
 wasm-bindgen-test = "0.3.29"
-ergotree-ir = { version = "^0.19.0", path = "../../ergotree-ir", features = ["arbitrary"] }
 ergo-lib = { version = "^0.19.0", path = "../../ergo-lib", features = ["arbitrary"] }
 
 [dev-dependencies.proptest]
diff --git a/bindings/ergo-lib-wasm/src/ast.rs b/bindings/ergo-lib-wasm/src/ast.rs
index 9766a969b..412fd6f9b 100644
--- a/bindings/ergo-lib-wasm/src/ast.rs
+++ b/bindings/ergo-lib-wasm/src/ast.rs
@@ -1,5 +1,7 @@
 //! Ergo constant values
 
+use crate::ast::js_conv::constant_from_js;
+use crate::ast::js_conv::constant_to_js;
 use crate::ergo_box::ErgoBox;
 use crate::error_conversion::to_js;
 use crate::utils::I64;
@@ -17,6 +19,8 @@ extern crate derive_more;
 use derive_more::{From, Into};
 use ergo_lib::ergotree_ir::bigint256::BigInt256;
 
+pub mod js_conv;
+
 /// Ergo constant(evaluated) values
 #[wasm_bindgen]
 #[derive(PartialEq, Eq, Debug, Clone, From, Into)]
@@ -24,6 +28,16 @@ pub struct Constant(ergo_lib::ergotree_ir::mir::constant::Constant);
 
 #[wasm_bindgen]
 impl Constant {
+    /// Returns the debug representation of the type of the constant
+    pub fn dbg_tpe(&self) -> String {
+        format!("{:?}", self.0.tpe)
+    }
+
+    /// Returns the debug representation of the value of the constant
+    pub fn dbg_inner(&self) -> String {
+        format!("{:?}", self.0)
+    }
+
     /// Decode from Base16-encoded ErgoTree serialized value
     pub fn decode_from_base16(base16_bytes_str: String) -> Result<Constant, JsValue> {
         let bytes = Base16DecodedBytes::try_from(base16_bytes_str.clone()).map_err(|_| {
@@ -266,4 +280,26 @@ impl Constant {
     pub fn is_unit(&self) -> bool {
         self.0.tpe == ergo_lib::ergotree_ir::types::stype::SType::SUnit
     }
+
+    /// Create a Constant from JS value
+    /// JS types are converted to the following Ergo types:
+    /// Number -> Int,
+    /// String -> Long,
+    /// BigInt -> BigInt,
+    /// use array_as_tuple() to encode Ergo tuples
+    pub fn from_js(value: &JsValue) -> Result<Constant, JsValue> {
+        constant_from_js(value).map(Into::into).map_err(to_js)
+    }
+
+    /// Extract JS value from Constant
+    /// Ergo types are converted to the following JS types:
+    /// Byte -> Number,
+    /// Short -> Number,
+    /// Int -> Number,
+    /// Long -> String,
+    /// BigInt -> BigInt,
+    /// Ergo tuples are encoded as arrays
+    pub fn to_js(&self) -> Result<JsValue, JsValue> {
+        constant_to_js(self.0.clone()).map_err(to_js)
+    }
 }
diff --git a/bindings/ergo-lib-wasm/src/ast/js_conv.rs b/bindings/ergo-lib-wasm/src/ast/js_conv.rs
new file mode 100644
index 000000000..2603404a8
--- /dev/null
+++ b/bindings/ergo-lib-wasm/src/ast/js_conv.rs
@@ -0,0 +1,184 @@
+//! Arbitrary JS type to Ergo data type conversion.
+
+#![allow(clippy::wildcard_enum_match_arm)]
+
+use std::convert::TryFrom;
+
+use ergo_lib::ergotree_ir::bigint256::BigInt256;
+use ergo_lib::ergotree_ir::mir::constant::Constant;
+use ergo_lib::ergotree_ir::mir::constant::Literal;
+use ergo_lib::ergotree_ir::mir::constant::TryExtractFromError;
+use ergo_lib::ergotree_ir::mir::constant::TryExtractInto;
+use ergo_lib::ergotree_ir::mir::value::CollKind;
+use ergo_lib::ergotree_ir::mir::value::NativeColl;
+use ergo_lib::ergotree_ir::types::stuple::STuple;
+use ergo_lib::ergotree_ir::types::stuple::TupleItems;
+use ergo_lib::ergotree_ir::types::stype::SType;
+use js_sys::Array;
+use js_sys::JsString;
+use js_sys::Number;
+use js_sys::Uint8Array;
+use num_traits::Num;
+use sigma_util::AsVecI8;
+use sigma_util::AsVecU8;
+use thiserror::Error;
+use wasm_bindgen::prelude::wasm_bindgen;
+use wasm_bindgen::JsCast;
+use wasm_bindgen::JsValue;
+
+const TUPLE_TOKEN: &str = "Tuple";
+
+/// Encode a JS array as an Ergo tuple.
+#[wasm_bindgen]
+pub fn array_as_tuple(items: Vec<JsValue>) -> JsValue {
+    let arr = Array::new();
+    arr.push(&JsValue::from_str(TUPLE_TOKEN));
+    for item in items {
+        arr.push(&item);
+    }
+    arr.into()
+}
+
+#[allow(missing_docs)]
+#[derive(Debug, Error)]
+pub enum ConvError {
+    #[error("not supported: {0:?}")]
+    NotSupported(JsValue),
+    #[error("unexpected: {0:?}")]
+    Unexpected(Constant),
+    #[error("IO error: {0}")]
+    TryExtractFromError(#[from] TryExtractFromError),
+    #[error("Failed to parse Long from string: {0}")]
+    FailedToParseLongFromString(String),
+    #[error("Failed to convert JS BigInt to Ergo BigInt: {0}")]
+    FailedToConvertJsBigInt(js_sys::BigInt),
+    #[error(
+        "Invalid tuple encoding, expected the first array item to be '{}''",
+        TUPLE_TOKEN
+    )]
+    InvalidTupleEncoding,
+}
+
+pub(crate) fn constant_from_js(val: &JsValue) -> Result<Constant, ConvError> {
+    if let Ok(bytes) = val.clone().dyn_into::<Uint8Array>() {
+        Ok(Constant {
+            tpe: SType::SColl(SType::SByte.into()),
+            v: Literal::Coll(CollKind::NativeColl(NativeColl::CollByte(
+                bytes.to_vec().as_vec_i8(),
+            ))),
+        })
+    } else if let Ok(arr) = val.clone().dyn_into::<Array>() {
+        if let Ok(str) = arr.get(0).dyn_into::<JsString>() {
+            // tuple
+            if str != TUPLE_TOKEN {
+                return Err(ConvError::InvalidTupleEncoding);
+            }
+            let mut v: Vec<Constant> = Vec::new();
+            for i in 1..arr.length() {
+                let elem_const = constant_from_js(&arr.get(i))?;
+                v.push(elem_const);
+            }
+            #[allow(clippy::unwrap_used)]
+            Ok(Constant {
+                tpe: SType::STuple(STuple {
+                    items: TupleItems::try_from(
+                        v.clone().into_iter().map(|c| c.tpe).collect::<Vec<SType>>(),
+                    )
+                    .unwrap(),
+                }),
+                v: Literal::Tup(
+                    TupleItems::try_from(v.into_iter().map(|c| c.v).collect::<Vec<Literal>>())
+                        .unwrap(),
+                ),
+            })
+        } else {
+            // regular array
+            let mut cs: Vec<Constant> = Vec::new();
+            for i in 0..arr.length() {
+                let elem_const = constant_from_js(&arr.get(i))?;
+                cs.push(elem_const);
+            }
+            let elem_tpe = cs[0].tpe.clone();
+            Ok(Constant {
+                tpe: SType::SColl(elem_tpe.clone().into()),
+                v: Literal::Coll(CollKind::WrappedColl {
+                    elem_tpe,
+                    items: cs.into_iter().map(|c| c.v).collect(),
+                }),
+            })
+        }
+    } else if let Ok(num) = val.clone().dyn_into::<Number>() {
+        let c: Constant = (num.value_of() as i32).into();
+        Ok(c)
+    } else if let Ok(long_js_str) = val.clone().dyn_into::<JsString>() {
+        let long_str: String = long_js_str.into();
+        let long: i64 = long_str
+            .parse::<i64>()
+            .map_err(|_| ConvError::FailedToParseLongFromString(long_str))?;
+        let c: Constant = long.into();
+        Ok(c)
+    } else if let Ok(bigint_js) = val.clone().dyn_into::<js_sys::BigInt>() {
+        let c: Constant = js_bigint_to_ergo_bigint(bigint_js)?.into();
+        Ok(c)
+    } else {
+        Err(ConvError::NotSupported(val.clone()))
+    }
+}
+
+pub(crate) fn constant_to_js(c: Constant) -> Result<JsValue, ConvError> {
+    Ok(match c.tpe {
+        SType::SBoolean => c.v.try_extract_into::<bool>()?.into(),
+        SType::SShort => c.v.try_extract_into::<i16>()?.into(),
+        SType::SByte => c.v.try_extract_into::<i8>()?.into(),
+        SType::SInt => c.v.try_extract_into::<i32>()?.into(),
+        SType::SLong => c.v.try_extract_into::<i64>()?.to_string().into(),
+        SType::SBigInt => ergo_bigint_to_js_bigint(c.v.try_extract_into::<BigInt256>()?).into(),
+        SType::SColl(_) => match c.v {
+            Literal::Coll(CollKind::NativeColl(NativeColl::CollByte(v))) => {
+                Uint8Array::from(v.as_vec_u8().as_slice()).into()
+            }
+            Literal::Coll(CollKind::WrappedColl { elem_tpe, items }) => {
+                let arr = Array::new();
+                for item in items {
+                    arr.push(&constant_to_js(Constant {
+                        tpe: elem_tpe.clone(),
+                        v: item,
+                    })?);
+                }
+                arr.into()
+            }
+            _ => return Err(ConvError::Unexpected(c)),
+        },
+        SType::STuple(ref item_tpes) => {
+            let vec: Vec<JsValue> = match c.v {
+                Literal::Tup(v) => v
+                    .into_iter()
+                    .zip(item_tpes.clone().items.into_iter())
+                    .map(|(v, tpe)| constant_to_js(Constant { tpe, v }))
+                    .collect::<Result<Vec<JsValue>, _>>()?,
+                _ => return Err(ConvError::Unexpected(c.clone())),
+            };
+            let arr = Array::new();
+            for item in vec {
+                arr.push(&item);
+            }
+            arr.into()
+        }
+        _ => return Err(ConvError::Unexpected(c.clone())),
+    })
+}
+
+fn ergo_bigint_to_js_bigint(bigint: BigInt256) -> js_sys::BigInt {
+    let bigint_str = bigint.to_string();
+    #[allow(clippy::unwrap_used)]
+    // since BigInt256 bounds are less the JS BigInt it should not fail
+    js_sys::BigInt::new(&bigint_str.into()).unwrap()
+}
+
+fn js_bigint_to_ergo_bigint(bigint: js_sys::BigInt) -> Result<BigInt256, ConvError> {
+    #[allow(clippy::unwrap_used)]
+    // safe, because it can only return an error on invalid radix
+    let bigint_js_str: String = bigint.to_string(10).unwrap().into();
+    BigInt256::from_str_radix(bigint_js_str.as_str(), 10)
+        .map_err(|_| ConvError::FailedToConvertJsBigInt(bigint))
+}
diff --git a/bindings/ergo-lib-wasm/src/error_conversion.rs b/bindings/ergo-lib-wasm/src/error_conversion.rs
index 9154b998f..3be6e1ba4 100644
--- a/bindings/ergo-lib-wasm/src/error_conversion.rs
+++ b/bindings/ergo-lib-wasm/src/error_conversion.rs
@@ -35,6 +35,8 @@ use serde_json::error::Error;
 use url::ParseError;
 use wasm_bindgen::JsValue;
 
+use crate::ast::js_conv::ConvError;
+
 /// Ideally we'd like to implement `From<E> for JsValue` for a range of different `ergo-lib` error
 /// types `E`, but Rust orphan rules prevent this. A way to get around this limitation is to wrap
 /// `Jsvalue` within a local type.
@@ -75,6 +77,21 @@ from_error_to_wrap!(DecodeError);
 from_error_to_wrap!(TryFromSliceError);
 from_error_to_wrap!(AddrParseError);
 from_error_to_wrap!(TransactionSignatureVerificationError);
+from_error_to_wrap!(ErgoTreeError);
+from_error_to_wrap!(ChildIndexError);
+from_error_to_wrap!(BoundedVecOutOfBounds);
+from_error_to_wrap!(ExtSecretKeyError);
+from_error_to_wrap!(DerivationPathError);
+from_error_to_wrap!(VerifierError);
+from_error_to_wrap!(ExtPubKeyError);
+from_error_to_wrap!(ParseIntError);
+#[cfg(feature = "rest")]
+from_error_to_wrap!(NodeError);
+#[cfg(feature = "rest")]
+from_error_to_wrap!(PeerDiscoveryError);
+#[cfg(feature = "rest")]
+from_error_to_wrap!(ParseError);
+from_error_to_wrap!(ConvError);
 
 macro_rules! from_error_to_wrap_via_debug {
     ($t:ident) => {
@@ -88,20 +105,6 @@ macro_rules! from_error_to_wrap_via_debug {
     };
 }
 
-from_error_to_wrap_via_debug!(ErgoTreeError);
 from_error_to_wrap_via_debug!(ErgoTreeConstantError);
 from_error_to_wrap_via_debug!(ErgoTreeConstantsParsingError);
-from_error_to_wrap_via_debug!(ParseIntError);
-from_error_to_wrap_via_debug!(ChildIndexError);
-from_error_to_wrap_via_debug!(BoundedVecOutOfBounds);
-from_error_to_wrap_via_debug!(ExtSecretKeyError);
-from_error_to_wrap_via_debug!(DerivationPathError);
-from_error_to_wrap_via_debug!(ExtPubKeyError);
 from_error_to_wrap_via_debug!(NipopowProofError);
-from_error_to_wrap_via_debug!(VerifierError);
-#[cfg(feature = "rest")]
-from_error_to_wrap_via_debug!(NodeError);
-#[cfg(feature = "rest")]
-from_error_to_wrap_via_debug!(ParseError);
-#[cfg(feature = "rest")]
-from_error_to_wrap_via_debug!(PeerDiscoveryError);
diff --git a/bindings/ergo-lib-wasm/src/utils.rs b/bindings/ergo-lib-wasm/src/utils.rs
index b192760c7..92d155180 100644
--- a/bindings/ergo-lib-wasm/src/utils.rs
+++ b/bindings/ergo-lib-wasm/src/utils.rs
@@ -20,7 +20,8 @@ impl MinerAddress {
     }
 }
 
-/// Wrapper for i64 for JS/TS
+/// Wrapper for i64 for JS/TS because JS Number can only represent 53 bits
+/// see https://stackoverflow.com/questions/17320706/javascript-long-integer
 #[wasm_bindgen]
 #[derive(PartialEq, Eq, Debug, Clone)]
 pub struct I64(i64);
diff --git a/ergotree-ir/src/types/stuple.rs b/ergotree-ir/src/types/stuple.rs
index 963c84d33..95afe5947 100644
--- a/ergotree-ir/src/types/stuple.rs
+++ b/ergotree-ir/src/types/stuple.rs
@@ -22,12 +22,18 @@ impl TryFrom<Vec<SType>> for STuple {
 }
 
 /// Tuple type
-#[derive(PartialEq, Eq, Debug, Clone)]
+#[derive(PartialEq, Eq, Clone)]
 pub struct STuple {
     /// Tuple element types
     pub items: TupleItems<SType>,
 }
 
+impl std::fmt::Debug for STuple {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.items.clone().to_vec().fmt(f)
+    }
+}
+
 impl STuple {
     /// Create a tuple type for a given type pair
     pub fn pair(t1: SType, t2: SType) -> Self {


Test patch:
diff --git a/bindings/ergo-lib-wasm/tests/test_constant.js b/bindings/ergo-lib-wasm/tests/test_constant.js
index 31b1829a5..ccee33d5f 100644
--- a/bindings/ergo-lib-wasm/tests/test_constant.js
+++ b/bindings/ergo-lib-wasm/tests/test_constant.js
@@ -22,7 +22,7 @@ it("roundtrip Constant i32", async () => {
   expect(decoded_c_value).equal(value);
 });
 
-it("roundtrip Constant i64", async () => {
+it("roundtrip i64 via to_i64", async () => {
   let value_str = "9223372036854775807"; // i64 max value
   let c = ergo_wasm.Constant.from_i64(ergo_wasm.I64.from_str(value_str));
   let encoded = c.encode_to_base16();
@@ -120,3 +120,71 @@ it("roundtrip ErgoBox", async () => {
   assert(decoded_c_value != null);
   expect(decoded_c_value.to_json().toString()).equal(box.to_json().toString());
 });
+
+it("roundtrip Coll[Coll[Byte]]", async () => {
+  let bytes1 = new Uint8Array([1, 2, 3]);
+  let bytes2 = new Uint8Array([3, 2, 1]);
+  let value = [bytes1, bytes2];
+  let c_js = ergo_wasm.Constant.from_js(value);
+  expect(c_js != null);
+  expect(c_js.dbg_tpe()).equal("SColl(SColl(SByte))");
+  assert.deepEqual(c_js.to_js(), value);
+});
+
+it("roundtrip Coll[(Coll[Byte], Coll[Byte])]", async () => {
+  let bytes1 = new Uint8Array([1, 2, 3]);
+  let bytes2 = new Uint8Array([3, 2, 1]);
+  let value = [ergo_wasm.array_as_tuple([bytes1, bytes2]), ergo_wasm.array_as_tuple([bytes2, bytes1])];
+  let expected_value = [[bytes1, bytes2], [bytes2, bytes1]];
+  let c_js = ergo_wasm.Constant.from_js(value);
+  expect(c_js != null);
+  expect(c_js.dbg_tpe()).equal("SColl(STuple([SColl(SByte), SColl(SByte)]))");
+  // console.log(c_js.dbg_inner());
+  assert.deepEqual(c_js.to_js(), expected_value);
+});
+
+it("roundtrip EIP-24 R7 monster type", async () => {
+  let bytes1 = new Uint8Array([1, 2, 3]);
+  let bytes2 = new Uint8Array([4, 5, 6]);
+  let value = ergo_wasm.array_as_tuple([
+    [ergo_wasm.array_as_tuple([bytes1, bytes2])],
+    ergo_wasm.array_as_tuple([
+      [ergo_wasm.array_as_tuple([bytes1, ergo_wasm.array_as_tuple([10, 11])])],
+      [ergo_wasm.array_as_tuple([bytes2, ergo_wasm.array_as_tuple([12, 13])])]
+    ])
+  ]);
+  let expected_value = [
+    [[bytes1, bytes2]],
+    [
+      [[bytes1, [10, 11]]],
+      [[bytes2, [12, 13]]]
+    ]
+  ];
+  let c_js = ergo_wasm.Constant.from_js(value);
+  expect(c_js != null);
+  expect(c_js.dbg_tpe()).equal("STuple([SColl(STuple([SColl(SByte), SColl(SByte)])), STuple([SColl(STuple([SColl(SByte), STuple([SInt, SInt])])), SColl(STuple([SColl(SByte), STuple([SInt, SInt])]))])])");
+  // console.log(c_js.dbg_inner());
+  assert.deepEqual(c_js.to_js(), expected_value);
+});
+
+it("roundtrip i64", async () => {
+  let value_str = "9223372036854775807"; // i64 max value
+  let c_js = ergo_wasm.Constant.from_js(value_str);
+  let decoded_c_value = c_js.to_js();
+  expect(c_js.dbg_tpe()).equal("SLong");
+  expect(decoded_c_value).equal(value_str);
+});
+
+it("roundtrip BigInt", async () => {
+  let bigint = BigInt(92233720368547758071111111111111111111111111n);
+  let c_js = ergo_wasm.Constant.from_js(bigint);
+  expect(c_js.dbg_tpe()).equal("SBigInt");
+  let decoded_c_value = c_js.to_js();
+  expect(decoded_c_value).equal(bigint);
+  assert.deepEqual(c_js.to_js(), bigint);
+});
+
+it("too big BigInt fail", async () => {
+  let bigint = BigInt(92233720368547758071111111111111111111111111111111111111111111111111111111111111111111111111n);
+  expect(function () {ergo_wasm.Constant.from_js(bigint);}).to.throw();
+});