diff --git a/Cargo.lock b/Cargo.lock index 60a024281c..59c79c5690 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5485,6 +5485,8 @@ dependencies = [ "solana-storage-proto", "solana-transaction-status", "solana-vote-program", + "spl-token", + "spl-token-2022", "static_assertions", "tempfile", "thiserror", @@ -6431,7 +6433,6 @@ dependencies = [ "solana-account-decoder", "solana-measure", "solana-metrics", - "solana-runtime", "solana-sdk 1.11.6", "solana-vote-program", "spl-associated-token-account", diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index b3cd01f828..b93bbf3d04 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -23,7 +23,9 @@ use { solana_client::{connection_cache::ConnectionCache, tpu_connection::TpuConnection}, solana_entry::entry::hash_transactions, solana_gossip::{cluster_info::ClusterInfo, contact_info::ContactInfo}, - solana_ledger::blockstore_processor::TransactionStatusSender, + solana_ledger::{ + blockstore_processor::TransactionStatusSender, token_balances::collect_token_balances, + }, solana_measure::{measure, measure::Measure}, solana_metrics::inc_new_counter_info, solana_perf::{ @@ -57,9 +59,7 @@ use { transport::TransportError, }, solana_streamer::sendmmsg::batch_send, - solana_transaction_status::token_balances::{ - collect_token_balances, TransactionTokenBalancesSet, - }, + solana_transaction_status::token_balances::TransactionTokenBalancesSet, std::{ cmp, collections::HashMap, diff --git a/ledger/Cargo.toml b/ledger/Cargo.toml index 4ed6240598..cb0e6aa3c5 100644 --- a/ledger/Cargo.toml +++ b/ledger/Cargo.toml @@ -34,6 +34,7 @@ reed-solomon-erasure = { version = "5.0.3", features = ["simd-accel"] } serde = "1.0.138" serde_bytes = "0.11.6" sha2 = "0.10.2" +solana-account-decoder = { path = "../account-decoder", version = "=1.11.6" } solana-bpf-loader-program = { path = "../programs/bpf_loader", version = "=1.11.6" } solana-entry = { path = "../entry", version = "=1.11.6" } solana-frozen-abi = { path = "../frozen-abi", version = "=1.11.6" } @@ -50,6 +51,8 @@ solana-storage-bigtable = { path = "../storage-bigtable", version = "=1.11.6" } solana-storage-proto = { path = "../storage-proto", version = "=1.11.6" } solana-transaction-status = { path = "../transaction-status", version = "=1.11.6" } solana-vote-program = { path = "../programs/vote", version = "=1.11.6" } +spl-token = { version = "=3.3.1", features = ["no-entrypoint"] } +spl-token-2022 = { version = "=0.4.2", features = ["no-entrypoint"] } static_assertions = "1.1.0" tempfile = "3.3.0" thiserror = "1.0" diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 22b8b872b1..321997a42e 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -2,6 +2,7 @@ use { crate::{ block_error::BlockError, blockstore::Blockstore, blockstore_db::BlockstoreError, blockstore_meta::SlotMeta, leader_schedule_cache::LeaderScheduleCache, + token_balances::collect_token_balances, }, chrono_humanize::{Accuracy, HumanTime, Tense}, crossbeam_channel::Sender, @@ -49,9 +50,7 @@ use { VersionedTransaction, }, }, - solana_transaction_status::token_balances::{ - collect_token_balances, TransactionTokenBalancesSet, - }, + solana_transaction_status::token_balances::TransactionTokenBalancesSet, std::{ borrow::Cow, collections::{HashMap, HashSet}, diff --git a/ledger/src/lib.rs b/ledger/src/lib.rs index e65a74cff2..f0aa3598de 100644 --- a/ledger/src/lib.rs +++ b/ledger/src/lib.rs @@ -28,6 +28,7 @@ mod shredder; pub mod sigverify_shreds; pub mod slot_stats; mod staking_utils; +pub mod token_balances; #[macro_use] extern crate solana_metrics; diff --git a/ledger/src/token_balances.rs b/ledger/src/token_balances.rs new file mode 100644 index 0000000000..673cc5556b --- /dev/null +++ b/ledger/src/token_balances.rs @@ -0,0 +1,477 @@ +use { + solana_account_decoder::parse_token::{ + is_known_spl_token_id, pubkey_from_spl_token, spl_token_native_mint, + token_amount_to_ui_amount, UiTokenAmount, + }, + solana_measure::measure::Measure, + solana_metrics::datapoint_debug, + solana_runtime::{bank::Bank, transaction_batch::TransactionBatch}, + solana_sdk::{account::ReadableAccount, pubkey::Pubkey}, + solana_transaction_status::{ + token_balances::TransactionTokenBalances, TransactionTokenBalance, + }, + spl_token_2022::{ + extension::StateWithExtensions, + state::{Account as TokenAccount, Mint}, + }, + std::collections::HashMap, +}; + +fn get_mint_decimals(bank: &Bank, mint: &Pubkey) -> Option { + if mint == &spl_token_native_mint() { + Some(spl_token::native_mint::DECIMALS) + } else { + let mint_account = bank.get_account(mint)?; + + if !is_known_spl_token_id(mint_account.owner()) { + return None; + } + + let decimals = StateWithExtensions::::unpack(mint_account.data()) + .map(|mint| mint.base.decimals) + .ok()?; + + Some(decimals) + } +} + +pub fn collect_token_balances( + bank: &Bank, + batch: &TransactionBatch, + mint_decimals: &mut HashMap, +) -> TransactionTokenBalances { + let mut balances: TransactionTokenBalances = vec![]; + let mut collect_time = Measure::start("collect_token_balances"); + + for transaction in batch.sanitized_transactions() { + let account_keys = transaction.message().account_keys(); + let has_token_program = account_keys.iter().any(is_known_spl_token_id); + + let mut transaction_balances: Vec = vec![]; + if has_token_program { + for (index, account_id) in account_keys.iter().enumerate() { + if transaction.message().is_invoked(index) || is_known_spl_token_id(account_id) { + continue; + } + + if let Some(TokenBalanceData { + mint, + ui_token_amount, + owner, + program_id, + }) = collect_token_balance_from_account(bank, account_id, mint_decimals) + { + transaction_balances.push(TransactionTokenBalance { + account_index: index as u8, + mint, + ui_token_amount, + owner, + program_id, + }); + } + } + } + balances.push(transaction_balances); + } + collect_time.stop(); + datapoint_debug!( + "collect_token_balances", + ("collect_time_us", collect_time.as_us(), i64), + ); + balances +} + +#[derive(Debug, PartialEq)] +struct TokenBalanceData { + mint: String, + owner: String, + ui_token_amount: UiTokenAmount, + program_id: String, +} + +fn collect_token_balance_from_account( + bank: &Bank, + account_id: &Pubkey, + mint_decimals: &mut HashMap, +) -> Option { + let account = bank.get_account(account_id)?; + + if !is_known_spl_token_id(account.owner()) { + return None; + } + + let token_account = StateWithExtensions::::unpack(account.data()).ok()?; + let mint = pubkey_from_spl_token(&token_account.base.mint); + + let decimals = mint_decimals.get(&mint).cloned().or_else(|| { + let decimals = get_mint_decimals(bank, &mint)?; + mint_decimals.insert(mint, decimals); + Some(decimals) + })?; + + Some(TokenBalanceData { + mint: token_account.base.mint.to_string(), + owner: token_account.base.owner.to_string(), + ui_token_amount: token_amount_to_ui_amount(token_account.base.amount, decimals), + program_id: account.owner().to_string(), + }) +} + +#[cfg(test)] +mod test { + use { + super::*, + solana_account_decoder::parse_token::{pubkey_from_spl_token, spl_token_pubkey}, + solana_sdk::{account::Account, genesis_config::create_genesis_config}, + spl_token_2022::{ + extension::{ + immutable_owner::ImmutableOwner, memo_transfer::MemoTransfer, + mint_close_authority::MintCloseAuthority, ExtensionType, StateWithExtensionsMut, + }, + pod::OptionalNonZeroPubkey, + solana_program::{program_option::COption, program_pack::Pack}, + }, + std::collections::BTreeMap, + }; + + #[test] + fn test_collect_token_balance_from_account() { + let (mut genesis_config, _mint_keypair) = create_genesis_config(500); + + // Add a variety of accounts, token and not + let account = Account::new(42, 55, &Pubkey::new_unique()); + + let mint_data = Mint { + mint_authority: COption::None, + supply: 4242, + decimals: 2, + is_initialized: true, + freeze_authority: COption::None, + }; + let mut data = [0; Mint::LEN]; + Mint::pack(mint_data, &mut data).unwrap(); + let mint_pubkey = Pubkey::new_unique(); + let mint = Account { + lamports: 100, + data: data.to_vec(), + owner: pubkey_from_spl_token(&spl_token::id()), + executable: false, + rent_epoch: 0, + }; + let other_mint_pubkey = Pubkey::new_unique(); + let other_mint = Account { + lamports: 100, + data: data.to_vec(), + owner: Pubkey::new_unique(), // !is_known_spl_token_id + executable: false, + rent_epoch: 0, + }; + + let token_owner = Pubkey::new_unique(); + let token_data = TokenAccount { + mint: spl_token_pubkey(&mint_pubkey), + owner: spl_token_pubkey(&token_owner), + amount: 42, + delegate: COption::None, + state: spl_token_2022::state::AccountState::Initialized, + is_native: COption::Some(100), + delegated_amount: 0, + close_authority: COption::None, + }; + let mut data = [0; TokenAccount::LEN]; + TokenAccount::pack(token_data, &mut data).unwrap(); + + let spl_token_account = Account { + lamports: 100, + data: data.to_vec(), + owner: pubkey_from_spl_token(&spl_token::id()), + executable: false, + rent_epoch: 0, + }; + let other_account = Account { + lamports: 100, + data: data.to_vec(), + owner: Pubkey::new_unique(), // !is_known_spl_token_id + executable: false, + rent_epoch: 0, + }; + + let other_mint_data = TokenAccount { + mint: spl_token_pubkey(&other_mint_pubkey), + owner: spl_token_pubkey(&token_owner), + amount: 42, + delegate: COption::None, + state: spl_token_2022::state::AccountState::Initialized, + is_native: COption::Some(100), + delegated_amount: 0, + close_authority: COption::None, + }; + let mut data = [0; TokenAccount::LEN]; + TokenAccount::pack(other_mint_data, &mut data).unwrap(); + + let other_mint_token_account = Account { + lamports: 100, + data: data.to_vec(), + owner: pubkey_from_spl_token(&spl_token::id()), + executable: false, + rent_epoch: 0, + }; + + let mut accounts = BTreeMap::new(); + + let account_pubkey = Pubkey::new_unique(); + accounts.insert(account_pubkey, account); + accounts.insert(mint_pubkey, mint); + accounts.insert(other_mint_pubkey, other_mint); + let spl_token_account_pubkey = Pubkey::new_unique(); + accounts.insert(spl_token_account_pubkey, spl_token_account); + let other_account_pubkey = Pubkey::new_unique(); + accounts.insert(other_account_pubkey, other_account); + let other_mint_account_pubkey = Pubkey::new_unique(); + accounts.insert(other_mint_account_pubkey, other_mint_token_account); + + genesis_config.accounts = accounts; + + let bank = Bank::new_for_tests(&genesis_config); + let mut mint_decimals = HashMap::new(); + + // Account is not owned by spl_token (nor does it have TokenAccount state) + assert_eq!( + collect_token_balance_from_account(&bank, &account_pubkey, &mut mint_decimals), + None + ); + + // Mint does not have TokenAccount state + assert_eq!( + collect_token_balance_from_account(&bank, &mint_pubkey, &mut mint_decimals), + None + ); + + // TokenAccount owned by spl_token::id() works + assert_eq!( + collect_token_balance_from_account( + &bank, + &spl_token_account_pubkey, + &mut mint_decimals + ), + Some(TokenBalanceData { + mint: mint_pubkey.to_string(), + owner: token_owner.to_string(), + ui_token_amount: UiTokenAmount { + ui_amount: Some(0.42), + decimals: 2, + amount: "42".to_string(), + ui_amount_string: "0.42".to_string(), + }, + program_id: spl_token::id().to_string(), + }) + ); + + // TokenAccount is not owned by known spl-token program_id + assert_eq!( + collect_token_balance_from_account(&bank, &other_account_pubkey, &mut mint_decimals), + None + ); + + // TokenAccount's mint is not owned by known spl-token program_id + assert_eq!( + collect_token_balance_from_account( + &bank, + &other_mint_account_pubkey, + &mut mint_decimals + ), + None + ); + } + + #[test] + fn test_collect_token_balance_from_spl_token_2022_account() { + let (mut genesis_config, _mint_keypair) = create_genesis_config(500); + + // Add a variety of accounts, token and not + let account = Account::new(42, 55, &Pubkey::new_unique()); + + let mint_authority = Pubkey::new_unique(); + let mint_size = + ExtensionType::get_account_len::(&[ExtensionType::MintCloseAuthority]); + let mint_base = Mint { + mint_authority: COption::None, + supply: 4242, + decimals: 2, + is_initialized: true, + freeze_authority: COption::None, + }; + let mut mint_data = vec![0; mint_size]; + let mut mint_state = + StateWithExtensionsMut::::unpack_uninitialized(&mut mint_data).unwrap(); + mint_state.base = mint_base; + mint_state.pack_base(); + mint_state.init_account_type().unwrap(); + let mut mint_close_authority = mint_state + .init_extension::(true) + .unwrap(); + mint_close_authority.close_authority = + OptionalNonZeroPubkey::try_from(Some(spl_token_pubkey(&mint_authority))).unwrap(); + + let mint_pubkey = Pubkey::new_unique(); + let mint = Account { + lamports: 100, + data: mint_data.to_vec(), + owner: pubkey_from_spl_token(&spl_token_2022::id()), + executable: false, + rent_epoch: 0, + }; + let other_mint_pubkey = Pubkey::new_unique(); + let other_mint = Account { + lamports: 100, + data: mint_data.to_vec(), + owner: Pubkey::new_unique(), + executable: false, + rent_epoch: 0, + }; + + let token_owner = Pubkey::new_unique(); + let token_base = TokenAccount { + mint: spl_token_pubkey(&mint_pubkey), + owner: spl_token_pubkey(&token_owner), + amount: 42, + delegate: COption::None, + state: spl_token_2022::state::AccountState::Initialized, + is_native: COption::Some(100), + delegated_amount: 0, + close_authority: COption::None, + }; + let account_size = ExtensionType::get_account_len::(&[ + ExtensionType::ImmutableOwner, + ExtensionType::MemoTransfer, + ]); + let mut account_data = vec![0; account_size]; + let mut account_state = + StateWithExtensionsMut::::unpack_uninitialized(&mut account_data) + .unwrap(); + account_state.base = token_base; + account_state.pack_base(); + account_state.init_account_type().unwrap(); + account_state + .init_extension::(true) + .unwrap(); + let mut memo_transfer = account_state.init_extension::(true).unwrap(); + memo_transfer.require_incoming_transfer_memos = true.into(); + + let spl_token_account = Account { + lamports: 100, + data: account_data.to_vec(), + owner: pubkey_from_spl_token(&spl_token_2022::id()), + executable: false, + rent_epoch: 0, + }; + let other_account = Account { + lamports: 100, + data: account_data.to_vec(), + owner: Pubkey::new_unique(), + executable: false, + rent_epoch: 0, + }; + + let other_mint_token_base = TokenAccount { + mint: spl_token_pubkey(&other_mint_pubkey), + owner: spl_token_pubkey(&token_owner), + amount: 42, + delegate: COption::None, + state: spl_token_2022::state::AccountState::Initialized, + is_native: COption::Some(100), + delegated_amount: 0, + close_authority: COption::None, + }; + let account_size = ExtensionType::get_account_len::(&[ + ExtensionType::ImmutableOwner, + ExtensionType::MemoTransfer, + ]); + let mut account_data = vec![0; account_size]; + let mut account_state = + StateWithExtensionsMut::::unpack_uninitialized(&mut account_data) + .unwrap(); + account_state.base = other_mint_token_base; + account_state.pack_base(); + account_state.init_account_type().unwrap(); + account_state + .init_extension::(true) + .unwrap(); + let mut memo_transfer = account_state.init_extension::(true).unwrap(); + memo_transfer.require_incoming_transfer_memos = true.into(); + + let other_mint_token_account = Account { + lamports: 100, + data: account_data.to_vec(), + owner: pubkey_from_spl_token(&spl_token_2022::id()), + executable: false, + rent_epoch: 0, + }; + + let mut accounts = BTreeMap::new(); + + let account_pubkey = Pubkey::new_unique(); + accounts.insert(account_pubkey, account); + accounts.insert(mint_pubkey, mint); + accounts.insert(other_mint_pubkey, other_mint); + let spl_token_account_pubkey = Pubkey::new_unique(); + accounts.insert(spl_token_account_pubkey, spl_token_account); + let other_account_pubkey = Pubkey::new_unique(); + accounts.insert(other_account_pubkey, other_account); + let other_mint_account_pubkey = Pubkey::new_unique(); + accounts.insert(other_mint_account_pubkey, other_mint_token_account); + + genesis_config.accounts = accounts; + + let bank = Bank::new_for_tests(&genesis_config); + let mut mint_decimals = HashMap::new(); + + // Account is not owned by spl_token (nor does it have TokenAccount state) + assert_eq!( + collect_token_balance_from_account(&bank, &account_pubkey, &mut mint_decimals), + None + ); + + // Mint does not have TokenAccount state + assert_eq!( + collect_token_balance_from_account(&bank, &mint_pubkey, &mut mint_decimals), + None + ); + + // TokenAccount owned by spl_token_2022::id() works + assert_eq!( + collect_token_balance_from_account( + &bank, + &spl_token_account_pubkey, + &mut mint_decimals + ), + Some(TokenBalanceData { + mint: mint_pubkey.to_string(), + owner: token_owner.to_string(), + ui_token_amount: UiTokenAmount { + ui_amount: Some(0.42), + decimals: 2, + amount: "42".to_string(), + ui_amount_string: "0.42".to_string(), + }, + program_id: spl_token_2022::id().to_string(), + }) + ); + + // TokenAccount is not owned by known spl-token program_id + assert_eq!( + collect_token_balance_from_account(&bank, &other_account_pubkey, &mut mint_decimals), + None + ); + + // TokenAccount's mint is not owned by known spl-token program_id + assert_eq!( + collect_token_balance_from_account( + &bank, + &other_mint_account_pubkey, + &mut mint_decimals + ), + None + ); + } +} diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index 6fc3d3c1b4..5727159837 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -4147,6 +4147,7 @@ dependencies = [ "solana-bpf-rust-realloc", "solana-bpf-rust-realloc-invoke", "solana-cli-output", + "solana-ledger", "solana-logger 1.11.6", "solana-measure", "solana-program-runtime", @@ -4955,6 +4956,7 @@ dependencies = [ "serde", "serde_bytes", "sha2 0.10.2", + "solana-account-decoder", "solana-bpf-loader-program", "solana-entry", "solana-frozen-abi 1.11.6", @@ -4971,6 +4973,8 @@ dependencies = [ "solana-storage-proto", "solana-transaction-status", "solana-vote-program", + "spl-token", + "spl-token-2022", "static_assertions", "tempfile", "thiserror", @@ -5648,7 +5652,6 @@ dependencies = [ "solana-account-decoder", "solana-measure", "solana-metrics", - "solana-runtime", "solana-sdk 1.11.6", "solana-vote-program", "spl-associated-token-account", diff --git a/programs/bpf/Cargo.toml b/programs/bpf/Cargo.toml index f5e3cf2424..6406c88271 100644 --- a/programs/bpf/Cargo.toml +++ b/programs/bpf/Cargo.toml @@ -40,6 +40,9 @@ solana-sdk = { path = "../../sdk", version = "=1.11.6" } solana-transaction-status = { path = "../../transaction-status", version = "=1.11.6" } solana_rbpf = "=0.2.31" +[dev-dependencies] +solana-ledger = { path = "../../ledger", version = "=1.11.6" } + [[bench]] name = "bpf_loader" diff --git a/programs/bpf/tests/programs.rs b/programs/bpf/tests/programs.rs index de399b5900..ea0f9edf37 100644 --- a/programs/bpf/tests/programs.rs +++ b/programs/bpf/tests/programs.rs @@ -18,6 +18,7 @@ use { solana_bpf_rust_invoke::instructions::*, solana_bpf_rust_realloc::instructions::*, solana_bpf_rust_realloc_invoke::instructions::*, + solana_ledger::token_balances::collect_token_balances, solana_program_runtime::{ compute_budget::ComputeBudget, invoke_context::with_mock_invoke_context, timings::ExecuteTimings, @@ -64,9 +65,8 @@ use { transaction::{SanitizedTransaction, Transaction, TransactionError, VersionedTransaction}, }, solana_transaction_status::{ - token_balances::collect_token_balances, ConfirmedTransactionWithStatusMeta, - InnerInstructions, TransactionStatusMeta, TransactionWithStatusMeta, - VersionedTransactionWithStatusMeta, + ConfirmedTransactionWithStatusMeta, InnerInstructions, TransactionStatusMeta, + TransactionWithStatusMeta, VersionedTransactionWithStatusMeta, }, std::{collections::HashMap, env, fs::File, io::Read, path::PathBuf, str::FromStr, sync::Arc}, }; diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index fb107ba32c..bf60bf14d4 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -35,6 +35,7 @@ //! already been signed and verified. #[allow(deprecated)] use solana_sdk::recent_blockhashes_account; +pub use solana_sdk::reward_type::RewardType; use { crate::{ account_overrides::AccountOverrides, @@ -1119,14 +1120,6 @@ impl PartialEq for Bank { } } -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, AbiExample, AbiEnumVisitor, Clone, Copy)] -pub enum RewardType { - Fee, - Rent, - Staking, - Voting, -} - #[derive(Debug)] pub enum RewardCalculationEvent<'a, 'b> { Staking(&'a Pubkey, &'b InflationPointCalculationEvent), @@ -1136,21 +1129,6 @@ fn null_tracer() -> Option { None:: } -impl fmt::Display for RewardType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - RewardType::Fee => "fee", - RewardType::Rent => "rent", - RewardType::Staking => "staking", - RewardType::Voting => "voting", - } - ) - } -} - pub trait DropCallback: fmt::Debug { fn callback(&self, b: &Bank); fn clone_box(&self) -> Box; diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index bc3c65dcbd..08da2ce824 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -41,6 +41,7 @@ pub mod program_utils; pub mod pubkey; pub mod quic; pub mod recent_blockhashes_account; +pub mod reward_type; pub mod rpc_port; pub mod secp256k1_instruction; pub mod shred_version; diff --git a/sdk/src/reward_type.rs b/sdk/src/reward_type.rs new file mode 100644 index 0000000000..feba745993 --- /dev/null +++ b/sdk/src/reward_type.rs @@ -0,0 +1,24 @@ +use std::fmt; + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, AbiExample, AbiEnumVisitor, Clone, Copy)] +pub enum RewardType { + Fee, + Rent, + Staking, + Voting, +} + +impl fmt::Display for RewardType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + RewardType::Fee => "fee", + RewardType::Rent => "rent", + RewardType::Staking => "staking", + RewardType::Voting => "voting", + } + ) + } +} diff --git a/transaction-status/Cargo.toml b/transaction-status/Cargo.toml index 36798952bc..b28a4cbbbd 100644 --- a/transaction-status/Cargo.toml +++ b/transaction-status/Cargo.toml @@ -23,7 +23,6 @@ serde_json = "1.0.81" solana-account-decoder = { path = "../account-decoder", version = "=1.11.6" } solana-measure = { path = "../measure", version = "=1.11.6" } solana-metrics = { path = "../metrics", version = "=1.11.6" } -solana-runtime = { path = "../runtime", version = "=1.11.6" } solana-sdk = { path = "../sdk", version = "=1.11.6" } solana-vote-program = { path = "../programs/vote", version = "=1.11.6" } spl-associated-token-account = { version = "=1.1.1", features = ["no-entrypoint"] } diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index 31a1a83741..d594af806b 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -1,6 +1,6 @@ #![allow(clippy::integer_arithmetic)] -pub use {crate::extract_memos::extract_and_fmt_memos, solana_runtime::bank::RewardType}; +pub use {crate::extract_memos::extract_and_fmt_memos, solana_sdk::reward_type::RewardType}; use { crate::{ parse_accounts::{parse_accounts, parse_static_accounts, ParsedAccount}, diff --git a/transaction-status/src/token_balances.rs b/transaction-status/src/token_balances.rs index b4c047410f..85a85a053f 100644 --- a/transaction-status/src/token_balances.rs +++ b/transaction-status/src/token_balances.rs @@ -1,19 +1,4 @@ -use { - crate::TransactionTokenBalance, - solana_account_decoder::parse_token::{ - is_known_spl_token_id, pubkey_from_spl_token, spl_token_native_mint, - token_amount_to_ui_amount, UiTokenAmount, - }, - solana_measure::measure::Measure, - solana_metrics::datapoint_debug, - solana_runtime::{bank::Bank, transaction_batch::TransactionBatch}, - solana_sdk::{account::ReadableAccount, pubkey::Pubkey}, - spl_token_2022::{ - extension::StateWithExtensions, - state::{Account as TokenAccount, Mint}, - }, - std::collections::HashMap, -}; +use crate::TransactionTokenBalance; pub type TransactionTokenBalances = Vec>; @@ -34,462 +19,3 @@ impl TransactionTokenBalancesSet { } } } - -fn get_mint_decimals(bank: &Bank, mint: &Pubkey) -> Option { - if mint == &spl_token_native_mint() { - Some(spl_token::native_mint::DECIMALS) - } else { - let mint_account = bank.get_account(mint)?; - - if !is_known_spl_token_id(mint_account.owner()) { - return None; - } - - let decimals = StateWithExtensions::::unpack(mint_account.data()) - .map(|mint| mint.base.decimals) - .ok()?; - - Some(decimals) - } -} - -pub fn collect_token_balances( - bank: &Bank, - batch: &TransactionBatch, - mint_decimals: &mut HashMap, -) -> TransactionTokenBalances { - let mut balances: TransactionTokenBalances = vec![]; - let mut collect_time = Measure::start("collect_token_balances"); - - for transaction in batch.sanitized_transactions() { - let account_keys = transaction.message().account_keys(); - let has_token_program = account_keys.iter().any(is_known_spl_token_id); - - let mut transaction_balances: Vec = vec![]; - if has_token_program { - for (index, account_id) in account_keys.iter().enumerate() { - if transaction.message().is_invoked(index) || is_known_spl_token_id(account_id) { - continue; - } - - if let Some(TokenBalanceData { - mint, - ui_token_amount, - owner, - program_id, - }) = collect_token_balance_from_account(bank, account_id, mint_decimals) - { - transaction_balances.push(TransactionTokenBalance { - account_index: index as u8, - mint, - ui_token_amount, - owner, - program_id, - }); - } - } - } - balances.push(transaction_balances); - } - collect_time.stop(); - datapoint_debug!( - "collect_token_balances", - ("collect_time_us", collect_time.as_us(), i64), - ); - balances -} - -#[derive(Debug, PartialEq)] -struct TokenBalanceData { - mint: String, - owner: String, - ui_token_amount: UiTokenAmount, - program_id: String, -} - -fn collect_token_balance_from_account( - bank: &Bank, - account_id: &Pubkey, - mint_decimals: &mut HashMap, -) -> Option { - let account = bank.get_account(account_id)?; - - if !is_known_spl_token_id(account.owner()) { - return None; - } - - let token_account = StateWithExtensions::::unpack(account.data()).ok()?; - let mint = pubkey_from_spl_token(&token_account.base.mint); - - let decimals = mint_decimals.get(&mint).cloned().or_else(|| { - let decimals = get_mint_decimals(bank, &mint)?; - mint_decimals.insert(mint, decimals); - Some(decimals) - })?; - - Some(TokenBalanceData { - mint: token_account.base.mint.to_string(), - owner: token_account.base.owner.to_string(), - ui_token_amount: token_amount_to_ui_amount(token_account.base.amount, decimals), - program_id: account.owner().to_string(), - }) -} - -#[cfg(test)] -mod test { - use { - super::*, - solana_account_decoder::parse_token::{pubkey_from_spl_token, spl_token_pubkey}, - solana_sdk::{account::Account, genesis_config::create_genesis_config}, - spl_token_2022::{ - extension::{ - immutable_owner::ImmutableOwner, memo_transfer::MemoTransfer, - mint_close_authority::MintCloseAuthority, ExtensionType, StateWithExtensionsMut, - }, - pod::OptionalNonZeroPubkey, - solana_program::{program_option::COption, program_pack::Pack}, - }, - std::collections::BTreeMap, - }; - - #[test] - fn test_collect_token_balance_from_account() { - let (mut genesis_config, _mint_keypair) = create_genesis_config(500); - - // Add a variety of accounts, token and not - let account = Account::new(42, 55, &Pubkey::new_unique()); - - let mint_data = Mint { - mint_authority: COption::None, - supply: 4242, - decimals: 2, - is_initialized: true, - freeze_authority: COption::None, - }; - let mut data = [0; Mint::LEN]; - Mint::pack(mint_data, &mut data).unwrap(); - let mint_pubkey = Pubkey::new_unique(); - let mint = Account { - lamports: 100, - data: data.to_vec(), - owner: pubkey_from_spl_token(&spl_token::id()), - executable: false, - rent_epoch: 0, - }; - let other_mint_pubkey = Pubkey::new_unique(); - let other_mint = Account { - lamports: 100, - data: data.to_vec(), - owner: Pubkey::new_unique(), // !is_known_spl_token_id - executable: false, - rent_epoch: 0, - }; - - let token_owner = Pubkey::new_unique(); - let token_data = TokenAccount { - mint: spl_token_pubkey(&mint_pubkey), - owner: spl_token_pubkey(&token_owner), - amount: 42, - delegate: COption::None, - state: spl_token_2022::state::AccountState::Initialized, - is_native: COption::Some(100), - delegated_amount: 0, - close_authority: COption::None, - }; - let mut data = [0; TokenAccount::LEN]; - TokenAccount::pack(token_data, &mut data).unwrap(); - - let spl_token_account = Account { - lamports: 100, - data: data.to_vec(), - owner: pubkey_from_spl_token(&spl_token::id()), - executable: false, - rent_epoch: 0, - }; - let other_account = Account { - lamports: 100, - data: data.to_vec(), - owner: Pubkey::new_unique(), // !is_known_spl_token_id - executable: false, - rent_epoch: 0, - }; - - let other_mint_data = TokenAccount { - mint: spl_token_pubkey(&other_mint_pubkey), - owner: spl_token_pubkey(&token_owner), - amount: 42, - delegate: COption::None, - state: spl_token_2022::state::AccountState::Initialized, - is_native: COption::Some(100), - delegated_amount: 0, - close_authority: COption::None, - }; - let mut data = [0; TokenAccount::LEN]; - TokenAccount::pack(other_mint_data, &mut data).unwrap(); - - let other_mint_token_account = Account { - lamports: 100, - data: data.to_vec(), - owner: pubkey_from_spl_token(&spl_token::id()), - executable: false, - rent_epoch: 0, - }; - - let mut accounts = BTreeMap::new(); - - let account_pubkey = Pubkey::new_unique(); - accounts.insert(account_pubkey, account); - accounts.insert(mint_pubkey, mint); - accounts.insert(other_mint_pubkey, other_mint); - let spl_token_account_pubkey = Pubkey::new_unique(); - accounts.insert(spl_token_account_pubkey, spl_token_account); - let other_account_pubkey = Pubkey::new_unique(); - accounts.insert(other_account_pubkey, other_account); - let other_mint_account_pubkey = Pubkey::new_unique(); - accounts.insert(other_mint_account_pubkey, other_mint_token_account); - - genesis_config.accounts = accounts; - - let bank = Bank::new_for_tests(&genesis_config); - let mut mint_decimals = HashMap::new(); - - // Account is not owned by spl_token (nor does it have TokenAccount state) - assert_eq!( - collect_token_balance_from_account(&bank, &account_pubkey, &mut mint_decimals), - None - ); - - // Mint does not have TokenAccount state - assert_eq!( - collect_token_balance_from_account(&bank, &mint_pubkey, &mut mint_decimals), - None - ); - - // TokenAccount owned by spl_token::id() works - assert_eq!( - collect_token_balance_from_account( - &bank, - &spl_token_account_pubkey, - &mut mint_decimals - ), - Some(TokenBalanceData { - mint: mint_pubkey.to_string(), - owner: token_owner.to_string(), - ui_token_amount: UiTokenAmount { - ui_amount: Some(0.42), - decimals: 2, - amount: "42".to_string(), - ui_amount_string: "0.42".to_string(), - }, - program_id: spl_token::id().to_string(), - }) - ); - - // TokenAccount is not owned by known spl-token program_id - assert_eq!( - collect_token_balance_from_account(&bank, &other_account_pubkey, &mut mint_decimals), - None - ); - - // TokenAccount's mint is not owned by known spl-token program_id - assert_eq!( - collect_token_balance_from_account( - &bank, - &other_mint_account_pubkey, - &mut mint_decimals - ), - None - ); - } - - #[test] - fn test_collect_token_balance_from_spl_token_2022_account() { - let (mut genesis_config, _mint_keypair) = create_genesis_config(500); - - // Add a variety of accounts, token and not - let account = Account::new(42, 55, &Pubkey::new_unique()); - - let mint_authority = Pubkey::new_unique(); - let mint_size = - ExtensionType::get_account_len::(&[ExtensionType::MintCloseAuthority]); - let mint_base = Mint { - mint_authority: COption::None, - supply: 4242, - decimals: 2, - is_initialized: true, - freeze_authority: COption::None, - }; - let mut mint_data = vec![0; mint_size]; - let mut mint_state = - StateWithExtensionsMut::::unpack_uninitialized(&mut mint_data).unwrap(); - mint_state.base = mint_base; - mint_state.pack_base(); - mint_state.init_account_type().unwrap(); - let mut mint_close_authority = mint_state - .init_extension::(true) - .unwrap(); - mint_close_authority.close_authority = - OptionalNonZeroPubkey::try_from(Some(spl_token_pubkey(&mint_authority))).unwrap(); - - let mint_pubkey = Pubkey::new_unique(); - let mint = Account { - lamports: 100, - data: mint_data.to_vec(), - owner: pubkey_from_spl_token(&spl_token_2022::id()), - executable: false, - rent_epoch: 0, - }; - let other_mint_pubkey = Pubkey::new_unique(); - let other_mint = Account { - lamports: 100, - data: mint_data.to_vec(), - owner: Pubkey::new_unique(), - executable: false, - rent_epoch: 0, - }; - - let token_owner = Pubkey::new_unique(); - let token_base = TokenAccount { - mint: spl_token_pubkey(&mint_pubkey), - owner: spl_token_pubkey(&token_owner), - amount: 42, - delegate: COption::None, - state: spl_token_2022::state::AccountState::Initialized, - is_native: COption::Some(100), - delegated_amount: 0, - close_authority: COption::None, - }; - let account_size = ExtensionType::get_account_len::(&[ - ExtensionType::ImmutableOwner, - ExtensionType::MemoTransfer, - ]); - let mut account_data = vec![0; account_size]; - let mut account_state = - StateWithExtensionsMut::::unpack_uninitialized(&mut account_data) - .unwrap(); - account_state.base = token_base; - account_state.pack_base(); - account_state.init_account_type().unwrap(); - account_state - .init_extension::(true) - .unwrap(); - let mut memo_transfer = account_state.init_extension::(true).unwrap(); - memo_transfer.require_incoming_transfer_memos = true.into(); - - let spl_token_account = Account { - lamports: 100, - data: account_data.to_vec(), - owner: pubkey_from_spl_token(&spl_token_2022::id()), - executable: false, - rent_epoch: 0, - }; - let other_account = Account { - lamports: 100, - data: account_data.to_vec(), - owner: Pubkey::new_unique(), - executable: false, - rent_epoch: 0, - }; - - let other_mint_token_base = TokenAccount { - mint: spl_token_pubkey(&other_mint_pubkey), - owner: spl_token_pubkey(&token_owner), - amount: 42, - delegate: COption::None, - state: spl_token_2022::state::AccountState::Initialized, - is_native: COption::Some(100), - delegated_amount: 0, - close_authority: COption::None, - }; - let account_size = ExtensionType::get_account_len::(&[ - ExtensionType::ImmutableOwner, - ExtensionType::MemoTransfer, - ]); - let mut account_data = vec![0; account_size]; - let mut account_state = - StateWithExtensionsMut::::unpack_uninitialized(&mut account_data) - .unwrap(); - account_state.base = other_mint_token_base; - account_state.pack_base(); - account_state.init_account_type().unwrap(); - account_state - .init_extension::(true) - .unwrap(); - let mut memo_transfer = account_state.init_extension::(true).unwrap(); - memo_transfer.require_incoming_transfer_memos = true.into(); - - let other_mint_token_account = Account { - lamports: 100, - data: account_data.to_vec(), - owner: pubkey_from_spl_token(&spl_token_2022::id()), - executable: false, - rent_epoch: 0, - }; - - let mut accounts = BTreeMap::new(); - - let account_pubkey = Pubkey::new_unique(); - accounts.insert(account_pubkey, account); - accounts.insert(mint_pubkey, mint); - accounts.insert(other_mint_pubkey, other_mint); - let spl_token_account_pubkey = Pubkey::new_unique(); - accounts.insert(spl_token_account_pubkey, spl_token_account); - let other_account_pubkey = Pubkey::new_unique(); - accounts.insert(other_account_pubkey, other_account); - let other_mint_account_pubkey = Pubkey::new_unique(); - accounts.insert(other_mint_account_pubkey, other_mint_token_account); - - genesis_config.accounts = accounts; - - let bank = Bank::new_for_tests(&genesis_config); - let mut mint_decimals = HashMap::new(); - - // Account is not owned by spl_token (nor does it have TokenAccount state) - assert_eq!( - collect_token_balance_from_account(&bank, &account_pubkey, &mut mint_decimals), - None - ); - - // Mint does not have TokenAccount state - assert_eq!( - collect_token_balance_from_account(&bank, &mint_pubkey, &mut mint_decimals), - None - ); - - // TokenAccount owned by spl_token_2022::id() works - assert_eq!( - collect_token_balance_from_account( - &bank, - &spl_token_account_pubkey, - &mut mint_decimals - ), - Some(TokenBalanceData { - mint: mint_pubkey.to_string(), - owner: token_owner.to_string(), - ui_token_amount: UiTokenAmount { - ui_amount: Some(0.42), - decimals: 2, - amount: "42".to_string(), - ui_amount_string: "0.42".to_string(), - }, - program_id: spl_token_2022::id().to_string(), - }) - ); - - // TokenAccount is not owned by known spl-token program_id - assert_eq!( - collect_token_balance_from_account(&bank, &other_account_pubkey, &mut mint_decimals), - None - ); - - // TokenAccount's mint is not owned by known spl-token program_id - assert_eq!( - collect_token_balance_from_account( - &bank, - &other_mint_account_pubkey, - &mut mint_decimals - ), - None - ); - } -}