Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions forester/tests/e2e_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,11 +389,15 @@ async fn e2e_test() {

// Create compressible token account with 0 epochs rent (instantly compressible)
// This account is picked up by the bootstrapping process
// First create a mint on-chain (required for token account creation)
let compressible_mint_keypair = Keypair::new();
let compressible_mint =
create_mint_helper_with_keypair(&mut rpc, &payer, &compressible_mint_keypair).await;
let compressible_account_bootstrap = create_compressible_token_account(
&mut rpc,
CreateCompressibleTokenAccountInputs {
owner: Keypair::new().pubkey(),
mint: Pubkey::new_unique(),
mint: compressible_mint,
num_prepaid_epochs: 0,
payer: &payer,
token_account_keypair: None,
Expand Down Expand Up @@ -439,11 +443,16 @@ async fn e2e_test() {

// Create 2nd compressible token account with 0 epochs rent (instantly compressible)
// This account is picked up by the subscriber
// First create a mint on-chain (required for token account creation)
let compressible_mint_keypair_subscriber = Keypair::new();
let compressible_mint_subscriber =
create_mint_helper_with_keypair(&mut rpc, &payer, &compressible_mint_keypair_subscriber)
.await;
let compressible_account_subscriber = create_compressible_token_account(
&mut rpc,
CreateCompressibleTokenAccountInputs {
owner: Keypair::new().pubkey(),
mint: Pubkey::new_unique(),
mint: compressible_mint_subscriber,
num_prepaid_epochs: 0,
payer: &payer,
token_account_keypair: None,
Expand Down
89 changes: 74 additions & 15 deletions forester/tests/test_compressible_ctoken.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ use forester_utils::{
rpc_pool::{SolanaRpcPool, SolanaRpcPoolBuilder},
};
use light_client::{
indexer::{AddressWithTree, Indexer},
local_test_validator::{spawn_validator, LightValidatorConfig},
rpc::{LightClient, LightClientConfig, Rpc},
};
use light_compressed_token_sdk::compressed_token::create_compressed_mint;
use light_compressed_token_sdk::compressed_token::create_compressed_mint::{
derive_mint_compressed_address, find_mint_address,
};
use light_registry::{
protocol_config::state::ProtocolConfigPda,
sdk::{
Expand All @@ -24,11 +27,77 @@ use light_registry::{
use light_test_utils::actions::legacy::{
create_compressible_token_account, CreateCompressibleTokenAccountInputs,
};
use light_token::instruction::{CreateMint, CreateMintParams};
use light_token_interface::state::TokenDataVersion;
use serial_test::serial;
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::Transaction};
use tokio::{sync::oneshot, time::sleep};

/// Helper to create a decompressed mint (auto-decompresses on creation).
/// Returns (mint_pda, mint_seed).
async fn create_decompressed_mint(
rpc: &mut LightClient,
payer: &Keypair,
mint_authority: Pubkey,
decimals: u8,
) -> (Pubkey, Keypair) {
let mint_seed = Keypair::new();
let address_tree = rpc.get_address_tree_v2();
let output_queue = rpc.get_random_state_tree_info().unwrap().queue;

// Derive compression address
let compression_address =
derive_mint_compressed_address(&mint_seed.pubkey(), &address_tree.tree);

let (mint_pda, bump) = find_mint_address(&mint_seed.pubkey());

// Get validity proof for the address
let rpc_result = rpc
.get_validity_proof(
vec![],
vec![AddressWithTree {
address: compression_address,
tree: address_tree.tree,
}],
None,
)
.await
.unwrap()
.value;

// Build params - rent_payment = 2 is the minimum required by the program
let params = CreateMintParams {
decimals,
address_merkle_tree_root_index: rpc_result.addresses[0].root_index,
mint_authority,
proof: rpc_result.proof.0.unwrap(),
compression_address,
mint: mint_pda,
bump,
freeze_authority: None,
extensions: None,
rent_payment: 2, // Minimum required epochs of rent prepayment
write_top_up: 0,
};

// Create instruction
let create_mint_builder = CreateMint::new(
params,
mint_seed.pubkey(),
payer.pubkey(),
address_tree.tree,
output_queue,
);
let instruction = create_mint_builder.instruction().unwrap();

// Send transaction
rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_seed])
.await
.expect("CreateMint should succeed");

(mint_pda, mint_seed)
}

/// Context returned from forester registration containing everything needed for compression testing
struct ForesterContext {
forester_keypair: Keypair,
Expand Down Expand Up @@ -231,13 +300,8 @@ async fn test_compressible_ctoken_compression() {
});

sleep(Duration::from_secs(2)).await;
// Create mint
let mint_seed = Keypair::new();
let address_tree = rpc.get_address_tree_v2().tree;
let mint = Pubkey::from(create_compressed_mint::derive_mint_compressed_address(
&mint_seed.pubkey(),
&address_tree,
));
// Create mint (must exist on-chain before creating token accounts)
let (mint, _mint_seed) = create_decompressed_mint(&mut rpc, &payer, payer.pubkey(), 9).await;
// Create first account with 2 epochs rent
let owner_keypair = Keypair::new();
let token_account_pubkey = create_compressible_token_account(
Expand Down Expand Up @@ -404,13 +468,8 @@ async fn test_compressible_ctoken_bootstrap() {
})
.count();

// Create mint
let mint_seed = Keypair::new();
let address_tree = rpc.get_address_tree_v2().tree;
let mint = Pubkey::from(create_compressed_mint::derive_mint_compressed_address(
&mint_seed.pubkey(),
&address_tree,
));
// Create mint (must exist on-chain before creating token accounts)
let (mint, _mint_seed) = create_decompressed_mint(&mut rpc, &payer, payer.pubkey(), 9).await;

// Create 3 compressible token accounts BEFORE bootstrap runs
let mut created_pubkeys = vec![];
Expand Down
113 changes: 113 additions & 0 deletions js/compressed-token/src/v3/actions/decompress-mint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {
ComputeBudgetProgram,
ConfirmOptions,
PublicKey,
Signer,
TransactionSignature,
} from '@solana/web3.js';
import {
Rpc,
buildAndSignTx,
sendAndConfirmTx,
DerivationMode,
bn,
CTOKEN_PROGRAM_ID,
assertBetaEnabled,
} from '@lightprotocol/stateless.js';
import { createDecompressMintInstruction } from '../instructions/decompress-mint';
import { getMintInterface } from '../get-mint-interface';

export interface DecompressMintParams {
/** Number of epochs to prepay rent (minimum 2, default: 16 for ~24 hours) */
rentPayment?: number;
/** Per-write top-up in lamports (default: 766 for ~2 epochs) */
writeTopUp?: number;
/** Compressible config account (default: LIGHT_TOKEN_CONFIG) */
configAccount?: PublicKey;
/** Rent sponsor PDA (default: LIGHT_TOKEN_RENT_SPONSOR) */
rentSponsor?: PublicKey;
}

/**
* Decompress a compressed mint to create the CMint Solana account.
*
* This makes the mint available on-chain, which is required before creating
* CToken associated token accounts. DecompressMint is **permissionless** -
* any account can call it.
*
* @param rpc - RPC connection
* @param payer - Fee payer (signer)
* @param mint - Mint address
* @param authority - Authority signer (can be any account, required for MintAction)
* @param params - Optional decompression parameters
* @param confirmOptions - Optional confirm options
* @returns Transaction signature
*/
export async function decompressMint(
rpc: Rpc,
payer: Signer,
mint: PublicKey,
authority?: Signer,
params?: DecompressMintParams,
confirmOptions?: ConfirmOptions,
): Promise<TransactionSignature> {
assertBetaEnabled();

// Use payer as authority if not provided (decompressMint is permissionless)
const effectiveAuthority = authority ?? payer;

const mintInterface = await getMintInterface(
rpc,
mint,
confirmOptions?.commitment,
CTOKEN_PROGRAM_ID,
);

if (!mintInterface.merkleContext) {
throw new Error('Mint does not have MerkleContext');
}

// Check if already decompressed
if (mintInterface.mintContext?.cmintDecompressed) {
throw new Error('Mint is already decompressed');
}

const validityProof = await rpc.getValidityProofV2(
[
{
hash: bn(mintInterface.merkleContext.hash),
leafIndex: mintInterface.merkleContext.leafIndex,
treeInfo: mintInterface.merkleContext.treeInfo,
proveByIndex: mintInterface.merkleContext.proveByIndex,
},
],
[],
DerivationMode.compressible,
);

const ix = createDecompressMintInstruction({
mintInterface,
authority: effectiveAuthority.publicKey,
payer: payer.publicKey,
validityProof,
rentPayment: params?.rentPayment,
writeTopUp: params?.writeTopUp,
configAccount: params?.configAccount,
rentSponsor: params?.rentSponsor,
});

const additionalSigners: Signer[] = [];
if (authority && !effectiveAuthority.publicKey.equals(payer.publicKey)) {
additionalSigners.push(effectiveAuthority);
}

const { blockhash } = await rpc.getLatestBlockhash();
const tx = buildAndSignTx(
[ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }), ix],
payer,
blockhash,
additionalSigners,
);

return await sendAndConfirmTx(rpc, tx, confirmOptions);
}
1 change: 1 addition & 0 deletions js/compressed-token/src/v3/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './create-mint-interface';
export * from './update-mint';
export * from './update-metadata';
export * from './decompress-mint';
export * from './create-associated-ctoken';
export * from './create-ata-interface';
export * from './mint-to';
Expand Down
31 changes: 7 additions & 24 deletions js/compressed-token/src/v3/actions/mint-to-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,29 @@ import {
Rpc,
buildAndSignTx,
sendAndConfirmTx,
DerivationMode,
bn,
assertBetaEnabled,
} from '@lightprotocol/stateless.js';
import { createMintToInterfaceInstruction } from '../instructions/mint-to-interface';
import { getMintInterface } from '../get-mint-interface';

/**
* Mint tokens to a decompressed/onchain token account.
* Works with SPL, Token-2022, and compressed token (c-token) mints.
* Works with SPL, Token-2022, and CToken mints.
*
* This function ONLY mints to decompressed onchain token accounts, never to compressed accounts.
* For CToken mints, the mint must be decompressed first (CMint account must exist on-chain).
*
* The signature matches the standard SPL mintTo for simplicity and consistency.
*
* @param rpc - RPC connection to use
* @param payer - Transaction fee payer
* @param mint - Mint address (SPL, Token-2022, or compressed mint)
* @param mint - Mint address (SPL, Token-2022, or CToken mint)
* @param destination - Destination token account address (must be an existing onchain token account)
* @param authority - Mint authority (can be Signer or PublicKey if multiSigners provided)
* @param amount - Amount to mint
* @param multiSigners - Optional: Multi-signature signers (default: [])
* @param confirmOptions - Optional: Transaction confirmation options
* @param programId - Optional: Token program ID (TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, or CTOKEN_PROGRAM_ID). If undefined, auto-detects.
* @param programId - Optional: Token program ID. If undefined, auto-detects.
*
* @returns Transaction signature
*/
Expand All @@ -56,23 +56,6 @@ export async function mintToInterface(
programId,
);

// Fetch validity proof if this is a compressed mint (has merkleContext)
let validityProof;
if (mintInterface.merkleContext) {
validityProof = await rpc.getValidityProofV2(
[
{
hash: bn(mintInterface.merkleContext.hash),
leafIndex: mintInterface.merkleContext.leafIndex,
treeInfo: mintInterface.merkleContext.treeInfo,
proveByIndex: mintInterface.merkleContext.proveByIndex,
},
],
[],
DerivationMode.compressible,
);
}

// Create instruction
const authorityPubkey =
authority instanceof PublicKey ? authority : authority.publicKey;
Expand All @@ -84,7 +67,7 @@ export async function mintToInterface(
authorityPubkey,
payer.publicKey,
amount,
validityProof,
undefined, // validityProof - not needed for simple CTokenMintTo
multiSignerPubkeys,
);

Expand All @@ -103,7 +86,7 @@ export async function mintToInterface(

const { blockhash } = await rpc.getLatestBlockhash();
const tx = buildAndSignTx(
[ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }), ix],
[ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }), ix],
payer,
blockhash,
signers,
Expand Down
Loading