Skip to main content

light_client/indexer/
types.rs

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