canic-core 0.86.5

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
//! Module: storage::stable::icp_refill
//!
//! Responsibility: define stable-memory schemas for ICP refill records.
//! Does not own: workflow decisions, ledger/CMC calls, or DTO projection.
//! Boundary: storage ops wrap these records for ICP refill workflows.

use crate::cdk::structures::btreemap::BTreeMap as StableBtreeMap;
use crate::{
    cdk::candid::Nat,
    cdk::structures::{DefaultMemoryImpl, memory::VirtualMemory},
    eager_static, impl_storable_bounded,
    role_contract::allocation::memory::observability::ICP_REFILL_RECORDS_ID,
    storage::prelude::*,
};
use std::cell::RefCell;

eager_static! {
    //
    // ICP_REFILL_RECORDS
    //
    static ICP_REFILL_RECORDS: RefCell<IcpRefillRecords> =
        RefCell::new(IcpRefillRecords::new(StableBtreeMap::init(
            crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.icp_refill_records.v1", ty = IcpRefillRecords, id = ICP_REFILL_RECORDS_ID),
        )));
}

///
/// IcpRefillRecordKey
///
/// Stable key for ICP refill records.
/// Owned by stable storage and generated by ICP refill storage ops.
///

#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct IcpRefillRecordKey(pub u64);

impl_storable_bounded!(IcpRefillRecordKey, 16, false);

///
/// IcpRefillRecordStatus
///
/// Stable status for one ICP refill operation record.
/// Owned by stable storage and converted to the boundary DTO enum by storage ops.
///

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[remain::sorted]
pub enum IcpRefillRecordStatus {
    Completed,
    Failed,
    InvalidTransaction,
    NotifyProcessing,
    Refunded,
    Requested,
    TransactionTooOld,
    Transferred,
}

///
/// IcpRefillRecordErrorCode
///
/// Stable error code for one ICP refill operation record.
/// Owned by stable storage and converted to the boundary DTO enum by storage ops.
///

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[remain::sorted]
pub enum IcpRefillRecordErrorCode {
    BadFee,
    Duplicate,
    FabricationUnavailable,
    InvalidLedgerBlockIndex,
    InvalidTransaction,
    LedgerTransferFailed,
    NotifyFailed,
    NotifyMaxAttempts,
    Processing,
    RateGateDenied,
    Refunded,
    RequestDenied,
    TransactionTooOld,
    TransferWindowStale,
}

///
/// IcpRefillRecord
///
/// Stable record for one ICP refill operation.
/// Owned by stable storage and projected through ICP refill storage ops.
///

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct IcpRefillRecord {
    pub id: u64,
    pub operation_id: [u8; 32],
    pub source_canister: Principal,
    pub source_subaccount: Option<[u8; 32]>,
    pub target_canister: Principal,
    pub ledger_canister_id: Principal,
    pub cmc_canister_id: Principal,
    pub cmc_to_account_owner: Principal,
    pub cmc_to_account_subaccount: Option<[u8; 32]>,
    pub amount_e8s: u64,
    pub fee_e8s: u64,
    pub memo: Vec<u8>,
    pub created_at_time_ns: u64,
    pub ledger_block_index: Option<u64>,
    pub notify_attempts: u32,
    pub cycles_sent: Option<Nat>,
    pub status: IcpRefillRecordStatus,
    pub error_code: Option<IcpRefillRecordErrorCode>,
    pub error_message: Option<String>,
    pub refund_block_index: Option<u64>,
    pub transaction_too_old_min_block_index: Option<u64>,
    pub created_at_ns: u64,
    pub updated_at_ns: u64,
}

impl IcpRefillRecord {
    pub const STATE_CONTRACT_NAME: &'static str = "IcpRefillRecord";
    pub const STORABLE_MAX_SIZE: u32 = 4096;
}

impl_storable_bounded!(IcpRefillRecord, IcpRefillRecord::STORABLE_MAX_SIZE, false);

///
/// IcpRefillEntryRecord
///
/// One logical ICP-refill snapshot row preserving its stable record key.
///

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IcpRefillEntryRecord {
    pub key: IcpRefillRecordKey,
    pub record: IcpRefillRecord,
}

///
/// IcpRefillRecordsData
///
/// Canonical ICP-refill-record allocation snapshot.
///

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct IcpRefillRecordsData {
    pub entries: Vec<IcpRefillEntryRecord>,
}

impl IcpRefillRecordsData {
    pub const STATE_CONTRACT_NAME: &'static str = "IcpRefillRecordsData";
}

///
/// IcpRefillRecords
///
/// Stable BTreeMap facade for ICP refill operation records.
/// Owned by stable storage and wrapped by ICP refill storage ops.
///

pub struct IcpRefillRecords {
    map: StableBtreeMap<IcpRefillRecordKey, IcpRefillRecord, VirtualMemory<DefaultMemoryImpl>>,
}

impl IcpRefillRecords {
    pub const fn new(
        map: StableBtreeMap<IcpRefillRecordKey, IcpRefillRecord, VirtualMemory<DefaultMemoryImpl>>,
    ) -> Self {
        Self { map }
    }

    pub(crate) fn insert(record: IcpRefillRecord) -> Option<IcpRefillRecord> {
        ICP_REFILL_RECORDS
            .with_borrow_mut(|records| records.map.insert(IcpRefillRecordKey(record.id), record))
    }

    #[must_use]
    pub(crate) fn get(id: u64) -> Option<IcpRefillRecord> {
        ICP_REFILL_RECORDS.with_borrow(|records| records.map.get(&IcpRefillRecordKey(id)))
    }

    #[must_use]
    pub(crate) fn data(offset: usize, limit: usize) -> IcpRefillRecordsData {
        IcpRefillRecordsData {
            entries: ICP_REFILL_RECORDS.with_borrow(|records| {
                records
                    .map
                    .iter()
                    .skip(offset)
                    .take(limit)
                    .map(|entry| IcpRefillEntryRecord {
                        key: *entry.key(),
                        record: entry.value(),
                    })
                    .collect()
            }),
        }
    }

    #[cfg(test)]
    pub(crate) fn import(data: IcpRefillRecordsData) {
        ICP_REFILL_RECORDS.with_borrow_mut(|records| {
            records.map.clear_new();
            for entry in data.entries {
                records.map.insert(entry.key, entry.record);
            }
        });
    }

    #[cfg(test)]
    pub(crate) fn clear_for_tests() {
        ICP_REFILL_RECORDS.with_borrow_mut(|records| records.map.clear_new());
    }
}

// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test::seams;

    #[test]
    fn icp_refill_records_round_trip_through_canonical_data_snapshot() {
        let _guard = seams::lock();
        IcpRefillRecords::clear_for_tests();
        let record = IcpRefillRecord {
            id: 1,
            operation_id: [2; 32],
            source_canister: seams::p(3),
            source_subaccount: Some([4; 32]),
            target_canister: seams::p(5),
            ledger_canister_id: seams::p(6),
            cmc_canister_id: seams::p(7),
            cmc_to_account_owner: seams::p(8),
            cmc_to_account_subaccount: Some([9; 32]),
            amount_e8s: 10,
            fee_e8s: 11,
            memo: vec![12],
            created_at_time_ns: 13,
            ledger_block_index: Some(14),
            notify_attempts: 15,
            cycles_sent: Some(Nat::from(16_u64)),
            status: IcpRefillRecordStatus::Completed,
            error_code: None,
            error_message: None,
            refund_block_index: None,
            transaction_too_old_min_block_index: None,
            created_at_ns: 17,
            updated_at_ns: 18,
        };
        IcpRefillRecords::insert(record);

        let data = IcpRefillRecords::data(0, usize::MAX);
        IcpRefillRecords::clear_for_tests();
        IcpRefillRecords::import(data.clone());
        assert_eq!(IcpRefillRecords::data(0, usize::MAX), data);
        IcpRefillRecords::clear_for_tests();
    }
}