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, Storable, cell::Cell, memory::VirtualMemory, storable::Bound,
11    },
12    ids::{IntentId, IntentResourceKey},
13    model::{
14        intent::{PayloadBinding, ReceiptBackedIntent, ReceiptBackedIntentState},
15        replay::OperationId,
16    },
17    role_contract::allocation::memory::intent::{
18        INTENT_META_ID, INTENT_PENDING_ID, INTENT_RECORDS_ID, INTENT_TOTALS_ID,
19        RECEIPT_BACKED_INTENT_RECORDS_ID,
20    },
21    storage::prelude::*,
22};
23use std::{borrow::Cow, cell::RefCell};
24
25//
26// INTENT STORE
27//
28
29pub const INTENT_STORE_SCHEMA_VERSION: u32 = 1;
30
31eager_static! {
32    static INTENT_META: RefCell<Cell<IntentStoreMetaRecord, VirtualMemory<DefaultMemoryImpl>>> =
33        RefCell::new(Cell::init(
34            crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_meta.v1", ty = IntentStoreMetaRecord, id = INTENT_META_ID),
35            IntentStoreMetaRecord::default(),
36        ));
37}
38
39eager_static! {
40    static RECEIPT_BACKED_INTENT_RECORDS: RefCell<
41        StableBtreeMap<
42            OperationId,
43            ReceiptBackedIntentRecord,
44            VirtualMemory<DefaultMemoryImpl>,
45        >
46    > = RefCell::new(StableBtreeMap::init(crate::ic_memory_key!(
47        authority = CANIC_CORE_MEMORY_AUTHORITY,
48        key = "canic.core.receipt_backed_intent_records.v1",
49        ty = ReceiptBackedIntentRecord,
50        id = RECEIPT_BACKED_INTENT_RECORDS_ID
51    )));
52}
53
54eager_static! {
55    static INTENT_RECORDS: RefCell<
56        StableBtreeMap<IntentId, IntentRecord, VirtualMemory<DefaultMemoryImpl>>
57    > = RefCell::new(
58        StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_records.v1", ty = IntentRecord, id = INTENT_RECORDS_ID)),
59    );
60}
61
62eager_static! {
63    static INTENT_TOTALS: RefCell<
64        StableBtreeMap<IntentResourceKey, IntentResourceTotalsRecord, VirtualMemory<DefaultMemoryImpl>>
65    > = RefCell::new(
66        StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_totals.v1", ty = IntentResourceTotalsRecord, id = INTENT_TOTALS_ID)),
67    );
68}
69
70eager_static! {
71    static INTENT_PENDING: RefCell<
72        StableBtreeMap<IntentId, IntentPendingEntryRecord, VirtualMemory<DefaultMemoryImpl>>
73    > = RefCell::new(
74        StableBtreeMap::init(crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.intent_pending.v1", ty = IntentPendingEntryRecord, id = INTENT_PENDING_ID)),
75    );
76}
77
78impl Storable for IntentId {
79    const BOUND: Bound = Bound::Bounded {
80        max_size: 8,
81        is_fixed_size: true,
82    };
83
84    fn to_bytes(&self) -> Cow<'_, [u8]> {
85        Cow::Owned(self.0.to_be_bytes().to_vec())
86    }
87
88    fn into_bytes(self) -> Vec<u8> {
89        self.0.to_be_bytes().to_vec()
90    }
91
92    fn from_bytes(bytes: Cow<[u8]>) -> Self {
93        let b = bytes.as_ref();
94
95        if b.len() != 8 {
96            return Self::default();
97        }
98
99        let mut arr = [0u8; 8];
100        arr.copy_from_slice(b);
101
102        Self(u64::from_be_bytes(arr))
103    }
104}
105
106impl Storable for OperationId {
107    const BOUND: Bound = Bound::Bounded {
108        max_size: 32,
109        is_fixed_size: true,
110    };
111
112    fn to_bytes(&self) -> Cow<'_, [u8]> {
113        Cow::Borrowed(self.as_bytes())
114    }
115
116    fn into_bytes(self) -> Vec<u8> {
117        self.as_bytes().to_vec()
118    }
119
120    fn from_bytes(bytes: Cow<[u8]>) -> Self {
121        let mut operation_id = [0_u8; 32];
122        if bytes.len() == operation_id.len() {
123            operation_id.copy_from_slice(bytes.as_ref());
124        }
125        Self::from_bytes(operation_id)
126    }
127}
128
129///
130/// IntentState
131///
132
133#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
134pub enum IntentState {
135    Pending,
136    Committed,
137    Aborted,
138}
139
140///
141/// IntentRecord
142///
143
144#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
145pub struct IntentRecord {
146    pub id: IntentId,
147    pub resource_key: IntentResourceKey,
148    pub quantity: u64,
149    pub state: IntentState,
150    pub created_at: u64,
151    // TTL is enforced logically at read time; cleanup is asynchronous.
152    pub ttl_secs: Option<u64>,
153}
154
155impl IntentRecord {
156    pub const STATE_CONTRACT_NAME: &'static str = "IntentRecord";
157    pub const STORABLE_MAX_SIZE: u32 = 256;
158}
159
160impl_storable_bounded!(IntentRecord, IntentRecord::STORABLE_MAX_SIZE, false);
161
162///
163/// IntentStoreMetaRecord
164///
165
166#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
167pub struct IntentStoreMetaRecord {
168    pub schema_version: u32,
169    pub next_intent_id: IntentId,
170    pub pending_total: u64,
171    pub committed_total: u64,
172    pub aborted_total: u64,
173}
174
175impl IntentStoreMetaRecord {
176    pub const STATE_CONTRACT_NAME: &'static str = "IntentStoreMetaRecord";
177    pub const STORABLE_MAX_SIZE: u32 = 96;
178}
179
180impl Default for IntentStoreMetaRecord {
181    fn default() -> Self {
182        Self {
183            schema_version: INTENT_STORE_SCHEMA_VERSION,
184            next_intent_id: IntentId(1),
185            pending_total: 0,
186            committed_total: 0,
187            aborted_total: 0,
188        }
189    }
190}
191
192impl_storable_bounded!(
193    IntentStoreMetaRecord,
194    IntentStoreMetaRecord::STORABLE_MAX_SIZE,
195    false
196);
197
198///
199/// IntentResourceTotalsRecord
200///
201
202#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
203pub struct IntentResourceTotalsRecord {
204    pub reserved_qty: u64,
205    pub committed_qty: u64,
206    pub pending_count: u64,
207}
208
209impl IntentResourceTotalsRecord {
210    pub const STATE_CONTRACT_NAME: &'static str = "IntentResourceTotalsRecord";
211    pub const STORABLE_MAX_SIZE: u32 = 64;
212}
213
214impl_storable_bounded!(
215    IntentResourceTotalsRecord,
216    IntentResourceTotalsRecord::STORABLE_MAX_SIZE,
217    false
218);
219
220///
221/// IntentPendingEntryRecord
222///
223
224#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
225pub struct IntentPendingEntryRecord {
226    pub resource_key: IntentResourceKey,
227    pub quantity: u64,
228    pub created_at: u64,
229    // TTL is enforced logically at read time; cleanup is asynchronous.
230    pub ttl_secs: Option<u64>,
231}
232
233impl IntentPendingEntryRecord {
234    pub const STATE_CONTRACT_NAME: &'static str = "IntentPendingEntryRecord";
235    pub const STORABLE_MAX_SIZE: u32 = 224;
236}
237
238impl_storable_bounded!(
239    IntentPendingEntryRecord,
240    IntentPendingEntryRecord::STORABLE_MAX_SIZE,
241    false
242);
243
244/// Stable representation of one permanently retained receipt-backed intent.
245#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
246pub struct ReceiptBackedIntentRecord {
247    pub schema_version: u32,
248    pub operation_id: OperationId,
249    pub payload_binding: PayloadBinding,
250    pub resource_key: IntentResourceKey,
251    pub quantity: u64,
252    pub state: ReceiptBackedIntentState,
253    pub revision: u64,
254    pub created_at_ns: u64,
255    pub updated_at_ns: u64,
256}
257
258impl ReceiptBackedIntentRecord {
259    pub const STATE_CONTRACT_NAME: &'static str = "ReceiptBackedIntentRecord";
260    pub const STORABLE_MAX_SIZE: u32 = 1024;
261
262    #[must_use]
263    pub fn into_intent(self) -> ReceiptBackedIntent {
264        ReceiptBackedIntent {
265            schema_version: self.schema_version,
266            operation_id: self.operation_id,
267            payload_binding: self.payload_binding,
268            resource_key: self.resource_key,
269            quantity: self.quantity,
270            state: self.state,
271            revision: self.revision,
272            created_at_ns: self.created_at_ns,
273            updated_at_ns: self.updated_at_ns,
274        }
275    }
276}
277
278impl_storable_bounded!(
279    ReceiptBackedIntentRecord,
280    ReceiptBackedIntentRecord::STORABLE_MAX_SIZE,
281    false
282);
283
284///
285/// IntentMetaData
286///
287/// Canonical intent-store metadata allocation snapshot.
288///
289
290#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
291pub struct IntentMetaData {
292    pub record: IntentStoreMetaRecord,
293}
294
295impl IntentMetaData {
296    pub const STATE_CONTRACT_NAME: &'static str = "IntentMetaData";
297}
298
299///
300/// IntentRecordEntryRecord
301///
302/// One logical intent-record snapshot row preserving its stable intent ID key.
303///
304
305#[derive(Clone, Debug, Eq, PartialEq)]
306pub struct IntentRecordEntryRecord {
307    pub intent_id: IntentId,
308    pub record: IntentRecord,
309}
310
311///
312/// IntentRecordsData
313///
314/// Canonical intent-records allocation snapshot.
315///
316
317#[derive(Clone, Debug, Default, Eq, PartialEq)]
318pub struct IntentRecordsData {
319    pub entries: Vec<IntentRecordEntryRecord>,
320}
321
322impl IntentRecordsData {
323    pub const STATE_CONTRACT_NAME: &'static str = "IntentRecordsData";
324}
325
326///
327/// IntentTotalsEntryRecord
328///
329/// One logical intent-total snapshot row preserving its stable resource key.
330///
331
332#[derive(Clone, Debug, Eq, PartialEq)]
333pub struct IntentTotalsEntryRecord {
334    pub resource_key: IntentResourceKey,
335    pub record: IntentResourceTotalsRecord,
336}
337
338///
339/// IntentTotalsData
340///
341/// Canonical intent-resource-totals allocation snapshot.
342///
343
344#[derive(Clone, Debug, Default, Eq, PartialEq)]
345pub struct IntentTotalsData {
346    pub entries: Vec<IntentTotalsEntryRecord>,
347}
348
349impl IntentTotalsData {
350    pub const STATE_CONTRACT_NAME: &'static str = "IntentTotalsData";
351}
352
353///
354/// IntentPendingIndexEntryRecord
355///
356/// One logical pending-intent snapshot row preserving its stable intent ID key.
357///
358
359#[derive(Clone, Debug, Eq, PartialEq)]
360pub struct IntentPendingIndexEntryRecord {
361    pub intent_id: IntentId,
362    pub record: IntentPendingEntryRecord,
363}
364
365///
366/// IntentPendingData
367///
368/// Canonical pending-intent allocation snapshot.
369///
370
371#[derive(Clone, Debug, Default, Eq, PartialEq)]
372pub struct IntentPendingData {
373    pub entries: Vec<IntentPendingIndexEntryRecord>,
374}
375
376impl IntentPendingData {
377    pub const STATE_CONTRACT_NAME: &'static str = "IntentPendingData";
378}
379
380/// One logical receipt-backed intent snapshot row.
381#[derive(Clone, Debug, Eq, PartialEq)]
382pub struct ReceiptBackedIntentEntryRecord {
383    pub operation_id: OperationId,
384    pub record: ReceiptBackedIntentRecord,
385}
386
387/// Canonical receipt-backed intent record allocation snapshot.
388#[derive(Clone, Debug, Default, Eq, PartialEq)]
389pub struct ReceiptBackedIntentsData {
390    pub entries: Vec<ReceiptBackedIntentEntryRecord>,
391}
392
393impl ReceiptBackedIntentsData {
394    pub const STATE_CONTRACT_NAME: &'static str = "ReceiptBackedIntentsData";
395}
396
397///
398/// IntentStore
399///
400
401pub struct IntentStore;
402
403impl IntentStore {
404    // -------------------------------------------------------------
405    // Meta
406    // -------------------------------------------------------------
407
408    #[must_use]
409    pub(crate) fn meta() -> IntentStoreMetaRecord {
410        INTENT_META.with_borrow(|cell| *cell.get())
411    }
412
413    pub(crate) fn set_meta(meta: IntentStoreMetaRecord) {
414        INTENT_META.with_borrow_mut(|cell| cell.set(meta));
415    }
416
417    // -------------------------------------------------------------
418    // Records
419    // -------------------------------------------------------------
420
421    #[must_use]
422    pub(crate) fn get_record(id: IntentId) -> Option<IntentRecord> {
423        INTENT_RECORDS.with_borrow(|map| map.get(&id))
424    }
425
426    pub(crate) fn insert_record(record: IntentRecord) -> Option<IntentRecord> {
427        INTENT_RECORDS.with_borrow_mut(|map| map.insert(record.id, record))
428    }
429
430    // -------------------------------------------------------------
431    // Totals
432    // -------------------------------------------------------------
433
434    #[must_use]
435    pub(crate) fn get_totals(key: &IntentResourceKey) -> Option<IntentResourceTotalsRecord> {
436        INTENT_TOTALS.with_borrow(|map| map.get(key))
437    }
438
439    pub(crate) fn set_totals(
440        key: IntentResourceKey,
441        totals: IntentResourceTotalsRecord,
442    ) -> Option<IntentResourceTotalsRecord> {
443        INTENT_TOTALS.with_borrow_mut(|map| map.insert(key, totals))
444    }
445
446    // -------------------------------------------------------------
447    // Pending index
448    // -------------------------------------------------------------
449
450    #[must_use]
451    pub(crate) fn get_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
452        INTENT_PENDING.with_borrow(|map| map.get(&id))
453    }
454
455    pub(crate) fn insert_pending(
456        id: IntentId,
457        entry: IntentPendingEntryRecord,
458    ) -> Option<IntentPendingEntryRecord> {
459        INTENT_PENDING.with_borrow_mut(|map| map.insert(id, entry))
460    }
461
462    pub(crate) fn remove_pending(id: IntentId) -> Option<IntentPendingEntryRecord> {
463        INTENT_PENDING.with_borrow_mut(|map| map.remove(&id))
464    }
465
466    pub(crate) fn with_pending_entries<R>(
467        f: impl FnOnce(
468            &StableBtreeMap<IntentId, IntentPendingEntryRecord, VirtualMemory<DefaultMemoryImpl>>,
469        ) -> R,
470    ) -> R {
471        INTENT_PENDING.with_borrow(|map| f(map))
472    }
473}
474
475/// Stable store for receipt-backed operations addressed by exact operation ID.
476pub struct ReceiptBackedIntentStore;
477
478impl ReceiptBackedIntentStore {
479    #[must_use]
480    pub(crate) fn len() -> u64 {
481        RECEIPT_BACKED_INTENT_RECORDS.with_borrow(StableBtreeMap::len)
482    }
483
484    #[must_use]
485    pub(crate) fn get(operation_id: OperationId) -> Option<ReceiptBackedIntentRecord> {
486        RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| records.get(&operation_id))
487    }
488
489    pub(crate) fn insert(record: ReceiptBackedIntentRecord) -> Option<ReceiptBackedIntentRecord> {
490        RECEIPT_BACKED_INTENT_RECORDS
491            .with_borrow_mut(|records| records.insert(record.operation_id, record))
492    }
493}
494
495//
496// ─────────────────────────────────────────────────────────────
497// Test helpers
498// ─────────────────────────────────────────────────────────────
499//
500
501#[cfg(test)]
502impl IntentStore {
503    #[must_use]
504    pub(crate) fn export_meta() -> IntentMetaData {
505        IntentMetaData {
506            record: Self::meta(),
507        }
508    }
509
510    pub(crate) fn import_meta(data: IntentMetaData) {
511        Self::set_meta(data.record);
512    }
513
514    #[must_use]
515    pub(crate) fn export_records() -> IntentRecordsData {
516        IntentRecordsData {
517            entries: INTENT_RECORDS.with_borrow(|map| {
518                map.iter()
519                    .map(|entry| IntentRecordEntryRecord {
520                        intent_id: *entry.key(),
521                        record: entry.value(),
522                    })
523                    .collect()
524            }),
525        }
526    }
527
528    pub(crate) fn import_records(data: IntentRecordsData) {
529        INTENT_RECORDS.with_borrow_mut(|map| {
530            map.clear_new();
531            for entry in data.entries {
532                map.insert(entry.intent_id, entry.record);
533            }
534        });
535    }
536
537    #[must_use]
538    pub(crate) fn export_totals() -> IntentTotalsData {
539        IntentTotalsData {
540            entries: INTENT_TOTALS.with_borrow(|map| {
541                map.iter()
542                    .map(|entry| IntentTotalsEntryRecord {
543                        resource_key: entry.key().clone(),
544                        record: entry.value(),
545                    })
546                    .collect()
547            }),
548        }
549    }
550
551    pub(crate) fn import_totals(data: IntentTotalsData) {
552        INTENT_TOTALS.with_borrow_mut(|map| {
553            map.clear_new();
554            for entry in data.entries {
555                map.insert(entry.resource_key, entry.record);
556            }
557        });
558    }
559
560    #[must_use]
561    pub(crate) fn export_pending() -> IntentPendingData {
562        IntentPendingData {
563            entries: INTENT_PENDING.with_borrow(|map| {
564                map.iter()
565                    .map(|entry| IntentPendingIndexEntryRecord {
566                        intent_id: *entry.key(),
567                        record: entry.value(),
568                    })
569                    .collect()
570            }),
571        }
572    }
573
574    pub(crate) fn import_pending(data: IntentPendingData) {
575        INTENT_PENDING.with_borrow_mut(|map| {
576            map.clear_new();
577            for entry in data.entries {
578                map.insert(entry.intent_id, entry.record);
579            }
580        });
581    }
582
583    pub(crate) fn reset_for_tests() {
584        INTENT_RECORDS.with_borrow_mut(StableBtreeMap::clear_new);
585        INTENT_TOTALS.with_borrow_mut(StableBtreeMap::clear_new);
586        INTENT_PENDING.with_borrow_mut(StableBtreeMap::clear_new);
587        INTENT_META.with_borrow_mut(|cell| cell.set(IntentStoreMetaRecord::default()));
588        ReceiptBackedIntentStore::reset_for_tests();
589    }
590}
591
592#[cfg(test)]
593impl ReceiptBackedIntentStore {
594    #[must_use]
595    pub(crate) fn export_records() -> ReceiptBackedIntentsData {
596        ReceiptBackedIntentsData {
597            entries: RECEIPT_BACKED_INTENT_RECORDS.with_borrow(|records| {
598                records
599                    .iter()
600                    .map(|entry| ReceiptBackedIntentEntryRecord {
601                        operation_id: *entry.key(),
602                        record: entry.value(),
603                    })
604                    .collect()
605            }),
606        }
607    }
608
609    pub(crate) fn import_records(data: ReceiptBackedIntentsData) {
610        RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(|records| {
611            records.clear_new();
612            for entry in data.entries {
613                records.insert(entry.operation_id, entry.record);
614            }
615        });
616    }
617
618    pub(crate) fn reset_for_tests() {
619        RECEIPT_BACKED_INTENT_RECORDS.with_borrow_mut(StableBtreeMap::clear_new);
620    }
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626    use crate::{
627        cdk::types::Principal,
628        model::intent::{
629            RECEIPT_BACKED_INTENT_SCHEMA_VERSION, TerminalEvidence, TerminalEvidenceDecision,
630        },
631    };
632
633    #[test]
634    fn intent_allocations_round_trip_through_canonical_data_snapshots() {
635        IntentStore::reset_for_tests();
636        let intent_id = IntentId(7);
637        let resource_key = IntentResourceKey::new("storage:uploads");
638        let record = IntentRecord {
639            id: intent_id,
640            resource_key: resource_key.clone(),
641            quantity: 11,
642            state: IntentState::Pending,
643            created_at: 13,
644            ttl_secs: Some(17),
645        };
646        let totals = IntentResourceTotalsRecord {
647            reserved_qty: 11,
648            committed_qty: 19,
649            pending_count: 1,
650        };
651        let pending = IntentPendingEntryRecord {
652            resource_key: resource_key.clone(),
653            quantity: 11,
654            created_at: 13,
655            ttl_secs: Some(17),
656        };
657        let meta = IntentStoreMetaRecord {
658            schema_version: INTENT_STORE_SCHEMA_VERSION,
659            next_intent_id: IntentId(8),
660            pending_total: 1,
661            committed_total: 2,
662            aborted_total: 3,
663        };
664
665        IntentStore::set_meta(meta);
666        IntentStore::insert_record(record);
667        IntentStore::set_totals(resource_key, totals);
668        IntentStore::insert_pending(intent_id, pending);
669
670        let meta_data = IntentStore::export_meta();
671        let records_data = IntentStore::export_records();
672        let totals_data = IntentStore::export_totals();
673        let pending_data = IntentStore::export_pending();
674
675        IntentStore::reset_for_tests();
676        IntentStore::import_meta(meta_data);
677        IntentStore::import_records(records_data.clone());
678        IntentStore::import_totals(totals_data.clone());
679        IntentStore::import_pending(pending_data.clone());
680
681        assert_eq!(IntentStore::export_meta(), meta_data);
682        assert_eq!(IntentStore::export_records(), records_data);
683        assert_eq!(IntentStore::export_totals(), totals_data);
684        assert_eq!(IntentStore::export_pending(), pending_data);
685        IntentStore::reset_for_tests();
686    }
687
688    #[test]
689    fn receipt_backed_allocations_round_trip_through_canonical_data_snapshots() {
690        IntentStore::reset_for_tests();
691        let operation_id = OperationId::from_bytes([7; 32]);
692        let evidence = TerminalEvidence::new(
693            Principal::from_slice(&[1; 29]),
694            TerminalEvidenceDecision::Committed,
695            [8; 32],
696        );
697        let record = ReceiptBackedIntentRecord {
698            schema_version: RECEIPT_BACKED_INTENT_SCHEMA_VERSION,
699            operation_id,
700            payload_binding: PayloadBinding::new([9; 32]),
701            resource_key: IntentResourceKey::new("mint:collection"),
702            quantity: 11,
703            state: ReceiptBackedIntentState::Committed { evidence },
704            revision: 2,
705            created_at_ns: 13,
706            updated_at_ns: 17,
707        };
708        ReceiptBackedIntentStore::insert(record);
709        let records_data = ReceiptBackedIntentStore::export_records();
710
711        ReceiptBackedIntentStore::reset_for_tests();
712        assert_eq!(
713            ReceiptBackedIntentStore::export_records(),
714            ReceiptBackedIntentsData::default()
715        );
716
717        ReceiptBackedIntentStore::import_records(records_data.clone());
718        assert_eq!(ReceiptBackedIntentStore::len(), 1);
719        assert_eq!(ReceiptBackedIntentStore::export_records(), records_data);
720        IntentStore::reset_for_tests();
721    }
722}