1#![warn(
4 elided_lifetimes_in_paths,
5 missing_debug_implementations,
6 missing_docs,
7 unsafe_op_in_unsafe_fn,
8 clippy::undocumented_unsafe_blocks,
9 clippy::missing_safety_doc
10)]
11
12use std::convert::TryFrom;
13use std::fmt::{self, Display, Formatter};
14use std::ops::{Add, AddAssign, Sub, SubAssign};
15
16use candid::{CandidType, Principal, types::reference::Func};
17use serde::{Deserialize, Serialize};
18use serde_bytes::ByteBuf;
19use sha2::Digest;
20
21use ic_cdk::call::{Call, CallResult};
22
23pub const DEFAULT_SUBACCOUNT: Subaccount = Subaccount([0; 32]);
25
26pub const DEFAULT_FEE: Tokens = Tokens { e8s: 10_000 };
28
29pub const MAINNET_LEDGER_CANISTER_ID: Principal =
31 Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x01]);
32
33pub const MAINNET_GOVERNANCE_CANISTER_ID: Principal =
35 Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01]);
36
37pub const MAINNET_CYCLES_MINTING_CANISTER_ID: Principal =
39 Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x01, 0x01]);
40
41#[derive(
43 CandidType, Serialize, Deserialize, Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord,
44)]
45pub struct Timestamp {
46 pub timestamp_nanos: u64,
48}
49
50#[derive(
57 CandidType, Serialize, Deserialize, Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord,
58)]
59pub struct Tokens {
60 e8s: u64,
61}
62
63impl Tokens {
64 pub const MAX: Self = Tokens { e8s: u64::MAX };
66 pub const ZERO: Self = Tokens { e8s: 0 };
68 pub const SUBDIVIDABLE_BY: u64 = 100_000_000;
70
71 pub const fn from_e8s(e8s: u64) -> Self {
73 Self { e8s }
74 }
75
76 pub const fn e8s(&self) -> u64 {
78 self.e8s
79 }
80}
81
82impl Add for Tokens {
83 type Output = Self;
84
85 fn add(self, other: Self) -> Self {
86 let e8s = self.e8s.checked_add(other.e8s).unwrap_or_else(|| {
87 panic!(
88 "Add Tokens {} + {} failed because the underlying u64 overflowed",
89 self.e8s, other.e8s
90 )
91 });
92 Self { e8s }
93 }
94}
95
96impl AddAssign for Tokens {
97 fn add_assign(&mut self, other: Self) {
98 *self = *self + other;
99 }
100}
101
102impl Sub for Tokens {
103 type Output = Self;
104 fn sub(self, other: Self) -> Self {
105 let e8s = self.e8s.checked_sub(other.e8s).unwrap_or_else(|| {
106 panic!(
107 "Subtracting Tokens {} - {} failed because the underlying u64 underflowed",
108 self.e8s, other.e8s
109 )
110 });
111 Self { e8s }
112 }
113}
114
115impl SubAssign for Tokens {
116 fn sub_assign(&mut self, other: Self) {
117 *self = *self - other;
118 }
119}
120
121impl Display for Tokens {
122 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
123 write!(
124 f,
125 "{}.{:08}",
126 self.e8s / Tokens::SUBDIVIDABLE_BY,
127 self.e8s % Tokens::SUBDIVIDABLE_BY
128 )
129 }
130}
131
132#[derive(
136 CandidType, Serialize, Deserialize, Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord,
137)]
138pub struct Subaccount(pub [u8; 32]);
139
140#[allow(clippy::range_plus_one)]
141impl From<Principal> for Subaccount {
142 fn from(principal: Principal) -> Self {
143 let mut subaccount = [0; 32];
144 let principal = principal.as_slice();
145 subaccount[0] = principal.len().try_into().unwrap();
146 subaccount[1..1 + principal.len()].copy_from_slice(principal);
147 Subaccount(subaccount)
148 }
149}
150
151#[derive(
154 CandidType, Serialize, Deserialize, Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord,
155)]
156pub struct AccountIdentifier([u8; 32]);
157
158impl AccountIdentifier {
159 pub fn new(owner: &Principal, subaccount: &Subaccount) -> Self {
161 let mut hasher = sha2::Sha224::new();
162 hasher.update(b"\x0Aaccount-id");
163 hasher.update(owner.as_slice());
164 hasher.update(&subaccount.0[..]);
165 let hash: [u8; 28] = hasher.finalize().into();
166
167 let mut hasher = crc32fast::Hasher::new();
168 hasher.update(&hash);
169 let crc32_bytes = hasher.finalize().to_be_bytes();
170
171 let mut result = [0u8; 32];
172 result[0..4].copy_from_slice(&crc32_bytes[..]);
173 result[4..32].copy_from_slice(hash.as_ref());
174 Self(result)
175 }
176
177 pub fn from_hex(hex_str: &str) -> Result<AccountIdentifier, String> {
179 let hex: Vec<u8> = hex::decode(hex_str).map_err(|e| e.to_string())?;
180 Self::from_slice(&hex[..]).map_err(|err| match err {
181 AccountIdParseError::InvalidLength(_) => format!(
183 "{} has a length of {} but we expected a length of 64 or 56",
184 hex_str,
185 hex_str.len()
186 ),
187 AccountIdParseError::InvalidChecksum(err) => err.to_string(),
188 })
189 }
190
191 pub fn from_slice(v: &[u8]) -> Result<AccountIdentifier, AccountIdParseError> {
200 match v.try_into() {
202 Ok(h) => {
203 check_sum(h).map_err(AccountIdParseError::InvalidChecksum)
205 }
206 Err(_) => {
207 match <&[u8] as TryInto<[u8; 28]>>::try_into(v) {
209 Ok(hash) => AccountIdentifier::try_from(hash)
210 .map_err(|_| AccountIdParseError::InvalidLength(v.to_vec())),
211 Err(_) => Err(AccountIdParseError::InvalidLength(v.to_vec())),
212 }
213 }
214 }
215 }
216
217 pub fn to_hex(&self) -> String {
219 hex::encode(self.0)
220 }
221
222 pub fn as_bytes(&self) -> &[u8; 32] {
224 &self.0
225 }
226
227 pub fn generate_checksum(&self) -> [u8; 4] {
229 let mut hasher = crc32fast::Hasher::new();
230 hasher.update(&self.0[4..]);
231 hasher.finalize().to_be_bytes()
232 }
233}
234
235fn check_sum(hex: [u8; 32]) -> Result<AccountIdentifier, ChecksumError> {
236 let found_checksum = &hex[0..4];
238
239 let mut hasher = crc32fast::Hasher::new();
240 hasher.update(&hex[4..]);
241 let expected_checksum = hasher.finalize().to_be_bytes();
242
243 if expected_checksum == found_checksum {
245 Ok(AccountIdentifier(hex))
246 } else {
247 Err(ChecksumError {
248 input: hex,
249 expected_checksum,
250 found_checksum: found_checksum.try_into().unwrap(),
251 })
252 }
253}
254
255impl TryFrom<[u8; 32]> for AccountIdentifier {
256 type Error = String;
257
258 fn try_from(bytes: [u8; 32]) -> Result<Self, Self::Error> {
259 let hash = &bytes[4..];
260 let mut hasher = crc32fast::Hasher::new();
261 hasher.update(hash);
262 let crc32_bytes = hasher.finalize().to_be_bytes();
263 if bytes[0..4] == crc32_bytes[0..4] {
264 Ok(Self(bytes))
265 } else {
266 Err("CRC-32 checksum failed to verify".to_string())
267 }
268 }
269}
270
271impl TryFrom<[u8; 28]> for AccountIdentifier {
272 type Error = String;
273
274 fn try_from(bytes: [u8; 28]) -> Result<Self, Self::Error> {
275 let mut hasher = crc32fast::Hasher::new();
276 hasher.update(bytes.as_slice());
277 let crc32_bytes = hasher.finalize().to_be_bytes();
278
279 let mut aid_bytes = [0u8; 32];
280 aid_bytes[..4].copy_from_slice(&crc32_bytes[..4]);
281 aid_bytes[4..].copy_from_slice(&bytes[..]);
282
283 Ok(Self(aid_bytes))
284 }
285}
286
287impl AsRef<[u8]> for AccountIdentifier {
288 fn as_ref(&self) -> &[u8] {
289 &self.0
290 }
291}
292
293impl Display for AccountIdentifier {
294 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
295 write!(f, "{}", hex::encode(self.as_ref()))
296 }
297}
298
299#[derive(Debug, PartialEq, Eq)]
301pub struct ChecksumError {
302 input: [u8; 32],
303 expected_checksum: [u8; 4],
304 found_checksum: [u8; 4],
305}
306
307impl Display for ChecksumError {
308 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
309 write!(
310 f,
311 "Checksum failed for {}, expected check bytes {} but found {}",
312 hex::encode(&self.input[..]),
313 hex::encode(self.expected_checksum),
314 hex::encode(self.found_checksum),
315 )
316 }
317}
318
319#[derive(Debug, PartialEq, Eq)]
321pub enum AccountIdParseError {
322 InvalidChecksum(ChecksumError),
324 InvalidLength(Vec<u8>),
326}
327
328impl Display for AccountIdParseError {
329 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
330 match self {
331 Self::InvalidChecksum(err) => write!(f, "{err}"),
332 Self::InvalidLength(input) => write!(
333 f,
334 "Received an invalid AccountIdentifier with length {} bytes instead of the expected 28 or 32.",
335 input.len()
336 ),
337 }
338 }
339}
340
341#[derive(CandidType, Serialize, Deserialize, Clone, Debug)]
343pub struct AccountBalanceArgs {
344 pub account: AccountIdentifier,
346}
347
348#[derive(
351 CandidType, Serialize, Deserialize, Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord,
352)]
353pub struct Memo(pub u64);
354
355#[derive(CandidType, Serialize, Deserialize, Clone, Debug)]
357pub struct TransferArgs {
358 pub memo: Memo,
361 pub amount: Tokens,
363 pub fee: Tokens,
366 pub from_subaccount: Option<Subaccount>,
370 pub to: AccountIdentifier,
373 pub created_at_time: Option<Timestamp>,
377}
378
379pub type BlockIndex = u64;
381
382pub type TransferResult = Result<BlockIndex, TransferError>;
384
385#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
387pub enum TransferError {
388 BadFee {
391 expected_fee: Tokens,
393 },
394 InsufficientFunds {
396 balance: Tokens,
398 },
399 TxTooOld {
403 allowed_window_nanos: u64,
405 },
406 TxCreatedInFuture,
410 TxDuplicate {
412 duplicate_of: BlockIndex,
414 },
415}
416
417impl Display for TransferError {
418 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
419 match self {
420 Self::BadFee { expected_fee } => {
421 write!(f, "transaction fee should be {expected_fee}")
422 }
423 Self::InsufficientFunds { balance } => {
424 write!(
425 f,
426 "the debit account doesn't have enough funds to complete the transaction, current balance: {balance}",
427 )
428 }
429 Self::TxTooOld {
430 allowed_window_nanos,
431 } => write!(
432 f,
433 "transaction is older than {} seconds",
434 allowed_window_nanos / 1_000_000_000
435 ),
436 Self::TxCreatedInFuture => write!(f, "transaction's created_at_time is in future"),
437 Self::TxDuplicate { duplicate_of } => write!(
438 f,
439 "transaction is a duplicate of another transaction in block {duplicate_of}"
440 ),
441 }
442 }
443}
444
445#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
447pub enum Operation {
448 Mint {
450 to: AccountIdentifier,
452 amount: Tokens,
454 },
455 Burn {
457 from: AccountIdentifier,
459 amount: Tokens,
461 },
462 Transfer {
464 from: AccountIdentifier,
466 to: AccountIdentifier,
468 amount: Tokens,
470 fee: Tokens,
472 },
473 Approve {
475 from: AccountIdentifier,
477 spender: AccountIdentifier,
479 expires_at: Option<Timestamp>,
482 fee: Tokens,
484 },
485 TransferFrom {
487 from: AccountIdentifier,
489 to: AccountIdentifier,
491 spender: AccountIdentifier,
493 amount: Tokens,
495 fee: Tokens,
497 },
498}
499
500#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
502pub struct Transaction {
503 pub memo: Memo,
505 pub operation: Option<Operation>,
507 pub created_at_time: Timestamp,
509 pub icrc1_memo: Option<ByteBuf>,
511}
512
513#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
515pub struct Block {
516 pub parent_hash: Option<[u8; 32]>,
518 pub transaction: Transaction,
520 pub timestamp: Timestamp,
522}
523
524#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
526pub struct GetBlocksArgs {
527 pub start: BlockIndex,
529 pub length: u64,
531}
532
533#[derive(CandidType, Deserialize, Clone, Debug)]
535pub struct QueryBlocksResponse {
536 pub chain_length: u64,
539 pub certificate: Option<ByteBuf>,
542 pub blocks: Vec<Block>,
550 pub first_block_index: BlockIndex,
553 pub archived_blocks: Vec<ArchivedBlockRange>,
559}
560
561#[derive(CandidType, Deserialize, Clone, Debug)]
563pub struct ArchivedBlockRange {
564 pub start: BlockIndex,
566 pub length: u64,
568 pub callback: QueryArchiveFn,
572}
573
574#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)]
576pub struct BlockRange {
577 pub blocks: Vec<Block>,
594}
595
596pub type GetBlocksResult = Result<BlockRange, GetBlocksError>;
598
599#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, CandidType)]
601pub enum GetBlocksError {
602 BadFirstBlockIndex {
605 requested_index: BlockIndex,
607 first_valid_index: BlockIndex,
609 },
610 Other {
612 error_code: u64,
614 error_message: String,
616 },
617}
618
619impl Display for GetBlocksError {
620 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
621 match self {
622 Self::BadFirstBlockIndex {
623 requested_index,
624 first_valid_index,
625 } => write!(
626 f,
627 "invalid first block index: requested block = {requested_index}, first valid block = {first_valid_index}"
628 ),
629 Self::Other {
630 error_code,
631 error_message,
632 } => write!(
633 f,
634 "failed to query blocks (error code {error_code}): {error_message}"
635 ),
636 }
637 }
638}
639
640#[derive(Debug, Clone, Deserialize)]
643#[serde(transparent)]
644pub struct QueryArchiveFn(Func);
645
646impl From<Func> for QueryArchiveFn {
647 fn from(func: Func) -> Self {
648 Self(func)
649 }
650}
651
652impl From<QueryArchiveFn> for Func {
653 fn from(query_func: QueryArchiveFn) -> Self {
654 query_func.0
655 }
656}
657
658impl CandidType for QueryArchiveFn {
659 fn _ty() -> candid::types::Type {
660 candid::func!((GetBlocksArgs) -> (GetBlocksResult) query)
661 }
662
663 fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
664 where
665 S: candid::types::Serializer,
666 {
667 Func::from(self.clone()).idl_serialize(serializer)
668 }
669}
670
671pub async fn account_balance(
688 ledger_canister_id: Principal,
689 args: &AccountBalanceArgs,
690) -> CallResult<Tokens> {
691 Ok(Call::bounded_wait(ledger_canister_id, "account_balance")
692 .with_arg(args)
693 .await?
694 .candid()?)
695}
696
697pub async fn transfer(
718 ledger_canister_id: Principal,
719 args: &TransferArgs,
720) -> CallResult<TransferResult> {
721 Ok(Call::bounded_wait(ledger_canister_id, "transfer")
722 .with_arg(args)
723 .await?
724 .candid()?)
725}
726
727#[derive(Serialize, Deserialize, CandidType, Clone, Hash, Debug, PartialEq, Eq)]
729pub struct Symbol {
730 pub symbol: String,
732}
733
734pub async fn token_symbol(ledger_canister_id: Principal) -> CallResult<Symbol> {
745 Ok(Call::bounded_wait(ledger_canister_id, "token_symbol")
746 .await?
747 .candid()?)
748}
749
750pub async fn query_blocks(
779 ledger_canister_id: Principal,
780 args: &GetBlocksArgs,
781) -> CallResult<QueryBlocksResponse> {
782 Ok(Call::bounded_wait(ledger_canister_id, "query_blocks")
783 .with_arg(args)
784 .await?
785 .candid()?)
786}
787
788pub async fn query_archived_blocks(
819 func: &QueryArchiveFn,
820 args: &GetBlocksArgs,
821) -> CallResult<GetBlocksResult> {
822 Ok(Call::bounded_wait(func.0.principal, &func.0.method)
823 .with_arg(args)
824 .await?
825 .candid()?)
826}
827
828#[cfg(test)]
829mod tests {
830 use std::string::ToString;
831
832 use super::*;
833
834 #[test]
835 fn test_account_id() {
836 assert_eq!(
837 "bdc4ee05d42cd0669786899f256c8fd7217fa71177bd1fa7b9534f568680a938".to_string(),
838 AccountIdentifier::new(
839 &Principal::from_text(
840 "iooej-vlrze-c5tme-tn7qt-vqe7z-7bsj5-ebxlc-hlzgs-lueo3-3yast-pae"
841 )
842 .unwrap(),
843 &DEFAULT_SUBACCOUNT,
844 )
845 .to_string()
846 );
847 }
848
849 #[test]
850 fn test_account_id_try_from() {
851 let mut bytes: [u8; 32] = [0; 32];
852 bytes.copy_from_slice(
853 &hex::decode("bdc4ee05d42cd0669786899f256c8fd7217fa71177bd1fa7b9534f568680a938")
854 .unwrap(),
855 );
856 assert!(AccountIdentifier::try_from(bytes).is_ok());
857 bytes[0] = 0;
858 assert!(AccountIdentifier::try_from(bytes).is_err());
859 }
860
861 #[test]
862 fn test_ledger_canister_id() {
863 assert_eq!(
864 MAINNET_LEDGER_CANISTER_ID,
865 Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap()
866 );
867 }
868
869 #[test]
870 fn test_governance_canister_id() {
871 assert_eq!(
872 MAINNET_GOVERNANCE_CANISTER_ID,
873 Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()
874 );
875 }
876
877 #[test]
878 fn test_cycles_minting_canister_id() {
879 assert_eq!(
880 MAINNET_CYCLES_MINTING_CANISTER_ID,
881 Principal::from_text("rkp4c-7iaaa-aaaaa-aaaca-cai").unwrap()
882 );
883 }
884
885 #[test]
886 fn principal_to_subaccount() {
887 let principal = Principal::from_text("4bkt6-4aaaa-aaaaf-aaaiq-cai").unwrap();
889 let subaccount = Subaccount::from(principal);
890 assert_eq!(
891 AccountIdentifier::new(&MAINNET_CYCLES_MINTING_CANISTER_ID, &subaccount).to_string(),
892 "d8646d1cbe44002026fa3e0d86d51a560b1c31d669bc8b7f66421c1b2feaa59f"
893 )
894 }
895
896 #[test]
900 fn check_hex_round_trip() {
901 let bytes: [u8; 32] = [
902 237, 196, 46, 168, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
903 7, 7, 7, 7, 7,
904 ];
905 let ai = AccountIdentifier::from_slice(bytes.as_ref())
906 .expect("Failed to create account identifier");
907 let res = ai.to_hex();
908 assert_eq!(
909 AccountIdentifier::from_hex(&res),
910 Ok(ai),
911 "The account identifier doesn't change after going back and forth between a string"
912 )
913 }
914
915 #[test]
918 fn check_bytes_round_trip() {
919 let bytes: [u8; 32] = [
920 237, 196, 46, 168, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
921 7, 7, 7, 7, 7,
922 ];
923 assert_eq!(
924 AccountIdentifier::from_slice(&bytes)
925 .expect("Failed to parse bytes as principal")
926 .as_bytes(),
927 &bytes,
928 "The account identifier doesn't change after going back and forth between a string"
929 )
930 }
931
932 #[test]
933 fn test_account_id_from_slice() {
934 let length_27 = b"123456789_123456789_1234567".to_vec();
935 assert_eq!(
936 AccountIdentifier::from_slice(&length_27),
937 Err(AccountIdParseError::InvalidLength(length_27))
938 );
939
940 let length_28 = b"123456789_123456789_12345678".to_vec();
941 assert_eq!(
942 AccountIdentifier::from_slice(&length_28),
943 Ok(AccountIdentifier::try_from(
944 <&[u8] as TryInto<[u8; 28]>>::try_into(&length_28).unwrap()
945 )
946 .unwrap())
947 );
948
949 let length_29 = b"123456789_123456789_123456789".to_vec();
950 assert_eq!(
951 AccountIdentifier::from_slice(&length_29),
952 Err(AccountIdParseError::InvalidLength(length_29))
953 );
954
955 let length_32 = [0; 32].to_vec();
956 assert_eq!(
957 AccountIdentifier::from_slice(&length_32),
958 Err(AccountIdParseError::InvalidChecksum(ChecksumError {
959 input: length_32.try_into().unwrap(),
960 expected_checksum: [128, 112, 119, 233],
961 found_checksum: [0, 0, 0, 0],
962 }))
963 );
964
965 let length_32 = [
967 128, 112, 119, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
968 0, 0, 0, 0, 0, 0,
969 ]
970 .to_vec();
971 assert_eq!(
972 AccountIdentifier::from_slice(&length_32),
973 Ok(AccountIdentifier::try_from(
974 <&[u8] as TryInto<[u8; 28]>>::try_into(&[0u8; 28]).unwrap()
975 )
976 .unwrap())
977 );
978 }
979
980 #[test]
981 fn test_account_id_from_hex() {
982 let length_56 = "00000000000000000000000000000000000000000000000000000000";
983 let aid_bytes = [
984 128, 112, 119, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
985 0, 0, 0, 0, 0, 0,
986 ];
987 assert_eq!(
988 AccountIdentifier::from_hex(length_56),
989 Ok(AccountIdentifier(aid_bytes))
990 );
991
992 let length_57 = "000000000000000000000000000000000000000000000000000000000";
993 assert!(AccountIdentifier::from_hex(length_57).is_err());
994
995 let length_58 = "0000000000000000000000000000000000000000000000000000000000";
996 assert_eq!(
997 AccountIdentifier::from_hex(length_58),
998 Err("0000000000000000000000000000000000000000000000000000000000 has a length of 58 but we expected a length of 64 or 56".to_string())
999 );
1000
1001 let length_64 = "0000000000000000000000000000000000000000000000000000000000000000";
1002 assert!(
1003 AccountIdentifier::from_hex(length_64)
1004 .unwrap_err()
1005 .contains("Checksum failed")
1006 );
1007
1008 let length_64 = "807077e900000000000000000000000000000000000000000000000000000000";
1010 assert_eq!(
1011 AccountIdentifier::from_hex(length_64),
1012 Ok(AccountIdentifier(aid_bytes))
1013 );
1014 }
1015}