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