use crate::{error::Result, TallyError};
use anchor_client::solana_client::rpc_client::RpcClient;
use anchor_client::solana_sdk::commitment_config::CommitmentConfig;
use anchor_client::solana_sdk::pubkey::Pubkey;
use anchor_client::solana_sdk::{account::Account, program_pack::Pack};
use spl_associated_token_account::get_associated_token_address;
use spl_token::state::{Account as TokenAccount, Mint};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenProgram {
Token,
Token2022,
}
impl TokenProgram {
#[must_use]
pub const fn program_id(&self) -> Pubkey {
match self {
Self::Token => spl_token::id(),
Self::Token2022 => spl_token_2022::id(),
}
}
}
pub fn get_associated_token_address_for_mint(wallet: &Pubkey, mint: &Pubkey) -> Result<Pubkey> {
Ok(get_associated_token_address(wallet, mint))
}
pub fn get_associated_token_address_with_program(
wallet: &Pubkey,
mint: &Pubkey,
token_program: TokenProgram,
) -> Result<Pubkey> {
Ok(
spl_associated_token_account::get_associated_token_address_with_program_id(
wallet,
mint,
&token_program.program_id(),
),
)
}
pub fn detect_token_program(rpc_client: &RpcClient, mint: &Pubkey) -> Result<TokenProgram> {
let account = rpc_client
.get_account_with_commitment(mint, CommitmentConfig::confirmed())
.map_err(|e| TallyError::Generic(format!("Failed to fetch mint account: {e}")))?
.value
.ok_or_else(|| TallyError::AccountNotFound(mint.to_string()))?;
if account.owner == spl_token::id() {
Ok(TokenProgram::Token)
} else if account.owner == spl_token_2022::id() {
Ok(TokenProgram::Token2022)
} else {
Err(TallyError::TokenProgramDetectionFailed {
mint: mint.to_string(),
})
}
}
pub fn parse_mint_account(account: &Account) -> Result<Mint> {
Mint::unpack(&account.data)
.map_err(|e| TallyError::Generic(format!("Failed to parse SPL Token mint: {e}")))
}
pub fn parse_token_account(account: &Account) -> Result<TokenAccount> {
TokenAccount::unpack(&account.data)
.map_err(|e| TallyError::Generic(format!("Failed to parse SPL Token account: {e}")))
}
pub fn get_ata_with_program_detection(
rpc_client: &RpcClient,
wallet: &Pubkey,
mint: &Pubkey,
) -> Result<(Pubkey, TokenProgram)> {
let token_program = detect_token_program(rpc_client, mint)?;
let ata = get_associated_token_address_with_program(wallet, mint, token_program)?;
Ok((ata, token_program))
}
pub fn get_token_account_info(
rpc_client: &RpcClient,
token_account: &Pubkey,
) -> Result<Option<(TokenAccount, TokenProgram)>> {
let Some(account) = rpc_client
.get_account_with_commitment(token_account, CommitmentConfig::confirmed())
.map_err(|e| TallyError::Generic(format!("Failed to fetch token account: {e}")))?
.value
else {
return Ok(None);
};
let token_program = if account.owner == spl_token::id() {
TokenProgram::Token
} else if account.owner == spl_token_2022::id() {
TokenProgram::Token2022
} else {
return Err(TallyError::InvalidTokenProgram {
expected: "SPL Token or Token-2022".to_string(),
found: account.owner.to_string(),
});
};
if token_program == TokenProgram::Token {
let token_account_data = parse_token_account(&account)?;
Ok(Some((token_account_data, token_program)))
} else {
Err(TallyError::Generic(
"Token-2022 account parsing not yet implemented".to_string(),
))
}
}
pub fn create_associated_token_account_instruction(
payer: &Pubkey,
wallet: &Pubkey,
mint: &Pubkey,
token_program: TokenProgram,
) -> Result<anchor_client::solana_sdk::instruction::Instruction> {
Ok(
spl_associated_token_account::instruction::create_associated_token_account(
payer,
wallet,
mint,
&token_program.program_id(),
),
)
}
#[cfg(test)]
mod tests {
use super::*;
use anchor_client::solana_sdk::signature::{Keypair, Signer};
use std::str::FromStr;
#[test]
fn test_token_program_program_id() {
assert_eq!(TokenProgram::Token.program_id(), spl_token::id());
assert_eq!(TokenProgram::Token2022.program_id(), spl_token_2022::id());
}
#[test]
fn test_get_associated_token_address() {
let wallet = Pubkey::from(Keypair::new().pubkey().to_bytes());
let mint = Pubkey::from(Keypair::new().pubkey().to_bytes());
let ata = get_associated_token_address_for_mint(&wallet, &mint).unwrap();
let ata2 = get_associated_token_address_for_mint(&wallet, &mint).unwrap();
assert_eq!(ata, ata2);
let wallet2 = Pubkey::from(Keypair::new().pubkey().to_bytes());
let ata3 = get_associated_token_address_for_mint(&wallet2, &mint).unwrap();
assert_ne!(ata, ata3);
}
#[test]
fn test_get_associated_token_address_with_program() {
let wallet = Pubkey::from(Keypair::new().pubkey().to_bytes());
let mint = Pubkey::from(Keypair::new().pubkey().to_bytes());
let ata_token =
get_associated_token_address_with_program(&wallet, &mint, TokenProgram::Token).unwrap();
let ata_token2022 =
get_associated_token_address_with_program(&wallet, &mint, TokenProgram::Token2022)
.unwrap();
assert_ne!(ata_token, ata_token2022);
}
#[test]
fn test_usdc_mainnet_ata() {
let usdc_mint = Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap();
let wallet = Pubkey::from(Keypair::new().pubkey().to_bytes());
let ata = get_associated_token_address_for_mint(&wallet, &usdc_mint).unwrap();
assert!(ata != Pubkey::default());
let ata_token =
get_associated_token_address_with_program(&wallet, &usdc_mint, TokenProgram::Token)
.unwrap();
assert_eq!(ata, ata_token);
}
#[test]
fn test_create_ata_instruction() {
let payer = Pubkey::from(Keypair::new().pubkey().to_bytes());
let wallet = Pubkey::from(Keypair::new().pubkey().to_bytes());
let mint = Pubkey::from(Keypair::new().pubkey().to_bytes());
let ix_token = create_associated_token_account_instruction(
&payer,
&wallet,
&mint,
TokenProgram::Token,
)
.unwrap();
let ix_token2022 = create_associated_token_account_instruction(
&payer,
&wallet,
&mint,
TokenProgram::Token2022,
)
.unwrap();
assert_eq!(ix_token.program_id, spl_associated_token_account::id());
assert_eq!(ix_token2022.program_id, spl_associated_token_account::id());
assert!(!ix_token.accounts.is_empty());
assert!(!ix_token2022.accounts.is_empty());
}
}