Skip to main content

bittensor_core/codec/
encode.rs

1//! SCALE encode with the seam's lenient input coercions: ss58 strings or
2//! 0x-hex for account ids, hex/str/bytes for byte carriers, bare variant
3//! names or `{"Variant": payload}` dicts for enums, `None` for Option-None,
4//! ints for compacts. Byte-equality with the previous codec is pinned by the
5//! golden call/payload/extrinsic vectors.
6
7// Client-side codec, not runtime code: buffers grow as values encode and
8// arithmetic operates on lengths and era math.
9#![allow(
10    clippy::indexing_slicing,
11    clippy::arithmetic_side_effects,
12    clippy::cast_possible_truncation
13)]
14
15use std::cell::Cell;
16
17use scale_info::{form::PortableForm, Field, TypeDef, TypeDefPrimitive};
18
19use crate::codec::value::Value;
20use crate::error::CoreError;
21use crate::keys::public_key_from_ss58;
22use crate::runtime::type_string::{Primitive, TypeSpec};
23use crate::runtime::Runtime;
24
25/// Recursion ceiling, mirroring `decode.rs`. Metadata is untrusted (it comes
26/// from the connected node) and `encode_fields` unwraps single-field newtypes
27/// without consuming value structure, so a self-referential composite would
28/// otherwise recurse until the stack aborts the process. Encoding has no
29/// cursor to carry depth, so a thread-local tracks it (encode never crosses
30/// threads mid-value).
31const MAX_ENCODE_DEPTH: usize = 256;
32
33thread_local! {
34    static ENCODE_DEPTH: Cell<usize> = const { Cell::new(0) };
35}
36
37struct DepthGuard;
38
39impl DepthGuard {
40    fn enter() -> Result<Self, CoreError> {
41        let depth = ENCODE_DEPTH.with(|d| {
42            let next = d.get() + 1;
43            d.set(next);
44            next
45        });
46        // The guard is constructed either way, so Drop rebalances the counter
47        // on both the error path and normal unwinding.
48        let guard = DepthGuard;
49        if depth > MAX_ENCODE_DEPTH {
50            return Err(CoreError::Codec(format!(
51                "encode recursion exceeds {MAX_ENCODE_DEPTH} levels (malformed or malicious type definition)"
52            )));
53        }
54        Ok(guard)
55    }
56}
57
58impl Drop for DepthGuard {
59    fn drop(&mut self) {
60        ENCODE_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
61    }
62}
63
64/// Compact-encode an unsigned integer.
65pub fn compact(value: u128, out: &mut Vec<u8>) -> Result<(), CoreError> {
66    if value < 1 << 6 {
67        out.push((value as u8) << 2);
68    } else if value < 1 << 14 {
69        out.extend_from_slice(&(((value as u16) << 2) | 0b01).to_le_bytes());
70    } else if value < 1 << 30 {
71        out.extend_from_slice(&(((value as u32) << 2) | 0b10).to_le_bytes());
72    } else {
73        let bytes = value.to_le_bytes();
74        let significant = 16 - bytes.iter().rev().take_while(|&&b| b == 0).count();
75        let len = significant.max(4);
76        out.push((((len - 4) as u8) << 2) | 0b11);
77        out.extend_from_slice(&bytes[..len]);
78    }
79    Ok(())
80}
81
82impl Runtime {
83    pub fn encode_spec(&self, spec: &TypeSpec, value: &Value) -> Result<Vec<u8>, CoreError> {
84        let mut out = Vec::new();
85        self.encode_value(spec, value, &mut out)?;
86        Ok(out)
87    }
88
89    pub fn encode_value(
90        &self,
91        spec: &TypeSpec,
92        value: &Value,
93        out: &mut Vec<u8>,
94    ) -> Result<(), CoreError> {
95        let _depth = DepthGuard::enter()?;
96        match spec {
97            TypeSpec::Id(id) => self.encode_id(*id, value, out),
98            TypeSpec::Primitive(p) => encode_primitive_spec(*p, value, out),
99            TypeSpec::Sequence(inner) => {
100                if matches!(**inner, TypeSpec::Primitive(Primitive::U8)) {
101                    let bytes = coerce_bytes(value, None)?;
102                    compact(bytes.len() as u128, out)?;
103                    out.extend_from_slice(&bytes);
104                    return Ok(());
105                }
106                let items = coerce_list(value)?;
107                compact(items.len() as u128, out)?;
108                for item in items {
109                    self.encode_value(inner, item, out)?;
110                }
111                Ok(())
112            }
113            TypeSpec::Option(inner) => {
114                if matches!(value, Value::Null) {
115                    out.push(0);
116                    return Ok(());
117                }
118                out.push(1);
119                self.encode_value(inner, value, out)
120            }
121            TypeSpec::Array(inner, len) => {
122                if matches!(**inner, TypeSpec::Primitive(Primitive::U8)) {
123                    let bytes = coerce_bytes(value, Some(*len as usize))?;
124                    out.extend_from_slice(&bytes);
125                    return Ok(());
126                }
127                let items = coerce_list(value)?;
128                if items.len() != *len as usize {
129                    return Err(CoreError::Codec(format!(
130                        "array expects {len} elements, got {}",
131                        items.len()
132                    )));
133                }
134                for item in items {
135                    self.encode_value(inner, item, out)?;
136                }
137                Ok(())
138            }
139            TypeSpec::Tuple(parts) => {
140                let items = coerce_list(value)?;
141                if items.len() != parts.len() {
142                    return Err(CoreError::Codec(format!(
143                        "tuple expects {} elements, got {}",
144                        parts.len(),
145                        items.len()
146                    )));
147                }
148                for (part, item) in parts.iter().zip(items) {
149                    self.encode_value(part, item, out)?;
150                }
151                Ok(())
152            }
153            TypeSpec::Compact(_) => compact(coerce_uint(value)?, out),
154            TypeSpec::Bytes => {
155                let bytes = coerce_bytes(value, None)?;
156                compact(bytes.len() as u128, out)?;
157                out.extend_from_slice(&bytes);
158                Ok(())
159            }
160            TypeSpec::AccountId => {
161                let raw = self.coerce_account_id(value)?;
162                out.extend_from_slice(&raw);
163                Ok(())
164            }
165            TypeSpec::Era => self.encode_era_value(value, out),
166            TypeSpec::Call => {
167                // Calls arrive as raw bytes (CallBytes) or a decoded call dict.
168                self.encode_call_input(value, out)
169            }
170            TypeSpec::Extrinsic => Err(CoreError::Codec(
171                "extrinsics are assembled by the transport layer".into(),
172            )),
173        }
174    }
175
176    pub fn encode_id(&self, id: u32, value: &Value, out: &mut Vec<u8>) -> Result<(), CoreError> {
177        let _depth = DepthGuard::enter()?;
178        let ty = self.resolve(id)?;
179        let segments = &ty.path.segments;
180        let last = segments.last().map(String::as_str);
181
182        match last {
183            Some("AccountId32") => {
184                let raw = self.coerce_account_id(value)?;
185                out.extend_from_slice(&raw);
186                return Ok(());
187            }
188            Some("Era") if segments.first().map(String::as_str) == Some("sp_runtime") => {
189                return self.encode_era_value(value, out);
190            }
191            Some("MultiAddress") => {
192                return self.encode_multiaddress(ty, value, out);
193            }
194            Some("BTreeMap") => {
195                return self.encode_btreemap(ty, value, out);
196            }
197            _ => {}
198        }
199
200        // The outer RuntimeCall (and pallet call enums embedded in params)
201        // accept pre-composed raw bytes.
202        if let Value::Bytes(raw) = value {
203            if self.extrinsic.call_type == Some(id) {
204                out.extend_from_slice(raw);
205                return Ok(());
206            }
207        }
208
209        match &ty.type_def {
210            TypeDef::Primitive(p) => encode_primitive(p, value, out),
211            TypeDef::Compact(_) => compact(coerce_uint(value)?, out),
212            TypeDef::Composite(composite) => self.encode_fields(&composite.fields, value, out),
213            TypeDef::Variant(variant) => {
214                let is_option = last == Some("Option") && segments.len() == 1;
215                if is_option {
216                    if matches!(value, Value::Null) {
217                        out.push(0);
218                        return Ok(());
219                    }
220                    let some = variant
221                        .variants
222                        .iter()
223                        .find(|v| v.name == "Some")
224                        .ok_or_else(|| CoreError::Codec("Option without Some".into()))?;
225                    out.push(some.index);
226                    return self.encode_fields(&some.fields, value, out);
227                }
228                let (name, payload) = coerce_variant(value)?;
229                let chosen = variant
230                    .variants
231                    .iter()
232                    .find(|v| v.name == name)
233                    .ok_or_else(|| {
234                        CoreError::Codec(format!("no variant named {name:?} in type {id}"))
235                    })?;
236                out.push(chosen.index);
237                if chosen.fields.is_empty() {
238                    return Ok(());
239                }
240                self.encode_fields(&chosen.fields, &payload, out)
241            }
242            TypeDef::Sequence(s) => {
243                if self.is_u8_encode(s.type_param.id) {
244                    let bytes = coerce_bytes(value, None)?;
245                    compact(bytes.len() as u128, out)?;
246                    out.extend_from_slice(&bytes);
247                    return Ok(());
248                }
249                let items = coerce_list(value)?;
250                compact(items.len() as u128, out)?;
251                for item in items {
252                    self.encode_id(s.type_param.id, item, out)?;
253                }
254                Ok(())
255            }
256            TypeDef::Array(a) => {
257                if self.is_u8_encode(a.type_param.id) {
258                    let bytes = coerce_bytes(value, Some(a.len as usize))?;
259                    out.extend_from_slice(&bytes);
260                    return Ok(());
261                }
262                let items = coerce_list(value)?;
263                if items.len() != a.len as usize {
264                    return Err(CoreError::Codec(format!(
265                        "array expects {} elements, got {}",
266                        a.len,
267                        items.len()
268                    )));
269                }
270                for item in items {
271                    self.encode_id(a.type_param.id, item, out)?;
272                }
273                Ok(())
274            }
275            TypeDef::Tuple(t) => {
276                if t.fields.is_empty() {
277                    return Ok(());
278                }
279                let items = coerce_list(value)?;
280                if items.len() != t.fields.len() {
281                    return Err(CoreError::Codec(format!(
282                        "tuple expects {} elements, got {}",
283                        t.fields.len(),
284                        items.len()
285                    )));
286                }
287                for (field, item) in t.fields.iter().zip(items) {
288                    self.encode_id(field.id, item, out)?;
289                }
290                Ok(())
291            }
292            TypeDef::BitSequence(_) => Err(CoreError::Codec(
293                "bit sequences are not used by this chain".into(),
294            )),
295        }
296    }
297
298    fn is_u8_encode(&self, id: u32) -> bool {
299        matches!(
300            self.types.resolve(id).map(|t| &t.type_def),
301            Some(TypeDef::Primitive(TypeDefPrimitive::U8))
302        )
303    }
304
305    /// Composite-body encode: named fields from a dict, single unnamed from
306    /// the bare value, several unnamed from a list/tuple.
307    fn encode_fields(
308        &self,
309        fields: &[Field<PortableForm>],
310        value: &Value,
311        out: &mut Vec<u8>,
312    ) -> Result<(), CoreError> {
313        if fields.is_empty() {
314            return Ok(());
315        }
316        let named = fields.iter().all(|f| f.name.is_some());
317        if named {
318            let Value::Dict(entries) = value else {
319                return Err(CoreError::Codec(format!(
320                    "struct expects a dict, got {value}"
321                )));
322            };
323            for field in fields {
324                let name = field.name.as_deref().unwrap_or_default();
325                let item = entries
326                    .iter()
327                    .find(|(k, _)| matches!(k, Value::Str(s) if s == name))
328                    .map(|(_, v)| v)
329                    .ok_or_else(|| CoreError::Codec(format!("missing struct field {name:?}")))?;
330                self.encode_id(field.ty.id, item, out)?;
331            }
332            return Ok(());
333        }
334        if fields.len() == 1 {
335            // Legacy-codec compat: scalecodec's newtype encode (a composite
336            // with one unnamed field, e.g. BoundedVec) consumed one
337            // list-nesting level, so legacy callers double-wrap sequence
338            // payloads — `CommitmentInfo { fields: [[{"Raw5": ...}]] }`.
339            // Prefer the bare sequence: its sole item can itself be a list
340            // (a tuple or nested sequence). Only retry the legacy wrapper
341            // when the canonical shape does not encode.
342            if let Value::List(items) | Value::Tuple(items) = value {
343                if items.len() == 1
344                    && matches!(items[0], Value::List(_))
345                    && matches!(
346                        self.resolve(fields[0].ty.id).map(|t| &t.type_def),
347                        Ok(TypeDef::Sequence(_))
348                    )
349                {
350                    let start = out.len();
351                    if self.encode_id(fields[0].ty.id, value, out).is_ok() {
352                        return Ok(());
353                    }
354                    out.truncate(start);
355                    return self.encode_id(fields[0].ty.id, &items[0], out);
356                }
357            }
358            return self.encode_id(fields[0].ty.id, value, out);
359        }
360        let items = coerce_list(value)?;
361        if items.len() != fields.len() {
362            return Err(CoreError::Codec(format!(
363                "expects {} unnamed fields, got {}",
364                fields.len(),
365                items.len()
366            )));
367        }
368        for (field, item) in fields.iter().zip(items) {
369            self.encode_id(field.ty.id, item, out)?;
370        }
371        Ok(())
372    }
373
374    fn encode_multiaddress(
375        &self,
376        ty: &scale_info::Type<PortableForm>,
377        value: &Value,
378        out: &mut Vec<u8>,
379    ) -> Result<(), CoreError> {
380        let TypeDef::Variant(variant) = &ty.type_def else {
381            return Err(CoreError::Codec("MultiAddress is not an enum".into()));
382        };
383        // Bare strings/bytes mean the Id variant; explicit dicts choose.
384        match value {
385            Value::Str(_) | Value::Bytes(_) => {
386                let id_variant = variant
387                    .variants
388                    .iter()
389                    .find(|v| v.name == "Id")
390                    .ok_or_else(|| CoreError::Codec("MultiAddress without Id".into()))?;
391                out.push(id_variant.index);
392                let raw = self.coerce_account_id(value)?;
393                out.extend_from_slice(&raw);
394                Ok(())
395            }
396            Value::Dict(_) => {
397                let (name, payload) = coerce_variant(value)?;
398                let chosen = variant
399                    .variants
400                    .iter()
401                    .find(|v| v.name == name)
402                    .ok_or_else(|| {
403                        CoreError::Codec(format!("no MultiAddress variant named {name:?}"))
404                    })?;
405                out.push(chosen.index);
406                self.encode_fields(&chosen.fields, &payload, out)
407            }
408            other => Err(CoreError::Codec(format!(
409                "cannot encode {other} as MultiAddress"
410            ))),
411        }
412    }
413
414    fn encode_btreemap(
415        &self,
416        ty: &scale_info::Type<PortableForm>,
417        value: &Value,
418        out: &mut Vec<u8>,
419    ) -> Result<(), CoreError> {
420        let TypeDef::Composite(composite) = &ty.type_def else {
421            return Err(CoreError::Codec("BTreeMap is not a composite".into()));
422        };
423        let inner = composite
424            .fields
425            .first()
426            .map(|f| f.ty.id)
427            .ok_or_else(|| CoreError::Codec("BTreeMap has no inner field".into()))?;
428        let TypeDef::Sequence(seq) = &self.resolve(inner)?.type_def else {
429            return Err(CoreError::Codec("BTreeMap inner is not a sequence".into()));
430        };
431        let TypeDef::Tuple(pair) = &self.resolve(seq.type_param.id)?.type_def else {
432            return Err(CoreError::Codec("BTreeMap pair is not a tuple".into()));
433        };
434        let (key_ty, value_ty) = match pair.fields.as_slice() {
435            [k, v] => (k.id, v.id),
436            _ => return Err(CoreError::Codec("BTreeMap pair is not 2-ary".into())),
437        };
438        let Value::Dict(entries) = value else {
439            return Err(CoreError::Codec("BTreeMap expects a dict".into()));
440        };
441        compact(entries.len() as u128, out)?;
442        for (k, v) in entries {
443            self.encode_id(key_ty, k, out)?;
444            self.encode_id(value_ty, v, out)?;
445        }
446        Ok(())
447    }
448
449    /// Era inputs: `"00"` (immortal), 0/None-like handled by caller, or
450    /// `{"period": N, "current": M}` / `{"period": N, "phase": P}`.
451    pub fn encode_era_value(&self, value: &Value, out: &mut Vec<u8>) -> Result<(), CoreError> {
452        match value {
453            Value::Str(s) if s == "00" => {
454                out.push(0);
455                Ok(())
456            }
457            Value::Dict(entries) => {
458                let get = |name: &str| -> Option<u64> {
459                    entries.iter().find_map(|(k, v)| match (k, v) {
460                        (Value::Str(s), Value::Int(i)) if s == name => u64::try_from(*i).ok(),
461                        (Value::Str(s), Value::Uint(u)) if s == name => u64::try_from(*u).ok(),
462                        _ => None,
463                    })
464                };
465                let period = get("period")
466                    .ok_or_else(|| CoreError::Codec("mortal era needs a period".into()))?;
467                let phase = match (get("phase"), get("current")) {
468                    (Some(phase), _) => phase,
469                    (None, Some(current)) => {
470                        let calculated = calculated_period(period);
471                        let quantize_factor = (calculated >> 12).max(1);
472                        (current % calculated) / quantize_factor * quantize_factor
473                    }
474                    (None, None) => {
475                        return Err(CoreError::Codec(
476                            "mortal era needs a phase or current block".into(),
477                        ))
478                    }
479                };
480                let calculated = calculated_period(period);
481                let quantize_factor = (calculated >> 12).max(1);
482                // The low nibble stores log2(period) - 1 (decode does
483                // `2 << low`), clamped to the mortal range.
484                let low = u64::from(calculated.trailing_zeros())
485                    .saturating_sub(1)
486                    .clamp(1, 15);
487                let encoded = low | ((phase / quantize_factor) << 4);
488                out.extend_from_slice(&(encoded as u16).to_le_bytes());
489                Ok(())
490            }
491            other => Err(CoreError::Codec(format!("cannot encode {other} as Era"))),
492        }
493    }
494
495    /// Coerce the seam's account-id inputs to raw key bytes: ss58 strings,
496    /// 0x-hex strings, or raw 32 bytes.
497    pub fn coerce_account_id(&self, value: &Value) -> Result<[u8; 32], CoreError> {
498        match value {
499            Value::Str(s) if s.starts_with("0x") => {
500                let raw = hex::decode(s.trim_start_matches("0x"))
501                    .map_err(|e| CoreError::Codec(format!("bad hex account id: {e}")))?;
502                raw.as_slice()
503                    .try_into()
504                    .map_err(|_| CoreError::Codec("account id must be 32 bytes".into()))
505            }
506            Value::Str(s) => public_key_from_ss58(s),
507            Value::Bytes(b) => b
508                .as_slice()
509                .try_into()
510                .map_err(|_| CoreError::Codec("account id must be 32 bytes".into())),
511            other => Err(CoreError::Codec(format!(
512                "cannot interpret {other} as an account id"
513            ))),
514        }
515    }
516
517    fn encode_call_input(&self, value: &Value, out: &mut Vec<u8>) -> Result<(), CoreError> {
518        match value {
519            Value::Bytes(raw) => {
520                out.extend_from_slice(raw);
521                Ok(())
522            }
523            Value::Dict(_) => {
524                let call_type = self
525                    .extrinsic
526                    .call_type
527                    .ok_or_else(|| CoreError::Codec("runtime has no call type".into()))?;
528                self.encode_id(call_type, value, out)
529            }
530            other => Err(CoreError::Codec(format!("cannot encode {other} as Call"))),
531        }
532    }
533
534    /// Compose a call: `(pallet, function, params)` -> raw SCALE call bytes.
535    ///
536    /// Param values may embed pre-composed calls as `Value::Bytes` (Sudo,
537    /// batches, proxies); they splice in verbatim.
538    pub fn compose_call(
539        &self,
540        pallet: &str,
541        function: &str,
542        params: &Value,
543    ) -> Result<Vec<u8>, CoreError> {
544        let pallet_info = self
545            .pallet(pallet)
546            .ok_or_else(|| CoreError::Codec(format!("pallet {pallet:?} not found")))?;
547        let calls_type = pallet_info
548            .calls_type
549            .ok_or_else(|| CoreError::Codec(format!("pallet {pallet:?} has no calls")))?;
550        let TypeDef::Variant(variant) = &self.resolve(calls_type)?.type_def else {
551            return Err(CoreError::Codec("call type is not an enum".into()));
552        };
553        let chosen = variant
554            .variants
555            .iter()
556            .find(|v| v.name == function)
557            .ok_or_else(|| CoreError::Codec(format!("call {pallet}.{function} not found")))?;
558        let mut out = vec![pallet_info.index, chosen.index];
559        match params {
560            Value::Dict(entries) => {
561                for field in &chosen.fields {
562                    let name = field.name.as_deref().unwrap_or_default();
563                    let item = entries
564                        .iter()
565                        .find(|(k, _)| matches!(k, Value::Str(s) if s == name))
566                        .map(|(_, v)| v)
567                        .ok_or_else(|| {
568                            CoreError::Codec(format!(
569                                "missing call param {name:?} for {pallet}.{function}"
570                            ))
571                        })?;
572                    self.encode_id(field.ty.id, item, &mut out)?;
573                }
574            }
575            Value::List(items) | Value::Tuple(items) => {
576                if items.len() != chosen.fields.len() {
577                    return Err(CoreError::Codec(format!(
578                        "call {pallet}.{function} expects {} positional params, got {}",
579                        chosen.fields.len(),
580                        items.len()
581                    )));
582                }
583                for (field, item) in chosen.fields.iter().zip(items.iter()) {
584                    self.encode_id(field.ty.id, item, &mut out)?;
585                }
586            }
587            other => {
588                return Err(CoreError::Codec(format!(
589                    "call params must be a dict or positional list, got {other}"
590                )));
591            }
592        }
593        Ok(out)
594    }
595}
596
597fn calculated_period(period: u64) -> u64 {
598    period.next_power_of_two().clamp(4, 1 << 16)
599}
600
601fn coerce_list(value: &Value) -> Result<&[Value], CoreError> {
602    match value {
603        Value::List(items) | Value::Tuple(items) => Ok(items),
604        other => Err(CoreError::Codec(format!("expected a list, got {other}"))),
605    }
606}
607
608fn coerce_uint(value: &Value) -> Result<u128, CoreError> {
609    match value {
610        Value::Int(i) => u128::try_from(*i)
611            .map_err(|_| CoreError::Codec("negative value for unsigned type".into())),
612        Value::Uint(u) => Ok(*u),
613        Value::Bool(b) => Ok(u128::from(*b)),
614        other => Err(CoreError::Codec(format!(
615            "expected an integer, got {other}"
616        ))),
617    }
618}
619
620fn coerce_int(value: &Value) -> Result<i128, CoreError> {
621    match value {
622        Value::Int(i) => Ok(*i),
623        Value::Uint(u) => i128::try_from(*u)
624            .map_err(|_| CoreError::Codec("value too large for signed type".into())),
625        other => Err(CoreError::Codec(format!(
626            "expected an integer, got {other}"
627        ))),
628    }
629}
630
631/// Byte-carrier inputs: raw bytes, "0x..." hex, or plain strings (utf8) —
632/// the inverse of the utf8-else-hex decode rendering. Lists of ints also
633/// accepted (cyscale did).
634fn coerce_bytes(value: &Value, expected_len: Option<usize>) -> Result<Vec<u8>, CoreError> {
635    let bytes = match value {
636        Value::Bytes(b) => b.clone(),
637        Value::Str(s) if s.starts_with("0x") => hex::decode(s.trim_start_matches("0x"))
638            .map_err(|e| CoreError::Codec(format!("bad hex: {e}")))?,
639        Value::Str(s) => s.as_bytes().to_vec(),
640        Value::List(items) | Value::Tuple(items) => {
641            let mut out = Vec::with_capacity(items.len());
642            for item in items {
643                let v = coerce_uint(item)?;
644                out.push(
645                    u8::try_from(v)
646                        .map_err(|_| CoreError::Codec("byte value out of range".into()))?,
647                );
648            }
649            out
650        }
651        other => return Err(CoreError::Codec(format!("expected bytes, got {other}"))),
652    };
653    if let Some(len) = expected_len {
654        if bytes.len() != len {
655            return Err(CoreError::Codec(format!(
656                "expected {len} bytes, got {}",
657                bytes.len()
658            )));
659        }
660    }
661    Ok(bytes)
662}
663
664/// Enum inputs: bare `"Variant"` strings or single-key `{"Variant": payload}`
665/// dicts. Returns the payload (unit payload as an empty tuple).
666fn coerce_variant(value: &Value) -> Result<(String, Value), CoreError> {
667    match value {
668        Value::Str(name) => Ok((name.clone(), Value::Tuple(Vec::new()))),
669        Value::Dict(entries) if entries.len() == 1 => match &entries[0] {
670            (Value::Str(name), payload) => Ok((name.clone(), payload.clone())),
671            _ => Err(CoreError::Codec("enum dict key must be a string".into())),
672        },
673        other => Err(CoreError::Codec(format!(
674            "expected an enum variant, got {other}"
675        ))),
676    }
677}
678
679fn encode_primitive_spec(p: Primitive, value: &Value, out: &mut Vec<u8>) -> Result<(), CoreError> {
680    use Primitive as P;
681    let def = match p {
682        P::Bool => TypeDefPrimitive::Bool,
683        P::Char => TypeDefPrimitive::Char,
684        P::Str => TypeDefPrimitive::Str,
685        P::U8 => TypeDefPrimitive::U8,
686        P::U16 => TypeDefPrimitive::U16,
687        P::U32 => TypeDefPrimitive::U32,
688        P::U64 => TypeDefPrimitive::U64,
689        P::U128 => TypeDefPrimitive::U128,
690        P::U256 => TypeDefPrimitive::U256,
691        P::I8 => TypeDefPrimitive::I8,
692        P::I16 => TypeDefPrimitive::I16,
693        P::I32 => TypeDefPrimitive::I32,
694        P::I64 => TypeDefPrimitive::I64,
695        P::I128 => TypeDefPrimitive::I128,
696        P::I256 => TypeDefPrimitive::I256,
697    };
698    encode_primitive(&def, value, out)
699}
700
701fn encode_primitive(
702    p: &TypeDefPrimitive,
703    value: &Value,
704    out: &mut Vec<u8>,
705) -> Result<(), CoreError> {
706    use TypeDefPrimitive as P;
707    match p {
708        P::Bool => {
709            let b = match value {
710                Value::Bool(b) => *b,
711                Value::Int(i) => *i != 0,
712                other => return Err(CoreError::Codec(format!("expected bool, got {other}"))),
713            };
714            out.push(u8::from(b));
715        }
716        P::Char => {
717            let Value::Str(s) = value else {
718                return Err(CoreError::Codec("expected a 1-char string".into()));
719            };
720            let c = s
721                .chars()
722                .next()
723                .ok_or_else(|| CoreError::Codec("expected a 1-char string".into()))?;
724            out.extend_from_slice(&(c as u32).to_le_bytes());
725        }
726        P::Str => {
727            let Value::Str(s) = value else {
728                return Err(CoreError::Codec(format!("expected a string, got {value}")));
729            };
730            compact(s.len() as u128, out)?;
731            out.extend_from_slice(s.as_bytes());
732        }
733        P::U8 => out.push(int_as::<u8>(value)?),
734        P::U16 => out.extend_from_slice(&int_as::<u16>(value)?.to_le_bytes()),
735        P::U32 => out.extend_from_slice(&int_as::<u32>(value)?.to_le_bytes()),
736        P::U64 => out.extend_from_slice(&int_as::<u64>(value)?.to_le_bytes()),
737        P::U128 => out.extend_from_slice(&coerce_uint(value)?.to_le_bytes()),
738        P::U256 => match value {
739            Value::U256(le) => out.extend_from_slice(le),
740            _ => {
741                let v = coerce_uint(value)?;
742                let mut buf = [0u8; 32];
743                buf[..16].copy_from_slice(&v.to_le_bytes());
744                out.extend_from_slice(&buf);
745            }
746        },
747        P::I8 => out.extend_from_slice(&sint_as::<i8>(value)?.to_le_bytes()),
748        P::I16 => out.extend_from_slice(&sint_as::<i16>(value)?.to_le_bytes()),
749        P::I32 => out.extend_from_slice(&sint_as::<i32>(value)?.to_le_bytes()),
750        P::I64 => out.extend_from_slice(&sint_as::<i64>(value)?.to_le_bytes()),
751        P::I128 => out.extend_from_slice(&coerce_int(value)?.to_le_bytes()),
752        P::I256 => match value {
753            Value::U256(le) => out.extend_from_slice(le),
754            _ => {
755                let v = coerce_int(value)?;
756                let mut buf = if v < 0 { [0xffu8; 32] } else { [0u8; 32] };
757                buf[..16].copy_from_slice(&v.to_le_bytes());
758                out.extend_from_slice(&buf);
759            }
760        },
761    }
762    Ok(())
763}
764
765fn int_as<T: TryFrom<u128>>(value: &Value) -> Result<T, CoreError> {
766    T::try_from(coerce_uint(value)?).map_err(|_| CoreError::Codec("integer out of range".into()))
767}
768
769fn sint_as<T: TryFrom<i128>>(value: &Value) -> Result<T, CoreError> {
770    T::try_from(coerce_int(value)?).map_err(|_| CoreError::Codec("integer out of range".into()))
771}