light_client/indexer/
types.rs

1use borsh::BorshDeserialize;
2use light_compressed_account::{
3    compressed_account::{
4        CompressedAccount as ProgramCompressedAccount, CompressedAccountData,
5        CompressedAccountWithMerkleContext,
6    },
7    instruction_data::compressed_proof::CompressedProof,
8    TreeType,
9};
10use light_indexed_merkle_tree::array::IndexedElement;
11use light_sdk::instruction::{
12    PackedAccounts, PackedAddressTreeInfo, PackedStateTreeInfo, ValidityProof,
13};
14use light_token::compat::{AccountState, TokenData};
15use light_token_interface::state::ExtensionStruct;
16use num_bigint::BigUint;
17use solana_pubkey::Pubkey;
18use tracing::warn;
19
20use super::{
21    base58::{decode_base58_option_to_pubkey, decode_base58_to_fixed_array},
22    tree_info::QUEUE_TREE_MAPPING,
23    IndexerError,
24};
25
26pub struct ProofOfLeaf {
27    pub leaf: [u8; 32],
28    pub proof: Vec<[u8; 32]>,
29}
30
31pub type Address = [u8; 32];
32pub type Hash = [u8; 32];
33
34#[derive(Debug, Clone, PartialEq)]
35pub struct QueueInfo {
36    pub tree: Pubkey,
37    pub queue: Pubkey,
38    pub queue_type: u8,
39    pub queue_size: u64,
40}
41
42#[derive(Debug, Clone, PartialEq, Default)]
43pub struct QueueInfoResult {
44    pub queues: Vec<QueueInfo>,
45    pub slot: u64,
46}
47
48#[derive(Debug, Clone, PartialEq, Default)]
49pub struct OutputQueueData {
50    pub leaf_indices: Vec<u64>,
51    pub account_hashes: Vec<[u8; 32]>,
52    pub old_leaves: Vec<[u8; 32]>,
53    pub first_queue_index: u64,
54    /// The tree's next_index - where new leaves will be appended
55    pub next_index: u64,
56    /// Pre-computed hash chains per ZKP batch (from on-chain)
57    pub leaves_hash_chains: Vec<[u8; 32]>,
58}
59
60/// V2 Input Queue Data
61#[derive(Debug, Clone, PartialEq, Default)]
62pub struct InputQueueData {
63    pub leaf_indices: Vec<u64>,
64    pub account_hashes: Vec<[u8; 32]>,
65    pub current_leaves: Vec<[u8; 32]>,
66    pub tx_hashes: Vec<[u8; 32]>,
67    /// Pre-computed nullifiers from indexer
68    pub nullifiers: Vec<[u8; 32]>,
69    pub first_queue_index: u64,
70    /// Pre-computed hash chains per ZKP batch (from on-chain)
71    pub leaves_hash_chains: Vec<[u8; 32]>,
72}
73
74/// State queue data with shared tree nodes for output and input queues
75#[derive(Debug, Clone, PartialEq, Default)]
76pub struct StateQueueData {
77    /// Shared deduplicated tree nodes for state queues (output + input)
78    /// node_index encoding: (level << 56) | position
79    pub nodes: Vec<u64>,
80    pub node_hashes: Vec<[u8; 32]>,
81    /// Initial root for the state tree (shared by output and input queues)
82    pub initial_root: [u8; 32],
83    /// Sequence number of the root
84    pub root_seq: u64,
85    /// Output queue data (if requested)
86    pub output_queue: Option<OutputQueueData>,
87    /// Input queue data (if requested)
88    pub input_queue: Option<InputQueueData>,
89}
90
91/// V2 Address Queue Data with deduplicated nodes
92/// Proofs are reconstructed from `nodes`/`node_hashes` using `low_element_indices`
93#[derive(Debug, Clone, PartialEq, Default)]
94pub struct AddressQueueData {
95    pub addresses: Vec<[u8; 32]>,
96    pub low_element_values: Vec<[u8; 32]>,
97    pub low_element_next_values: Vec<[u8; 32]>,
98    pub low_element_indices: Vec<u64>,
99    pub low_element_next_indices: Vec<u64>,
100    /// Deduplicated node indices - encoding: (level << 56) | position
101    pub nodes: Vec<u64>,
102    /// Hashes corresponding to each node index
103    pub node_hashes: Vec<[u8; 32]>,
104    pub initial_root: [u8; 32],
105    pub leaves_hash_chains: Vec<[u8; 32]>,
106    pub subtrees: Vec<[u8; 32]>,
107    pub start_index: u64,
108    pub root_seq: u64,
109}
110
111impl AddressQueueData {
112    /// Reconstruct a merkle proof for a given low_element_index from the deduplicated nodes.
113    /// The tree_height is needed to know how many levels to traverse.
114    pub fn reconstruct_proof(
115        &self,
116        address_idx: usize,
117        tree_height: u8,
118    ) -> Result<Vec<[u8; 32]>, IndexerError> {
119        let leaf_index = self.low_element_indices[address_idx];
120        let mut proof = Vec::with_capacity(tree_height as usize);
121        let mut pos = leaf_index;
122
123        for level in 0..tree_height {
124            let sibling_pos = if pos.is_multiple_of(2) {
125                pos + 1
126            } else {
127                pos - 1
128            };
129            let sibling_idx = Self::encode_node_index(level, sibling_pos);
130
131            if let Some(hash_idx) = self.nodes.iter().position(|&n| n == sibling_idx) {
132                proof.push(self.node_hashes[hash_idx]);
133            } else {
134                return Err(IndexerError::MissingResult {
135                    context: "reconstruct_proof".to_string(),
136                    message: format!(
137                        "Missing proof node at level {} position {} (encoded: {})",
138                        level, sibling_pos, sibling_idx
139                    ),
140                });
141            }
142            pos /= 2;
143        }
144
145        Ok(proof)
146    }
147
148    /// Reconstruct all proofs for all addresses
149    pub fn reconstruct_all_proofs(
150        &self,
151        tree_height: u8,
152    ) -> Result<Vec<Vec<[u8; 32]>>, IndexerError> {
153        (0..self.addresses.len())
154            .map(|i| self.reconstruct_proof(i, tree_height))
155            .collect()
156    }
157
158    /// Encode node index: (level << 56) | position
159    #[inline]
160    fn encode_node_index(level: u8, position: u64) -> u64 {
161        ((level as u64) << 56) | position
162    }
163}
164
165/// V2 Queue Elements Result with deduplicated node data
166#[derive(Debug, Clone, PartialEq, Default)]
167pub struct QueueElementsResult {
168    pub state_queue: Option<StateQueueData>,
169    pub address_queue: Option<AddressQueueData>,
170}
171
172#[derive(Debug, Clone, PartialEq, Default)]
173pub struct MerkleProofWithContext {
174    pub proof: Vec<[u8; 32]>,
175    pub root: [u8; 32],
176    pub leaf_index: u64,
177    pub leaf: [u8; 32],
178    pub merkle_tree: [u8; 32],
179    pub root_seq: u64,
180    pub tx_hash: Option<[u8; 32]>,
181    pub account_hash: [u8; 32],
182}
183
184#[derive(Debug, Clone, PartialEq, Default)]
185pub struct MerkleProof {
186    pub hash: [u8; 32],
187    pub leaf_index: u64,
188    pub merkle_tree: Pubkey,
189    pub proof: Vec<[u8; 32]>,
190    pub root_seq: u64,
191    pub root: [u8; 32],
192}
193
194#[derive(Debug, Clone, Copy, PartialEq)]
195pub struct AddressWithTree {
196    pub address: Address,
197    pub tree: Pubkey,
198}
199
200#[derive(Clone, Default, Debug, PartialEq)]
201pub struct NewAddressProofWithContext {
202    pub merkle_tree: Pubkey,
203    pub root: [u8; 32],
204    pub root_seq: u64,
205    pub low_address_index: u64,
206    pub low_address_value: [u8; 32],
207    pub low_address_next_index: u64,
208    pub low_address_next_value: [u8; 32],
209    pub low_address_proof: Vec<[u8; 32]>,
210    pub new_low_element: Option<IndexedElement<usize>>,
211    pub new_element: Option<IndexedElement<usize>>,
212    pub new_element_next_value: Option<BigUint>,
213}
214
215#[derive(Debug, Default, Clone, PartialEq)]
216pub struct ValidityProofWithContext {
217    pub proof: ValidityProof,
218    pub accounts: Vec<AccountProofInputs>,
219    pub addresses: Vec<AddressProofInputs>,
220}
221
222// TODO: add get_public_inputs
223// -> to make it easier to use light-verifier with get_validity_proof()
224impl ValidityProofWithContext {
225    pub fn get_root_indices(&self) -> Vec<Option<u16>> {
226        self.accounts
227            .iter()
228            .map(|account| account.root_index.root_index())
229            .collect()
230    }
231
232    pub fn get_address_root_indices(&self) -> Vec<u16> {
233        self.addresses
234            .iter()
235            .map(|address| address.root_index)
236            .collect()
237    }
238}
239
240#[derive(Clone, Default, Debug, PartialEq)]
241pub struct AccountProofInputs {
242    pub hash: [u8; 32],
243    pub root: [u8; 32],
244    pub root_index: RootIndex,
245    pub leaf_index: u64,
246    pub tree_info: TreeInfo,
247}
248
249#[derive(Clone, Default, Copy, Debug, PartialEq)]
250pub struct RootIndex {
251    proof_by_index: bool,
252    root_index: u16,
253}
254
255impl RootIndex {
256    pub fn new_none() -> Self {
257        Self {
258            proof_by_index: true,
259            root_index: 0,
260        }
261    }
262
263    pub fn new_some(root_index: u16) -> Self {
264        Self {
265            proof_by_index: false,
266            root_index,
267        }
268    }
269
270    pub fn proof_by_index(&self) -> bool {
271        self.proof_by_index
272    }
273
274    pub fn root_index(&self) -> Option<u16> {
275        if !self.proof_by_index {
276            Some(self.root_index)
277        } else {
278            None
279        }
280    }
281}
282
283impl AccountProofInputs {
284    pub fn from_api_model(
285        value: &photon_api::models::AccountProofInputs,
286    ) -> Result<Self, IndexerError> {
287        let root_index = {
288            if value.root_index.prove_by_index {
289                RootIndex::new_none()
290            } else {
291                RootIndex::new_some(value.root_index.root_index)
292            }
293        };
294        Ok(Self {
295            hash: decode_base58_to_fixed_array(&value.hash)?,
296            root: decode_base58_to_fixed_array(&value.root)?,
297            root_index,
298            leaf_index: value.leaf_index,
299            tree_info: TreeInfo::from_api_model(&value.merkle_context)?,
300        })
301    }
302}
303
304#[derive(Clone, Default, Debug, PartialEq)]
305pub struct AddressProofInputs {
306    pub address: [u8; 32],
307    pub root: [u8; 32],
308    pub root_index: u16,
309    pub tree_info: TreeInfo,
310}
311
312impl AddressProofInputs {
313    pub fn from_api_model(
314        value: &photon_api::models::AddressProofInputs,
315    ) -> Result<Self, IndexerError> {
316        Ok(Self {
317            address: decode_base58_to_fixed_array(&value.address)?,
318            root: decode_base58_to_fixed_array(&value.root)?,
319            root_index: value.root_index,
320            tree_info: TreeInfo::from_api_model(&value.merkle_context)?,
321        })
322    }
323}
324
325#[derive(Clone, Default, Debug, PartialEq)]
326pub struct PackedStateTreeInfos {
327    pub packed_tree_infos: Vec<PackedStateTreeInfo>,
328    pub output_tree_index: u8,
329}
330
331#[derive(Clone, Default, Debug, PartialEq)]
332pub struct PackedTreeInfos {
333    pub state_trees: Option<PackedStateTreeInfos>,
334    pub address_trees: Vec<PackedAddressTreeInfo>,
335}
336
337impl ValidityProofWithContext {
338    pub fn pack_tree_infos(&self, packed_accounts: &mut PackedAccounts) -> PackedTreeInfos {
339        let mut packed_tree_infos = Vec::new();
340        let mut address_trees = Vec::new();
341        let mut output_tree_index = None;
342        for account in self.accounts.iter() {
343            // Pack TreeInfo
344            let merkle_tree_pubkey_index = packed_accounts.insert_or_get(account.tree_info.tree);
345            let queue_pubkey_index = packed_accounts.insert_or_get(account.tree_info.queue);
346            let tree_info_packed = PackedStateTreeInfo {
347                root_index: account.root_index.root_index,
348                merkle_tree_pubkey_index,
349                queue_pubkey_index,
350                leaf_index: account.leaf_index as u32,
351                prove_by_index: account.root_index.proof_by_index(),
352            };
353            packed_tree_infos.push(tree_info_packed);
354
355            // If a next Merkle tree exists the Merkle tree is full -> use the next Merkle tree for new state.
356            // Else use the current Merkle tree for new state.
357            if let Some(next) = account.tree_info.next_tree_info {
358                // SAFETY: account will always have a state Merkle tree context.
359                // pack_output_tree_index only panics on an address Merkle tree context.
360                let index = next.pack_output_tree_index(packed_accounts).unwrap();
361                if output_tree_index.is_none() {
362                    output_tree_index = Some(index);
363                }
364            } else {
365                // SAFETY: account will always have a state Merkle tree context.
366                // pack_output_tree_index only panics on an address Merkle tree context.
367                let index = account
368                    .tree_info
369                    .pack_output_tree_index(packed_accounts)
370                    .unwrap();
371                if output_tree_index.is_none() {
372                    output_tree_index = Some(index);
373                }
374            }
375        }
376
377        for address in self.addresses.iter() {
378            // Pack AddressTreeInfo
379            let address_merkle_tree_pubkey_index =
380                packed_accounts.insert_or_get(address.tree_info.tree);
381            let address_queue_pubkey_index = packed_accounts.insert_or_get(address.tree_info.queue);
382            address_trees.push(PackedAddressTreeInfo {
383                address_merkle_tree_pubkey_index,
384                address_queue_pubkey_index,
385                root_index: address.root_index,
386            });
387        }
388        let packed_tree_infos = if packed_tree_infos.is_empty() {
389            None
390        } else {
391            Some(PackedStateTreeInfos {
392                packed_tree_infos,
393                output_tree_index: output_tree_index.unwrap(),
394            })
395        };
396        PackedTreeInfos {
397            state_trees: packed_tree_infos,
398            address_trees,
399        }
400    }
401
402    pub fn from_api_model(
403        value: photon_api::models::CompressedProofWithContext,
404        num_hashes: usize,
405    ) -> Result<Self, IndexerError> {
406        let proof = ValidityProof::new(Some(CompressedProof {
407            a: value
408                .compressed_proof
409                .a
410                .try_into()
411                .map_err(|_| IndexerError::InvalidResponseData)?,
412            b: value
413                .compressed_proof
414                .b
415                .try_into()
416                .map_err(|_| IndexerError::InvalidResponseData)?,
417            c: value
418                .compressed_proof
419                .c
420                .try_into()
421                .map_err(|_| IndexerError::InvalidResponseData)?,
422        }));
423
424        // Convert account data from V1 flat arrays to V2 structured format
425        let accounts = (0..num_hashes)
426            .map(|i| {
427                let tree_pubkey =
428                    Pubkey::new_from_array(decode_base58_to_fixed_array(&value.merkle_trees[i])?);
429                let tree_info = super::tree_info::QUEUE_TREE_MAPPING
430                    .get(&value.merkle_trees[i])
431                    .ok_or(IndexerError::InvalidResponseData)?;
432
433                Ok(AccountProofInputs {
434                    hash: decode_base58_to_fixed_array(&value.leaves[i])?,
435                    root: decode_base58_to_fixed_array(&value.roots[i])?,
436                    root_index: RootIndex::new_some(value.root_indices[i] as u16),
437                    leaf_index: value.leaf_indices[i] as u64,
438                    tree_info: TreeInfo {
439                        tree_type: tree_info.tree_type,
440                        tree: tree_pubkey,
441                        queue: tree_info.queue,
442                        cpi_context: tree_info.cpi_context,
443                        next_tree_info: None,
444                    },
445                })
446            })
447            .collect::<Result<Vec<_>, IndexerError>>()?;
448
449        // Convert address data from remaining indices (if any)
450        let addresses = if value.root_indices.len() > num_hashes {
451            (num_hashes..value.root_indices.len())
452                .map(|i| {
453                    let tree_pubkey = Pubkey::new_from_array(decode_base58_to_fixed_array(
454                        &value.merkle_trees[i],
455                    )?);
456                    let tree_info = super::tree_info::QUEUE_TREE_MAPPING
457                        .get(&value.merkle_trees[i])
458                        .ok_or(IndexerError::InvalidResponseData)?;
459
460                    Ok(AddressProofInputs {
461                        address: decode_base58_to_fixed_array(&value.leaves[i])?, // Address is in leaves
462                        root: decode_base58_to_fixed_array(&value.roots[i])?,
463                        root_index: value.root_indices[i] as u16,
464                        tree_info: TreeInfo {
465                            tree_type: tree_info.tree_type,
466                            tree: tree_pubkey,
467                            queue: tree_info.queue,
468                            cpi_context: tree_info.cpi_context,
469                            next_tree_info: None,
470                        },
471                    })
472                })
473                .collect::<Result<Vec<_>, IndexerError>>()?
474        } else {
475            Vec::new()
476        };
477
478        Ok(Self {
479            proof,
480            accounts,
481            addresses,
482        })
483    }
484
485    pub fn from_api_model_v2(
486        value: photon_api::models::CompressedProofWithContextV2,
487    ) -> Result<Self, IndexerError> {
488        let proof = if let Some(proof) = value.compressed_proof {
489            ValidityProof::new(Some(CompressedProof {
490                a: proof
491                    .a
492                    .try_into()
493                    .map_err(|_| IndexerError::InvalidResponseData)?,
494                b: proof
495                    .b
496                    .try_into()
497                    .map_err(|_| IndexerError::InvalidResponseData)?,
498                c: proof
499                    .c
500                    .try_into()
501                    .map_err(|_| IndexerError::InvalidResponseData)?,
502            }))
503        } else {
504            ValidityProof::new(None)
505        };
506
507        let accounts = value
508            .accounts
509            .iter()
510            .map(AccountProofInputs::from_api_model)
511            .collect::<Result<Vec<_>, IndexerError>>()?;
512
513        let addresses = value
514            .addresses
515            .iter()
516            .map(AddressProofInputs::from_api_model)
517            .collect::<Result<Vec<_>, IndexerError>>()?;
518
519        Ok(Self {
520            proof,
521            accounts,
522            addresses,
523        })
524    }
525}
526
527#[derive(Clone, Copy, Default, Debug, PartialEq)]
528pub struct NextTreeInfo {
529    pub cpi_context: Option<Pubkey>,
530    pub queue: Pubkey,
531    pub tree: Pubkey,
532    pub tree_type: TreeType,
533}
534
535impl NextTreeInfo {
536    /// Get the index of the output tree in the packed accounts.
537    /// For StateV1, it returns the index of the tree account.
538    /// For StateV2, it returns the index of the queue account.
539    /// (For V2 trees new state is inserted into the output queue.
540    /// The forester updates the tree from the queue asynchronously.)
541    pub fn pack_output_tree_index(
542        &self,
543        packed_accounts: &mut PackedAccounts,
544    ) -> Result<u8, IndexerError> {
545        match self.tree_type {
546            TreeType::StateV1 => Ok(packed_accounts.insert_or_get(self.tree)),
547            TreeType::StateV2 => Ok(packed_accounts.insert_or_get(self.queue)),
548            _ => Err(IndexerError::InvalidPackTreeType),
549        }
550    }
551    pub fn from_api_model(
552        value: &photon_api::models::TreeContextInfo,
553    ) -> Result<Self, IndexerError> {
554        Ok(Self {
555            tree_type: TreeType::from(value.tree_type as u64),
556            tree: Pubkey::new_from_array(decode_base58_to_fixed_array(&value.tree)?),
557            queue: Pubkey::new_from_array(decode_base58_to_fixed_array(&value.queue)?),
558            cpi_context: decode_base58_option_to_pubkey(&value.cpi_context)?,
559        })
560    }
561}
562
563impl TryFrom<&photon_api::models::TreeContextInfo> for NextTreeInfo {
564    type Error = IndexerError;
565
566    fn try_from(value: &photon_api::models::TreeContextInfo) -> Result<Self, Self::Error> {
567        Ok(Self {
568            tree_type: TreeType::from(value.tree_type as u64),
569            tree: Pubkey::new_from_array(decode_base58_to_fixed_array(&value.tree)?),
570            queue: Pubkey::new_from_array(decode_base58_to_fixed_array(&value.queue)?),
571            cpi_context: decode_base58_option_to_pubkey(&value.cpi_context)?,
572        })
573    }
574}
575
576#[derive(Clone, Copy, Default, Debug, PartialEq)]
577pub struct TreeInfo {
578    pub cpi_context: Option<Pubkey>,
579    pub next_tree_info: Option<NextTreeInfo>,
580    pub queue: Pubkey,
581    pub tree: Pubkey,
582    pub tree_type: TreeType,
583}
584
585impl TreeInfo {
586    /// Get the index of the output tree in the packed accounts.
587    /// For StateV1, it returns the index of the tree account.
588    /// For StateV2, it returns the index of the queue account.
589    /// (For V2 trees new state is inserted into the output queue.
590    /// The forester updates the tree from the queue asynchronously.)
591    pub fn pack_output_tree_index(
592        &self,
593        packed_accounts: &mut PackedAccounts,
594    ) -> Result<u8, IndexerError> {
595        match self.tree_type {
596            TreeType::StateV1 => Ok(packed_accounts.insert_or_get(self.tree)),
597            TreeType::StateV2 => Ok(packed_accounts.insert_or_get(self.queue)),
598            _ => Err(IndexerError::InvalidPackTreeType),
599        }
600    }
601
602    pub fn get_output_pubkey(&self) -> Result<Pubkey, IndexerError> {
603        match self.tree_type {
604            TreeType::StateV1 => Ok(self.tree),
605            TreeType::StateV2 => Ok(self.queue),
606            _ => Err(IndexerError::InvalidPackTreeType),
607        }
608    }
609
610    pub fn from_api_model(
611        value: &photon_api::models::MerkleContextV2,
612    ) -> Result<Self, IndexerError> {
613        Ok(Self {
614            tree_type: TreeType::from(value.tree_type as u64),
615            tree: Pubkey::new_from_array(decode_base58_to_fixed_array(&value.tree)?),
616            queue: Pubkey::new_from_array(decode_base58_to_fixed_array(&value.queue)?),
617            cpi_context: decode_base58_option_to_pubkey(&value.cpi_context)?,
618            next_tree_info: value
619                .next_tree_context
620                .as_ref()
621                .map(|tree_info| NextTreeInfo::from_api_model(tree_info.as_ref()))
622                .transpose()?,
623        })
624    }
625
626    pub fn to_light_merkle_context(
627        &self,
628        leaf_index: u32,
629        prove_by_index: bool,
630    ) -> light_compressed_account::compressed_account::MerkleContext {
631        use light_compressed_account::Pubkey;
632        light_compressed_account::compressed_account::MerkleContext {
633            merkle_tree_pubkey: Pubkey::new_from_array(self.tree.to_bytes()),
634            queue_pubkey: Pubkey::new_from_array(self.queue.to_bytes()),
635            leaf_index,
636            tree_type: self.tree_type,
637            prove_by_index,
638        }
639    }
640}
641
642#[derive(Clone, Default, Debug, PartialEq)]
643pub struct CompressedAccount {
644    pub address: Option<[u8; 32]>,
645    pub data: Option<CompressedAccountData>,
646    pub hash: [u8; 32],
647    pub lamports: u64,
648    pub leaf_index: u32,
649    pub owner: Pubkey,
650    pub prove_by_index: bool,
651    pub seq: Option<u64>,
652    pub slot_created: u64,
653    pub tree_info: TreeInfo,
654}
655
656impl TryFrom<CompressedAccountWithMerkleContext> for CompressedAccount {
657    type Error = IndexerError;
658
659    fn try_from(account: CompressedAccountWithMerkleContext) -> Result<Self, Self::Error> {
660        let hash = account
661            .hash()
662            .map_err(|_| IndexerError::InvalidResponseData)?;
663        // Breaks light-program-test
664        let tree_info = QUEUE_TREE_MAPPING.get(
665            &Pubkey::new_from_array(account.merkle_context.merkle_tree_pubkey.to_bytes())
666                .to_string(),
667        );
668        let cpi_context = if let Some(tree_info) = tree_info {
669            tree_info.cpi_context
670        } else {
671            warn!("Cpi context not found in queue tree mapping");
672            None
673        };
674        Ok(CompressedAccount {
675            address: account.compressed_account.address,
676            data: account.compressed_account.data,
677            hash,
678            lamports: account.compressed_account.lamports,
679            leaf_index: account.merkle_context.leaf_index,
680            tree_info: TreeInfo {
681                tree: Pubkey::new_from_array(account.merkle_context.merkle_tree_pubkey.to_bytes()),
682                queue: Pubkey::new_from_array(account.merkle_context.queue_pubkey.to_bytes()),
683                tree_type: account.merkle_context.tree_type,
684                cpi_context,
685                next_tree_info: None,
686            },
687            owner: Pubkey::new_from_array(account.compressed_account.owner.to_bytes()),
688            prove_by_index: account.merkle_context.prove_by_index,
689            seq: None,
690            slot_created: u64::MAX,
691        })
692    }
693}
694
695impl From<CompressedAccount> for CompressedAccountWithMerkleContext {
696    fn from(account: CompressedAccount) -> Self {
697        use light_compressed_account::Pubkey;
698        let compressed_account = ProgramCompressedAccount {
699            owner: Pubkey::new_from_array(account.owner.to_bytes()),
700            lamports: account.lamports,
701            address: account.address,
702            data: account.data,
703        };
704
705        let merkle_context = account
706            .tree_info
707            .to_light_merkle_context(account.leaf_index, account.prove_by_index);
708
709        CompressedAccountWithMerkleContext {
710            compressed_account,
711            merkle_context,
712        }
713    }
714}
715
716impl TryFrom<&photon_api::models::AccountV2> for CompressedAccount {
717    type Error = IndexerError;
718
719    fn try_from(account: &photon_api::models::AccountV2) -> Result<Self, Self::Error> {
720        let data = if let Some(data) = &account.data {
721            Ok::<Option<CompressedAccountData>, IndexerError>(Some(CompressedAccountData {
722                discriminator: data.discriminator.to_le_bytes(),
723                data: base64::decode_config(&data.data, base64::STANDARD_NO_PAD)
724                    .map_err(|_| IndexerError::InvalidResponseData)?,
725                data_hash: decode_base58_to_fixed_array(&data.data_hash)?,
726            }))
727        } else {
728            Ok::<Option<CompressedAccountData>, IndexerError>(None)
729        }?;
730
731        let owner = Pubkey::new_from_array(decode_base58_to_fixed_array(&account.owner)?);
732        let address = account
733            .address
734            .as_ref()
735            .map(|address| decode_base58_to_fixed_array(address))
736            .transpose()?;
737        let hash = decode_base58_to_fixed_array(&account.hash)?;
738
739        let tree_info = TreeInfo {
740            tree: Pubkey::new_from_array(decode_base58_to_fixed_array(
741                &account.merkle_context.tree,
742            )?),
743            queue: Pubkey::new_from_array(decode_base58_to_fixed_array(
744                &account.merkle_context.queue,
745            )?),
746            tree_type: TreeType::from(account.merkle_context.tree_type as u64),
747            cpi_context: decode_base58_option_to_pubkey(&account.merkle_context.cpi_context)?,
748            next_tree_info: account
749                .merkle_context
750                .next_tree_context
751                .as_ref()
752                .map(|ctx| NextTreeInfo::try_from(ctx.as_ref()))
753                .transpose()?,
754        };
755
756        Ok(CompressedAccount {
757            owner,
758            address,
759            data,
760            hash,
761            lamports: account.lamports,
762            leaf_index: account.leaf_index,
763            seq: account.seq,
764            slot_created: account.slot_created,
765            tree_info,
766            prove_by_index: account.prove_by_index,
767        })
768    }
769}
770
771impl TryFrom<&photon_api::models::Account> for CompressedAccount {
772    type Error = IndexerError;
773
774    fn try_from(account: &photon_api::models::Account) -> Result<Self, Self::Error> {
775        let data = if let Some(data) = &account.data {
776            Ok::<Option<CompressedAccountData>, IndexerError>(Some(CompressedAccountData {
777                discriminator: data.discriminator.to_le_bytes(),
778                data: base64::decode_config(&data.data, base64::STANDARD_NO_PAD)
779                    .map_err(|_| IndexerError::InvalidResponseData)?,
780                data_hash: decode_base58_to_fixed_array(&data.data_hash)?,
781            }))
782        } else {
783            Ok::<Option<CompressedAccountData>, IndexerError>(None)
784        }?;
785        let owner = Pubkey::new_from_array(decode_base58_to_fixed_array(&account.owner)?);
786        let address = account
787            .address
788            .as_ref()
789            .map(|address| decode_base58_to_fixed_array(address))
790            .transpose()?;
791        let hash = decode_base58_to_fixed_array(&account.hash)?;
792        let seq = account.seq;
793        let slot_created = account.slot_created;
794        let lamports = account.lamports;
795        let leaf_index = account.leaf_index;
796
797        let tree_info = QUEUE_TREE_MAPPING
798            .get(&account.tree)
799            .ok_or(IndexerError::InvalidResponseData)?;
800
801        let tree_info = TreeInfo {
802            cpi_context: tree_info.cpi_context,
803            queue: tree_info.queue,
804            tree_type: tree_info.tree_type,
805            next_tree_info: None,
806            tree: tree_info.tree,
807        };
808
809        Ok(CompressedAccount {
810            owner,
811            address,
812            data,
813            hash,
814            lamports,
815            leaf_index,
816            seq,
817            slot_created,
818            tree_info,
819            prove_by_index: false,
820        })
821    }
822}
823
824#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)]
825pub struct StateMerkleTreeAccounts {
826    pub merkle_tree: Pubkey,
827    pub nullifier_queue: Pubkey,
828    pub cpi_context: Pubkey,
829    pub tree_type: TreeType,
830}
831
832#[allow(clippy::from_over_into)]
833impl Into<TreeInfo> for StateMerkleTreeAccounts {
834    fn into(self) -> TreeInfo {
835        TreeInfo {
836            tree: self.merkle_tree,
837            queue: self.nullifier_queue,
838            cpi_context: Some(self.cpi_context),
839            tree_type: self.tree_type,
840            next_tree_info: None,
841        }
842    }
843}
844
845#[derive(Debug, Clone, Copy)]
846pub struct AddressMerkleTreeAccounts {
847    pub merkle_tree: Pubkey,
848    pub queue: Pubkey,
849}
850
851#[derive(Clone, Default, Debug, PartialEq)]
852pub struct CompressedTokenAccount {
853    /// Token-specific data (mint, owner, amount, delegate, state, tlv)
854    pub token: TokenData,
855    /// General account information (address, hash, lamports, merkle context, etc.)
856    pub account: CompressedAccount,
857}
858
859impl TryFrom<&photon_api::models::TokenAccount> for CompressedTokenAccount {
860    type Error = IndexerError;
861
862    fn try_from(token_account: &photon_api::models::TokenAccount) -> Result<Self, Self::Error> {
863        let account = CompressedAccount::try_from(token_account.account.as_ref())?;
864
865        let token = TokenData {
866            mint: Pubkey::new_from_array(decode_base58_to_fixed_array(
867                &token_account.token_data.mint,
868            )?),
869            owner: Pubkey::new_from_array(decode_base58_to_fixed_array(
870                &token_account.token_data.owner,
871            )?),
872            amount: token_account.token_data.amount,
873            delegate: token_account
874                .token_data
875                .delegate
876                .as_ref()
877                .map(|d| decode_base58_to_fixed_array(d).map(Pubkey::new_from_array))
878                .transpose()?,
879            state: match token_account.token_data.state {
880                photon_api::models::AccountState::Initialized => AccountState::Initialized,
881                photon_api::models::AccountState::Frozen => AccountState::Frozen,
882            },
883            tlv: token_account
884                .token_data
885                .tlv
886                .as_ref()
887                .map(|tlv| {
888                    let bytes = base64::decode_config(tlv, base64::STANDARD_NO_PAD)
889                        .map_err(|_| IndexerError::InvalidResponseData)?;
890                    Vec::<ExtensionStruct>::deserialize(&mut bytes.as_slice())
891                        .map_err(|_| IndexerError::InvalidResponseData)
892                })
893                .transpose()?,
894        };
895
896        Ok(CompressedTokenAccount { token, account })
897    }
898}
899
900impl TryFrom<&photon_api::models::TokenAccountV2> for CompressedTokenAccount {
901    type Error = IndexerError;
902
903    fn try_from(token_account: &photon_api::models::TokenAccountV2) -> Result<Self, Self::Error> {
904        let account = CompressedAccount::try_from(token_account.account.as_ref())?;
905
906        let token = TokenData {
907            mint: Pubkey::new_from_array(decode_base58_to_fixed_array(
908                &token_account.token_data.mint,
909            )?),
910            owner: Pubkey::new_from_array(decode_base58_to_fixed_array(
911                &token_account.token_data.owner,
912            )?),
913            amount: token_account.token_data.amount,
914            delegate: token_account
915                .token_data
916                .delegate
917                .as_ref()
918                .map(|d| decode_base58_to_fixed_array(d).map(Pubkey::new_from_array))
919                .transpose()?,
920            state: match token_account.token_data.state {
921                photon_api::models::AccountState::Initialized => AccountState::Initialized,
922                photon_api::models::AccountState::Frozen => AccountState::Frozen,
923            },
924            tlv: token_account
925                .token_data
926                .tlv
927                .as_ref()
928                .map(|tlv| {
929                    let bytes = base64::decode_config(tlv, base64::STANDARD_NO_PAD)
930                        .map_err(|_| IndexerError::InvalidResponseData)?;
931                    Vec::<ExtensionStruct>::deserialize(&mut bytes.as_slice())
932                        .map_err(|_| IndexerError::InvalidResponseData)
933                })
934                .transpose()?,
935        };
936
937        Ok(CompressedTokenAccount { token, account })
938    }
939}
940
941#[allow(clippy::from_over_into)]
942impl Into<light_token::compat::TokenDataWithMerkleContext> for CompressedTokenAccount {
943    fn into(self) -> light_token::compat::TokenDataWithMerkleContext {
944        let compressed_account = CompressedAccountWithMerkleContext::from(self.account);
945
946        light_token::compat::TokenDataWithMerkleContext {
947            token_data: self.token,
948            compressed_account,
949        }
950    }
951}
952
953#[allow(clippy::from_over_into)]
954impl Into<Vec<light_token::compat::TokenDataWithMerkleContext>>
955    for super::response::Response<super::response::ItemsWithCursor<CompressedTokenAccount>>
956{
957    fn into(self) -> Vec<light_token::compat::TokenDataWithMerkleContext> {
958        self.value
959            .items
960            .into_iter()
961            .map(
962                |token_account| light_token::compat::TokenDataWithMerkleContext {
963                    token_data: token_account.token,
964                    compressed_account: CompressedAccountWithMerkleContext::from(
965                        token_account.account.clone(),
966                    ),
967                },
968            )
969            .collect::<Vec<light_token::compat::TokenDataWithMerkleContext>>()
970    }
971}
972
973impl TryFrom<light_token::compat::TokenDataWithMerkleContext> for CompressedTokenAccount {
974    type Error = IndexerError;
975
976    fn try_from(
977        token_data_with_context: light_token::compat::TokenDataWithMerkleContext,
978    ) -> Result<Self, Self::Error> {
979        let account = CompressedAccount::try_from(token_data_with_context.compressed_account)?;
980
981        Ok(CompressedTokenAccount {
982            token: token_data_with_context.token_data,
983            account,
984        })
985    }
986}
987
988#[derive(Clone, Default, Debug, PartialEq)]
989pub struct TokenBalance {
990    pub balance: u64,
991    pub mint: Pubkey,
992}
993
994impl TryFrom<&photon_api::models::TokenBalance> for TokenBalance {
995    type Error = IndexerError;
996
997    fn try_from(token_balance: &photon_api::models::TokenBalance) -> Result<Self, Self::Error> {
998        Ok(TokenBalance {
999            balance: token_balance.balance,
1000            mint: Pubkey::new_from_array(decode_base58_to_fixed_array(&token_balance.mint)?),
1001        })
1002    }
1003}
1004
1005#[derive(Debug, Clone, PartialEq, Default)]
1006pub struct SignatureWithMetadata {
1007    pub block_time: u64,
1008    pub signature: String,
1009    pub slot: u64,
1010}
1011
1012impl TryFrom<&photon_api::models::SignatureInfo> for SignatureWithMetadata {
1013    type Error = IndexerError;
1014
1015    fn try_from(sig_info: &photon_api::models::SignatureInfo) -> Result<Self, Self::Error> {
1016        Ok(SignatureWithMetadata {
1017            block_time: sig_info.block_time,
1018            signature: sig_info.signature.clone(),
1019            slot: sig_info.slot,
1020        })
1021    }
1022}
1023
1024#[derive(Clone, Default, Debug, PartialEq)]
1025pub struct OwnerBalance {
1026    pub balance: u64,
1027    pub owner: Pubkey,
1028}
1029
1030impl TryFrom<&photon_api::models::OwnerBalance> for OwnerBalance {
1031    type Error = IndexerError;
1032
1033    fn try_from(owner_balance: &photon_api::models::OwnerBalance) -> Result<Self, Self::Error> {
1034        Ok(OwnerBalance {
1035            balance: owner_balance.balance,
1036            owner: Pubkey::new_from_array(decode_base58_to_fixed_array(&owner_balance.owner)?),
1037        })
1038    }
1039}