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, Deserialize, Eq, Hash, PartialEq, Serialize)]
23pub struct Account {
24 owner: Principal,
25 subaccount: Option<Subaccount>,
26}
27
28impl Account {
29 const PRINCIPAL_MAX_LEN: usize = Principal::MAX_LENGTH_IN_BYTES as usize;
30 const TAG_SUBACCOUNT: u8 = 0x80;
31
32 pub fn new<P: Into<Principal>, S: Into<Subaccount>>(owner: P, subaccount: Option<S>) -> Self {
34 Self {
35 owner: owner.into(),
36 subaccount: subaccount.map(Into::into),
37 }
38 }
39
40 #[must_use]
42 pub const fn from_owner_and_subaccount(
43 owner: Principal,
44 subaccount: Option<Subaccount>,
45 ) -> Self {
46 Self { owner, subaccount }
47 }
48
49 #[must_use]
51 pub const fn owner(&self) -> Principal {
52 self.owner
53 }
54
55 #[must_use]
57 pub const fn subaccount(&self) -> Option<Subaccount> {
58 self.subaccount
59 }
60
61 #[expect(clippy::cast_possible_truncation)]
62 fn ordering_tag(&self) -> u8 {
63 let mut tag = self.owner.as_slice().len().min(u8::MAX as usize) as u8;
64 if self.subaccount.is_some() {
65 tag |= Self::TAG_SUBACCOUNT;
66 }
67 tag
68 }
69}
70
71impl Display for Account {
72 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
73 Display::fmt(&ic_platform::Account::from(*self), formatter)
74 }
75}
76
77impl From<ic_platform::Account> for Account {
79 fn from(value: ic_platform::Account) -> Self {
80 Self::from_owner_and_subaccount(
81 value.owner.into(),
82 value.subaccount.map(Subaccount::from_array),
83 )
84 }
85}
86
87impl From<Account> for ic_platform::Account {
88 fn from(value: Account) -> Self {
89 Self {
90 owner: value.owner.into(),
91 subaccount: value.subaccount.map(Subaccount::to_bytes),
92 }
93 }
94}
95
96impl From<Principal> for Account {
97 fn from(owner: Principal) -> Self {
98 Self {
99 owner,
100 subaccount: None,
101 }
102 }
103}
104
105impl FromStr for Account {
106 type Err = String;
107
108 fn from_str(input: &str) -> Result<Self, Self::Err> {
109 if input.len() > ic_platform::Account::MAX_LENGTH_IN_TEXT || !input.is_ascii() {
110 return Err("invalid account text width or alphabet".into());
111 }
112 if let Some((prefix, hex)) = input.split_once('.')
116 && prefix
117 .rsplit_once('-')
118 .is_some_and(|(_, checksum)| checksum.len() == 7)
119 && (hex.is_empty() || hex.len() > 64)
120 {
121 return Err("invalid account subaccount width or leading zero".into());
122 }
123 input
124 .parse::<ic_platform::Account>()
125 .map(Self::from)
126 .map_err(|error| match error {
127 ic_platform::AccountError::TextTooLong => {
128 "invalid account text width or alphabet".into()
129 }
130 ic_platform::AccountError::InvalidPrincipal(_) => {
131 PrincipalError::InvalidText.to_string()
132 }
133 ic_platform::AccountError::MissingChecksum => "missing account checksum".into(),
134 ic_platform::AccountError::InvalidChecksum => "invalid account checksum".into(),
135 ic_platform::AccountError::InvalidSubaccount => {
136 SchemaContractError::InvalidLiteral.to_string()
137 }
138 ic_platform::AccountError::LeadingZeroesInSubaccount
139 | ic_platform::AccountError::DefaultSubaccountShouldBeOmitted => {
140 "invalid account subaccount width or leading zero".into()
141 }
142 })
143 }
144}
145
146impl Ord for Account {
147 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
148 self.ordering_tag()
149 .cmp(&other.ordering_tag())
150 .then_with(|| {
151 let mut left = [0; Self::PRINCIPAL_MAX_LEN];
152 let left_bytes = self.owner.as_slice();
153 left[..left_bytes.len()].copy_from_slice(left_bytes);
154 let mut right = [0; Self::PRINCIPAL_MAX_LEN];
155 let right_bytes = other.owner.as_slice();
156 right[..right_bytes.len()].copy_from_slice(right_bytes);
157 left.cmp(&right)
158 })
159 .then_with(|| {
160 self.subaccount
161 .unwrap_or(Subaccount::MIN)
162 .cmp(&other.subaccount.unwrap_or(Subaccount::MIN))
163 })
164 }
165}
166
167impl PartialOrd for Account {
168 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
169 Some(self.cmp(other))
170 }
171}