Skip to main content

ic_query/icrc/model/contracts/reports/
account_history.rs

1//! Module: icrc::model::contracts::reports::account_history
2//!
3//! Responsibility: serialized ICRC account-history page, cache, and row contracts.
4//! Does not own: ledger-wide history, requests, live transport, cache mechanics, or rendering.
5//! Boundary: owns the stable JSON and persisted snapshot shapes for indexed account history.
6
7use crate::cache::{CacheCollectionCompleteness, CacheValidationStatus};
8use serde::{Deserialize as SerdeDeserialize, Serialize};
9use serde_json::Value as JsonValue;
10
11///
12/// IcrcAccountTransactionPageData
13///
14/// Source-layer account-history page returned by an ICRC index canister.
15///
16
17#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
18pub struct IcrcAccountTransactionPageData {
19    /// Index canister that answered the query.
20    pub index_canister_id: String,
21    /// Account balance reported by the index.
22    pub balance: String,
23    /// Oldest transaction id known for the account.
24    pub oldest_transaction_id: Option<String>,
25    /// Cursor for requesting the next older page.
26    pub next_start: Option<String>,
27    /// Ledger token symbol.
28    pub token_symbol: String,
29    /// Ledger token decimals.
30    pub decimals: u8,
31    /// Account transactions in index response order.
32    pub transactions: Vec<IcrcAccountTransactionRow>,
33}
34
35///
36/// IcrcAccountTransactionPageReport
37///
38/// Serializable report for a backward page of ICRC index account transactions.
39///
40
41#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
42pub struct IcrcAccountTransactionPageReport {
43    /// Shared record, flattened in serialized output.
44    #[serde(flatten)]
45    pub provenance: super::IcrcReportProvenance,
46    /// Queried account owner principal.
47    pub account_owner: String,
48    /// Queried subaccount as normalized hex.
49    pub subaccount_hex: Option<String>,
50    /// Exclusive block-index cursor supplied by the caller.
51    pub requested_start: Option<String>,
52    /// Maximum number of transactions requested.
53    pub requested_limit: u32,
54    /// Validated index response and ledger display metadata.
55    #[serde(flatten)]
56    pub page: IcrcAccountTransactionPageData,
57}
58
59///
60/// IcrcAccountTransactionContext
61///
62/// Collected account identity and acquisition metadata shared by complete-history views.
63///
64
65#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
66pub struct IcrcAccountTransactionContext {
67    /// Ledger canister whose history was collected.
68    pub ledger_canister_id: String,
69    /// Verified index canister used for every page.
70    pub index_canister_id: String,
71    /// Queried account owner principal.
72    pub account_owner: String,
73    /// Queried subaccount as normalized hex.
74    pub subaccount_hex: Option<String>,
75    /// Explicit IC API endpoint used for collection.
76    pub source_endpoint: String,
77    /// Complete collection start timestamp.
78    pub collection_started_at: String,
79    /// Complete collection finish timestamp.
80    pub collection_completed_at: String,
81    /// Collector identity.
82    pub fetched_by: String,
83}
84
85///
86/// IcrcAccountTransactionSnapshot
87///
88/// Complete persisted account-history snapshot collected by exhausting the index API.
89///
90
91#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
92#[serde(deny_unknown_fields)]
93pub struct IcrcAccountTransactionSnapshot {
94    /// Shared identity and collection metadata, flattened in JSON.
95    #[serde(flatten)]
96    pub context: IcrcAccountTransactionContext,
97    /// Cache schema version.
98    pub schema_version: u32,
99    /// Account balance reported by the first index page.
100    pub balance: String,
101    /// Ledger token symbol used for text rendering.
102    pub token_symbol: String,
103    /// Ledger token decimals used for text rendering.
104    pub decimals: u8,
105    /// Highest collected transaction id.
106    pub newest_transaction_id: Option<String>,
107    /// Lowest collected transaction id.
108    pub oldest_transaction_id: Option<String>,
109    /// Complete-collection evidence.
110    pub completeness: CacheCollectionCompleteness,
111    /// Canonical newest-first account transactions.
112    pub transactions: Vec<IcrcAccountTransactionRow>,
113}
114
115///
116/// IcrcAccountTransactionRefreshReport
117///
118/// Serializable forced-refresh outcome for one complete account-history cache.
119///
120
121#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
122pub struct IcrcAccountTransactionRefreshReport {
123    /// Shared identity and collection metadata, flattened in JSON.
124    #[serde(flatten)]
125    pub context: IcrcAccountTransactionContext,
126    /// Report schema version.
127    pub schema_version: u32,
128    /// Number of unique transactions published.
129    pub transaction_count: usize,
130    /// Highest published transaction id.
131    pub newest_transaction_id: Option<String>,
132    /// Lowest published transaction id.
133    pub oldest_transaction_id: Option<String>,
134    /// Maximum transactions requested per source page.
135    pub page_size: u32,
136    /// Number of source pages collected.
137    pub page_count: u32,
138    /// Whether the source guarantees one point-in-time snapshot.
139    pub point_in_time_guaranteed: bool,
140    /// Whether a prior complete cache existed.
141    pub replaced_existing_cache: bool,
142    /// Non-fatal error encountered finalizing the refresh-attempt sidecar.
143    pub attempt_finalization_error: Option<String>,
144    /// Published complete-cache path.
145    pub cache_path: String,
146    /// Refresh-attempt sidecar path.
147    pub refresh_attempt_path: String,
148    /// Refresh lock path.
149    pub refresh_lock_path: String,
150}
151
152///
153/// IcrcAccountTransactionListReport
154///
155/// Serializable cache-only view over a complete account-history snapshot.
156///
157
158#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
159pub struct IcrcAccountTransactionListReport {
160    /// Shared identity and collection metadata, flattened in JSON.
161    #[serde(flatten)]
162    pub context: IcrcAccountTransactionContext,
163    /// Report schema version.
164    pub schema_version: u32,
165    /// Maximum cached rows requested by this view.
166    pub requested_limit: u32,
167    /// Stable requested ordering name.
168    pub sort: String,
169    /// Total rows in the complete cache.
170    pub total_transaction_count: usize,
171    /// Rows returned by this view.
172    pub returned_transaction_count: usize,
173    /// Highest transaction id in the complete cache.
174    pub newest_transaction_id: Option<String>,
175    /// Lowest transaction id in the complete cache.
176    pub oldest_transaction_id: Option<String>,
177    /// Account balance captured from the first index page.
178    pub balance: String,
179    /// Ledger token symbol used for text rendering.
180    pub token_symbol: String,
181    /// Ledger token decimals used for text rendering.
182    pub decimals: u8,
183    /// Whether source exhaustion was proven.
184    pub complete: bool,
185    /// Whether the source guaranteed one point-in-time snapshot.
186    pub point_in_time_guaranteed: bool,
187    /// Maximum transactions requested per source page.
188    pub page_size: u32,
189    /// Number of source pages collected.
190    pub page_count: u32,
191    /// Complete-cache path read by this view.
192    pub cache_path: String,
193    /// Selected cached rows in requested order.
194    pub transactions: Vec<IcrcAccountTransactionRow>,
195}
196
197///
198/// IcrcAccountTransactionCacheStatusReport
199///
200/// Serializable local cache and latest-refresh status.
201///
202
203#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
204pub struct IcrcAccountTransactionCacheStatusReport {
205    /// Report schema version.
206    pub schema_version: u32,
207    /// Ledger canister in the requested cache identity.
208    pub ledger_canister_id: String,
209    /// Account owner in the requested cache identity.
210    pub account_owner: String,
211    /// Subaccount in the requested cache identity.
212    pub subaccount_hex: Option<String>,
213    /// IC API endpoint in the requested cache identity.
214    pub source_endpoint: String,
215    /// Whether a cache file exists at the expected path.
216    pub found: bool,
217    /// Validation summary when a cache file exists.
218    pub cache: Option<IcrcAccountTransactionCacheSummary>,
219    /// Expected complete-cache path.
220    pub expected_cache_path: String,
221    /// Refresh-attempt sidecar path.
222    pub refresh_attempt_path: String,
223    /// Refresh lock path.
224    pub refresh_lock_path: String,
225    /// Latest refresh-attempt state when present.
226    pub latest_attempt: Option<IcrcAccountTransactionRefreshAttemptStatus>,
227}
228
229///
230/// IcrcAccountTransactionCacheSummary
231///
232/// Existing-file admission outcome; invalid files carry no invented collection metadata.
233///
234
235#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
236#[serde(tag = "cache_status", rename_all = "snake_case")]
237pub enum IcrcAccountTransactionCacheSummary {
238    /// One admitted complete snapshot.
239    Valid {
240        /// Verified collection facts, flattened in JSON.
241        #[serde(flatten)]
242        data: IcrcAccountTransactionValidCacheSummary,
243    },
244    /// Existing file that failed admission.
245    Invalid {
246        /// Existing file path.
247        cache_path: String,
248        /// Original validation failure.
249        cache_error: String,
250    },
251}
252
253impl IcrcAccountTransactionCacheSummary {
254    /// Return the typed admission status.
255    #[must_use]
256    pub const fn cache_status(&self) -> CacheValidationStatus {
257        match self {
258            Self::Valid { .. } => CacheValidationStatus::Valid,
259            Self::Invalid { .. } => CacheValidationStatus::Invalid,
260        }
261    }
262}
263
264///
265/// IcrcAccountTransactionValidCacheSummary
266///
267/// Facts read only from an admitted complete account-history snapshot.
268///
269
270#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
271pub struct IcrcAccountTransactionValidCacheSummary {
272    /// Verified index canister.
273    pub index_canister_id: String,
274    /// Number of cached transaction rows.
275    pub transaction_count: usize,
276    /// Highest cached transaction id.
277    pub newest_transaction_id: Option<String>,
278    /// Lowest cached transaction id.
279    pub oldest_transaction_id: Option<String>,
280    /// Maximum transactions requested per source page.
281    pub page_size: u32,
282    /// Number of source pages collected.
283    pub page_count: u32,
284    /// Whether the source guaranteed one point-in-time snapshot.
285    pub point_in_time_guaranteed: bool,
286    /// Complete collection start timestamp.
287    pub collection_started_at: String,
288    /// Complete collection finish timestamp.
289    pub collection_completed_at: String,
290    /// Complete-cache path.
291    pub cache_path: String,
292}
293
294///
295/// IcrcAccountTransactionRefreshAttemptStatus
296///
297/// Serializable status of the latest complete-history refresh attempt.
298///
299
300#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
301pub struct IcrcAccountTransactionRefreshAttemptStatus {
302    /// Explicit or resolved index canister recorded by the attempt.
303    pub index_canister_id: Option<String>,
304    /// Validated shared lifecycle evidence.
305    #[serde(flatten)]
306    pub lifecycle: crate::cache::SnapshotRefreshAttemptStatus,
307}
308
309///
310/// IcrcAccountRow
311///
312/// Structured ledger account or legacy ICP account identifier in transaction rows.
313///
314
315#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
316#[serde(tag = "account_kind", rename_all = "snake_case", deny_unknown_fields)]
317pub enum IcrcAccountRow {
318    /// ICRC ledger account identity.
319    Structured {
320        /// Account owner principal.
321        owner: String,
322        /// Optional 32-byte subaccount as lowercase hex.
323        subaccount_hex: Option<String>,
324    },
325    /// Legacy ICP account identifier retained exactly as returned by the index.
326    Legacy {
327        /// Legacy account identifier text.
328        account_identifier: String,
329    },
330}
331
332///
333/// IcrcAccountTransactionRow
334///
335/// Serializable projected and lossless JSON representation of one index transaction.
336///
337
338#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
339pub struct IcrcAccountTransactionRow {
340    /// Ledger block index of the transaction.
341    pub id: String,
342    /// Index-reported transaction kind.
343    pub kind: String,
344    /// Ledger transaction timestamp as Unix nanoseconds when present.
345    pub timestamp_unix_nanos: Option<String>,
346    /// Operation amount in ledger base units when the operation carries one.
347    pub amount_base_units: Option<String>,
348    /// Operation fee in ledger base units when the operation carries one.
349    pub fee_base_units: Option<String>,
350    /// Source account when present.
351    pub from: Option<IcrcAccountRow>,
352    /// Destination account when present.
353    pub to: Option<IcrcAccountRow>,
354    /// Spender account when present.
355    pub spender: Option<IcrcAccountRow>,
356    /// Operation memo as lowercase hex when present.
357    pub memo_hex: Option<String>,
358    /// Caller-supplied creation time as Unix nanoseconds when present.
359    pub created_at_time_unix_nanos: Option<String>,
360    /// Approval expiry as Unix nanoseconds when present.
361    pub expires_at_unix_nanos: Option<String>,
362    /// Expected prior allowance in base units when present.
363    pub expected_allowance_base_units: Option<String>,
364    /// Lossless JSON projection of every typed transaction field returned by the index.
365    pub raw_transaction: JsonValue,
366}
367
368#[cfg(test)]
369mod account_tests {
370    use super::IcrcAccountRow;
371    use serde_json::json;
372
373    #[test]
374    fn account_forms_round_trip_and_refuse_ambiguous_or_former_shapes() {
375        for account in [
376            IcrcAccountRow::Structured {
377                owner: "aaaaa-aa".to_string(),
378                subaccount_hex: None,
379            },
380            IcrcAccountRow::Legacy {
381                account_identifier: "original-index-identifier".to_string(),
382            },
383        ] {
384            let value = serde_json::to_value(&account).unwrap();
385            assert_eq!(
386                serde_json::from_value::<IcrcAccountRow>(value).unwrap(),
387                account
388            );
389        }
390        for value in [
391            json!({"owner":"aaaaa-aa", "subaccount_hex":null, "account_identifier":null}),
392            json!({"account_kind":"legacy", "account_identifier":"id", "owner":"aaaaa-aa"}),
393            json!({"account_kind":"structured", "subaccount_hex":null}),
394            json!({"account_kind":"structured", "owner":"aaaaa-aa", "account_identifier":"id"}),
395        ] {
396            assert!(serde_json::from_value::<IcrcAccountRow>(value).is_err());
397        }
398    }
399}