Skip to main content

ic_query/icrc/
model.rs

1//! Module: icrc::model
2//!
3//! Responsibility: typed ICRC command requests, reports, source rows, and errors.
4//! Does not own: clap parsing, live transport, or text rendering.
5//! Boundary: preserves raw ICRC fields for stable JSON output.
6
7#[cfg(feature = "cli")]
8use crate::cli::common::CurrentUnixSecsError;
9use crate::hex::hex_bytes;
10#[cfg(feature = "host")]
11use crate::runtime::RuntimeError;
12use serde::Serialize;
13use serde_json::Value as JsonValue;
14use std::io;
15use thiserror::Error as ThisError;
16
17///
18/// IcrcError
19///
20/// Error surfaced by generic ICRC command parsing, report building, and live calls.
21///
22#[derive(Debug, ThisError)]
23pub enum IcrcError {
24    #[error("{0}")]
25    Usage(String),
26
27    #[cfg(feature = "cli")]
28    #[error(transparent)]
29    Clock(#[from] CurrentUnixSecsError),
30
31    #[cfg(feature = "host")]
32    #[error("failed to create Tokio runtime for ICRC query: {0}")]
33    Runtime(#[from] RuntimeError),
34
35    #[error("failed to build IC agent for endpoint {endpoint}: {reason}")]
36    AgentBuild { endpoint: String, reason: String },
37
38    #[error("invalid {field}: {reason}")]
39    InvalidPrincipal { field: &'static str, reason: String },
40
41    #[error("invalid subaccount hex: {reason}")]
42    InvalidSubaccountHex { reason: String },
43
44    #[error("invalid subaccount length: expected 32 bytes, got {bytes}")]
45    InvalidSubaccountLength { bytes: usize },
46
47    #[error("failed to encode Candid request for {message}: {reason}")]
48    CandidEncode {
49        message: &'static str,
50        reason: String,
51    },
52
53    #[error("ICRC ledger method {method} failed: {reason}")]
54    AgentCall {
55        method: &'static str,
56        reason: String,
57    },
58
59    #[error("failed to decode Candid response {message}: {reason}")]
60    CandidDecode {
61        message: &'static str,
62        reason: String,
63    },
64
65    #[error(transparent)]
66    Io(#[from] io::Error),
67
68    #[error(transparent)]
69    Json(#[from] serde_json::Error),
70}
71
72///
73/// IcrcTokenRequest
74///
75/// Request accepted by the generic ICRC token metadata report builder.
76///
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub struct IcrcTokenRequest {
79    pub source_endpoint: String,
80    pub now_unix_secs: u64,
81    pub ledger_canister_id: String,
82}
83
84///
85/// IcrcBalanceRequest
86///
87/// Request accepted by the generic ICRC account balance report builder.
88///
89#[derive(Clone, Debug, Eq, PartialEq)]
90pub struct IcrcBalanceRequest {
91    pub source_endpoint: String,
92    pub now_unix_secs: u64,
93    pub ledger_canister_id: String,
94    pub account_owner: String,
95    pub subaccount_hex: Option<String>,
96}
97
98///
99/// IcrcAllowanceRequest
100///
101/// Request accepted by the generic ICRC allowance report builder.
102///
103#[derive(Clone, Debug, Eq, PartialEq)]
104pub struct IcrcAllowanceRequest {
105    pub source_endpoint: String,
106    pub now_unix_secs: u64,
107    pub ledger_canister_id: String,
108    pub account_owner: String,
109    pub account_subaccount_hex: Option<String>,
110    pub spender_owner: String,
111    pub spender_subaccount_hex: Option<String>,
112}
113
114///
115/// IcrcIndexRequest
116///
117/// Request accepted by the generic ICRC index discovery report builder.
118///
119#[derive(Clone, Debug, Eq, PartialEq)]
120pub struct IcrcIndexRequest {
121    pub source_endpoint: String,
122    pub now_unix_secs: u64,
123    pub ledger_canister_id: String,
124}
125
126///
127/// IcrcTransactionsRequest
128///
129/// Request accepted by the generic ICRC transaction history report builder.
130///
131#[derive(Clone, Debug, Eq, PartialEq)]
132pub struct IcrcTransactionsRequest {
133    pub source_endpoint: String,
134    pub now_unix_secs: u64,
135    pub ledger_canister_id: String,
136    pub start: u64,
137    pub limit: u32,
138    pub follow_archives: bool,
139}
140
141///
142/// IcrcBlockTypesRequest
143///
144/// Request accepted by the generic ICRC supported block types report builder.
145///
146#[derive(Clone, Debug, Eq, PartialEq)]
147pub struct IcrcBlockTypesRequest {
148    pub source_endpoint: String,
149    pub now_unix_secs: u64,
150    pub ledger_canister_id: String,
151}
152
153///
154/// IcrcArchivesRequest
155///
156/// Request accepted by the generic ICRC archives report builder.
157///
158#[derive(Clone, Debug, Eq, PartialEq)]
159pub struct IcrcArchivesRequest {
160    pub source_endpoint: String,
161    pub now_unix_secs: u64,
162    pub ledger_canister_id: String,
163    pub from_canister_id: Option<String>,
164}
165
166///
167/// IcrcTipCertificateRequest
168///
169/// Request accepted by the generic ICRC-3 tip certificate report builder.
170///
171#[derive(Clone, Debug, Eq, PartialEq)]
172pub struct IcrcTipCertificateRequest {
173    pub source_endpoint: String,
174    pub now_unix_secs: u64,
175    pub ledger_canister_id: String,
176}
177
178///
179/// IcrcCapabilitiesRequest
180///
181/// Request accepted by the generic ICRC ledger capabilities report builder.
182///
183#[derive(Clone, Debug, Eq, PartialEq)]
184pub struct IcrcCapabilitiesRequest {
185    pub source_endpoint: String,
186    pub now_unix_secs: u64,
187    pub ledger_canister_id: String,
188}
189
190///
191/// IcrcTokenReport
192///
193/// Serializable report for generic ICRC ledger token metadata.
194///
195#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
196pub struct IcrcTokenReport {
197    pub schema_version: u32,
198    pub ledger_canister_id: String,
199    pub fetched_at: String,
200    pub source_endpoint: String,
201    pub fetched_by: String,
202    pub token_name: String,
203    pub token_symbol: String,
204    pub decimals: u8,
205    pub transfer_fee: String,
206    pub total_supply: String,
207    pub minting_account_owner: Option<String>,
208    pub minting_account_subaccount_hex: Option<String>,
209    pub supported_standards: Vec<IcrcTokenStandardRow>,
210    pub metadata: Vec<IcrcTokenMetadataRow>,
211}
212
213///
214/// IcrcBalanceReport
215///
216/// Serializable report for one generic ICRC account balance lookup.
217///
218#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
219pub struct IcrcBalanceReport {
220    pub schema_version: u32,
221    pub ledger_canister_id: String,
222    pub account_owner: String,
223    pub subaccount_hex: Option<String>,
224    pub fetched_at: String,
225    pub source_endpoint: String,
226    pub fetched_by: String,
227    pub token_symbol: String,
228    pub decimals: u8,
229    pub balance: String,
230}
231
232///
233/// IcrcAllowanceReport
234///
235/// Serializable report for one generic ICRC allowance lookup.
236///
237#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
238pub struct IcrcAllowanceReport {
239    pub schema_version: u32,
240    pub ledger_canister_id: String,
241    pub account_owner: String,
242    pub account_subaccount_hex: Option<String>,
243    pub spender_owner: String,
244    pub spender_subaccount_hex: Option<String>,
245    pub fetched_at: String,
246    pub source_endpoint: String,
247    pub fetched_by: String,
248    pub token_symbol: String,
249    pub decimals: u8,
250    pub allowance: String,
251    pub expires_at_unix_nanos: Option<String>,
252}
253
254///
255/// IcrcIndexReport
256///
257/// Serializable report for one generic ICRC-106 index discovery lookup.
258///
259#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
260pub struct IcrcIndexReport {
261    pub schema_version: u32,
262    pub ledger_canister_id: String,
263    pub fetched_at: String,
264    pub source_endpoint: String,
265    pub fetched_by: String,
266    pub index_canister_id: Option<String>,
267    pub index_error: Option<String>,
268}
269
270///
271/// IcrcTransactionsReport
272///
273/// Serializable report for a generic ICRC ledger transaction/block history page.
274///
275#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
276pub struct IcrcTransactionsReport {
277    pub schema_version: u32,
278    pub ledger_canister_id: String,
279    pub fetched_at: String,
280    pub source_endpoint: String,
281    pub fetched_by: String,
282    pub requested_start: String,
283    pub requested_limit: u32,
284    pub follow_archives: bool,
285    pub log_length: Option<String>,
286    pub blocks: Vec<IcrcTransactionBlockRow>,
287    pub archived_blocks: Vec<IcrcArchivedBlocksRow>,
288    pub followed_archive_blocks: Vec<IcrcFollowedArchiveBlockRow>,
289    pub archive_follow_errors: Vec<IcrcArchiveFollowErrorRow>,
290}
291
292///
293/// IcrcBlockTypesReport
294///
295/// Serializable report for generic ICRC-3 supported block type discovery.
296///
297#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
298pub struct IcrcBlockTypesReport {
299    pub schema_version: u32,
300    pub ledger_canister_id: String,
301    pub fetched_at: String,
302    pub source_endpoint: String,
303    pub fetched_by: String,
304    pub block_types: Vec<IcrcBlockTypeRow>,
305}
306
307///
308/// IcrcArchivesReport
309///
310/// Serializable report for generic ICRC-3 archive range discovery.
311///
312#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
313pub struct IcrcArchivesReport {
314    pub schema_version: u32,
315    pub ledger_canister_id: String,
316    pub from_canister_id: Option<String>,
317    pub fetched_at: String,
318    pub source_endpoint: String,
319    pub fetched_by: String,
320    pub archives: Vec<IcrcArchiveRow>,
321}
322
323///
324/// IcrcTipCertificateReport
325///
326/// Serializable report for a generic ICRC-3 ledger tip certificate.
327///
328#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
329pub struct IcrcTipCertificateReport {
330    pub schema_version: u32,
331    pub ledger_canister_id: String,
332    pub fetched_at: String,
333    pub source_endpoint: String,
334    pub fetched_by: String,
335    pub certificate_present: bool,
336    pub certificate_hex: Option<String>,
337    pub certificate_bytes: Option<usize>,
338    pub hash_tree_hex: Option<String>,
339    pub hash_tree_bytes: Option<usize>,
340}
341
342///
343/// IcrcCapabilitiesReport
344///
345/// Serializable report for generic ICRC ledger endpoint capabilities.
346///
347#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
348pub struct IcrcCapabilitiesReport {
349    pub schema_version: u32,
350    pub ledger_canister_id: String,
351    pub fetched_at: String,
352    pub source_endpoint: String,
353    pub fetched_by: String,
354    pub supported_standards: Vec<IcrcTokenStandardRow>,
355    pub capabilities: Vec<IcrcCapabilityRow>,
356}
357
358///
359/// IcrcCapabilityRow
360///
361/// Serializable row for one probed generic ICRC ledger capability.
362///
363#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
364pub struct IcrcCapabilityRow {
365    pub capability: String,
366    pub method: String,
367    pub status: String,
368    pub details: Option<String>,
369    pub error: Option<String>,
370}
371
372///
373/// IcrcTokenStandardRow
374///
375/// Serializable row for one ICRC standard supported by a ledger.
376///
377#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
378pub struct IcrcTokenStandardRow {
379    pub name: String,
380    pub url: String,
381}
382
383///
384/// IcrcTokenMetadataRow
385///
386/// Serializable row for one raw ICRC ledger metadata entry.
387///
388#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
389pub struct IcrcTokenMetadataRow {
390    pub key: String,
391    pub value_type: String,
392    pub value: JsonValue,
393}
394
395///
396/// IcrcTransactionBlockRow
397///
398/// Serializable row for one ICRC-3 block returned by a ledger canister.
399///
400#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
401pub struct IcrcTransactionBlockRow {
402    pub index: String,
403    pub block_type: Option<String>,
404    pub transaction_kind: Option<String>,
405    pub timestamp_unix_nanos: Option<String>,
406    pub amount_base_units: Option<String>,
407    pub raw_block: JsonValue,
408}
409
410///
411/// IcrcArchivedBlocksRow
412///
413/// Serializable row for one ICRC-3 archive callback returned by a ledger canister.
414///
415#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
416pub struct IcrcArchivedBlocksRow {
417    pub callback_canister_id: String,
418    pub callback_method: String,
419    pub ranges: Vec<IcrcArchivedRangeRow>,
420}
421
422///
423/// IcrcArchivedRangeRow
424///
425/// Serializable row for one ICRC-3 archived block range.
426///
427#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
428pub struct IcrcArchivedRangeRow {
429    pub start: String,
430    pub length: String,
431}
432
433///
434/// IcrcFollowedArchiveBlockRow
435///
436/// Serializable row for one ICRC-3 block fetched from an archive callback.
437///
438#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
439pub struct IcrcFollowedArchiveBlockRow {
440    pub archive_canister_id: String,
441    pub callback_method: String,
442    pub index: String,
443    pub block_type: Option<String>,
444    pub transaction_kind: Option<String>,
445    pub timestamp_unix_nanos: Option<String>,
446    pub amount_base_units: Option<String>,
447    pub raw_block: JsonValue,
448}
449
450///
451/// IcrcArchiveFollowErrorRow
452///
453/// Serializable row for one archive callback that could not be followed.
454///
455#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
456pub struct IcrcArchiveFollowErrorRow {
457    pub callback_canister_id: String,
458    pub callback_method: String,
459    pub ranges: Vec<IcrcArchivedRangeRow>,
460    pub error: String,
461}
462
463///
464/// IcrcBlockTypeRow
465///
466/// Serializable row for one supported ICRC-3 block type.
467///
468#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
469pub struct IcrcBlockTypeRow {
470    pub block_type: String,
471    pub url: String,
472}
473
474///
475/// IcrcArchiveRow
476///
477/// Serializable row for one ICRC-3 archive range.
478///
479#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
480pub struct IcrcArchiveRow {
481    pub canister_id: String,
482    pub start: String,
483    pub end: String,
484}
485
486///
487/// IcrcTokenData
488///
489/// Source-layer token metadata returned by an ICRC ledger.
490///
491#[derive(Clone, Debug, Eq, PartialEq)]
492pub(in crate::icrc) struct IcrcTokenData {
493    pub(in crate::icrc) token_name: String,
494    pub(in crate::icrc) token_symbol: String,
495    pub(in crate::icrc) decimals: u8,
496    pub(in crate::icrc) transfer_fee: String,
497    pub(in crate::icrc) total_supply: String,
498    pub(in crate::icrc) minting_account_owner: Option<String>,
499    pub(in crate::icrc) minting_account_subaccount_hex: Option<String>,
500    pub(in crate::icrc) supported_standards: Vec<IcrcTokenStandardRow>,
501    pub(in crate::icrc) metadata: Vec<IcrcTokenMetadataRow>,
502}
503
504///
505/// IcrcBalanceData
506///
507/// Source-layer balance result plus enough token metadata for display.
508///
509#[derive(Clone, Debug, Eq, PartialEq)]
510pub(in crate::icrc) struct IcrcBalanceData {
511    pub(in crate::icrc) token_symbol: String,
512    pub(in crate::icrc) decimals: u8,
513    pub(in crate::icrc) balance: String,
514}
515
516///
517/// IcrcAllowanceData
518///
519/// Source-layer allowance result plus enough token metadata for display.
520///
521#[derive(Clone, Debug, Eq, PartialEq)]
522pub(in crate::icrc) struct IcrcAllowanceData {
523    pub(in crate::icrc) token_symbol: String,
524    pub(in crate::icrc) decimals: u8,
525    pub(in crate::icrc) allowance: String,
526    pub(in crate::icrc) expires_at_unix_nanos: Option<String>,
527}
528
529///
530/// IcrcIndexData
531///
532/// Source-layer ICRC-106 index discovery result.
533///
534#[derive(Clone, Debug, Eq, PartialEq)]
535pub(in crate::icrc) struct IcrcIndexData {
536    pub(in crate::icrc) index_canister_id: Option<String>,
537    pub(in crate::icrc) index_error: Option<String>,
538}
539
540///
541/// IcrcTransactionsData
542///
543/// Source-layer ICRC-3 block history result from a ledger canister.
544///
545#[derive(Clone, Debug, PartialEq)]
546pub(in crate::icrc) struct IcrcTransactionsData {
547    pub(in crate::icrc) log_length: Option<String>,
548    pub(in crate::icrc) blocks: Vec<IcrcTransactionBlockRow>,
549    pub(in crate::icrc) archived_blocks: Vec<IcrcArchivedBlocksRow>,
550    pub(in crate::icrc) followed_archive_blocks: Vec<IcrcFollowedArchiveBlockRow>,
551    pub(in crate::icrc) archive_follow_errors: Vec<IcrcArchiveFollowErrorRow>,
552}
553
554///
555/// IcrcBlockTypesData
556///
557/// Source-layer ICRC-3 supported block types result.
558///
559#[derive(Clone, Debug, Eq, PartialEq)]
560pub(in crate::icrc) struct IcrcBlockTypesData {
561    pub(in crate::icrc) block_types: Vec<IcrcBlockTypeRow>,
562}
563
564///
565/// IcrcArchivesData
566///
567/// Source-layer ICRC-3 archive range discovery result.
568///
569#[derive(Clone, Debug, Eq, PartialEq)]
570pub(in crate::icrc) struct IcrcArchivesData {
571    pub(in crate::icrc) archives: Vec<IcrcArchiveRow>,
572}
573
574///
575/// IcrcTipCertificateData
576///
577/// Source-layer ICRC-3 tip certificate result.
578///
579#[derive(Clone, Debug, Eq, PartialEq)]
580pub(in crate::icrc) struct IcrcTipCertificateData {
581    pub(in crate::icrc) certificate_hex: Option<String>,
582    pub(in crate::icrc) certificate_bytes: Option<usize>,
583    pub(in crate::icrc) hash_tree_hex: Option<String>,
584    pub(in crate::icrc) hash_tree_bytes: Option<usize>,
585}
586
587///
588/// IcrcCapabilitiesData
589///
590/// Source-layer generic ICRC ledger capability probe result.
591///
592#[derive(Clone, Debug, Eq, PartialEq)]
593pub(in crate::icrc) struct IcrcCapabilitiesData {
594    pub(in crate::icrc) supported_standards: Vec<IcrcTokenStandardRow>,
595    pub(in crate::icrc) capabilities: Vec<IcrcCapabilityRow>,
596}
597
598pub(in crate::icrc) fn normalize_subaccount_hex(value: &str) -> Result<String, IcrcError> {
599    let bytes = subaccount_bytes_from_hex(value)?;
600    Ok(hex_bytes(&bytes))
601}
602
603pub(in crate::icrc) fn subaccount_bytes_from_hex(value: &str) -> Result<Vec<u8>, IcrcError> {
604    let value = value.trim();
605    if !value.len().is_multiple_of(2) {
606        return Err(IcrcError::InvalidSubaccountHex {
607            reason: "hex string must contain an even number of characters".to_string(),
608        });
609    }
610    let bytes = (0..value.len())
611        .step_by(2)
612        .map(|index| {
613            u8::from_str_radix(&value[index..index + 2], 16).map_err(|err| {
614                IcrcError::InvalidSubaccountHex {
615                    reason: err.to_string(),
616                }
617            })
618        })
619        .collect::<Result<Vec<_>, _>>()?;
620    if bytes.len() != 32 {
621        return Err(IcrcError::InvalidSubaccountLength { bytes: bytes.len() });
622    }
623    Ok(bytes)
624}