From f7fb5aebac5387ad1e0b4a32d582631f59894de9 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Tue, 7 Jul 2020 09:35:35 -0600 Subject: [PATCH] v1.2 instruction and account decoding backports (#10939) * Revert "Rpc: add filter to getProgramAccounts (#10888) (#10932)" This reverts commit 9311a6e35661a90e6b0fc990845b0bc5c4de10db. * Add jsonParsed option for EncodedTransactions; add memo parser (#10711) * Add jsonParsed option for EncodedTransactions; add memo parser * Use kebab case for program names * Add account-key parsing * Add parse test * Update transaction encoding docs (#10833) * Add account-decoder utilities (#10846) * Fix comment and make less pub * Add account-decoder crate and use to decode vote and system (nonce) accounts * Update docs * Rename RpcAccount struct * s/Rpc/Display * Call it jsonParsed and update docs * Revert "s/Rpc/Display" This reverts commit 6e7149f503f560f1e9237981058ff05642bb7db5. * s/Rpc/Ui * Add tests * Ui more things * Comments * Update struct prefixes to Ui (#10874) * Update comments * Use Ui prefix * Rpc: add filter to getProgramAccounts (#10888) * Add RpcFilterType, and implement CompareBytes for getProgramAccounts * Accept bytes in bs58 * Rename to memcmp * Add Memcmp optional encoding field * Add dataSize filter * Update docs * Clippy * Simplify tests that don't need to test account contents; add multiple-filter tests --- Cargo.lock | 169 ++++++++++++++------ Cargo.toml | 1 + account-decoder/Cargo.toml | 25 +++ account-decoder/src/lib.rs | 80 +++++++++ account-decoder/src/parse_account_data.rs | 80 +++++++++ account-decoder/src/parse_nonce.rs | 66 ++++++++ account-decoder/src/parse_vote.rs | 134 ++++++++++++++++ cli/Cargo.toml | 1 + cli/src/cli.rs | 9 +- cli/src/display.rs | 6 +- cli/src/offline/blockhash_query.rs | 5 +- client/Cargo.toml | 3 +- client/src/rpc_client.rs | 13 +- client/src/rpc_config.rs | 13 +- client/src/rpc_response.rs | 46 +----- core/Cargo.toml | 5 +- core/src/rpc.rs | 66 +++++--- core/src/rpc_pubsub.rs | 9 +- core/src/rpc_subscriptions.rs | 19 ++- core/tests/rpc.rs | 5 +- docs/src/apps/jsonrpc-api.md | 27 +++- ledger/src/blockstore.rs | 28 ++-- stake-monitor/src/lib.rs | 6 +- transaction-status/Cargo.toml | 4 + transaction-status/src/lib.rs | 151 ++++++++++++----- transaction-status/src/parse_accounts.rs | 56 +++++++ transaction-status/src/parse_instruction.rs | 59 +++++++ watchtower/src/main.rs | 4 +- 28 files changed, 870 insertions(+), 220 deletions(-) create mode 100644 account-decoder/Cargo.toml create mode 100644 account-decoder/src/lib.rs create mode 100644 account-decoder/src/parse_account_data.rs create mode 100644 account-decoder/src/parse_nonce.rs create mode 100644 account-decoder/src/parse_vote.rs create mode 100644 transaction-status/src/parse_accounts.rs create mode 100644 transaction-status/src/parse_instruction.rs diff --git a/Cargo.lock b/Cargo.lock index 41523726ac..4701d80189 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3691,6 +3691,23 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "solana-account-decoder" +version = "1.2.9" +dependencies = [ + "Inflector", + "bincode", + "bs58 0.3.1", + "lazy_static", + "serde", + "serde_derive", + "serde_json", + "solana-sdk 1.2.9", + "solana-vote-program", + "spl-memo", + "thiserror", +] + [[package]] name = "solana-accounts-bench" version = "1.2.9" @@ -3703,7 +3720,7 @@ dependencies = [ "solana-logger", "solana-measure", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", ] [[package]] @@ -3722,7 +3739,7 @@ dependencies = [ "solana-measure", "solana-perf", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "solana-streamer", "solana-version", ] @@ -3751,7 +3768,7 @@ dependencies = [ "solana-metrics", "solana-net-utils", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "solana-version", ] @@ -3792,7 +3809,7 @@ dependencies = [ "solana-move-loader-program", "solana-net-utils", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "solana-version", ] @@ -3809,7 +3826,7 @@ dependencies = [ "rand 0.7.3", "solana-logger", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "solana_rbpf", "thiserror", ] @@ -3825,7 +3842,7 @@ dependencies = [ "num-traits", "serde", "serde_derive", - "solana-sdk", + "solana-sdk 1.2.9", ] [[package]] @@ -3840,7 +3857,7 @@ dependencies = [ "serde", "serde_derive", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "thiserror", ] @@ -3852,7 +3869,7 @@ dependencies = [ "clap", "rpassword", "solana-remote-wallet", - "solana-sdk", + "solana-sdk 1.2.9", "thiserror", "tiny-bip39", "url 2.1.1", @@ -3880,6 +3897,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "solana-account-decoder", "solana-budget-program", "solana-clap-utils", "solana-cli-config", @@ -3891,7 +3909,7 @@ dependencies = [ "solana-net-utils", "solana-remote-wallet", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "solana-stake-program", "solana-transaction-status", "solana-version", @@ -3930,9 +3948,10 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "solana-account-decoder", "solana-logger", "solana-net-utils", - "solana-sdk", + "solana-sdk 1.2.9", "solana-transaction-status", "solana-vote-program", "thiserror", @@ -3950,7 +3969,7 @@ dependencies = [ "serde", "serde_derive", "solana-logger", - "solana-sdk", + "solana-sdk 1.2.9", ] [[package]] @@ -3989,6 +4008,7 @@ dependencies = [ "serde_json", "serial_test", "serial_test_derive", + "solana-account-decoder", "solana-bpf-loader-program", "solana-budget-program", "solana-clap-utils", @@ -4004,7 +4024,7 @@ dependencies = [ "solana-perf", "solana-rayon-threadlimit", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "solana-stake-program", "solana-streamer", "solana-sys-tuner", @@ -4060,7 +4080,7 @@ dependencies = [ "solana-logger", "solana-net-utils", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "solana-version", ] @@ -4074,7 +4094,7 @@ dependencies = [ "log 0.4.8", "reqwest", "solana-ledger", - "solana-sdk", + "solana-sdk 1.2.9", "tar", ] @@ -4091,7 +4111,7 @@ dependencies = [ "solana-logger", "solana-metrics", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "thiserror", ] @@ -4100,7 +4120,7 @@ name = "solana-failure-program" version = "1.2.9" dependencies = [ "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", ] [[package]] @@ -4117,7 +4137,7 @@ dependencies = [ "solana-clap-utils", "solana-logger", "solana-metrics", - "solana-sdk", + "solana-sdk 1.2.9", "solana-version", "tokio 0.1.22", "tokio-codec", @@ -4137,7 +4157,7 @@ dependencies = [ "solana-genesis-programs", "solana-ledger", "solana-logger", - "solana-sdk", + "solana-sdk 1.2.9", "solana-stake-program", "solana-version", "solana-vote-program", @@ -4153,7 +4173,7 @@ dependencies = [ "solana-budget-program", "solana-exchange-program", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "solana-vest-program", ] @@ -4167,7 +4187,7 @@ dependencies = [ "solana-core", "solana-logger", "solana-net-utils", - "solana-sdk", + "solana-sdk 1.2.9", "solana-version", ] @@ -4195,7 +4215,7 @@ dependencies = [ "solana-client", "solana-config-program", "solana-logger", - "solana-sdk", + "solana-sdk 1.2.9", "solana-version", "tar", "tempdir", @@ -4215,7 +4235,7 @@ dependencies = [ "solana-clap-utils", "solana-cli-config", "solana-remote-wallet", - "solana-sdk", + "solana-sdk 1.2.9", "solana-version", "tiny-bip39", ] @@ -4260,7 +4280,7 @@ dependencies = [ "solana-perf", "solana-rayon-threadlimit", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "solana-stake-program", "solana-transaction-status", "solana-vote-program", @@ -4291,7 +4311,7 @@ dependencies = [ "solana-ledger", "solana-logger", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "solana-stake-program", "solana-transaction-status", "solana-version", @@ -4308,7 +4328,7 @@ dependencies = [ "solana-logger", "solana-move-loader-program", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "solana_libra_types", ] @@ -4333,7 +4353,7 @@ dependencies = [ "solana-logger", "solana-rayon-threadlimit", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "solana-stake-program", "solana-vest-program", "solana-vote-program", @@ -4370,7 +4390,7 @@ dependencies = [ "jemallocator", "log 0.4.8", "solana-metrics", - "solana-sdk", + "solana-sdk 1.2.9", ] [[package]] @@ -4379,7 +4399,7 @@ version = "1.2.9" dependencies = [ "fast-math", "hex 0.4.2", - "solana-sdk", + "solana-sdk 1.2.9", ] [[package]] @@ -4394,7 +4414,7 @@ dependencies = [ "reqwest", "serial_test", "serial_test_derive", - "solana-sdk", + "solana-sdk 1.2.9", ] [[package]] @@ -4411,7 +4431,7 @@ dependencies = [ "serde_derive", "serde_json", "solana-logger", - "solana-sdk", + "solana-sdk 1.2.9", "solana_libra_bytecode_verifier", "solana_libra_canonical_serialization", "solana_libra_compiler", @@ -4464,7 +4484,7 @@ version = "1.2.9" dependencies = [ "log 0.4.8", "solana-logger", - "solana-sdk", + "solana-sdk 1.2.9", ] [[package]] @@ -4484,7 +4504,7 @@ dependencies = [ "num-derive 0.3.0", "num-traits", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "thiserror", ] @@ -4506,7 +4526,7 @@ dependencies = [ "solana-logger", "solana-metrics", "solana-rayon-threadlimit", - "solana-sdk", + "solana-sdk 1.2.9", ] [[package]] @@ -4526,7 +4546,7 @@ dependencies = [ "solana-metrics", "solana-net-utils", "solana-notifier", - "solana-sdk", + "solana-sdk 1.2.9", "solana-stake-program", "tar", ] @@ -4552,7 +4572,7 @@ dependencies = [ "num-traits", "parking_lot 0.10.2", "semver", - "solana-sdk", + "solana-sdk 1.2.9", "thiserror", "url 2.1.1", ] @@ -4586,7 +4606,7 @@ dependencies = [ "solana-metrics", "solana-noop-program", "solana-rayon-threadlimit", - "solana-sdk", + "solana-sdk 1.2.9", "solana-stake-program", "solana-vote-program", "tempfile", @@ -4601,6 +4621,30 @@ dependencies = [ "serde", ] +[[package]] +name = "solana-sdk" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b628fa500e0b83df3e96f7cc21dc998d8841a994f1c2109475273e6448afd4" +dependencies = [ + "bincode", + "bs58 0.3.1", + "bv", + "hex 0.4.2", + "hmac", + "itertools 0.9.0", + "log 0.4.8", + "num-derive 0.3.0", + "num-traits", + "pbkdf2", + "serde", + "serde_bytes", + "serde_derive", + "sha2", + "solana-sdk-macro 1.2.4", + "thiserror", +] + [[package]] name = "solana-sdk" version = "1.2.9" @@ -4630,11 +4674,23 @@ dependencies = [ "sha2", "solana-crate-features", "solana-logger", - "solana-sdk-macro", + "solana-sdk-macro 1.2.9", "thiserror", "tiny-bip39", ] +[[package]] +name = "solana-sdk-macro" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5f311e7735323eb0ad348c68170c2503a2c56cfa1a261646d8182b373fa670" +dependencies = [ + "bs58 0.3.1", + "proc-macro2 1.0.18", + "quote 1.0.7", + "syn 1.0.33", +] + [[package]] name = "solana-sdk-macro" version = "1.2.9" @@ -4655,7 +4711,7 @@ dependencies = [ "solana-client", "solana-remote-wallet", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "solana-stake-program", ] @@ -4677,7 +4733,7 @@ dependencies = [ "solana-local-cluster", "solana-logger", "solana-metrics", - "solana-sdk", + "solana-sdk 1.2.9", "solana-stake-program", "solana-transaction-status", "solana-version", @@ -4697,7 +4753,7 @@ dependencies = [ "solana-logger", "solana-metrics", "solana-notifier", - "solana-sdk", + "solana-sdk 1.2.9", "solana-stake-program", "solana-transaction-status", ] @@ -4715,7 +4771,7 @@ dependencies = [ "solana-config-program", "solana-logger", "solana-metrics", - "solana-sdk", + "solana-sdk 1.2.9", "solana-vote-program", "thiserror", ] @@ -4731,7 +4787,7 @@ dependencies = [ "solana-measure", "solana-metrics", "solana-perf", - "solana-sdk", + "solana-sdk 1.2.9", "thiserror", ] @@ -4770,7 +4826,7 @@ dependencies = [ "solana-core", "solana-remote-wallet", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "solana-stake-program", "solana-transaction-status", "tempfile", @@ -4781,11 +4837,15 @@ dependencies = [ name = "solana-transaction-status" version = "1.2.9" dependencies = [ + "Inflector", "bincode", "bs58 0.3.1", + "lazy_static", "serde", "serde_derive", - "solana-sdk", + "serde_json", + "solana-sdk 1.2.9", + "spl-memo", ] [[package]] @@ -4819,7 +4879,7 @@ dependencies = [ "solana-net-utils", "solana-perf", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "solana-version", "solana-vote-program", "solana-vote-signer", @@ -4831,7 +4891,7 @@ version = "1.2.9" dependencies = [ "serde", "serde_derive", - "solana-sdk", + "solana-sdk 1.2.9", ] [[package]] @@ -4846,7 +4906,7 @@ dependencies = [ "serde_derive", "solana-config-program", "solana-runtime", - "solana-sdk", + "solana-sdk 1.2.9", "thiserror", ] @@ -4861,7 +4921,7 @@ dependencies = [ "serde", "serde_derive", "solana-metrics", - "solana-sdk", + "solana-sdk 1.2.9", "thiserror", ] @@ -4877,7 +4937,7 @@ dependencies = [ "serde_json", "solana-clap-utils", "solana-metrics", - "solana-sdk", + "solana-sdk 1.2.9", "solana-version", ] @@ -4895,7 +4955,7 @@ dependencies = [ "solana-logger", "solana-metrics", "solana-notifier", - "solana-sdk", + "solana-sdk 1.2.9", "solana-transaction-status", "solana-version", "solana-vote-program", @@ -5268,6 +5328,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spl-memo" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e6b954ac8b1df3f0bbb6ad1f21607be304f3cc9914bb9107c44b2065c8479e" +dependencies = [ + "solana-sdk 1.2.4", +] + [[package]] name = "stable_deref_trait" version = "1.1.1" diff --git a/Cargo.toml b/Cargo.toml index 5108ab02bf..17448b0f98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,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 0000000000..8083aa895c --- /dev/null +++ b/account-decoder/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "solana-account-decoder" +version = "1.2.9" +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.2.9" } +solana-vote-program = { path = "../programs/vote", version = "1.2.9" } +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 0000000000..ec2d52f30a --- /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 0000000000..2e46d8e0bd --- /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 0000000000..5f82d0dbab --- /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 0000000000..051de67198 --- /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 86210c36b0..6fc24ca93f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -27,6 +27,7 @@ reqwest = { version = "0.10.4", default-features = false, features = ["blocking" serde = "1.0.110" serde_derive = "1.0.103" serde_json = "1.0.53" +solana-account-decoder = { path = "../account-decoder", version = "1.2.9" } solana-budget-program = { path = "../programs/budget", version = "1.2.9" } solana-clap-utils = { path = "../clap-utils", version = "1.2.9" } solana-cli-config = { path = "../cli-config", version = "1.2.9" } diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 0580b7b936..2726dba172 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, COMMITMENT_ARG}, @@ -28,7 +29,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; @@ -57,7 +58,7 @@ use solana_stake_program::{ stake_instruction::LockupArgs, stake_state::{Lockup, StakeAuthorize}, }; -use solana_transaction_status::{EncodedTransaction, TransactionEncoding}; +use solana_transaction_status::{EncodedTransaction, UiTransactionEncoding}; use solana_vote_program::vote_state::VoteAuthorize; use std::{ error, @@ -1173,7 +1174,7 @@ fn process_confirm( if let Some(transaction_status) = status { if config.verbose { match rpc_client - .get_confirmed_transaction(signature, TransactionEncoding::Binary) + .get_confirmed_transaction(signature, UiTransactionEncoding::Binary) { Ok(confirmed_transaction) => { println!( @@ -1226,7 +1227,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/display.rs b/cli/src/display.rs index b089564b17..a623383051 100644 --- a/cli/src/display.rs +++ b/cli/src/display.rs @@ -5,7 +5,7 @@ use solana_sdk::{ hash::Hash, native_token::lamports_to_sol, program_utils::limited_deserialize, transaction::Transaction, }; -use solana_transaction_status::RpcTransactionStatusMeta; +use solana_transaction_status::UiTransactionStatusMeta; use std::{fmt, io}; // Pretty print a "name value" @@ -68,7 +68,7 @@ pub fn println_signers( pub fn write_transaction( w: &mut W, transaction: &Transaction, - transaction_status: &Option, + transaction_status: &Option, prefix: &str, ) -> io::Result<()> { let message = &transaction.message; @@ -191,7 +191,7 @@ pub fn write_transaction( pub fn println_transaction( transaction: &Transaction, - transaction_status: &Option, + transaction_status: &Option, prefix: &str, ) { let mut w = Vec::new(); diff --git a/cli/src/offline/blockhash_query.rs b/cli/src/offline/blockhash_query.rs index b6ee58a57d..3fe9cea2bf 100644 --- a/cli/src/offline/blockhash_query.rs +++ b/cli/src/offline/blockhash_query.rs @@ -106,9 +106,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, @@ -344,7 +345,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 64349bbdfb..c0ea21919e 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -19,9 +19,10 @@ reqwest = { version = "0.10.4", default-features = false, features = ["blocking" serde = "1.0.110" serde_derive = "1.0.103" serde_json = "1.0.53" -solana-transaction-status = { path = "../transaction-status", version = "1.2.9" } +solana-account-decoder = { path = "../account-decoder", version = "1.2.9" } solana-net-utils = { path = "../net-utils", version = "1.2.9" } solana-sdk = { path = "../sdk", version = "1.2.9" } +solana-transaction-status = { path = "../transaction-status", version = "1.2.9" } solana-vote-program = { path = "../programs/vote", version = "1.2.9" } thiserror = "1.0" tungstenite = "0.10.1" diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index 91c2d6a6a8..f38c498a20 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::{ @@ -28,7 +29,7 @@ use solana_sdk::{ transaction::{self, Transaction}, }; use solana_transaction_status::{ - ConfirmedBlock, ConfirmedTransaction, TransactionEncoding, TransactionStatus, + ConfirmedBlock, ConfirmedTransaction, TransactionStatus, UiTransactionEncoding, }; use solana_vote_program::vote_state::MAX_LOCKOUT_HISTORY; use std::{ @@ -238,13 +239,13 @@ impl RpcClient { } pub fn get_confirmed_block(&self, slot: Slot) -> ClientResult { - self.get_confirmed_block_with_encoding(slot, TransactionEncoding::Json) + self.get_confirmed_block_with_encoding(slot, UiTransactionEncoding::Json) } pub fn get_confirmed_block_with_encoding( &self, slot: Slot, - encoding: TransactionEncoding, + encoding: UiTransactionEncoding, ) -> ClientResult { self.send(RpcRequest::GetConfirmedBlock, json!([slot, encoding])) } @@ -285,7 +286,7 @@ impl RpcClient { pub fn get_confirmed_transaction( &self, signature: &Signature, - encoding: TransactionEncoding, + encoding: UiTransactionEncoding, ) -> ClientResult { self.send( RpcRequest::GetConfirmedTransaction, @@ -452,9 +453,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 a3b282244f..02509e30a6 100644 --- a/client/src/rpc_config.rs +++ b/client/src/rpc_config.rs @@ -1,4 +1,5 @@ use crate::rpc_filter::RpcFilterType; +use solana_account_decoder::UiAccountEncoding; use solana_sdk::{clock::Epoch, commitment_config::CommitmentConfig}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -44,8 +45,16 @@ pub struct RpcStakeConfig { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct RpcProgramAccountsConfig { - pub filters: Option>, +pub struct RpcAccountInfoConfig { + pub encoding: Option, #[serde(flatten)] pub commitment: Option, } + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcProgramAccountsConfig { + pub filters: Option>, + #[serde(flatten)] + pub account_config: RpcAccountInfoConfig, +} diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index 275bd8c803..327f333449 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 7fa8d4b2e5..02195efa30 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -42,11 +42,11 @@ regex = "1.3.7" serde = "1.0.110" serde_derive = "1.0.103" serde_json = "1.0.53" +solana-account-decoder = { path = "../account-decoder", version = "1.2.9" } solana-bpf-loader-program = { path = "../programs/bpf_loader", version = "1.2.9" } solana-budget-program = { path = "../programs/budget", version = "1.2.9" } solana-clap-utils = { path = "../clap-utils", version = "1.2.9" } solana-client = { path = "../client", version = "1.2.9" } -solana-transaction-status = { path = "../transaction-status", version = "1.2.9" } solana-faucet = { path = "../faucet", version = "1.2.9" } solana-genesis-programs = { path = "../genesis-programs", version = "1.2.9" } solana-ledger = { path = "../ledger", version = "1.2.9" } @@ -60,10 +60,11 @@ solana-runtime = { path = "../runtime", version = "1.2.9" } solana-sdk = { path = "../sdk", version = "1.2.9" } solana-stake-program = { path = "../programs/stake", version = "1.2.9" } solana-streamer = { path = "../streamer", version = "1.2.9" } +solana-sys-tuner = { path = "../sys-tuner", version = "1.2.9" } +solana-transaction-status = { path = "../transaction-status", version = "1.2.9" } solana-version = { path = "../version", version = "1.2.9" } solana-vote-program = { path = "../programs/vote", version = "1.2.9" } solana-vote-signer = { path = "../vote-signer", version = "1.2.9" } -solana-sys-tuner = { path = "../sys-tuner", version = "1.2.9" } tempfile = "3.1.0" thiserror = "1.0" tokio = "0.1" diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 3971274959..caab886ec7 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -13,6 +13,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_filter::RpcFilterType, @@ -45,7 +46,7 @@ use solana_sdk::{ }; use solana_stake_program::stake_state::StakeState; use solana_transaction_status::{ - ConfirmedBlock, ConfirmedTransaction, TransactionEncoding, TransactionStatus, + ConfirmedBlock, ConfirmedTransaction, TransactionStatus, UiTransactionEncoding, }; use solana_vote_program::vote_state::{VoteState, MAX_LOCKOUT_HISTORY}; use std::{ @@ -160,10 +161,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( @@ -179,11 +186,13 @@ impl JsonRpcRequestProcessor { pub fn get_program_accounts( &self, program_id: &Pubkey, - commitment: Option, + config: Option, filters: Vec, ) -> 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() .filter(|(_, account)| { @@ -194,7 +203,7 @@ impl JsonRpcRequestProcessor { }) .map(|(pubkey, account)| RpcKeyedAccount { pubkey: pubkey.to_string(), - account: RpcAccount::encode(account), + account: UiAccount::encode(account, encoding.clone()), }) .collect()) } @@ -493,7 +502,7 @@ impl JsonRpcRequestProcessor { pub fn get_confirmed_block( &self, slot: Slot, - encoding: Option, + encoding: Option, ) -> Result> { if self.config.enable_rpc_transaction_history && slot @@ -662,7 +671,7 @@ impl JsonRpcRequestProcessor { pub fn get_confirmed_transaction( &self, signature: Signature, - encoding: Option, + encoding: Option, ) -> Result> { if self.config.enable_rpc_transaction_history { Ok(self @@ -847,8 +856,8 @@ pub trait RpcSol { &self, meta: Self::Metadata, pubkey_str: String, - commitment: Option, - ) -> Result>>; + config: Option, + ) -> Result>>; #[rpc(meta, name = "getProgramAccounts")] fn get_program_accounts( @@ -1042,7 +1051,7 @@ pub trait RpcSol { &self, meta: Self::Metadata, slot: Slot, - encoding: Option, + encoding: Option, ) -> Result>; #[rpc(meta, name = "getBlockTime")] @@ -1061,7 +1070,7 @@ pub trait RpcSol { &self, meta: Self::Metadata, signature_str: String, - encoding: Option, + encoding: Option, ) -> Result>; #[rpc(meta, name = "getConfirmedSignaturesForAddress")] @@ -1104,11 +1113,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( @@ -1135,15 +1144,18 @@ impl RpcSol for RpcSolImpl { program_id_str ); let program_id = verify_pubkey(program_id_str)?; - let (commitment, filters) = if let Some(config) = config { - (config.commitment, config.filters.unwrap_or_default()) + let (config, filters) = if let Some(config) = config { + ( + Some(config.account_config), + config.filters.unwrap_or_default(), + ) } else { (None, vec![]) }; for filter in &filters { verify_filter(filter)?; } - meta.get_program_accounts(&program_id, commitment, filters) + meta.get_program_accounts(&program_id, config, filters) } fn get_inflation_governor( @@ -1548,7 +1560,7 @@ impl RpcSol for RpcSolImpl { &self, meta: Self::Metadata, slot: Slot, - encoding: Option, + encoding: Option, ) -> Result> { meta.get_confirmed_block(slot, encoding) } @@ -1570,7 +1582,7 @@ impl RpcSol for RpcSolImpl { &self, meta: Self::Metadata, signature_str: String, - encoding: Option, + encoding: Option, ) -> Result> { let signature = verify_signature(&signature_str)?; meta.get_confirmed_transaction(signature, encoding) @@ -1679,7 +1691,7 @@ pub mod tests { system_instruction, system_program, system_transaction, transaction::{self, TransactionError}, }; - use solana_transaction_status::{EncodedTransaction, TransactionWithStatusMeta}; + use solana_transaction_status::{EncodedTransaction, TransactionWithStatusMeta, UiMessage}; use solana_vote_program::{ vote_instruction, vote_state::{Vote, VoteInit, MAX_LOCKOUT_HISTORY}, @@ -3416,7 +3428,11 @@ pub mod tests { if let EncodedTransaction::Json(transaction) = transaction { if transaction.signatures[0] == confirmed_block_signatures[0].to_string() { let meta = meta.unwrap(); - assert_eq!(transaction.message.recent_blockhash, blockhash.to_string()); + let transaction_recent_blockhash = match transaction.message { + UiMessage::Parsed(message) => message.recent_blockhash, + UiMessage::Raw(message) => message.recent_blockhash, + }; + assert_eq!(transaction_recent_blockhash, blockhash.to_string()); assert_eq!(meta.status, Ok(())); assert_eq!(meta.err, None); } else if transaction.signatures[0] == confirmed_block_signatures[1].to_string() { diff --git a/core/src/rpc_pubsub.rs b/core/src/rpc_pubsub.rs index d8dad96bd6..c133bd11d9 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_ledger::{bank_forks::BankForks, blockstore::Blockstore}; 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, ); @@ -177,7 +176,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 30810ae3f1..fbe8cb85f0 100644 --- a/core/src/rpc_subscriptions.rs +++ b/core/src/rpc_subscriptions.rs @@ -8,8 +8,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_ledger::{bank_forks::BankForks, blockstore::Blockstore}; use solana_runtime::bank::Bank; @@ -92,7 +93,7 @@ struct SubscriptionData { last_notified_slot: RwLock, } type RpcAccountSubscriptions = - RwLock>>>>; + RwLock>>>>; type RpcProgramSubscriptions = RwLock>>>>; type RpcSignatureSubscriptions = RwLock< @@ -227,12 +228,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) @@ -262,7 +269,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, @@ -456,7 +463,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 feb2f1bffb..37bb955e42 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 4e33791cb1..2fabb077c2 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -138,7 +138,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: @@ -148,7 +151,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 @@ -156,10 +159,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":{"executable":false,"owner":"4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM","lamports":1,"data":"Joig2k8Ax4JPMpWhXRyc2jMa7Wejz4X1xqVi3i7QRkmVj1ChUgNc4VNpGUQePJGBAui3c6886peU9GEbjsyeANN8JGStprwLbLwcw5wpPjuQQb9mwrjVmoDQBjj3MzZKgeHn6wmnQ5k8DBFuoCYKWWsJfH2gv9FvCzrN6K1CRcQZzF","rentEpoch":2}},"id":1} +{"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":{"data":{"nonce":{"initialized":{"authority":"Bbqg1M4YVVfbhEzwA9SpC9FhsaG83YMTYoR4a8oTDLX","blockhash":"3xLP3jK6dVJwpeGeTDYTwdDK3TKchUf1gYYGHa4sF3XJ","feeCalculator":{"lamportsPerSignature":5000}}}},"executable":false,"lamports":1000000000,"owner":"11111111111111111111111111111111","rentEpoch":2}},"id":1} ``` ### getBalance @@ -280,7 +289,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" or "binary". If 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: @@ -399,7 +409,8 @@ Returns transaction details for a confirmed transaction #### Parameters: * `` - transaction signature as base-58 encoded string -* `` - (optional) encoding for the returned Transaction, either "json" or "binary". If 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: @@ -778,6 +789,8 @@ Returns all accounts owned by the provided program Pubkey * `` - Pubkey of program, as base-58 encoded string * `` - (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 ``. * (optional) `filters: ` - filter results using various [filter objects](jsonrpc-api.md#filters); account must meet all filter criteria to be included in results ##### Filters: @@ -795,7 +808,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/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 252b54add9..9adfd4ffb4 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -37,8 +37,8 @@ use solana_sdk::{ transaction::Transaction, }; use solana_transaction_status::{ - ConfirmedBlock, ConfirmedTransaction, EncodedTransaction, Rewards, RpcTransactionStatusMeta, - TransactionEncoding, TransactionStatusMeta, TransactionWithStatusMeta, + ConfirmedBlock, ConfirmedTransaction, EncodedTransaction, Rewards, TransactionStatusMeta, + TransactionWithStatusMeta, UiTransactionEncoding, UiTransactionStatusMeta, }; use solana_vote_program::{vote_instruction::VoteInstruction, vote_state::TIMESTAMP_SLOT_INTERVAL}; use std::{ @@ -1580,7 +1580,7 @@ impl Blockstore { pub fn get_confirmed_block( &self, slot: Slot, - encoding: Option, + encoding: Option, ) -> Result { datapoint_info!( "blockstore-rpc-api", @@ -1592,7 +1592,7 @@ impl Blockstore { if *lowest_cleanup_slot > 0 && *lowest_cleanup_slot >= slot { return Err(BlockstoreError::SlotCleanedUp); } - let encoding = encoding.unwrap_or(TransactionEncoding::Json); + let encoding = encoding.unwrap_or(UiTransactionEncoding::Json); if self.is_root(slot) { let slot_meta_cf = self.db.column::(); let slot_meta = match slot_meta_cf.get(slot)? { @@ -1643,7 +1643,7 @@ impl Blockstore { fn map_transactions_to_statuses<'a>( &self, slot: Slot, - encoding: TransactionEncoding, + encoding: UiTransactionEncoding, iterator: impl Iterator + 'a, ) -> Vec { iterator @@ -1655,7 +1655,7 @@ impl Blockstore { meta: self .read_transaction_status((signature, slot)) .expect("Expect database get to succeed") - .map(RpcTransactionStatusMeta::from), + .map(UiTransactionStatusMeta::from), } }) .collect() @@ -1825,7 +1825,7 @@ impl Blockstore { pub fn get_confirmed_transaction( &self, signature: Signature, - encoding: Option, + encoding: Option, ) -> Result> { datapoint_info!( "blockstore-rpc-api", @@ -1834,7 +1834,7 @@ impl Blockstore { if let Some((slot, status)) = self.get_transaction_status(signature.clone())? { let transaction = self.find_transaction_in_slot(slot, signature)? .expect("Transaction to exist in slot entries if it exists in statuses and hasn't been cleaned up"); - let encoding = encoding.unwrap_or(TransactionEncoding::Json); + let encoding = encoding.unwrap_or(UiTransactionEncoding::Json); let encoded_transaction = EncodedTransaction::encode(transaction, encoding); Ok(Some(ConfirmedTransaction { slot, @@ -5164,7 +5164,7 @@ pub mod tests { .put_meta_bytes(slot - 1, &serialize(&parent_meta).unwrap()) .unwrap(); - let expected_transactions: Vec<(Transaction, Option)> = entries + let expected_transactions: Vec<(Transaction, Option)> = entries .iter() .cloned() .filter(|entry| !entry.is_tick()) @@ -5228,7 +5228,7 @@ pub mod tests { .iter() .cloned() .map(|(tx, meta)| TransactionWithStatusMeta { - transaction: EncodedTransaction::encode(tx, TransactionEncoding::Json), + transaction: EncodedTransaction::encode(tx, UiTransactionEncoding::Json), meta, }) .collect(), @@ -5249,7 +5249,7 @@ pub mod tests { .iter() .cloned() .map(|(tx, meta)| TransactionWithStatusMeta { - transaction: EncodedTransaction::encode(tx, TransactionEncoding::Json), + transaction: EncodedTransaction::encode(tx, UiTransactionEncoding::Json), meta, }) .collect(), @@ -5866,7 +5866,7 @@ pub mod tests { blockstore.insert_shreds(shreds, None, false).unwrap(); blockstore.set_roots(&[slot - 1, slot]).unwrap(); - let expected_transactions: Vec<(Transaction, Option)> = entries + let expected_transactions: Vec<(Transaction, Option)> = entries .iter() .cloned() .filter(|entry| !entry.is_tick()) @@ -5909,7 +5909,7 @@ pub mod tests { for (transaction, status) in expected_transactions.clone() { let signature = transaction.signatures[0]; let encoded_transaction = - EncodedTransaction::encode(transaction, TransactionEncoding::Json); + EncodedTransaction::encode(transaction, UiTransactionEncoding::Json); let expected_transaction = ConfirmedTransaction { slot, transaction: TransactionWithStatusMeta { @@ -6137,7 +6137,7 @@ pub mod tests { let map = blockstore.map_transactions_to_statuses( slot, - TransactionEncoding::Json, + UiTransactionEncoding::Json, transactions.into_iter(), ); assert_eq!(map.len(), 5); diff --git a/stake-monitor/src/lib.rs b/stake-monitor/src/lib.rs index bd24fffae7..3911cd3419 100644 --- a/stake-monitor/src/lib.rs +++ b/stake-monitor/src/lib.rs @@ -7,7 +7,7 @@ use solana_sdk::{ pubkey::Pubkey, signature::Signature, transaction::Transaction, }; use solana_stake_program::{stake_instruction::StakeInstruction, stake_state::Lockup}; -use solana_transaction_status::{ConfirmedBlock, RpcTransactionStatusMeta, TransactionEncoding}; +use solana_transaction_status::{ConfirmedBlock, UiTransactionEncoding, UiTransactionStatusMeta}; use std::{collections::HashMap, thread::sleep, time::Duration}; pub type PubkeyString = String; @@ -65,7 +65,7 @@ impl AccountsInfo { fn process_transaction( slot: Slot, transaction: &Transaction, - meta: &RpcTransactionStatusMeta, + meta: &UiTransactionStatusMeta, accounts: &mut HashMap, ) { let mut last_instruction = true; @@ -289,7 +289,7 @@ fn load_blocks( let mut blocks = vec![]; for slot in slots.into_iter() { let block = - rpc_client.get_confirmed_block_with_encoding(slot, TransactionEncoding::Binary)?; + rpc_client.get_confirmed_block_with_encoding(slot, UiTransactionEncoding::Binary)?; blocks.push((slot, block)); } Ok(blocks) diff --git a/transaction-status/Cargo.toml b/transaction-status/Cargo.toml index 63209f6805..25a6742878 100644 --- a/transaction-status/Cargo.toml +++ b/transaction-status/Cargo.toml @@ -11,9 +11,13 @@ 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.2.9" } +spl-memo = "1.0.0" serde = "1.0.110" serde_derive = "1.0.103" +serde_json = "1.0.54" [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index fb42d20748..e3cd4d67b8 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -1,22 +1,48 @@ #[macro_use] +extern crate lazy_static; +#[macro_use] extern crate serde_derive; +pub mod parse_accounts; +pub mod parse_instruction; + +use crate::{parse_accounts::parse_accounts, parse_instruction::parse}; +use serde_json::Value; use solana_sdk::{ clock::Slot, commitment_config::CommitmentConfig, + instruction::CompiledInstruction, message::MessageHeader, transaction::{Result, Transaction, TransactionError}, }; -/// A duplicate representation of a Message for pretty JSON serialization +/// A duplicate representation of an Instruction for pretty JSON serialization +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", untagged)] +pub enum UiInstruction { + Compiled(UiCompiledInstruction), + Parsed(Value), +} + +/// A duplicate representation of a CompiledInstruction for pretty JSON serialization #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct RpcCompiledInstruction { +pub struct UiCompiledInstruction { pub program_id_index: u8, pub accounts: Vec, pub data: String, } +impl From<&CompiledInstruction> for UiCompiledInstruction { + fn from(instruction: &CompiledInstruction) -> Self { + Self { + program_id_index: instruction.program_id_index, + accounts: instruction.accounts.clone(), + data: bs58::encode(instruction.data.clone()).into_string(), + } + } +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TransactionStatusMeta { @@ -37,9 +63,10 @@ impl Default for TransactionStatusMeta { } } +/// A duplicate representation of TransactionStatusMeta with `err` field #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct RpcTransactionStatusMeta { +pub struct UiTransactionStatusMeta { pub err: Option, pub status: Result<()>, // This field is deprecated. See https://github.com/solana-labs/solana/issues/9302 pub fee: u64, @@ -47,7 +74,7 @@ pub struct RpcTransactionStatusMeta { pub post_balances: Vec, } -impl From for RpcTransactionStatusMeta { +impl From for UiTransactionStatusMeta { fn from(meta: TransactionStatusMeta) -> Self { Self { err: meta.status.clone().err(), @@ -104,76 +131,112 @@ pub struct ConfirmedTransaction { /// A duplicate representation of a Transaction for pretty JSON serialization #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct RpcTransaction { +pub struct UiTransaction { pub signatures: Vec, - pub message: RpcMessage, + pub message: UiMessage, } -/// A duplicate representation of a Message for pretty JSON serialization +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", untagged)] +pub enum UiMessage { + Parsed(UiParsedMessage), + Raw(UiRawMessage), +} + +/// A duplicate representation of a Message, in raw format, for pretty JSON serialization #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct RpcMessage { +pub struct UiRawMessage { pub header: MessageHeader, pub account_keys: Vec, pub recent_blockhash: String, - pub instructions: Vec, + pub instructions: Vec, +} + +/// A duplicate representation of a Message, in parsed format, for pretty JSON serialization +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UiParsedMessage { + pub account_keys: Value, + pub recent_blockhash: String, + pub instructions: Vec, } #[derive(Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TransactionWithStatusMeta { pub transaction: EncodedTransaction, - pub meta: Option, + pub meta: Option, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[serde(rename_all = "camelCase")] -pub enum TransactionEncoding { +pub enum UiTransactionEncoding { Binary, Json, + JsonParsed, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", untagged)] pub enum EncodedTransaction { Binary(String), - Json(RpcTransaction), + Json(UiTransaction), } impl EncodedTransaction { - pub fn encode(transaction: Transaction, encoding: TransactionEncoding) -> Self { - if encoding == TransactionEncoding::Json { - EncodedTransaction::Json(RpcTransaction { - signatures: transaction - .signatures - .iter() - .map(|sig| sig.to_string()) - .collect(), - message: RpcMessage { - header: transaction.message.header, - account_keys: transaction - .message - .account_keys - .iter() - .map(|pubkey| pubkey.to_string()) - .collect(), - recent_blockhash: transaction.message.recent_blockhash.to_string(), - instructions: transaction - .message - .instructions - .iter() - .map(|instruction| RpcCompiledInstruction { - program_id_index: instruction.program_id_index, - accounts: instruction.accounts.clone(), - data: bs58::encode(instruction.data.clone()).into_string(), - }) - .collect(), - }, - }) - } else { - EncodedTransaction::Binary( + pub fn encode(transaction: Transaction, encoding: UiTransactionEncoding) -> Self { + match encoding { + UiTransactionEncoding::Binary => EncodedTransaction::Binary( bs58::encode(bincode::serialize(&transaction).unwrap()).into_string(), - ) + ), + _ => { + let message = if encoding == UiTransactionEncoding::Json { + UiMessage::Raw(UiRawMessage { + header: transaction.message.header, + account_keys: transaction + .message + .account_keys + .iter() + .map(|pubkey| pubkey.to_string()) + .collect(), + recent_blockhash: transaction.message.recent_blockhash.to_string(), + instructions: transaction + .message + .instructions + .iter() + .map(|instruction| instruction.into()) + .collect(), + }) + } else { + UiMessage::Parsed(UiParsedMessage { + account_keys: parse_accounts(&transaction.message), + recent_blockhash: transaction.message.recent_blockhash.to_string(), + instructions: transaction + .message + .instructions + .iter() + .map(|instruction| { + let program_id = + instruction.program_id(&transaction.message.account_keys); + if let Some(parsed_instruction) = parse(program_id, instruction) { + UiInstruction::Parsed(parsed_instruction) + } else { + UiInstruction::Compiled(instruction.into()) + } + }) + .collect(), + }) + }; + EncodedTransaction::Json(UiTransaction { + signatures: transaction + .signatures + .iter() + .map(|sig| sig.to_string()) + .collect(), + message, + }) + } } } pub fn decode(&self) -> Option { diff --git a/transaction-status/src/parse_accounts.rs b/transaction-status/src/parse_accounts.rs new file mode 100644 index 0000000000..71ef7bf91e --- /dev/null +++ b/transaction-status/src/parse_accounts.rs @@ -0,0 +1,56 @@ +use serde_json::{json, Value}; +use solana_sdk::message::Message; + +type AccountAttributes = Vec; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +enum AccountAttribute { + Signer, + Writable, +} + +pub fn parse_accounts(message: &Message) -> Value { + let mut accounts: Vec = vec![]; + for (i, account_key) in message.account_keys.iter().enumerate() { + let mut attributes: AccountAttributes = vec![]; + if message.is_writable(i) { + attributes.push(AccountAttribute::Writable); + } + if message.is_signer(i) { + attributes.push(AccountAttribute::Signer); + } + accounts.push(json!({ account_key.to_string(): attributes })); + } + json!(accounts) +} + +#[cfg(test)] +mod test { + use super::*; + use solana_sdk::{message::MessageHeader, pubkey::Pubkey}; + + #[test] + fn test_parse_accounts() { + let pubkey0 = Pubkey::new_rand(); + let pubkey1 = Pubkey::new_rand(); + let pubkey2 = Pubkey::new_rand(); + let pubkey3 = Pubkey::new_rand(); + let mut message = Message::default(); + message.header = MessageHeader { + num_required_signatures: 2, + num_readonly_signed_accounts: 1, + num_readonly_unsigned_accounts: 1, + }; + message.account_keys = vec![pubkey0, pubkey1, pubkey2, pubkey3]; + + let expected_json = json!([ + {pubkey0.to_string(): ["writable", "signer"]}, + {pubkey1.to_string(): ["signer"]}, + {pubkey2.to_string(): ["writable"]}, + {pubkey3.to_string(): []}, + ]); + + assert_eq!(parse_accounts(&message), expected_json); + } +} diff --git a/transaction-status/src/parse_instruction.rs b/transaction-status/src/parse_instruction.rs new file mode 100644 index 0000000000..14bd14fd3a --- /dev/null +++ b/transaction-status/src/parse_instruction.rs @@ -0,0 +1,59 @@ +use inflector::Inflector; +use serde_json::{json, Value}; +use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey}; +use std::{ + collections::HashMap, + str::{from_utf8, FromStr}, +}; + +lazy_static! { + static ref MEMO_PROGRAM_ID: Pubkey = Pubkey::from_str(&spl_memo::id().to_string()).unwrap(); + static ref PARSABLE_PROGRAM_IDS: HashMap = { + let mut m = HashMap::new(); + m.insert(*MEMO_PROGRAM_ID, ParsableProgram::SplMemo); + m + }; +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +enum ParsableProgram { + SplMemo, +} + +pub fn parse(program_id: &Pubkey, instruction: &CompiledInstruction) -> Option { + PARSABLE_PROGRAM_IDS.get(program_id).map(|program_name| { + let parsed_json = match program_name { + ParsableProgram::SplMemo => parse_memo(instruction), + }; + json!({ format!("{:?}", program_name).to_kebab_case(): parsed_json }) + }) +} + +fn parse_memo(instruction: &CompiledInstruction) -> Value { + Value::String(from_utf8(&instruction.data).unwrap().to_string()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_parse() { + let memo_instruction = CompiledInstruction { + program_id_index: 0, + accounts: vec![], + data: vec![240, 159, 166, 150], + }; + let expected_json = json!({ + "spl-memo": "🦖" + }); + assert_eq!( + parse(&MEMO_PROGRAM_ID, &memo_instruction), + Some(expected_json) + ); + + let non_parsable_program_id = Pubkey::new(&[1; 32]); + assert_eq!(parse(&non_parsable_program_id, &memo_instruction), None); + } +} diff --git a/watchtower/src/main.rs b/watchtower/src/main.rs index a0fff73c14..3c16b538f2 100644 --- a/watchtower/src/main.rs +++ b/watchtower/src/main.rs @@ -15,7 +15,7 @@ use solana_sdk::{ clock::Slot, hash::Hash, native_token::lamports_to_sol, program_utils::limited_deserialize, pubkey::Pubkey, }; -use solana_transaction_status::{ConfirmedBlock, TransactionEncoding}; +use solana_transaction_status::{ConfirmedBlock, UiTransactionEncoding}; use solana_vote_program::vote_instruction::VoteInstruction; use std::{ error, @@ -229,7 +229,7 @@ fn load_blocks( let mut blocks = vec![]; for slot in slots.into_iter() { let block = - rpc_client.get_confirmed_block_with_encoding(slot, TransactionEncoding::Binary)?; + rpc_client.get_confirmed_block_with_encoding(slot, UiTransactionEncoding::Binary)?; blocks.push((slot, block)); } Ok(blocks)