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 fn fork(&self) -> Self;
264
265 fn principal(&self) -> Principal;
267
268 async fn time(&self) -> std::time::SystemTime;
270
271 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 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}