1use std::borrow::Cow;
2
3use crate::{AccountId, ParseAccountError};
4
5#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash)]
29#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
30#[cfg_attr(feature = "abi", derive(borsh::BorshSchema))]
31pub struct AccountIdRef(pub(crate) str);
32
33#[derive(PartialEq)]
40pub enum AccountType {
41 UtilityAccount,
43 EthAccount,
45 Reserved,
46}
47
48impl AccountType {
49 pub fn is_valid(&self) -> bool {
50 match &self {
51 Self::UtilityAccount => true,
52 Self::EthAccount => true,
53 Self::Reserved => false,
54 }
55 }
56}
57
58impl AccountIdRef {
59 pub const MIN_LEN: usize = crate::validation::MIN_LEN;
61 pub const MAX_LEN: usize = crate::validation::MAX_LEN;
63
64 pub fn new<S: AsRef<str> + ?Sized>(id: &S) -> Result<&Self, ParseAccountError> {
68 let id = id.as_ref();
69 crate::validation::validate(id)?;
70
71 Ok(unsafe { &*(id as *const str as *const Self) })
75 }
76
77 pub const fn new_or_panic(id: &str) -> &Self {
84 crate::validation::validate_const(id);
85
86 unsafe { &*(id as *const str as *const Self) }
87 }
88
89 pub(crate) fn new_unvalidated<S: AsRef<str> + ?Sized>(id: &S) -> &Self {
94 let id = id.as_ref();
95 #[cfg(not(feature = "internal_unstable"))]
100 debug_assert!(crate::validation::validate(id).is_ok());
101
102 unsafe { &*(id as *const str as *const Self) }
104 }
105
106 pub fn as_bytes(&self) -> &[u8] {
108 self.0.as_bytes()
109 }
110
111 pub fn as_str(&self) -> &str {
122 &self.0
123 }
124
125 pub fn get_account_type(&self) -> AccountType {
150 if crate::validation::is_eth_implicit(self.as_str()) {
151 return AccountType::EthAccount;
152 }
153 if crate::validation::is_valid_implicit(self.as_str()) {
154 return AccountType::UtilityAccount;
155 }
156
157 AccountType::Reserved
158 }
159
160 pub fn is_system(&self) -> bool {
176 self == "system"
177 }
178
179 pub const fn len(&self) -> usize {
181 self.0.len()
182 }
183
184 pub fn get_parent_account_id(&self) -> Option<&AccountIdRef> {
199 let parent_str = self.as_str().split_once('.')?.1;
200 Some(AccountIdRef::new_unvalidated(parent_str))
201 }
202}
203
204impl std::fmt::Display for AccountIdRef {
205 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206 std::fmt::Display::fmt(&self.0, f)
207 }
208}
209
210impl ToOwned for AccountIdRef {
211 type Owned = AccountId;
212
213 fn to_owned(&self) -> Self::Owned {
214 AccountId(self.0.into())
215 }
216}
217
218impl<'a> From<&'a AccountIdRef> for AccountId {
219 fn from(id: &'a AccountIdRef) -> Self {
220 id.to_owned()
221 }
222}
223
224impl<'s> TryFrom<&'s str> for &'s AccountIdRef {
225 type Error = ParseAccountError;
226
227 fn try_from(value: &'s str) -> Result<Self, Self::Error> {
228 AccountIdRef::new(value)
229 }
230}
231
232impl AsRef<str> for AccountIdRef {
233 fn as_ref(&self) -> &str {
234 &self.0
235 }
236}
237
238impl PartialEq<AccountIdRef> for String {
239 fn eq(&self, other: &AccountIdRef) -> bool {
240 self == &other.0
241 }
242}
243
244impl PartialEq<String> for AccountIdRef {
245 fn eq(&self, other: &String) -> bool {
246 &self.0 == other
247 }
248}
249
250impl PartialEq<AccountIdRef> for str {
251 fn eq(&self, other: &AccountIdRef) -> bool {
252 self == &other.0
253 }
254}
255
256impl PartialEq<str> for AccountIdRef {
257 fn eq(&self, other: &str) -> bool {
258 &self.0 == other
259 }
260}
261
262impl<'a> PartialEq<AccountIdRef> for &'a str {
263 fn eq(&self, other: &AccountIdRef) -> bool {
264 *self == &other.0
265 }
266}
267
268impl<'a> PartialEq<&'a str> for AccountIdRef {
269 fn eq(&self, other: &&'a str) -> bool {
270 &self.0 == *other
271 }
272}
273
274impl<'a> PartialEq<&'a AccountIdRef> for str {
275 fn eq(&self, other: &&'a AccountIdRef) -> bool {
276 self == &other.0
277 }
278}
279
280impl<'a> PartialEq<str> for &'a AccountIdRef {
281 fn eq(&self, other: &str) -> bool {
282 &self.0 == other
283 }
284}
285
286impl<'a> PartialEq<&'a AccountIdRef> for String {
287 fn eq(&self, other: &&'a AccountIdRef) -> bool {
288 self == &other.0
289 }
290}
291
292impl<'a> PartialEq<String> for &'a AccountIdRef {
293 fn eq(&self, other: &String) -> bool {
294 &self.0 == other
295 }
296}
297
298impl PartialOrd<AccountIdRef> for String {
299 fn partial_cmp(&self, other: &AccountIdRef) -> Option<std::cmp::Ordering> {
300 self.as_str().partial_cmp(&other.0)
301 }
302}
303
304impl PartialOrd<String> for AccountIdRef {
305 fn partial_cmp(&self, other: &String) -> Option<std::cmp::Ordering> {
306 self.0.partial_cmp(other.as_str())
307 }
308}
309
310impl PartialOrd<AccountIdRef> for str {
311 fn partial_cmp(&self, other: &AccountIdRef) -> Option<std::cmp::Ordering> {
312 self.partial_cmp(other.as_str())
313 }
314}
315
316impl PartialOrd<str> for AccountIdRef {
317 fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
318 self.as_str().partial_cmp(other)
319 }
320}
321
322impl<'a> PartialOrd<AccountIdRef> for &'a str {
323 fn partial_cmp(&self, other: &AccountIdRef) -> Option<std::cmp::Ordering> {
324 self.partial_cmp(&other.as_str())
325 }
326}
327
328impl<'a> PartialOrd<&'a str> for AccountIdRef {
329 fn partial_cmp(&self, other: &&'a str) -> Option<std::cmp::Ordering> {
330 self.as_str().partial_cmp(*other)
331 }
332}
333
334impl<'a> PartialOrd<&'a AccountIdRef> for String {
335 fn partial_cmp(&self, other: &&'a AccountIdRef) -> Option<std::cmp::Ordering> {
336 self.as_str().partial_cmp(&other.0)
337 }
338}
339
340impl<'a> PartialOrd<String> for &'a AccountIdRef {
341 fn partial_cmp(&self, other: &String) -> Option<std::cmp::Ordering> {
342 self.0.partial_cmp(other.as_str())
343 }
344}
345
346impl<'a> PartialOrd<&'a AccountIdRef> for str {
347 fn partial_cmp(&self, other: &&'a AccountIdRef) -> Option<std::cmp::Ordering> {
348 self.partial_cmp(other.as_str())
349 }
350}
351
352impl<'a> PartialOrd<str> for &'a AccountIdRef {
353 fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
354 self.as_str().partial_cmp(other)
355 }
356}
357
358impl<'a> From<&'a AccountIdRef> for Cow<'a, AccountIdRef> {
359 fn from(value: &'a AccountIdRef) -> Self {
360 Cow::Borrowed(value)
361 }
362}
363
364#[cfg(feature = "arbitrary")]
365impl<'a> arbitrary::Arbitrary<'a> for &'a AccountIdRef {
366 fn size_hint(_depth: usize) -> (usize, Option<usize>) {
367 (crate::validation::MIN_LEN, Some(crate::validation::MAX_LEN))
368 }
369
370 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
371 let mut s = u.arbitrary::<&str>()?;
372
373 loop {
374 match AccountIdRef::new(s) {
375 Ok(account_id) => break Ok(account_id),
376 Err(ParseAccountError {
377 char: Some((idx, _)),
378 ..
379 }) => {
380 s = &s[..idx];
381 continue;
382 }
383 _ => break Err(arbitrary::Error::IncorrectFormat),
384 }
385 }
386 }
387
388 fn arbitrary_take_rest(u: arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
389 let s = <&str as arbitrary::Arbitrary>::arbitrary_take_rest(u)?;
390 AccountIdRef::new(s).map_err(|_| arbitrary::Error::IncorrectFormat)
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use crate::ParseErrorKind;
397
398 use super::*;
399
400 #[test]
401 #[cfg(feature = "schemars")]
402 fn test_schemars() {
403 let schema = schemars::schema_for!(AccountIdRef);
404 let json_schema = serde_json::to_value(&schema).unwrap();
405 assert_eq!(
406 json_schema,
407 serde_json::json!({
408 "$schema": "http://json-schema.org/draft-07/schema#",
409 "description": "Account identifier. This is the human readable UTF-8 string which is used internally to index accounts on the network and their respective state.\n\nThis is the \"referenced\" version of the account ID. It is to [`AccountId`] what [`str`] is to [`String`], and works quite similarly to [`Path`]. Like with [`str`] and [`Path`], you can't have a value of type `AccountIdRef`, but you can have a reference like `&AccountIdRef` or `&mut AccountIdRef`.\n\nThis type supports zero-copy deserialization offered by [`serde`](https://docs.rs/serde/), but cannot do the same for [`borsh`](https://docs.rs/borsh/) since the latter does not support zero-copy.\n\n# Examples ``` use unc_account_id::{AccountId, AccountIdRef}; use std::convert::{TryFrom, TryInto};\n\n// Construction let alice = AccountIdRef::new(\"alice.unc\").unwrap(); assert!(AccountIdRef::new(\"invalid.\").is_err()); ```\n\n[`FromStr`]: std::str::FromStr [`Path`]: std::path::Path",
410 "title": "AccountIdRef",
411 "type": "string"
412 }
413 )
414 );
415 }
416
417 #[test]
418 fn test_err_kind_classification() {
419 let id = AccountIdRef::new("ErinMoriarty.unc");
420 debug_assert!(
421 matches!(
422 id,
423 Err(ParseAccountError {
424 kind: ParseErrorKind::InvalidChar,
425 char: Some((0, 'E'))
426 })
427 ),
428 "{:?}",
429 id
430 );
431
432 let id = AccountIdRef::new("-KarlUrban.unc");
433 debug_assert!(
434 matches!(
435 id,
436 Err(ParseAccountError {
437 kind: ParseErrorKind::RedundantSeparator,
438 char: Some((0, '-'))
439 })
440 ),
441 "{:?}",
442 id
443 );
444
445 let id = AccountIdRef::new("anthonystarr.");
446 debug_assert!(
447 matches!(
448 id,
449 Err(ParseAccountError {
450 kind: ParseErrorKind::RedundantSeparator,
451 char: Some((12, '.'))
452 })
453 ),
454 "{:?}",
455 id
456 );
457
458 let id = AccountIdRef::new("jack__Quaid.unc");
459 debug_assert!(
460 matches!(
461 id,
462 Err(ParseAccountError {
463 kind: ParseErrorKind::RedundantSeparator,
464 char: Some((5, '_'))
465 })
466 ),
467 "{:?}",
468 id
469 );
470 }
471
472 #[test]
473 fn test_is_account_id_unc_implicit() {
474 let valid_unc_implicit_account_ids = &[
475 "0000000000000000000000000000000000000000000000000000000000000000",
476 "6174617461746174617461746174617461746174617461746174617461746174",
477 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
478 "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
479 "20782e20662e64666420482123494b6b6c677573646b6c66676a646b6c736667",
480 ];
481 for valid_account_id in valid_unc_implicit_account_ids {
482 assert!(
483 matches!(
484 AccountIdRef::new(valid_account_id),
485 Ok(account_id) if account_id.get_account_type() == AccountType::UtilityAccount
486 ),
487 "Account ID {} should be valid 64-len hex",
488 valid_account_id
489 );
490 }
491
492 let invalid_unc_implicit_account_ids = &[
493 "000000000000000000000000000000000000000000000000000000000000000",
494 "6.74617461746174617461746174617461746174617461746174617461746174",
495 "012-456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
496 "fffff_ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
497 "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo",
498 "00000000000000000000000000000000000000000000000000000000000000",
499 ];
500 for invalid_account_id in invalid_unc_implicit_account_ids {
501 assert!(
502 !matches!(
503 AccountIdRef::new(invalid_account_id),
504 Ok(account_id) if account_id.get_account_type() == AccountType::UtilityAccount
505 ),
506 "Account ID {} is not a Utility-implicit account",
507 invalid_account_id
508 );
509 }
510 }
511
512 #[test]
513 fn test_is_account_id_eth_implicit() {
514 let valid_eth_implicit_account_ids = &[
515 "0x0000000000000000000000000000000000000000",
516 "0x6174617461746174617461746174617461746174",
517 "0x0123456789abcdef0123456789abcdef01234567",
518 "0xffffffffffffffffffffffffffffffffffffffff",
519 "0x20782e20662e64666420482123494b6b6c677573",
520 ];
521 for valid_account_id in valid_eth_implicit_account_ids {
522 assert!(
523 matches!(
524 valid_account_id.parse::<AccountId>(),
525 Ok(account_id) if account_id.get_account_type() == AccountType::EthAccount
526 ),
527 "Account ID {} should be valid 42-len hex, starting with 0x",
528 valid_account_id
529 );
530 }
531
532 let invalid_eth_implicit_account_ids = &[
533 "04b794f5ea0ba39494ce839613fffba74279579268",
534 "0x000000000000000000000000000000000000000",
535 "0x6.74617461746174617461746174617461746174",
536 "0x012-456789abcdef0123456789abcdef01234567",
537 "0xfffff_ffffffffffffffffffffffffffffffffff",
538 "0xoooooooooooooooooooooooooooooooooooooooo",
539 "0x00000000000000000000000000000000000000000",
540 "0000000000000000000000000000000000000000000000000000000000000000",
541 ];
542 for invalid_account_id in invalid_eth_implicit_account_ids {
543 assert!(
544 !matches!(
545 invalid_account_id.parse::<AccountId>(),
546 Ok(account_id) if account_id.get_account_type() == AccountType::EthAccount
547 ),
548 "Account ID {} is not an ETH-implicit account",
549 invalid_account_id
550 );
551 }
552 }
553
554 #[test]
555 #[cfg(feature = "arbitrary")]
556 fn test_arbitrary() {
557 let corpus = [
558 ("a|bcd", None),
559 ("ab|cde", Some("ab")),
560 ("a_-b", None),
561 ("ab_-c", Some("ab")),
562 ("a", None),
563 ("miraclx.unc", Some("miraclx.unc")),
564 (
565 "01234567890123456789012345678901234567890123456789012345678901234",
566 None,
567 ),
568 ];
569
570 for (input, expected_output) in corpus {
571 assert!(input.len() <= u8::MAX as usize);
572 let data = [input.as_bytes(), &[input.len() as _]].concat();
573 let mut u = arbitrary::Unstructured::new(&data);
574
575 assert_eq!(
576 u.arbitrary::<&AccountIdRef>()
577 .ok()
578 .map(AsRef::<str>::as_ref),
579 expected_output
580 );
581 }
582 }
583}