1use light_compressed_account::{
4 compressed_account::PackedMerkleContext, instruction_data::compressed_proof::ValidityProof,
5};
6use light_sdk::{compressible::Pack, instruction::PackedAccounts};
7use light_token::{
8 compat::AccountState,
9 compressed_token::{
10 transfer2::{
11 create_transfer2_instruction, Transfer2AccountsMetaConfig, Transfer2Config,
12 Transfer2Inputs,
13 },
14 CTokenAccount2,
15 },
16 instruction::{
17 derive_token_ata, CreateAssociatedTokenAccount, DecompressMint, LIGHT_TOKEN_PROGRAM_ID,
18 },
19};
20use light_token_interface::{
21 instructions::{
22 extensions::{CompressedOnlyExtensionInstructionData, ExtensionInstructionData},
23 mint_action::{MintInstructionData, MintWithContext},
24 transfer2::MultiInputTokenDataWithContext,
25 },
26 state::{ExtensionStruct, TokenDataVersion},
27};
28use solana_instruction::Instruction;
29use solana_pubkey::Pubkey;
30use thiserror::Error;
31
32use super::{
33 decompress_mint::{DEFAULT_RENT_PAYMENT, DEFAULT_WRITE_TOP_UP},
34 instructions::{self, DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR},
35 light_program_interface::{AccountSpec, PdaSpec},
36 AccountInterface, TokenAccountInterface,
37};
38use crate::indexer::{
39 CompressedAccount, CompressedTokenAccount, Indexer, IndexerError, ValidityProofWithContext,
40};
41
42#[derive(Debug, Error)]
43pub enum LoadAccountsError {
44 #[error("Indexer error: {0}")]
45 Indexer(#[from] IndexerError),
46
47 #[error("Build instruction failed: {0}")]
48 BuildInstruction(String),
49
50 #[error("Token SDK error: {0}")]
51 TokenSdk(#[from] light_token::error::TokenSdkError),
52
53 #[error("Cold PDA at index {index} (pubkey {pubkey}) missing data")]
54 MissingPdaCompressed { index: usize, pubkey: Pubkey },
55
56 #[error("Cold ATA at index {index} (pubkey {pubkey}) missing data")]
57 MissingAtaCompressed { index: usize, pubkey: Pubkey },
58
59 #[error("Cold mint at index {index} (mint {mint}) missing hash")]
60 MissingMintHash { index: usize, mint: Pubkey },
61
62 #[error("ATA at index {index} (pubkey {pubkey}) missing compressed data or ATA bump")]
63 MissingAtaContext { index: usize, pubkey: Pubkey },
64
65 #[error("Tree info index {index} out of bounds (len {len})")]
66 TreeInfoIndexOutOfBounds { index: usize, len: usize },
67}
68
69const MAX_ATAS_PER_IX: usize = 8;
70
71#[allow(clippy::too_many_arguments)]
74pub async fn create_load_instructions<V, I>(
75 specs: &[AccountSpec<V>],
76 fee_payer: Pubkey,
77 compression_config: Pubkey,
78 rent_sponsor: Pubkey,
79 indexer: &I,
80) -> Result<Vec<Instruction>, LoadAccountsError>
81where
82 V: Pack + Clone + std::fmt::Debug,
83 I: Indexer,
84{
85 if !super::light_program_interface::any_cold(specs) {
86 return Ok(vec![]);
87 }
88
89 let cold_pdas: Vec<_> = specs
90 .iter()
91 .filter_map(|s| match s {
92 AccountSpec::Pda(p) if p.is_cold() => Some(p),
93 _ => None,
94 })
95 .collect();
96
97 let cold_atas: Vec<_> = specs
98 .iter()
99 .filter_map(|s| match s {
100 AccountSpec::Ata(a) if a.is_cold() => Some(a),
101 _ => None,
102 })
103 .collect();
104
105 let cold_mints: Vec<_> = specs
106 .iter()
107 .filter_map(|s| match s {
108 AccountSpec::Mint(m) if m.is_cold() => Some(m),
109 _ => None,
110 })
111 .collect();
112
113 let pda_hashes = collect_pda_hashes(&cold_pdas)?;
114 let ata_hashes = collect_ata_hashes(&cold_atas)?;
115 let mint_hashes = collect_mint_hashes(&cold_mints)?;
116
117 let (pda_proofs, ata_proofs, mint_proofs) = futures::join!(
118 fetch_proofs(&pda_hashes, indexer),
119 fetch_proofs_batched(&ata_hashes, MAX_ATAS_PER_IX, indexer),
120 fetch_proofs(&mint_hashes, indexer),
121 );
122
123 let pda_proofs = pda_proofs?;
124 let ata_proofs = ata_proofs?;
125 let mint_proofs = mint_proofs?;
126
127 let mut out = Vec::new();
128
129 for (spec, proof) in cold_pdas.iter().zip(pda_proofs) {
130 out.push(build_pda_load(
131 &[*spec],
132 proof,
133 fee_payer,
134 compression_config,
135 rent_sponsor,
136 )?);
137 }
138
139 let ata_chunks: Vec<_> = cold_atas.chunks(MAX_ATAS_PER_IX).collect();
140 for (chunk, proof) in ata_chunks.into_iter().zip(ata_proofs) {
141 out.extend(build_ata_load(chunk, proof, fee_payer)?);
142 }
143
144 for (iface, proof) in cold_mints.iter().zip(mint_proofs) {
145 out.push(build_mint_load(iface, proof, fee_payer)?);
146 }
147
148 Ok(out)
149}
150
151fn collect_pda_hashes<V>(specs: &[&PdaSpec<V>]) -> Result<Vec<[u8; 32]>, LoadAccountsError> {
152 specs
153 .iter()
154 .enumerate()
155 .map(|(i, s)| {
156 s.hash().ok_or(LoadAccountsError::MissingPdaCompressed {
157 index: i,
158 pubkey: s.address(),
159 })
160 })
161 .collect()
162}
163
164fn collect_ata_hashes(
165 ifaces: &[&TokenAccountInterface],
166) -> Result<Vec<[u8; 32]>, LoadAccountsError> {
167 ifaces
168 .iter()
169 .enumerate()
170 .map(|(i, s)| {
171 s.hash().ok_or(LoadAccountsError::MissingAtaCompressed {
172 index: i,
173 pubkey: s.key,
174 })
175 })
176 .collect()
177}
178
179fn collect_mint_hashes(ifaces: &[&AccountInterface]) -> Result<Vec<[u8; 32]>, LoadAccountsError> {
180 ifaces
181 .iter()
182 .enumerate()
183 .map(|(i, s)| {
184 s.hash().ok_or(LoadAccountsError::MissingMintHash {
185 index: i,
186 mint: s.key,
187 })
188 })
189 .collect()
190}
191
192async fn fetch_proofs<I: Indexer>(
193 hashes: &[[u8; 32]],
194 indexer: &I,
195) -> Result<Vec<ValidityProofWithContext>, IndexerError> {
196 if hashes.is_empty() {
197 return Ok(vec![]);
198 }
199 let mut proofs = Vec::with_capacity(hashes.len());
200 for hash in hashes {
201 proofs.push(
202 indexer
203 .get_validity_proof(vec![*hash], vec![], None)
204 .await?
205 .value,
206 );
207 }
208 Ok(proofs)
209}
210
211async fn fetch_proofs_batched<I: Indexer>(
212 hashes: &[[u8; 32]],
213 batch_size: usize,
214 indexer: &I,
215) -> Result<Vec<ValidityProofWithContext>, IndexerError> {
216 if hashes.is_empty() {
217 return Ok(vec![]);
218 }
219 let mut proofs = Vec::with_capacity(hashes.len().div_ceil(batch_size));
220 for chunk in hashes.chunks(batch_size) {
221 proofs.push(
222 indexer
223 .get_validity_proof(chunk.to_vec(), vec![], None)
224 .await?
225 .value,
226 );
227 }
228 Ok(proofs)
229}
230
231fn build_pda_load<V>(
232 specs: &[&PdaSpec<V>],
233 proof: ValidityProofWithContext,
234 fee_payer: Pubkey,
235 compression_config: Pubkey,
236 rent_sponsor: Pubkey,
237) -> Result<Instruction, LoadAccountsError>
238where
239 V: Pack + Clone + std::fmt::Debug,
240{
241 let has_tokens = specs.iter().any(|s| {
242 s.compressed()
243 .map(|c| c.owner == LIGHT_TOKEN_PROGRAM_ID)
244 .unwrap_or(false)
245 });
246
247 let metas = if has_tokens {
248 instructions::load::accounts(fee_payer, compression_config, rent_sponsor)
249 } else {
250 instructions::load::accounts_pda_only(fee_payer, compression_config, rent_sponsor)
251 };
252
253 let hot_addresses: Vec<Pubkey> = specs.iter().map(|s| s.address()).collect();
254 let cold_accounts: Vec<(CompressedAccount, V)> = specs
255 .iter()
256 .map(|s| {
257 let compressed = s.compressed().expect("cold spec must have data").clone();
258 (compressed, s.variant.clone())
259 })
260 .collect();
261
262 let program_id = specs.first().map(|s| s.program_id()).unwrap_or_default();
263
264 instructions::create_decompress_accounts_idempotent_instruction(
265 &program_id,
266 &DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR,
267 &hot_addresses,
268 &cold_accounts,
269 &metas,
270 proof,
271 )
272 .map_err(|e| LoadAccountsError::BuildInstruction(e.to_string()))
273}
274
275struct AtaContext<'a> {
276 compressed: &'a CompressedTokenAccount,
277 wallet_owner: Pubkey,
278 mint: Pubkey,
279 bump: u8,
280}
281
282impl<'a> AtaContext<'a> {
283 fn from_interface(
284 iface: &'a TokenAccountInterface,
285 index: usize,
286 ) -> Result<Self, LoadAccountsError> {
287 let compressed = iface
288 .compressed()
289 .ok_or(LoadAccountsError::MissingAtaContext {
290 index,
291 pubkey: iface.key,
292 })?;
293 let bump = iface
294 .ata_bump()
295 .ok_or(LoadAccountsError::MissingAtaContext {
296 index,
297 pubkey: iface.key,
298 })?;
299 Ok(Self {
300 compressed,
301 wallet_owner: iface.owner(),
302 mint: iface.mint(),
303 bump,
304 })
305 }
306}
307
308fn build_ata_load(
309 ifaces: &[&TokenAccountInterface],
310 proof: ValidityProofWithContext,
311 fee_payer: Pubkey,
312) -> Result<Vec<Instruction>, LoadAccountsError> {
313 let contexts: Vec<AtaContext> = ifaces
314 .iter()
315 .enumerate()
316 .map(|(i, a)| AtaContext::from_interface(a, i))
317 .collect::<Result<Vec<_>, _>>()?;
318
319 let mut out = Vec::with_capacity(contexts.len() + 1);
320
321 for ctx in &contexts {
322 let ix = CreateAssociatedTokenAccount::new(fee_payer, ctx.wallet_owner, ctx.mint)
323 .idempotent()
324 .instruction()
325 .map_err(|e| LoadAccountsError::BuildInstruction(e.to_string()))?;
326 out.push(ix);
327 }
328
329 out.push(build_transfer2(&contexts, proof, fee_payer)?);
330 Ok(out)
331}
332
333fn build_transfer2(
334 contexts: &[AtaContext],
335 proof: ValidityProofWithContext,
336 fee_payer: Pubkey,
337) -> Result<Instruction, LoadAccountsError> {
338 let mut packed = PackedAccounts::default();
339 let packed_trees = proof.pack_tree_infos(&mut packed);
340 let tree_infos = packed_trees
341 .state_trees
342 .as_ref()
343 .ok_or_else(|| LoadAccountsError::BuildInstruction("no state trees".into()))?;
344
345 let mut token_accounts = Vec::with_capacity(contexts.len());
346 let mut tlv_data: Vec<Vec<ExtensionInstructionData>> = Vec::with_capacity(contexts.len());
347 let mut has_tlv = false;
348
349 for (i, ctx) in contexts.iter().enumerate() {
350 let token = &ctx.compressed.token;
351 let tree = tree_infos.packed_tree_infos.get(i).ok_or(
352 LoadAccountsError::TreeInfoIndexOutOfBounds {
353 index: i,
354 len: tree_infos.packed_tree_infos.len(),
355 },
356 )?;
357
358 let owner_idx = packed.insert_or_get_config(ctx.wallet_owner, true, false);
359 let ata_idx = packed.insert_or_get(derive_token_ata(&ctx.wallet_owner, &ctx.mint).0);
360 let mint_idx = packed.insert_or_get(token.mint);
361 let delegate_idx = token.delegate.map(|d| packed.insert_or_get(d)).unwrap_or(0);
362
363 let source = MultiInputTokenDataWithContext {
364 owner: ata_idx,
365 amount: token.amount,
366 has_delegate: token.delegate.is_some(),
367 delegate: delegate_idx,
368 mint: mint_idx,
369 version: TokenDataVersion::ShaFlat as u8,
370 merkle_context: PackedMerkleContext {
371 merkle_tree_pubkey_index: tree.merkle_tree_pubkey_index,
372 queue_pubkey_index: tree.queue_pubkey_index,
373 prove_by_index: tree.prove_by_index,
374 leaf_index: tree.leaf_index,
375 },
376 root_index: tree.root_index,
377 };
378
379 let mut ctoken = CTokenAccount2::new(vec![source])
380 .map_err(|e| LoadAccountsError::BuildInstruction(e.to_string()))?;
381 ctoken
382 .decompress(token.amount, ata_idx)
383 .map_err(|e| LoadAccountsError::BuildInstruction(e.to_string()))?;
384 token_accounts.push(ctoken);
385
386 let is_frozen = token.state == AccountState::Frozen;
387 let tlv: Vec<ExtensionInstructionData> = token
388 .tlv
389 .as_ref()
390 .map(|exts| {
391 exts.iter()
392 .filter_map(|ext| match ext {
393 ExtensionStruct::CompressedOnly(co) => {
394 Some(ExtensionInstructionData::CompressedOnly(
395 CompressedOnlyExtensionInstructionData {
396 delegated_amount: co.delegated_amount,
397 withheld_transfer_fee: co.withheld_transfer_fee,
398 is_frozen,
399 compression_index: i as u8,
400 is_ata: true,
401 bump: ctx.bump,
402 owner_index: owner_idx,
403 },
404 ))
405 }
406 _ => None,
407 })
408 .collect()
409 })
410 .unwrap_or_default();
411
412 if !tlv.is_empty() {
413 has_tlv = true;
414 }
415 tlv_data.push(tlv);
416 }
417
418 let (metas, _, _) = packed.to_account_metas();
419
420 create_transfer2_instruction(Transfer2Inputs {
421 meta_config: Transfer2AccountsMetaConfig::new(fee_payer, metas),
422 token_accounts,
423 transfer_config: Transfer2Config::default().filter_zero_amount_outputs(),
424 validity_proof: proof.proof,
425 in_tlv: if has_tlv { Some(tlv_data) } else { None },
426 ..Default::default()
427 })
428 .map_err(|e| LoadAccountsError::BuildInstruction(e.to_string()))
429}
430
431fn build_mint_load(
432 iface: &AccountInterface,
433 proof: ValidityProofWithContext,
434 fee_payer: Pubkey,
435) -> Result<Instruction, LoadAccountsError> {
436 let acc = proof
437 .accounts
438 .first()
439 .ok_or_else(|| LoadAccountsError::BuildInstruction("proof has no accounts".into()))?;
440 let state_tree = acc.tree_info.tree;
441 let input_queue = acc.tree_info.queue;
442 let output_queue = acc
443 .tree_info
444 .next_tree_info
445 .as_ref()
446 .map(|n| n.queue)
447 .unwrap_or(input_queue);
448
449 let mint_data = iface
450 .as_mint()
451 .ok_or_else(|| LoadAccountsError::BuildInstruction("missing mint_data".into()))?;
452 let compressed_address = iface
453 .mint_compressed_address()
454 .ok_or_else(|| LoadAccountsError::BuildInstruction("missing compressed_address".into()))?;
455 let mint_ix_data = MintInstructionData::try_from(mint_data)
456 .map_err(|_| LoadAccountsError::BuildInstruction("invalid mint data".into()))?;
457
458 DecompressMint {
459 payer: fee_payer,
460 authority: fee_payer,
461 state_tree,
462 input_queue,
463 output_queue,
464 compressed_mint_with_context: MintWithContext {
465 leaf_index: acc.leaf_index as u32,
466 prove_by_index: acc.root_index.proof_by_index(),
467 root_index: acc.root_index.root_index().unwrap_or_default(),
468 address: compressed_address,
469 mint: Some(mint_ix_data),
470 },
471 proof: ValidityProof(proof.proof.into()),
472 rent_payment: DEFAULT_RENT_PAYMENT,
473 write_top_up: DEFAULT_WRITE_TOP_UP,
474 }
475 .instruction()
476 .map_err(|e| LoadAccountsError::BuildInstruction(e.to_string()))
477}