diff --git a/src/api/api.rs b/src/api/api.rs index fa4fc36..09d216a 100644 --- a/src/api/api.rs +++ b/src/api/api.rs @@ -13,7 +13,7 @@ use super::method::get_compressed_balance_by_owner::{ get_compressed_balance_by_owner, GetCompressedBalanceByOwnerRequest, }; use super::method::get_compressed_token_balances_by_owner::{ - get_compressed_token_balances_by_owner, GetCompressedTokenBalancesByOwner, + get_compressed_token_balances_by_owner, GetCompressedTokenBalancesByOwnerRequest, TokenBalancesResponse, }; use super::method::get_compression_signatures_for_account::{ @@ -168,7 +168,7 @@ impl PhotonApi { pub async fn get_compressed_token_balances_by_owner( &self, - request: GetCompressedTokenBalancesByOwner, + request: GetCompressedTokenBalancesByOwnerRequest, ) -> Result { get_compressed_token_balances_by_owner(&self.db_conn, request).await } @@ -298,7 +298,7 @@ impl PhotonApi { }, OpenApiSpec { name: "getCompressedTokenBalancesByOwner".to_string(), - request: Some(GetCompressedTokenBalancesByOwner::schema().1), + request: Some(GetCompressedTokenBalancesByOwnerRequest::schema().1), response: TokenBalancesResponse::schema().1, }, OpenApiSpec { diff --git a/src/api/method/get_compressed_balance_by_owner.rs b/src/api/method/get_compressed_balance_by_owner.rs index 068a17b..372016b 100644 --- a/src/api/method/get_compressed_balance_by_owner.rs +++ b/src/api/method/get_compressed_balance_by_owner.rs @@ -1,6 +1,6 @@ use crate::common::typedefs::serializable_pubkey::SerializablePubkey; use crate::common::typedefs::unsigned_integer::UnsignedInteger; -use crate::dao::generated::accounts; +use crate::dao::generated::owner_balances; use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -22,14 +22,10 @@ pub async fn get_compressed_balance_by_owner( let context = Context::extract(conn).await?; let owner = request.owner; - let balances = accounts::Entity::find() + let balances = owner_balances::Entity::find() .select_only() - .column(accounts::Column::Lamports) - .filter( - accounts::Column::Owner - .eq::>(owner.into()) - .and(accounts::Column::Spent.eq(false)), - ) + .column(owner_balances::Column::Lamports) + .filter(owner_balances::Column::Owner.eq::>(owner.into())) .into_model::() .all(conn) .await? @@ -38,6 +34,12 @@ pub async fn get_compressed_balance_by_owner( .collect::, PhotonApiError>>()?; let total_balance = balances.iter().sum::(); + if total_balance == 0 { + return Err(PhotonApiError::RecordNotFound(format!( + "No balance found for owner: {}", + owner + ))); + } Ok(AccountBalanceResponse { value: UnsignedInteger(total_balance), 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 d895fc0..47b818a 100644 --- a/src/api/method/get_compressed_token_balances_by_owner.rs +++ b/src/api/method/get_compressed_token_balances_by_owner.rs @@ -31,7 +31,7 @@ pub struct TokenBalancesResponse { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, Default)] -pub struct GetCompressedTokenBalancesByOwner { +pub struct GetCompressedTokenBalancesByOwnerRequest { pub owner: SerializablePubkey, pub mint: Option, pub cursor: Option, @@ -40,9 +40,9 @@ pub struct GetCompressedTokenBalancesByOwner { pub async fn get_compressed_token_balances_by_owner( conn: &DatabaseConnection, - request: GetCompressedTokenBalancesByOwner, + request: GetCompressedTokenBalancesByOwnerRequest, ) -> Result { - let GetCompressedTokenBalancesByOwner { + let GetCompressedTokenBalancesByOwnerRequest { owner, mint, cursor, diff --git a/src/dao/generated/accounts.rs b/src/dao/generated/accounts.rs index dd7a7ec..5379739 100644 --- a/src/dao/generated/accounts.rs +++ b/src/dao/generated/accounts.rs @@ -16,6 +16,7 @@ pub struct Model { pub seq: Option, pub slot_updated: i64, pub spent: bool, + pub prev_spent: Option, #[sea_orm(column_type = "Decimal(Some((20, 0)))")] pub lamports: Decimal, #[sea_orm(column_type = "Decimal(Some((20, 0)))", nullable)] diff --git a/src/dao/generated/mod.rs b/src/dao/generated/mod.rs index 881f93a..5589779 100644 --- a/src/dao/generated/mod.rs +++ b/src/dao/generated/mod.rs @@ -5,6 +5,8 @@ pub mod prelude; pub mod account_transactions; pub mod accounts; pub mod blocks; +pub mod owner_balances; pub mod state_trees; pub mod token_accounts; +pub mod token_owner_balances; pub mod transactions; diff --git a/src/dao/generated/owner_balances.rs b/src/dao/generated/owner_balances.rs new file mode 100644 index 0000000..1ec794e --- /dev/null +++ b/src/dao/generated/owner_balances.rs @@ -0,0 +1,17 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "owner_balances")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub owner: Vec, + #[sea_orm(column_type = "Decimal(Some((20, 0)))")] + pub lamports: Decimal, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/dao/generated/prelude.rs b/src/dao/generated/prelude.rs index f076062..bf728a4 100644 --- a/src/dao/generated/prelude.rs +++ b/src/dao/generated/prelude.rs @@ -3,6 +3,8 @@ pub use super::account_transactions::Entity as AccountTransactions; pub use super::accounts::Entity as Accounts; pub use super::blocks::Entity as Blocks; +pub use super::owner_balances::Entity as OwnerBalances; pub use super::state_trees::Entity as StateTrees; pub use super::token_accounts::Entity as TokenAccounts; +pub use super::token_owner_balances::Entity as TokenOwnerBalances; pub use super::transactions::Entity as Transactions; diff --git a/src/dao/generated/token_accounts.rs b/src/dao/generated/token_accounts.rs index 419eefc..a3605b5 100644 --- a/src/dao/generated/token_accounts.rs +++ b/src/dao/generated/token_accounts.rs @@ -12,6 +12,7 @@ pub struct Model { pub delegate: Option>, pub state: i32, pub spent: bool, + pub prev_spent: Option, #[sea_orm(column_type = "Decimal(Some((20, 0)))")] pub amount: Decimal, #[sea_orm(column_type = "Decimal(Some((20, 0)))", nullable)] diff --git a/src/dao/generated/token_owner_balances.rs b/src/dao/generated/token_owner_balances.rs new file mode 100644 index 0000000..1a4c076 --- /dev/null +++ b/src/dao/generated/token_owner_balances.rs @@ -0,0 +1,19 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "token_owner_balances")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub owner: Vec, + #[sea_orm(primary_key, auto_increment = false)] + pub mint: Vec, + #[sea_orm(column_type = "Decimal(Some((20, 0)))")] + pub amount: Decimal, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/ingester/persist/mod.rs b/src/ingester/persist/mod.rs index 73e3094..04976b1 100644 --- a/src/ingester/persist/mod.rs +++ b/src/ingester/persist/mod.rs @@ -1,5 +1,3 @@ -use std::collections::HashSet; - use super::{ error, parser::state_update::{AccountTransaction, EnrichedPathNode}, @@ -16,8 +14,10 @@ use crate::{ use borsh::BorshDeserialize; use log::debug; use sea_orm::{ - sea_query::OnConflict, ConnectionTrait, DatabaseTransaction, EntityTrait, QueryTrait, Set, + sea_query::OnConflict, ConnectionTrait, DatabaseBackend, DatabaseTransaction, EntityTrait, + QueryTrait, Set, Statement, }; +use std::collections::{HashMap, HashSet}; use error::IngesterError; use solana_program::pubkey; @@ -117,7 +117,7 @@ async fn spend_input_accounts( data: Set(None), owner: Set(account.owner.0.to_bytes().to_vec()), discriminator: Set(None), - lamports: Set(Decimal::from(0)), + lamports: Set(Decimal::from(account.lamports.0)), slot_updated: Set(account.slot_updated.0 as i64), tree: Set(account.tree.0.to_bytes().to_vec()), leaf_index: Set(account.leaf_index.0 as i64), @@ -125,23 +125,29 @@ async fn spend_input_accounts( }) .collect(); - if !in_account_models.is_empty() { - accounts::Entity::insert_many(in_account_models) - .on_conflict( - OnConflict::column(accounts::Column::Hash) - .update_columns([ - accounts::Column::Hash, - accounts::Column::Data, - accounts::Column::Lamports, - accounts::Column::Spent, - accounts::Column::SlotUpdated, - accounts::Column::Tree, - ]) - .to_owned(), - ) - .exec(txn) - .await?; - } + let query = accounts::Entity::insert_many(in_account_models) + .on_conflict( + OnConflict::column(accounts::Column::Hash) + .update_columns([ + accounts::Column::Hash, + accounts::Column::Data, + accounts::Column::Lamports, + accounts::Column::Spent, + accounts::Column::SlotUpdated, + accounts::Column::Tree, + ]) + .to_owned(), + ) + .build(txn.get_database_backend()); + + execute_account_update_query_and_update_balances( + txn, + query, + AccountType::Account, + ModificationType::Spend, + ) + .await?; + let mut token_models = Vec::new(); for in_accounts in in_accounts { let token_data = parse_token_data(in_accounts)?; @@ -149,7 +155,7 @@ async fn spend_input_accounts( token_models.push(token_accounts::ActiveModel { hash: Set(in_accounts.hash.to_vec()), spent: Set(true), - amount: Set(Decimal::from(0)), + amount: Set(Decimal::from(token_data.amount.0)), owner: Set(token_data.owner.to_bytes_vec()), mint: Set(token_data.mint.to_bytes_vec()), state: Set(token_data.state as i32), @@ -160,7 +166,7 @@ async fn spend_input_accounts( } if !token_models.is_empty() { debug!("Marking {} token accounts as spent...", token_models.len()); - token_accounts::Entity::insert_many(token_models) + let query = token_accounts::Entity::insert_many(token_models) .on_conflict( OnConflict::column(token_accounts::Column::Hash) .update_columns([ @@ -170,8 +176,14 @@ async fn spend_input_accounts( ]) .to_owned(), ) - .exec(txn) - .await?; + .build(txn.get_database_backend()); + execute_account_update_query_and_update_balances( + txn, + query, + AccountType::TokenAccount, + ModificationType::Spend, + ) + .await?; } Ok(()) @@ -182,6 +194,130 @@ pub struct EnrichedTokenAccount { pub hash: Hash, } +#[derive(Debug)] +enum AccountType { + Account, + TokenAccount, +} + +#[derive(Debug)] +enum ModificationType { + Append, + Spend, +} + +fn bytes_to_sql_format(database_backend: DatabaseBackend, bytes: Vec) -> String { + match database_backend { + DatabaseBackend::Postgres => bytes_to_postgres_sql_format(bytes), + DatabaseBackend::Sqlite => bytes_to_sqlite_sql_format(bytes), + _ => panic!("Unsupported database backend"), + } +} + +fn bytes_to_postgres_sql_format(bytes: Vec) -> String { + let hex_string = bytes + .iter() + .map(|byte| format!("{:02x}", byte)) + .collect::(); + format!("'\\x{}'", hex_string) // Properly formatted for PostgreSQL BYTEA +} + +fn bytes_to_sqlite_sql_format(bytes: Vec) -> String { + let hex_string = bytes + .iter() + .map(|byte| format!("{:02x}", byte)) + .collect::(); + format!("X'{}'", hex_string) // Properly formatted for SQLite BLOB +} + +async fn execute_account_update_query_and_update_balances( + txn: &DatabaseTransaction, + mut query: Statement, + account_type: AccountType, + modification_type: ModificationType, +) -> Result<(), IngesterError> { + let (original_table_name, owner_table_name, balance_column, additional_columns) = + match account_type { + AccountType::Account => ("accounts", "owner_balances", "lamports", ""), + AccountType::TokenAccount => { + ("token_accounts", "token_owner_balances", "amount", ", mint") + } + }; + let prev_spent_set = match modification_type { + ModificationType::Append => "".to_string(), + ModificationType::Spend => { + format!(", \"prev_spent\" = \"{original_table_name}\".\"spent\"") + } + }; + query.sql = format!( + "{} {} RETURNING owner,prev_spent,{}{}", + query.sql, prev_spent_set, balance_column, additional_columns + ); + let result = txn.query_all(query).await.map_err(|e| { + IngesterError::DatabaseError(format!( + "Got error appending {:?} accounts {}", + account_type, + e.to_string() + )) + })?; + let multiplier = Decimal::from(match &modification_type { + ModificationType::Append => 1, + ModificationType::Spend => -1, + }); + let mut balance_modifications = HashMap::new(); + let db_backend = txn.get_database_backend(); + for row in result { + let prev_spent: Option = row.try_get("", "prev_spent")?; + match (prev_spent, &modification_type) { + (_, ModificationType::Append) | (Some(false), ModificationType::Spend) => { + let mut amount_of_interest = match db_backend { + DatabaseBackend::Postgres => row.try_get("", balance_column)?, + DatabaseBackend::Sqlite => { + let amount: i64 = row.try_get("", balance_column)?; + Decimal::from(amount) + } + _ => panic!("Unsupported database backend"), + }; + amount_of_interest *= multiplier; + let owner = bytes_to_sql_format(db_backend, row.try_get("", "owner")?); + let key = match account_type { + AccountType::Account => owner, + AccountType::TokenAccount => { + format!( + "{},{}", + owner, + bytes_to_sql_format(db_backend, row.try_get("", "mint")?) + ) + } + }; + balance_modifications + .entry(key) + .and_modify(|amount| *amount += amount_of_interest) + .or_insert(amount_of_interest); + } + _ => {} + } + } + let values = balance_modifications + .into_iter() + .filter(|(_, value)| *value != Decimal::from(0)) + .map(|(key, value)| format!("({}, {})", key, value)) + .collect::>(); + + if values.len() > 0 { + let values_string = values.join(", "); + let raw_sql = format!( + "INSERT INTO {owner_table_name} (owner {additional_columns}, {balance_column}) + VALUES {values_string} ON CONFLICT (owner{additional_columns}) + DO UPDATE SET {balance_column} = {owner_table_name}.{balance_column} + excluded.{balance_column}", + ); + txn.execute(Statement::from_string(db_backend, raw_sql)) + .await?; + } + + Ok(()) +} + async fn append_output_accounts( txn: &DatabaseTransaction, out_accounts: &[Account], @@ -206,6 +342,7 @@ async fn append_output_accounts( spent: Set(false), slot_updated: Set(account.slot_updated.0 as i64), seq: Set(account.seq.map(|s| s.0 as i64)), + prev_spent: Set(None), }); if let Some(token_data) = parse_token_data(account)? { @@ -216,12 +353,6 @@ async fn append_output_accounts( } } - // The state tree is append-only so conflicts only occur if a record is already inserted or - // marked as spent spent. - // - // We first build the query and then execute it because SeaORM has a bug where it always throws - // an error if we do not insert a record in an insert statement. However, in this case, it's - // expected not to insert anything if the key already exists. if !out_accounts.is_empty() { let query = accounts::Entity::insert_many(account_models) .on_conflict( @@ -230,7 +361,14 @@ async fn append_output_accounts( .to_owned(), ) .build(txn.get_database_backend()); - txn.execute(query).await?; + execute_account_update_query_and_update_balances( + txn, + query, + AccountType::Account, + ModificationType::Append, + ) + .await?; + if !token_accounts.is_empty() { debug!("Persisting {} token accounts...", token_accounts.len()); persist_token_accounts(txn, token_accounts).await?; @@ -257,13 +395,11 @@ pub async fn persist_token_accounts( delegated_amount: Set(Decimal::from(token_data.delegated_amount.0)), is_native: Set(token_data.is_native.map(|x| Decimal::from(x.0))), spent: Set(false), + prev_spent: Set(None), }, ) .collect::>(); - // We first build the query and then execute it because SeaORM has a bug where it always throws - // an error if we do not insert a record in an insert statement. However, in this case, it's - // expected not to insert anything if the key already exists. let query = token_accounts::Entity::insert_many(token_models) .on_conflict( OnConflict::column(token_accounts::Column::Hash) @@ -271,7 +407,14 @@ pub async fn persist_token_accounts( .to_owned(), ) .build(txn.get_database_backend()); - txn.execute(query).await?; + + execute_account_update_query_and_update_balances( + txn, + query, + AccountType::TokenAccount, + ModificationType::Append, + ) + .await?; Ok(()) } diff --git a/src/migration/m20220101_000001_init.rs b/src/migration/m20220101_000001_init.rs index 9e71ed6..4826a27 100644 --- a/src/migration/m20220101_000001_init.rs +++ b/src/migration/m20220101_000001_init.rs @@ -5,7 +5,9 @@ use sea_orm_migration::{ use crate::migration::model::table::{Accounts, StateTrees, TokenAccounts}; -use super::model::table::{AccountTransactions, Blocks, Transactions}; +use super::model::table::{ + AccountTransactions, Blocks, OwnerBalances, TokenOwnerBalances, Transactions, +}; #[derive(DeriveMigrationName)] pub struct Migration; @@ -21,7 +23,6 @@ async fn execute_sql<'a>(manager: &SchemaManager<'_>, sql: &str) -> Result<(), D Ok(()) } -// TODO: Consider adding composite primary keys instead of autogenerated primary keys. #[async_trait::async_trait] impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { @@ -31,14 +32,15 @@ impl MigrationTrait for Migration { " DO $$ DECLARE - type_exists BOOLEAN := EXISTS (SELECT 1 FROM pg_type WHERE typname = 'uint64_t'); + type_exists BOOLEAN := EXISTS (SELECT 1 FROM pg_type WHERE typname = 'bigint2'); BEGIN IF NOT type_exists THEN - CREATE DOMAIN uint64_t AS numeric(20, 0) CHECK (VALUE >= 0); + CREATE DOMAIN bigint2 AS numeric(20, 0); END IF; END $$; ", - ).await?; + ) + .await?; } manager @@ -105,6 +107,7 @@ impl MigrationTrait for Migration { .not_null(), ) .col(ColumnDef::new(Accounts::Spent).boolean().not_null()) + .col(ColumnDef::new(Accounts::PrevSpent).boolean()) .primary_key(Index::create().name("pk_accounts").col(Accounts::Hash)) .to_owned(), ) @@ -147,6 +150,7 @@ impl MigrationTrait for Migration { // support enums. .col(ColumnDef::new(TokenAccounts::State).integer().not_null()) .col(ColumnDef::new(TokenAccounts::Spent).boolean().not_null()) + .col(ColumnDef::new(TokenAccounts::PrevSpent).boolean()) .foreign_key( ForeignKey::create() .name("token_accounts_hash_fk") @@ -163,35 +167,83 @@ impl MigrationTrait for Migration { ) .await?; + manager + .create_table( + Table::create() + .table(OwnerBalances::Table) + .if_not_exists() + .col(ColumnDef::new(OwnerBalances::Owner).binary().not_null()) + .primary_key( + Index::create() + .name("pk_owner_balances") + .col(OwnerBalances::Owner), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(TokenOwnerBalances::Table) + .if_not_exists() + .col( + ColumnDef::new(TokenOwnerBalances::Owner) + .binary() + .not_null(), + ) + .col(ColumnDef::new(TokenOwnerBalances::Mint).binary().not_null()) + .primary_key( + Index::create() + .name("pk_token_owner_balances") + .col(TokenOwnerBalances::Owner) + .col(TokenOwnerBalances::Mint), + ) + .to_owned(), + ) + .await?; + match manager.get_database_backend() { DatabaseBackend::Postgres => { execute_sql( manager, - "ALTER TABLE accounts ADD COLUMN lamports uint64_t NOT NULL;", + "ALTER TABLE accounts ADD COLUMN lamports bigint2 NOT NULL;", + ) + .await?; + + execute_sql( + manager, + "ALTER TABLE accounts ADD COLUMN discriminator bigint2;", + ) + .await?; + + execute_sql( + manager, + "ALTER TABLE token_accounts ADD COLUMN amount bigint2 NOT NULL;", ) .await?; execute_sql( manager, - "ALTER TABLE accounts ADD COLUMN discriminator uint64_t;", + "ALTER TABLE token_accounts ADD COLUMN is_native bigint2;", ) .await?; execute_sql( manager, - "ALTER TABLE token_accounts ADD COLUMN amount uint64_t NOT NULL;", + "ALTER TABLE token_accounts ADD COLUMN delegated_amount bigint2 NOT NULL;", ) .await?; execute_sql( manager, - "ALTER TABLE token_accounts ADD COLUMN is_native uint64_t;", + "ALTER TABLE owner_balances ADD COLUMN lamports bigint2 NOT NULL;", ) .await?; execute_sql( manager, - "ALTER TABLE token_accounts ADD COLUMN delegated_amount uint64_t NOT NULL;", + "ALTER TABLE token_owner_balances ADD COLUMN amount bigint2 NOT NULL;", ) .await?; } @@ -220,6 +272,18 @@ impl MigrationTrait for Migration { "ALTER TABLE token_accounts ADD COLUMN delegated_amount REAL;", ) .await?; + + execute_sql( + manager, + "ALTER TABLE owner_balances ADD COLUMN lamports REAL;", + ) + .await?; + + execute_sql( + manager, + "ALTER TABLE token_owner_balances ADD COLUMN amount REAL;", + ) + .await?; } _ => { unimplemented!("Unsupported database type") diff --git a/src/migration/model/table.rs b/src/migration/model/table.rs index ded4c86..c03aa0e 100644 --- a/src/migration/model/table.rs +++ b/src/migration/model/table.rs @@ -23,6 +23,7 @@ pub enum Accounts { Tree, LeafIndex, Spent, + PrevSpent, Seq, SlotUpdated, } @@ -36,6 +37,7 @@ pub enum TokenAccounts { Delegate, State, Spent, + PrevSpent, } #[derive(Copy, Clone, Iden)] @@ -62,3 +64,16 @@ pub enum AccountTransactions { Hash, Signature, } + +#[derive(Copy, Clone, Iden)] +pub enum OwnerBalances { + Table, + Owner, +} + +#[derive(Copy, Clone, Iden)] +pub enum TokenOwnerBalances { + Table, + Owner, + Mint, +} diff --git a/tests/integration_tests/e2e_tests.rs b/tests/integration_tests/e2e_tests.rs index 4ac2eac..b9265d9 100644 --- a/tests/integration_tests/e2e_tests.rs +++ b/tests/integration_tests/e2e_tests.rs @@ -127,9 +127,22 @@ async fn test_e2e_mint_and_transfer( format!("{}-{}-token-signatures", name.clone(), person), signatures ); + + let token_balances = setup + .api + .get_compressed_token_balances_by_owner(photon_indexer::api::method::get_compressed_token_balances_by_owner::GetCompressedTokenBalancesByOwnerRequest { + owner: pubkey.clone(), + ..Default::default() + }) + .await + .unwrap(); + + assert_json_snapshot!( + format!("{}-{}-token-balances", name.clone(), person), + token_balances + ); } } - for (txn_name, txn_signature) in [("mint", mint_tx), ("transfer", transfer_tx)] { let txn = cached_fetch_transaction(&setup, txn_signature).await; let txn_signature = SerializableSignature(Signature::from_str(txn_signature).unwrap()); @@ -357,38 +370,42 @@ async fn test_debug_incorrect_root( Pubkey::try_from("4Vuk7ucQkkKbbF9mr7FoAq3tf5KbPsm1y436zg368L9U").unwrap(), ); let txs = [compress_tx, transfer_tx]; + let number_of_indexes = [1, 2]; for tx_permutation in txs.iter().permutations(txs.len()) { for individually in [true, false] { reset_tables(&setup.db_conn).await.unwrap(); - - // HACK: We index a block so that API methods can fetch the current slot. - index_block( - &setup.db_conn, - &BlockInfo { - metadata: BlockMetadata { - slot: 0, - ..Default::default() - }, - ..Default::default() - }, - ) - .await - .unwrap(); - - match individually { - true => { - for tx in tx_permutation.iter() { - index_transaction(&setup, tx).await; - } - } - false => { - index_multiple_transactions( - &setup, - #[allow(suspicious_double_ref_op)] - tx_permutation.iter().map(|x| *x.clone()).collect(), + for number in number_of_indexes.iter() { + for _ in 0..*number { + // HACK: We index a block so that API methods can fetch the current slot. + index_block( + &setup.db_conn, + &BlockInfo { + metadata: BlockMetadata { + slot: 0, + ..Default::default() + }, + ..Default::default() + }, ) - .await; + .await + .unwrap(); + + match individually { + true => { + for tx in tx_permutation.iter() { + index_transaction(&setup, tx).await; + } + } + false => { + index_multiple_transactions( + &setup, + #[allow(suspicious_double_ref_op)] + tx_permutation.iter().map(|x| *x.clone()).collect(), + ) + .await; + } + } } } let accounts = setup @@ -423,6 +440,24 @@ async fn test_debug_incorrect_root( .await .unwrap(); assert_json_snapshot!(format!("{}-signatures", name.clone()), signatures); + + let balance = setup + .api + .get_compressed_balance_by_owner(photon_indexer::api::method::get_compressed_balance_by_owner::GetCompressedBalanceByOwnerRequest { + owner: payer_pubkey.clone(), + ..Default::default() + }) + .await + .unwrap(); + assert_eq!( + accounts + .value + .items + .iter() + .map(|x| x.lamports.0) + .sum::(), + balance.value.0, + ); } } } diff --git a/tests/integration_tests/mock_tests.rs b/tests/integration_tests/mock_tests.rs index 5e4d2fb..6e3ccd8 100644 --- a/tests/integration_tests/mock_tests.rs +++ b/tests/integration_tests/mock_tests.rs @@ -4,7 +4,7 @@ use function_name::named; use photon_indexer::api::error::PhotonApiError; use photon_indexer::api::method::get_compressed_accounts_by_owner::GetCompressedAccountsByOwnerRequest; use photon_indexer::api::method::get_compressed_balance_by_owner::GetCompressedBalanceByOwnerRequest; -use photon_indexer::api::method::get_compressed_token_balances_by_owner::GetCompressedTokenBalancesByOwner; +use photon_indexer::api::method::get_compressed_token_balances_by_owner::GetCompressedTokenBalancesByOwnerRequest; use photon_indexer::api::method::get_multiple_compressed_accounts::GetMultipleCompressedAccountsRequest; use photon_indexer::api::method::utils::{ CompressedAccountRequest, GetCompressedTokenAccountsByDelegate, @@ -472,7 +472,7 @@ async fn test_persist_token_data( *balance += token_account.token_data.amount.0; } for (mint, balance) in mint_to_balance.iter() { - let request = GetCompressedTokenBalancesByOwner { + let request = GetCompressedTokenBalancesByOwnerRequest { owner: SerializablePubkey::from(owner), mint: Some(mint.clone()), ..Default::default() diff --git a/tests/integration_tests/snapshots/integration_tests__e2e_tests__e2e_mint_and_transfer-bob-token-balances.snap b/tests/integration_tests/snapshots/integration_tests__e2e_tests__e2e_mint_and_transfer-bob-token-balances.snap new file mode 100644 index 0000000..f1d60d9 --- /dev/null +++ b/tests/integration_tests/snapshots/integration_tests__e2e_tests__e2e_mint_and_transfer-bob-token-balances.snap @@ -0,0 +1,18 @@ +--- +source: tests/integration_tests/e2e_tests.rs +expression: token_balances +--- +{ + "context": { + "slot": 0 + }, + "value": { + "token_balances": [ + { + "mint": "6VjSatiqNLk7j52hfQmRY7vari3FLanqZBgE8AY7ocQs", + "balance": 300 + } + ], + "cursor": null + } +} diff --git a/tests/integration_tests/snapshots/integration_tests__e2e_tests__e2e_mint_and_transfer-charles-token-balances.snap b/tests/integration_tests/snapshots/integration_tests__e2e_tests__e2e_mint_and_transfer-charles-token-balances.snap new file mode 100644 index 0000000..fa25171 --- /dev/null +++ b/tests/integration_tests/snapshots/integration_tests__e2e_tests__e2e_mint_and_transfer-charles-token-balances.snap @@ -0,0 +1,18 @@ +--- +source: tests/integration_tests/e2e_tests.rs +expression: token_balances +--- +{ + "context": { + "slot": 0 + }, + "value": { + "token_balances": [ + { + "mint": "6VjSatiqNLk7j52hfQmRY7vari3FLanqZBgE8AY7ocQs", + "balance": 700 + } + ], + "cursor": null + } +} diff --git a/tests/integration_tests/snapshots/integration_tests__e2e_tests__e2e_mint_and_transfer-original_owner-accounts.snap.new b/tests/integration_tests/snapshots/integration_tests__e2e_tests__e2e_mint_and_transfer-original_owner-accounts.snap.new new file mode 100644 index 0000000..fe9f2db --- /dev/null +++ b/tests/integration_tests/snapshots/integration_tests__e2e_tests__e2e_mint_and_transfer-original_owner-accounts.snap.new @@ -0,0 +1,41 @@ +--- +source: tests/integration_tests/e2e_tests.rs +assertion_line: 92 +expression: accounts +--- +{ + "context": { + "slot": 0 + }, + "value": { + "items": [ + { + "account": { + "hash": "oZ7nqSySeoVcM3ZF6KjSE3Q4wKj9NTWVA3dF4akhych", + "address": null, + "data": { + "discriminator": 2, + "data": "Uaavg4642BlU0qzdHNvTw5bjJPaj3sslz6Mzu1JUXgRrecV+aglSOSgsBIGOlhEvPwOkABupelZMI4UqPx6l/CwBAAAAAAAAAAEAAAAAAAAAAAA=", + "dataHash": "2oxvZ6Cr89GyYziwQeYhjwyCodJw4D8pr8aCd6AmymoT" + }, + "owner": "9sixVEthz2kMSKfeApZXHwuboT6DZuT6crAYJTciUCqE", + "lamports": 0, + "tree": "5bdFnXU47QjzGpzHfXnxcEi5WXyxzEAZzd1vrE39bf1W", + "leafIndex": 2, + "seq": 3, + "slotUpdated": 0 + }, + "tokenData": { + "mint": "6VjSatiqNLk7j52hfQmRY7vari3FLanqZBgE8AY7ocQs", + "owner": "8EYKVyNCsDFHkxos7V4kr8bMouYU2nPJ1QXk2ET8FBc7", + "amount": 300, + "delegate": null, + "state": "initialized", + "isNative": null, + "delegatedAmount": 0 + } + } + ], + "cursor": null + } +}