1use crate::cdk::structures::btreemap::BTreeMap as StableBtreeMap;
8use crate::{
9 cdk::structures::{
10 DefaultMemoryImpl, Memory, 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_ELIGIBILITY_ID, APPLICATION_RECEIPT_REPLAY_ID, INTENT_EXPIRY_INDEX_ID,
19 INTENT_META_ID, INTENT_PENDING_ID, INTENT_RECORDS_ID, INTENT_TOTALS_ID,
20 PLACEMENT_ACKNOWLEDGEMENT_INDEX_ID, 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;
32pub const APPLICATION_RECEIPT_ELIGIBILITY_SCHEMA_VERSION: u32 = 1;
33const WASM_PAGE_BYTES: u64 = 65_536;
34const APPLICATION_RECEIPT_ELIGIBILITY_MIN_NODE_ENTRIES: u64 = 5;
35const APPLICATION_RECEIPT_ELIGIBILITY_CHUNK_BYTES: u64 = 2_378;
36const APPLICATION_RECEIPT_ELIGIBILITY_FIXED_BYTES: u64 = 116;
37
38type StableIntentMemory = VirtualMemory<DefaultMemoryImpl>;
39type ApplicationReceiptEligibilityMap = StableBtreeMap<
40 ApplicationReceiptEligibilityKeyRecord,
41 ApplicationReceiptEligibilityRecord,
42 StableIntentMemory,
43>;
44type ApplicationReceiptEligibilityState = (ApplicationReceiptEligibilityMap, StableIntentMemory);
45
46eager_static! {
47 static INTENT_META: RefCell<Cell<IntentStoreMetaRecord, VirtualMemory<DefaultMemoryImpl>>> =
48 RefCell::new(Cell::init(
49 crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_meta.v1", ty = IntentStoreMetaRecord, id = INTENT_META_ID),
50 IntentStoreMetaRecord::default(),
51 ));
52}
53
54eager_static! {
55 static APPLICATION_RECEIPT_REPLAY: RefCell<
56 StableBtreeMap<
57 OperationId,
58 ApplicationReceiptReplayRecord,
59 VirtualMemory<DefaultMemoryImpl>,
60 >
61 > = RefCell::new(StableBtreeMap::init(crate::ic_memory_key!(
62 authority = CANIC_CORE_MEMORY_AUTHORITY,
63 key = "canic.core.application_receipt_replay.v1",
64 ty = ApplicationReceiptReplayRecord,
65 id = APPLICATION_RECEIPT_REPLAY_ID
66 )));
67}
68
69eager_static! {
70 static APPLICATION_RECEIPT_ELIGIBILITY: RefCell<ApplicationReceiptEligibilityState> = {
71 let memory = crate::ic_memory_key!(
72 authority = CANIC_CORE_MEMORY_AUTHORITY,
73 key = "canic.core.application_receipt_eligibility.v1",
74 ty = ApplicationReceiptEligibilityRecord,
75 id = APPLICATION_RECEIPT_ELIGIBILITY_ID
76 );
77 let map = StableBtreeMap::init(memory.clone());
78 RefCell::new((map, memory))
79 };
80}
81
82eager_static! {
83 static RECEIPT_BACKED_INTENT_RECORDS: RefCell<
84 StableBtreeMap<
85 OperationId,
86 ReceiptBackedIntentRecord,
87 VirtualMemory<DefaultMemoryImpl>,
88 >
89 > = RefCell::new(StableBtreeMap::init(crate::ic_memory_key!(
90 authority = CANIC_CORE_MEMORY_AUTHORITY,
91 key = "canic.core.receipt_backed_intent_records.v1",
92 ty = ReceiptBackedIntentRecord,
93 id = RECEIPT_BACKED_INTENT_RECORDS_ID
94 )));
95}
96
97eager_static! {
98 static INTENT_EXPIRY_INDEX: RefCell<
99 StableBtreeMap<IntentExpiryKeyRecord, IntentExpiryEntryRecord, VirtualMemory<DefaultMemoryImpl>>
100 > = RefCell::new(
101 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)),
102 );
103}
104
105eager_static! {
106 static PLACEMENT_ACKNOWLEDGEMENT_INDEX: RefCell<
107 StableBtreeMap<
108 OperationId,
109 PlacementAcknowledgementEntryRecord,
110 VirtualMemory<DefaultMemoryImpl>,
111 >
112 > = RefCell::new(StableBtreeMap::init(crate::ic_memory_key!(
113 authority = CANIC_CORE_MEMORY_AUTHORITY,
114 key = "canic.core.placement_acknowledgement_index.v1",
115 ty = PlacementAcknowledgementEntryRecord,
116 id = PLACEMENT_ACKNOWLEDGEMENT_INDEX_ID
117 )));
118}
119
120eager_static! {
121 static INTENT_RECORDS: RefCell<
122 StableBtreeMap<IntentId, IntentRecord, VirtualMemory<DefaultMemoryImpl>>
123 > = RefCell::new(
124 StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_records.v1", ty = IntentRecord, id = INTENT_RECORDS_ID)),
125 );
126}
127
128eager_static! {
129 static INTENT_TOTALS: RefCell<
130 StableBtreeMap<IntentResourceKey, IntentResourceTotalsRecord, VirtualMemory<DefaultMemoryImpl>>
131 > = RefCell::new(
132 StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_totals.v1", ty = IntentResourceTotalsRecord, id = INTENT_TOTALS_ID)),
133 );
134}
135
136eager_static! {
137 static INTENT_PENDING: RefCell<
138 StableBtreeMap<IntentId, IntentPendingEntryRecord, VirtualMemory<DefaultMemoryImpl>>
139 > = RefCell::new(
140 StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_pending.v1", ty = IntentPendingEntryRecord, id = INTENT_PENDING_ID)),
141 );
142}
143
144impl Storable for IntentId {
145 const BOUND: Bound = Bound::Bounded {
146 max_size: 8,
147 is_fixed_size: true,
148 };
149
150 fn to_bytes(&self) -> Cow<'_, [u8]> {
151 Cow::Owned(self.0.to_be_bytes().to_vec())
152 }
153
154 fn into_bytes(self) -> Vec<u8> {
155 self.0.to_be_bytes().to_vec()
156 }
157
158 fn from_bytes(bytes: Cow<[u8]>) -> Self {
164 let bytes = <[u8; 8]>::try_from(bytes.as_ref()).unwrap_or_else(|_| {
165 panic!(
166 "stable IntentId is {} bytes; expected 8",
167 bytes.as_ref().len()
168 )
169 });
170
171 Self(u64::from_be_bytes(bytes))
172 }
173}
174
175impl Storable for OperationId {
176 const BOUND: Bound = Bound::Bounded {
177 max_size: 32,
178 is_fixed_size: true,
179 };
180
181 fn to_bytes(&self) -> Cow<'_, [u8]> {
182 Cow::Borrowed(self.as_bytes())
183 }
184
185 fn into_bytes(self) -> Vec<u8> {
186 self.as_bytes().to_vec()
187 }
188
189 fn from_bytes(bytes: Cow<[u8]>) -> Self {
195 let operation_id = <[u8; 32]>::try_from(bytes.as_ref()).unwrap_or_else(|_| {
196 panic!(
197 "stable OperationId is {} bytes; expected 32",
198 bytes.as_ref().len()
199 )
200 });
201 Self::from_bytes(operation_id)
202 }
203}
204
205#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
207pub struct IntentExpiryKeyRecord {
208 pub due_at_secs: u64,
209 pub intent_id: IntentId,
210}
211
212impl Storable for IntentExpiryKeyRecord {
213 const BOUND: Bound = Bound::Bounded {
214 max_size: 16,
215 is_fixed_size: true,
216 };
217
218 fn to_bytes(&self) -> Cow<'_, [u8]> {
219 Cow::Owned(self.into_bytes())
220 }
221
222 fn into_bytes(self) -> Vec<u8> {
223 let mut bytes = Vec::with_capacity(16);
224 bytes.extend_from_slice(&self.due_at_secs.to_be_bytes());
225 bytes.extend_from_slice(&self.intent_id.0.to_be_bytes());
226 bytes
227 }
228
229 fn from_bytes(bytes: Cow<[u8]>) -> Self {
235 let bytes = <[u8; 16]>::try_from(bytes.as_ref()).unwrap_or_else(|_| {
236 panic!(
237 "stable IntentExpiryKeyRecord is {} bytes; expected 16",
238 bytes.as_ref().len()
239 )
240 });
241 let (due_at_secs, intent_id) = bytes.split_at(8);
242 Self {
243 due_at_secs: u64::from_be_bytes(
244 due_at_secs.try_into().expect("expiry key deadline width"),
245 ),
246 intent_id: IntentId(u64::from_be_bytes(
247 intent_id.try_into().expect("expiry key intent width"),
248 )),
249 }
250 }
251}
252
253#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
255pub struct IntentExpiryEntryRecord {
256 pub intent_id: IntentId,
257}
258
259impl IntentExpiryEntryRecord {
260 pub const STATE_CONTRACT_NAME: &'static str = "IntentExpiryEntryRecord";
261 pub const STORABLE_MAX_SIZE: u32 = 32;
262}
263
264impl_storable_bounded!(
265 IntentExpiryEntryRecord,
266 IntentExpiryEntryRecord::STORABLE_MAX_SIZE,
267 false
268);
269
270#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
275pub enum IntentState {
276 Pending,
277 Committed,
278 Aborted,
279}
280
281#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
286pub struct IntentRecord {
287 pub id: IntentId,
288 pub resource_key: IntentResourceKey,
289 pub quantity: u64,
290 pub state: IntentState,
291 pub created_at: u64,
292 pub ttl_secs: Option<u64>,
294}
295
296impl IntentRecord {
297 pub const STATE_CONTRACT_NAME: &'static str = "IntentRecord";
298 pub const STORABLE_MAX_SIZE: u32 = 229;
299}
300
301impl_storable_bounded!(IntentRecord, IntentRecord::STORABLE_MAX_SIZE, false);
302
303#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
308pub struct IntentStoreMetaRecord {
309 pub schema_version: u32,
310 pub next_intent_id: IntentId,
311 pub pending_total: u64,
312 pub committed_total: u64,
313 pub aborted_total: u64,
314}
315
316impl IntentStoreMetaRecord {
317 pub const STATE_CONTRACT_NAME: &'static str = "IntentStoreMetaRecord";
318 pub const STORABLE_MAX_SIZE: u32 = 96;
319}
320
321impl Default for IntentStoreMetaRecord {
322 fn default() -> Self {
323 Self {
324 schema_version: INTENT_STORE_SCHEMA_VERSION,
325 next_intent_id: IntentId(1),
326 pending_total: 0,
327 committed_total: 0,
328 aborted_total: 0,
329 }
330 }
331}
332
333impl_storable_bounded!(
334 IntentStoreMetaRecord,
335 IntentStoreMetaRecord::STORABLE_MAX_SIZE,
336 false
337);
338
339#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
344pub struct IntentResourceTotalsRecord {
345 pub reserved_qty: u64,
346 pub committed_qty: u64,
347 pub pending_count: u64,
348}
349
350impl IntentResourceTotalsRecord {
351 pub const STATE_CONTRACT_NAME: &'static str = "IntentResourceTotalsRecord";
352 pub const STORABLE_MAX_SIZE: u32 = 69;
353}
354
355impl_storable_bounded!(
356 IntentResourceTotalsRecord,
357 IntentResourceTotalsRecord::STORABLE_MAX_SIZE,
358 false
359);
360
361#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
366pub struct IntentPendingEntryRecord {
367 pub resource_key: IntentResourceKey,
368 pub quantity: u64,
369 pub created_at: u64,
370 pub ttl_secs: Option<u64>,
372}
373
374impl IntentPendingEntryRecord {
375 pub const STATE_CONTRACT_NAME: &'static str = "IntentPendingEntryRecord";
376 pub const STORABLE_MAX_SIZE: u32 = 224;
377}
378
379impl_storable_bounded!(
380 IntentPendingEntryRecord,
381 IntentPendingEntryRecord::STORABLE_MAX_SIZE,
382 false
383);
384
385#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
387pub struct ReceiptBackedIntentRecord {
388 pub schema_version: u32,
389 pub operation_id: OperationId,
390 pub payload_binding: PayloadBinding,
391 pub resource_key: IntentResourceKey,
392 pub quantity: u64,
393 pub state: ReceiptBackedIntentState,
394 pub revision: u64,
395 pub created_at_ns: u64,
396 pub updated_at_ns: u64,
397}
398
399impl ReceiptBackedIntentRecord {
400 pub const STATE_CONTRACT_NAME: &'static str = "ReceiptBackedIntentRecord";
401 pub const STORABLE_MAX_SIZE: u32 = 1024;
402
403 #[must_use]
404 pub fn into_intent(self) -> ReceiptBackedIntent {
405 ReceiptBackedIntent {
406 schema_version: self.schema_version,
407 operation_id: self.operation_id,
408 payload_binding: self.payload_binding,
409 resource_key: self.resource_key,
410 quantity: self.quantity,
411 state: self.state,
412 revision: self.revision,
413 created_at_ns: self.created_at_ns,
414 updated_at_ns: self.updated_at_ns,
415 }
416 }
417}
418
419impl_storable_bounded!(
420 ReceiptBackedIntentRecord,
421 ReceiptBackedIntentRecord::STORABLE_MAX_SIZE,
422 false
423);
424
425#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
427pub struct ApplicationReceiptReplayRecord {
428 pub schema_version: u32,
429 pub operation_id: OperationId,
430 pub replay_deadline_ns: u64,
431}
432
433impl ApplicationReceiptReplayRecord {
434 pub const STATE_CONTRACT_NAME: &'static str = "ApplicationReceiptReplayRecord";
435 pub const STORABLE_MAX_SIZE: u32 = 124;
436}
437
438impl_storable_bounded!(
439 ApplicationReceiptReplayRecord,
440 ApplicationReceiptReplayRecord::STORABLE_MAX_SIZE,
441 false
442);
443
444#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
446pub struct ApplicationReceiptEligibilityKeyRecord {
447 pub eligible_at_ns: u64,
448 pub operation_id: OperationId,
449}
450
451impl Storable for ApplicationReceiptEligibilityKeyRecord {
452 const BOUND: Bound = Bound::Bounded {
453 max_size: 40,
454 is_fixed_size: true,
455 };
456
457 fn to_bytes(&self) -> Cow<'_, [u8]> {
458 Cow::Owned(self.into_bytes())
459 }
460
461 fn into_bytes(self) -> Vec<u8> {
462 let mut bytes = Vec::with_capacity(40);
463 bytes.extend_from_slice(&self.eligible_at_ns.to_be_bytes());
464 bytes.extend_from_slice(self.operation_id.as_bytes());
465 bytes
466 }
467
468 fn from_bytes(bytes: Cow<[u8]>) -> Self {
474 let bytes = <[u8; 40]>::try_from(bytes.as_ref()).unwrap_or_else(|_| {
475 panic!(
476 "stable ApplicationReceiptEligibilityKeyRecord is {} bytes; expected 40",
477 bytes.as_ref().len()
478 )
479 });
480 let (eligible_at_ns, operation_id) = bytes.split_at(8);
481 Self {
482 eligible_at_ns: u64::from_be_bytes(
483 eligible_at_ns
484 .try_into()
485 .expect("eligibility deadline width"),
486 ),
487 operation_id: OperationId::from_bytes(
488 operation_id
489 .try_into()
490 .expect("eligibility operation identity width"),
491 ),
492 }
493 }
494}
495
496#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
498pub struct ApplicationReceiptEligibilityRecord {
499 pub schema_version: u32,
500 pub operation_id: OperationId,
501 pub payload_binding: PayloadBinding,
502 pub terminal_revision: u64,
503}
504
505impl ApplicationReceiptEligibilityRecord {
506 pub const STATE_CONTRACT_NAME: &'static str = "ApplicationReceiptEligibilityRecord";
507 pub const STORABLE_MAX_SIZE: u32 = 229;
508}
509
510impl_storable_bounded!(
511 ApplicationReceiptEligibilityRecord,
512 ApplicationReceiptEligibilityRecord::STORABLE_MAX_SIZE,
513 false
514);
515
516#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
518pub struct PlacementAcknowledgementEntryRecord {
519 pub operation_id: OperationId,
520}
521
522impl PlacementAcknowledgementEntryRecord {
523 pub const STATE_CONTRACT_NAME: &'static str = "PlacementAcknowledgementEntryRecord";
524 pub const STORABLE_MAX_SIZE: u32 = 128;
525}
526
527impl_storable_bounded!(
528 PlacementAcknowledgementEntryRecord,
529 PlacementAcknowledgementEntryRecord::STORABLE_MAX_SIZE,
530 false
531);
532
533#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
540pub struct IntentMetaData {
541 pub record: IntentStoreMetaRecord,
542}
543
544impl IntentMetaData {
545 pub const STATE_CONTRACT_NAME: &'static str = "IntentMetaData";
546}
547
548#[derive(Clone, Debug, Eq, PartialEq)]
555pub struct IntentRecordEntryRecord {
556 pub intent_id: IntentId,
557 pub record: IntentRecord,
558}
559
560#[derive(Clone, Debug, Default, Eq, PartialEq)]
567pub struct IntentRecordsData {
568 pub entries: Vec<IntentRecordEntryRecord>,
569}
570
571impl IntentRecordsData {
572 pub const STATE_CONTRACT_NAME: &'static str = "IntentRecordsData";
573}
574
575#[derive(Clone, Debug, Eq, PartialEq)]
582pub struct IntentTotalsEntryRecord {
583 pub resource_key: IntentResourceKey,
584 pub record: IntentResourceTotalsRecord,
585}
586
587#[derive(Clone, Debug, Default, Eq, PartialEq)]
594pub struct IntentTotalsData {
595 pub entries: Vec<IntentTotalsEntryRecord>,
596}
597
598impl IntentTotalsData {
599 pub const STATE_CONTRACT_NAME: &'static str = "IntentTotalsData";
600}
601
602#[derive(Clone, Debug, Eq, PartialEq)]
609pub struct IntentPendingIndexEntryRecord {
610 pub intent_id: IntentId,
611 pub record: IntentPendingEntryRecord,
612}
613
614#[derive(Clone, Debug, Default, Eq, PartialEq)]
621pub struct IntentPendingData {
622 pub entries: Vec<IntentPendingIndexEntryRecord>,
623}
624
625impl IntentPendingData {
626 pub const STATE_CONTRACT_NAME: &'static str = "IntentPendingData";
627}
628
629#[derive(Clone, Copy, Debug, Eq, PartialEq)]
631pub struct IntentExpiryIndexEntryRecord {
632 pub key: IntentExpiryKeyRecord,
633 pub record: IntentExpiryEntryRecord,
634}
635
636#[derive(Clone, Debug, Default, Eq, PartialEq)]
638pub struct IntentExpiryIndexData {
639 pub entries: Vec<IntentExpiryIndexEntryRecord>,
640}
641
642impl IntentExpiryIndexData {
643 pub const STATE_CONTRACT_NAME: &'static str = "IntentExpiryIndexData";
644}
645
646#[derive(Clone, Debug, Eq, PartialEq)]
648pub struct ReceiptBackedIntentEntryRecord {
649 pub operation_id: OperationId,
650 pub record: ReceiptBackedIntentRecord,
651}
652
653#[derive(Clone, Debug, Default, Eq, PartialEq)]
655pub struct ReceiptBackedIntentsData {
656 pub entries: Vec<ReceiptBackedIntentEntryRecord>,
657}
658
659impl ReceiptBackedIntentsData {
660 pub const STATE_CONTRACT_NAME: &'static str = "ReceiptBackedIntentsData";
661}
662
663#[derive(Clone, Copy, Debug, Eq, PartialEq)]
665pub struct ApplicationReceiptReplayEntryRecord {
666 pub operation_id: OperationId,
667 pub record: ApplicationReceiptReplayRecord,
668}
669
670#[derive(Clone, Debug, Default, Eq, PartialEq)]
672pub struct ApplicationReceiptReplayData {
673 pub entries: Vec<ApplicationReceiptReplayEntryRecord>,
674}
675
676#[derive(Clone, Copy, Debug, Eq, PartialEq)]
678pub struct ApplicationReceiptEligibilityEntryRecord {
679 pub key: ApplicationReceiptEligibilityKeyRecord,
680 pub record: ApplicationReceiptEligibilityRecord,
681}
682
683#[derive(Clone, Debug, Default, Eq, PartialEq)]
685pub struct ApplicationReceiptEligibilityData {
686 pub entries: Vec<ApplicationReceiptEligibilityEntryRecord>,
687}
688
689impl ApplicationReceiptEligibilityData {
690 pub const STATE_CONTRACT_NAME: &'static str = "ApplicationReceiptEligibilityData";
691}
692
693impl ApplicationReceiptReplayData {
694 pub const STATE_CONTRACT_NAME: &'static str = "ApplicationReceiptReplayData";
695}
696
697#[derive(Clone, Copy, Debug, Eq, PartialEq)]
699pub struct PlacementAcknowledgementIndexEntryRecord {
700 pub operation_id: OperationId,
701 pub record: PlacementAcknowledgementEntryRecord,
702}
703
704#[derive(Clone, Debug, Default, Eq, PartialEq)]
706pub struct PlacementAcknowledgementIndexData {
707 pub entries: Vec<PlacementAcknowledgementIndexEntryRecord>,
708}
709
710impl PlacementAcknowledgementIndexData {
711 pub const STATE_CONTRACT_NAME: &'static str = "PlacementAcknowledgementIndexData";
712}
713
714pub struct IntentStore;
719
720impl IntentStore {
721 #[must_use]
726 pub(crate) fn meta() -> IntentStoreMetaRecord {
727 INTENT_META.with_borrow(|cell| *cell.get())
728 }
729
730 pub(crate) fn set_meta(meta: IntentStoreMetaRecord) {
731 INTENT_META.with_borrow_mut(|cell| cell.set(meta));
732 }
733
734 #[must_use]
739 pub(crate) fn get_record(id: IntentId) -> Option<IntentRecord> {
740 INTENT_RECORDS.with_borrow(|map| map.get(&id))
741 }
742
743 pub(crate) fn insert_record(record: IntentRecord) -> Option<IntentRecord> {
744 INTENT_RECORDS.with_borrow_mut(|map| map.insert(record.id, record))
745 }
746
747 #[must_use]
752 pub(crate) fn get_totals(key: &IntentResourceKey) -> Option<IntentResourceTotalsRecord> {
753 INTENT_TOTALS.with_borrow(|map| map.get(key))
754 }
755
756 #[must_use]
757 pub(crate) fn totals_len() -> u64 {
758 INTENT_TOTALS.with_borrow(StableBtreeMap::len)
759 }
760
761 pub(crate) fn set_totals(
762 key: IntentResourceKey,
763 totals: IntentResourceTotalsRecord,
764 ) -> Option<IntentResourceTotalsRecord> {
765 INTENT_TOTALS.with_borrow_mut(|map| map.insert(key, totals))
766 }
767
768 pub(crate) fn remove_totals(key: &IntentResourceKey) -> Option<IntentResourceTotalsRecord> {
769 INTENT_TOTALS.with_borrow_mut(|map| map.remove(key))
770 }
771
772 #[must_use]
777 pub(crate) fn get_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
778 INTENT_PENDING.with_borrow(|map| map.get(&id))
779 }
780
781 pub(crate) fn insert_pending(
782 id: IntentId,
783 entry: IntentPendingEntryRecord,
784 ) -> Option<IntentPendingEntryRecord> {
785 INTENT_PENDING.with_borrow_mut(|map| map.insert(id, entry))
786 }
787
788 pub(crate) fn remove_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
789 INTENT_PENDING.with_borrow_mut(|map| map.remove(&id))
790 }
791
792 pub(crate) fn with_pending_entries<R>(
793 f: impl FnOnce(
794 &StableBtreeMap<IntentId, IntentPendingEntryRecord, VirtualMemory<DefaultMemoryImpl>>,
795 ) -> R,
796 ) -> R {
797 INTENT_PENDING.with_borrow(|map| f(map))
798 }
799
800 #[must_use]
805 pub(crate) fn get_expiry(key: IntentExpiryKeyRecord) -> Option<IntentExpiryEntryRecord> {
806 INTENT_EXPIRY_INDEX.with_borrow(|map| map.get(&key))
807 }
808
809 pub(crate) fn insert_expiry(
810 key: IntentExpiryKeyRecord,
811 record: IntentExpiryEntryRecord,
812 ) -> Option<IntentExpiryEntryRecord> {
813 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| map.insert(key, record))
814 }
815
816 pub(crate) fn remove_expiry(key: IntentExpiryKeyRecord) -> Option<IntentExpiryEntryRecord> {
817 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| map.remove(&key))
818 }
819
820 pub(crate) fn clear_expiry_index() {
821 INTENT_EXPIRY_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
822 }
823
824 pub(crate) fn with_expiry_entries<R>(
825 f: impl FnOnce(
826 &StableBtreeMap<
827 IntentExpiryKeyRecord,
828 IntentExpiryEntryRecord,
829 VirtualMemory<DefaultMemoryImpl>,
830 >,
831 ) -> R,
832 ) -> R {
833 INTENT_EXPIRY_INDEX.with_borrow(|map| f(map))
834 }
835}
836
837pub struct ReceiptBackedIntentStore;
839
840impl ReceiptBackedIntentStore {
841 #[must_use]
842 pub(crate) fn len() -> u64 {
843 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(StableBtreeMap::len)
844 }
845
846 #[must_use]
847 pub(crate) fn get(operation_id: OperationId) -> Option<ReceiptBackedIntentRecord> {
848 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| records.get(&operation_id))
849 }
850
851 pub(crate) fn insert(record: ReceiptBackedIntentRecord) -> Option<ReceiptBackedIntentRecord> {
852 RECEIPT_BACKED_INTENT_RECORDS
853 .with_borrow_mut(|records| records.insert(record.operation_id, record))
854 }
855
856 pub(crate) fn remove(operation_id: OperationId) -> Option<ReceiptBackedIntentRecord> {
857 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(|records| records.remove(&operation_id))
858 }
859
860 pub(crate) fn with_records<R>(
861 f: impl FnOnce(
862 &StableBtreeMap<
863 OperationId,
864 ReceiptBackedIntentRecord,
865 VirtualMemory<DefaultMemoryImpl>,
866 >,
867 ) -> R,
868 ) -> R {
869 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| f(records))
870 }
871
872 #[must_use]
873 pub(crate) fn get_application_replay(
874 operation_id: OperationId,
875 ) -> Option<ApplicationReceiptReplayRecord> {
876 APPLICATION_RECEIPT_REPLAY.with_borrow(|records| records.get(&operation_id))
877 }
878
879 pub(crate) fn insert_application_replay(
880 record: ApplicationReceiptReplayRecord,
881 ) -> Option<ApplicationReceiptReplayRecord> {
882 APPLICATION_RECEIPT_REPLAY
883 .with_borrow_mut(|records| records.insert(record.operation_id, record))
884 }
885
886 pub(crate) fn remove_application_replay(
887 operation_id: OperationId,
888 ) -> Option<ApplicationReceiptReplayRecord> {
889 APPLICATION_RECEIPT_REPLAY.with_borrow_mut(|records| records.remove(&operation_id))
890 }
891
892 pub(crate) fn with_application_replay<R>(
893 f: impl FnOnce(
894 &StableBtreeMap<
895 OperationId,
896 ApplicationReceiptReplayRecord,
897 VirtualMemory<DefaultMemoryImpl>,
898 >,
899 ) -> R,
900 ) -> R {
901 APPLICATION_RECEIPT_REPLAY.with_borrow(|records| f(records))
902 }
903
904 #[must_use]
905 pub(crate) fn application_replay_len() -> u64 {
906 APPLICATION_RECEIPT_REPLAY.with_borrow(StableBtreeMap::len)
907 }
908
909 pub(crate) fn reserve_application_eligibility_capacity(record_count: u64) -> bool {
911 let Some(required_pages) = application_eligibility_required_pages(record_count) else {
912 return false;
913 };
914
915 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| {
916 let current_pages = state.1.size();
917 current_pages >= required_pages || state.1.grow(required_pages - current_pages) >= 0
918 })
919 }
920
921 #[must_use]
922 pub(crate) fn get_application_eligibility(
923 key: ApplicationReceiptEligibilityKeyRecord,
924 ) -> Option<ApplicationReceiptEligibilityRecord> {
925 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| state.0.get(&key))
926 }
927
928 pub(crate) fn insert_application_eligibility(
929 key: ApplicationReceiptEligibilityKeyRecord,
930 record: ApplicationReceiptEligibilityRecord,
931 ) -> Option<ApplicationReceiptEligibilityRecord> {
932 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow_mut(|state| state.0.insert(key, record))
933 }
934
935 pub(crate) fn remove_application_eligibility(
936 key: ApplicationReceiptEligibilityKeyRecord,
937 ) -> Option<ApplicationReceiptEligibilityRecord> {
938 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow_mut(|state| state.0.remove(&key))
939 }
940
941 pub(crate) fn with_application_eligibility<R>(
942 f: impl FnOnce(
943 &StableBtreeMap<
944 ApplicationReceiptEligibilityKeyRecord,
945 ApplicationReceiptEligibilityRecord,
946 VirtualMemory<DefaultMemoryImpl>,
947 >,
948 ) -> R,
949 ) -> R {
950 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| f(&state.0))
951 }
952
953 #[must_use]
954 pub(crate) fn first_application_eligibility() -> Option<(
955 ApplicationReceiptEligibilityKeyRecord,
956 ApplicationReceiptEligibilityRecord,
957 )> {
958 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| {
959 state
960 .0
961 .iter()
962 .next()
963 .map(|entry| (*entry.key(), entry.value()))
964 })
965 }
966
967 #[must_use]
968 pub(crate) fn application_eligibility_reserved_pages() -> u64 {
969 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| state.1.size())
970 }
971
972 #[must_use]
973 pub(crate) fn get_placement_acknowledgement(
974 operation_id: OperationId,
975 ) -> Option<PlacementAcknowledgementEntryRecord> {
976 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| index.get(&operation_id))
977 }
978
979 pub(crate) fn insert_placement_acknowledgement(
980 record: PlacementAcknowledgementEntryRecord,
981 ) -> Option<PlacementAcknowledgementEntryRecord> {
982 PLACEMENT_ACKNOWLEDGEMENT_INDEX
983 .with_borrow_mut(|index| index.insert(record.operation_id, record))
984 }
985
986 pub(crate) fn remove_placement_acknowledgement(
987 operation_id: OperationId,
988 ) -> Option<PlacementAcknowledgementEntryRecord> {
989 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(|index| index.remove(&operation_id))
990 }
991
992 pub(crate) fn clear_placement_acknowledgement_index() {
993 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
994 }
995
996 pub(crate) fn with_placement_acknowledgements<R>(
997 f: impl FnOnce(
998 &StableBtreeMap<
999 OperationId,
1000 PlacementAcknowledgementEntryRecord,
1001 VirtualMemory<DefaultMemoryImpl>,
1002 >,
1003 ) -> R,
1004 ) -> R {
1005 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| f(index))
1006 }
1007}
1008
1009pub(super) fn application_eligibility_required_pages(record_count: u64) -> Option<u64> {
1010 let maximum_nodes = record_count
1015 .checked_add(APPLICATION_RECEIPT_ELIGIBILITY_MIN_NODE_ENTRIES - 1)?
1016 / APPLICATION_RECEIPT_ELIGIBILITY_MIN_NODE_ENTRIES;
1017 let required_bytes = APPLICATION_RECEIPT_ELIGIBILITY_FIXED_BYTES
1018 .checked_add(maximum_nodes.checked_mul(APPLICATION_RECEIPT_ELIGIBILITY_CHUNK_BYTES)?)?
1019 .checked_add(WASM_PAGE_BYTES - 1)?;
1020 Some(required_bytes / WASM_PAGE_BYTES)
1021}
1022
1023#[cfg(test)]
1030impl IntentStore {
1031 #[must_use]
1032 pub(crate) fn export_meta() -> IntentMetaData {
1033 IntentMetaData {
1034 record: Self::meta(),
1035 }
1036 }
1037
1038 pub(crate) fn import_meta(data: IntentMetaData) {
1039 Self::set_meta(data.record);
1040 }
1041
1042 #[must_use]
1043 pub(crate) fn export_records() -> IntentRecordsData {
1044 IntentRecordsData {
1045 entries: INTENT_RECORDS.with_borrow(|map| {
1046 map.iter()
1047 .map(|entry| IntentRecordEntryRecord {
1048 intent_id: *entry.key(),
1049 record: entry.value(),
1050 })
1051 .collect()
1052 }),
1053 }
1054 }
1055
1056 pub(crate) fn import_records(data: IntentRecordsData) {
1057 INTENT_RECORDS.with_borrow_mut(|map| {
1058 map.clear_new();
1059 for entry in data.entries {
1060 map.insert(entry.intent_id, entry.record);
1061 }
1062 });
1063 }
1064
1065 #[must_use]
1066 pub(crate) fn export_totals() -> IntentTotalsData {
1067 IntentTotalsData {
1068 entries: INTENT_TOTALS.with_borrow(|map| {
1069 map.iter()
1070 .map(|entry| IntentTotalsEntryRecord {
1071 resource_key: entry.key().clone(),
1072 record: entry.value(),
1073 })
1074 .collect()
1075 }),
1076 }
1077 }
1078
1079 pub(crate) fn import_totals(data: IntentTotalsData) {
1080 INTENT_TOTALS.with_borrow_mut(|map| {
1081 map.clear_new();
1082 for entry in data.entries {
1083 map.insert(entry.resource_key, entry.record);
1084 }
1085 });
1086 }
1087
1088 #[must_use]
1089 pub(crate) fn export_pending() -> IntentPendingData {
1090 IntentPendingData {
1091 entries: INTENT_PENDING.with_borrow(|map| {
1092 map.iter()
1093 .map(|entry| IntentPendingIndexEntryRecord {
1094 intent_id: *entry.key(),
1095 record: entry.value(),
1096 })
1097 .collect()
1098 }),
1099 }
1100 }
1101
1102 pub(crate) fn import_pending(data: IntentPendingData) {
1103 INTENT_PENDING.with_borrow_mut(|map| {
1104 map.clear_new();
1105 for entry in data.entries {
1106 map.insert(entry.intent_id, entry.record);
1107 }
1108 });
1109 }
1110
1111 #[must_use]
1112 pub(crate) fn export_expiry_index() -> IntentExpiryIndexData {
1113 IntentExpiryIndexData {
1114 entries: INTENT_EXPIRY_INDEX.with_borrow(|map| {
1115 map.iter()
1116 .map(|entry| IntentExpiryIndexEntryRecord {
1117 key: *entry.key(),
1118 record: entry.value(),
1119 })
1120 .collect()
1121 }),
1122 }
1123 }
1124
1125 pub(crate) fn import_expiry_index(data: IntentExpiryIndexData) {
1126 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| {
1127 map.clear_new();
1128 for entry in data.entries {
1129 map.insert(entry.key, entry.record);
1130 }
1131 });
1132 }
1133
1134 pub(crate) fn reset_for_tests() {
1135 INTENT_RECORDS.with_borrow_mut(StableBtreeMap::clear_new);
1136 INTENT_TOTALS.with_borrow_mut(StableBtreeMap::clear_new);
1137 INTENT_PENDING.with_borrow_mut(StableBtreeMap::clear_new);
1138 INTENT_EXPIRY_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
1139 INTENT_META.with_borrow_mut(|cell| cell.set(IntentStoreMetaRecord::default()));
1140 ReceiptBackedIntentStore::reset_for_tests();
1141 }
1142}
1143
1144#[cfg(test)]
1145impl ReceiptBackedIntentStore {
1146 #[must_use]
1147 pub(crate) fn export_records() -> ReceiptBackedIntentsData {
1148 ReceiptBackedIntentsData {
1149 entries: RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| {
1150 records
1151 .iter()
1152 .map(|entry| ReceiptBackedIntentEntryRecord {
1153 operation_id: *entry.key(),
1154 record: entry.value(),
1155 })
1156 .collect()
1157 }),
1158 }
1159 }
1160
1161 pub(crate) fn import_records(data: ReceiptBackedIntentsData) {
1162 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(|records| {
1163 records.clear_new();
1164 for entry in data.entries {
1165 records.insert(entry.operation_id, entry.record);
1166 }
1167 });
1168 }
1169
1170 #[must_use]
1171 pub(crate) fn export_application_replay() -> ApplicationReceiptReplayData {
1172 ApplicationReceiptReplayData {
1173 entries: APPLICATION_RECEIPT_REPLAY.with_borrow(|records| {
1174 records
1175 .iter()
1176 .map(|entry| ApplicationReceiptReplayEntryRecord {
1177 operation_id: *entry.key(),
1178 record: entry.value(),
1179 })
1180 .collect()
1181 }),
1182 }
1183 }
1184
1185 #[must_use]
1186 pub(crate) fn export_application_eligibility() -> ApplicationReceiptEligibilityData {
1187 ApplicationReceiptEligibilityData {
1188 entries: APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| {
1189 state
1190 .0
1191 .iter()
1192 .map(|entry| ApplicationReceiptEligibilityEntryRecord {
1193 key: *entry.key(),
1194 record: entry.value(),
1195 })
1196 .collect()
1197 }),
1198 }
1199 }
1200
1201 pub(crate) fn import_application_eligibility(data: ApplicationReceiptEligibilityData) {
1202 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow_mut(|state| {
1203 state.0.clear_new();
1204 for entry in data.entries {
1205 state.0.insert(entry.key, entry.record);
1206 }
1207 });
1208 }
1209
1210 pub(crate) fn import_application_replay(data: ApplicationReceiptReplayData) {
1211 APPLICATION_RECEIPT_REPLAY.with_borrow_mut(|records| {
1212 records.clear_new();
1213 for entry in data.entries {
1214 records.insert(entry.operation_id, entry.record);
1215 }
1216 });
1217 }
1218
1219 #[must_use]
1220 pub(crate) fn export_placement_acknowledgement_index() -> PlacementAcknowledgementIndexData {
1221 PlacementAcknowledgementIndexData {
1222 entries: PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| {
1223 index
1224 .iter()
1225 .map(|entry| PlacementAcknowledgementIndexEntryRecord {
1226 operation_id: *entry.key(),
1227 record: entry.value(),
1228 })
1229 .collect()
1230 }),
1231 }
1232 }
1233
1234 pub(crate) fn import_placement_acknowledgement_index(data: PlacementAcknowledgementIndexData) {
1235 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(|index| {
1236 index.clear_new();
1237 for entry in data.entries {
1238 index.insert(entry.operation_id, entry.record);
1239 }
1240 });
1241 }
1242
1243 pub(crate) fn reset_for_tests() {
1244 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(StableBtreeMap::clear_new);
1245 APPLICATION_RECEIPT_REPLAY.with_borrow_mut(StableBtreeMap::clear_new);
1246 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow_mut(|state| state.0.clear_new());
1247 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
1248 }
1249}
1250
1251#[cfg(test)]
1252mod tests {
1253 use super::*;
1254 use crate::{
1255 cdk::types::Principal,
1256 model::intent::{
1257 RECEIPT_BACKED_INTENT_SCHEMA_VERSION, TerminalEvidence, TerminalEvidenceDecision,
1258 },
1259 };
1260
1261 #[test]
1262 #[should_panic(expected = "stable IntentId is 7 bytes; expected 8")]
1263 fn malformed_stable_intent_id_fails_closed() {
1264 let _ = <IntentId as Storable>::from_bytes(Cow::Owned(vec![0; 7]));
1265 }
1266
1267 #[test]
1268 #[should_panic(expected = "stable OperationId is 31 bytes; expected 32")]
1269 fn malformed_stable_operation_id_fails_closed() {
1270 let _ = <OperationId as Storable>::from_bytes(Cow::Owned(vec![0; 31]));
1271 }
1272
1273 #[test]
1274 #[should_panic(
1275 expected = "stable ApplicationReceiptEligibilityKeyRecord is 39 bytes; expected 40"
1276 )]
1277 fn malformed_stable_application_eligibility_key_fails_closed() {
1278 let _ = ApplicationReceiptEligibilityKeyRecord::from_bytes(Cow::Owned(vec![0; 39]));
1279 }
1280
1281 #[test]
1282 fn application_eligibility_capacity_reservation_is_conservative_and_bounded() {
1283 assert_eq!(application_eligibility_required_pages(0), Some(1));
1284 assert_eq!(application_eligibility_required_pages(1), Some(1));
1285 assert_eq!(application_eligibility_required_pages(1_000), Some(8));
1286 assert_eq!(application_eligibility_required_pages(u64::MAX), None);
1287 assert!(!ReceiptBackedIntentStore::reserve_application_eligibility_capacity(u64::MAX));
1288 }
1289
1290 #[test]
1291 #[should_panic(expected = "stable IntentExpiryKeyRecord is 15 bytes; expected 16")]
1292 fn malformed_stable_intent_expiry_key_fails_closed() {
1293 let _ = <IntentExpiryKeyRecord as Storable>::from_bytes(Cow::Owned(vec![0; 15]));
1294 }
1295
1296 #[test]
1297 fn stable_intent_expiry_key_preserves_deadline_then_identity_order() {
1298 let keys = [
1299 IntentExpiryKeyRecord {
1300 due_at_secs: 11,
1301 intent_id: IntentId(1),
1302 },
1303 IntentExpiryKeyRecord {
1304 due_at_secs: 10,
1305 intent_id: IntentId(2),
1306 },
1307 IntentExpiryKeyRecord {
1308 due_at_secs: 10,
1309 intent_id: IntentId(1),
1310 },
1311 ];
1312 let mut encoded = keys.map(Storable::into_bytes);
1313 encoded.sort();
1314 assert_eq!(
1315 encoded,
1316 [
1317 keys[2].into_bytes(),
1318 keys[1].into_bytes(),
1319 keys[0].into_bytes()
1320 ]
1321 );
1322 }
1323
1324 #[test]
1325 fn intent_allocations_round_trip_through_canonical_data_snapshots() {
1326 IntentStore::reset_for_tests();
1327 let intent_id = IntentId(7);
1328 let resource_key = IntentResourceKey::new("storage:uploads");
1329 let record = IntentRecord {
1330 id: intent_id,
1331 resource_key: resource_key.clone(),
1332 quantity: 11,
1333 state: IntentState::Pending,
1334 created_at: 13,
1335 ttl_secs: Some(17),
1336 };
1337 let totals = IntentResourceTotalsRecord {
1338 reserved_qty: 11,
1339 committed_qty: 19,
1340 pending_count: 1,
1341 };
1342 let pending = IntentPendingEntryRecord {
1343 resource_key: resource_key.clone(),
1344 quantity: 11,
1345 created_at: 13,
1346 ttl_secs: Some(17),
1347 };
1348 let meta = IntentStoreMetaRecord {
1349 schema_version: INTENT_STORE_SCHEMA_VERSION,
1350 next_intent_id: IntentId(8),
1351 pending_total: 1,
1352 committed_total: 2,
1353 aborted_total: 3,
1354 };
1355
1356 IntentStore::set_meta(meta);
1357 IntentStore::insert_record(record);
1358 IntentStore::set_totals(resource_key, totals);
1359 IntentStore::insert_pending(intent_id, pending);
1360 let expiry_key = IntentExpiryKeyRecord {
1361 due_at_secs: 31,
1362 intent_id,
1363 };
1364 IntentStore::insert_expiry(expiry_key, IntentExpiryEntryRecord { intent_id });
1365
1366 let meta_data = IntentStore::export_meta();
1367 let records_data = IntentStore::export_records();
1368 let totals_data = IntentStore::export_totals();
1369 let pending_data = IntentStore::export_pending();
1370 let expiry_data = IntentStore::export_expiry_index();
1371
1372 IntentStore::reset_for_tests();
1373 IntentStore::import_meta(meta_data);
1374 IntentStore::import_records(records_data.clone());
1375 IntentStore::import_totals(totals_data.clone());
1376 IntentStore::import_pending(pending_data.clone());
1377 IntentStore::import_expiry_index(expiry_data.clone());
1378
1379 assert_eq!(IntentStore::export_meta(), meta_data);
1380 assert_eq!(IntentStore::export_records(), records_data);
1381 assert_eq!(IntentStore::export_totals(), totals_data);
1382 assert_eq!(IntentStore::export_pending(), pending_data);
1383 assert_eq!(IntentStore::export_expiry_index(), expiry_data);
1384 IntentStore::reset_for_tests();
1385 }
1386
1387 #[test]
1388 fn receipt_backed_allocations_round_trip_through_canonical_data_snapshots() {
1389 IntentStore::reset_for_tests();
1390 let application_operation_id = OperationId::from_bytes([7; 32]);
1391 let placement_operation_id = OperationId::from_bytes([8; 32]);
1392 let evidence = TerminalEvidence::new(
1393 Principal::from_slice(&[1; 29]),
1394 TerminalEvidenceDecision::Committed,
1395 [8; 32],
1396 );
1397 let record = ReceiptBackedIntentRecord {
1398 schema_version: RECEIPT_BACKED_INTENT_SCHEMA_VERSION,
1399 operation_id: application_operation_id,
1400 payload_binding: PayloadBinding::new([9; 32]),
1401 resource_key: IntentResourceKey::new("mint:collection"),
1402 quantity: 11,
1403 state: ReceiptBackedIntentState::Committed { evidence },
1404 revision: 2,
1405 created_at_ns: 13,
1406 updated_at_ns: 17,
1407 };
1408 ReceiptBackedIntentStore::insert(record);
1409 ReceiptBackedIntentStore::insert(ReceiptBackedIntentRecord {
1410 schema_version: RECEIPT_BACKED_INTENT_SCHEMA_VERSION,
1411 operation_id: placement_operation_id,
1412 payload_binding: PayloadBinding::new([10; 32]),
1413 resource_key: IntentResourceKey::new(format!("canic:placement:{}", "a".repeat(64))),
1414 quantity: 1,
1415 state: ReceiptBackedIntentState::Committed { evidence },
1416 revision: 2,
1417 created_at_ns: 13,
1418 updated_at_ns: 17,
1419 });
1420 ReceiptBackedIntentStore::insert_application_replay(ApplicationReceiptReplayRecord {
1421 schema_version: APPLICATION_RECEIPT_REPLAY_SCHEMA_VERSION,
1422 operation_id: application_operation_id,
1423 replay_deadline_ns: 23,
1424 });
1425 let eligibility_key = ApplicationReceiptEligibilityKeyRecord {
1426 eligible_at_ns: 17 + crate::model::intent::RECEIPT_TERMINAL_OBSERVATION_GRACE_NS,
1427 operation_id: application_operation_id,
1428 };
1429 ReceiptBackedIntentStore::insert_application_eligibility(
1430 eligibility_key,
1431 ApplicationReceiptEligibilityRecord {
1432 schema_version: APPLICATION_RECEIPT_ELIGIBILITY_SCHEMA_VERSION,
1433 operation_id: application_operation_id,
1434 payload_binding: PayloadBinding::new([9; 32]),
1435 terminal_revision: 2,
1436 },
1437 );
1438 ReceiptBackedIntentStore::insert_placement_acknowledgement(
1439 PlacementAcknowledgementEntryRecord {
1440 operation_id: placement_operation_id,
1441 },
1442 );
1443 let records_data = ReceiptBackedIntentStore::export_records();
1444 let replay_data = ReceiptBackedIntentStore::export_application_replay();
1445 let eligibility_data = ReceiptBackedIntentStore::export_application_eligibility();
1446 let acknowledgement_data =
1447 ReceiptBackedIntentStore::export_placement_acknowledgement_index();
1448
1449 ReceiptBackedIntentStore::reset_for_tests();
1450 assert_eq!(
1451 ReceiptBackedIntentStore::export_records(),
1452 ReceiptBackedIntentsData::default()
1453 );
1454 assert_eq!(
1455 ReceiptBackedIntentStore::export_application_replay(),
1456 ApplicationReceiptReplayData::default()
1457 );
1458 assert_eq!(
1459 ReceiptBackedIntentStore::export_application_eligibility(),
1460 ApplicationReceiptEligibilityData::default()
1461 );
1462 assert_eq!(
1463 ReceiptBackedIntentStore::export_placement_acknowledgement_index(),
1464 PlacementAcknowledgementIndexData::default()
1465 );
1466
1467 ReceiptBackedIntentStore::import_records(records_data.clone());
1468 ReceiptBackedIntentStore::import_application_replay(replay_data.clone());
1469 ReceiptBackedIntentStore::import_application_eligibility(eligibility_data.clone());
1470 ReceiptBackedIntentStore::import_placement_acknowledgement_index(
1471 acknowledgement_data.clone(),
1472 );
1473 assert_eq!(ReceiptBackedIntentStore::len(), 2);
1474 assert_eq!(ReceiptBackedIntentStore::export_records(), records_data);
1475 assert_eq!(
1476 ReceiptBackedIntentStore::export_application_replay(),
1477 replay_data
1478 );
1479 assert_eq!(
1480 ReceiptBackedIntentStore::export_application_eligibility(),
1481 eligibility_data
1482 );
1483 assert_eq!(
1484 ReceiptBackedIntentStore::export_placement_acknowledgement_index(),
1485 acknowledgement_data
1486 );
1487 IntentStore::reset_for_tests();
1488 }
1489}