use crate::{error::ProgramError, result::ProgramResult, address::Address, account::AccountView};
pub const EXT_UNINITIALIZED: u16 = 0;
pub const EXT_TRANSFER_FEE_CONFIG: u16 = 1;
pub const EXT_TRANSFER_FEE_AMOUNT: u16 = 2;
pub const EXT_MINT_CLOSE_AUTHORITY: u16 = 3;
pub const EXT_CONFIDENTIAL_TRANSFER_MINT: u16 = 4;
pub const EXT_CONFIDENTIAL_TRANSFER_ACCOUNT: u16 = 5;
pub const EXT_DEFAULT_ACCOUNT_STATE: u16 = 6;
pub const EXT_IMMUTABLE_OWNER: u16 = 7;
pub const EXT_MEMO_TRANSFER: u16 = 8;
pub const EXT_NON_TRANSFERABLE: u16 = 9;
pub const EXT_INTEREST_BEARING_CONFIG: u16 = 10;
pub const EXT_CPI_GUARD: u16 = 11;
pub const EXT_PERMANENT_DELEGATE: u16 = 12;
pub const EXT_NON_TRANSFERABLE_ACCOUNT: u16 = 13;
pub const EXT_TRANSFER_HOOK: u16 = 14;
pub const EXT_TRANSFER_HOOK_ACCOUNT: u16 = 15;
pub const EXT_CONFIDENTIAL_TRANSFER_FEE_CONFIG: u16 = 16;
pub const EXT_CONFIDENTIAL_TRANSFER_FEE_AMOUNT: u16 = 17;
pub const EXT_METADATA_POINTER: u16 = 18;
pub const EXT_TOKEN_METADATA: u16 = 19;
pub const EXT_GROUP_POINTER: u16 = 20;
pub const EXT_TOKEN_GROUP: u16 = 21;
pub const EXT_GROUP_MEMBER_POINTER: u16 = 22;
pub const EXT_TOKEN_GROUP_MEMBER: u16 = 23;
pub const EXT_SCALED_UI_AMOUNT_CONFIG: u16 = 24;
pub const EXT_PAUSABLE_CONFIG: u16 = 25;
pub const EXT_PAUSABLE_ACCOUNT: u16 = 26;
pub const ACCOUNT_TYPE_MINT: u8 = 0x01;
pub const ACCOUNT_TYPE_TOKEN: u8 = 0x02;
pub const BASE_MINT_LEN: usize = 82;
pub const BASE_TOKEN_LEN: usize = 165;
pub const ACCOUNT_TYPE_OFFSET: usize = BASE_TOKEN_LEN;
pub const TLV_OFFSET: usize = ACCOUNT_TYPE_OFFSET + 1;
pub const MINT_EXTENSION_PADDING_START: usize = BASE_MINT_LEN;
pub const MINT_EXTENSION_PADDING_END: usize = ACCOUNT_TYPE_OFFSET;
#[inline]
pub fn find_extension<'a>(tlv_bytes: &'a [u8], ext_type: u16) -> Option<&'a [u8]> {
let mut cursor = 0usize;
while cursor + 4 <= tlv_bytes.len() {
let t = u16::from_le_bytes([tlv_bytes[cursor], tlv_bytes[cursor + 1]]);
let len = u16::from_le_bytes([tlv_bytes[cursor + 2], tlv_bytes[cursor + 3]]) as usize;
let data_start = cursor + 4;
let data_end = data_start + len;
if data_end > tlv_bytes.len() {
return None;
}
if t == ext_type {
return Some(&tlv_bytes[data_start..data_end]);
}
if t == EXT_UNINITIALIZED {
return None;
}
cursor = data_end;
}
None
}
#[inline]
pub fn mint_tlv_region(data: &[u8]) -> Option<&[u8]> {
if data.len() <= TLV_OFFSET {
return None;
}
let kind = data[ACCOUNT_TYPE_OFFSET];
if kind != ACCOUNT_TYPE_MINT && kind != 0 {
return None;
}
Some(&data[TLV_OFFSET..])
}
#[inline]
pub fn token_account_tlv_region(data: &[u8]) -> Option<&[u8]> {
if data.len() <= TLV_OFFSET {
return None;
}
let kind = data[ACCOUNT_TYPE_OFFSET];
if kind != ACCOUNT_TYPE_TOKEN && kind != 0 {
return None;
}
Some(&data[TLV_OFFSET..])
}
#[inline]
pub fn require_non_transferable(mint: &AccountView) -> ProgramResult {
let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
if find_extension(tlv, EXT_NON_TRANSFERABLE).is_some() {
Ok(())
} else {
Err(ProgramError::InvalidAccountData)
}
}
#[inline]
pub fn require_mint_close_authority(
mint: &AccountView,
expected: &Address,
) -> ProgramResult {
let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
let ext = find_extension(tlv, EXT_MINT_CLOSE_AUTHORITY).ok_or(ProgramError::InvalidAccountData)?;
if ext.len() < 32 {
return Err(ProgramError::InvalidAccountData);
}
if &ext[..32] == expected.as_array() {
Ok(())
} else {
Err(ProgramError::IncorrectAuthority)
}
}
#[inline]
pub fn require_permanent_delegate(
mint: &AccountView,
expected: &Address,
) -> ProgramResult {
let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
let ext = find_extension(tlv, EXT_PERMANENT_DELEGATE).ok_or(ProgramError::InvalidAccountData)?;
if ext.len() < 32 {
return Err(ProgramError::InvalidAccountData);
}
if &ext[..32] == expected.as_array() {
Ok(())
} else {
Err(ProgramError::IncorrectAuthority)
}
}
#[inline]
pub fn require_transfer_hook_program(
mint: &AccountView,
expected: &Address,
) -> ProgramResult {
let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
let ext = find_extension(tlv, EXT_TRANSFER_HOOK).ok_or(ProgramError::InvalidAccountData)?;
if ext.len() < 64 {
return Err(ProgramError::InvalidAccountData);
}
if &ext[32..64] == expected.as_array() {
Ok(())
} else {
Err(ProgramError::IncorrectProgramId)
}
}
#[inline]
pub fn require_transfer_hook_authority(
mint: &AccountView,
expected: &Address,
) -> ProgramResult {
let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
let ext = find_extension(tlv, EXT_TRANSFER_HOOK).ok_or(ProgramError::InvalidAccountData)?;
if ext.len() < 32 {
return Err(ProgramError::InvalidAccountData);
}
if &ext[..32] == expected.as_array() {
Ok(())
} else {
Err(ProgramError::IncorrectAuthority)
}
}
#[inline]
pub fn require_metadata_pointer_address(
mint: &AccountView,
expected: &Address,
) -> ProgramResult {
let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
let ext = find_extension(tlv, EXT_METADATA_POINTER).ok_or(ProgramError::InvalidAccountData)?;
if ext.len() < 64 {
return Err(ProgramError::InvalidAccountData);
}
if &ext[32..64] == expected.as_array() {
Ok(())
} else {
Err(ProgramError::InvalidAccountData)
}
}
#[inline]
pub fn require_metadata_pointer_authority(
mint: &AccountView,
expected: &Address,
) -> ProgramResult {
let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
let ext = find_extension(tlv, EXT_METADATA_POINTER).ok_or(ProgramError::InvalidAccountData)?;
if ext.len() < 32 {
return Err(ProgramError::InvalidAccountData);
}
if &ext[..32] == expected.as_array() {
Ok(())
} else {
Err(ProgramError::IncorrectAuthority)
}
}
#[inline]
pub fn require_immutable_owner(token_account: &AccountView) -> ProgramResult {
let data = token_account
.try_borrow()
.map_err(|_| ProgramError::AccountBorrowFailed)?;
let tlv = token_account_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
if find_extension(tlv, EXT_IMMUTABLE_OWNER).is_some() {
Ok(())
} else {
Err(ProgramError::InvalidAccountData)
}
}
#[inline]
pub fn require_default_account_state(mint: &AccountView, expected: u8) -> ProgramResult {
let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
let ext = find_extension(tlv, EXT_DEFAULT_ACCOUNT_STATE)
.ok_or(ProgramError::InvalidAccountData)?;
if ext.is_empty() {
return Err(ProgramError::InvalidAccountData);
}
if ext[0] == expected {
Ok(())
} else {
Err(ProgramError::InvalidAccountData)
}
}
#[inline]
pub fn require_interest_bearing_authority(
mint: &AccountView,
expected: &Address,
) -> ProgramResult {
let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
let ext = find_extension(tlv, EXT_INTEREST_BEARING_CONFIG)
.ok_or(ProgramError::InvalidAccountData)?;
if ext.len() < 32 {
return Err(ProgramError::InvalidAccountData);
}
if &ext[..32] == expected.as_array() {
Ok(())
} else {
Err(ProgramError::IncorrectAuthority)
}
}
#[inline]
pub fn require_transfer_fee_config_authority(
mint: &AccountView,
expected: &Address,
) -> ProgramResult {
let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
let ext = find_extension(tlv, EXT_TRANSFER_FEE_CONFIG)
.ok_or(ProgramError::InvalidAccountData)?;
if ext.len() < 32 {
return Err(ProgramError::InvalidAccountData);
}
if &ext[..32] == expected.as_array() {
Ok(())
} else {
Err(ProgramError::IncorrectAuthority)
}
}
#[inline]
pub fn require_transfer_fee_withdraw_authority(
mint: &AccountView,
expected: &Address,
) -> ProgramResult {
let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
let ext = find_extension(tlv, EXT_TRANSFER_FEE_CONFIG)
.ok_or(ProgramError::InvalidAccountData)?;
if ext.len() < 64 {
return Err(ProgramError::InvalidAccountData);
}
if &ext[32..64] == expected.as_array() {
Ok(())
} else {
Err(ProgramError::IncorrectAuthority)
}
}
#[cfg(test)]
mod tests {
extern crate alloc;
use super::*;
fn mint_with_exts(exts: &[(u16, &[u8])]) -> alloc::vec::Vec<u8> {
let mut v = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
v.push(ACCOUNT_TYPE_MINT);
for (ty, payload) in exts {
v.extend_from_slice(&ty.to_le_bytes());
v.extend_from_slice(&(payload.len() as u16).to_le_bytes());
v.extend_from_slice(payload);
}
debug_assert!(v.len() > TLV_OFFSET);
v
}
fn one_ext_mint(ext_type: u16, payload: &[u8]) -> alloc::vec::Vec<u8> {
mint_with_exts(&[(ext_type, payload)])
}
fn token_account_with_exts(exts: &[(u16, &[u8])]) -> alloc::vec::Vec<u8> {
let mut v = alloc::vec![0u8; BASE_TOKEN_LEN];
v.push(ACCOUNT_TYPE_TOKEN);
for (ty, payload) in exts {
v.extend_from_slice(&ty.to_le_bytes());
v.extend_from_slice(&(payload.len() as u16).to_le_bytes());
v.extend_from_slice(payload);
}
v
}
#[test]
fn offset_constants_match_authoritative_spec() {
assert_eq!(BASE_MINT_LEN, 82);
assert_eq!(BASE_TOKEN_LEN, 165);
assert_eq!(ACCOUNT_TYPE_OFFSET, 165);
assert_eq!(TLV_OFFSET, 166);
assert_eq!(MINT_EXTENSION_PADDING_START, 82);
assert_eq!(MINT_EXTENSION_PADDING_END, 165);
assert_eq!(ACCOUNT_TYPE_MINT, 0x01);
assert_eq!(ACCOUNT_TYPE_TOKEN, 0x02);
}
#[test]
fn real_layout_mint_tlv_region_starts_at_166() {
let data = one_ext_mint(EXT_NON_TRANSFERABLE, &[]);
let tlv = mint_tlv_region(&data).expect("extended mint must yield TLV region");
assert_eq!(tlv.as_ptr() as usize - data.as_ptr() as usize, TLV_OFFSET);
assert_eq!(u16::from_le_bytes([tlv[0], tlv[1]]), EXT_NON_TRANSFERABLE);
assert_eq!(u16::from_le_bytes([tlv[2], tlv[3]]), 0);
}
#[test]
fn real_layout_mint_padding_is_not_treated_as_tlv() {
let data = one_ext_mint(EXT_TRANSFER_HOOK, &[0u8; 64]);
let tlv = mint_tlv_region(&data).expect("tlv region");
assert!(find_extension(tlv, EXT_TRANSFER_HOOK).is_some());
}
#[test]
fn find_extension_returns_payload_slice() {
let data = one_ext_mint(EXT_NON_TRANSFERABLE, &[]);
let tlv = mint_tlv_region(&data).unwrap();
assert!(find_extension(tlv, EXT_NON_TRANSFERABLE).is_some());
}
#[test]
fn find_extension_returns_none_when_absent() {
let data = one_ext_mint(EXT_NON_TRANSFERABLE, &[]);
let tlv = mint_tlv_region(&data).unwrap();
assert!(find_extension(tlv, EXT_TRANSFER_HOOK).is_none());
}
#[test]
fn find_extension_bails_on_malformed_length() {
let mut data = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
data.push(ACCOUNT_TYPE_MINT);
data.extend_from_slice(&EXT_TRANSFER_HOOK.to_le_bytes());
data.extend_from_slice(&999u16.to_le_bytes());
let tlv = mint_tlv_region(&data).unwrap();
assert!(find_extension(tlv, EXT_TRANSFER_HOOK).is_none());
}
#[test]
fn find_extension_finds_second_entry() {
let data = mint_with_exts(&[
(EXT_METADATA_POINTER, &[1u8; 64]),
(EXT_PERMANENT_DELEGATE, &[2u8; 32]),
]);
let tlv = mint_tlv_region(&data).unwrap();
let perm = find_extension(tlv, EXT_PERMANENT_DELEGATE).unwrap();
assert_eq!(perm, &[2u8; 32]);
}
#[test]
fn mint_tlv_region_rejects_short_account() {
let data = alloc::vec![0u8; 40];
assert!(mint_tlv_region(&data).is_none());
let data = alloc::vec![0u8; TLV_OFFSET];
assert!(mint_tlv_region(&data).is_none());
}
#[test]
fn mint_tlv_region_rejects_wrong_account_kind() {
let mut data = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
data.push(ACCOUNT_TYPE_TOKEN);
data.push(0); assert!(mint_tlv_region(&data).is_none());
}
#[test]
fn mint_tlv_region_accepts_zero_kind_byte() {
let mut data = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
data.push(0u8);
data.push(0); assert!(mint_tlv_region(&data).is_some());
}
#[test]
fn token_account_tlv_region_accepts_zero_kind_byte() {
let mut data = alloc::vec![0u8; BASE_TOKEN_LEN];
data.push(0u8);
data.push(0); assert!(token_account_tlv_region(&data).is_some());
}
#[test]
fn token_account_tlv_region_rejects_mint_kind() {
let mut data = alloc::vec![0u8; BASE_TOKEN_LEN];
data.push(ACCOUNT_TYPE_MINT);
data.push(0);
assert!(token_account_tlv_region(&data).is_none());
}
#[test]
fn token_account_tlv_region_returns_real_tlv() {
let data = token_account_with_exts(&[(EXT_IMMUTABLE_OWNER, &[])]);
let tlv = token_account_tlv_region(&data).unwrap();
assert_eq!(tlv.as_ptr() as usize - data.as_ptr() as usize, TLV_OFFSET);
assert!(find_extension(tlv, EXT_IMMUTABLE_OWNER).is_some());
}
}