diff --git a/Cargo.lock b/Cargo.lock index 4834e8e04d028a..21b3f10c5080a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3629,6 +3629,23 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "solana-account-decoder" +version = "1.3.0" +dependencies = [ + "Inflector", + "bincode", + "bs58 0.3.1", + "lazy_static", + "serde", + "serde_derive", + "serde_json", + "solana-sdk 1.3.0", + "solana-vote-program", + "spl-memo", + "thiserror", +] + [[package]] name = "solana-accounts-bench" version = "1.3.0" @@ -3816,6 +3833,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "solana-account-decoder", "solana-budget-program", "solana-clap-utils", "solana-cli-config", @@ -3866,6 +3884,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "solana-account-decoder", "solana-logger", "solana-net-utils", "solana-sdk 1.3.0", @@ -3925,6 +3944,7 @@ dependencies = [ "serde_json", "serial_test", "serial_test_derive", + "solana-account-decoder", "solana-bpf-loader-program", "solana-budget-program", "solana-clap-utils", @@ -4792,6 +4812,8 @@ dependencies = [ "serde_derive", "serde_json", "solana-sdk 1.3.0", + "solana-stake-program", + "solana-vote-program", "spl-memo", ] diff --git a/Cargo.toml b/Cargo.toml index 7879451cd6b507..b103370cee09f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ members = [ "sys-tuner", "tokens", "transaction-status", + "account-decoder", "upload-perf", "net-utils", "version", diff --git a/account-decoder/Cargo.toml b/account-decoder/Cargo.toml new file mode 100644 index 00000000000000..909a8ced11019b --- /dev/null +++ b/account-decoder/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "solana-account-decoder" +version = "1.3.0" +description = "Solana account decoder" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +homepage = "https://solana.com/" +license = "Apache-2.0" +edition = "2018" + +[dependencies] +bincode = "1.2.1" +bs58 = "0.3.1" +Inflector = "0.11.4" +lazy_static = "1.4.0" +solana-sdk = { path = "../sdk", version = "1.3.0" } +solana-vote-program = { path = "../programs/vote", version = "1.3.0" } +spl-memo = "1.0.0" +serde = "1.0.112" +serde_derive = "1.0.103" +serde_json = "1.0.54" +thiserror = "1.0" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/account-decoder/src/lib.rs b/account-decoder/src/lib.rs new file mode 100644 index 00000000000000..ec2d52f30a6bdf --- /dev/null +++ b/account-decoder/src/lib.rs @@ -0,0 +1,80 @@ +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate serde_derive; + +pub mod parse_account_data; +pub mod parse_nonce; +pub mod parse_vote; + +use crate::parse_account_data::parse_account_data; +use serde_json::Value; +use solana_sdk::{account::Account, clock::Epoch, pubkey::Pubkey}; +use std::str::FromStr; + +/// A duplicate representation of an Account for pretty JSON serialization +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct UiAccount { + pub lamports: u64, + pub data: UiAccountData, + pub owner: String, + pub executable: bool, + pub rent_epoch: Epoch, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", untagged)] +pub enum UiAccountData { + Binary(String), + Json(Value), +} + +impl From> for UiAccountData { + fn from(data: Vec) -> Self { + Self::Binary(bs58::encode(data).into_string()) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum UiAccountEncoding { + Binary, + JsonParsed, +} + +impl UiAccount { + pub fn encode(account: Account, encoding: UiAccountEncoding) -> Self { + let data = match encoding { + UiAccountEncoding::Binary => account.data.into(), + UiAccountEncoding::JsonParsed => { + if let Ok(parsed_data) = parse_account_data(&account.owner, &account.data) { + UiAccountData::Json(parsed_data) + } else { + account.data.into() + } + } + }; + UiAccount { + lamports: account.lamports, + data, + owner: account.owner.to_string(), + executable: account.executable, + rent_epoch: account.rent_epoch, + } + } + + pub fn decode(&self) -> Option { + let data = match &self.data { + UiAccountData::Json(_) => None, + UiAccountData::Binary(blob) => bs58::decode(blob).into_vec().ok(), + }?; + Some(Account { + lamports: self.lamports, + data, + owner: Pubkey::from_str(&self.owner).ok()?, + executable: self.executable, + rent_epoch: self.rent_epoch, + }) + } +} diff --git a/account-decoder/src/parse_account_data.rs b/account-decoder/src/parse_account_data.rs new file mode 100644 index 00000000000000..2e46d8e0bdbe00 --- /dev/null +++ b/account-decoder/src/parse_account_data.rs @@ -0,0 +1,80 @@ +use crate::{parse_nonce::parse_nonce, parse_vote::parse_vote}; +use inflector::Inflector; +use serde_json::{json, Value}; +use solana_sdk::{instruction::InstructionError, pubkey::Pubkey, system_program}; +use std::{collections::HashMap, str::FromStr}; +use thiserror::Error; + +lazy_static! { + static ref SYSTEM_PROGRAM_ID: Pubkey = + Pubkey::from_str(&system_program::id().to_string()).unwrap(); + static ref VOTE_PROGRAM_ID: Pubkey = + Pubkey::from_str(&solana_vote_program::id().to_string()).unwrap(); + pub static ref PARSABLE_PROGRAM_IDS: HashMap = { + let mut m = HashMap::new(); + m.insert(*SYSTEM_PROGRAM_ID, ParsableAccount::Nonce); + m.insert(*VOTE_PROGRAM_ID, ParsableAccount::Vote); + m + }; +} + +#[derive(Error, Debug)] +pub enum ParseAccountError { + #[error("Program not parsable")] + ProgramNotParsable, + + #[error("Instruction error")] + InstructionError(#[from] InstructionError), + + #[error("Serde json error")] + SerdeJsonError(#[from] serde_json::error::Error), +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ParsableAccount { + Nonce, + Vote, +} + +pub fn parse_account_data(program_id: &Pubkey, data: &[u8]) -> Result { + let program_name = PARSABLE_PROGRAM_IDS + .get(program_id) + .ok_or_else(|| ParseAccountError::ProgramNotParsable)?; + let parsed_json = match program_name { + ParsableAccount::Nonce => serde_json::to_value(parse_nonce(data)?)?, + ParsableAccount::Vote => serde_json::to_value(parse_vote(data)?)?, + }; + Ok(json!({ + format!("{:?}", program_name).to_kebab_case(): parsed_json + })) +} + +#[cfg(test)] +mod test { + use super::*; + use solana_sdk::nonce::{ + state::{Data, Versions}, + State, + }; + use solana_vote_program::vote_state::{VoteState, VoteStateVersions}; + + #[test] + fn test_parse_account_data() { + let other_program = Pubkey::new_rand(); + let data = vec![0; 4]; + assert!(parse_account_data(&other_program, &data).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).unwrap(); + assert!(parsed.as_object().unwrap().contains_key("vote")); + + 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).unwrap(); + assert!(parsed.as_object().unwrap().contains_key("nonce")); + } +} diff --git a/account-decoder/src/parse_nonce.rs b/account-decoder/src/parse_nonce.rs new file mode 100644 index 00000000000000..5f82d0dbab5f5d --- /dev/null +++ b/account-decoder/src/parse_nonce.rs @@ -0,0 +1,66 @@ +use crate::parse_account_data::ParseAccountError; +use solana_sdk::{ + fee_calculator::FeeCalculator, + instruction::InstructionError, + nonce::{state::Versions, State}, +}; + +pub fn parse_nonce(data: &[u8]) -> Result { + let nonce_state: Versions = bincode::deserialize(data) + .map_err(|_| ParseAccountError::from(InstructionError::InvalidAccountData))?; + let nonce_state = nonce_state.convert_to_current(); + match nonce_state { + State::Uninitialized => Ok(UiNonceState::Uninitialized), + State::Initialized(data) => Ok(UiNonceState::Initialized(UiNonceData { + authority: data.authority.to_string(), + blockhash: data.blockhash.to_string(), + fee_calculator: data.fee_calculator, + })), + } +} + +/// A duplicate representation of NonceState for pretty JSON serialization +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum UiNonceState { + Uninitialized, + Initialized(UiNonceData), +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiNonceData { + pub authority: String, + pub blockhash: String, + pub fee_calculator: FeeCalculator, +} + +#[cfg(test)] +mod test { + use super::*; + use solana_sdk::{ + hash::Hash, + nonce::{ + state::{Data, Versions}, + State, + }, + pubkey::Pubkey, + }; + + #[test] + fn test_parse_nonce() { + let nonce_data = Versions::new_current(State::Initialized(Data::default())); + let nonce_account_data = bincode::serialize(&nonce_data).unwrap(); + assert_eq!( + parse_nonce(&nonce_account_data).unwrap(), + UiNonceState::Initialized(UiNonceData { + authority: Pubkey::default().to_string(), + blockhash: Hash::default().to_string(), + fee_calculator: FeeCalculator::default(), + }), + ); + + let bad_data = vec![0; 4]; + assert!(parse_nonce(&bad_data).is_err()); + } +} diff --git a/account-decoder/src/parse_vote.rs b/account-decoder/src/parse_vote.rs new file mode 100644 index 00000000000000..051de671985675 --- /dev/null +++ b/account-decoder/src/parse_vote.rs @@ -0,0 +1,134 @@ +use crate::parse_account_data::ParseAccountError; +use solana_sdk::{ + clock::{Epoch, Slot}, + pubkey::Pubkey, +}; +use solana_vote_program::vote_state::{BlockTimestamp, Lockout, VoteState}; + +pub fn parse_vote(data: &[u8]) -> Result { + let mut vote_state = VoteState::deserialize(data).map_err(ParseAccountError::from)?; + let epoch_credits = vote_state + .epoch_credits() + .iter() + .map(|(epoch, credits, previous_credits)| UiEpochCredits { + epoch: *epoch, + credits: *credits, + previous_credits: *previous_credits, + }) + .collect(); + let votes = vote_state + .votes + .iter() + .map(|lockout| UiLockout { + slot: lockout.slot, + confirmation_count: lockout.confirmation_count, + }) + .collect(); + let authorized_voters = vote_state + .authorized_voters() + .iter() + .map(|(epoch, authorized_voter)| UiAuthorizedVoters { + epoch: *epoch, + authorized_voter: authorized_voter.to_string(), + }) + .collect(); + let prior_voters = vote_state + .prior_voters() + .buf() + .iter() + .filter(|(pubkey, _, _)| pubkey != &Pubkey::default()) + .map( + |(authorized_pubkey, epoch_of_last_authorized_switch, target_epoch)| UiPriorVoters { + authorized_pubkey: authorized_pubkey.to_string(), + epoch_of_last_authorized_switch: *epoch_of_last_authorized_switch, + target_epoch: *target_epoch, + }, + ) + .collect(); + Ok(UiVoteState { + node_pubkey: vote_state.node_pubkey.to_string(), + authorized_withdrawer: vote_state.authorized_withdrawer.to_string(), + commission: vote_state.commission, + votes, + root_slot: vote_state.root_slot, + authorized_voters, + prior_voters, + epoch_credits, + last_timestamp: vote_state.last_timestamp, + }) +} + +/// A duplicate representation of VoteState for pretty JSON serialization +#[derive(Debug, Serialize, Deserialize, Default, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiVoteState { + node_pubkey: String, + authorized_withdrawer: String, + commission: u8, + votes: Vec, + root_slot: Option, + authorized_voters: Vec, + prior_voters: Vec, + epoch_credits: Vec, + last_timestamp: BlockTimestamp, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct UiLockout { + slot: Slot, + confirmation_count: u32, +} + +impl From<&Lockout> for UiLockout { + fn from(lockout: &Lockout) -> Self { + Self { + slot: lockout.slot, + confirmation_count: lockout.confirmation_count, + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct UiAuthorizedVoters { + epoch: Epoch, + authorized_voter: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct UiPriorVoters { + authorized_pubkey: String, + epoch_of_last_authorized_switch: Epoch, + target_epoch: Epoch, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct UiEpochCredits { + epoch: Epoch, + credits: u64, + previous_credits: u64, +} + +#[cfg(test)] +mod test { + use super::*; + use solana_vote_program::vote_state::VoteStateVersions; + + #[test] + fn test_parse_vote() { + 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 mut expected_vote_state = UiVoteState::default(); + expected_vote_state.node_pubkey = Pubkey::default().to_string(); + expected_vote_state.authorized_withdrawer = Pubkey::default().to_string(); + assert_eq!(parse_vote(&vote_account_data).unwrap(), expected_vote_state,); + + let bad_data = vec![0; 4]; + assert!(parse_vote(&bad_data).is_err()); + } +} diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 62ae1eefb068ef..2746280f60d8e0 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -27,6 +27,7 @@ reqwest = { version = "0.10.6", default-features = false, features = ["blocking" serde = "1.0.112" serde_derive = "1.0.103" serde_json = "1.0.54" +solana-account-decoder = { path = "../account-decoder", version = "1.3.0" } solana-budget-program = { path = "../programs/budget", version = "1.3.0" } solana-clap-utils = { path = "../clap-utils", version = "1.3.0" } solana-cli-config = { path = "../cli-config", version = "1.3.0" } diff --git a/cli/src/cli.rs b/cli/src/cli.rs index d30bc94e599a15..db010a2752bc87 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -15,6 +15,7 @@ use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; use log::*; use num_traits::FromPrimitive; use serde_json::{self, json, Value}; +use solana_account_decoder::{UiAccount, UiAccountEncoding}; use solana_budget_program::budget_instruction::{self, BudgetError}; use solana_clap_utils::{ commitment::commitment_arg_with_default, input_parsers::*, input_validators::*, @@ -24,7 +25,7 @@ use solana_client::{ client_error::{ClientError, ClientErrorKind, Result as ClientResult}, rpc_client::RpcClient, rpc_config::{RpcLargestAccountsFilter, RpcSendTransactionConfig}, - rpc_response::{RpcAccount, RpcKeyedAccount}, + rpc_response::RpcKeyedAccount, }; #[cfg(not(test))] use solana_faucet::faucet::request_airdrop_transaction; @@ -1225,7 +1226,7 @@ fn process_show_account( let cli_account = CliAccount { keyed_account: RpcKeyedAccount { pubkey: account_pubkey.to_string(), - account: RpcAccount::encode(account), + account: UiAccount::encode(account, UiAccountEncoding::Binary), }, use_lamports_unit, }; diff --git a/cli/src/offline/blockhash_query.rs b/cli/src/offline/blockhash_query.rs index 5765a13c4c61b5..09c3924f637b5a 100644 --- a/cli/src/offline/blockhash_query.rs +++ b/cli/src/offline/blockhash_query.rs @@ -111,9 +111,10 @@ mod tests { use crate::{nonce::nonce_arg, offline::blockhash_query::BlockhashQuery}; use clap::App; use serde_json::{self, json, Value}; + use solana_account_decoder::{UiAccount, UiAccountEncoding}; use solana_client::{ rpc_request::RpcRequest, - rpc_response::{Response, RpcAccount, RpcFeeCalculator, RpcResponseContext}, + rpc_response::{Response, RpcFeeCalculator, RpcResponseContext}, }; use solana_sdk::{ account::Account, fee_calculator::FeeCalculator, hash::hash, nonce, system_program, @@ -349,7 +350,7 @@ mod tests { ) .unwrap(); let nonce_pubkey = Pubkey::new(&[4u8; 32]); - let rpc_nonce_account = RpcAccount::encode(nonce_account); + let rpc_nonce_account = UiAccount::encode(nonce_account, UiAccountEncoding::Binary); let get_account_response = json!(Response { context: RpcResponseContext { slot: 1 }, value: json!(Some(rpc_nonce_account)), diff --git a/client/Cargo.toml b/client/Cargo.toml index 827cd048c92c01..95e3bd2ea687b6 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -19,9 +19,10 @@ reqwest = { version = "0.10.6", default-features = false, features = ["blocking" serde = "1.0.112" serde_derive = "1.0.103" serde_json = "1.0.54" -solana-transaction-status = { path = "../transaction-status", version = "1.3.0" } +solana-account-decoder = { path = "../account-decoder", version = "1.3.0" } solana-net-utils = { path = "../net-utils", version = "1.3.0" } solana-sdk = { path = "../sdk", version = "1.3.0" } +solana-transaction-status = { path = "../transaction-status", version = "1.3.0" } solana-vote-program = { path = "../programs/vote", version = "1.3.0" } thiserror = "1.0" tungstenite = "0.10.1" diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index de0c2c59678e33..3de5dd559c7229 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -11,6 +11,7 @@ use bincode::serialize; use indicatif::{ProgressBar, ProgressStyle}; use log::*; use serde_json::{json, Value}; +use solana_account_decoder::UiAccount; use solana_sdk::{ account::Account, clock::{ @@ -440,9 +441,9 @@ impl RpcClient { let Response { context, value: rpc_account, - } = serde_json::from_value::>>(result_json)?; + } = serde_json::from_value::>>(result_json)?; trace!("Response account {:?} {:?}", pubkey, rpc_account); - let account = rpc_account.and_then(|rpc_account| rpc_account.decode().ok()); + let account = rpc_account.and_then(|rpc_account| rpc_account.decode()); Ok(Response { context, value: account, diff --git a/client/src/rpc_config.rs b/client/src/rpc_config.rs index d48ecddd168260..91220cbc50ffcc 100644 --- a/client/src/rpc_config.rs +++ b/client/src/rpc_config.rs @@ -1,3 +1,4 @@ +use solana_account_decoder::UiAccountEncoding; use solana_sdk::{clock::Epoch, commitment_config::CommitmentConfig}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -40,3 +41,11 @@ pub struct RpcInflationConfig { #[serde(flatten)] pub commitment: Option, } + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcAccountInfoConfig { + pub encoding: Option, + #[serde(flatten)] + pub commitment: Option, +} diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index b78f42fe4a4f42..9c6e9b5a458f5c 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -1,13 +1,12 @@ -use crate::{client_error, rpc_request::RpcError}; +use crate::client_error; +use solana_account_decoder::UiAccount; use solana_sdk::{ - account::Account, clock::{Epoch, Slot}, fee_calculator::{FeeCalculator, FeeRateGovernor}, inflation::Inflation, - pubkey::Pubkey, transaction::{Result, TransactionError}, }; -use std::{collections::HashMap, net::SocketAddr, str::FromStr}; +use std::{collections::HashMap, net::SocketAddr}; pub type RpcResult = client_error::Result>; @@ -91,7 +90,7 @@ pub struct RpcInflationRate { #[serde(rename_all = "camelCase")] pub struct RpcKeyedAccount { pub pubkey: String, - pub account: RpcAccount, + pub account: UiAccount, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -100,43 +99,6 @@ pub struct RpcSignatureResult { pub err: Option, } -/// A duplicate representation of a Message for pretty JSON serialization -#[derive(Serialize, Deserialize, Clone, Debug)] -#[serde(rename_all = "camelCase")] -pub struct RpcAccount { - pub lamports: u64, - pub data: String, - pub owner: String, - pub executable: bool, - pub rent_epoch: Epoch, -} - -impl RpcAccount { - pub fn encode(account: Account) -> Self { - RpcAccount { - lamports: account.lamports, - data: bs58::encode(account.data.clone()).into_string(), - owner: account.owner.to_string(), - executable: account.executable, - rent_epoch: account.rent_epoch, - } - } - - pub fn decode(&self) -> std::result::Result { - Ok(Account { - lamports: self.lamports, - data: bs58::decode(self.data.clone()).into_vec().map_err(|_| { - RpcError::RpcRequestError("Could not parse encoded account data".to_string()) - })?, - owner: Pubkey::from_str(&self.owner).map_err(|_| { - RpcError::RpcRequestError("Could not parse encoded account owner".to_string()) - })?, - executable: self.executable, - rent_epoch: self.rent_epoch, - }) - } -} - #[derive(Serialize, Deserialize, Clone, Debug)] pub struct RpcContactInfo { /// Pubkey of the node as a base-58 string diff --git a/core/Cargo.toml b/core/Cargo.toml index 9c6078a37520f1..0f426dd37bf0ad 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -42,11 +42,11 @@ regex = "1.3.9" serde = "1.0.112" serde_derive = "1.0.103" serde_json = "1.0.54" +solana-account-decoder = { path = "../account-decoder", version = "1.3.0" } solana-bpf-loader-program = { path = "../programs/bpf_loader", version = "1.3.0" } solana-budget-program = { path = "../programs/budget", version = "1.3.0" } solana-clap-utils = { path = "../clap-utils", version = "1.3.0" } solana-client = { path = "../client", version = "1.3.0" } -solana-transaction-status = { path = "../transaction-status", version = "1.3.0" } solana-faucet = { path = "../faucet", version = "1.3.0" } solana-genesis-programs = { path = "../genesis-programs", version = "1.3.0" } solana-ledger = { path = "../ledger", version = "1.3.0" } @@ -60,10 +60,11 @@ solana-runtime = { path = "../runtime", version = "1.3.0" } solana-sdk = { path = "../sdk", version = "1.3.0" } solana-stake-program = { path = "../programs/stake", version = "1.3.0" } solana-streamer = { path = "../streamer", version = "1.3.0" } +solana-sys-tuner = { path = "../sys-tuner", version = "1.3.0" } +solana-transaction-status = { path = "../transaction-status", version = "1.3.0" } solana-version = { path = "../version", version = "1.3.0" } solana-vote-program = { path = "../programs/vote", version = "1.3.0" } solana-vote-signer = { path = "../vote-signer", version = "1.3.0" } -solana-sys-tuner = { path = "../sys-tuner", version = "1.3.0" } tempfile = "3.1.0" thiserror = "1.0" tokio = "0.1" diff --git a/core/src/rpc.rs b/core/src/rpc.rs index a52e2845908fd3..9e57bfd6c24138 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -8,6 +8,7 @@ use crate::{ use bincode::serialize; use jsonrpc_core::{Error, Metadata, Result}; use jsonrpc_derive::rpc; +use solana_account_decoder::{UiAccount, UiAccountEncoding}; use solana_client::{ rpc_config::*, rpc_request::{ @@ -205,10 +206,16 @@ impl JsonRpcRequestProcessor { pub fn get_account_info( &self, pubkey: &Pubkey, - commitment: Option, - ) -> Result>> { - let bank = self.bank(commitment)?; - new_response(&bank, bank.get_account(pubkey).map(RpcAccount::encode)) + config: Option, + ) -> Result>> { + let config = config.unwrap_or_default(); + let bank = self.bank(config.commitment)?; + let encoding = config.encoding.unwrap_or(UiAccountEncoding::Binary); + new_response( + &bank, + bank.get_account(pubkey) + .map(|account| UiAccount::encode(account, encoding)), + ) } pub fn get_minimum_balance_for_rent_exemption( @@ -224,15 +231,17 @@ impl JsonRpcRequestProcessor { pub fn get_program_accounts( &self, program_id: &Pubkey, - commitment: Option, + config: Option, ) -> Result> { - Ok(self - .bank(commitment)? + let config = config.unwrap_or_default(); + let bank = self.bank(config.commitment)?; + let encoding = config.encoding.unwrap_or(UiAccountEncoding::Binary); + Ok(bank .get_program_accounts(Some(&program_id)) .into_iter() .map(|(pubkey, account)| RpcKeyedAccount { pubkey: pubkey.to_string(), - account: RpcAccount::encode(account), + account: UiAccount::encode(account, encoding.clone()), }) .collect()) } @@ -823,15 +832,15 @@ pub trait RpcSol { &self, meta: Self::Metadata, pubkey_str: String, - commitment: Option, - ) -> Result>>; + config: Option, + ) -> Result>>; #[rpc(meta, name = "getProgramAccounts")] fn get_program_accounts( &self, meta: Self::Metadata, program_id_str: String, - commitment: Option, + config: Option, ) -> Result>; #[rpc(meta, name = "getMinimumBalanceForRentExemption")] @@ -1076,11 +1085,11 @@ impl RpcSol for RpcSolImpl { &self, meta: Self::Metadata, pubkey_str: String, - commitment: Option, - ) -> Result>> { + config: Option, + ) -> Result>> { debug!("get_account_info rpc request received: {:?}", pubkey_str); let pubkey = verify_pubkey(pubkey_str)?; - meta.get_account_info(&pubkey, commitment) + meta.get_account_info(&pubkey, config) } fn get_minimum_balance_for_rent_exemption( @@ -1100,14 +1109,14 @@ impl RpcSol for RpcSolImpl { &self, meta: Self::Metadata, program_id_str: String, - commitment: Option, + config: Option, ) -> Result> { debug!( "get_program_accounts rpc request received: {:?}", program_id_str ); let program_id = verify_pubkey(program_id_str)?; - meta.get_program_accounts(&program_id, commitment) + meta.get_program_accounts(&program_id, config) } fn get_inflation_governor( diff --git a/core/src/rpc_pubsub.rs b/core/src/rpc_pubsub.rs index 50db2d318d9883..03ab24cc535fbf 100644 --- a/core/src/rpc_pubsub.rs +++ b/core/src/rpc_pubsub.rs @@ -4,9 +4,8 @@ use crate::rpc_subscriptions::{RpcSubscriptions, RpcVote, SlotInfo}; use jsonrpc_core::{Error, ErrorCode, Result}; use jsonrpc_derive::rpc; use jsonrpc_pubsub::{typed::Subscriber, Session, SubscriptionId}; -use solana_client::rpc_response::{ - Response as RpcResponse, RpcAccount, RpcKeyedAccount, RpcSignatureResult, -}; +use solana_account_decoder::UiAccount; +use solana_client::rpc_response::{Response as RpcResponse, RpcKeyedAccount, RpcSignatureResult}; #[cfg(test)] use solana_runtime::bank_forks::BankForks; use solana_sdk::{ @@ -37,7 +36,7 @@ pub trait RpcSolPubSub { fn account_subscribe( &self, meta: Self::Metadata, - subscriber: Subscriber>, + subscriber: Subscriber>, pubkey_str: String, commitment: Option, ); @@ -172,7 +171,7 @@ impl RpcSolPubSub for RpcSolPubSubImpl { fn account_subscribe( &self, _meta: Self::Metadata, - subscriber: Subscriber>, + subscriber: Subscriber>, pubkey_str: String, commitment: Option, ) { diff --git a/core/src/rpc_subscriptions.rs b/core/src/rpc_subscriptions.rs index 852f93d5111326..905a0600e0e307 100644 --- a/core/src/rpc_subscriptions.rs +++ b/core/src/rpc_subscriptions.rs @@ -7,8 +7,9 @@ use jsonrpc_pubsub::{ SubscriptionId, }; use serde::Serialize; +use solana_account_decoder::{UiAccount, UiAccountEncoding}; use solana_client::rpc_response::{ - Response, RpcAccount, RpcKeyedAccount, RpcResponseContext, RpcSignatureResult, + Response, RpcKeyedAccount, RpcResponseContext, RpcSignatureResult, }; use solana_runtime::{bank::Bank, bank_forks::BankForks, commitment::BlockCommitmentCache}; use solana_sdk::{ @@ -90,7 +91,7 @@ struct SubscriptionData { last_notified_slot: RwLock, } type RpcAccountSubscriptions = - RwLock>>>>; + RwLock>>>>; type RpcProgramSubscriptions = RwLock>>>>; type RpcSignatureSubscriptions = RwLock< @@ -225,12 +226,18 @@ impl RpcNotifier { fn filter_account_result( result: Option<(Account, Slot)>, last_notified_slot: Slot, -) -> (Box>, Slot) { +) -> (Box>, Slot) { if let Some((account, fork)) = result { // If fork < last_notified_slot this means that we last notified for a fork // and should notify that the account state has been reverted. if fork != last_notified_slot { - return (Box::new(iter::once(RpcAccount::encode(account))), fork); + return ( + Box::new(iter::once(UiAccount::encode( + account, + UiAccountEncoding::Binary, + ))), + fork, + ); } } (Box::new(iter::empty()), last_notified_slot) @@ -260,7 +267,7 @@ fn filter_program_results( .into_iter() .map(|(pubkey, account)| RpcKeyedAccount { pubkey: pubkey.to_string(), - account: RpcAccount::encode(account), + account: UiAccount::encode(account, UiAccountEncoding::Binary), }), ), last_notified_slot, @@ -449,7 +456,7 @@ impl RpcSubscriptions { pubkey: Pubkey, commitment: Option, sub_id: SubscriptionId, - subscriber: Subscriber>, + subscriber: Subscriber>, ) { let commitment_level = commitment .unwrap_or_else(CommitmentConfig::single) diff --git a/core/tests/rpc.rs b/core/tests/rpc.rs index 3ffa0e607728a3..e94a2e2cfdb9a8 100644 --- a/core/tests/rpc.rs +++ b/core/tests/rpc.rs @@ -7,9 +7,10 @@ use jsonrpc_core_client::transports::ws; use log::*; use reqwest::{self, header::CONTENT_TYPE}; use serde_json::{json, Value}; +use solana_account_decoder::UiAccount; use solana_client::{ rpc_client::{get_rpc_request_str, RpcClient}, - rpc_response::{Response, RpcAccount, RpcSignatureResult}, + rpc_response::{Response, RpcSignatureResult}, }; use solana_core::contact_info::ContactInfo; use solana_core::{rpc_pubsub::gen_client::Client as PubsubClient, validator::TestValidator}; @@ -172,7 +173,7 @@ fn test_rpc_subscriptions() { // Track when subscriptions are ready let (ready_sender, ready_receiver) = channel::<()>(); // Track account notifications are received - let (account_sender, account_receiver) = channel::>(); + let (account_sender, account_receiver) = channel::>(); // Track when status notifications are received let (status_sender, status_receiver) = channel::<(String, Response)>(); diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index 6afaeeefb5fbc8..785ce5055ae042 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -137,7 +137,10 @@ Returns all information associated with the account of provided Pubkey #### Parameters: * `` - Pubkey of account to query, as base-58 encoded string -* `` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) +* `` - (optional) Configuration object containing the following optional fields: + * (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + * (optional) `encoding: ` - encoding for Account data, either "binary" or jsonParsed". If parameter not provided, the default encoding is binary. + Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a parser cannot be found, the field falls back to binary encoding, detectable when the `data` field is type ``. #### Results: @@ -147,7 +150,7 @@ The result will be an RpcResponse JSON object with `value` equal to: * `` - otherwise, a JSON object containing: * `lamports: `, number of lamports assigned to this account, as a u64 * `owner: `, base-58 encoded Pubkey of the program this account has been assigned to - * `data: `, base-58 encoded data associated with the account + * `data: `, data associated with the account, either as base-58 encoded binary data or JSON format `{: }`, depending on encoding parameter * `executable: `, boolean indicating if the account contains a program \(and is strictly read-only\) * `rentEpoch`: , the epoch at which this account will next owe rent, as u64 @@ -155,10 +158,16 @@ The result will be an RpcResponse JSON object with `value` equal to: ```bash // Request -curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getAccountInfo", "params":["2gVkYWexTHR5Hb2aLeQN3tnngvWzisFKXDUPrgMHpdST"]}' http://localhost:8899 +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getAccountInfo", "params":["4fYNw3dojWmQ4dXtSGE9epjRGy9pFSx62YypT7avPYvA"]}' http://localhost:8899 + +// Result +{"jsonrpc":"2.0","result":{"context":{"slot":1},"value":{"data":"11116bv5nS2h3y12kD1yUKeMZvGcKLSjQgX6BeV7u1FrjeJcKfsHRTPuR3oZ1EioKtYGiYxpxMG5vpbZLsbcBYBEmZZcMKaSoGx9JZeAuWf","executable":false,"lamports":1000000000,"owner":"11111111111111111111111111111111","rentEpoch":2}},"id":1} + +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getAccountInfo", "params":["4fYNw3dojWmQ4dXtSGE9epjRGy9pFSx62YypT7avPYvA",{"encoding":"json"}]}' http://localhost:8899 // Result -{"jsonrpc":"2.0","result":{"context":{"slot":1},"value":{"executable":false,"owner":"4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM","lamports":1,"data":"Joig2k8Ax4JPMpWhXRyc2jMa7Wejz4X1xqVi3i7QRkmVj1ChUgNc4VNpGUQePJGBAui3c6886peU9GEbjsyeANN8JGStprwLbLwcw5wpPjuQQb9mwrjVmoDQBjj3MzZKgeHn6wmnQ5k8DBFuoCYKWWsJfH2gv9FvCzrN6K1CRcQZzF","rentEpoch":2}},"id":1} +{"jsonrpc":"2.0","result":{"context":{"slot":1},"value":{"data":{"nonce":{"initialized":{"authority":"Bbqg1M4YVVfbhEzwA9SpC9FhsaG83YMTYoR4a8oTDLX","blockhash":"3xLP3jK6dVJwpeGeTDYTwdDK3TKchUf1gYYGHa4sF3XJ","feeCalculator":{"lamportsPerSignature":5000}}}},"executable":false,"lamports":1000000000,"owner":"11111111111111111111111111111111","rentEpoch":2}},"id":1} ``` ### getBalance @@ -279,9 +288,8 @@ Returns identity and transaction information about a confirmed block in the ledg #### Parameters: * `` - slot, as u64 integer -* `` - (optional) encoding for each returned Transaction, either "json", "jsonParsed", or "binary". - Parsed-JSON encoding attempts to use program-specific instruction parsers to return more human-readable and explicit program data. - If parameter not provided, the default encoding is JSON. +* `` - (optional) encoding for each returned Transaction, either "json", "jsonParsed", or "binary". If parameter not provided, the default encoding is JSON. + Parsed-JSON encoding attempts to use program-specific instruction parsers to return more human-readable and explicit data in the `transaction.message.instructions` list. If parsed-JSON is requested but a parser cannot be found, the instruction falls back to regular JSON encoding (`accounts`, `data`, and `programIdIndex` fields). #### Results: @@ -400,9 +408,8 @@ Returns transaction details for a confirmed transaction #### Parameters: * `` - transaction signature as base-58 encoded string -* `` - (optional) encoding for the returned Transaction, either "json", "jsonParsed", or "binary". - Parsed-JSON encoding attempts to use program-specific instruction parsers to return more human-readable and explicit program data. - If parameter not provided, the default encoding is JSON. +* `` - (optional) encoding for the returned Transaction, either "json", "jsonParsed", or "binary". If parameter not provided, the default encoding is JSON. + Parsed-JSON encoding attempts to use program-specific instruction parsers to return more human-readable and explicit data in the `transaction.message.instructions` list. If parsed-JSON is requested but a parser cannot be found, the instruction falls back to regular JSON encoding (`accounts`, `data`, and `programIdIndex` fields). #### Results: @@ -779,7 +786,10 @@ Returns all accounts owned by the provided program Pubkey #### Parameters: * `` - Pubkey of program, as base-58 encoded string -* `` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) +* `` - (optional) Configuration object containing the following optional fields: + * (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + * (optional) `encoding: ` - encoding for Account data, either "binary" or jsonParsed". If parameter not provided, the default encoding is binary. + Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a parser cannot be found, the field falls back to binary encoding, detectable when the `data` field is type ``. #### Results: @@ -789,7 +799,7 @@ The result field will be an array of JSON objects, which will contain: * `account: ` - a JSON object, with the following sub fields: * `lamports: `, number of lamports assigned to this account, as a u64 * `owner: `, base-58 encoded Pubkey of the program this account has been assigned to - * `data: `, base-58 encoded data associated with the account + `data: `, data associated with the account, either as base-58 encoded binary data or JSON format `{: }`, depending on encoding parameter * `executable: `, boolean indicating if the account contains a program \(and is strictly read-only\) * `rentEpoch`: , the epoch at which this account will next owe rent, as u64 diff --git a/transaction-status/Cargo.toml b/transaction-status/Cargo.toml index 59a0db4c987bf2..e17b00011e8692 100644 --- a/transaction-status/Cargo.toml +++ b/transaction-status/Cargo.toml @@ -14,6 +14,8 @@ bs58 = "0.3.1" Inflector = "0.11.4" lazy_static = "1.4.0" solana-sdk = { path = "../sdk", version = "1.3.0" } +solana-stake-program = { path = "../programs/stake", version = "1.3.0" } +solana-vote-program = { path = "../programs/vote", version = "1.3.0" } spl-memo = "1.0.0" serde = "1.0.112" serde_derive = "1.0.103" diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index 2d8fe079b2e5ad..c1019e6a562736 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -23,7 +23,7 @@ pub enum RpcInstruction { Parsed(Value), } -/// A duplicate representation of a Message for pretty JSON serialization +/// A duplicate representation of a CompiledInstruction for pretty JSON serialization #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcCompiledInstruction { diff --git a/transaction-status/src/parse_instruction.rs b/transaction-status/src/parse_instruction.rs index e971735a42d67b..14bd14fd3a4112 100644 --- a/transaction-status/src/parse_instruction.rs +++ b/transaction-status/src/parse_instruction.rs @@ -8,7 +8,7 @@ use std::{ lazy_static! { static ref MEMO_PROGRAM_ID: Pubkey = Pubkey::from_str(&spl_memo::id().to_string()).unwrap(); - pub static ref PARSABLE_PROGRAM_IDS: HashMap = { + static ref PARSABLE_PROGRAM_IDS: HashMap = { let mut m = HashMap::new(); m.insert(*MEMO_PROGRAM_ID, ParsableProgram::SplMemo); m @@ -17,7 +17,7 @@ lazy_static! { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub enum ParsableProgram { +enum ParsableProgram { SplMemo, }