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