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
85impl IcrcTokenRequest {
86    #[must_use]
87    pub fn new(
88        source_endpoint: impl Into<String>,
89        now_unix_secs: u64,
90        ledger_canister_id: impl Into<String>,
91    ) -> Self {
92        Self {
93            source_endpoint: source_endpoint.into(),
94            now_unix_secs,
95            ledger_canister_id: ledger_canister_id.into(),
96        }
97    }
98}
99
100///
101/// IcrcBalanceRequest
102///
103/// Request accepted by the generic ICRC account balance report builder.
104///
105#[derive(Clone, Debug, Eq, PartialEq)]
106pub struct IcrcBalanceRequest {
107    pub source_endpoint: String,
108    pub now_unix_secs: u64,
109    pub ledger_canister_id: String,
110    pub account_owner: String,
111    pub subaccount_hex: Option<String>,
112}
113
114impl IcrcBalanceRequest {
115    #[must_use]
116    pub fn new(
117        source_endpoint: impl Into<String>,
118        now_unix_secs: u64,
119        ledger_canister_id: impl Into<String>,
120        account_owner: impl Into<String>,
121    ) -> Self {
122        Self {
123            source_endpoint: source_endpoint.into(),
124            now_unix_secs,
125            ledger_canister_id: ledger_canister_id.into(),
126            account_owner: account_owner.into(),
127            subaccount_hex: None,
128        }
129    }
130
131    #[must_use]
132    pub fn with_subaccount_hex(mut self, subaccount_hex: impl Into<String>) -> Self {
133        self.subaccount_hex = Some(subaccount_hex.into());
134        self
135    }
136}
137
138///
139/// IcrcAllowanceRequest
140///
141/// Request accepted by the generic ICRC allowance report builder.
142///
143#[derive(Clone, Debug, Eq, PartialEq)]
144pub struct IcrcAllowanceRequest {
145    pub source_endpoint: String,
146    pub now_unix_secs: u64,
147    pub ledger_canister_id: String,
148    pub account_owner: String,
149    pub account_subaccount_hex: Option<String>,
150    pub spender_owner: String,
151    pub spender_subaccount_hex: Option<String>,
152}
153
154impl IcrcAllowanceRequest {
155    #[must_use]
156    pub fn new(
157        source_endpoint: impl Into<String>,
158        now_unix_secs: u64,
159        ledger_canister_id: impl Into<String>,
160        account_owner: impl Into<String>,
161        spender_owner: impl Into<String>,
162    ) -> Self {
163        Self {
164            source_endpoint: source_endpoint.into(),
165            now_unix_secs,
166            ledger_canister_id: ledger_canister_id.into(),
167            account_owner: account_owner.into(),
168            account_subaccount_hex: None,
169            spender_owner: spender_owner.into(),
170            spender_subaccount_hex: None,
171        }
172    }
173
174    #[must_use]
175    pub fn with_account_subaccount_hex(
176        mut self,
177        account_subaccount_hex: impl Into<String>,
178    ) -> Self {
179        self.account_subaccount_hex = Some(account_subaccount_hex.into());
180        self
181    }
182
183    #[must_use]
184    pub fn with_spender_subaccount_hex(
185        mut self,
186        spender_subaccount_hex: impl Into<String>,
187    ) -> Self {
188        self.spender_subaccount_hex = Some(spender_subaccount_hex.into());
189        self
190    }
191}
192
193///
194/// IcrcIndexRequest
195///
196/// Request accepted by the generic ICRC index discovery report builder.
197///
198#[derive(Clone, Debug, Eq, PartialEq)]
199pub struct IcrcIndexRequest {
200    pub source_endpoint: String,
201    pub now_unix_secs: u64,
202    pub ledger_canister_id: String,
203}
204
205impl IcrcIndexRequest {
206    #[must_use]
207    pub fn new(
208        source_endpoint: impl Into<String>,
209        now_unix_secs: u64,
210        ledger_canister_id: impl Into<String>,
211    ) -> Self {
212        Self {
213            source_endpoint: source_endpoint.into(),
214            now_unix_secs,
215            ledger_canister_id: ledger_canister_id.into(),
216        }
217    }
218}
219
220///
221/// IcrcTransactionsRequest
222///
223/// Request accepted by the generic ICRC transaction history report builder.
224///
225#[derive(Clone, Debug, Eq, PartialEq)]
226pub struct IcrcTransactionsRequest {
227    pub source_endpoint: String,
228    pub now_unix_secs: u64,
229    pub ledger_canister_id: String,
230    pub start: u64,
231    pub limit: u32,
232    pub follow_archives: bool,
233}
234
235impl IcrcTransactionsRequest {
236    #[must_use]
237    pub fn new(
238        source_endpoint: impl Into<String>,
239        now_unix_secs: u64,
240        ledger_canister_id: impl Into<String>,
241        start: u64,
242        limit: u32,
243    ) -> Self {
244        Self {
245            source_endpoint: source_endpoint.into(),
246            now_unix_secs,
247            ledger_canister_id: ledger_canister_id.into(),
248            start,
249            limit,
250            follow_archives: false,
251        }
252    }
253
254    #[must_use]
255    pub const fn with_follow_archives(mut self, follow_archives: bool) -> Self {
256        self.follow_archives = follow_archives;
257        self
258    }
259}
260
261///
262/// IcrcBlockTypesRequest
263///
264/// Request accepted by the generic ICRC supported block types report builder.
265///
266#[derive(Clone, Debug, Eq, PartialEq)]
267pub struct IcrcBlockTypesRequest {
268    pub source_endpoint: String,
269    pub now_unix_secs: u64,
270    pub ledger_canister_id: String,
271}
272
273impl IcrcBlockTypesRequest {
274    #[must_use]
275    pub fn new(
276        source_endpoint: impl Into<String>,
277        now_unix_secs: u64,
278        ledger_canister_id: impl Into<String>,
279    ) -> Self {
280        Self {
281            source_endpoint: source_endpoint.into(),
282            now_unix_secs,
283            ledger_canister_id: ledger_canister_id.into(),
284        }
285    }
286}
287
288///
289/// IcrcArchivesRequest
290///
291/// Request accepted by the generic ICRC archives report builder.
292///
293#[derive(Clone, Debug, Eq, PartialEq)]
294pub struct IcrcArchivesRequest {
295    pub source_endpoint: String,
296    pub now_unix_secs: u64,
297    pub ledger_canister_id: String,
298    pub from_canister_id: Option<String>,
299}
300
301impl IcrcArchivesRequest {
302    #[must_use]
303    pub fn new(
304        source_endpoint: impl Into<String>,
305        now_unix_secs: u64,
306        ledger_canister_id: impl Into<String>,
307    ) -> Self {
308        Self {
309            source_endpoint: source_endpoint.into(),
310            now_unix_secs,
311            ledger_canister_id: ledger_canister_id.into(),
312            from_canister_id: None,
313        }
314    }
315
316    #[must_use]
317    pub fn with_from_canister_id(mut self, from_canister_id: impl Into<String>) -> Self {
318        self.from_canister_id = Some(from_canister_id.into());
319        self
320    }
321}
322
323///
324/// IcrcTipCertificateRequest
325///
326/// Request accepted by the generic ICRC-3 tip certificate report builder.
327///
328#[derive(Clone, Debug, Eq, PartialEq)]
329pub struct IcrcTipCertificateRequest {
330    pub source_endpoint: String,
331    pub now_unix_secs: u64,
332    pub ledger_canister_id: String,
333}
334
335impl IcrcTipCertificateRequest {
336    #[must_use]
337    pub fn new(
338        source_endpoint: impl Into<String>,
339        now_unix_secs: u64,
340        ledger_canister_id: impl Into<String>,
341    ) -> Self {
342        Self {
343            source_endpoint: source_endpoint.into(),
344            now_unix_secs,
345            ledger_canister_id: ledger_canister_id.into(),
346        }
347    }
348}
349
350///
351/// IcrcCapabilitiesRequest
352///
353/// Request accepted by the generic ICRC ledger capabilities report builder.
354///
355#[derive(Clone, Debug, Eq, PartialEq)]
356pub struct IcrcCapabilitiesRequest {
357    pub source_endpoint: String,
358    pub now_unix_secs: u64,
359    pub ledger_canister_id: String,
360}
361
362impl IcrcCapabilitiesRequest {
363    #[must_use]
364    pub fn new(
365        source_endpoint: impl Into<String>,
366        now_unix_secs: u64,
367        ledger_canister_id: impl Into<String>,
368    ) -> Self {
369        Self {
370            source_endpoint: source_endpoint.into(),
371            now_unix_secs,
372            ledger_canister_id: ledger_canister_id.into(),
373        }
374    }
375}
376
377///
378/// IcrcTokenReport
379///
380/// Serializable report for generic ICRC ledger token metadata.
381///
382#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
383pub struct IcrcTokenReport {
384    pub schema_version: u32,
385    pub ledger_canister_id: String,
386    pub fetched_at: String,
387    pub source_endpoint: String,
388    pub fetched_by: String,
389    pub token_name: String,
390    pub token_symbol: String,
391    pub decimals: u8,
392    pub transfer_fee: String,
393    pub total_supply: String,
394    pub minting_account_owner: Option<String>,
395    pub minting_account_subaccount_hex: Option<String>,
396    pub supported_standards: Vec<IcrcTokenStandardRow>,
397    pub metadata: Vec<IcrcTokenMetadataRow>,
398}
399
400///
401/// IcrcBalanceReport
402///
403/// Serializable report for one generic ICRC account balance lookup.
404///
405#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
406pub struct IcrcBalanceReport {
407    pub schema_version: u32,
408    pub ledger_canister_id: String,
409    pub account_owner: String,
410    pub subaccount_hex: Option<String>,
411    pub fetched_at: String,
412    pub source_endpoint: String,
413    pub fetched_by: String,
414    pub token_symbol: String,
415    pub decimals: u8,
416    pub balance: String,
417}
418
419///
420/// IcrcAllowanceReport
421///
422/// Serializable report for one generic ICRC allowance lookup.
423///
424#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
425pub struct IcrcAllowanceReport {
426    pub schema_version: u32,
427    pub ledger_canister_id: String,
428    pub account_owner: String,
429    pub account_subaccount_hex: Option<String>,
430    pub spender_owner: String,
431    pub spender_subaccount_hex: Option<String>,
432    pub fetched_at: String,
433    pub source_endpoint: String,
434    pub fetched_by: String,
435    pub token_symbol: String,
436    pub decimals: u8,
437    pub allowance: String,
438    pub expires_at_unix_nanos: Option<String>,
439}
440
441///
442/// IcrcIndexReport
443///
444/// Serializable report for one generic ICRC-106 index discovery lookup.
445///
446#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
447pub struct IcrcIndexReport {
448    pub schema_version: u32,
449    pub ledger_canister_id: String,
450    pub fetched_at: String,
451    pub source_endpoint: String,
452    pub fetched_by: String,
453    pub index_canister_id: Option<String>,
454    pub index_error: Option<String>,
455}
456
457///
458/// IcrcTransactionsReport
459///
460/// Serializable report for a generic ICRC ledger transaction/block history page.
461///
462#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
463pub struct IcrcTransactionsReport {
464    pub schema_version: u32,
465    pub ledger_canister_id: String,
466    pub fetched_at: String,
467    pub source_endpoint: String,
468    pub fetched_by: String,
469    pub requested_start: String,
470    pub requested_limit: u32,
471    pub follow_archives: bool,
472    pub log_length: Option<String>,
473    pub blocks: Vec<IcrcTransactionBlockRow>,
474    pub archived_blocks: Vec<IcrcArchivedBlocksRow>,
475    pub followed_archive_blocks: Vec<IcrcFollowedArchiveBlockRow>,
476    pub archive_follow_errors: Vec<IcrcArchiveFollowErrorRow>,
477}
478
479///
480/// IcrcBlockTypesReport
481///
482/// Serializable report for generic ICRC-3 supported block type discovery.
483///
484#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
485pub struct IcrcBlockTypesReport {
486    pub schema_version: u32,
487    pub ledger_canister_id: String,
488    pub fetched_at: String,
489    pub source_endpoint: String,
490    pub fetched_by: String,
491    pub block_types: Vec<IcrcBlockTypeRow>,
492}
493
494///
495/// IcrcArchivesReport
496///
497/// Serializable report for generic ICRC-3 archive range discovery.
498///
499#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
500pub struct IcrcArchivesReport {
501    pub schema_version: u32,
502    pub ledger_canister_id: String,
503    pub from_canister_id: Option<String>,
504    pub fetched_at: String,
505    pub source_endpoint: String,
506    pub fetched_by: String,
507    pub archives: Vec<IcrcArchiveRow>,
508}
509
510///
511/// IcrcTipCertificateReport
512///
513/// Serializable report for a generic ICRC-3 ledger tip certificate.
514///
515#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
516pub struct IcrcTipCertificateReport {
517    pub schema_version: u32,
518    pub ledger_canister_id: String,
519    pub fetched_at: String,
520    pub source_endpoint: String,
521    pub fetched_by: String,
522    pub certificate_present: bool,
523    pub certificate_hex: Option<String>,
524    pub certificate_bytes: Option<usize>,
525    pub hash_tree_hex: Option<String>,
526    pub hash_tree_bytes: Option<usize>,
527}
528
529///
530/// IcrcCapabilitiesReport
531///
532/// Serializable report for generic ICRC ledger endpoint capabilities.
533///
534#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
535pub struct IcrcCapabilitiesReport {
536    pub schema_version: u32,
537    pub ledger_canister_id: String,
538    pub fetched_at: String,
539    pub source_endpoint: String,
540    pub fetched_by: String,
541    pub supported_standards: Vec<IcrcTokenStandardRow>,
542    pub capabilities: Vec<IcrcCapabilityRow>,
543}
544
545///
546/// IcrcCapabilityRow
547///
548/// Serializable row for one probed generic ICRC ledger capability.
549///
550#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
551pub struct IcrcCapabilityRow {
552    pub capability: String,
553    pub method: String,
554    pub status: String,
555    pub details: Option<String>,
556    pub error: Option<String>,
557}
558
559///
560/// IcrcTokenStandardRow
561///
562/// Serializable row for one ICRC standard supported by a ledger.
563///
564#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
565pub struct IcrcTokenStandardRow {
566    pub name: String,
567    pub url: String,
568}
569
570///
571/// IcrcTokenMetadataRow
572///
573/// Serializable row for one raw ICRC ledger metadata entry.
574///
575#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
576pub struct IcrcTokenMetadataRow {
577    pub key: String,
578    pub value_type: String,
579    pub value: JsonValue,
580}
581
582///
583/// IcrcTransactionBlockRow
584///
585/// Serializable row for one ICRC-3 block returned by a ledger canister.
586///
587#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
588pub struct IcrcTransactionBlockRow {
589    pub index: String,
590    pub block_type: Option<String>,
591    pub transaction_kind: Option<String>,
592    pub timestamp_unix_nanos: Option<String>,
593    pub amount_base_units: Option<String>,
594    pub raw_block: JsonValue,
595}
596
597///
598/// IcrcArchivedBlocksRow
599///
600/// Serializable row for one ICRC-3 archive callback returned by a ledger canister.
601///
602#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
603pub struct IcrcArchivedBlocksRow {
604    pub callback_canister_id: String,
605    pub callback_method: String,
606    pub ranges: Vec<IcrcArchivedRangeRow>,
607}
608
609///
610/// IcrcArchivedRangeRow
611///
612/// Serializable row for one ICRC-3 archived block range.
613///
614#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
615pub struct IcrcArchivedRangeRow {
616    pub start: String,
617    pub length: String,
618}
619
620///
621/// IcrcFollowedArchiveBlockRow
622///
623/// Serializable row for one ICRC-3 block fetched from an archive callback.
624///
625#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
626pub struct IcrcFollowedArchiveBlockRow {
627    pub archive_canister_id: String,
628    pub callback_method: String,
629    pub index: String,
630    pub block_type: Option<String>,
631    pub transaction_kind: Option<String>,
632    pub timestamp_unix_nanos: Option<String>,
633    pub amount_base_units: Option<String>,
634    pub raw_block: JsonValue,
635}
636
637///
638/// IcrcArchiveFollowErrorRow
639///
640/// Serializable row for one archive callback that could not be followed.
641///
642#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
643pub struct IcrcArchiveFollowErrorRow {
644    pub callback_canister_id: String,
645    pub callback_method: String,
646    pub ranges: Vec<IcrcArchivedRangeRow>,
647    pub error: String,
648}
649
650///
651/// IcrcBlockTypeRow
652///
653/// Serializable row for one supported ICRC-3 block type.
654///
655#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
656pub struct IcrcBlockTypeRow {
657    pub block_type: String,
658    pub url: String,
659}
660
661///
662/// IcrcArchiveRow
663///
664/// Serializable row for one ICRC-3 archive range.
665///
666#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
667pub struct IcrcArchiveRow {
668    pub canister_id: String,
669    pub start: String,
670    pub end: String,
671}
672
673///
674/// IcrcTokenData
675///
676/// Source-layer token metadata returned by an ICRC ledger.
677///
678#[cfg(feature = "host")]
679#[derive(Clone, Debug, Eq, PartialEq)]
680pub(in crate::icrc) struct IcrcTokenData {
681    pub(in crate::icrc) token_name: String,
682    pub(in crate::icrc) token_symbol: String,
683    pub(in crate::icrc) decimals: u8,
684    pub(in crate::icrc) transfer_fee: String,
685    pub(in crate::icrc) total_supply: String,
686    pub(in crate::icrc) minting_account_owner: Option<String>,
687    pub(in crate::icrc) minting_account_subaccount_hex: Option<String>,
688    pub(in crate::icrc) supported_standards: Vec<IcrcTokenStandardRow>,
689    pub(in crate::icrc) metadata: Vec<IcrcTokenMetadataRow>,
690}
691
692///
693/// IcrcBalanceData
694///
695/// Source-layer balance result plus enough token metadata for display.
696///
697#[cfg(feature = "host")]
698#[derive(Clone, Debug, Eq, PartialEq)]
699pub(in crate::icrc) struct IcrcBalanceData {
700    pub(in crate::icrc) token_symbol: String,
701    pub(in crate::icrc) decimals: u8,
702    pub(in crate::icrc) balance: String,
703}
704
705///
706/// IcrcAllowanceData
707///
708/// Source-layer allowance result plus enough token metadata for display.
709///
710#[cfg(feature = "host")]
711#[derive(Clone, Debug, Eq, PartialEq)]
712pub(in crate::icrc) struct IcrcAllowanceData {
713    pub(in crate::icrc) token_symbol: String,
714    pub(in crate::icrc) decimals: u8,
715    pub(in crate::icrc) allowance: String,
716    pub(in crate::icrc) expires_at_unix_nanos: Option<String>,
717}
718
719///
720/// IcrcIndexData
721///
722/// Source-layer ICRC-106 index discovery result.
723///
724#[cfg(feature = "host")]
725#[derive(Clone, Debug, Eq, PartialEq)]
726pub(in crate::icrc) struct IcrcIndexData {
727    pub(in crate::icrc) index_canister_id: Option<String>,
728    pub(in crate::icrc) index_error: Option<String>,
729}
730
731///
732/// IcrcTransactionsData
733///
734/// Source-layer ICRC-3 block history result from a ledger canister.
735///
736#[cfg(feature = "host")]
737#[derive(Clone, Debug, PartialEq)]
738pub(in crate::icrc) struct IcrcTransactionsData {
739    pub(in crate::icrc) log_length: Option<String>,
740    pub(in crate::icrc) blocks: Vec<IcrcTransactionBlockRow>,
741    pub(in crate::icrc) archived_blocks: Vec<IcrcArchivedBlocksRow>,
742    pub(in crate::icrc) followed_archive_blocks: Vec<IcrcFollowedArchiveBlockRow>,
743    pub(in crate::icrc) archive_follow_errors: Vec<IcrcArchiveFollowErrorRow>,
744}
745
746///
747/// IcrcBlockTypesData
748///
749/// Source-layer ICRC-3 supported block types result.
750///
751#[cfg(feature = "host")]
752#[derive(Clone, Debug, Eq, PartialEq)]
753pub(in crate::icrc) struct IcrcBlockTypesData {
754    pub(in crate::icrc) block_types: Vec<IcrcBlockTypeRow>,
755}
756
757///
758/// IcrcArchivesData
759///
760/// Source-layer ICRC-3 archive range discovery result.
761///
762#[cfg(feature = "host")]
763#[derive(Clone, Debug, Eq, PartialEq)]
764pub(in crate::icrc) struct IcrcArchivesData {
765    pub(in crate::icrc) archives: Vec<IcrcArchiveRow>,
766}
767
768///
769/// IcrcTipCertificateData
770///
771/// Source-layer ICRC-3 tip certificate result.
772///
773#[cfg(feature = "host")]
774#[derive(Clone, Debug, Eq, PartialEq)]
775pub(in crate::icrc) struct IcrcTipCertificateData {
776    pub(in crate::icrc) certificate_hex: Option<String>,
777    pub(in crate::icrc) certificate_bytes: Option<usize>,
778    pub(in crate::icrc) hash_tree_hex: Option<String>,
779    pub(in crate::icrc) hash_tree_bytes: Option<usize>,
780}
781
782///
783/// IcrcCapabilitiesData
784///
785/// Source-layer generic ICRC ledger capability probe result.
786///
787#[cfg(feature = "host")]
788#[derive(Clone, Debug, Eq, PartialEq)]
789pub(in crate::icrc) struct IcrcCapabilitiesData {
790    pub(in crate::icrc) supported_standards: Vec<IcrcTokenStandardRow>,
791    pub(in crate::icrc) capabilities: Vec<IcrcCapabilityRow>,
792}
793
794#[cfg(feature = "host")]
795pub(in crate::icrc) fn normalize_subaccount_hex(value: &str) -> Result<String, IcrcError> {
796    let bytes = subaccount_bytes_from_hex(value)?;
797    Ok(hex_bytes(&bytes))
798}
799
800#[cfg(feature = "host")]
801pub(in crate::icrc) fn subaccount_bytes_from_hex(value: &str) -> Result<Vec<u8>, IcrcError> {
802    let value = value.trim();
803    if !value.len().is_multiple_of(2) {
804        return Err(IcrcError::InvalidSubaccountHex {
805            reason: "hex string must contain an even number of characters".to_string(),
806        });
807    }
808    let bytes = (0..value.len())
809        .step_by(2)
810        .map(|index| {
811            u8::from_str_radix(&value[index..index + 2], 16).map_err(|err| {
812                IcrcError::InvalidSubaccountHex {
813                    reason: err.to_string(),
814                }
815            })
816        })
817        .collect::<Result<Vec<_>, _>>()?;
818    if bytes.len() != 32 {
819        return Err(IcrcError::InvalidSubaccountLength { bytes: bytes.len() });
820    }
821    Ok(bytes)
822}