pub mod extensions;
pub use extensions::{AccountState, MintExtension, TransferFee};
use crate::error::{Error, Result};
use crate::pubkey::{ids, Pubkey};
use crate::rpc::Account;
pub const MINT_LEN: usize = 82;
pub const TOKEN_ACCOUNT_LEN: usize = 165;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenProgram {
Legacy,
Token2022,
}
impl TokenProgram {
pub const fn id(self) -> Pubkey {
match self {
TokenProgram::Legacy => ids::TOKEN_PROGRAM,
TokenProgram::Token2022 => ids::TOKEN_2022_PROGRAM,
}
}
pub fn from_owner(owner: &Pubkey) -> Option<Self> {
if *owner == ids::TOKEN_PROGRAM {
Some(TokenProgram::Legacy)
} else if *owner == ids::TOKEN_2022_PROGRAM {
Some(TokenProgram::Token2022)
} else {
None
}
}
pub const fn name(self) -> &'static str {
match self {
TokenProgram::Legacy => "spl-token",
TokenProgram::Token2022 => "token-2022",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Mint {
pub mint_authority: Option<Pubkey>,
pub supply: u64,
pub decimals: u8,
pub is_initialized: bool,
pub freeze_authority: Option<Pubkey>,
}
impl Mint {
pub fn unpack(data: &[u8]) -> Result<Self> {
if data.len() < MINT_LEN {
return Err(Error::InvalidAccountData(format!(
"mint is {} bytes, expected at least {MINT_LEN}",
data.len()
)));
}
Ok(Mint {
mint_authority: coption_pubkey(&data[0..36])?,
supply: u64::from_le_bytes(data[36..44].try_into().unwrap()),
decimals: data[44],
is_initialized: data[45] != 0,
freeze_authority: coption_pubkey(&data[46..82])?,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MintState {
pub address: Pubkey,
pub program: TokenProgram,
pub mint: Mint,
pub extensions: Vec<MintExtension>,
}
impl MintState {
pub fn parse(address: Pubkey, account: &Account) -> Result<Self> {
let program = TokenProgram::from_owner(&account.owner).ok_or_else(|| {
Error::InvalidAccountData(format!(
"{} is owned by {}, which is not a token program",
address.abbreviated(),
account.owner.abbreviated()
))
})?;
let mint = Mint::unpack(&account.data)?;
let extensions = match program {
TokenProgram::Legacy => Vec::new(),
TokenProgram::Token2022 => extensions::parse_mint_extensions(&account.data)?,
};
Ok(MintState {
address,
program,
mint,
extensions,
})
}
pub fn extension(&self, kind: u16) -> Option<&MintExtension> {
self.extensions.iter().find(|e| e.kind() == kind)
}
pub fn permanent_delegate(&self) -> Option<Pubkey> {
match self.extension(12) {
Some(MintExtension::PermanentDelegate { delegate }) => *delegate,
_ => None,
}
}
pub fn transfer_hook_program(&self) -> Option<Pubkey> {
match self.extension(14) {
Some(MintExtension::TransferHook { program_id, .. }) => *program_id,
_ => None,
}
}
pub fn is_non_transferable(&self) -> bool {
self.extension(9).is_some()
}
pub fn defaults_to_frozen(&self) -> bool {
matches!(
self.extension(6),
Some(MintExtension::DefaultAccountState {
state: AccountState::Frozen
})
)
}
pub fn is_paused(&self) -> bool {
matches!(
self.extension(26),
Some(MintExtension::PausableConfig { paused: true, .. })
)
}
pub fn transfer_fee_bps(&self) -> Option<u16> {
match self.extension(1) {
Some(MintExtension::TransferFeeConfig { newer, .. }) => Some(newer.basis_points),
_ => None,
}
}
pub fn ui_supply(&self) -> String {
crate::shape::ui_amount(self.mint.supply as u128, self.mint.decimals)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TokenAccount {
pub mint: Pubkey,
pub owner: Pubkey,
pub amount: u64,
pub delegate: Option<Pubkey>,
pub state: AccountState,
pub is_native: bool,
pub delegated_amount: u64,
pub close_authority: Option<Pubkey>,
}
impl TokenAccount {
pub fn unpack(data: &[u8]) -> Result<Self> {
if data.len() < TOKEN_ACCOUNT_LEN {
return Err(Error::InvalidAccountData(format!(
"token account is {} bytes, expected at least {TOKEN_ACCOUNT_LEN}",
data.len()
)));
}
Ok(TokenAccount {
mint: pubkey_at(&data[0..32]),
owner: pubkey_at(&data[32..64]),
amount: u64::from_le_bytes(data[64..72].try_into().unwrap()),
delegate: coption_pubkey(&data[72..108])?,
state: AccountState::from(data[108]),
is_native: u32::from_le_bytes(data[109..113].try_into().unwrap()) == 1,
delegated_amount: u64::from_le_bytes(data[121..129].try_into().unwrap()),
close_authority: coption_pubkey(&data[129..165])?,
})
}
pub fn is_frozen(&self) -> bool {
self.state == AccountState::Frozen
}
}
pub fn associated_token_address(
owner: &Pubkey,
mint: &Pubkey,
program: TokenProgram,
) -> Result<Pubkey> {
let program_id = program.id();
let (address, _bump) = Pubkey::find_program_address(
&[owner.as_bytes(), program_id.as_bytes(), mint.as_bytes()],
&ids::ASSOCIATED_TOKEN_PROGRAM,
)?;
Ok(address)
}
fn pubkey_at(b: &[u8]) -> Pubkey {
let mut a = [0u8; 32];
a.copy_from_slice(&b[..32]);
Pubkey::new_from_array(a)
}
fn coption_pubkey(b: &[u8]) -> Result<Option<Pubkey>> {
if b.len() < 36 {
return Err(Error::InvalidAccountData("truncated COption".into()));
}
match u32::from_le_bytes(b[0..4].try_into().unwrap()) {
0 => Ok(None),
1 => Ok(Some(pubkey_at(&b[4..36]))),
other => Err(Error::InvalidAccountData(format!(
"COption discriminant {other} is neither 0 nor 1"
))),
}
}