use crate::{Principal, PrincipalError};
use serde::{Deserialize, Serialize};
use std::{fmt, hash::Hash, str::FromStr};
pub type Subaccount = [u8; 32];
pub const DEFAULT_SUBACCOUNT: &Subaccount = &[0; 32];
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Account {
pub owner: Principal,
pub subaccount: Option<Subaccount>,
}
impl Account {
pub const MAX_LENGTH_IN_TEXT: usize = Principal::MAX_LENGTH_IN_TEXT + 1 + 7 + 1 + 64;
pub fn effective_subaccount(&self) -> &Subaccount {
self.subaccount.as_ref().unwrap_or(DEFAULT_SUBACCOUNT)
}
}
impl PartialEq for Account {
fn eq(&self, other: &Self) -> bool {
self.owner == other.owner && self.effective_subaccount() == other.effective_subaccount()
}
}
impl Eq for Account {}
impl PartialOrd for Account {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Account {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.owner.cmp(&other.owner).then_with(|| {
self.effective_subaccount()
.cmp(other.effective_subaccount())
})
}
}
impl Hash for Account {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.owner.hash(state);
self.effective_subaccount().hash(state);
}
}
impl From<Principal> for Account {
fn from(owner: Principal) -> Self {
Self {
owner,
subaccount: None,
}
}
}
fn checksum(owner: &[u8], subaccount: &Subaccount) -> [u8; 7] {
let mut hasher = crc32fast::Hasher::new();
hasher.update(owner);
hasher.update(subaccount);
let mut encoded = [0; 7];
data_encoding::BASE32_NOPAD.encode_mut(&hasher.finalize().to_be_bytes(), &mut encoded);
encoded.make_ascii_lowercase();
encoded
}
impl fmt::Display for Account {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.owner)?;
let subaccount = self.effective_subaccount();
if subaccount == DEFAULT_SUBACCOUNT {
return Ok(());
}
let checksum = checksum(self.owner.as_slice(), subaccount);
let checksum = std::str::from_utf8(&checksum).map_err(|_| fmt::Error)?;
write!(f, "-{checksum}.")?;
let first = subaccount
.iter()
.position(|byte| *byte != 0)
.ok_or(fmt::Error)?;
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut hex = [0u8; 64];
let mut written = 0;
for (index, &byte) in subaccount[first..].iter().enumerate() {
if index != 0 || byte >= 16 {
hex[written] = HEX[(byte >> 4) as usize];
written += 1;
}
hex[written] = HEX[(byte & 15) as usize];
written += 1;
}
f.write_str(std::str::from_utf8(&hex[..written]).map_err(|_| fmt::Error)?)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AccountError {
TextTooLong,
InvalidPrincipal(PrincipalError),
MissingChecksum,
InvalidChecksum,
InvalidSubaccount,
LeadingZeroesInSubaccount,
DefaultSubaccountShouldBeOmitted,
}
impl fmt::Display for AccountError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TextTooLong => f.write_str("account text is too long"),
Self::InvalidPrincipal(error) => write!(f, "invalid principal: {error}"),
Self::MissingChecksum => f.write_str("missing account checksum"),
Self::InvalidChecksum => f.write_str("invalid account checksum"),
Self::InvalidSubaccount => f.write_str("subaccount must contain at most 64 hex digits"),
Self::LeadingZeroesInSubaccount => {
f.write_str("subaccount should not have leading zeroes")
}
Self::DefaultSubaccountShouldBeOmitted => {
f.write_str("default subaccount should be omitted")
}
}
}
}
impl std::error::Error for AccountError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidPrincipal(error) => Some(error),
_ => None,
}
}
}
impl FromStr for Account {
type Err = AccountError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
if text.len() > Self::MAX_LENGTH_IN_TEXT {
return Err(AccountError::TextTooLong);
}
let Some((prefix, hex)) = text.split_once('.') else {
return text
.parse::<Principal>()
.map(Self::from)
.map_err(AccountError::InvalidPrincipal);
};
let (owner, supplied_checksum) = prefix
.rsplit_once('-')
.filter(|(_, sum)| sum.len() == 7)
.ok_or(AccountError::MissingChecksum)?;
if hex.starts_with('0') {
return Err(AccountError::LeadingZeroesInSubaccount);
}
let owner: Principal = owner
.parse::<Principal>()
.map_err(AccountError::InvalidPrincipal)?;
if hex.len() > 64 {
return Err(AccountError::InvalidSubaccount);
}
if hex.is_empty() {
return Err(AccountError::DefaultSubaccountShouldBeOmitted);
}
let mut subaccount = [0; 32];
for (index, byte) in hex.bytes().enumerate() {
let nibble = match byte {
b'0'..=b'9' => byte - b'0',
b'a'..=b'f' => byte - b'a' + 10,
b'A'..=b'F' => byte - b'A' + 10,
_ => return Err(AccountError::InvalidSubaccount),
};
let offset = 64 - hex.len() + index;
subaccount[offset / 2] |= nibble << (if offset % 2 == 0 { 4 } else { 0 });
}
if supplied_checksum.as_bytes() != checksum(owner.as_slice(), &subaccount) {
return Err(AccountError::InvalidChecksum);
}
Ok(Self {
owner,
subaccount: Some(subaccount),
})
}
}
#[cfg(feature = "icrc-ledger-types")]
pub use icrc_ledger_types::icrc1::account::Account as ExternalAccount;
#[cfg(feature = "icrc-ledger-types")]
impl From<ExternalAccount> for Account {
fn from(value: ExternalAccount) -> Self {
Self {
owner: value.owner.into(),
subaccount: value.subaccount,
}
}
}
#[cfg(feature = "icrc-ledger-types")]
impl From<Account> for ExternalAccount {
fn from(value: Account) -> Self {
Self {
owner: value.owner.into(),
subaccount: value.subaccount,
}
}
}
#[cfg(test)]
mod tests;