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