Skip to main content

light_client/interface/
instructions.rs

1//! Instruction builders for load/save operations.
2
3#[cfg(feature = "anchor")]
4use anchor_lang::{AnchorDeserialize, AnchorSerialize};
5#[cfg(not(feature = "anchor"))]
6use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize};
7use light_account::{
8    CompressedAccountData, InitializeLightConfigParams, Pack, UpdateLightConfigParams,
9};
10use light_sdk::instruction::{
11    account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts,
12    SystemAccountMetaConfig, ValidityProof,
13};
14use light_token::constants::{
15    LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_CPI_AUTHORITY, LIGHT_TOKEN_PROGRAM_ID,
16    RENT_SPONSOR_V1 as RENT_SPONSOR,
17};
18use solana_instruction::{AccountMeta, Instruction};
19use solana_pubkey::Pubkey;
20
21use crate::indexer::{CompressedAccount, TreeInfo, ValidityProofWithContext};
22
23#[inline]
24fn get_output_queue(tree_info: &TreeInfo) -> Pubkey {
25    tree_info
26        .next_tree_info
27        .as_ref()
28        .map(|next| next.queue)
29        .unwrap_or(tree_info.queue)
30}
31
32#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
33pub struct LoadAccountsData<T> {
34    pub system_accounts_offset: u8,
35    pub token_accounts_offset: u8,
36    pub output_queue_index: u8,
37    pub proof: ValidityProof,
38    pub compressed_accounts: Vec<CompressedAccountData<T>>,
39}
40
41#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
42pub struct SaveAccountsData {
43    pub proof: ValidityProof,
44    pub compressed_accounts: Vec<CompressedAccountMetaNoLamportsNoAddress>,
45    pub system_accounts_offset: u8,
46}
47
48// Discriminators (match on-chain instruction names)
49pub const INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR: [u8; 8] =
50    [133, 228, 12, 169, 56, 76, 222, 61];
51pub const UPDATE_COMPRESSION_CONFIG_DISCRIMINATOR: [u8; 8] = [135, 215, 243, 81, 163, 146, 33, 70];
52pub const DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR: [u8; 8] =
53    [114, 67, 61, 123, 234, 31, 1, 112];
54pub const COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR: [u8; 8] =
55    [70, 236, 171, 120, 164, 93, 113, 181];
56
57/// Account metas for load operations.
58pub mod load {
59    use super::*;
60
61    /// With token support.
62    pub fn accounts(fee_payer: Pubkey, config: Pubkey, rent_sponsor: Pubkey) -> Vec<AccountMeta> {
63        vec![
64            AccountMeta::new(fee_payer, true),
65            AccountMeta::new_readonly(config, false),
66            AccountMeta::new(rent_sponsor, false),
67            AccountMeta::new(RENT_SPONSOR, false),
68            AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false),
69            AccountMeta::new_readonly(LIGHT_TOKEN_CPI_AUTHORITY, false),
70            AccountMeta::new_readonly(LIGHT_TOKEN_CONFIG, false),
71        ]
72    }
73
74    /// PDAs only (no tokens).
75    pub fn accounts_pda_only(
76        fee_payer: Pubkey,
77        config: Pubkey,
78        rent_sponsor: Pubkey,
79    ) -> Vec<AccountMeta> {
80        vec![
81            AccountMeta::new(fee_payer, true),
82            AccountMeta::new_readonly(config, false),
83            AccountMeta::new(rent_sponsor, false),
84            AccountMeta::new(rent_sponsor, false), // placeholder for ctoken_rent_sponsor
85            AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false),
86            AccountMeta::new_readonly(LIGHT_TOKEN_CPI_AUTHORITY, false),
87            AccountMeta::new_readonly(LIGHT_TOKEN_CONFIG, false),
88        ]
89    }
90}
91
92#[allow(clippy::too_many_arguments)]
93pub fn initialize_config(
94    program_id: &Pubkey,
95    discriminator: &[u8],
96    payer: &Pubkey,
97    authority: &Pubkey,
98    rent_sponsor: Pubkey,
99    address_space: Vec<Pubkey>,
100    config_bump: Option<u8>,
101) -> Instruction {
102    let config_bump = config_bump.unwrap_or(0);
103    let config_bump_u16 = config_bump as u16;
104    let (config_pda, _) = Pubkey::find_program_address(
105        &[
106            light_account::LIGHT_CONFIG_SEED,
107            &config_bump_u16.to_le_bytes(),
108        ],
109        program_id,
110    );
111
112    let bpf_loader = solana_pubkey::pubkey!("BPFLoaderUpgradeab1e11111111111111111111111");
113    let (program_data_pda, _) = Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader);
114
115    let system_program = solana_pubkey::pubkey!("11111111111111111111111111111111");
116    let accounts = vec![
117        AccountMeta::new(*payer, true),
118        AccountMeta::new(config_pda, false),
119        AccountMeta::new_readonly(program_data_pda, false),
120        AccountMeta::new_readonly(*authority, true),
121        AccountMeta::new_readonly(system_program, false),
122    ];
123
124    let params = InitializeLightConfigParams {
125        rent_sponsor: rent_sponsor.to_bytes(),
126        compression_authority: authority.to_bytes(),
127        rent_config: Default::default(),
128        write_top_up: 0,
129        address_space: address_space.iter().map(|p| p.to_bytes()).collect(),
130        config_bump,
131    };
132    let serialized = params.try_to_vec().expect("serialize params");
133    let mut data = Vec::with_capacity(discriminator.len() + serialized.len());
134    data.extend_from_slice(discriminator);
135    data.extend_from_slice(&serialized);
136
137    Instruction {
138        program_id: *program_id,
139        accounts,
140        data,
141    }
142}
143
144pub fn update_config(
145    program_id: &Pubkey,
146    discriminator: &[u8],
147    authority: &Pubkey,
148    new_rent_sponsor: Option<Pubkey>,
149    new_address_space: Option<Vec<Pubkey>>,
150    new_update_authority: Option<Pubkey>,
151) -> Instruction {
152    let (config_pda, _) = Pubkey::find_program_address(
153        &[light_account::LIGHT_CONFIG_SEED, &0u16.to_le_bytes()],
154        program_id,
155    );
156
157    let accounts = vec![
158        AccountMeta::new(config_pda, false),
159        AccountMeta::new_readonly(*authority, true),
160    ];
161
162    let params = UpdateLightConfigParams {
163        new_update_authority: new_update_authority.map(|p| p.to_bytes()),
164        new_rent_sponsor: new_rent_sponsor.map(|p| p.to_bytes()),
165        new_compression_authority: None,
166        new_rent_config: None,
167        new_write_top_up: None,
168        new_address_space: new_address_space.map(|v| v.iter().map(|p| p.to_bytes()).collect()),
169    };
170    let serialized = params.try_to_vec().expect("serialize params");
171    let mut data = Vec::with_capacity(discriminator.len() + serialized.len());
172    data.extend_from_slice(discriminator);
173    data.extend_from_slice(&serialized);
174
175    Instruction {
176        program_id: *program_id,
177        accounts,
178        data,
179    }
180}
181
182/// Build load (decompress) instruction.
183#[allow(clippy::too_many_arguments)]
184pub fn create_decompress_accounts_idempotent_instruction<T>(
185    program_id: &Pubkey,
186    discriminator: &[u8],
187    hot_addresses: &[Pubkey],
188    cold_accounts: &[(CompressedAccount, T)],
189    program_account_metas: &[AccountMeta],
190    proof: ValidityProofWithContext,
191) -> Result<Instruction, Box<dyn std::error::Error>>
192where
193    T: Pack<solana_instruction::AccountMeta> + Clone + std::fmt::Debug,
194{
195    if cold_accounts.is_empty() {
196        return Err("cold_accounts cannot be empty".into());
197    }
198    if hot_addresses.len() != cold_accounts.len() {
199        return Err("hot_addresses and cold_accounts must have same length".into());
200    }
201
202    let mut remaining_accounts = PackedAccounts::default();
203
204    // Separate PDA and token indices so PDAs come first in the output.
205    let mut pda_indices = Vec::new();
206    let mut token_indices = Vec::new();
207    for (i, (acc, _)) in cold_accounts.iter().enumerate() {
208        if acc.owner == LIGHT_TOKEN_PROGRAM_ID {
209            token_indices.push(i);
210        } else {
211            pda_indices.push(i);
212        }
213    }
214    let has_pdas = !pda_indices.is_empty();
215    let has_tokens = !token_indices.is_empty();
216    if !has_tokens && !has_pdas {
217        return Err("No tokens or PDAs found".into());
218    }
219
220    // When mixing PDAs + tokens, use first token's CPI context
221    if has_pdas && has_tokens {
222        let first_token_acc = &cold_accounts[token_indices[0]];
223        let first_token_cpi = first_token_acc
224            .0
225            .tree_info
226            .cpi_context
227            .ok_or("missing cpi_context on token account")?;
228        let config = SystemAccountMetaConfig::new_with_cpi_context(*program_id, first_token_cpi);
229        remaining_accounts.add_system_accounts_v2(config)?;
230    } else {
231        remaining_accounts.add_system_accounts_v2(SystemAccountMetaConfig::new(*program_id))?;
232    }
233
234    let output_queue = get_output_queue(&cold_accounts[0].0.tree_info);
235    let output_state_tree_index = remaining_accounts.insert_or_get(output_queue);
236
237    let packed_tree_infos = proof.pack_tree_infos(&mut remaining_accounts);
238    let tree_infos = &packed_tree_infos
239        .state_trees
240        .as_ref()
241        .ok_or("missing state_trees in packed_tree_infos")?
242        .packed_tree_infos;
243
244    let mut accounts = program_account_metas.to_vec();
245    let mut typed_accounts = Vec::with_capacity(cold_accounts.len());
246
247    // Process PDAs first, then tokens, to match on-chain split_at(token_accounts_offset).
248    for &i in pda_indices.iter().chain(token_indices.iter()) {
249        let (acc, data) = &cold_accounts[i];
250        let _queue_index = remaining_accounts.insert_or_get(acc.tree_info.queue);
251        let tree_info = tree_infos
252            .get(i)
253            .copied()
254            .ok_or("tree info index out of bounds")?;
255
256        let packed_data = data.pack(&mut remaining_accounts)?;
257        typed_accounts.push(CompressedAccountData {
258            tree_info,
259            data: packed_data,
260        });
261    }
262
263    let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas();
264    accounts.extend(system_accounts);
265
266    // Append hot addresses in the same order: PDAs first, then tokens.
267    for &i in pda_indices.iter().chain(token_indices.iter()) {
268        accounts.push(AccountMeta::new(hot_addresses[i], false));
269    }
270
271    // system_accounts_offset must account for program_account_metas
272    let full_offset = program_account_metas.len() + system_accounts_offset;
273    let token_accounts_offset = pda_indices.len() as u8;
274    let ix_data = LoadAccountsData {
275        proof: proof.proof,
276        compressed_accounts: typed_accounts,
277        system_accounts_offset: full_offset as u8,
278        token_accounts_offset,
279        output_queue_index: output_state_tree_index,
280    };
281
282    let serialized = ix_data.try_to_vec()?;
283    let mut data = Vec::with_capacity(discriminator.len() + serialized.len());
284    data.extend_from_slice(discriminator);
285    data.extend_from_slice(&serialized);
286
287    Ok(Instruction {
288        program_id: *program_id,
289        accounts,
290        data,
291    })
292}
293
294/// Build compress instruction.
295pub fn build_compress_accounts_idempotent(
296    program_id: &Pubkey,
297    discriminator: &[u8],
298    account_pubkeys: &[Pubkey],
299    program_account_metas: &[AccountMeta],
300    proof: ValidityProofWithContext,
301) -> Result<Instruction, Box<dyn std::error::Error>> {
302    if proof.accounts.is_empty() {
303        return Err("proof.accounts cannot be empty".into());
304    }
305
306    let mut remaining_accounts = PackedAccounts::default();
307    remaining_accounts.add_system_accounts_v2(SystemAccountMetaConfig::new(*program_id))?;
308
309    let output_queue = get_output_queue(&proof.accounts[0].tree_info);
310    let output_state_tree_index = remaining_accounts.insert_or_get(output_queue);
311
312    let packed_tree_infos = proof.pack_tree_infos(&mut remaining_accounts);
313    let tree_infos = packed_tree_infos
314        .state_trees
315        .as_ref()
316        .ok_or("missing state_trees in packed_tree_infos")?;
317
318    let cold_metas: Vec<_> = tree_infos
319        .packed_tree_infos
320        .iter()
321        .map(|tree_info| CompressedAccountMetaNoLamportsNoAddress {
322            tree_info: *tree_info,
323            output_state_tree_index,
324        })
325        .collect();
326
327    let mut accounts = program_account_metas.to_vec();
328    let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas();
329    accounts.extend(system_accounts);
330
331    for pubkey in account_pubkeys {
332        accounts.push(AccountMeta::new(*pubkey, false));
333    }
334
335    // system_accounts_offset must account for program_account_metas
336    let full_offset = program_account_metas.len() + system_accounts_offset;
337    let ix_data = SaveAccountsData {
338        proof: proof.proof,
339        compressed_accounts: cold_metas,
340        system_accounts_offset: full_offset as u8,
341    };
342
343    let serialized = ix_data.try_to_vec()?;
344    let mut data = Vec::with_capacity(discriminator.len() + serialized.len());
345    data.extend_from_slice(discriminator);
346    data.extend_from_slice(&serialized);
347
348    Ok(Instruction {
349        program_id: *program_id,
350        accounts,
351        data,
352    })
353}