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"))]
108pub enum TxDatumFieldValue {
109    /// Bytes datum field value.
110    Bytes(String),
111    /// Integer datum field value
112    #[allow(dead_code)]
113    Int(u32),
114}
115
116/// [TxDatumBuilder] is a [TxDatum] builder utility.
117#[derive(Debug, Serialize)]
118pub struct TxDatumBuilder {
119    constructor: usize,
120    fields: Vec<HashMap<TxDatumFieldTypeName, TxDatumFieldValue>>,
121}
122
123impl TxDatumBuilder {
124    /// [TxDatumBuilder] factory
125    pub fn new() -> Self {
126        Self {
127            constructor: 0,
128            fields: Vec::new(),
129        }
130    }
131
132    /// Add a field to the builder
133    pub fn add_field(&mut self, field_value: TxDatumFieldValue) -> &mut TxDatumBuilder {
134        match &field_value {
135            TxDatumFieldValue::Bytes(datum_str) => {
136                // TODO: Remove this chunking of the bytes fields once the cardano-cli 1.36.0+ is released
137                // The bytes fields are currently limited to 128 bytes and need to be chunked in multiple fields
138                let field_type = TxDatumFieldTypeName::from(&field_value);
139                let field_value_chunks = datum_str.as_bytes().chunks(128);
140                for field_value_chunk in field_value_chunks {
141                    let mut field = HashMap::new();
142                    field.insert(
143                        field_type,
144                        TxDatumFieldValue::Bytes(
145                            std::str::from_utf8(field_value_chunk).unwrap().to_string(),
146                        ),
147                    );
148                    self.fields.push(field);
149                }
150            }
151            _ => {
152                let mut field = HashMap::new();
153                field.insert(TxDatumFieldTypeName::from(&field_value), field_value);
154                self.fields.push(field);
155            }
156        }
157
158        self
159    }
160
161    /// Build a [TxDatum]
162    pub fn build(&self) -> Result<TxDatum, TxDatumError> {
163        Ok(TxDatum(
164            serde_json::to_string(&self).map_err(TxDatumError::Build)?,
165        ))
166    }
167}
168
169impl Default for TxDatumBuilder {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175#[cfg(test)]
176mod test {
177    use super::*;
178
179    fn dummy_tx_datum() -> TxDatum {
180        let mut tx_datum_builder = TxDatumBuilder::new();
181
182        tx_datum_builder
183            .add_field(TxDatumFieldValue::Bytes("bytes0".to_string()))
184            .add_field(TxDatumFieldValue::Int(0))
185            .add_field(TxDatumFieldValue::Int(1))
186            .add_field(TxDatumFieldValue::Bytes("bytes1".to_string()))
187            .add_field(TxDatumFieldValue::Bytes("bytes2".to_string()))
188            .add_field(TxDatumFieldValue::Int(2))
189            .add_field(TxDatumFieldValue::Bytes("012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789".to_string()))
190            .build()
191            .expect("tx_datum build should not fail")
192    }
193
194    #[test]
195    fn test_build_tx_datum() {
196        let tx_datum = dummy_tx_datum();
197        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());
198        assert_eq!(tx_datum_expected, tx_datum);
199    }
200
201    #[test]
202    fn test_can_retrieve_field_raw_value_bytes() {
203        let tx_datum = dummy_tx_datum();
204        assert_eq!(
205            "bytes0",
206            tx_datum
207                .get_nth_field_by_type(&TxDatumFieldTypeName::Bytes, 0)
208                .unwrap()
209                .as_str()
210                .unwrap()
211        );
212        assert_eq!(
213            "bytes1",
214            tx_datum
215                .get_nth_field_by_type(&TxDatumFieldTypeName::Bytes, 1)
216                .unwrap()
217                .as_str()
218                .unwrap()
219        );
220        assert_eq!(
221            "bytes2",
222            tx_datum
223                .get_nth_field_by_type(&TxDatumFieldTypeName::Bytes, 2)
224                .unwrap()
225                .as_str()
226                .unwrap()
227        );
228        tx_datum
229            .get_nth_field_by_type(&TxDatumFieldTypeName::Bytes, 100)
230            .expect_err("should have returned an error");
231    }
232
233    #[test]
234    fn test_can_retrieve_field_raw_value_int() {
235        let tx_datum = dummy_tx_datum();
236        assert_eq!(
237            0,
238            tx_datum
239                .get_nth_field_by_type(&TxDatumFieldTypeName::Int, 0)
240                .unwrap()
241                .as_u64()
242                .unwrap()
243        );
244        assert_eq!(
245            1,
246            tx_datum
247                .get_nth_field_by_type(&TxDatumFieldTypeName::Int, 1)
248                .unwrap()
249                .as_u64()
250                .unwrap()
251        );
252        assert_eq!(
253            2,
254            tx_datum
255                .get_nth_field_by_type(&TxDatumFieldTypeName::Int, 2)
256                .unwrap()
257                .as_u64()
258                .unwrap()
259        );
260        tx_datum
261            .get_nth_field_by_type(&TxDatumFieldTypeName::Int, 100)
262            .expect_err("should have returned an error");
263    }
264}