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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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}