1use 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)]
10pub struct SlotLength {
12 pub milliseconds: u32,
14}
15
16#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
17pub struct TimeSeconds {
19 pub seconds: u64,
21}
22
23#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Default)]
24pub struct OgmiosBytesSize {
26 pub bytes: u32,
28}
29
30#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
31#[serde(rename_all = "camelCase")]
32pub struct OgmiosUtxo {
34 pub transaction: OgmiosTx,
36 pub index: u16,
38 pub address: String,
40 pub value: OgmiosValue,
42 pub datum: Option<Datum>,
44 pub datum_hash: Option<DatumHash>,
46 pub script: Option<OgmiosScript>,
48}
49
50impl OgmiosUtxo {
51 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)]
65pub struct Datum {
67 #[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)]
86pub struct DatumHash {
88 #[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)]
106pub struct OgmiosScript {
108 pub language: String,
110 #[serde(deserialize_with = "parse_bytes")]
112 pub cbor: Vec<u8>,
113 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")]
129pub enum NativeScript {
131 Signature {
133 #[serde(deserialize_with = "parse_bytes_array")]
134 from: [u8; 28],
136 },
137 All {
139 from: Vec<NativeScript>,
141 },
142 Any {
144 from: Vec<NativeScript>,
146 },
147 #[serde(rename_all = "camelCase")]
148 Some {
150 from: Vec<NativeScript>,
152 at_least: u32,
154 },
155 Before {
157 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
173type ScriptHash = [u8; 28];
175
176#[derive(Clone, Debug, Default, PartialEq, Eq)]
177pub struct OgmiosValue {
179 pub lovelace: u64,
181 pub native_tokens: HashMap<ScriptHash, Vec<Asset>>,
183}
184
185impl OgmiosValue {
186 pub fn new_lovelace(lovelace: u64) -> Self {
188 Self { lovelace, native_tokens: HashMap::new() }
189 }
190}
191
192#[derive(Clone, Debug, PartialEq, Eq)]
194pub struct Asset {
195 pub name: Vec<u8>,
197 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)]
239pub struct OgmiosTx {
241 #[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}