mithril_cardano_node_chain/entities/
datum.rs

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