Skip to content

Commit f319c2c

Browse files
committed
feat: scaffold extensions dir
1 parent 0f3cc2f commit f319c2c

File tree

5 files changed

+276
-3
lines changed

5 files changed

+276
-3
lines changed

Cargo.lock

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
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+
}

programs/token-2022/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![no_std]
22

3+
pub mod extensions;
34
pub mod instructions;
45
pub mod state;
56

@@ -15,3 +16,12 @@ fn write_bytes(destination: &mut [MaybeUninit<u8>], source: &[u8]) {
1516
d.write(*s);
1617
}
1718
}
19+
20+
///
21+
/// # Safety
22+
///
23+
/// This function is unsafe because it transmutes the input data to the output type and return a reference.
24+
pub unsafe fn from_bytes_ref<T>(data: &[u8]) -> &T {
25+
assert_eq!(data.len(), core::mem::size_of::<T>());
26+
&*(data.as_ptr() as *const T)
27+
}

programs/token-2022/src/state/mint.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ use pinocchio::{
44
pubkey::Pubkey,
55
};
66

7-
use crate::ID;
7+
use crate::{
8+
extensions::{BaseState, BaseStateVariant, EXTENSION_START_OFFSET},
9+
ID,
10+
};
811

912
/// Mint data.
1013
#[repr(C)]
@@ -148,3 +151,14 @@ impl Mint {
148151
&self.freeze_authority
149152
}
150153
}
154+
155+
/// Number of padding bytes after a Mint account's base data before extensions start.
156+
pub const MINT_EXTENSION_PADDING: usize = 83;
157+
158+
impl BaseState for Mint {
159+
const BASE_STATE: BaseStateVariant = BaseStateVariant::Mint;
160+
const LEN: usize = Mint::BASE_LEN;
161+
fn extension_data_start_index() -> Option<usize> {
162+
Some(Mint::LEN + MINT_EXTENSION_PADDING + EXTENSION_START_OFFSET)
163+
}
164+
}

programs/token-2022/src/state/token.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ use pinocchio::{
55
pubkey::Pubkey,
66
};
77

8-
use crate::ID;
8+
use crate::{
9+
extensions::{BaseState, BaseStateVariant, EXTENSION_START_OFFSET},
10+
ID,
11+
};
912

1013
/// Token account data.
1114
#[repr(C)]
@@ -201,3 +204,11 @@ impl TokenAccount {
201204
self.state == AccountState::Frozen as u8
202205
}
203206
}
207+
208+
impl BaseState for TokenAccount {
209+
const BASE_STATE: BaseStateVariant = BaseStateVariant::TokenAccount;
210+
const LEN: usize = TokenAccount::BASE_LEN;
211+
fn extension_data_start_index() -> Option<usize> {
212+
Some(TokenAccount::LEN + EXTENSION_START_OFFSET)
213+
}
214+
}

0 commit comments

Comments
 (0)