mithril_common/chain_observer/
model.rs

1use anyhow::anyhow;
2use serde::Serialize;
3use serde_json::Value;
4use std::collections::HashMap;
5use strum::{Display, EnumDiscriminants};
6use thiserror::Error;
7
8use crate::{StdError, StdResult};
9
10cfg_fs! {
11    use serde::Deserialize;
12    use anyhow::Context;
13    use pallas_codec::minicbor::{Decode, Decoder, decode};
14    use pallas_primitives::{alonzo::PlutusData, ToCanonicalJson};
15
16    /// [Datum] represents an inline datum from UTxO.
17    #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
18    #[serde(rename_all = "lowercase")]
19    pub struct Datum(pub PlutusData);
20
21    impl ToCanonicalJson for Datum {
22        fn to_json(&self) -> serde_json::Value {
23            self.0.to_json()
24        }
25    }
26
27    impl<'a, C> Decode<'a, C> for Datum {
28        fn decode(d: &mut Decoder<'a>, ctx: &mut C) -> Result<Self, decode::Error> {
29            PlutusData::decode(d, ctx).map(Datum)
30        }
31    }
32
33    /// Inspects the given bytes and returns a decoded `R` instance.
34    pub fn try_inspect<R>(inner: Vec<u8>) -> StdResult<R>
35    where
36        for<'b> R: Decode<'b, ()>,
37    {
38        decode(&inner).map_err(|e| anyhow!(e)).with_context(|| {
39            format!(
40                "failed to decode datum: {}",
41                hex::encode(&inner)
42            )
43        })
44    }
45
46    /// [Datums] represents a list of [TxDatum].
47    pub type Datums = Vec<TxDatum>;
48}
49
50/// [ChainAddress] represents an on chain address.
51pub type ChainAddress = String;
52
53/// [TxDatum] related errors.
54#[derive(Debug, Error)]
55pub enum TxDatumError {
56    /// Error raised when the content could not be parsed.
57    #[error("could not parse tx datum")]
58    InvalidContent(#[source] StdError),
59
60    /// Error raised when building the tx datum failed.
61    #[error("could not build tx datum")]
62    Build(#[source] serde_json::Error),
63}
64
65/// [TxDatum] represents transaction Datum.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct TxDatum(pub String);
68
69impl TxDatum {
70    /// Retrieves the fields of the datum with given type
71    pub fn get_fields_by_type(&self, type_name: &TxDatumFieldTypeName) -> StdResult<Vec<Value>> {
72        let tx_datum_raw = &self.0;
73        // 1- Parse the Utxo raw data to a hashmap
74        let v: HashMap<String, Value> = serde_json::from_str(tx_datum_raw).map_err(|e| {
75            TxDatumError::InvalidContent(anyhow!(e).context("tx datum was = '{tx_datum_raw}'"))
76        })?;
77        // 2- Convert the 'fields' entry to a vec of json objects
78        let fields = v.get("fields").ok_or_else(|| {
79            TxDatumError::InvalidContent(
80                anyhow!("Error: missing 'fields' entry, tx datum was = '{tx_datum_raw}'"),
81            )
82        })?.as_array().ok_or_else(|| {
83            TxDatumError::InvalidContent(
84                anyhow!("Error: 'fields' entry is not correctly structured, tx datum was = '{tx_datum_raw}'"),
85            )
86        })?;
87        // 3- Filter the vec (keep the ones that match the given type), and retrieve the nth entry of this filtered vec
88        Ok(fields
89            .iter()
90            .filter(|&field| field.get(type_name.to_string()).is_some())
91            .map(|field| field.get(type_name.to_string()).unwrap().to_owned())
92            .collect::<_>())
93    }
94
95    /// Retrieves the nth field of the datum with given type
96    pub fn get_nth_field_by_type(
97        &self,
98        type_name: &TxDatumFieldTypeName,
99        index: usize,
100    ) -> StdResult<Value> {
101        Ok(self
102            .get_fields_by_type(type_name)?
103            .get(index)
104            .ok_or_else(|| {
105                TxDatumError::InvalidContent(anyhow!("Error: missing field at index {index}"))
106            })?
107            .to_owned())
108    }
109}
110
111/// [TxDatumFieldValue] represents a field value of TxDatum.
112#[derive(Debug, EnumDiscriminants, Serialize, Display)]
113#[serde(untagged, rename_all = "lowercase")]
114#[strum(serialize_all = "lowercase")]
115#[strum_discriminants(derive(Serialize, Hash, Display))]
116#[strum_discriminants(name(TxDatumFieldTypeName))]
117#[strum_discriminants(strum(serialize_all = "lowercase"))]
118#[strum_discriminants(serde(rename_all = "lowercase"))]
119pub enum TxDatumFieldValue {
120    /// Bytes datum field value.
121    Bytes(String),
122    /// Integer datum field value
123    #[allow(dead_code)]
124    Int(u32),
125}
126
127/// [TxDatumBuilder] is a [TxDatum] builder utility.
128#[derive(Debug, Serialize)]
129pub struct TxDatumBuilder {
130    constructor: usize,
131    fields: Vec<HashMap<TxDatumFieldTypeName, TxDatumFieldValue>>,
132}
133
134impl TxDatumBuilder {
135    /// [TxDatumBuilder] factory
136    pub fn new() -> Self {
137        Self {
138            constructor: 0,
139            fields: Vec::new(),
140        }
141    }
142
143    /// Add a field to the builder
144    pub fn add_field(&mut self, field_value: TxDatumFieldValue) -> &mut TxDatumBuilder {
145        match &field_value {
146            TxDatumFieldValue::Bytes(datum_str) => {
147                // TODO: Remove this chunking of the bytes fields once the cardano-cli 1.36.0+ is released
148                // The bytes fields are currently limited to 128 bytes and need to be chunked in multiple fields
149                let field_type = TxDatumFieldTypeName::from(&field_value);
150                let field_value_chunks = datum_str.as_bytes().chunks(128);
151                for field_value_chunk in field_value_chunks {
152                    let mut field = HashMap::new();
153                    field.insert(
154                        field_type,
155                        TxDatumFieldValue::Bytes(
156                            std::str::from_utf8(field_value_chunk).unwrap().to_string(),
157                        ),
158                    );
159                    self.fields.push(field);
160                }
161            }
162            _ => {
163                let mut field = HashMap::new();
164                field.insert(TxDatumFieldTypeName::from(&field_value), field_value);
165                self.fields.push(field);
166            }
167        }
168
169        self
170    }
171
172    /// Build a [TxDatum]
173    pub fn build(&self) -> Result<TxDatum, TxDatumError> {
174        Ok(TxDatum(
175            serde_json::to_string(&self).map_err(TxDatumError::Build)?,
176        ))
177    }
178}
179
180impl Default for TxDatumBuilder {
181    fn default() -> Self {
182        Self::new()
183    }
184}
185
186#[cfg(test)]
187mod test {
188    use super::*;
189
190    fn dummy_tx_datum() -> TxDatum {
191        let mut tx_datum_builder = TxDatumBuilder::new();
192        let tx_datum = tx_datum_builder
193            .add_field(TxDatumFieldValue::Bytes("bytes0".to_string()))
194            .add_field(TxDatumFieldValue::Int(0))
195            .add_field(TxDatumFieldValue::Int(1))
196            .add_field(TxDatumFieldValue::Bytes("bytes1".to_string()))
197            .add_field(TxDatumFieldValue::Bytes("bytes2".to_string()))
198            .add_field(TxDatumFieldValue::Int(2))
199            .add_field(TxDatumFieldValue::Bytes("012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789".to_string()))
200            .build()
201            .expect("tx_datum build should not fail");
202        tx_datum
203    }
204
205    #[test]
206    fn test_build_tx_datum() {
207        let tx_datum = dummy_tx_datum();
208        let tx_datum_expected = TxDatum(r#"{"constructor":0,"fields":[{"bytes":"bytes0"},{"int":0},{"int":1},{"bytes":"bytes1"},{"bytes":"bytes2"},{"int":2},{"bytes":"01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567"},{"bytes":"8901234567890123456789"}]}"#.to_string());
209        assert_eq!(tx_datum_expected, tx_datum);
210    }
211
212    #[test]
213    fn test_can_retrieve_field_raw_value_bytes() {
214        let tx_datum = dummy_tx_datum();
215        assert_eq!(
216            "bytes0",
217            tx_datum
218                .get_nth_field_by_type(&TxDatumFieldTypeName::Bytes, 0)
219                .unwrap()
220                .as_str()
221                .unwrap()
222        );
223        assert_eq!(
224            "bytes1",
225            tx_datum
226                .get_nth_field_by_type(&TxDatumFieldTypeName::Bytes, 1)
227                .unwrap()
228                .as_str()
229                .unwrap()
230        );
231        assert_eq!(
232            "bytes2",
233            tx_datum
234                .get_nth_field_by_type(&TxDatumFieldTypeName::Bytes, 2)
235                .unwrap()
236                .as_str()
237                .unwrap()
238        );
239        tx_datum
240            .get_nth_field_by_type(&TxDatumFieldTypeName::Bytes, 100)
241            .expect_err("should have returned an error");
242    }
243
244    #[test]
245    fn test_can_retrieve_field_raw_value_int() {
246        let tx_datum = dummy_tx_datum();
247        assert_eq!(
248            0,
249            tx_datum
250                .get_nth_field_by_type(&TxDatumFieldTypeName::Int, 0)
251                .unwrap()
252                .as_u64()
253                .unwrap()
254        );
255        assert_eq!(
256            1,
257            tx_datum
258                .get_nth_field_by_type(&TxDatumFieldTypeName::Int, 1)
259                .unwrap()
260                .as_u64()
261                .unwrap()
262        );
263        assert_eq!(
264            2,
265            tx_datum
266                .get_nth_field_by_type(&TxDatumFieldTypeName::Int, 2)
267                .unwrap()
268                .as_u64()
269                .unwrap()
270        );
271        tx_datum
272            .get_nth_field_by_type(&TxDatumFieldTypeName::Int, 100)
273            .expect_err("should have returned an error");
274    }
275}