Skip to main content

icrc_ledger_agent/
lib.rs

1use candid::{Decode, Encode, Nat, Principal};
2use ic_agent::{
3    Agent,
4    hash_tree::{Label, LookupResult},
5};
6use ic_cbor::CertificateToCbor;
7use ic_certification::{
8    Certificate, HashTree,
9    hash_tree::{HashTreeNode, SubtreeLookupResult},
10};
11use icrc_ledger_types::icrc::generic_value::Hash;
12use icrc_ledger_types::icrc1::account::Account;
13use icrc_ledger_types::icrc1::transfer::{BlockIndex, TransferArg, TransferError};
14use icrc_ledger_types::icrc2::allowance::{Allowance, AllowanceArgs};
15use icrc_ledger_types::icrc2::approve::{ApproveArgs, ApproveError};
16use icrc_ledger_types::icrc2::transfer_from::{TransferFromArgs, TransferFromError};
17use icrc_ledger_types::icrc3::archive::{ArchivedRange, QueryBlockArchiveFn};
18use icrc_ledger_types::icrc3::blocks::ICRC3DataCertificate;
19use icrc_ledger_types::icrc3::blocks::{GetBlocksRequest, GetBlocksResponse};
20use icrc_ledger_types::{
21    icrc::generic_metadata_value::MetadataValue as Value, icrc::metadata_key::MetadataKey,
22    icrc3::blocks::BlockRange,
23};
24
25#[derive(Debug)]
26pub enum Icrc1AgentError {
27    AgentError(ic_agent::AgentError),
28    CandidError(candid::Error),
29    VerificationFailed(String),
30}
31
32impl From<ic_agent::AgentError> for Icrc1AgentError {
33    fn from(e: ic_agent::AgentError) -> Self {
34        Self::AgentError(e)
35    }
36}
37
38impl From<candid::Error> for Icrc1AgentError {
39    fn from(e: candid::Error) -> Self {
40        Self::CandidError(e)
41    }
42}
43
44impl std::fmt::Display for Icrc1AgentError {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        write!(f, "{self:?}")
47    }
48}
49
50impl std::error::Error for Icrc1AgentError {}
51
52pub enum CallMode {
53    Query,
54    Update,
55}
56
57/// An Agent to make calls to a [ICRC-1 Ledger](https://github.com/dfinity/ICRC-1).
58///
59/// Each query method in this agent takes in input
60/// the mode to allow to either use a query call or
61/// update calls.
62#[derive(Debug, Clone)]
63pub struct Icrc1Agent {
64    pub agent: Agent,
65    pub ledger_canister_id: Principal,
66}
67
68impl Icrc1Agent {
69    async fn query<S: Into<String>>(
70        &self,
71        method_name: S,
72        arg: &[u8],
73    ) -> Result<Vec<u8>, Icrc1AgentError> {
74        self.agent
75            .query(&self.ledger_canister_id, method_name)
76            .with_arg(arg)
77            .call()
78            .await
79            .map_err(Icrc1AgentError::AgentError)
80    }
81
82    async fn update<S: Into<String>>(
83        &self,
84        method_name: S,
85        arg: &[u8],
86    ) -> Result<Vec<u8>, Icrc1AgentError> {
87        self.agent
88            .update(&self.ledger_canister_id, method_name)
89            .with_arg(arg)
90            .call_and_wait()
91            .await
92            .map_err(Icrc1AgentError::AgentError)
93    }
94
95    /// Returns the balance of the account given as argument.
96    pub async fn balance_of(
97        &self,
98        account: Account,
99        mode: CallMode,
100    ) -> Result<Nat, Icrc1AgentError> {
101        Ok(match mode {
102            CallMode::Query => Decode!(
103                &self.query("icrc1_balance_of", &Encode!(&account)?).await?,
104                Nat
105            )?,
106            CallMode::Update => Decode!(
107                &self.update("icrc1_balance_of", &Encode!(&account)?).await?,
108                Nat
109            )?,
110        })
111    }
112
113    /// Returns the number of decimals the token uses (e.g., 8 means to divide the token amount by 100000000 to get its user representation).
114    pub async fn decimals(&self, mode: CallMode) -> Result<u8, Icrc1AgentError> {
115        Ok(match mode {
116            CallMode::Query => Decode!(&self.query("icrc1_decimals", &Encode!()?).await?, u8)?,
117            CallMode::Update => Decode!(&self.update("icrc1_decimals", &Encode!()?).await?, u8)?,
118        })
119    }
120
121    /// Returns the name of the token (e.g., MyToken).
122    pub async fn name(&self, mode: CallMode) -> Result<String, Icrc1AgentError> {
123        Ok(match mode {
124            CallMode::Query => Decode!(&self.query("icrc1_name", &Encode!()?).await?, String)?,
125            CallMode::Update => Decode!(&self.update("icrc1_name", &Encode!()?).await?, String)?,
126        })
127    }
128
129    /// Returns the list of metadata entries for this ledger
130    pub async fn metadata(
131        &self,
132        mode: CallMode,
133    ) -> Result<Vec<(MetadataKey, Value)>, Icrc1AgentError> {
134        Ok(match mode {
135            CallMode::Query => Decode!(
136                &self.query("icrc1_metadata", &Encode!()?).await?,
137                Vec<(MetadataKey, Value)>
138            )?,
139            CallMode::Update => Decode!(
140                &self.update("icrc1_metadata", &Encode!()?).await?,
141                Vec<(MetadataKey, Value)>
142            )?,
143        })
144    }
145
146    /// Returns the symbol of the token (e.g., ICP).
147    pub async fn symbol(&self, mode: CallMode) -> Result<String, Icrc1AgentError> {
148        Ok(match mode {
149            CallMode::Query => Decode!(&self.query("icrc1_symbol", &Encode!()?).await?, String)?,
150            CallMode::Update => Decode!(&self.update("icrc1_symbol", &Encode!()?).await?, String)?,
151        })
152    }
153
154    /// Returns the balance of the account given as argument.
155    pub async fn total_supply(&self, mode: CallMode) -> Result<Nat, Icrc1AgentError> {
156        Ok(match mode {
157            CallMode::Query => Decode!(&self.query("icrc1_total_supply", &Encode!()?).await?, Nat)?,
158            CallMode::Update => {
159                Decode!(&self.update("icrc1_total_supply", &Encode!()?).await?, Nat)?
160            }
161        })
162    }
163
164    // Returns the transfer fee.
165    pub async fn fee(&self, mode: CallMode) -> Result<Nat, Icrc1AgentError> {
166        Ok(match mode {
167            CallMode::Query => Decode!(&self.query("icrc1_fee", &Encode!()?).await?, Nat)?,
168            CallMode::Update => Decode!(&self.update("icrc1_fee", &Encode!()?).await?, Nat)?,
169        })
170    }
171
172    // Returns the minting account if this ledger supports minting and burning tokens.
173    pub async fn minting_account(
174        &self,
175        mode: CallMode,
176    ) -> Result<Option<Account>, Icrc1AgentError> {
177        Ok(match mode {
178            CallMode::Query => Decode!(
179                &self.query("icrc1_minting_account", &Encode!()?).await?,
180                Option<Account>
181            )?,
182            CallMode::Update => Decode!(
183                &self.update("icrc1_minting_account", &Encode!()?).await?,
184                Option<Account>
185            )?,
186        })
187    }
188
189    /// Transfers amount of tokens from the account (caller, from_subaccount) to the account (to_principal, to_subaccount).
190    pub async fn transfer(
191        &self,
192        args: TransferArg,
193    ) -> Result<Result<Nat, TransferError>, Icrc1AgentError> {
194        Ok(
195            Decode!(&self.update("icrc1_transfer", &Encode!(&args)?).await?, Result<Nat, TransferError>)?,
196        )
197    }
198
199    pub async fn approve(
200        &self,
201        args: ApproveArgs,
202    ) -> Result<Result<Nat, ApproveError>, Icrc1AgentError> {
203        Ok(
204            Decode!(&self.update("icrc2_approve", &Encode!(&args)?).await?, Result<Nat, ApproveError>)?,
205        )
206    }
207
208    /// Returns the allowance of the `spender` from the `account`.
209    pub async fn allowance(
210        &self,
211        account: Account,
212        spender: Account,
213        mode: CallMode,
214    ) -> Result<Allowance, Icrc1AgentError> {
215        let args = AllowanceArgs { account, spender };
216        Ok(match mode {
217            CallMode::Query => Decode!(
218                &self.query("icrc2_allowance", &Encode!(&args)?).await?,
219                Allowance
220            )?,
221            CallMode::Update => Decode!(
222                &self.update("icrc2_allowance", &Encode!(&args)?).await?,
223                Allowance
224            )?,
225        })
226    }
227
228    pub async fn transfer_from(
229        &self,
230        args: TransferFromArgs,
231    ) -> Result<Result<Nat, TransferFromError>, Icrc1AgentError> {
232        Ok(
233            Decode!(&self.update("icrc2_transfer_from", &Encode!(&args)?).await?, Result<Nat, TransferFromError>)?,
234        )
235    }
236
237    pub async fn get_blocks(
238        &self,
239        args: GetBlocksRequest,
240    ) -> Result<GetBlocksResponse, Icrc1AgentError> {
241        Ok(Decode!(
242            &self.query("get_blocks", &Encode!(&args)?).await?,
243            GetBlocksResponse
244        )?)
245    }
246
247    pub async fn get_blocks_from_archive(
248        &self,
249        archived_blocks: ArchivedRange<QueryBlockArchiveFn>,
250    ) -> Result<BlockRange, Icrc1AgentError> {
251        let args = GetBlocksRequest {
252            start: archived_blocks.start,
253            length: archived_blocks.length,
254        };
255        Ok(Decode!(
256            &self
257                .agent
258                .query(
259                    &archived_blocks.callback.canister_id,
260                    &archived_blocks.callback.method
261                )
262                .with_arg(Encode!(&args)?)
263                .call()
264                .await
265                .map_err(Icrc1AgentError::AgentError)?,
266            BlockRange
267        )?)
268    }
269
270    pub async fn icrc3_get_tip_certificate(&self) -> Result<ICRC3DataCertificate, Icrc1AgentError> {
271        Decode!(
272            &self.query("icrc3_get_tip_certificate", &Encode!()?).await?,
273            Option<ICRC3DataCertificate>
274        )?
275        .ok_or(Icrc1AgentError::VerificationFailed(
276            "ICRC3DataCertificate not found".to_string(),
277        ))
278    }
279
280    /// The function performs the following checks:
281    /// 1. Check whether the certificate is valid and has authority over ledger_canister_id.
282    /// 2. Check whether the certified data at path ["canister", ledger_canister_id, "certified_data"] is equal to root_hash.
283    pub async fn verify_root_hash(
284        &self,
285        certificate: &Certificate,
286        root_hash: &Hash,
287    ) -> Result<(), Icrc1AgentError> {
288        self.agent
289            .verify(certificate, self.ledger_canister_id)
290            .map_err(Icrc1AgentError::AgentError)?;
291
292        let certified_data_path: [Label<Vec<u8>>; 3] = [
293            "canister".into(),
294            self.ledger_canister_id.as_slice().into(),
295            "certified_data".into(),
296        ];
297
298        let cert_hash = match certificate.tree.lookup_path(&certified_data_path) {
299            LookupResult::Found(v) => v,
300            _ => {
301                return Err(Icrc1AgentError::VerificationFailed(format!(
302                    "could not find certified_data for canister: {}",
303                    self.ledger_canister_id
304                )));
305            }
306        };
307
308        if cert_hash != root_hash {
309            return Err(Icrc1AgentError::VerificationFailed(
310                "certified_data does not match the root_hash".to_string(),
311            ));
312        }
313        Ok(())
314    }
315
316    /// Returns the hash of the last block in the chain and this block's index.
317    /// Returns an error if the hash and/or the index do not pass validation against the IC certificate.
318    /// Returns None if the blockchain has no blocks and this can be verified by the certificate.
319    pub async fn get_certified_chain_tip(
320        &self,
321    ) -> Result<Option<(Hash, BlockIndex)>, Icrc1AgentError> {
322        let ICRC3DataCertificate {
323            certificate,
324            hash_tree,
325        } = self.icrc3_get_tip_certificate().await?;
326        let certificate = match Certificate::from_cbor(certificate.as_slice()) {
327            Ok(certificate) => certificate,
328            Err(e) => {
329                return Err(Icrc1AgentError::VerificationFailed(format!(
330                    "Unable to deserialize CBOR encoded Certificate: {e}"
331                )));
332            }
333        };
334        let hash_tree: HashTree = match ciborium::de::from_reader(hash_tree.as_slice()) {
335            Ok(hash_tree) => hash_tree,
336            Err(e) => {
337                return Err(Icrc1AgentError::VerificationFailed(format!(
338                    "Unable to deserialize CBOR encoded hash_tree: {e}"
339                )));
340            }
341        };
342        self.verify_root_hash(&certificate, &hash_tree.digest())
343            .await?;
344        let last_block_index_encoded = match lookup_leaf(&hash_tree, "last_block_index")? {
345            Some(last_block_index) => last_block_index,
346            None => {
347                return Ok(None);
348            }
349        };
350
351        fn convert_block_hash(block_hash: Vec<u8>) -> Result<Hash, Icrc1AgentError> {
352            block_hash
353                .clone()
354                .try_into()
355                .or(Err(Icrc1AgentError::VerificationFailed(format!(
356                "DataCertificate last_block_hash bytes: {}, cannot be decoded as last_block_hash",
357                hex::encode(block_hash)
358            ))))
359        }
360
361        // We use two different decoding strategies depending on the presence of the tip_hash in the hash_tree.
362        match (
363            lookup_leaf(&hash_tree, "tip_hash")?,
364            lookup_leaf(&hash_tree, "last_block_hash")?,
365        ) {
366            (Some(tip_hash), _) => {
367                let last_block_index_bytes: [u8; 8] = match last_block_index_encoded
368                    .clone()
369                    .try_into()
370                {
371                    Ok(last_block_index_bytes) => last_block_index_bytes,
372                    Err(_) => {
373                        return Err(Icrc1AgentError::VerificationFailed(format!(
374                            "DataCertificate hash_tree bytes: {}, cannot be decoded as last_block_index",
375                            hex::encode(last_block_index_encoded)
376                        )));
377                    }
378                };
379                let last_block_index = u64::from_be_bytes(last_block_index_bytes);
380                Ok(Some((
381                    convert_block_hash(tip_hash)?,
382                    Nat::from(last_block_index),
383                )))
384            }
385            (_, Some(last_block_hash_vec)) => {
386                let mut decode_buf = std::io::Cursor::new(&last_block_index_encoded);
387                let last_block_index = leb128::read::unsigned(&mut decode_buf).map_err(|e| {
388                    Icrc1AgentError::VerificationFailed(format!(
389                        "Unable to decode last_block_index: {e}"
390                    ))
391                })?;
392                Ok(Some((
393                    convert_block_hash(last_block_hash_vec)?,
394                    Nat::from(last_block_index),
395                )))
396            }
397            _ => Ok(None),
398        }
399    }
400}
401
402fn lookup_leaf(hash_tree: &HashTree, leaf_name: &str) -> Result<Option<Vec<u8>>, Icrc1AgentError> {
403    match hash_tree.lookup_subtree([leaf_name.as_bytes()]) {
404        SubtreeLookupResult::Found(tree) => match tree.as_ref() {
405            HashTreeNode::Leaf(result) => Ok(Some(result.clone())),
406            _ => Err(Icrc1AgentError::VerificationFailed(format!(
407                "`{leaf_name}` value in the hash_tree should be a leaf"
408            ))),
409        },
410        SubtreeLookupResult::Absent => Ok(None),
411        _ => Err(Icrc1AgentError::VerificationFailed(format!(
412            "`{leaf_name}` not found in the response hash_tree"
413        ))),
414    }
415}