use crate::{fmt, str, str::FromStr, AsBytes, Box, String, ToString, Vec};
use borsh::{io, BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
pub const MIN_ACCOUNT_ID_LEN: usize = 2;
pub const MAX_ACCOUNT_ID_LEN: usize = 64;
#[derive(
Default, BorshSerialize, Serialize, Eq, Ord, Hash, Clone, Debug, PartialEq, PartialOrd,
)]
pub struct AccountId(Box<str>);
impl AccountId {
pub fn new(account_id: &str) -> Result<Self, ParseAccountError> {
Self::validate(account_id)?;
Ok(Self(account_id.into()))
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
self.as_ref().as_bytes()
}
pub fn validate(account_id: &str) -> Result<(), ParseAccountError> {
if account_id.len() < MIN_ACCOUNT_ID_LEN {
Err(ParseAccountError::TooShort)
} else if account_id.len() > MAX_ACCOUNT_ID_LEN {
Err(ParseAccountError::TooLong)
} else {
let mut last_char_is_separator = true;
for c in account_id.bytes() {
let current_char_is_separator = match c {
b'a'..=b'z' | b'0'..=b'9' => false,
b'-' | b'_' | b'.' => true,
_ => {
return Err(ParseAccountError::Invalid);
}
};
if current_char_is_separator && last_char_is_separator {
return Err(ParseAccountError::Invalid);
}
last_char_is_separator = current_char_is_separator;
}
(!last_char_is_separator)
.then_some(())
.ok_or(ParseAccountError::Invalid)
}
}
#[must_use]
pub fn is_top_level_account_id(&self) -> bool {
self.0.len() >= MIN_ACCOUNT_ID_LEN
&& self.0.len() <= MAX_ACCOUNT_ID_LEN
&& self.as_ref() != "system"
&& !self.as_ref().contains('.')
}
#[must_use]
pub fn is_sub_account_of(&self, parent_account_id: &Self) -> bool {
if parent_account_id.0.len() >= self.0.len() {
return false;
}
let (prefix, suffix) = self.0.split_at(self.0.len() - parent_account_id.0.len());
prefix.find('.') == Some(prefix.len() - 1) && suffix == parent_account_id.as_ref()
}
}
impl BorshDeserialize for AccountId {
fn deserialize_reader<R: io::Read>(reader: &mut R) -> io::Result<Self> {
let account: String = BorshDeserialize::deserialize_reader(reader)?;
if account.is_empty() {
return Ok(Self::default());
}
Self::try_from(account)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
}
}
impl<'de> Deserialize<'de> for AccountId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
D::Error: serde::de::Error,
{
let account = <String as Deserialize>::deserialize(deserializer)?;
Self::try_from(account).map_err(serde::de::Error::custom)
}
}
impl TryFrom<String> for AccountId {
type Error = ParseAccountError;
fn try_from(account_id: String) -> Result<Self, Self::Error> {
let account_id = account_id.into_boxed_str();
Self::validate(&account_id)?;
Ok(Self(account_id))
}
}
impl TryFrom<&[u8]> for AccountId {
type Error = ParseAccountError;
fn try_from(account_id: &[u8]) -> Result<Self, Self::Error> {
let account_id = str::from_utf8(account_id).map_err(|_| ParseAccountError::Invalid)?;
Self::new(account_id)
}
}
impl TryFrom<Vec<u8>> for AccountId {
type Error = ParseAccountError;
fn try_from(account_id: Vec<u8>) -> Result<Self, Self::Error> {
String::from_utf8(account_id)
.map_err(|_| ParseAccountError::Invalid)
.and_then(Self::try_from)
}
}
impl FromStr for AccountId {
type Err = ParseAccountError;
fn from_str(account_id: &str) -> Result<Self, Self::Err> {
Self::new(account_id)
}
}
impl From<AccountId> for String {
fn from(account_id: AccountId) -> Self {
account_id.0.into_string()
}
}
impl fmt::Display for AccountId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<AccountId> for Box<str> {
fn from(account_id: AccountId) -> Self {
account_id.0
}
}
impl From<AccountId> for Vec<u8> {
fn from(account_id: AccountId) -> Self {
account_id.0.into_boxed_bytes().into_vec()
}
}
impl AsBytes for AccountId {
fn as_bytes(&self) -> &[u8] {
self.as_bytes()
}
}
impl<T: ?Sized> AsRef<T> for AccountId
where
Box<str>: AsRef<T>,
{
fn as_ref(&self) -> &T {
self.0.as_ref()
}
}
#[derive(Eq, Hash, Clone, Debug, PartialEq)]
pub enum ParseAccountError {
TooLong,
TooShort,
Invalid,
}
impl AsRef<[u8]> for ParseAccountError {
fn as_ref(&self) -> &[u8] {
match self {
Self::TooLong => b"ERR_ACCOUNT_ID_TO_LONG",
Self::TooShort => b"ERR_ACCOUNT_ID_TO_SHORT",
Self::Invalid => b"ERR_ACCOUNT_ID_TO_INVALID",
}
}
}
impl fmt::Display for ParseAccountError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let msg = str::from_utf8(self.as_ref()).map_err(|_| fmt::Error)?;
write!(f, "{msg}")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn is_implicit(account_id: &str) -> bool {
account_id.len() == 64
&& account_id
.as_bytes()
.iter()
.all(|b| matches!(b, b'a'..=b'f' | b'0'..=b'9'))
}
pub const OK_ACCOUNT_IDS: [&str; 24] = [
"aa",
"a-a",
"a-aa",
"100",
"0o",
"com",
"near",
"bowen",
"b-o_w_e-n",
"b.owen",
"bro.wen",
"a.ha",
"a.b-a.ra",
"system",
"over.9000",
"google.com",
"illia.cheapaccounts.near",
"0o0ooo00oo00o",
"alex-skidanov",
"10-4.8-2",
"b-o_w_e-n",
"no_lols",
"0123456789012345678901234567890123456789012345678901234567890123",
"near.a",
];
pub const BAD_ACCOUNT_IDS: [&str; 24] = [
"a",
"A",
"Abc",
"-near",
"near-",
"-near-",
"near.",
".near",
"near@",
"@near",
"неар",
"@@@@@",
"0__0",
"0_-_0",
"0_-_0",
"..",
"a..near",
"nEar",
"_bowen",
"hello world",
"abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz",
"01234567890123456789012345678901234567890123456789012345678901234",
"some-complex-address@gmail.com",
"sub.buy_d1gitz@atata@b0-rg.c_0_m",
];
#[test]
fn test_is_valid_account_id() {
for account_id in OK_ACCOUNT_IDS {
if let Err(err) = AccountId::validate(account_id) {
panic!("Valid account id {account_id:?} marked invalid: {err}");
}
}
for account_id in BAD_ACCOUNT_IDS {
assert!(
AccountId::validate(account_id).is_err(),
"Valid account id {account_id:?} marked valid"
);
}
}
#[test]
fn test_is_valid_top_level_account_id() {
let ok_top_level_account_ids = &[
"aa",
"a-a",
"a-aa",
"100",
"0o",
"com",
"near",
"bowen",
"b-o_w_e-n",
"0o0ooo00oo00o",
"alex-skidanov",
"b-o_w_e-n",
"no_lols",
"0123456789012345678901234567890123456789012345678901234567890123",
];
for account_id in ok_top_level_account_ids {
assert!(
account_id
.parse::<AccountId>()
.is_ok_and(|account_id| account_id.is_top_level_account_id()),
"Valid top level account id {account_id:?} marked invalid",
);
}
let bad_top_level_account_ids = &[
"near.a",
"b.owen",
"bro.wen",
"a.ha",
"a.b-a.ra",
"some-complex-address@gmail.com",
"sub.buy_d1gitz@atata@b0-rg.c_0_m",
"over.9000",
"google.com",
"illia.cheapaccounts.near",
"10-4.8-2",
"a",
"A",
"Abc",
"-near",
"near-",
"-near-",
"near.",
".near",
"near@",
"@near",
"неар",
"@@@@@",
"0__0",
"0_-_0",
"0_-_0",
"..",
"a..near",
"nEar",
"_bowen",
"hello world",
"abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz",
"01234567890123456789012345678901234567890123456789012345678901234",
"system",
];
for account_id in bad_top_level_account_ids {
assert!(
!account_id
.parse::<AccountId>()
.is_ok_and(|account_id| account_id.is_top_level_account_id()),
"Invalid top level account id {account_id:?} marked valid",
);
}
}
#[test]
fn test_is_valid_sub_account_id() {
let ok_pairs = &[
("test", "a.test"),
("test-me", "abc.test-me"),
("gmail.com", "abc.gmail.com"),
("gmail.com", "abc-lol.gmail.com"),
("gmail.com", "abc_lol.gmail.com"),
("gmail.com", "bro-abc_lol.gmail.com"),
("g0", "0g.g0"),
("1g", "1g.1g"),
("5-3", "4_2.5-3"),
];
for (signer_id, sub_account_id) in ok_pairs {
assert!(
matches!(
(signer_id.parse::<AccountId>(), sub_account_id.parse::<AccountId>()),
(Ok(signer_id), Ok(sub_account_id)) if sub_account_id.is_sub_account_of(&signer_id)
),
"Failed to create sub-account {sub_account_id:?} by account {signer_id:?}",
);
}
let bad_pairs = &[
("test", ".test"),
("test", "test"),
("test", "a1.a.test"),
("test", "est"),
("test", ""),
("test", "st"),
("test5", "ббб"),
("test", "a-test"),
("test", "etest"),
("test", "a.etest"),
("test", "retest"),
("test-me", "abc-.test-me"),
("test-me", "Abc.test-me"),
("test-me", "-abc.test-me"),
("test-me", "a--c.test-me"),
("test-me", "a_-c.test-me"),
("test-me", "a-_c.test-me"),
("test-me", "_abc.test-me"),
("test-me", "abc_.test-me"),
("test-me", "..test-me"),
("test-me", "a..test-me"),
("gmail.com", "a.abc@gmail.com"),
("gmail.com", ".abc@gmail.com"),
("gmail.com", ".abc@gmail@com"),
("gmail.com", "abc@gmail@com"),
("test", "a@test"),
("test_me", "abc@test_me"),
("gmail.com", "abc@gmail.com"),
("gmail@com", "abc.gmail@com"),
("gmail.com", "abc-lol@gmail.com"),
("gmail@com", "abc_lol.gmail@com"),
("gmail@com", "bro-abc_lol.gmail@com"),
(
"gmail.com",
"123456789012345678901234567890123456789012345678901234567890@gmail.com",
),
(
"123456789012345678901234567890123456789012345678901234567890",
"1234567890.123456789012345678901234567890123456789012345678901234567890",
),
("aa", "ъ@aa"),
("aa", "ъ.aa"),
];
for (signer_id, sub_account_id) in bad_pairs {
assert!(
!matches!(
(signer_id.parse::<AccountId>(), sub_account_id.parse::<AccountId>()),
(Ok(signer_id), Ok(sub_account_id)) if sub_account_id.is_sub_account_of(&signer_id)
),
"Invalid sub-account {sub_account_id:?} created by account {signer_id:?}",
);
}
}
#[test]
fn test_is_account_id_64_len_hex() {
let valid_64_len_hex_account_ids = &[
"0000000000000000000000000000000000000000000000000000000000000000",
"6174617461746174617461746174617461746174617461746174617461746174",
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"20782e20662e64666420482123494b6b6c677573646b6c66676a646b6c736667",
];
for valid_account_id in valid_64_len_hex_account_ids {
assert!(
matches!(
valid_account_id.parse::<AccountId>(),
Ok(account_id) if is_implicit(account_id.as_ref())
),
"Account ID {valid_account_id} should be valid 64-len hex",
);
assert!(
is_implicit(valid_account_id),
"Account ID {valid_account_id} should be valid 64-len hex",
);
}
let invalid_64_len_hex_account_ids = &[
"000000000000000000000000000000000000000000000000000000000000000",
"6.74617461746174617461746174617461746174617461746174617461746174",
"012-456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"fffff_ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo",
"00000000000000000000000000000000000000000000000000000000000000",
];
for invalid_account_id in invalid_64_len_hex_account_ids {
assert!(
!matches!(
invalid_account_id.parse::<AccountId>(),
Ok(account_id) if is_implicit(account_id.as_ref())
),
"Account ID {invalid_account_id} should be invalid 64-len hex",
);
assert!(
!is_implicit(invalid_account_id),
"Account ID {invalid_account_id} should be invalid 64-len hex",
);
}
}
#[test]
fn test_json_deserialize_account_id() {
assert!(serde_json::from_str::<AccountId>(r#""test.near""#).is_ok());
assert!(serde_json::from_str::<AccountId>(r#""test@.near""#).is_err());
}
}