1use std::ops::Deref;
2
3use pallas_codec::tree::{IndexedNode, Visit, fold_tree, walk_tree};
4use serde_json::{Value, json};
5
6use crate::ToCanonicalJson;
7
8impl<A> super::Constr<A> {
9 pub fn constructor_value(&self) -> Option<u64> {
10 match self.tag {
11 121..=127 => Some(self.tag - 121),
12 1280..=1400 => Some(self.tag - 1280 + 7),
13 102 => self.any_constructor,
14 _ => None,
15 }
16 }
17}
18
19fn object(entries: impl IntoIterator<Item = (&'static str, Value)>) -> Value {
22 let mut map = serde_json::Map::new();
23 for (key, value) in entries {
24 map.insert(key.to_string(), value);
25 }
26 Value::Object(map)
27}
28
29fn plutus_leaf_json(x: &super::PlutusData) -> Option<serde_json::Value> {
31 let value = match x {
32 super::PlutusData::BigInt(int) => match int {
33 super::BigInt::Int(n) => match i64::try_from(*n.deref()) {
34 Ok(x) => json!({ "int": x }),
35 Err(_) => {
36 json!({ "bignint": hex::encode(i128::from(*n.deref()).to_be_bytes()) })
37 }
38 },
39 super::BigInt::BigUInt(x) => json!({ "biguint": hex::encode(x.as_slice())}),
45 super::BigInt::BigNInt(x) => json!({ "bignint": hex::encode(x.as_slice())}),
46 },
47 super::PlutusData::BoundedBytes(x) => json!({ "bytes": hex::encode(x.as_slice())}),
48 _ => return None,
49 };
50 Some(value)
51}
52
53impl ToCanonicalJson for super::PlutusData {
55 fn to_json(&self) -> serde_json::Value {
61 use super::PlutusData;
62
63 fold_tree(self, |node, children: Vec<Value>| match node {
64 PlutusData::Constr(x) => object([
65 ("constructor", json!(x.constructor_value())),
66 ("fields", Value::Array(children)),
67 ]),
68 PlutusData::Map(_) => {
69 let mut children = children.into_iter();
70 let mut map = Vec::with_capacity(children.len() / 2);
71 while let (Some(k), Some(v)) = (children.next(), children.next()) {
72 map.push(object([("k", k), ("v", v)]));
73 }
74 object([("map", Value::Array(map))])
75 }
76 PlutusData::Array(_) => object([("list", Value::Array(children))]),
77 leaf => plutus_leaf_json(leaf).expect("leaf variant"),
78 })
79 }
80
81 fn to_json_string(&self) -> String {
84 use std::fmt::Write;
85
86 use super::PlutusData;
87
88 let mut out = String::new();
89 let mut rendered: Vec<usize> = Vec::new();
92 walk_tree::<_, std::fmt::Error>(self, |visit| match visit {
93 Visit::Enter(PlutusData::Constr(x)) => {
94 rendered.push(0);
95 match x.constructor_value() {
96 Some(n) => write!(out, r#"{{"constructor":{n},"fields":["#),
97 None => write!(out, r#"{{"constructor":null,"fields":["#),
98 }
99 }
100 Visit::Enter(node @ PlutusData::Map(_)) => {
101 rendered.push(0);
102 write!(out, r#"{{"map":["#)?;
103 if node.child_count() > 0 {
104 write!(out, r#"{{"k":"#)?;
105 }
106 Ok(())
107 }
108 Visit::Enter(PlutusData::Array(_)) => {
109 rendered.push(0);
110 write!(out, r#"{{"list":["#)
111 }
112 Visit::Enter(leaf) => {
113 write!(out, "{}", plutus_leaf_json(leaf).expect("leaf variant"))
114 }
115 Visit::Between(PlutusData::Map(_)) => {
116 let count = rendered.last_mut().expect("inside a map");
117 *count += 1;
118 if count.is_multiple_of(2) {
119 write!(out, r#"}},{{"k":"#)
120 } else {
121 write!(out, r#","v":"#)
122 }
123 }
124 Visit::Between(_) => write!(out, ","),
125 Visit::Exit(node @ PlutusData::Map(_)) => {
126 rendered.pop();
127 if node.child_count() > 0 {
128 write!(out, "}}]}}")
129 } else {
130 write!(out, "]}}")
131 }
132 }
133 Visit::Exit(PlutusData::Constr(_) | PlutusData::Array(_)) => {
134 rendered.pop();
135 write!(out, "]}}")
136 }
137 Visit::Exit(_) => Ok(()),
138 })
139 .expect("writing to a String cannot fail");
140 out
141 }
142}
143
144impl ToCanonicalJson for super::NativeScript {
145 fn to_json(&self) -> serde_json::Value {
151 use super::NativeScript;
152
153 fold_tree(self, |node, children: Vec<Value>| match node {
154 NativeScript::ScriptPubkey(x) => json!({ "keyHash": x.to_string(), "type": "sig"}),
155 NativeScript::ScriptAll(_) => {
156 object([("scripts", Value::Array(children)), ("type", json!("all"))])
157 }
158 NativeScript::ScriptAny(_) => {
159 object([("scripts", Value::Array(children)), ("type", json!("any"))])
160 }
161 NativeScript::ScriptNOfK(n, _) => object([
162 ("required", json!(n)),
163 ("scripts", Value::Array(children)),
164 ("type", json!("atLeast")),
165 ]),
166 NativeScript::InvalidBefore(slot) => json!({ "type": "after", "slot": slot }),
167 NativeScript::InvalidHereafter(slot) => json!({"type": "before", "slot": slot }),
168 })
169 }
170
171 fn to_json_string(&self) -> String {
174 use std::fmt::Write;
175
176 use super::NativeScript;
177
178 let mut out = String::new();
179 walk_tree::<_, std::fmt::Error>(self, |visit| match visit {
180 Visit::Enter(NativeScript::ScriptPubkey(x)) => {
181 write!(out, r#"{{"keyHash":"{x}","type":"sig"}}"#)
182 }
183 Visit::Enter(NativeScript::ScriptAll(_) | NativeScript::ScriptAny(_)) => {
184 write!(out, r#"{{"scripts":["#)
185 }
186 Visit::Enter(NativeScript::ScriptNOfK(n, _)) => {
187 write!(out, r#"{{"required":{n},"scripts":["#)
188 }
189 Visit::Enter(NativeScript::InvalidBefore(slot)) => {
190 write!(out, r#"{{"slot":{slot},"type":"after"}}"#)
191 }
192 Visit::Enter(NativeScript::InvalidHereafter(slot)) => {
193 write!(out, r#"{{"slot":{slot},"type":"before"}}"#)
194 }
195 Visit::Between(_) => write!(out, ","),
196 Visit::Exit(NativeScript::ScriptAll(_)) => write!(out, r#"],"type":"all"}}"#),
197 Visit::Exit(NativeScript::ScriptAny(_)) => write!(out, r#"],"type":"any"}}"#),
198 Visit::Exit(NativeScript::ScriptNOfK(..)) => write!(out, r#"],"type":"atLeast"}}"#),
199 Visit::Exit(_) => Ok(()),
200 })
201 .expect("writing to a String cannot fail");
202 out
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use pallas_codec::minicbor;
209
210 use crate::{ToCanonicalJson, alonzo::Block};
211
212 type BlockWrapper<'a> = (u16, Block<'a>);
213
214 #[test]
215 fn test_datums_serialize_as_expected() {
216 let test_blocks = [(
217 include_str!("../../../test_data/alonzo9.block"),
218 include_str!("../../../test_data/alonzo9.datums"),
219 )];
220
221 for (idx, (block_str, jsonl_str)) in test_blocks.iter().enumerate() {
222 println!("decoding json block {}", idx + 1);
223
224 let bytes = hex::decode(block_str).unwrap_or_else(|_| panic!("bad block file {idx}"));
225
226 let (_, block): BlockWrapper = minicbor::decode(&bytes[..])
227 .unwrap_or_else(|_| panic!("error decoding cbor for file {idx}"));
228
229 let mut datums = jsonl_str.lines();
230
231 for ws in block.transaction_witness_sets.iter() {
232 if let Some(pds) = &ws.plutus_data {
233 for pd in pds.iter() {
234 let expected: serde_json::Value =
235 serde_json::from_str(datums.next().unwrap()).unwrap();
236 let current = pd.to_json();
237 assert_eq!(current, expected);
238
239 let text = pd.to_json_string();
240 let parsed: serde_json::Value = serde_json::from_str(&text).unwrap();
241 assert_eq!(parsed, expected);
242 assert_eq!(text, current.to_string());
243 }
244 }
245 }
246 }
247 }
248
249 #[test]
250 fn test_native_scripts_serialize_as_expected() {
251 let test_blocks = [(
252 include_str!("../../../test_data/alonzo9.block"),
253 include_str!("../../../test_data/alonzo9.native"),
254 )];
255
256 for (idx, (block_str, jsonl_str)) in test_blocks.iter().enumerate() {
257 println!("decoding json block {}", idx + 1);
258
259 let bytes = hex::decode(block_str).unwrap_or_else(|_| panic!("bad block file {idx}"));
260
261 let (_, block): BlockWrapper = minicbor::decode(&bytes[..])
262 .unwrap_or_else(|_| panic!("error decoding cbor for file {idx}"));
263
264 let mut scripts = jsonl_str.lines();
265
266 for ws in block.transaction_witness_sets.iter() {
267 if let Some(nss) = &ws.native_script {
268 for ns in nss.iter() {
269 let expected: serde_json::Value =
270 serde_json::from_str(scripts.next().unwrap()).unwrap();
271 let current = ns.to_json();
272 assert_eq!(current, expected);
273
274 let text: serde_json::Value =
275 serde_json::from_str(&ns.to_json_string()).unwrap();
276 assert_eq!(text, expected);
277 }
278 }
279 }
280 }
281 }
282
283 #[test]
284 fn native_script_json_preserves_mixed_shape_trees() {
285 use crate::alonzo::NativeScript;
286 use serde_json::json;
287
288 let script = NativeScript::ScriptNOfK(
291 2,
292 vec![
293 NativeScript::ScriptPubkey([1; 28].into()),
294 NativeScript::ScriptAll(vec![
295 NativeScript::ScriptPubkey([2; 28].into()),
296 NativeScript::ScriptAny(vec![
297 NativeScript::InvalidBefore(100),
298 NativeScript::InvalidHereafter(200),
299 ]),
300 ]),
301 NativeScript::ScriptPubkey([3; 28].into()),
302 ],
303 );
304
305 let expected = json!({
306 "type": "atLeast",
307 "required": 2,
308 "scripts": [
309 { "type": "sig", "keyHash": hex::encode([1u8; 28]) },
310 { "type": "all", "scripts": [
311 { "type": "sig", "keyHash": hex::encode([2u8; 28]) },
312 { "type": "any", "scripts": [
313 { "type": "after", "slot": 100 },
314 { "type": "before", "slot": 200 },
315 ]},
316 ]},
317 { "type": "sig", "keyHash": hex::encode([3u8; 28]) },
318 ],
319 });
320
321 assert_eq!(script.to_json(), expected);
322
323 let text: serde_json::Value = serde_json::from_str(&script.to_json_string()).unwrap();
324 assert_eq!(text, expected);
325 }
326
327 fn deep_datum_shapes(depth: usize) -> Vec<(crate::PlutusData, String)> {
328 use crate::{BigInt, Constr, Int, KeyValuePairs, MaybeIndefArray, PlutusData};
329
330 let int = |n: i64| PlutusData::BigInt(BigInt::Int(Int::from(n)));
331 let mut constr = int(0);
332 let mut map = int(0);
333 let mut list = int(0);
334 for _ in 0..depth {
335 constr = PlutusData::Constr(Constr {
336 tag: 121,
337 any_constructor: None,
338 fields: MaybeIndefArray::Indef(vec![constr]),
339 });
340 map = PlutusData::Map(KeyValuePairs::Def(vec![(int(1), map)]));
341 list = PlutusData::Array(MaybeIndefArray::Def(vec![list]));
342 }
343 let leaf = r#"{"int":0}"#;
344 vec![
345 (
346 constr,
347 format!(
348 "{}{leaf}{}",
349 r#"{"constructor":0,"fields":["#.repeat(depth),
350 "]}".repeat(depth)
351 ),
352 ),
353 (
354 map,
355 format!(
356 "{}{leaf}{}",
357 r#"{"map":[{"k":{"int":1},"v":"#.repeat(depth),
358 "}]}".repeat(depth)
359 ),
360 ),
361 (
362 list,
363 format!(
364 "{}{leaf}{}",
365 r#"{"list":["#.repeat(depth),
366 "]}".repeat(depth)
367 ),
368 ),
369 ]
370 }
371
372 #[test]
373 fn plutus_data_json_preserves_mixed_shapes() {
374 use crate::PlutusData;
375
376 let bytes = hex::decode("d866820586a101809fffa040c249010000000000000000d87980").unwrap();
378 let data: PlutusData = minicbor::decode(&bytes).unwrap();
379 let expected = r#"{"constructor":5,"fields":[{"map":[{"k":{"int":1},"v":{"list":[]}}]},{"list":[]},{"map":[]},{"bytes":""},{"biguint":"010000000000000000"},{"constructor":0,"fields":[]}]}"#;
380 assert_eq!(data.to_json_string(), expected);
381 assert_eq!(data.to_json().to_string(), expected);
382 }
383
384 #[test]
385 fn plutus_data_json_string_handles_deeply_nested_data_on_a_small_stack() {
386 std::thread::Builder::new()
387 .stack_size(128 * 1024)
388 .spawn(|| {
389 for (data, expected) in deep_datum_shapes(20_000) {
390 let data = std::mem::ManuallyDrop::new(data);
392 assert_eq!(data.to_json_string(), expected);
393 }
394 })
395 .unwrap()
396 .join()
397 .unwrap();
398 }
399
400 #[test]
401 fn plutus_data_json_handles_deeply_nested_data_on_a_small_stack() {
402 std::thread::Builder::new()
403 .stack_size(128 * 1024)
404 .spawn(|| {
405 for (data, _) in deep_datum_shapes(20_000) {
406 let data = std::mem::ManuallyDrop::new(data);
407 let json = std::mem::ManuallyDrop::new(data.to_json());
410
411 let mut depth = 0;
412 let mut cursor: &serde_json::Value = &json;
413 loop {
414 let next = if let Some(fields) = cursor.get("fields") {
415 fields.get(0)
416 } else if let Some(map) = cursor.get("map") {
417 map.get(0).and_then(|pair| pair.get("v"))
418 } else if let Some(list) = cursor.get("list") {
419 list.get(0)
420 } else {
421 break;
422 };
423 cursor = next.expect("container carries a child");
424 depth += 1;
425 }
426 assert_eq!(depth, 20_000);
427 assert_eq!(cursor["int"], 0);
428 }
429 })
430 .unwrap()
431 .join()
432 .unwrap();
433 }
434
435 #[test]
436 fn native_script_json_string_handles_deeply_nested_scripts_on_a_small_stack() {
437 use crate::alonzo::NativeScript;
438
439 std::thread::Builder::new()
440 .stack_size(128 * 1024)
441 .spawn(|| {
442 let mut script = NativeScript::ScriptPubkey([0; 28].into());
443 for _ in 0..20_000 {
444 script = NativeScript::ScriptAll(vec![script]);
445 }
446
447 let expected = format!(
448 "{}{{\"keyHash\":\"{}\",\"type\":\"sig\"}}{}",
449 r#"{"scripts":["#.repeat(20_000),
450 hex::encode([0u8; 28]),
451 r#"],"type":"all"}"#.repeat(20_000),
452 );
453 assert_eq!(script.to_json_string(), expected);
454 })
455 .unwrap()
456 .join()
457 .unwrap();
458 }
459
460 #[test]
461 fn native_script_json_handles_deeply_nested_scripts_on_a_small_stack() {
462 use crate::alonzo::NativeScript;
463
464 std::thread::Builder::new()
467 .stack_size(128 * 1024)
468 .spawn(|| {
469 let mut script = NativeScript::ScriptPubkey([0; 28].into());
470 for _ in 0..20_000 {
471 script = NativeScript::ScriptAll(vec![script]);
472 }
473
474 let json = std::mem::ManuallyDrop::new(script.to_json());
478
479 let mut depth = 0;
480 let mut cursor: &serde_json::Value = &json;
481 while let Some(scripts) = cursor.get("scripts") {
482 cursor = scripts.get(0).expect("all must carry a child");
483 depth += 1;
484 }
485 assert_eq!(depth, 20_000);
486 assert_eq!(cursor["type"], "sig");
487 })
488 .unwrap()
489 .join()
490 .unwrap();
491 }
492}