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
23pub 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 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 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 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 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 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
1718impl 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
1857fn 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 Ok(light_token::compat::TokenData::default())
1869 }
1870 }
1871}