icrc1_test_env/
lib.rs

1use async_trait::async_trait;
2use candid::utils::{ArgumentDecoder, ArgumentEncoder};
3use candid::Principal;
4use candid::{CandidType, Int, Nat};
5use serde::Deserialize;
6use thiserror::Error;
7
8pub type Subaccount = [u8; 32];
9
10#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
11pub struct Account {
12    pub owner: Principal,
13    pub subaccount: Option<Subaccount>,
14}
15
16impl From<Principal> for Account {
17    fn from(owner: Principal) -> Self {
18        Self {
19            owner,
20            subaccount: None,
21        }
22    }
23}
24
25#[derive(CandidType, Deserialize, PartialEq, Clone, Debug)]
26pub struct SupportedStandard {
27    pub name: String,
28    pub url: String,
29}
30
31#[derive(CandidType, Clone, Debug, Deserialize, PartialEq)]
32pub enum Value {
33    Text(String),
34    Blob(Vec<u8>),
35    Nat(Nat),
36    Int(Int),
37}
38
39#[derive(CandidType, Deserialize, PartialEq, Eq, Debug, Clone, Error)]
40pub enum TransferError {
41    #[error("Invalid transfer fee, the ledger expected fee {expected_fee}")]
42    BadFee { expected_fee: Nat },
43    #[error("Invalid burn amount, the minimal burn amount is {min_burn_amount}")]
44    BadBurn { min_burn_amount: Nat },
45    #[error("The account owner doesn't have enough funds to for the transfer, balance: {balance}")]
46    InsufficientFunds { balance: Nat },
47    #[error("created_at_time is too far in the past")]
48    TooOld,
49    #[error("created_at_time is too far in the future, ledger time: {ledger_time}")]
50    CreatedInFuture { ledger_time: u64 },
51    #[error("the transfer is a duplicate of transaction {duplicate_of}")]
52    Duplicate { duplicate_of: Nat },
53    #[error("the ledger is temporarily unavailable")]
54    TemporarilyUnavailable,
55    #[error("generic error (code {error_code}): {message}")]
56    GenericError { error_code: Nat, message: String },
57}
58
59#[derive(CandidType, Debug, Clone)]
60pub struct Transfer {
61    from_subaccount: Option<Subaccount>,
62    amount: Nat,
63    to: Account,
64    fee: Option<Nat>,
65    created_at_time: Option<u64>,
66    memo: Option<Vec<u8>>,
67}
68
69impl Transfer {
70    pub fn amount_to(amount: impl Into<Nat>, to: impl Into<Account>) -> Self {
71        Self {
72            from_subaccount: None,
73            amount: amount.into(),
74            to: to.into(),
75            fee: None,
76            created_at_time: None,
77            memo: None,
78        }
79    }
80
81    pub fn from_subaccount(mut self, from_subaccount: Subaccount) -> Self {
82        self.from_subaccount = Some(from_subaccount);
83        self
84    }
85
86    pub fn fee(mut self, fee: impl Into<Nat>) -> Self {
87        self.fee = Some(fee.into());
88        self
89    }
90
91    pub fn created_at_time(mut self, time: u64) -> Self {
92        self.created_at_time = Some(time);
93        self
94    }
95
96    pub fn memo(mut self, memo: impl Into<Vec<u8>>) -> Self {
97        self.memo = Some(memo.into());
98        self
99    }
100}
101
102#[derive(CandidType, Clone, Debug, PartialEq, Eq)]
103pub struct ApproveArgs {
104    pub from_subaccount: Option<Subaccount>,
105    pub spender: Account,
106    pub amount: Nat,
107    pub expected_allowance: Option<Nat>,
108    pub expires_at: Option<u64>,
109    pub memo: Option<Vec<u8>>,
110    pub fee: Option<Nat>,
111    pub created_at_time: Option<u64>,
112}
113
114impl ApproveArgs {
115    pub fn approve_amount(amount: impl Into<Nat>, spender: impl Into<Account>) -> Self {
116        Self {
117            amount: amount.into(),
118            fee: None,
119            created_at_time: None,
120            memo: None,
121            from_subaccount: None,
122            spender: spender.into(),
123            expected_allowance: None,
124            expires_at: None,
125        }
126    }
127
128    pub fn expected_allowance(mut self, expected_allowance: Nat) -> Self {
129        self.expected_allowance = Some(expected_allowance);
130        self
131    }
132
133    pub fn fee(mut self, fee: impl Into<Nat>) -> Self {
134        self.fee = Some(fee.into());
135        self
136    }
137
138    pub fn created_at_time(mut self, time: u64) -> Self {
139        self.created_at_time = Some(time);
140        self
141    }
142
143    pub fn expires_at(mut self, time: u64) -> Self {
144        self.expires_at = Some(time);
145        self
146    }
147
148    pub fn memo(mut self, memo: impl Into<Vec<u8>>) -> Self {
149        self.memo = Some(memo.into());
150        self
151    }
152}
153
154#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq, Error)]
155pub enum ApproveError {
156    #[error("Invalid transfer fee, the ledger expected fee {expected_fee}")]
157    BadFee { expected_fee: Nat },
158    #[error("The account owner doesn't have enough funds to for the approval, balance: {balance}")]
159    InsufficientFunds { balance: Nat },
160    #[error("The allowance changed, current allowance: {current_allowance}")]
161    AllowanceChanged { current_allowance: Nat },
162    #[error("the approval expiration time is in the past, ledger time: {ledger_time}")]
163    Expired { ledger_time: u64 },
164    #[error("created_at_time is too far in the past")]
165    TooOld,
166    #[error("created_at_time is too far in the future, ledger time: {ledger_time}")]
167    CreatedInFuture { ledger_time: u64 },
168    #[error("the transfer is a duplicate of transaction {duplicate_of}")]
169    Duplicate { duplicate_of: Nat },
170    #[error("the ledger is temporarily unavailable")]
171    TemporarilyUnavailable,
172    #[error("generic error (code {error_code}): {message}")]
173    GenericError { error_code: Nat, message: String },
174}
175
176#[derive(CandidType, Clone, Debug, PartialEq, Eq)]
177pub struct TransferFromArgs {
178    pub spender_subaccount: Option<Subaccount>,
179    pub from: Account,
180    pub to: Account,
181    pub amount: Nat,
182    pub fee: Option<Nat>,
183    pub memo: Option<Vec<u8>>,
184    pub created_at_time: Option<u64>,
185}
186
187impl TransferFromArgs {
188    pub fn transfer_from(
189        amount: impl Into<Nat>,
190        to: impl Into<Account>,
191        from: impl Into<Account>,
192    ) -> Self {
193        Self {
194            spender_subaccount: None,
195            amount: amount.into(),
196            to: to.into(),
197            fee: None,
198            created_at_time: None,
199            memo: None,
200            from: from.into(),
201        }
202    }
203
204    pub fn from_subaccount(mut self, spender_subaccount: Subaccount) -> Self {
205        self.spender_subaccount = Some(spender_subaccount);
206        self
207    }
208
209    pub fn fee(mut self, fee: impl Into<Nat>) -> Self {
210        self.fee = Some(fee.into());
211        self
212    }
213
214    pub fn created_at_time(mut self, time: u64) -> Self {
215        self.created_at_time = Some(time);
216        self
217    }
218
219    pub fn memo(mut self, memo: impl Into<Vec<u8>>) -> Self {
220        self.memo = Some(memo.into());
221        self
222    }
223}
224
225#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq, Error)]
226pub enum TransferFromError {
227    #[error("Invalid transfer fee, the ledger expected fee {expected_fee}")]
228    BadFee { expected_fee: Nat },
229    #[error("Invalid burn amount, the minimal burn amount is {min_burn_amount}")]
230    BadBurn { min_burn_amount: Nat },
231    #[error("The account owner doesn't have enough funds to for the transfer, balance: {balance}")]
232    InsufficientFunds { balance: Nat },
233    #[error("The account owner doesn't have allowance for the transfer, allowance: {allowance}")]
234    InsufficientAllowance { allowance: Nat },
235    #[error("created_at_time is too far in the past")]
236    TooOld,
237    #[error("created_at_time is too far in the future, ledger time: {ledger_time}")]
238    CreatedInFuture { ledger_time: u64 },
239    #[error("the transfer is a duplicate of transaction {duplicate_of}")]
240    Duplicate { duplicate_of: Nat },
241    #[error("the ledger is temporarily unavailable")]
242    TemporarilyUnavailable,
243    #[error("generic error (code {error_code}): {message}")]
244    GenericError { error_code: Nat, message: String },
245}
246
247#[derive(CandidType, Clone, Debug, PartialEq, Eq)]
248pub struct AllowanceArgs {
249    pub account: Account,
250    pub spender: Account,
251}
252
253#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)]
254pub struct Allowance {
255    pub allowance: Nat,
256    #[serde(default)]
257    pub expires_at: Option<u64>,
258}
259
260#[async_trait(?Send)]
261pub trait LedgerEnv {
262    /// Creates a new environment pointing to the same ledger but using a new caller.
263    fn fork(&self) -> Self;
264
265    /// Returns the caller's principal.
266    fn principal(&self) -> Principal;
267
268    /// Returns the approximation of the current ledger time.
269    async fn time(&self) -> std::time::SystemTime;
270
271    /// Executes a query call with the specified arguments on the ledger.
272    async fn query<Input, Output>(&self, method: &str, input: Input) -> anyhow::Result<Output>
273    where
274        Input: ArgumentEncoder + std::fmt::Debug,
275        Output: for<'a> ArgumentDecoder<'a>;
276
277    /// Executes an update call with the specified arguments on the ledger.
278    async fn update<Input, Output>(&self, method: &str, input: Input) -> anyhow::Result<Output>
279    where
280        Input: ArgumentEncoder + std::fmt::Debug,
281        Output: for<'a> ArgumentDecoder<'a>;
282}
283
284pub mod icrc1 {
285    use crate::{Account, LedgerEnv, SupportedStandard, Transfer, TransferError, Value};
286    use candid::Nat;
287
288    pub async fn transfer(
289        ledger: &impl LedgerEnv,
290        arg: Transfer,
291    ) -> anyhow::Result<Result<Nat, TransferError>> {
292        ledger.update("icrc1_transfer", (arg,)).await.map(|(t,)| t)
293    }
294
295    pub async fn balance_of(
296        ledger: &impl LedgerEnv,
297        account: impl Into<Account>,
298    ) -> anyhow::Result<Nat> {
299        ledger
300            .query("icrc1_balance_of", (account.into(),))
301            .await
302            .map(|(t,)| t)
303    }
304
305    pub async fn supported_standards(
306        ledger: &impl LedgerEnv,
307    ) -> anyhow::Result<Vec<SupportedStandard>> {
308        ledger
309            .query("icrc1_supported_standards", ())
310            .await
311            .map(|(t,)| t)
312    }
313
314    pub async fn metadata(ledger: &impl LedgerEnv) -> anyhow::Result<Vec<(String, Value)>> {
315        ledger.query("icrc1_metadata", ()).await.map(|(t,)| t)
316    }
317
318    pub async fn minting_account(ledger: &impl LedgerEnv) -> anyhow::Result<Option<Account>> {
319        ledger
320            .query("icrc1_minting_account", ())
321            .await
322            .map(|(t,)| t)
323    }
324
325    pub async fn token_name(ledger: &impl LedgerEnv) -> anyhow::Result<String> {
326        ledger.query("icrc1_name", ()).await.map(|(t,)| t)
327    }
328
329    pub async fn token_symbol(ledger: &impl LedgerEnv) -> anyhow::Result<String> {
330        ledger.query("icrc1_symbol", ()).await.map(|(t,)| t)
331    }
332
333    pub async fn token_decimals(ledger: &impl LedgerEnv) -> anyhow::Result<u8> {
334        ledger.query("icrc1_decimals", ()).await.map(|(t,)| t)
335    }
336
337    pub async fn transfer_fee(ledger: &impl LedgerEnv) -> anyhow::Result<Nat> {
338        ledger.query("icrc1_fee", ()).await.map(|(t,)| t)
339    }
340}
341
342pub mod icrc2 {
343    use crate::{
344        Allowance, AllowanceArgs, ApproveArgs, ApproveError, LedgerEnv, TransferFromArgs,
345        TransferFromError,
346    };
347    use candid::Nat;
348
349    pub async fn approve(
350        ledger: &impl LedgerEnv,
351        arg: ApproveArgs,
352    ) -> anyhow::Result<Result<Nat, ApproveError>> {
353        ledger.update("icrc2_approve", (arg,)).await.map(|(t,)| t)
354    }
355
356    pub async fn transfer_from(
357        ledger: &impl LedgerEnv,
358        arg: TransferFromArgs,
359    ) -> anyhow::Result<Result<Nat, TransferFromError>> {
360        ledger
361            .update("icrc2_transfer_from", (arg,))
362            .await
363            .map(|(t,)| t)
364    }
365
366    pub async fn allowance(
367        ledger: &impl LedgerEnv,
368        arg: AllowanceArgs,
369    ) -> anyhow::Result<Allowance> {
370        ledger.query("icrc2_allowance", (arg,)).await.map(|(t,)| t)
371    }
372}