canic_cdk/types/
account.rs

1use crate::icrc_ledger_types::icrc1::account::Account as IcrcAccount;
2use candid::{CandidType, Principal};
3use serde::{Deserialize, Serialize};
4use std::{
5    cmp::Ordering,
6    fmt::{self, Display},
7    hash::{Hash, Hasher},
8    str::FromStr,
9};
10
11///
12/// Subaccount
13///
14
15pub type Subaccount = [u8; 32];
16
17pub const DEFAULT_SUBACCOUNT: &Subaccount = &[0; 32];
18
19///
20/// Account
21///
22/// Code ported from icrc-ledger-types as we don't want to include that one, it's out of
23/// date and has a lot of extra dependencies
24///
25
26#[derive(CandidType, Clone, Copy, Debug, Deserialize, Serialize)]
27pub struct Account {
28    pub owner: Principal,
29    pub subaccount: Option<Subaccount>,
30}
31
32impl Account {
33    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    /// The effective subaccount of an account - the subaccount if it is set, otherwise the default
41    /// subaccount of all zeroes.
42    #[must_use]
43    pub fn effective_subaccount(&self) -> &Subaccount {
44        self.subaccount.as_ref().unwrap_or(DEFAULT_SUBACCOUNT)
45    }
46}
47
48impl Display for Account {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        // https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-1/TextualEncoding.md#textual-encoding-of-icrc-1-accounts
51        match &self.subaccount {
52            None => write!(f, "{}", self.owner),
53            Some(subaccount) if subaccount == &[0; 32] => write!(f, "{}", self.owner),
54            Some(subaccount) => {
55                let checksum = full_account_checksum(self.owner.as_slice(), subaccount.as_slice());
56                let hex_subaccount = hex::encode(subaccount.as_slice());
57                let hex_subaccount = hex_subaccount.trim_start_matches('0');
58                write!(f, "{}-{}.{}", self.owner, checksum, hex_subaccount)
59            }
60        }
61    }
62}
63
64impl Eq for Account {}
65
66impl FromStr for Account {
67    type Err = String;
68
69    fn from_str(s: &str) -> Result<Self, Self::Err> {
70        let acc = IcrcAccount::from_str(s).map_err(|e| e.to_string())?;
71
72        Ok(Self::new(acc.owner, acc.subaccount))
73    }
74}
75
76impl PartialEq for Account {
77    fn eq(&self, other: &Self) -> bool {
78        self.owner == other.owner && self.effective_subaccount() == other.effective_subaccount()
79    }
80}
81
82impl From<Principal> for Account {
83    fn from(owner: Principal) -> Self {
84        Self {
85            owner,
86            subaccount: None,
87        }
88    }
89}
90
91impl Hash for Account {
92    fn hash<H: Hasher>(&self, state: &mut H) {
93        self.owner.hash(state);
94        self.effective_subaccount().hash(state);
95    }
96}
97
98impl Ord for Account {
99    fn cmp(&self, other: &Self) -> Ordering {
100        self.owner.cmp(&other.owner).then_with(|| {
101            self.effective_subaccount()
102                .cmp(other.effective_subaccount())
103        })
104    }
105}
106
107impl PartialOrd for Account {
108    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
109        Some(self.cmp(other))
110    }
111}
112
113// make your internal code public, dfinity!
114fn full_account_checksum(owner: &[u8], subaccount: &[u8]) -> String {
115    let mut crc32hasher = crc32fast::Hasher::new();
116    crc32hasher.update(owner);
117    crc32hasher.update(subaccount);
118    let checksum = crc32hasher.finalize().to_be_bytes();
119
120    base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &checksum).to_lowercase()
121}
122
123///
124/// TESTS
125///
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn subaccount_checksum_matches_reference() {
133        // Values lifted from icrc-ledger-types, base32(crc32(owner + subaccount)) of
134        // 0x01 bytes (owner) plus 0x02 bytes (subaccount).
135        let owner = [0x01; 29];
136        let subaccount = [0x02; 32];
137
138        let checksum = full_account_checksum(owner.as_slice(), subaccount.as_slice());
139        assert_eq!(checksum, "izgikni");
140    }
141}