|
| 1 | +use crate::from_bytes_ref; |
| 2 | +use pinocchio::program_error::ProgramError; |
| 3 | + |
| 4 | +/// Offset from the start of the extension area where the first extension begins. |
| 5 | +/// In SPL Token, this is typically 1 byte after the padding section. |
| 6 | +pub const EXTENSION_START_OFFSET: usize = 1; |
| 7 | + |
| 8 | +/// Size (in bytes) of the `extension length` field in the serialized account data. |
| 9 | +pub const EXTENSION_LEN_BYTES_LEN: usize = 2; |
| 10 | + |
| 11 | +/// Size (in bytes) of the `extension type` field in the serialized account data. |
| 12 | +pub const EXTENSION_TYPE_BYTES_LEN: usize = 2; |
| 13 | + |
| 14 | +/// Size (in bytes) of the extension header (type + length). |
| 15 | +pub const EXTENSION_HEADER_LEN: usize = EXTENSION_TYPE_BYTES_LEN + EXTENSION_LEN_BYTES_LEN; |
| 16 | + |
| 17 | +#[repr(u16)] |
| 18 | +#[derive(PartialEq, Eq, Clone, Copy)] |
| 19 | +pub enum ExtensionType { |
| 20 | + /// Used as padding if the account size would otherwise be 355, same as a |
| 21 | + /// multisig |
| 22 | + Uninitialized, |
| 23 | + /// Includes transfer fee rate info and accompanying authorities to withdraw |
| 24 | + /// and set the fee |
| 25 | + TransferFeeConfig, |
| 26 | + /// Includes withheld transfer fees |
| 27 | + TransferFeeAmount, |
| 28 | + /// Includes an optional mint close authority |
| 29 | + MintCloseAuthority, |
| 30 | + /// Auditor configuration for confidential transfers |
| 31 | + ConfidentialTransferMint, |
| 32 | + /// State for confidential transfers |
| 33 | + ConfidentialTransferAccount, |
| 34 | + /// Specifies the default Account::state for new Accounts |
| 35 | + DefaultAccountState, |
| 36 | + /// Indicates that the Account owner authority cannot be changed |
| 37 | + ImmutableOwner, |
| 38 | + /// Require inbound transfers to have memo |
| 39 | + MemoTransfer, |
| 40 | + /// Indicates that the tokens from this mint can't be transferred |
| 41 | + NonTransferable, |
| 42 | + /// Tokens accrue interest over time, |
| 43 | + InterestBearingConfig, |
| 44 | + /// Locks privileged token operations from happening via CPI |
| 45 | + CpiGuard, |
| 46 | + /// Includes an optional permanent delegate |
| 47 | + PermanentDelegate, |
| 48 | + /// Indicates that the tokens in this account belong to a non-transferable |
| 49 | + /// mint |
| 50 | + NonTransferableAccount, |
| 51 | + /// Mint requires a CPI to a program implementing the "transfer hook" |
| 52 | + /// interface |
| 53 | + TransferHook, |
| 54 | + /// Indicates that the tokens in this account belong to a mint with a |
| 55 | + /// transfer hook |
| 56 | + TransferHookAccount, |
| 57 | + /// Includes encrypted withheld fees and the encryption public that they are |
| 58 | + /// encrypted under |
| 59 | + ConfidentialTransferFeeConfig, |
| 60 | + /// Includes confidential withheld transfer fees |
| 61 | + ConfidentialTransferFeeAmount, |
| 62 | + /// Mint contains a pointer to another account (or the same account) that |
| 63 | + /// holds metadata |
| 64 | + MetadataPointer, |
| 65 | + /// Mint contains token-metadata |
| 66 | + TokenMetadata, |
| 67 | + /// Mint contains a pointer to another account (or the same account) that |
| 68 | + /// holds group configurations |
| 69 | + GroupPointer, |
| 70 | + /// Mint contains token group configurations |
| 71 | + TokenGroup, |
| 72 | + /// Mint contains a pointer to another account (or the same account) that |
| 73 | + /// holds group member configurations |
| 74 | + GroupMemberPointer, |
| 75 | + /// Mint contains token group member configurations |
| 76 | + TokenGroupMember, |
| 77 | + /// Mint allowing the minting and burning of confidential tokens |
| 78 | + ConfidentialMintBurn, |
| 79 | + /// Tokens whose UI amount is scaled by a given amount |
| 80 | + ScaledUiAmount, |
| 81 | + /// Tokens where minting / burning / transferring can be paused |
| 82 | + Pausable, |
| 83 | + /// Indicates that the account belongs to a pausable mint |
| 84 | + PausableAccount, |
| 85 | +} |
| 86 | + |
| 87 | +impl ExtensionType { |
| 88 | + #[inline(always)] |
| 89 | + fn from_bytes(val: [u8; 2]) -> Option<Self> { |
| 90 | + match u16::from_le_bytes(val) { |
| 91 | + 0 => Some(Self::Uninitialized), |
| 92 | + 1 => Some(Self::TransferFeeConfig), |
| 93 | + 2 => Some(Self::TransferFeeAmount), |
| 94 | + 3 => Some(Self::MintCloseAuthority), |
| 95 | + 4 => Some(Self::ConfidentialTransferMint), |
| 96 | + 5 => Some(Self::ConfidentialTransferAccount), |
| 97 | + 6 => Some(Self::DefaultAccountState), |
| 98 | + 7 => Some(Self::ImmutableOwner), |
| 99 | + 8 => Some(Self::MemoTransfer), |
| 100 | + 9 => Some(Self::NonTransferable), |
| 101 | + 10 => Some(Self::InterestBearingConfig), |
| 102 | + 11 => Some(Self::CpiGuard), |
| 103 | + 12 => Some(Self::PermanentDelegate), |
| 104 | + 13 => Some(Self::NonTransferableAccount), |
| 105 | + 14 => Some(Self::TransferHook), |
| 106 | + 15 => Some(Self::TransferHookAccount), |
| 107 | + 16 => Some(Self::ConfidentialTransferFeeConfig), |
| 108 | + 17 => Some(Self::ConfidentialTransferFeeAmount), |
| 109 | + 18 => Some(Self::MetadataPointer), |
| 110 | + 19 => Some(Self::TokenMetadata), |
| 111 | + 20 => Some(Self::GroupPointer), |
| 112 | + 21 => Some(Self::TokenGroup), |
| 113 | + 22 => Some(Self::GroupMemberPointer), |
| 114 | + 23 => Some(Self::TokenGroupMember), |
| 115 | + 24 => Some(Self::ConfidentialMintBurn), |
| 116 | + 25 => Some(Self::ScaledUiAmount), |
| 117 | + 26 => Some(Self::Pausable), |
| 118 | + 27 => Some(Self::PausableAccount), |
| 119 | + _ => None, |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + #[inline(always)] |
| 124 | + pub fn to_bytes(&self) -> [u8; 2] { |
| 125 | + u16::to_le_bytes(*self as u16) |
| 126 | + } |
| 127 | +} |
| 128 | + |
| 129 | +/// Represents the type of "base state" for the account |
| 130 | +/// (either a Mint account or a TokenAccount). |
| 131 | +#[derive(PartialEq, Eq)] |
| 132 | +pub enum BaseStateVariant { |
| 133 | + Mint, |
| 134 | + TokenAccount, |
| 135 | +} |
| 136 | + |
| 137 | +/// Trait that must be implemented by Token-2022 state (Mint, TokenAccount) |
| 138 | +/// to define the base state and its layout. |
| 139 | +pub trait BaseState { |
| 140 | + /// Which base state variant this type represents. |
| 141 | + const BASE_STATE: BaseStateVariant; |
| 142 | + |
| 143 | + /// Size in bytes of the base account data (without extensions). |
| 144 | + const LEN: usize; |
| 145 | + |
| 146 | + /// Where extension data starts (offset into account data). |
| 147 | + fn extension_data_start_index() -> Option<usize>; |
| 148 | +} |
| 149 | + |
| 150 | +/// Describes whether an extension has a fixed or variable size. |
| 151 | +pub enum ExtensionLen { |
| 152 | + FixedSize(u16), |
| 153 | + VariableSize, |
| 154 | +} |
| 155 | + |
| 156 | +/// Trait implemented by all Token-2022 extensions. |
| 157 | +/// Defines extension type, size, and which base state it belongs to. |
| 158 | +pub trait Extension { |
| 159 | + const TYPE: ExtensionType; |
| 160 | + const LEN: ExtensionLen; |
| 161 | + const BASE_STATE: BaseStateVariant; |
| 162 | +} |
| 163 | + |
| 164 | +/// Reads an extension of type `E` from the provided account data bytes |
| 165 | +/// correspoing to base state B. |
| 166 | +/// Returns `Ok(Some(&E))` if found, `Ok(None)` if not present, |
| 167 | +/// or `Err(ProgramError)` if parsing fails. |
| 168 | +/// |
| 169 | +/// Safety: Uses `from_bytes_ref` to cast extension bytes into a reference. |
| 170 | +#[inline] |
| 171 | +pub fn get_extension_from_acc_data<E: Extension, B: BaseState>( |
| 172 | + acc_data_bytes: &[u8], |
| 173 | +) -> Result<Option<&E>, ProgramError> { |
| 174 | + // Ensure extension is only searched in its matching base state. |
| 175 | + if B::BASE_STATE != E::BASE_STATE { |
| 176 | + return Ok(None); |
| 177 | + } |
| 178 | + |
| 179 | + // Get where extension data begins. |
| 180 | + let ext_data_start_index = match B::extension_data_start_index() { |
| 181 | + Some(idx) => idx, |
| 182 | + None => return Ok(None), |
| 183 | + }; |
| 184 | + |
| 185 | + // Slice out the extension area from account data. |
| 186 | + let ext_bytes = acc_data_bytes |
| 187 | + .get(ext_data_start_index..) |
| 188 | + .ok_or(ProgramError::AccountDataTooSmall)?; |
| 189 | + |
| 190 | + let mut start = 0; |
| 191 | + let end = ext_bytes.len(); |
| 192 | + |
| 193 | + // Walk through each extension in the serialized list. |
| 194 | + while start < end { |
| 195 | + let ext_type_idx = start; |
| 196 | + let ext_len_idx = ext_type_idx + EXTENSION_TYPE_BYTES_LEN; |
| 197 | + let ext_data_idx = ext_len_idx + EXTENSION_LEN_BYTES_LEN; |
| 198 | + |
| 199 | + // Parse extension type. |
| 200 | + let ext_type = ext_bytes[ext_type_idx..(ext_type_idx + EXTENSION_TYPE_BYTES_LEN)] |
| 201 | + .try_into() |
| 202 | + .map(ExtensionType::from_bytes) |
| 203 | + .map_err(|_| ProgramError::InvalidAccountData)? |
| 204 | + .ok_or(ProgramError::InvalidAccountData)?; |
| 205 | + |
| 206 | + // Parse extension length. |
| 207 | + let ext_len = ext_bytes[ext_len_idx..(ext_len_idx + EXTENSION_LEN_BYTES_LEN)] |
| 208 | + .try_into() |
| 209 | + .map(u16::from_le_bytes) |
| 210 | + .map_err(|_| ProgramError::InvalidAccountData)?; |
| 211 | + |
| 212 | + // Match ext len and check if this is the extension we’re looking for, then return it. |
| 213 | + match E::LEN { |
| 214 | + ExtensionLen::FixedSize(size) => { |
| 215 | + if ext_type == E::TYPE && ext_len == size { |
| 216 | + return Ok(Some(unsafe { |
| 217 | + from_bytes_ref(&ext_bytes[ext_data_idx..ext_data_idx + size as usize]) |
| 218 | + })); |
| 219 | + } |
| 220 | + } |
| 221 | + ExtensionLen::VariableSize => { |
| 222 | + if ext_type == E::TYPE { |
| 223 | + return Ok(Some(unsafe { |
| 224 | + from_bytes_ref(&ext_bytes[ext_data_idx..ext_data_idx + ext_len as usize]) |
| 225 | + })); |
| 226 | + } |
| 227 | + } |
| 228 | + } |
| 229 | + |
| 230 | + // Advance to next extension in the serialized account data bytes |
| 231 | + start += EXTENSION_HEADER_LEN + ext_len as usize; |
| 232 | + } |
| 233 | + |
| 234 | + Ok(None) |
| 235 | +} |
0 commit comments