partner_chains_plutus_data/
lib.rs

1//! This crate contains datum types used by Plutus smart contracts from [raw_scripts::SCRIPTS].
2#![deny(missing_docs)]
3use cardano_serialization_lib::{PlutusData, PlutusList};
4
5pub mod bridge;
6pub mod candidate_keys;
7pub mod d_param;
8pub mod governed_map;
9pub mod permissioned_candidates;
10pub mod registered_candidates;
11pub mod reserve;
12pub mod version_oracle;
13
14#[derive(Debug, PartialEq, thiserror::Error)]
15#[error("Could not decode {datum:?} to {to}: {msg}")]
16/// Error type for decoding failures of [PlutusData].
17pub struct DataDecodingError {
18	datum: PlutusData,
19	to: String,
20	msg: String,
21}
22
23type DecodingResult<T> = std::result::Result<T, DataDecodingError>;
24
25/// Extension trait for [PlutusData].
26pub trait PlutusDataExtensions {
27	/// Tries to interpret a [PlutusData] as [u64].
28	fn as_u64(&self) -> Option<u64>;
29	/// Tries to interpret a [PlutusData] as [u32].
30	fn as_u32(&self) -> Option<u32>;
31	/// Tries to interpret a [PlutusData] as [u16].
32	fn as_u16(&self) -> Option<u16>;
33}
34
35impl PlutusDataExtensions for PlutusData {
36	fn as_u64(&self) -> Option<u64> {
37		self.as_integer()?.as_u64().map(u64::from)
38	}
39	fn as_u32(&self) -> Option<u32> {
40		u32::try_from(self.as_integer()?.as_u64()?).ok()
41	}
42	fn as_u16(&self) -> Option<u16> {
43		u16::try_from(self.as_u32()?).ok()
44	}
45}
46
47/// Trait that provides decoding of versioned generic plutus data.
48///
49/// Versioned generic plutus data contain a version number and two data sections:
50/// - datum - the data with stable schema, read and validated by smart contracts
51/// - appendix - generic data with evolving schema indicated by the version number, not used by smart contracts
52///
53/// The corresponding definition in the smart contracts repo is:
54/// ```haskell
55/// data VersionedGenericDatum a = VersionedGenericDatum
56///     { datum :: a
57///     , genericData :: BuiltinData
58///     , version :: Integer
59///     }
60/// ```
61pub(crate) trait VersionedDatum: Sized {
62	/// Parses versioned plutus data.
63	fn decode(data: &PlutusData) -> DecodingResult<Self>;
64}
65
66/// Trait that provides decoding of versioned generic plutus data with a legacy schema support.
67///
68/// It is assumed that versions 0 and legacy are equivalent.
69pub(crate) trait VersionedDatumWithLegacy: Sized {
70	const NAME: &str;
71
72	/// Parses plutus data schema that was used before datum versioning was added. Kept for backwards compatibility.
73	fn decode_legacy(data: &PlutusData) -> Result<Self, String>;
74
75	/// Parses versioned plutus data.
76	///
77	/// Parameters:
78	/// * `version` - version number
79	/// * `datum` - datum with schema specified by smart-contract
80	/// * `appendix` - generic data ignored by smart-contract logic, schema is version dependent
81	fn decode_versioned(
82		version: u64,
83		datum: &PlutusData,
84		appendix: &PlutusData,
85	) -> Result<Self, String>;
86}
87
88impl<T: VersionedDatumWithLegacy> VersionedDatum for T {
89	fn decode(data: &PlutusData) -> DecodingResult<Self> {
90		(match plutus_data_version_and_payload(data) {
91			None => Self::decode_legacy(data),
92			Some(VersionedGenericDatum { datum, appendix, version }) => {
93				Self::decode_versioned(version, &datum, &appendix)
94			},
95		})
96		.map_err(|msg| decoding_error_and_log(data, Self::NAME, &msg))
97	}
98}
99
100fn plutus_data_version_and_payload(data: &PlutusData) -> Option<VersionedGenericDatum> {
101	let fields = data.as_list().filter(|outer_list| outer_list.len() == 3)?;
102
103	Some(VersionedGenericDatum {
104		datum: fields.get(0),
105		appendix: fields.get(1),
106		version: fields.get(2).as_u64()?,
107	})
108}
109
110fn decoding_error_and_log(data: &PlutusData, to: &str, msg: &str) -> DataDecodingError {
111	log::error!("Could not decode {data:?} to {to}: {msg}");
112	DataDecodingError { datum: data.clone(), to: to.to_string(), msg: msg.to_string() }
113}
114
115/// This struct has the same shape as `VersionedGenericDatum` from smart-contracts.
116/// It is used to help implementing a proper `From` trait for `PlutusData` for
117/// datum types.
118pub struct VersionedGenericDatum {
119	/// data that is interpreted by smart-contract
120	pub datum: PlutusData,
121	/// generic data ignored by smart-contract logic, schema is version dependent
122	pub appendix: PlutusData,
123	/// version number
124	pub version: u64,
125}
126
127impl From<VersionedGenericDatum> for PlutusData {
128	fn from(value: VersionedGenericDatum) -> Self {
129		let mut list = PlutusList::new();
130		list.add(&value.datum);
131		list.add(&value.appendix);
132		list.add(&PlutusData::new_integer(&value.version.into()));
133		PlutusData::new_list(&list)
134	}
135}
136
137/// Script hash of plutus scripts for encoding into PlutusData
138#[derive(Clone, Debug, PartialEq)]
139pub struct ScriptHash([u8; 28]);
140
141impl TryFrom<PlutusData> for ScriptHash {
142	type Error = DataDecodingError;
143	fn try_from(datum: PlutusData) -> DecodingResult<Self> {
144		datum
145			.as_bytes()
146			.and_then(|bytes| Some(ScriptHash(bytes.try_into().ok()?)))
147			.ok_or_else(|| decoding_error_and_log(&datum, "ScriptHash", "Expected [u8;32]"))
148	}
149}
150
151impl From<ScriptHash> for PlutusData {
152	fn from(datum: ScriptHash) -> Self {
153		PlutusData::new_bytes(datum.0.to_vec())
154	}
155}
156
157impl From<[u8; 28]> for ScriptHash {
158	fn from(value: [u8; 28]) -> Self {
159		ScriptHash(value)
160	}
161}
162
163impl From<cardano_serialization_lib::ScriptHash> for ScriptHash {
164	fn from(value: cardano_serialization_lib::ScriptHash) -> Self {
165		ScriptHash(value.to_bytes().as_slice().try_into().expect("infallible conversion"))
166	}
167}
168
169#[cfg(test)]
170pub(crate) mod test_helpers {
171	use cardano_serialization_lib::PlutusData;
172	macro_rules! test_plutus_data {
173		($json:tt) => {
174			cardano_serialization_lib::encode_json_value_to_plutus_datum(
175				serde_json::json!($json),
176				cardano_serialization_lib::PlutusDatumSchema::DetailedSchema,
177			)
178			.expect("test data is valid")
179		};
180	}
181	pub(crate) use test_plutus_data;
182
183	pub(crate) fn json_to_plutus_data(json: serde_json::Value) -> PlutusData {
184		cardano_serialization_lib::encode_json_value_to_plutus_datum(
185			json,
186			cardano_serialization_lib::PlutusDatumSchema::DetailedSchema,
187		)
188		.expect("test data is valid")
189	}
190
191	pub(crate) fn plutus_data_to_json(data: PlutusData) -> serde_json::Value {
192		cardano_serialization_lib::decode_plutus_datum_to_json_value(
193			&data,
194			cardano_serialization_lib::PlutusDatumSchema::DetailedSchema,
195		)
196		.expect("test data is valid")
197	}
198}