mithril_cardano_node_chain/entities/
datum.rs

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