Skip to main content

bittensor_core/codec/
value.rs

1//! The decoded-value model: the exact shapes cyscale rendered SCALE data as,
2//! which the SDK's reads/intents layer pattern-matches on. The shape corpus
3//! (sdk/python/tests/fixtures/shape_corpus) is the contract; this enum is its
4//! Rust carrier, materialized 1:1 into Python objects by the binding crate.
5
6#![allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)]
7
8use std::fmt;
9
10/// One decoded SCALE value in cyscale's shape conventions.
11#[derive(Debug, Clone, PartialEq)]
12pub enum Value {
13    /// Python ``None`` (Option misses, ``Pays::No``-style unit payload holes).
14    Null,
15    Bool(bool),
16    /// All signed integers (and any unsigned that fits).
17    Int(i128),
18    /// Unsigned integers above ``i128::MAX`` (u128 range).
19    Uint(u128),
20    /// u256: little-endian bytes, materialized as an arbitrary-size int.
21    U256([u8; 32]),
22    Str(String),
23    /// Raw bytes (kept distinct so the binding can hand back ``bytes`` where
24    /// cyscale did; most byte-like types render as 0x-hex strings instead).
25    Bytes(Vec<u8>),
26    /// Python ``list``.
27    List(Vec<Value>),
28    /// Python ``tuple`` (SCALE tuples and multi-unnamed-field shapes).
29    Tuple(Vec<Value>),
30    /// Python ``dict`` in insertion order; keys are values because BTreeMap
31    /// keys keep their native type (ints stay ints).
32    Dict(Vec<(Value, Value)>),
33}
34
35impl Value {
36    pub fn str(s: impl Into<String>) -> Self {
37        Value::Str(s.into())
38    }
39
40    pub fn hex(data: &[u8]) -> Self {
41        Value::Str(format!("0x{}", hex::encode(data)))
42    }
43
44    /// Struct-style dict with string keys.
45    pub fn record(fields: Vec<(String, Value)>) -> Self {
46        Value::Dict(
47            fields
48                .into_iter()
49                .map(|(k, v)| (Value::Str(k), v))
50                .collect(),
51        )
52    }
53}
54
55/// Render as the JSON the corpus recorder's ``jsonable()`` produced: bytes as
56/// 0x-hex, tuples as lists, dict keys stringified. Only used by tests to
57/// compare against corpus fixtures.
58pub fn to_corpus_json(value: &Value) -> serde_json::Value {
59    use serde_json::json;
60    match value {
61        Value::Null => serde_json::Value::Null,
62        Value::Bool(b) => json!(b),
63        Value::Int(i) => serde_json::Value::Number(
64            serde_json::Number::from_i128(*i).unwrap_or_else(|| serde_json::Number::from(0)),
65        ),
66        Value::Uint(u) => serde_json::Value::Number(
67            serde_json::Number::from_u128(*u).unwrap_or_else(|| serde_json::Number::from(0)),
68        ),
69        Value::U256(le) => {
70            // The recorder went through Python ints and json.dumps, which
71            // prints arbitrary precision; serde_json's arbitrary_precision
72            // feature lets the comparison carry it digit-exact.
73            let number = u256_decimal(le)
74                .parse::<serde_json::Number>()
75                .unwrap_or_else(|_| serde_json::Number::from(0));
76            serde_json::Value::Number(number)
77        }
78        Value::Str(s) => json!(s),
79        Value::Bytes(b) => json!(format!("0x{}", hex::encode(b))),
80        Value::List(items) | Value::Tuple(items) => {
81            serde_json::Value::Array(items.iter().map(to_corpus_json).collect())
82        }
83        Value::Dict(entries) => {
84            let mut map = serde_json::Map::new();
85            for (k, v) in entries {
86                map.insert(corpus_key(k), to_corpus_json(v));
87            }
88            serde_json::Value::Object(map)
89        }
90    }
91}
92
93/// ``str(key)`` the way Python renders dict keys in the recorder.
94fn corpus_key(key: &Value) -> String {
95    match key {
96        Value::Str(s) => s.clone(),
97        Value::Int(i) => i.to_string(),
98        Value::Uint(u) => u.to_string(),
99        Value::Bool(b) => (if *b { "True" } else { "False" }).to_string(),
100        other => format!("{other:?}"),
101    }
102}
103
104/// Decimal rendering of a little-endian u256.
105pub fn u256_decimal(le: &[u8; 32]) -> String {
106    // Repeated division by 10 over the big-endian limbs; 32 bytes is small
107    // enough that the simple O(n * digits) loop is instant.
108    let mut limbs: Vec<u8> = le.iter().rev().copied().collect(); // big-endian
109    let mut digits = Vec::new();
110    while limbs.iter().any(|&b| b != 0) {
111        let mut remainder: u32 = 0;
112        for byte in limbs.iter_mut() {
113            let value = (remainder << 8) | u32::from(*byte);
114            *byte = (value / 10) as u8;
115            remainder = value % 10;
116        }
117        digits.push(char::from(b'0' + remainder as u8));
118    }
119    if digits.is_empty() {
120        return "0".to_string();
121    }
122    digits.iter().rev().collect()
123}
124
125impl fmt::Display for Value {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        write!(f, "{}", to_corpus_json(self))
128    }
129}