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