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 pub(crate) fn set_totals(
757 key: IntentResourceKey,
758 totals: IntentResourceTotalsRecord,
759 ) -> Option<IntentResourceTotalsRecord> {
760 INTENT_TOTALS.with_borrow_mut(|map| map.insert(key, totals))
761 }
762
763 pub(crate) fn remove_totals(key: &IntentResourceKey) -> Option<IntentResourceTotalsRecord> {
764 INTENT_TOTALS.with_borrow_mut(|map| map.remove(key))
765 }
766
767 #[must_use]
772 pub(crate) fn get_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
773 INTENT_PENDING.with_borrow(|map| map.get(&id))
774 }
775
776 pub(crate) fn insert_pending(
777 id: IntentId,
778 entry: IntentPendingEntryRecord,
779 ) -> Option<IntentPendingEntryRecord> {
780 INTENT_PENDING.with_borrow_mut(|map| map.insert(id, entry))
781 }
782
783 pub(crate) fn remove_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
784 INTENT_PENDING.with_borrow_mut(|map| map.remove(&id))
785 }
786
787 pub(crate) fn with_pending_entries<R>(
788 f: impl FnOnce(
789 &StableBtreeMap<IntentId, IntentPendingEntryRecord, VirtualMemory<DefaultMemoryImpl>>,
790 ) -> R,
791 ) -> R {
792 INTENT_PENDING.with_borrow(|map| f(map))
793 }
794
795 #[must_use]
800 pub(crate) fn get_expiry(key: IntentExpiryKeyRecord) -> Option<IntentExpiryEntryRecord> {
801 INTENT_EXPIRY_INDEX.with_borrow(|map| map.get(&key))
802 }
803
804 pub(crate) fn insert_expiry(
805 key: IntentExpiryKeyRecord,
806 record: IntentExpiryEntryRecord,
807 ) -> Option<IntentExpiryEntryRecord> {
808 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| map.insert(key, record))
809 }
810
811 pub(crate) fn remove_expiry(key: IntentExpiryKeyRecord) -> Option<IntentExpiryEntryRecord> {
812 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| map.remove(&key))
813 }
814
815 pub(crate) fn clear_expiry_index() {
816 INTENT_EXPIRY_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
817 }
818
819 pub(crate) fn with_expiry_entries<R>(
820 f: impl FnOnce(
821 &StableBtreeMap<
822 IntentExpiryKeyRecord,
823 IntentExpiryEntryRecord,
824 VirtualMemory<DefaultMemoryImpl>,
825 >,
826 ) -> R,
827 ) -> R {
828 INTENT_EXPIRY_INDEX.with_borrow(|map| f(map))
829 }
830}
831
832pub struct ReceiptBackedIntentStore;
834
835impl ReceiptBackedIntentStore {
836 #[must_use]
837 pub(crate) fn len() -> u64 {
838 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(StableBtreeMap::len)
839 }
840
841 #[must_use]
842 pub(crate) fn get(operation_id: OperationId) -> Option<ReceiptBackedIntentRecord> {
843 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| records.get(&operation_id))
844 }
845
846 pub(crate) fn insert(record: ReceiptBackedIntentRecord) -> Option<ReceiptBackedIntentRecord> {
847 RECEIPT_BACKED_INTENT_RECORDS
848 .with_borrow_mut(|records| records.insert(record.operation_id, record))
849 }
850
851 pub(crate) fn remove(operation_id: OperationId) -> Option<ReceiptBackedIntentRecord> {
852 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(|records| records.remove(&operation_id))
853 }
854
855 pub(crate) fn with_records<R>(
856 f: impl FnOnce(
857 &StableBtreeMap<
858 OperationId,
859 ReceiptBackedIntentRecord,
860 VirtualMemory<DefaultMemoryImpl>,
861 >,
862 ) -> R,
863 ) -> R {
864 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| f(records))
865 }
866
867 #[must_use]
868 pub(crate) fn get_application_replay(
869 operation_id: OperationId,
870 ) -> Option<ApplicationReceiptReplayRecord> {
871 APPLICATION_RECEIPT_REPLAY.with_borrow(|records| records.get(&operation_id))
872 }
873
874 pub(crate) fn insert_application_replay(
875 record: ApplicationReceiptReplayRecord,
876 ) -> Option<ApplicationReceiptReplayRecord> {
877 APPLICATION_RECEIPT_REPLAY
878 .with_borrow_mut(|records| records.insert(record.operation_id, record))
879 }
880
881 pub(crate) fn remove_application_replay(
882 operation_id: OperationId,
883 ) -> Option<ApplicationReceiptReplayRecord> {
884 APPLICATION_RECEIPT_REPLAY.with_borrow_mut(|records| records.remove(&operation_id))
885 }
886
887 pub(crate) fn with_application_replay<R>(
888 f: impl FnOnce(
889 &StableBtreeMap<
890 OperationId,
891 ApplicationReceiptReplayRecord,
892 VirtualMemory<DefaultMemoryImpl>,
893 >,
894 ) -> R,
895 ) -> R {
896 APPLICATION_RECEIPT_REPLAY.with_borrow(|records| f(records))
897 }
898
899 #[must_use]
900 pub(crate) fn application_replay_len() -> u64 {
901 APPLICATION_RECEIPT_REPLAY.with_borrow(StableBtreeMap::len)
902 }
903
904 pub(crate) fn reserve_application_eligibility_capacity(record_count: u64) -> bool {
906 let Some(required_pages) = application_eligibility_required_pages(record_count) else {
907 return false;
908 };
909
910 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| {
911 let current_pages = state.1.size();
912 current_pages >= required_pages || state.1.grow(required_pages - current_pages) >= 0
913 })
914 }
915
916 #[must_use]
917 pub(crate) fn get_application_eligibility(
918 key: ApplicationReceiptEligibilityKeyRecord,
919 ) -> Option<ApplicationReceiptEligibilityRecord> {
920 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| state.0.get(&key))
921 }
922
923 pub(crate) fn insert_application_eligibility(
924 key: ApplicationReceiptEligibilityKeyRecord,
925 record: ApplicationReceiptEligibilityRecord,
926 ) -> Option<ApplicationReceiptEligibilityRecord> {
927 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow_mut(|state| state.0.insert(key, record))
928 }
929
930 pub(crate) fn remove_application_eligibility(
931 key: ApplicationReceiptEligibilityKeyRecord,
932 ) -> Option<ApplicationReceiptEligibilityRecord> {
933 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow_mut(|state| state.0.remove(&key))
934 }
935
936 pub(crate) fn with_application_eligibility<R>(
937 f: impl FnOnce(
938 &StableBtreeMap<
939 ApplicationReceiptEligibilityKeyRecord,
940 ApplicationReceiptEligibilityRecord,
941 VirtualMemory<DefaultMemoryImpl>,
942 >,
943 ) -> R,
944 ) -> R {
945 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| f(&state.0))
946 }
947
948 #[must_use]
949 pub(crate) fn first_application_eligibility() -> Option<(
950 ApplicationReceiptEligibilityKeyRecord,
951 ApplicationReceiptEligibilityRecord,
952 )> {
953 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| {
954 state
955 .0
956 .iter()
957 .next()
958 .map(|entry| (*entry.key(), entry.value()))
959 })
960 }
961
962 #[must_use]
963 pub(crate) fn application_eligibility_reserved_pages() -> u64 {
964 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| state.1.size())
965 }
966
967 #[must_use]
968 pub(crate) fn get_placement_acknowledgement(
969 operation_id: OperationId,
970 ) -> Option<PlacementAcknowledgementEntryRecord> {
971 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| index.get(&operation_id))
972 }
973
974 pub(crate) fn insert_placement_acknowledgement(
975 record: PlacementAcknowledgementEntryRecord,
976 ) -> Option<PlacementAcknowledgementEntryRecord> {
977 PLACEMENT_ACKNOWLEDGEMENT_INDEX
978 .with_borrow_mut(|index| index.insert(record.operation_id, record))
979 }
980
981 pub(crate) fn remove_placement_acknowledgement(
982 operation_id: OperationId,
983 ) -> Option<PlacementAcknowledgementEntryRecord> {
984 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(|index| index.remove(&operation_id))
985 }
986
987 pub(crate) fn clear_placement_acknowledgement_index() {
988 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
989 }
990
991 pub(crate) fn with_placement_acknowledgements<R>(
992 f: impl FnOnce(
993 &StableBtreeMap<
994 OperationId,
995 PlacementAcknowledgementEntryRecord,
996 VirtualMemory<DefaultMemoryImpl>,
997 >,
998 ) -> R,
999 ) -> R {
1000 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| f(index))
1001 }
1002}
1003
1004pub(super) fn application_eligibility_required_pages(record_count: u64) -> Option<u64> {
1005 let maximum_nodes = record_count
1010 .checked_add(APPLICATION_RECEIPT_ELIGIBILITY_MIN_NODE_ENTRIES - 1)?
1011 / APPLICATION_RECEIPT_ELIGIBILITY_MIN_NODE_ENTRIES;
1012 let required_bytes = APPLICATION_RECEIPT_ELIGIBILITY_FIXED_BYTES
1013 .checked_add(maximum_nodes.checked_mul(APPLICATION_RECEIPT_ELIGIBILITY_CHUNK_BYTES)?)?
1014 .checked_add(WASM_PAGE_BYTES - 1)?;
1015 Some(required_bytes / WASM_PAGE_BYTES)
1016}
1017
1018#[cfg(test)]
1025impl IntentStore {
1026 #[must_use]
1027 pub(crate) fn export_meta() -> IntentMetaData {
1028 IntentMetaData {
1029 record: Self::meta(),
1030 }
1031 }
1032
1033 pub(crate) fn import_meta(data: IntentMetaData) {
1034 Self::set_meta(data.record);
1035 }
1036
1037 #[must_use]
1038 pub(crate) fn export_records() -> IntentRecordsData {
1039 IntentRecordsData {
1040 entries: INTENT_RECORDS.with_borrow(|map| {
1041 map.iter()
1042 .map(|entry| IntentRecordEntryRecord {
1043 intent_id: *entry.key(),
1044 record: entry.value(),
1045 })
1046 .collect()
1047 }),
1048 }
1049 }
1050
1051 pub(crate) fn import_records(data: IntentRecordsData) {
1052 INTENT_RECORDS.with_borrow_mut(|map| {
1053 map.clear_new();
1054 for entry in data.entries {
1055 map.insert(entry.intent_id, entry.record);
1056 }
1057 });
1058 }
1059
1060 #[must_use]
1061 pub(crate) fn export_totals() -> IntentTotalsData {
1062 IntentTotalsData {
1063 entries: INTENT_TOTALS.with_borrow(|map| {
1064 map.iter()
1065 .map(|entry| IntentTotalsEntryRecord {
1066 resource_key: entry.key().clone(),
1067 record: entry.value(),
1068 })
1069 .collect()
1070 }),
1071 }
1072 }
1073
1074 pub(crate) fn import_totals(data: IntentTotalsData) {
1075 INTENT_TOTALS.with_borrow_mut(|map| {
1076 map.clear_new();
1077 for entry in data.entries {
1078 map.insert(entry.resource_key, entry.record);
1079 }
1080 });
1081 }
1082
1083 #[must_use]
1084 pub(crate) fn export_pending() -> IntentPendingData {
1085 IntentPendingData {
1086 entries: INTENT_PENDING.with_borrow(|map| {
1087 map.iter()
1088 .map(|entry| IntentPendingIndexEntryRecord {
1089 intent_id: *entry.key(),
1090 record: entry.value(),
1091 })
1092 .collect()
1093 }),
1094 }
1095 }
1096
1097 pub(crate) fn import_pending(data: IntentPendingData) {
1098 INTENT_PENDING.with_borrow_mut(|map| {
1099 map.clear_new();
1100 for entry in data.entries {
1101 map.insert(entry.intent_id, entry.record);
1102 }
1103 });
1104 }
1105
1106 #[must_use]
1107 pub(crate) fn export_expiry_index() -> IntentExpiryIndexData {
1108 IntentExpiryIndexData {
1109 entries: INTENT_EXPIRY_INDEX.with_borrow(|map| {
1110 map.iter()
1111 .map(|entry| IntentExpiryIndexEntryRecord {
1112 key: *entry.key(),
1113 record: entry.value(),
1114 })
1115 .collect()
1116 }),
1117 }
1118 }
1119
1120 pub(crate) fn import_expiry_index(data: IntentExpiryIndexData) {
1121 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| {
1122 map.clear_new();
1123 for entry in data.entries {
1124 map.insert(entry.key, entry.record);
1125 }
1126 });
1127 }
1128
1129 pub(crate) fn reset_for_tests() {
1130 INTENT_RECORDS.with_borrow_mut(StableBtreeMap::clear_new);
1131 INTENT_TOTALS.with_borrow_mut(StableBtreeMap::clear_new);
1132 INTENT_PENDING.with_borrow_mut(StableBtreeMap::clear_new);
1133 INTENT_EXPIRY_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
1134 INTENT_META.with_borrow_mut(|cell| cell.set(IntentStoreMetaRecord::default()));
1135 ReceiptBackedIntentStore::reset_for_tests();
1136 }
1137}
1138
1139#[cfg(test)]
1140impl ReceiptBackedIntentStore {
1141 #[must_use]
1142 pub(crate) fn export_records() -> ReceiptBackedIntentsData {
1143 ReceiptBackedIntentsData {
1144 entries: RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| {
1145 records
1146 .iter()
1147 .map(|entry| ReceiptBackedIntentEntryRecord {
1148 operation_id: *entry.key(),
1149 record: entry.value(),
1150 })
1151 .collect()
1152 }),
1153 }
1154 }
1155
1156 pub(crate) fn import_records(data: ReceiptBackedIntentsData) {
1157 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(|records| {
1158 records.clear_new();
1159 for entry in data.entries {
1160 records.insert(entry.operation_id, entry.record);
1161 }
1162 });
1163 }
1164
1165 #[must_use]
1166 pub(crate) fn export_application_replay() -> ApplicationReceiptReplayData {
1167 ApplicationReceiptReplayData {
1168 entries: APPLICATION_RECEIPT_REPLAY.with_borrow(|records| {
1169 records
1170 .iter()
1171 .map(|entry| ApplicationReceiptReplayEntryRecord {
1172 operation_id: *entry.key(),
1173 record: entry.value(),
1174 })
1175 .collect()
1176 }),
1177 }
1178 }
1179
1180 #[must_use]
1181 pub(crate) fn export_application_eligibility() -> ApplicationReceiptEligibilityData {
1182 ApplicationReceiptEligibilityData {
1183 entries: APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| {
1184 state
1185 .0
1186 .iter()
1187 .map(|entry| ApplicationReceiptEligibilityEntryRecord {
1188 key: *entry.key(),
1189 record: entry.value(),
1190 })
1191 .collect()
1192 }),
1193 }
1194 }
1195
1196 pub(crate) fn import_application_eligibility(data: ApplicationReceiptEligibilityData) {
1197 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow_mut(|state| {
1198 state.0.clear_new();
1199 for entry in data.entries {
1200 state.0.insert(entry.key, entry.record);
1201 }
1202 });
1203 }
1204
1205 pub(crate) fn import_application_replay(data: ApplicationReceiptReplayData) {
1206 APPLICATION_RECEIPT_REPLAY.with_borrow_mut(|records| {
1207 records.clear_new();
1208 for entry in data.entries {
1209 records.insert(entry.operation_id, entry.record);
1210 }
1211 });
1212 }
1213
1214 #[must_use]
1215 pub(crate) fn export_placement_acknowledgement_index() -> PlacementAcknowledgementIndexData {
1216 PlacementAcknowledgementIndexData {
1217 entries: PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| {
1218 index
1219 .iter()
1220 .map(|entry| PlacementAcknowledgementIndexEntryRecord {
1221 operation_id: *entry.key(),
1222 record: entry.value(),
1223 })
1224 .collect()
1225 }),
1226 }
1227 }
1228
1229 pub(crate) fn import_placement_acknowledgement_index(data: PlacementAcknowledgementIndexData) {
1230 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(|index| {
1231 index.clear_new();
1232 for entry in data.entries {
1233 index.insert(entry.operation_id, entry.record);
1234 }
1235 });
1236 }
1237
1238 pub(crate) fn reset_for_tests() {
1239 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(StableBtreeMap::clear_new);
1240 APPLICATION_RECEIPT_REPLAY.with_borrow_mut(StableBtreeMap::clear_new);
1241 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow_mut(|state| state.0.clear_new());
1242 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
1243 }
1244}
1245
1246#[cfg(test)]
1247mod tests {
1248 use super::*;
1249 use crate::{
1250 cdk::types::Principal,
1251 model::intent::{
1252 RECEIPT_BACKED_INTENT_SCHEMA_VERSION, TerminalEvidence, TerminalEvidenceDecision,
1253 },
1254 };
1255
1256 #[test]
1257 #[should_panic(expected = "stable IntentId is 7 bytes; expected 8")]
1258 fn malformed_stable_intent_id_fails_closed() {
1259 let _ = <IntentId as Storable>::from_bytes(Cow::Owned(vec![0; 7]));
1260 }
1261
1262 #[test]
1263 #[should_panic(expected = "stable OperationId is 31 bytes; expected 32")]
1264 fn malformed_stable_operation_id_fails_closed() {
1265 let _ = <OperationId as Storable>::from_bytes(Cow::Owned(vec![0; 31]));
1266 }
1267
1268 #[test]
1269 #[should_panic(
1270 expected = "stable ApplicationReceiptEligibilityKeyRecord is 39 bytes; expected 40"
1271 )]
1272 fn malformed_stable_application_eligibility_key_fails_closed() {
1273 let _ = ApplicationReceiptEligibilityKeyRecord::from_bytes(Cow::Owned(vec![0; 39]));
1274 }
1275
1276 #[test]
1277 fn application_eligibility_capacity_reservation_is_conservative_and_bounded() {
1278 assert_eq!(application_eligibility_required_pages(0), Some(1));
1279 assert_eq!(application_eligibility_required_pages(1), Some(1));
1280 assert_eq!(application_eligibility_required_pages(1_000), Some(8));
1281 assert_eq!(application_eligibility_required_pages(u64::MAX), None);
1282 assert!(!ReceiptBackedIntentStore::reserve_application_eligibility_capacity(u64::MAX));
1283 }
1284
1285 #[test]
1286 #[should_panic(expected = "stable IntentExpiryKeyRecord is 15 bytes; expected 16")]
1287 fn malformed_stable_intent_expiry_key_fails_closed() {
1288 let _ = <IntentExpiryKeyRecord as Storable>::from_bytes(Cow::Owned(vec![0; 15]));
1289 }
1290
1291 #[test]
1292 fn stable_intent_expiry_key_preserves_deadline_then_identity_order() {
1293 let keys = [
1294 IntentExpiryKeyRecord {
1295 due_at_secs: 11,
1296 intent_id: IntentId(1),
1297 },
1298 IntentExpiryKeyRecord {
1299 due_at_secs: 10,
1300 intent_id: IntentId(2),
1301 },
1302 IntentExpiryKeyRecord {
1303 due_at_secs: 10,
1304 intent_id: IntentId(1),
1305 },
1306 ];
1307 let mut encoded = keys.map(Storable::into_bytes);
1308 encoded.sort();
1309 assert_eq!(
1310 encoded,
1311 [
1312 keys[2].into_bytes(),
1313 keys[1].into_bytes(),
1314 keys[0].into_bytes()
1315 ]
1316 );
1317 }
1318
1319 #[test]
1320 fn intent_allocations_round_trip_through_canonical_data_snapshots() {
1321 IntentStore::reset_for_tests();
1322 let intent_id = IntentId(7);
1323 let resource_key = IntentResourceKey::new("storage:uploads");
1324 let record = IntentRecord {
1325 id: intent_id,
1326 resource_key: resource_key.clone(),
1327 quantity: 11,
1328 state: IntentState::Pending,
1329 created_at: 13,
1330 ttl_secs: Some(17),
1331 };
1332 let totals = IntentResourceTotalsRecord {
1333 reserved_qty: 11,
1334 committed_qty: 19,
1335 pending_count: 1,
1336 };
1337 let pending = IntentPendingEntryRecord {
1338 resource_key: resource_key.clone(),
1339 quantity: 11,
1340 created_at: 13,
1341 ttl_secs: Some(17),
1342 };
1343 let meta = IntentStoreMetaRecord {
1344 schema_version: INTENT_STORE_SCHEMA_VERSION,
1345 next_intent_id: IntentId(8),
1346 pending_total: 1,
1347 committed_total: 2,
1348 aborted_total: 3,
1349 };
1350
1351 IntentStore::set_meta(meta);
1352 IntentStore::insert_record(record);
1353 IntentStore::set_totals(resource_key, totals);
1354 IntentStore::insert_pending(intent_id, pending);
1355 let expiry_key = IntentExpiryKeyRecord {
1356 due_at_secs: 31,
1357 intent_id,
1358 };
1359 IntentStore::insert_expiry(expiry_key, IntentExpiryEntryRecord { intent_id });
1360
1361 let meta_data = IntentStore::export_meta();
1362 let records_data = IntentStore::export_records();
1363 let totals_data = IntentStore::export_totals();
1364 let pending_data = IntentStore::export_pending();
1365 let expiry_data = IntentStore::export_expiry_index();
1366
1367 IntentStore::reset_for_tests();
1368 IntentStore::import_meta(meta_data);
1369 IntentStore::import_records(records_data.clone());
1370 IntentStore::import_totals(totals_data.clone());
1371 IntentStore::import_pending(pending_data.clone());
1372 IntentStore::import_expiry_index(expiry_data.clone());
1373
1374 assert_eq!(IntentStore::export_meta(), meta_data);
1375 assert_eq!(IntentStore::export_records(), records_data);
1376 assert_eq!(IntentStore::export_totals(), totals_data);
1377 assert_eq!(IntentStore::export_pending(), pending_data);
1378 assert_eq!(IntentStore::export_expiry_index(), expiry_data);
1379 IntentStore::reset_for_tests();
1380 }
1381
1382 #[test]
1383 fn receipt_backed_allocations_round_trip_through_canonical_data_snapshots() {
1384 IntentStore::reset_for_tests();
1385 let application_operation_id = OperationId::from_bytes([7; 32]);
1386 let placement_operation_id = OperationId::from_bytes([8; 32]);
1387 let evidence = TerminalEvidence::new(
1388 Principal::from_slice(&[1; 29]),
1389 TerminalEvidenceDecision::Committed,
1390 [8; 32],
1391 );
1392 let record = ReceiptBackedIntentRecord {
1393 schema_version: RECEIPT_BACKED_INTENT_SCHEMA_VERSION,
1394 operation_id: application_operation_id,
1395 payload_binding: PayloadBinding::new([9; 32]),
1396 resource_key: IntentResourceKey::new("mint:collection"),
1397 quantity: 11,
1398 state: ReceiptBackedIntentState::Committed { evidence },
1399 revision: 2,
1400 created_at_ns: 13,
1401 updated_at_ns: 17,
1402 };
1403 ReceiptBackedIntentStore::insert(record);
1404 ReceiptBackedIntentStore::insert(ReceiptBackedIntentRecord {
1405 schema_version: RECEIPT_BACKED_INTENT_SCHEMA_VERSION,
1406 operation_id: placement_operation_id,
1407 payload_binding: PayloadBinding::new([10; 32]),
1408 resource_key: IntentResourceKey::new(format!("canic:placement:{}", "a".repeat(64))),
1409 quantity: 1,
1410 state: ReceiptBackedIntentState::Committed { evidence },
1411 revision: 2,
1412 created_at_ns: 13,
1413 updated_at_ns: 17,
1414 });
1415 ReceiptBackedIntentStore::insert_application_replay(ApplicationReceiptReplayRecord {
1416 schema_version: APPLICATION_RECEIPT_REPLAY_SCHEMA_VERSION,
1417 operation_id: application_operation_id,
1418 replay_deadline_ns: 23,
1419 });
1420 let eligibility_key = ApplicationReceiptEligibilityKeyRecord {
1421 eligible_at_ns: 17 + crate::model::intent::RECEIPT_TERMINAL_OBSERVATION_GRACE_NS,
1422 operation_id: application_operation_id,
1423 };
1424 ReceiptBackedIntentStore::insert_application_eligibility(
1425 eligibility_key,
1426 ApplicationReceiptEligibilityRecord {
1427 schema_version: APPLICATION_RECEIPT_ELIGIBILITY_SCHEMA_VERSION,
1428 operation_id: application_operation_id,
1429 payload_binding: PayloadBinding::new([9; 32]),
1430 terminal_revision: 2,
1431 },
1432 );
1433 ReceiptBackedIntentStore::insert_placement_acknowledgement(
1434 PlacementAcknowledgementEntryRecord {
1435 operation_id: placement_operation_id,
1436 },
1437 );
1438 let records_data = ReceiptBackedIntentStore::export_records();
1439 let replay_data = ReceiptBackedIntentStore::export_application_replay();
1440 let eligibility_data = ReceiptBackedIntentStore::export_application_eligibility();
1441 let acknowledgement_data =
1442 ReceiptBackedIntentStore::export_placement_acknowledgement_index();
1443
1444 ReceiptBackedIntentStore::reset_for_tests();
1445 assert_eq!(
1446 ReceiptBackedIntentStore::export_records(),
1447 ReceiptBackedIntentsData::default()
1448 );
1449 assert_eq!(
1450 ReceiptBackedIntentStore::export_application_replay(),
1451 ApplicationReceiptReplayData::default()
1452 );
1453 assert_eq!(
1454 ReceiptBackedIntentStore::export_application_eligibility(),
1455 ApplicationReceiptEligibilityData::default()
1456 );
1457 assert_eq!(
1458 ReceiptBackedIntentStore::export_placement_acknowledgement_index(),
1459 PlacementAcknowledgementIndexData::default()
1460 );
1461
1462 ReceiptBackedIntentStore::import_records(records_data.clone());
1463 ReceiptBackedIntentStore::import_application_replay(replay_data.clone());
1464 ReceiptBackedIntentStore::import_application_eligibility(eligibility_data.clone());
1465 ReceiptBackedIntentStore::import_placement_acknowledgement_index(
1466 acknowledgement_data.clone(),
1467 );
1468 assert_eq!(ReceiptBackedIntentStore::len(), 2);
1469 assert_eq!(ReceiptBackedIntentStore::export_records(), records_data);
1470 assert_eq!(
1471 ReceiptBackedIntentStore::export_application_replay(),
1472 replay_data
1473 );
1474 assert_eq!(
1475 ReceiptBackedIntentStore::export_application_eligibility(),
1476 eligibility_data
1477 );
1478 assert_eq!(
1479 ReceiptBackedIntentStore::export_placement_acknowledgement_index(),
1480 acknowledgement_data
1481 );
1482 IntentStore::reset_for_tests();
1483 }
1484}