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