1use crate::token::{
2 interface::TokenMint,
3 spl_token_2022_util::{
4 try_parse_account_extension, try_parse_mint_extension, AccountExtension, MintExtension,
5 ParsedExtension,
6 },
7};
8
9use super::interface::{TokenInterface, TokenState};
10use async_trait::async_trait;
11use solana_program::{program_pack::Pack, pubkey::Pubkey};
12use solana_sdk::instruction::Instruction;
13use spl_associated_token_account_interface::{
14 address::get_associated_token_address_with_program_id,
15 instruction::create_associated_token_account,
16};
17use spl_token_2022_interface::{
18 extension::{transfer_fee::TransferFeeConfig, ExtensionType, StateWithExtensions},
19 state::{Account as Token2022AccountState, AccountState, Mint as Token2022MintState},
20};
21use std::{collections::HashMap, fmt::Debug};
22
23#[derive(Debug)]
24pub struct Token2022Account {
25 pub mint: Pubkey,
26 pub owner: Pubkey,
27 pub amount: u64,
28 pub delegate: Option<Pubkey>,
29 pub state: u8,
30 pub is_native: Option<u64>,
31 pub delegated_amount: u64,
32 pub close_authority: Option<Pubkey>,
33 pub extensions_types: Vec<ExtensionType>,
35 pub extensions: HashMap<u16, ParsedExtension>,
37}
38
39impl TokenState for Token2022Account {
40 fn mint(&self) -> Pubkey {
41 self.mint
42 }
43 fn owner(&self) -> Pubkey {
44 self.owner
45 }
46 fn amount(&self) -> u64 {
47 self.amount
48 }
49 fn decimals(&self) -> u8 {
50 0
51 }
52 fn as_any(&self) -> &dyn std::any::Any {
53 self
54 }
55}
56
57impl Token2022Account {
58 pub fn has_memo_extension(&self) -> bool {
62 self.has_extension(ExtensionType::MemoTransfer)
63 }
64
65 pub fn has_immutable_owner_extension(&self) -> bool {
66 self.has_extension(ExtensionType::ImmutableOwner)
67 }
68
69 pub fn has_default_account_state_extension(&self) -> bool {
70 self.has_extension(ExtensionType::DefaultAccountState)
71 }
72}
73
74impl Token2022Extensions for Token2022Account {
75 fn get_extensions(&self) -> &HashMap<u16, ParsedExtension> {
76 &self.extensions
77 }
78
79 fn get_extension_types(&self) -> &Vec<ExtensionType> {
80 &self.extensions_types
81 }
82
83 fn has_confidential_transfer_extension(&self) -> bool {
88 self.has_extension(ExtensionType::ConfidentialTransferAccount)
89 }
90
91 fn has_transfer_hook_extension(&self) -> bool {
92 self.has_extension(ExtensionType::TransferHookAccount)
93 }
94
95 fn has_pausable_extension(&self) -> bool {
96 self.has_extension(ExtensionType::PausableAccount)
97 }
98
99 fn is_non_transferable(&self) -> bool {
100 self.has_extension(ExtensionType::NonTransferableAccount)
101 }
102}
103
104#[derive(Debug)]
105pub struct Token2022Mint {
106 pub mint: Pubkey,
107 pub mint_authority: Option<Pubkey>,
108 pub supply: u64,
109 pub decimals: u8,
110 pub is_initialized: bool,
111 pub freeze_authority: Option<Pubkey>,
112 pub extensions_types: Vec<ExtensionType>,
114 pub extensions: HashMap<u16, ParsedExtension>,
116}
117
118impl TokenMint for Token2022Mint {
119 fn address(&self) -> Pubkey {
120 self.mint
121 }
122
123 fn decimals(&self) -> u8 {
124 self.decimals
125 }
126
127 fn mint_authority(&self) -> Option<Pubkey> {
128 self.mint_authority
129 }
130
131 fn supply(&self) -> u64 {
132 self.supply
133 }
134
135 fn freeze_authority(&self) -> Option<Pubkey> {
136 self.freeze_authority
137 }
138
139 fn is_initialized(&self) -> bool {
140 self.is_initialized
141 }
142
143 fn get_token_program(&self) -> Box<dyn TokenInterface> {
144 Box::new(Token2022Program::new())
145 }
146
147 fn as_any(&self) -> &dyn std::any::Any {
148 self
149 }
150}
151
152impl Token2022Mint {
153 fn get_transfer_fee(&self) -> Option<TransferFeeConfig> {
154 match self.get_extension(ExtensionType::TransferFeeConfig) {
155 Some(ParsedExtension::Mint(MintExtension::TransferFeeConfig(config))) => Some(*config),
156 _ => None,
157 }
158 }
159
160 pub fn calculate_transfer_fee(
163 &self,
164 amount: u64,
165 current_epoch: u64,
166 ) -> Result<Option<u64>, crate::error::KoraError> {
167 if let Some(fee_config) = self.get_transfer_fee() {
168 let transfer_fee = if current_epoch >= u64::from(fee_config.newer_transfer_fee.epoch) {
169 &fee_config.newer_transfer_fee
170 } else {
171 &fee_config.older_transfer_fee
172 };
173
174 let basis_points = u16::from(transfer_fee.transfer_fee_basis_points);
175 let maximum_fee = u64::from(transfer_fee.maximum_fee);
176
177 let fee_amount = (amount as u128)
178 .checked_mul(basis_points as u128)
179 .and_then(|product| product.checked_div(10_000))
180 .and_then(
181 |result| if result <= u64::MAX as u128 { Some(result as u64) } else { None },
182 )
183 .ok_or_else(|| {
184 log::error!(
185 "Transfer fee calculation overflow: amount={}, basis_points={}",
186 amount,
187 basis_points
188 );
189 crate::error::KoraError::ValidationError(format!(
190 "Transfer fee calculation overflow: amount={}, basis_points={}",
191 amount, basis_points
192 ))
193 })?;
194 Ok(Some(std::cmp::min(fee_amount, maximum_fee)))
195 } else {
196 Ok(None)
197 }
198 }
199
200 pub fn has_confidential_mint_burn_extension(&self) -> bool {
201 self.has_extension(ExtensionType::ConfidentialMintBurn)
202 }
203
204 pub fn has_mint_close_authority_extension(&self) -> bool {
205 self.has_extension(ExtensionType::MintCloseAuthority)
206 }
207
208 pub fn has_interest_bearing_extension(&self) -> bool {
209 self.has_extension(ExtensionType::InterestBearingConfig)
210 }
211
212 pub fn has_permanent_delegate_extension(&self) -> bool {
213 self.has_extension(ExtensionType::PermanentDelegate)
214 }
215}
216
217impl Token2022Extensions for Token2022Mint {
218 fn get_extensions(&self) -> &HashMap<u16, ParsedExtension> {
219 &self.extensions
220 }
221
222 fn get_extension_types(&self) -> &Vec<ExtensionType> {
223 &self.extensions_types
224 }
225
226 fn has_confidential_transfer_extension(&self) -> bool {
231 self.has_extension(ExtensionType::ConfidentialTransferMint)
232 }
233
234 fn has_transfer_hook_extension(&self) -> bool {
235 self.has_extension(ExtensionType::TransferHook)
236 }
237
238 fn has_pausable_extension(&self) -> bool {
239 self.has_extension(ExtensionType::Pausable)
240 }
241
242 fn is_non_transferable(&self) -> bool {
243 self.has_extension(ExtensionType::NonTransferable)
244 }
245}
246
247pub struct Token2022Program;
248
249impl Token2022Program {
250 pub fn new() -> Self {
251 Self
252 }
253}
254
255impl Default for Token2022Program {
256 fn default() -> Self {
257 Self::new()
258 }
259}
260
261#[async_trait]
262impl TokenInterface for Token2022Program {
263 fn program_id(&self) -> Pubkey {
264 spl_token_2022_interface::id()
265 }
266
267 fn unpack_token_account(
268 &self,
269 data: &[u8],
270 ) -> Result<Box<dyn TokenState + Send + Sync>, Box<dyn std::error::Error + Send + Sync>> {
271 let account = StateWithExtensions::<Token2022AccountState>::unpack(data)?;
272 let base = account.base;
273
274 let mut extensions = HashMap::new();
276 let mut extensions_types = Vec::new();
277
278 if data.len() > Token2022AccountState::LEN {
279 for &extension_type in AccountExtension::EXTENSIONS {
280 if let Some(parsed_ext) = try_parse_account_extension(&account, extension_type) {
281 extensions.insert(extension_type as u16, parsed_ext);
282 extensions_types.push(extension_type);
283 }
284 }
285 }
286
287 Ok(Box::new(Token2022Account {
288 mint: base.mint,
289 owner: base.owner,
290 amount: base.amount,
291 delegate: base.delegate.into(),
292 state: match base.state {
293 AccountState::Uninitialized => 0,
294 AccountState::Initialized => 1,
295 AccountState::Frozen => 2,
296 },
297 is_native: base.is_native.into(),
298 delegated_amount: base.delegated_amount,
299 close_authority: base.close_authority.into(),
300 extensions_types,
301 extensions,
302 }))
303 }
304
305 fn create_initialize_account_instruction(
306 &self,
307 account: &Pubkey,
308 mint: &Pubkey,
309 owner: &Pubkey,
310 ) -> Result<Instruction, Box<dyn std::error::Error + Send + Sync>> {
311 Ok(spl_token_2022_interface::instruction::initialize_account3(
312 &self.program_id(),
313 account,
314 mint,
315 owner,
316 )?)
317 }
318
319 fn create_transfer_instruction(
320 &self,
321 source: &Pubkey,
322 destination: &Pubkey,
323 authority: &Pubkey,
324 amount: u64,
325 ) -> Result<Instruction, Box<dyn std::error::Error + Send + Sync>> {
326 #[allow(deprecated)]
328 Ok(spl_token_2022_interface::instruction::transfer(
329 &self.program_id(),
330 source,
331 destination,
332 authority,
333 &[],
334 amount,
335 )?)
336 }
337
338 fn create_transfer_checked_instruction(
339 &self,
340 source: &Pubkey,
341 mint: &Pubkey,
342 destination: &Pubkey,
343 authority: &Pubkey,
344 amount: u64,
345 decimals: u8,
346 ) -> Result<Instruction, Box<dyn std::error::Error + Send + Sync>> {
347 Ok(spl_token_2022_interface::instruction::transfer_checked(
348 &self.program_id(),
349 source,
350 mint,
351 destination,
352 authority,
353 &[],
354 amount,
355 decimals,
356 )?)
357 }
358
359 fn get_associated_token_address(&self, wallet: &Pubkey, mint: &Pubkey) -> Pubkey {
360 get_associated_token_address_with_program_id(wallet, mint, &self.program_id())
361 }
362
363 fn create_associated_token_account_instruction(
364 &self,
365 funding_account: &Pubkey,
366 wallet: &Pubkey,
367 mint: &Pubkey,
368 ) -> Instruction {
369 create_associated_token_account(funding_account, wallet, mint, &self.program_id())
370 }
371
372 fn unpack_mint(
373 &self,
374 mint: &Pubkey,
375 mint_data: &[u8],
376 ) -> Result<Box<dyn TokenMint + Send + Sync>, Box<dyn std::error::Error + Send + Sync>> {
377 let mint_with_extensions = StateWithExtensions::<Token2022MintState>::unpack(mint_data)?;
378 let base = mint_with_extensions.base;
379
380 let mut extensions = HashMap::new();
382 let mut extensions_types = Vec::new();
383
384 if mint_data.len() > Token2022MintState::LEN {
385 for &extension_type in MintExtension::EXTENSIONS {
386 if let Some(parsed_ext) =
387 try_parse_mint_extension(&mint_with_extensions, extension_type)
388 {
389 extensions.insert(extension_type as u16, parsed_ext);
390 extensions_types.push(extension_type);
391 }
392 }
393 }
394
395 Ok(Box::new(Token2022Mint {
396 mint: *mint,
397 mint_authority: base.mint_authority.into(),
398 supply: base.supply,
399 decimals: base.decimals,
400 is_initialized: base.is_initialized,
401 freeze_authority: base.freeze_authority.into(),
402 extensions_types,
403 extensions,
404 }))
405 }
406}
407
408pub trait Token2022Extensions {
410 fn get_extensions(&self) -> &HashMap<u16, ParsedExtension>;
412
413 fn get_extension_types(&self) -> &Vec<ExtensionType>;
415
416 fn extension_key(ext_type: ExtensionType) -> u16 {
418 ext_type as u16
419 }
420
421 fn has_extension(&self, extension_type: ExtensionType) -> bool {
423 self.get_extension_types().contains(&extension_type)
424 }
425
426 fn get_extension(&self, extension_type: ExtensionType) -> Option<&ParsedExtension> {
428 self.get_extensions().get(&Self::extension_key(extension_type))
429 }
430
431 fn has_confidential_transfer_extension(&self) -> bool;
432
433 fn has_transfer_hook_extension(&self) -> bool;
434
435 fn has_pausable_extension(&self) -> bool;
436
437 fn is_non_transferable(&self) -> bool;
439}
440
441#[cfg(test)]
442mod tests {
443 use crate::tests::common::{
444 create_transfer_fee_config, MintAccountMockBuilder, TokenAccountMockBuilder,
445 };
446
447 use super::*;
448 use solana_sdk::pubkey::Pubkey;
449 use spl_pod::{
450 optional_keys::OptionalNonZeroPubkey,
451 primitives::{PodU16, PodU64},
452 };
453 use spl_token_2022_interface::extension::{
454 transfer_fee::{TransferFee, TransferFeeConfig},
455 ExtensionType,
456 };
457
458 pub fn create_test_extensions() -> HashMap<u16, ParsedExtension> {
459 let mut extensions = HashMap::new();
460 extensions.insert(
461 ExtensionType::TransferFeeConfig as u16,
462 ParsedExtension::Mint(MintExtension::TransferFeeConfig(create_transfer_fee_config(
463 100, 1000,
464 ))),
465 );
466 extensions
467 }
468
469 #[test]
470 fn test_token_program_token2022() {
471 let program = Token2022Program::new();
472 assert_eq!(program.program_id(), spl_token_2022_interface::id());
473 }
474
475 #[test]
476 fn test_token2022_program_creation() {
477 let program = Token2022Program::new();
478 assert_eq!(program.program_id(), spl_token_2022_interface::id());
479 }
480
481 #[test]
482 fn test_token2022_account_state() {
483 let mint = Pubkey::new_unique();
484 let owner = Pubkey::new_unique();
485 let amount = 1000;
486
487 let account = Token2022Account {
489 mint,
490 owner,
491 amount,
492 delegate: None,
493 state: 1, is_native: None,
495 delegated_amount: 0,
496 close_authority: None,
497 extensions_types: vec![ExtensionType::TransferFeeConfig],
498 extensions: create_test_extensions(),
499 };
500
501 assert_eq!(account.mint(), mint);
503 assert_eq!(account.owner(), owner);
504 assert_eq!(account.amount(), amount);
505
506 assert!(!account.extensions.is_empty());
508 }
509
510 #[test]
511 fn test_token2022_transfer_instruction() {
512 let source = Pubkey::new_unique();
513 let dest = Pubkey::new_unique();
514 let authority = Pubkey::new_unique();
515 let amount = 100;
516
517 let program = Token2022Program::new();
519 let ix = program.create_transfer_instruction(&source, &dest, &authority, amount).unwrap();
520
521 assert_eq!(ix.program_id, spl_token_2022_interface::id());
522 assert_eq!(ix.accounts[0].pubkey, source);
524 assert_eq!(ix.accounts[1].pubkey, dest);
525 assert_eq!(ix.accounts[2].pubkey, authority);
526 }
527
528 #[test]
529 fn test_token2022_transfer_checked_instruction() {
530 let source = Pubkey::new_unique();
531 let dest = Pubkey::new_unique();
532 let mint = Pubkey::new_unique();
533 let authority = Pubkey::new_unique();
534 let amount = 100;
535 let decimals = 9;
536
537 let program = Token2022Program::new();
538 let ix = program
539 .create_transfer_checked_instruction(
540 &source, &mint, &dest, &authority, amount, decimals,
541 )
542 .unwrap();
543
544 assert_eq!(ix.program_id, spl_token_2022_interface::id());
545 assert_eq!(ix.accounts[0].pubkey, source);
547 assert_eq!(ix.accounts[1].pubkey, mint);
548 assert_eq!(ix.accounts[2].pubkey, dest);
549 assert_eq!(ix.accounts[3].pubkey, authority);
550 }
551
552 #[test]
553 fn test_token2022_ata_derivation() {
554 let program = Token2022Program::new();
555 let wallet = Pubkey::new_unique();
556 let mint = Pubkey::new_unique();
557
558 let ata = program.get_associated_token_address(&wallet, &mint);
559
560 let expected_ata =
562 spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
563 &wallet,
564 &mint,
565 &spl_token_2022_interface::id(),
566 );
567 assert_eq!(ata, expected_ata);
568 }
569
570 #[test]
571 fn test_token2022_account_state_extensions() {
572 let owner = Pubkey::new_unique();
573 let mint = Pubkey::new_unique();
574 let amount = 1000;
575
576 let token_account = TokenAccountMockBuilder::new()
577 .with_mint(&mint)
578 .with_owner(&owner)
579 .with_amount(amount)
580 .build_as_custom_token2022_token_account(HashMap::new());
581
582 assert!(!token_account.has_extension(ExtensionType::TransferFeeConfig));
584 assert!(!token_account.has_extension(ExtensionType::NonTransferableAccount));
585 assert!(!token_account.has_extension(ExtensionType::CpiGuard));
586 }
587
588 #[test]
589 fn test_token2022_extension_support() {
590 let mint = Pubkey::new_unique();
591 let owner = Pubkey::new_unique();
592 let amount = 1000;
593
594 let token_account = TokenAccountMockBuilder::new()
595 .with_mint(&mint)
596 .with_owner(&owner)
597 .with_amount(amount)
598 .build_as_custom_token2022_token_account(create_test_extensions());
599
600 assert_eq!(token_account.mint(), mint);
601 assert_eq!(token_account.owner(), owner);
602 assert_eq!(token_account.amount(), amount);
603
604 assert!(!token_account.extensions.is_empty());
605 }
606
607 #[test]
608 fn test_token2022_mint_transfer_fee_edge_cases() {
609 let mint_pubkey = Pubkey::new_unique();
610
611 let mint = MintAccountMockBuilder::new()
612 .build_as_custom_token2022_mint(mint_pubkey, HashMap::new());
613
614 let fee = mint.calculate_transfer_fee(1000, 0).unwrap();
615 assert!(fee.is_none(), "Mint without transfer fee config should return None");
616
617 let mint = MintAccountMockBuilder::new()
618 .build_as_custom_token2022_mint(mint_pubkey, create_test_extensions());
619
620 let zero_fee = mint.calculate_transfer_fee(0, 0).unwrap();
622 assert!(zero_fee.is_some());
623 assert_eq!(zero_fee.unwrap(), 0, "Zero amount should result in zero fee");
624
625 let large_amount_fee = mint.calculate_transfer_fee(1_000_000, 0).unwrap();
627 assert!(large_amount_fee.is_some());
628 assert_eq!(large_amount_fee.unwrap(), 1000, "Large amount should be capped at maximum fee");
629 }
630
631 #[test]
632 fn test_token2022_mint_specific_extensions() {
633 let mint_pubkey = Pubkey::new_unique();
634 let mint = Token2022Mint {
635 mint: mint_pubkey,
636 mint_authority: None,
637 supply: 0,
638 decimals: 6,
639 is_initialized: true,
640 freeze_authority: None,
641 extensions_types: vec![
642 ExtensionType::InterestBearingConfig,
643 ExtensionType::PermanentDelegate,
644 ExtensionType::MintCloseAuthority,
645 ],
646 extensions: HashMap::new(), };
648
649 assert!(mint.has_interest_bearing_extension());
650 assert!(mint.has_permanent_delegate_extension());
651 assert!(mint.has_mint_close_authority_extension());
652 assert!(!mint.has_confidential_mint_burn_extension());
653 }
654
655 #[test]
656 fn test_token2022_account_extension_methods() {
657 let account = TokenAccountMockBuilder::new()
658 .with_extensions(vec![
659 ExtensionType::MemoTransfer,
660 ExtensionType::ImmutableOwner,
661 ExtensionType::DefaultAccountState,
662 ExtensionType::ConfidentialTransferAccount,
663 ExtensionType::TransferHookAccount,
664 ExtensionType::PausableAccount,
665 ])
666 .build_as_custom_token2022_token_account(HashMap::new());
667
668 assert!(account.has_memo_extension());
670 assert!(account.has_immutable_owner_extension());
671 assert!(account.has_default_account_state_extension());
672 assert!(account.has_confidential_transfer_extension());
673 assert!(account.has_transfer_hook_extension());
674 assert!(account.has_pausable_extension());
675
676 assert!(!account.is_non_transferable());
678 }
679
680 #[test]
681 fn test_token2022_mint_transfer_fee_calculation_with_fee() {
682 let mint_pubkey = Pubkey::new_unique();
683 let mut extensions = HashMap::new();
684 extensions.insert(
685 ExtensionType::TransferFeeConfig as u16,
686 ParsedExtension::Mint(MintExtension::TransferFeeConfig(
687 crate::tests::account_mock::create_transfer_fee_config(250, 1000),
688 )), );
690
691 let mint = MintAccountMockBuilder::new()
692 .with_extensions(vec![ExtensionType::TransferFeeConfig])
693 .build_as_custom_token2022_mint(mint_pubkey, extensions);
694
695 let test_cases = vec![
697 (10000, 250), (100000, 1000), (1000, 25), (0, 0), ];
702
703 for (amount, _expected_adjusted) in test_cases {
704 let expected_fee = mint.calculate_transfer_fee(amount, 0).unwrap().unwrap_or(0);
705 let expected_result = amount.saturating_sub(expected_fee);
706 assert_eq!(expected_result, expected_result);
707 }
708 }
709
710 #[test]
711 fn test_token2022_mint_transfer_fee_epoch_handling() {
712 let mint_pubkey = Pubkey::new_unique();
713
714 let transfer_fee_config = TransferFeeConfig {
716 transfer_fee_config_authority: OptionalNonZeroPubkey::try_from(Some(
717 spl_pod::solana_pubkey::Pubkey::new_unique(),
718 ))
719 .unwrap(),
720 withdraw_withheld_authority: OptionalNonZeroPubkey::try_from(Some(
721 spl_pod::solana_pubkey::Pubkey::new_unique(),
722 ))
723 .unwrap(),
724 withheld_amount: PodU64::from(0),
725 older_transfer_fee: TransferFee {
726 epoch: PodU64::from(0),
727 transfer_fee_basis_points: PodU16::from(100), maximum_fee: PodU64::from(500),
729 },
730 newer_transfer_fee: TransferFee {
731 epoch: PodU64::from(10),
732 transfer_fee_basis_points: PodU16::from(200), maximum_fee: PodU64::from(1000),
734 },
735 };
736
737 let mut extensions = HashMap::new();
738 extensions.insert(
739 ExtensionType::TransferFeeConfig as u16,
740 ParsedExtension::Mint(MintExtension::TransferFeeConfig(transfer_fee_config)),
741 );
742
743 let mint = MintAccountMockBuilder::new()
744 .with_extensions(vec![ExtensionType::TransferFeeConfig])
745 .build_as_custom_token2022_mint(mint_pubkey, extensions);
746
747 let fee_old = mint.calculate_transfer_fee(10000, 5).unwrap().unwrap();
749 assert_eq!(fee_old, 100); let fee_new = mint.calculate_transfer_fee(10000, 15).unwrap().unwrap();
753 assert_eq!(fee_new, 200); }
755
756 #[test]
757 fn test_token2022_mint_all_extension_methods() {
758 let mint = MintAccountMockBuilder::new()
759 .with_extensions(vec![
760 ExtensionType::InterestBearingConfig,
761 ExtensionType::PermanentDelegate,
762 ExtensionType::MintCloseAuthority,
763 ExtensionType::ConfidentialMintBurn,
764 ExtensionType::ConfidentialTransferMint,
765 ExtensionType::TransferHook,
766 ExtensionType::Pausable,
767 ])
768 .build_as_custom_token2022_mint(Pubkey::new_unique(), HashMap::new());
769
770 assert!(mint.has_interest_bearing_extension());
772 assert!(mint.has_permanent_delegate_extension());
773 assert!(mint.has_mint_close_authority_extension());
774 assert!(mint.has_confidential_mint_burn_extension());
775 assert!(mint.has_confidential_transfer_extension());
776 assert!(mint.has_transfer_hook_extension());
777 assert!(mint.has_pausable_extension());
778
779 assert!(!mint.is_non_transferable());
781 }
782}