use crate::error::{Error, Result};
use crate::pubkey::Pubkey;
pub const BASE_ACCOUNT_LEN: usize = 165;
pub const ACCOUNT_TYPE_OFFSET: usize = 165;
pub const TLV_START: usize = 166;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TransferFee {
pub epoch: u64,
pub maximum_fee: u64,
pub basis_points: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccountState {
Uninitialized,
Initialized,
Frozen,
Unknown(u8),
}
impl From<u8> for AccountState {
fn from(b: u8) -> Self {
match b {
0 => AccountState::Uninitialized,
1 => AccountState::Initialized,
2 => AccountState::Frozen,
other => AccountState::Unknown(other),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum MintExtension {
TransferFeeConfig {
config_authority: Option<Pubkey>,
withdraw_withheld_authority: Option<Pubkey>,
withheld_amount: u64,
older: TransferFee,
newer: TransferFee,
},
MintCloseAuthority {
authority: Option<Pubkey>,
},
ConfidentialTransferMint {
authority: Option<Pubkey>,
auto_approve_new_accounts: bool,
auditor_elgamal_pubkey: Option<[u8; 32]>,
},
DefaultAccountState {
state: AccountState,
},
NonTransferable,
InterestBearingConfig {
rate_authority: Option<Pubkey>,
current_rate_bps: i16,
},
PermanentDelegate {
delegate: Option<Pubkey>,
},
TransferHook {
authority: Option<Pubkey>,
program_id: Option<Pubkey>,
},
MetadataPointer {
authority: Option<Pubkey>,
metadata_address: Option<Pubkey>,
},
TokenMetadata {
update_authority: Option<Pubkey>,
name: String,
symbol: String,
uri: String,
additional_keys: Vec<String>,
},
GroupPointer {
authority: Option<Pubkey>,
address: Option<Pubkey>,
},
GroupMemberPointer {
authority: Option<Pubkey>,
address: Option<Pubkey>,
},
ScaledUiAmountConfig {
authority: Option<Pubkey>,
multiplier: f64,
new_multiplier: f64,
},
PausableConfig {
authority: Option<Pubkey>,
paused: bool,
},
Known {
kind: u16,
name: &'static str,
len: usize,
},
Unknown {
kind: u16,
len: usize,
},
Malformed {
kind: u16,
declared_len: usize,
},
}
impl MintExtension {
pub fn kind(&self) -> u16 {
match self {
MintExtension::TransferFeeConfig { .. } => 1,
MintExtension::MintCloseAuthority { .. } => 3,
MintExtension::ConfidentialTransferMint { .. } => 4,
MintExtension::DefaultAccountState { .. } => 6,
MintExtension::NonTransferable => 9,
MintExtension::InterestBearingConfig { .. } => 10,
MintExtension::PermanentDelegate { .. } => 12,
MintExtension::TransferHook { .. } => 14,
MintExtension::MetadataPointer { .. } => 18,
MintExtension::TokenMetadata { .. } => 19,
MintExtension::GroupPointer { .. } => 20,
MintExtension::GroupMemberPointer { .. } => 22,
MintExtension::ScaledUiAmountConfig { .. } => 25,
MintExtension::PausableConfig { .. } => 26,
MintExtension::Known { kind, .. }
| MintExtension::Unknown { kind, .. }
| MintExtension::Malformed { kind, .. } => *kind,
}
}
pub fn label(&self) -> String {
match self {
MintExtension::TransferFeeConfig { .. } => "transfer fee".into(),
MintExtension::MintCloseAuthority { .. } => "mint close authority".into(),
MintExtension::ConfidentialTransferMint { .. } => "confidential transfers".into(),
MintExtension::DefaultAccountState { .. } => "default account state".into(),
MintExtension::NonTransferable => "non-transferable".into(),
MintExtension::InterestBearingConfig { .. } => "interest bearing".into(),
MintExtension::PermanentDelegate { .. } => "permanent delegate".into(),
MintExtension::TransferHook { .. } => "transfer hook".into(),
MintExtension::MetadataPointer { .. } => "metadata pointer".into(),
MintExtension::TokenMetadata { .. } => "token metadata".into(),
MintExtension::GroupPointer { .. } => "group pointer".into(),
MintExtension::GroupMemberPointer { .. } => "group member pointer".into(),
MintExtension::ScaledUiAmountConfig { .. } => "scaled ui amount".into(),
MintExtension::PausableConfig { .. } => "pausable".into(),
MintExtension::Known { name, .. } => (*name).to_string(),
MintExtension::Unknown { kind, .. } => format!("unknown extension #{kind}"),
MintExtension::Malformed { kind, .. } => format!("malformed extension #{kind}"),
}
}
}
pub fn parse_mint_extensions(data: &[u8]) -> Result<Vec<MintExtension>> {
if data.len() <= TLV_START {
return Ok(Vec::new());
}
match data[ACCOUNT_TYPE_OFFSET] {
1 => {}
0 => return Ok(Vec::new()),
other => {
return Err(Error::InvalidAccountData(format!(
"expected a token-2022 mint (account_type 1), found {other}"
)))
}
}
let mut out = Vec::new();
let mut cursor = TLV_START;
while cursor + 4 <= data.len() {
let kind = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
let len = u16::from_le_bytes([data[cursor + 2], data[cursor + 3]]) as usize;
cursor += 4;
if kind == 0 && len == 0 {
break;
}
if cursor + len > data.len() {
out.push(MintExtension::Malformed {
kind,
declared_len: len,
});
break;
}
let value = &data[cursor..cursor + len];
cursor += len;
out.push(decode(kind, value));
if out.len() >= 64 {
break;
}
}
Ok(out)
}
fn decode(kind: u16, v: &[u8]) -> MintExtension {
match kind {
1 if v.len() >= 108 => MintExtension::TransferFeeConfig {
config_authority: optional_pubkey(&v[0..32]),
withdraw_withheld_authority: optional_pubkey(&v[32..64]),
withheld_amount: u64_le(&v[64..72]),
older: transfer_fee(&v[72..90]),
newer: transfer_fee(&v[90..108]),
},
3 if v.len() >= 32 => MintExtension::MintCloseAuthority {
authority: optional_pubkey(&v[0..32]),
},
4 if v.len() >= 65 => MintExtension::ConfidentialTransferMint {
authority: optional_pubkey(&v[0..32]),
auto_approve_new_accounts: v[32] != 0,
auditor_elgamal_pubkey: {
let mut b = [0u8; 32];
b.copy_from_slice(&v[33..65]);
if b == [0u8; 32] {
None
} else {
Some(b)
}
},
},
6 if !v.is_empty() => MintExtension::DefaultAccountState {
state: AccountState::from(v[0]),
},
9 => MintExtension::NonTransferable,
10 if v.len() >= 52 => MintExtension::InterestBearingConfig {
rate_authority: optional_pubkey(&v[0..32]),
current_rate_bps: i16::from_le_bytes([v[50], v[51]]),
},
12 if v.len() >= 32 => MintExtension::PermanentDelegate {
delegate: optional_pubkey(&v[0..32]),
},
14 if v.len() >= 64 => MintExtension::TransferHook {
authority: optional_pubkey(&v[0..32]),
program_id: optional_pubkey(&v[32..64]),
},
18 if v.len() >= 64 => MintExtension::MetadataPointer {
authority: optional_pubkey(&v[0..32]),
metadata_address: optional_pubkey(&v[32..64]),
},
19 => decode_token_metadata(v),
20 if v.len() >= 64 => MintExtension::GroupPointer {
authority: optional_pubkey(&v[0..32]),
address: optional_pubkey(&v[32..64]),
},
22 if v.len() >= 64 => MintExtension::GroupMemberPointer {
authority: optional_pubkey(&v[0..32]),
address: optional_pubkey(&v[32..64]),
},
25 if v.len() >= 56 => MintExtension::ScaledUiAmountConfig {
authority: optional_pubkey(&v[0..32]),
multiplier: f64_le(&v[32..40]),
new_multiplier: f64_le(&v[48..56]),
},
26 if v.len() >= 33 => MintExtension::PausableConfig {
authority: optional_pubkey(&v[0..32]),
paused: v[32] != 0,
},
2 => known(kind, "transfer fee amount", v),
5 => known(kind, "confidential transfer account", v),
7 => known(kind, "immutable owner", v),
8 => known(kind, "memo transfer", v),
11 => known(kind, "cpi guard", v),
13 => known(kind, "non-transferable account", v),
15 => known(kind, "transfer hook account", v),
16 => known(kind, "confidential transfer fee config", v),
17 => known(kind, "confidential transfer fee amount", v),
21 => known(kind, "token group", v),
23 => known(kind, "token group member", v),
24 => known(kind, "confidential mint/burn", v),
27 => known(kind, "pausable account", v),
1 | 3 | 4 | 6 | 10 | 12 | 14 | 18 | 20 | 22 | 25 | 26 => MintExtension::Malformed {
kind,
declared_len: v.len(),
},
_ => MintExtension::Unknown {
kind,
len: v.len(),
},
}
}
fn known(kind: u16, name: &'static str, v: &[u8]) -> MintExtension {
MintExtension::Known {
kind,
name,
len: v.len(),
}
}
fn decode_token_metadata(v: &[u8]) -> MintExtension {
let mut c = 0usize;
let update_authority = match take(v, &mut c, 32) {
Some(b) => optional_pubkey(b),
None => return MintExtension::Malformed { kind: 19, declared_len: v.len() },
};
if take(v, &mut c, 32).is_none() {
return MintExtension::Malformed { kind: 19, declared_len: v.len() };
}
let name = match take_string(v, &mut c) {
Some(s) => s,
None => return MintExtension::Malformed { kind: 19, declared_len: v.len() },
};
let symbol = match take_string(v, &mut c) {
Some(s) => s,
None => return MintExtension::Malformed { kind: 19, declared_len: v.len() },
};
let uri = match take_string(v, &mut c) {
Some(s) => s,
None => return MintExtension::Malformed { kind: 19, declared_len: v.len() },
};
let mut additional_keys = Vec::new();
if let Some(count_bytes) = take(v, &mut c, 4) {
let count = u32::from_le_bytes([
count_bytes[0],
count_bytes[1],
count_bytes[2],
count_bytes[3],
]);
for _ in 0..count.min(16) {
match (take_string(v, &mut c), take_string(v, &mut c)) {
(Some(k), Some(_)) => additional_keys.push(k),
_ => break,
}
}
}
MintExtension::TokenMetadata {
update_authority,
name,
symbol,
uri,
additional_keys,
}
}
fn take<'a>(v: &'a [u8], cursor: &mut usize, n: usize) -> Option<&'a [u8]> {
let end = cursor.checked_add(n)?;
if end > v.len() {
return None;
}
let out = &v[*cursor..end];
*cursor = end;
Some(out)
}
fn take_string(v: &[u8], cursor: &mut usize) -> Option<String> {
let len_bytes = take(v, cursor, 4)?;
let len = u32::from_le_bytes([len_bytes[0], len_bytes[1], len_bytes[2], len_bytes[3]]) as usize;
if len > v.len() {
return None;
}
let bytes = take(v, cursor, len)?;
Some(String::from_utf8_lossy(bytes).into_owned())
}
fn optional_pubkey(b: &[u8]) -> Option<Pubkey> {
if b.len() != 32 || b.iter().all(|x| *x == 0) {
return None;
}
let mut arr = [0u8; 32];
arr.copy_from_slice(b);
Some(Pubkey::new_from_array(arr))
}
fn transfer_fee(b: &[u8]) -> TransferFee {
TransferFee {
epoch: u64_le(&b[0..8]),
maximum_fee: u64_le(&b[8..16]),
basis_points: u16::from_le_bytes([b[16], b[17]]),
}
}
fn u64_le(b: &[u8]) -> u64 {
let mut a = [0u8; 8];
a.copy_from_slice(&b[..8]);
u64::from_le_bytes(a)
}
fn f64_le(b: &[u8]) -> f64 {
let mut a = [0u8; 8];
a.copy_from_slice(&b[..8]);
f64::from_le_bytes(a)
}