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