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

Issue:
JSON from explorer API parsing error on `spendingProof` for `Transaction`
GET on https://api-testnet.ergoplatform.com/api/v1/transactions/06b02c29a8c1a528c18bab4b6c92d447dc5ff0d99a591ddce2878631c555c97b the following JSON is returned:
```json
{
  "summary": {
    "id": "06b02c29a8c1a528c18bab4b6c92d447dc5ff0d99a591ddce2878631c555c97b",
    "timestamp": 1673515595990,
    "index": 2,
    "size": 563,
    "confirmationsCount": 1,
    "block": {
      "id": "00437b41b0b7d991ddb0b33897ebbd13caa69111dfcaaced91357fad34c158d2",
      "height": 225127
    }
  },
  "ioSummary": {
    "totalCoinsTransferred": 84400000,
    "totalFee": 1100000,
    "feePerByte": 1953.8188277087033
  },
  "inputs": [
    {
      "id": "61a9e57e6635d02196fadc4ddf40a902c043c692bc6c7b452e02fa467e6e1fd7",
      "value": 6700000,
      "index": 0,
      "spendingProof": "736d882bbfa1767cef64e718eb74f76b96891fb8ad4e8fdd987ec4550b39d7cda0c285f8346e8f842acbae0b3d8e2b52d039cf1dacefc51c",
      "transactionId": "06b02c29a8c1a528c18bab4b6c92d447dc5ff0d99a591ddce2878631c555c97b",
      "outputTransactionId": "88291edf57563b34cb4e4cfae78efb1ad814ef6bff79969b64973a7ec89b59a7",
      "outputIndex": 2,
      "address": "3Wy3BaCjGDWE3bjjZkNo3aWaMz3cYrePMFhchcKovY9uG9vhpAuW"
    },
    {
      "id": "5c3a0991a2f16a19e44ab7f9bafa626654690950a7bb22cfb6b27ea85fbcad2b",
      "value": 77700000,
      "index": 1,
      "spendingProof": "2a6f3500752e5dae22bbe6185a3f85bc45298f784af1519a42127581e671318cf9aede3a5e1a3fea3c1c3e82acec241e08cc7b462a2737e7",
      "transactionId": "06b02c29a8c1a528c18bab4b6c92d447dc5ff0d99a591ddce2878631c555c97b",
      "outputTransactionId": "88291edf57563b34cb4e4cfae78efb1ad814ef6bff79969b64973a7ec89b59a7",
      "outputIndex": 1,
      "address": "3Wy3BaCjGDWE3bjjZkNo3aWaMz3cYrePMFhchcKovY9uG9vhpAuW"
    }
  ],
  "dataInputs": [],
  "outputs": [
    {
      "id": "151386a7845a2798a2a35dc8c41f658fac7b61f4aa08f57c97c912fec10b3d4b",
      "txId": "06b02c29a8c1a528c18bab4b6c92d447dc5ff0d99a591ddce2878631c555c97b",
      "value": 10000000,
      "index": 0,
      "creationHeight": 225125,
      "ergoTree": "0008cd0340191c66444bad57a1c80b19fa86d7483b4cde996747ce2cd76a81f3a5dd2aa6",
      "address": "3Wy3BaCjGDWE3bjjZkNo3aWaMz3cYrePMFhchcKovY9uG9vhpAuW",
      "assets": [
        {
          "tokenId": "61a9e57e6635d02196fadc4ddf40a902c043c692bc6c7b452e02fa467e6e1fd7",
          "index": 0,
          "amount": 1,
          "name": "refresh NFT",
          "decimals": 0,
          "type": "EIP-004"
        }
      ],
      "additionalRegisters": {
        "R4": "0e0b72656672657368204e4654",
        "R5": "0e0b72656672657368204e4654",
        "R6": "0e0130"
      },
      "spentTransactionId": "2aab1f61a3757273de101add778f54b1fe14c026b3cf5dee486308a6d131eafc",
      "mainChain": true
    },
    {
      "id": "93d7311b5a5adf256da4a3f60137c693339a25e735f96c1b00aaa55905f09c71",
      "txId": "06b02c29a8c1a528c18bab4b6c92d447dc5ff0d99a591ddce2878631c555c97b",
      "value": 66600000,
      "index": 1,
      "creationHeight": 225125,
      "ergoTree": "0008cd0340191c66444bad57a1c80b19fa86d7483b4cde996747ce2cd76a81f3a5dd2aa6",
      "address": "3Wy3BaCjGDWE3bjjZkNo3aWaMz3cYrePMFhchcKovY9uG9vhpAuW",
      "assets": [],
      "additionalRegisters": {},
      "spentTransactionId": "ef286c8553b30302b92598c6bfd1c3cb19174a6b44fa91be22a4b7a82ca5f657",
      "mainChain": true
    },
    {
      "id": "eb31b0d20cff2ed7907028c100220ae7ffbf9f9c9da201a66391d42c08a6ac71",
      "txId": "06b02c29a8c1a528c18bab4b6c92d447dc5ff0d99a591ddce2878631c555c97b",
      "value": 6700000,
      "index": 2,
      "creationHeight": 225125,
      "ergoTree": "0008cd0340191c66444bad57a1c80b19fa86d7483b4cde996747ce2cd76a81f3a5dd2aa6",
      "address": "3Wy3BaCjGDWE3bjjZkNo3aWaMz3cYrePMFhchcKovY9uG9vhpAuW",
      "assets": [
        {
          "tokenId": "41fd981d3d747e07b0353e1299558522d519090481cd2daea7833656f7a1e8b4",
          "index": 0,
          "amount": 15,
          "name": "reward token",
          "decimals": 0,
          "type": "EIP-004"
        },
        {
          "tokenId": "61d4e13d8fc4ad1080edb21c1a65698de461be87e130e67e914d7dc0519d3535",
          "index": 1,
          "amount": 15,
          "name": "ballot token",
          "decimals": 0,
          "type": "EIP-004"
        }
      ],
      "additionalRegisters": {},
      "spentTransactionId": "ef286c8553b30302b92598c6bfd1c3cb19174a6b44fa91be22a4b7a82ca5f657",
      "mainChain": true
    },
    {
      "id": "34c3647b9c615ac82a5de8802a667b90c2bc978c76658a876eb3457bba1029d1",
      "txId": "06b02c29a8c1a528c18bab4b6c92d447dc5ff0d99a591ddce2878631c555c97b",
      "value": 1100000,
      "index": 3,
      "creationHeight": 225125,
      "ergoTree": "1005040004000e36100204a00b08cd0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ea02d192a39a8cc7a701730073011001020402d19683030193a38cc7b2a57300000193c2b2a57301007473027303830108cdeeac93b1a57304",
      "address": "Bf1X9JgQTUtgntaer91B24n6kP8L2kqEiQqNf1z97BKo9UbnW3WRP9VXu8BXd1LsYCiYbHJEdWKxkF5YNx5n7m31wsDjbEuB3B13ZMDVBWkepGmWfGa71otpFViHDCuvbw1uNicAQnfuWfnj8fbCa4",
      "assets": [],
      "additionalRegisters": {},
      "spentTransactionId": "3268cddee82451103eb8c953fa0064d290309703c73c0f08ecece7edb1380b05",
      "mainChain": true
    }
  ]
}
```


Discussion / hints:

Fix patch:
diff --git a/ergo-lib/src/chain/transaction/input.rs b/ergo-lib/src/chain/transaction/input.rs
index 988bcca15..bbd52c980 100644
--- a/ergo-lib/src/chain/transaction/input.rs
+++ b/ergo-lib/src/chain/transaction/input.rs
@@ -76,7 +76,13 @@ pub struct Input {
     #[cfg_attr(feature = "json", serde(rename = "boxId", alias = "id"))]
     pub box_id: BoxId,
     /// proof of spending correctness
-    #[cfg_attr(feature = "json", serde(rename = "spendingProof",))]
+    #[cfg_attr(
+        feature = "json",
+        serde(
+            rename = "spendingProof",
+            deserialize_with = "ergotree_ir::chain::json::t_as_string_or_struct"
+        )
+    )]
     pub spending_proof: ProverResult,
 }
 
diff --git a/ergo-lib/src/chain/transaction/input/prover_result.rs b/ergo-lib/src/chain/transaction/input/prover_result.rs
index 00e8af2ec..20f2b2699 100644
--- a/ergo-lib/src/chain/transaction/input/prover_result.rs
+++ b/ergo-lib/src/chain/transaction/input/prover_result.rs
@@ -9,14 +9,10 @@ use ergotree_ir::serialization::SigmaSerializable;
 use ergotree_ir::serialization::SigmaSerializeResult;
 
 #[cfg(feature = "json")]
-use crate::chain::json::context_extension::ContextExtensionSerde;
-#[cfg(feature = "json")]
-use serde::ser::SerializeStruct;
-#[cfg(feature = "json")]
-use serde::{Deserialize, Serialize};
+pub(crate) mod json;
 
 /// Wrapped IR [`ProverResult`] for Serde
-#[cfg_attr(feature = "json", derive(Deserialize))]
+#[cfg_attr(feature = "json", derive(serde::Deserialize))]
 #[derive(PartialEq, Eq, Debug, Clone)]
 pub struct ProverResult {
     /// proof that satisfies final sigma proposition
@@ -35,22 +31,6 @@ pub struct ProverResult {
     pub extension: ContextExtension,
 }
 
-#[cfg(feature = "json")]
-impl Serialize for ProverResult {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-    {
-        let mut s = serializer.serialize_struct("ProverResult", 2)?;
-        s.serialize_field("proofBytes", &String::from(self.proof.clone()))?;
-        s.serialize_field(
-            "extension",
-            &ContextExtensionSerde::from(self.extension.clone()),
-        )?;
-        s.end()
-    }
-}
-
 impl From<ergotree_interpreter::sigma_protocol::prover::ProverResult> for ProverResult {
     fn from(ir: ergotree_interpreter::sigma_protocol::prover::ProverResult) -> Self {
         ProverResult {
diff --git a/ergo-lib/src/chain/transaction/input/prover_result/json.rs b/ergo-lib/src/chain/transaction/input/prover_result/json.rs
new file mode 100644
index 000000000..858cf5f8f
--- /dev/null
+++ b/ergo-lib/src/chain/transaction/input/prover_result/json.rs
@@ -0,0 +1,58 @@
+use std::str::FromStr;
+
+use crate::chain::json::context_extension::ContextExtensionSerde;
+use ergotree_interpreter::sigma_protocol::prover::ContextExtension;
+use serde::ser::SerializeStruct;
+use serde::Serialize;
+
+use super::ProverResult;
+
+impl Serialize for ProverResult {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        let mut s = serializer.serialize_struct("ProverResult", 2)?;
+        s.serialize_field("proofBytes", &String::from(self.proof.clone()))?;
+        s.serialize_field(
+            "extension",
+            &ContextExtensionSerde::from(self.extension.clone()),
+        )?;
+        s.end()
+    }
+}
+
+impl FromStr for ProverResult {
+    type Err = base16::DecodeError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let proof_bytes: Vec<u8> = base16::decode(s)?;
+        Ok(ProverResult {
+            proof: proof_bytes.into(),
+            extension: ContextExtension::empty(),
+        })
+    }
+}
+
+#[allow(clippy::unwrap_used)]
+#[cfg(test)]
+mod tests {
+    use crate::chain::transaction::Input;
+
+    #[test]
+    fn parse_proof_explorer_api() {
+        // https://github.com/ergoplatform/sigma-rust/issues/670
+        let json_str = r#"{
+      "id": "61a9e57e6635d02196fadc4ddf40a902c043c692bc6c7b452e02fa467e6e1fd7",
+      "value": 6700000,
+      "index": 0,
+      "spendingProof": "736d882bbfa1767cef64e718eb74f76b96891fb8ad4e8fdd987ec4550b39d7cda0c285f8346e8f842acbae0b3d8e2b52d039cf1dacefc51c",
+      "transactionId": "06b02c29a8c1a528c18bab4b6c92d447dc5ff0d99a591ddce2878631c555c97b",
+      "outputTransactionId": "88291edf57563b34cb4e4cfae78efb1ad814ef6bff79969b64973a7ec89b59a7",
+      "outputIndex": 2,
+      "address": "3Wy3BaCjGDWE3bjjZkNo3aWaMz3cYrePMFhchcKovY9uG9vhpAuW"
+        }"#;
+        let input: Input = serde_json::from_str(json_str).unwrap();
+        assert_eq!(input.spending_proof.proof.to_bytes().len(), 56);
+    }
+}
diff --git a/ergotree-ir/src/chain/json.rs b/ergotree-ir/src/chain/json.rs
index 6fd89dd91..cf550d91a 100644
--- a/ergotree-ir/src/chain/json.rs
+++ b/ergotree-ir/src/chain/json.rs
@@ -1,5 +1,14 @@
 //! JSON serialization
 
+use std::fmt;
+use std::marker::PhantomData;
+use std::str::FromStr;
+
+use serde::de;
+use serde::de::MapAccess;
+use serde::de::Visitor;
+use serde::Deserialize;
+use serde::Deserializer;
 use serde::Serializer;
 
 pub(crate) mod box_value;
@@ -16,3 +25,56 @@ where
 {
     serializer.serialize_str(&base16::encode_lower(bytes.as_ref()))
 }
+
+// via https://serde.rs/string-or-struct.html
+/// Parses type either from a string or a struct
+pub fn t_as_string_or_struct<'de, T, D, E>(deserializer: D) -> Result<T, D::Error>
+where
+    T: Deserialize<'de> + FromStr<Err = E>,
+    D: Deserializer<'de>,
+    E: std::fmt::Display,
+{
+    // This is a Visitor that forwards string types to T's `FromStr` impl and
+    // forwards map types to T's `Deserialize` impl. The `PhantomData` is to
+    // keep the compiler from complaining about T being an unused generic type
+    // parameter. We need T in order to know the Value type for the Visitor
+    // impl.
+    struct StringOrStruct<T>(PhantomData<fn() -> T>);
+
+    impl<'de, T, FromStrErr> Visitor<'de> for StringOrStruct<T>
+    where
+        T: Deserialize<'de> + FromStr<Err = FromStrErr>,
+        FromStrErr: std::fmt::Display,
+    {
+        type Value = T;
+
+        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+            formatter.write_str("string or map")
+        }
+
+        fn visit_str<E>(self, value: &str) -> Result<T, E>
+        where
+            E: de::Error,
+        {
+            FromStr::from_str(value).map_err(|e| {
+                de::Error::custom(format!(
+                    "error: {}, while parsing from string: {:?}",
+                    e, value
+                ))
+            })
+        }
+
+        fn visit_map<M>(self, map: M) -> Result<T, M::Error>
+        where
+            M: MapAccess<'de>,
+        {
+            // `MapAccessDeserializer` is a wrapper that turns a `MapAccess`
+            // into a `Deserializer`, allowing it to be used as the input to T's
+            // `Deserialize` implementation. T then deserializes itself using
+            // the entries from the map visitor.
+            Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
+        }
+    }
+
+    deserializer.deserialize_any(StringOrStruct(PhantomData))
+}
diff --git a/ergotree-ir/src/chain/json/ergo_box.rs b/ergotree-ir/src/chain/json/ergo_box.rs
index 46147656b..af031c59d 100644
--- a/ergotree-ir/src/chain/json/ergo_box.rs
+++ b/ergotree-ir/src/chain/json/ergo_box.rs
@@ -12,13 +12,9 @@ use crate::mir::constant::Constant;
 use crate::serialization::SigmaParsingError;
 use crate::serialization::SigmaSerializable;
 use crate::serialization::SigmaSerializationError;
-use core::fmt;
 use ergo_chain_types::Base16DecodedBytes;
-use serde::de::{self, MapAccess, Visitor};
-use serde::Deserializer;
 use std::convert::TryFrom;
 use std::convert::TryInto;
-use std::marker::PhantomData;
 use std::str::FromStr;
 
 extern crate derive_more;
@@ -192,7 +188,7 @@ pub enum ErgoBoxFromJsonError {
 }
 
 #[derive(Deserialize, PartialEq, Eq, Debug, Clone)]
-pub struct ConstantHolder(#[serde(deserialize_with = "constant_as_string_or_struct")] RichConstant);
+pub struct ConstantHolder(#[serde(deserialize_with = "super::t_as_string_or_struct")] RichConstant);
 
 impl From<ConstantHolder> for RegisterValue {
     fn from(ch: ConstantHolder) -> Self {
@@ -239,56 +235,6 @@ impl FromStr for RichConstant {
     }
 }
 
-// via https://serde.rs/string-or-struct.html
-pub fn constant_as_string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error>
-where
-    T: Deserialize<'de> + FromStr<Err = ConstantParsingError>,
-    D: Deserializer<'de>,
-{
-    // This is a Visitor that forwards string types to T's `FromStr` impl and
-    // forwards map types to T's `Deserialize` impl. The `PhantomData` is to
-    // keep the compiler from complaining about T being an unused generic type
-    // parameter. We need T in order to know the Value type for the Visitor
-    // impl.
-    struct StringOrStruct<T>(PhantomData<fn() -> T>);
-
-    impl<'de, T> Visitor<'de> for StringOrStruct<T>
-    where
-        T: Deserialize<'de> + FromStr<Err = ConstantParsingError>,
-    {
-        type Value = T;
-
-        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
-            formatter.write_str("string or map")
-        }
-
-        fn visit_str<E>(self, value: &str) -> Result<T, E>
-        where
-            E: de::Error,
-        {
-            FromStr::from_str(value).map_err(|e| {
-                de::Error::custom(format!(
-                    "error: {}, while parsing constant from string: {:?}",
-                    e, value
-                ))
-            })
-        }
-
-        fn visit_map<M>(self, map: M) -> Result<T, M::Error>
-        where
-            M: MapAccess<'de>,
-        {
-            // `MapAccessDeserializer` is a wrapper that turns a `MapAccess`
-            // into a `Deserializer`, allowing it to be used as the input to T's
-            // `Deserialize` implementation. T then deserializes itself using
-            // the entries from the map visitor.
-            Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
-        }
-    }
-
-    deserializer.deserialize_any(StringOrStruct(PhantomData))
-}
-
 #[allow(clippy::panic)]
 #[allow(clippy::unwrap_used)]
 #[cfg(test)]


Test patch: