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
18pub 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 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}