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 APPLICATION_RECEIPT_REPLAY_ID, INTENT_EXPIRY_INDEX_ID, INTENT_META_ID, INTENT_PENDING_ID,
19 INTENT_RECORDS_ID, INTENT_TOTALS_ID, PLACEMENT_ACKNOWLEDGEMENT_INDEX_ID,
20 RECEIPT_BACKED_INTENT_RECORDS_ID,
21 },
22 storage::prelude::*,
23};
24use std::{borrow::Cow, cell::RefCell};
25
26pub const INTENT_STORE_SCHEMA_VERSION: u32 = 1;
31pub const APPLICATION_RECEIPT_REPLAY_SCHEMA_VERSION: u32 = 1;
32
33eager_static! {
34 static INTENT_META: RefCell<Cell<IntentStoreMetaRecord, VirtualMemory<DefaultMemoryImpl>>> =
35 RefCell::new(Cell::init(
36 crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_meta.v1", ty = IntentStoreMetaRecord, id = INTENT_META_ID),
37 IntentStoreMetaRecord::default(),
38 ));
39}
40
41eager_static! {
42 static APPLICATION_RECEIPT_REPLAY: RefCell<
43 StableBtreeMap<
44 OperationId,
45 ApplicationReceiptReplayRecord,
46 VirtualMemory<DefaultMemoryImpl>,
47 >
48 > = RefCell::new(StableBtreeMap::init(crate::ic_memory_key!(
49 authority = CANIC_CORE_MEMORY_AUTHORITY,
50 key = "canic.core.application_receipt_replay.v1",
51 ty = ApplicationReceiptReplayRecord,
52 id = APPLICATION_RECEIPT_REPLAY_ID
53 )));
54}
55
56eager_static! {
57 static RECEIPT_BACKED_INTENT_RECORDS: RefCell<
58 StableBtreeMap<
59 OperationId,
60 ReceiptBackedIntentRecord,
61 VirtualMemory<DefaultMemoryImpl>,
62 >
63 > = RefCell::new(StableBtreeMap::init(crate::ic_memory_key!(
64 authority = CANIC_CORE_MEMORY_AUTHORITY,
65 key = "canic.core.receipt_backed_intent_records.v1",
66 ty = ReceiptBackedIntentRecord,
67 id = RECEIPT_BACKED_INTENT_RECORDS_ID
68 )));
69}
70
71eager_static! {
72 static INTENT_EXPIRY_INDEX: RefCell<
73 StableBtreeMap<IntentExpiryKeyRecord, IntentExpiryEntryRecord, VirtualMemory<DefaultMemoryImpl>>
74 > = RefCell::new(
75 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)),
76 );
77}
78
79eager_static! {
80 static PLACEMENT_ACKNOWLEDGEMENT_INDEX: RefCell<
81 StableBtreeMap<
82 OperationId,
83 PlacementAcknowledgementEntryRecord,
84 VirtualMemory<DefaultMemoryImpl>,
85 >
86 > = RefCell::new(StableBtreeMap::init(crate::ic_memory_key!(
87 authority = CANIC_CORE_MEMORY_AUTHORITY,
88 key = "canic.core.placement_acknowledgement_index.v1",
89 ty = PlacementAcknowledgementEntryRecord,
90 id = PLACEMENT_ACKNOWLEDGEMENT_INDEX_ID
91 )));
92}
93
94eager_static! {
95 static INTENT_RECORDS: RefCell<
96 StableBtreeMap<IntentId, IntentRecord, VirtualMemory<DefaultMemoryImpl>>
97 > = RefCell::new(
98 StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_records.v1", ty = IntentRecord, id = INTENT_RECORDS_ID)),
99 );
100}
101
102eager_static! {
103 static INTENT_TOTALS: RefCell<
104 StableBtreeMap<IntentResourceKey, IntentResourceTotalsRecord, VirtualMemory<DefaultMemoryImpl>>
105 > = RefCell::new(
106 StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_totals.v1", ty = IntentResourceTotalsRecord, id = INTENT_TOTALS_ID)),
107 );
108}
109
110eager_static! {
111 static INTENT_PENDING: RefCell<
112 StableBtreeMap<IntentId, IntentPendingEntryRecord, VirtualMemory<DefaultMemoryImpl>>
113 > = RefCell::new(
114 StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_pending.v1", ty = IntentPendingEntryRecord, id = INTENT_PENDING_ID)),
115 );
116}
117
118impl Storable for IntentId {
119 const BOUND: Bound = Bound::Bounded {
120 max_size: 8,
121 is_fixed_size: true,
122 };
123
124 fn to_bytes(&self) -> Cow<'_, [u8]> {
125 Cow::Owned(self.0.to_be_bytes().to_vec())
126 }
127
128 fn into_bytes(self) -> Vec<u8> {
129 self.0.to_be_bytes().to_vec()
130 }
131
132 fn from_bytes(bytes: Cow<[u8]>) -> Self {
138 let bytes = <[u8; 8]>::try_from(bytes.as_ref()).unwrap_or_else(|_| {
139 panic!(
140 "stable IntentId is {} bytes; expected 8",
141 bytes.as_ref().len()
142 )
143 });
144
145 Self(u64::from_be_bytes(bytes))
146 }
147}
148
149impl Storable for OperationId {
150 const BOUND: Bound = Bound::Bounded {
151 max_size: 32,
152 is_fixed_size: true,
153 };
154
155 fn to_bytes(&self) -> Cow<'_, [u8]> {
156 Cow::Borrowed(self.as_bytes())
157 }
158
159 fn into_bytes(self) -> Vec<u8> {
160 self.as_bytes().to_vec()
161 }
162
163 fn from_bytes(bytes: Cow<[u8]>) -> Self {
169 let operation_id = <[u8; 32]>::try_from(bytes.as_ref()).unwrap_or_else(|_| {
170 panic!(
171 "stable OperationId is {} bytes; expected 32",
172 bytes.as_ref().len()
173 )
174 });
175 Self::from_bytes(operation_id)
176 }
177}
178
179#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
181pub struct IntentExpiryKeyRecord {
182 pub due_at_secs: u64,
183 pub intent_id: IntentId,
184}
185
186impl Storable for IntentExpiryKeyRecord {
187 const BOUND: Bound = Bound::Bounded {
188 max_size: 16,
189 is_fixed_size: true,
190 };
191
192 fn to_bytes(&self) -> Cow<'_, [u8]> {
193 Cow::Owned(self.into_bytes())
194 }
195
196 fn into_bytes(self) -> Vec<u8> {
197 let mut bytes = Vec::with_capacity(16);
198 bytes.extend_from_slice(&self.due_at_secs.to_be_bytes());
199 bytes.extend_from_slice(&self.intent_id.0.to_be_bytes());
200 bytes
201 }
202
203 fn from_bytes(bytes: Cow<[u8]>) -> Self {
209 let bytes = <[u8; 16]>::try_from(bytes.as_ref()).unwrap_or_else(|_| {
210 panic!(
211 "stable IntentExpiryKeyRecord is {} bytes; expected 16",
212 bytes.as_ref().len()
213 )
214 });
215 let (due_at_secs, intent_id) = bytes.split_at(8);
216 Self {
217 due_at_secs: u64::from_be_bytes(
218 due_at_secs.try_into().expect("expiry key deadline width"),
219 ),
220 intent_id: IntentId(u64::from_be_bytes(
221 intent_id.try_into().expect("expiry key intent width"),
222 )),
223 }
224 }
225}
226
227#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
229pub struct IntentExpiryEntryRecord {
230 pub intent_id: IntentId,
231}
232
233impl IntentExpiryEntryRecord {
234 pub const STATE_CONTRACT_NAME: &'static str = "IntentExpiryEntryRecord";
235 pub const STORABLE_MAX_SIZE: u32 = 32;
236}
237
238impl_storable_bounded!(
239 IntentExpiryEntryRecord,
240 IntentExpiryEntryRecord::STORABLE_MAX_SIZE,
241 false
242);
243
244#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
249pub enum IntentState {
250 Pending,
251 Committed,
252 Aborted,
253}
254
255#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
260pub struct IntentRecord {
261 pub id: IntentId,
262 pub resource_key: IntentResourceKey,
263 pub quantity: u64,
264 pub state: IntentState,
265 pub created_at: u64,
266 pub ttl_secs: Option<u64>,
268}
269
270impl IntentRecord {
271 pub const STATE_CONTRACT_NAME: &'static str = "IntentRecord";
272 pub const STORABLE_MAX_SIZE: u32 = 256;
273}
274
275impl_storable_bounded!(IntentRecord, IntentRecord::STORABLE_MAX_SIZE, false);
276
277#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
282pub struct IntentStoreMetaRecord {
283 pub schema_version: u32,
284 pub next_intent_id: IntentId,
285 pub pending_total: u64,
286 pub committed_total: u64,
287 pub aborted_total: u64,
288}
289
290impl IntentStoreMetaRecord {
291 pub const STATE_CONTRACT_NAME: &'static str = "IntentStoreMetaRecord";
292 pub const STORABLE_MAX_SIZE: u32 = 96;
293}
294
295impl Default for IntentStoreMetaRecord {
296 fn default() -> Self {
297 Self {
298 schema_version: INTENT_STORE_SCHEMA_VERSION,
299 next_intent_id: IntentId(1),
300 pending_total: 0,
301 committed_total: 0,
302 aborted_total: 0,
303 }
304 }
305}
306
307impl_storable_bounded!(
308 IntentStoreMetaRecord,
309 IntentStoreMetaRecord::STORABLE_MAX_SIZE,
310 false
311);
312
313#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
318pub struct IntentResourceTotalsRecord {
319 pub reserved_qty: u64,
320 pub committed_qty: u64,
321 pub pending_count: u64,
322}
323
324impl IntentResourceTotalsRecord {
325 pub const STATE_CONTRACT_NAME: &'static str = "IntentResourceTotalsRecord";
326 pub const STORABLE_MAX_SIZE: u32 = 69;
327}
328
329impl_storable_bounded!(
330 IntentResourceTotalsRecord,
331 IntentResourceTotalsRecord::STORABLE_MAX_SIZE,
332 false
333);
334
335#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
340pub struct IntentPendingEntryRecord {
341 pub resource_key: IntentResourceKey,
342 pub quantity: u64,
343 pub created_at: u64,
344 pub ttl_secs: Option<u64>,
346}
347
348impl IntentPendingEntryRecord {
349 pub const STATE_CONTRACT_NAME: &'static str = "IntentPendingEntryRecord";
350 pub const STORABLE_MAX_SIZE: u32 = 224;
351}
352
353impl_storable_bounded!(
354 IntentPendingEntryRecord,
355 IntentPendingEntryRecord::STORABLE_MAX_SIZE,
356 false
357);
358
359#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
361pub struct ReceiptBackedIntentRecord {
362 pub schema_version: u32,
363 pub operation_id: OperationId,
364 pub payload_binding: PayloadBinding,
365 pub resource_key: IntentResourceKey,
366 pub quantity: u64,
367 pub state: ReceiptBackedIntentState,
368 pub revision: u64,
369 pub created_at_ns: u64,
370 pub updated_at_ns: u64,
371}
372
373impl ReceiptBackedIntentRecord {
374 pub const STATE_CONTRACT_NAME: &'static str = "ReceiptBackedIntentRecord";
375 pub const STORABLE_MAX_SIZE: u32 = 1024;
376
377 #[must_use]
378 pub fn into_intent(self) -> ReceiptBackedIntent {
379 ReceiptBackedIntent {
380 schema_version: self.schema_version,
381 operation_id: self.operation_id,
382 payload_binding: self.payload_binding,
383 resource_key: self.resource_key,
384 quantity: self.quantity,
385 state: self.state,
386 revision: self.revision,
387 created_at_ns: self.created_at_ns,
388 updated_at_ns: self.updated_at_ns,
389 }
390 }
391}
392
393impl_storable_bounded!(
394 ReceiptBackedIntentRecord,
395 ReceiptBackedIntentRecord::STORABLE_MAX_SIZE,
396 false
397);
398
399#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
401pub struct ApplicationReceiptReplayRecord {
402 pub schema_version: u32,
403 pub operation_id: OperationId,
404 pub replay_deadline_ns: u64,
405}
406
407impl ApplicationReceiptReplayRecord {
408 pub const STATE_CONTRACT_NAME: &'static str = "ApplicationReceiptReplayRecord";
409 pub const STORABLE_MAX_SIZE: u32 = 124;
410}
411
412impl_storable_bounded!(
413 ApplicationReceiptReplayRecord,
414 ApplicationReceiptReplayRecord::STORABLE_MAX_SIZE,
415 false
416);
417
418#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
420pub struct PlacementAcknowledgementEntryRecord {
421 pub operation_id: OperationId,
422}
423
424impl PlacementAcknowledgementEntryRecord {
425 pub const STATE_CONTRACT_NAME: &'static str = "PlacementAcknowledgementEntryRecord";
426 pub const STORABLE_MAX_SIZE: u32 = 128;
427}
428
429impl_storable_bounded!(
430 PlacementAcknowledgementEntryRecord,
431 PlacementAcknowledgementEntryRecord::STORABLE_MAX_SIZE,
432 false
433);
434
435#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
442pub struct IntentMetaData {
443 pub record: IntentStoreMetaRecord,
444}
445
446impl IntentMetaData {
447 pub const STATE_CONTRACT_NAME: &'static str = "IntentMetaData";
448}
449
450#[derive(Clone, Debug, Eq, PartialEq)]
457pub struct IntentRecordEntryRecord {
458 pub intent_id: IntentId,
459 pub record: IntentRecord,
460}
461
462#[derive(Clone, Debug, Default, Eq, PartialEq)]
469pub struct IntentRecordsData {
470 pub entries: Vec<IntentRecordEntryRecord>,
471}
472
473impl IntentRecordsData {
474 pub const STATE_CONTRACT_NAME: &'static str = "IntentRecordsData";
475}
476
477#[derive(Clone, Debug, Eq, PartialEq)]
484pub struct IntentTotalsEntryRecord {
485 pub resource_key: IntentResourceKey,
486 pub record: IntentResourceTotalsRecord,
487}
488
489#[derive(Clone, Debug, Default, Eq, PartialEq)]
496pub struct IntentTotalsData {
497 pub entries: Vec<IntentTotalsEntryRecord>,
498}
499
500impl IntentTotalsData {
501 pub const STATE_CONTRACT_NAME: &'static str = "IntentTotalsData";
502}
503
504#[derive(Clone, Debug, Eq, PartialEq)]
511pub struct IntentPendingIndexEntryRecord {
512 pub intent_id: IntentId,
513 pub record: IntentPendingEntryRecord,
514}
515
516#[derive(Clone, Debug, Default, Eq, PartialEq)]
523pub struct IntentPendingData {
524 pub entries: Vec<IntentPendingIndexEntryRecord>,
525}
526
527impl IntentPendingData {
528 pub const STATE_CONTRACT_NAME: &'static str = "IntentPendingData";
529}
530
531#[derive(Clone, Copy, Debug, Eq, PartialEq)]
533pub struct IntentExpiryIndexEntryRecord {
534 pub key: IntentExpiryKeyRecord,
535 pub record: IntentExpiryEntryRecord,
536}
537
538#[derive(Clone, Debug, Default, Eq, PartialEq)]
540pub struct IntentExpiryIndexData {
541 pub entries: Vec<IntentExpiryIndexEntryRecord>,
542}
543
544impl IntentExpiryIndexData {
545 pub const STATE_CONTRACT_NAME: &'static str = "IntentExpiryIndexData";
546}
547
548#[derive(Clone, Debug, Eq, PartialEq)]
550pub struct ReceiptBackedIntentEntryRecord {
551 pub operation_id: OperationId,
552 pub record: ReceiptBackedIntentRecord,
553}
554
555#[derive(Clone, Debug, Default, Eq, PartialEq)]
557pub struct ReceiptBackedIntentsData {
558 pub entries: Vec<ReceiptBackedIntentEntryRecord>,
559}
560
561impl ReceiptBackedIntentsData {
562 pub const STATE_CONTRACT_NAME: &'static str = "ReceiptBackedIntentsData";
563}
564
565#[derive(Clone, Copy, Debug, Eq, PartialEq)]
567pub struct ApplicationReceiptReplayEntryRecord {
568 pub operation_id: OperationId,
569 pub record: ApplicationReceiptReplayRecord,
570}
571
572#[derive(Clone, Debug, Default, Eq, PartialEq)]
574pub struct ApplicationReceiptReplayData {
575 pub entries: Vec<ApplicationReceiptReplayEntryRecord>,
576}
577
578impl ApplicationReceiptReplayData {
579 pub const STATE_CONTRACT_NAME: &'static str = "ApplicationReceiptReplayData";
580}
581
582#[derive(Clone, Copy, Debug, Eq, PartialEq)]
584pub struct PlacementAcknowledgementIndexEntryRecord {
585 pub operation_id: OperationId,
586 pub record: PlacementAcknowledgementEntryRecord,
587}
588
589#[derive(Clone, Debug, Default, Eq, PartialEq)]
591pub struct PlacementAcknowledgementIndexData {
592 pub entries: Vec<PlacementAcknowledgementIndexEntryRecord>,
593}
594
595impl PlacementAcknowledgementIndexData {
596 pub const STATE_CONTRACT_NAME: &'static str = "PlacementAcknowledgementIndexData";
597}
598
599pub struct IntentStore;
604
605impl IntentStore {
606 #[must_use]
611 pub(crate) fn meta() -> IntentStoreMetaRecord {
612 INTENT_META.with_borrow(|cell| *cell.get())
613 }
614
615 pub(crate) fn set_meta(meta: IntentStoreMetaRecord) {
616 INTENT_META.with_borrow_mut(|cell| cell.set(meta));
617 }
618
619 #[must_use]
624 pub(crate) fn get_record(id: IntentId) -> Option<IntentRecord> {
625 INTENT_RECORDS.with_borrow(|map| map.get(&id))
626 }
627
628 pub(crate) fn insert_record(record: IntentRecord) -> Option<IntentRecord> {
629 INTENT_RECORDS.with_borrow_mut(|map| map.insert(record.id, record))
630 }
631
632 #[must_use]
637 pub(crate) fn get_totals(key: &IntentResourceKey) -> Option<IntentResourceTotalsRecord> {
638 INTENT_TOTALS.with_borrow(|map| map.get(key))
639 }
640
641 pub(crate) fn set_totals(
642 key: IntentResourceKey,
643 totals: IntentResourceTotalsRecord,
644 ) -> Option<IntentResourceTotalsRecord> {
645 INTENT_TOTALS.with_borrow_mut(|map| map.insert(key, totals))
646 }
647
648 pub(crate) fn remove_totals(key: &IntentResourceKey) -> Option<IntentResourceTotalsRecord> {
649 INTENT_TOTALS.with_borrow_mut(|map| map.remove(key))
650 }
651
652 #[must_use]
657 pub(crate) fn get_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
658 INTENT_PENDING.with_borrow(|map| map.get(&id))
659 }
660
661 pub(crate) fn insert_pending(
662 id: IntentId,
663 entry: IntentPendingEntryRecord,
664 ) -> Option<IntentPendingEntryRecord> {
665 INTENT_PENDING.with_borrow_mut(|map| map.insert(id, entry))
666 }
667
668 pub(crate) fn remove_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
669 INTENT_PENDING.with_borrow_mut(|map| map.remove(&id))
670 }
671
672 pub(crate) fn with_pending_entries<R>(
673 f: impl FnOnce(
674 &StableBtreeMap<IntentId, IntentPendingEntryRecord, VirtualMemory<DefaultMemoryImpl>>,
675 ) -> R,
676 ) -> R {
677 INTENT_PENDING.with_borrow(|map| f(map))
678 }
679
680 #[must_use]
685 pub(crate) fn get_expiry(key: IntentExpiryKeyRecord) -> Option<IntentExpiryEntryRecord> {
686 INTENT_EXPIRY_INDEX.with_borrow(|map| map.get(&key))
687 }
688
689 pub(crate) fn insert_expiry(
690 key: IntentExpiryKeyRecord,
691 record: IntentExpiryEntryRecord,
692 ) -> Option<IntentExpiryEntryRecord> {
693 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| map.insert(key, record))
694 }
695
696 pub(crate) fn remove_expiry(key: IntentExpiryKeyRecord) -> Option<IntentExpiryEntryRecord> {
697 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| map.remove(&key))
698 }
699
700 pub(crate) fn clear_expiry_index() {
701 INTENT_EXPIRY_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
702 }
703
704 pub(crate) fn with_expiry_entries<R>(
705 f: impl FnOnce(
706 &StableBtreeMap<
707 IntentExpiryKeyRecord,
708 IntentExpiryEntryRecord,
709 VirtualMemory<DefaultMemoryImpl>,
710 >,
711 ) -> R,
712 ) -> R {
713 INTENT_EXPIRY_INDEX.with_borrow(|map| f(map))
714 }
715}
716
717pub struct ReceiptBackedIntentStore;
719
720impl ReceiptBackedIntentStore {
721 #[must_use]
722 pub(crate) fn len() -> u64 {
723 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(StableBtreeMap::len)
724 }
725
726 #[must_use]
727 pub(crate) fn get(operation_id: OperationId) -> Option<ReceiptBackedIntentRecord> {
728 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| records.get(&operation_id))
729 }
730
731 pub(crate) fn insert(record: ReceiptBackedIntentRecord) -> Option<ReceiptBackedIntentRecord> {
732 RECEIPT_BACKED_INTENT_RECORDS
733 .with_borrow_mut(|records| records.insert(record.operation_id, record))
734 }
735
736 pub(crate) fn remove(operation_id: OperationId) -> Option<ReceiptBackedIntentRecord> {
737 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(|records| records.remove(&operation_id))
738 }
739
740 pub(crate) fn with_records<R>(
741 f: impl FnOnce(
742 &StableBtreeMap<
743 OperationId,
744 ReceiptBackedIntentRecord,
745 VirtualMemory<DefaultMemoryImpl>,
746 >,
747 ) -> R,
748 ) -> R {
749 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| f(records))
750 }
751
752 #[must_use]
753 pub(crate) fn get_application_replay(
754 operation_id: OperationId,
755 ) -> Option<ApplicationReceiptReplayRecord> {
756 APPLICATION_RECEIPT_REPLAY.with_borrow(|records| records.get(&operation_id))
757 }
758
759 pub(crate) fn insert_application_replay(
760 record: ApplicationReceiptReplayRecord,
761 ) -> Option<ApplicationReceiptReplayRecord> {
762 APPLICATION_RECEIPT_REPLAY
763 .with_borrow_mut(|records| records.insert(record.operation_id, record))
764 }
765
766 pub(crate) fn with_application_replay<R>(
767 f: impl FnOnce(
768 &StableBtreeMap<
769 OperationId,
770 ApplicationReceiptReplayRecord,
771 VirtualMemory<DefaultMemoryImpl>,
772 >,
773 ) -> R,
774 ) -> R {
775 APPLICATION_RECEIPT_REPLAY.with_borrow(|records| f(records))
776 }
777
778 #[must_use]
779 pub(crate) fn get_placement_acknowledgement(
780 operation_id: OperationId,
781 ) -> Option<PlacementAcknowledgementEntryRecord> {
782 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| index.get(&operation_id))
783 }
784
785 pub(crate) fn insert_placement_acknowledgement(
786 record: PlacementAcknowledgementEntryRecord,
787 ) -> Option<PlacementAcknowledgementEntryRecord> {
788 PLACEMENT_ACKNOWLEDGEMENT_INDEX
789 .with_borrow_mut(|index| index.insert(record.operation_id, record))
790 }
791
792 pub(crate) fn remove_placement_acknowledgement(
793 operation_id: OperationId,
794 ) -> Option<PlacementAcknowledgementEntryRecord> {
795 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(|index| index.remove(&operation_id))
796 }
797
798 pub(crate) fn clear_placement_acknowledgement_index() {
799 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
800 }
801
802 pub(crate) fn with_placement_acknowledgements<R>(
803 f: impl FnOnce(
804 &StableBtreeMap<
805 OperationId,
806 PlacementAcknowledgementEntryRecord,
807 VirtualMemory<DefaultMemoryImpl>,
808 >,
809 ) -> R,
810 ) -> R {
811 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| f(index))
812 }
813}
814
815#[cfg(test)]
822impl IntentStore {
823 #[must_use]
824 pub(crate) fn export_meta() -> IntentMetaData {
825 IntentMetaData {
826 record: Self::meta(),
827 }
828 }
829
830 pub(crate) fn import_meta(data: IntentMetaData) {
831 Self::set_meta(data.record);
832 }
833
834 #[must_use]
835 pub(crate) fn export_records() -> IntentRecordsData {
836 IntentRecordsData {
837 entries: INTENT_RECORDS.with_borrow(|map| {
838 map.iter()
839 .map(|entry| IntentRecordEntryRecord {
840 intent_id: *entry.key(),
841 record: entry.value(),
842 })
843 .collect()
844 }),
845 }
846 }
847
848 pub(crate) fn import_records(data: IntentRecordsData) {
849 INTENT_RECORDS.with_borrow_mut(|map| {
850 map.clear_new();
851 for entry in data.entries {
852 map.insert(entry.intent_id, entry.record);
853 }
854 });
855 }
856
857 #[must_use]
858 pub(crate) fn export_totals() -> IntentTotalsData {
859 IntentTotalsData {
860 entries: INTENT_TOTALS.with_borrow(|map| {
861 map.iter()
862 .map(|entry| IntentTotalsEntryRecord {
863 resource_key: entry.key().clone(),
864 record: entry.value(),
865 })
866 .collect()
867 }),
868 }
869 }
870
871 pub(crate) fn import_totals(data: IntentTotalsData) {
872 INTENT_TOTALS.with_borrow_mut(|map| {
873 map.clear_new();
874 for entry in data.entries {
875 map.insert(entry.resource_key, entry.record);
876 }
877 });
878 }
879
880 #[must_use]
881 pub(crate) fn export_pending() -> IntentPendingData {
882 IntentPendingData {
883 entries: INTENT_PENDING.with_borrow(|map| {
884 map.iter()
885 .map(|entry| IntentPendingIndexEntryRecord {
886 intent_id: *entry.key(),
887 record: entry.value(),
888 })
889 .collect()
890 }),
891 }
892 }
893
894 pub(crate) fn import_pending(data: IntentPendingData) {
895 INTENT_PENDING.with_borrow_mut(|map| {
896 map.clear_new();
897 for entry in data.entries {
898 map.insert(entry.intent_id, entry.record);
899 }
900 });
901 }
902
903 #[must_use]
904 pub(crate) fn export_expiry_index() -> IntentExpiryIndexData {
905 IntentExpiryIndexData {
906 entries: INTENT_EXPIRY_INDEX.with_borrow(|map| {
907 map.iter()
908 .map(|entry| IntentExpiryIndexEntryRecord {
909 key: *entry.key(),
910 record: entry.value(),
911 })
912 .collect()
913 }),
914 }
915 }
916
917 pub(crate) fn import_expiry_index(data: IntentExpiryIndexData) {
918 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| {
919 map.clear_new();
920 for entry in data.entries {
921 map.insert(entry.key, entry.record);
922 }
923 });
924 }
925
926 pub(crate) fn reset_for_tests() {
927 INTENT_RECORDS.with_borrow_mut(StableBtreeMap::clear_new);
928 INTENT_TOTALS.with_borrow_mut(StableBtreeMap::clear_new);
929 INTENT_PENDING.with_borrow_mut(StableBtreeMap::clear_new);
930 INTENT_EXPIRY_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
931 INTENT_META.with_borrow_mut(|cell| cell.set(IntentStoreMetaRecord::default()));
932 ReceiptBackedIntentStore::reset_for_tests();
933 }
934}
935
936#[cfg(test)]
937impl ReceiptBackedIntentStore {
938 #[must_use]
939 pub(crate) fn export_records() -> ReceiptBackedIntentsData {
940 ReceiptBackedIntentsData {
941 entries: RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| {
942 records
943 .iter()
944 .map(|entry| ReceiptBackedIntentEntryRecord {
945 operation_id: *entry.key(),
946 record: entry.value(),
947 })
948 .collect()
949 }),
950 }
951 }
952
953 pub(crate) fn import_records(data: ReceiptBackedIntentsData) {
954 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(|records| {
955 records.clear_new();
956 for entry in data.entries {
957 records.insert(entry.operation_id, entry.record);
958 }
959 });
960 }
961
962 #[must_use]
963 pub(crate) fn export_application_replay() -> ApplicationReceiptReplayData {
964 ApplicationReceiptReplayData {
965 entries: APPLICATION_RECEIPT_REPLAY.with_borrow(|records| {
966 records
967 .iter()
968 .map(|entry| ApplicationReceiptReplayEntryRecord {
969 operation_id: *entry.key(),
970 record: entry.value(),
971 })
972 .collect()
973 }),
974 }
975 }
976
977 pub(crate) fn import_application_replay(data: ApplicationReceiptReplayData) {
978 APPLICATION_RECEIPT_REPLAY.with_borrow_mut(|records| {
979 records.clear_new();
980 for entry in data.entries {
981 records.insert(entry.operation_id, entry.record);
982 }
983 });
984 }
985
986 #[must_use]
987 pub(crate) fn export_placement_acknowledgement_index() -> PlacementAcknowledgementIndexData {
988 PlacementAcknowledgementIndexData {
989 entries: PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| {
990 index
991 .iter()
992 .map(|entry| PlacementAcknowledgementIndexEntryRecord {
993 operation_id: *entry.key(),
994 record: entry.value(),
995 })
996 .collect()
997 }),
998 }
999 }
1000
1001 pub(crate) fn import_placement_acknowledgement_index(data: PlacementAcknowledgementIndexData) {
1002 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(|index| {
1003 index.clear_new();
1004 for entry in data.entries {
1005 index.insert(entry.operation_id, entry.record);
1006 }
1007 });
1008 }
1009
1010 pub(crate) fn reset_for_tests() {
1011 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(StableBtreeMap::clear_new);
1012 APPLICATION_RECEIPT_REPLAY.with_borrow_mut(StableBtreeMap::clear_new);
1013 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
1014 }
1015}
1016
1017#[cfg(test)]
1018mod tests {
1019 use super::*;
1020 use crate::{
1021 cdk::types::Principal,
1022 model::intent::{
1023 RECEIPT_BACKED_INTENT_SCHEMA_VERSION, TerminalEvidence, TerminalEvidenceDecision,
1024 },
1025 };
1026
1027 #[test]
1028 #[should_panic(expected = "stable IntentId is 7 bytes; expected 8")]
1029 fn malformed_stable_intent_id_fails_closed() {
1030 let _ = <IntentId as Storable>::from_bytes(Cow::Owned(vec![0; 7]));
1031 }
1032
1033 #[test]
1034 #[should_panic(expected = "stable OperationId is 31 bytes; expected 32")]
1035 fn malformed_stable_operation_id_fails_closed() {
1036 let _ = <OperationId as Storable>::from_bytes(Cow::Owned(vec![0; 31]));
1037 }
1038
1039 #[test]
1040 #[should_panic(expected = "stable IntentExpiryKeyRecord is 15 bytes; expected 16")]
1041 fn malformed_stable_intent_expiry_key_fails_closed() {
1042 let _ = <IntentExpiryKeyRecord as Storable>::from_bytes(Cow::Owned(vec![0; 15]));
1043 }
1044
1045 #[test]
1046 fn stable_intent_expiry_key_preserves_deadline_then_identity_order() {
1047 let keys = [
1048 IntentExpiryKeyRecord {
1049 due_at_secs: 11,
1050 intent_id: IntentId(1),
1051 },
1052 IntentExpiryKeyRecord {
1053 due_at_secs: 10,
1054 intent_id: IntentId(2),
1055 },
1056 IntentExpiryKeyRecord {
1057 due_at_secs: 10,
1058 intent_id: IntentId(1),
1059 },
1060 ];
1061 let mut encoded = keys.map(Storable::into_bytes);
1062 encoded.sort();
1063 assert_eq!(
1064 encoded,
1065 [
1066 keys[2].into_bytes(),
1067 keys[1].into_bytes(),
1068 keys[0].into_bytes()
1069 ]
1070 );
1071 }
1072
1073 #[test]
1074 fn intent_allocations_round_trip_through_canonical_data_snapshots() {
1075 IntentStore::reset_for_tests();
1076 let intent_id = IntentId(7);
1077 let resource_key = IntentResourceKey::new("storage:uploads");
1078 let record = IntentRecord {
1079 id: intent_id,
1080 resource_key: resource_key.clone(),
1081 quantity: 11,
1082 state: IntentState::Pending,
1083 created_at: 13,
1084 ttl_secs: Some(17),
1085 };
1086 let totals = IntentResourceTotalsRecord {
1087 reserved_qty: 11,
1088 committed_qty: 19,
1089 pending_count: 1,
1090 };
1091 let pending = IntentPendingEntryRecord {
1092 resource_key: resource_key.clone(),
1093 quantity: 11,
1094 created_at: 13,
1095 ttl_secs: Some(17),
1096 };
1097 let meta = IntentStoreMetaRecord {
1098 schema_version: INTENT_STORE_SCHEMA_VERSION,
1099 next_intent_id: IntentId(8),
1100 pending_total: 1,
1101 committed_total: 2,
1102 aborted_total: 3,
1103 };
1104
1105 IntentStore::set_meta(meta);
1106 IntentStore::insert_record(record);
1107 IntentStore::set_totals(resource_key, totals);
1108 IntentStore::insert_pending(intent_id, pending);
1109 let expiry_key = IntentExpiryKeyRecord {
1110 due_at_secs: 31,
1111 intent_id,
1112 };
1113 IntentStore::insert_expiry(expiry_key, IntentExpiryEntryRecord { intent_id });
1114
1115 let meta_data = IntentStore::export_meta();
1116 let records_data = IntentStore::export_records();
1117 let totals_data = IntentStore::export_totals();
1118 let pending_data = IntentStore::export_pending();
1119 let expiry_data = IntentStore::export_expiry_index();
1120
1121 IntentStore::reset_for_tests();
1122 IntentStore::import_meta(meta_data);
1123 IntentStore::import_records(records_data.clone());
1124 IntentStore::import_totals(totals_data.clone());
1125 IntentStore::import_pending(pending_data.clone());
1126 IntentStore::import_expiry_index(expiry_data.clone());
1127
1128 assert_eq!(IntentStore::export_meta(), meta_data);
1129 assert_eq!(IntentStore::export_records(), records_data);
1130 assert_eq!(IntentStore::export_totals(), totals_data);
1131 assert_eq!(IntentStore::export_pending(), pending_data);
1132 assert_eq!(IntentStore::export_expiry_index(), expiry_data);
1133 IntentStore::reset_for_tests();
1134 }
1135
1136 #[test]
1137 fn receipt_backed_allocations_round_trip_through_canonical_data_snapshots() {
1138 IntentStore::reset_for_tests();
1139 let application_operation_id = OperationId::from_bytes([7; 32]);
1140 let placement_operation_id = OperationId::from_bytes([8; 32]);
1141 let evidence = TerminalEvidence::new(
1142 Principal::from_slice(&[1; 29]),
1143 TerminalEvidenceDecision::Committed,
1144 [8; 32],
1145 );
1146 let record = ReceiptBackedIntentRecord {
1147 schema_version: RECEIPT_BACKED_INTENT_SCHEMA_VERSION,
1148 operation_id: application_operation_id,
1149 payload_binding: PayloadBinding::new([9; 32]),
1150 resource_key: IntentResourceKey::new("mint:collection"),
1151 quantity: 11,
1152 state: ReceiptBackedIntentState::Committed { evidence },
1153 revision: 2,
1154 created_at_ns: 13,
1155 updated_at_ns: 17,
1156 };
1157 ReceiptBackedIntentStore::insert(record);
1158 ReceiptBackedIntentStore::insert(ReceiptBackedIntentRecord {
1159 schema_version: RECEIPT_BACKED_INTENT_SCHEMA_VERSION,
1160 operation_id: placement_operation_id,
1161 payload_binding: PayloadBinding::new([10; 32]),
1162 resource_key: IntentResourceKey::new(format!("canic:placement:{}", "a".repeat(64))),
1163 quantity: 1,
1164 state: ReceiptBackedIntentState::Committed { evidence },
1165 revision: 2,
1166 created_at_ns: 13,
1167 updated_at_ns: 17,
1168 });
1169 ReceiptBackedIntentStore::insert_application_replay(ApplicationReceiptReplayRecord {
1170 schema_version: APPLICATION_RECEIPT_REPLAY_SCHEMA_VERSION,
1171 operation_id: application_operation_id,
1172 replay_deadline_ns: 23,
1173 });
1174 ReceiptBackedIntentStore::insert_placement_acknowledgement(
1175 PlacementAcknowledgementEntryRecord {
1176 operation_id: placement_operation_id,
1177 },
1178 );
1179 let records_data = ReceiptBackedIntentStore::export_records();
1180 let replay_data = ReceiptBackedIntentStore::export_application_replay();
1181 let acknowledgement_data =
1182 ReceiptBackedIntentStore::export_placement_acknowledgement_index();
1183
1184 ReceiptBackedIntentStore::reset_for_tests();
1185 assert_eq!(
1186 ReceiptBackedIntentStore::export_records(),
1187 ReceiptBackedIntentsData::default()
1188 );
1189 assert_eq!(
1190 ReceiptBackedIntentStore::export_application_replay(),
1191 ApplicationReceiptReplayData::default()
1192 );
1193 assert_eq!(
1194 ReceiptBackedIntentStore::export_placement_acknowledgement_index(),
1195 PlacementAcknowledgementIndexData::default()
1196 );
1197
1198 ReceiptBackedIntentStore::import_records(records_data.clone());
1199 ReceiptBackedIntentStore::import_application_replay(replay_data.clone());
1200 ReceiptBackedIntentStore::import_placement_acknowledgement_index(
1201 acknowledgement_data.clone(),
1202 );
1203 assert_eq!(ReceiptBackedIntentStore::len(), 2);
1204 assert_eq!(ReceiptBackedIntentStore::export_records(), records_data);
1205 assert_eq!(
1206 ReceiptBackedIntentStore::export_application_replay(),
1207 replay_data
1208 );
1209 assert_eq!(
1210 ReceiptBackedIntentStore::export_placement_acknowledgement_index(),
1211 acknowledgement_data
1212 );
1213 IntentStore::reset_for_tests();
1214 }
1215}