ic_ledger_types/
lib.rs

1//! A library of types to communicate with the ICP ledger canister.
2
3#![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
23/// The subaccount that is used by default.
24pub const DEFAULT_SUBACCOUNT: Subaccount = Subaccount([0; 32]);
25
26/// The default fee for ledger transactions.
27pub const DEFAULT_FEE: Tokens = Tokens { e8s: 10_000 };
28
29/// Id of the ledger canister on the IC.
30pub const MAINNET_LEDGER_CANISTER_ID: Principal =
31    Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x01]);
32
33/// Id of the governance canister on the IC.
34pub const MAINNET_GOVERNANCE_CANISTER_ID: Principal =
35    Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01]);
36
37/// Id of the cycles minting canister on the IC.
38pub const MAINNET_CYCLES_MINTING_CANISTER_ID: Principal =
39    Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x01, 0x01]);
40
41/// Number of nanoseconds from the UNIX epoch in UTC timezone.
42#[derive(
43    CandidType, Serialize, Deserialize, Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord,
44)]
45pub struct Timestamp {
46    /// Number of nanoseconds from the UNIX epoch in UTC timezone.
47    pub timestamp_nanos: u64,
48}
49
50/// A type for representing amounts of Tokens.
51///
52/// # Panics
53///
54/// * Arithmetics (addition, subtraction) on the Tokens type panics if the underlying type
55///   overflows.
56#[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    /// The maximum number of Tokens we can hold on a single account.
65    pub const MAX: Self = Tokens { e8s: u64::MAX };
66    /// Zero Tokens.
67    pub const ZERO: Self = Tokens { e8s: 0 };
68    /// How many times can Tokenss be divided
69    pub const SUBDIVIDABLE_BY: u64 = 100_000_000;
70
71    /// Constructs an amount of Tokens from the number of 10^-8 Tokens.
72    pub const fn from_e8s(e8s: u64) -> Self {
73        Self { e8s }
74    }
75
76    /// Returns the number of 10^-8 Tokens in this amount.
77    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/// Subaccount is an arbitrary 32-byte byte array.
133/// Ledger uses subaccounts to compute account address, which enables one
134/// principal to control multiple ledger accounts.
135#[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/// `AccountIdentifier` is a 32-byte array.
152/// The first 4 bytes is a big-endian encoding of a CRC32 checksum of the last 28 bytes.
153#[derive(
154    CandidType, Serialize, Deserialize, Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord,
155)]
156pub struct AccountIdentifier([u8; 32]);
157
158impl AccountIdentifier {
159    /// Creates a new account identifier from a principal and subaccount.
160    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    /// Convert hex string into `AccountIdentifier`.
178    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            // Since the input was provided in hex, return an error that is hex-friendly.
182            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    /// Converts a blob into an `AccountIdentifier`.
192    ///
193    /// The blob can be either:
194    ///
195    /// 1. The 32-byte canonical format (4 byte checksum + 28 byte hash).
196    /// 2. The 28-byte hash.
197    ///
198    /// If the 32-byte canonical format is provided, the checksum is verified.
199    pub fn from_slice(v: &[u8]) -> Result<AccountIdentifier, AccountIdParseError> {
200        // Try parsing it as a 32-byte blob.
201        match v.try_into() {
202            Ok(h) => {
203                // It's a 32-byte blob. Validate the checksum.
204                check_sum(h).map_err(AccountIdParseError::InvalidChecksum)
205            }
206            Err(_) => {
207                // Try parsing it as a 28-byte hash.
208                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    /// Convert `AccountIdentifier` into hex string.
218    pub fn to_hex(&self) -> String {
219        hex::encode(self.0)
220    }
221
222    /// Provide the account identifier as bytes.
223    pub fn as_bytes(&self) -> &[u8; 32] {
224        &self.0
225    }
226
227    /// Returns the checksum of the account identifier.
228    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    // Get the checksum provided
237    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    // Check the generated checksum matches
244    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/// An error for reporting invalid checksums.
300#[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/// An error for reporting invalid Account Identifiers.
320#[derive(Debug, PartialEq, Eq)]
321pub enum AccountIdParseError {
322    /// The checksum failed to verify.
323    InvalidChecksum(ChecksumError),
324    /// The length of the input was invalid.
325    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/// Arguments for the `account_balance` call.
342#[derive(CandidType, Serialize, Deserialize, Clone, Debug)]
343pub struct AccountBalanceArgs {
344    /// The account identifier to query the balance of.
345    pub account: AccountIdentifier,
346}
347
348/// An arbitrary number associated with a transaction.
349/// The caller can set it in a `transfer` call as a correlation identifier.
350#[derive(
351    CandidType, Serialize, Deserialize, Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord,
352)]
353pub struct Memo(pub u64);
354
355/// Arguments for the `transfer` call.
356#[derive(CandidType, Serialize, Deserialize, Clone, Debug)]
357pub struct TransferArgs {
358    /// Transaction memo.
359    /// See docs for the [`Memo`] type.
360    pub memo: Memo,
361    /// The amount that the caller wants to transfer to the destination address.
362    pub amount: Tokens,
363    /// The amount that the caller pays for the transaction.
364    /// Must be 10000 e8s.
365    pub fee: Tokens,
366    /// The subaccount from which the caller wants to transfer funds.
367    /// If `None`, the ledger uses the default (all zeros) subaccount to compute the source address.
368    /// See docs for the [`Subaccount`] type.
369    pub from_subaccount: Option<Subaccount>,
370    /// The destination account.
371    /// If the transfer is successful, the balance of this address increases by `amount`.
372    pub to: AccountIdentifier,
373    /// The point in time when the caller created this request.
374    /// If `None`, the ledger uses the current IC time as the timestamp.
375    /// Transactions more than one day old will be rejected.
376    pub created_at_time: Option<Timestamp>,
377}
378
379/// The sequence number of a block in the Tokens ledger blockchain.
380pub type BlockIndex = u64;
381
382/// Result of the `transfer` call.
383pub type TransferResult = Result<BlockIndex, TransferError>;
384
385/// Error of the `transfer` call.
386#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
387pub enum TransferError {
388    /// The fee that the caller specified in the transfer request was not the one that the ledger expects.
389    /// The caller can change the transfer fee to the `expected_fee` and retry the request.
390    BadFee {
391        /// The account specified by the caller doesn't have enough funds.
392        expected_fee: Tokens,
393    },
394    /// The caller did not have enough ICP in the specified subaccount.
395    InsufficientFunds {
396        /// The caller's balance.
397        balance: Tokens,
398    },
399    /// The request is too old.
400    /// The ledger only accepts requests created within a 24-hour window.
401    /// This is a non-recoverable error.
402    TxTooOld {
403        /// The permitted duration between `created_at_time` and now.
404        allowed_window_nanos: u64,
405    },
406    /// The caller specified a `created_at_time` that is too far in the future.
407    /// The caller can retry the request later.
408    /// This may also be caused by clock desynchronization.
409    TxCreatedInFuture,
410    /// The ledger has already executed the request.
411    TxDuplicate {
412        /// The index of the block containing the original transaction.
413        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/// The content of a ledger transaction.
446#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
447pub enum Operation {
448    /// Tokens were minted, usually via spawning/disbursing neuron maturity or as node operator rewards.
449    Mint {
450        /// The account that the tokens were transferred to.
451        to: AccountIdentifier,
452        /// The amount that was transferred.
453        amount: Tokens,
454    },
455    /// Tokens were burned, usually to create cycles for a canister.
456    Burn {
457        /// The account that sent the tokens to be burned.
458        from: AccountIdentifier,
459        /// The amount that was burned.
460        amount: Tokens,
461    },
462    /// Tokens were transferred from one account to another.
463    Transfer {
464        /// The account the tokens were transferred from.
465        from: AccountIdentifier,
466        /// The account the tokens were transferred to.
467        to: AccountIdentifier,
468        /// The amount of tokens that were transferred.
469        amount: Tokens,
470        /// The fee that was charged for the transfer.
471        fee: Tokens,
472    },
473    /// An account approved another account to transfer tokens on its behalf.
474    Approve {
475        /// The account that owns the tokens.
476        from: AccountIdentifier,
477        /// The account that was enabled to spend them.
478        spender: AccountIdentifier,
479        // TODO: add the allowance_e8s field after the official ICRC-2 release.
480        /// The expiration date for this approval.
481        expires_at: Option<Timestamp>,
482        /// The fee that was charged for the approval.
483        fee: Tokens,
484    },
485    /// An account transferred tokens from another account on its behalf, following an approval.
486    TransferFrom {
487        /// The account that the tokens were transferred from.
488        from: AccountIdentifier,
489        /// The account that the tokens were transferred to.
490        to: AccountIdentifier,
491        /// The account that performed the transfer.
492        spender: AccountIdentifier,
493        /// The amount that was transferred.
494        amount: Tokens,
495        /// The fee that was charged for the transfer.
496        fee: Tokens,
497    },
498}
499
500/// A recorded ledger transaction.
501#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
502pub struct Transaction {
503    /// The memo that was provided for the transaction.
504    pub memo: Memo,
505    /// The content of the transaction.
506    pub operation: Option<Operation>,
507    /// The time at which the client of the ledger constructed the transaction.
508    pub created_at_time: Timestamp,
509    /// The memo that was provided to the `icrc1_transfer` method.
510    pub icrc1_memo: Option<ByteBuf>,
511}
512
513/// A single record in the ledger.
514#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
515pub struct Block {
516    /// The hash of the parent block.
517    pub parent_hash: Option<[u8; 32]>,
518    /// The transaction that occurred in this block.
519    pub transaction: Transaction,
520    /// The time at which the ledger constructed the block.
521    pub timestamp: Timestamp,
522}
523
524/// Arguments for the `get_blocks` function.
525#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
526pub struct GetBlocksArgs {
527    /// The index of the first block to fetch.
528    pub start: BlockIndex,
529    /// Max number of blocks to fetch.
530    pub length: u64,
531}
532
533/// Return type for the `query_blocks` function.
534#[derive(CandidType, Deserialize, Clone, Debug)]
535pub struct QueryBlocksResponse {
536    /// The total number of blocks in the chain.
537    /// If the chain length is positive, the index of the last block is `chain_length - 1`.
538    pub chain_length: u64,
539    /// The replica certificate for the last block hash (see [Encoding of Certificates](https://internetcomputer.org/docs/current/references/ic-interface-spec#certification-encoding)).
540    /// Only available when *querying* blocks from a canister.
541    pub certificate: Option<ByteBuf>,
542    /// List of blocks that were available in the ledger when it processed the call.
543    ///
544    /// The blocks form a contiguous range, with the first block having index
545    /// `first_block_index` (see below), and the last block having index
546    /// `first_block_index + blocks.len() - 1`.
547    ///
548    /// The block range can be an arbitrary sub-range of the originally requested range.
549    pub blocks: Vec<Block>,
550    /// The index of the first block in [`QueryBlocksResponse::blocks`].
551    /// If the `blocks` vector is empty, the exact value of this field is not specified.
552    pub first_block_index: BlockIndex,
553    /// Encoded functions for fetching archived blocks whose indices fall into the
554    /// requested range.
555    ///
556    /// For each entry `e` in `archived_blocks`, `e.start..e.start + e.length` is a sub-range
557    /// of the originally requested block range.
558    pub archived_blocks: Vec<ArchivedBlockRange>,
559}
560
561/// A function that can be called to retrieve a range of archived blocks.
562#[derive(CandidType, Deserialize, Clone, Debug)]
563pub struct ArchivedBlockRange {
564    /// The index of the first archived block that can be fetched using `callback`.
565    pub start: BlockIndex,
566    /// The number of blocks that can be fetched using `callback`.
567    pub length: u64,
568    /// The function that should be called to fetch the archived blocks.
569    /// The range of the blocks accessible using this function is given by the `start`
570    /// and `length` fields above.
571    pub callback: QueryArchiveFn,
572}
573
574/// A prefix of the block range specified in the `get_blocks` and [`query_archived_blocks`] function.
575#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)]
576pub struct BlockRange {
577    /// A prefix of the requested block range.
578    /// The index of the first block is equal to [`GetBlocksArgs.start`](GetBlocksArgs).
579    ///
580    /// ## Note
581    ///
582    /// The number of blocks might be less than the requested
583    /// [`GetBlocksArgs.length`](GetBlocksArgs) for various reasons, for example:
584    ///
585    /// 1. The query might have hit the replica with an outdated state
586    ///    that doesn't have the full block range yet.
587    /// 2. The requested range is too large to fit into a single reply.
588    ///
589    /// The list of blocks can be empty if:
590    ///
591    /// 1. [`GetBlocksArgs.length`](GetBlocksArgs) was zero.
592    /// 2. [`GetBlocksArgs.start`](GetBlocksArgs) was larger than the last block known to the canister.
593    pub blocks: Vec<Block>,
594}
595
596/// The return type of `get_blocks`.
597pub type GetBlocksResult = Result<BlockRange, GetBlocksError>;
598
599/// An error indicating that the arguments passed to `get_blocks` or [`query_archived_blocks`] were invalid.
600#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, CandidType)]
601pub enum GetBlocksError {
602    /// The [`GetBlocksArgs.start`](GetBlocksArgs) argument was smaller than the first block
603    /// served by the canister that received the request.
604    BadFirstBlockIndex {
605        /// The index that was requested.
606        requested_index: BlockIndex,
607        /// The minimum index that can be requested, for this particular call.
608        first_valid_index: BlockIndex,
609    },
610    /// Reserved for future use.
611    Other {
612        /// A machine-readable error code.
613        error_code: u64,
614        /// A human-readable error message.
615        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/// Function type used by `query_blocks` for fetching blocks from the archive.
641/// Has the signature `(`[`GetBlocksArgs`]`) -> (`[`GetBlocksResult`]`)`.
642#[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
671/// Calls the `account_balance` method on the specified canister.
672///
673/// # Example
674/// ```no_run
675/// use ic_cdk::api::msg_caller;
676/// use ic_ledger_types::{AccountIdentifier, AccountBalanceArgs, Tokens, DEFAULT_SUBACCOUNT, MAINNET_LEDGER_CANISTER_ID, account_balance};
677///
678/// async fn check_callers_balance() -> Tokens {
679///   account_balance(
680///     MAINNET_LEDGER_CANISTER_ID,
681///     &AccountBalanceArgs {
682///       account: AccountIdentifier::new(&msg_caller(), &DEFAULT_SUBACCOUNT)
683///     }
684///   ).await.expect("call to ledger failed")
685/// }
686/// ```
687pub 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
697/// Calls the "transfer" method on the specified canister.
698/// # Example
699/// ```no_run
700/// use ic_cdk::api::msg_caller;
701/// use ic_ledger_types::{AccountIdentifier, BlockIndex, Memo, TransferArgs, Tokens, DEFAULT_SUBACCOUNT, DEFAULT_FEE, MAINNET_LEDGER_CANISTER_ID, transfer};
702///
703/// async fn transfer_to_caller() -> BlockIndex {
704///   transfer(
705///     MAINNET_LEDGER_CANISTER_ID,
706///     &TransferArgs {
707///       memo: Memo(0),
708///       amount: Tokens::from_e8s(1_000_000),
709///       fee: DEFAULT_FEE,
710///       from_subaccount: None,
711///       to: AccountIdentifier::new(&msg_caller(), &DEFAULT_SUBACCOUNT),
712///       created_at_time: None,
713///     }
714///   ).await.expect("call to ledger failed").expect("transfer failed")
715/// }
716/// ```
717pub 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/// Return type of the `token_symbol` function.
728#[derive(Serialize, Deserialize, CandidType, Clone, Hash, Debug, PartialEq, Eq)]
729pub struct Symbol {
730    /// A token's trade symbol, e.g. 'ICP'.
731    pub symbol: String,
732}
733
734/// Calls the `token_symbol` method on the specified canister.
735/// # Example
736/// ```no_run
737/// use candid::Principal;
738/// use ic_ledger_types::{Symbol, token_symbol};
739///
740/// async fn symbol(ledger_canister_id: Principal) -> String {
741///   token_symbol(ledger_canister_id).await.expect("call to ledger failed").symbol
742/// }
743/// ```
744pub 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
750/// Calls the `query_block` method on the specified canister.
751/// # Example
752/// ```no_run
753/// use candid::Principal;
754/// use ic_cdk::call::CallResult;
755/// use ic_ledger_types::{BlockIndex, Block, GetBlocksArgs, query_blocks, query_archived_blocks};
756///
757/// async fn query_one_block(ledger: Principal, block_index: BlockIndex) -> CallResult<Option<Block>> {
758///   let args = GetBlocksArgs { start: block_index, length: 1 };
759///
760///   let blocks_result = query_blocks(ledger, &args).await?;
761///
762///   if blocks_result.blocks.len() >= 1 {
763///       debug_assert_eq!(blocks_result.first_block_index, block_index);
764///       return Ok(blocks_result.blocks.into_iter().next());
765///   }
766///
767///   if let Some(func) = blocks_result
768///       .archived_blocks
769///       .into_iter()
770///       .find_map(|b| (b.start <= block_index && (block_index - b.start) < b.length).then(|| b.callback)) {
771///       match query_archived_blocks(&func, &args).await? {
772///           Ok(range) => return Ok(range.blocks.into_iter().next()),
773///           _ => (),
774///       }
775///   }
776///   Ok(None)
777/// }
778pub 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
788/// Continues a query started in [`query_blocks`] by calling its returned archive function.
789///
790/// # Example
791///
792/// ```no_run
793/// use candid::Principal;
794/// use ic_cdk::call::CallResult;
795/// use ic_ledger_types::{BlockIndex, Block, GetBlocksArgs, query_blocks, query_archived_blocks};
796///
797/// async fn query_one_block(ledger: Principal, block_index: BlockIndex) -> CallResult<Option<Block>> {
798///   let args = GetBlocksArgs { start: block_index, length: 1 };
799///
800///   let blocks_result = query_blocks(ledger, &args).await?;
801///
802///   if blocks_result.blocks.len() >= 1 {
803///       debug_assert_eq!(blocks_result.first_block_index, block_index);
804///       return Ok(blocks_result.blocks.into_iter().next());
805///   }
806///
807///   if let Some(func) = blocks_result
808///       .archived_blocks
809///       .into_iter()
810///       .find_map(|b| (b.start <= block_index && (block_index - b.start) < b.length).then(|| b.callback)) {
811///       match query_archived_blocks(&func, &args).await? {
812///           Ok(range) => return Ok(range.blocks.into_iter().next()),
813///           _ => (),
814///       }
815///   }
816///   Ok(None)
817/// }
818pub 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        // The account generated is the account used to top up canister 4bkt6-4aaaa-aaaaf-aaaiq-cai
888        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    /// Verifies that these conversions yield the same result:
897    /// * bytes -> AccountIdentifier -> hex -> AccountIdentifier
898    /// * bytes -> AccountIdentifier
899    #[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    /// Verifies that this convertion yields the original data:
916    /// * bytes -> AccountIdentifier -> bytes
917    #[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        // A 32-byte address with a valid checksum
966        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        // Try again with correct checksum
1009        let length_64 = "807077e900000000000000000000000000000000000000000000000000000000";
1010        assert_eq!(
1011            AccountIdentifier::from_hex(length_64),
1012            Ok(AccountIdentifier(aid_bytes))
1013        );
1014    }
1015}