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 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#[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 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#[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")]
728pub 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#[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#[serde(deny_unknown_fields, rename_all = "camelCase")]
754pub struct GetNonPaginatedSignaturesResponseWithError {
755 pub context: Context,
756 pub value: SignatureInfoListWithError,
757}