diff --git a/Cargo.lock b/Cargo.lock index d53d445b01..25be241e4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3207,11 +3207,14 @@ dependencies = [ "base64 0.12.3", "bincode", "bs58", + "bv", "lazy_static", "serde", "serde_derive", "serde_json", + "solana-config-program", "solana-sdk 1.4.0", + "solana-stake-program", "solana-vote-program", "spl-token", "thiserror", diff --git a/account-decoder/Cargo.toml b/account-decoder/Cargo.toml index 84852695db..24caf23cd2 100644 --- a/account-decoder/Cargo.toml +++ b/account-decoder/Cargo.toml @@ -12,9 +12,12 @@ edition = "2018" base64 = "0.12.3" bincode = "1.3.1" bs58 = "0.3.1" +bv = "0.11.1" Inflector = "0.11.4" lazy_static = "1.4.0" +solana-config-program = { path = "../programs/config", version = "1.4.0" } solana-sdk = { path = "../sdk", version = "1.4.0" } +solana-stake-program = { path = "../programs/stake", version = "1.4.0" } solana-vote-program = { path = "../programs/vote", version = "1.4.0" } spl-token-v1-0 = { package = "spl-token", version = "1.0.6", features = ["skip-no-mangle"] } serde = "1.0.112" diff --git a/account-decoder/src/lib.rs b/account-decoder/src/lib.rs index d9d6a10340..108e1c1b32 100644 --- a/account-decoder/src/lib.rs +++ b/account-decoder/src/lib.rs @@ -4,12 +4,16 @@ extern crate lazy_static; extern crate serde_derive; pub mod parse_account_data; +pub mod parse_config; pub mod parse_nonce; +pub mod parse_stake; +pub mod parse_sysvar; pub mod parse_token; pub mod parse_vote; +pub mod validator_info; use crate::parse_account_data::{parse_account_data, AccountAdditionalData, ParsedAccount}; -use solana_sdk::{account::Account, clock::Epoch, pubkey::Pubkey}; +use solana_sdk::{account::Account, clock::Epoch, fee_calculator::FeeCalculator, pubkey::Pubkey}; use std::str::FromStr; pub type StringAmount = String; @@ -49,6 +53,7 @@ pub enum UiAccountEncoding { impl UiAccount { pub fn encode( + pubkey: &Pubkey, account: Account, encoding: UiAccountEncoding, additional_data: Option, @@ -58,7 +63,7 @@ impl UiAccount { UiAccountEncoding::Binary64 => UiAccountData::Binary64(base64::encode(account.data)), UiAccountEncoding::JsonParsed => { if let Ok(parsed_data) = - parse_account_data(&account.owner, &account.data, additional_data) + parse_account_data(pubkey, &account.owner, &account.data, additional_data) { UiAccountData::Json(parsed_data) } else { @@ -90,3 +95,25 @@ impl UiAccount { }) } } + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiFeeCalculator { + pub lamports_per_signature: StringAmount, +} + +impl From for UiFeeCalculator { + fn from(fee_calculator: FeeCalculator) -> Self { + Self { + lamports_per_signature: fee_calculator.lamports_per_signature.to_string(), + } + } +} + +impl Default for UiFeeCalculator { + fn default() -> Self { + Self { + lamports_per_signature: "0".to_string(), + } + } +} diff --git a/account-decoder/src/parse_account_data.rs b/account-decoder/src/parse_account_data.rs index a196726238..e55c2632f5 100644 --- a/account-decoder/src/parse_account_data.rs +++ b/account-decoder/src/parse_account_data.rs @@ -1,22 +1,31 @@ use crate::{ + parse_config::parse_config, parse_nonce::parse_nonce, + parse_stake::parse_stake, + parse_sysvar::parse_sysvar, parse_token::{parse_token, spl_token_id_v1_0}, parse_vote::parse_vote, }; use inflector::Inflector; use serde_json::Value; -use solana_sdk::{instruction::InstructionError, pubkey::Pubkey, system_program}; +use solana_sdk::{instruction::InstructionError, pubkey::Pubkey, system_program, sysvar}; use std::collections::HashMap; use thiserror::Error; lazy_static! { + static ref CONFIG_PROGRAM_ID: Pubkey = solana_config_program::id(); + static ref STAKE_PROGRAM_ID: Pubkey = solana_stake_program::id(); static ref SYSTEM_PROGRAM_ID: Pubkey = system_program::id(); + static ref SYSVAR_PROGRAM_ID: Pubkey = sysvar::id(); static ref TOKEN_PROGRAM_ID: Pubkey = spl_token_id_v1_0(); static ref VOTE_PROGRAM_ID: Pubkey = solana_vote_program::id(); pub static ref PARSABLE_PROGRAM_IDS: HashMap = { let mut m = HashMap::new(); + m.insert(*CONFIG_PROGRAM_ID, ParsableAccount::Config); m.insert(*SYSTEM_PROGRAM_ID, ParsableAccount::Nonce); m.insert(*TOKEN_PROGRAM_ID, ParsableAccount::SplToken); + m.insert(*STAKE_PROGRAM_ID, ParsableAccount::Stake); + m.insert(*SYSVAR_PROGRAM_ID, ParsableAccount::Sysvar); m.insert(*VOTE_PROGRAM_ID, ParsableAccount::Vote); m }; @@ -50,8 +59,11 @@ pub struct ParsedAccount { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum ParsableAccount { + Config, Nonce, SplToken, + Stake, + Sysvar, Vote, } @@ -61,6 +73,7 @@ pub struct AccountAdditionalData { } pub fn parse_account_data( + pubkey: &Pubkey, program_id: &Pubkey, data: &[u8], additional_data: Option, @@ -70,10 +83,13 @@ pub fn parse_account_data( .ok_or_else(|| ParseAccountError::ProgramNotParsable)?; let additional_data = additional_data.unwrap_or_default(); let parsed_json = match program_name { + ParsableAccount::Config => serde_json::to_value(parse_config(data, pubkey)?)?, ParsableAccount::Nonce => serde_json::to_value(parse_nonce(data)?)?, ParsableAccount::SplToken => { serde_json::to_value(parse_token(data, additional_data.spl_token_decimals)?)? } + ParsableAccount::Stake => serde_json::to_value(parse_stake(data)?)?, + ParsableAccount::Sysvar => serde_json::to_value(parse_sysvar(data, pubkey)?)?, ParsableAccount::Vote => serde_json::to_value(parse_vote(data)?)?, }; Ok(ParsedAccount { @@ -93,21 +109,33 @@ mod test { #[test] fn test_parse_account_data() { + let account_pubkey = Pubkey::new_rand(); let other_program = Pubkey::new_rand(); let data = vec![0; 4]; - assert!(parse_account_data(&other_program, &data, None).is_err()); + assert!(parse_account_data(&account_pubkey, &other_program, &data, None).is_err()); let vote_state = VoteState::default(); let mut vote_account_data: Vec = vec![0; VoteState::size_of()]; let versioned = VoteStateVersions::Current(Box::new(vote_state)); VoteState::serialize(&versioned, &mut vote_account_data).unwrap(); - let parsed = - parse_account_data(&solana_vote_program::id(), &vote_account_data, None).unwrap(); + let parsed = parse_account_data( + &account_pubkey, + &solana_vote_program::id(), + &vote_account_data, + None, + ) + .unwrap(); assert_eq!(parsed.program, "vote".to_string()); let nonce_data = Versions::new_current(State::Initialized(Data::default())); let nonce_account_data = bincode::serialize(&nonce_data).unwrap(); - let parsed = parse_account_data(&system_program::id(), &nonce_account_data, None).unwrap(); + let parsed = parse_account_data( + &account_pubkey, + &system_program::id(), + &nonce_account_data, + None, + ) + .unwrap(); assert_eq!(parsed.program, "nonce".to_string()); } } diff --git a/account-decoder/src/parse_config.rs b/account-decoder/src/parse_config.rs new file mode 100644 index 0000000000..f17702ef84 --- /dev/null +++ b/account-decoder/src/parse_config.rs @@ -0,0 +1,146 @@ +use crate::{ + parse_account_data::{ParsableAccount, ParseAccountError}, + validator_info, +}; +use bincode::deserialize; +use serde_json::Value; +use solana_config_program::{get_config_data, ConfigKeys}; +use solana_sdk::pubkey::Pubkey; +use solana_stake_program::config::Config as StakeConfig; + +pub fn parse_config(data: &[u8], pubkey: &Pubkey) -> Result { + let parsed_account = if pubkey == &solana_stake_program::config::id() { + get_config_data(data) + .ok() + .and_then(|data| deserialize::(data).ok()) + .map(|config| ConfigAccountType::StakeConfig(config.into())) + } else { + deserialize::(data).ok().and_then(|key_list| { + if !key_list.keys.is_empty() && key_list.keys[0].0 == validator_info::id() { + parse_config_data::(data, key_list.keys).and_then(|validator_info| { + Some(ConfigAccountType::ValidatorInfo(UiConfig { + keys: validator_info.keys, + config_data: serde_json::from_str(&validator_info.config_data).ok()?, + })) + }) + } else { + None + } + }) + }; + parsed_account.ok_or(ParseAccountError::AccountNotParsable( + ParsableAccount::Config, + )) +} + +fn parse_config_data(data: &[u8], keys: Vec<(Pubkey, bool)>) -> Option> +where + T: serde::de::DeserializeOwned, +{ + let config_data: T = deserialize(&get_config_data(data).ok()?).ok()?; + let keys = keys + .iter() + .map(|key| UiConfigKey { + pubkey: key.0.to_string(), + signer: key.1, + }) + .collect(); + Some(UiConfig { keys, config_data }) +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase", tag = "type", content = "info")] +pub enum ConfigAccountType { + StakeConfig(UiStakeConfig), + ValidatorInfo(UiConfig), +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiConfigKey { + pub pubkey: String, + pub signer: bool, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiStakeConfig { + pub warmup_cooldown_rate: f64, + pub slash_penalty: u8, +} + +impl From for UiStakeConfig { + fn from(config: StakeConfig) -> Self { + Self { + warmup_cooldown_rate: config.warmup_cooldown_rate, + slash_penalty: config.slash_penalty, + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiConfig { + pub keys: Vec, + pub config_data: T, +} + +#[cfg(test)] +mod test { + use super::*; + use crate::validator_info::ValidatorInfo; + use serde_json::json; + use solana_config_program::create_config_account; + + #[test] + fn test_parse_config() { + let stake_config = StakeConfig { + warmup_cooldown_rate: 0.25, + slash_penalty: 50, + }; + let stake_config_account = create_config_account(vec![], &stake_config, 10); + assert_eq!( + parse_config( + &stake_config_account.data, + &solana_stake_program::config::id() + ) + .unwrap(), + ConfigAccountType::StakeConfig(UiStakeConfig { + warmup_cooldown_rate: 0.25, + slash_penalty: 50, + }), + ); + + let validator_info = ValidatorInfo { + info: serde_json::to_string(&json!({ + "name": "Solana", + })) + .unwrap(), + }; + let info_pubkey = Pubkey::new_rand(); + let validator_info_config_account = create_config_account( + vec![(validator_info::id(), false), (info_pubkey, true)], + &validator_info, + 10, + ); + assert_eq!( + parse_config(&validator_info_config_account.data, &info_pubkey).unwrap(), + ConfigAccountType::ValidatorInfo(UiConfig { + keys: vec![ + UiConfigKey { + pubkey: validator_info::id().to_string(), + signer: false, + }, + UiConfigKey { + pubkey: info_pubkey.to_string(), + signer: true, + } + ], + config_data: serde_json::from_str(r#"{"name":"Solana"}"#).unwrap(), + }), + ); + + let bad_data = vec![0; 4]; + assert!(parse_config(&bad_data, &info_pubkey).is_err()); + } +} diff --git a/account-decoder/src/parse_nonce.rs b/account-decoder/src/parse_nonce.rs index 6693350d4f..f75766c77a 100644 --- a/account-decoder/src/parse_nonce.rs +++ b/account-decoder/src/parse_nonce.rs @@ -1,6 +1,5 @@ -use crate::parse_account_data::ParseAccountError; +use crate::{parse_account_data::ParseAccountError, UiFeeCalculator}; use solana_sdk::{ - fee_calculator::FeeCalculator, instruction::InstructionError, nonce::{state::Versions, State}, }; @@ -14,7 +13,7 @@ pub fn parse_nonce(data: &[u8]) -> Result { State::Initialized(data) => Ok(UiNonceState::Initialized(UiNonceData { authority: data.authority.to_string(), blockhash: data.blockhash.to_string(), - fee_calculator: data.fee_calculator, + fee_calculator: data.fee_calculator.into(), })), } } @@ -32,7 +31,7 @@ pub enum UiNonceState { pub struct UiNonceData { pub authority: String, pub blockhash: String, - pub fee_calculator: FeeCalculator, + pub fee_calculator: UiFeeCalculator, } #[cfg(test)] @@ -56,7 +55,9 @@ mod test { UiNonceState::Initialized(UiNonceData { authority: Pubkey::default().to_string(), blockhash: Hash::default().to_string(), - fee_calculator: FeeCalculator::default(), + fee_calculator: UiFeeCalculator { + lamports_per_signature: 0.to_string(), + }, }), ); diff --git a/account-decoder/src/parse_stake.rs b/account-decoder/src/parse_stake.rs new file mode 100644 index 0000000000..86d01e1067 --- /dev/null +++ b/account-decoder/src/parse_stake.rs @@ -0,0 +1,235 @@ +use crate::{ + parse_account_data::{ParsableAccount, ParseAccountError}, + StringAmount, +}; +use bincode::deserialize; +use solana_sdk::clock::{Epoch, UnixTimestamp}; +use solana_stake_program::stake_state::{Authorized, Delegation, Lockup, Meta, Stake, StakeState}; + +pub fn parse_stake(data: &[u8]) -> Result { + let stake_state: StakeState = deserialize(data) + .map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::Stake))?; + let parsed_account = match stake_state { + StakeState::Uninitialized => StakeAccountType::Uninitialized, + StakeState::Initialized(meta) => StakeAccountType::Initialized(UiStakeAccount { + meta: meta.into(), + stake: None, + }), + StakeState::Stake(meta, stake) => StakeAccountType::Delegated(UiStakeAccount { + meta: meta.into(), + stake: Some(stake.into()), + }), + StakeState::RewardsPool => StakeAccountType::RewardsPool, + }; + Ok(parsed_account) +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase", tag = "type", content = "info")] +pub enum StakeAccountType { + Uninitialized, + Initialized(UiStakeAccount), + Delegated(UiStakeAccount), + RewardsPool, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiStakeAccount { + pub meta: UiMeta, + pub stake: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiMeta { + pub rent_exempt_reserve: StringAmount, + pub authorized: UiAuthorized, + pub lockup: UiLockup, +} + +impl From for UiMeta { + fn from(meta: Meta) -> Self { + Self { + rent_exempt_reserve: meta.rent_exempt_reserve.to_string(), + authorized: meta.authorized.into(), + lockup: meta.lockup.into(), + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiLockup { + pub unix_timestamp: UnixTimestamp, + pub epoch: Epoch, + pub custodian: String, +} + +impl From for UiLockup { + fn from(lockup: Lockup) -> Self { + Self { + unix_timestamp: lockup.unix_timestamp, + epoch: lockup.epoch, + custodian: lockup.custodian.to_string(), + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiAuthorized { + pub staker: String, + pub withdrawer: String, +} + +impl From for UiAuthorized { + fn from(authorized: Authorized) -> Self { + Self { + staker: authorized.staker.to_string(), + withdrawer: authorized.withdrawer.to_string(), + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiStake { + pub delegation: UiDelegation, + pub credits_observed: u64, +} + +impl From for UiStake { + fn from(stake: Stake) -> Self { + Self { + delegation: stake.delegation.into(), + credits_observed: stake.credits_observed, + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiDelegation { + pub voter: String, + pub stake: StringAmount, + pub activation_epoch: StringAmount, + pub deactivation_epoch: StringAmount, + pub warmup_cooldown_rate: f64, +} + +impl From for UiDelegation { + fn from(delegation: Delegation) -> Self { + Self { + voter: delegation.voter_pubkey.to_string(), + stake: delegation.stake.to_string(), + activation_epoch: delegation.activation_epoch.to_string(), + deactivation_epoch: delegation.deactivation_epoch.to_string(), + warmup_cooldown_rate: delegation.warmup_cooldown_rate, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use bincode::serialize; + use solana_sdk::pubkey::Pubkey; + + #[test] + fn test_parse_stake() { + let stake_state = StakeState::Uninitialized; + let stake_data = serialize(&stake_state).unwrap(); + assert_eq!( + parse_stake(&stake_data).unwrap(), + StakeAccountType::Uninitialized + ); + + let pubkey = Pubkey::new_rand(); + let custodian = Pubkey::new_rand(); + let authorized = Authorized::auto(&pubkey); + let lockup = Lockup { + unix_timestamp: 0, + epoch: 1, + custodian, + }; + let meta = Meta { + rent_exempt_reserve: 42, + authorized, + lockup, + }; + + let stake_state = StakeState::Initialized(meta); + let stake_data = serialize(&stake_state).unwrap(); + assert_eq!( + parse_stake(&stake_data).unwrap(), + StakeAccountType::Initialized(UiStakeAccount { + meta: UiMeta { + rent_exempt_reserve: 42.to_string(), + authorized: UiAuthorized { + staker: pubkey.to_string(), + withdrawer: pubkey.to_string(), + }, + lockup: UiLockup { + unix_timestamp: 0, + epoch: 1, + custodian: custodian.to_string(), + } + }, + stake: None, + }) + ); + + let voter_pubkey = Pubkey::new_rand(); + let stake = Stake { + delegation: Delegation { + voter_pubkey, + stake: 20, + activation_epoch: 2, + deactivation_epoch: std::u64::MAX, + warmup_cooldown_rate: 0.25, + }, + credits_observed: 10, + }; + + let stake_state = StakeState::Stake(meta, stake); + let stake_data = serialize(&stake_state).unwrap(); + assert_eq!( + parse_stake(&stake_data).unwrap(), + StakeAccountType::Delegated(UiStakeAccount { + meta: UiMeta { + rent_exempt_reserve: 42.to_string(), + authorized: UiAuthorized { + staker: pubkey.to_string(), + withdrawer: pubkey.to_string(), + }, + lockup: UiLockup { + unix_timestamp: 0, + epoch: 1, + custodian: custodian.to_string(), + } + }, + stake: Some(UiStake { + delegation: UiDelegation { + voter: voter_pubkey.to_string(), + stake: 20.to_string(), + activation_epoch: 2.to_string(), + deactivation_epoch: std::u64::MAX.to_string(), + warmup_cooldown_rate: 0.25, + }, + credits_observed: 10, + }) + }) + ); + + let stake_state = StakeState::RewardsPool; + let stake_data = serialize(&stake_state).unwrap(); + assert_eq!( + parse_stake(&stake_data).unwrap(), + StakeAccountType::RewardsPool + ); + + let bad_data = vec![1, 2, 3, 4]; + assert!(parse_stake(&bad_data).is_err()); + } +} diff --git a/account-decoder/src/parse_sysvar.rs b/account-decoder/src/parse_sysvar.rs new file mode 100644 index 0000000000..9f9436830d --- /dev/null +++ b/account-decoder/src/parse_sysvar.rs @@ -0,0 +1,328 @@ +use crate::{ + parse_account_data::{ParsableAccount, ParseAccountError}, + StringAmount, UiFeeCalculator, +}; +use bincode::deserialize; +use bv::BitVec; +use solana_sdk::{ + clock::{Clock, Epoch, Slot, UnixTimestamp}, + epoch_schedule::EpochSchedule, + pubkey::Pubkey, + rent::Rent, + slot_hashes::SlotHashes, + slot_history::{self, SlotHistory}, + stake_history::{StakeHistory, StakeHistoryEntry}, + sysvar::{self, fees::Fees, recent_blockhashes::RecentBlockhashes, rewards::Rewards}, +}; + +pub fn parse_sysvar(data: &[u8], pubkey: &Pubkey) -> Result { + let parsed_account = { + if pubkey == &sysvar::clock::id() { + deserialize::(data) + .ok() + .map(|clock| SysvarAccountType::Clock(clock.into())) + } else if pubkey == &sysvar::epoch_schedule::id() { + deserialize(data).ok().map(SysvarAccountType::EpochSchedule) + } else if pubkey == &sysvar::fees::id() { + deserialize::(data) + .ok() + .map(|fees| SysvarAccountType::Fees(fees.into())) + } else if pubkey == &sysvar::recent_blockhashes::id() { + deserialize::(data) + .ok() + .map(|recent_blockhashes| { + let recent_blockhashes = recent_blockhashes + .iter() + .map(|entry| UiRecentBlockhashesEntry { + blockhash: entry.blockhash.to_string(), + fee_calculator: entry.fee_calculator.clone().into(), + }) + .collect(); + SysvarAccountType::RecentBlockhashes(recent_blockhashes) + }) + } else if pubkey == &sysvar::rent::id() { + deserialize::(data) + .ok() + .map(|rent| SysvarAccountType::Rent(rent.into())) + } else if pubkey == &sysvar::rewards::id() { + deserialize::(data) + .ok() + .map(|rewards| SysvarAccountType::Rewards(rewards.into())) + } else if pubkey == &sysvar::slot_hashes::id() { + deserialize::(data).ok().map(|slot_hashes| { + let slot_hashes = slot_hashes + .iter() + .map(|slot_hash| UiSlotHashEntry { + slot: slot_hash.0, + hash: slot_hash.1.to_string(), + }) + .collect(); + SysvarAccountType::SlotHashes(slot_hashes) + }) + } else if pubkey == &sysvar::slot_history::id() { + deserialize::(data).ok().map(|slot_history| { + SysvarAccountType::SlotHistory(UiSlotHistory { + next_slot: slot_history.next_slot, + bits: format!("{:?}", SlotHistoryBits(slot_history.bits)), + }) + }) + } else if pubkey == &sysvar::stake_history::id() { + deserialize::(data).ok().map(|stake_history| { + let stake_history = stake_history + .iter() + .map(|entry| UiStakeHistoryEntry { + epoch: entry.0, + stake_history: entry.1.clone(), + }) + .collect(); + SysvarAccountType::StakeHistory(stake_history) + }) + } else { + None + } + }; + parsed_account.ok_or(ParseAccountError::AccountNotParsable( + ParsableAccount::Sysvar, + )) +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase", tag = "type", content = "info")] +pub enum SysvarAccountType { + Clock(UiClock), + EpochSchedule(EpochSchedule), + Fees(UiFees), + RecentBlockhashes(Vec), + Rent(UiRent), + Rewards(UiRewards), + SlotHashes(Vec), + SlotHistory(UiSlotHistory), + StakeHistory(Vec), +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +pub struct UiClock { + pub slot: Slot, + pub epoch: Epoch, + pub leader_schedule_epoch: Epoch, + pub unix_timestamp: UnixTimestamp, +} + +impl From for UiClock { + fn from(clock: Clock) -> Self { + Self { + slot: clock.slot, + epoch: clock.epoch, + leader_schedule_epoch: clock.leader_schedule_epoch, + unix_timestamp: clock.unix_timestamp, + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +pub struct UiFees { + pub fee_calculator: UiFeeCalculator, +} +impl From for UiFees { + fn from(fees: Fees) -> Self { + Self { + fee_calculator: fees.fee_calculator.into(), + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +pub struct UiRent { + pub lamports_per_byte_year: StringAmount, + pub exemption_threshold: f64, + pub burn_percent: u8, +} + +impl From for UiRent { + fn from(rent: Rent) -> Self { + Self { + lamports_per_byte_year: rent.lamports_per_byte_year.to_string(), + exemption_threshold: rent.exemption_threshold, + burn_percent: rent.burn_percent, + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +pub struct UiRewards { + pub validator_point_value: f64, +} + +impl From for UiRewards { + fn from(rewards: Rewards) -> Self { + Self { + validator_point_value: rewards.validator_point_value, + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiRecentBlockhashesEntry { + pub blockhash: String, + pub fee_calculator: UiFeeCalculator, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiSlotHashEntry { + pub slot: Slot, + pub hash: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiSlotHistory { + pub next_slot: Slot, + pub bits: String, +} + +struct SlotHistoryBits(BitVec); + +impl std::fmt::Debug for SlotHistoryBits { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for i in 0..slot_history::MAX_ENTRIES { + if self.0.get(i) { + write!(f, "1")?; + } else { + write!(f, "0")?; + } + } + Ok(()) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiStakeHistoryEntry { + pub epoch: Epoch, + pub stake_history: StakeHistoryEntry, +} + +#[cfg(test)] +mod test { + use super::*; + use solana_sdk::{ + fee_calculator::FeeCalculator, + hash::Hash, + sysvar::{recent_blockhashes::IterItem, Sysvar}, + }; + use std::iter::FromIterator; + + #[test] + fn test_parse_sysvars() { + let clock_sysvar = Clock::default().create_account(1); + assert_eq!( + parse_sysvar(&clock_sysvar.data, &sysvar::clock::id()).unwrap(), + SysvarAccountType::Clock(UiClock::default()), + ); + + let epoch_schedule = EpochSchedule { + slots_per_epoch: 12, + leader_schedule_slot_offset: 0, + warmup: false, + first_normal_epoch: 1, + first_normal_slot: 12, + }; + let epoch_schedule_sysvar = epoch_schedule.create_account(1); + assert_eq!( + parse_sysvar(&epoch_schedule_sysvar.data, &sysvar::epoch_schedule::id()).unwrap(), + SysvarAccountType::EpochSchedule(epoch_schedule), + ); + + let fees_sysvar = Fees::default().create_account(1); + assert_eq!( + parse_sysvar(&fees_sysvar.data, &sysvar::fees::id()).unwrap(), + SysvarAccountType::Fees(UiFees::default()), + ); + + let hash = Hash::new(&[1; 32]); + let fee_calculator = FeeCalculator { + lamports_per_signature: 10, + }; + let recent_blockhashes = + RecentBlockhashes::from_iter(vec![IterItem(0, &hash, &fee_calculator)].into_iter()); + let recent_blockhashes_sysvar = recent_blockhashes.create_account(1); + assert_eq!( + parse_sysvar( + &recent_blockhashes_sysvar.data, + &sysvar::recent_blockhashes::id() + ) + .unwrap(), + SysvarAccountType::RecentBlockhashes(vec![UiRecentBlockhashesEntry { + blockhash: hash.to_string(), + fee_calculator: fee_calculator.into(), + }]), + ); + + let rent = Rent { + lamports_per_byte_year: 10, + exemption_threshold: 2.0, + burn_percent: 5, + }; + let rent_sysvar = rent.create_account(1); + assert_eq!( + parse_sysvar(&rent_sysvar.data, &sysvar::rent::id()).unwrap(), + SysvarAccountType::Rent(rent.into()), + ); + + let rewards_sysvar = Rewards::default().create_account(1); + assert_eq!( + parse_sysvar(&rewards_sysvar.data, &sysvar::rewards::id()).unwrap(), + SysvarAccountType::Rewards(UiRewards::default()), + ); + + let mut slot_hashes = SlotHashes::default(); + slot_hashes.add(1, hash); + let slot_hashes_sysvar = slot_hashes.create_account(1); + assert_eq!( + parse_sysvar(&slot_hashes_sysvar.data, &sysvar::slot_hashes::id()).unwrap(), + SysvarAccountType::SlotHashes(vec![UiSlotHashEntry { + slot: 1, + hash: hash.to_string(), + }]), + ); + + let mut slot_history = SlotHistory::default(); + slot_history.add(42); + let slot_history_sysvar = slot_history.create_account(1); + assert_eq!( + parse_sysvar(&slot_history_sysvar.data, &sysvar::slot_history::id()).unwrap(), + SysvarAccountType::SlotHistory(UiSlotHistory { + next_slot: slot_history.next_slot, + bits: format!("{:?}", SlotHistoryBits(slot_history.bits)), + }), + ); + + let mut stake_history = StakeHistory::default(); + let stake_history_entry = StakeHistoryEntry { + effective: 10, + activating: 2, + deactivating: 3, + }; + stake_history.add(1, stake_history_entry.clone()); + let stake_history_sysvar = stake_history.create_account(1); + assert_eq!( + parse_sysvar(&stake_history_sysvar.data, &sysvar::stake_history::id()).unwrap(), + SysvarAccountType::StakeHistory(vec![UiStakeHistoryEntry { + epoch: 1, + stake_history: stake_history_entry, + }]), + ); + + let bad_pubkey = Pubkey::new_rand(); + assert!(parse_sysvar(&stake_history_sysvar.data, &bad_pubkey).is_err()); + + let bad_data = vec![0; 4]; + assert!(parse_sysvar(&bad_data, &sysvar::stake_history::id()).is_err()); + } +} diff --git a/account-decoder/src/parse_vote.rs b/account-decoder/src/parse_vote.rs index 70cd691af1..2ff9d4dd13 100644 --- a/account-decoder/src/parse_vote.rs +++ b/account-decoder/src/parse_vote.rs @@ -1,4 +1,4 @@ -use crate::parse_account_data::ParseAccountError; +use crate::{parse_account_data::ParseAccountError, StringAmount}; use solana_sdk::{ clock::{Epoch, Slot}, pubkey::Pubkey, @@ -12,8 +12,8 @@ pub fn parse_vote(data: &[u8]) -> Result { .iter() .map(|(epoch, credits, previous_credits)| UiEpochCredits { epoch: *epoch, - credits: *credits, - previous_credits: *previous_credits, + credits: credits.to_string(), + previous_credits: previous_credits.to_string(), }) .collect(); let votes = vote_state @@ -115,8 +115,8 @@ struct UiPriorVoters { #[serde(rename_all = "camelCase")] struct UiEpochCredits { epoch: Epoch, - credits: u64, - previous_credits: u64, + credits: StringAmount, + previous_credits: StringAmount, } #[cfg(test)] diff --git a/account-decoder/src/validator_info.rs b/account-decoder/src/validator_info.rs new file mode 100644 index 0000000000..85a98286bc --- /dev/null +++ b/account-decoder/src/validator_info.rs @@ -0,0 +1,18 @@ +use solana_config_program::ConfigState; + +pub const MAX_SHORT_FIELD_LENGTH: usize = 70; +pub const MAX_LONG_FIELD_LENGTH: usize = 300; +pub const MAX_VALIDATOR_INFO: u64 = 576; + +solana_sdk::declare_id!("Va1idator1nfo111111111111111111111111111111"); + +#[derive(Debug, Deserialize, PartialEq, Serialize, Default)] +pub struct ValidatorInfo { + pub info: String, +} + +impl ConfigState for ValidatorInfo { + fn max_space() -> u64 { + MAX_VALIDATOR_INFO + } +} diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 2119233778..936350ae10 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1120,7 +1120,7 @@ fn process_show_account( let cli_account = CliAccount { keyed_account: RpcKeyedAccount { pubkey: account_pubkey.to_string(), - account: UiAccount::encode(account, UiAccountEncoding::Binary, None), + account: UiAccount::encode(account_pubkey, account, UiAccountEncoding::Binary, None), }, use_lamports_unit, }; diff --git a/cli/src/offline/blockhash_query.rs b/cli/src/offline/blockhash_query.rs index d2da79e091..c15de0b241 100644 --- a/cli/src/offline/blockhash_query.rs +++ b/cli/src/offline/blockhash_query.rs @@ -350,7 +350,12 @@ mod tests { ) .unwrap(); let nonce_pubkey = Pubkey::new(&[4u8; 32]); - let rpc_nonce_account = UiAccount::encode(nonce_account, UiAccountEncoding::Binary64, None); + let rpc_nonce_account = UiAccount::encode( + &nonce_pubkey, + nonce_account, + UiAccountEncoding::Binary64, + None, + ); let get_account_response = json!(Response { context: RpcResponseContext { slot: 1 }, value: json!(Some(rpc_nonce_account)), diff --git a/cli/src/validator_info.rs b/cli/src/validator_info.rs index 207a2df061..c94f661d50 100644 --- a/cli/src/validator_info.rs +++ b/cli/src/validator_info.rs @@ -6,9 +6,10 @@ use crate::{ use bincode::deserialize; use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; use reqwest::blocking::Client; -use serde_derive::{Deserialize, Serialize}; use serde_json::{Map, Value}; - +use solana_account_decoder::validator_info::{ + self, ValidatorInfo, MAX_LONG_FIELD_LENGTH, MAX_SHORT_FIELD_LENGTH, +}; use solana_clap_utils::{ input_parsers::pubkey_of, input_validators::{is_pubkey, is_url}, @@ -27,23 +28,6 @@ use solana_sdk::{ }; use std::{error, sync::Arc}; -pub const MAX_SHORT_FIELD_LENGTH: usize = 70; -pub const MAX_LONG_FIELD_LENGTH: usize = 300; -pub const MAX_VALIDATOR_INFO: u64 = 576; - -solana_sdk::declare_id!("Va1idator1nfo111111111111111111111111111111"); - -#[derive(Debug, Deserialize, PartialEq, Serialize, Default)] -pub struct ValidatorInfo { - info: String, -} - -impl ConfigState for ValidatorInfo { - fn max_space() -> u64 { - MAX_VALIDATOR_INFO - } -} - // Return an error if a validator details are longer than the max length. pub fn check_details_length(string: String) -> Result<(), String> { if string.len() > MAX_LONG_FIELD_LENGTH { @@ -289,7 +273,7 @@ pub fn process_set_validator_info( .iter() .filter(|(_, account)| { let key_list: ConfigKeys = deserialize(&account.data).map_err(|_| false).unwrap(); - key_list.keys.contains(&(id(), false)) + key_list.keys.contains(&(validator_info::id(), false)) }) .find(|(pubkey, account)| { let (validator_pubkey, _) = parse_validator_info(&pubkey, &account).unwrap(); @@ -328,7 +312,10 @@ pub fn process_set_validator_info( }; let build_message = |lamports| { - let keys = vec![(id(), false), (config.signers[0].pubkey(), true)]; + let keys = vec![ + (validator_info::id(), false), + (config.signers[0].pubkey(), true), + ]; if balance == 0 { println!( "Publishing info for Validator {:?}", @@ -401,7 +388,7 @@ pub fn process_get_validator_info( let key_list: ConfigKeys = deserialize(&validator_info_account.data) .map_err(|_| false) .unwrap(); - key_list.keys.contains(&(id(), false)) + key_list.keys.contains(&(validator_info::id(), false)) }) .collect() }; @@ -503,7 +490,7 @@ mod tests { #[test] fn test_parse_validator_info() { let pubkey = Pubkey::new_rand(); - let keys = vec![(id(), false), (pubkey, true)]; + let keys = vec![(validator_info::id(), false), (pubkey, true)]; let config = ConfigKeys { keys }; let mut info = Map::new(); diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 15621220ed..21911c7b4f 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -247,7 +247,7 @@ impl JsonRpcRequestProcessor { let mut response = None; if let Some(account) = bank.get_account(pubkey) { if account.owner == spl_token_id_v1_0() && encoding == UiAccountEncoding::JsonParsed { - response = get_parsed_token_account(bank.clone(), account); + response = get_parsed_token_account(bank.clone(), pubkey, account); } else if encoding == UiAccountEncoding::Binary && account.data.len() > 128 { let message = "Encoded binary (base 58) data should be less than 128 bytes, please use Binary64 encoding.".to_string(); return Err(error::Error { @@ -256,7 +256,7 @@ impl JsonRpcRequestProcessor { data: None, }); } else { - response = Some(UiAccount::encode(account, encoding, None)); + response = Some(UiAccount::encode(pubkey, account, encoding, None)); } } @@ -288,7 +288,7 @@ impl JsonRpcRequestProcessor { keyed_accounts .map(|(pubkey, account)| RpcKeyedAccount { pubkey: pubkey.to_string(), - account: UiAccount::encode(account, encoding.clone(), None), + account: UiAccount::encode(&pubkey, account, encoding.clone(), None), }) .collect() } @@ -1134,7 +1134,7 @@ impl JsonRpcRequestProcessor { keyed_accounts .map(|(pubkey, account)| RpcKeyedAccount { pubkey: pubkey.to_string(), - account: UiAccount::encode(account, encoding.clone(), None), + account: UiAccount::encode(&pubkey, account, encoding.clone(), None), }) .collect() }; @@ -1185,7 +1185,7 @@ impl JsonRpcRequestProcessor { keyed_accounts .map(|(pubkey, account)| RpcKeyedAccount { pubkey: pubkey.to_string(), - account: UiAccount::encode(account, encoding.clone(), None), + account: UiAccount::encode(&pubkey, account, encoding.clone(), None), }) .collect() }; @@ -1242,11 +1242,16 @@ fn get_filtered_program_accounts( }) } -pub(crate) fn get_parsed_token_account(bank: Arc, account: Account) -> Option { +pub(crate) fn get_parsed_token_account( + bank: Arc, + pubkey: &Pubkey, + account: Account, +) -> Option { get_token_account_mint(&account.data) .and_then(|mint_pubkey| get_mint_owner_and_decimals(&bank, &mint_pubkey).ok()) .map(|(_, decimals)| { UiAccount::encode( + pubkey, account, UiAccountEncoding::JsonParsed, Some(AccountAdditionalData { @@ -1274,6 +1279,7 @@ where RpcKeyedAccount { pubkey: pubkey.to_string(), account: UiAccount::encode( + &pubkey, account, UiAccountEncoding::JsonParsed, Some(AccountAdditionalData { spl_token_decimals }), diff --git a/core/src/rpc_pubsub.rs b/core/src/rpc_pubsub.rs index 6b0a43dded..3787f4527e 100644 --- a/core/src/rpc_pubsub.rs +++ b/core/src/rpc_pubsub.rs @@ -672,8 +672,13 @@ mod tests { .get_account(&nonce_account.pubkey()) .unwrap() .data; - let expected_data = - parse_account_data(&system_program::id(), &expected_data, None).unwrap(); + let expected_data = parse_account_data( + &nonce_account.pubkey(), + &system_program::id(), + &expected_data, + None, + ) + .unwrap(); let expected = json!({ "jsonrpc": "2.0", "method": "accountNotification", diff --git a/core/src/rpc_subscriptions.rs b/core/src/rpc_subscriptions.rs index 4993913775..c1b169183d 100644 --- a/core/src/rpc_subscriptions.rs +++ b/core/src/rpc_subscriptions.rs @@ -179,7 +179,7 @@ where K: Eq + Hash + Clone + Copy, S: Clone + Serialize, B: Fn(&Bank, &K) -> X, - F: Fn(X, Slot, Option, Option>) -> (Box>, Slot), + F: Fn(X, &K, Slot, Option, Option>) -> (Box>, Slot), X: Clone + Serialize + Default, T: Clone, { @@ -211,6 +211,7 @@ where let mut w_last_notified_slot = last_notified_slot.write().unwrap(); let (filter_results, result_slot) = filter_results( results, + hashmap_key, *w_last_notified_slot, config.as_ref().cloned(), bank, @@ -245,6 +246,7 @@ impl RpcNotifier { fn filter_account_result( result: Option<(Account, Slot)>, + pubkey: &Pubkey, last_notified_slot: Slot, encoding: Option, bank: Option>, @@ -256,12 +258,14 @@ fn filter_account_result( let encoding = encoding.unwrap_or(UiAccountEncoding::Binary); if account.owner == spl_token_id_v1_0() && encoding == UiAccountEncoding::JsonParsed { let bank = bank.unwrap(); // If result.is_some(), bank must also be Some - if let Some(ui_account) = get_parsed_token_account(bank, account) { + if let Some(ui_account) = get_parsed_token_account(bank, pubkey, account) { return (Box::new(iter::once(ui_account)), fork); } } else { return ( - Box::new(iter::once(UiAccount::encode(account, encoding, None))), + Box::new(iter::once(UiAccount::encode( + pubkey, account, encoding, None, + ))), fork, ); } @@ -272,6 +276,7 @@ fn filter_account_result( fn filter_signature_result( result: Option>, + _signature: &Signature, last_notified_slot: Slot, _config: Option<()>, _bank: Option>, @@ -288,6 +293,7 @@ fn filter_signature_result( fn filter_program_results( accounts: Vec<(Pubkey, Account)>, + _program_id: &Pubkey, last_notified_slot: Slot, config: Option, bank: Option>, @@ -309,7 +315,7 @@ fn filter_program_results( Box::new( keyed_accounts.map(move |(pubkey, account)| RpcKeyedAccount { pubkey: pubkey.to_string(), - account: UiAccount::encode(account, encoding.clone(), None), + account: UiAccount::encode(&pubkey, account, encoding.clone(), None), }), ) };