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