light_token/compressed_token/v2/transfer2/
instruction.rs

1use light_compressed_account::instruction_data::compressed_proof::ValidityProof;
2use light_program_profiler::profile;
3use light_token_interface::{
4    instructions::{
5        extensions::ExtensionInstructionData,
6        transfer2::{CompressedCpiContext, CompressedTokenInstructionDataTransfer2},
7    },
8    LIGHT_TOKEN_PROGRAM_ID, TRANSFER2,
9};
10use solana_instruction::Instruction;
11use solana_pubkey::Pubkey;
12
13use super::account_metas::{get_transfer2_instruction_account_metas, Transfer2AccountsMetaConfig};
14use crate::{
15    compressed_token::CTokenAccount2,
16    error::{Result, TokenSdkError},
17    AnchorSerialize,
18};
19
20#[derive(Debug, Default, PartialEq, Copy, Clone)]
21pub struct Transfer2Config {
22    pub cpi_context: Option<CompressedCpiContext>,
23    pub with_transaction_hash: bool,
24    pub sol_pool_pda: bool,
25    pub sol_decompression_recipient: Option<Pubkey>,
26    pub filter_zero_amount_outputs: bool,
27    /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (0 = no limit)
28    pub max_top_up: u16,
29}
30
31impl Transfer2Config {
32    pub fn new() -> Self {
33        Default::default()
34    }
35
36    pub fn with_cpi_context(mut self, cpi_context: CompressedCpiContext) -> Self {
37        self.cpi_context = Some(cpi_context);
38        self
39    }
40
41    pub fn with_transaction_hash(mut self) -> Self {
42        self.with_transaction_hash = true;
43        self
44    }
45
46    pub fn with_sol_pool(mut self, sol_decompression_recipient: Pubkey) -> Self {
47        self.sol_pool_pda = true;
48        self.sol_decompression_recipient = Some(sol_decompression_recipient);
49        self
50    }
51
52    pub fn filter_zero_amount_outputs(mut self) -> Self {
53        self.filter_zero_amount_outputs = true;
54        self
55    }
56
57    /// Set maximum per-account top-up lamports (0 = no limit)
58    pub fn with_max_top_up(mut self, max_top_up: u16) -> Self {
59        self.max_top_up = max_top_up;
60        self
61    }
62}
63
64/// Multi-transfer input parameters
65#[derive(Debug, Clone, PartialEq, Default)]
66pub struct Transfer2Inputs {
67    pub token_accounts: Vec<CTokenAccount2>,
68    pub validity_proof: ValidityProof,
69    pub transfer_config: Transfer2Config,
70    pub meta_config: Transfer2AccountsMetaConfig,
71    // pub tree_pubkeys: Vec<Pubkey>,
72    // pub packed_pubkeys: Vec<Pubkey>, // Owners, Delegates, Mints
73    pub in_lamports: Option<Vec<u64>>,
74    pub out_lamports: Option<Vec<u64>>,
75    pub output_queue: u8,
76    /// TLV extensions for input compressed accounts (one Vec per input account).
77    /// Used to pass extension state (e.g., CompressedOnly) for decompress operations.
78    pub in_tlv: Option<Vec<Vec<ExtensionInstructionData>>>,
79}
80
81/// Create the instruction for compressed token multi-transfer operations
82#[profile]
83pub fn create_transfer2_instruction(inputs: Transfer2Inputs) -> Result<Instruction> {
84    let Transfer2Inputs {
85        token_accounts,
86        validity_proof,
87        transfer_config,
88        meta_config,
89        in_lamports,
90        out_lamports,
91        output_queue,
92        in_tlv,
93    } = inputs;
94    let mut input_token_data_with_context = Vec::new();
95    let mut output_compressed_accounts = Vec::new();
96    let mut collected_compressions = Vec::new();
97
98    // Process each token account and convert to multi-transfer format
99    for token_account in token_accounts {
100        // Collect compression if present
101        if let Some(compression) = token_account.compression() {
102            collected_compressions.push(*compression);
103        }
104        let (inputs, output) = token_account.into_inputs_and_outputs();
105        // Collect inputs directly (they're already in the right format)
106        input_token_data_with_context.extend(inputs);
107
108        // Add output if not zero amount (when filtering is enabled)
109        if !transfer_config.filter_zero_amount_outputs || output.amount > 0 {
110            output_compressed_accounts.push(output);
111        }
112    }
113
114    // Create instruction data
115    let instruction_data = CompressedTokenInstructionDataTransfer2 {
116        with_transaction_hash: transfer_config.with_transaction_hash,
117        with_lamports_change_account_merkle_tree_index: false, // TODO: support in future
118        lamports_change_account_merkle_tree_index: 0,
119        lamports_change_account_owner_index: 0,
120        output_queue,
121        proof: validity_proof.into(),
122        in_token_data: input_token_data_with_context,
123        out_token_data: output_compressed_accounts,
124        in_lamports,
125        out_lamports,
126        in_tlv,
127        out_tlv: None, // Out TLV is only for CompressAndClose by rent authority
128        compressions: if collected_compressions.is_empty() {
129            None
130        } else {
131            Some(collected_compressions)
132        },
133        cpi_context: transfer_config.cpi_context,
134        max_top_up: transfer_config.max_top_up,
135    };
136
137    // Serialize instruction data
138    let serialized = instruction_data
139        .try_to_vec()
140        .map_err(|_| TokenSdkError::SerializationError)?;
141
142    // Build instruction data with discriminator
143    let mut data = Vec::with_capacity(1 + serialized.len());
144    data.push(TRANSFER2);
145    data.extend(serialized);
146
147    let account_metas = get_transfer2_instruction_account_metas(meta_config);
148
149    Ok(Instruction {
150        program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID),
151        accounts: account_metas,
152        data,
153    })
154}
155
156/*
157/// Create a multi-transfer instruction
158pub fn transfer2(inputs: create_transfer2_instruction) -> Result<Instruction> {
159    let create_transfer2_instruction {
160        fee_payer,
161        authority,
162        validity_proof,
163        token_accounts,
164        tree_pubkeys,
165        config,
166    } = inputs;
167
168    // Validate that no token account has been used
169    for token_account in &token_accounts {
170        if token_account.method_used {
171            return Err(TokenSdkError::MethodUsed);
172        }
173    }
174
175    let config = config.unwrap_or_default();
176    let meta_config = Transfer2AccountsMetaConfig::new(fee_payer, authority)
177        .with_sol_pool(
178            config.sol_pool_pda.unwrap_or_default(),
179            config.sol_decompression_recipient.unwrap_or_default(),
180        )
181        .with_cpi_context();
182
183    create_transfer2_instruction(
184        token_accounts,
185        validity_proof,
186        config,
187        meta_config,
188        tree_pubkeys,
189    )
190}
191*/