light_token_interface/pool_derivation.rs
1//! SPL interface PDA derivation utilities for Light Protocol.
2//!
3//! This module provides functions to derive SPL interface PDAs (token pools) for both regular
4//! and restricted mints. Restricted mints (those with Pausable, PermanentDelegate,
5//! TransferFeeConfig, TransferHook, or DefaultAccountState extensions) use a different derivation path
6//! to prevent accidental compression via legacy anchor instructions.
7
8use solana_pubkey::Pubkey;
9use spl_token_2022::{
10 extension::{BaseStateWithExtensions, PodStateWithExtensions},
11 pod::PodMint,
12};
13
14use crate::{
15 constants::{LIGHT_TOKEN_PROGRAM_ID, POOL_SEED, RESTRICTED_POOL_SEED},
16 is_restricted_extension,
17};
18
19/// Maximum number of pool accounts per mint.
20pub const NUM_MAX_POOL_ACCOUNTS: u8 = 5;
21
22// ============================================================================
23// SPL interface PDA derivation (uses LIGHT_TOKEN_PROGRAM_ID)
24// ============================================================================
25
26/// Find the SPL interface PDA for a given mint (index 0).
27///
28/// # Arguments
29/// * `mint` - The mint public key
30/// * `restricted` - Whether to use restricted derivation (for mints with restricted extensions)
31///
32/// # Seed format
33/// - Regular: `["pool", mint]`
34/// - Restricted: `["pool", mint, "restricted"]`
35pub fn find_spl_interface_pda(mint: &Pubkey, restricted: bool) -> (Pubkey, u8) {
36 find_spl_interface_pda_with_index(mint, 0, restricted)
37}
38
39/// Find the SPL interface PDA for a given mint and index.
40///
41/// # Arguments
42/// * `mint` - The mint public key
43/// * `index` - The pool index (0-4)
44/// * `restricted` - Whether to use restricted derivation (for mints with restricted extensions)
45///
46/// # Seed format
47/// - Regular index 0: `["pool", mint]`
48/// - Regular index 1-4: `["pool", mint, index]`
49/// - Restricted index 0: `["pool", mint, "restricted"]`
50/// - Restricted index 1-4: `["pool", mint, "restricted", index]`
51pub fn find_spl_interface_pda_with_index(
52 mint: &Pubkey,
53 index: u8,
54 restricted: bool,
55) -> (Pubkey, u8) {
56 let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID);
57 let index_bytes = [index];
58
59 let seeds: &[&[u8]] = if restricted {
60 if index == 0 {
61 &[POOL_SEED, mint.as_ref(), RESTRICTED_POOL_SEED]
62 } else {
63 &[POOL_SEED, mint.as_ref(), RESTRICTED_POOL_SEED, &index_bytes]
64 }
65 } else if index == 0 {
66 &[POOL_SEED, mint.as_ref()]
67 } else {
68 &[POOL_SEED, mint.as_ref(), &index_bytes]
69 };
70
71 Pubkey::find_program_address(seeds, &program_id)
72}
73
74/// Get the SPL interface PDA address for a given mint (index 0).
75pub fn get_spl_interface_pda(mint: &Pubkey, restricted: bool) -> Pubkey {
76 find_spl_interface_pda(mint, restricted).0
77}
78
79// ============================================================================
80// Validation
81// ============================================================================
82
83/// Validate that an SPL interface PDA is correctly derived.
84///
85/// # Arguments
86/// * `mint_bytes` - The mint public key as bytes
87/// * `spl_interface_pubkey` - The SPL interface PDA to validate
88/// * `pool_index` - The pool index (0-4)
89/// * `bump` - Optional bump seed for faster validation
90/// * `restricted` - Whether to validate against restricted derivation
91///
92/// # Returns
93/// `true` if the PDA is valid, `false` otherwise
94#[inline(always)]
95pub fn is_valid_spl_interface_pda(
96 mint_bytes: &[u8],
97 spl_interface_pubkey: &Pubkey,
98 pool_index: u8,
99 bump: Option<u8>,
100 restricted: bool,
101) -> bool {
102 let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID);
103 let index_bytes = [pool_index];
104
105 let pda = if let Some(bump) = bump {
106 // Fast path: use provided bump to derive address directly
107 let bump_bytes = [bump];
108 let seeds: &[&[u8]] = if restricted {
109 if pool_index == 0 {
110 &[POOL_SEED, mint_bytes, RESTRICTED_POOL_SEED, &bump_bytes]
111 } else {
112 &[
113 POOL_SEED,
114 mint_bytes,
115 RESTRICTED_POOL_SEED,
116 &index_bytes,
117 &bump_bytes,
118 ]
119 }
120 } else if pool_index == 0 {
121 &[POOL_SEED, mint_bytes, &bump_bytes]
122 } else {
123 &[POOL_SEED, mint_bytes, &index_bytes, &bump_bytes]
124 };
125
126 match Pubkey::create_program_address(seeds, &program_id) {
127 Ok(pda) => pda,
128 Err(_) => return false,
129 }
130 } else {
131 // Slow path: find program address
132 let seeds: &[&[u8]] = if restricted {
133 if pool_index == 0 {
134 &[POOL_SEED, mint_bytes, RESTRICTED_POOL_SEED]
135 } else {
136 &[POOL_SEED, mint_bytes, RESTRICTED_POOL_SEED, &index_bytes]
137 }
138 } else if pool_index == 0 {
139 &[POOL_SEED, mint_bytes]
140 } else {
141 &[POOL_SEED, mint_bytes, &index_bytes]
142 };
143
144 Pubkey::find_program_address(seeds, &program_id).0
145 };
146
147 pda == *spl_interface_pubkey
148}
149
150// ============================================================================
151// Mint extension helpers
152// ============================================================================
153
154/// Check if a mint has any restricted extensions.
155///
156/// Restricted extensions (Pausable, PermanentDelegate, TransferFeeConfig, TransferHook, DefaultAccountState)
157/// require using the restricted pool derivation path.
158///
159/// # Arguments
160/// * `mint_data` - The raw mint account data
161///
162/// # Returns
163/// `true` if the mint has any restricted extensions, `false` otherwise
164pub fn has_restricted_extensions(mint_data: &[u8]) -> bool {
165 let mint = match PodStateWithExtensions::<PodMint>::unpack(mint_data) {
166 Ok(mint) => mint,
167 Err(_) => return false,
168 };
169
170 let extensions = match mint.get_extension_types() {
171 Ok(exts) => exts,
172 Err(_) => return false,
173 };
174
175 extensions.iter().any(is_restricted_extension)
176}