1use anyhow::{bail, Context};
2use candid::Nat;
3use futures::StreamExt;
4use icrc1_test_env::icrc1::{
5 balance_of, metadata, minting_account, supported_standards, token_decimals, token_name,
6 token_symbol, transfer, transfer_fee,
7};
8use icrc1_test_env::icrc2::{allowance, approve, transfer_from};
9use icrc1_test_env::ApproveArgs;
10use icrc1_test_env::TransferFromArgs;
11use icrc1_test_env::{Account, LedgerEnv, Transfer, TransferError, Value};
12use icrc1_test_env::{AllowanceArgs, ApproveError, TransferFromError};
13use std::future::Future;
14use std::pin::Pin;
15use std::time::SystemTime;
16
17pub enum Outcome {
18 Passed,
19 Skipped { reason: String },
20}
21
22pub type TestResult = anyhow::Result<Outcome>;
23
24pub struct Test {
25 name: String,
26 action: Pin<Box<dyn Future<Output = TestResult>>>,
27}
28
29pub fn test(name: impl Into<String>, body: impl Future<Output = TestResult> + 'static) -> Test {
30 Test {
31 name: name.into(),
32 action: Box::pin(body),
33 }
34}
35
36fn lookup<'a, K, V, U>(meta: &'a [(K, V)], key: &U) -> Option<&'a V>
37where
38 K: PartialEq<U>,
39 U: ?Sized,
40{
41 meta.iter().find_map(|(k, v)| (k == key).then_some(v))
42}
43
44fn assert_equal<T: PartialEq + std::fmt::Debug>(lhs: T, rhs: T) -> anyhow::Result<()> {
45 if lhs != rhs {
46 bail!("{:?} ≠ {:?}", lhs, rhs)
47 }
48 Ok(())
49}
50
51fn assert_not_equal<T: PartialEq + std::fmt::Debug>(lhs: T, rhs: T) -> anyhow::Result<()> {
52 if lhs == rhs {
53 bail!("{:?} = {:?}", lhs, rhs)
54 }
55 Ok(())
56}
57
58async fn time_nanos(ledger_env: &impl LedgerEnv) -> u64 {
59 ledger_env
60 .time()
61 .await
62 .duration_since(SystemTime::UNIX_EPOCH)
63 .unwrap()
64 .as_nanos() as u64
65}
66
67async fn assert_balance(
68 ledger: &impl LedgerEnv,
69 account: impl Into<Account>,
70 expected: impl Into<Nat>,
71) -> anyhow::Result<()> {
72 let account = account.into();
73 let actual = balance_of(ledger, account.clone()).await?;
74 let expected = expected.into();
75
76 if expected != actual {
77 bail!(
78 "Expected the balance of account {:?} to be {}, got {}",
79 account,
80 expected,
81 actual
82 )
83 }
84 Ok(())
85}
86
87async fn assert_allowance(
88 ledger_env: &impl LedgerEnv,
89 from: impl Into<Account>,
90 spender: impl Into<Account>,
91 expected_allowance: impl Into<Nat>,
92 expires_at: Option<u64>,
93) -> anyhow::Result<()> {
94 let from: Account = from.into();
95 let spender: Account = spender.into();
96 let expected_allowance: Nat = expected_allowance.into();
97 let allowance = allowance(
98 ledger_env,
99 AllowanceArgs {
100 account: from.clone(),
101 spender: spender.clone(),
102 },
103 )
104 .await?;
105 if allowance.allowance != expected_allowance {
106 bail!(
107 "Expected the {:?} -> {:?} allowance to be {}, got {}",
108 from,
109 spender,
110 expected_allowance,
111 allowance.allowance
112 );
113 }
114 if allowance.expires_at != expires_at {
115 bail!("Approval {:?} -> {:?} , wrong expiration", from, spender,);
116 }
117 Ok(())
118}
119
120async fn setup_test_account(
121 ledger_env: &impl LedgerEnv,
122 amount: Nat,
123) -> anyhow::Result<impl LedgerEnv> {
124 let balance = balance_of(ledger_env, ledger_env.principal()).await?;
125 assert!(balance >= amount.clone() + transfer_fee(ledger_env).await?);
126 let receiver_env = ledger_env.fork();
127 let receiver = receiver_env.principal();
128 assert_balance(&receiver_env, receiver, 0u8).await?;
129 let _tx = transfer(ledger_env, Transfer::amount_to(amount.clone(), receiver)).await??;
130 assert_balance(
131 &receiver_env,
132 Account {
133 owner: receiver,
134 subaccount: None,
135 },
136 amount.clone(),
137 )
138 .await?;
139 Ok(receiver_env)
140}
141
142pub async fn icrc1_test_transfer(ledger_env: impl LedgerEnv) -> TestResult {
145 let fee = transfer_fee(&ledger_env).await?;
146 let transfer_amount = Nat::from(10_000u16);
147 let initial_balance: Nat = transfer_amount.clone() + fee.clone();
148 let p1_env = setup_test_account(&ledger_env, initial_balance).await?;
149 let p2_env = ledger_env.fork();
150
151 let balance_p1 = balance_of(&p1_env, p1_env.principal()).await?;
152 let balance_p2 = balance_of(&p2_env, p2_env.principal()).await?;
153
154 let _tx = transfer(
155 &p1_env,
156 Transfer::amount_to(transfer_amount.clone(), p2_env.principal()),
157 )
158 .await??;
159
160 assert_balance(
161 &ledger_env,
162 Account {
163 owner: p2_env.principal(),
164 subaccount: None,
165 },
166 balance_p2.clone() + transfer_amount.clone(),
167 )
168 .await?;
169
170 assert_balance(
171 &ledger_env,
172 Account {
173 owner: p2_env.principal(),
174 subaccount: Some([0; 32]),
175 },
176 balance_p2 + transfer_amount.clone(),
177 )
178 .await?;
179
180 assert_balance(
181 &ledger_env,
182 p1_env.principal(),
183 balance_p1 - transfer_amount.clone() - fee,
184 )
185 .await?;
186
187 Ok(Outcome::Passed)
188}
189
190pub async fn icrc1_test_burn(ledger_env: impl LedgerEnv) -> TestResult {
193 let minting_account = match minting_account(&ledger_env).await? {
194 Some(account) => account,
195 None => {
196 return Ok(Outcome::Skipped {
197 reason: "the ledger does not support burn transactions".to_string(),
198 });
199 }
200 };
201
202 assert_balance(&ledger_env, minting_account.clone(), 0u8)
203 .await
204 .context("minting account cannot hold any funds")?;
205
206 let burn_amount = Nat::from(10_000u16);
207 let p1_env = setup_test_account(&ledger_env, burn_amount.clone()).await?;
208
209 let _tx = transfer(
211 &p1_env,
212 Transfer::amount_to(burn_amount.clone(), minting_account.clone()),
213 )
214 .await?
215 .with_context(|| {
216 format!(
217 "failed to transfer {} tokens to {:?}",
218 burn_amount,
219 minting_account.clone()
220 )
221 })?;
222
223 assert_balance(&p1_env, p1_env.principal(), 0u8).await?;
224 assert_balance(&ledger_env, minting_account, 0u8).await?;
225
226 Ok(Outcome::Passed)
227}
228
229pub async fn icrc1_test_metadata(ledger: impl LedgerEnv) -> TestResult {
231 let mut metadata = metadata(&ledger).await?;
232 metadata.sort_by(|l, r| l.0.cmp(&r.0));
233
234 for ((k1, _), (k2, _)) in metadata.iter().zip(metadata.iter().skip(1)) {
235 if k1 == k2 {
236 bail!("Key {} is duplicated in the metadata", k1);
237 }
238 }
239
240 if let Some(name) = lookup(&metadata, "icrc1:name") {
241 assert_equal(&Value::Text(token_name(&ledger).await?), name)
242 .context("icrc1:name metadata entry does not match the icrc1_name endpoint")?;
243 }
244 if let Some(sym) = lookup(&metadata, "icrc1:symbol") {
245 assert_equal(&Value::Text(token_symbol(&ledger).await?), sym)
246 .context("icrc1:symol metadata entry does not match the icrc1_symbol endpoint")?;
247 }
248 if let Some(meta_decimals) = lookup(&metadata, "icrc1:decimals") {
249 let decimals = token_decimals(&ledger).await?;
250 assert_equal(&Value::Nat(Nat::from(decimals)), meta_decimals)
251 .context("icrc1:decimals metadata entry does not match the icrc1_decimals endpoint")?;
252 }
253 if let Some(fee) = lookup(&metadata, "icrc1:fee") {
254 assert_equal(&Value::Nat(transfer_fee(&ledger).await?), fee)
255 .context("icrc1:fee metadata entry does not match the icrc1_fee endpoint")?;
256 }
257 Ok(Outcome::Passed)
258}
259
260pub async fn icrc1_test_supported_standards(ledger: impl LedgerEnv) -> anyhow::Result<Outcome> {
262 let stds = supported_standards(&ledger).await?;
263 if !stds.iter().any(|std| std.name == "ICRC-1") {
264 bail!("The ledger does not claim support for ICRC-1: {:?}", stds);
265 }
266
267 Ok(Outcome::Passed)
268}
269
270pub async fn icrc2_test_supported_standards(ledger: impl LedgerEnv) -> anyhow::Result<Outcome> {
272 let stds = supported_standards(&ledger).await?;
273 if !(stds.iter().any(|std| std.name == "ICRC-2") && stds.iter().any(|std| std.name == "ICRC-1"))
275 {
276 bail!(
277 "The ledger does not claim support for ICRC-1 and ICRC-2: {:?}",
278 stds
279 );
280 }
281
282 Ok(Outcome::Passed)
283}
284
285pub async fn icrc2_test_approve(ledger_env: impl LedgerEnv) -> anyhow::Result<Outcome> {
287 let fee = transfer_fee(&ledger_env).await?;
288 let initial_balance: Nat = fee.clone() * 2u8;
289 let p1_env = setup_test_account(&ledger_env, initial_balance.clone()).await?;
290 let p2_env = ledger_env.fork();
291 let p2_subaccount = Account {
292 owner: p2_env.principal(),
293 subaccount: Some([1; 32]),
294 };
295 let approve_amount = fee.clone();
296
297 approve(
298 &p1_env,
299 ApproveArgs::approve_amount(approve_amount.clone(), p2_env.principal()),
300 )
301 .await??;
302
303 assert_allowance(
304 &p1_env,
305 p1_env.principal(),
306 p2_env.principal(),
307 approve_amount.clone(),
308 None,
309 )
310 .await?;
311
312 assert_allowance(
313 &p1_env,
314 p1_env.principal(),
315 p2_subaccount.clone(),
316 0u8,
317 None,
318 )
319 .await?;
320
321 assert_balance(&ledger_env, p1_env.principal(), fee.clone()).await?;
322 assert_balance(&ledger_env, p2_env.principal(), 0u8).await?;
323 assert_balance(&ledger_env, p2_subaccount.clone(), 0u8).await?;
324
325 approve(
327 &p1_env,
328 ApproveArgs::approve_amount(approve_amount.clone() * 2u8, p2_subaccount.clone()),
329 )
330 .await??;
331
332 assert_allowance(
333 &p1_env,
334 p1_env.principal(),
335 p2_env.principal(),
336 approve_amount.clone(),
337 None,
338 )
339 .await?;
340
341 assert_allowance(
342 &p1_env,
343 p1_env.principal(),
344 p2_subaccount.clone(),
345 approve_amount.clone() * 2u8,
346 None,
347 )
348 .await?;
349
350 assert_balance(&ledger_env, p1_env.principal(), 0u8).await?;
351 assert_balance(&ledger_env, p2_env.principal(), 0u8).await?;
352 assert_balance(&ledger_env, p2_subaccount, 0u8).await?;
353
354 match approve(
356 &p1_env,
357 ApproveArgs::approve_amount(fee.clone() * 3u8, p2_env.principal()),
358 )
359 .await?
360 {
361 Ok(_) => bail!("expected ApproveError::InsufficientFunds, got Ok result"),
362 Err(e) => match e {
363 ApproveError::InsufficientFunds { balance } => {
364 if balance != 0u8 {
365 bail!("wrong balance, expected 0, got: {}", balance);
366 }
367 }
368 _ => return Err(e).context("expected ApproveError::InsufficientFunds"),
369 },
370 }
371
372 Ok(Outcome::Passed)
373}
374
375pub async fn icrc2_test_approve_expiration(ledger_env: impl LedgerEnv) -> anyhow::Result<Outcome> {
377 let fee = transfer_fee(&ledger_env).await?;
378 let initial_balance: Nat = fee.clone() * 2u8;
379 let p1_env = setup_test_account(&ledger_env, initial_balance.clone()).await?;
380 let p2_env = ledger_env.fork();
381 let approve_amount = fee.clone();
382 let now = time_nanos(&ledger_env).await;
383
384 match approve(
386 &p1_env,
387 ApproveArgs::approve_amount(approve_amount.clone(), p2_env.principal()).expires_at(now - 1),
388 )
389 .await?
390 {
391 Ok(_) => bail!("expected ApproveError::Expired, got Ok result"),
392 Err(e) => match e {
393 ApproveError::Expired { .. } => {}
394 _ => return Err(e).context("expected ApproveError::Expired"),
395 },
396 }
397
398 assert_allowance(&p1_env, p1_env.principal(), p2_env.principal(), 0u8, None).await?;
399
400 assert_balance(&ledger_env, p1_env.principal(), initial_balance.clone()).await?;
401 assert_balance(&ledger_env, p2_env.principal(), 0u8).await?;
402
403 let expiration = u64::MAX;
405 approve(
406 &p1_env,
407 ApproveArgs::approve_amount(approve_amount.clone(), p2_env.principal())
408 .expires_at(expiration),
409 )
410 .await??;
411
412 assert_allowance(
413 &p1_env,
414 p1_env.principal(),
415 p2_env.principal(),
416 approve_amount.clone(),
417 Some(expiration),
418 )
419 .await?;
420
421 assert_balance(&ledger_env, p1_env.principal(), fee).await?;
422 assert_balance(&ledger_env, p2_env.principal(), 0u8).await?;
423
424 let new_expiration = expiration - 1;
426 approve(
427 &p1_env,
428 ApproveArgs::approve_amount(approve_amount.clone(), p2_env.principal())
429 .expires_at(new_expiration),
430 )
431 .await??;
432
433 assert_allowance(
434 &p1_env,
435 p1_env.principal(),
436 p2_env.principal(),
437 approve_amount,
438 Some(new_expiration),
439 )
440 .await?;
441
442 assert_balance(&ledger_env, p1_env.principal(), 0u8).await?;
443 assert_balance(&ledger_env, p2_env.principal(), 0u8).await?;
444
445 Ok(Outcome::Passed)
446}
447
448pub async fn icrc2_test_approve_expected_allowance(
450 ledger_env: impl LedgerEnv,
451) -> anyhow::Result<Outcome> {
452 let fee = transfer_fee(&ledger_env).await?;
453 let initial_balance: Nat = fee.clone() * 2u8;
454 let p1_env = setup_test_account(&ledger_env, initial_balance.clone()).await?;
455 let p2_env = ledger_env.fork();
456 let approve_amount = fee.clone();
457
458 approve(
459 &p1_env,
460 ApproveArgs::approve_amount(approve_amount.clone(), p2_env.principal()),
461 )
462 .await??;
463
464 let new_approve_amount: Nat = fee.clone() * 2u8;
466 match approve(
467 &p1_env,
468 ApproveArgs::approve_amount(new_approve_amount.clone(), p2_env.principal())
469 .expected_allowance(fee.clone() * 2u8),
470 )
471 .await?
472 {
473 Ok(_) => bail!("expected ApproveError::AllowanceChanged, got Ok result"),
474 Err(e) => match e {
475 ApproveError::AllowanceChanged { current_allowance } => {
476 if current_allowance != approve_amount {
477 bail!(
478 "wrong current_allowance, expected {}, got: {}",
479 approve_amount,
480 current_allowance
481 );
482 }
483 }
484 _ => return Err(e).context("expected ApproveError::AllowanceChanged"),
485 },
486 }
487
488 approve(
490 &p1_env,
491 ApproveArgs::approve_amount(new_approve_amount.clone(), p2_env.principal())
492 .expected_allowance(approve_amount),
493 )
494 .await??;
495
496 assert_allowance(
497 &p1_env,
498 p1_env.principal(),
499 p2_env.principal(),
500 new_approve_amount,
501 None,
502 )
503 .await?;
504
505 assert_balance(&ledger_env, p1_env.principal(), 0u8).await?;
506 assert_balance(&ledger_env, p2_env.principal(), 0u8).await?;
507
508 Ok(Outcome::Passed)
509}
510
511pub async fn icrc2_test_transfer_from(ledger_env: impl LedgerEnv) -> anyhow::Result<Outcome> {
513 let fee = transfer_fee(&ledger_env).await?;
514 let transfer_amount = fee.clone();
516 let initial_balance: Nat = transfer_amount.clone() * 2u8 + fee.clone() * 2u8;
517 let p1_env = setup_test_account(&ledger_env, initial_balance.clone()).await?;
518 let p2_env = ledger_env.fork();
519 let p3_env = ledger_env.fork();
520
521 let approve_amount: Nat = transfer_amount.clone() + fee.clone();
523
524 approve(
525 &p1_env,
526 ApproveArgs::approve_amount(approve_amount.clone(), p2_env.principal()),
527 )
528 .await??;
529
530 let transfer_amount = approve_amount - fee.clone() - Nat::from(1u8);
532 transfer_from(
533 &p2_env,
534 TransferFromArgs::transfer_from(
535 transfer_amount.clone(),
536 p3_env.principal(),
537 p1_env.principal(),
538 ),
539 )
540 .await??;
541
542 assert_balance(
543 &ledger_env,
544 p1_env.principal(),
545 initial_balance - fee.clone() - fee - transfer_amount.clone(),
547 )
548 .await?;
549 assert_balance(&ledger_env, p2_env.principal(), 0u8).await?;
551 assert_balance(&ledger_env, p3_env.principal(), transfer_amount).await?;
553
554 assert_allowance(
555 &p1_env,
556 p1_env.principal(),
557 p2_env.principal(),
558 Nat::from(1u8),
559 None,
560 )
561 .await?;
562 Ok(Outcome::Passed)
563}
564
565pub async fn icrc2_test_transfer_from_insufficient_funds(
567 ledger_env: impl LedgerEnv,
568) -> anyhow::Result<Outcome> {
569 let fee = transfer_fee(&ledger_env).await?;
570 let transfer_amount = fee.clone();
571 let initial_balance: Nat = transfer_amount.clone() + fee.clone();
573 let p1_env = setup_test_account(&ledger_env, initial_balance.clone()).await?;
574 let p2_env = ledger_env.fork();
575 let p3_env = ledger_env.fork();
576
577 let approve_amount: Nat = transfer_amount.clone() + fee.clone();
579 approve(
580 &p1_env,
581 ApproveArgs::approve_amount(approve_amount.clone(), p2_env.principal()),
582 )
583 .await??;
584
585 match transfer_from(
586 &p2_env,
587 TransferFromArgs::transfer_from(
588 transfer_amount.clone(),
589 p3_env.principal(),
590 p1_env.principal(),
591 ),
592 )
593 .await?
594 {
595 Ok(_) => bail!("expected TransferFromError::InsufficientFunds, got Ok result"),
596 Err(e) => match e {
597 TransferFromError::InsufficientFunds { balance } => {
598 if balance != transfer_amount {
599 bail!(
600 "wrong balance, expected {}, got: {}",
601 transfer_amount,
602 balance
603 );
604 }
605 }
606 _ => return Err(e).context("expected TransferFromError::InsufficientFunds"),
607 },
608 }
609
610 assert_balance(&ledger_env, p1_env.principal(), transfer_amount).await?;
612 assert_balance(&ledger_env, p2_env.principal(), 0u8).await?;
613 assert_balance(&ledger_env, p3_env.principal(), 0u8).await?;
614
615 assert_allowance(
617 &p1_env,
618 p1_env.principal(),
619 p2_env.principal(),
620 approve_amount,
621 None,
622 )
623 .await?;
624
625 Ok(Outcome::Passed)
626}
627
628pub async fn icrc2_test_transfer_from_insufficient_allowance(
630 ledger_env: impl LedgerEnv,
631) -> anyhow::Result<Outcome> {
632 let fee = transfer_fee(&ledger_env).await?;
633 let transfer_amount = fee.clone();
634 let initial_balance: Nat = transfer_amount.clone() + fee.clone();
635 let p1_env = setup_test_account(&ledger_env, initial_balance.clone()).await?;
636 let p2_env = ledger_env.fork();
637 let p3_env = ledger_env.fork();
638
639 match transfer_from(
640 &p2_env,
641 TransferFromArgs::transfer_from(
642 transfer_amount.clone(),
643 p3_env.principal(),
644 p1_env.principal(),
645 ),
646 )
647 .await?
648 {
649 Ok(_) => bail!("expected TransferFromError::InsufficientAllowance, got Ok result"),
650 Err(e) => match e {
651 TransferFromError::InsufficientAllowance { allowance } => {
652 if allowance != 0u8 {
653 bail!("wrong allowance, expected 0, got: {}", allowance);
654 }
655 }
656 _ => return Err(e).context("expected TransferFromError::InsufficientAllowance"),
657 },
658 }
659
660 assert_balance(&ledger_env, p1_env.principal(), initial_balance).await?;
662 assert_balance(&ledger_env, p2_env.principal(), 0u8).await?;
663 assert_balance(&ledger_env, p3_env.principal(), 0u8).await?;
664
665 Ok(Outcome::Passed)
666}
667
668pub async fn icrc2_test_transfer_from_self(ledger_env: impl LedgerEnv) -> anyhow::Result<Outcome> {
670 let fee = transfer_fee(&ledger_env).await?;
671 let transfer_amount = fee.clone();
672 let initial_balance: Nat = transfer_amount.clone() + fee.clone();
673 let p1_env = setup_test_account(&ledger_env, initial_balance.clone()).await?;
674 let p2_env = ledger_env.fork();
675
676 transfer_from(
678 &p1_env,
679 TransferFromArgs::transfer_from(
680 transfer_amount.clone(),
681 p2_env.principal(),
682 p1_env.principal(),
683 ),
684 )
685 .await??;
686
687 assert_balance(&ledger_env, p1_env.principal(), 0u8).await?;
689 assert_balance(&ledger_env, p2_env.principal(), transfer_amount).await?;
691
692 Ok(Outcome::Passed)
693}
694
695pub async fn icrc1_test_tx_deduplication(ledger_env: impl LedgerEnv) -> anyhow::Result<Outcome> {
697 let fee = transfer_fee(&ledger_env).await?;
698 let transfer_amount = Nat::from(10_000u64);
699 let initial_balance: Nat = transfer_amount.clone() * 7u8 + fee.clone() * 7u8;
700 let p1_env = setup_test_account(&ledger_env, initial_balance.clone()).await?;
702 let p2_env = p1_env.fork();
703
704 let transfer_args = Transfer::amount_to(transfer_amount.clone(), p2_env.principal());
706 transfer(&p1_env, transfer_args.clone())
707 .await?
708 .context("failed to execute the first no-dedup transfer")?;
709
710 assert_balance(&p1_env, p2_env.principal(), transfer_amount.clone()).await?;
711
712 transfer(&p1_env, transfer_args.clone())
713 .await?
714 .context("failed to execute the second no-dedup transfer")?;
715
716 assert_balance(&p1_env, p2_env.principal(), transfer_amount.clone() * 2u8).await?;
717
718 let transfer_args = transfer_args.created_at_time(time_nanos(&ledger_env).await);
721
722 let txid = match transfer(&p1_env, transfer_args.clone()).await? {
723 Ok(txid) => txid,
724 Err(TransferError::TooOld) => {
725 return Ok(Outcome::Skipped {
726 reason: "the ledger does not support deduplication".to_string(),
727 })
728 }
729 Err(e) => return Err(e).context("failed to execute the first dedup transfer"),
730 };
731
732 assert_balance(&p1_env, p2_env.principal(), transfer_amount.clone() * 3u8).await?;
733
734 assert_equal(
736 Err(TransferError::Duplicate {
737 duplicate_of: txid.clone(),
738 }),
739 transfer(&p1_env, transfer_args.clone()).await?,
740 )?;
741
742 assert_balance(&p1_env, p2_env.principal(), transfer_amount.clone() * 3u8).await?;
743
744 let transfer_args = transfer_args.fee(fee.clone());
747
748 let txid_2 = transfer(&p1_env, transfer_args.clone())
749 .await?
750 .context("failed to execute the transfer with an explicitly set fee field")?;
751
752 assert_balance(&p1_env, p2_env.principal(), transfer_amount.clone() * 4u8).await?;
753
754 assert_not_equal(&txid, &txid_2).context("duplicate txid")?;
755
756 assert_equal(
758 Err(TransferError::Duplicate {
759 duplicate_of: txid_2.clone(),
760 }),
761 transfer(&p1_env, transfer_args.clone()).await?,
762 )?;
763
764 assert_balance(&p1_env, p2_env.principal(), transfer_amount.clone() * 4u8).await?;
765
766 let transfer_args = transfer_args.memo(vec![1, 2, 3]);
769
770 let txid_3 = transfer(&p1_env, transfer_args.clone())
771 .await?
772 .context("failed to execute the transfer with an explicitly set memo field")?;
773
774 assert_balance(&p1_env, p2_env.principal(), transfer_amount.clone() * 5u8).await?;
775
776 assert_not_equal(&txid, &txid_3).context("duplicate txid")?;
777 assert_not_equal(&txid_2, &txid_3).context("duplicate txid")?;
778
779 assert_equal(
781 Err(TransferError::Duplicate {
782 duplicate_of: txid_3,
783 }),
784 transfer(&p1_env, transfer_args.clone()).await?,
785 )?;
786
787 assert_balance(&p1_env, p2_env.principal(), transfer_amount.clone() * 5u8).await?;
788
789 let now = time_nanos(&ledger_env).await;
790
791 transfer(
795 &p1_env,
796 Transfer::amount_to(
797 transfer_amount.clone(),
798 Account {
799 owner: p2_env.principal(),
800 subaccount: None,
801 },
802 )
803 .memo(vec![0])
804 .created_at_time(now),
805 )
806 .await?
807 .context("failed to execute the transfer with an empty subaccount")?;
808
809 assert_balance(&p1_env, p2_env.principal(), transfer_amount.clone() * 6u8).await?;
810
811 transfer(
812 &p1_env,
813 Transfer::amount_to(
814 transfer_amount.clone(),
815 Account {
816 owner: p2_env.principal(),
817 subaccount: Some([0; 32]),
818 },
819 )
820 .memo(vec![0])
821 .created_at_time(now),
822 )
823 .await?
824 .context("failed to execute the transfer with the default subaccount")?;
825
826 assert_balance(&p1_env, p2_env.principal(), transfer_amount.clone() * 7u8).await?;
827
828 Ok(Outcome::Passed)
829}
830
831pub async fn icrc1_test_bad_fee(ledger_env: impl LedgerEnv) -> anyhow::Result<Outcome> {
833 let fee = transfer_fee(&ledger_env).await?;
834 let transfer_amount = Nat::from(10_000u16);
835 let initial_balance: Nat = transfer_amount.clone() + fee.clone();
836 let p1_env = setup_test_account(&ledger_env, initial_balance).await?;
838 let p2_env = p1_env.fork();
839
840 let mut transfer_args = Transfer::amount_to(transfer_amount.clone(), p2_env.principal());
841 transfer_args = transfer_args.fee(fee.clone() + Nat::from(1u8));
843 match transfer(&ledger_env, transfer_args.clone()).await? {
844 Ok(_) => return Err(anyhow::Error::msg("Expected Bad Fee Error")),
845 Err(err) => match err {
846 TransferError::BadFee { expected_fee } => {
847 if expected_fee != transfer_fee(&ledger_env).await? {
848 return Err(anyhow::Error::msg(format!(
849 "Expected BadFee argument to be {}, got {}",
850 fee, expected_fee
851 )));
852 }
853 }
854 _ => return Err(anyhow::Error::msg("Expected BadFee error")),
855 },
856 }
857 Ok(Outcome::Passed)
858}
859
860pub async fn icrc1_test_future_transfer(ledger_env: impl LedgerEnv) -> anyhow::Result<Outcome> {
862 let fee = transfer_fee(&ledger_env).await?;
863 let transfer_amount = Nat::from(10_000u16);
864 let initial_balance: Nat = transfer_amount.clone() + fee.clone();
865 let p1_env = setup_test_account(&ledger_env, initial_balance).await?;
867 let p2_env = p1_env.fork();
868
869 let mut transfer_args = Transfer::amount_to(transfer_amount, p2_env.principal());
870
871 transfer_args = transfer_args.created_at_time(u64::MAX);
873 match transfer(&ledger_env, transfer_args).await? {
874 Err(TransferError::CreatedInFuture { ledger_time: _ }) => Ok(Outcome::Passed),
875 other => bail!("expected CreatedInFuture error, got: {:?}", other),
876 }
877}
878
879pub async fn icrc1_test_memo_bytes_length(ledger_env: impl LedgerEnv) -> anyhow::Result<Outcome> {
881 let fee = transfer_fee(&ledger_env).await?;
882 let transfer_amount = Nat::from(10_000u16);
883 let initial_balance: Nat = transfer_amount.clone() + fee.clone();
884 let p1_env = setup_test_account(&ledger_env, initial_balance).await?;
886 let p2_env = p1_env.fork();
887
888 let transfer_args = Transfer::amount_to(transfer_amount, p2_env.principal()).memo([1u8; 32]);
889 match transfer(&ledger_env, transfer_args.clone()).await? {
891 Ok(_) => Ok(Outcome::Passed),
892 Err(err) => bail!(
893 "Expected memo with 32 bytes to succeed but got error: {:?}",
894 err
895 ),
896 }
897}
898
899pub fn icrc1_test_suite(env: impl LedgerEnv + 'static + Clone) -> Vec<Test> {
901 vec![
902 test("icrc1:transfer", icrc1_test_transfer(env.clone())),
903 test("icrc1:burn", icrc1_test_burn(env.clone())),
904 test("icrc1:metadata", icrc1_test_metadata(env.clone())),
905 test(
906 "icrc1:supported_standards",
907 icrc1_test_supported_standards(env.clone()),
908 ),
909 test(
910 "icrc1:tx_deduplication",
911 icrc1_test_tx_deduplication(env.clone()),
912 ),
913 test(
914 "icrc1:memo_bytes_length",
915 icrc1_test_memo_bytes_length(env.clone()),
916 ),
917 test(
918 "icrc1:future_transfers",
919 icrc1_test_future_transfer(env.clone()),
920 ),
921 test("icrc1:bad_fee", icrc1_test_bad_fee(env)),
922 ]
923}
924
925pub fn icrc2_test_suite(env: impl LedgerEnv + 'static + Clone) -> Vec<Test> {
927 vec![
928 test(
929 "icrc2:supported_standards",
930 icrc2_test_supported_standards(env.clone()),
931 ),
932 test("icrc2:approve", icrc2_test_approve(env.clone())),
933 test(
934 "icrc2:approve_expiration",
935 icrc2_test_approve_expiration(env.clone()),
936 ),
937 test(
938 "icrc2:approve_expected_allowance",
939 icrc2_test_approve_expected_allowance(env.clone()),
940 ),
941 test("icrc2:transfer_from", icrc2_test_transfer_from(env.clone())),
942 test(
943 "icrc2:transfer_from_insufficient_funds",
944 icrc2_test_transfer_from_insufficient_funds(env.clone()),
945 ),
946 test(
947 "icrc2:transfer_from_insufficient_allowance",
948 icrc2_test_transfer_from_insufficient_allowance(env.clone()),
949 ),
950 test(
951 "icrc2:transfer_from_self",
952 icrc2_test_transfer_from_self(env.clone()),
953 ),
954 ]
955}
956
957pub async fn test_suite(env: impl LedgerEnv + 'static + Clone) -> Vec<Test> {
958 match supported_standards(&env).await {
959 Ok(standard) => {
960 let mut tests = vec![];
961 if standard.iter().any(|std| std.name == "ICRC-1") {
962 tests.append(&mut icrc1_test_suite(env.clone()));
963 }
964 if standard.iter().any(|std| std.name == "ICRC-2") {
965 tests.append(&mut icrc2_test_suite(env));
966 }
967 tests
968 }
969 Err(_) => {
970 println!("No standard is supported by the given ledger: Is the endpoint 'icrc1_supported_standards' implemented correctly?");
971 vec![]
972 }
973 }
974}
975pub async fn execute_tests(tests: Vec<Test>) -> bool {
978 use futures::stream::FuturesOrdered;
979
980 let mut names = Vec::new();
981 let mut futures = FuturesOrdered::new();
982
983 for test in tests.into_iter() {
984 names.push(test.name);
985 futures.push_back(test.action);
986 }
987
988 println!("TAP version 14");
989 println!("1..{}", futures.len());
990
991 let mut idx = 0;
992 let mut success = true;
993 while let Some(result) = futures.next().await {
994 match result {
995 Ok(Outcome::Passed) => {
996 println!("ok {} - {}", idx + 1, names[idx]);
997 }
998 Ok(Outcome::Skipped { reason }) => {
999 println!("ok {} - {} # SKIP {}", idx + 1, names[idx], reason);
1000 }
1001 Err(err) => {
1002 success = false;
1003
1004 for line in format!("{:?}", err).lines() {
1005 println!("# {}", line);
1006 }
1007
1008 println!("not ok {} - {}", idx + 1, names[idx]);
1009 }
1010 }
1011 idx += 1;
1012 }
1013
1014 success
1015}