1#[cfg(test)]
4mod tests;
5
6use std::{
7 fmt::{self, Display, Formatter},
8 str::FromStr,
9};
10
11use serde::{Deserialize, Serialize};
12
13use crate::{Principal, PrincipalError, SchemaContractError, Subaccount};
14
15#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
22#[cfg_attr(
23 all(feature = "wire-schema", not(target_family = "wasm")),
24 derive(ic_wire::Wire)
25)]
26pub struct Account {
27 owner: Principal,
28 subaccount: Option<Subaccount>,
29}
30
31impl Account {
32 const PRINCIPAL_MAX_LEN: usize = Principal::MAX_LENGTH_IN_BYTES as usize;
33 const TAG_SUBACCOUNT: u8 = 0x80;
34
35 pub fn new<P: Into<Principal>, S: Into<Subaccount>>(owner: P, subaccount: Option<S>) -> Self {
37 Self::from_owner_and_subaccount(owner.into(), subaccount.map(Into::into))
38 }
39
40 #[must_use]
42 pub const fn from_owner_and_subaccount(
43 owner: Principal,
44 subaccount: Option<Subaccount>,
45 ) -> Self {
46 Self {
47 owner,
48 subaccount: Self::canonical_subaccount(subaccount),
49 }
50 }
51
52 const fn canonical_subaccount(subaccount: Option<Subaccount>) -> Option<Subaccount> {
55 let Some(value) = subaccount else {
56 return None;
57 };
58 let bytes = value.to_bytes();
59 let mut index = 0;
60 while index < bytes.len() {
61 if bytes[index] != 0 {
62 return Some(value);
63 }
64 index += 1;
65 }
66 None
67 }
68
69 #[must_use]
71 pub const fn owner(&self) -> Principal {
72 self.owner
73 }
74
75 #[must_use]
77 pub const fn subaccount(&self) -> Option<Subaccount> {
78 self.subaccount
79 }
80
81 #[expect(clippy::cast_possible_truncation)]
82 fn ordering_tag(&self) -> u8 {
83 let mut tag = self.owner.as_slice().len().min(u8::MAX as usize) as u8;
84 if self.subaccount.is_some() {
85 tag |= Self::TAG_SUBACCOUNT;
86 }
87 tag
88 }
89}
90
91impl<'de> Deserialize<'de> for Account {
94 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
95 where
96 D: serde::Deserializer<'de>,
97 {
98 #[derive(Deserialize)]
99 #[serde(rename = "Account")]
100 struct Fields {
101 owner: Principal,
102 subaccount: Option<Subaccount>,
103 }
104 let fields = Fields::deserialize(deserializer)?;
105 Ok(Self::from_owner_and_subaccount(
106 fields.owner,
107 fields.subaccount,
108 ))
109 }
110}
111
112impl Display for Account {
113 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
114 Display::fmt(&ic_platform_types::Account::from(*self), formatter)
115 }
116}
117
118impl From<ic_platform_types::Account> for Account {
120 fn from(value: ic_platform_types::Account) -> Self {
121 Self::from_owner_and_subaccount(
122 value.owner.into(),
123 value.subaccount.map(Subaccount::from_array),
124 )
125 }
126}
127
128impl From<Account> for ic_platform_types::Account {
129 fn from(value: Account) -> Self {
130 Self {
131 owner: value.owner.into(),
132 subaccount: value.subaccount.map(Subaccount::to_bytes),
133 }
134 }
135}
136
137impl From<Principal> for Account {
138 fn from(owner: Principal) -> Self {
139 Self {
140 owner,
141 subaccount: None,
142 }
143 }
144}
145
146impl FromStr for Account {
147 type Err = String;
148
149 fn from_str(input: &str) -> Result<Self, Self::Err> {
150 if input.len() > ic_platform_types::Account::MAX_LENGTH_IN_TEXT || !input.is_ascii() {
151 return Err("invalid account text width or alphabet".into());
152 }
153 if let Some((prefix, hex)) = input.split_once('.')
157 && prefix
158 .rsplit_once('-')
159 .is_some_and(|(_, checksum)| checksum.len() == 7)
160 && (hex.is_empty() || hex.len() > 64)
161 {
162 return Err("invalid account subaccount width or leading zero".into());
163 }
164 input
165 .parse::<ic_platform_types::Account>()
166 .map(Self::from)
167 .map_err(|error| match error {
168 ic_platform_types::AccountError::TextTooLong => {
169 "invalid account text width or alphabet".into()
170 }
171 ic_platform_types::AccountError::InvalidPrincipal(_) => {
172 PrincipalError::InvalidText.to_string()
173 }
174 ic_platform_types::AccountError::MissingChecksum => {
175 "missing account checksum".into()
176 }
177 ic_platform_types::AccountError::InvalidChecksum => {
178 "invalid account checksum".into()
179 }
180 ic_platform_types::AccountError::InvalidSubaccount => {
181 SchemaContractError::InvalidLiteral.to_string()
182 }
183 ic_platform_types::AccountError::LeadingZeroesInSubaccount
184 | ic_platform_types::AccountError::DefaultSubaccountShouldBeOmitted => {
185 "invalid account subaccount width or leading zero".into()
186 }
187 })
188 }
189}
190
191impl Ord for Account {
192 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
193 self.ordering_tag()
194 .cmp(&other.ordering_tag())
195 .then_with(|| {
196 let mut left = [0; Self::PRINCIPAL_MAX_LEN];
197 let left_bytes = self.owner.as_slice();
198 left[..left_bytes.len()].copy_from_slice(left_bytes);
199 let mut right = [0; Self::PRINCIPAL_MAX_LEN];
200 let right_bytes = other.owner.as_slice();
201 right[..right_bytes.len()].copy_from_slice(right_bytes);
202 left.cmp(&right)
203 })
204 .then_with(|| {
205 self.subaccount
206 .unwrap_or(Subaccount::MIN)
207 .cmp(&other.subaccount.unwrap_or(Subaccount::MIN))
208 })
209 }
210}
211
212impl PartialOrd for Account {
213 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
214 Some(self.cmp(other))
215 }
216}