From cf6c6ebde6b931e63205446f5019b5ac650d5c92 Mon Sep 17 00:00:00 2001 From: pmantica11 <151664502+pmantica11@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:42:23 -0600 Subject: [PATCH] Add getCompressedMintTokenHolders endpoint and fix pagination for signatures and compressed token accounts (#241) * Increase poller timeout * Fix poller bug * Intermediate commit * Intermediate commit * Intermediate commit * Intermediate commit * Intermediate commit * Update docs --- src/api/api.rs | 15 ++ .../get_compressed_mint_token_holders.rs | 110 ++++++++++++++ .../get_compressed_token_balances_by_owner.rs | 81 ++++++---- src/api/method/mod.rs | 1 + src/api/method/utils.rs | 24 +-- src/api/rpc_server.rs | 10 ++ src/migration/m20241008_000006_init.rs | 54 +++++++ src/migration/mod.rs | 2 + src/openapi/mod.rs | 6 + src/openapi/specs/getCompressedAccount.yaml | 2 +- .../specs/getCompressedAccountBalance.yaml | 2 +- .../specs/getCompressedAccountProof.yaml | 8 +- .../specs/getCompressedAccountsByOwner.yaml | 2 +- .../specs/getCompressedBalanceByOwner.yaml | 2 +- .../specs/getCompressedMintTokenHolders.yaml | 140 ++++++++++++++++++ .../getCompressedTokenAccountBalance.yaml | 2 +- .../getCompressedTokenAccountsByDelegate.yaml | 8 +- .../getCompressedTokenAccountsByOwner.yaml | 8 +- .../getCompressedTokenBalancesByOwner.yaml | 5 +- .../getCompressionSignaturesForAccount.yaml | 4 +- .../getCompressionSignaturesForAddress.yaml | 6 +- .../getCompressionSignaturesForOwner.yaml | 6 +- ...getCompressionSignaturesForTokenOwner.yaml | 6 +- src/openapi/specs/getIndexerHealth.yaml | 2 +- src/openapi/specs/getIndexerSlot.yaml | 2 +- .../specs/getLatestCompressionSignatures.yaml | 2 +- .../specs/getLatestNonVotingSignatures.yaml | 2 +- .../getMultipleCompressedAccountProofs.yaml | 8 +- .../specs/getMultipleCompressedAccounts.yaml | 8 +- .../specs/getMultipleNewAddressProofs.yaml | 8 +- .../specs/getMultipleNewAddressProofsV2.yaml | 8 +- .../getTransactionWithCompressionInfo.yaml | 8 +- src/openapi/specs/getValidityProof.yaml | 8 +- tests/integration_tests/mock_tests.rs | 54 ++++++- 34 files changed, 517 insertions(+), 97 deletions(-) create mode 100644 src/api/method/get_compressed_mint_token_holders.rs create mode 100644 src/migration/m20241008_000006_init.rs create mode 100644 src/openapi/specs/getCompressedMintTokenHolders.yaml diff --git a/src/api/api.rs b/src/api/api.rs index c2e4021a..41c71950 100644 --- a/src/api/api.rs +++ b/src/api/api.rs @@ -12,6 +12,9 @@ use super::method::get_compressed_account::AccountResponse; use super::method::get_compressed_balance_by_owner::{ get_compressed_balance_by_owner, GetCompressedBalanceByOwnerRequest, }; +use super::method::get_compressed_mint_token_holders::{ + get_compressed_mint_token_holders, GetCompressedMintTokenHoldersRequest, OwnerBalancesResponse, +}; use super::method::get_compressed_token_balances_by_owner::{ get_compressed_token_balances_by_owner, GetCompressedTokenBalancesByOwnerRequest, TokenBalancesResponse, @@ -210,6 +213,13 @@ impl PhotonApi { get_compressed_accounts_by_owner(self.db_conn.as_ref(), request).await } + pub async fn get_compressed_mint_token_holders( + &self, + request: GetCompressedMintTokenHoldersRequest, + ) -> Result { + get_compressed_mint_token_holders(self.db_conn.as_ref(), request).await + } + pub async fn get_multiple_compressed_accounts( &self, request: GetMultipleCompressedAccountsRequest, @@ -306,6 +316,11 @@ impl PhotonApi { request: Some(GetCompressedAccountsByOwnerRequest::schema().1), response: GetCompressedAccountsByOwnerResponse::schema().1, }, + OpenApiSpec { + name: "getCompressedMintTokenHolders".to_string(), + request: Some(GetCompressedMintTokenHoldersRequest::schema().1), + response: OwnerBalancesResponse::schema().1, + }, OpenApiSpec { name: "getMultipleCompressedAccounts".to_string(), request: Some(GetMultipleCompressedAccountsRequest::adjusted_schema()), diff --git a/src/api/method/get_compressed_mint_token_holders.rs b/src/api/method/get_compressed_mint_token_holders.rs new file mode 100644 index 00000000..06ea443e --- /dev/null +++ b/src/api/method/get_compressed_mint_token_holders.rs @@ -0,0 +1,110 @@ +use byteorder::{ByteOrder, LittleEndian}; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, QuerySelect}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::common::typedefs::bs58_string::Base58String; +use crate::common::typedefs::serializable_pubkey::SerializablePubkey; +use crate::common::typedefs::unsigned_integer::UnsignedInteger; +use crate::dao::generated::token_owner_balances; + +use super::super::error::PhotonApiError; +use super::utils::{parse_decimal, Context, Limit, PAGE_LIMIT}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +pub struct OwnerBalance { + pub owner: SerializablePubkey, + pub balance: UnsignedInteger, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +pub struct OwnerBalanceList { + pub items: Vec, + pub cursor: Option, +} + +// We do not use generics to simplify documentation generation. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct OwnerBalancesResponse { + pub context: Context, + pub value: OwnerBalanceList, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, Default)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct GetCompressedMintTokenHoldersRequest { + pub mint: SerializablePubkey, + pub cursor: Option, + pub limit: Option, +} + +pub async fn get_compressed_mint_token_holders( + conn: &DatabaseConnection, + request: GetCompressedMintTokenHoldersRequest, +) -> Result { + let context = Context::extract(conn).await?; + let GetCompressedMintTokenHoldersRequest { + mint, + cursor, + limit, + } = request; + let mut filter = token_owner_balances::Column::Mint.eq::>(mint.into()); + if let Some(cursor) = cursor { + let bytes = cursor.0; + let expected_cursor_length = 40; + let (balance, owner) = if bytes.len() == expected_cursor_length { + let (balance, owner) = bytes.split_at(expected_cursor_length); + (balance, owner) + } else { + return Err(PhotonApiError::ValidationError(format!( + "Invalid cursor length. Expected {}. Received {}.", + expected_cursor_length, + bytes.len() + ))); + }; + let balance = LittleEndian::read_u64(&balance); + filter = filter.and( + token_owner_balances::Column::Amount.lt(balance).or( + token_owner_balances::Column::Amount + .eq(balance) + .and(token_owner_balances::Column::Owner.gt::>(owner.into())), + ), + ); + } + let limit = limit.map(|l| l.value()).unwrap_or(PAGE_LIMIT); + + let items = token_owner_balances::Entity::find() + .filter(filter) + .order_by_desc(token_owner_balances::Column::Amount) + .order_by_asc(token_owner_balances::Column::Mint) + .limit(limit) + .all(conn) + .await? + .drain(..) + .map(|token_owner_balance| { + Ok(OwnerBalance { + owner: token_owner_balance.owner.try_into()?, + balance: UnsignedInteger(parse_decimal(token_owner_balance.amount)?), + }) + }) + .collect::, PhotonApiError>>()?; + + let mut cursor = items.last().map(|item| { + Base58String({ + let item = item.clone(); + let mut bytes: Vec = Vec::new(); + bytes.extend_from_slice(&item.balance.0.to_le_bytes()); + bytes.extend_from_slice(&item.owner.0.to_bytes()); + bytes + }) + }); + if items.len() < limit as usize { + cursor = None; + } + + Ok(OwnerBalancesResponse { + value: OwnerBalanceList { items, cursor }, + context, + }) +} diff --git a/src/api/method/get_compressed_token_balances_by_owner.rs b/src/api/method/get_compressed_token_balances_by_owner.rs index d33f3738..99767857 100644 --- a/src/api/method/get_compressed_token_balances_by_owner.rs +++ b/src/api/method/get_compressed_token_balances_by_owner.rs @@ -1,15 +1,18 @@ -use std::collections::HashMap; -use sea_orm::DatabaseConnection; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, QuerySelect}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use crate::common::typedefs::bs58_string::Base58String; use crate::common::typedefs::serializable_pubkey::SerializablePubkey; use crate::common::typedefs::unsigned_integer::UnsignedInteger; +use crate::dao::generated::token_owner_balances; -use super::utils::{Authority, Context, GetCompressedTokenAccountsByAuthorityOptions, Limit}; -use super::{super::error::PhotonApiError, utils::fetch_token_accounts}; +use super::utils::{ + parse_decimal, Context, Limit, + PAGE_LIMIT, +}; +use super::super::error::PhotonApiError; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] pub struct TokenBalance { @@ -20,7 +23,7 @@ pub struct TokenBalance { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] pub struct TokenBalanceList { pub token_balances: Vec, - pub cursor: Option, + pub cursor: Option, } // We do not use generics to simplify documentation generation. @@ -44,40 +47,64 @@ pub async fn get_compressed_token_balances_by_owner( conn: &DatabaseConnection, request: GetCompressedTokenBalancesByOwnerRequest, ) -> Result { + let context = Context::extract(conn).await?; let GetCompressedTokenBalancesByOwnerRequest { owner, mint, cursor, limit, } = request; + let mut filter = token_owner_balances::Column::Owner.eq::>(owner.into()); + if let Some(mint) = mint { + filter = filter.and(token_owner_balances::Column::Mint.eq::>(mint.into())); + } + if let Some(cursor) = cursor { + let bytes = cursor.0; + let expected_cursor_length = 32; + let mint = if bytes.len() == expected_cursor_length { + bytes.to_vec() + } else { + return Err(PhotonApiError::ValidationError(format!( + "Invalid cursor length. Expected {}. Received {}.", + expected_cursor_length, + bytes.len() + ))); + }; + filter = filter.and(token_owner_balances::Column::Mint.gte::>(mint.into())); + } + let limit = limit.map(|l| l.value()).unwrap_or(PAGE_LIMIT); - let options = GetCompressedTokenAccountsByAuthorityOptions { - mint, - cursor, - limit, - }; - let token_accounts = fetch_token_accounts(conn, Authority::Owner(owner), options).await?; - let mut mint_to_balance: HashMap = HashMap::new(); + let items = token_owner_balances::Entity::find() + .filter(filter) + .order_by_asc(token_owner_balances::Column::Mint) + .limit(limit) + .all(conn) + .await? + .drain(..) + .map(|token_owner_balance| { + Ok(TokenBalance { + mint: token_owner_balance.mint.try_into()?, + balance: UnsignedInteger(parse_decimal(token_owner_balance.amount)?), + }) + }) + .collect::, PhotonApiError>>()?; - for token_account in token_accounts.value.items.iter() { - let balance = mint_to_balance - .entry(token_account.token_data.mint) - .or_insert(0); - *balance += token_account.token_data.amount.0; - } - let token_balances: Vec = mint_to_balance - .into_iter() - .map(|(mint, balance)| TokenBalance { - mint, - balance: UnsignedInteger(balance), + let mut cursor = items.last().map(|item| { + Base58String({ + let item = item.clone(); + let bytes: Vec = item.mint.into(); + bytes }) - .collect(); + }); + if items.len() < limit as usize { + cursor = None; + } Ok(TokenBalancesResponse { - context: token_accounts.context, value: TokenBalanceList { - token_balances, - cursor: None, + token_balances: items, + cursor, }, + context, }) } diff --git a/src/api/method/mod.rs b/src/api/method/mod.rs index c9bd4297..9774cfd4 100644 --- a/src/api/method/mod.rs +++ b/src/api/method/mod.rs @@ -3,6 +3,7 @@ pub mod get_compressed_account_balance; pub mod get_compressed_account_proof; pub mod get_compressed_accounts_by_owner; pub mod get_compressed_balance_by_owner; +pub mod get_compressed_mint_token_holders; pub mod get_compressed_token_account_balance; pub mod get_compressed_token_accounts_by_delegate; pub mod get_compressed_token_accounts_by_owner; diff --git a/src/api/method/utils.rs b/src/api/method/utils.rs index f38991a8..a6f69d73 100644 --- a/src/api/method/utils.rs +++ b/src/api/method/utils.rs @@ -271,9 +271,14 @@ pub async fn fetch_token_accounts( ))); } let (mint, hash) = bytes.split_at(32); - filter = filter - .and(token_accounts::Column::Mint.gte::>(mint.into())) - .and(token_accounts::Column::Hash.gt::>(hash.into())); + + filter = filter.and( + token_accounts::Column::Mint.gt::>(mint.into()).or( + token_accounts::Column::Mint + .eq::>(mint.into()) + .and(token_accounts::Column::Hash.gt::>(hash.into())), + ), + ); } if let Some(l) = options.limit { limit = l.value(); @@ -282,9 +287,11 @@ pub async fn fetch_token_accounts( let items = token_accounts::Entity::find() .find_also_related(accounts::Entity) .filter(filter) - .order_by_asc(token_accounts::Column::Mint) - .order_by_asc(token_accounts::Column::Hash) + .order_by(token_accounts::Column::Mint, sea_orm::Order::Asc) + .order_by(token_accounts::Column::Hash, sea_orm::Order::Asc) .limit(limit) + .order_by(token_accounts::Column::Mint, sea_orm::Order::Asc) + .order_by(token_accounts::Column::Hash, sea_orm::Order::Asc) .all(conn) .await? .drain(..) @@ -572,15 +579,14 @@ fn compute_cursor_filter( let signature = Signature::try_from(signature).map_err(|_| { PhotonApiError::ValidationError("Invalid signature in cursor".to_string()) })?; - let (slot_arg_index, signature_arg_index) = - (num_preceding_args + 1, num_preceding_args + 2); Ok(( format!( - "AND transactions.slot <= ${} AND transactions.signature < ${}", - slot_arg_index, signature_arg_index + "AND (transactions.slot < ${} OR (transactions.slot = ${} AND transactions.signature < ${}))", + num_preceding_args + 1, num_preceding_args + 2, num_preceding_args + 3 ), vec![ + slot.into(), slot.into(), Into::>::into(Into::<[u8; 64]>::into(signature)).into(), ], diff --git a/src/api/rpc_server.rs b/src/api/rpc_server.rs index d3c95e10..f63329d1 100644 --- a/src/api/rpc_server.rs +++ b/src/api/rpc_server.rs @@ -291,6 +291,16 @@ fn build_rpc_module(api_and_indexer: PhotonApi) -> Result, .map_err(Into::into) }, )?; + module.register_async_method( + "getCompressedMintTokenHolders", + |rpc_params, rpc_context| async move { + let api = rpc_context.as_ref(); + let payload = rpc_params.parse()?; + api.get_compressed_mint_token_holders(payload) + .await + .map_err(Into::into) + }, + )?; Ok(module) } diff --git a/src/migration/m20241008_000006_init.rs b/src/migration/m20241008_000006_init.rs new file mode 100644 index 00000000..5b13e74c --- /dev/null +++ b/src/migration/m20241008_000006_init.rs @@ -0,0 +1,54 @@ +use sea_orm_migration::prelude::*; +use sea_orm_migration::sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; + +use crate::migration::model::table::TokenOwnerBalances; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +async fn execute_sql<'a>(manager: &SchemaManager<'_>, sql: &str) -> Result<(), DbErr> { + manager + .get_connection() + .execute(Statement::from_string( + manager.get_database_backend(), + sql.to_string(), + )) + .await?; + Ok(()) +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + if manager.get_database_backend() == DatabaseBackend::Postgres { + // Create index concurrently for Postgres + execute_sql( + manager, + "CREATE INDEX CONCURRENTLY IF NOT EXISTS token_holder_mint_balance_owner_idx ON token_owner_balances (mint, amount, owner);", + ) + .await?; + } else { + // For other databases, create index normally + execute_sql( + manager, + "CREATE INDEX IF NOT EXISTS token_holder_mint_balance_owner_idx ON token_owner_balances (mint, amount, owner);", + ) + .await?; + } + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_index( + Index::drop() + .name("token_holder_mint_balance_owner_idx") + .table(TokenOwnerBalances::Table) + .to_owned(), + ) + .await?; + + Ok(()) + } +} diff --git a/src/migration/mod.rs b/src/migration/mod.rs index c77359d6..d737d348 100644 --- a/src/migration/mod.rs +++ b/src/migration/mod.rs @@ -5,6 +5,7 @@ mod m20240623_000002_init; mod m20240624_000003_init; mod m20240807_000004_init; mod m20240914_000005_init; +mod m20241008_000006_init; mod model; pub struct Migrator; @@ -18,6 +19,7 @@ impl MigratorTrait for Migrator { Box::new(m20240624_000003_init::Migration), Box::new(m20240807_000004_init::Migration), Box::new(m20240914_000005_init::Migration), + Box::new(m20241008_000006_init::Migration), ] } } diff --git a/src/openapi/mod.rs b/src/openapi/mod.rs index f9b3eeeb..e568ceff 100644 --- a/src/openapi/mod.rs +++ b/src/openapi/mod.rs @@ -5,6 +5,9 @@ use crate::api::method::get_compressed_accounts_by_owner::DataSlice; use crate::api::method::get_compressed_accounts_by_owner::FilterSelector; use crate::api::method::get_compressed_accounts_by_owner::Memcmp; use crate::api::method::get_compressed_accounts_by_owner::PaginatedAccountList; +use crate::api::method::get_compressed_mint_token_holders::OwnerBalance; +use crate::api::method::get_compressed_mint_token_holders::OwnerBalanceList; +use crate::api::method::get_compressed_mint_token_holders::OwnerBalancesResponse; use crate::api::method::get_compressed_token_account_balance::TokenAccountBalance; use crate::api::method::get_compressed_token_balances_by_owner::TokenBalance; use crate::api::method::get_compressed_token_balances_by_owner::TokenBalanceList; @@ -102,6 +105,9 @@ const JSON_CONTENT_TYPE: &str = "application/json"; Memcmp, AddressListWithTrees, AddressWithTree, + OwnerBalance, + OwnerBalanceList, + OwnerBalancesResponse, )))] struct ApiDoc; diff --git a/src/openapi/specs/getCompressedAccount.yaml b/src/openapi/specs/getCompressedAccount.yaml index 6a59fa8e..3b86c2ac 100644 --- a/src/openapi/specs/getCompressedAccount.yaml +++ b/src/openapi/specs/getCompressedAccount.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: diff --git a/src/openapi/specs/getCompressedAccountBalance.yaml b/src/openapi/specs/getCompressedAccountBalance.yaml index 78e837f8..3f5b43df 100644 --- a/src/openapi/specs/getCompressedAccountBalance.yaml +++ b/src/openapi/specs/getCompressedAccountBalance.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: diff --git a/src/openapi/specs/getCompressedAccountProof.yaml b/src/openapi/specs/getCompressedAccountProof.yaml index f3753085..1172b5a5 100644 --- a/src/openapi/specs/getCompressedAccountProof.yaml +++ b/src/openapi/specs/getCompressedAccountProof.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: @@ -93,7 +93,7 @@ components: Hash: type: string description: A 32-byte hash represented as a base58 string. - example: 1111111Af7Udc9v3L82dQM5b4zee1Xt77Be4czzbH + example: 1111111BTngbpkVTh3nGGdFdufHcG5TN7hXV6AfDy MerkleProofWithContext: type: object required: @@ -126,5 +126,5 @@ components: SerializablePubkey: type: string description: A Solana public key represented as a base58 string. - default: 1111111AFmseVrdL9f9oyCzZefL9tG6UbvhMPRAGw - example: 1111111AFmseVrdL9f9oyCzZefL9tG6UbvhMPRAGw + default: 1111111B4T5ciTCkWauSqVAcVKy88ofjcSamrapud + example: 1111111B4T5ciTCkWauSqVAcVKy88ofjcSamrapud diff --git a/src/openapi/specs/getCompressedAccountsByOwner.yaml b/src/openapi/specs/getCompressedAccountsByOwner.yaml index 29b3f960..eaa86a91 100644 --- a/src/openapi/specs/getCompressedAccountsByOwner.yaml +++ b/src/openapi/specs/getCompressedAccountsByOwner.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: diff --git a/src/openapi/specs/getCompressedBalanceByOwner.yaml b/src/openapi/specs/getCompressedBalanceByOwner.yaml index c67759a4..d169c2c2 100644 --- a/src/openapi/specs/getCompressedBalanceByOwner.yaml +++ b/src/openapi/specs/getCompressedBalanceByOwner.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: diff --git a/src/openapi/specs/getCompressedMintTokenHolders.yaml b/src/openapi/specs/getCompressedMintTokenHolders.yaml new file mode 100644 index 00000000..50f6ea8b --- /dev/null +++ b/src/openapi/specs/getCompressedMintTokenHolders.yaml @@ -0,0 +1,140 @@ +openapi: 3.0.3 +info: + title: photon-indexer + description: Solana indexer for general compression + license: + name: Apache-2.0 + version: 0.45.0 +servers: +- url: https://devnet.helius-rpc.com?api-key= +paths: + /: + summary: getCompressedMintTokenHolders + post: + requestBody: + content: + application/json: + schema: + type: object + required: + - jsonrpc + - id + - method + - params + properties: + id: + type: string + description: An ID to identify the request. + enum: + - test-account + jsonrpc: + type: string + description: The version of the JSON-RPC protocol. + enum: + - '2.0' + method: + type: string + description: The name of the method to invoke. + enum: + - getCompressedMintTokenHolders + params: + type: object + required: + - mint + properties: + cursor: + allOf: + - $ref: '#/components/schemas/Base58String' + nullable: true + limit: + allOf: + - $ref: '#/components/schemas/Limit' + nullable: true + mint: + $ref: '#/components/schemas/SerializablePubkey' + additionalProperties: false + required: true + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - context + - value + properties: + context: + $ref: '#/components/schemas/Context' + value: + $ref: '#/components/schemas/OwnerBalanceList' + additionalProperties: false + '429': + description: Exceeded rate limit. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '500': + description: The server encountered an unexpected condition that prevented it from fulfilling the request. + content: + application/json: + schema: + type: object + properties: + error: + type: string +components: + schemas: + Base58String: + type: string + description: A base 58 encoded string. + default: 3J98t1WpEZ73CNm + example: 3J98t1WpEZ73CNm + Context: + type: object + required: + - slot + properties: + slot: + type: integer + default: 100 + example: 100 + Limit: + type: integer + format: int64 + minimum: 0 + OwnerBalance: + type: object + required: + - owner + - balance + properties: + balance: + $ref: '#/components/schemas/UnsignedInteger' + owner: + $ref: '#/components/schemas/SerializablePubkey' + OwnerBalanceList: + type: object + required: + - items + properties: + cursor: + $ref: '#/components/schemas/Base58String' + items: + type: array + items: + $ref: '#/components/schemas/OwnerBalance' + SerializablePubkey: + type: string + description: A Solana public key represented as a base58 string. + default: 11111117353mdUKehx9GW6JNHznGt5oSZs9fWkVkB + example: 11111117353mdUKehx9GW6JNHznGt5oSZs9fWkVkB + UnsignedInteger: + type: integer + default: 100 + example: 100 diff --git a/src/openapi/specs/getCompressedTokenAccountBalance.yaml b/src/openapi/specs/getCompressedTokenAccountBalance.yaml index c863555b..1d85fada 100644 --- a/src/openapi/specs/getCompressedTokenAccountBalance.yaml +++ b/src/openapi/specs/getCompressedTokenAccountBalance.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: diff --git a/src/openapi/specs/getCompressedTokenAccountsByDelegate.yaml b/src/openapi/specs/getCompressedTokenAccountsByDelegate.yaml index 553d501b..6e407a8d 100644 --- a/src/openapi/specs/getCompressedTokenAccountsByDelegate.yaml +++ b/src/openapi/specs/getCompressedTokenAccountsByDelegate.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: @@ -165,7 +165,7 @@ components: Hash: type: string description: A 32-byte hash represented as a base58 string. - example: 111111193m4hAxmCcGXMfnjVPfNhWSjb69sDgffKu + example: 11111119rSGfPZLcyCGzY4uYEL1fkzJr6fke9qKxb Limit: type: integer format: int64 @@ -173,8 +173,8 @@ components: SerializablePubkey: type: string description: A Solana public key represented as a base58 string. - default: 11111118eRTi4fUVRoeYEeeTyL4DPAwxatvWT5q1Z - example: 11111118eRTi4fUVRoeYEeeTyL4DPAwxatvWT5q1Z + default: 11111119T6fgHG3unjQB6vpWozhBdiXDbQovvFVeF + example: 11111119T6fgHG3unjQB6vpWozhBdiXDbQovvFVeF TokenAcccount: type: object required: diff --git a/src/openapi/specs/getCompressedTokenAccountsByOwner.yaml b/src/openapi/specs/getCompressedTokenAccountsByOwner.yaml index d2ba0a19..12cb105b 100644 --- a/src/openapi/specs/getCompressedTokenAccountsByOwner.yaml +++ b/src/openapi/specs/getCompressedTokenAccountsByOwner.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: @@ -165,7 +165,7 @@ components: Hash: type: string description: A 32-byte hash represented as a base58 string. - example: 11111118F5rixNBnFLmioWZSYzjjFuAL5dyoDVzhD + example: 111111193m4hAxmCcGXMfnjVPfNhWSjb69sDgffKu Limit: type: integer format: int64 @@ -173,8 +173,8 @@ components: SerializablePubkey: type: string description: A Solana public key represented as a base58 string. - default: 11111117qkFjr4u54stuNNUR8fRF8dNhaP35yvANs - example: 11111117qkFjr4u54stuNNUR8fRF8dNhaP35yvANs + default: 11111118eRTi4fUVRoeYEeeTyL4DPAwxatvWT5q1Z + example: 11111118eRTi4fUVRoeYEeeTyL4DPAwxatvWT5q1Z TokenAcccount: type: object required: diff --git a/src/openapi/specs/getCompressedTokenBalancesByOwner.yaml b/src/openapi/specs/getCompressedTokenBalancesByOwner.yaml index 14e61801..a687a0a5 100644 --- a/src/openapi/specs/getCompressedTokenBalancesByOwner.yaml +++ b/src/openapi/specs/getCompressedTokenBalancesByOwner.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: @@ -133,8 +133,7 @@ components: - token_balances properties: cursor: - type: string - nullable: true + $ref: '#/components/schemas/Base58String' token_balances: type: array items: diff --git a/src/openapi/specs/getCompressionSignaturesForAccount.yaml b/src/openapi/specs/getCompressionSignaturesForAccount.yaml index f81fe8c4..152acec2 100644 --- a/src/openapi/specs/getCompressionSignaturesForAccount.yaml +++ b/src/openapi/specs/getCompressionSignaturesForAccount.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: @@ -93,7 +93,7 @@ components: Hash: type: string description: A 32-byte hash represented as a base58 string. - example: 1111111EgVWUh8o98knojjwqGKqVGFkQ9m5AxqKkj + example: 1111111FVAiSujNZVgYSc27t6zUTWoKfAGxbRzzPR SerializableSignature: type: string description: A Solana transaction signature. diff --git a/src/openapi/specs/getCompressionSignaturesForAddress.yaml b/src/openapi/specs/getCompressionSignaturesForAddress.yaml index 306a5037..8ab5a71a 100644 --- a/src/openapi/specs/getCompressionSignaturesForAddress.yaml +++ b/src/openapi/specs/getCompressionSignaturesForAddress.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: @@ -117,8 +117,8 @@ components: SerializablePubkey: type: string description: A Solana public key represented as a base58 string. - default: 1111111F5q7ToS5rKDfdAt2rgf9yPXY2f21tCRA55 - example: 1111111F5q7ToS5rKDfdAt2rgf9yPXY2f21tCRA55 + default: 1111111FtWKS22fGg9RG3ACuXKnwe57HfXuJfaphm + example: 1111111FtWKS22fGg9RG3ACuXKnwe57HfXuJfaphm SerializableSignature: type: string description: A Solana transaction signature. diff --git a/src/openapi/specs/getCompressionSignaturesForOwner.yaml b/src/openapi/specs/getCompressionSignaturesForOwner.yaml index 235d4d12..ea965c52 100644 --- a/src/openapi/specs/getCompressionSignaturesForOwner.yaml +++ b/src/openapi/specs/getCompressionSignaturesForOwner.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: @@ -117,8 +117,8 @@ components: SerializablePubkey: type: string description: A Solana public key represented as a base58 string. - default: 1111111FtWKS22fGg9RG3ACuXKnwe57HfXuJfaphm - example: 1111111FtWKS22fGg9RG3ACuXKnwe57HfXuJfaphm + default: 1111111GhBXQEdEh35AtuSNxMzRutcgYg3nj8kVLT + example: 1111111GhBXQEdEh35AtuSNxMzRutcgYg3nj8kVLT SerializableSignature: type: string description: A Solana transaction signature. diff --git a/src/openapi/specs/getCompressionSignaturesForTokenOwner.yaml b/src/openapi/specs/getCompressionSignaturesForTokenOwner.yaml index 7a8c553e..afebc568 100644 --- a/src/openapi/specs/getCompressionSignaturesForTokenOwner.yaml +++ b/src/openapi/specs/getCompressionSignaturesForTokenOwner.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: @@ -117,8 +117,8 @@ components: SerializablePubkey: type: string description: A Solana public key represented as a base58 string. - default: 1111111GhBXQEdEh35AtuSNxMzRutcgYg3nj8kVLT - example: 1111111GhBXQEdEh35AtuSNxMzRutcgYg3nj8kVLT + default: 1111111HVrjNTDp7PzvXmiZ1Cf4t9AFogZg9bv9y9 + example: 1111111HVrjNTDp7PzvXmiZ1Cf4t9AFogZg9bv9y9 SerializableSignature: type: string description: A Solana transaction signature. diff --git a/src/openapi/specs/getIndexerHealth.yaml b/src/openapi/specs/getIndexerHealth.yaml index fc49818d..096a9ca8 100644 --- a/src/openapi/specs/getIndexerHealth.yaml +++ b/src/openapi/specs/getIndexerHealth.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: diff --git a/src/openapi/specs/getIndexerSlot.yaml b/src/openapi/specs/getIndexerSlot.yaml index b68c5ca3..b15d1582 100644 --- a/src/openapi/specs/getIndexerSlot.yaml +++ b/src/openapi/specs/getIndexerSlot.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: diff --git a/src/openapi/specs/getLatestCompressionSignatures.yaml b/src/openapi/specs/getLatestCompressionSignatures.yaml index 5a47c712..fdae07fb 100644 --- a/src/openapi/specs/getLatestCompressionSignatures.yaml +++ b/src/openapi/specs/getLatestCompressionSignatures.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: diff --git a/src/openapi/specs/getLatestNonVotingSignatures.yaml b/src/openapi/specs/getLatestNonVotingSignatures.yaml index ac852834..df6ad327 100644 --- a/src/openapi/specs/getLatestNonVotingSignatures.yaml +++ b/src/openapi/specs/getLatestNonVotingSignatures.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: diff --git a/src/openapi/specs/getMultipleCompressedAccountProofs.yaml b/src/openapi/specs/getMultipleCompressedAccountProofs.yaml index 58e21cb9..2e517f9f 100644 --- a/src/openapi/specs/getMultipleCompressedAccountProofs.yaml +++ b/src/openapi/specs/getMultipleCompressedAccountProofs.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: @@ -92,7 +92,7 @@ components: Hash: type: string description: A 32-byte hash represented as a base58 string. - example: 1111111BTngbpkVTh3nGGdFdufHcG5TN7hXV6AfDy + example: 1111111CGTta3M4t3yXu8uRgkKvaWd2d8DQuZLKrf MerkleProofWithContext: type: object required: @@ -125,5 +125,5 @@ components: SerializablePubkey: type: string description: A Solana public key represented as a base58 string. - default: 1111111B4T5ciTCkWauSqVAcVKy88ofjcSamrapud - example: 1111111B4T5ciTCkWauSqVAcVKy88ofjcSamrapud + default: 1111111Bs8Haw3nAsWf5hmLfKzc6PMEzcxUCKkVYK + example: 1111111Bs8Haw3nAsWf5hmLfKzc6PMEzcxUCKkVYK diff --git a/src/openapi/specs/getMultipleCompressedAccounts.yaml b/src/openapi/specs/getMultipleCompressedAccounts.yaml index 4c79c436..a41ece1d 100644 --- a/src/openapi/specs/getMultipleCompressedAccounts.yaml +++ b/src/openapi/specs/getMultipleCompressedAccounts.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: @@ -172,12 +172,12 @@ components: Hash: type: string description: A 32-byte hash represented as a base58 string. - example: 11111117SQekjmcMtR25wEPPiL6m1Mb5586NkLL4X + example: 11111118F5rixNBnFLmioWZSYzjjFuAL5dyoDVzhD SerializablePubkey: type: string description: A Solana public key represented as a base58 string. - default: 11111117353mdUKehx9GW6JNHznGt5oSZs9fWkVkB - example: 11111117353mdUKehx9GW6JNHznGt5oSZs9fWkVkB + default: 11111117qkFjr4u54stuNNUR8fRF8dNhaP35yvANs + example: 11111117qkFjr4u54stuNNUR8fRF8dNhaP35yvANs UnsignedInteger: type: integer default: 100 diff --git a/src/openapi/specs/getMultipleNewAddressProofs.yaml b/src/openapi/specs/getMultipleNewAddressProofs.yaml index 01455238..9afb6096 100644 --- a/src/openapi/specs/getMultipleNewAddressProofs.yaml +++ b/src/openapi/specs/getMultipleNewAddressProofs.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: @@ -91,7 +91,7 @@ components: Hash: type: string description: A 32-byte hash represented as a base58 string. - example: 1111111CGTta3M4t3yXu8uRgkKvaWd2d8DQuZLKrf + example: 1111111D596YFweJQuHY1BbjazZYmAbt8jJL2VzVM MerkleContextWithNewAddressProof: type: object required: @@ -135,5 +135,5 @@ components: SerializablePubkey: type: string description: A Solana public key represented as a base58 string. - default: 1111111Bs8Haw3nAsWf5hmLfKzc6PMEzcxUCKkVYK - example: 1111111Bs8Haw3nAsWf5hmLfKzc6PMEzcxUCKkVYK + default: 1111111CfoVZ9eMbESQia3WiAfF4dtpFdUMcnvAB1 + example: 1111111CfoVZ9eMbESQia3WiAfF4dtpFdUMcnvAB1 diff --git a/src/openapi/specs/getMultipleNewAddressProofsV2.yaml b/src/openapi/specs/getMultipleNewAddressProofsV2.yaml index 4107abbc..075926ef 100644 --- a/src/openapi/specs/getMultipleNewAddressProofsV2.yaml +++ b/src/openapi/specs/getMultipleNewAddressProofsV2.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: @@ -102,7 +102,7 @@ components: Hash: type: string description: A 32-byte hash represented as a base58 string. - example: 1111111D596YFweJQuHY1BbjazZYmAbt8jJL2VzVM + example: 1111111DspJWUYDimq3AsTmnRfCX1iB99FBkVff83 MerkleContextWithNewAddressProof: type: object required: @@ -146,5 +146,5 @@ components: SerializablePubkey: type: string description: A Solana public key represented as a base58 string. - default: 1111111CfoVZ9eMbESQia3WiAfF4dtpFdUMcnvAB1 - example: 1111111CfoVZ9eMbESQia3WiAfF4dtpFdUMcnvAB1 + default: 1111111DUUhXNEw1bNAMSKgm1Kt2tSPWdzF3G5poh + example: 1111111DUUhXNEw1bNAMSKgm1Kt2tSPWdzF3G5poh diff --git a/src/openapi/specs/getTransactionWithCompressionInfo.yaml b/src/openapi/specs/getTransactionWithCompressionInfo.yaml index 710bfd37..8e84fdb1 100644 --- a/src/openapi/specs/getTransactionWithCompressionInfo.yaml +++ b/src/openapi/specs/getTransactionWithCompressionInfo.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: @@ -160,12 +160,12 @@ components: Hash: type: string description: A 32-byte hash represented as a base58 string. - example: 11111119rSGfPZLcyCGzY4uYEL1fkzJr6fke9qKxb + example: 1111111Af7Udc9v3L82dQM5b4zee1Xt77Be4czzbH SerializablePubkey: type: string description: A Solana public key represented as a base58 string. - default: 11111119T6fgHG3unjQB6vpWozhBdiXDbQovvFVeF - example: 11111119T6fgHG3unjQB6vpWozhBdiXDbQovvFVeF + default: 1111111AFmseVrdL9f9oyCzZefL9tG6UbvhMPRAGw + example: 1111111AFmseVrdL9f9oyCzZefL9tG6UbvhMPRAGw SerializableSignature: type: string description: A Solana transaction signature. diff --git a/src/openapi/specs/getValidityProof.yaml b/src/openapi/specs/getValidityProof.yaml index e50f1738..5c28b6da 100644 --- a/src/openapi/specs/getValidityProof.yaml +++ b/src/openapi/specs/getValidityProof.yaml @@ -4,7 +4,7 @@ info: description: Solana indexer for general compression license: name: Apache-2.0 - version: 0.44.0 + version: 0.45.0 servers: - url: https://devnet.helius-rpc.com?api-key= paths: @@ -165,9 +165,9 @@ components: Hash: type: string description: A 32-byte hash represented as a base58 string. - example: 1111111DspJWUYDimq3AsTmnRfCX1iB99FBkVff83 + example: 1111111EgVWUh8o98knojjwqGKqVGFkQ9m5AxqKkj SerializablePubkey: type: string description: A Solana public key represented as a base58 string. - default: 1111111DUUhXNEw1bNAMSKgm1Kt2tSPWdzF3G5poh - example: 1111111DUUhXNEw1bNAMSKgm1Kt2tSPWdzF3G5poh + default: 1111111EH9uVaqWRxHuzJbroqzX18yxmeW8TjFVSP + example: 1111111EH9uVaqWRxHuzJbroqzX18yxmeW8TjFVSP diff --git a/tests/integration_tests/mock_tests.rs b/tests/integration_tests/mock_tests.rs index e34e3de8..24f53a1d 100644 --- a/tests/integration_tests/mock_tests.rs +++ b/tests/integration_tests/mock_tests.rs @@ -325,6 +325,8 @@ async fn test_multiple_accounts( async fn test_persist_token_data( #[values(DatabaseBackend::Sqlite, DatabaseBackend::Postgres)] db_backend: DatabaseBackend, ) { + use photon_indexer::api::method::get_compressed_mint_token_holders::GetCompressedMintTokenHoldersRequest; + let name = trim_test_name(function_name!()); let setup = setup(name, db_backend).await; let mint1 = SerializablePubkey::new_unique(); @@ -375,7 +377,26 @@ async fn test_persist_token_data( state: AccountState::frozen, tlv: None, }; - let all_token_data = vec![token_data1, token_data2, token_data3]; + let token_data4 = TokenData { + mint: mint1, + owner: owner2, + amount: UnsignedInteger(4), + delegate: Some(delegate1), + state: AccountState::frozen, + tlv: None, + }; + let all_token_data = vec![token_data1, token_data2, token_data3, token_data4]; + + let mut mint_to_owner_to_balance = HashMap::new(); + for token_data in all_token_data.clone() { + let mint = token_data.mint; + let owner = token_data.owner; + let mint_owner_balances = mint_to_owner_to_balance + .entry(mint) + .or_insert(HashMap::new()); + let balance = mint_owner_balances.entry(owner).or_insert(0); + *balance += token_data.amount.0; + } let txn = sea_orm::TransactionTrait::begin(setup.db_conn.as_ref()) .await @@ -429,7 +450,7 @@ async fn test_persist_token_data( .value; verify_responses_match_tlv_data(res.clone(), owner_tlv); - for owner in [owner1, owner2] { + for owner in [owner2] { let owner_tlv = all_token_data .iter() .filter(|x| x.owner == owner) @@ -546,6 +567,35 @@ async fn test_persist_token_data( assert_eq!(paginated_res, res.items); verify_responses_match_tlv_data(res, delegate_tlv) } + + for (mint, owner_to_balance) in mint_to_owner_to_balance.iter() { + let mut items = Vec::new(); + + let mut cursor: Option = None; + loop { + let res = setup + .api + .get_compressed_mint_token_holders(GetCompressedMintTokenHoldersRequest { + mint: mint.clone(), + limit: Some(photon_indexer::api::method::utils::Limit::new(100).unwrap()), + cursor, + }) + .await + .unwrap() + .value; + for item in res.items.clone() { + items.push(item); + } + cursor = res.cursor; + if cursor.is_none() { + break; + } + } + assert_eq!(items.len(), owner_to_balance.len()); + for item in items { + assert_eq!(item.balance.0, *owner_to_balance.get(&item.owner).unwrap()); + } + } } #[tokio::test]