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! {
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),
)));
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct IcpRefillRecordKey(pub u64);
impl_storable_bounded!(IcpRefillRecordKey, 16, false);
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[remain::sorted]
pub enum IcpRefillRecordStatus {
Completed,
Failed,
InvalidTransaction,
NotifyProcessing,
Refunded,
Requested,
TransactionTooOld,
Transferred,
}
#[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,
}
#[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);
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IcpRefillEntryRecord {
pub key: IcpRefillRecordKey,
pub record: IcpRefillRecord,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct IcpRefillRecordsData {
pub entries: Vec<IcpRefillEntryRecord>,
}
impl IcpRefillRecordsData {
pub const STATE_CONTRACT_NAME: &'static str = "IcpRefillRecordsData";
}
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());
}
}
#[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();
}
}