Skip to main content

icydb_schema/
subaccount.rs

1//! Canonical fixed-width subaccount atom.
2
3use crate::{Principal, SchemaContractError, atom::deserialize_fixed_bytes};
4
5use serde::{Deserialize, Deserializer, Serialize};
6use std::fmt::{self, Display};
7
8//
9// Subaccount
10//
11
12type SubaccountBytes = [u8; 32];
13
14#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
15/// A canonical 32-byte ICRC account subaccount.
16pub struct Subaccount(SubaccountBytes);
17
18impl Subaccount {
19    /// The lexicographically smallest subaccount.
20    pub const MIN: Self = Self::from_array([0x00; 32]);
21    /// The lexicographically largest subaccount.
22    pub const MAX: Self = Self::from_array([0xFF; 32]);
23
24    /// Return the fixed-width byte array.
25    #[must_use]
26    pub const fn to_array(&self) -> [u8; 32] {
27        self.0
28    }
29
30    /// Construct from the exact fixed-width byte array.
31    #[must_use]
32    pub const fn from_array(array: [u8; 32]) -> Self {
33        Self(array)
34    }
35
36    /// Parse 64 ASCII hex digits, optionally prefixed by lowercase `0x`.
37    ///
38    /// # Errors
39    /// Returns [`SchemaContractError::InvalidLiteral`] for invalid width,
40    /// non-ASCII input or non-hex characters. No trimming is performed.
41    pub fn from_hex(text: &str) -> Result<Self, SchemaContractError> {
42        let text = text.strip_prefix("0x").unwrap_or(text);
43        if text.len() != 64 || !text.bytes().all(|byte| byte.is_ascii_hexdigit()) {
44            return Err(SchemaContractError::InvalidLiteral);
45        }
46        let mut bytes = [0; 32];
47        for (index, pair) in text.as_bytes().as_chunks::<2>().0.iter().enumerate() {
48            let pair =
49                std::str::from_utf8(pair).map_err(|_| SchemaContractError::InvalidLiteral)?;
50            bytes[index] =
51                u8::from_str_radix(pair, 16).map_err(|_| SchemaContractError::InvalidLiteral)?;
52        }
53        Ok(Self::from_array(bytes))
54    }
55
56    /// Borrow the fixed-width bytes.
57    #[must_use]
58    pub const fn as_slice(&self) -> &[u8] {
59        &self.0
60    }
61
62    /// Consume the value and return its fixed-width bytes.
63    #[must_use]
64    pub const fn to_bytes(self) -> [u8; 32] {
65        self.0
66    }
67}
68
69impl<'de> Deserialize<'de> for Subaccount {
70    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
71    where
72        D: Deserializer<'de>,
73    {
74        if deserializer.is_human_readable() {
75            return SubaccountBytes::deserialize(deserializer).map(Self);
76        }
77
78        deserialize_fixed_bytes(deserializer).map(Self::from_array)
79    }
80}
81
82impl Display for Subaccount {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        for byte in &self.0 {
85            write!(f, "{byte:02x}")?;
86        }
87
88        Ok(())
89    }
90}
91
92// code taken from
93// <https://docs.rs/ic-ledger-types/latest/src/ic_ledger_types/lib.rs.html#140-148>
94#[expect(clippy::cast_possible_truncation)]
95impl From<Principal> for Subaccount {
96    fn from(principal: Principal) -> Self {
97        let mut bytes = [0u8; 32];
98        let p = principal.as_slice();
99
100        // Defensive check: Principals are currently <= 29 bytes
101        let len = p.len().min(31); // reserve 1 byte for the length prefix
102        bytes[0] = len as u8;
103
104        // Copy safely without panic risk
105        bytes[1..=len].copy_from_slice(&p[..len]);
106
107        Self(bytes)
108    }
109}
110
111impl Serialize for Subaccount {
112    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
113    where
114        S: serde::Serializer,
115    {
116        if serializer.is_human_readable() {
117            self.0.serialize(serializer)
118        } else {
119            serializer.serialize_bytes(&self.0)
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests;