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 = 69;
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 pub(crate) fn remove_totals(key: &IntentResourceKey) -> Option<IntentResourceTotalsRecord> {
596 INTENT_TOTALS.with_borrow_mut(|map| map.remove(key))
597 }
598
599 #[must_use]
604 pub(crate) fn get_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
605 INTENT_PENDING.with_borrow(|map| map.get(&id))
606 }
607
608 pub(crate) fn insert_pending(
609 id: IntentId,
610 entry: IntentPendingEntryRecord,
611 ) -> Option<IntentPendingEntryRecord> {
612 INTENT_PENDING.with_borrow_mut(|map| map.insert(id, entry))
613 }
614
615 pub(crate) fn remove_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
616 INTENT_PENDING.with_borrow_mut(|map| map.remove(&id))
617 }
618
619 pub(crate) fn with_pending_entries<R>(
620 f: impl FnOnce(
621 &StableBtreeMap<IntentId, IntentPendingEntryRecord, VirtualMemory<DefaultMemoryImpl>>,
622 ) -> R,
623 ) -> R {
624 INTENT_PENDING.with_borrow(|map| f(map))
625 }
626
627 #[must_use]
632 pub(crate) fn get_expiry(key: IntentExpiryKeyRecord) -> Option<IntentExpiryEntryRecord> {
633 INTENT_EXPIRY_INDEX.with_borrow(|map| map.get(&key))
634 }
635
636 pub(crate) fn insert_expiry(
637 key: IntentExpiryKeyRecord,
638 record: IntentExpiryEntryRecord,
639 ) -> Option<IntentExpiryEntryRecord> {
640 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| map.insert(key, record))
641 }
642
643 pub(crate) fn remove_expiry(key: IntentExpiryKeyRecord) -> Option<IntentExpiryEntryRecord> {
644 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| map.remove(&key))
645 }
646
647 pub(crate) fn clear_expiry_index() {
648 INTENT_EXPIRY_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
649 }
650
651 pub(crate) fn with_expiry_entries<R>(
652 f: impl FnOnce(
653 &StableBtreeMap<
654 IntentExpiryKeyRecord,
655 IntentExpiryEntryRecord,
656 VirtualMemory<DefaultMemoryImpl>,
657 >,
658 ) -> R,
659 ) -> R {
660 INTENT_EXPIRY_INDEX.with_borrow(|map| f(map))
661 }
662}
663
664pub struct ReceiptBackedIntentStore;
666
667impl ReceiptBackedIntentStore {
668 #[must_use]
669 pub(crate) fn len() -> u64 {
670 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(StableBtreeMap::len)
671 }
672
673 #[must_use]
674 pub(crate) fn get(operation_id: OperationId) -> Option<ReceiptBackedIntentRecord> {
675 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| records.get(&operation_id))
676 }
677
678 pub(crate) fn insert(record: ReceiptBackedIntentRecord) -> Option<ReceiptBackedIntentRecord> {
679 RECEIPT_BACKED_INTENT_RECORDS
680 .with_borrow_mut(|records| records.insert(record.operation_id, record))
681 }
682
683 pub(crate) fn remove(operation_id: OperationId) -> Option<ReceiptBackedIntentRecord> {
684 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(|records| records.remove(&operation_id))
685 }
686
687 pub(crate) fn with_records<R>(
688 f: impl FnOnce(
689 &StableBtreeMap<
690 OperationId,
691 ReceiptBackedIntentRecord,
692 VirtualMemory<DefaultMemoryImpl>,
693 >,
694 ) -> R,
695 ) -> R {
696 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| f(records))
697 }
698
699 #[must_use]
700 pub(crate) fn get_placement_acknowledgement(
701 operation_id: OperationId,
702 ) -> Option<PlacementAcknowledgementEntryRecord> {
703 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| index.get(&operation_id))
704 }
705
706 pub(crate) fn insert_placement_acknowledgement(
707 record: PlacementAcknowledgementEntryRecord,
708 ) -> Option<PlacementAcknowledgementEntryRecord> {
709 PLACEMENT_ACKNOWLEDGEMENT_INDEX
710 .with_borrow_mut(|index| index.insert(record.operation_id, record))
711 }
712
713 pub(crate) fn remove_placement_acknowledgement(
714 operation_id: OperationId,
715 ) -> Option<PlacementAcknowledgementEntryRecord> {
716 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(|index| index.remove(&operation_id))
717 }
718
719 pub(crate) fn clear_placement_acknowledgement_index() {
720 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
721 }
722
723 pub(crate) fn with_placement_acknowledgements<R>(
724 f: impl FnOnce(
725 &StableBtreeMap<
726 OperationId,
727 PlacementAcknowledgementEntryRecord,
728 VirtualMemory<DefaultMemoryImpl>,
729 >,
730 ) -> R,
731 ) -> R {
732 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| f(index))
733 }
734}
735
736#[cfg(test)]
743impl IntentStore {
744 #[must_use]
745 pub(crate) fn export_meta() -> IntentMetaData {
746 IntentMetaData {
747 record: Self::meta(),
748 }
749 }
750
751 pub(crate) fn import_meta(data: IntentMetaData) {
752 Self::set_meta(data.record);
753 }
754
755 #[must_use]
756 pub(crate) fn export_records() -> IntentRecordsData {
757 IntentRecordsData {
758 entries: INTENT_RECORDS.with_borrow(|map| {
759 map.iter()
760 .map(|entry| IntentRecordEntryRecord {
761 intent_id: *entry.key(),
762 record: entry.value(),
763 })
764 .collect()
765 }),
766 }
767 }
768
769 pub(crate) fn import_records(data: IntentRecordsData) {
770 INTENT_RECORDS.with_borrow_mut(|map| {
771 map.clear_new();
772 for entry in data.entries {
773 map.insert(entry.intent_id, entry.record);
774 }
775 });
776 }
777
778 #[must_use]
779 pub(crate) fn export_totals() -> IntentTotalsData {
780 IntentTotalsData {
781 entries: INTENT_TOTALS.with_borrow(|map| {
782 map.iter()
783 .map(|entry| IntentTotalsEntryRecord {
784 resource_key: entry.key().clone(),
785 record: entry.value(),
786 })
787 .collect()
788 }),
789 }
790 }
791
792 pub(crate) fn import_totals(data: IntentTotalsData) {
793 INTENT_TOTALS.with_borrow_mut(|map| {
794 map.clear_new();
795 for entry in data.entries {
796 map.insert(entry.resource_key, entry.record);
797 }
798 });
799 }
800
801 #[must_use]
802 pub(crate) fn export_pending() -> IntentPendingData {
803 IntentPendingData {
804 entries: INTENT_PENDING.with_borrow(|map| {
805 map.iter()
806 .map(|entry| IntentPendingIndexEntryRecord {
807 intent_id: *entry.key(),
808 record: entry.value(),
809 })
810 .collect()
811 }),
812 }
813 }
814
815 pub(crate) fn import_pending(data: IntentPendingData) {
816 INTENT_PENDING.with_borrow_mut(|map| {
817 map.clear_new();
818 for entry in data.entries {
819 map.insert(entry.intent_id, entry.record);
820 }
821 });
822 }
823
824 #[must_use]
825 pub(crate) fn export_expiry_index() -> IntentExpiryIndexData {
826 IntentExpiryIndexData {
827 entries: INTENT_EXPIRY_INDEX.with_borrow(|map| {
828 map.iter()
829 .map(|entry| IntentExpiryIndexEntryRecord {
830 key: *entry.key(),
831 record: entry.value(),
832 })
833 .collect()
834 }),
835 }
836 }
837
838 pub(crate) fn import_expiry_index(data: IntentExpiryIndexData) {
839 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| {
840 map.clear_new();
841 for entry in data.entries {
842 map.insert(entry.key, entry.record);
843 }
844 });
845 }
846
847 pub(crate) fn reset_for_tests() {
848 INTENT_RECORDS.with_borrow_mut(StableBtreeMap::clear_new);
849 INTENT_TOTALS.with_borrow_mut(StableBtreeMap::clear_new);
850 INTENT_PENDING.with_borrow_mut(StableBtreeMap::clear_new);
851 INTENT_EXPIRY_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
852 INTENT_META.with_borrow_mut(|cell| cell.set(IntentStoreMetaRecord::default()));
853 ReceiptBackedIntentStore::reset_for_tests();
854 }
855}
856
857#[cfg(test)]
858impl ReceiptBackedIntentStore {
859 #[must_use]
860 pub(crate) fn export_records() -> ReceiptBackedIntentsData {
861 ReceiptBackedIntentsData {
862 entries: RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| {
863 records
864 .iter()
865 .map(|entry| ReceiptBackedIntentEntryRecord {
866 operation_id: *entry.key(),
867 record: entry.value(),
868 })
869 .collect()
870 }),
871 }
872 }
873
874 pub(crate) fn import_records(data: ReceiptBackedIntentsData) {
875 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(|records| {
876 records.clear_new();
877 for entry in data.entries {
878 records.insert(entry.operation_id, entry.record);
879 }
880 });
881 }
882
883 #[must_use]
884 pub(crate) fn export_placement_acknowledgement_index() -> PlacementAcknowledgementIndexData {
885 PlacementAcknowledgementIndexData {
886 entries: PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| {
887 index
888 .iter()
889 .map(|entry| PlacementAcknowledgementIndexEntryRecord {
890 operation_id: *entry.key(),
891 record: entry.value(),
892 })
893 .collect()
894 }),
895 }
896 }
897
898 pub(crate) fn import_placement_acknowledgement_index(data: PlacementAcknowledgementIndexData) {
899 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(|index| {
900 index.clear_new();
901 for entry in data.entries {
902 index.insert(entry.operation_id, entry.record);
903 }
904 });
905 }
906
907 pub(crate) fn reset_for_tests() {
908 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(StableBtreeMap::clear_new);
909 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
910 }
911}
912
913#[cfg(test)]
914mod tests {
915 use super::*;
916 use crate::{
917 cdk::types::Principal,
918 model::intent::{
919 RECEIPT_BACKED_INTENT_SCHEMA_VERSION, TerminalEvidence, TerminalEvidenceDecision,
920 },
921 };
922
923 #[test]
924 #[should_panic(expected = "stable IntentId is 7 bytes; expected 8")]
925 fn malformed_stable_intent_id_fails_closed() {
926 let _ = <IntentId as Storable>::from_bytes(Cow::Owned(vec![0; 7]));
927 }
928
929 #[test]
930 #[should_panic(expected = "stable OperationId is 31 bytes; expected 32")]
931 fn malformed_stable_operation_id_fails_closed() {
932 let _ = <OperationId as Storable>::from_bytes(Cow::Owned(vec![0; 31]));
933 }
934
935 #[test]
936 #[should_panic(expected = "stable IntentExpiryKeyRecord is 15 bytes; expected 16")]
937 fn malformed_stable_intent_expiry_key_fails_closed() {
938 let _ = <IntentExpiryKeyRecord as Storable>::from_bytes(Cow::Owned(vec![0; 15]));
939 }
940
941 #[test]
942 fn stable_intent_expiry_key_preserves_deadline_then_identity_order() {
943 let keys = [
944 IntentExpiryKeyRecord {
945 due_at_secs: 11,
946 intent_id: IntentId(1),
947 },
948 IntentExpiryKeyRecord {
949 due_at_secs: 10,
950 intent_id: IntentId(2),
951 },
952 IntentExpiryKeyRecord {
953 due_at_secs: 10,
954 intent_id: IntentId(1),
955 },
956 ];
957 let mut encoded = keys.map(Storable::into_bytes);
958 encoded.sort();
959 assert_eq!(
960 encoded,
961 [
962 keys[2].into_bytes(),
963 keys[1].into_bytes(),
964 keys[0].into_bytes()
965 ]
966 );
967 }
968
969 #[test]
970 fn intent_allocations_round_trip_through_canonical_data_snapshots() {
971 IntentStore::reset_for_tests();
972 let intent_id = IntentId(7);
973 let resource_key = IntentResourceKey::new("storage:uploads");
974 let record = IntentRecord {
975 id: intent_id,
976 resource_key: resource_key.clone(),
977 quantity: 11,
978 state: IntentState::Pending,
979 created_at: 13,
980 ttl_secs: Some(17),
981 };
982 let totals = IntentResourceTotalsRecord {
983 reserved_qty: 11,
984 committed_qty: 19,
985 pending_count: 1,
986 };
987 let pending = IntentPendingEntryRecord {
988 resource_key: resource_key.clone(),
989 quantity: 11,
990 created_at: 13,
991 ttl_secs: Some(17),
992 };
993 let meta = IntentStoreMetaRecord {
994 schema_version: INTENT_STORE_SCHEMA_VERSION,
995 next_intent_id: IntentId(8),
996 pending_total: 1,
997 committed_total: 2,
998 aborted_total: 3,
999 };
1000
1001 IntentStore::set_meta(meta);
1002 IntentStore::insert_record(record);
1003 IntentStore::set_totals(resource_key, totals);
1004 IntentStore::insert_pending(intent_id, pending);
1005 let expiry_key = IntentExpiryKeyRecord {
1006 due_at_secs: 31,
1007 intent_id,
1008 };
1009 IntentStore::insert_expiry(expiry_key, IntentExpiryEntryRecord { intent_id });
1010
1011 let meta_data = IntentStore::export_meta();
1012 let records_data = IntentStore::export_records();
1013 let totals_data = IntentStore::export_totals();
1014 let pending_data = IntentStore::export_pending();
1015 let expiry_data = IntentStore::export_expiry_index();
1016
1017 IntentStore::reset_for_tests();
1018 IntentStore::import_meta(meta_data);
1019 IntentStore::import_records(records_data.clone());
1020 IntentStore::import_totals(totals_data.clone());
1021 IntentStore::import_pending(pending_data.clone());
1022 IntentStore::import_expiry_index(expiry_data.clone());
1023
1024 assert_eq!(IntentStore::export_meta(), meta_data);
1025 assert_eq!(IntentStore::export_records(), records_data);
1026 assert_eq!(IntentStore::export_totals(), totals_data);
1027 assert_eq!(IntentStore::export_pending(), pending_data);
1028 assert_eq!(IntentStore::export_expiry_index(), expiry_data);
1029 IntentStore::reset_for_tests();
1030 }
1031
1032 #[test]
1033 fn receipt_backed_allocations_round_trip_through_canonical_data_snapshots() {
1034 IntentStore::reset_for_tests();
1035 let operation_id = OperationId::from_bytes([7; 32]);
1036 let evidence = TerminalEvidence::new(
1037 Principal::from_slice(&[1; 29]),
1038 TerminalEvidenceDecision::Committed,
1039 [8; 32],
1040 );
1041 let record = ReceiptBackedIntentRecord {
1042 schema_version: RECEIPT_BACKED_INTENT_SCHEMA_VERSION,
1043 operation_id,
1044 payload_binding: PayloadBinding::new([9; 32]),
1045 resource_key: IntentResourceKey::new("mint:collection"),
1046 quantity: 11,
1047 state: ReceiptBackedIntentState::Committed { evidence },
1048 revision: 2,
1049 created_at_ns: 13,
1050 updated_at_ns: 17,
1051 };
1052 ReceiptBackedIntentStore::insert(record);
1053 ReceiptBackedIntentStore::insert_placement_acknowledgement(
1054 PlacementAcknowledgementEntryRecord { operation_id },
1055 );
1056 let records_data = ReceiptBackedIntentStore::export_records();
1057 let acknowledgement_data =
1058 ReceiptBackedIntentStore::export_placement_acknowledgement_index();
1059
1060 ReceiptBackedIntentStore::reset_for_tests();
1061 assert_eq!(
1062 ReceiptBackedIntentStore::export_records(),
1063 ReceiptBackedIntentsData::default()
1064 );
1065 assert_eq!(
1066 ReceiptBackedIntentStore::export_placement_acknowledgement_index(),
1067 PlacementAcknowledgementIndexData::default()
1068 );
1069
1070 ReceiptBackedIntentStore::import_records(records_data.clone());
1071 ReceiptBackedIntentStore::import_placement_acknowledgement_index(
1072 acknowledgement_data.clone(),
1073 );
1074 assert_eq!(ReceiptBackedIntentStore::len(), 1);
1075 assert_eq!(ReceiptBackedIntentStore::export_records(), records_data);
1076 assert_eq!(
1077 ReceiptBackedIntentStore::export_placement_acknowledgement_index(),
1078 acknowledgement_data
1079 );
1080 IntentStore::reset_for_tests();
1081 }
1082}