ogmios_client/
types.rs

1//! Common types used in the Ogmios API.
2
3use serde::{Deserialize, Deserializer};
4use sidechain_domain::{McTxHash, UtxoId};
5use std::collections::HashMap;
6use std::fmt::Debug;
7use std::str::FromStr;
8
9#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Default)]
10/// Represents the length of a slot in milliseconds.
11pub struct SlotLength {
12	/// The length of a slot in milliseconds.
13	pub milliseconds: u32,
14}
15
16#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
17/// Represents the time in seconds.
18pub struct TimeSeconds {
19	/// The time in seconds.
20	pub seconds: u64,
21}
22
23#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Default)]
24/// Represents the size of a transaction in bytes.
25pub struct OgmiosBytesSize {
26	/// The size of a transaction in bytes.
27	pub bytes: u32,
28}
29
30#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
31#[serde(rename_all = "camelCase")]
32/// Represents a UTXO.
33pub struct OgmiosUtxo {
34	/// The transaction hash.
35	pub transaction: OgmiosTx,
36	/// The index of the UTXO within the transaction.
37	pub index: u16,
38	/// The Bech32 address of the UTXO.
39	pub address: String,
40	/// The value of the UTXO.
41	pub value: OgmiosValue,
42	/// The datum of the UTXO.
43	pub datum: Option<Datum>,
44	/// The hash of the datum of the UTXO.
45	pub datum_hash: Option<DatumHash>,
46	/// The reference script of the UTXO.
47	pub script: Option<OgmiosScript>,
48}
49
50impl OgmiosUtxo {
51	/// Returns the UTXO ID.
52	pub fn utxo_id(&self) -> UtxoId {
53		UtxoId::new(self.transaction.id, self.index)
54	}
55}
56
57impl core::fmt::Display for OgmiosUtxo {
58	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59		write!(f, "{}#{}", hex::encode(self.transaction.id), self.index)
60	}
61}
62
63#[derive(Clone, Deserialize, Eq, PartialEq)]
64#[serde(transparent)]
65/// Represents a datum.
66pub struct Datum {
67	/// The bytes of the datum.
68	#[serde(deserialize_with = "parse_bytes")]
69	pub bytes: Vec<u8>,
70}
71
72impl From<Vec<u8>> for Datum {
73	fn from(bytes: Vec<u8>) -> Self {
74		Datum { bytes }
75	}
76}
77
78impl std::fmt::Debug for Datum {
79	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80		f.debug_struct("Datum").field("bytes", &hex::encode(&self.bytes)).finish()
81	}
82}
83
84#[derive(Clone, Deserialize, Eq, PartialEq)]
85#[serde(transparent)]
86/// Represents a datum hash.
87pub struct DatumHash {
88	/// The bytes of the datum hash.
89	#[serde(deserialize_with = "parse_bytes_array")]
90	pub bytes: [u8; 32],
91}
92
93impl From<[u8; 32]> for DatumHash {
94	fn from(bytes: [u8; 32]) -> Self {
95		DatumHash { bytes }
96	}
97}
98
99impl std::fmt::Debug for DatumHash {
100	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101		f.debug_struct("DatumHash").field("bytes", &hex::encode(self.bytes)).finish()
102	}
103}
104
105#[derive(Clone, Deserialize, Eq, PartialEq)]
106/// Represents a cardano script.
107pub struct OgmiosScript {
108	/// The language of the script.
109	pub language: String,
110	/// The CBOR representation of the script (in case of Plutus scripts).
111	#[serde(deserialize_with = "parse_bytes")]
112	pub cbor: Vec<u8>,
113	/// The JSON representation of the script (in case of Native scripts).
114	pub json: Option<NativeScript>,
115}
116
117impl std::fmt::Debug for OgmiosScript {
118	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119		f.debug_struct("PlutusScript")
120			.field("language", &self.language)
121			.field("cbor", &hex::encode(&self.cbor))
122			.field("json", &self.json)
123			.finish()
124	}
125}
126
127#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
128#[serde(tag = "clause", rename_all = "lowercase")]
129/// Represents a cardano native script.
130pub enum NativeScript {
131	/// Represents a signature script.
132	Signature {
133		#[serde(deserialize_with = "parse_bytes_array")]
134		/// The public key hash of the signer.
135		from: [u8; 28],
136	},
137	/// Represents an all script.
138	All {
139		/// The scripts to check.
140		from: Vec<NativeScript>,
141	},
142	/// Represents an any script.
143	Any {
144		/// The scripts to check.
145		from: Vec<NativeScript>,
146	},
147	#[serde(rename_all = "camelCase")]
148	/// Represents a some script.
149	Some {
150		/// The scripts to check.
151		from: Vec<NativeScript>,
152		/// The minimum number of scripts that must be satisfied.
153		at_least: u32,
154	},
155	/// Represents a before script.
156	Before {
157		/// The slot number.
158		slot: u64,
159	},
160}
161
162impl<'de> Deserialize<'de> for OgmiosValue {
163	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
164	where
165		D: Deserializer<'de>,
166	{
167		let value = serde_json::Value::deserialize(deserializer)?;
168		TryFrom::try_from(value)
169			.map_err(|e| serde::de::Error::custom(format!("failed to parse OgmiosValue: {e}")))
170	}
171}
172
173/// Represents a script hash.
174type ScriptHash = [u8; 28];
175
176#[derive(Clone, Debug, Default, PartialEq, Eq)]
177/// Represents the value of a UTXO.
178pub struct OgmiosValue {
179	/// The amount of lovelace in the UTXO.
180	pub lovelace: u64,
181	/// The native tokens in the UTXO.
182	pub native_tokens: HashMap<ScriptHash, Vec<Asset>>,
183}
184
185impl OgmiosValue {
186	/// Creates a new UTXO value with only lovelace.
187	pub fn new_lovelace(lovelace: u64) -> Self {
188		Self { lovelace, native_tokens: HashMap::new() }
189	}
190}
191
192/// Represents an asset of an UTXO.
193#[derive(Clone, Debug, PartialEq, Eq)]
194pub struct Asset {
195	/// The name of the asset.
196	pub name: Vec<u8>,
197	/// The amount of the asset.
198	pub amount: u64,
199}
200
201impl TryFrom<serde_json::Value> for OgmiosValue {
202	type Error = &'static str;
203	fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
204		let value = value.as_object().ok_or("expected top level object")?;
205		let mut lovelace = 0u64;
206		let mut native_tokens = HashMap::new();
207		value.into_iter().try_for_each(|(policy_id, assets)| {
208			let asset_to_amount = assets.as_object().ok_or("expected an object")?;
209			if policy_id == "ada" {
210				let amount = asset_to_amount.get("lovelace").ok_or("expected lovelace amount")?;
211				lovelace = amount.as_u64().ok_or("expected lovelace amount to be u64")?;
212				Ok::<(), &'static str>(())
213			} else {
214				let policy_id = hex::decode(policy_id)
215					.map_err(|_| "expected policy id to be hexstring")?
216					.try_into()
217					.map_err(|_| "expected policy id to be 28 bytes")?;
218				let assets: Result<Vec<_>, &str> = asset_to_amount
219					.into_iter()
220					.map(|(asset_name, amount)| {
221						let name = hex::decode(asset_name)
222							.map_err(|_| "expected asset name to be hexstring");
223						let amount = amount
224							.as_number()
225							.and_then(|n| n.clone().as_u64())
226							.ok_or("expected asset amount to be u64");
227						name.and_then(|name| amount.map(|amount| Asset { name, amount }))
228					})
229					.collect();
230				native_tokens.insert(policy_id, assets?);
231				Ok::<(), &'static str>(())
232			}
233		})?;
234		Ok(Self { lovelace, native_tokens })
235	}
236}
237
238#[derive(Clone, Default, Deserialize, Eq, PartialEq)]
239/// Transaction identifier.
240pub struct OgmiosTx {
241	/// The transaction hash.
242	#[serde(deserialize_with = "parse_bytes_array")]
243	pub id: [u8; 32],
244}
245
246impl From<McTxHash> for OgmiosTx {
247	fn from(id: McTxHash) -> Self {
248		Self { id: id.0 }
249	}
250}
251
252impl From<[u8; 32]> for OgmiosTx {
253	fn from(id: [u8; 32]) -> Self {
254		Self { id }
255	}
256}
257
258impl Debug for OgmiosTx {
259	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260		f.debug_struct("OgmiosTx").field("id", &hex::encode(self.id)).finish()
261	}
262}
263
264pub(crate) fn parse_bytes<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
265where
266	D: Deserializer<'de>,
267{
268	let buf = String::deserialize(deserializer)?;
269	hex::decode(buf).map_err(serde::de::Error::custom)
270}
271
272pub(crate) fn parse_bytes_array<'de, D, const N: usize>(
273	deserializer: D,
274) -> Result<[u8; N], D::Error>
275where
276	D: Deserializer<'de>,
277{
278	let bytes = parse_bytes(deserializer)?;
279	TryFrom::try_from(bytes).map_err(|_| serde::de::Error::custom(format!("expected {} bytes", N)))
280}
281
282pub(crate) fn parse_fraction_decimal<'de, D>(deserializer: D) -> Result<fraction::Decimal, D::Error>
283where
284	D: Deserializer<'de>,
285{
286	let buf = String::deserialize(deserializer)?;
287	fraction::Decimal::from_str(&buf).map_err(serde::de::Error::custom)
288}
289
290pub(crate) fn parse_fraction_ratio_u64<'de, D>(
291	deserializer: D,
292) -> Result<fraction::Ratio<u64>, D::Error>
293where
294	D: Deserializer<'de>,
295{
296	let buf = String::deserialize(deserializer)?;
297	fraction::Ratio::<u64>::from_str(&buf).map_err(serde::de::Error::custom)
298}
299
300#[cfg(test)]
301mod tests {
302	use super::OgmiosUtxo;
303	use crate::types::{Asset, NativeScript, OgmiosScript, OgmiosTx, OgmiosValue};
304	use hex_literal::hex;
305
306	#[test]
307	fn parse_ada_only_value() {
308		let value = serde_json::json!({
309			"ada": {
310				"lovelace": 18446744073709551615u64
311			}
312		});
313		let value: OgmiosValue = serde_json::from_value(value).unwrap();
314		assert_eq!(value.lovelace, 18446744073709551615u64);
315		assert_eq!(value.native_tokens.len(), 0);
316	}
317
318	#[test]
319	fn parse_value_with_native_tokens() {
320		let value = serde_json::json!({
321			"ada": {
322				"lovelace": 3
323			},
324			"e0d4479b3dbb53b1aecd48f7ef524a9cf166585923d91d9c72ed02cb": {
325				"": 18446744073709551615i128
326			},
327			"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": {
328				"aaaa": 1,
329			}
330		});
331		let value: OgmiosValue = serde_json::from_value(value).unwrap();
332		assert_eq!(value.lovelace, 3);
333		assert_eq!(
334			value
335				.native_tokens
336				.get(&hex!("e0d4479b3dbb53b1aecd48f7ef524a9cf166585923d91d9c72ed02cb"))
337				.unwrap()
338				.clone(),
339			vec![Asset { name: vec![], amount: 18446744073709551615u64 }]
340		);
341		let assets = value
342			.native_tokens
343			.get(&hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
344			.unwrap()
345			.clone();
346		assert_eq!(
347			assets.iter().find(|asset| asset.name == hex!("aaaa").to_vec()).unwrap().amount,
348			1
349		);
350	}
351
352	#[test]
353	fn parse_utxo_with_datum() {
354		let value = serde_json::json!({
355			"transaction": { "id": "106b0d7d1544c97941777041699412fb7c8b94855210987327199620c0599580" },
356			"index": 1,
357			"address": "addr_test1vqezxrh24ts0775hulcg3ejcwj7hns8792vnn8met6z9gwsxt87zy",
358			"value": { "ada": {	"lovelace": 1356118 } },
359			"datum": "d8799fff",
360			"datumHash": "c248757d390181c517a5beadc9c3fe64bf821d3e889a963fc717003ec248757d"
361		});
362		let utxo: OgmiosUtxo = serde_json::from_value(value).unwrap();
363		assert_eq!(
364			utxo,
365			OgmiosUtxo {
366				transaction: OgmiosTx {
367					id: hex!("106b0d7d1544c97941777041699412fb7c8b94855210987327199620c0599580")
368				},
369				index: 1,
370				address: "addr_test1vqezxrh24ts0775hulcg3ejcwj7hns8792vnn8met6z9gwsxt87zy"
371					.to_string(),
372				value: OgmiosValue::new_lovelace(1356118),
373				datum: Some(hex!("d8799fff").to_vec().into()),
374				datum_hash: Some(
375					hex!("c248757d390181c517a5beadc9c3fe64bf821d3e889a963fc717003ec248757d").into()
376				),
377				script: None,
378			}
379		)
380	}
381
382	#[test]
383	fn parse_utxo_with_plutus_script() {
384		let value = serde_json::json!({
385			"transaction": {
386			  "id": "106b0d7d1544c97941777041699412fb7c8b94855210987327199620c0599580"
387			},
388			"index": 1,
389			"address": "addr_test1vqezxrh24ts0775hulcg3ejcwj7hns8792vnn8met6z9gwsxt87zy",
390			"value": { "ada": { "lovelace": 1356118 } },
391			"script": {
392				"cbor": "aabbccdd00112233",
393				"language": "plutus:v3"
394			}
395		});
396		let utxo: OgmiosUtxo = serde_json::from_value(value).unwrap();
397		assert_eq!(
398			utxo,
399			OgmiosUtxo {
400				transaction: OgmiosTx {
401					id: hex!("106b0d7d1544c97941777041699412fb7c8b94855210987327199620c0599580")
402				},
403				index: 1,
404				address: "addr_test1vqezxrh24ts0775hulcg3ejcwj7hns8792vnn8met6z9gwsxt87zy"
405					.to_string(),
406				value: OgmiosValue::new_lovelace(1356118),
407				datum: None,
408				datum_hash: None,
409				script: Some(OgmiosScript {
410					language: "plutus:v3".into(),
411					cbor: hex!("aabbccdd00112233").to_vec(),
412					json: None,
413				})
414			}
415		)
416	}
417
418	#[test]
419	fn parse_utxo_with_native_script() {
420		let value = serde_json::json!({
421			"transaction": { "id": "106b0d7d1544c97941777041699412fb7c8b94855210987327199620c0599580" },
422			"index": 1,
423			"address": "addr_test1vqezxrh24ts0775hulcg3ejcwj7hns8792vnn8met6z9gwsxt87zy",
424			"value": { "ada": {	"lovelace": 1356118 } },
425			"script": {
426				"language": "native",
427				"json": {
428					"clause": "some",
429					"atLeast": 1,
430					"from":[
431						{"clause": "signature","from": "a1a2a3a4a5a6a7a1a2a3a4a5a6a7a1a2a3a4a5a6a7a1a2a3a4a5a6a7"},
432						{"clause": "before", "slot": 100 }
433					]
434				},
435				"cbor": "830301818200581ce8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b"
436			}
437		});
438		let utxo: OgmiosUtxo = serde_json::from_value(value).unwrap();
439		assert_eq!(
440			utxo,
441			OgmiosUtxo {
442				transaction: OgmiosTx {
443					id: hex!("106b0d7d1544c97941777041699412fb7c8b94855210987327199620c0599580")
444				},
445				index: 1,
446				address: "addr_test1vqezxrh24ts0775hulcg3ejcwj7hns8792vnn8met6z9gwsxt87zy"
447					.to_string(),
448				value: OgmiosValue::new_lovelace(1356118),
449				datum: None,
450				datum_hash: None,
451				script: Some(OgmiosScript {
452					language: "native".into(),
453					json: Some(NativeScript::Some {
454						from: vec![
455							NativeScript::Signature {
456								from: hex!(
457									"a1a2a3a4a5a6a7a1a2a3a4a5a6a7a1a2a3a4a5a6a7a1a2a3a4a5a6a7"
458								)
459							},
460							NativeScript::Before { slot: 100 }
461						],
462						at_least: 1
463					}),
464					cbor: hex!(
465						"830301818200581ce8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b"
466					)
467					.to_vec()
468				})
469			}
470		)
471	}
472}