1use borsh::BorshDeserialize;
4use light_compressed_account::instruction_data::compressed_proof::ValidityProof;
5use light_token::instruction::{derive_mint_compressed_address, DecompressMint};
6use light_token_interface::{
7 instructions::mint_action::{MintInstructionData, MintWithContext},
8 state::Mint,
9 MINT_ADDRESS_TREE,
10};
11use solana_account::Account;
12use solana_instruction::Instruction;
13use solana_pubkey::Pubkey;
14use thiserror::Error;
15
16use crate::indexer::{CompressedAccount, Indexer, ValidityProofWithContext};
17
18#[derive(Debug, Error)]
20pub enum DecompressMintError {
21 #[error("Mint not found for address {address:?}")]
22 MintNotFound { address: Pubkey },
23
24 #[error("Missing mint data in cold account")]
25 MissingMintData,
26
27 #[error("Program error: {0}")]
28 ProgramError(#[from] solana_program_error::ProgramError),
29
30 #[error("Mint already hot")]
31 AlreadyDecompressed,
32
33 #[error("Validity proof required for cold mint")]
34 ProofRequired,
35
36 #[error("Indexer error: {0}")]
37 IndexerError(#[from] crate::indexer::IndexerError),
38}
39
40#[derive(Debug, Clone)]
42#[allow(clippy::large_enum_variant)]
43pub enum MintState {
44 Hot { account: Account },
46 Cold {
48 compressed: CompressedAccount,
49 mint_data: Mint,
50 },
51 None,
53}
54
55#[derive(Debug, Clone)]
57pub struct MintInterface {
58 pub mint: Pubkey,
59 pub address_tree: Pubkey,
60 pub compressed_address: [u8; 32],
61 pub state: MintState,
62}
63
64impl MintInterface {
65 #[inline]
66 pub fn is_cold(&self) -> bool {
67 matches!(self.state, MintState::Cold { .. })
68 }
69
70 #[inline]
71 pub fn is_hot(&self) -> bool {
72 matches!(self.state, MintState::Hot { .. })
73 }
74
75 pub fn hash(&self) -> Option<[u8; 32]> {
76 match &self.state {
77 MintState::Cold { compressed, .. } => Some(compressed.hash),
78 _ => None,
79 }
80 }
81
82 pub fn account(&self) -> Option<&Account> {
83 match &self.state {
84 MintState::Hot { account } => Some(account),
85 _ => None,
86 }
87 }
88
89 pub fn compressed(&self) -> Option<(&CompressedAccount, &Mint)> {
90 match &self.state {
91 MintState::Cold {
92 compressed,
93 mint_data,
94 } => Some((compressed, mint_data)),
95 _ => None,
96 }
97 }
98}
99
100pub const DEFAULT_RENT_PAYMENT: u8 = 2;
101pub const DEFAULT_WRITE_TOP_UP: u32 = 0;
102
103pub fn build_decompress_mint(
105 mint: &MintInterface,
106 fee_payer: Pubkey,
107 validity_proof: Option<ValidityProofWithContext>,
108 rent_payment: Option<u8>,
109 write_top_up: Option<u32>,
110) -> Result<Vec<Instruction>, DecompressMintError> {
111 let mint_data = match &mint.state {
113 MintState::Hot { .. } | MintState::None => return Ok(vec![]),
114 MintState::Cold { mint_data, .. } => mint_data,
115 };
116
117 if mint_data.metadata.mint_decompressed {
119 return Ok(vec![]);
120 }
121
122 let proof_result = validity_proof.ok_or(DecompressMintError::ProofRequired)?;
124
125 let account_info = &proof_result.accounts[0];
127 let state_tree = account_info.tree_info.tree;
128 let input_queue = account_info.tree_info.queue;
129 let output_queue = account_info
130 .tree_info
131 .next_tree_info
132 .as_ref()
133 .map(|next| next.queue)
134 .unwrap_or(input_queue);
135
136 let mint_instruction_data = MintInstructionData::try_from(mint_data.clone())
138 .map_err(|_| DecompressMintError::MissingMintData)?;
139
140 let compressed_mint_with_context = MintWithContext {
141 leaf_index: account_info.leaf_index as u32,
142 prove_by_index: account_info.root_index.proof_by_index(),
143 root_index: account_info.root_index.root_index().unwrap_or_default(),
144 address: mint.compressed_address,
145 mint: Some(mint_instruction_data),
146 };
147
148 let decompress = DecompressMint {
150 payer: fee_payer,
151 authority: fee_payer, state_tree,
153 input_queue,
154 output_queue,
155 compressed_mint_with_context,
156 proof: ValidityProof(proof_result.proof.into()),
157 rent_payment: rent_payment.unwrap_or(DEFAULT_RENT_PAYMENT),
158 write_top_up: write_top_up.unwrap_or(DEFAULT_WRITE_TOP_UP),
159 };
160
161 let ix = decompress
162 .instruction()
163 .map_err(DecompressMintError::from)?;
164 Ok(vec![ix])
165}
166
167pub async fn decompress_mint<I: Indexer>(
169 mint: &MintInterface,
170 fee_payer: Pubkey,
171 indexer: &I,
172) -> Result<Vec<Instruction>, DecompressMintError> {
173 let hash = match mint.hash() {
175 Some(h) => h,
176 None => return Ok(vec![]),
177 };
178
179 if let Some((_, mint_data)) = mint.compressed() {
181 if mint_data.metadata.mint_decompressed {
182 return Ok(vec![]);
183 }
184 }
185
186 let proof = indexer
188 .get_validity_proof(vec![hash], vec![], None)
189 .await?
190 .value;
191
192 build_decompress_mint(mint, fee_payer, Some(proof), None, None)
194}
195
196#[derive(Debug, Clone)]
198pub struct DecompressMintRequest {
199 pub mint_seed_pubkey: Pubkey,
200 pub address_tree: Option<Pubkey>,
201 pub rent_payment: Option<u8>,
202 pub write_top_up: Option<u32>,
203}
204
205impl DecompressMintRequest {
206 pub fn new(mint_seed_pubkey: Pubkey) -> Self {
207 Self {
208 mint_seed_pubkey,
209 address_tree: None,
210 rent_payment: None,
211 write_top_up: None,
212 }
213 }
214
215 pub fn with_address_tree(mut self, address_tree: Pubkey) -> Self {
216 self.address_tree = Some(address_tree);
217 self
218 }
219
220 pub fn with_rent_payment(mut self, rent_payment: u8) -> Self {
221 self.rent_payment = Some(rent_payment);
222 self
223 }
224
225 pub fn with_write_top_up(mut self, write_top_up: u32) -> Self {
226 self.write_top_up = Some(write_top_up);
227 self
228 }
229}
230
231pub async fn decompress_mint_idempotent<I: Indexer>(
233 request: DecompressMintRequest,
234 fee_payer: Pubkey,
235 indexer: &I,
236) -> Result<Vec<Instruction>, DecompressMintError> {
237 let address_tree = request
239 .address_tree
240 .unwrap_or(Pubkey::new_from_array(MINT_ADDRESS_TREE));
241 let compressed_address =
242 derive_mint_compressed_address(&request.mint_seed_pubkey, &address_tree);
243
244 let compressed_account = indexer
246 .get_compressed_account(compressed_address, None)
247 .await?
248 .value
249 .ok_or(DecompressMintError::MintNotFound {
250 address: request.mint_seed_pubkey,
251 })?;
252
253 let data = match compressed_account.data.as_ref() {
255 Some(d) if !d.data.is_empty() => d,
256 _ => return Ok(vec![]), };
258
259 let mint_data =
261 Mint::try_from_slice(&data.data).map_err(|_| DecompressMintError::MissingMintData)?;
262
263 if mint_data.metadata.mint_decompressed {
265 return Ok(vec![]);
266 }
267
268 let proof_result = indexer
270 .get_validity_proof(vec![compressed_account.hash], vec![], None)
271 .await?
272 .value;
273
274 let account_info = &proof_result.accounts[0];
276 let state_tree = account_info.tree_info.tree;
277 let input_queue = account_info.tree_info.queue;
278 let output_queue = account_info
279 .tree_info
280 .next_tree_info
281 .as_ref()
282 .map(|next| next.queue)
283 .unwrap_or(input_queue);
284
285 let mint_instruction_data = MintInstructionData::try_from(mint_data)
287 .map_err(|_| DecompressMintError::MissingMintData)?;
288
289 let compressed_mint_with_context = MintWithContext {
290 leaf_index: account_info.leaf_index as u32,
291 prove_by_index: account_info.root_index.proof_by_index(),
292 root_index: account_info.root_index.root_index().unwrap_or_default(),
293 address: compressed_address,
294 mint: Some(mint_instruction_data),
295 };
296
297 let decompress = DecompressMint {
299 payer: fee_payer,
300 authority: fee_payer, state_tree,
302 input_queue,
303 output_queue,
304 compressed_mint_with_context,
305 proof: ValidityProof(proof_result.proof.into()),
306 rent_payment: request.rent_payment.unwrap_or(DEFAULT_RENT_PAYMENT),
307 write_top_up: request.write_top_up.unwrap_or(DEFAULT_WRITE_TOP_UP),
308 };
309
310 let ix = decompress
311 .instruction()
312 .map_err(DecompressMintError::from)?;
313 Ok(vec![ix])
314}
315
316pub fn create_mint_interface(
318 address: Pubkey,
319 address_tree: Pubkey,
320 onchain_account: Option<Account>,
321 compressed: Option<(CompressedAccount, Mint)>,
322) -> MintInterface {
323 let compressed_address = light_compressed_account::address::derive_address(
324 &address.to_bytes(),
325 &address_tree.to_bytes(),
326 &light_token_interface::LIGHT_TOKEN_PROGRAM_ID,
327 );
328
329 let state = if let Some(account) = onchain_account {
330 MintState::Hot { account }
331 } else if let Some((compressed, mint_data)) = compressed {
332 MintState::Cold {
333 compressed,
334 mint_data,
335 }
336 } else {
337 MintState::None
338 };
339
340 MintInterface {
341 mint: address,
342 address_tree,
343 compressed_address,
344 state,
345 }
346}