From acad96a3941c7b6970d2d3b2291798fb5ab25d18 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 5 Dec 2024 10:31:34 -0500 Subject: [PATCH] [Feat] encode by string (#8) * bump ver * add encode method * fix sequences * fix tuple encode * nump ver * use prerelease * add encode example to readme --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 38 ++ bt_decode.pyi | 18 + pyproject.toml | 2 +- src/lib.rs | 609 +++++++++++++++++++++++++++- tests/test_encode_by_type_string.py | 238 +++++++++++ 7 files changed, 903 insertions(+), 6 deletions(-) create mode 100644 tests/test_encode_by_type_string.py diff --git a/Cargo.lock b/Cargo.lock index 7292d96..1868369 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bt_decode" -version = "0.1.0" +version = "0.4.0-a0" dependencies = [ "custom_derive", "frame-metadata 16.0.0", diff --git a/Cargo.toml b/Cargo.toml index 00ef8aa..62d4d7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bt_decode" -version = "0.1.0" +version = "0.4.0-a0" edition = "2021" [lib] diff --git a/README.md b/README.md index ac23e7a..9353f35 100644 --- a/README.md +++ b/README.md @@ -288,3 +288,41 @@ neurons_lite: List[NeuronInfoLite] = bt_decode.decode( ) ) ``` + +### encode by type string +*Note: This feature is unstable, but working for multiple types.* + +You may also encode using a type-string formed from existing types by passing the metadata as pulled from a node (or formed manually). +```python +import bittensor, bt_decode, scalecodec +# Get subtensor connection +sub = bittensor.subtensor() +# Create a param for the RPC call, using v15 metadata +v15_int = scalecodec.U32() +v15_int.value = 15 +# Make the RPC call to grab the metadata +metadata_rpc_result = sub.substrate.rpc_request("state_call", [ + "Metadata_metadata_at_version", + v15_int.encode().to_hex(), + sub.substrate.get_chain_finalised_head() +]) +# Decode the metadata into a PortableRegistry type +metadata_option_hex_str = metadata_rpc_result['result'] +metadata_option_bytes = bytes.fromhex(metadata_option_hex_str[2:]) +metadata_v15 = bt_decode.MetadataV15.decode_from_metadata_option(metadata_option_bytes) +registry = bt_decode.PortableRegistry.from_metadata_v15( metadata_v15 ) + + +## Encode an integer as a compact u16 +compact_u16: list[int] = bt_decode.encode( + "Compact", # type-string, + 2**16-1, + registry +) +# [254, 255, 3, 0] +compact_u16_py_scale_codec = scalecodec.Compact() +compact_u16_py_scale_codec.value = 2**16-1 +compact_u16_py_scale_codec.encode() + +assert list(compact_u16_py_scale_codec.data.data) == compact_u16 +``` diff --git a/bt_decode.pyi b/bt_decode.pyi index 0405eb4..ebf41e4 100644 --- a/bt_decode.pyi +++ b/bt_decode.pyi @@ -349,3 +349,21 @@ def decode( type_string: str, portable_registry: PortableRegistry, encoded: bytes ) -> Any: pass + +def encode( + type_string: str, portable_registry: PortableRegistry, to_encode: Any +) -> list[int]: + """ + Encode a python object to bytes. + + Returns a list of integers representing the encoded bytes. + + Example: + >>> import bittensor as bt + >>> res = bt.decode.encode("u128", bt.decode.PortableRegistry.from_json(...), 1234567890) + >>> res + [210, 2, 150, 73, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + >>> bytes(res).hex() + 'd2029649000000000000000000000000' + """ + pass diff --git a/pyproject.toml b/pyproject.toml index 551fa79..e92c768 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "bt-decode" -version = "0.3.0a" +version = "0.4.0-a0" description = "A wrapper around the scale-codec crate for fast scale-decoding of Bittensor data structures." readme = "README.md" license = {file = "LICENSE"} diff --git a/src/lib.rs b/src/lib.rs index 2033c24..334a1ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,12 +29,17 @@ mod dyndecoder; #[pymodule(name = "bt_decode")] mod bt_decode { - use std::collections::HashMap; + use std::{collections::HashMap, u128}; use dyndecoder::{fill_memo_using_well_known_types, get_type_id_from_type_string}; use frame_metadata::v15::RuntimeMetadataV15; - use pyo3::types::{PyDict, PyTuple}; - use scale_value::{self, scale::decode_as_type, Composite, Primitive, Value, ValueDef}; + use pyo3::types::{PyDict, PyInt, PyList, PyTuple}; + use scale_info::{form::PortableForm, TypeDefComposite}; + use scale_value::{ + self, + scale::{decode_as_type, encode_as_type}, + Composite, Primitive, Value, ValueDef, Variant, + }; use super::*; @@ -449,6 +454,574 @@ mod bt_decode { } } + fn py_isinstance(py: Python, value: &Py, type_name: &str) -> PyResult { + let locals = PyDict::new_bound(py); + locals.set_item("value", value)?; + + py.run_bound( + &*format!("ret = isinstance(value, {})", type_name), + None, + Some(&locals), + ) + .map_err(|e| { + PyErr::new::(format!( + "Error checking isinstance of: {}: {:?}", + type_name, e + )) + })?; + let ret = locals.get_item("ret").unwrap().unwrap(); + let result = ret.extract::()?; + + Ok(result) + } + + fn py_is_positive(py: Python, value: &Py) -> PyResult { + let locals = PyDict::new_bound(py); + locals.set_item("value", value)?; + + py.run_bound("ret = value >= 0", None, Some(&locals)) + .unwrap(); + let ret = locals.get_item("ret").unwrap().unwrap(); + let result = ret.extract::()?; + + Ok(result) + } + + fn py_has_dict_method(py: Python, value: &Py) -> PyResult { + let locals = PyDict::new_bound(py); + locals.set_item("value", value)?; + + py.run_bound("ret = hasattr(value, \'__dict__\')", None, Some(&locals)) + .unwrap(); + let ret = locals.get_item("ret").unwrap().unwrap(); + let result = ret.extract::()?; + + Ok(result) + } + + fn py_to_dict<'py>(py: Python<'py>, value: &Py) -> PyResult> { + let ret = value.call_method0(py, "__dict__")?; + + let result = ret.downcast_bound::(py)?; + + Ok(result.clone()) + } + + fn int_type_def_to_value( + py: Python, + py_int: &Py, + ty: &scale_info::Type, + type_id: u32, + ) -> PyResult> { + if py_is_positive(py, py_int)? { + let value = py_int.extract::(py)?; + match &ty.type_def { + scale_info::TypeDef::Primitive(scale_info::TypeDefPrimitive::U128) => { + let value = + Value::with_context(ValueDef::Primitive(Primitive::U128(value)), type_id); + return Ok(value); + } + scale_info::TypeDef::Primitive(scale_info::TypeDefPrimitive::U64) => { + let value = + Value::with_context(ValueDef::Primitive(Primitive::U128(value)), type_id); + return Ok(value); + } + scale_info::TypeDef::Primitive(scale_info::TypeDefPrimitive::U32) => { + let value = + Value::with_context(ValueDef::Primitive(Primitive::U128(value)), type_id); + return Ok(value); + } + scale_info::TypeDef::Primitive(scale_info::TypeDefPrimitive::U16) => { + let value = + Value::with_context(ValueDef::Primitive(Primitive::U128(value)), type_id); + return Ok(value); + } + scale_info::TypeDef::Primitive(scale_info::TypeDefPrimitive::U8) => { + let value = + Value::with_context(ValueDef::Primitive(Primitive::U128(value)), type_id); + return Ok(value); + } + _ => { + return Err(PyErr::new::(format!( + "Invalid type for u128 data: {}", + value + ))); + } + } + } else { + let value = py_int.extract::(py)?; + match ty.type_def { + scale_info::TypeDef::Primitive(scale_info::TypeDefPrimitive::I128) => { + let value = + Value::with_context(ValueDef::Primitive(Primitive::I128(value)), type_id); + return Ok(value); + } + scale_info::TypeDef::Primitive(scale_info::TypeDefPrimitive::I64) => { + let value = + Value::with_context(ValueDef::Primitive(Primitive::I128(value)), type_id); + return Ok(value); + } + scale_info::TypeDef::Primitive(scale_info::TypeDefPrimitive::I32) => { + let value = + Value::with_context(ValueDef::Primitive(Primitive::I128(value)), type_id); + return Ok(value); + } + scale_info::TypeDef::Primitive(scale_info::TypeDefPrimitive::I16) => { + let value = + Value::with_context(ValueDef::Primitive(Primitive::I128(value)), type_id); + return Ok(value); + } + scale_info::TypeDef::Primitive(scale_info::TypeDefPrimitive::I8) => { + let value = + Value::with_context(ValueDef::Primitive(Primitive::I128(value)), type_id); + return Ok(value); + } + _ => { + return Err(PyErr::new::(format!( + "Invalid type for i128 data: {}", + value + ))); + } + } + } + } + + fn pylist_to_value( + py: Python, + py_list: &Bound<'_, PyList>, + ty: &scale_info::Type, + type_id: u32, + portable_registry: &PyPortableRegistry, + ) -> PyResult> { + match &ty.type_def { + scale_info::TypeDef::Array(inner) => { + let ty_param = inner.type_param; + let ty_param_id: u32 = ty_param.id; + let ty_ = portable_registry + .registry + .resolve(ty_param_id) + .expect(&format!("Failed to resolve type (1): {:?}", ty_param)); + + let items = py_list + .iter() + .map(|item| { + pyobject_to_value( + py, + item.as_any().as_unbound(), + &ty_, + ty_param_id, + portable_registry, + ) + }) + .collect::>>>()?; + + let value = + Value::with_context(ValueDef::Composite(Composite::Unnamed(items)), type_id); + return Ok(value); + } + scale_info::TypeDef::Tuple(_inner) => { + dbg!(_inner, py_list); + let items = py_list + .iter() + .zip(_inner.fields.clone()) + .map(|(item, ty_)| { + let ty_id: u32 = ty_.id; + let ty_ = portable_registry + .registry + .resolve(ty_id) + .expect(&format!("Failed to resolve type (1): {:?}", ty_)); + pyobject_to_value( + py, + item.as_any().as_unbound(), + &ty_, + ty_id, + portable_registry, + ) + }) + .collect::>>>()?; + + let value = + Value::with_context(ValueDef::Composite(Composite::Unnamed(items)), type_id); + return Ok(value); + } + scale_info::TypeDef::Sequence(inner) => { + let ty_param = inner.type_param; + let ty_param_id: u32 = ty_param.id; + let ty_ = portable_registry + .registry + .resolve(ty_param_id) + .expect(&format!("Failed to resolve type (1): {:?}", ty_param)); + + let items = py_list + .iter() + .map(|item| { + pyobject_to_value( + py, + item.as_any().as_unbound(), + &ty_, + ty_param_id, + portable_registry, + ) + }) + .collect::>>>()?; + + let value = + Value::with_context(ValueDef::Composite(Composite::Unnamed(items)), type_id); + return Ok(value); + } + scale_info::TypeDef::Composite(TypeDefComposite { fields }) => { + if fields.len() == 0 { + return Err(PyErr::new::(format!( + "Unexpected 0 fields for unnamed composite type: {:?}", + ty + ))); + } + + let vals = fields + .iter() + .zip(py_list) + .map(|(field, item)| { + let ty_ = portable_registry + .registry + .resolve(field.ty.id) + .expect(&format!("Failed to resolve type for field: {:?}", field)); + + pyobject_to_value( + py, + item.as_any().as_unbound(), + ty_, + field.ty.id, + portable_registry, + ) + .unwrap() + }) + .collect::>>(); + + let value = + Value::with_context(ValueDef::Composite(Composite::Unnamed(vals)), type_id); + return Ok(value); + } + _ => { + return Err(PyErr::new::(format!( + "Invalid type for a list of data: {}", + py_list + ))); + } + } + } + + fn pyobject_to_value_no_option_check( + py: Python, + to_encode: &Py, + ty: &scale_info::Type, + type_id: u32, + portable_registry: &PyPortableRegistry, + ) -> PyResult> { + if to_encode.is_none(py) { + // If none and NOT option, + return Err(PyErr::new::(format!( + "Invalid type for None: {:?}", + ty.type_def + ))); + } + + if py_isinstance(py, to_encode, "bool")? { + let value = to_encode.extract::(py)?; + + match ty.type_def { + scale_info::TypeDef::Primitive(scale_info::TypeDefPrimitive::Bool) => { + let value = + Value::with_context(ValueDef::Primitive(Primitive::Bool(value)), type_id); + return Ok(value); + } + _ => { + return Err(PyErr::new::(format!( + "Invalid type for bool data: {}", + value + ))); + } + } + } else if py_isinstance(py, to_encode, "str")? { + if to_encode.extract::(py).is_ok() + && matches!( + ty.type_def, + scale_info::TypeDef::Primitive(scale_info::TypeDefPrimitive::Char) + ) + { + let char_value = to_encode.extract::(py)?; + let value = + Value::with_context(ValueDef::Primitive(Primitive::Char(char_value)), type_id); + + Ok(value) + } else if let Ok(str_value) = to_encode.extract::(py) { + match ty.type_def { + scale_info::TypeDef::Primitive(scale_info::TypeDefPrimitive::Str) => { + let value = Value::with_context( + ValueDef::Primitive(Primitive::String(str_value)), + type_id, + ); + return Ok(value); + } + _ => { + return Err(PyErr::new::(format!( + "Invalid type for string data: {}", + str_value + ))); + } + } + } else { + return Err(PyErr::new::(format!( + "Invalid type for string data: {}", + to_encode + ))); + } + } else if py_isinstance(py, to_encode, "int")? + && matches!(&ty.type_def, scale_info::TypeDef::Primitive(_)) + { + let as_py_int = to_encode.downcast_bound::(py)?.as_unbound(); + + return int_type_def_to_value(py, as_py_int, ty, type_id); + } else if py_isinstance(py, to_encode, "int")? + && matches!(&ty.type_def, scale_info::TypeDef::Compact(_)) + { + // Must be a Compact int + let as_py_int = to_encode.downcast_bound::(py)?.as_unbound(); + + match &ty.type_def { + scale_info::TypeDef::Compact(inner) => { + let inner_type_id = inner.type_param.id; + let inner_type_ = portable_registry.registry.resolve(inner_type_id); + if let Some(inner_type) = inner_type_ { + let mut inner_value = + int_type_def_to_value(py, as_py_int, inner_type, inner_type_id)?; + inner_value.context = type_id; + return Ok(inner_value); + } + } + _ => {} + } + + return Err(PyErr::new::(format!( + "Invalid type for u128 data: {}", + to_encode + ))); + } else if py_isinstance(py, to_encode, "tuple")? { + let tuple_value = to_encode.downcast_bound::(py)?; + let as_list = tuple_value.to_list(); + + pylist_to_value(py, &as_list, ty, type_id, portable_registry).map_err(|_e| { + PyErr::new::(format!( + "Invalid type for tuple data: {}", + tuple_value + )) + }) + } else if py_isinstance(py, to_encode, "list")? { + let as_list = to_encode.downcast_bound::(py)?; + + pylist_to_value(py, &as_list, ty, type_id, portable_registry).map_err(|_e| { + PyErr::new::(format!( + "Invalid type for list data: {}", + as_list + )) + }) + } else if py_isinstance(py, to_encode, "dict")? { + let py_dict = to_encode.downcast_bound::(py)?; + + match &ty.type_def { + scale_info::TypeDef::Composite(inner) => { + let fields = &inner.fields; + let mut dict: Vec<(String, Value)> = vec![]; + + for field in fields.iter() { + let field_name = + field.name.clone().ok_or(PyErr::new::< + pyo3::exceptions::PyValueError, + _, + >(format!( + "Invalid type for dict, type: {:?}", + ty.type_def + )))?; + + let value_from_dict = + py_dict.get_item(field_name.clone())?.ok_or(PyErr::new::< + pyo3::exceptions::PyValueError, + _, + >( + format!( + "Invalid type for dict; missing field {}, type: {:?}", + field_name.clone(), + ty.type_def + ) + ))?; + + let inner_type = + portable_registry + .registry + .resolve(field.ty.id) + .expect(&format!( + "Inner type: {:?} was not in registry after being registered", + field.ty + )); + + let as_value = pyobject_to_value( + py, + value_from_dict.as_unbound(), + inner_type, + field.ty.id, + portable_registry, + )?; + + dict.push((field_name, as_value)); + } + + let value = + Value::with_context(ValueDef::Composite(Composite::Named(dict)), type_id); + return Ok(value); + } + _ => { + return Err(PyErr::new::(format!( + "Invalid type for dict data: {}", + py_dict + ))); + } + } + + //} else if let Ok(value) = to_encode.downcast_bound::(py) { + } else if py_has_dict_method(py, to_encode)? { + // Convert object to dict + let py_dict = py_to_dict(py, to_encode)?; + + match &ty.type_def { + scale_info::TypeDef::Composite(TypeDefComposite { fields }) => { + let mut dict: Vec<(String, Value)> = vec![]; + + for field in fields.iter() { + let field_name = + field.name.clone().ok_or(PyErr::new::< + pyo3::exceptions::PyValueError, + _, + >(format!( + "Invalid type for dict, type: {:?}", + ty.type_def + )))?; + + let value_from_dict = + py_dict.get_item(field_name.clone())?.ok_or(PyErr::new::< + pyo3::exceptions::PyValueError, + _, + >( + format!( + "Invalid type for dict; missing field {}, type: {:?}", + field_name.clone(), + ty.type_def + ) + ))?; + + let inner_type = + portable_registry + .registry + .resolve(field.ty.id) + .expect(&format!( + "Inner type: {:?} was not in registry after being registered", + field.ty + )); + + let as_value = pyobject_to_value( + py, + value_from_dict.as_unbound(), + inner_type, + field.ty.id, + portable_registry, + )?; + + dict.push((field_name, as_value)); + } + + let value = + Value::with_context(ValueDef::Composite(Composite::Named(dict)), type_id); + return Ok(value); + } + _ => { + return Err(PyErr::new::(format!( + "Invalid type for dict data: {}", + to_encode + ))); + } + } + } else { + return Err(PyErr::new::(format!( + "Invalid type for data: {} of type {}", + to_encode, + to_encode.getattr(py, "__class__").unwrap_or(py.None()) + ))); + } + } + + fn pyobject_to_value( + py: Python, + to_encode: &Py, + ty: &scale_info::Type, + type_id: u32, + portable_registry: &PyPortableRegistry, + ) -> PyResult> { + // Check if the expected type is an option + match ty.type_def.clone() { + scale_info::TypeDef::Variant(inner) => { + let is_option: bool = if inner.variants.len() == 2 { + let variant_names = inner + .variants + .iter() + .map(|v| &*v.name) + .collect::>(); + variant_names.contains(&"Some") && variant_names.contains(&"None") + } else { + false + }; + + if is_option { + if to_encode.is_none(py) { + // None + let none_variant: scale_value::Variant = + Variant::unnamed_fields("None", vec![]); // No fields because it's None + + return Ok(Value::with_context( + ValueDef::Variant(none_variant), + type_id, + )); + } else { + // Some + // Get inner type + let inner_type_id: u32 = inner.variants[1].fields[0].ty.id; + let inner_type: &scale_info::Type = portable_registry + .registry + .resolve(inner_type_id) + .ok_or(PyErr::new::(format!( + "Could not find inner_type: {:?} for Option: {:?}", + ty.type_def, inner_type_id + )))?; + let inner_value: Value = pyobject_to_value_no_option_check( + py, + to_encode, + inner_type, + inner_type_id, + portable_registry, + )?; + let some_variant: scale_value::Variant = + Variant::unnamed_fields("Some", vec![inner_value]); // No fields because it's None + + return Ok(Value::with_context( + ValueDef::Variant(some_variant), + type_id, + )); + } + } // else: Regular conversion + } + _ => {} // Regular conversion + } + + return pyobject_to_value_no_option_check(py, to_encode, ty, type_id, portable_registry); + } + #[pyfunction(name = "decode")] fn py_decode( py: Python, @@ -471,4 +1044,34 @@ mod bt_decode { value_to_pyobject(py, decoded) } + + #[pyfunction(name = "encode")] + fn py_encode( + py: Python, + type_string: &str, + portable_registry: &PyPortableRegistry, + to_encode: Py, + ) -> PyResult> { + // Create a memoization table for the type string to type id conversion + let mut memo = HashMap::::new(); + + let mut curr_registry = portable_registry.registry.clone(); + + fill_memo_using_well_known_types(&mut memo, &curr_registry); + + let type_id: u32 = get_type_id_from_type_string(&mut memo, type_string, &mut curr_registry) + .expect("Failed to get type id from type string"); + + let ty = curr_registry + .resolve(type_id) + .expect(&format!("Failed to resolve type (0): {:?}", type_string)); + + let as_value: Value = + pyobject_to_value(py, &to_encode, ty, type_id, portable_registry)?; + + let mut encoded: Vec = Vec::::new(); + encode_as_type(&as_value, type_id, &curr_registry, &mut encoded).expect("Failed to encode"); + + Ok(encoded) + } } diff --git a/tests/test_encode_by_type_string.py b/tests/test_encode_by_type_string.py new file mode 100644 index 0000000..cf72c02 --- /dev/null +++ b/tests/test_encode_by_type_string.py @@ -0,0 +1,238 @@ +from typing import Any, Dict, Tuple + +import pytest + +import bt_decode + + +TEST_TYPE_STRING_SCALE_INFO_DECODING: Dict[str, Tuple[str, Any]] = { + "scale_info::2": ("01", 1), # u8 + "scale_info::441": ( + "c40352ca71e26e83b6c86058fd4d3c9643ea5dc11f120a7c80f47ec5770b457d8853018ca894cb3d02aaf9b96741c831a3970cf250a58ec46e6a66f269be0b4b040400ba94330000000000c7020000e0aaf22c000000000000000000000000ad240404000000000000000000000000000000000000000000000000000000000000000000048853018ca894cb3d02aaf9b96741c831a3970cf250a58ec46e6a66f269be0b4b6220f458c056ce4900c0bc4276030000006e1e9b00000404feff0300009d03", + { + "hotkey": ( + ( + 196, + 3, + 82, + 202, + 113, + 226, + 110, + 131, + 182, + 200, + 96, + 88, + 253, + 77, + 60, + 150, + 67, + 234, + 93, + 193, + 31, + 18, + 10, + 124, + 128, + 244, + 126, + 197, + 119, + 11, + 69, + 125, + ), + ), + "coldkey": ( + ( + 136, + 83, + 1, + 140, + 168, + 148, + 203, + 61, + 2, + 170, + 249, + 185, + 103, + 65, + 200, + 49, + 163, + 151, + 12, + 242, + 80, + 165, + 142, + 196, + 110, + 106, + 102, + 242, + 105, + 190, + 11, + 75, + ), + ), + "uid": 1, + "netuid": 1, + "active": False, + "axon_info": { + "block": 3380410, + "version": 711, + "ip": 754100960, + "port": 9389, + "ip_type": 4, + "protocol": 4, + "placeholder1": 0, + "placeholder2": 0, + }, + "prometheus_info": { + "block": 0, + "version": 0, + "ip": 0, + "port": 0, + "ip_type": 0, + }, + "stake": ( + ( + ( + ( + 136, + 83, + 1, + 140, + 168, + 148, + 203, + 61, + 2, + 170, + 249, + 185, + 103, + 65, + 200, + 49, + 163, + 151, + 12, + 242, + 80, + 165, + 142, + 196, + 110, + 106, + 102, + 242, + 105, + 190, + 11, + 75, + ), + ), + 373098520, + ), + ), + "rank": 48, + "emission": 1209237, + "incentive": 48, + "consensus": 47, + "trust": 56720, + "validator_trust": 0, + "dividends": 0, + "last_update": 2541467, + "validator_permit": False, + "weights": ((1, 65535),), + "bonds": (), + "pruning_score": 231, + }, + ), # NeuronInfo + "Option ": ("00", None), + "Option": ("010100", 1), # u16 +} + + +TEST_TYPE_STRING_PLAIN_DECODING: Dict[str, Tuple[str, Any]] = { + "bool": ("01", True), + "bool ": ("00", False), + "u8": ("01", 1), + "u16": ("0100", 1), + "u32": ("01000000", 1), + "u64": ("0100000000000000", 1), + "u128": ("01000000000000000000000000000000", 1), + "Compact": ("00", 0), + "Compact ": ("fd03", 2**8 - 1), + "Compact": ("feff0300", 2**16 - 1), + "Compact": ("03ffffffff", 2**32 - 1), + "Compact": ("13ffffffffffffffff", 2**64 - 1), + "Option": ("010c", 12), + "Option": ("00", None), + "Option": ("00", None), + "Option ": ("0101000000", 1), + "()": ("", ()), + "[u8; 4]": ("62616265", (98, 97, 98, 101)), + "[u8; 4]": ("62616265", [98, 97, 98, 101]), + "Vec": ("0c010203", (1, 2, 3)), + "Vec ": ("00", []), + "Vec ": ("00", ()), + "(u8, u16) ": ("7bffff", (123, 2**16-1)), + "str": ("0c666f6f", "foo"), +} + +TEST_TYPES_JSON = "tests/test_types.json" + + +@pytest.mark.parametrize( + "type_string,test_hex,test_value", + [(x, y, z) for x, (y, z) in TEST_TYPE_STRING_PLAIN_DECODING.items()], +) +class TestEncodeByPlainTypeString: + # Test combinations of human-readable type strings and hex-encoded values + registry: bt_decode.PortableRegistry + + @classmethod + def setup_class(cls) -> None: + with open(TEST_TYPES_JSON, "r") as f: + types_json_str = f.read() + + cls.registry = bt_decode.PortableRegistry.from_json(types_json_str) + + def test_encode_values(self, type_string: str, test_value: Any, test_hex: str): + type_string = type_string.strip() + + test_bytes = bytes.fromhex(test_hex) + actual: list[int] = bt_decode.encode(type_string, self.registry, test_value) + assert bytes(actual) == test_bytes + + +@pytest.mark.parametrize( + "type_string,test_hex,test_value", + [(x, y, z) for x, (y, z) in TEST_TYPE_STRING_SCALE_INFO_DECODING.items()], +) +class TestEncodeByScaleInfoTypeString: + # Test combinations of scale_info::NUM -formatted type strings and hex-encoded values + registry: bt_decode.PortableRegistry + + @classmethod + def setup_class(cls) -> None: + with open(TEST_TYPES_JSON, "r") as f: + types_json_str = f.read() + + cls.registry = bt_decode.PortableRegistry.from_json(types_json_str) + + def test_encode_values(self, type_string: str, test_value: Any, test_hex: str): + type_string = type_string.strip() + + test_bytes = bytes.fromhex(test_hex) + actual: list[int] = bt_decode.encode(type_string, self.registry, test_value) + assert bytes(actual) == test_bytes