near_kit/types/
account.rs1use std::fmt::{self, Display};
4use std::str::FromStr;
5
6use borsh::{BorshDeserialize, BorshSerialize};
7use serde::{Deserialize, Serialize};
8
9use crate::error::ParseAccountIdError;
10
11#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
30#[serde(transparent)]
31pub struct AccountId(String);
32
33impl AccountId {
34 pub fn new(s: impl Into<String>) -> Result<Self, ParseAccountIdError> {
36 let s = s.into();
37 Self::validate(&s)?;
38 Ok(Self(s))
39 }
40
41 #[doc(hidden)]
43 pub fn new_unchecked(s: impl Into<String>) -> Self {
44 Self(s.into())
45 }
46
47 pub fn parse_lenient(s: impl AsRef<str>) -> Self {
66 s.as_ref()
67 .parse()
68 .unwrap_or_else(|_| Self::new_unchecked(s.as_ref()))
69 }
70
71 fn validate(s: &str) -> Result<(), ParseAccountIdError> {
73 if s.is_empty() {
74 return Err(ParseAccountIdError::Empty);
75 }
76
77 if s.len() > 64 {
78 return Err(ParseAccountIdError::TooLong(s.to_string()));
79 }
80
81 if let Some(hex_part) = s.strip_prefix("0x") {
83 if s.len() != 42 {
84 return Err(ParseAccountIdError::InvalidFormat(s.to_string()));
85 }
86 for c in hex_part.chars() {
88 if !c.is_ascii_hexdigit() {
89 return Err(ParseAccountIdError::InvalidChar(s.to_string(), c));
90 }
91 }
92 return Ok(());
93 }
94
95 if s.len() == 64 && s.chars().all(|c| c.is_ascii_hexdigit()) {
97 return Ok(());
98 }
99
100 if s.len() < 2 {
102 return Err(ParseAccountIdError::TooShort(s.to_string()));
103 }
104
105 for c in s.chars() {
107 if !matches!(c, 'a'..='z' | '0'..='9' | '_' | '-' | '.') {
108 return Err(ParseAccountIdError::InvalidChar(s.to_string(), c));
109 }
110 }
111
112 if s.starts_with('.') || s.ends_with('.') || s.contains("..") {
114 return Err(ParseAccountIdError::InvalidFormat(s.to_string()));
115 }
116
117 for part in s.split('.') {
119 if part.is_empty() {
120 return Err(ParseAccountIdError::InvalidFormat(s.to_string()));
121 }
122 if part.starts_with('-') || part.ends_with('-') {
123 return Err(ParseAccountIdError::InvalidFormat(s.to_string()));
124 }
125 if part.starts_with('_') || part.ends_with('_') {
126 return Err(ParseAccountIdError::InvalidFormat(s.to_string()));
127 }
128 }
129
130 Ok(())
131 }
132
133 pub fn is_implicit(&self) -> bool {
135 self.0.len() == 64 && self.0.chars().all(|c| c.is_ascii_hexdigit())
136 }
137
138 pub fn is_evm_implicit(&self) -> bool {
140 self.0.starts_with("0x") && self.0.len() == 42
141 }
142
143 pub fn is_named(&self) -> bool {
145 !self.is_implicit() && !self.is_evm_implicit()
146 }
147
148 pub fn is_top_level(&self) -> bool {
150 self.is_named() && !self.0.contains('.')
151 }
152
153 pub fn is_sub_account_of(&self, parent: &AccountId) -> bool {
155 if !self.is_named() || !parent.is_named() {
156 return false;
157 }
158 self.0.ends_with(&format!(".{}", parent.0)) && self.0.len() > parent.0.len() + 1
159 }
160
161 pub fn parent(&self) -> Option<AccountId> {
163 if !self.is_named() {
164 return None;
165 }
166 self.0
167 .find('.')
168 .map(|i| AccountId(self.0[i + 1..].to_string()))
169 }
170
171 pub fn as_str(&self) -> &str {
173 &self.0
174 }
175}
176
177impl FromStr for AccountId {
178 type Err = ParseAccountIdError;
179
180 fn from_str(s: &str) -> Result<Self, Self::Err> {
181 Self::new(s)
182 }
183}
184
185impl TryFrom<&str> for AccountId {
186 type Error = ParseAccountIdError;
187
188 fn try_from(s: &str) -> Result<Self, Self::Error> {
189 Self::new(s)
190 }
191}
192
193impl TryFrom<String> for AccountId {
194 type Error = ParseAccountIdError;
195
196 fn try_from(s: String) -> Result<Self, Self::Error> {
197 Self::new(s)
198 }
199}
200
201impl Display for AccountId {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 write!(f, "{}", self.0)
204 }
205}
206
207impl AsRef<str> for AccountId {
208 fn as_ref(&self) -> &str {
209 &self.0
210 }
211}
212
213impl BorshSerialize for AccountId {
214 fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
215 borsh::BorshSerialize::serialize(&self.0, writer)
216 }
217}
218
219impl BorshDeserialize for AccountId {
220 fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
221 let s = String::deserialize_reader(reader)?;
222 Ok(Self(s))
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_valid_named_accounts() {
232 assert!("alice.testnet".parse::<AccountId>().is_ok());
233 assert!("bob.near".parse::<AccountId>().is_ok());
234 assert!("sub.alice.testnet".parse::<AccountId>().is_ok());
235 assert!("a1.b2.c3.testnet".parse::<AccountId>().is_ok());
236 assert!("test_account.near".parse::<AccountId>().is_ok());
237 assert!("test-account.near".parse::<AccountId>().is_ok());
238 }
239
240 #[test]
241 fn test_valid_implicit_accounts() {
242 let hex64 = "0".repeat(64);
243 let account: AccountId = hex64.parse().unwrap();
244 assert!(account.is_implicit());
245 assert!(!account.is_named());
246 }
247
248 #[test]
249 fn test_valid_evm_accounts() {
250 let evm = format!("0x{}", "a".repeat(40));
251 let account: AccountId = evm.parse().unwrap();
252 assert!(account.is_evm_implicit());
253 assert!(!account.is_named());
254 }
255
256 #[test]
257 fn test_invalid_accounts() {
258 assert!("".parse::<AccountId>().is_err());
259 assert!("a".parse::<AccountId>().is_err()); assert!("A.near".parse::<AccountId>().is_err()); assert!(".alice.near".parse::<AccountId>().is_err()); assert!("alice.near.".parse::<AccountId>().is_err()); assert!("alice..near".parse::<AccountId>().is_err()); assert!("-alice.near".parse::<AccountId>().is_err()); }
266
267 #[test]
268 fn test_parent() {
269 let account: AccountId = "sub.alice.testnet".parse().unwrap();
270 let parent = account.parent().unwrap();
271 assert_eq!(parent.as_str(), "alice.testnet");
272
273 let top: AccountId = "testnet".parse().unwrap();
274 assert!(top.parent().is_none());
275 }
276
277 #[test]
278 fn test_is_sub_account_of() {
279 let sub: AccountId = "sub.alice.testnet".parse().unwrap();
280 let parent: AccountId = "alice.testnet".parse().unwrap();
281 let testnet: AccountId = "testnet".parse().unwrap();
282
283 assert!(sub.is_sub_account_of(&parent));
284 assert!(parent.is_sub_account_of(&testnet));
285 }
286}