1use borsh::BorshDeserialize;
4use light_compressed_account::{
5 address::derive_address, instruction_data::compressed_proof::ValidityProof,
6};
7use light_token::instruction::DecompressMint;
8use light_token_interface::{
9 instructions::mint_action::{MintInstructionData, MintWithContext},
10 state::Mint,
11 MINT_ADDRESS_TREE,
12};
13use solana_account::Account;
14use solana_instruction::Instruction;
15use solana_pubkey::Pubkey;
16use thiserror::Error;
17
18use super::AccountInterface;
19use crate::indexer::{CompressedAccount, Indexer, ValidityProofWithContext};
20
21#[derive(Debug, Error)]
23pub enum DecompressMintError {
24 #[error("Mint not found for address {address:?}")]
25 MintNotFound { address: Pubkey },
26
27 #[error("Missing mint data in cold account")]
28 MissingMintData,
29
30 #[error("Program error: {0}")]
31 ProgramError(#[from] solana_program_error::ProgramError),
32
33 #[error("Mint already hot")]
34 AlreadyDecompressed,
35
36 #[error("Validity proof required for cold mint")]
37 ProofRequired,
38
39 #[error("Indexer error: {0}")]
40 IndexerError(#[from] crate::indexer::IndexerError),
41}
42
43#[derive(Debug, Clone, PartialEq, Default)]
45#[allow(clippy::large_enum_variant)]
46pub enum MintState {
47 Hot { account: Account },
49 Cold {
51 compressed: CompressedAccount,
52 mint_data: Mint,
53 },
54 #[default]
56 None,
57}
58
59#[derive(Debug, Clone, PartialEq, Default)]
61pub struct MintInterface {
62 pub mint: Pubkey,
63 pub address_tree: Pubkey,
64 pub compressed_address: [u8; 32],
65 pub state: MintState,
66}
67
68impl MintInterface {
69 #[inline]
70 pub fn is_cold(&self) -> bool {
71 matches!(self.state, MintState::Cold { .. })
72 }
73
74 #[inline]
75 pub fn is_hot(&self) -> bool {
76 matches!(self.state, MintState::Hot { .. })
77 }
78
79 pub fn hash(&self) -> Option<[u8; 32]> {
80 match &self.state {
81 MintState::Cold { compressed, .. } => Some(compressed.hash),
82 _ => None,
83 }
84 }
85
86 pub fn account(&self) -> Option<&Account> {
87 match &self.state {
88 MintState::Hot { account } => Some(account),
89 _ => None,
90 }
91 }
92
93 pub fn compressed(&self) -> Option<(&CompressedAccount, &Mint)> {
94 match &self.state {
95 MintState::Cold {
96 compressed,
97 mint_data,
98 } => Some((compressed, mint_data)),
99 _ => None,
100 }
101 }
102}
103
104impl From<MintInterface> for AccountInterface {
105 fn from(mi: MintInterface) -> Self {
106 match mi.state {
107 MintState::Hot { account } => Self {
108 key: mi.mint,
109 account,
110 cold: None,
111 },
112 MintState::Cold {
113 compressed,
114 mint_data: _,
115 } => {
116 let data = compressed
117 .data
118 .as_ref()
119 .map(|d| {
120 let mut buf = d.discriminator.to_vec();
121 buf.extend_from_slice(&d.data);
122 buf
123 })
124 .unwrap_or_default();
125
126 Self {
127 key: mi.mint,
128 account: Account {
129 lamports: compressed.lamports,
130 data,
131 owner: Pubkey::new_from_array(
132 light_token_interface::LIGHT_TOKEN_PROGRAM_ID,
133 ),
134 executable: false,
135 rent_epoch: 0,
136 },
137 cold: Some(compressed),
138 }
139 }
140 MintState::None => Self {
141 key: mi.mint,
142 account: Account::default(),
143 cold: None,
144 },
145 }
146 }
147}
148
149pub const DEFAULT_RENT_PAYMENT: u8 = 2;
150pub const DEFAULT_WRITE_TOP_UP: u32 = 0;
151
152pub fn build_decompress_mint(
154 mint: &MintInterface,
155 fee_payer: Pubkey,
156 validity_proof: Option<ValidityProofWithContext>,
157 rent_payment: Option<u8>,
158 write_top_up: Option<u32>,
159) -> Result<Vec<Instruction>, DecompressMintError> {
160 let mint_data = match &mint.state {
162 MintState::Hot { .. } | MintState::None => return Ok(vec![]),
163 MintState::Cold { mint_data, .. } => mint_data,
164 };
165
166 if mint_data.metadata.mint_decompressed {
168 return Ok(vec![]);
169 }
170
171 let proof_result = validity_proof.ok_or(DecompressMintError::ProofRequired)?;
173
174 let account_info = &proof_result.accounts[0];
176 let state_tree = account_info.tree_info.tree;
177 let input_queue = account_info.tree_info.queue;
178 let output_queue = account_info
179 .tree_info
180 .next_tree_info
181 .as_ref()
182 .map(|next| next.queue)
183 .unwrap_or(input_queue);
184
185 let mint_instruction_data = MintInstructionData::try_from(mint_data.clone())
187 .map_err(|_| DecompressMintError::MissingMintData)?;
188
189 let compressed_mint_with_context = MintWithContext {
190 leaf_index: account_info.leaf_index as u32,
191 prove_by_index: account_info.root_index.proof_by_index(),
192 root_index: account_info.root_index.root_index().unwrap_or_default(),
193 address: mint.compressed_address,
194 mint: Some(mint_instruction_data),
195 };
196
197 let decompress = DecompressMint {
199 payer: fee_payer,
200 authority: fee_payer, state_tree,
202 input_queue,
203 output_queue,
204 compressed_mint_with_context,
205 proof: ValidityProof(proof_result.proof.into()),
206 rent_payment: rent_payment.unwrap_or(DEFAULT_RENT_PAYMENT),
207 write_top_up: write_top_up.unwrap_or(DEFAULT_WRITE_TOP_UP),
208 };
209
210 let ix = decompress
211 .instruction()
212 .map_err(DecompressMintError::from)?;
213 Ok(vec![ix])
214}
215
216pub async fn decompress_mint<I: Indexer>(
218 mint: &MintInterface,
219 fee_payer: Pubkey,
220 indexer: &I,
221) -> Result<Vec<Instruction>, DecompressMintError> {
222 let hash = match mint.hash() {
224 Some(h) => h,
225 None => return Ok(vec![]),
226 };
227
228 if let Some((_, mint_data)) = mint.compressed() {
230 if mint_data.metadata.mint_decompressed {
231 return Ok(vec![]);
232 }
233 }
234
235 let proof = indexer
237 .get_validity_proof(vec![hash], vec![], None)
238 .await?
239 .value;
240
241 build_decompress_mint(mint, fee_payer, Some(proof), None, None)
243}
244
245#[derive(Debug, Clone)]
247pub struct DecompressMintRequest {
248 pub mint_seed_pubkey: Pubkey,
249 pub address_tree: Option<Pubkey>,
250 pub rent_payment: Option<u8>,
251 pub write_top_up: Option<u32>,
252}
253
254impl DecompressMintRequest {
255 pub fn new(mint_seed_pubkey: Pubkey) -> Self {
256 Self {
257 mint_seed_pubkey,
258 address_tree: None,
259 rent_payment: None,
260 write_top_up: None,
261 }
262 }
263
264 pub fn with_address_tree(mut self, address_tree: Pubkey) -> Self {
265 self.address_tree = Some(address_tree);
266 self
267 }
268
269 pub fn with_rent_payment(mut self, rent_payment: u8) -> Self {
270 self.rent_payment = Some(rent_payment);
271 self
272 }
273
274 pub fn with_write_top_up(mut self, write_top_up: u32) -> Self {
275 self.write_top_up = Some(write_top_up);
276 self
277 }
278}
279
280pub async fn decompress_mint_idempotent<I: Indexer>(
282 request: DecompressMintRequest,
283 fee_payer: Pubkey,
284 indexer: &I,
285) -> Result<Vec<Instruction>, DecompressMintError> {
286 let address_tree = request
288 .address_tree
289 .unwrap_or(Pubkey::new_from_array(MINT_ADDRESS_TREE));
290 let compressed_address = derive_address(
291 &request.mint_seed_pubkey.to_bytes(),
292 &address_tree.to_bytes(),
293 &light_token_interface::LIGHT_TOKEN_PROGRAM_ID,
294 );
295
296 let compressed_account = indexer
298 .get_compressed_account(compressed_address, None)
299 .await?
300 .value
301 .ok_or(DecompressMintError::MintNotFound {
302 address: request.mint_seed_pubkey,
303 })?;
304
305 let data = match compressed_account.data.as_ref() {
307 Some(d) if !d.data.is_empty() => d,
308 _ => return Ok(vec![]), };
310
311 let mint_data =
313 Mint::try_from_slice(&data.data).map_err(|_| DecompressMintError::MissingMintData)?;
314
315 if mint_data.metadata.mint_decompressed {
317 return Ok(vec![]);
318 }
319
320 let proof_result = indexer
322 .get_validity_proof(vec![compressed_account.hash], vec![], None)
323 .await?
324 .value;
325
326 let account_info = &proof_result.accounts[0];
328 let state_tree = account_info.tree_info.tree;
329 let input_queue = account_info.tree_info.queue;
330 let output_queue = account_info
331 .tree_info
332 .next_tree_info
333 .as_ref()
334 .map(|next| next.queue)
335 .unwrap_or(input_queue);
336
337 let mint_instruction_data = MintInstructionData::try_from(mint_data)
339 .map_err(|_| DecompressMintError::MissingMintData)?;
340
341 let compressed_mint_with_context = MintWithContext {
342 leaf_index: account_info.leaf_index as u32,
343 prove_by_index: account_info.root_index.proof_by_index(),
344 root_index: account_info.root_index.root_index().unwrap_or_default(),
345 address: compressed_address,
346 mint: Some(mint_instruction_data),
347 };
348
349 let decompress = DecompressMint {
351 payer: fee_payer,
352 authority: fee_payer, state_tree,
354 input_queue,
355 output_queue,
356 compressed_mint_with_context,
357 proof: ValidityProof(proof_result.proof.into()),
358 rent_payment: request.rent_payment.unwrap_or(DEFAULT_RENT_PAYMENT),
359 write_top_up: request.write_top_up.unwrap_or(DEFAULT_WRITE_TOP_UP),
360 };
361
362 let ix = decompress
363 .instruction()
364 .map_err(DecompressMintError::from)?;
365 Ok(vec![ix])
366}
367
368pub fn create_mint_interface(
370 address: Pubkey,
371 address_tree: Pubkey,
372 onchain_account: Option<Account>,
373 compressed: Option<(CompressedAccount, Mint)>,
374) -> MintInterface {
375 let compressed_address = light_compressed_account::address::derive_address(
376 &address.to_bytes(),
377 &address_tree.to_bytes(),
378 &light_token_interface::LIGHT_TOKEN_PROGRAM_ID,
379 );
380
381 let state = if let Some(account) = onchain_account {
382 MintState::Hot { account }
383 } else if let Some((compressed, mint_data)) = compressed {
384 MintState::Cold {
385 compressed,
386 mint_data,
387 }
388 } else {
389 MintState::None
390 };
391
392 MintInterface {
393 mint: address,
394 address_tree,
395 compressed_address,
396 state,
397 }
398}