ic_platform/account/
mod.rs1use crate::{Principal, PrincipalError};
15use serde::{Deserialize, Serialize};
16use std::{fmt, hash::Hash, str::FromStr};
17
18pub type Subaccount = [u8; 32];
20
21pub const DEFAULT_SUBACCOUNT: &Subaccount = &[0; 32];
23
24#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
26pub struct Account {
27 pub owner: Principal,
29 pub subaccount: Option<Subaccount>,
31}
32
33impl Account {
34 pub const MAX_LENGTH_IN_TEXT: usize = Principal::MAX_LENGTH_IN_TEXT + 1 + 7 + 1 + 64;
36
37 pub fn effective_subaccount(&self) -> &Subaccount {
39 self.subaccount.as_ref().unwrap_or(DEFAULT_SUBACCOUNT)
40 }
41}
42
43impl PartialEq for Account {
44 fn eq(&self, other: &Self) -> bool {
45 self.owner == other.owner && self.effective_subaccount() == other.effective_subaccount()
46 }
47}
48impl Eq for Account {}
49impl PartialOrd for Account {
50 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
51 Some(self.cmp(other))
52 }
53}
54impl Ord for Account {
55 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
56 self.owner.cmp(&other.owner).then_with(|| {
57 self.effective_subaccount()
58 .cmp(other.effective_subaccount())
59 })
60 }
61}
62impl Hash for Account {
63 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
64 self.owner.hash(state);
65 self.effective_subaccount().hash(state);
66 }
67}
68impl From<Principal> for Account {
69 fn from(owner: Principal) -> Self {
70 Self {
71 owner,
72 subaccount: None,
73 }
74 }
75}
76
77fn checksum(owner: &[u8], subaccount: &Subaccount) -> [u8; 7] {
79 let mut hasher = crc32fast::Hasher::new();
80 hasher.update(owner);
81 hasher.update(subaccount);
82 let mut encoded = [0; 7];
83 data_encoding::BASE32_NOPAD.encode_mut(&hasher.finalize().to_be_bytes(), &mut encoded);
84 encoded.make_ascii_lowercase();
85 encoded
86}
87
88impl fmt::Display for Account {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 write!(f, "{}", self.owner)?;
91 let subaccount = self.effective_subaccount();
92 if subaccount == DEFAULT_SUBACCOUNT {
93 return Ok(());
94 }
95 let checksum = checksum(self.owner.as_slice(), subaccount);
96 let checksum = std::str::from_utf8(&checksum).map_err(|_| fmt::Error)?;
97 write!(f, "-{checksum}.")?;
98 let first = subaccount
99 .iter()
100 .position(|byte| *byte != 0)
101 .ok_or(fmt::Error)?;
102 const HEX: &[u8; 16] = b"0123456789abcdef";
105 let mut hex = [0u8; 64];
106 let mut written = 0;
107 for (index, &byte) in subaccount[first..].iter().enumerate() {
108 if index != 0 || byte >= 16 {
109 hex[written] = HEX[(byte >> 4) as usize];
110 written += 1;
111 }
112 hex[written] = HEX[(byte & 15) as usize];
113 written += 1;
114 }
115 f.write_str(std::str::from_utf8(&hex[..written]).map_err(|_| fmt::Error)?)
116 }
117}
118
119#[derive(Clone, Debug, PartialEq, Eq)]
121pub enum AccountError {
122 TextTooLong,
124 InvalidPrincipal(PrincipalError),
126 MissingChecksum,
128 InvalidChecksum,
130 InvalidSubaccount,
132 LeadingZeroesInSubaccount,
134 DefaultSubaccountShouldBeOmitted,
136}
137impl fmt::Display for AccountError {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 match self {
140 Self::TextTooLong => f.write_str("account text is too long"),
141 Self::InvalidPrincipal(error) => write!(f, "invalid principal: {error}"),
142 Self::MissingChecksum => f.write_str("missing account checksum"),
143 Self::InvalidChecksum => f.write_str("invalid account checksum"),
144 Self::InvalidSubaccount => f.write_str("subaccount must contain at most 64 hex digits"),
145 Self::LeadingZeroesInSubaccount => {
146 f.write_str("subaccount should not have leading zeroes")
147 }
148 Self::DefaultSubaccountShouldBeOmitted => {
149 f.write_str("default subaccount should be omitted")
150 }
151 }
152 }
153}
154impl std::error::Error for AccountError {
155 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
156 match self {
157 Self::InvalidPrincipal(error) => Some(error),
158 _ => None,
159 }
160 }
161}
162impl FromStr for Account {
163 type Err = AccountError;
164
165 fn from_str(text: &str) -> Result<Self, Self::Err> {
166 if text.len() > Self::MAX_LENGTH_IN_TEXT {
167 return Err(AccountError::TextTooLong);
168 }
169 let Some((prefix, hex)) = text.split_once('.') else {
170 return text
171 .parse::<Principal>()
172 .map(Self::from)
173 .map_err(AccountError::InvalidPrincipal);
174 };
175 let (owner, supplied_checksum) = prefix
176 .rsplit_once('-')
177 .filter(|(_, sum)| sum.len() == 7)
178 .ok_or(AccountError::MissingChecksum)?;
179 if hex.starts_with('0') {
180 return Err(AccountError::LeadingZeroesInSubaccount);
181 }
182 let owner: Principal = owner
183 .parse::<Principal>()
184 .map_err(AccountError::InvalidPrincipal)?;
185 if hex.len() > 64 {
186 return Err(AccountError::InvalidSubaccount);
187 }
188 if hex.is_empty() {
189 return Err(AccountError::DefaultSubaccountShouldBeOmitted);
190 }
191 let mut subaccount = [0; 32];
192 for (index, byte) in hex.bytes().enumerate() {
193 let nibble = match byte {
194 b'0'..=b'9' => byte - b'0',
195 b'a'..=b'f' => byte - b'a' + 10,
196 b'A'..=b'F' => byte - b'A' + 10,
197 _ => return Err(AccountError::InvalidSubaccount),
198 };
199 let offset = 64 - hex.len() + index;
200 subaccount[offset / 2] |= nibble << (if offset % 2 == 0 { 4 } else { 0 });
201 }
202 if supplied_checksum.as_bytes() != checksum(owner.as_slice(), &subaccount) {
203 return Err(AccountError::InvalidChecksum);
204 }
205 Ok(Self {
206 owner,
207 subaccount: Some(subaccount),
208 })
209 }
210}
211
212#[cfg(feature = "icrc-ledger-types")]
214pub use icrc_ledger_types::icrc1::account::Account as ExternalAccount;
215
216#[cfg(feature = "icrc-ledger-types")]
217impl From<ExternalAccount> for Account {
218 fn from(value: ExternalAccount) -> Self {
219 Self {
220 owner: value.owner.into(),
221 subaccount: value.subaccount,
222 }
223 }
224}
225#[cfg(feature = "icrc-ledger-types")]
226impl From<Account> for ExternalAccount {
227 fn from(value: Account) -> Self {
228 Self {
229 owner: value.owner.into(),
230 subaccount: value.subaccount,
231 }
232 }
233}
234
235#[cfg(test)]
236mod tests;