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