diff --git a/pallas-primitives/Cargo.toml b/pallas-primitives/Cargo.toml index 13e098e..7e15377 100644 --- a/pallas-primitives/Cargo.toml +++ b/pallas-primitives/Cargo.toml @@ -20,3 +20,9 @@ pallas-crypto = { version = "0.8.0", path = "../pallas-crypto" } pallas-codec = { version = "0.8.0", path = "../pallas-codec" } base58 = "0.2.0" bech32 = "0.8.1" +serde = { version ="1.0.136", optional = true } +serde_json = { version ="1.0.79", optional = true } + +[features] +json = ["serde", "serde_json"] +default = ["json"] \ No newline at end of file diff --git a/pallas-primitives/src/alonzo/crypto.rs b/pallas-primitives/src/alonzo/crypto.rs index f07424b..bf1c548 100644 --- a/pallas-primitives/src/alonzo/crypto.rs +++ b/pallas-primitives/src/alonzo/crypto.rs @@ -1,4 +1,4 @@ -use super::{AuxiliaryData, Header, PlutusData, TransactionBody}; +use super::{AuxiliaryData, Header, NativeScript, PlutusData, TransactionBody}; use pallas_crypto::hash::{Hash, Hasher}; pub fn hash_block_header(data: &Header) -> Hash<32> { @@ -14,10 +14,23 @@ pub fn hash_transaction(data: &TransactionBody) -> Hash<32> { Hasher::<256>::hash_cbor(data) } +#[deprecated(note = "use PlutusData::to_hash instead")] pub fn hash_plutus_data(data: &PlutusData) -> Hash<32> { Hasher::<256>::hash_cbor(data) } +impl NativeScript { + pub fn to_hash(&self) -> Hash<28> { + Hasher::<224>::hash_cbor(self) + } +} + +impl PlutusData { + pub fn to_hash(&self) -> Hash<32> { + Hasher::<256>::hash_cbor(self) + } +} + impl TransactionBody { pub fn to_hash(&self) -> Hash<32> { Hasher::<256>::hash_cbor(self) diff --git a/pallas-primitives/src/alonzo/json.rs b/pallas-primitives/src/alonzo/json.rs new file mode 100644 index 0000000..4b8dcab --- /dev/null +++ b/pallas-primitives/src/alonzo/json.rs @@ -0,0 +1,134 @@ +use serde_json::json; + +use crate::ToCanonicalJson; + +// infered from https://github.com/input-output-hk/cardano-node/blob/c1efb2f97134c0607c982246a36e3da7266ac194/cardano-api/src/Cardano/Api/ScriptData.hs#L254 +impl ToCanonicalJson for super::PlutusData { + fn to_json(&self) -> serde_json::Value { + match self { + super::PlutusData::Constr(x) => { + let constructor = x.prefix.map(|x| x as u64).unwrap_or(x.tag); + let fields: Vec<_> = x.values.iter().map(|i| i.to_json()).collect(); + json!({ "constructor": constructor, "fields": fields }) + } + super::PlutusData::Map(x) => { + let map: Vec<_> = x + .iter() + .map(|(k, v)| json!({ "k": k.to_json(), "v": v.to_json() })) + .collect(); + json!({ "map": map }) + } + super::PlutusData::BigInt(int) => match int { + super::BigInt::Int(n) => match i64::try_from(*n) { + Ok(x) => json!({ "int": x }), + Err(_) => json!({ "bignint": hex::encode(i128::from(*n).to_be_bytes()) }), + }, + // WARNING / TODO: the CDDL shows a bignum variants of arbitrary length expressed as + // bytes, but I can't find the corresponding mapping to JSON in the + // Haskell implementation. Not sure what I'm missing. For the time + // being, I'll invent a new JSON expression that uses hex strings as + // a way to express the values. + super::BigInt::BigUInt(x) => json!({ "biguint": hex::encode(x.as_slice())}), + super::BigInt::BigNInt(x) => json!({ "bignint": hex::encode(x.as_slice())}), + }, + super::PlutusData::BoundedBytes(x) => json!({ "bytes": hex::encode(x.as_slice())}), + super::PlutusData::Array(x) => { + let list: Vec<_> = x.iter().map(|i| i.to_json()).collect(); + json!({ "list": list }) + } + super::PlutusData::ArrayIndef(x) => { + let list: Vec<_> = x.iter().map(|i| i.to_json()).collect(); + json!({ "list": list }) + } + } + } +} + +impl ToCanonicalJson for super::NativeScript { + fn to_json(&self) -> serde_json::Value { + match self { + super::NativeScript::ScriptPubkey(x) => { + json!({ "keyHash": x.to_string(), "type": "sig"}) + } + super::NativeScript::ScriptAll(x) => { + let scripts: Vec<_> = x.iter().map(|i| i.to_json()).collect(); + json!({ "type": "all", "scripts": scripts}) + } + super::NativeScript::ScriptAny(x) => { + let scripts: Vec<_> = x.iter().map(|i| i.to_json()).collect(); + json!({ "type": "any", "scripts": scripts}) + } + super::NativeScript::ScriptNOfK(n, k) => { + let scripts: Vec<_> = k.iter().map(|i| i.to_json()).collect(); + json!({ "type": "atLeast", "required": n, "scripts" : scripts }) + } + super::NativeScript::InvalidBefore(slot) => json!({ "type": "before", "slot": slot }), + super::NativeScript::InvalidHereafter(slot) => json!({"type": "after", "slot": slot }), + } + } +} + +#[cfg(test)] +mod tests { + use crate::{alonzo::BlockWrapper, Fragment, ToCanonicalJson}; + + #[test] + fn test_datums_serialize_as_expected() { + let test_blocks = vec![( + include_str!("test_data/test9.block"), + include_str!("test_data/test9.datums"), + )]; + + for (idx, (block_str, jsonl_str)) in test_blocks.iter().enumerate() { + println!("decoding json block {}", idx + 1); + + let bytes = hex::decode(block_str).expect(&format!("bad block file {}", idx)); + + let BlockWrapper(_, block) = BlockWrapper::decode_fragment(&bytes[..]) + .expect(&format!("error decoding cbor for file {}", idx)); + + let mut datums = jsonl_str.lines(); + + for ws in block.transaction_witness_sets.iter() { + if let Some(pds) = &ws.plutus_data { + for pd in pds.iter() { + let expected: serde_json::Value = + serde_json::from_str(datums.next().unwrap()).unwrap(); + let current = pd.to_json(); + assert_eq!(current, expected); + } + } + } + } + } + + #[test] + fn test_native_scripts_serialize_as_expected() { + let test_blocks = vec![( + include_str!("test_data/test9.block"), + include_str!("test_data/test9.native"), + )]; + + for (idx, (block_str, jsonl_str)) in test_blocks.iter().enumerate() { + println!("decoding json block {}", idx + 1); + + let bytes = hex::decode(block_str).expect(&format!("bad block file {}", idx)); + + let BlockWrapper(_, block) = BlockWrapper::decode_fragment(&bytes[..]) + .expect(&format!("error decoding cbor for file {}", idx)); + + let mut scripts = jsonl_str.lines(); + + for ws in block.transaction_witness_sets.iter() { + if let Some(nss) = &ws.native_script { + for ns in nss.iter() { + let expected: serde_json::Value = + serde_json::from_str(scripts.next().unwrap()).unwrap(); + let current = ns.to_json(); + assert_eq!(current, expected); + } + } + } + } + } +} diff --git a/pallas-primitives/src/alonzo/mod.rs b/pallas-primitives/src/alonzo/mod.rs index 9add788..ee21683 100644 --- a/pallas-primitives/src/alonzo/mod.rs +++ b/pallas-primitives/src/alonzo/mod.rs @@ -3,4 +3,7 @@ mod model; pub mod address; pub mod crypto; +#[cfg(feature = "json")] +pub mod json; + pub use model::*; diff --git a/pallas-primitives/src/alonzo/test_data/test9.datums b/pallas-primitives/src/alonzo/test_data/test9.datums new file mode 100644 index 0000000..a55f9f9 --- /dev/null +++ b/pallas-primitives/src/alonzo/test_data/test9.datums @@ -0,0 +1,4 @@ +{"constructor":0,"fields":[{"bytes":"11f76aa14dae8aa6ac6578af2287bfc4c8e7a3da566548da747e026c"},{"int":250000000},{"bytes":"c56d4cceb8a8550534968e1bf165137ca41e908d2d780cc1402079bd"},{"bytes":"4368696c6c65644b6f6e6734373636"},{"bytes":"1656abe748903d4610c6db6373f036c8aec1b19ece5c93b6f25b5233"},{"int":30}]} +{"constructor":0,"fields":[{"bytes":"11f76aa14dae8aa6ac6578af2287bfc4c8e7a3da566548da747e026c"},{"int":190000000},{"bytes":"c56d4cceb8a8550534968e1bf165137ca41e908d2d780cc1402079bd"},{"bytes":"4368696c6c65644b6f6e6734373636"},{"bytes":"1656abe748903d4610c6db6373f036c8aec1b19ece5c93b6f25b5233"},{"int":30}]} +{"constructor":0,"fields":[{"bytes":"92f5aef1645f377ce472eb092ac8a572f9811d3ab2d5dd39cbe07064"},{"int":380000000},{"bytes":"bff82d31352d9bdfdb49e243ab74af715488631f330b2cf064178f90"},{"bytes":"426c6f636b4f776c734269747479303833"},{"bytes":"68b82dc4fb2d515501728de5bfcb1fd0a47eb3dd628d3340e10afa28"},{"biguint":"00"}]} +{"constructor":0,"fields":[{"bytes":"92f5aef1645f377ce472eb092ac8a572f9811d3ab2d5dd39cbe07064"},{"int":350000000},{"bytes":"bff82d31352d9bdfdb49e243ab74af715488631f330b2cf064178f90"},{"bytes":"426c6f636b4f776c734269747479303833"},{"bytes":"68b82dc4fb2d515501728de5bfcb1fd0a47eb3dd628d3340e10afa28"},{"biguint":"00"}]} \ No newline at end of file diff --git a/pallas-primitives/src/alonzo/test_data/test9.native b/pallas-primitives/src/alonzo/test_data/test9.native new file mode 100644 index 0000000..53c71b8 --- /dev/null +++ b/pallas-primitives/src/alonzo/test_data/test9.native @@ -0,0 +1 @@ +{"scripts":[{"keyHash":"4d04380dcb9fbad5aff8e2f4e19394ef4e5e11b37932838f01984a12","type":"sig"},{"slot":112500819,"type":"after"}],"type":"all"} diff --git a/pallas-primitives/src/framework.rs b/pallas-primitives/src/framework.rs index a160855..9c7bffc 100644 --- a/pallas-primitives/src/framework.rs +++ b/pallas-primitives/src/framework.rs @@ -23,6 +23,11 @@ where } } +#[cfg(feature = "json")] +pub trait ToCanonicalJson { + fn to_json(&self) -> serde_json::Value; +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum Era { Byron,