Decode native-program and sysvar accounts (#11463)
* Pass pubkey in to account-decoder for sysvars * Decode sysvar accounts * Decode config accounts; move validator-info lower * Decode stake accounts * Review comments * Stringify any account lamports and epochs that can be set to u64::MAX
This commit is contained in:
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -3207,11 +3207,14 @@ dependencies = [
|
|||||||
"base64 0.12.3",
|
"base64 0.12.3",
|
||||||
"bincode",
|
"bincode",
|
||||||
"bs58",
|
"bs58",
|
||||||
|
"bv",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"solana-config-program",
|
||||||
"solana-sdk 1.4.0",
|
"solana-sdk 1.4.0",
|
||||||
|
"solana-stake-program",
|
||||||
"solana-vote-program",
|
"solana-vote-program",
|
||||||
"spl-token",
|
"spl-token",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
@ -12,9 +12,12 @@ edition = "2018"
|
|||||||
base64 = "0.12.3"
|
base64 = "0.12.3"
|
||||||
bincode = "1.3.1"
|
bincode = "1.3.1"
|
||||||
bs58 = "0.3.1"
|
bs58 = "0.3.1"
|
||||||
|
bv = "0.11.1"
|
||||||
Inflector = "0.11.4"
|
Inflector = "0.11.4"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
solana-config-program = { path = "../programs/config", version = "1.4.0" }
|
||||||
solana-sdk = { path = "../sdk", 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" }
|
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"] }
|
spl-token-v1-0 = { package = "spl-token", version = "1.0.6", features = ["skip-no-mangle"] }
|
||||||
serde = "1.0.112"
|
serde = "1.0.112"
|
||||||
|
@ -4,12 +4,16 @@ extern crate lazy_static;
|
|||||||
extern crate serde_derive;
|
extern crate serde_derive;
|
||||||
|
|
||||||
pub mod parse_account_data;
|
pub mod parse_account_data;
|
||||||
|
pub mod parse_config;
|
||||||
pub mod parse_nonce;
|
pub mod parse_nonce;
|
||||||
|
pub mod parse_stake;
|
||||||
|
pub mod parse_sysvar;
|
||||||
pub mod parse_token;
|
pub mod parse_token;
|
||||||
pub mod parse_vote;
|
pub mod parse_vote;
|
||||||
|
pub mod validator_info;
|
||||||
|
|
||||||
use crate::parse_account_data::{parse_account_data, AccountAdditionalData, ParsedAccount};
|
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;
|
use std::str::FromStr;
|
||||||
|
|
||||||
pub type StringAmount = String;
|
pub type StringAmount = String;
|
||||||
@ -49,6 +53,7 @@ pub enum UiAccountEncoding {
|
|||||||
|
|
||||||
impl UiAccount {
|
impl UiAccount {
|
||||||
pub fn encode(
|
pub fn encode(
|
||||||
|
pubkey: &Pubkey,
|
||||||
account: Account,
|
account: Account,
|
||||||
encoding: UiAccountEncoding,
|
encoding: UiAccountEncoding,
|
||||||
additional_data: Option<AccountAdditionalData>,
|
additional_data: Option<AccountAdditionalData>,
|
||||||
@ -58,7 +63,7 @@ impl UiAccount {
|
|||||||
UiAccountEncoding::Binary64 => UiAccountData::Binary64(base64::encode(account.data)),
|
UiAccountEncoding::Binary64 => UiAccountData::Binary64(base64::encode(account.data)),
|
||||||
UiAccountEncoding::JsonParsed => {
|
UiAccountEncoding::JsonParsed => {
|
||||||
if let Ok(parsed_data) =
|
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)
|
UiAccountData::Json(parsed_data)
|
||||||
} else {
|
} 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<FeeCalculator> 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,22 +1,31 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
parse_config::parse_config,
|
||||||
parse_nonce::parse_nonce,
|
parse_nonce::parse_nonce,
|
||||||
|
parse_stake::parse_stake,
|
||||||
|
parse_sysvar::parse_sysvar,
|
||||||
parse_token::{parse_token, spl_token_id_v1_0},
|
parse_token::{parse_token, spl_token_id_v1_0},
|
||||||
parse_vote::parse_vote,
|
parse_vote::parse_vote,
|
||||||
};
|
};
|
||||||
use inflector::Inflector;
|
use inflector::Inflector;
|
||||||
use serde_json::Value;
|
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 std::collections::HashMap;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
lazy_static! {
|
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 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 TOKEN_PROGRAM_ID: Pubkey = spl_token_id_v1_0();
|
||||||
static ref VOTE_PROGRAM_ID: Pubkey = solana_vote_program::id();
|
static ref VOTE_PROGRAM_ID: Pubkey = solana_vote_program::id();
|
||||||
pub static ref PARSABLE_PROGRAM_IDS: HashMap<Pubkey, ParsableAccount> = {
|
pub static ref PARSABLE_PROGRAM_IDS: HashMap<Pubkey, ParsableAccount> = {
|
||||||
let mut m = HashMap::new();
|
let mut m = HashMap::new();
|
||||||
|
m.insert(*CONFIG_PROGRAM_ID, ParsableAccount::Config);
|
||||||
m.insert(*SYSTEM_PROGRAM_ID, ParsableAccount::Nonce);
|
m.insert(*SYSTEM_PROGRAM_ID, ParsableAccount::Nonce);
|
||||||
m.insert(*TOKEN_PROGRAM_ID, ParsableAccount::SplToken);
|
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.insert(*VOTE_PROGRAM_ID, ParsableAccount::Vote);
|
||||||
m
|
m
|
||||||
};
|
};
|
||||||
@ -50,8 +59,11 @@ pub struct ParsedAccount {
|
|||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum ParsableAccount {
|
pub enum ParsableAccount {
|
||||||
|
Config,
|
||||||
Nonce,
|
Nonce,
|
||||||
SplToken,
|
SplToken,
|
||||||
|
Stake,
|
||||||
|
Sysvar,
|
||||||
Vote,
|
Vote,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,6 +73,7 @@ pub struct AccountAdditionalData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_account_data(
|
pub fn parse_account_data(
|
||||||
|
pubkey: &Pubkey,
|
||||||
program_id: &Pubkey,
|
program_id: &Pubkey,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
additional_data: Option<AccountAdditionalData>,
|
additional_data: Option<AccountAdditionalData>,
|
||||||
@ -70,10 +83,13 @@ pub fn parse_account_data(
|
|||||||
.ok_or_else(|| ParseAccountError::ProgramNotParsable)?;
|
.ok_or_else(|| ParseAccountError::ProgramNotParsable)?;
|
||||||
let additional_data = additional_data.unwrap_or_default();
|
let additional_data = additional_data.unwrap_or_default();
|
||||||
let parsed_json = match program_name {
|
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::Nonce => serde_json::to_value(parse_nonce(data)?)?,
|
||||||
ParsableAccount::SplToken => {
|
ParsableAccount::SplToken => {
|
||||||
serde_json::to_value(parse_token(data, additional_data.spl_token_decimals)?)?
|
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)?)?,
|
ParsableAccount::Vote => serde_json::to_value(parse_vote(data)?)?,
|
||||||
};
|
};
|
||||||
Ok(ParsedAccount {
|
Ok(ParsedAccount {
|
||||||
@ -93,21 +109,33 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_account_data() {
|
fn test_parse_account_data() {
|
||||||
|
let account_pubkey = Pubkey::new_rand();
|
||||||
let other_program = Pubkey::new_rand();
|
let other_program = Pubkey::new_rand();
|
||||||
let data = vec![0; 4];
|
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 vote_state = VoteState::default();
|
||||||
let mut vote_account_data: Vec<u8> = vec![0; VoteState::size_of()];
|
let mut vote_account_data: Vec<u8> = vec![0; VoteState::size_of()];
|
||||||
let versioned = VoteStateVersions::Current(Box::new(vote_state));
|
let versioned = VoteStateVersions::Current(Box::new(vote_state));
|
||||||
VoteState::serialize(&versioned, &mut vote_account_data).unwrap();
|
VoteState::serialize(&versioned, &mut vote_account_data).unwrap();
|
||||||
let parsed =
|
let parsed = parse_account_data(
|
||||||
parse_account_data(&solana_vote_program::id(), &vote_account_data, None).unwrap();
|
&account_pubkey,
|
||||||
|
&solana_vote_program::id(),
|
||||||
|
&vote_account_data,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(parsed.program, "vote".to_string());
|
assert_eq!(parsed.program, "vote".to_string());
|
||||||
|
|
||||||
let nonce_data = Versions::new_current(State::Initialized(Data::default()));
|
let nonce_data = Versions::new_current(State::Initialized(Data::default()));
|
||||||
let nonce_account_data = bincode::serialize(&nonce_data).unwrap();
|
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());
|
assert_eq!(parsed.program, "nonce".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
146
account-decoder/src/parse_config.rs
Normal file
146
account-decoder/src/parse_config.rs
Normal file
@ -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<ConfigAccountType, ParseAccountError> {
|
||||||
|
let parsed_account = if pubkey == &solana_stake_program::config::id() {
|
||||||
|
get_config_data(data)
|
||||||
|
.ok()
|
||||||
|
.and_then(|data| deserialize::<StakeConfig>(data).ok())
|
||||||
|
.map(|config| ConfigAccountType::StakeConfig(config.into()))
|
||||||
|
} else {
|
||||||
|
deserialize::<ConfigKeys>(data).ok().and_then(|key_list| {
|
||||||
|
if !key_list.keys.is_empty() && key_list.keys[0].0 == validator_info::id() {
|
||||||
|
parse_config_data::<String>(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<T>(data: &[u8], keys: Vec<(Pubkey, bool)>) -> Option<UiConfig<T>>
|
||||||
|
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<Value>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<StakeConfig> 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<T> {
|
||||||
|
pub keys: Vec<UiConfigKey>,
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
use crate::parse_account_data::ParseAccountError;
|
use crate::{parse_account_data::ParseAccountError, UiFeeCalculator};
|
||||||
use solana_sdk::{
|
use solana_sdk::{
|
||||||
fee_calculator::FeeCalculator,
|
|
||||||
instruction::InstructionError,
|
instruction::InstructionError,
|
||||||
nonce::{state::Versions, State},
|
nonce::{state::Versions, State},
|
||||||
};
|
};
|
||||||
@ -14,7 +13,7 @@ pub fn parse_nonce(data: &[u8]) -> Result<UiNonceState, ParseAccountError> {
|
|||||||
State::Initialized(data) => Ok(UiNonceState::Initialized(UiNonceData {
|
State::Initialized(data) => Ok(UiNonceState::Initialized(UiNonceData {
|
||||||
authority: data.authority.to_string(),
|
authority: data.authority.to_string(),
|
||||||
blockhash: data.blockhash.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 struct UiNonceData {
|
||||||
pub authority: String,
|
pub authority: String,
|
||||||
pub blockhash: String,
|
pub blockhash: String,
|
||||||
pub fee_calculator: FeeCalculator,
|
pub fee_calculator: UiFeeCalculator,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -56,7 +55,9 @@ mod test {
|
|||||||
UiNonceState::Initialized(UiNonceData {
|
UiNonceState::Initialized(UiNonceData {
|
||||||
authority: Pubkey::default().to_string(),
|
authority: Pubkey::default().to_string(),
|
||||||
blockhash: Hash::default().to_string(),
|
blockhash: Hash::default().to_string(),
|
||||||
fee_calculator: FeeCalculator::default(),
|
fee_calculator: UiFeeCalculator {
|
||||||
|
lamports_per_signature: 0.to_string(),
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
235
account-decoder/src/parse_stake.rs
Normal file
235
account-decoder/src/parse_stake.rs
Normal file
@ -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<StakeAccountType, ParseAccountError> {
|
||||||
|
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<UiStake>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Meta> 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<Lockup> 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<Authorized> 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<Stake> 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<Delegation> 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());
|
||||||
|
}
|
||||||
|
}
|
328
account-decoder/src/parse_sysvar.rs
Normal file
328
account-decoder/src/parse_sysvar.rs
Normal file
@ -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<SysvarAccountType, ParseAccountError> {
|
||||||
|
let parsed_account = {
|
||||||
|
if pubkey == &sysvar::clock::id() {
|
||||||
|
deserialize::<Clock>(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::<Fees>(data)
|
||||||
|
.ok()
|
||||||
|
.map(|fees| SysvarAccountType::Fees(fees.into()))
|
||||||
|
} else if pubkey == &sysvar::recent_blockhashes::id() {
|
||||||
|
deserialize::<RecentBlockhashes>(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::<Rent>(data)
|
||||||
|
.ok()
|
||||||
|
.map(|rent| SysvarAccountType::Rent(rent.into()))
|
||||||
|
} else if pubkey == &sysvar::rewards::id() {
|
||||||
|
deserialize::<Rewards>(data)
|
||||||
|
.ok()
|
||||||
|
.map(|rewards| SysvarAccountType::Rewards(rewards.into()))
|
||||||
|
} else if pubkey == &sysvar::slot_hashes::id() {
|
||||||
|
deserialize::<SlotHashes>(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::<SlotHistory>(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::<StakeHistory>(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<UiRecentBlockhashesEntry>),
|
||||||
|
Rent(UiRent),
|
||||||
|
Rewards(UiRewards),
|
||||||
|
SlotHashes(Vec<UiSlotHashEntry>),
|
||||||
|
SlotHistory(UiSlotHistory),
|
||||||
|
StakeHistory(Vec<UiStakeHistoryEntry>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Clock> 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<Fees> 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<Rent> 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<Rewards> 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<u64>);
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
use crate::parse_account_data::ParseAccountError;
|
use crate::{parse_account_data::ParseAccountError, StringAmount};
|
||||||
use solana_sdk::{
|
use solana_sdk::{
|
||||||
clock::{Epoch, Slot},
|
clock::{Epoch, Slot},
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
@ -12,8 +12,8 @@ pub fn parse_vote(data: &[u8]) -> Result<VoteAccountType, ParseAccountError> {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|(epoch, credits, previous_credits)| UiEpochCredits {
|
.map(|(epoch, credits, previous_credits)| UiEpochCredits {
|
||||||
epoch: *epoch,
|
epoch: *epoch,
|
||||||
credits: *credits,
|
credits: credits.to_string(),
|
||||||
previous_credits: *previous_credits,
|
previous_credits: previous_credits.to_string(),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let votes = vote_state
|
let votes = vote_state
|
||||||
@ -115,8 +115,8 @@ struct UiPriorVoters {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct UiEpochCredits {
|
struct UiEpochCredits {
|
||||||
epoch: Epoch,
|
epoch: Epoch,
|
||||||
credits: u64,
|
credits: StringAmount,
|
||||||
previous_credits: u64,
|
previous_credits: StringAmount,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
18
account-decoder/src/validator_info.rs
Normal file
18
account-decoder/src/validator_info.rs
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -1120,7 +1120,7 @@ fn process_show_account(
|
|||||||
let cli_account = CliAccount {
|
let cli_account = CliAccount {
|
||||||
keyed_account: RpcKeyedAccount {
|
keyed_account: RpcKeyedAccount {
|
||||||
pubkey: account_pubkey.to_string(),
|
pubkey: account_pubkey.to_string(),
|
||||||
account: UiAccount::encode(account, UiAccountEncoding::Binary, None),
|
account: UiAccount::encode(account_pubkey, account, UiAccountEncoding::Binary, None),
|
||||||
},
|
},
|
||||||
use_lamports_unit,
|
use_lamports_unit,
|
||||||
};
|
};
|
||||||
|
@ -350,7 +350,12 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let nonce_pubkey = Pubkey::new(&[4u8; 32]);
|
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 {
|
let get_account_response = json!(Response {
|
||||||
context: RpcResponseContext { slot: 1 },
|
context: RpcResponseContext { slot: 1 },
|
||||||
value: json!(Some(rpc_nonce_account)),
|
value: json!(Some(rpc_nonce_account)),
|
||||||
|
@ -6,9 +6,10 @@ use crate::{
|
|||||||
use bincode::deserialize;
|
use bincode::deserialize;
|
||||||
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
|
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
|
||||||
use serde_json::{Map, Value};
|
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::{
|
use solana_clap_utils::{
|
||||||
input_parsers::pubkey_of,
|
input_parsers::pubkey_of,
|
||||||
input_validators::{is_pubkey, is_url},
|
input_validators::{is_pubkey, is_url},
|
||||||
@ -27,23 +28,6 @@ use solana_sdk::{
|
|||||||
};
|
};
|
||||||
use std::{error, sync::Arc};
|
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.
|
// Return an error if a validator details are longer than the max length.
|
||||||
pub fn check_details_length(string: String) -> Result<(), String> {
|
pub fn check_details_length(string: String) -> Result<(), String> {
|
||||||
if string.len() > MAX_LONG_FIELD_LENGTH {
|
if string.len() > MAX_LONG_FIELD_LENGTH {
|
||||||
@ -289,7 +273,7 @@ pub fn process_set_validator_info(
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, account)| {
|
.filter(|(_, account)| {
|
||||||
let key_list: ConfigKeys = deserialize(&account.data).map_err(|_| false).unwrap();
|
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)| {
|
.find(|(pubkey, account)| {
|
||||||
let (validator_pubkey, _) = parse_validator_info(&pubkey, &account).unwrap();
|
let (validator_pubkey, _) = parse_validator_info(&pubkey, &account).unwrap();
|
||||||
@ -328,7 +312,10 @@ pub fn process_set_validator_info(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let build_message = |lamports| {
|
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 {
|
if balance == 0 {
|
||||||
println!(
|
println!(
|
||||||
"Publishing info for Validator {:?}",
|
"Publishing info for Validator {:?}",
|
||||||
@ -401,7 +388,7 @@ pub fn process_get_validator_info(
|
|||||||
let key_list: ConfigKeys = deserialize(&validator_info_account.data)
|
let key_list: ConfigKeys = deserialize(&validator_info_account.data)
|
||||||
.map_err(|_| false)
|
.map_err(|_| false)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
key_list.keys.contains(&(id(), false))
|
key_list.keys.contains(&(validator_info::id(), false))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
@ -503,7 +490,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_parse_validator_info() {
|
fn test_parse_validator_info() {
|
||||||
let pubkey = Pubkey::new_rand();
|
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 config = ConfigKeys { keys };
|
||||||
|
|
||||||
let mut info = Map::new();
|
let mut info = Map::new();
|
||||||
|
@ -247,7 +247,7 @@ impl JsonRpcRequestProcessor {
|
|||||||
let mut response = None;
|
let mut response = None;
|
||||||
if let Some(account) = bank.get_account(pubkey) {
|
if let Some(account) = bank.get_account(pubkey) {
|
||||||
if account.owner == spl_token_id_v1_0() && encoding == UiAccountEncoding::JsonParsed {
|
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 {
|
} 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();
|
let message = "Encoded binary (base 58) data should be less than 128 bytes, please use Binary64 encoding.".to_string();
|
||||||
return Err(error::Error {
|
return Err(error::Error {
|
||||||
@ -256,7 +256,7 @@ impl JsonRpcRequestProcessor {
|
|||||||
data: None,
|
data: None,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
response = Some(UiAccount::encode(account, encoding, None));
|
response = Some(UiAccount::encode(pubkey, account, encoding, None));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,7 +288,7 @@ impl JsonRpcRequestProcessor {
|
|||||||
keyed_accounts
|
keyed_accounts
|
||||||
.map(|(pubkey, account)| RpcKeyedAccount {
|
.map(|(pubkey, account)| RpcKeyedAccount {
|
||||||
pubkey: pubkey.to_string(),
|
pubkey: pubkey.to_string(),
|
||||||
account: UiAccount::encode(account, encoding.clone(), None),
|
account: UiAccount::encode(&pubkey, account, encoding.clone(), None),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@ -1134,7 +1134,7 @@ impl JsonRpcRequestProcessor {
|
|||||||
keyed_accounts
|
keyed_accounts
|
||||||
.map(|(pubkey, account)| RpcKeyedAccount {
|
.map(|(pubkey, account)| RpcKeyedAccount {
|
||||||
pubkey: pubkey.to_string(),
|
pubkey: pubkey.to_string(),
|
||||||
account: UiAccount::encode(account, encoding.clone(), None),
|
account: UiAccount::encode(&pubkey, account, encoding.clone(), None),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
@ -1185,7 +1185,7 @@ impl JsonRpcRequestProcessor {
|
|||||||
keyed_accounts
|
keyed_accounts
|
||||||
.map(|(pubkey, account)| RpcKeyedAccount {
|
.map(|(pubkey, account)| RpcKeyedAccount {
|
||||||
pubkey: pubkey.to_string(),
|
pubkey: pubkey.to_string(),
|
||||||
account: UiAccount::encode(account, encoding.clone(), None),
|
account: UiAccount::encode(&pubkey, account, encoding.clone(), None),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
@ -1242,11 +1242,16 @@ fn get_filtered_program_accounts(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_parsed_token_account(bank: Arc<Bank>, account: Account) -> Option<UiAccount> {
|
pub(crate) fn get_parsed_token_account(
|
||||||
|
bank: Arc<Bank>,
|
||||||
|
pubkey: &Pubkey,
|
||||||
|
account: Account,
|
||||||
|
) -> Option<UiAccount> {
|
||||||
get_token_account_mint(&account.data)
|
get_token_account_mint(&account.data)
|
||||||
.and_then(|mint_pubkey| get_mint_owner_and_decimals(&bank, &mint_pubkey).ok())
|
.and_then(|mint_pubkey| get_mint_owner_and_decimals(&bank, &mint_pubkey).ok())
|
||||||
.map(|(_, decimals)| {
|
.map(|(_, decimals)| {
|
||||||
UiAccount::encode(
|
UiAccount::encode(
|
||||||
|
pubkey,
|
||||||
account,
|
account,
|
||||||
UiAccountEncoding::JsonParsed,
|
UiAccountEncoding::JsonParsed,
|
||||||
Some(AccountAdditionalData {
|
Some(AccountAdditionalData {
|
||||||
@ -1274,6 +1279,7 @@ where
|
|||||||
RpcKeyedAccount {
|
RpcKeyedAccount {
|
||||||
pubkey: pubkey.to_string(),
|
pubkey: pubkey.to_string(),
|
||||||
account: UiAccount::encode(
|
account: UiAccount::encode(
|
||||||
|
&pubkey,
|
||||||
account,
|
account,
|
||||||
UiAccountEncoding::JsonParsed,
|
UiAccountEncoding::JsonParsed,
|
||||||
Some(AccountAdditionalData { spl_token_decimals }),
|
Some(AccountAdditionalData { spl_token_decimals }),
|
||||||
|
@ -672,8 +672,13 @@ mod tests {
|
|||||||
.get_account(&nonce_account.pubkey())
|
.get_account(&nonce_account.pubkey())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.data;
|
.data;
|
||||||
let expected_data =
|
let expected_data = parse_account_data(
|
||||||
parse_account_data(&system_program::id(), &expected_data, None).unwrap();
|
&nonce_account.pubkey(),
|
||||||
|
&system_program::id(),
|
||||||
|
&expected_data,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let expected = json!({
|
let expected = json!({
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "accountNotification",
|
"method": "accountNotification",
|
||||||
|
@ -179,7 +179,7 @@ where
|
|||||||
K: Eq + Hash + Clone + Copy,
|
K: Eq + Hash + Clone + Copy,
|
||||||
S: Clone + Serialize,
|
S: Clone + Serialize,
|
||||||
B: Fn(&Bank, &K) -> X,
|
B: Fn(&Bank, &K) -> X,
|
||||||
F: Fn(X, Slot, Option<T>, Option<Arc<Bank>>) -> (Box<dyn Iterator<Item = S>>, Slot),
|
F: Fn(X, &K, Slot, Option<T>, Option<Arc<Bank>>) -> (Box<dyn Iterator<Item = S>>, Slot),
|
||||||
X: Clone + Serialize + Default,
|
X: Clone + Serialize + Default,
|
||||||
T: Clone,
|
T: Clone,
|
||||||
{
|
{
|
||||||
@ -211,6 +211,7 @@ where
|
|||||||
let mut w_last_notified_slot = last_notified_slot.write().unwrap();
|
let mut w_last_notified_slot = last_notified_slot.write().unwrap();
|
||||||
let (filter_results, result_slot) = filter_results(
|
let (filter_results, result_slot) = filter_results(
|
||||||
results,
|
results,
|
||||||
|
hashmap_key,
|
||||||
*w_last_notified_slot,
|
*w_last_notified_slot,
|
||||||
config.as_ref().cloned(),
|
config.as_ref().cloned(),
|
||||||
bank,
|
bank,
|
||||||
@ -245,6 +246,7 @@ impl RpcNotifier {
|
|||||||
|
|
||||||
fn filter_account_result(
|
fn filter_account_result(
|
||||||
result: Option<(Account, Slot)>,
|
result: Option<(Account, Slot)>,
|
||||||
|
pubkey: &Pubkey,
|
||||||
last_notified_slot: Slot,
|
last_notified_slot: Slot,
|
||||||
encoding: Option<UiAccountEncoding>,
|
encoding: Option<UiAccountEncoding>,
|
||||||
bank: Option<Arc<Bank>>,
|
bank: Option<Arc<Bank>>,
|
||||||
@ -256,12 +258,14 @@ fn filter_account_result(
|
|||||||
let encoding = encoding.unwrap_or(UiAccountEncoding::Binary);
|
let encoding = encoding.unwrap_or(UiAccountEncoding::Binary);
|
||||||
if account.owner == spl_token_id_v1_0() && encoding == UiAccountEncoding::JsonParsed {
|
if account.owner == spl_token_id_v1_0() && encoding == UiAccountEncoding::JsonParsed {
|
||||||
let bank = bank.unwrap(); // If result.is_some(), bank must also be Some
|
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);
|
return (Box::new(iter::once(ui_account)), fork);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
Box::new(iter::once(UiAccount::encode(account, encoding, None))),
|
Box::new(iter::once(UiAccount::encode(
|
||||||
|
pubkey, account, encoding, None,
|
||||||
|
))),
|
||||||
fork,
|
fork,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -272,6 +276,7 @@ fn filter_account_result(
|
|||||||
|
|
||||||
fn filter_signature_result(
|
fn filter_signature_result(
|
||||||
result: Option<transaction::Result<()>>,
|
result: Option<transaction::Result<()>>,
|
||||||
|
_signature: &Signature,
|
||||||
last_notified_slot: Slot,
|
last_notified_slot: Slot,
|
||||||
_config: Option<()>,
|
_config: Option<()>,
|
||||||
_bank: Option<Arc<Bank>>,
|
_bank: Option<Arc<Bank>>,
|
||||||
@ -288,6 +293,7 @@ fn filter_signature_result(
|
|||||||
|
|
||||||
fn filter_program_results(
|
fn filter_program_results(
|
||||||
accounts: Vec<(Pubkey, Account)>,
|
accounts: Vec<(Pubkey, Account)>,
|
||||||
|
_program_id: &Pubkey,
|
||||||
last_notified_slot: Slot,
|
last_notified_slot: Slot,
|
||||||
config: Option<ProgramConfig>,
|
config: Option<ProgramConfig>,
|
||||||
bank: Option<Arc<Bank>>,
|
bank: Option<Arc<Bank>>,
|
||||||
@ -309,7 +315,7 @@ fn filter_program_results(
|
|||||||
Box::new(
|
Box::new(
|
||||||
keyed_accounts.map(move |(pubkey, account)| RpcKeyedAccount {
|
keyed_accounts.map(move |(pubkey, account)| RpcKeyedAccount {
|
||||||
pubkey: pubkey.to_string(),
|
pubkey: pubkey.to_string(),
|
||||||
account: UiAccount::encode(account, encoding.clone(), None),
|
account: UiAccount::encode(&pubkey, account, encoding.clone(), None),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user