icrc_ledger_types/icrc21/
lib.rs

1use super::errors::ErrorInfo;
2use super::requests::ConsentMessageRequest;
3use super::requests::DisplayMessageType;
4use super::responses::{ConsentInfo, ConsentMessage};
5use crate::icrc1::account::Account;
6use crate::icrc1::transfer::TransferArg;
7use crate::icrc2::approve::ApproveArgs;
8use crate::icrc2::transfer_from::TransferFromArgs;
9use crate::icrc21::errors::Icrc21Error;
10use crate::icrc21::requests::ConsentMessageMetadata;
11use candid::Decode;
12use candid::{Nat, Principal};
13use serde_bytes::ByteBuf;
14use std::fmt::{self, Display};
15use strum::{self, IntoEnumIterator};
16use strum_macros::{Display, EnumIter, EnumString};
17
18// Maximum number of bytes that an argument to an ICRC-1 ledger function can have when passed to the ICRC-21 endpoint.
19pub const MAX_CONSENT_MESSAGE_ARG_SIZE_BYTES: u16 = 500;
20
21#[derive(Debug, EnumString, EnumIter, Display)]
22pub enum Icrc21Function {
23    #[strum(serialize = "icrc1_transfer")]
24    Transfer,
25    #[strum(serialize = "icrc2_approve")]
26    Approve,
27    #[strum(serialize = "icrc2_transfer_from")]
28    TransferFrom,
29    #[strum(serialize = "transfer")]
30    GenericTransfer,
31}
32
33pub enum AccountOrId {
34    Account(Account),
35    AccountIdAddress(Option<String>),
36}
37
38impl AccountOrId {
39    pub fn is_anonymous(&self) -> bool {
40        match self {
41            AccountOrId::Account(account) => account.owner == Principal::anonymous(),
42            AccountOrId::AccountIdAddress(addr) => addr.is_none(),
43        }
44    }
45}
46
47impl Display for AccountOrId {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            AccountOrId::Account(account) => write!(f, "{account}"),
51            AccountOrId::AccountIdAddress(Some(str)) => write!(f, "{str}"),
52            AccountOrId::AccountIdAddress(None) => write!(f, ""),
53        }
54    }
55}
56
57pub enum GenericMemo {
58    Icrc1Memo(ByteBuf),
59    IntMemo(u64),
60}
61
62pub struct ConsentMessageBuilder {
63    function: Icrc21Function,
64    display_type: Option<DisplayMessageType>,
65    approver: Option<Account>,
66    spender: Option<Account>,
67    from: Option<AccountOrId>,
68    receiver: Option<AccountOrId>,
69    amount: Option<Nat>,
70    token_symbol: Option<String>,
71    token_name: Option<String>,
72    ledger_fee: Option<Nat>,
73    memo: Option<GenericMemo>,
74    expected_allowance: Option<Nat>,
75    expires_at: Option<u64>,
76    utc_offset_minutes: Option<i16>,
77    decimals: u8,
78}
79
80impl ConsentMessageBuilder {
81    pub fn new(icrc21_function: &str, decimals: u8) -> Result<Self, Icrc21Error> {
82        let icrc21_function =
83            icrc21_function
84                .parse::<Icrc21Function>()
85                .map_err(|err| Icrc21Error::UnsupportedCanisterCall(ErrorInfo {                    description: format!("The function provided is not supported: {}.\n Supported functions for ICRC-21 are: {:?}.\n Error is: {:?}",icrc21_function,Icrc21Function::iter().map(|f|f.to_string()).collect::<Vec<String>>(),err)})
86                )?;
87
88        Ok(Self {
89            function: icrc21_function,
90            display_type: None,
91            approver: None,
92            spender: None,
93            from: None,
94            receiver: None,
95            amount: None,
96            token_symbol: None,
97            token_name: None,
98            ledger_fee: None,
99            utc_offset_minutes: None,
100            memo: None,
101            expected_allowance: None,
102            expires_at: None,
103            decimals,
104        })
105    }
106
107    pub fn with_approver_account(mut self, approver: Account) -> Self {
108        self.approver = Some(approver);
109        self
110    }
111
112    pub fn with_spender_account(mut self, spender: Account) -> Self {
113        self.spender = Some(spender);
114        self
115    }
116
117    pub fn with_from_account(mut self, from: AccountOrId) -> Self {
118        self.from = Some(from);
119        self
120    }
121
122    pub fn with_receiver_account(mut self, receiver: AccountOrId) -> Self {
123        self.receiver = Some(receiver);
124        self
125    }
126
127    pub fn with_amount(mut self, amount: Nat) -> Self {
128        self.amount = Some(amount);
129        self
130    }
131
132    pub fn with_token_symbol(mut self, token_symbol: String) -> Self {
133        self.token_symbol = Some(token_symbol);
134        self
135    }
136
137    pub fn with_token_name(mut self, token_name: String) -> Self {
138        self.token_name = Some(token_name);
139        self
140    }
141
142    pub fn with_ledger_fee(mut self, ledger_fee: Nat) -> Self {
143        self.ledger_fee = Some(ledger_fee);
144        self
145    }
146
147    pub fn with_memo(mut self, memo: GenericMemo) -> Self {
148        self.memo = Some(memo);
149        self
150    }
151
152    pub fn with_expected_allowance(mut self, expected_allowance: Nat) -> Self {
153        self.expected_allowance = Some(expected_allowance);
154        self
155    }
156
157    pub fn with_expires_at(mut self, expires_at: u64) -> Self {
158        self.expires_at = Some(expires_at);
159        self
160    }
161
162    pub fn with_display_type(mut self, display_type: DisplayMessageType) -> Self {
163        self.display_type = Some(display_type);
164        self
165    }
166
167    pub fn with_utc_offset_minutes(mut self, utc_offset_minutes: i16) -> Self {
168        self.utc_offset_minutes = Some(utc_offset_minutes);
169        self
170    }
171
172    pub fn build(self) -> Result<ConsentMessage, Icrc21Error> {
173        let mut message = match self.display_type {
174            Some(DisplayMessageType::GenericDisplay) | None => {
175                ConsentMessage::GenericDisplayMessage(Default::default())
176            }
177            Some(DisplayMessageType::FieldsDisplay) => {
178                ConsentMessage::FieldsDisplayMessage(Default::default())
179            }
180        };
181        match self.function {
182            Icrc21Function::Transfer | Icrc21Function::GenericTransfer => {
183                let from_account = self.from.ok_or(Icrc21Error::GenericError {
184                    error_code: Nat::from(500u64),
185                    description: "From account has to be specified.".to_owned(),
186                })?;
187                let receiver_account = self.receiver.ok_or(Icrc21Error::GenericError {
188                    error_code: Nat::from(500u64),
189                    description: "Receiver account has to be specified.".to_owned(),
190                })?;
191
192                let token_symbol = self.token_symbol.ok_or(Icrc21Error::GenericError {
193                    error_code: Nat::from(500u64),
194                    description: "Token Symbol must be specified.".to_owned(),
195                })?;
196                let token_name = self.token_name.ok_or(Icrc21Error::GenericError {
197                    error_code: Nat::from(500u64),
198                    description: "Token Name must be specified.".to_owned(),
199                })?;
200
201                message.add_intent(Icrc21Function::Transfer, Some(token_name));
202                if !from_account.is_anonymous() {
203                    message.add_account("From", from_account.to_string());
204                }
205                message.add_amount(self.amount, self.decimals, &token_symbol)?;
206                message.add_account("To", receiver_account.to_string());
207                message.add_fee(
208                    Icrc21Function::Transfer,
209                    self.ledger_fee,
210                    self.decimals,
211                    &token_symbol,
212                )?;
213            }
214            Icrc21Function::Approve => {
215                let approver_account = self.approver.ok_or(Icrc21Error::GenericError {
216                    error_code: Nat::from(500u64),
217                    description: "Approver account has to be specified.".to_owned(),
218                })?;
219                let spender_account = self.spender.ok_or(Icrc21Error::GenericError {
220                    error_code: Nat::from(500u64),
221                    description: "Spender account has to be specified.".to_owned(),
222                })?;
223                let token_symbol = self.token_symbol.ok_or(Icrc21Error::GenericError {
224                    error_code: Nat::from(500u64),
225                    description: "Token symbol must be specified.".to_owned(),
226                })?;
227
228                message.add_intent(Icrc21Function::Approve, None);
229                if approver_account.owner != Principal::anonymous() {
230                    message.add_account("From", approver_account.to_string());
231                }
232                message.add_account("Approve to spender", spender_account.to_string());
233                message.add_allowance(self.amount, self.decimals, &token_symbol)?;
234                if let Some(expected_allowance) = self.expected_allowance {
235                    message.add_existing_allowance(
236                        expected_allowance,
237                        self.decimals,
238                        &token_symbol,
239                    )?;
240                }
241                message.add_expiration(self.expires_at, self.utc_offset_minutes);
242                message.add_fee(
243                    Icrc21Function::Approve,
244                    self.ledger_fee,
245                    self.decimals,
246                    &token_symbol,
247                )?;
248                if approver_account.owner != Principal::anonymous() {
249                    message.add_account("Fees paid by", approver_account.to_string());
250                }
251            }
252            Icrc21Function::TransferFrom => {
253                let from_account = self.from.ok_or(Icrc21Error::GenericError {
254                    error_code: Nat::from(500u64),
255                    description: "From account has to be specified.".to_owned(),
256                })?;
257                let receiver_account = self.receiver.ok_or(Icrc21Error::GenericError {
258                    error_code: Nat::from(500u64),
259                    description: "Receiver account has to be specified.".to_owned(),
260                })?;
261                let spender_account = self.spender.ok_or(Icrc21Error::GenericError {
262                    error_code: Nat::from(500u64),
263                    description: "Spender account has to be specified.".to_owned(),
264                })?;
265
266                let token_symbol = self.token_symbol.ok_or(Icrc21Error::GenericError {
267                    error_code: Nat::from(500u64),
268                    description: "Token symbol must be specified.".to_owned(),
269                })?;
270                let token_name = self.token_name.ok_or(Icrc21Error::GenericError {
271                    error_code: Nat::from(500u64),
272                    description: "Token Name must be specified.".to_owned(),
273                })?;
274                message.add_intent(Icrc21Function::TransferFrom, Some(token_name));
275                message.add_account("From", from_account.to_string());
276                message.add_amount(self.amount, self.decimals, &token_symbol)?;
277                if spender_account.owner != Principal::anonymous() {
278                    message.add_account("Spender", spender_account.to_string());
279                }
280                message.add_account("To", receiver_account.to_string());
281                message.add_fee(
282                    Icrc21Function::TransferFrom,
283                    self.ledger_fee,
284                    self.decimals,
285                    &token_symbol,
286                )?;
287            }
288        };
289
290        if let Some(memo) = self.memo {
291            message.add_memo(memo);
292        }
293
294        Ok(message)
295    }
296}
297
298pub struct GenericTransferArgs {
299    pub from: AccountOrId,
300    pub receiver: AccountOrId,
301    pub amount: Nat,
302    pub memo: Option<GenericMemo>,
303}
304
305pub fn build_icrc21_consent_info_for_icrc1_and_icrc2_endpoints(
306    consent_msg_request: ConsentMessageRequest,
307    caller_principal: Principal,
308    ledger_fee: Nat,
309    token_symbol: String,
310    token_name: String,
311    decimals: u8,
312) -> Result<ConsentInfo, Icrc21Error> {
313    build_icrc21_consent_info(
314        consent_msg_request,
315        caller_principal,
316        ledger_fee,
317        token_symbol,
318        token_name,
319        decimals,
320        None,
321    )
322}
323
324pub fn build_icrc21_consent_info(
325    consent_msg_request: ConsentMessageRequest,
326    caller_principal: Principal,
327    ledger_fee: Nat,
328    token_symbol: String,
329    token_name: String,
330    decimals: u8,
331    transfer_args: Option<GenericTransferArgs>,
332) -> Result<ConsentInfo, Icrc21Error> {
333    if consent_msg_request.arg.len() > MAX_CONSENT_MESSAGE_ARG_SIZE_BYTES as usize {
334        return Err(Icrc21Error::UnsupportedCanisterCall(ErrorInfo {
335            description: format!(
336                "The argument size is too large. The maximum allowed size is {MAX_CONSENT_MESSAGE_ARG_SIZE_BYTES} bytes."
337            ),
338        }));
339    }
340
341    // for now, respond in English regardless of what the client requested
342    let metadata = ConsentMessageMetadata {
343        language: "en".to_string(),
344        utc_offset_minutes: consent_msg_request
345            .user_preferences
346            .metadata
347            .utc_offset_minutes,
348    };
349
350    let mut display_message_builder =
351        ConsentMessageBuilder::new(&consent_msg_request.method, decimals)?
352            .with_ledger_fee(ledger_fee.clone())
353            .with_token_symbol(token_symbol)
354            .with_token_name(token_name);
355
356    if let Some(offset) = consent_msg_request
357        .user_preferences
358        .metadata
359        .utc_offset_minutes
360    {
361        display_message_builder = display_message_builder.with_utc_offset_minutes(offset);
362    }
363
364    if let Some(display_type) = consent_msg_request.user_preferences.device_spec {
365        display_message_builder = display_message_builder.with_display_type(display_type);
366    }
367
368    let consent_message = match display_message_builder.function {
369        Icrc21Function::Transfer => {
370            let TransferArg {
371                memo,
372                amount,
373                from_subaccount,
374                to,
375                fee,
376                created_at_time: _,
377            } = Decode!(&consent_msg_request.arg, TransferArg).map_err(|e| {
378                Icrc21Error::UnsupportedCanisterCall(ErrorInfo {
379                    description: format!("Failed to decode TransferArg: {e}"),
380                })
381            })?;
382            icrc21_check_fee(&fee, &ledger_fee)?;
383            let sender = Account {
384                owner: caller_principal,
385                subaccount: from_subaccount,
386            };
387            display_message_builder = display_message_builder
388                .with_amount(amount)
389                .with_receiver_account(AccountOrId::Account(to))
390                .with_from_account(AccountOrId::Account(sender));
391
392            if let Some(memo) = memo {
393                display_message_builder =
394                    display_message_builder.with_memo(GenericMemo::Icrc1Memo(memo.0));
395            }
396            display_message_builder.build()
397        }
398        Icrc21Function::TransferFrom => {
399            let TransferFromArgs {
400                memo,
401                amount,
402                from,
403                to,
404                spender_subaccount,
405                fee,
406                created_at_time: _,
407            } = Decode!(&consent_msg_request.arg, TransferFromArgs).map_err(|e| {
408                Icrc21Error::UnsupportedCanisterCall(ErrorInfo {
409                    description: format!("Failed to decode TransferFromArgs: {e}"),
410                })
411            })?;
412            icrc21_check_fee(&fee, &ledger_fee)?;
413            let spender = Account {
414                owner: caller_principal,
415                subaccount: spender_subaccount,
416            };
417            display_message_builder = display_message_builder
418                .with_amount(amount)
419                .with_receiver_account(AccountOrId::Account(to))
420                .with_from_account(AccountOrId::Account(from))
421                .with_spender_account(spender);
422
423            if let Some(memo) = memo {
424                display_message_builder =
425                    display_message_builder.with_memo(GenericMemo::Icrc1Memo(memo.0));
426            }
427            display_message_builder.build()
428        }
429        Icrc21Function::Approve => {
430            let ApproveArgs {
431                memo,
432                amount,
433                from_subaccount,
434                spender,
435                expires_at,
436                expected_allowance,
437                fee,
438                created_at_time: _,
439            } = Decode!(&consent_msg_request.arg, ApproveArgs).map_err(|e| {
440                Icrc21Error::UnsupportedCanisterCall(ErrorInfo {
441                    description: format!("Failed to decode ApproveArgs: {e}"),
442                })
443            })?;
444            icrc21_check_fee(&fee, &ledger_fee)?;
445            let approver = Account {
446                owner: caller_principal,
447                subaccount: from_subaccount,
448            };
449            let spender = Account {
450                owner: spender.owner,
451                subaccount: spender.subaccount,
452            };
453            display_message_builder = display_message_builder
454                .with_amount(amount)
455                .with_approver_account(approver)
456                .with_spender_account(spender);
457
458            if let Some(memo) = memo {
459                display_message_builder =
460                    display_message_builder.with_memo(GenericMemo::Icrc1Memo(memo.0));
461            }
462            if let Some(expires_at) = expires_at {
463                display_message_builder = display_message_builder.with_expires_at(expires_at);
464            }
465            if let Some(expected_allowance) = expected_allowance {
466                display_message_builder =
467                    display_message_builder.with_expected_allowance(expected_allowance);
468            }
469            display_message_builder.build()
470        }
471        Icrc21Function::GenericTransfer => {
472            let transfer_args = match transfer_args {
473                Some(args) => args,
474                None => {
475                    return Err(Icrc21Error::UnsupportedCanisterCall(ErrorInfo {
476                        description: "transfer args should be provided".to_string(),
477                    }));
478                }
479            };
480            display_message_builder = display_message_builder
481                .with_amount(transfer_args.amount)
482                .with_receiver_account(transfer_args.receiver)
483                .with_from_account(transfer_args.from);
484            if let Some(memo) = transfer_args.memo {
485                display_message_builder = display_message_builder.with_memo(memo);
486            }
487            display_message_builder.build()
488        }
489    }?;
490
491    Ok(ConsentInfo {
492        metadata,
493        consent_message,
494    })
495}
496
497pub fn icrc21_check_fee(fee: &Option<Nat>, ledger_fee: &Nat) -> Result<(), Icrc21Error> {
498    if let Some(fee) = fee
499        && fee != ledger_fee
500    {
501        return Err(Icrc21Error::UnsupportedCanisterCall(ErrorInfo {
502            description: format!(
503                "The fee specified in the arguments ({fee}) is different than the ledger fee ({ledger_fee})"
504            ),
505        }));
506    }
507    Ok(())
508}