diff --git a/Cargo.lock b/Cargo.lock index 7e237a8d29..c81221eb15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4951,6 +4951,7 @@ dependencies = [ "prost", "serde", "serde_derive", + "solana-account-decoder", "solana-sdk", "solana-transaction-status", ] diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index e1338da9d1..4e0f91edee 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -38,8 +38,13 @@ use solana_sdk::{ timing::{duration_as_ms, timestamp}, transaction::{self, Transaction, TransactionError}, }; +use solana_transaction_status::token_balances::{ + collect_token_balances, TransactionTokenBalancesSet, +}; use std::{ - cmp, env, + cmp, + collections::HashMap, + env, net::UdpSocket, sync::atomic::AtomicBool, sync::mpsc::Receiver, @@ -530,6 +535,15 @@ impl BankingStage { } else { vec![] }; + + let mut mint_decimals: HashMap = HashMap::new(); + + let pre_token_balances = if transaction_status_sender.is_some() { + collect_token_balances(&bank, &batch, &mut mint_decimals) + } else { + vec![] + }; + let ( mut loaded_accounts, results, @@ -574,12 +588,14 @@ impl BankingStage { bank_utils::find_and_send_votes(txs, &tx_results, Some(gossip_vote_sender)); if let Some(sender) = transaction_status_sender { let post_balances = bank.collect_balances(batch); + let post_token_balances = collect_token_balances(&bank, &batch, &mut mint_decimals); send_transaction_status_batch( bank.clone(), batch.transactions(), batch.iteration_order_vec(), tx_results.execution_results, TransactionBalancesSet::new(pre_balances, post_balances), + TransactionTokenBalancesSet::new(pre_token_balances, post_token_balances), inner_instructions, transaction_logs, sender, diff --git a/core/src/transaction_status_service.rs b/core/src/transaction_status_service.rs index 3a71742daa..0a6115dc15 100644 --- a/core/src/transaction_status_service.rs +++ b/core/src/transaction_status_service.rs @@ -54,6 +54,7 @@ impl TransactionStatusService { iteration_order, statuses, balances, + token_balances, inner_instructions, transaction_logs, } = write_transaction_status_receiver.recv_timeout(Duration::from_secs(1))?; @@ -64,6 +65,8 @@ impl TransactionStatusService { (status, nonce_rollback), pre_balances, post_balances, + pre_token_balances, + post_token_balances, inner_instructions, log_messages, ) in izip!( @@ -71,6 +74,8 @@ impl TransactionStatusService { statuses, balances.pre_balances, balances.post_balances, + token_balances.pre_token_balances, + token_balances.post_token_balances, inner_instructions, transaction_logs ) { @@ -98,6 +103,8 @@ impl TransactionStatusService { }); let log_messages = Some(log_messages); + let pre_token_balances = Some(pre_token_balances); + let post_token_balances = Some(post_token_balances); blockstore .write_transaction_status( @@ -112,6 +119,8 @@ impl TransactionStatusService { post_balances, inner_instructions, log_messages, + pre_token_balances, + post_token_balances, }, ) .expect("Expect database write to succeed"); diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 6999ec0eae..c3b56f527b 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -5837,6 +5837,8 @@ pub mod tests { post_balances: post_balances.clone(), inner_instructions: Some(vec![]), log_messages: Some(vec![]), + pre_token_balances: Some(vec![]), + post_token_balances: Some(vec![]), }, ) .unwrap(); @@ -5851,6 +5853,8 @@ pub mod tests { post_balances: post_balances.clone(), inner_instructions: Some(vec![]), log_messages: Some(vec![]), + pre_token_balances: Some(vec![]), + post_token_balances: Some(vec![]), }, ) .unwrap(); @@ -5863,6 +5867,8 @@ pub mod tests { post_balances, inner_instructions: Some(vec![]), log_messages: Some(vec![]), + pre_token_balances: Some(vec![]), + post_token_balances: Some(vec![]), }), } }) @@ -6053,6 +6059,8 @@ pub mod tests { instructions: vec![CompiledInstruction::new(1, &(), vec![0])], }]; let log_messages_vec = vec![String::from("Test message\n")]; + let pre_token_balances_vec = vec![]; + let post_token_balances_vec = vec![]; // result not found assert!(transaction_status_cf @@ -6073,6 +6081,8 @@ pub mod tests { post_balances: post_balances_vec.clone(), inner_instructions: Some(inner_instructions_vec.clone()), log_messages: Some(log_messages_vec.clone()), + pre_token_balances: Some(pre_token_balances_vec.clone()), + post_token_balances: Some(post_token_balances_vec.clone()) }, ) .is_ok()); @@ -6085,6 +6095,8 @@ pub mod tests { post_balances, inner_instructions, log_messages, + pre_token_balances, + post_token_balances, } = transaction_status_cf .get((0, Signature::default(), 0)) .unwrap() @@ -6095,6 +6107,8 @@ pub mod tests { assert_eq!(post_balances, post_balances_vec); assert_eq!(inner_instructions.unwrap(), inner_instructions_vec); assert_eq!(log_messages.unwrap(), log_messages_vec); + assert_eq!(pre_token_balances.unwrap(), pre_token_balances_vec); + assert_eq!(post_token_balances.unwrap(), post_token_balances_vec); // insert value assert!(transaction_status_cf @@ -6107,6 +6121,8 @@ pub mod tests { post_balances: post_balances_vec.clone(), inner_instructions: Some(inner_instructions_vec.clone()), log_messages: Some(log_messages_vec.clone()), + pre_token_balances: Some(pre_token_balances_vec.clone()), + post_token_balances: Some(post_token_balances_vec.clone()) }, ) .is_ok()); @@ -6119,6 +6135,8 @@ pub mod tests { post_balances, inner_instructions, log_messages, + pre_token_balances, + post_token_balances, } = transaction_status_cf .get((0, Signature::new(&[2u8; 64]), 9)) .unwrap() @@ -6131,6 +6149,8 @@ pub mod tests { assert_eq!(post_balances, post_balances_vec); assert_eq!(inner_instructions.unwrap(), inner_instructions_vec); assert_eq!(log_messages.unwrap(), log_messages_vec); + assert_eq!(pre_token_balances.unwrap(), pre_token_balances_vec); + assert_eq!(post_token_balances.unwrap(), post_token_balances_vec); } Blockstore::destroy(&blockstore_path).expect("Expected successful database destruction"); } @@ -6359,6 +6379,8 @@ pub mod tests { post_balances: post_balances_vec, inner_instructions: Some(vec![]), log_messages: Some(vec![]), + pre_token_balances: Some(vec![]), + post_token_balances: Some(vec![]), }; let signature1 = Signature::new(&[1u8; 64]); @@ -6493,6 +6515,8 @@ pub mod tests { instructions: vec![CompiledInstruction::new(1, &(), vec![0])], }]); let log_messages = Some(vec![String::from("Test message\n")]); + let pre_token_balances = Some(vec![]); + let post_token_balances = Some(vec![]); let signature = transaction.signatures[0]; blockstore .transaction_status_cf @@ -6505,6 +6529,8 @@ pub mod tests { post_balances: post_balances.clone(), inner_instructions: inner_instructions.clone(), log_messages: log_messages.clone(), + pre_token_balances: pre_token_balances.clone(), + post_token_balances: post_token_balances.clone(), }, ) .unwrap(); @@ -6517,6 +6543,8 @@ pub mod tests { post_balances, inner_instructions, log_messages, + pre_token_balances, + post_token_balances, }), } }) @@ -6958,6 +6986,8 @@ pub mod tests { post_balances: vec![], inner_instructions: Some(vec![]), log_messages: Some(vec![]), + pre_token_balances: Some(vec![]), + post_token_balances: Some(vec![]), }, ) .unwrap(); diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index c5e55ea343..96d32a1e49 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -36,6 +36,10 @@ use solana_sdk::{ timing::duration_as_ms, transaction::{Result, Transaction, TransactionError}, }; +use solana_transaction_status::token_balances::{ + collect_token_balances, TransactionTokenBalancesSet, +}; + use std::{ cell::RefCell, collections::{HashMap, HashSet}, @@ -102,6 +106,16 @@ fn execute_batch( transaction_status_sender: Option, replay_vote_sender: Option<&ReplayVoteSender>, ) -> Result<()> { + let record_token_balances = transaction_status_sender.is_some(); + + let mut mint_decimals: HashMap = HashMap::new(); + + let pre_token_balances = if record_token_balances { + collect_token_balances(&bank, &batch, &mut mint_decimals) + } else { + vec![] + }; + let (tx_results, balances, inner_instructions, transaction_logs) = batch.bank().load_execute_and_commit_transactions( batch, @@ -120,12 +134,22 @@ fn execute_batch( } = tx_results; if let Some(sender) = transaction_status_sender { + let post_token_balances = if record_token_balances { + collect_token_balances(&bank, &batch, &mut mint_decimals) + } else { + vec![] + }; + + let token_balances = + TransactionTokenBalancesSet::new(pre_token_balances, post_token_balances); + send_transaction_status_batch( bank.clone(), batch.transactions(), batch.iteration_order_vec(), execution_results, balances, + token_balances, inner_instructions, transaction_logs, sender, @@ -1038,6 +1062,7 @@ pub struct TransactionStatusBatch { pub iteration_order: Option>, pub statuses: Vec, pub balances: TransactionBalancesSet, + pub token_balances: TransactionTokenBalancesSet, pub inner_instructions: Vec>, pub transaction_logs: Vec, } @@ -1050,6 +1075,7 @@ pub fn send_transaction_status_batch( iteration_order: Option>, statuses: Vec, balances: TransactionBalancesSet, + token_balances: TransactionTokenBalancesSet, inner_instructions: Vec>, transaction_logs: Vec, transaction_status_sender: TransactionStatusSender, @@ -1061,6 +1087,7 @@ pub fn send_transaction_status_batch( iteration_order, statuses, balances, + token_balances, inner_instructions, transaction_logs, }) { diff --git a/storage-bigtable/src/bigtable.rs b/storage-bigtable/src/bigtable.rs index a0ac3e7336..c774b2370a 100644 --- a/storage-bigtable/src/bigtable.rs +++ b/storage-bigtable/src/bigtable.rs @@ -656,6 +656,8 @@ mod tests { post_balances: vec![0, 42, 1], inner_instructions: Some(vec![]), log_messages: Some(vec![]), + pre_token_balances: Some(vec![]), + post_token_balances: Some(vec![]), }), }; let block = ConfirmedBlock { @@ -705,6 +707,8 @@ mod tests { if let Some(meta) = &mut block.transactions[0].meta { meta.inner_instructions = None; // Legacy bincode implementation does not support inner_instructions meta.log_messages = None; // Legacy bincode implementation does not support log_messages + meta.pre_token_balances = None; // Legacy bincode implementation does not support token balances + meta.post_token_balances = None; // Legacy bincode implementation does not support token balances } assert_eq!(block, bincode_block.into()); } else { diff --git a/storage-bigtable/src/lib.rs b/storage-bigtable/src/lib.rs index 2ca00e8a86..37f874790f 100644 --- a/storage-bigtable/src/lib.rs +++ b/storage-bigtable/src/lib.rs @@ -183,6 +183,8 @@ impl From for TransactionStatusMeta { post_balances, inner_instructions: None, log_messages: None, + pre_token_balances: None, + post_token_balances: None, } } } diff --git a/storage-proto/Cargo.toml b/storage-proto/Cargo.toml index fa606aaef4..a5428682b2 100644 --- a/storage-proto/Cargo.toml +++ b/storage-proto/Cargo.toml @@ -13,6 +13,7 @@ bincode = "1.2.1" prost = "0.6.1" serde = "1.0.112" serde_derive = "1.0.103" +solana-account-decoder = { path = "../account-decoder", version = "1.5.0" } solana-sdk = { path = "../sdk", version = "1.5.0" } solana-transaction-status = { path = "../transaction-status", version = "1.5.0" } diff --git a/storage-proto/proto/solana.storage.confirmed_block.rs b/storage-proto/proto/solana.storage.confirmed_block.rs index b0d9971336..9a36f4f8a0 100644 --- a/storage-proto/proto/solana.storage.confirmed_block.rs +++ b/storage-proto/proto/solana.storage.confirmed_block.rs @@ -61,6 +61,10 @@ pub struct TransactionStatusMeta { pub inner_instructions: ::std::vec::Vec, #[prost(string, repeated, tag = "6")] pub log_messages: ::std::vec::Vec, + #[prost(message, repeated, tag = "7")] + pub pre_token_balances: ::std::vec::Vec, + #[prost(message, repeated, tag = "8")] + pub post_token_balances: ::std::vec::Vec, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct TransactionError { @@ -84,6 +88,24 @@ pub struct CompiledInstruction { pub data: std::vec::Vec, } #[derive(Clone, PartialEq, ::prost::Message)] +pub struct TokenBalance { + #[prost(uint32, tag = "1")] + pub account_index: u32, + #[prost(string, tag = "2")] + pub mint: std::string::String, + #[prost(message, optional, tag = "3")] + pub ui_token_amount: ::std::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UiTokenAmount { + #[prost(double, tag = "1")] + pub ui_amount: f64, + #[prost(uint32, tag = "2")] + pub decimals: u32, + #[prost(string, tag = "3")] + pub amount: std::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] pub struct Reward { #[prost(string, tag = "1")] pub pubkey: std::string::String, diff --git a/storage-proto/src/confirmed_block.proto b/storage-proto/src/confirmed_block.proto index a42b87ca38..5875c60782 100644 --- a/storage-proto/src/confirmed_block.proto +++ b/storage-proto/src/confirmed_block.proto @@ -41,6 +41,8 @@ message TransactionStatusMeta { repeated uint64 post_balances = 4; repeated InnerInstructions inner_instructions = 5; repeated string log_messages = 6; + repeated TokenBalance pre_token_balances = 7; + repeated TokenBalance post_token_balances = 8; } message TransactionError { @@ -58,6 +60,18 @@ message CompiledInstruction { bytes data = 3; } +message TokenBalance { + uint32 account_index = 1; + string mint = 2; + UiTokenAmount ui_token_amount = 3; +} + +message UiTokenAmount { + double ui_amount = 1; + uint32 decimals = 2; + string amount = 3; +} + enum RewardType { Unspecified = 0; Fee = 1; diff --git a/storage-proto/src/convert.rs b/storage-proto/src/convert.rs index 9bc1b378b8..4e1cc6fde6 100644 --- a/storage-proto/src/convert.rs +++ b/storage-proto/src/convert.rs @@ -1,4 +1,5 @@ use crate::StoredExtendedRewards; +use solana_account_decoder::parse_token::UiTokenAmount; use solana_sdk::{ hash::Hash, instruction::CompiledInstruction, @@ -9,7 +10,7 @@ use solana_sdk::{ }; use solana_transaction_status::{ ConfirmedBlock, InnerInstructions, Reward, RewardType, TransactionStatusMeta, - TransactionWithStatusMeta, + TransactionTokenBalance, TransactionWithStatusMeta, }; use std::convert::{TryFrom, TryInto}; @@ -260,6 +261,8 @@ impl From for generated::TransactionStatusMeta { post_balances, inner_instructions, log_messages, + pre_token_balances, + post_token_balances, } = value; let err = match status { Ok(()) => None, @@ -273,6 +276,17 @@ impl From for generated::TransactionStatusMeta { .map(|ii| ii.into()) .collect(); let log_messages = log_messages.unwrap_or_default(); + let pre_token_balances = pre_token_balances + .unwrap_or_default() + .into_iter() + .map(|balance| balance.into()) + .collect(); + let post_token_balances = post_token_balances + .unwrap_or_default() + .into_iter() + .map(|balance| balance.into()) + .collect(); + Self { err, fee, @@ -280,6 +294,8 @@ impl From for generated::TransactionStatusMeta { post_balances, inner_instructions, log_messages, + pre_token_balances, + post_token_balances, } } } @@ -295,6 +311,8 @@ impl TryFrom for TransactionStatusMeta { post_balances, inner_instructions, log_messages, + pre_token_balances, + post_token_balances, } = value; let status = match &err { None => Ok(()), @@ -307,6 +325,18 @@ impl TryFrom for TransactionStatusMeta { .collect(), ); let log_messages = Some(log_messages); + let pre_token_balances = Some( + pre_token_balances + .into_iter() + .map(|balance| balance.into()) + .collect(), + ); + let post_token_balances = Some( + post_token_balances + .into_iter() + .map(|balance| balance.into()) + .collect(), + ); Ok(Self { status, fee, @@ -314,6 +344,8 @@ impl TryFrom for TransactionStatusMeta { post_balances, inner_instructions, log_messages, + pre_token_balances, + post_token_balances, }) } } @@ -336,6 +368,35 @@ impl From for InnerInstructions { } } +impl From for generated::TokenBalance { + fn from(value: TransactionTokenBalance) -> Self { + Self { + account_index: value.account_index as u32, + mint: value.mint, + ui_token_amount: Some(generated::UiTokenAmount { + ui_amount: value.ui_token_amount.ui_amount, + decimals: value.ui_token_amount.decimals as u32, + amount: value.ui_token_amount.amount, + }), + } + } +} + +impl From for TransactionTokenBalance { + fn from(value: generated::TokenBalance) -> Self { + let ui_token_amount = value.ui_token_amount.unwrap_or_default(); + Self { + account_index: value.account_index as u8, + mint: value.mint, + ui_token_amount: UiTokenAmount { + ui_amount: ui_token_amount.ui_amount, + decimals: ui_token_amount.decimals as u8, + amount: ui_token_amount.amount, + }, + } + } +} + impl From for generated::CompiledInstruction { fn from(value: CompiledInstruction) -> Self { Self { diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index d2697d6617..3a0a47b0cf 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -10,11 +10,13 @@ pub mod parse_stake; pub mod parse_system; pub mod parse_token; pub mod parse_vote; +pub mod token_balances; use crate::{ parse_accounts::{parse_accounts, ParsedAccount}, parse_instruction::{parse, ParsedInstruction}, }; +use solana_account_decoder::parse_token::UiTokenAmount; pub use solana_runtime::bank::RewardType; use solana_sdk::{ clock::{Slot, UnixTimestamp}, @@ -27,7 +29,6 @@ use solana_sdk::{ transaction::{Result, Transaction, TransactionError}, }; use std::fmt; - /// A duplicate representation of an Instruction for pretty JSON serialization #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", untagged)] @@ -115,6 +116,31 @@ pub struct UiInnerInstructions { pub instructions: Vec, } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct TransactionTokenBalance { + pub account_index: u8, + pub mint: String, + pub ui_token_amount: UiTokenAmount, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UiTransactionTokenBalance { + pub account_index: u8, + pub mint: String, + pub ui_token_amount: UiTokenAmount, +} + +impl From for UiTransactionTokenBalance { + fn from(token_balance: TransactionTokenBalance) -> Self { + Self { + account_index: token_balance.account_index, + mint: token_balance.mint, + ui_token_amount: token_balance.ui_token_amount, + } + } +} + impl UiInnerInstructions { fn parse(inner_instructions: InnerInstructions, message: &Message) -> Self { Self { @@ -152,6 +178,10 @@ pub struct TransactionStatusMeta { pub inner_instructions: Option>, #[serde(deserialize_with = "default_on_eof")] pub log_messages: Option>, + #[serde(deserialize_with = "default_on_eof")] + pub pre_token_balances: Option>, + #[serde(deserialize_with = "default_on_eof")] + pub post_token_balances: Option>, } impl Default for TransactionStatusMeta { @@ -163,6 +193,8 @@ impl Default for TransactionStatusMeta { post_balances: vec![], inner_instructions: None, log_messages: None, + pre_token_balances: None, + post_token_balances: None, } } } @@ -178,6 +210,8 @@ pub struct UiTransactionStatusMeta { pub post_balances: Vec, pub inner_instructions: Option>, pub log_messages: Option>, + pub pre_token_balances: Option>, + pub post_token_balances: Option>, } impl UiTransactionStatusMeta { @@ -194,6 +228,12 @@ impl UiTransactionStatusMeta { .collect() }), log_messages: meta.log_messages, + pre_token_balances: meta + .pre_token_balances + .map(|balance| balance.into_iter().map(|balance| balance.into()).collect()), + post_token_balances: meta + .post_token_balances + .map(|balance| balance.into_iter().map(|balance| balance.into()).collect()), } } } @@ -210,6 +250,12 @@ impl From for UiTransactionStatusMeta { .inner_instructions .map(|ixs| ixs.into_iter().map(|ix| ix.into()).collect()), log_messages: meta.log_messages, + pre_token_balances: meta + .pre_token_balances + .map(|balance| balance.into_iter().map(|balance| balance.into()).collect()), + post_token_balances: meta + .post_token_balances + .map(|balance| balance.into_iter().map(|balance| balance.into()).collect()), } } } diff --git a/transaction-status/src/token_balances.rs b/transaction-status/src/token_balances.rs new file mode 100644 index 0000000000..be815f0272 --- /dev/null +++ b/transaction-status/src/token_balances.rs @@ -0,0 +1,113 @@ +use crate::TransactionTokenBalance; +use solana_account_decoder::parse_token::{ + spl_token_id_v2_0, spl_token_v2_0_native_mint, token_amount_to_ui_amount, UiTokenAmount, +}; +use solana_runtime::{ + bank::Bank, transaction_batch::TransactionBatch, transaction_utils::OrderedIterator, +}; +use solana_sdk::pubkey::Pubkey; +use spl_token_v2_0::{ + solana_program::program_pack::Pack, + state::{Account as TokenAccount, Mint}, +}; +use std::{collections::HashMap, str::FromStr}; + +pub type TransactionTokenBalances = Vec>; + +pub struct TransactionTokenBalancesSet { + pub pre_token_balances: TransactionTokenBalances, + pub post_token_balances: TransactionTokenBalances, +} + +impl TransactionTokenBalancesSet { + pub fn new( + pre_token_balances: TransactionTokenBalances, + post_token_balances: TransactionTokenBalances, + ) -> Self { + assert_eq!(pre_token_balances.len(), post_token_balances.len()); + Self { + pre_token_balances, + post_token_balances, + } + } +} + +fn is_token_program(program_id: &Pubkey) -> bool { + program_id == &spl_token_id_v2_0() +} + +fn get_mint_decimals(bank: &Bank, mint: &Pubkey) -> Option { + if mint == &spl_token_v2_0_native_mint() { + Some(spl_token_v2_0::native_mint::DECIMALS) + } else { + let mint_account = bank.get_account(mint)?; + + let decimals = Mint::unpack(&mint_account.data) + .map(|mint| mint.decimals) + .ok()?; + + Some(decimals) + } +} + +pub fn collect_token_balances( + bank: &Bank, + batch: &TransactionBatch, + mut mint_decimals: &mut HashMap, +) -> TransactionTokenBalances { + let mut balances: TransactionTokenBalances = vec![]; + + for (_, transaction) in OrderedIterator::new(batch.transactions(), batch.iteration_order()) { + let account_keys = &transaction.message.account_keys; + let mut fetch_account_hash: HashMap = HashMap::new(); + for instruction in transaction.message.instructions.iter() { + if let Some(program_id) = account_keys.get(instruction.program_id_index as usize) { + if is_token_program(&program_id) { + for account in &instruction.accounts { + fetch_account_hash.insert(*account, true); + } + } + } + } + + let mut transaction_balances: Vec = vec![]; + for index in fetch_account_hash.keys() { + if let Some(account_id) = account_keys.get(*index as usize) { + if let Some((mint, ui_token_amount)) = + collect_token_balance_from_account(&bank, account_id, &mut mint_decimals) + { + transaction_balances.push(TransactionTokenBalance { + account_index: *index, + mint, + ui_token_amount, + }); + } + } + } + balances.push(transaction_balances); + } + balances +} + +pub fn collect_token_balance_from_account( + bank: &Bank, + account_id: &Pubkey, + mint_decimals: &mut HashMap, +) -> Option<(String, UiTokenAmount)> { + let account = bank.get_account(account_id)?; + + let token_account = TokenAccount::unpack(&account.data).ok()?; + let mint_string = &token_account.mint.to_string(); + let mint = &Pubkey::from_str(&mint_string).unwrap_or_default(); + + let decimals = mint_decimals.get(&mint).cloned().or_else(|| { + let decimals = get_mint_decimals(bank, &mint)?; + mint_decimals.insert(*mint, decimals); + Some(decimals) + })?; + + Some(( + mint_string.to_string(), + token_amount_to_ui_amount(token_account.amount, decimals), + )) +}