ic-query 0.101.0

Internet Computer query library for CloudEngine, NNS, SNS, ICRC, system canisters, and public network metadata
Documentation
//! Module: icrc::live::account_transactions::wire::generic
//!
//! Responsibility: decode and losslessly project the generic ICRC index response.
//! Does not own: transport, index discovery, pagination state, ICP-index compatibility, or reports.
//! Boundary: keeps the current ICRC index Candid contract separate from deployed ICP wire types.

use super::super::collection::AccountTransactionsPage;
use super::{
    TransactionSummary,
    json::{Account, Decimal, Hex, json_record},
};
use crate::{
    hex::hex_bytes,
    icrc::{
        ledger::nat_text,
        model::{IcrcAccountRow, IcrcAccountTransactionError, IcrcAccountTransactionRow},
    },
};
use ic_platform::Principal;
use ic_platform::candid_codec::Nat;
use ic_platform::icrc_ledger_types::Account as LedgerAccount;

type IcrcIndexTransactionsResult = Result<IcrcIndexTransactions, IcrcIndexTransactionsError>;

ic_platform::candid_record! {
    #[derive(Debug, Eq, PartialEq)]
    struct IcrcIndexTransactions {
        596_483_356 => balance: Nat,
        3_331_539_157 => transactions: Vec<IcrcIndexTransactionWithId>,
        3_582_638_454 => oldest_tx_id: Option<Nat>,
    }
}

ic_platform::candid_record! {
    #[derive(Debug, Eq, PartialEq)]
    struct IcrcIndexTransactionsError {
        2_584_819_143 => message: String,
    }
}

ic_platform::candid_record! {
    #[derive(Debug, Eq, PartialEq)]
    struct IcrcIndexTransactionWithId {
        23_515 => id: Nat,
        1_266_835_934 => transaction: IcrcIndexTransaction,
    }
}

ic_platform::candid_record! {
    #[derive(Debug, Eq, PartialEq)]
    struct IcrcIndexTransaction {
        1_092_621_391 => burn: Option<IcrcIndexBurn>,
        1_191_829_844 => kind: String,
        1_214_008_994 => mint: Option<IcrcIndexMint>,
        1_378_506_061 => approve: Option<IcrcIndexApprove>,
        1_577_942_963 => authorized_burn: Option<IcrcIndexAuthorizedBurn>,
        1_699_330_566 => authorized_mint: Option<IcrcIndexAuthorizedMint>,
        1_907_380_948 => fee_collector: Option<IcrcIndexFeeCollector>,
        2_781_795_542 => timestamp: u64,
        3_664_621_355 => transfer: Option<IcrcIndexTransfer>,
    }
}

ic_platform::candid_record! {
    #[derive(Debug, Eq, PartialEq)]
    struct IcrcIndexApprove {
        5_094_982 => fee: Option<Nat>,
        1_136_829_802 => from: LedgerAccount,
        1_213_809_850 => memo: Option<Vec<u8>>,
        3_258_775_938 => created_at_time: Option<u64>,
        3_573_748_184 => amount: Nat,
        3_622_243_857 => expected_allowance: Option<Nat>,
        3_680_359_390 => expires_at: Option<u64>,
        3_868_658_507 => spender: LedgerAccount,
    }
}

ic_platform::candid_record! {
    #[derive(Debug, Eq, PartialEq)]
    struct IcrcIndexBurn {
        5_094_982 => fee: Option<Nat>,
        1_136_829_802 => from: LedgerAccount,
        1_213_809_850 => memo: Option<Vec<u8>>,
        3_258_775_938 => created_at_time: Option<u64>,
        3_573_748_184 => amount: Nat,
        3_868_658_507 => spender: Option<LedgerAccount>,
    }
}

ic_platform::candid_record! {
    #[derive(Debug, Eq, PartialEq)]
    struct IcrcIndexMint {
        25_979 => to: LedgerAccount,
        5_094_982 => fee: Option<Nat>,
        1_213_809_850 => memo: Option<Vec<u8>>,
        3_258_775_938 => created_at_time: Option<u64>,
        3_573_748_184 => amount: Nat,
    }
}

ic_platform::candid_record! {
    #[derive(Debug, Eq, PartialEq)]
    struct IcrcIndexTransfer {
        25_979 => to: LedgerAccount,
        5_094_982 => fee: Option<Nat>,
        1_136_829_802 => from: LedgerAccount,
        1_213_809_850 => memo: Option<Vec<u8>>,
        3_258_775_938 => created_at_time: Option<u64>,
        3_573_748_184 => amount: Nat,
        3_868_658_507 => spender: Option<LedgerAccount>,
    }
}

ic_platform::candid_record! {
    #[derive(Debug, Eq, PartialEq)]
    struct IcrcIndexFeeCollector {
        25_983 => ts: Option<u64>,
        1_214_554_659 => mthd: Option<String>,
        1_907_380_948 => fee_collector: Option<LedgerAccount>,
        3_068_679_307 => caller: Option<Principal>,
    }
}

ic_platform::candid_record! {
    #[derive(Debug, Eq, PartialEq)]
    struct IcrcIndexAuthorizedMint {
        25_979 => to: LedgerAccount,
        1_214_554_659 => mthd: Option<String>,
        3_068_679_307 => caller: Option<Principal>,
        3_258_775_938 => created_at_time: Option<u64>,
        3_573_748_184 => amount: Nat,
        4_238_151_620 => reason: Option<String>,
    }
}

ic_platform::candid_record! {
    #[derive(Debug, Eq, PartialEq)]
    struct IcrcIndexAuthorizedBurn {
        1_136_829_802 => from: LedgerAccount,
        1_214_554_659 => mthd: Option<String>,
        3_068_679_307 => caller: Option<Principal>,
        3_258_775_938 => created_at_time: Option<u64>,
        3_573_748_184 => amount: Nat,
        4_238_151_620 => reason: Option<String>,
    }
}

pub(super) fn decode(
    bytes: &[u8],
    index_canister: &Principal,
) -> Result<
    Result<AccountTransactionsPage, IcrcAccountTransactionError>,
    crate::candid_codec::CandidReplyError,
> {
    crate::candid_codec::decode_reply::<IcrcIndexTransactionsResult>(bytes)
        .map(|result| account_transactions_page(result, index_canister))
}

fn account_transactions_page(
    result: IcrcIndexTransactionsResult,
    index_canister: &Principal,
) -> Result<AccountTransactionsPage, IcrcAccountTransactionError> {
    let transactions = result.map_err(|error| IcrcAccountTransactionError::IndexQuery {
        index_canister_id: index_canister.to_text(),
        message: error.message,
    })?;
    Ok(AccountTransactionsPage {
        balance: nat_text(&transactions.balance),
        oldest_transaction_id: transactions.oldest_tx_id.as_ref().map(nat_text),
        transactions: transactions
            .transactions
            .into_iter()
            .map(account_transaction_row)
            .collect(),
    })
}

fn account_transaction_row(transaction: IcrcIndexTransactionWithId) -> IcrcAccountTransactionRow {
    let summary = transaction_summary(&transaction.transaction);
    let raw = serde_json::to_value(&transaction.transaction)
        .expect("index transaction JSON serialization is infallible");
    summary.into_row(
        nat_text(&transaction.id),
        transaction.transaction.kind,
        Some(transaction.transaction.timestamp.to_string()),
        raw,
    )
}

fn transaction_summary(transaction: &IcrcIndexTransaction) -> TransactionSummary {
    if let Some(transfer) = transaction.transfer.as_ref() {
        return TransactionSummary {
            amount_base_units: Some(nat_text(&transfer.amount)),
            fee_base_units: transfer.fee.as_ref().map(nat_text),
            from: Some(account_row(&transfer.from)),
            to: Some(account_row(&transfer.to)),
            spender: transfer.spender.as_ref().map(account_row),
            memo_hex: transfer.memo.as_deref().map(hex_bytes),
            created_at_time_unix_nanos: transfer.created_at_time.map(|time| time.to_string()),
            ..TransactionSummary::default()
        };
    }
    if let Some(mint) = transaction.mint.as_ref() {
        return TransactionSummary {
            amount_base_units: Some(nat_text(&mint.amount)),
            fee_base_units: mint.fee.as_ref().map(nat_text),
            to: Some(account_row(&mint.to)),
            memo_hex: mint.memo.as_deref().map(hex_bytes),
            created_at_time_unix_nanos: mint.created_at_time.map(|time| time.to_string()),
            ..TransactionSummary::default()
        };
    }
    if let Some(burn) = transaction.burn.as_ref() {
        return TransactionSummary {
            amount_base_units: Some(nat_text(&burn.amount)),
            fee_base_units: burn.fee.as_ref().map(nat_text),
            from: Some(account_row(&burn.from)),
            spender: burn.spender.as_ref().map(account_row),
            memo_hex: burn.memo.as_deref().map(hex_bytes),
            created_at_time_unix_nanos: burn.created_at_time.map(|time| time.to_string()),
            ..TransactionSummary::default()
        };
    }
    if let Some(approve) = transaction.approve.as_ref() {
        return TransactionSummary {
            amount_base_units: Some(nat_text(&approve.amount)),
            fee_base_units: approve.fee.as_ref().map(nat_text),
            from: Some(account_row(&approve.from)),
            spender: Some(account_row(&approve.spender)),
            memo_hex: approve.memo.as_deref().map(hex_bytes),
            created_at_time_unix_nanos: approve.created_at_time.map(|time| time.to_string()),
            expires_at_unix_nanos: approve.expires_at.map(|time| time.to_string()),
            expected_allowance_base_units: approve.expected_allowance.as_ref().map(nat_text),
            ..TransactionSummary::default()
        };
    }
    if let Some(mint) = transaction.authorized_mint.as_ref() {
        return TransactionSummary {
            amount_base_units: Some(nat_text(&mint.amount)),
            to: Some(account_row(&mint.to)),
            created_at_time_unix_nanos: mint.created_at_time.map(|time| time.to_string()),
            ..TransactionSummary::default()
        };
    }
    if let Some(burn) = transaction.authorized_burn.as_ref() {
        return TransactionSummary {
            amount_base_units: Some(nat_text(&burn.amount)),
            from: Some(account_row(&burn.from)),
            created_at_time_unix_nanos: burn.created_at_time.map(|time| time.to_string()),
            ..TransactionSummary::default()
        };
    }
    TransactionSummary::default()
}

fn account_row(account: &LedgerAccount) -> IcrcAccountRow {
    IcrcAccountRow::Structured {
        owner: account.owner.to_text(),
        subaccount_hex: account
            .subaccount
            .as_ref()
            .map(<[u8; 32]>::as_slice)
            .map(hex_bytes),
    }
}

json_record!(IcrcIndexTransaction, transaction, {
    "kind" => transaction.kind,
    "timestamp_unix_nanos" => Decimal(transaction.timestamp),
    "burn" => transaction.burn,
    "mint" => transaction.mint,
    "approve" => transaction.approve,
    "fee_collector" => transaction.fee_collector,
    "authorized_mint" => transaction.authorized_mint,
    "authorized_burn" => transaction.authorized_burn,
    "transfer" => transaction.transfer,
});

json_record!(IcrcIndexApprove, approve, {
    "fee_base_units" => approve.fee.as_ref().map(|value| Decimal(&value.0)),
    "from" => Account(&approve.from),
    "memo_hex" => approve.memo.as_deref().map(Hex),
    "created_at_time_unix_nanos" => approve.created_at_time.map(Decimal),
    "amount_base_units" => Decimal(&approve.amount.0),
    "expected_allowance_base_units" => approve.expected_allowance.as_ref().map(|value| Decimal(&value.0)),
    "expires_at_unix_nanos" => approve.expires_at.map(Decimal),
    "spender" => Account(&approve.spender),
});

json_record!(IcrcIndexBurn, burn, {
    "from" => Account(&burn.from),
    "memo_hex" => burn.memo.as_deref().map(Hex),
    "created_at_time_unix_nanos" => burn.created_at_time.map(Decimal),
    "amount_base_units" => Decimal(&burn.amount.0),
    "spender" => burn.spender.as_ref().map(Account),
    "fee_base_units" => burn.fee.as_ref().map(|value| Decimal(&value.0)),
});

json_record!(IcrcIndexMint, mint, {
    "to" => Account(&mint.to),
    "memo_hex" => mint.memo.as_deref().map(Hex),
    "created_at_time_unix_nanos" => mint.created_at_time.map(Decimal),
    "amount_base_units" => Decimal(&mint.amount.0),
    "fee_base_units" => mint.fee.as_ref().map(|value| Decimal(&value.0)),
});

json_record!(IcrcIndexTransfer, transfer, {
    "to" => Account(&transfer.to),
    "fee_base_units" => transfer.fee.as_ref().map(|value| Decimal(&value.0)),
    "from" => Account(&transfer.from),
    "memo_hex" => transfer.memo.as_deref().map(Hex),
    "created_at_time_unix_nanos" => transfer.created_at_time.map(Decimal),
    "amount_base_units" => Decimal(&transfer.amount.0),
    "spender" => transfer.spender.as_ref().map(Account),
});

json_record!(IcrcIndexFeeCollector, collector, {
    "caller" => collector.caller.as_ref().map(Principal::to_text),
    "fee_collector" => collector.fee_collector.as_ref().map(Account),
    "timestamp_unix_nanos" => collector.ts.map(Decimal),
    "method" => collector.mthd,
});

json_record!(IcrcIndexAuthorizedMint, mint, {
    "to" => Account(&mint.to),
    "amount_base_units" => Decimal(&mint.amount.0),
    "created_at_time_unix_nanos" => mint.created_at_time.map(Decimal),
    "caller" => mint.caller.as_ref().map(Principal::to_text),
    "method" => mint.mthd,
    "reason" => mint.reason,
});

json_record!(IcrcIndexAuthorizedBurn, burn, {
    "from" => Account(&burn.from),
    "amount_base_units" => Decimal(&burn.amount.0),
    "created_at_time_unix_nanos" => burn.created_at_time.map(Decimal),
    "caller" => burn.caller.as_ref().map(Principal::to_text),
    "method" => burn.mthd,
    "reason" => burn.reason,
});

#[cfg(test)]
mod tests;