light_client/indexer/
types.rs

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