light_compressed_token/
lib.rs

1use anchor_lang::prelude::*;
2
3pub mod constants;
4pub mod process_compress_spl_token_account;
5pub mod process_mint;
6pub mod process_transfer;
7use process_compress_spl_token_account::process_compress_spl_token_account;
8pub mod spl_compression;
9pub use process_mint::*;
10pub mod token_data;
11pub use token_data::TokenData;
12pub mod delegation;
13pub mod freeze;
14pub mod instructions;
15pub use instructions::*;
16pub mod burn;
17pub use burn::*;
18pub mod batch_compress;
19use light_compressed_account::instruction_data::cpi_context::CompressedCpiContext;
20
21use crate::process_transfer::CompressedTokenInstructionDataTransfer;
22declare_id!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m");
23
24#[cfg(not(feature = "no-entrypoint"))]
25solana_security_txt::security_txt! {
26    name: "light-compressed-token",
27    project_url: "lightprotocol.com",
28    contacts: "email:security@lightprotocol.com",
29    policy: "https://github.com/Lightprotocol/light-protocol/blob/main/SECURITY.md",
30    source_code: "https://github.com/Lightprotocol/light-protocol"
31}
32
33#[program]
34pub mod light_compressed_token {
35
36    use constants::{NOT_FROZEN, NUM_MAX_POOL_ACCOUNTS};
37    use light_zero_copy::borsh::Deserialize;
38    use spl_compression::check_spl_token_pool_derivation_with_index;
39
40    use super::*;
41
42    /// This instruction creates a token pool for a given mint. Every spl mint
43    /// can have one token pool. When a token is compressed the tokens are
44    /// transferrred to the token pool, and their compressed equivalent is
45    /// minted into a Merkle tree.
46    pub fn create_token_pool<'info>(
47        ctx: Context<'_, '_, '_, 'info, CreateTokenPoolInstruction<'info>>,
48    ) -> Result<()> {
49        create_token_pool::assert_mint_extensions(
50            &ctx.accounts.mint.to_account_info().try_borrow_data()?,
51        )
52    }
53
54    /// This instruction creates an additional token pool for a given mint.
55    /// The maximum number of token pools per mint is 5.
56    pub fn add_token_pool<'info>(
57        ctx: Context<'_, '_, '_, 'info, AddTokenPoolInstruction<'info>>,
58        token_pool_index: u8,
59    ) -> Result<()> {
60        if token_pool_index >= NUM_MAX_POOL_ACCOUNTS {
61            return err!(ErrorCode::InvalidTokenPoolBump);
62        }
63        // Check that token pool account with previous bump already exists.
64        check_spl_token_pool_derivation_with_index(
65            &ctx.accounts.mint.key().to_bytes(),
66            &ctx.accounts.existing_token_pool_pda.key(),
67            &[token_pool_index.saturating_sub(1)],
68        )
69    }
70
71    /// Mints tokens from an spl token mint to a list of compressed accounts.
72    /// Minted tokens are transferred to a pool account owned by the compressed
73    /// token program. The instruction creates one compressed output account for
74    /// every amount and pubkey input pair. A constant amount of lamports can be
75    /// transferred to each output account to enable. A use case to add lamports
76    /// to a compressed token account is to prevent spam. This is the only way
77    /// to add lamports to a compressed token account.
78    pub fn mint_to<'info>(
79        ctx: Context<'_, '_, '_, 'info, MintToInstruction<'info>>,
80        public_keys: Vec<Pubkey>,
81        amounts: Vec<u64>,
82        lamports: Option<u64>,
83    ) -> Result<()> {
84        process_mint_to_or_compress::<MINT_TO>(
85            ctx,
86            public_keys.as_slice(),
87            amounts.as_slice(),
88            lamports,
89            None,
90            None,
91        )
92    }
93
94    /// Batch compress tokens to an of recipients.
95    pub fn batch_compress<'info>(
96        ctx: Context<'_, '_, '_, 'info, MintToInstruction<'info>>,
97        inputs: Vec<u8>,
98    ) -> Result<()> {
99        let (inputs, _) = batch_compress::BatchCompressInstructionData::zero_copy_at(&inputs)
100            .map_err(ProgramError::from)?;
101        if inputs.amounts.is_some() && inputs.amount.is_some() {
102            return Err(crate::ErrorCode::AmountsAndAmountProvided.into());
103        }
104        let amounts = if let Some(amount) = inputs.amount {
105            vec![*amount; inputs.pubkeys.len()]
106        } else if let Some(amounts) = inputs.amounts {
107            amounts.to_vec()
108        } else {
109            return Err(crate::ErrorCode::NoAmount.into());
110        };
111
112        process_mint_to_or_compress::<COMPRESS>(
113            ctx,
114            inputs.pubkeys.as_slice(),
115            amounts.as_slice(),
116            inputs.lamports.map(|x| (*x).into()),
117            Some(inputs.index),
118            Some(inputs.bump),
119        )
120    }
121
122    /// Compresses the balance of an spl token account sub an optional remaining
123    /// amount. This instruction does not close the spl token account. To close
124    /// the account bundle a close spl account instruction in your transaction.
125    pub fn compress_spl_token_account<'info>(
126        ctx: Context<'_, '_, '_, 'info, TransferInstruction<'info>>,
127        owner: Pubkey,
128        remaining_amount: Option<u64>,
129        cpi_context: Option<CompressedCpiContext>,
130    ) -> Result<()> {
131        process_compress_spl_token_account(ctx, owner, remaining_amount, cpi_context)
132    }
133
134    /// Transfers compressed tokens from one account to another. All accounts
135    /// must be of the same mint. Additional spl tokens can be compressed or
136    /// decompressed. In one transaction only compression or decompression is
137    /// possible. Lamports can be transferred alongside tokens. If output token
138    /// accounts specify less lamports than inputs the remaining lamports are
139    /// transferred to an output compressed account. Signer must be owner or
140    /// delegate. If a delegated token account is transferred the delegate is
141    /// not preserved.
142    pub fn transfer<'info>(
143        ctx: Context<'_, '_, '_, 'info, TransferInstruction<'info>>,
144        inputs: Vec<u8>,
145    ) -> Result<()> {
146        let mut inputs = inputs;
147        // Borsh ignores excess bytes -> push bool false for with_transaction_hash field.
148        inputs.extend_from_slice(&[0u8; 1]);
149        let inputs: CompressedTokenInstructionDataTransfer =
150            CompressedTokenInstructionDataTransfer::deserialize(&mut inputs.as_slice())?;
151        process_transfer::process_transfer(ctx, inputs)
152    }
153
154    /// Delegates an amount to a delegate. A compressed token account is either
155    /// completely delegated or not. Prior delegates are not preserved. Cannot
156    /// be called by a delegate.
157    /// The instruction creates two output accounts:
158    /// 1. one account with delegated amount
159    /// 2. one account with remaining(change) amount
160    pub fn approve<'info>(
161        ctx: Context<'_, '_, '_, 'info, GenericInstruction<'info>>,
162        inputs: Vec<u8>,
163    ) -> Result<()> {
164        delegation::process_approve(ctx, inputs)
165    }
166
167    /// Revokes a delegation. The instruction merges all inputs into one output
168    /// account. Cannot be called by a delegate. Delegates are not preserved.
169    pub fn revoke<'info>(
170        ctx: Context<'_, '_, '_, 'info, GenericInstruction<'info>>,
171        inputs: Vec<u8>,
172    ) -> Result<()> {
173        delegation::process_revoke(ctx, inputs)
174    }
175
176    /// Freezes compressed token accounts. Inputs must not be frozen. Creates as
177    /// many outputs as inputs. Balances and delegates are preserved.
178    pub fn freeze<'info>(
179        ctx: Context<'_, '_, '_, 'info, FreezeInstruction<'info>>,
180        inputs: Vec<u8>,
181    ) -> Result<()> {
182        // Inputs are not frozen, outputs are frozen.
183        freeze::process_freeze_or_thaw::<NOT_FROZEN, true>(ctx, inputs)
184    }
185
186    /// Thaws frozen compressed token accounts. Inputs must be frozen. Creates
187    /// as many outputs as inputs. Balances and delegates are preserved.
188    pub fn thaw<'info>(
189        ctx: Context<'_, '_, '_, 'info, FreezeInstruction<'info>>,
190        inputs: Vec<u8>,
191    ) -> Result<()> {
192        // Inputs are frozen, outputs are not frozen.
193        freeze::process_freeze_or_thaw::<true, NOT_FROZEN>(ctx, inputs)
194    }
195
196    /// Burns compressed tokens and spl tokens from the pool account. Delegates
197    /// can burn tokens. The output compressed token account remains delegated.
198    /// Creates one output compressed token account.
199    pub fn burn<'info>(
200        ctx: Context<'_, '_, '_, 'info, BurnInstruction<'info>>,
201        inputs: Vec<u8>,
202    ) -> Result<()> {
203        burn::process_burn(ctx, inputs)
204    }
205
206    /// This function is a stub to allow Anchor to include the input types in
207    /// the IDL. It should not be included in production builds nor be called in
208    /// practice.
209    #[cfg(feature = "idl-build")]
210    pub fn stub_idl_build<'info>(
211        _ctx: Context<'_, '_, '_, 'info, TransferInstruction<'info>>,
212        _inputs1: CompressedTokenInstructionDataTransfer,
213        _inputs2: TokenData,
214    ) -> Result<()> {
215        Err(ErrorCode::InstructionNotCallable.into())
216    }
217}
218
219#[error_code]
220pub enum ErrorCode {
221    #[msg("public keys and amounts must be of same length")]
222    PublicKeyAmountMissmatch,
223    #[msg("ComputeInputSumFailed")]
224    ComputeInputSumFailed,
225    #[msg("ComputeOutputSumFailed")]
226    ComputeOutputSumFailed,
227    #[msg("ComputeCompressSumFailed")]
228    ComputeCompressSumFailed,
229    #[msg("ComputeDecompressSumFailed")]
230    ComputeDecompressSumFailed,
231    #[msg("SumCheckFailed")]
232    SumCheckFailed,
233    #[msg("DecompressRecipientUndefinedForDecompress")]
234    DecompressRecipientUndefinedForDecompress,
235    #[msg("CompressedPdaUndefinedForDecompress")]
236    CompressedPdaUndefinedForDecompress,
237    #[msg("DeCompressAmountUndefinedForDecompress")]
238    DeCompressAmountUndefinedForDecompress,
239    #[msg("CompressedPdaUndefinedForCompress")]
240    CompressedPdaUndefinedForCompress,
241    #[msg("DeCompressAmountUndefinedForCompress")]
242    DeCompressAmountUndefinedForCompress,
243    #[msg("DelegateSignerCheckFailed")]
244    DelegateSignerCheckFailed,
245    #[msg("Minted amount greater than u64::MAX")]
246    MintTooLarge,
247    #[msg("SplTokenSupplyMismatch")]
248    SplTokenSupplyMismatch,
249    #[msg("HeapMemoryCheckFailed")]
250    HeapMemoryCheckFailed,
251    #[msg("The instruction is not callable")]
252    InstructionNotCallable,
253    #[msg("ArithmeticUnderflow")]
254    ArithmeticUnderflow,
255    #[msg("HashToFieldError")]
256    HashToFieldError,
257    #[msg("Expected the authority to be also a mint authority")]
258    InvalidAuthorityMint,
259    #[msg("Provided authority is not the freeze authority")]
260    InvalidFreezeAuthority,
261    InvalidDelegateIndex,
262    TokenPoolPdaUndefined,
263    #[msg("Compress or decompress recipient is the same account as the token pool pda.")]
264    IsTokenPoolPda,
265    InvalidTokenPoolPda,
266    NoInputTokenAccountsProvided,
267    NoInputsProvided,
268    MintHasNoFreezeAuthority,
269    MintWithInvalidExtension,
270    #[msg("The token account balance is less than the remaining amount.")]
271    InsufficientTokenAccountBalance,
272    #[msg("Max number of token pools reached.")]
273    InvalidTokenPoolBump,
274    FailedToDecompress,
275    FailedToBurnSplTokensFromTokenPool,
276    NoMatchingBumpFound,
277    NoAmount,
278    AmountsAndAmountProvided,
279}