Skip to main content

light_client/indexer/
photon_indexer.rs

1use std::{fmt::Debug, time::Duration};
2
3use async_trait::async_trait;
4use bs58;
5use light_sdk_types::constants::STATE_MERKLE_TREE_CANOPY_DEPTH;
6use photon_api::apis::configuration::Configuration;
7use solana_pubkey::Pubkey;
8use tracing::{error, trace, warn};
9
10use super::types::{
11    AccountInterface, CompressedAccount, CompressedTokenAccount, OwnerBalance,
12    SignatureWithMetadata, TokenAccountInterface, TokenBalance,
13};
14use crate::indexer::{
15    base58::Base58Conversions,
16    config::RetryConfig,
17    response::{Context, Items, ItemsWithCursor, Response},
18    Address, AddressWithTree, GetCompressedAccountsByOwnerConfig,
19    GetCompressedTokenAccountsByOwnerOrDelegateOptions, Hash, Indexer, IndexerError,
20    IndexerRpcConfig, MerkleProof, NewAddressProofWithContext, PaginatedOptions,
21};
22
23// Tests are in program-tests/client-test/tests/light-client.rs
24pub struct PhotonIndexer {
25    configuration: Configuration,
26}
27
28impl PhotonIndexer {
29    pub fn default_path() -> String {
30        "http://127.0.0.1:8784".to_string()
31    }
32}
33
34impl PhotonIndexer {
35    async fn retry<F, Fut, T>(
36        &self,
37        config: RetryConfig,
38        mut operation: F,
39    ) -> Result<T, IndexerError>
40    where
41        F: FnMut() -> Fut,
42        Fut: std::future::Future<Output = Result<T, IndexerError>>,
43    {
44        let max_retries = config.num_retries;
45        let mut attempts = 0;
46        let mut delay_ms = config.delay_ms;
47        let max_delay_ms = config.max_delay_ms;
48
49        loop {
50            attempts += 1;
51
52            trace!(
53                "Attempt {}/{}: No rate limiter configured",
54                attempts,
55                max_retries
56            );
57
58            trace!("Attempt {}/{}: Executing operation", attempts, max_retries);
59            let result = operation().await;
60
61            match result {
62                Ok(value) => {
63                    trace!("Attempt {}/{}: Operation succeeded.", attempts, max_retries);
64                    return Ok(value);
65                }
66                Err(e) => {
67                    let is_retryable = match &e {
68                        IndexerError::ApiError(_) => {
69                            warn!("API Error: {}", e);
70                            true
71                        }
72                        IndexerError::PhotonError {
73                            context: _,
74                            message: _,
75                        } => {
76                            warn!("Operation failed, checking if retryable...");
77                            true
78                        }
79                        IndexerError::IndexerNotSyncedToSlot => true,
80                        IndexerError::Base58DecodeError { .. } => false,
81                        IndexerError::AccountNotFound => false,
82                        IndexerError::InvalidParameters(_) => false,
83                        IndexerError::NotImplemented(_) => false,
84                        _ => false,
85                    };
86
87                    if is_retryable && attempts < max_retries {
88                        warn!(
89                            "Attempt {}/{}: Operation failed. Retrying",
90                            attempts, max_retries
91                        );
92
93                        tokio::time::sleep(Duration::from_millis(delay_ms)).await;
94                        delay_ms = std::cmp::min(delay_ms * 2, max_delay_ms);
95                    } else {
96                        if is_retryable {
97                            error!("Operation failed after max retries.");
98                        } else {
99                            error!("Operation failed with non-retryable error.");
100                        }
101                        return Err(e);
102                    }
103                }
104            }
105        }
106    }
107}
108
109impl PhotonIndexer {
110    pub fn new(url: String) -> Self {
111        let configuration = Configuration::new(url);
112        PhotonIndexer { configuration }
113    }
114
115    pub fn new_with_config(configuration: Configuration) -> Self {
116        PhotonIndexer { configuration }
117    }
118
119    fn extract_result<T>(context: &str, result: Option<T>) -> Result<T, IndexerError> {
120        result.ok_or_else(|| IndexerError::missing_result(context, "value not present"))
121    }
122
123    fn check_api_error<E: std::fmt::Debug>(
124        context: &str,
125        error: Option<E>,
126    ) -> Result<(), IndexerError> {
127        if let Some(error) = error {
128            return Err(IndexerError::ApiError(format!(
129                "API error in {}: {:?}",
130                context, error
131            )));
132        }
133        Ok(())
134    }
135
136    fn extract_result_with_error_check<T, E: std::fmt::Debug>(
137        context: &str,
138        error: Option<E>,
139        result: Option<T>,
140    ) -> Result<T, IndexerError> {
141        Self::check_api_error(context, error)?;
142        Self::extract_result(context, result)
143    }
144
145    fn build_account_params(
146        &self,
147        address: Option<Address>,
148        hash: Option<Hash>,
149    ) -> Result<photon_api::types::PostGetCompressedAccountBodyParams, IndexerError> {
150        match (address, hash) {
151            (None, None) => Err(IndexerError::InvalidParameters(
152                "Either address or hash must be provided".to_string(),
153            )),
154            (Some(_), Some(_)) => Err(IndexerError::InvalidParameters(
155                "Only one of address or hash must be provided".to_string(),
156            )),
157            (address, hash) => Ok(photon_api::types::PostGetCompressedAccountBodyParams {
158                address: address.map(|x| photon_api::types::SerializablePubkey(x.to_base58())),
159                hash: hash.map(|x| photon_api::types::Hash(x.to_base58())),
160            }),
161        }
162    }
163}
164
165impl Debug for PhotonIndexer {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        f.debug_struct("PhotonIndexer")
168            .field("configuration", &self.configuration)
169            .finish()
170    }
171}
172
173#[async_trait]
174impl Indexer for PhotonIndexer {
175    async fn get_compressed_account(
176        &self,
177        address: Address,
178        config: Option<IndexerRpcConfig>,
179    ) -> Result<Response<Option<CompressedAccount>>, IndexerError> {
180        let config = config.unwrap_or_default();
181        self.retry(config.retry_config, || async {
182            let params = self.build_account_params(Some(address), None)?;
183            let request = photon_api::apis::default_api::make_get_compressed_account_body(params);
184
185            let result = photon_api::apis::default_api::get_compressed_account_post(
186                &self.configuration,
187                request,
188            )
189            .await?;
190
191            let api_response = result.result.ok_or_else(|| {
192                IndexerError::ApiError(
193                    result
194                        .error
195                        .map(|e| format!("{:?}", e))
196                        .unwrap_or_else(|| "Unknown error".to_string()),
197                )
198            })?;
199
200            if api_response.context.slot < config.slot {
201                return Err(IndexerError::IndexerNotSyncedToSlot);
202            }
203            let account = match api_response.value {
204                Some(ref acc) => Some(CompressedAccount::try_from(acc)?),
205                None => None,
206            };
207
208            Ok(Response {
209                context: Context {
210                    slot: api_response.context.slot,
211                },
212                value: account,
213            })
214        })
215        .await
216    }
217
218    async fn get_compressed_account_by_hash(
219        &self,
220        hash: Hash,
221        config: Option<IndexerRpcConfig>,
222    ) -> Result<Response<Option<CompressedAccount>>, IndexerError> {
223        let config = config.unwrap_or_default();
224        self.retry(config.retry_config, || async {
225            let params = self.build_account_params(None, Some(hash))?;
226            let request = photon_api::apis::default_api::make_get_compressed_account_body(params);
227
228            let result = photon_api::apis::default_api::get_compressed_account_post(
229                &self.configuration,
230                request,
231            )
232            .await?;
233
234            Self::check_api_error("get_compressed_account_by_hash", result.error)?;
235            let api_response =
236                Self::extract_result("get_compressed_account_by_hash", result.result)?;
237
238            if api_response.context.slot < config.slot {
239                return Err(IndexerError::IndexerNotSyncedToSlot);
240            }
241            let account = match api_response.value {
242                Some(ref acc) => Some(CompressedAccount::try_from(acc)?),
243                None => None,
244            };
245
246            Ok(Response {
247                context: Context {
248                    slot: api_response.context.slot,
249                },
250                value: account,
251            })
252        })
253        .await
254    }
255
256    async fn get_compressed_accounts_by_owner(
257        &self,
258        owner: &Pubkey,
259        options: Option<GetCompressedAccountsByOwnerConfig>,
260        config: Option<IndexerRpcConfig>,
261    ) -> Result<Response<ItemsWithCursor<CompressedAccount>>, IndexerError> {
262        let config = config.unwrap_or_default();
263        self.retry(config.retry_config, || async {
264            #[cfg(feature = "v2")]
265            {
266                let params = photon_api::types::PostGetCompressedAccountsByOwnerV2BodyParams {
267                    cursor: options.as_ref().and_then(|o| o.cursor.clone()).map(photon_api::types::Hash),
268                    data_slice: options.as_ref().and_then(|o| {
269                        o.data_slice.as_ref().map(|ds| {
270                            photon_api::types::DataSlice {
271                                length: ds.length as u64,
272                                offset: ds.offset as u64,
273                            }
274                        })
275                    }),
276                    filters: options.as_ref().and_then(|o| o.filters_to_photon()).unwrap_or_default(),
277                    limit: options.as_ref().and_then(|o| o.limit).map(|l| photon_api::types::Limit(l as u64)),
278                    owner: photon_api::types::SerializablePubkey(owner.to_string()),
279                };
280                let request = photon_api::apis::default_api::make_get_compressed_accounts_by_owner_v2_body(params);
281                let result =
282                    photon_api::apis::default_api::get_compressed_accounts_by_owner_v2_post(
283                        &self.configuration,
284                        request,
285                    )
286                    .await?;
287
288                Self::check_api_error("get_compressed_accounts_by_owner_v2", result.error)?;
289                let response = Self::extract_result("get_compressed_accounts_by_owner_v2", result.result)?;
290
291                if response.context.slot < config.slot {
292                    return Err(IndexerError::IndexerNotSyncedToSlot);
293                }
294                let accounts: Result<Vec<_>, _> = response
295                    .value
296                    .items
297                    .iter()
298                    .map(CompressedAccount::try_from)
299                    .collect();
300
301                let cursor = response.value.cursor.map(|h| h.0);
302
303                Ok(Response {
304                    context: Context {
305                        slot: response.context.slot,
306                    },
307                    value: ItemsWithCursor {
308                        items: accounts?,
309                        cursor,
310                    },
311                })
312            }
313            #[cfg(not(feature = "v2"))]
314            {
315                let params = photon_api::types::PostGetCompressedAccountsByOwnerBodyParams {
316                    cursor: options.as_ref().and_then(|o| o.cursor.clone()).map(photon_api::types::Hash),
317                    data_slice: options.as_ref().and_then(|o| {
318                        o.data_slice.as_ref().map(|ds| {
319                            photon_api::types::DataSlice {
320                                length: ds.length as u64,
321                                offset: ds.offset as u64,
322                            }
323                        })
324                    }),
325                    filters: options.as_ref().and_then(|o| o.filters_to_photon()).unwrap_or_default(),
326                    limit: options.as_ref().and_then(|o| o.limit).map(|l| photon_api::types::Limit(l as u64)),
327                    owner: photon_api::types::SerializablePubkey(owner.to_string()),
328                };
329                let request = photon_api::types::PostGetCompressedAccountsByOwnerBody {
330                    id: photon_api::types::PostGetCompressedAccountsByOwnerBodyId::TestAccount,
331                    jsonrpc: photon_api::types::PostGetCompressedAccountsByOwnerBodyJsonrpc::X20,
332                    method: photon_api::types::PostGetCompressedAccountsByOwnerBodyMethod::GetCompressedAccountsByOwner,
333                    params,
334                };
335                let result = photon_api::apis::default_api::get_compressed_accounts_by_owner_post(
336                    &self.configuration,
337                    request,
338                )
339                .await?;
340
341                Self::check_api_error("get_compressed_accounts_by_owner", result.error)?;
342                let response = Self::extract_result("get_compressed_accounts_by_owner", result.result)?;
343
344                if response.context.slot < config.slot {
345                    return Err(IndexerError::IndexerNotSyncedToSlot);
346                }
347                let accounts: Result<Vec<_>, _> = response
348                    .value
349                    .items
350                    .iter()
351                    .map(CompressedAccount::try_from)
352                    .collect();
353
354                let cursor = response.value.cursor.map(|h| h.0);
355
356                Ok(Response {
357                    context: Context {
358                        slot: response.context.slot,
359                    },
360                    value: ItemsWithCursor {
361                        items: accounts?,
362                        cursor,
363                    },
364                })
365            }
366        })
367        .await
368    }
369
370    async fn get_compressed_balance(
371        &self,
372        address: Option<Address>,
373        hash: Option<Hash>,
374        config: Option<IndexerRpcConfig>,
375    ) -> Result<Response<u64>, IndexerError> {
376        let config = config.unwrap_or_default();
377        self.retry(config.retry_config, || async {
378            let params = photon_api::types::PostGetCompressedAccountBalanceBodyParams {
379                address: address.map(|x| photon_api::types::SerializablePubkey(x.to_base58())),
380                hash: hash.map(|x| photon_api::types::Hash(x.to_base58())),
381            };
382            let request =
383                photon_api::apis::default_api::make_get_compressed_account_balance_body(params);
384
385            let result = photon_api::apis::default_api::get_compressed_account_balance_post(
386                &self.configuration,
387                request,
388            )
389            .await?;
390
391            Self::check_api_error("get_compressed_account_balance", result.error)?;
392            let api_response =
393                Self::extract_result("get_compressed_account_balance", result.result)?;
394
395            if api_response.context.slot < config.slot {
396                return Err(IndexerError::IndexerNotSyncedToSlot);
397            }
398            Ok(Response {
399                context: Context {
400                    slot: api_response.context.slot,
401                },
402                value: api_response.value.0,
403            })
404        })
405        .await
406    }
407
408    async fn get_compressed_balance_by_owner(
409        &self,
410        owner: &Pubkey,
411        config: Option<IndexerRpcConfig>,
412    ) -> Result<Response<u64>, IndexerError> {
413        let config = config.unwrap_or_default();
414        self.retry(config.retry_config, || async {
415            let params = photon_api::types::PostGetCompressedBalanceByOwnerBodyParams {
416                owner: photon_api::types::SerializablePubkey(owner.to_string()),
417            };
418            let request =
419                photon_api::apis::default_api::make_get_compressed_balance_by_owner_body(params);
420
421            let result = photon_api::apis::default_api::get_compressed_balance_by_owner_post(
422                &self.configuration,
423                request,
424            )
425            .await?;
426
427            Self::check_api_error("get_compressed_balance_by_owner", result.error)?;
428            let api_response =
429                Self::extract_result("get_compressed_balance_by_owner", result.result)?;
430
431            if api_response.context.slot < config.slot {
432                return Err(IndexerError::IndexerNotSyncedToSlot);
433            }
434            Ok(Response {
435                context: Context {
436                    slot: api_response.context.slot,
437                },
438                value: api_response.value.0,
439            })
440        })
441        .await
442    }
443
444    async fn get_compressed_mint_token_holders(
445        &self,
446        mint: &Pubkey,
447        options: Option<PaginatedOptions>,
448        config: Option<IndexerRpcConfig>,
449    ) -> Result<Response<ItemsWithCursor<OwnerBalance>>, IndexerError> {
450        let config = config.unwrap_or_default();
451        self.retry(config.retry_config, || async {
452            let params = photon_api::types::PostGetCompressedMintTokenHoldersBodyParams {
453                mint: photon_api::types::SerializablePubkey(mint.to_string()),
454                cursor: options
455                    .as_ref()
456                    .and_then(|o| o.cursor.clone())
457                    .map(photon_api::types::Base58String),
458                limit: options
459                    .as_ref()
460                    .and_then(|o| o.limit)
461                    .map(|l| photon_api::types::Limit(l as u64)),
462            };
463            let request =
464                photon_api::apis::default_api::make_get_compressed_mint_token_holders_body(params);
465
466            let result = photon_api::apis::default_api::get_compressed_mint_token_holders_post(
467                &self.configuration,
468                request,
469            )
470            .await?;
471
472            Self::check_api_error("get_compressed_mint_token_holders", result.error)?;
473            let api_response =
474                Self::extract_result("get_compressed_mint_token_holders", result.result)?;
475
476            if api_response.context.slot < config.slot {
477                return Err(IndexerError::IndexerNotSyncedToSlot);
478            }
479            let owner_balances: Result<Vec<_>, _> = api_response
480                .value
481                .items
482                .iter()
483                .map(OwnerBalance::try_from)
484                .collect();
485
486            let cursor = api_response.value.cursor.map(|c| c.0);
487
488            Ok(Response {
489                context: Context {
490                    slot: api_response.context.slot,
491                },
492                value: ItemsWithCursor {
493                    items: owner_balances?,
494                    cursor,
495                },
496            })
497        })
498        .await
499    }
500
501    async fn get_compressed_token_account_balance(
502        &self,
503        address: Option<Address>,
504        hash: Option<Hash>,
505        config: Option<IndexerRpcConfig>,
506    ) -> Result<Response<u64>, IndexerError> {
507        let config = config.unwrap_or_default();
508        self.retry(config.retry_config, || async {
509            let params = photon_api::types::PostGetCompressedTokenAccountBalanceBodyParams {
510                address: address.map(|x| photon_api::types::SerializablePubkey(x.to_base58())),
511                hash: hash.map(|x| photon_api::types::Hash(x.to_base58())),
512            };
513            let request =
514                photon_api::apis::default_api::make_get_compressed_token_account_balance_body(
515                    params,
516                );
517
518            let result = photon_api::apis::default_api::get_compressed_token_account_balance_post(
519                &self.configuration,
520                request,
521            )
522            .await?;
523
524            Self::check_api_error("get_compressed_token_account_balance", result.error)?;
525            let api_response =
526                Self::extract_result("get_compressed_token_account_balance", result.result)?;
527
528            if api_response.context.slot < config.slot {
529                return Err(IndexerError::IndexerNotSyncedToSlot);
530            }
531            Ok(Response {
532                context: Context {
533                    slot: api_response.context.slot,
534                },
535                value: api_response.value.amount.0,
536            })
537        })
538        .await
539    }
540
541    async fn get_compressed_token_accounts_by_delegate(
542        &self,
543        delegate: &Pubkey,
544        options: Option<GetCompressedTokenAccountsByOwnerOrDelegateOptions>,
545        config: Option<IndexerRpcConfig>,
546    ) -> Result<Response<ItemsWithCursor<CompressedTokenAccount>>, IndexerError> {
547        let config = config.unwrap_or_default();
548        self.retry(config.retry_config, || async {
549            #[cfg(feature = "v2")]
550            {
551                let params = photon_api::types::PostGetCompressedTokenAccountsByDelegateV2BodyParams {
552                    cursor: options.as_ref().and_then(|o| o.cursor.clone()).map(photon_api::types::Base58String),
553                    limit: options.as_ref().and_then(|o| o.limit).map(|l| photon_api::types::Limit(l as u64)),
554                    mint: options.as_ref().and_then(|o| o.mint.as_ref()).map(|x| photon_api::types::SerializablePubkey(x.to_string())),
555                    delegate: photon_api::types::SerializablePubkey(delegate.to_string()),
556                };
557                let request = photon_api::apis::default_api::make_get_compressed_token_accounts_by_delegate_v2_body(params);
558
559                let result = photon_api::apis::default_api::get_compressed_token_accounts_by_delegate_v2_post(
560                    &self.configuration,
561                    request,
562                )
563                .await?;
564
565                Self::check_api_error("get_compressed_token_accounts_by_delegate_v2", result.error)?;
566                let response = Self::extract_result("get_compressed_token_accounts_by_delegate_v2", result.result)?;
567
568                if response.context.slot < config.slot {
569                    return Err(IndexerError::IndexerNotSyncedToSlot);
570                }
571
572                let token_accounts: Result<Vec<_>, _> = response
573                    .value
574                    .items
575                    .iter()
576                    .map(CompressedTokenAccount::try_from)
577                    .collect();
578
579                let cursor = response.value.cursor.map(|h| h.0);
580
581                Ok(Response {
582                    context: Context {
583                        slot: response.context.slot,
584                    },
585                    value: ItemsWithCursor {
586                        items: token_accounts?,
587                        cursor,
588                    },
589                })
590            }
591            #[cfg(not(feature = "v2"))]
592            {
593                let params = photon_api::types::PostGetCompressedTokenAccountsByDelegateBodyParams {
594                    delegate: photon_api::types::SerializablePubkey(delegate.to_string()),
595                    mint: options.as_ref().and_then(|o| o.mint.as_ref()).map(|x| photon_api::types::SerializablePubkey(x.to_string())),
596                    cursor: options.as_ref().and_then(|o| o.cursor.clone()).map(photon_api::types::Base58String),
597                    limit: options.as_ref().and_then(|o| o.limit).map(|l| photon_api::types::Limit(l as u64)),
598                };
599                let request = photon_api::types::PostGetCompressedTokenAccountsByDelegateBody {
600                    id: photon_api::types::PostGetCompressedTokenAccountsByDelegateBodyId::TestAccount,
601                    jsonrpc: photon_api::types::PostGetCompressedTokenAccountsByDelegateBodyJsonrpc::X20,
602                    method: photon_api::types::PostGetCompressedTokenAccountsByDelegateBodyMethod::GetCompressedTokenAccountsByDelegate,
603                    params,
604                };
605
606                let result = photon_api::apis::default_api::get_compressed_token_accounts_by_delegate_post(
607                    &self.configuration,
608                    request,
609                )
610                .await?;
611
612                Self::check_api_error("get_compressed_token_accounts_by_delegate", result.error)?;
613                let response = Self::extract_result("get_compressed_token_accounts_by_delegate", result.result)?;
614
615                if response.context.slot < config.slot {
616                    return Err(IndexerError::IndexerNotSyncedToSlot);
617                }
618
619                let token_accounts: Result<Vec<_>, _> = response
620                    .value
621                    .items
622                    .iter()
623                    .map(CompressedTokenAccount::try_from)
624                    .collect();
625
626                let cursor = response.value.cursor.map(|h| h.0);
627
628                Ok(Response {
629                    context: Context {
630                        slot: response.context.slot,
631                    },
632                    value: ItemsWithCursor {
633                        items: token_accounts?,
634                        cursor,
635                    },
636                })
637            }
638        })
639        .await
640    }
641
642    async fn get_compressed_token_accounts_by_owner(
643        &self,
644        owner: &Pubkey,
645        options: Option<GetCompressedTokenAccountsByOwnerOrDelegateOptions>,
646        config: Option<IndexerRpcConfig>,
647    ) -> Result<Response<ItemsWithCursor<CompressedTokenAccount>>, IndexerError> {
648        let config = config.unwrap_or_default();
649        self.retry(config.retry_config, || async {
650            #[cfg(feature = "v2")]
651            {
652                let params = photon_api::types::PostGetCompressedTokenAccountsByOwnerV2BodyParams {
653                    cursor: options.as_ref().and_then(|o| o.cursor.clone()).map(photon_api::types::Base58String),
654                    limit: options.as_ref().and_then(|o| o.limit).map(|l| photon_api::types::Limit(l as u64)),
655                    mint: options
656                        .as_ref()
657                        .and_then(|o| o.mint.as_ref())
658                        .map(|x| photon_api::types::SerializablePubkey(x.to_string())),
659                    owner: photon_api::types::SerializablePubkey(owner.to_string()),
660                };
661                let request = photon_api::apis::default_api::make_get_compressed_token_accounts_by_owner_v2_body(params);
662                let result =
663                    photon_api::apis::default_api::get_compressed_token_accounts_by_owner_v2_post(
664                        &self.configuration,
665                        request,
666                    )
667                    .await?;
668
669                Self::check_api_error("get_compressed_token_accounts_by_owner_v2", result.error)?;
670                let response = Self::extract_result("get_compressed_token_accounts_by_owner_v2", result.result)?;
671
672                if response.context.slot < config.slot {
673                    return Err(IndexerError::IndexerNotSyncedToSlot);
674                }
675                let token_accounts: Result<Vec<_>, _> = response
676                    .value
677                    .items
678                    .iter()
679                    .map(CompressedTokenAccount::try_from)
680                    .collect();
681
682                let cursor = response.value.cursor.map(|h| h.0);
683
684                Ok(Response {
685                    context: Context {
686                        slot: response.context.slot,
687                    },
688                    value: ItemsWithCursor {
689                        items: token_accounts?,
690                        cursor,
691                    },
692                })
693            }
694            #[cfg(not(feature = "v2"))]
695            {
696                let params = photon_api::types::PostGetCompressedTokenAccountsByOwnerBodyParams {
697                    owner: photon_api::types::SerializablePubkey(owner.to_string()),
698                    mint: options
699                        .as_ref()
700                        .and_then(|o| o.mint.as_ref())
701                        .map(|x| photon_api::types::SerializablePubkey(x.to_string())),
702                    cursor: options.as_ref().and_then(|o| o.cursor.clone()).map(photon_api::types::Base58String),
703                    limit: options.as_ref().and_then(|o| o.limit).map(|l| photon_api::types::Limit(l as u64)),
704                };
705                let request = photon_api::types::PostGetCompressedTokenAccountsByOwnerBody {
706                    id: photon_api::types::PostGetCompressedTokenAccountsByOwnerBodyId::TestAccount,
707                    jsonrpc: photon_api::types::PostGetCompressedTokenAccountsByOwnerBodyJsonrpc::X20,
708                    method: photon_api::types::PostGetCompressedTokenAccountsByOwnerBodyMethod::GetCompressedTokenAccountsByOwner,
709                    params,
710                };
711
712                let result =
713                    photon_api::apis::default_api::get_compressed_token_accounts_by_owner_post(
714                        &self.configuration,
715                        request,
716                    )
717                    .await?;
718
719                Self::check_api_error("get_compressed_token_accounts_by_owner", result.error)?;
720                let response = Self::extract_result("get_compressed_token_accounts_by_owner", result.result)?;
721
722                if response.context.slot < config.slot {
723                    return Err(IndexerError::IndexerNotSyncedToSlot);
724                }
725                let token_accounts: Result<Vec<_>, _> = response
726                    .value
727                    .items
728                    .iter()
729                    .map(CompressedTokenAccount::try_from)
730                    .collect();
731
732                let cursor = response.value.cursor.map(|h| h.0);
733
734                Ok(Response {
735                    context: Context {
736                        slot: response.context.slot,
737                    },
738                    value: ItemsWithCursor {
739                        items: token_accounts?,
740                        cursor,
741                    },
742                })
743            }
744        })
745        .await
746    }
747
748    async fn get_compressed_token_balances_by_owner_v2(
749        &self,
750        owner: &Pubkey,
751        options: Option<GetCompressedTokenAccountsByOwnerOrDelegateOptions>,
752        config: Option<IndexerRpcConfig>,
753    ) -> Result<Response<ItemsWithCursor<TokenBalance>>, IndexerError> {
754        let config = config.unwrap_or_default();
755        self.retry(config.retry_config, || async {
756            #[cfg(feature = "v2")]
757            {
758                let params = photon_api::types::PostGetCompressedTokenBalancesByOwnerV2BodyParams {
759                    owner: photon_api::types::SerializablePubkey(owner.to_string()),
760                    mint: options
761                        .as_ref()
762                        .and_then(|o| o.mint.as_ref())
763                        .map(|x| photon_api::types::SerializablePubkey(x.to_string())),
764                    cursor: options.as_ref().and_then(|o| o.cursor.clone()).map(photon_api::types::Base58String),
765                    limit: options.as_ref().and_then(|o| o.limit).map(|l| photon_api::types::Limit(l as u64)),
766                };
767                let request = photon_api::apis::default_api::make_get_compressed_token_balances_by_owner_v2_body(params);
768
769                let result =
770                    photon_api::apis::default_api::get_compressed_token_balances_by_owner_v2_post(
771                        &self.configuration,
772                        request,
773                    )
774                    .await?;
775
776                Self::check_api_error("get_compressed_token_balances_by_owner_v2", result.error)?;
777                let api_response = Self::extract_result("get_compressed_token_balances_by_owner_v2", result.result)?;
778
779                if api_response.context.slot < config.slot {
780                    return Err(IndexerError::IndexerNotSyncedToSlot);
781                }
782
783                let token_balances: Result<Vec<_>, _> = api_response
784                    .value
785                    .items
786                    .iter()
787                    .map(TokenBalance::try_from)
788                    .collect();
789
790                Ok(Response {
791                    context: Context {
792                        slot: api_response.context.slot,
793                    },
794                    value: ItemsWithCursor {
795                        items: token_balances?,
796                        cursor: api_response.value.cursor.map(|c| c.0),
797                    },
798                })
799            }
800            #[cfg(not(feature = "v2"))]
801            {
802                let params = photon_api::types::PostGetCompressedTokenBalancesByOwnerBodyParams {
803                    owner: photon_api::types::SerializablePubkey(owner.to_string()),
804                    mint: options
805                        .as_ref()
806                        .and_then(|o| o.mint.as_ref())
807                        .map(|x| photon_api::types::SerializablePubkey(x.to_string())),
808                    cursor: options.as_ref().and_then(|o| o.cursor.clone()).map(photon_api::types::Base58String),
809                    limit: options.as_ref().and_then(|o| o.limit).map(|l| photon_api::types::Limit(l as u64)),
810                };
811                let request = photon_api::types::PostGetCompressedTokenBalancesByOwnerBody {
812                    id: photon_api::types::PostGetCompressedTokenBalancesByOwnerBodyId::TestAccount,
813                    jsonrpc: photon_api::types::PostGetCompressedTokenBalancesByOwnerBodyJsonrpc::X20,
814                    method: photon_api::types::PostGetCompressedTokenBalancesByOwnerBodyMethod::GetCompressedTokenBalancesByOwner,
815                    params,
816                };
817
818                let result =
819                    photon_api::apis::default_api::get_compressed_token_balances_by_owner_post(
820                        &self.configuration,
821                        request,
822                    )
823                    .await?;
824
825                Self::check_api_error("get_compressed_token_balances_by_owner", result.error)?;
826                let api_response = Self::extract_result("get_compressed_token_balances_by_owner", result.result)?;
827
828                if api_response.context.slot < config.slot {
829                    return Err(IndexerError::IndexerNotSyncedToSlot);
830                }
831
832                let token_balances: Result<Vec<_>, _> = api_response
833                    .value
834                    .token_balances
835                    .iter()
836                    .map(TokenBalance::try_from)
837                    .collect();
838
839                Ok(Response {
840                    context: Context {
841                        slot: api_response.context.slot,
842                    },
843                    value: ItemsWithCursor {
844                        items: token_balances?,
845                        cursor: api_response.value.cursor.map(|c| c.0),
846                    },
847                })
848            }
849        })
850        .await
851    }
852
853    async fn get_compression_signatures_for_account(
854        &self,
855        hash: Hash,
856        config: Option<IndexerRpcConfig>,
857    ) -> Result<Response<Items<SignatureWithMetadata>>, IndexerError> {
858        let config = config.unwrap_or_default();
859        self.retry(config.retry_config, || async {
860            let params = photon_api::types::PostGetCompressionSignaturesForAccountBodyParams {
861                hash: photon_api::types::Hash(hash.to_base58()),
862            };
863            let request =
864                photon_api::apis::default_api::make_get_compression_signatures_for_account_body(
865                    params,
866                );
867
868            let result =
869                photon_api::apis::default_api::get_compression_signatures_for_account_post(
870                    &self.configuration,
871                    request,
872                )
873                .await?;
874
875            Self::check_api_error("get_compression_signatures_for_account", result.error)?;
876            let api_response =
877                Self::extract_result("get_compression_signatures_for_account", result.result)?;
878
879            if api_response.context.slot < config.slot {
880                return Err(IndexerError::IndexerNotSyncedToSlot);
881            }
882            let signatures: Vec<SignatureWithMetadata> = api_response
883                .value
884                .items
885                .iter()
886                .map(SignatureWithMetadata::from)
887                .collect();
888
889            Ok(Response {
890                context: Context {
891                    slot: api_response.context.slot,
892                },
893                value: Items { items: signatures },
894            })
895        })
896        .await
897    }
898
899    async fn get_compression_signatures_for_address(
900        &self,
901        address: &[u8; 32],
902        options: Option<PaginatedOptions>,
903        config: Option<IndexerRpcConfig>,
904    ) -> Result<Response<ItemsWithCursor<SignatureWithMetadata>>, IndexerError> {
905        let config = config.unwrap_or_default();
906        self.retry(config.retry_config, || async {
907            let params = photon_api::types::PostGetCompressionSignaturesForAddressBodyParams {
908                address: photon_api::types::SerializablePubkey(address.to_base58()),
909                cursor: options.as_ref().and_then(|o| o.cursor.clone()),
910                limit: options
911                    .as_ref()
912                    .and_then(|o| o.limit)
913                    .map(|l| photon_api::types::Limit(l as u64)),
914            };
915            let request =
916                photon_api::apis::default_api::make_get_compression_signatures_for_address_body(
917                    params,
918                );
919
920            let result =
921                photon_api::apis::default_api::get_compression_signatures_for_address_post(
922                    &self.configuration,
923                    request,
924                )
925                .await?;
926
927            Self::check_api_error("get_compression_signatures_for_address", result.error)?;
928            let api_response =
929                Self::extract_result("get_compression_signatures_for_address", result.result)?;
930
931            if api_response.context.slot < config.slot {
932                return Err(IndexerError::IndexerNotSyncedToSlot);
933            }
934
935            let signatures: Vec<SignatureWithMetadata> = api_response
936                .value
937                .items
938                .iter()
939                .map(SignatureWithMetadata::from)
940                .collect();
941
942            let cursor = api_response.value.cursor;
943
944            Ok(Response {
945                context: Context {
946                    slot: api_response.context.slot,
947                },
948                value: ItemsWithCursor {
949                    items: signatures,
950                    cursor,
951                },
952            })
953        })
954        .await
955    }
956
957    async fn get_compression_signatures_for_owner(
958        &self,
959        owner: &Pubkey,
960        options: Option<PaginatedOptions>,
961        config: Option<IndexerRpcConfig>,
962    ) -> Result<Response<ItemsWithCursor<SignatureWithMetadata>>, IndexerError> {
963        let config = config.unwrap_or_default();
964        self.retry(config.retry_config, || async {
965            let params = photon_api::types::PostGetCompressionSignaturesForOwnerBodyParams {
966                owner: photon_api::types::SerializablePubkey(owner.to_string()),
967                cursor: options.as_ref().and_then(|o| o.cursor.clone()),
968                limit: options
969                    .as_ref()
970                    .and_then(|o| o.limit)
971                    .map(|l| photon_api::types::Limit(l as u64)),
972            };
973            let request =
974                photon_api::apis::default_api::make_get_compression_signatures_for_owner_body(
975                    params,
976                );
977
978            let result = photon_api::apis::default_api::get_compression_signatures_for_owner_post(
979                &self.configuration,
980                request,
981            )
982            .await?;
983
984            Self::check_api_error("get_compression_signatures_for_owner", result.error)?;
985            let api_response =
986                Self::extract_result("get_compression_signatures_for_owner", result.result)?;
987
988            if api_response.context.slot < config.slot {
989                return Err(IndexerError::IndexerNotSyncedToSlot);
990            }
991
992            let signatures: Vec<SignatureWithMetadata> = api_response
993                .value
994                .items
995                .iter()
996                .map(SignatureWithMetadata::from)
997                .collect();
998
999            let cursor = api_response.value.cursor;
1000
1001            Ok(Response {
1002                context: Context {
1003                    slot: api_response.context.slot,
1004                },
1005                value: ItemsWithCursor {
1006                    items: signatures,
1007                    cursor,
1008                },
1009            })
1010        })
1011        .await
1012    }
1013
1014    async fn get_compression_signatures_for_token_owner(
1015        &self,
1016        owner: &Pubkey,
1017        options: Option<PaginatedOptions>,
1018        config: Option<IndexerRpcConfig>,
1019    ) -> Result<Response<ItemsWithCursor<SignatureWithMetadata>>, IndexerError> {
1020        let config = config.unwrap_or_default();
1021        self.retry(config.retry_config, || async {
1022            let params = photon_api::types::PostGetCompressionSignaturesForTokenOwnerBodyParams {
1023                owner: photon_api::types::SerializablePubkey(owner.to_string()),
1024                cursor: options.as_ref().and_then(|o| o.cursor.clone()),
1025                limit: options
1026                    .as_ref()
1027                    .and_then(|o| o.limit)
1028                    .map(|l| photon_api::types::Limit(l as u64)),
1029            };
1030            let request =
1031                photon_api::apis::default_api::make_get_compression_signatures_for_token_owner_body(
1032                    params,
1033                );
1034
1035            let result =
1036                photon_api::apis::default_api::get_compression_signatures_for_token_owner_post(
1037                    &self.configuration,
1038                    request,
1039                )
1040                .await?;
1041
1042            Self::check_api_error("get_compression_signatures_for_token_owner", result.error)?;
1043            let api_response =
1044                Self::extract_result("get_compression_signatures_for_token_owner", result.result)?;
1045
1046            if api_response.context.slot < config.slot {
1047                return Err(IndexerError::IndexerNotSyncedToSlot);
1048            }
1049
1050            let signatures: Vec<SignatureWithMetadata> = api_response
1051                .value
1052                .items
1053                .iter()
1054                .map(SignatureWithMetadata::from)
1055                .collect();
1056
1057            let cursor = api_response.value.cursor;
1058
1059            Ok(Response {
1060                context: Context {
1061                    slot: api_response.context.slot,
1062                },
1063                value: ItemsWithCursor {
1064                    items: signatures,
1065                    cursor,
1066                },
1067            })
1068        })
1069        .await
1070    }
1071
1072    async fn get_indexer_health(&self, config: Option<RetryConfig>) -> Result<bool, IndexerError> {
1073        let config = config.unwrap_or_default();
1074        self.retry(config, || async {
1075            let request = photon_api::apis::default_api::make_get_indexer_health_body();
1076
1077            let result = photon_api::apis::default_api::get_indexer_health_post(
1078                &self.configuration,
1079                request,
1080            )
1081            .await?;
1082
1083            Self::check_api_error("get_indexer_health", result.error)?;
1084            // result.result is not Optional for this endpoint
1085            let _health = result.result;
1086
1087            Ok(true)
1088        })
1089        .await
1090    }
1091
1092    async fn get_indexer_slot(&self, config: Option<RetryConfig>) -> Result<u64, IndexerError> {
1093        let config = config.unwrap_or_default();
1094        self.retry(config, || async {
1095            let request = photon_api::apis::default_api::make_get_indexer_slot_body();
1096
1097            let result =
1098                photon_api::apis::default_api::get_indexer_slot_post(&self.configuration, request)
1099                    .await?;
1100
1101            Self::check_api_error("get_indexer_slot", result.error)?;
1102            // result.result is u64 directly for this endpoint
1103            Ok(result.result)
1104        })
1105        .await
1106    }
1107
1108    async fn get_multiple_compressed_account_proofs(
1109        &self,
1110        hashes: Vec<[u8; 32]>,
1111        config: Option<IndexerRpcConfig>,
1112    ) -> Result<Response<Items<MerkleProof>>, IndexerError> {
1113        let config = config.unwrap_or_default();
1114        self.retry(config.retry_config, || async {
1115            let hashes_for_async = hashes.clone();
1116
1117            let params: Vec<photon_api::types::Hash> = hashes_for_async
1118                .into_iter()
1119                .map(|hash| photon_api::types::Hash(bs58::encode(hash).into_string()))
1120                .collect();
1121            let request =
1122                photon_api::apis::default_api::make_get_multiple_compressed_account_proofs_body(
1123                    params,
1124                );
1125
1126            let result =
1127                photon_api::apis::default_api::get_multiple_compressed_account_proofs_post(
1128                    &self.configuration,
1129                    request,
1130                )
1131                .await?;
1132
1133            Self::check_api_error("get_multiple_compressed_account_proofs", result.error)?;
1134            let photon_proofs =
1135                Self::extract_result("get_multiple_compressed_account_proofs", result.result)?;
1136
1137            if photon_proofs.context.slot < config.slot {
1138                return Err(IndexerError::IndexerNotSyncedToSlot);
1139            }
1140
1141            let proofs = photon_proofs
1142                .value
1143                .iter()
1144                .map(|x| {
1145                    let mut proof_vec = x.proof.clone();
1146                    if proof_vec.len() < STATE_MERKLE_TREE_CANOPY_DEPTH {
1147                        return Err(IndexerError::InvalidParameters(format!(
1148                            "Merkle proof length ({}) is less than canopy depth ({})",
1149                            proof_vec.len(),
1150                            STATE_MERKLE_TREE_CANOPY_DEPTH,
1151                        )));
1152                    }
1153                    proof_vec.truncate(proof_vec.len() - STATE_MERKLE_TREE_CANOPY_DEPTH);
1154
1155                    let proof = proof_vec
1156                        .iter()
1157                        .map(|s| Hash::from_base58(s))
1158                        .collect::<Result<Vec<[u8; 32]>, IndexerError>>()
1159                        .map_err(|e| IndexerError::Base58DecodeError {
1160                            field: "proof".to_string(),
1161                            message: e.to_string(),
1162                        })?;
1163
1164                    Ok(MerkleProof {
1165                        hash: <[u8; 32] as Base58Conversions>::from_base58(&x.hash)?,
1166                        leaf_index: x.leaf_index as u64,
1167                        merkle_tree: Pubkey::from_str_const(x.merkle_tree.0.as_str()),
1168                        proof,
1169                        root_seq: x.root_seq,
1170                        root: <[u8; 32] as Base58Conversions>::from_base58(&x.root)?,
1171                    })
1172                })
1173                .collect::<Result<Vec<MerkleProof>, IndexerError>>()?;
1174
1175            Ok(Response {
1176                context: Context {
1177                    slot: photon_proofs.context.slot,
1178                },
1179                value: Items { items: proofs },
1180            })
1181        })
1182        .await
1183    }
1184
1185    async fn get_multiple_compressed_accounts(
1186        &self,
1187        addresses: Option<Vec<Address>>,
1188        hashes: Option<Vec<Hash>>,
1189        config: Option<IndexerRpcConfig>,
1190    ) -> Result<Response<Items<Option<CompressedAccount>>>, IndexerError> {
1191        let config = config.unwrap_or_default();
1192        self.retry(config.retry_config, || async {
1193            let hashes = hashes.clone();
1194            let addresses = addresses.clone();
1195            let params = photon_api::types::PostGetMultipleCompressedAccountsBodyParams {
1196                addresses: addresses.map(|x| {
1197                    x.iter()
1198                        .map(|a| photon_api::types::SerializablePubkey(a.to_base58()))
1199                        .collect()
1200                }),
1201                hashes: hashes.map(|x| {
1202                    x.iter()
1203                        .map(|h| photon_api::types::Hash(h.to_base58()))
1204                        .collect()
1205                }),
1206            };
1207            let request =
1208                photon_api::apis::default_api::make_get_multiple_compressed_accounts_body(params);
1209
1210            let result = photon_api::apis::default_api::get_multiple_compressed_accounts_post(
1211                &self.configuration,
1212                request,
1213            )
1214            .await?;
1215
1216            Self::check_api_error("get_multiple_compressed_accounts", result.error)?;
1217            let api_response =
1218                Self::extract_result("get_multiple_compressed_accounts", result.result)?;
1219
1220            if api_response.context.slot < config.slot {
1221                return Err(IndexerError::IndexerNotSyncedToSlot);
1222            }
1223            let accounts = api_response
1224                .value
1225                .items
1226                .iter()
1227                .map(|account_opt| match account_opt {
1228                    Some(account) => CompressedAccount::try_from(account).map(Some),
1229                    None => Ok(None),
1230                })
1231                .collect::<Result<Vec<Option<CompressedAccount>>, IndexerError>>()?;
1232
1233            Ok(Response {
1234                context: Context {
1235                    slot: api_response.context.slot,
1236                },
1237                value: Items { items: accounts },
1238            })
1239        })
1240        .await
1241    }
1242
1243    async fn get_multiple_new_address_proofs(
1244        &self,
1245        merkle_tree_pubkey: [u8; 32],
1246        addresses: Vec<[u8; 32]>,
1247        config: Option<IndexerRpcConfig>,
1248    ) -> Result<Response<Items<NewAddressProofWithContext>>, IndexerError> {
1249        let config = config.unwrap_or_default();
1250        self.retry(config.retry_config, || async {
1251            let params: Vec<photon_api::types::AddressWithTree> = addresses
1252                .iter()
1253                .map(|x| photon_api::types::AddressWithTree {
1254                    address: photon_api::types::SerializablePubkey(bs58::encode(x).into_string()),
1255                    tree: photon_api::types::SerializablePubkey(
1256                        bs58::encode(&merkle_tree_pubkey).into_string(),
1257                    ),
1258                })
1259                .collect();
1260
1261            let request =
1262                photon_api::apis::default_api::make_get_multiple_new_address_proofs_v2_body(params);
1263
1264            let result = photon_api::apis::default_api::get_multiple_new_address_proofs_v2_post(
1265                &self.configuration,
1266                request,
1267            )
1268            .await?;
1269
1270            Self::check_api_error("get_multiple_new_address_proofs", result.error)?;
1271            let api_response =
1272                match Self::extract_result("get_multiple_new_address_proofs", result.result) {
1273                    Ok(proofs) => proofs,
1274                    Err(e) => {
1275                        error!("Failed to extract proofs: {:?}", e);
1276                        return Err(e);
1277                    }
1278                };
1279
1280            if api_response.context.slot < config.slot {
1281                return Err(IndexerError::IndexerNotSyncedToSlot);
1282            }
1283            let photon_proofs = api_response.value;
1284            let mut proofs = Vec::new();
1285            for photon_proof in photon_proofs {
1286                let tree_pubkey = Hash::from_base58(&photon_proof.merkle_tree.0).map_err(|e| {
1287                    IndexerError::Base58DecodeError {
1288                        field: "merkle_tree".to_string(),
1289                        message: e.to_string(),
1290                    }
1291                })?;
1292
1293                let low_address_value = Hash::from_base58(&photon_proof.lower_range_address.0)
1294                    .map_err(|e| IndexerError::Base58DecodeError {
1295                        field: "lower_range_address".to_string(),
1296                        message: e.to_string(),
1297                    })?;
1298
1299                let next_address_value = Hash::from_base58(&photon_proof.higher_range_address.0)
1300                    .map_err(|e| IndexerError::Base58DecodeError {
1301                        field: "higher_range_address".to_string(),
1302                        message: e.to_string(),
1303                    })?;
1304
1305                let mut proof_vec: Vec<[u8; 32]> = photon_proof
1306                    .proof
1307                    .iter()
1308                    .map(|x| Hash::from_base58(x))
1309                    .collect::<Result<Vec<[u8; 32]>, IndexerError>>()?;
1310
1311                const ADDRESS_TREE_CANOPY_DEPTH: usize = 10;
1312                if proof_vec.len() < ADDRESS_TREE_CANOPY_DEPTH {
1313                    return Err(IndexerError::InvalidParameters(format!(
1314                        "Address proof length ({}) is less than canopy depth ({})",
1315                        proof_vec.len(),
1316                        ADDRESS_TREE_CANOPY_DEPTH,
1317                    )));
1318                }
1319                proof_vec.truncate(proof_vec.len() - ADDRESS_TREE_CANOPY_DEPTH);
1320                let mut proof_arr = [[0u8; 32]; 16];
1321                proof_arr.copy_from_slice(&proof_vec);
1322
1323                let root = Hash::from_base58(&photon_proof.root).map_err(|e| {
1324                    IndexerError::Base58DecodeError {
1325                        field: "root".to_string(),
1326                        message: e.to_string(),
1327                    }
1328                })?;
1329
1330                let proof = NewAddressProofWithContext {
1331                    merkle_tree: tree_pubkey.into(),
1332                    low_address_index: photon_proof.low_element_leaf_index as u64,
1333                    low_address_value,
1334                    low_address_next_index: photon_proof.next_index as u64,
1335                    low_address_next_value: next_address_value,
1336                    low_address_proof: proof_arr.to_vec(),
1337                    root,
1338                    root_seq: photon_proof.root_seq,
1339                    new_low_element: None,
1340                    new_element: None,
1341                    new_element_next_value: None,
1342                };
1343                proofs.push(proof);
1344            }
1345
1346            Ok(Response {
1347                context: Context {
1348                    slot: api_response.context.slot,
1349                },
1350                value: Items { items: proofs },
1351            })
1352        })
1353        .await
1354    }
1355
1356    async fn get_validity_proof(
1357        &self,
1358        hashes: Vec<Hash>,
1359        new_addresses_with_trees: Vec<AddressWithTree>,
1360        config: Option<IndexerRpcConfig>,
1361    ) -> Result<Response<super::types::ValidityProofWithContext>, IndexerError> {
1362        let config = config.unwrap_or_default();
1363        self.retry(config.retry_config, || async {
1364            #[cfg(feature = "v2")]
1365            {
1366                let params = photon_api::types::PostGetValidityProofV2BodyParams {
1367                    hashes: hashes
1368                        .iter()
1369                        .map(|x| photon_api::types::Hash(x.to_base58()))
1370                        .collect(),
1371                    new_addresses_with_trees: new_addresses_with_trees
1372                        .iter()
1373                        .map(|x| photon_api::types::AddressWithTree {
1374                            address: photon_api::types::SerializablePubkey(x.address.to_base58()),
1375                            tree: photon_api::types::SerializablePubkey(x.tree.to_string()),
1376                        })
1377                        .collect(),
1378                };
1379                let request =
1380                    photon_api::apis::default_api::make_get_validity_proof_v2_body(params);
1381
1382                let result = photon_api::apis::default_api::get_validity_proof_v2_post(
1383                    &self.configuration,
1384                    request,
1385                )
1386                .await?;
1387
1388                Self::check_api_error("get_validity_proof_v2", result.error)?;
1389                let api_response = Self::extract_result("get_validity_proof_v2", result.result)?;
1390
1391                if api_response.context.slot < config.slot {
1392                    return Err(IndexerError::IndexerNotSyncedToSlot);
1393                }
1394                let validity_proof =
1395                    super::types::ValidityProofWithContext::from_api_model_v2(api_response.value)?;
1396
1397                Ok(Response {
1398                    context: Context {
1399                        slot: api_response.context.slot,
1400                    },
1401                    value: validity_proof,
1402                })
1403            }
1404            #[cfg(not(feature = "v2"))]
1405            {
1406                let params = photon_api::types::PostGetValidityProofBodyParams {
1407                    hashes: hashes
1408                        .iter()
1409                        .map(|x| photon_api::types::Hash(x.to_base58()))
1410                        .collect(),
1411                    new_addresses_with_trees: new_addresses_with_trees
1412                        .iter()
1413                        .map(|x| photon_api::types::AddressWithTree {
1414                            address: photon_api::types::SerializablePubkey(x.address.to_base58()),
1415                            tree: photon_api::types::SerializablePubkey(x.tree.to_string()),
1416                        })
1417                        .collect(),
1418                };
1419                let request = photon_api::apis::default_api::make_get_validity_proof_body(params);
1420
1421                let result = photon_api::apis::default_api::get_validity_proof_post(
1422                    &self.configuration,
1423                    request,
1424                )
1425                .await?;
1426
1427                Self::check_api_error("get_validity_proof", result.error)?;
1428                let api_response = Self::extract_result("get_validity_proof", result.result)?;
1429
1430                if api_response.context.slot < config.slot {
1431                    return Err(IndexerError::IndexerNotSyncedToSlot);
1432                }
1433                let validity_proof = super::types::ValidityProofWithContext::from_api_model(
1434                    api_response.value,
1435                    hashes.len(),
1436                )?;
1437
1438                Ok(Response {
1439                    context: Context {
1440                        slot: api_response.context.slot,
1441                    },
1442                    value: validity_proof,
1443                })
1444            }
1445        })
1446        .await
1447    }
1448
1449    async fn get_queue_info(
1450        &self,
1451        config: Option<IndexerRpcConfig>,
1452    ) -> Result<Response<super::QueueInfoResult>, IndexerError> {
1453        let config = config.unwrap_or_default();
1454        self.retry(config.retry_config, || async {
1455            let params = photon_api::types::PostGetQueueInfoBodyParams { trees: None };
1456            let request = photon_api::apis::default_api::make_get_queue_info_body(params);
1457
1458            let result =
1459                photon_api::apis::default_api::get_queue_info_post(&self.configuration, request)
1460                    .await?;
1461
1462            let api_response = Self::extract_result_with_error_check(
1463                "get_queue_info",
1464                result.error,
1465                result.result,
1466            )?;
1467
1468            if api_response.slot < config.slot {
1469                return Err(IndexerError::IndexerNotSyncedToSlot);
1470            }
1471
1472            let queues = api_response
1473                .queues
1474                .iter()
1475                .map(|q| -> Result<_, IndexerError> {
1476                    let tree_bytes = super::base58::decode_base58_to_fixed_array(&q.tree)?;
1477                    let queue_bytes = super::base58::decode_base58_to_fixed_array(&q.queue)?;
1478
1479                    Ok(super::QueueInfo {
1480                        tree: Pubkey::new_from_array(tree_bytes),
1481                        queue: Pubkey::new_from_array(queue_bytes),
1482                        queue_type: q.queue_type,
1483                        queue_size: q.queue_size,
1484                    })
1485                })
1486                .collect::<Result<Vec<_>, _>>()?;
1487
1488            Ok(Response {
1489                context: Context {
1490                    slot: api_response.slot,
1491                },
1492                value: super::QueueInfoResult {
1493                    queues,
1494                    slot: api_response.slot,
1495                },
1496            })
1497        })
1498        .await
1499    }
1500
1501    async fn get_queue_elements(
1502        &mut self,
1503        merkle_tree_pubkey: [u8; 32],
1504        options: super::QueueElementsV2Options,
1505        config: Option<IndexerRpcConfig>,
1506    ) -> Result<Response<super::QueueElementsResult>, IndexerError> {
1507        let config = config.unwrap_or_default();
1508        self.retry(config.retry_config, || async {
1509            let tree_hash =
1510                photon_api::types::Hash(bs58::encode(&merkle_tree_pubkey).into_string());
1511
1512            // Build queue request objects
1513            let output_queue = if options.output_queue_limit.is_some()
1514                || options.output_queue_start_index.is_some()
1515            {
1516                Some(photon_api::types::QueueRequest {
1517                    limit: options.output_queue_limit.unwrap_or(100),
1518                    start_index: options.output_queue_start_index,
1519                    zkp_batch_size: options.output_queue_zkp_batch_size,
1520                })
1521            } else {
1522                None
1523            };
1524
1525            let input_queue = if options.input_queue_limit.is_some()
1526                || options.input_queue_start_index.is_some()
1527            {
1528                Some(photon_api::types::QueueRequest {
1529                    limit: options.input_queue_limit.unwrap_or(100),
1530                    start_index: options.input_queue_start_index,
1531                    zkp_batch_size: options.input_queue_zkp_batch_size,
1532                })
1533            } else {
1534                None
1535            };
1536
1537            let address_queue = if options.address_queue_limit.is_some()
1538                || options.address_queue_start_index.is_some()
1539            {
1540                Some(photon_api::types::QueueRequest {
1541                    limit: options.address_queue_limit.unwrap_or(100),
1542                    start_index: options.address_queue_start_index,
1543                    zkp_batch_size: options.address_queue_zkp_batch_size,
1544                })
1545            } else {
1546                None
1547            };
1548
1549            let params = photon_api::types::PostGetQueueElementsBodyParams {
1550                tree: tree_hash,
1551                output_queue,
1552                input_queue,
1553                address_queue,
1554            };
1555            let request = photon_api::apis::default_api::make_get_queue_elements_body(params);
1556
1557            let result = photon_api::apis::default_api::get_queue_elements_post(
1558                &self.configuration,
1559                request,
1560            )
1561            .await?;
1562
1563            Self::check_api_error("get_queue_elements", result.error)?;
1564            let api_response = Self::extract_result("get_queue_elements", result.result)?;
1565
1566            if api_response.context.slot < config.slot {
1567                return Err(IndexerError::IndexerNotSyncedToSlot);
1568            }
1569
1570            // Convert API StateQueueData to local StateQueueData
1571            let state_queue = if let Some(sq) = api_response.state_queue {
1572                let output_queue = if let Some(oq) = sq.output_queue {
1573                    Some(super::OutputQueueData {
1574                        leaf_indices: oq.leaf_indices.clone(),
1575                        account_hashes: oq
1576                            .account_hashes
1577                            .iter()
1578                            .map(|h| super::base58::decode_base58_to_fixed_array(&h.0))
1579                            .collect::<Result<Vec<_>, _>>()?,
1580                        old_leaves: oq
1581                            .leaves
1582                            .iter()
1583                            .map(|h| super::base58::decode_base58_to_fixed_array(&h.0))
1584                            .collect::<Result<Vec<_>, _>>()?,
1585                        first_queue_index: oq.first_queue_index,
1586                        next_index: oq.next_index,
1587                        leaves_hash_chains: oq
1588                            .leaves_hash_chains
1589                            .iter()
1590                            .map(|h| super::base58::decode_base58_to_fixed_array(&h.0))
1591                            .collect::<Result<Vec<_>, _>>()?,
1592                    })
1593                } else {
1594                    None
1595                };
1596
1597                let input_queue = if let Some(iq) = sq.input_queue {
1598                    Some(super::InputQueueData {
1599                        leaf_indices: iq.leaf_indices.clone(),
1600                        account_hashes: iq
1601                            .account_hashes
1602                            .iter()
1603                            .map(|h| super::base58::decode_base58_to_fixed_array(&h.0))
1604                            .collect::<Result<Vec<_>, _>>()?,
1605                        current_leaves: iq
1606                            .leaves
1607                            .iter()
1608                            .map(|h| super::base58::decode_base58_to_fixed_array(&h.0))
1609                            .collect::<Result<Vec<_>, _>>()?,
1610                        tx_hashes: iq
1611                            .tx_hashes
1612                            .iter()
1613                            .map(|h| super::base58::decode_base58_to_fixed_array(&h.0))
1614                            .collect::<Result<Vec<_>, _>>()?,
1615                        nullifiers: iq
1616                            .nullifiers
1617                            .iter()
1618                            .map(|h| super::base58::decode_base58_to_fixed_array(&h.0))
1619                            .collect::<Result<Vec<_>, _>>()?,
1620                        first_queue_index: iq.first_queue_index,
1621                        leaves_hash_chains: iq
1622                            .leaves_hash_chains
1623                            .iter()
1624                            .map(|h| super::base58::decode_base58_to_fixed_array(&h.0))
1625                            .collect::<Result<Vec<_>, _>>()?,
1626                    })
1627                } else {
1628                    None
1629                };
1630
1631                Some(super::StateQueueData {
1632                    nodes: sq.nodes.iter().map(|n| n.index).collect(),
1633                    node_hashes: sq
1634                        .nodes
1635                        .iter()
1636                        .map(|n| super::base58::decode_base58_to_fixed_array(&n.hash.0))
1637                        .collect::<Result<Vec<_>, _>>()?,
1638                    initial_root: super::base58::decode_base58_to_fixed_array(&sq.initial_root.0)?,
1639                    root_seq: sq.root_seq,
1640                    output_queue,
1641                    input_queue,
1642                })
1643            } else {
1644                None
1645            };
1646
1647            // Convert API AddressQueueData to local AddressQueueData
1648            let address_queue = if let Some(aq) = api_response.address_queue {
1649                Some(super::AddressQueueData {
1650                    addresses: aq
1651                        .addresses
1652                        .iter()
1653                        .map(|h| super::base58::decode_base58_to_fixed_array(&h.0))
1654                        .collect::<Result<Vec<_>, _>>()?,
1655                    low_element_values: aq
1656                        .low_element_values
1657                        .iter()
1658                        .map(|h| super::base58::decode_base58_to_fixed_array(&h.0))
1659                        .collect::<Result<Vec<_>, _>>()?,
1660                    low_element_next_values: aq
1661                        .low_element_next_values
1662                        .iter()
1663                        .map(|h| super::base58::decode_base58_to_fixed_array(&h.0))
1664                        .collect::<Result<Vec<_>, _>>()?,
1665                    low_element_indices: aq.low_element_indices.clone(),
1666                    low_element_next_indices: aq.low_element_next_indices.clone(),
1667                    nodes: aq.nodes.iter().map(|n| n.index).collect(),
1668                    node_hashes: aq
1669                        .nodes
1670                        .iter()
1671                        .map(|n| super::base58::decode_base58_to_fixed_array(&n.hash.0))
1672                        .collect::<Result<Vec<_>, _>>()?,
1673                    initial_root: super::base58::decode_base58_to_fixed_array(&aq.initial_root.0)?,
1674                    leaves_hash_chains: aq
1675                        .leaves_hash_chains
1676                        .iter()
1677                        .map(|h| super::base58::decode_base58_to_fixed_array(&h.0))
1678                        .collect::<Result<Vec<_>, _>>()?,
1679                    subtrees: aq
1680                        .subtrees
1681                        .iter()
1682                        .map(|h| super::base58::decode_base58_to_fixed_array(&h.0))
1683                        .collect::<Result<Vec<_>, _>>()?,
1684                    start_index: aq.start_index,
1685                    root_seq: aq.root_seq,
1686                })
1687            } else {
1688                None
1689            };
1690
1691            Ok(Response {
1692                context: Context {
1693                    slot: api_response.context.slot,
1694                },
1695                value: super::QueueElementsResult {
1696                    state_queue,
1697                    address_queue,
1698                },
1699            })
1700        })
1701        .await
1702    }
1703
1704    async fn get_subtrees(
1705        &self,
1706        _merkle_tree_pubkey: [u8; 32],
1707        _config: Option<IndexerRpcConfig>,
1708    ) -> Result<Response<Items<[u8; 32]>>, IndexerError> {
1709        #[cfg(not(feature = "v2"))]
1710        unimplemented!();
1711        #[cfg(feature = "v2")]
1712        {
1713            todo!();
1714        }
1715    }
1716}
1717
1718// ============ Interface Methods ============
1719// These methods use the Interface endpoints that race hot (on-chain) and cold (compressed) lookups.
1720impl PhotonIndexer {
1721    pub async fn get_account_interface(
1722        &self,
1723        address: &Pubkey,
1724        config: Option<IndexerRpcConfig>,
1725    ) -> Result<Response<Option<AccountInterface>>, IndexerError> {
1726        let config = config.unwrap_or_default();
1727        self.retry(config.retry_config, || async {
1728            let params = photon_api::types::PostGetAccountInterfaceBodyParams {
1729                address: photon_api::types::SerializablePubkey(address.to_string()),
1730            };
1731            let request = photon_api::apis::default_api::make_get_account_interface_body(params);
1732
1733            let result = photon_api::apis::default_api::get_account_interface_post(
1734                &self.configuration,
1735                request,
1736            )
1737            .await?;
1738
1739            let api_response = Self::extract_result_with_error_check(
1740                "get_account_interface",
1741                result.error,
1742                result.result,
1743            )?;
1744
1745            if api_response.context.slot < config.slot {
1746                return Err(IndexerError::IndexerNotSyncedToSlot);
1747            }
1748
1749            let account = match api_response.value {
1750                Some(ref ai) => Some(AccountInterface::try_from(ai)?),
1751                None => None,
1752            };
1753
1754            Ok(Response {
1755                context: Context {
1756                    slot: api_response.context.slot,
1757                },
1758                value: account,
1759            })
1760        })
1761        .await
1762    }
1763
1764    pub async fn get_token_account_interface(
1765        &self,
1766        address: &Pubkey,
1767        config: Option<IndexerRpcConfig>,
1768    ) -> Result<Response<Option<TokenAccountInterface>>, IndexerError> {
1769        let response = self.get_account_interface(address, config).await?;
1770        let value = match response.value {
1771            Some(ai) => {
1772                let token = parse_token_data_from_indexer_account(&ai)?;
1773                Some(TokenAccountInterface { account: ai, token })
1774            }
1775            None => None,
1776        };
1777        Ok(Response {
1778            context: response.context,
1779            value,
1780        })
1781    }
1782
1783    pub async fn get_associated_token_account_interface(
1784        &self,
1785        owner: &Pubkey,
1786        mint: &Pubkey,
1787        config: Option<IndexerRpcConfig>,
1788    ) -> Result<Response<Option<TokenAccountInterface>>, IndexerError> {
1789        let ata_address = light_token::instruction::get_associated_token_address(owner, mint);
1790        let response = self.get_account_interface(&ata_address, config).await?;
1791        let value = match response.value {
1792            Some(ai) => {
1793                let token = parse_token_data_from_indexer_account(&ai)?;
1794                Some(TokenAccountInterface { account: ai, token })
1795            }
1796            None => None,
1797        };
1798        Ok(Response {
1799            context: response.context,
1800            value,
1801        })
1802    }
1803
1804    pub async fn get_multiple_account_interfaces(
1805        &self,
1806        addresses: Vec<&Pubkey>,
1807        config: Option<IndexerRpcConfig>,
1808    ) -> Result<Response<Vec<Option<AccountInterface>>>, IndexerError> {
1809        let config = config.unwrap_or_default();
1810        self.retry(config.retry_config, || async {
1811            let params = photon_api::types::PostGetMultipleAccountInterfacesBodyParams {
1812                addresses: addresses
1813                    .iter()
1814                    .map(|addr| photon_api::types::SerializablePubkey(addr.to_string()))
1815                    .collect(),
1816            };
1817            let request =
1818                photon_api::apis::default_api::make_get_multiple_account_interfaces_body(params);
1819
1820            let result = photon_api::apis::default_api::get_multiple_account_interfaces_post(
1821                &self.configuration,
1822                request,
1823            )
1824            .await?;
1825
1826            let api_response = Self::extract_result_with_error_check(
1827                "get_multiple_account_interfaces",
1828                result.error,
1829                result.result,
1830            )?;
1831
1832            if api_response.context.slot < config.slot {
1833                return Err(IndexerError::IndexerNotSyncedToSlot);
1834            }
1835
1836            let accounts: Result<Vec<Option<AccountInterface>>, IndexerError> = api_response
1837                .value
1838                .into_iter()
1839                .map(|maybe_acc| {
1840                    maybe_acc
1841                        .map(|ai| AccountInterface::try_from(&ai))
1842                        .transpose()
1843                })
1844                .collect();
1845
1846            Ok(Response {
1847                context: Context {
1848                    slot: api_response.context.slot,
1849                },
1850                value: accounts?,
1851            })
1852        })
1853        .await
1854    }
1855}
1856
1857/// Parse token data from an indexer AccountInterface.
1858/// For compressed (cold) accounts: borsh-deserializes TokenData from the cold data bytes.
1859/// For on-chain (hot) accounts: returns default TokenData (downstream conversion re-parses from SPL layout).
1860fn parse_token_data_from_indexer_account(
1861    ai: &AccountInterface,
1862) -> Result<light_token::compat::TokenData, IndexerError> {
1863    match &ai.cold {
1864        Some(cold) => borsh::BorshDeserialize::deserialize(&mut cold.data.data.as_slice())
1865            .map_err(|e| IndexerError::decode_error("token_data", e)),
1866        None => {
1867            // Hot account — downstream will re-parse from SPL account data directly
1868            Ok(light_token::compat::TokenData::default())
1869        }
1870    }
1871}