use crate::cdk::structures::btreemap::BTreeMap as StableBtreeMap;
use crate::{
cdk::structures::{
DefaultMemoryImpl, Storable, cell::Cell, memory::VirtualMemory, storable::Bound,
},
ids::{IntentId, IntentResourceKey},
role_contract::allocation::memory::intent::{
INTENT_META_ID, INTENT_PENDING_ID, INTENT_RECORDS_ID, INTENT_TOTALS_ID,
},
storage::prelude::*,
};
use std::{borrow::Cow, cell::RefCell};
pub const INTENT_STORE_SCHEMA_VERSION: u32 = 1;
eager_static! {
static INTENT_META: RefCell<Cell<IntentStoreMetaRecord, VirtualMemory<DefaultMemoryImpl>>> =
RefCell::new(Cell::init(
crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_meta.v1", ty = IntentStoreMetaRecord, id = INTENT_META_ID),
IntentStoreMetaRecord::default(),
));
}
eager_static! {
static INTENT_RECORDS: RefCell<
StableBtreeMap<IntentId, IntentRecord, VirtualMemory<DefaultMemoryImpl>>
> = RefCell::new(
StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_records.v1", ty = IntentRecord, id = INTENT_RECORDS_ID)),
);
}
eager_static! {
static INTENT_TOTALS: RefCell<
StableBtreeMap<IntentResourceKey, IntentResourceTotalsRecord, VirtualMemory<DefaultMemoryImpl>>
> = RefCell::new(
StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_totals.v1", ty = IntentResourceTotalsRecord, id = INTENT_TOTALS_ID)),
);
}
eager_static! {
static INTENT_PENDING: RefCell<
StableBtreeMap<IntentId, IntentPendingEntryRecord, VirtualMemory<DefaultMemoryImpl>>
> = RefCell::new(
StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_pending.v1", ty = IntentPendingEntryRecord, id = INTENT_PENDING_ID)),
);
}
impl Storable for IntentId {
const BOUND: Bound = Bound::Bounded {
max_size: 8,
is_fixed_size: true,
};
fn to_bytes(&self) -> Cow<'_, [u8]> {
Cow::Owned(self.0.to_be_bytes().to_vec())
}
fn into_bytes(self) -> Vec<u8> {
self.0.to_be_bytes().to_vec()
}
fn from_bytes(bytes: Cow<[u8]>) -> Self {
let b = bytes.as_ref();
if b.len() != 8 {
return Self::default();
}
let mut arr = [0u8; 8];
arr.copy_from_slice(b);
Self(u64::from_be_bytes(arr))
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum IntentState {
Pending,
Committed,
Aborted,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct IntentRecord {
pub id: IntentId,
pub resource_key: IntentResourceKey,
pub quantity: u64,
pub state: IntentState,
pub created_at: u64,
pub ttl_secs: Option<u64>,
}
impl IntentRecord {
pub const STATE_CONTRACT_NAME: &'static str = "IntentRecord";
pub const STORABLE_MAX_SIZE: u32 = 256;
}
impl_storable_bounded!(IntentRecord, IntentRecord::STORABLE_MAX_SIZE, false);
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct IntentStoreMetaRecord {
pub schema_version: u32,
pub next_intent_id: IntentId,
pub pending_total: u64,
pub committed_total: u64,
pub aborted_total: u64,
}
impl IntentStoreMetaRecord {
pub const STATE_CONTRACT_NAME: &'static str = "IntentStoreMetaRecord";
pub const STORABLE_MAX_SIZE: u32 = 96;
}
impl Default for IntentStoreMetaRecord {
fn default() -> Self {
Self {
schema_version: INTENT_STORE_SCHEMA_VERSION,
next_intent_id: IntentId(1),
pending_total: 0,
committed_total: 0,
aborted_total: 0,
}
}
}
impl_storable_bounded!(
IntentStoreMetaRecord,
IntentStoreMetaRecord::STORABLE_MAX_SIZE,
false
);
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct IntentResourceTotalsRecord {
pub reserved_qty: u64,
pub committed_qty: u64,
pub pending_count: u64,
}
impl IntentResourceTotalsRecord {
pub const STATE_CONTRACT_NAME: &'static str = "IntentResourceTotalsRecord";
pub const STORABLE_MAX_SIZE: u32 = 64;
}
impl_storable_bounded!(
IntentResourceTotalsRecord,
IntentResourceTotalsRecord::STORABLE_MAX_SIZE,
false
);
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct IntentPendingEntryRecord {
pub resource_key: IntentResourceKey,
pub quantity: u64,
pub created_at: u64,
pub ttl_secs: Option<u64>,
}
impl IntentPendingEntryRecord {
pub const STATE_CONTRACT_NAME: &'static str = "IntentPendingEntryRecord";
pub const STORABLE_MAX_SIZE: u32 = 224;
}
impl_storable_bounded!(
IntentPendingEntryRecord,
IntentPendingEntryRecord::STORABLE_MAX_SIZE,
false
);
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct IntentMetaData {
pub record: IntentStoreMetaRecord,
}
impl IntentMetaData {
pub const STATE_CONTRACT_NAME: &'static str = "IntentMetaData";
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IntentRecordEntryRecord {
pub intent_id: IntentId,
pub record: IntentRecord,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct IntentRecordsData {
pub entries: Vec<IntentRecordEntryRecord>,
}
impl IntentRecordsData {
pub const STATE_CONTRACT_NAME: &'static str = "IntentRecordsData";
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IntentTotalsEntryRecord {
pub resource_key: IntentResourceKey,
pub record: IntentResourceTotalsRecord,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct IntentTotalsData {
pub entries: Vec<IntentTotalsEntryRecord>,
}
impl IntentTotalsData {
pub const STATE_CONTRACT_NAME: &'static str = "IntentTotalsData";
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IntentPendingIndexEntryRecord {
pub intent_id: IntentId,
pub record: IntentPendingEntryRecord,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct IntentPendingData {
pub entries: Vec<IntentPendingIndexEntryRecord>,
}
impl IntentPendingData {
pub const STATE_CONTRACT_NAME: &'static str = "IntentPendingData";
}
pub struct IntentStore;
impl IntentStore {
#[must_use]
pub(crate) fn meta() -> IntentStoreMetaRecord {
INTENT_META.with_borrow(|cell| *cell.get())
}
pub(crate) fn set_meta(meta: IntentStoreMetaRecord) {
INTENT_META.with_borrow_mut(|cell| cell.set(meta));
}
#[must_use]
pub(crate) fn get_record(id: IntentId) -> Option<IntentRecord> {
INTENT_RECORDS.with_borrow(|map| map.get(&id))
}
pub(crate) fn insert_record(record: IntentRecord) -> Option<IntentRecord> {
INTENT_RECORDS.with_borrow_mut(|map| map.insert(record.id, record))
}
#[must_use]
pub(crate) fn get_totals(key: &IntentResourceKey) -> Option<IntentResourceTotalsRecord> {
INTENT_TOTALS.with_borrow(|map| map.get(key))
}
pub(crate) fn set_totals(
key: IntentResourceKey,
totals: IntentResourceTotalsRecord,
) -> Option<IntentResourceTotalsRecord> {
INTENT_TOTALS.with_borrow_mut(|map| map.insert(key, totals))
}
#[must_use]
pub(crate) fn get_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
INTENT_PENDING.with_borrow(|map| map.get(&id))
}
pub(crate) fn insert_pending(
id: IntentId,
entry: IntentPendingEntryRecord,
) -> Option<IntentPendingEntryRecord> {
INTENT_PENDING.with_borrow_mut(|map| map.insert(id, entry))
}
pub(crate) fn remove_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
INTENT_PENDING.with_borrow_mut(|map| map.remove(&id))
}
pub(crate) fn with_pending_entries<R>(
f: impl FnOnce(
&StableBtreeMap<IntentId, IntentPendingEntryRecord, VirtualMemory<DefaultMemoryImpl>>,
) -> R,
) -> R {
INTENT_PENDING.with_borrow(|map| f(map))
}
}
#[cfg(test)]
impl IntentStore {
#[must_use]
pub(crate) fn export_meta() -> IntentMetaData {
IntentMetaData {
record: Self::meta(),
}
}
pub(crate) fn import_meta(data: IntentMetaData) {
Self::set_meta(data.record);
}
#[must_use]
pub(crate) fn export_records() -> IntentRecordsData {
IntentRecordsData {
entries: INTENT_RECORDS.with_borrow(|map| {
map.iter()
.map(|entry| IntentRecordEntryRecord {
intent_id: *entry.key(),
record: entry.value(),
})
.collect()
}),
}
}
pub(crate) fn import_records(data: IntentRecordsData) {
INTENT_RECORDS.with_borrow_mut(|map| {
map.clear_new();
for entry in data.entries {
map.insert(entry.intent_id, entry.record);
}
});
}
#[must_use]
pub(crate) fn export_totals() -> IntentTotalsData {
IntentTotalsData {
entries: INTENT_TOTALS.with_borrow(|map| {
map.iter()
.map(|entry| IntentTotalsEntryRecord {
resource_key: entry.key().clone(),
record: entry.value(),
})
.collect()
}),
}
}
pub(crate) fn import_totals(data: IntentTotalsData) {
INTENT_TOTALS.with_borrow_mut(|map| {
map.clear_new();
for entry in data.entries {
map.insert(entry.resource_key, entry.record);
}
});
}
#[must_use]
pub(crate) fn export_pending() -> IntentPendingData {
IntentPendingData {
entries: INTENT_PENDING.with_borrow(|map| {
map.iter()
.map(|entry| IntentPendingIndexEntryRecord {
intent_id: *entry.key(),
record: entry.value(),
})
.collect()
}),
}
}
pub(crate) fn import_pending(data: IntentPendingData) {
INTENT_PENDING.with_borrow_mut(|map| {
map.clear_new();
for entry in data.entries {
map.insert(entry.intent_id, entry.record);
}
});
}
pub(crate) fn reset_for_tests() {
INTENT_RECORDS.with_borrow_mut(StableBtreeMap::clear_new);
INTENT_TOTALS.with_borrow_mut(StableBtreeMap::clear_new);
INTENT_PENDING.with_borrow_mut(StableBtreeMap::clear_new);
INTENT_META.with_borrow_mut(|cell| cell.set(IntentStoreMetaRecord::default()));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn intent_allocations_round_trip_through_canonical_data_snapshots() {
IntentStore::reset_for_tests();
let intent_id = IntentId(7);
let resource_key = IntentResourceKey::new("storage:uploads");
let record = IntentRecord {
id: intent_id,
resource_key: resource_key.clone(),
quantity: 11,
state: IntentState::Pending,
created_at: 13,
ttl_secs: Some(17),
};
let totals = IntentResourceTotalsRecord {
reserved_qty: 11,
committed_qty: 19,
pending_count: 1,
};
let pending = IntentPendingEntryRecord {
resource_key: resource_key.clone(),
quantity: 11,
created_at: 13,
ttl_secs: Some(17),
};
let meta = IntentStoreMetaRecord {
schema_version: INTENT_STORE_SCHEMA_VERSION,
next_intent_id: IntentId(8),
pending_total: 1,
committed_total: 2,
aborted_total: 3,
};
IntentStore::set_meta(meta);
IntentStore::insert_record(record);
IntentStore::set_totals(resource_key, totals);
IntentStore::insert_pending(intent_id, pending);
let meta_data = IntentStore::export_meta();
let records_data = IntentStore::export_records();
let totals_data = IntentStore::export_totals();
let pending_data = IntentStore::export_pending();
IntentStore::reset_for_tests();
IntentStore::import_meta(meta_data);
IntentStore::import_records(records_data.clone());
IntentStore::import_totals(totals_data.clone());
IntentStore::import_pending(pending_data.clone());
assert_eq!(IntentStore::export_meta(), meta_data);
assert_eq!(IntentStore::export_records(), records_data);
assert_eq!(IntentStore::export_totals(), totals_data);
assert_eq!(IntentStore::export_pending(), pending_data);
IntentStore::reset_for_tests();
}
}