1use std::{
4 fmt::{self, Display, Formatter},
5 str::FromStr,
6};
7
8use candid::CandidType;
9use icrc_ledger_types::icrc1::account::Account as LedgerAccount;
10use serde::{Deserialize, Serialize};
11
12use crate::{Principal, Subaccount};
13
14#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
16pub struct Account {
17 owner: Principal,
18 subaccount: Option<Subaccount>,
19}
20
21impl Account {
22 const PRINCIPAL_MAX_LEN: usize = Principal::MAX_LENGTH_IN_BYTES as usize;
23 const TAG_SUBACCOUNT: u8 = 0x80;
24
25 pub fn new<P: Into<Principal>, S: Into<Subaccount>>(owner: P, subaccount: Option<S>) -> Self {
27 Self {
28 owner: owner.into(),
29 subaccount: subaccount.map(Into::into),
30 }
31 }
32
33 #[must_use]
35 pub const fn from_owner_and_subaccount(
36 owner: Principal,
37 subaccount: Option<Subaccount>,
38 ) -> Self {
39 Self { owner, subaccount }
40 }
41
42 #[must_use]
44 pub const fn owner(&self) -> Principal {
45 self.owner
46 }
47
48 #[must_use]
50 pub const fn subaccount(&self) -> Option<Subaccount> {
51 self.subaccount
52 }
53
54 #[must_use]
56 pub fn to_icrc_type(self) -> LedgerAccount {
57 LedgerAccount {
58 owner: self.owner.into(),
59 subaccount: self.subaccount.map(|value| value.to_array()),
60 }
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(&self.to_icrc_type(), formatter)
76 }
77}
78
79impl From<Account> for LedgerAccount {
80 fn from(value: Account) -> Self {
81 value.to_icrc_type()
82 }
83}
84
85impl From<LedgerAccount> for Account {
86 fn from(value: LedgerAccount) -> Self {
87 Self {
88 owner: value.owner.into(),
89 subaccount: value.subaccount.map(Subaccount::from_array),
90 }
91 }
92}
93
94impl From<Principal> for Account {
95 fn from(owner: Principal) -> Self {
96 Self {
97 owner,
98 subaccount: None,
99 }
100 }
101}
102
103impl FromStr for Account {
104 type Err = String;
105
106 fn from_str(input: &str) -> Result<Self, Self::Err> {
107 LedgerAccount::from_str(input)
108 .map(Self::from)
109 .map_err(|error| error.to_string())
110 }
111}
112
113impl Ord for Account {
114 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
115 self.ordering_tag()
116 .cmp(&other.ordering_tag())
117 .then_with(|| {
118 let mut left = [0; Self::PRINCIPAL_MAX_LEN];
119 let left_bytes = self.owner.as_slice();
120 left[..left_bytes.len()].copy_from_slice(left_bytes);
121 let mut right = [0; Self::PRINCIPAL_MAX_LEN];
122 let right_bytes = other.owner.as_slice();
123 right[..right_bytes.len()].copy_from_slice(right_bytes);
124 left.cmp(&right)
125 })
126 .then_with(|| {
127 self.subaccount
128 .unwrap_or(Subaccount::MIN)
129 .cmp(&other.subaccount.unwrap_or(Subaccount::MIN))
130 })
131 }
132}
133
134impl PartialOrd for Account {
135 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
136 Some(self.cmp(other))
137 }
138}