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 model::{
14 intent::{PayloadBinding, ReceiptBackedIntent, ReceiptBackedIntentState},
15 replay::OperationId,
16 },
17 role_contract::allocation::memory::intent::{
18 INTENT_EXPIRY_INDEX_ID, INTENT_META_ID, INTENT_PENDING_ID, INTENT_RECORDS_ID,
19 INTENT_TOTALS_ID, PLACEMENT_ACKNOWLEDGEMENT_INDEX_ID, RECEIPT_BACKED_INTENT_RECORDS_ID,
20 },
21 storage::prelude::*,
22};
23use std::{borrow::Cow, cell::RefCell};
24
25pub const INTENT_STORE_SCHEMA_VERSION: u32 = 1;
30
31eager_static! {
32 static INTENT_META: RefCell<Cell<IntentStoreMetaRecord, VirtualMemory<DefaultMemoryImpl>>> =
33 RefCell::new(Cell::init(
34 crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_meta.v1", ty = IntentStoreMetaRecord, id = INTENT_META_ID),
35 IntentStoreMetaRecord::default(),
36 ));
37}
38
39eager_static! {
40 static RECEIPT_BACKED_INTENT_RECORDS: RefCell<
41 StableBtreeMap<
42 OperationId,
43 ReceiptBackedIntentRecord,
44 VirtualMemory<DefaultMemoryImpl>,
45 >
46 > = RefCell::new(StableBtreeMap::init(crate::ic_memory_key!(
47 authority = CANIC_CORE_MEMORY_AUTHORITY,
48 key = "canic.core.receipt_backed_intent_records.v1",
49 ty = ReceiptBackedIntentRecord,
50 id = RECEIPT_BACKED_INTENT_RECORDS_ID
51 )));
52}
53
54eager_static! {
55 static INTENT_EXPIRY_INDEX: RefCell<
56 StableBtreeMap<IntentExpiryKeyRecord, IntentExpiryEntryRecord, VirtualMemory<DefaultMemoryImpl>>
57 > = RefCell::new(
58 StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_expiry_index.v1", ty = IntentExpiryEntryRecord, id = INTENT_EXPIRY_INDEX_ID)),
59 );
60}
61
62eager_static! {
63 static PLACEMENT_ACKNOWLEDGEMENT_INDEX: RefCell<
64 StableBtreeMap<
65 OperationId,
66 PlacementAcknowledgementEntryRecord,
67 VirtualMemory<DefaultMemoryImpl>,
68 >
69 > = RefCell::new(StableBtreeMap::init(crate::ic_memory_key!(
70 authority = CANIC_CORE_MEMORY_AUTHORITY,
71 key = "canic.core.placement_acknowledgement_index.v1",
72 ty = PlacementAcknowledgementEntryRecord,
73 id = PLACEMENT_ACKNOWLEDGEMENT_INDEX_ID
74 )));
75}
76
77eager_static! {
78 static INTENT_RECORDS: RefCell<
79 StableBtreeMap<IntentId, IntentRecord, VirtualMemory<DefaultMemoryImpl>>
80 > = RefCell::new(
81 StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_records.v1", ty = IntentRecord, id = INTENT_RECORDS_ID)),
82 );
83}
84
85eager_static! {
86 static INTENT_TOTALS: RefCell<
87 StableBtreeMap<IntentResourceKey, IntentResourceTotalsRecord, VirtualMemory<DefaultMemoryImpl>>
88 > = RefCell::new(
89 StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_totals.v1", ty = IntentResourceTotalsRecord, id = INTENT_TOTALS_ID)),
90 );
91}
92
93eager_static! {
94 static INTENT_PENDING: RefCell<
95 StableBtreeMap<IntentId, IntentPendingEntryRecord, VirtualMemory<DefaultMemoryImpl>>
96 > = RefCell::new(
97 StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_pending.v1", ty = IntentPendingEntryRecord, id = INTENT_PENDING_ID)),
98 );
99}
100
101impl Storable for IntentId {
102 const BOUND: Bound = Bound::Bounded {
103 max_size: 8,
104 is_fixed_size: true,
105 };
106
107 fn to_bytes(&self) -> Cow<'_, [u8]> {
108 Cow::Owned(self.0.to_be_bytes().to_vec())
109 }
110
111 fn into_bytes(self) -> Vec<u8> {
112 self.0.to_be_bytes().to_vec()
113 }
114
115 fn from_bytes(bytes: Cow<[u8]>) -> Self {
121 let bytes = <[u8; 8]>::try_from(bytes.as_ref()).unwrap_or_else(|_| {
122 panic!(
123 "stable IntentId is {} bytes; expected 8",
124 bytes.as_ref().len()
125 )
126 });
127
128 Self(u64::from_be_bytes(bytes))
129 }
130}
131
132impl Storable for OperationId {
133 const BOUND: Bound = Bound::Bounded {
134 max_size: 32,
135 is_fixed_size: true,
136 };
137
138 fn to_bytes(&self) -> Cow<'_, [u8]> {
139 Cow::Borrowed(self.as_bytes())
140 }
141
142 fn into_bytes(self) -> Vec<u8> {
143 self.as_bytes().to_vec()
144 }
145
146 fn from_bytes(bytes: Cow<[u8]>) -> Self {
152 let operation_id = <[u8; 32]>::try_from(bytes.as_ref()).unwrap_or_else(|_| {
153 panic!(
154 "stable OperationId is {} bytes; expected 32",
155 bytes.as_ref().len()
156 )
157 });
158 Self::from_bytes(operation_id)
159 }
160}
161
162#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
164pub struct IntentExpiryKeyRecord {
165 pub due_at_secs: u64,
166 pub intent_id: IntentId,
167}
168
169impl Storable for IntentExpiryKeyRecord {
170 const BOUND: Bound = Bound::Bounded {
171 max_size: 16,
172 is_fixed_size: true,
173 };
174
175 fn to_bytes(&self) -> Cow<'_, [u8]> {
176 Cow::Owned(self.into_bytes())
177 }
178
179 fn into_bytes(self) -> Vec<u8> {
180 let mut bytes = Vec::with_capacity(16);
181 bytes.extend_from_slice(&self.due_at_secs.to_be_bytes());
182 bytes.extend_from_slice(&self.intent_id.0.to_be_bytes());
183 bytes
184 }
185
186 fn from_bytes(bytes: Cow<[u8]>) -> Self {
192 let bytes = <[u8; 16]>::try_from(bytes.as_ref()).unwrap_or_else(|_| {
193 panic!(
194 "stable IntentExpiryKeyRecord is {} bytes; expected 16",
195 bytes.as_ref().len()
196 )
197 });
198 let (due_at_secs, intent_id) = bytes.split_at(8);
199 Self {
200 due_at_secs: u64::from_be_bytes(
201 due_at_secs.try_into().expect("expiry key deadline width"),
202 ),
203 intent_id: IntentId(u64::from_be_bytes(
204 intent_id.try_into().expect("expiry key intent width"),
205 )),
206 }
207 }
208}
209
210#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
212pub struct IntentExpiryEntryRecord {
213 pub intent_id: IntentId,
214}
215
216impl IntentExpiryEntryRecord {
217 pub const STATE_CONTRACT_NAME: &'static str = "IntentExpiryEntryRecord";
218 pub const STORABLE_MAX_SIZE: u32 = 32;
219}
220
221impl_storable_bounded!(
222 IntentExpiryEntryRecord,
223 IntentExpiryEntryRecord::STORABLE_MAX_SIZE,
224 false
225);
226
227#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
232pub enum IntentState {
233 Pending,
234 Committed,
235 Aborted,
236}
237
238#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
243pub struct IntentRecord {
244 pub id: IntentId,
245 pub resource_key: IntentResourceKey,
246 pub quantity: u64,
247 pub state: IntentState,
248 pub created_at: u64,
249 pub ttl_secs: Option<u64>,
251}
252
253impl IntentRecord {
254 pub const STATE_CONTRACT_NAME: &'static str = "IntentRecord";
255 pub const STORABLE_MAX_SIZE: u32 = 256;
256}
257
258impl_storable_bounded!(IntentRecord, IntentRecord::STORABLE_MAX_SIZE, false);
259
260#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
265pub struct IntentStoreMetaRecord {
266 pub schema_version: u32,
267 pub next_intent_id: IntentId,
268 pub pending_total: u64,
269 pub committed_total: u64,
270 pub aborted_total: u64,
271}
272
273impl IntentStoreMetaRecord {
274 pub const STATE_CONTRACT_NAME: &'static str = "IntentStoreMetaRecord";
275 pub const STORABLE_MAX_SIZE: u32 = 96;
276}
277
278impl Default for IntentStoreMetaRecord {
279 fn default() -> Self {
280 Self {
281 schema_version: INTENT_STORE_SCHEMA_VERSION,
282 next_intent_id: IntentId(1),
283 pending_total: 0,
284 committed_total: 0,
285 aborted_total: 0,
286 }
287 }
288}
289
290impl_storable_bounded!(
291 IntentStoreMetaRecord,
292 IntentStoreMetaRecord::STORABLE_MAX_SIZE,
293 false
294);
295
296#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
301pub struct IntentResourceTotalsRecord {
302 pub reserved_qty: u64,
303 pub committed_qty: u64,
304 pub pending_count: u64,
305}
306
307impl IntentResourceTotalsRecord {
308 pub const STATE_CONTRACT_NAME: &'static str = "IntentResourceTotalsRecord";
309 pub const STORABLE_MAX_SIZE: u32 = 64;
310}
311
312impl_storable_bounded!(
313 IntentResourceTotalsRecord,
314 IntentResourceTotalsRecord::STORABLE_MAX_SIZE,
315 false
316);
317
318#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
323pub struct IntentPendingEntryRecord {
324 pub resource_key: IntentResourceKey,
325 pub quantity: u64,
326 pub created_at: u64,
327 pub ttl_secs: Option<u64>,
329}
330
331impl IntentPendingEntryRecord {
332 pub const STATE_CONTRACT_NAME: &'static str = "IntentPendingEntryRecord";
333 pub const STORABLE_MAX_SIZE: u32 = 224;
334}
335
336impl_storable_bounded!(
337 IntentPendingEntryRecord,
338 IntentPendingEntryRecord::STORABLE_MAX_SIZE,
339 false
340);
341
342#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
344pub struct ReceiptBackedIntentRecord {
345 pub schema_version: u32,
346 pub operation_id: OperationId,
347 pub payload_binding: PayloadBinding,
348 pub resource_key: IntentResourceKey,
349 pub quantity: u64,
350 pub state: ReceiptBackedIntentState,
351 pub revision: u64,
352 pub created_at_ns: u64,
353 pub updated_at_ns: u64,
354}
355
356impl ReceiptBackedIntentRecord {
357 pub const STATE_CONTRACT_NAME: &'static str = "ReceiptBackedIntentRecord";
358 pub const STORABLE_MAX_SIZE: u32 = 1024;
359
360 #[must_use]
361 pub fn into_intent(self) -> ReceiptBackedIntent {
362 ReceiptBackedIntent {
363 schema_version: self.schema_version,
364 operation_id: self.operation_id,
365 payload_binding: self.payload_binding,
366 resource_key: self.resource_key,
367 quantity: self.quantity,
368 state: self.state,
369 revision: self.revision,
370 created_at_ns: self.created_at_ns,
371 updated_at_ns: self.updated_at_ns,
372 }
373 }
374}
375
376impl_storable_bounded!(
377 ReceiptBackedIntentRecord,
378 ReceiptBackedIntentRecord::STORABLE_MAX_SIZE,
379 false
380);
381
382#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
384pub struct PlacementAcknowledgementEntryRecord {
385 pub operation_id: OperationId,
386}
387
388impl PlacementAcknowledgementEntryRecord {
389 pub const STATE_CONTRACT_NAME: &'static str = "PlacementAcknowledgementEntryRecord";
390 pub const STORABLE_MAX_SIZE: u32 = 128;
391}
392
393impl_storable_bounded!(
394 PlacementAcknowledgementEntryRecord,
395 PlacementAcknowledgementEntryRecord::STORABLE_MAX_SIZE,
396 false
397);
398
399#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
406pub struct IntentMetaData {
407 pub record: IntentStoreMetaRecord,
408}
409
410impl IntentMetaData {
411 pub const STATE_CONTRACT_NAME: &'static str = "IntentMetaData";
412}
413
414#[derive(Clone, Debug, Eq, PartialEq)]
421pub struct IntentRecordEntryRecord {
422 pub intent_id: IntentId,
423 pub record: IntentRecord,
424}
425
426#[derive(Clone, Debug, Default, Eq, PartialEq)]
433pub struct IntentRecordsData {
434 pub entries: Vec<IntentRecordEntryRecord>,
435}
436
437impl IntentRecordsData {
438 pub const STATE_CONTRACT_NAME: &'static str = "IntentRecordsData";
439}
440
441#[derive(Clone, Debug, Eq, PartialEq)]
448pub struct IntentTotalsEntryRecord {
449 pub resource_key: IntentResourceKey,
450 pub record: IntentResourceTotalsRecord,
451}
452
453#[derive(Clone, Debug, Default, Eq, PartialEq)]
460pub struct IntentTotalsData {
461 pub entries: Vec<IntentTotalsEntryRecord>,
462}
463
464impl IntentTotalsData {
465 pub const STATE_CONTRACT_NAME: &'static str = "IntentTotalsData";
466}
467
468#[derive(Clone, Debug, Eq, PartialEq)]
475pub struct IntentPendingIndexEntryRecord {
476 pub intent_id: IntentId,
477 pub record: IntentPendingEntryRecord,
478}
479
480#[derive(Clone, Debug, Default, Eq, PartialEq)]
487pub struct IntentPendingData {
488 pub entries: Vec<IntentPendingIndexEntryRecord>,
489}
490
491impl IntentPendingData {
492 pub const STATE_CONTRACT_NAME: &'static str = "IntentPendingData";
493}
494
495#[derive(Clone, Copy, Debug, Eq, PartialEq)]
497pub struct IntentExpiryIndexEntryRecord {
498 pub key: IntentExpiryKeyRecord,
499 pub record: IntentExpiryEntryRecord,
500}
501
502#[derive(Clone, Debug, Default, Eq, PartialEq)]
504pub struct IntentExpiryIndexData {
505 pub entries: Vec<IntentExpiryIndexEntryRecord>,
506}
507
508impl IntentExpiryIndexData {
509 pub const STATE_CONTRACT_NAME: &'static str = "IntentExpiryIndexData";
510}
511
512#[derive(Clone, Debug, Eq, PartialEq)]
514pub struct ReceiptBackedIntentEntryRecord {
515 pub operation_id: OperationId,
516 pub record: ReceiptBackedIntentRecord,
517}
518
519#[derive(Clone, Debug, Default, Eq, PartialEq)]
521pub struct ReceiptBackedIntentsData {
522 pub entries: Vec<ReceiptBackedIntentEntryRecord>,
523}
524
525impl ReceiptBackedIntentsData {
526 pub const STATE_CONTRACT_NAME: &'static str = "ReceiptBackedIntentsData";
527}
528
529#[derive(Clone, Copy, Debug, Eq, PartialEq)]
531pub struct PlacementAcknowledgementIndexEntryRecord {
532 pub operation_id: OperationId,
533 pub record: PlacementAcknowledgementEntryRecord,
534}
535
536#[derive(Clone, Debug, Default, Eq, PartialEq)]
538pub struct PlacementAcknowledgementIndexData {
539 pub entries: Vec<PlacementAcknowledgementIndexEntryRecord>,
540}
541
542impl PlacementAcknowledgementIndexData {
543 pub const STATE_CONTRACT_NAME: &'static str = "PlacementAcknowledgementIndexData";
544}
545
546pub struct IntentStore;
551
552impl IntentStore {
553 #[must_use]
558 pub(crate) fn meta() -> IntentStoreMetaRecord {
559 INTENT_META.with_borrow(|cell| *cell.get())
560 }
561
562 pub(crate) fn set_meta(meta: IntentStoreMetaRecord) {
563 INTENT_META.with_borrow_mut(|cell| cell.set(meta));
564 }
565
566 #[must_use]
571 pub(crate) fn get_record(id: IntentId) -> Option<IntentRecord> {
572 INTENT_RECORDS.with_borrow(|map| map.get(&id))
573 }
574
575 pub(crate) fn insert_record(record: IntentRecord) -> Option<IntentRecord> {
576 INTENT_RECORDS.with_borrow_mut(|map| map.insert(record.id, record))
577 }
578
579 #[must_use]
584 pub(crate) fn get_totals(key: &IntentResourceKey) -> Option<IntentResourceTotalsRecord> {
585 INTENT_TOTALS.with_borrow(|map| map.get(key))
586 }
587
588 pub(crate) fn set_totals(
589 key: IntentResourceKey,
590 totals: IntentResourceTotalsRecord,
591 ) -> Option<IntentResourceTotalsRecord> {
592 INTENT_TOTALS.with_borrow_mut(|map| map.insert(key, totals))
593 }
594
595 #[must_use]
600 pub(crate) fn get_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
601 INTENT_PENDING.with_borrow(|map| map.get(&id))
602 }
603
604 pub(crate) fn insert_pending(
605 id: IntentId,
606 entry: IntentPendingEntryRecord,
607 ) -> Option<IntentPendingEntryRecord> {
608 INTENT_PENDING.with_borrow_mut(|map| map.insert(id, entry))
609 }
610
611 pub(crate) fn remove_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
612 INTENT_PENDING.with_borrow_mut(|map| map.remove(&id))
613 }
614
615 pub(crate) fn with_pending_entries<R>(
616 f: impl FnOnce(
617 &StableBtreeMap<IntentId, IntentPendingEntryRecord, VirtualMemory<DefaultMemoryImpl>>,
618 ) -> R,
619 ) -> R {
620 INTENT_PENDING.with_borrow(|map| f(map))
621 }
622
623 #[must_use]
628 pub(crate) fn get_expiry(key: IntentExpiryKeyRecord) -> Option<IntentExpiryEntryRecord> {
629 INTENT_EXPIRY_INDEX.with_borrow(|map| map.get(&key))
630 }
631
632 pub(crate) fn insert_expiry(
633 key: IntentExpiryKeyRecord,
634 record: IntentExpiryEntryRecord,
635 ) -> Option<IntentExpiryEntryRecord> {
636 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| map.insert(key, record))
637 }
638
639 pub(crate) fn remove_expiry(key: IntentExpiryKeyRecord) -> Option<IntentExpiryEntryRecord> {
640 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| map.remove(&key))
641 }
642
643 pub(crate) fn clear_expiry_index() {
644 INTENT_EXPIRY_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
645 }
646
647 pub(crate) fn with_expiry_entries<R>(
648 f: impl FnOnce(
649 &StableBtreeMap<
650 IntentExpiryKeyRecord,
651 IntentExpiryEntryRecord,
652 VirtualMemory<DefaultMemoryImpl>,
653 >,
654 ) -> R,
655 ) -> R {
656 INTENT_EXPIRY_INDEX.with_borrow(|map| f(map))
657 }
658}
659
660pub struct ReceiptBackedIntentStore;
662
663impl ReceiptBackedIntentStore {
664 #[must_use]
665 pub(crate) fn len() -> u64 {
666 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(StableBtreeMap::len)
667 }
668
669 #[must_use]
670 pub(crate) fn get(operation_id: OperationId) -> Option<ReceiptBackedIntentRecord> {
671 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| records.get(&operation_id))
672 }
673
674 pub(crate) fn insert(record: ReceiptBackedIntentRecord) -> Option<ReceiptBackedIntentRecord> {
675 RECEIPT_BACKED_INTENT_RECORDS
676 .with_borrow_mut(|records| records.insert(record.operation_id, record))
677 }
678
679 pub(crate) fn remove(operation_id: OperationId) -> Option<ReceiptBackedIntentRecord> {
680 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(|records| records.remove(&operation_id))
681 }
682
683 pub(crate) fn with_records<R>(
684 f: impl FnOnce(
685 &StableBtreeMap<
686 OperationId,
687 ReceiptBackedIntentRecord,
688 VirtualMemory<DefaultMemoryImpl>,
689 >,
690 ) -> R,
691 ) -> R {
692 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| f(records))
693 }
694
695 #[must_use]
696 pub(crate) fn get_placement_acknowledgement(
697 operation_id: OperationId,
698 ) -> Option<PlacementAcknowledgementEntryRecord> {
699 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| index.get(&operation_id))
700 }
701
702 pub(crate) fn insert_placement_acknowledgement(
703 record: PlacementAcknowledgementEntryRecord,
704 ) -> Option<PlacementAcknowledgementEntryRecord> {
705 PLACEMENT_ACKNOWLEDGEMENT_INDEX
706 .with_borrow_mut(|index| index.insert(record.operation_id, record))
707 }
708
709 pub(crate) fn remove_placement_acknowledgement(
710 operation_id: OperationId,
711 ) -> Option<PlacementAcknowledgementEntryRecord> {
712 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(|index| index.remove(&operation_id))
713 }
714
715 pub(crate) fn clear_placement_acknowledgement_index() {
716 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
717 }
718
719 pub(crate) fn with_placement_acknowledgements<R>(
720 f: impl FnOnce(
721 &StableBtreeMap<
722 OperationId,
723 PlacementAcknowledgementEntryRecord,
724 VirtualMemory<DefaultMemoryImpl>,
725 >,
726 ) -> R,
727 ) -> R {
728 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| f(index))
729 }
730}
731
732#[cfg(test)]
739impl IntentStore {
740 #[must_use]
741 pub(crate) fn export_meta() -> IntentMetaData {
742 IntentMetaData {
743 record: Self::meta(),
744 }
745 }
746
747 pub(crate) fn import_meta(data: IntentMetaData) {
748 Self::set_meta(data.record);
749 }
750
751 #[must_use]
752 pub(crate) fn export_records() -> IntentRecordsData {
753 IntentRecordsData {
754 entries: INTENT_RECORDS.with_borrow(|map| {
755 map.iter()
756 .map(|entry| IntentRecordEntryRecord {
757 intent_id: *entry.key(),
758 record: entry.value(),
759 })
760 .collect()
761 }),
762 }
763 }
764
765 pub(crate) fn import_records(data: IntentRecordsData) {
766 INTENT_RECORDS.with_borrow_mut(|map| {
767 map.clear_new();
768 for entry in data.entries {
769 map.insert(entry.intent_id, entry.record);
770 }
771 });
772 }
773
774 #[must_use]
775 pub(crate) fn export_totals() -> IntentTotalsData {
776 IntentTotalsData {
777 entries: INTENT_TOTALS.with_borrow(|map| {
778 map.iter()
779 .map(|entry| IntentTotalsEntryRecord {
780 resource_key: entry.key().clone(),
781 record: entry.value(),
782 })
783 .collect()
784 }),
785 }
786 }
787
788 pub(crate) fn import_totals(data: IntentTotalsData) {
789 INTENT_TOTALS.with_borrow_mut(|map| {
790 map.clear_new();
791 for entry in data.entries {
792 map.insert(entry.resource_key, entry.record);
793 }
794 });
795 }
796
797 #[must_use]
798 pub(crate) fn export_pending() -> IntentPendingData {
799 IntentPendingData {
800 entries: INTENT_PENDING.with_borrow(|map| {
801 map.iter()
802 .map(|entry| IntentPendingIndexEntryRecord {
803 intent_id: *entry.key(),
804 record: entry.value(),
805 })
806 .collect()
807 }),
808 }
809 }
810
811 pub(crate) fn import_pending(data: IntentPendingData) {
812 INTENT_PENDING.with_borrow_mut(|map| {
813 map.clear_new();
814 for entry in data.entries {
815 map.insert(entry.intent_id, entry.record);
816 }
817 });
818 }
819
820 #[must_use]
821 pub(crate) fn export_expiry_index() -> IntentExpiryIndexData {
822 IntentExpiryIndexData {
823 entries: INTENT_EXPIRY_INDEX.with_borrow(|map| {
824 map.iter()
825 .map(|entry| IntentExpiryIndexEntryRecord {
826 key: *entry.key(),
827 record: entry.value(),
828 })
829 .collect()
830 }),
831 }
832 }
833
834 pub(crate) fn import_expiry_index(data: IntentExpiryIndexData) {
835 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| {
836 map.clear_new();
837 for entry in data.entries {
838 map.insert(entry.key, entry.record);
839 }
840 });
841 }
842
843 pub(crate) fn reset_for_tests() {
844 INTENT_RECORDS.with_borrow_mut(StableBtreeMap::clear_new);
845 INTENT_TOTALS.with_borrow_mut(StableBtreeMap::clear_new);
846 INTENT_PENDING.with_borrow_mut(StableBtreeMap::clear_new);
847 INTENT_EXPIRY_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
848 INTENT_META.with_borrow_mut(|cell| cell.set(IntentStoreMetaRecord::default()));
849 ReceiptBackedIntentStore::reset_for_tests();
850 }
851}
852
853#[cfg(test)]
854impl ReceiptBackedIntentStore {
855 #[must_use]
856 pub(crate) fn export_records() -> ReceiptBackedIntentsData {
857 ReceiptBackedIntentsData {
858 entries: RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| {
859 records
860 .iter()
861 .map(|entry| ReceiptBackedIntentEntryRecord {
862 operation_id: *entry.key(),
863 record: entry.value(),
864 })
865 .collect()
866 }),
867 }
868 }
869
870 pub(crate) fn import_records(data: ReceiptBackedIntentsData) {
871 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(|records| {
872 records.clear_new();
873 for entry in data.entries {
874 records.insert(entry.operation_id, entry.record);
875 }
876 });
877 }
878
879 #[must_use]
880 pub(crate) fn export_placement_acknowledgement_index() -> PlacementAcknowledgementIndexData {
881 PlacementAcknowledgementIndexData {
882 entries: PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| {
883 index
884 .iter()
885 .map(|entry| PlacementAcknowledgementIndexEntryRecord {
886 operation_id: *entry.key(),
887 record: entry.value(),
888 })
889 .collect()
890 }),
891 }
892 }
893
894 pub(crate) fn import_placement_acknowledgement_index(data: PlacementAcknowledgementIndexData) {
895 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(|index| {
896 index.clear_new();
897 for entry in data.entries {
898 index.insert(entry.operation_id, entry.record);
899 }
900 });
901 }
902
903 pub(crate) fn reset_for_tests() {
904 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(StableBtreeMap::clear_new);
905 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
906 }
907}
908
909#[cfg(test)]
910mod tests {
911 use super::*;
912 use crate::{
913 cdk::types::Principal,
914 model::intent::{
915 RECEIPT_BACKED_INTENT_SCHEMA_VERSION, TerminalEvidence, TerminalEvidenceDecision,
916 },
917 };
918
919 #[test]
920 #[should_panic(expected = "stable IntentId is 7 bytes; expected 8")]
921 fn malformed_stable_intent_id_fails_closed() {
922 let _ = <IntentId as Storable>::from_bytes(Cow::Owned(vec![0; 7]));
923 }
924
925 #[test]
926 #[should_panic(expected = "stable OperationId is 31 bytes; expected 32")]
927 fn malformed_stable_operation_id_fails_closed() {
928 let _ = <OperationId as Storable>::from_bytes(Cow::Owned(vec![0; 31]));
929 }
930
931 #[test]
932 #[should_panic(expected = "stable IntentExpiryKeyRecord is 15 bytes; expected 16")]
933 fn malformed_stable_intent_expiry_key_fails_closed() {
934 let _ = <IntentExpiryKeyRecord as Storable>::from_bytes(Cow::Owned(vec![0; 15]));
935 }
936
937 #[test]
938 fn stable_intent_expiry_key_preserves_deadline_then_identity_order() {
939 let keys = [
940 IntentExpiryKeyRecord {
941 due_at_secs: 11,
942 intent_id: IntentId(1),
943 },
944 IntentExpiryKeyRecord {
945 due_at_secs: 10,
946 intent_id: IntentId(2),
947 },
948 IntentExpiryKeyRecord {
949 due_at_secs: 10,
950 intent_id: IntentId(1),
951 },
952 ];
953 let mut encoded = keys.map(Storable::into_bytes);
954 encoded.sort();
955 assert_eq!(
956 encoded,
957 [
958 keys[2].into_bytes(),
959 keys[1].into_bytes(),
960 keys[0].into_bytes()
961 ]
962 );
963 }
964
965 #[test]
966 fn intent_allocations_round_trip_through_canonical_data_snapshots() {
967 IntentStore::reset_for_tests();
968 let intent_id = IntentId(7);
969 let resource_key = IntentResourceKey::new("storage:uploads");
970 let record = IntentRecord {
971 id: intent_id,
972 resource_key: resource_key.clone(),
973 quantity: 11,
974 state: IntentState::Pending,
975 created_at: 13,
976 ttl_secs: Some(17),
977 };
978 let totals = IntentResourceTotalsRecord {
979 reserved_qty: 11,
980 committed_qty: 19,
981 pending_count: 1,
982 };
983 let pending = IntentPendingEntryRecord {
984 resource_key: resource_key.clone(),
985 quantity: 11,
986 created_at: 13,
987 ttl_secs: Some(17),
988 };
989 let meta = IntentStoreMetaRecord {
990 schema_version: INTENT_STORE_SCHEMA_VERSION,
991 next_intent_id: IntentId(8),
992 pending_total: 1,
993 committed_total: 2,
994 aborted_total: 3,
995 };
996
997 IntentStore::set_meta(meta);
998 IntentStore::insert_record(record);
999 IntentStore::set_totals(resource_key, totals);
1000 IntentStore::insert_pending(intent_id, pending);
1001 let expiry_key = IntentExpiryKeyRecord {
1002 due_at_secs: 31,
1003 intent_id,
1004 };
1005 IntentStore::insert_expiry(expiry_key, IntentExpiryEntryRecord { intent_id });
1006
1007 let meta_data = IntentStore::export_meta();
1008 let records_data = IntentStore::export_records();
1009 let totals_data = IntentStore::export_totals();
1010 let pending_data = IntentStore::export_pending();
1011 let expiry_data = IntentStore::export_expiry_index();
1012
1013 IntentStore::reset_for_tests();
1014 IntentStore::import_meta(meta_data);
1015 IntentStore::import_records(records_data.clone());
1016 IntentStore::import_totals(totals_data.clone());
1017 IntentStore::import_pending(pending_data.clone());
1018 IntentStore::import_expiry_index(expiry_data.clone());
1019
1020 assert_eq!(IntentStore::export_meta(), meta_data);
1021 assert_eq!(IntentStore::export_records(), records_data);
1022 assert_eq!(IntentStore::export_totals(), totals_data);
1023 assert_eq!(IntentStore::export_pending(), pending_data);
1024 assert_eq!(IntentStore::export_expiry_index(), expiry_data);
1025 IntentStore::reset_for_tests();
1026 }
1027
1028 #[test]
1029 fn receipt_backed_allocations_round_trip_through_canonical_data_snapshots() {
1030 IntentStore::reset_for_tests();
1031 let operation_id = OperationId::from_bytes([7; 32]);
1032 let evidence = TerminalEvidence::new(
1033 Principal::from_slice(&[1; 29]),
1034 TerminalEvidenceDecision::Committed,
1035 [8; 32],
1036 );
1037 let record = ReceiptBackedIntentRecord {
1038 schema_version: RECEIPT_BACKED_INTENT_SCHEMA_VERSION,
1039 operation_id,
1040 payload_binding: PayloadBinding::new([9; 32]),
1041 resource_key: IntentResourceKey::new("mint:collection"),
1042 quantity: 11,
1043 state: ReceiptBackedIntentState::Committed { evidence },
1044 revision: 2,
1045 created_at_ns: 13,
1046 updated_at_ns: 17,
1047 };
1048 ReceiptBackedIntentStore::insert(record);
1049 ReceiptBackedIntentStore::insert_placement_acknowledgement(
1050 PlacementAcknowledgementEntryRecord { operation_id },
1051 );
1052 let records_data = ReceiptBackedIntentStore::export_records();
1053 let acknowledgement_data =
1054 ReceiptBackedIntentStore::export_placement_acknowledgement_index();
1055
1056 ReceiptBackedIntentStore::reset_for_tests();
1057 assert_eq!(
1058 ReceiptBackedIntentStore::export_records(),
1059 ReceiptBackedIntentsData::default()
1060 );
1061 assert_eq!(
1062 ReceiptBackedIntentStore::export_placement_acknowledgement_index(),
1063 PlacementAcknowledgementIndexData::default()
1064 );
1065
1066 ReceiptBackedIntentStore::import_records(records_data.clone());
1067 ReceiptBackedIntentStore::import_placement_acknowledgement_index(
1068 acknowledgement_data.clone(),
1069 );
1070 assert_eq!(ReceiptBackedIntentStore::len(), 1);
1071 assert_eq!(ReceiptBackedIntentStore::export_records(), records_data);
1072 assert_eq!(
1073 ReceiptBackedIntentStore::export_placement_acknowledgement_index(),
1074 acknowledgement_data
1075 );
1076 IntentStore::reset_for_tests();
1077 }
1078}