Skip to main content

canic_core/storage/stable/
intent.rs

1//! Stable-memory intent store primitives.
2//!
3//! Data-only storage slots for cross-canister intent tracking. The ops layer
4//! enforces mechanical invariants (uniqueness, monotonic state transitions,
5//! aggregate consistency). Policy and capacity decisions live above this layer.
6
7use 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
29//
30// INTENT STORE
31//
32
33pub 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    /// Decode the exact fixed-width stable intent identity.
146    ///
147    /// # Panics
148    ///
149    /// Panics when stable memory contains an intent ID that is not exactly eight bytes.
150    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    /// Decode the exact fixed-width stable operation identity.
177    ///
178    /// # Panics
179    ///
180    /// Panics when stable memory contains an operation ID that is not exactly 32 bytes.
181    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/// Ordered stable key for one finite local-intent cleanup deadline.
193#[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    /// Decode the exact fixed-width stable intent-expiry key.
217    ///
218    /// # Panics
219    ///
220    /// Panics when stable memory contains a key that is not exactly sixteen bytes.
221    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/// Stable value for one finite local-intent cleanup deadline.
241#[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///
258/// IntentState
259///
260
261#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
262pub enum IntentState {
263    Pending,
264    Committed,
265    Aborted,
266}
267
268///
269/// IntentRecord
270///
271
272#[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    // TTL is enforced logically at read time; the derived index schedules cleanup.
280    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///
291/// IntentStoreMetaRecord
292///
293
294#[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///
329/// IntentResourceTotalsRecord
330///
331
332#[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    /// Window counters may be reclaimed after this deadline once nothing is pending.
338    /// Permanent resource accounting has no deadline.
339    #[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///
355/// IntentPendingEntryRecord
356///
357
358#[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    // TTL is enforced logically at read time; cleanup is asynchronous.
364    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/// Stable representation of one durable receipt-backed intent.
379#[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/// Replay retention owned by its enclosing application receipt.
420#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
421pub struct ApplicationReceiptRetentionRecord {
422    pub replay_deadline_ns: u64,
423}
424
425/// Ordered stable key for one application receipt terminal-retention deadline.
426#[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    /// Decode the exact fixed-width stable eligibility key.
450    ///
451    /// # Panics
452    ///
453    /// Panics when stable memory contains a key that is not exactly forty bytes.
454    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/// Exact immutable binding for one application terminal eligibility entry.
478#[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/// Stable value proving that an exact placement operation is queued for root acknowledgement.
498#[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///
515/// IntentMetaData
516///
517/// Canonical intent-store metadata allocation snapshot.
518///
519
520#[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///
530/// IntentRecordEntryRecord
531///
532/// One logical intent-record snapshot row preserving its stable intent ID key.
533///
534
535#[derive(Clone, Debug, Eq, PartialEq)]
536pub struct IntentRecordEntryRecord {
537    pub intent_id: IntentId,
538    pub record: IntentRecord,
539}
540
541///
542/// IntentRecordsData
543///
544/// Canonical intent-records allocation snapshot.
545///
546
547#[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///
557/// IntentTotalsEntryRecord
558///
559/// One logical intent-total snapshot row preserving its stable resource key.
560///
561
562#[derive(Clone, Debug, Eq, PartialEq)]
563pub struct IntentTotalsEntryRecord {
564    pub resource_key: IntentResourceKey,
565    pub record: IntentResourceTotalsRecord,
566}
567
568///
569/// IntentTotalsData
570///
571/// Canonical intent-resource-totals allocation snapshot.
572///
573
574#[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///
584/// IntentPendingIndexEntryRecord
585///
586/// One logical pending-intent snapshot row preserving its stable intent ID key.
587///
588
589#[derive(Clone, Debug, Eq, PartialEq)]
590pub struct IntentPendingIndexEntryRecord {
591    pub intent_id: IntentId,
592    pub record: IntentPendingEntryRecord,
593}
594
595///
596/// IntentPendingData
597///
598/// Canonical pending-intent allocation snapshot.
599///
600
601#[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/// One logical finite-expiry snapshot row preserving its stable ordered key.
611#[derive(Clone, Copy, Debug, Eq, PartialEq)]
612pub struct IntentExpiryIndexEntryRecord {
613    pub key: IntentExpiryKeyRecord,
614    pub record: IntentExpiryEntryRecord,
615}
616
617/// Canonical finite-expiry index allocation snapshot.
618#[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/// One logical receipt-backed intent snapshot row.
628#[derive(Clone, Debug, Eq, PartialEq)]
629pub struct ReceiptBackedIntentEntryRecord {
630    pub operation_id: OperationId,
631    pub record: ReceiptBackedIntentRecord,
632}
633
634/// Canonical receipt-backed intent record allocation snapshot.
635#[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/// One logical application terminal-eligibility snapshot row.
645#[derive(Clone, Copy, Debug, Eq, PartialEq)]
646pub struct ApplicationReceiptEligibilityEntryRecord {
647    pub key: ApplicationReceiptEligibilityKeyRecord,
648    pub record: ApplicationReceiptEligibilityRecord,
649}
650
651/// Canonical application terminal-eligibility allocation snapshot.
652#[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/// One logical placement-acknowledgement index snapshot row.
662#[derive(Clone, Copy, Debug, Eq, PartialEq)]
663pub struct PlacementAcknowledgementIndexEntryRecord {
664    pub operation_id: OperationId,
665    pub record: PlacementAcknowledgementEntryRecord,
666}
667
668/// Canonical placement-acknowledgement derived-index allocation snapshot.
669#[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
678///
679/// IntentStore
680///
681
682pub struct IntentStore;
683
684impl IntentStore {
685    // -------------------------------------------------------------
686    // Meta
687    // -------------------------------------------------------------
688
689    #[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    // -------------------------------------------------------------
699    // Records
700    // -------------------------------------------------------------
701
702    #[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    // -------------------------------------------------------------
712    // Totals
713    // -------------------------------------------------------------
714
715    #[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    /// Read a bounded prefix for aggregate reclamation after admission validates map size.
726    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    // -------------------------------------------------------------
750    // Pending index
751    // -------------------------------------------------------------
752
753    #[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    // -------------------------------------------------------------
778    // Finite-expiry derived index
779    // -------------------------------------------------------------
780
781    #[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
814/// Stable store for receipt-backed operations addressed by exact operation ID.
815pub 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    /// Read the maintained application count without scanning primary records.
866    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    /// Provision the pinned B-tree's maximum live-node envelope before admission.
888    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    // ic-stable-structures 0.7.2 uses minimum degree six, so every non-root
989    // node owns at least five entries. The 2,362-byte node allocation has a
990    // 16-byte allocator header; 116 bytes cover the map, allocator, and spare
991    // chunk headers. The explicit admission-limit probe locks these pinned values.
992    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//
1002// ─────────────────────────────────────────────────────────────
1003// Test helpers
1004// ─────────────────────────────────────────────────────────────
1005//
1006
1007#[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}