1pub mod batch;
9pub mod decode;
10pub mod encode;
11pub mod extrinsic;
12pub mod storage;
13pub mod value;
14
15pub use value::Value;
16
17#[cfg(test)]
18mod corpus_tests {
19 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
20
21 use codec::Decode;
22
23 use crate::codec::value::Value;
24 use crate::runtime::type_string::TypeSpec;
25 use crate::runtime::Runtime;
26 use scale_info::{form::PortableForm, TypeDef};
27
28 fn json_u64(v: &serde_json::Value) -> u64 {
29 match v {
30 serde_json::Value::Number(n) => n.to_string().parse().unwrap(),
31 _ => panic!("fixture value {v} is not an unsigned integer"),
32 }
33 }
34
35 fn json_i64(v: &serde_json::Value) -> i64 {
36 match v {
37 serde_json::Value::Number(n) => n.to_string().parse().unwrap(),
38 _ => panic!("fixture value {v} is not a signed integer"),
39 }
40 }
41
42 fn as_u32(value: u64) -> u32 {
43 u32::try_from(value).expect("fixture integer fits u32")
44 }
45
46 fn as_u16(value: u64) -> u16 {
47 u16::try_from(value).expect("fixture integer fits u16")
48 }
49
50 fn as_u8(value: u64) -> u8 {
51 u8::try_from(value).expect("fixture integer fits u8")
52 }
53
54 fn value_from_json(v: &serde_json::Value) -> Value {
57 match v {
58 serde_json::Value::Null => Value::Null,
59 serde_json::Value::Bool(b) => Value::Bool(*b),
60 serde_json::Value::Number(n) => {
61 let text = n.to_string();
62 if let Ok(i) = text.parse::<i128>() {
63 Value::Int(i128::from(i))
64 } else if let Ok(u) = text.parse::<u128>() {
65 Value::Uint(u)
66 } else {
67 panic!("fixture number {n} is not an integer")
68 }
69 }
70 serde_json::Value::String(s) => Value::Str(s.clone()),
71 serde_json::Value::Array(items) => {
72 Value::List(items.iter().map(value_from_json).collect())
73 }
74 serde_json::Value::Object(map) => Value::Dict(
75 map.iter()
76 .map(|(k, v)| (Value::Str(k.clone()), value_from_json(v)))
77 .collect(),
78 ),
79 }
80 }
81
82 fn golden() -> serde_json::Value {
83 let path = concat!(
84 env!("CARGO_MANIFEST_DIR"),
85 "/../python/tests/fixtures/golden.json"
86 );
87 let raw = std::fs::read_to_string(path).expect("golden.json fixture exists");
88 serde_json::from_str(&raw).unwrap()
89 }
90
91 fn golden_metadata_v15() -> Vec<u8> {
92 let golden = golden();
93 let hex_str = golden["metadata"]["v15_hex"]
94 .as_str()
95 .expect("golden.json has metadata.v15_hex");
96 let raw = hex::decode(hex_str.trim_start_matches("0x")).unwrap();
97 Option::<Vec<u8>>::decode(&mut &raw[..])
98 .unwrap()
99 .expect("fixture metadata is Some")
100 }
101
102 fn runtime() -> Runtime {
103 let g = golden();
104 Runtime::parse(
105 &golden_metadata_v15(),
106 as_u32(json_u64(&g["network"]["spec_version"])),
107 as_u32(json_u64(&g["network"]["transaction_version"])),
108 as_u16(json_u64(&g["network"]["ss58_format"])),
109 )
110 .expect("golden metadata parses")
111 }
112
113 #[test]
114 fn singleton_tuple_sequence_accepts_flat_and_legacy_wrapped_values() {
115 #[derive(codec::Encode, scale_info::TypeInfo)]
116 struct Legs(Vec<(u16, u16, u64, u64)>);
117
118 let mut registry = scale_info::Registry::new();
119 let id = registry.register_type(&scale_info::meta_type::<Legs>()).id;
120 let mut rt = runtime();
121 rt.types = registry.into();
122 let flat = value_from_json(&serde_json::json!([[0, 2, 1_000_000_000u64, 0]]));
123 let wrapped = Value::List(vec![flat.clone()]);
124 let expected = codec::Encode::encode(&Legs(vec![(0, 2, 1_000_000_000, 0)]));
125 for value in [flat, wrapped] {
126 let mut out = vec![99];
128 rt.encode_value(&TypeSpec::Id(id), &value, &mut out)
129 .unwrap();
130 assert_eq!(out, [vec![99], expected.clone()].concat());
131 }
132 }
133
134 #[test]
135 fn singleton_nested_sequence_preserves_its_inner_sequence() {
136 #[derive(codec::Encode, scale_info::TypeInfo)]
137 struct Rows(Vec<Vec<u16>>);
138
139 let mut registry = scale_info::Registry::new();
140 let id = registry.register_type(&scale_info::meta_type::<Rows>()).id;
141 let mut rt = runtime();
142 rt.types = registry.into();
143 for row in [vec![], vec![1], vec![1, 2]] {
144 let value = value_from_json(&serde_json::json!([row]));
145 assert_eq!(
146 rt.encode_spec(&TypeSpec::Id(id), &value).unwrap(),
147 codec::Encode::encode(&Rows(vec![row]))
148 );
149 }
150 }
151
152 #[test]
155 fn decoder_reproduces_the_shape_corpus() {
156 let path = concat!(
157 env!("CARGO_MANIFEST_DIR"),
158 "/../python/tests/fixtures/shape_corpus/corpus.json"
159 );
160 let raw = std::fs::read_to_string(path).expect("shape corpus recorded");
161 let corpus: serde_json::Value = serde_json::from_str(&raw).unwrap();
162 let rt = runtime();
163 assert_eq!(
164 as_u32(json_u64(&corpus["spec_version"])),
165 rt.spec_version,
166 "corpus and golden fixture must be recorded from the same runtime"
167 );
168
169 let mut failures: Vec<String> = Vec::new();
170 let mut checked = 0usize;
171 for ty in corpus["types"].as_array().unwrap() {
172 let id = as_u32(json_u64(&ty["id"]));
173 let name = ty["name"].as_str().unwrap();
174 for (i, sample) in ty["samples"].as_array().unwrap().iter().enumerate() {
175 let scale_hex = sample["scale_hex"].as_str().unwrap();
176 let data = hex::decode(scale_hex.trim_start_matches("0x")).unwrap();
177 let expected = &sample["decoded"];
178 checked += 1;
179 match rt.decode_spec(&TypeSpec::Id(id), &data, true) {
180 Ok(value) => {
181 let got = crate::codec::value::to_corpus_json(&value);
182 if &got != expected {
183 failures.push(format!(
184 "type {id} {name} sample {i}:\n bytes {scale_hex}\n expected {expected}\n got {got}"
185 ));
186 }
187 }
188 Err(e) => failures.push(format!(
189 "type {id} {name} sample {i}: decode error {e} (bytes {scale_hex})"
190 )),
191 }
192 }
193 }
194 assert!(
195 failures.is_empty(),
196 "{} of {} corpus samples diverged (first 20):\n{}",
197 failures.len(),
198 checked,
199 failures
200 .iter()
201 .take(20)
202 .cloned()
203 .collect::<Vec<_>>()
204 .join("\n")
205 );
206 }
207
208 #[test]
212 fn pathological_nesting_errors_instead_of_overflowing() {
213 let rt = runtime();
214
215 let mut spec = TypeSpec::Primitive(crate::runtime::type_string::Primitive::U8);
220 for _ in 0..1000 {
221 spec = TypeSpec::Tuple(vec![spec]);
222 }
223
224 let err = rt
225 .decode_spec(&spec, &[0u8], true)
226 .expect_err("deep nesting must not decode");
227 assert!(
228 err.to_string().contains("recursion"),
229 "unexpected error: {err}"
230 );
231
232 let mut value = Value::Int(0);
233 for _ in 0..1000 {
234 value = Value::Tuple(vec![value]);
235 }
236 let err = rt
237 .encode_spec(&spec, &value)
238 .expect_err("deep nesting must not encode");
239 assert!(
240 err.to_string().contains("recursion"),
241 "unexpected error: {err}"
242 );
243 }
244
245 #[test]
249 fn composed_calls_match_the_golden_vectors() {
250 let g = golden();
251 let rt = runtime();
252 let addr = |i: usize| g["ss58"][i]["address"].as_str().unwrap().to_string();
253 let (alice, bob, charlie) = (addr(0), addr(1), addr(2));
254
255 let transfer = |dest: &str, value: i128| {
256 rt.compose_call(
257 "Balances",
258 "transfer_keep_alive",
259 &Value::record(vec![
260 ("dest".into(), Value::str(dest)),
261 ("value".into(), Value::Int(value)),
262 ]),
263 )
264 .unwrap()
265 };
266
267 for case in g["calls"].as_array().unwrap() {
268 let module = case["module"].as_str().unwrap();
269 let function = case["function"].as_str().unwrap();
270 let composed = match module {
271 "Sudo" => {
272 let inner = rt
273 .compose_call(
274 "System",
275 "remark",
276 &Value::record(vec![("remark".into(), Value::str("0xdeadbeef"))]),
277 )
278 .unwrap();
279 rt.compose_call(
280 "Sudo",
281 "sudo",
282 &Value::record(vec![("call".into(), Value::Bytes(inner))]),
283 )
284 }
285 "Utility" => {
286 let t1 = transfer(&bob, 1);
287 let t2 = transfer(&charlie, 2);
288 rt.compose_call(
289 "Utility",
290 "batch",
291 &Value::record(vec![(
292 "calls".into(),
293 Value::List(vec![Value::Bytes(t1), Value::Bytes(t2)]),
294 )]),
295 )
296 }
297 "Proxy" => {
298 let t1 = transfer(&bob, 1);
299 rt.compose_call(
300 "Proxy",
301 "proxy",
302 &Value::record(vec![
303 ("real".into(), Value::str(alice.clone())),
304 ("force_proxy_type".into(), Value::str("Transfer")),
305 ("call".into(), Value::Bytes(t1)),
306 ]),
307 )
308 }
309 _ => rt.compose_call(module, function, &value_from_json(&case["params"])),
310 }
311 .unwrap_or_else(|e| panic!("{module}.{function} failed to compose: {e}"));
312 let expected = case["data_hex"].as_str().unwrap();
313 assert_eq!(
314 format!("0x{}", hex::encode(&composed)),
315 expected,
316 "{module}.{function} encoding diverged"
317 );
318 }
319 }
320
321 #[test]
324 fn storage_keys_match_the_golden_vectors() {
325 let g = golden();
326 let rt = runtime();
327 for case in g["storage_keys"].as_array().unwrap() {
328 let pallet = case["pallet"].as_str().unwrap();
329 let name = case["storage_function"].as_str().unwrap();
330 let entry = rt
331 .storage_entry(pallet, name)
332 .unwrap_or_else(|| panic!("{pallet}.{name} not found"));
333 let params: Vec<Value> = case["params"]
334 .as_array()
335 .unwrap()
336 .iter()
337 .map(value_from_json)
338 .collect();
339 let key = rt
340 .storage_key(entry, ¶ms)
341 .unwrap_or_else(|e| panic!("{pallet}.{name} key failed: {e}"));
342 assert_eq!(
343 format!("0x{}", hex::encode(&key)),
344 case["key_hex"].as_str().unwrap(),
345 "{pallet}.{name} storage key diverged"
346 );
347 }
348 }
349
350 #[test]
353 fn map_keys_recover_from_the_golden_query_maps() {
354 let g = golden();
355 let rt = runtime();
356 for case in g["query_maps"].as_array().unwrap() {
357 let pallet = case["pallet"].as_str().unwrap();
358 let name = case["storage_function"].as_str().unwrap();
359 let entry = rt.storage_entry(pallet, name).unwrap();
360 let fixed = case["params"].as_array().unwrap().len();
361 let free = entry.key_types.len() - fixed;
362 for (raw_key, expected_pair) in case["raw_keys"]
363 .as_array()
364 .unwrap()
365 .iter()
366 .zip(case["pairs"].as_array().unwrap())
367 {
368 let key = hex::decode(raw_key.as_str().unwrap().trim_start_matches("0x")).unwrap();
369 let recovered = rt
370 .decode_storage_key_params(entry, &key, fixed)
371 .unwrap_or_else(|e| panic!("{pallet}.{name} key recovery failed: {e}"));
372 assert_eq!(recovered.len(), free);
373 let expected_key = &expected_pair[0];
375 let got = if free == 1 {
376 crate::codec::value::to_corpus_json(&recovered[0])
377 } else {
378 crate::codec::value::to_corpus_json(&Value::List(recovered))
379 };
380 assert_eq!(&got, expected_key, "{pallet}.{name} recovered key diverged");
381 }
382 }
383 }
384
385 fn h256(hex_str: &str) -> [u8; 32] {
386 hex::decode(hex_str.trim_start_matches("0x"))
387 .unwrap()
388 .try_into()
389 .unwrap()
390 }
391
392 fn golden_transfer(rt: &Runtime, g: &serde_json::Value) -> Vec<u8> {
393 rt.compose_call(
394 "Balances",
395 "transfer_keep_alive",
396 &Value::record(vec![
397 (
398 "dest".into(),
399 Value::str(g["ss58"][1]["address"].as_str().unwrap()),
400 ),
401 ("value".into(), Value::Int(12345)),
402 ]),
403 )
404 .unwrap()
405 }
406
407 #[test]
410 fn signature_payloads_match_the_golden_vectors() {
411 let g = golden();
412 let rt = runtime();
413 let call = golden_transfer(&rt, &g);
414 let genesis = h256(g["network"]["genesis_hash"].as_str().unwrap());
415
416 let immortal = &g["signature_payloads"][0];
417 let payload = rt
418 .signature_payload(
419 &call,
420 &crate::codec::extrinsic::TxParams {
421 era: Value::str("00"),
422 nonce: json_u64(&immortal["nonce"]),
423 tip: 0,
424 tip_asset_id: None,
425 genesis_hash: genesis,
426 era_block_hash: genesis,
427 metadata_hash: None,
428 },
429 )
430 .unwrap();
431 assert_eq!(
432 format!("0x{}", hex::encode(&payload)),
433 immortal["payload_hex"].as_str().unwrap()
434 );
435
436 let mortal = &g["signature_payloads"][1];
437 let payload = rt
438 .signature_payload(
439 &call,
440 &crate::codec::extrinsic::TxParams {
441 era: Value::record(vec![
442 (
443 "period".into(),
444 Value::Int(i128::from(json_i64(&mortal["era"]["period"]))),
445 ),
446 (
447 "current".into(),
448 Value::Int(i128::from(json_i64(&mortal["era"]["current"]))),
449 ),
450 ]),
451 nonce: json_u64(&mortal["nonce"]),
452 tip: 0,
453 tip_asset_id: None,
454 genesis_hash: genesis,
455 era_block_hash: h256(mortal["era_birth_block_hash"].as_str().unwrap()),
456 metadata_hash: None,
457 },
458 )
459 .unwrap();
460 assert_eq!(
461 format!("0x{}", hex::encode(&payload)),
462 mortal["payload_hex"].as_str().unwrap()
463 );
464 }
465
466 #[test]
469 fn signed_extrinsics_match_the_golden_vectors() {
470 let g = golden();
471 let rt = runtime();
472 let call = golden_transfer(&rt, &g);
473 for case in g["extrinsics"].as_array().unwrap() {
474 let era = if case["era"] == "00" {
475 Value::str("00")
476 } else {
477 value_from_json(&case["era"])
478 };
479 let signature = hex::decode(
480 case["signature_hex"]
481 .as_str()
482 .unwrap()
483 .trim_start_matches("0x"),
484 )
485 .unwrap();
486 let (data, hash) = rt
487 .encode_signed_extrinsic(
488 &call,
489 h256(case["public_key_hex"].as_str().unwrap()),
490 &signature,
491 as_u8(json_u64(&case["signature_version"])),
492 &crate::codec::extrinsic::TxParams {
493 era,
494 nonce: json_u64(&case["nonce"]),
495 tip: json_u64(&case["tip"]).into(),
496 tip_asset_id: None,
497 genesis_hash: [0; 32],
498 era_block_hash: [0; 32],
499 metadata_hash: None,
500 },
501 )
502 .unwrap();
503 assert_eq!(
504 format!("0x{}", hex::encode(&data)),
505 case["extrinsic_hex"].as_str().unwrap()
506 );
507 assert_eq!(
508 format!("0x{}", hex::encode(hash)),
509 case["extrinsic_hash"].as_str().unwrap()
510 );
511 }
512 }
513
514 #[test]
516 fn extrinsics_decode_like_the_golden_block() {
517 let g = golden();
518 let rt = runtime();
519 let raws = g["block"]["raw"]["block"]["extrinsics"].as_array().unwrap();
520 let olds = g["block"]["decoded_extrinsics"].as_array().unwrap();
521 assert_eq!(raws.len(), olds.len());
522 for (raw, old) in raws.iter().zip(olds) {
523 let data = hex::decode(raw.as_str().unwrap().trim_start_matches("0x")).unwrap();
524 let decoded = rt.decode_extrinsic(&data, true).unwrap();
525 assert_eq!(&crate::codec::value::to_corpus_json(&decoded), old);
526 }
527 }
528
529 #[test]
532 fn multisig_accounts_match_the_golden_vectors() {
533 let g = golden();
534 let ss58_format = as_u16(json_u64(&g["network"]["ss58_format"]));
535 for case in g["multisig"].as_array().unwrap() {
536 let signatories: Vec<[u8; 32]> = case["signatories"]
537 .as_array()
538 .unwrap()
539 .iter()
540 .map(|s| crate::keys::public_key_from_ss58(s.as_str().unwrap()).unwrap())
541 .collect();
542 let threshold = as_u16(json_u64(&case["threshold"]));
543 let (account, sorted) =
544 crate::codec::extrinsic::multisig_account_id(&signatories, threshold).unwrap();
545 assert_eq!(
546 crate::keys::ss58_from_public(account, ss58_format),
547 case["ss58_address"].as_str().unwrap()
548 );
549 assert_eq!(sorted.len(), signatories.len());
550 }
551 }
552
553 #[test]
555 fn composed_calls_decode_back() {
556 let g = golden();
557 let rt = runtime();
558 let case = &g["calls"].as_array().unwrap()[0];
559 let data =
560 hex::decode(case["data_hex"].as_str().unwrap().trim_start_matches("0x")).unwrap();
561 let decoded = rt.decode_spec(&TypeSpec::Call, &data, true).unwrap();
562 let json = crate::codec::value::to_corpus_json(&decoded);
563 assert_eq!(json["call_module"], "Balances");
564 assert_eq!(json["call_function"], "transfer_keep_alive");
565 assert_eq!(json["call_args"][0]["value"], g["ss58"][1]["address"]);
566 }
567
568 fn bounded_vec_of(rt: &Runtime, elem: impl Fn(&scale_info::Type<PortableForm>) -> bool) -> u32 {
570 rt.types
571 .types
572 .iter()
573 .find(|t| {
574 let TypeDef::Composite(c) = &t.ty.type_def else {
575 return false;
576 };
577 if t.ty.path.segments.last().map(String::as_str) != Some("BoundedVec") {
578 return false;
579 };
580 let [only] = c.fields.as_slice() else {
581 return false;
582 };
583 let Ok(TypeDef::Sequence(s)) = rt.resolve(only.ty.id).map(|t| &t.type_def) else {
584 return false;
585 };
586 only.name.is_none() && rt.resolve(s.type_param.id).is_ok_and(&elem)
587 })
588 .map(|t| t.id)
589 .expect("metadata has a matching BoundedVec")
590 }
591
592 #[test]
596 fn newtype_sequences_unwrap_only_double_wrapped_payloads() {
597 let g = golden();
598 let rt = runtime();
599 let addr = |i: usize| Value::str(g["ss58"][i]["address"].as_str().unwrap());
600 let enc = |id: u32, v: &Value| rt.encode_spec(&TypeSpec::Id(id), v);
601 let pair = |a: i128, b: i128| Value::List(vec![Value::Int(a), Value::Int(b)]);
602 let list = |items: Vec<Value>| Value::List(items);
603
604 let pairs = bounded_vec_of(&rt, |t| match &t.type_def {
605 TypeDef::Tuple(tup) => {
606 tup.fields.len() == 2
607 && tup.fields.iter().all(|f| {
608 rt.resolve(f.id)
609 .is_ok_and(|p| matches!(p.type_def, TypeDef::Primitive(_)))
610 })
611 }
612 _ => false,
613 });
614 let one = enc(pairs, &list(vec![pair(1, 5)])).unwrap();
615 assert_eq!(one[0], 1 << 2, "flat one-entry list encodes one pair");
616 let two = enc(pairs, &list(vec![pair(1, 5), pair(1, 7)])).unwrap();
617 assert_eq!(two[0], 2 << 2);
618 assert_eq!(
619 enc(pairs, &list(vec![list(vec![pair(1, 5)])])).unwrap(),
620 one
621 );
622 assert_eq!(
623 enc(pairs, &list(vec![list(vec![pair(1, 5), pair(1, 7)])])).unwrap(),
624 two
625 );
626
627 let accounts = bounded_vec_of(&rt, |t| {
628 t.path.segments.last().map(String::as_str) == Some("AccountId32")
629 });
630 let flat = enc(accounts, &list(vec![addr(0), addr(1)])).unwrap();
631 assert_eq!(
632 enc(accounts, &list(vec![list(vec![addr(0), addr(1)])])).unwrap(),
633 flat
634 );
635
636 let data = bounded_vec_of(&rt, |t| {
637 t.path.segments.last().map(String::as_str) == Some("Data")
638 && matches!(t.type_def, TypeDef::Variant(_))
639 });
640 let raw = Value::record(vec![("Raw5".into(), Value::str("0x0102030405"))]);
641 let flat = enc(data, &list(vec![raw.clone()])).unwrap();
642 assert_eq!(enc(data, &list(vec![list(vec![raw])])).unwrap(), flat);
643 }
644}