photon_indexer/api/method/
utils.rs

1use crate::common::typedefs::account::{Account, AccountData};
2use crate::common::typedefs::bs58_string::Base58String;
3use crate::common::typedefs::bs64_string::Base64String;
4use crate::common::typedefs::serializable_signature::SerializableSignature;
5use crate::common::typedefs::token_data::{AccountState, TokenData};
6use crate::common::typedefs::unix_timestamp::UnixTimestamp;
7use crate::common::typedefs::unsigned_integer::UnsignedInteger;
8use crate::dao::generated::{accounts, blocks, token_accounts};
9
10use byteorder::{ByteOrder, LittleEndian};
11use sea_orm::sea_query::SimpleExpr;
12use sea_orm::{
13    ColumnTrait, ConnectionTrait, DatabaseConnection, EntityTrait, FromQueryResult, QueryFilter,
14    QueryOrder, QuerySelect, Statement, Value,
15};
16use serde::{de, Deserialize, Deserializer, Serialize};
17use solana_sdk::signature::Signature;
18
19use sqlx::types::Decimal;
20use utoipa::openapi::{ObjectBuilder, RefOr, Schema, SchemaType};
21use utoipa::ToSchema;
22
23use crate::common::typedefs::hash::Hash;
24use crate::common::typedefs::serializable_pubkey::SerializablePubkey;
25
26use super::super::error::PhotonApiError;
27use sea_orm_migration::sea_query::Expr;
28
29pub const PAGE_LIMIT: u64 = 1000;
30
31pub fn parse_decimal(value: Decimal) -> Result<u64, PhotonApiError> {
32    value
33        .to_string()
34        .parse::<u64>()
35        .map_err(|_| PhotonApiError::UnexpectedError("Invalid decimal value".to_string()))
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, ToSchema)]
39pub struct Limit(u64);
40
41impl Limit {
42    pub fn new(value: u64) -> Result<Self, &'static str> {
43        if value > PAGE_LIMIT {
44            Err("Value must be less than or equal to 1000")
45        } else {
46            Ok(Limit(value))
47        }
48    }
49
50    pub fn value(&self) -> u64 {
51        self.0
52    }
53}
54
55impl Default for Limit {
56    fn default() -> Self {
57        Limit(PAGE_LIMIT)
58    }
59}
60
61impl<'de> Deserialize<'de> for Limit {
62    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63    where
64        D: Deserializer<'de>,
65    {
66        let value = u64::deserialize(deserializer)?;
67        if value > PAGE_LIMIT {
68            Err(de::Error::invalid_value(
69                de::Unexpected::Unsigned(value),
70                &"a value less than or equal to 1000",
71            ))
72        } else {
73            Ok(Limit(value))
74        }
75    }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, FromQueryResult)]
79#[serde(deny_unknown_fields, rename_all = "camelCase")]
80pub struct Context {
81    pub slot: u64,
82}
83
84impl<'__s> ToSchema<'__s> for Context {
85    fn schema() -> (&'__s str, RefOr<Schema>) {
86        let schema = Schema::Object(
87            ObjectBuilder::new()
88                .schema_type(SchemaType::Object)
89                .property("slot", UnsignedInteger::schema().1)
90                .required("slot")
91                .build(),
92        );
93        ("Context", RefOr::T(schema))
94    }
95
96    fn aliases() -> Vec<(&'static str, utoipa::openapi::schema::Schema)> {
97        Vec::new()
98    }
99}
100
101#[derive(FromQueryResult)]
102pub struct ContextModel {
103    // Postgres and SQLlite do not support u64 as return type. We need to use i64 and cast it to u64.
104    pub slot: i64,
105}
106
107impl Context {
108    pub async fn extract(db: &DatabaseConnection) -> Result<Self, PhotonApiError> {
109        let context = blocks::Entity::find()
110            .select_only()
111            .column_as(Expr::col(blocks::Column::Slot).max(), "slot")
112            .into_model::<ContextModel>()
113            .one(db)
114            .await?
115            .ok_or(PhotonApiError::RecordNotFound(
116                "No data has been indexed".to_string(),
117            ))?;
118        Ok(Context {
119            slot: context.slot as u64,
120        })
121    }
122}
123
124pub fn parse_discriminator(discriminator: Option<Vec<u8>>) -> Option<u64> {
125    discriminator.map(|discriminator| LittleEndian::read_u64(&discriminator))
126}
127
128fn parse_leaf_index(leaf_index: i64) -> Result<u32, PhotonApiError> {
129    leaf_index
130        .try_into()
131        .map_err(|_| PhotonApiError::UnexpectedError("Invalid leaf index".to_string()))
132}
133
134pub fn parse_account_model(account: accounts::Model) -> Result<Account, PhotonApiError> {
135    let data = match (account.data, account.data_hash, account.discriminator) {
136        (Some(data), Some(data_hash), Some(discriminator)) => Some(AccountData {
137            data: Base64String(data),
138            data_hash: data_hash.try_into()?,
139            discriminator: UnsignedInteger(parse_decimal(discriminator)?),
140        }),
141        (None, None, None) => None,
142        _ => {
143            return Err(PhotonApiError::UnexpectedError(
144                "Invalid account data".to_string(),
145            ))
146        }
147    };
148
149    Ok(Account {
150        hash: account.hash.try_into()?,
151        address: account
152            .address
153            .map(SerializablePubkey::try_from)
154            .transpose()?,
155        data,
156        owner: account.owner.try_into()?,
157        tree: account.tree.try_into()?,
158        leaf_index: UnsignedInteger(parse_leaf_index(account.leaf_index)? as u64),
159        lamports: UnsignedInteger(parse_decimal(account.lamports)?),
160        slot_created: UnsignedInteger(account.slot_created as u64),
161        seq: UnsignedInteger(account.seq as u64),
162    })
163}
164
165// We do not use generics to simplify documentation generation.
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, ToSchema)]
167#[serde(deny_unknown_fields, rename_all = "camelCase")]
168pub struct TokenAccountListResponse {
169    pub context: Context,
170    pub value: TokenAccountList,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, ToSchema, Default)]
174#[serde(deny_unknown_fields, rename_all = "camelCase")]
175pub struct TokenAcccount {
176    pub account: Account,
177    pub token_data: TokenData,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, ToSchema, Default)]
181#[serde(rename_all = "camelCase")]
182pub struct TokenAccountList {
183    pub items: Vec<TokenAcccount>,
184    pub cursor: Option<Base58String>,
185}
186
187pub enum Authority {
188    Owner(SerializablePubkey),
189    Delegate(SerializablePubkey),
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, Default)]
193#[serde(deny_unknown_fields, rename_all = "camelCase")]
194pub struct GetCompressedTokenAccountsByAuthorityOptions {
195    pub mint: Option<SerializablePubkey>,
196    pub cursor: Option<Base58String>,
197    pub limit: Option<Limit>,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, Default)]
201#[serde(deny_unknown_fields, rename_all = "camelCase")]
202pub struct GetCompressedTokenAccountsByOwner {
203    pub owner: SerializablePubkey,
204    #[serde(default)]
205    pub mint: Option<SerializablePubkey>,
206    #[serde(default)]
207    pub cursor: Option<Base58String>,
208    #[serde(default)]
209    pub limit: Option<Limit>,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, Default)]
213#[serde(deny_unknown_fields, rename_all = "camelCase")]
214pub struct GetCompressedTokenAccountsByDelegate {
215    pub delegate: SerializablePubkey,
216    #[serde(default)]
217    pub mint: Option<SerializablePubkey>,
218    #[serde(default)]
219    pub cursor: Option<Base58String>,
220    #[serde(default)]
221    pub limit: Option<Limit>,
222}
223
224#[derive(FromQueryResult)]
225pub struct EnrichedTokenAccountModel {
226    pub hash: Vec<u8>,
227    pub owner: Vec<u8>,
228    pub mint: Vec<u8>,
229    pub amount: Decimal,
230    pub delegate: Option<Vec<u8>>,
231    pub frozen: bool,
232    pub delegated_amount: Decimal,
233    pub spent: bool,
234    pub slot_updated: i64,
235    // Needed for generating proof
236    pub data: Vec<u8>,
237    pub data_hash: Option<Vec<u8>>,
238    pub discriminator: Vec<u8>,
239    pub lamports: Decimal,
240    pub tree: Option<Vec<u8>>,
241    pub leaf_index: Option<i64>,
242    pub seq: Option<i64>,
243}
244
245pub async fn fetch_token_accounts(
246    conn: &sea_orm::DatabaseConnection,
247    owner_or_delegate: Authority,
248    options: GetCompressedTokenAccountsByAuthorityOptions,
249) -> Result<TokenAccountListResponse, PhotonApiError> {
250    let context = Context::extract(conn).await?;
251    let mut filter = match owner_or_delegate {
252        Authority::Owner(owner) => token_accounts::Column::Owner.eq::<Vec<u8>>(owner.into()),
253        Authority::Delegate(delegate) => {
254            token_accounts::Column::Delegate.eq::<Vec<u8>>(delegate.into())
255        }
256    }
257    .and(token_accounts::Column::Spent.eq(false));
258
259    let mut limit = PAGE_LIMIT;
260    if let Some(mint) = options.mint {
261        filter = filter.and(token_accounts::Column::Mint.eq::<Vec<u8>>(mint.into()));
262    }
263    if let Some(cursor) = options.cursor {
264        let bytes = cursor.0;
265        let expected_cursor_length = 64;
266        if bytes.len() != expected_cursor_length {
267            return Err(PhotonApiError::ValidationError(format!(
268                "Invalid cursor length. Expected {}. Received {}.",
269                expected_cursor_length,
270                bytes.len()
271            )));
272        }
273        let (mint, hash) = bytes.split_at(32);
274
275        filter = filter.and(
276            token_accounts::Column::Mint.gt::<Vec<u8>>(mint.into()).or(
277                token_accounts::Column::Mint
278                    .eq::<Vec<u8>>(mint.into())
279                    .and(token_accounts::Column::Hash.gt::<Vec<u8>>(hash.into())),
280            ),
281        );
282    }
283    if let Some(l) = options.limit {
284        limit = l.value();
285    }
286
287    let items = token_accounts::Entity::find()
288        .find_also_related(accounts::Entity)
289        .filter(filter)
290        .order_by(token_accounts::Column::Mint, sea_orm::Order::Asc)
291        .order_by(token_accounts::Column::Hash, sea_orm::Order::Asc)
292        .limit(limit)
293        .order_by(token_accounts::Column::Mint, sea_orm::Order::Asc)
294        .order_by(token_accounts::Column::Hash, sea_orm::Order::Asc)
295        .all(conn)
296        .await?
297        .drain(..)
298        .map(|(token_account, account)| {
299            let account = account.ok_or(PhotonApiError::RecordNotFound(
300                "Base account not found for token account".to_string(),
301            ))?;
302            Ok(TokenAcccount {
303                account: parse_account_model(account)?,
304                token_data: TokenData {
305                    mint: token_account.mint.try_into()?,
306                    owner: token_account.owner.try_into()?,
307                    amount: UnsignedInteger(parse_decimal(token_account.amount)?),
308                    delegate: token_account
309                        .delegate
310                        .map(SerializablePubkey::try_from)
311                        .transpose()?,
312                    state: (AccountState::try_from(token_account.state as u8)).map_err(|e| {
313                        PhotonApiError::UnexpectedError(format!(
314                            "Unable to parse account state {}",
315                            e
316                        ))
317                    })?,
318                    tlv: token_account.tlv.map(Base64String),
319                },
320            })
321        })
322        .collect::<Result<Vec<TokenAcccount>, PhotonApiError>>()?;
323
324    let mut cursor = items.last().map(|item| {
325        Base58String({
326            let item = item.clone();
327            let mut bytes: Vec<u8> = item.token_data.mint.into();
328            let hash_bytes: Vec<u8> = item.account.hash.into();
329            bytes.extend_from_slice(hash_bytes.as_slice());
330            bytes
331        })
332    });
333    if items.len() < limit as usize {
334        cursor = None;
335    }
336
337    Ok(TokenAccountListResponse {
338        value: TokenAccountList { items, cursor },
339        context,
340    })
341}
342
343#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, Default)]
344#[serde(deny_unknown_fields, rename_all = "camelCase")]
345pub struct CompressedAccountRequest {
346    #[serde(default)]
347    pub address: Option<SerializablePubkey>,
348    #[serde(default)]
349    pub hash: Option<Hash>,
350}
351
352impl CompressedAccountRequest {
353    pub fn adjusted_schema() -> RefOr<Schema> {
354        let mut schema = CompressedAccountRequest::schema().1;
355        let object = match schema {
356            RefOr::T(Schema::Object(ref mut object)) => {
357                let example = serde_json::to_value(CompressedAccountRequest {
358                    hash: Some(Hash::default()),
359                    address: None,
360                })
361                .unwrap();
362                object.default = Some(example.clone());
363                object.example = Some(example);
364                object.description = Some("Request for compressed account data".to_string());
365                object.clone()
366            }
367            _ => unimplemented!(),
368        };
369        RefOr::T(Schema::Object(object))
370    }
371}
372
373pub enum AccountIdentifier {
374    Address(SerializablePubkey),
375    Hash(Hash),
376}
377
378pub enum AccountDataTable {
379    Accounts,
380    TokenAccounts,
381}
382
383impl AccountIdentifier {
384    pub fn filter(&self, table: AccountDataTable) -> SimpleExpr {
385        match table {
386            AccountDataTable::Accounts => match &self {
387                AccountIdentifier::Address(address) => {
388                    accounts::Column::Address.eq::<Vec<u8>>((*address).into())
389                }
390                AccountIdentifier::Hash(hash) => accounts::Column::Hash.eq(hash.to_vec()),
391            }
392            .and(accounts::Column::Spent.eq(false)),
393            AccountDataTable::TokenAccounts => match &self {
394                AccountIdentifier::Address(address) => {
395                    token_accounts::Column::Owner.eq::<Vec<u8>>((*address).into())
396                }
397                AccountIdentifier::Hash(hash) => token_accounts::Column::Hash.eq(hash.to_vec()),
398            }
399            .and(token_accounts::Column::Spent.eq(false)),
400        }
401    }
402
403    pub fn not_found_error(&self) -> PhotonApiError {
404        match &self {
405            AccountIdentifier::Address(address) => {
406                PhotonApiError::RecordNotFound(format!("Account {} not found", address))
407            }
408            AccountIdentifier::Hash(hash) => {
409                PhotonApiError::RecordNotFound(format!("Account with hash {} not found", hash))
410            }
411        }
412    }
413}
414
415impl CompressedAccountRequest {
416    pub fn parse_id(&self) -> Result<AccountIdentifier, PhotonApiError> {
417        if let Some(address) = &self.address {
418            Ok(AccountIdentifier::Address(*address))
419        } else if let Some(hash) = &self.hash {
420            Ok(AccountIdentifier::Hash(hash.clone()))
421        } else {
422            Err(PhotonApiError::ValidationError(
423                "Either address or hash must be provided".to_string(),
424            ))
425        }
426    }
427}
428
429#[derive(FromQueryResult)]
430pub struct BalanceModel {
431    pub amount: Decimal,
432}
433
434#[derive(FromQueryResult)]
435pub struct LamportModel {
436    pub lamports: Decimal,
437}
438
439#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, Default)]
440#[serde(rename_all = "camelCase")]
441pub struct HashRequest {
442    pub hash: Hash,
443}
444
445#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, Default)]
446#[serde(rename_all = "camelCase")]
447pub struct SignatureInfo {
448    pub signature: SerializableSignature,
449    pub slot: UnsignedInteger,
450    pub block_time: UnixTimestamp,
451}
452
453#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, Default)]
454#[serde(rename_all = "camelCase")]
455pub struct SignatureInfoWithError {
456    pub signature: SerializableSignature,
457    pub slot: UnsignedInteger,
458    pub block_time: UnixTimestamp,
459    pub error: Option<String>,
460}
461
462#[derive(FromQueryResult)]
463pub struct SignatureInfoModel {
464    pub signature: Vec<u8>,
465    pub slot: i64,
466    pub block_time: i64,
467    pub error: Option<String>,
468}
469
470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, Default)]
471#[serde(rename_all = "camelCase")]
472pub struct SignatureInfoList {
473    pub items: Vec<SignatureInfo>,
474}
475
476#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, Default)]
477#[serde(rename_all = "camelCase")]
478pub struct SignatureInfoListWithError {
479    pub items: Vec<SignatureInfoWithError>,
480}
481
482#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, Default)]
483#[serde(rename_all = "camelCase")]
484pub struct PaginatedSignatureInfoList {
485    pub items: Vec<SignatureInfo>,
486    pub cursor: Option<String>,
487}
488
489#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, Default)]
490#[serde(rename_all = "camelCase")]
491pub struct PaginatedSignatureInfoListWithError {
492    pub items: Vec<SignatureInfoWithError>,
493    pub cursor: Option<String>,
494}
495
496impl From<PaginatedSignatureInfoListWithError> for PaginatedSignatureInfoList {
497    fn from(paginated: PaginatedSignatureInfoListWithError) -> Self {
498        PaginatedSignatureInfoList {
499            items: paginated.items.into_iter().map(Into::into).collect(),
500            cursor: paginated.cursor,
501        }
502    }
503}
504
505impl From<SignatureInfoWithError> for SignatureInfo {
506    fn from(signature_info: SignatureInfoWithError) -> Self {
507        SignatureInfo {
508            signature: signature_info.signature,
509            slot: signature_info.slot,
510            block_time: signature_info.block_time,
511        }
512    }
513}
514
515pub enum SignatureFilter {
516    Account(Hash),
517    Address(SerializablePubkey),
518    Owner(SerializablePubkey),
519}
520
521#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
522pub enum SignatureSearchType {
523    Standard,
524    Token,
525}
526
527fn compute_search_filter_and_arg(
528    search_type: SignatureSearchType,
529    signature_filter: SignatureFilter,
530) -> Result<(String, Value), PhotonApiError> {
531    if search_type == SignatureSearchType::Token {
532        match signature_filter {
533            SignatureFilter::Owner(_) => {}
534            _ => {
535                return Err(PhotonApiError::ValidationError(
536                    "Only owner search is supported for token signatures".to_string(),
537                ))
538            }
539        }
540    }
541    let base_table = match search_type {
542        SignatureSearchType::Standard => "accounts",
543        SignatureSearchType::Token => "token_accounts",
544    };
545    let (filter, arg): (String, Vec<u8>) = match signature_filter {
546        SignatureFilter::Account(hash) => ("WHERE account_transactions.hash = $1".to_string(), hash.into()),
547        SignatureFilter::Address(address) => {
548            ("JOIN accounts ON account_transactions.hash = accounts.hash WHERE accounts.address = $1".to_string(), address.into())
549        }
550        SignatureFilter::Owner(owner) => (format!(
551            "JOIN {base_table} ON account_transactions.hash = {base_table}.hash WHERE {base_table}.owner = $1"
552        ), owner.into()),
553    };
554    let arg: Value = arg.into();
555    Ok((filter, arg))
556}
557
558fn compute_cursor_filter(
559    cursor: Option<String>,
560    num_preceding_args: i64,
561) -> Result<(String, Vec<Value>), PhotonApiError> {
562    match cursor {
563        Some(cursor) => {
564            let bytes = bs58::decode(cursor.clone()).into_vec().map_err(|_| {
565                PhotonApiError::ValidationError(format!("Invalid cursor {}", cursor))
566            })?;
567            let slot_bytes = 8;
568            let signature_bytes = 64;
569            let expected_cursor_length = slot_bytes + signature_bytes;
570            if bytes.len() != expected_cursor_length {
571                return Err(PhotonApiError::ValidationError(format!(
572                    "Invalid cursor length. Expected {}. Received {}.",
573                    expected_cursor_length,
574                    bytes.len()
575                )));
576            }
577            let (slot, signature) = bytes.split_at(slot_bytes);
578            let slot = LittleEndian::read_u64(slot);
579            let signature = Signature::try_from(signature).map_err(|_| {
580                PhotonApiError::ValidationError("Invalid signature in cursor".to_string())
581            })?;
582
583            Ok((
584                format!(
585                    "AND (transactions.slot < ${} OR (transactions.slot = ${} AND transactions.signature < ${}))",
586                    num_preceding_args + 1, num_preceding_args + 2, num_preceding_args + 3
587                ),
588                vec![
589                    slot.into(),
590                    slot.into(),
591                    Into::<Vec<u8>>::into(Into::<[u8; 64]>::into(signature)).into(),
592                ],
593            ))
594        }
595        None => Ok(("".to_string(), vec![])),
596    }
597}
598
599fn compute_raw_sql_query_and_args(
600    search_type: SignatureSearchType,
601    signature_filter: Option<SignatureFilter>,
602    only_compressed: bool,
603    cursor: Option<String>,
604    limit: u64,
605) -> Result<(String, Vec<Value>), PhotonApiError> {
606    match signature_filter {
607        Some(signature_filter) => {
608            let (cursor_filter, cursor_args) = compute_cursor_filter(cursor, 1)?;
609
610            let (filter, arg) = compute_search_filter_and_arg(search_type, signature_filter)?;
611
612            let raw_sql = format!(
613                "
614                SELECT DISTINCT transactions.signature, transactions.slot, transactions.error, blocks.block_time
615                FROM account_transactions
616                JOIN transactions ON account_transactions.signature = transactions.signature
617                JOIN blocks ON transactions.slot = blocks.slot
618                {filter}
619                {cursor_filter}
620                ORDER BY transactions.slot DESC, transactions.signature DESC
621                LIMIT {limit}
622            "
623            );
624
625            Ok((raw_sql, vec![arg].into_iter().chain(cursor_args).collect()))
626        }
627        None => {
628            if search_type == SignatureSearchType::Token {
629                return Err(PhotonApiError::ValidationError(
630                    "Token search requires a filter".to_string(),
631                ));
632            }
633            let compression_filter = if only_compressed {
634                "AND transactions.uses_compression = true"
635            } else {
636                ""
637            };
638            let (cursor_filter, cursor_args) = compute_cursor_filter(cursor, 0)?;
639            let raw_sql = format!(
640                "
641                SELECT transactions.signature, transactions.slot, transactions.error, blocks.block_time
642                FROM transactions
643                JOIN blocks ON transactions.slot = blocks.slot
644                {cursor_filter}
645                {compression_filter}
646                ORDER BY transactions.slot DESC, transactions.signature DESC
647                LIMIT {limit}
648            "
649            );
650            Ok((raw_sql, cursor_args))
651        }
652    }
653}
654
655pub async fn search_for_signatures(
656    conn: &DatabaseConnection,
657    search_type: SignatureSearchType,
658    signature_filter: Option<SignatureFilter>,
659    only_compressed: bool,
660    cursor: Option<String>,
661    limit: Option<Limit>,
662) -> Result<PaginatedSignatureInfoListWithError, PhotonApiError> {
663    let limit = limit.unwrap_or_default().0;
664    let (raw_sql, args) = compute_raw_sql_query_and_args(
665        search_type,
666        signature_filter,
667        only_compressed,
668        cursor,
669        limit,
670    )?;
671
672    let signatures: Vec<SignatureInfoModel> = SignatureInfoModel::find_by_statement(
673        Statement::from_sql_and_values(conn.get_database_backend(), &raw_sql, args.clone()),
674    )
675    .all(conn)
676    .await?;
677
678    let signatures = parse_signatures_infos(signatures)?;
679
680    let cursor = match signatures.len() < limit as usize {
681        true => None,
682        false => signatures.last().map(|signature| {
683            bs58::encode::<Vec<u8>>({
684                let mut bytes = signature.slot.0.to_le_bytes().to_vec();
685                bytes.extend_from_slice(signature.signature.0.as_ref());
686                bytes
687            })
688            .into_string()
689        }),
690    };
691
692    Ok(PaginatedSignatureInfoListWithError {
693        items: signatures,
694        cursor,
695    })
696}
697
698pub fn parse_signatures_infos(
699    signatures: Vec<SignatureInfoModel>,
700) -> Result<Vec<SignatureInfoWithError>, PhotonApiError> {
701    signatures.into_iter().map(parse_signature_info).collect()
702}
703
704fn parse_signature_info(
705    model: SignatureInfoModel,
706) -> Result<SignatureInfoWithError, PhotonApiError> {
707    Ok(SignatureInfoWithError {
708        signature: SerializableSignature(
709            Signature::try_from(model.signature)
710                .map_err(|_| PhotonApiError::UnexpectedError("Invalid signature".to_string()))?,
711        ),
712        slot: UnsignedInteger(model.slot as u64),
713        block_time: UnixTimestamp(model.block_time as u64),
714        error: model.error,
715    })
716}
717
718#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
719// We do not use generics to simplify documentation generation.
720#[serde(deny_unknown_fields, rename_all = "camelCase")]
721pub struct GetPaginatedSignaturesResponse {
722    pub context: Context,
723    pub value: PaginatedSignatureInfoList,
724}
725
726#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
727#[serde(deny_unknown_fields, rename_all = "camelCase")]
728// We do not use generics to simplify documentation generation.
729pub struct AccountBalanceResponse {
730    pub context: Context,
731    pub value: UnsignedInteger,
732}
733
734#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, Default)]
735#[serde(deny_unknown_fields, rename_all = "camelCase")]
736pub struct GetLatestSignaturesRequest {
737    #[serde(default)]
738    pub limit: Option<Limit>,
739    #[serde(default)]
740    pub cursor: Option<String>,
741}
742
743#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
744// We do not use generics to simplify documentation generation.
745#[serde(deny_unknown_fields, rename_all = "camelCase")]
746pub struct GetNonPaginatedSignaturesResponse {
747    pub context: Context,
748    pub value: SignatureInfoList,
749}
750
751#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
752// We do not use generics to simplify documentation generation.
753#[serde(deny_unknown_fields, rename_all = "camelCase")]
754pub struct GetNonPaginatedSignaturesResponseWithError {
755    pub context: Context,
756    pub value: SignatureInfoListWithError,
757}