Skip to content

Commit

Permalink
Merge pull request #145 from dfinity/add-icrc2-basic-tests
Browse files Browse the repository at this point in the history
feat(FI-871) add basic tests for approve and transfer from
  • Loading branch information
NikolasHai authored Aug 9, 2023
2 parents 25e9f3f + 1e9d667 commit 85b7613
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 75 deletions.
6 changes: 5 additions & 1 deletion Cargo.Bazel.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"checksum": "fd7fe8ea967274d2f13641d3bbfad70cb48ad849c110fd74440b62e666029e40",
"checksum": "ad9783ce8e08ab1bcbcc853142e7d93648924113cbe1f4dffb641461593f5afe",
"crates": {
"addr2line 0.20.0": {
"name": "addr2line",
Expand Down Expand Up @@ -5060,6 +5060,10 @@
{
"id": "serde 1.0.171",
"target": "serde"
},
{
"id": "thiserror 1.0.44",
"target": "thiserror"
}
],
"selects": {}
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ rand = "0.8.5"
ring = "0.16.20"
serde = "=1.0.171"
tempfile = "3.3"
thiserror = "1"
tokio = { version = "1.20.1", features = ["macros"] }

[workspace.package]
authors = ["DFINITY Stiftung"]
edition = "2018"
repository = "https://github.com/dfinity/ICRC-1"
rust-version = "1.31.0"
license = "Apache-2.0"
license = "Apache-2.0"
6 changes: 4 additions & 2 deletions ref/ICRC1.mo
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ actor class Ledger(init : { initial_mints : [{ account : { owner : Principal; su
};
if (accountsEqual(args.to, account)) { sum += args.amount };
};
case (#Approve(_)) {};
case (#Approve(args)) {
if (accountsEqual(args.from, account)) { sum -= tx.fee };
};
};
};
sum;
Expand All @@ -140,7 +142,7 @@ actor class Ledger(init : { initial_mints : [{ account : { owner : Principal; su
case (#Burn(args)) { total -= args.amount };
case (#Mint(args)) { total += args.amount };
case (#Transfer(_)) { total -= tx.fee };
case (#Approve(_)) {};
case (#Approve(_)) { total -= tx.fee };
};
};
total;
Expand Down
3 changes: 2 additions & 1 deletion test/env/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ path = "lib.rs"

[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
candid = { workspace = true }
serde = { workspace = true }
async-trait = { workspace = true }
thiserror = { workspace = true }
244 changes: 201 additions & 43 deletions test/env/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ use candid::utils::{ArgumentDecoder, ArgumentEncoder};
use candid::Principal;
use candid::{CandidType, Int, Nat};
use serde::Deserialize;
use std::fmt;
use thiserror::Error;

pub type Subaccount = [u8; 32];

#[derive(CandidType, Clone, Debug, Deserialize)]
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct Account {
pub owner: Principal,
pub subaccount: Option<Subaccount>,
Expand Down Expand Up @@ -36,58 +36,26 @@ pub enum Value {
Int(Int),
}

#[derive(CandidType, Deserialize, PartialEq, Eq, Debug, Clone)]
#[derive(CandidType, Deserialize, PartialEq, Eq, Debug, Clone, Error)]
pub enum TransferError {
#[error("Invalid transfer fee, the ledger expected fee {expected_fee}")]
BadFee { expected_fee: Nat },
#[error("Invalid burn amount, the minimal burn amount is {min_burn_amount}")]
BadBurn { min_burn_amount: Nat },
#[error("The account owner doesn't have enough funds to for the transfer, balance: {balance}")]
InsufficientFunds { balance: Nat },
#[error("created_at_time is too far in the past")]
TooOld,
#[error("created_at_time is too far in the future, ledger time: {ledger_time}")]
CreatedInFuture { ledger_time: u64 },
#[error("the transfer is a duplicate of transaction {duplicate_of}")]
Duplicate { duplicate_of: Nat },
#[error("the ledger is temporarily unavailable")]
TemporarilyUnavailable,
#[error("generic error (code {error_code}): {message}")]
GenericError { error_code: Nat, message: String },
}

impl fmt::Display for TransferError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BadFee { expected_fee } => write!(
f,
"Invalid transfer fee, the ledger expected fee {}",
expected_fee
),
Self::BadBurn { min_burn_amount } => write!(
f,
"Invalid burn amount, the minimal burn amount is {}",
min_burn_amount
),
Self::InsufficientFunds { balance } => write!(
f,
"The account owner doesn't have enough funds to for the transfer, balance: {}",
balance
),
Self::TooOld => write!(f, "created_at_time is too far in the past"),
Self::CreatedInFuture { ledger_time } => write!(
f,
"created_at_time is too far in the future, ledger time: {}",
ledger_time
),
Self::Duplicate { duplicate_of } => write!(
f,
"the transfer is a duplicate of transaction {}",
duplicate_of
),
Self::TemporarilyUnavailable => write!(f, "the ledger is temporarily unavailable"),
Self::GenericError {
error_code,
message,
} => write!(f, "generic error (code {}): {}", error_code, message),
}
}
}

impl std::error::Error for TransferError {}

#[derive(CandidType, Debug, Clone)]
pub struct Transfer {
from_subaccount: Option<Subaccount>,
Expand Down Expand Up @@ -131,6 +99,164 @@ impl Transfer {
}
}

#[derive(CandidType, Clone, Debug, PartialEq, Eq)]
pub struct ApproveArgs {
pub from_subaccount: Option<Subaccount>,
pub spender: Account,
pub amount: Nat,
pub expected_allowance: Option<Nat>,
pub expires_at: Option<u64>,
pub memo: Option<Vec<u8>>,
pub fee: Option<Nat>,
pub created_at_time: Option<u64>,
}

impl ApproveArgs {
pub fn approve_amount(amount: impl Into<Nat>, spender: impl Into<Account>) -> Self {
Self {
amount: amount.into(),
fee: None,
created_at_time: None,
memo: None,
from_subaccount: None,
spender: spender.into(),
expected_allowance: None,
expires_at: None,
}
}

pub fn expected_allowance(mut self, expected_allowance: Nat) -> Self {
self.expected_allowance = Some(expected_allowance);
self
}

pub fn fee(mut self, fee: impl Into<Nat>) -> Self {
self.fee = Some(fee.into());
self
}

pub fn created_at_time(mut self, time: u64) -> Self {
self.created_at_time = Some(time);
self
}

pub fn expires_at(mut self, time: u64) -> Self {
self.expires_at = Some(time);
self
}

pub fn memo(mut self, memo: impl Into<Vec<u8>>) -> Self {
self.memo = Some(memo.into());
self
}
}

#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq, Error)]
pub enum ApproveError {
#[error("Invalid transfer fee, the ledger expected fee {expected_fee}")]
BadFee { expected_fee: Nat },
#[error("The account owner doesn't have enough funds to for the approval, balance: {balance}")]
InsufficientFunds { balance: Nat },
#[error("The allowance changed, current allowance: {current_allowance}")]
AllowanceChanged { current_allowance: Nat },
#[error("the approval expiration time is in the past, ledger time: {ledger_time}")]
Expired { ledger_time: u64 },
#[error("created_at_time is too far in the past")]
TooOld,
#[error("created_at_time is too far in the future, ledger time: {ledger_time}")]
CreatedInFuture { ledger_time: u64 },
#[error("the transfer is a duplicate of transaction {duplicate_of}")]
Duplicate { duplicate_of: Nat },
#[error("the ledger is temporarily unavailable")]
TemporarilyUnavailable,
#[error("generic error (code {error_code}): {message}")]
GenericError { error_code: Nat, message: String },
}

#[derive(CandidType, Clone, Debug, PartialEq, Eq)]
pub struct TransferFromArgs {
pub spender_subaccount: Option<Subaccount>,
pub from: Account,
pub to: Account,
pub amount: Nat,
pub fee: Option<Nat>,
pub memo: Option<Vec<u8>>,
pub created_at_time: Option<u64>,
}

impl TransferFromArgs {
pub fn transfer_from(
amount: impl Into<Nat>,
to: impl Into<Account>,
from: impl Into<Account>,
) -> Self {
Self {
spender_subaccount: None,
amount: amount.into(),
to: to.into(),
fee: None,
created_at_time: None,
memo: None,
from: from.into(),
}
}

pub fn from_subaccount(mut self, spender_subaccount: Subaccount) -> Self {
self.spender_subaccount = Some(spender_subaccount);
self
}

pub fn fee(mut self, fee: impl Into<Nat>) -> Self {
self.fee = Some(fee.into());
self
}

pub fn created_at_time(mut self, time: u64) -> Self {
self.created_at_time = Some(time);
self
}

pub fn memo(mut self, memo: impl Into<Vec<u8>>) -> Self {
self.memo = Some(memo.into());
self
}
}

#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq, Error)]
pub enum TransferFromError {
#[error("Invalid transfer fee, the ledger expected fee {expected_fee}")]
BadFee { expected_fee: Nat },
#[error("Invalid burn amount, the minimal burn amount is {min_burn_amount}")]
BadBurn { min_burn_amount: Nat },
#[error("The account owner doesn't have enough funds to for the transfer, balance: {balance}")]
InsufficientFunds { balance: Nat },
#[error("The account owner doesn't have allowance for the transfer, allowance: {allowance}")]
InsufficientAllowance { allowance: Nat },
#[error("created_at_time is too far in the past")]
TooOld,
#[error("created_at_time is too far in the future, ledger time: {ledger_time}")]
CreatedInFuture { ledger_time: u64 },
#[error("the transfer is a duplicate of transaction {duplicate_of}")]
Duplicate { duplicate_of: Nat },
#[error("the ledger is temporarily unavailable")]
TemporarilyUnavailable,
#[error("generic error (code {error_code}): {message}")]
GenericError { error_code: Nat, message: String },
}

#[derive(CandidType, Clone, Debug, PartialEq, Eq)]
pub struct AllowanceArgs {
pub account: Account,
pub spender: Account,
}

#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Allowance {
pub allowance: Nat,
#[serde(default)]
pub expires_at: Option<u64>,
}

#[async_trait(?Send)]
pub trait LedgerEnv {
/// Creates a new environment pointing to the same ledger but using a new caller.
Expand Down Expand Up @@ -212,3 +338,35 @@ pub mod icrc1 {
ledger.query("icrc1_fee", ()).await.map(|(t,)| t)
}
}

pub mod icrc2 {
use crate::{
Allowance, AllowanceArgs, ApproveArgs, ApproveError, LedgerEnv, TransferFromArgs,
TransferFromError,
};
use candid::Nat;

pub async fn approve(
ledger: &impl LedgerEnv,
arg: ApproveArgs,
) -> anyhow::Result<Result<Nat, ApproveError>> {
ledger.update("icrc2_approve", (arg,)).await.map(|(t,)| t)
}

pub async fn transfer_from(
ledger: &impl LedgerEnv,
arg: TransferFromArgs,
) -> anyhow::Result<Result<Nat, TransferFromError>> {
ledger
.update("icrc2_transfer_from", (arg,))
.await
.map(|(t,)| t)
}

pub async fn allowance(
ledger: &impl LedgerEnv,
arg: AllowanceArgs,
) -> anyhow::Result<Allowance> {
ledger.query("icrc2_allowance", (arg,)).await.map(|(t,)| t)
}
}
Loading

0 comments on commit 85b7613

Please sign in to comment.