use borsh::BorshDeserialize;
use light_compressed_account::{
address::derive_address, instruction_data::compressed_proof::ValidityProof,
};
use light_token::instruction::DecompressMint;
use light_token_interface::{
instructions::mint_action::{MintInstructionData, MintWithContext},
state::Mint,
MINT_ADDRESS_TREE,
};
use solana_account::Account;
use solana_instruction::Instruction;
use solana_pubkey::Pubkey;
use thiserror::Error;
use super::AccountInterface;
use crate::indexer::{CompressedAccount, Indexer, ValidityProofWithContext};
#[derive(Debug, Error)]
pub enum DecompressMintError {
#[error("Mint not found for address {address:?}")]
MintNotFound { address: Pubkey },
#[error("Missing mint data in cold account")]
MissingMintData,
#[error("Program error: {0}")]
ProgramError(#[from] solana_program_error::ProgramError),
#[error("Mint already hot")]
AlreadyDecompressed,
#[error("Validity proof required for cold mint")]
ProofRequired,
#[error("Indexer error: {0}")]
IndexerError(#[from] crate::indexer::IndexerError),
}
#[derive(Debug, Clone, PartialEq, Default)]
#[allow(clippy::large_enum_variant)]
pub enum MintState {
Hot { account: Account },
Cold {
compressed: CompressedAccount,
mint_data: Mint,
},
#[default]
None,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct MintInterface {
pub mint: Pubkey,
pub address_tree: Pubkey,
pub compressed_address: [u8; 32],
pub state: MintState,
}
impl MintInterface {
#[inline]
pub fn is_cold(&self) -> bool {
matches!(self.state, MintState::Cold { .. })
}
#[inline]
pub fn is_hot(&self) -> bool {
matches!(self.state, MintState::Hot { .. })
}
pub fn hash(&self) -> Option<[u8; 32]> {
match &self.state {
MintState::Cold { compressed, .. } => Some(compressed.hash),
_ => None,
}
}
pub fn account(&self) -> Option<&Account> {
match &self.state {
MintState::Hot { account } => Some(account),
_ => None,
}
}
pub fn compressed(&self) -> Option<(&CompressedAccount, &Mint)> {
match &self.state {
MintState::Cold {
compressed,
mint_data,
} => Some((compressed, mint_data)),
_ => None,
}
}
}
impl From<MintInterface> for AccountInterface {
fn from(mi: MintInterface) -> Self {
match mi.state {
MintState::Hot { account } => Self {
key: mi.mint,
account,
cold: None,
},
MintState::Cold {
compressed,
mint_data: _,
} => {
let data = compressed
.data
.as_ref()
.map(|d| {
let mut buf = d.discriminator.to_vec();
buf.extend_from_slice(&d.data);
buf
})
.unwrap_or_default();
Self {
key: mi.mint,
account: Account {
lamports: compressed.lamports,
data,
owner: Pubkey::new_from_array(
light_token_interface::LIGHT_TOKEN_PROGRAM_ID,
),
executable: false,
rent_epoch: 0,
},
cold: Some(compressed),
}
}
MintState::None => Self {
key: mi.mint,
account: Account::default(),
cold: None,
},
}
}
}
pub const DEFAULT_RENT_PAYMENT: u8 = 2;
pub const DEFAULT_WRITE_TOP_UP: u32 = 0;
pub fn build_decompress_mint(
mint: &MintInterface,
fee_payer: Pubkey,
validity_proof: Option<ValidityProofWithContext>,
rent_payment: Option<u8>,
write_top_up: Option<u32>,
) -> Result<Vec<Instruction>, DecompressMintError> {
let mint_data = match &mint.state {
MintState::Hot { .. } | MintState::None => return Ok(vec![]),
MintState::Cold { mint_data, .. } => mint_data,
};
if mint_data.metadata.mint_decompressed {
return Ok(vec![]);
}
let proof_result = validity_proof.ok_or(DecompressMintError::ProofRequired)?;
let account_info = &proof_result.accounts[0];
let state_tree = account_info.tree_info.tree;
let input_queue = account_info.tree_info.queue;
let output_queue = account_info
.tree_info
.next_tree_info
.as_ref()
.map(|next| next.queue)
.unwrap_or(input_queue);
let mint_instruction_data = MintInstructionData::try_from(mint_data.clone())
.map_err(|_| DecompressMintError::MissingMintData)?;
let compressed_mint_with_context = MintWithContext {
leaf_index: account_info.leaf_index as u32,
prove_by_index: account_info.root_index.proof_by_index(),
root_index: account_info.root_index.root_index().unwrap_or_default(),
address: mint.compressed_address,
mint: Some(mint_instruction_data),
};
let decompress = DecompressMint {
payer: fee_payer,
authority: fee_payer, state_tree,
input_queue,
output_queue,
compressed_mint_with_context,
proof: ValidityProof(proof_result.proof.into()),
rent_payment: rent_payment.unwrap_or(DEFAULT_RENT_PAYMENT),
write_top_up: write_top_up.unwrap_or(DEFAULT_WRITE_TOP_UP),
};
let ix = decompress
.instruction()
.map_err(DecompressMintError::from)?;
Ok(vec![ix])
}
pub async fn decompress_mint<I: Indexer>(
mint: &MintInterface,
fee_payer: Pubkey,
indexer: &I,
) -> Result<Vec<Instruction>, DecompressMintError> {
let hash = match mint.hash() {
Some(h) => h,
None => return Ok(vec![]),
};
if let Some((_, mint_data)) = mint.compressed() {
if mint_data.metadata.mint_decompressed {
return Ok(vec![]);
}
}
let proof = indexer
.get_validity_proof(vec![hash], vec![], None)
.await?
.value;
build_decompress_mint(mint, fee_payer, Some(proof), None, None)
}
#[derive(Debug, Clone)]
pub struct DecompressMintRequest {
pub mint_seed_pubkey: Pubkey,
pub address_tree: Option<Pubkey>,
pub rent_payment: Option<u8>,
pub write_top_up: Option<u32>,
}
impl DecompressMintRequest {
pub fn new(mint_seed_pubkey: Pubkey) -> Self {
Self {
mint_seed_pubkey,
address_tree: None,
rent_payment: None,
write_top_up: None,
}
}
pub fn with_address_tree(mut self, address_tree: Pubkey) -> Self {
self.address_tree = Some(address_tree);
self
}
pub fn with_rent_payment(mut self, rent_payment: u8) -> Self {
self.rent_payment = Some(rent_payment);
self
}
pub fn with_write_top_up(mut self, write_top_up: u32) -> Self {
self.write_top_up = Some(write_top_up);
self
}
}
pub async fn decompress_mint_idempotent<I: Indexer>(
request: DecompressMintRequest,
fee_payer: Pubkey,
indexer: &I,
) -> Result<Vec<Instruction>, DecompressMintError> {
let address_tree = request
.address_tree
.unwrap_or(Pubkey::new_from_array(MINT_ADDRESS_TREE));
let compressed_address = derive_address(
&request.mint_seed_pubkey.to_bytes(),
&address_tree.to_bytes(),
&light_token_interface::LIGHT_TOKEN_PROGRAM_ID,
);
let compressed_account = indexer
.get_compressed_account(compressed_address, None)
.await?
.value
.ok_or(DecompressMintError::MintNotFound {
address: request.mint_seed_pubkey,
})?;
let data = match compressed_account.data.as_ref() {
Some(d) if !d.data.is_empty() => d,
_ => return Ok(vec![]), };
let mint_data =
Mint::try_from_slice(&data.data).map_err(|_| DecompressMintError::MissingMintData)?;
if mint_data.metadata.mint_decompressed {
return Ok(vec![]);
}
let proof_result = indexer
.get_validity_proof(vec![compressed_account.hash], vec![], None)
.await?
.value;
let account_info = &proof_result.accounts[0];
let state_tree = account_info.tree_info.tree;
let input_queue = account_info.tree_info.queue;
let output_queue = account_info
.tree_info
.next_tree_info
.as_ref()
.map(|next| next.queue)
.unwrap_or(input_queue);
let mint_instruction_data = MintInstructionData::try_from(mint_data)
.map_err(|_| DecompressMintError::MissingMintData)?;
let compressed_mint_with_context = MintWithContext {
leaf_index: account_info.leaf_index as u32,
prove_by_index: account_info.root_index.proof_by_index(),
root_index: account_info.root_index.root_index().unwrap_or_default(),
address: compressed_address,
mint: Some(mint_instruction_data),
};
let decompress = DecompressMint {
payer: fee_payer,
authority: fee_payer, state_tree,
input_queue,
output_queue,
compressed_mint_with_context,
proof: ValidityProof(proof_result.proof.into()),
rent_payment: request.rent_payment.unwrap_or(DEFAULT_RENT_PAYMENT),
write_top_up: request.write_top_up.unwrap_or(DEFAULT_WRITE_TOP_UP),
};
let ix = decompress
.instruction()
.map_err(DecompressMintError::from)?;
Ok(vec![ix])
}
pub fn create_mint_interface(
address: Pubkey,
address_tree: Pubkey,
onchain_account: Option<Account>,
compressed: Option<(CompressedAccount, Mint)>,
) -> MintInterface {
let compressed_address = light_compressed_account::address::derive_address(
&address.to_bytes(),
&address_tree.to_bytes(),
&light_token_interface::LIGHT_TOKEN_PROGRAM_ID,
);
let state = if let Some(account) = onchain_account {
MintState::Hot { account }
} else if let Some((compressed, mint_data)) = compressed {
MintState::Cold {
compressed,
mint_data,
}
} else {
MintState::None
};
MintInterface {
mint: address,
address_tree,
compressed_address,
state,
}
}