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
48std::thread_local! {
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
56std::thread_local! {
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
69std::thread_local! {
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
84std::thread_local! {
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
92std::thread_local! {
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
107std::thread_local! {
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
115std::thread_local! {
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
123std::thread_local! {
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 #[serde(deserialize_with = "crate::cdk::serialize::required_option")]
340 pub retain_until_secs: Option<u64>,
341}
342
343impl IntentResourceTotalsRecord {
344 pub const STATE_CONTRACT_NAME: &'static str = "IntentResourceTotalsRecord";
345 pub const STORABLE_MAX_SIZE: u32 = 96;
346}
347
348impl_storable_bounded!(
349 IntentResourceTotalsRecord,
350 IntentResourceTotalsRecord::STORABLE_MAX_SIZE,
351 false
352);
353
354#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
359pub struct IntentPendingEntryRecord {
360 pub resource_key: IntentResourceKey,
361 pub quantity: u64,
362 pub created_at: u64,
363 pub ttl_secs: Option<u64>,
365}
366
367impl IntentPendingEntryRecord {
368 pub const STATE_CONTRACT_NAME: &'static str = "IntentPendingEntryRecord";
369 pub const STORABLE_MAX_SIZE: u32 = 224;
370}
371
372impl_storable_bounded!(
373 IntentPendingEntryRecord,
374 IntentPendingEntryRecord::STORABLE_MAX_SIZE,
375 false
376);
377
378#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
380pub struct ReceiptBackedIntentRecord {
381 pub application_retention: Option<ApplicationReceiptRetentionRecord>,
382 pub schema_version: u32,
383 pub operation_id: OperationId,
384 pub payload_binding: PayloadBinding,
385 pub resource_key: IntentResourceKey,
386 pub quantity: u64,
387 pub state: ReceiptBackedIntentState,
388 pub revision: u64,
389 pub created_at_ns: u64,
390 pub updated_at_ns: u64,
391}
392
393impl ReceiptBackedIntentRecord {
394 pub const STATE_CONTRACT_NAME: &'static str = "ReceiptBackedIntentRecord";
395 pub const STORABLE_MAX_SIZE: u32 = 1024;
396
397 #[must_use]
398 pub fn into_intent(self) -> ReceiptBackedIntent {
399 ReceiptBackedIntent {
400 schema_version: self.schema_version,
401 operation_id: self.operation_id,
402 payload_binding: self.payload_binding,
403 resource_key: self.resource_key,
404 quantity: self.quantity,
405 state: self.state,
406 revision: self.revision,
407 created_at_ns: self.created_at_ns,
408 updated_at_ns: self.updated_at_ns,
409 }
410 }
411}
412
413impl_storable_bounded!(
414 ReceiptBackedIntentRecord,
415 ReceiptBackedIntentRecord::STORABLE_MAX_SIZE,
416 false
417);
418
419#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
421pub struct ApplicationReceiptRetentionRecord {
422 pub replay_deadline_ns: u64,
423}
424
425#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
427pub struct ApplicationReceiptEligibilityKeyRecord {
428 pub eligible_at_ns: u64,
429 pub operation_id: OperationId,
430}
431
432impl Storable for ApplicationReceiptEligibilityKeyRecord {
433 const BOUND: Bound = Bound::Bounded {
434 max_size: 40,
435 is_fixed_size: true,
436 };
437
438 fn to_bytes(&self) -> Cow<'_, [u8]> {
439 Cow::Owned(self.into_bytes())
440 }
441
442 fn into_bytes(self) -> Vec<u8> {
443 let mut bytes = Vec::with_capacity(40);
444 bytes.extend_from_slice(&self.eligible_at_ns.to_be_bytes());
445 bytes.extend_from_slice(self.operation_id.as_bytes());
446 bytes
447 }
448
449 fn from_bytes(bytes: Cow<[u8]>) -> Self {
455 let bytes = <[u8; 40]>::try_from(bytes.as_ref()).unwrap_or_else(|_| {
456 panic!(
457 "stable ApplicationReceiptEligibilityKeyRecord is {} bytes; expected 40",
458 bytes.as_ref().len()
459 )
460 });
461 let (eligible_at_ns, operation_id) = bytes.split_at(8);
462 Self {
463 eligible_at_ns: u64::from_be_bytes(
464 eligible_at_ns
465 .try_into()
466 .expect("eligibility deadline width"),
467 ),
468 operation_id: OperationId::from_bytes(
469 operation_id
470 .try_into()
471 .expect("eligibility operation identity width"),
472 ),
473 }
474 }
475}
476
477#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
479pub struct ApplicationReceiptEligibilityRecord {
480 pub schema_version: u32,
481 pub operation_id: OperationId,
482 pub payload_binding: PayloadBinding,
483 pub terminal_revision: u64,
484}
485
486impl ApplicationReceiptEligibilityRecord {
487 pub const STATE_CONTRACT_NAME: &'static str = "ApplicationReceiptEligibilityRecord";
488 pub const STORABLE_MAX_SIZE: u32 = 229;
489}
490
491impl_storable_bounded!(
492 ApplicationReceiptEligibilityRecord,
493 ApplicationReceiptEligibilityRecord::STORABLE_MAX_SIZE,
494 false
495);
496
497#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
499pub struct PlacementAcknowledgementEntryRecord {
500 pub operation_id: OperationId,
501}
502
503impl PlacementAcknowledgementEntryRecord {
504 pub const STATE_CONTRACT_NAME: &'static str = "PlacementAcknowledgementEntryRecord";
505 pub const STORABLE_MAX_SIZE: u32 = 128;
506}
507
508impl_storable_bounded!(
509 PlacementAcknowledgementEntryRecord,
510 PlacementAcknowledgementEntryRecord::STORABLE_MAX_SIZE,
511 false
512);
513
514#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
521pub struct IntentMetaData {
522 pub record: IntentStoreMetaRecord,
523}
524
525impl IntentMetaData {
526 pub const STATE_CONTRACT_NAME: &'static str = "IntentMetaData";
527}
528
529#[derive(Clone, Debug, Eq, PartialEq)]
536pub struct IntentRecordEntryRecord {
537 pub intent_id: IntentId,
538 pub record: IntentRecord,
539}
540
541#[derive(Clone, Debug, Default, Eq, PartialEq)]
548pub struct IntentRecordsData {
549 pub entries: Vec<IntentRecordEntryRecord>,
550}
551
552impl IntentRecordsData {
553 pub const STATE_CONTRACT_NAME: &'static str = "IntentRecordsData";
554}
555
556#[derive(Clone, Debug, Eq, PartialEq)]
563pub struct IntentTotalsEntryRecord {
564 pub resource_key: IntentResourceKey,
565 pub record: IntentResourceTotalsRecord,
566}
567
568#[derive(Clone, Debug, Default, Eq, PartialEq)]
575pub struct IntentTotalsData {
576 pub entries: Vec<IntentTotalsEntryRecord>,
577}
578
579impl IntentTotalsData {
580 pub const STATE_CONTRACT_NAME: &'static str = "IntentTotalsData";
581}
582
583#[derive(Clone, Debug, Eq, PartialEq)]
590pub struct IntentPendingIndexEntryRecord {
591 pub intent_id: IntentId,
592 pub record: IntentPendingEntryRecord,
593}
594
595#[derive(Clone, Debug, Default, Eq, PartialEq)]
602pub struct IntentPendingData {
603 pub entries: Vec<IntentPendingIndexEntryRecord>,
604}
605
606impl IntentPendingData {
607 pub const STATE_CONTRACT_NAME: &'static str = "IntentPendingData";
608}
609
610#[derive(Clone, Copy, Debug, Eq, PartialEq)]
612pub struct IntentExpiryIndexEntryRecord {
613 pub key: IntentExpiryKeyRecord,
614 pub record: IntentExpiryEntryRecord,
615}
616
617#[derive(Clone, Debug, Default, Eq, PartialEq)]
619pub struct IntentExpiryIndexData {
620 pub entries: Vec<IntentExpiryIndexEntryRecord>,
621}
622
623impl IntentExpiryIndexData {
624 pub const STATE_CONTRACT_NAME: &'static str = "IntentExpiryIndexData";
625}
626
627#[derive(Clone, Debug, Eq, PartialEq)]
629pub struct ReceiptBackedIntentEntryRecord {
630 pub operation_id: OperationId,
631 pub record: ReceiptBackedIntentRecord,
632}
633
634#[derive(Clone, Debug, Default, Eq, PartialEq)]
636pub struct ReceiptBackedIntentsData {
637 pub entries: Vec<ReceiptBackedIntentEntryRecord>,
638}
639
640impl ReceiptBackedIntentsData {
641 pub const STATE_CONTRACT_NAME: &'static str = "ReceiptBackedIntentsData";
642}
643
644#[derive(Clone, Copy, Debug, Eq, PartialEq)]
646pub struct ApplicationReceiptEligibilityEntryRecord {
647 pub key: ApplicationReceiptEligibilityKeyRecord,
648 pub record: ApplicationReceiptEligibilityRecord,
649}
650
651#[derive(Clone, Debug, Default, Eq, PartialEq)]
653pub struct ApplicationReceiptEligibilityData {
654 pub entries: Vec<ApplicationReceiptEligibilityEntryRecord>,
655}
656
657impl ApplicationReceiptEligibilityData {
658 pub const STATE_CONTRACT_NAME: &'static str = "ApplicationReceiptEligibilityData";
659}
660
661#[derive(Clone, Copy, Debug, Eq, PartialEq)]
663pub struct PlacementAcknowledgementIndexEntryRecord {
664 pub operation_id: OperationId,
665 pub record: PlacementAcknowledgementEntryRecord,
666}
667
668#[derive(Clone, Debug, Default, Eq, PartialEq)]
670pub struct PlacementAcknowledgementIndexData {
671 pub entries: Vec<PlacementAcknowledgementIndexEntryRecord>,
672}
673
674impl PlacementAcknowledgementIndexData {
675 pub const STATE_CONTRACT_NAME: &'static str = "PlacementAcknowledgementIndexData";
676}
677
678pub struct IntentStore;
683
684impl IntentStore {
685 #[must_use]
690 pub(crate) fn meta() -> IntentStoreMetaRecord {
691 INTENT_META.with_borrow(|cell| *cell.get())
692 }
693
694 pub(crate) fn set_meta(meta: IntentStoreMetaRecord) {
695 INTENT_META.with_borrow_mut(|cell| cell.set(meta));
696 }
697
698 #[must_use]
703 pub(crate) fn get_record(id: IntentId) -> Option<IntentRecord> {
704 INTENT_RECORDS.with_borrow(|map| map.get(&id))
705 }
706
707 pub(crate) fn insert_record(record: IntentRecord) -> Option<IntentRecord> {
708 INTENT_RECORDS.with_borrow_mut(|map| map.insert(record.id, record))
709 }
710
711 #[must_use]
716 pub(crate) fn get_totals(key: &IntentResourceKey) -> Option<IntentResourceTotalsRecord> {
717 INTENT_TOTALS.with_borrow(|map| map.get(key))
718 }
719
720 #[must_use]
721 pub(crate) fn totals_len() -> u64 {
722 INTENT_TOTALS.with_borrow(StableBtreeMap::len)
723 }
724
725 pub(crate) fn totals_entries(limit: usize) -> Vec<IntentTotalsEntryRecord> {
727 INTENT_TOTALS.with_borrow(|map| {
728 map.iter()
729 .take(limit)
730 .map(|entry| IntentTotalsEntryRecord {
731 resource_key: entry.key().clone(),
732 record: entry.value(),
733 })
734 .collect()
735 })
736 }
737
738 pub(crate) fn set_totals(
739 key: IntentResourceKey,
740 totals: IntentResourceTotalsRecord,
741 ) -> Option<IntentResourceTotalsRecord> {
742 INTENT_TOTALS.with_borrow_mut(|map| map.insert(key, totals))
743 }
744
745 pub(crate) fn remove_totals(key: &IntentResourceKey) -> Option<IntentResourceTotalsRecord> {
746 INTENT_TOTALS.with_borrow_mut(|map| map.remove(key))
747 }
748
749 #[must_use]
754 pub(crate) fn get_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
755 INTENT_PENDING.with_borrow(|map| map.get(&id))
756 }
757
758 pub(crate) fn insert_pending(
759 id: IntentId,
760 entry: IntentPendingEntryRecord,
761 ) -> Option<IntentPendingEntryRecord> {
762 INTENT_PENDING.with_borrow_mut(|map| map.insert(id, entry))
763 }
764
765 pub(crate) fn remove_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
766 INTENT_PENDING.with_borrow_mut(|map| map.remove(&id))
767 }
768
769 pub(crate) fn with_pending_entries<R>(
770 f: impl FnOnce(
771 &StableBtreeMap<IntentId, IntentPendingEntryRecord, RuntimeMemory<DefaultMemoryImpl>>,
772 ) -> R,
773 ) -> R {
774 INTENT_PENDING.with_borrow(|map| f(map))
775 }
776
777 #[must_use]
782 pub(crate) fn get_expiry(key: IntentExpiryKeyRecord) -> Option<IntentExpiryEntryRecord> {
783 INTENT_EXPIRY_INDEX.with_borrow(|map| map.get(&key))
784 }
785
786 pub(crate) fn insert_expiry(
787 key: IntentExpiryKeyRecord,
788 record: IntentExpiryEntryRecord,
789 ) -> Option<IntentExpiryEntryRecord> {
790 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| map.insert(key, record))
791 }
792
793 pub(crate) fn remove_expiry(key: IntentExpiryKeyRecord) -> Option<IntentExpiryEntryRecord> {
794 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| map.remove(&key))
795 }
796
797 pub(crate) fn clear_expiry_index() {
798 INTENT_EXPIRY_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
799 }
800
801 pub(crate) fn with_expiry_entries<R>(
802 f: impl FnOnce(
803 &StableBtreeMap<
804 IntentExpiryKeyRecord,
805 IntentExpiryEntryRecord,
806 RuntimeMemory<DefaultMemoryImpl>,
807 >,
808 ) -> R,
809 ) -> R {
810 INTENT_EXPIRY_INDEX.with_borrow(|map| f(map))
811 }
812}
813
814pub struct ReceiptBackedIntentStore;
816
817impl ReceiptBackedIntentStore {
818 #[must_use]
819 pub(crate) fn len() -> u64 {
820 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(StableBtreeMap::len)
821 }
822
823 #[must_use]
824 pub(crate) fn get(operation_id: OperationId) -> Option<ReceiptBackedIntentRecord> {
825 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| records.get(&operation_id))
826 }
827
828 pub(crate) fn insert(record: ReceiptBackedIntentRecord) -> Option<ReceiptBackedIntentRecord> {
829 let application = record.application_retention.is_some();
830 let previous = RECEIPT_BACKED_INTENT_RECORDS
831 .with_borrow_mut(|records| records.insert(record.operation_id, record));
832 Self::update_application_count(
833 previous
834 .as_ref()
835 .is_some_and(|record| record.application_retention.is_some()),
836 application,
837 );
838 previous
839 }
840
841 pub(crate) fn remove(operation_id: OperationId) -> Option<ReceiptBackedIntentRecord> {
842 let previous =
843 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(|records| records.remove(&operation_id));
844 Self::update_application_count(
845 previous
846 .as_ref()
847 .is_some_and(|record| record.application_retention.is_some()),
848 false,
849 );
850 previous
851 }
852
853 pub(crate) fn with_records<R>(
854 f: impl FnOnce(
855 &StableBtreeMap<
856 OperationId,
857 ReceiptBackedIntentRecord,
858 RuntimeMemory<DefaultMemoryImpl>,
859 >,
860 ) -> R,
861 ) -> R {
862 RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| f(records))
863 }
864
865 pub(crate) fn application_count() -> u64 {
867 IntentStore::meta().application_receipt_count
868 }
869
870 fn update_application_count(previous: bool, next: bool) {
871 if previous == next {
872 return;
873 }
874 let mut meta = IntentStore::meta();
875 meta.application_receipt_count = if next {
876 meta.application_receipt_count
877 .checked_add(1)
878 .expect("application receipt count overflow")
879 } else {
880 meta.application_receipt_count
881 .checked_sub(1)
882 .expect("application receipt count underflow")
883 };
884 IntentStore::set_meta(meta);
885 }
886
887 pub(crate) fn reserve_application_eligibility_capacity(record_count: u64) -> bool {
889 let Some(required_pages) = application_eligibility_required_pages(record_count) else {
890 return false;
891 };
892
893 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| {
894 let current_pages = state.1.size();
895 current_pages >= required_pages || state.1.grow(required_pages - current_pages) >= 0
896 })
897 }
898
899 #[must_use]
900 pub(crate) fn get_application_eligibility(
901 key: ApplicationReceiptEligibilityKeyRecord,
902 ) -> Option<ApplicationReceiptEligibilityRecord> {
903 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| state.0.get(&key))
904 }
905
906 pub(crate) fn insert_application_eligibility(
907 key: ApplicationReceiptEligibilityKeyRecord,
908 record: ApplicationReceiptEligibilityRecord,
909 ) -> Option<ApplicationReceiptEligibilityRecord> {
910 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow_mut(|state| state.0.insert(key, record))
911 }
912
913 pub(crate) fn remove_application_eligibility(
914 key: ApplicationReceiptEligibilityKeyRecord,
915 ) -> Option<ApplicationReceiptEligibilityRecord> {
916 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow_mut(|state| state.0.remove(&key))
917 }
918
919 pub(crate) fn with_application_eligibility<R>(
920 f: impl FnOnce(
921 &StableBtreeMap<
922 ApplicationReceiptEligibilityKeyRecord,
923 ApplicationReceiptEligibilityRecord,
924 RuntimeMemory<DefaultMemoryImpl>,
925 >,
926 ) -> R,
927 ) -> R {
928 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| f(&state.0))
929 }
930
931 #[must_use]
932 pub(crate) fn first_application_eligibility() -> Option<(
933 ApplicationReceiptEligibilityKeyRecord,
934 ApplicationReceiptEligibilityRecord,
935 )> {
936 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| {
937 state
938 .0
939 .iter()
940 .next()
941 .map(|entry| (*entry.key(), entry.value()))
942 })
943 }
944
945 #[must_use]
946 pub(crate) fn application_eligibility_reserved_pages() -> u64 {
947 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| state.1.size())
948 }
949
950 #[must_use]
951 pub(crate) fn get_placement_acknowledgement(
952 operation_id: OperationId,
953 ) -> Option<PlacementAcknowledgementEntryRecord> {
954 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| index.get(&operation_id))
955 }
956
957 pub(crate) fn insert_placement_acknowledgement(
958 record: PlacementAcknowledgementEntryRecord,
959 ) -> Option<PlacementAcknowledgementEntryRecord> {
960 PLACEMENT_ACKNOWLEDGEMENT_INDEX
961 .with_borrow_mut(|index| index.insert(record.operation_id, record))
962 }
963
964 pub(crate) fn remove_placement_acknowledgement(
965 operation_id: OperationId,
966 ) -> Option<PlacementAcknowledgementEntryRecord> {
967 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(|index| index.remove(&operation_id))
968 }
969
970 pub(crate) fn clear_placement_acknowledgement_index() {
971 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
972 }
973
974 pub(crate) fn with_placement_acknowledgements<R>(
975 f: impl FnOnce(
976 &StableBtreeMap<
977 OperationId,
978 PlacementAcknowledgementEntryRecord,
979 RuntimeMemory<DefaultMemoryImpl>,
980 >,
981 ) -> R,
982 ) -> R {
983 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| f(index))
984 }
985}
986
987pub(super) fn application_eligibility_required_pages(record_count: u64) -> Option<u64> {
988 let maximum_nodes = record_count
993 .checked_add(APPLICATION_RECEIPT_ELIGIBILITY_MIN_NODE_ENTRIES - 1)?
994 / APPLICATION_RECEIPT_ELIGIBILITY_MIN_NODE_ENTRIES;
995 let required_bytes = APPLICATION_RECEIPT_ELIGIBILITY_FIXED_BYTES
996 .checked_add(maximum_nodes.checked_mul(APPLICATION_RECEIPT_ELIGIBILITY_CHUNK_BYTES)?)?
997 .checked_add(WASM_PAGE_BYTES - 1)?;
998 Some(required_bytes / WASM_PAGE_BYTES)
999}
1000
1001#[cfg(test)]
1008impl IntentStore {
1009 #[must_use]
1010 pub(crate) fn export_meta() -> IntentMetaData {
1011 IntentMetaData {
1012 record: Self::meta(),
1013 }
1014 }
1015
1016 fn import_meta(data: IntentMetaData) {
1017 Self::set_meta(data.record);
1018 }
1019
1020 #[must_use]
1021 pub(crate) fn export_records() -> IntentRecordsData {
1022 IntentRecordsData {
1023 entries: INTENT_RECORDS.with_borrow(|map| {
1024 map.iter()
1025 .map(|entry| IntentRecordEntryRecord {
1026 intent_id: *entry.key(),
1027 record: entry.value(),
1028 })
1029 .collect()
1030 }),
1031 }
1032 }
1033
1034 pub(crate) fn import_records(data: IntentRecordsData) {
1035 INTENT_RECORDS.with_borrow_mut(|map| {
1036 map.clear_new();
1037 for entry in data.entries {
1038 map.insert(entry.intent_id, entry.record);
1039 }
1040 });
1041 }
1042
1043 #[must_use]
1044 pub(crate) fn export_totals() -> IntentTotalsData {
1045 IntentTotalsData {
1046 entries: INTENT_TOTALS.with_borrow(|map| {
1047 map.iter()
1048 .map(|entry| IntentTotalsEntryRecord {
1049 resource_key: entry.key().clone(),
1050 record: entry.value(),
1051 })
1052 .collect()
1053 }),
1054 }
1055 }
1056
1057 pub(crate) fn import_totals(data: IntentTotalsData) {
1058 INTENT_TOTALS.with_borrow_mut(|map| {
1059 map.clear_new();
1060 for entry in data.entries {
1061 map.insert(entry.resource_key, entry.record);
1062 }
1063 });
1064 }
1065
1066 #[must_use]
1067 pub(crate) fn export_pending() -> IntentPendingData {
1068 IntentPendingData {
1069 entries: INTENT_PENDING.with_borrow(|map| {
1070 map.iter()
1071 .map(|entry| IntentPendingIndexEntryRecord {
1072 intent_id: *entry.key(),
1073 record: entry.value(),
1074 })
1075 .collect()
1076 }),
1077 }
1078 }
1079
1080 fn import_pending(data: IntentPendingData) {
1081 INTENT_PENDING.with_borrow_mut(|map| {
1082 map.clear_new();
1083 for entry in data.entries {
1084 map.insert(entry.intent_id, entry.record);
1085 }
1086 });
1087 }
1088
1089 #[must_use]
1090 pub(crate) fn export_expiry_index() -> IntentExpiryIndexData {
1091 IntentExpiryIndexData {
1092 entries: INTENT_EXPIRY_INDEX.with_borrow(|map| {
1093 map.iter()
1094 .map(|entry| IntentExpiryIndexEntryRecord {
1095 key: *entry.key(),
1096 record: entry.value(),
1097 })
1098 .collect()
1099 }),
1100 }
1101 }
1102
1103 fn import_expiry_index(data: IntentExpiryIndexData) {
1104 INTENT_EXPIRY_INDEX.with_borrow_mut(|map| {
1105 map.clear_new();
1106 for entry in data.entries {
1107 map.insert(entry.key, entry.record);
1108 }
1109 });
1110 }
1111
1112 pub(crate) fn reset_for_tests() {
1113 INTENT_RECORDS.with_borrow_mut(StableBtreeMap::clear_new);
1114 INTENT_TOTALS.with_borrow_mut(StableBtreeMap::clear_new);
1115 INTENT_PENDING.with_borrow_mut(StableBtreeMap::clear_new);
1116 INTENT_EXPIRY_INDEX.with_borrow_mut(StableBtreeMap::clear_new);
1117 INTENT_META.with_borrow_mut(|cell| cell.set(IntentStoreMetaRecord::default()));
1118 ReceiptBackedIntentStore::reset_for_tests();
1119 }
1120}
1121
1122#[cfg(test)]
1123impl ReceiptBackedIntentStore {
1124 #[must_use]
1125 pub(crate) fn export_records() -> ReceiptBackedIntentsData {
1126 ReceiptBackedIntentsData {
1127 entries: RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| {
1128 records
1129 .iter()
1130 .map(|entry| ReceiptBackedIntentEntryRecord {
1131 operation_id: *entry.key(),
1132 record: entry.value(),
1133 })
1134 .collect()
1135 }),
1136 }
1137 }
1138
1139 pub(crate) fn import_records(data: ReceiptBackedIntentsData) {
1140 let count = data
1141 .entries
1142 .iter()
1143 .filter(|entry| entry.record.application_retention.is_some())
1144 .count() as u64;
1145 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(|records| {
1146 records.clear_new();
1147 for entry in data.entries {
1148 records.insert(entry.operation_id, entry.record);
1149 }
1150 });
1151 let mut meta = IntentStore::meta();
1152 meta.application_receipt_count = count;
1153 IntentStore::set_meta(meta);
1154 }
1155
1156 #[must_use]
1157 pub(crate) fn export_application_eligibility() -> ApplicationReceiptEligibilityData {
1158 ApplicationReceiptEligibilityData {
1159 entries: APPLICATION_RECEIPT_ELIGIBILITY.with_borrow(|state| {
1160 state
1161 .0
1162 .iter()
1163 .map(|entry| ApplicationReceiptEligibilityEntryRecord {
1164 key: *entry.key(),
1165 record: entry.value(),
1166 })
1167 .collect()
1168 }),
1169 }
1170 }
1171
1172 pub(crate) fn import_application_eligibility(data: ApplicationReceiptEligibilityData) {
1173 APPLICATION_RECEIPT_ELIGIBILITY.with_borrow_mut(|state| {
1174 state.0.clear_new();
1175 for entry in data.entries {
1176 state.0.insert(entry.key, entry.record);
1177 }
1178 });
1179 }
1180
1181 #[must_use]
1182 pub(crate) fn export_placement_acknowledgement_index() -> PlacementAcknowledgementIndexData {
1183 PlacementAcknowledgementIndexData {
1184 entries: PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow(|index| {
1185 index
1186 .iter()
1187 .map(|entry| PlacementAcknowledgementIndexEntryRecord {
1188 operation_id: *entry.key(),
1189 record: entry.value(),
1190 })
1191 .collect()
1192 }),
1193 }
1194 }
1195
1196 pub(crate) fn import_placement_acknowledgement_index(data: PlacementAcknowledgementIndexData) {
1197 PLACEMENT_ACKNOWLEDGEMENT_INDEX.with_borrow_mut(|index| {
1198 index.clear_new();
1199 for entry in data.entries {
1200 index.insert(entry.operation_id, entry.record);
1201 }
1202 });
1203 }
1204
1205 pub(crate) fn reset_for_tests() {
1206 RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(StableBtreeMap::clear_new);
1207 let mut meta = IntentStore::meta();
1208 meta.application_receipt_count = 0;
1209 IntentStore::set_meta(meta);
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
1215#[cfg(test)]
1216mod tests {
1217 use super::*;
1218 use crate::{
1219 cdk::types::Principal,
1220 model::intent::{
1221 RECEIPT_BACKED_INTENT_SCHEMA_VERSION, TerminalEvidence, TerminalEvidenceDecision,
1222 },
1223 };
1224
1225 #[test]
1226 #[should_panic(expected = "stable IntentId is 7 bytes; expected 8")]
1227 fn malformed_stable_intent_id_fails_closed() {
1228 let _ = <IntentId as Storable>::from_bytes(Cow::Owned(vec![0; 7]));
1229 }
1230
1231 #[test]
1232 #[should_panic(expected = "stable OperationId is 31 bytes; expected 32")]
1233 fn malformed_stable_operation_id_fails_closed() {
1234 let _ = <OperationId as Storable>::from_bytes(Cow::Owned(vec![0; 31]));
1235 }
1236
1237 #[test]
1238 #[should_panic(
1239 expected = "stable ApplicationReceiptEligibilityKeyRecord is 39 bytes; expected 40"
1240 )]
1241 fn malformed_stable_application_eligibility_key_fails_closed() {
1242 let _ = ApplicationReceiptEligibilityKeyRecord::from_bytes(Cow::Owned(vec![0; 39]));
1243 }
1244
1245 #[test]
1246 fn application_eligibility_capacity_reservation_is_conservative_and_bounded() {
1247 assert_eq!(application_eligibility_required_pages(0), Some(1));
1248 assert_eq!(application_eligibility_required_pages(1), Some(1));
1249 assert_eq!(application_eligibility_required_pages(1_000), Some(8));
1250 assert_eq!(application_eligibility_required_pages(u64::MAX), None);
1251 assert!(!ReceiptBackedIntentStore::reserve_application_eligibility_capacity(u64::MAX));
1252 }
1253
1254 #[test]
1255 #[should_panic(expected = "stable IntentExpiryKeyRecord is 15 bytes; expected 16")]
1256 fn malformed_stable_intent_expiry_key_fails_closed() {
1257 let _ = <IntentExpiryKeyRecord as Storable>::from_bytes(Cow::Owned(vec![0; 15]));
1258 }
1259
1260 #[test]
1261 fn stable_intent_expiry_key_preserves_deadline_then_identity_order() {
1262 let keys = [
1263 IntentExpiryKeyRecord {
1264 due_at_secs: 11,
1265 intent_id: IntentId(1),
1266 },
1267 IntentExpiryKeyRecord {
1268 due_at_secs: 10,
1269 intent_id: IntentId(2),
1270 },
1271 IntentExpiryKeyRecord {
1272 due_at_secs: 10,
1273 intent_id: IntentId(1),
1274 },
1275 ];
1276 let mut encoded = keys.map(Storable::into_bytes);
1277 encoded.sort();
1278 assert_eq!(
1279 encoded,
1280 [
1281 keys[2].into_bytes(),
1282 keys[1].into_bytes(),
1283 keys[0].into_bytes()
1284 ]
1285 );
1286 }
1287
1288 #[test]
1289 fn metadata_encoding_covers_full_width_application_count() {
1290 let record = IntentStoreMetaRecord {
1291 schema_version: u32::MAX,
1292 next_intent_id: IntentId(u64::MAX),
1293 pending_total: u64::MAX,
1294 committed_total: u64::MAX,
1295 aborted_total: u64::MAX,
1296 application_receipt_count: u64::MAX,
1297 };
1298 assert!(record.to_bytes().len() <= IntentStoreMetaRecord::STORABLE_MAX_SIZE as usize);
1299 }
1300
1301 #[test]
1302 fn intent_allocations_round_trip_through_canonical_data_snapshots() {
1303 IntentStore::reset_for_tests();
1304 let intent_id = IntentId(7);
1305 let resource_key = IntentResourceKey::new("storage:uploads");
1306 let record = IntentRecord {
1307 id: intent_id,
1308 resource_key: resource_key.clone(),
1309 quantity: 11,
1310 state: IntentState::Pending,
1311 created_at: 13,
1312 ttl_secs: Some(17),
1313 };
1314 let totals = IntentResourceTotalsRecord {
1315 reserved_qty: 11,
1316 committed_qty: 19,
1317 pending_count: 1,
1318 retain_until_secs: None,
1319 };
1320 let pending = IntentPendingEntryRecord {
1321 resource_key: resource_key.clone(),
1322 quantity: 11,
1323 created_at: 13,
1324 ttl_secs: Some(17),
1325 };
1326 let meta = IntentStoreMetaRecord {
1327 schema_version: INTENT_STORE_SCHEMA_VERSION,
1328 next_intent_id: IntentId(8),
1329 pending_total: 1,
1330 committed_total: 2,
1331 aborted_total: 3,
1332 application_receipt_count: 0,
1333 };
1334
1335 IntentStore::set_meta(meta);
1336 IntentStore::insert_record(record);
1337 IntentStore::set_totals(resource_key, totals);
1338 IntentStore::insert_pending(intent_id, pending);
1339 let expiry_key = IntentExpiryKeyRecord {
1340 due_at_secs: 31,
1341 intent_id,
1342 };
1343 IntentStore::insert_expiry(expiry_key, IntentExpiryEntryRecord { intent_id });
1344
1345 let meta_data = IntentStore::export_meta();
1346 let records_data = IntentStore::export_records();
1347 let totals_data = IntentStore::export_totals();
1348 let pending_data = IntentStore::export_pending();
1349 let expiry_data = IntentStore::export_expiry_index();
1350
1351 IntentStore::reset_for_tests();
1352 IntentStore::import_meta(meta_data);
1353 IntentStore::import_records(records_data.clone());
1354 IntentStore::import_totals(totals_data.clone());
1355 IntentStore::import_pending(pending_data.clone());
1356 IntentStore::import_expiry_index(expiry_data.clone());
1357
1358 assert_eq!(IntentStore::export_meta(), meta_data);
1359 assert_eq!(IntentStore::export_records(), records_data);
1360 assert_eq!(IntentStore::export_totals(), totals_data);
1361 assert_eq!(IntentStore::export_pending(), pending_data);
1362 assert_eq!(IntentStore::export_expiry_index(), expiry_data);
1363 IntentStore::reset_for_tests();
1364 }
1365
1366 #[test]
1367 fn maximum_resource_totals_fit_the_stable_bound() {
1368 let record = IntentResourceTotalsRecord {
1369 reserved_qty: u64::MAX,
1370 committed_qty: u64::MAX,
1371 pending_count: u64::MAX,
1372 retain_until_secs: Some(u64::MAX),
1373 };
1374 let bytes = record.to_bytes();
1375 assert!(bytes.len() <= IntentResourceTotalsRecord::STORABLE_MAX_SIZE as usize);
1376 assert_eq!(IntentResourceTotalsRecord::from_bytes(bytes), record);
1377 }
1378
1379 #[test]
1380 fn receipt_backed_allocations_round_trip_through_canonical_data_snapshots() {
1381 IntentStore::reset_for_tests();
1382 let application_operation_id = OperationId::from_bytes([7; 32]);
1383 let placement_operation_id = OperationId::from_bytes([8; 32]);
1384 let evidence = TerminalEvidence::new(
1385 Principal::from_slice(&[1; 29]),
1386 TerminalEvidenceDecision::Committed,
1387 [8; 32],
1388 );
1389 let record = ReceiptBackedIntentRecord {
1390 application_retention: Some(ApplicationReceiptRetentionRecord {
1391 replay_deadline_ns: 23,
1392 }),
1393 schema_version: RECEIPT_BACKED_INTENT_SCHEMA_VERSION,
1394 operation_id: application_operation_id,
1395 payload_binding: PayloadBinding::new([9; 32]),
1396 resource_key: IntentResourceKey::new("mint:collection"),
1397 quantity: 11,
1398 state: ReceiptBackedIntentState::Committed { evidence },
1399 revision: 2,
1400 created_at_ns: 13,
1401 updated_at_ns: 17,
1402 };
1403 ReceiptBackedIntentStore::insert(record);
1404 ReceiptBackedIntentStore::insert(ReceiptBackedIntentRecord {
1405 application_retention: None,
1406 schema_version: RECEIPT_BACKED_INTENT_SCHEMA_VERSION,
1407 operation_id: placement_operation_id,
1408 payload_binding: PayloadBinding::new([10; 32]),
1409 resource_key: IntentResourceKey::new(format!("canic:placement:{}", "a".repeat(64))),
1410 quantity: 1,
1411 state: ReceiptBackedIntentState::Committed { evidence },
1412 revision: 2,
1413 created_at_ns: 13,
1414 updated_at_ns: 17,
1415 });
1416 let eligibility_key = ApplicationReceiptEligibilityKeyRecord {
1417 eligible_at_ns: 17 + crate::model::intent::RECEIPT_TERMINAL_OBSERVATION_GRACE_NS,
1418 operation_id: application_operation_id,
1419 };
1420 ReceiptBackedIntentStore::insert_application_eligibility(
1421 eligibility_key,
1422 ApplicationReceiptEligibilityRecord {
1423 schema_version: APPLICATION_RECEIPT_ELIGIBILITY_SCHEMA_VERSION,
1424 operation_id: application_operation_id,
1425 payload_binding: PayloadBinding::new([9; 32]),
1426 terminal_revision: 2,
1427 },
1428 );
1429 ReceiptBackedIntentStore::insert_placement_acknowledgement(
1430 PlacementAcknowledgementEntryRecord {
1431 operation_id: placement_operation_id,
1432 },
1433 );
1434 let records_data = ReceiptBackedIntentStore::export_records();
1435 let eligibility_data = ReceiptBackedIntentStore::export_application_eligibility();
1436 let acknowledgement_data =
1437 ReceiptBackedIntentStore::export_placement_acknowledgement_index();
1438
1439 ReceiptBackedIntentStore::reset_for_tests();
1440 assert_eq!(
1441 ReceiptBackedIntentStore::export_records(),
1442 ReceiptBackedIntentsData::default()
1443 );
1444 assert_eq!(
1445 ReceiptBackedIntentStore::export_application_eligibility(),
1446 ApplicationReceiptEligibilityData::default()
1447 );
1448 assert_eq!(
1449 ReceiptBackedIntentStore::export_placement_acknowledgement_index(),
1450 PlacementAcknowledgementIndexData::default()
1451 );
1452
1453 ReceiptBackedIntentStore::import_records(records_data.clone());
1454 ReceiptBackedIntentStore::import_application_eligibility(eligibility_data.clone());
1455 ReceiptBackedIntentStore::import_placement_acknowledgement_index(
1456 acknowledgement_data.clone(),
1457 );
1458 assert_eq!(ReceiptBackedIntentStore::len(), 2);
1459 assert_eq!(ReceiptBackedIntentStore::export_records(), records_data);
1460 assert_eq!(
1461 ReceiptBackedIntentStore::export_application_eligibility(),
1462 eligibility_data
1463 );
1464 assert_eq!(
1465 ReceiptBackedIntentStore::export_placement_acknowledgement_index(),
1466 acknowledgement_data
1467 );
1468 IntentStore::reset_for_tests();
1469 }
1470}