light_compressible/
config.rs

1use bytemuck::{Pod, Zeroable};
2use light_account_checks::discriminator::Discriminator;
3use solana_pubkey::{pubkey, Pubkey};
4
5use crate::{error::CompressibleError, rent::RentConfig, AnchorDeserialize, AnchorSerialize};
6
7pub const COMPRESSIBLE_CONFIG_SEED: &[u8] = b"compressible_config";
8
9#[derive(Debug, PartialEq)]
10#[repr(u8)]
11pub enum CompressibleConfigState {
12    Inactive,
13    Active,
14    Deprecated,
15}
16
17impl TryFrom<u8> for CompressibleConfigState {
18    type Error = CompressibleError;
19
20    fn try_from(value: u8) -> Result<Self, Self::Error> {
21        match value {
22            0 => Ok(CompressibleConfigState::Inactive),
23            1 => Ok(CompressibleConfigState::Active),
24            2 => Ok(CompressibleConfigState::Deprecated),
25            _ => Err(CompressibleError::InvalidState(value)),
26        }
27    }
28}
29
30#[derive(Clone, Debug, AnchorDeserialize, PartialEq, AnchorSerialize, Copy, Pod, Zeroable)]
31#[repr(C)]
32pub struct CompressibleConfig {
33    /// Config version for future upgrades
34    pub version: u16,
35    /// 1 Compressible Config pda is active, 0 is inactive, 2 is deprecated.
36    /// - inactive, config cannot be used
37    /// - active, config can be used
38    /// - deprecated, no new ctoken account can be created with this config, other instructions work.
39    pub state: u8,
40    /// CompressibleConfig PDA bump seed
41    pub bump: u8,
42    /// Update authority can update the CompressibleConfig.
43    pub update_authority: Pubkey,
44    /// Withdrawal authority can withdraw funds from the rent recipient pda.
45    pub withdrawal_authority: Pubkey,
46    /// Light Token program pda:
47    /// 1. pays rent exemption at compressible ctoken account creation
48    /// 2. receives rent exemption at compressible ctoken account closure
49    /// 3. receives rent from compressible ctoken accounts with Claim, or compress and close instructions.
50    pub rent_sponsor: Pubkey,
51    /// Registry program pda, can Claim from and compress and close compressible ctoken accounts.
52    pub compression_authority: Pubkey,
53    pub rent_sponsor_bump: u8,
54    pub compression_authority_bump: u8,
55    /// Rent function parameters,
56    /// used to calculate whether the account is compressible.
57    pub rent_config: RentConfig,
58
59    /// Address space for compressed accounts (currently 1 address_tree allowed)
60    pub address_space: [Pubkey; 4],
61    pub _place_holder: [u8; 32],
62}
63
64impl CompressibleConfig {
65    /// Validates that the config is active (can be used for all operations)
66    pub fn validate_active(&self) -> Result<(), CompressibleError> {
67        let state = CompressibleConfigState::try_from(self.state)?;
68        if state != CompressibleConfigState::Active {
69            return Err(CompressibleError::InvalidState(self.state));
70        }
71        Ok(())
72    }
73
74    /// Validates that the config is not inactive (can be used for new account creation)
75    pub fn validate_not_inactive(&self) -> Result<(), CompressibleError> {
76        let state = CompressibleConfigState::try_from(self.state)?;
77        if state == CompressibleConfigState::Inactive {
78            return Err(CompressibleError::InvalidState(self.state));
79        }
80        Ok(())
81    }
82}
83
84#[cfg(feature = "anchor")]
85impl anchor_lang::Discriminator for CompressibleConfig {
86    const DISCRIMINATOR: &'static [u8] = &[180, 4, 231, 26, 220, 144, 55, 168];
87}
88
89#[cfg(feature = "anchor")]
90impl anchor_lang::AccountDeserialize for CompressibleConfig {
91    fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
92        // Skip the discriminator (first 8 bytes) and deserialize the rest
93        let mut data: &[u8] = &buf[8..];
94        Self::deserialize(&mut data)
95            .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into())
96    }
97
98    fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
99        use anchor_lang::Discriminator;
100
101        // Check discriminator first
102        if buf.len() < 8 {
103            return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound.into());
104        }
105
106        let given_disc = &buf[..8];
107        if given_disc != Self::DISCRIMINATOR {
108            return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch.into());
109        }
110
111        Self::try_deserialize_unchecked(buf)
112    }
113}
114
115#[cfg(feature = "anchor")]
116impl anchor_lang::AccountSerialize for CompressibleConfig {
117    fn try_serialize<W: std::io::Write>(&self, writer: &mut W) -> anchor_lang::Result<()> {
118        use anchor_lang::Discriminator;
119
120        // Write discriminator first
121        if writer.write_all(Self::DISCRIMINATOR).is_err() {
122            return Err(anchor_lang::error::ErrorCode::AccountDidNotSerialize.into());
123        }
124
125        // Then serialize the actual account data
126        if self.serialize(writer).is_err() {
127            return Err(anchor_lang::error::ErrorCode::AccountDidNotSerialize.into());
128        }
129        Ok(())
130    }
131}
132
133#[cfg(feature = "anchor")]
134impl anchor_lang::Owner for CompressibleConfig {
135    fn owner() -> anchor_lang::prelude::Pubkey {
136        pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX")
137    }
138}
139
140#[cfg(feature = "anchor")]
141impl anchor_lang::Space for CompressibleConfig {
142    const INIT_SPACE: usize = 8 + std::mem::size_of::<Self>(); // 8 bytes for discriminator + struct size
143}
144
145impl Discriminator for CompressibleConfig {
146    const LIGHT_DISCRIMINATOR: [u8; 8] = [180, 4, 231, 26, 220, 144, 55, 168];
147    const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = Self::LIGHT_DISCRIMINATOR.as_slice();
148}
149
150impl CompressibleConfig {
151    pub const LEN: usize = std::mem::size_of::<Self>();
152
153    pub fn light_token_v1(update_authority: Pubkey, withdrawal_authority: Pubkey) -> Self {
154        Self::new_light_token(
155            1,
156            true,
157            update_authority,
158            withdrawal_authority,
159            RentConfig::default(),
160        )
161    }
162
163    pub fn new_light_token(
164        version: u16,
165        active: bool,
166        update_authority: Pubkey,
167        withdrawal_authority: Pubkey,
168        rent_config: RentConfig,
169    ) -> Self {
170        let mut address_space = [Pubkey::default(); 4];
171        address_space[0] = pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx");
172        Self::new(
173            version,
174            active,
175            update_authority,
176            withdrawal_authority,
177            &pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"),
178            &pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX"),
179            address_space,
180            rent_config,
181        )
182    }
183
184    pub fn get_compression_authority_seeds(version: u16) -> [Vec<u8>; 2] {
185        [
186            b"compression_authority".to_vec(),
187            version.to_le_bytes().to_vec(),
188        ]
189    }
190
191    pub fn get_rent_sponsor_seeds(version: u16) -> [Vec<u8>; 2] {
192        [b"rent_sponsor".to_vec(), version.to_le_bytes().to_vec()]
193    }
194
195    #[allow(clippy::too_many_arguments)]
196    pub fn new(
197        version: u16,
198        active: bool,
199        update_authority: Pubkey,
200        withdrawal_authority: Pubkey,
201        rent_sponsor_program_id: &Pubkey,
202        owner_program_id: &Pubkey,
203        address_space: [Pubkey; 4],
204        rent_config: RentConfig,
205    ) -> Self {
206        let version_bytes = version.to_le_bytes();
207        let compression_authority_seeds = [
208            b"compression_authority".as_slice(),
209            version_bytes.as_slice(),
210        ];
211        let rent_sponsor_seeds = [b"rent_sponsor".as_slice(), version_bytes.as_slice()];
212        let (compression_authority, compression_authority_bump) =
213            solana_pubkey::Pubkey::find_program_address(
214                compression_authority_seeds.as_slice(),
215                owner_program_id,
216            );
217        let (rent_sponsor, rent_sponsor_bump) = solana_pubkey::Pubkey::find_program_address(
218            rent_sponsor_seeds.as_slice(),
219            rent_sponsor_program_id,
220        );
221        let (_, bump) = Self::derive_pda(owner_program_id, version);
222
223        Self {
224            version,
225            state: active as u8,
226            bump,
227            update_authority,
228            withdrawal_authority,
229            rent_sponsor,
230            compression_authority,
231            rent_sponsor_bump,
232            compression_authority_bump,
233            rent_config,
234            address_space,
235            _place_holder: [0u8; 32],
236        }
237    }
238
239    /// Derives the config PDA address with config bump
240    pub fn derive_pda(program_id: &Pubkey, config_bump: u16) -> (Pubkey, u8) {
241        Pubkey::find_program_address(
242            &[COMPRESSIBLE_CONFIG_SEED, &config_bump.to_le_bytes()],
243            program_id,
244        )
245    }
246
247    /// Derives the default config PDA address (config_bump = 1)
248    pub fn light_token_v1_config_pda() -> Pubkey {
249        Self::derive_pda(&pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX"), 1).0
250    }
251
252    /// Derives the default config PDA address (config_bump = 1)
253    pub fn derive_v1_config_pda(program_id: &Pubkey) -> (Pubkey, u8) {
254        Self::derive_pda(program_id, 1)
255    }
256
257    /// Derives the default config PDA address (config_bump = 0)
258    pub fn derive_default_pda(program_id: &Pubkey) -> (Pubkey, u8) {
259        Self::derive_pda(program_id, 0)
260    }
261
262    pub fn derive_compression_authority_pda(program_id: &Pubkey, version: u16) -> (Pubkey, u8) {
263        let seeds = Self::get_compression_authority_seeds(version);
264        let seeds_refs: [&[u8]; 2] = [seeds[0].as_slice(), seeds[1].as_slice()];
265        Pubkey::find_program_address(&seeds_refs, program_id)
266    }
267
268    pub fn derive_rent_sponsor_pda(program_id: &Pubkey, version: u16) -> (Pubkey, u8) {
269        let seeds = Self::get_rent_sponsor_seeds(version);
270        let seeds_refs: [&[u8]; 2] = [seeds[0].as_slice(), seeds[1].as_slice()];
271        Pubkey::find_program_address(&seeds_refs, program_id)
272    }
273
274    /// Derives the default ctoken compression authority PDA (version = 1)
275    pub fn light_token_v1_compression_authority_pda() -> Pubkey {
276        Self::derive_compression_authority_pda(
277            &pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX"),
278            1,
279        )
280        .0
281    }
282    /// Derives the default ctoken rent sponsor PDA (version = 1)
283    pub fn light_token_v1_rent_sponsor_pda() -> Pubkey {
284        Self::derive_rent_sponsor_pda(&pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"), 1).0
285    }
286}