Skip to main content

icydb_schema/
subaccount.rs

1//! Canonical fixed-width subaccount atom.
2
3use crate::Principal;
4use candid::CandidType;
5use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError};
6use std::fmt::{self, Display};
7
8//
9// Subaccount
10//
11
12type SubaccountBytes = [u8; 32];
13
14#[derive(CandidType, 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    /// Borrow the fixed-width bytes.
37    #[must_use]
38    pub const fn as_slice(&self) -> &[u8] {
39        &self.0
40    }
41
42    /// Consume the value and return its fixed-width bytes.
43    #[must_use]
44    pub const fn to_bytes(self) -> [u8; 32] {
45        self.0
46    }
47}
48
49impl<'de> Deserialize<'de> for Subaccount {
50    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
51    where
52        D: Deserializer<'de>,
53    {
54        // Candid validates nat8 elements through sequence ingress. Read into a
55        // fixed array and inspect at most one excess byte before rejecting.
56        struct SubaccountBytesVisitor;
57
58        impl<'de> serde::de::Visitor<'de> for SubaccountBytesVisitor {
59            type Value = Subaccount;
60
61            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62                formatter.write_str("exactly 32 subaccount bytes")
63            }
64
65            fn visit_seq<A: serde::de::SeqAccess<'de>>(
66                self,
67                mut sequence: A,
68            ) -> Result<Subaccount, A::Error> {
69                let mut bytes = [0; 32];
70                for (index, byte) in bytes.iter_mut().enumerate() {
71                    *byte = sequence
72                        .next_element()?
73                        .ok_or_else(|| A::Error::invalid_length(index, &self))?;
74                }
75                if sequence.next_element::<u8>()?.is_some() {
76                    return Err(A::Error::invalid_length(33, &self));
77                }
78                Ok(Subaccount::from_array(bytes))
79            }
80        }
81
82        if deserializer.is_human_readable() {
83            return SubaccountBytes::deserialize(deserializer).map(Self);
84        }
85
86        deserializer.deserialize_seq(SubaccountBytesVisitor)
87    }
88}
89
90impl Display for Subaccount {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        for byte in &self.0 {
93            write!(f, "{byte:02x}")?;
94        }
95
96        Ok(())
97    }
98}
99
100// code taken from
101// <https://docs.rs/ic-ledger-types/latest/src/ic_ledger_types/lib.rs.html#140-148>
102#[expect(clippy::cast_possible_truncation)]
103impl From<Principal> for Subaccount {
104    fn from(principal: Principal) -> Self {
105        let mut bytes = [0u8; 32];
106        let p = principal.as_slice();
107
108        // Defensive check: Principals are currently <= 29 bytes
109        let len = p.len().min(31); // reserve 1 byte for the length prefix
110        bytes[0] = len as u8;
111
112        // Copy safely without panic risk
113        bytes[1..=len].copy_from_slice(&p[..len]);
114
115        Self(bytes)
116    }
117}
118
119impl Serialize for Subaccount {
120    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
121    where
122        S: serde::Serializer,
123    {
124        if serializer.is_human_readable() {
125            self.0.serialize(serializer)
126        } else {
127            serializer.serialize_bytes(&self.0)
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::Account;
136
137    #[test]
138    fn binary_serde_and_candid_preserve_exact_subaccount_bytes() {
139        for byte in [0, 23, 24, 128, 255] {
140            let value = Subaccount::from_array([byte; 32]);
141            let mut encoded = Vec::new();
142            ciborium::into_writer(&value, &mut encoded).unwrap();
143            assert_eq!(encoded.len(), 34);
144            let wire: ciborium::Value = ciborium::from_reader(encoded.as_slice()).unwrap();
145            assert_eq!(wire, ciborium::Value::Bytes(value.to_bytes().to_vec()));
146            assert_eq!(
147                ciborium::from_reader::<Subaccount, _>(encoded.as_slice()).unwrap(),
148                value
149            );
150
151            let json = serde_json::to_string(&value).unwrap();
152            assert_eq!(json, serde_json::to_string(&value.to_array()).unwrap());
153            assert_eq!(serde_json::from_str::<Subaccount>(&json).unwrap(), value);
154
155            let candid = candid::encode_one(value).unwrap();
156            assert_eq!(candid, candid::encode_one(value.to_array()).unwrap());
157            assert_eq!(candid.len(), 42);
158            assert_eq!(candid::decode_one::<Subaccount>(&candid).unwrap(), value);
159
160            let batch = vec![value; 1_000];
161            encoded.clear();
162            ciborium::into_writer(&batch, &mut encoded).unwrap();
163            assert_eq!(encoded.len(), 34_003);
164            assert_eq!(
165                ciborium::from_reader::<Vec<Subaccount>, _>(encoded.as_slice()).unwrap(),
166                batch
167            );
168        }
169    }
170
171    #[test]
172    fn binary_subaccount_ingress_enforces_width_and_byte_elements() {
173        for len in [0, 31, 33, 1_024] {
174            let wire = candid::encode_one(vec![0_u8; len]).unwrap();
175            assert!(candid::decode_one::<Subaccount>(&wire).is_err());
176            let mut encoded = Vec::new();
177            ciborium::into_writer(&serde_bytes::Bytes::new(&vec![0; len]), &mut encoded).unwrap();
178            assert!(ciborium::from_reader::<Subaccount, _>(encoded.as_slice()).is_err());
179        }
180        let wire = candid::encode_one(vec![0_u16; 32]).unwrap();
181        assert!(candid::decode_one::<Subaccount>(&wire).is_err());
182
183        let mut encoded = Vec::new();
184        ciborium::into_writer(&Subaccount::MAX, &mut encoded).unwrap();
185        for end in 0..encoded.len() {
186            assert!(ciborium::from_reader::<Subaccount, _>(&encoded[..end]).is_err());
187        }
188    }
189
190    #[test]
191    fn accounts_roundtrip_optional_subaccounts_in_all_transports() {
192        for subaccount in [None, Some(Subaccount::MIN), Some(Subaccount::MAX)] {
193            let account = Account::from_owner_and_subaccount(Principal::anonymous(), subaccount);
194            let mut encoded = Vec::new();
195            ciborium::into_writer(&account, &mut encoded).unwrap();
196            assert_eq!(encoded.len(), if subaccount.is_some() { 54 } else { 21 });
197            assert_eq!(
198                ciborium::from_reader::<Account, _>(encoded.as_slice()).unwrap(),
199                account
200            );
201            let json = serde_json::to_string(&account).unwrap();
202            assert_eq!(serde_json::from_str::<Account>(&json).unwrap(), account);
203            let candid = candid::encode_one(account).unwrap();
204            assert_eq!(candid, candid::encode_one(account.to_icrc_type()).unwrap());
205            assert_eq!(candid::decode_one::<Account>(&candid).unwrap(), account);
206        }
207    }
208}