photon_indexer/api/method/
get_multiple_compressed_accounts.rs

1use std::collections::HashMap;
2
3use crate::{common::typedefs::account::Account, dao::generated::accounts};
4use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
5use serde::{Deserialize, Serialize};
6use utoipa::{
7    openapi::{RefOr, Schema},
8    ToSchema,
9};
10
11use super::{
12    super::error::PhotonApiError,
13    utils::{Context, PAGE_LIMIT},
14};
15use crate::common::typedefs::hash::Hash;
16use crate::common::typedefs::serializable_pubkey::SerializablePubkey;
17
18use super::utils::parse_account_model;
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, Default)]
21#[serde(deny_unknown_fields, rename_all = "camelCase")]
22pub struct GetMultipleCompressedAccountsRequest {
23    #[serde(default)]
24    pub hashes: Option<Vec<Hash>>,
25    #[serde(default)]
26    pub addresses: Option<Vec<SerializablePubkey>>,
27}
28
29impl GetMultipleCompressedAccountsRequest {
30    pub fn adjusted_schema() -> RefOr<Schema> {
31        let mut schema = GetMultipleCompressedAccountsRequest::schema().1;
32        let object = match schema {
33            RefOr::T(Schema::Object(ref mut object)) => {
34                let example = serde_json::to_value(GetMultipleCompressedAccountsRequest {
35                    hashes: Some(vec![Hash::new_unique(), Hash::new_unique()]),
36                    addresses: None,
37                })
38                .unwrap();
39                object.default = Some(example.clone());
40                object.example = Some(example);
41                object.description = Some("Request for compressed account data".to_string());
42                object.clone()
43            }
44            _ => unimplemented!(),
45        };
46        RefOr::T(Schema::Object(object))
47    }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, ToSchema, Default)]
51#[serde(deny_unknown_fields, rename_all = "camelCase")]
52pub struct AccountList {
53    pub items: Vec<Option<Account>>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, ToSchema)]
57// We do not use generics in order to simplify documentation generation
58#[serde(deny_unknown_fields, rename_all = "camelCase")]
59pub struct GetMultipleCompressedAccountsResponse {
60    pub context: Context,
61    pub value: AccountList,
62}
63
64pub async fn fetch_accounts_from_hashes(
65    conn: &DatabaseConnection,
66    hashes: Vec<Hash>,
67    spent: bool,
68) -> Result<Vec<Option<accounts::Model>>, PhotonApiError> {
69    let raw_hashes: Vec<Vec<u8>> = hashes.into_iter().map(|hash| hash.to_vec()).collect();
70
71    let accounts = accounts::Entity::find()
72        .filter(
73            accounts::Column::Hash
74                .is_in(raw_hashes.clone())
75                .and(accounts::Column::Spent.eq(spent)),
76        )
77        .all(conn)
78        .await
79        .map_err(|e| PhotonApiError::UnexpectedError(format!("DB error: {}", e)))?;
80
81    let hash_to_account: HashMap<Vec<u8>, accounts::Model> = accounts
82        .into_iter()
83        .map(|account| (account.hash.clone(), account))
84        .collect();
85
86    Ok(raw_hashes
87        .into_iter()
88        .map(|hash| hash_to_account.get(&hash).cloned())
89        .collect())
90}
91
92async fn fetch_account_from_addresses(
93    conn: &DatabaseConnection,
94    addresses: Vec<SerializablePubkey>,
95) -> Result<Vec<Option<accounts::Model>>, PhotonApiError> {
96    let raw_addresses: Vec<Vec<u8>> = addresses.into_iter().map(|addr| addr.into()).collect();
97    let accounts = accounts::Entity::find()
98        .filter(
99            accounts::Column::Address
100                .is_in(raw_addresses.clone())
101                .and(accounts::Column::Spent.eq(false)),
102        )
103        .all(conn)
104        .await
105        .map_err(|e| PhotonApiError::UnexpectedError(format!("DB error: {}", e)))?;
106    let address_to_account: HashMap<Option<Vec<u8>>, accounts::Model> = accounts
107        .into_iter()
108        .map(|account| (account.address.clone(), account))
109        .collect();
110    Ok(raw_addresses
111        .into_iter()
112        .map(|addr| address_to_account.get(&Some(addr)).cloned())
113        .collect())
114}
115
116pub async fn get_multiple_compressed_accounts(
117    conn: &DatabaseConnection,
118    request: GetMultipleCompressedAccountsRequest,
119) -> Result<GetMultipleCompressedAccountsResponse, PhotonApiError> {
120    let context = Context::extract(conn).await?;
121
122    let accounts = match (request.hashes, request.addresses) {
123        (Some(hashes), None) => {
124            if hashes.len() > PAGE_LIMIT as usize {
125                return Err(PhotonApiError::ValidationError(format!(
126                    "Too many hashes requested {}. Maximum allowed: {}",
127                    hashes.len(),
128                    PAGE_LIMIT
129                )));
130            }
131            fetch_accounts_from_hashes(conn, hashes, false).await?
132        }
133        (None, Some(addresses)) => {
134            if addresses.len() > PAGE_LIMIT as usize {
135                return Err(PhotonApiError::ValidationError(format!(
136                    "Too many addresses requested {}. Maximum allowed: {}",
137                    addresses.len(),
138                    PAGE_LIMIT
139                )));
140            }
141            fetch_account_from_addresses(conn, addresses).await?
142        }
143        _ => panic!("Either hashes or addresses must be provided"),
144    };
145
146    Ok(GetMultipleCompressedAccountsResponse {
147        context,
148        value: AccountList {
149            items: accounts
150                .into_iter()
151                .map(|x| x.map(parse_account_model).transpose())
152                .collect::<Result<Vec<_>, _>>()?,
153        },
154    })
155}