1use crate::cdk::structures::btreemap::BTreeMap as StableBtreeMap;
8use crate::{
9 cdk::structures::{
10 DefaultMemoryImpl, Storable, cell::Cell, memory::VirtualMemory, storable::Bound,
11 },
12 ids::{IntentId, IntentResourceKey},
13 role_contract::allocation::memory::intent::{
14 INTENT_META_ID, INTENT_PENDING_ID, INTENT_RECORDS_ID, INTENT_TOTALS_ID,
15 },
16 storage::prelude::*,
17};
18use std::{borrow::Cow, cell::RefCell};
19
20pub const INTENT_STORE_SCHEMA_VERSION: u32 = 1;
25
26eager_static! {
27 static INTENT_META: RefCell<Cell<IntentStoreMetaRecord, VirtualMemory<DefaultMemoryImpl>>> =
28 RefCell::new(Cell::init(
29 crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_meta.v1", ty = IntentStoreMetaRecord, id = INTENT_META_ID),
30 IntentStoreMetaRecord::default(),
31 ));
32}
33
34eager_static! {
35 static INTENT_RECORDS: RefCell<
36 StableBtreeMap<IntentId, IntentRecord, VirtualMemory<DefaultMemoryImpl>>
37 > = RefCell::new(
38 StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_records.v1", ty = IntentRecord, id = INTENT_RECORDS_ID)),
39 );
40}
41
42eager_static! {
43 static INTENT_TOTALS: RefCell<
44 StableBtreeMap<IntentResourceKey, IntentResourceTotalsRecord, VirtualMemory<DefaultMemoryImpl>>
45 > = RefCell::new(
46 StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_totals.v1", ty = IntentResourceTotalsRecord, id = INTENT_TOTALS_ID)),
47 );
48}
49
50eager_static! {
51 static INTENT_PENDING: RefCell<
52 StableBtreeMap<IntentId, IntentPendingEntryRecord, VirtualMemory<DefaultMemoryImpl>>
53 > = RefCell::new(
54 StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_pending.v1", ty = IntentPendingEntryRecord, id = INTENT_PENDING_ID)),
55 );
56}
57
58impl Storable for IntentId {
59 const BOUND: Bound = Bound::Bounded {
60 max_size: 8,
61 is_fixed_size: true,
62 };
63
64 fn to_bytes(&self) -> Cow<'_, [u8]> {
65 Cow::Owned(self.0.to_be_bytes().to_vec())
66 }
67
68 fn into_bytes(self) -> Vec<u8> {
69 self.0.to_be_bytes().to_vec()
70 }
71
72 fn from_bytes(bytes: Cow<[u8]>) -> Self {
73 let b = bytes.as_ref();
74
75 if b.len() != 8 {
76 return Self::default();
77 }
78
79 let mut arr = [0u8; 8];
80 arr.copy_from_slice(b);
81
82 Self(u64::from_be_bytes(arr))
83 }
84}
85
86#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
91pub enum IntentState {
92 Pending,
93 Committed,
94 Aborted,
95}
96
97#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
102pub struct IntentRecord {
103 pub id: IntentId,
104 pub resource_key: IntentResourceKey,
105 pub quantity: u64,
106 pub state: IntentState,
107 pub created_at: u64,
108 pub ttl_secs: Option<u64>,
110}
111
112impl IntentRecord {
113 pub const STATE_CONTRACT_NAME: &'static str = "IntentRecord";
114 pub const STORABLE_MAX_SIZE: u32 = 256;
115}
116
117impl_storable_bounded!(IntentRecord, IntentRecord::STORABLE_MAX_SIZE, false);
118
119#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
124pub struct IntentStoreMetaRecord {
125 pub schema_version: u32,
126 pub next_intent_id: IntentId,
127 pub pending_total: u64,
128 pub committed_total: u64,
129 pub aborted_total: u64,
130}
131
132impl IntentStoreMetaRecord {
133 pub const STATE_CONTRACT_NAME: &'static str = "IntentStoreMetaRecord";
134 pub const STORABLE_MAX_SIZE: u32 = 96;
135}
136
137impl Default for IntentStoreMetaRecord {
138 fn default() -> Self {
139 Self {
140 schema_version: INTENT_STORE_SCHEMA_VERSION,
141 next_intent_id: IntentId(1),
142 pending_total: 0,
143 committed_total: 0,
144 aborted_total: 0,
145 }
146 }
147}
148
149impl_storable_bounded!(
150 IntentStoreMetaRecord,
151 IntentStoreMetaRecord::STORABLE_MAX_SIZE,
152 false
153);
154
155#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
160pub struct IntentResourceTotalsRecord {
161 pub reserved_qty: u64,
162 pub committed_qty: u64,
163 pub pending_count: u64,
164}
165
166impl IntentResourceTotalsRecord {
167 pub const STATE_CONTRACT_NAME: &'static str = "IntentResourceTotalsRecord";
168 pub const STORABLE_MAX_SIZE: u32 = 64;
169}
170
171impl_storable_bounded!(
172 IntentResourceTotalsRecord,
173 IntentResourceTotalsRecord::STORABLE_MAX_SIZE,
174 false
175);
176
177#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
182pub struct IntentPendingEntryRecord {
183 pub resource_key: IntentResourceKey,
184 pub quantity: u64,
185 pub created_at: u64,
186 pub ttl_secs: Option<u64>,
188}
189
190impl IntentPendingEntryRecord {
191 pub const STATE_CONTRACT_NAME: &'static str = "IntentPendingEntryRecord";
192 pub const STORABLE_MAX_SIZE: u32 = 224;
193}
194
195impl_storable_bounded!(
196 IntentPendingEntryRecord,
197 IntentPendingEntryRecord::STORABLE_MAX_SIZE,
198 false
199);
200
201#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
208pub struct IntentMetaData {
209 pub record: IntentStoreMetaRecord,
210}
211
212impl IntentMetaData {
213 pub const STATE_CONTRACT_NAME: &'static str = "IntentMetaData";
214}
215
216#[derive(Clone, Debug, Eq, PartialEq)]
223pub struct IntentRecordEntryRecord {
224 pub intent_id: IntentId,
225 pub record: IntentRecord,
226}
227
228#[derive(Clone, Debug, Default, Eq, PartialEq)]
235pub struct IntentRecordsData {
236 pub entries: Vec<IntentRecordEntryRecord>,
237}
238
239impl IntentRecordsData {
240 pub const STATE_CONTRACT_NAME: &'static str = "IntentRecordsData";
241}
242
243#[derive(Clone, Debug, Eq, PartialEq)]
250pub struct IntentTotalsEntryRecord {
251 pub resource_key: IntentResourceKey,
252 pub record: IntentResourceTotalsRecord,
253}
254
255#[derive(Clone, Debug, Default, Eq, PartialEq)]
262pub struct IntentTotalsData {
263 pub entries: Vec<IntentTotalsEntryRecord>,
264}
265
266impl IntentTotalsData {
267 pub const STATE_CONTRACT_NAME: &'static str = "IntentTotalsData";
268}
269
270#[derive(Clone, Debug, Eq, PartialEq)]
277pub struct IntentPendingIndexEntryRecord {
278 pub intent_id: IntentId,
279 pub record: IntentPendingEntryRecord,
280}
281
282#[derive(Clone, Debug, Default, Eq, PartialEq)]
289pub struct IntentPendingData {
290 pub entries: Vec<IntentPendingIndexEntryRecord>,
291}
292
293impl IntentPendingData {
294 pub const STATE_CONTRACT_NAME: &'static str = "IntentPendingData";
295}
296
297pub struct IntentStore;
302
303impl IntentStore {
304 #[must_use]
309 pub(crate) fn meta() -> IntentStoreMetaRecord {
310 INTENT_META.with_borrow(|cell| *cell.get())
311 }
312
313 pub(crate) fn set_meta(meta: IntentStoreMetaRecord) {
314 INTENT_META.with_borrow_mut(|cell| cell.set(meta));
315 }
316
317 #[must_use]
322 pub(crate) fn get_record(id: IntentId) -> Option<IntentRecord> {
323 INTENT_RECORDS.with_borrow(|map| map.get(&id))
324 }
325
326 pub(crate) fn insert_record(record: IntentRecord) -> Option<IntentRecord> {
327 INTENT_RECORDS.with_borrow_mut(|map| map.insert(record.id, record))
328 }
329
330 #[must_use]
335 pub(crate) fn get_totals(key: &IntentResourceKey) -> Option<IntentResourceTotalsRecord> {
336 INTENT_TOTALS.with_borrow(|map| map.get(key))
337 }
338
339 pub(crate) fn set_totals(
340 key: IntentResourceKey,
341 totals: IntentResourceTotalsRecord,
342 ) -> Option<IntentResourceTotalsRecord> {
343 INTENT_TOTALS.with_borrow_mut(|map| map.insert(key, totals))
344 }
345
346 #[must_use]
351 pub(crate) fn get_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
352 INTENT_PENDING.with_borrow(|map| map.get(&id))
353 }
354
355 pub(crate) fn insert_pending(
356 id: IntentId,
357 entry: IntentPendingEntryRecord,
358 ) -> Option<IntentPendingEntryRecord> {
359 INTENT_PENDING.with_borrow_mut(|map| map.insert(id, entry))
360 }
361
362 pub(crate) fn remove_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
363 INTENT_PENDING.with_borrow_mut(|map| map.remove(&id))
364 }
365
366 pub(crate) fn with_pending_entries<R>(
367 f: impl FnOnce(
368 &StableBtreeMap<IntentId, IntentPendingEntryRecord, VirtualMemory<DefaultMemoryImpl>>,
369 ) -> R,
370 ) -> R {
371 INTENT_PENDING.with_borrow(|map| f(map))
372 }
373}
374
375#[cfg(test)]
382impl IntentStore {
383 #[must_use]
384 pub(crate) fn export_meta() -> IntentMetaData {
385 IntentMetaData {
386 record: Self::meta(),
387 }
388 }
389
390 pub(crate) fn import_meta(data: IntentMetaData) {
391 Self::set_meta(data.record);
392 }
393
394 #[must_use]
395 pub(crate) fn export_records() -> IntentRecordsData {
396 IntentRecordsData {
397 entries: INTENT_RECORDS.with_borrow(|map| {
398 map.iter()
399 .map(|entry| IntentRecordEntryRecord {
400 intent_id: *entry.key(),
401 record: entry.value(),
402 })
403 .collect()
404 }),
405 }
406 }
407
408 pub(crate) fn import_records(data: IntentRecordsData) {
409 INTENT_RECORDS.with_borrow_mut(|map| {
410 map.clear_new();
411 for entry in data.entries {
412 map.insert(entry.intent_id, entry.record);
413 }
414 });
415 }
416
417 #[must_use]
418 pub(crate) fn export_totals() -> IntentTotalsData {
419 IntentTotalsData {
420 entries: INTENT_TOTALS.with_borrow(|map| {
421 map.iter()
422 .map(|entry| IntentTotalsEntryRecord {
423 resource_key: entry.key().clone(),
424 record: entry.value(),
425 })
426 .collect()
427 }),
428 }
429 }
430
431 pub(crate) fn import_totals(data: IntentTotalsData) {
432 INTENT_TOTALS.with_borrow_mut(|map| {
433 map.clear_new();
434 for entry in data.entries {
435 map.insert(entry.resource_key, entry.record);
436 }
437 });
438 }
439
440 #[must_use]
441 pub(crate) fn export_pending() -> IntentPendingData {
442 IntentPendingData {
443 entries: INTENT_PENDING.with_borrow(|map| {
444 map.iter()
445 .map(|entry| IntentPendingIndexEntryRecord {
446 intent_id: *entry.key(),
447 record: entry.value(),
448 })
449 .collect()
450 }),
451 }
452 }
453
454 pub(crate) fn import_pending(data: IntentPendingData) {
455 INTENT_PENDING.with_borrow_mut(|map| {
456 map.clear_new();
457 for entry in data.entries {
458 map.insert(entry.intent_id, entry.record);
459 }
460 });
461 }
462
463 pub(crate) fn reset_for_tests() {
464 INTENT_RECORDS.with_borrow_mut(StableBtreeMap::clear_new);
465 INTENT_TOTALS.with_borrow_mut(StableBtreeMap::clear_new);
466 INTENT_PENDING.with_borrow_mut(StableBtreeMap::clear_new);
467 INTENT_META.with_borrow_mut(|cell| cell.set(IntentStoreMetaRecord::default()));
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474
475 #[test]
476 fn intent_allocations_round_trip_through_canonical_data_snapshots() {
477 IntentStore::reset_for_tests();
478 let intent_id = IntentId(7);
479 let resource_key = IntentResourceKey::new("storage:uploads");
480 let record = IntentRecord {
481 id: intent_id,
482 resource_key: resource_key.clone(),
483 quantity: 11,
484 state: IntentState::Pending,
485 created_at: 13,
486 ttl_secs: Some(17),
487 };
488 let totals = IntentResourceTotalsRecord {
489 reserved_qty: 11,
490 committed_qty: 19,
491 pending_count: 1,
492 };
493 let pending = IntentPendingEntryRecord {
494 resource_key: resource_key.clone(),
495 quantity: 11,
496 created_at: 13,
497 ttl_secs: Some(17),
498 };
499 let meta = IntentStoreMetaRecord {
500 schema_version: INTENT_STORE_SCHEMA_VERSION,
501 next_intent_id: IntentId(8),
502 pending_total: 1,
503 committed_total: 2,
504 aborted_total: 3,
505 };
506
507 IntentStore::set_meta(meta);
508 IntentStore::insert_record(record);
509 IntentStore::set_totals(resource_key, totals);
510 IntentStore::insert_pending(intent_id, pending);
511
512 let meta_data = IntentStore::export_meta();
513 let records_data = IntentStore::export_records();
514 let totals_data = IntentStore::export_totals();
515 let pending_data = IntentStore::export_pending();
516
517 IntentStore::reset_for_tests();
518 IntentStore::import_meta(meta_data);
519 IntentStore::import_records(records_data.clone());
520 IntentStore::import_totals(totals_data.clone());
521 IntentStore::import_pending(pending_data.clone());
522
523 assert_eq!(IntentStore::export_meta(), meta_data);
524 assert_eq!(IntentStore::export_records(), records_data);
525 assert_eq!(IntentStore::export_totals(), totals_data);
526 assert_eq!(IntentStore::export_pending(), pending_data);
527 IntentStore::reset_for_tests();
528 }
529}