solana_account_decoder_wasm/
parse_account_data.rs

1use std::collections::HashMap;
2
3use base64::Engine;
4use base64::prelude::BASE64_STANDARD;
5use inflector::Inflector;
6use serde::Deserialize;
7use serde::Serialize;
8use solana_account::ReadableAccount;
9pub use solana_account_decoder_client_types_wasm::ParsedAccount;
10use solana_account_decoder_client_types_wasm::UiAccount;
11use solana_account_decoder_client_types_wasm::UiAccountData;
12use solana_account_decoder_client_types_wasm::UiAccountEncoding;
13use solana_account_decoder_client_types_wasm::UiDataSliceConfig;
14use solana_clock::UnixTimestamp;
15use solana_instruction::error::InstructionError;
16use solana_pubkey::Pubkey;
17use solana_sdk_ids::address_lookup_table;
18use solana_sdk_ids::bpf_loader_upgradeable;
19use solana_sdk_ids::config;
20use solana_sdk_ids::stake;
21use solana_sdk_ids::system_program;
22use solana_sdk_ids::sysvar;
23use solana_sdk_ids::vote;
24use spl_token_2022_interface::extension::interest_bearing_mint::InterestBearingConfig;
25use spl_token_2022_interface::extension::scaled_ui_amount::ScaledUiAmountConfig;
26use thiserror::Error;
27
28use crate::MAX_BASE58_BYTES;
29use crate::parse_address_lookup_table::parse_address_lookup_table;
30use crate::parse_bpf_loader::parse_bpf_upgradeable_loader;
31use crate::parse_config::parse_config;
32use crate::parse_nonce::parse_nonce;
33use crate::parse_stake::parse_stake;
34use crate::parse_sysvar::parse_sysvar;
35use crate::parse_token::parse_token_v3;
36use crate::parse_vote::parse_vote;
37use crate::slice_data;
38
39pub static PARSABLE_PROGRAM_IDS: std::sync::LazyLock<HashMap<Pubkey, ParsableAccount>> =
40	std::sync::LazyLock::new(|| {
41		let mut m = HashMap::new();
42		m.insert(
43			address_lookup_table::id(),
44			ParsableAccount::AddressLookupTable,
45		);
46		m.insert(
47			bpf_loader_upgradeable::id(),
48			ParsableAccount::BpfUpgradeableLoader,
49		);
50		m.insert(config::id(), ParsableAccount::Config);
51		m.insert(system_program::id(), ParsableAccount::Nonce);
52		m.insert(spl_token_interface::id(), ParsableAccount::SplToken);
53		m.insert(
54			spl_token_2022_interface::id(),
55			ParsableAccount::SplToken2022,
56		);
57		m.insert(stake::id(), ParsableAccount::Stake);
58		m.insert(sysvar::id(), ParsableAccount::Sysvar);
59		m.insert(vote::id(), ParsableAccount::Vote);
60		m
61	});
62
63#[derive(Error, Debug)]
64pub enum ParseAccountError {
65	#[error("{0:?} account not parsable")]
66	AccountNotParsable(ParsableAccount),
67
68	#[error("Program not parsable")]
69	ProgramNotParsable,
70
71	#[error("Additional data required to parse: {0}")]
72	AdditionalDataMissing(String),
73
74	#[error("Instruction error")]
75	InstructionError(#[from] InstructionError),
76
77	#[error("Serde json error")]
78	SerdeJsonError(#[from] serde_json::error::Error),
79}
80
81#[derive(Debug, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub enum ParsableAccount {
84	AddressLookupTable,
85	BpfUpgradeableLoader,
86	Config,
87	Nonce,
88	SplToken,
89	SplToken2022,
90	Stake,
91	Sysvar,
92	Vote,
93}
94
95pub trait EncodeUiAccount {}
96
97fn encode_ui_accout_bs58<T: ReadableAccount>(
98	account: &T,
99	data_slice_config: Option<UiDataSliceConfig>,
100) -> String {
101	let slice = slice_data(account.data(), data_slice_config);
102	if slice.len() <= MAX_BASE58_BYTES {
103		bs58::encode(slice).into_string()
104	} else {
105		"error: data too large for bs58 encoding".to_string()
106	}
107}
108
109pub fn encode_ui_account<T: ReadableAccount>(
110	pubkey: &Pubkey,
111	account: &T,
112	encoding: UiAccountEncoding,
113	additional_data: Option<AccountAdditionalDataV3>,
114	data_slice_config: Option<UiDataSliceConfig>,
115) -> UiAccount {
116	let space = account.data().len();
117	let data = match encoding {
118		UiAccountEncoding::Binary => {
119			let data = encode_ui_accout_bs58(account, data_slice_config);
120			UiAccountData::LegacyBinary(data)
121		}
122		UiAccountEncoding::Base58 => {
123			let data = encode_ui_accout_bs58(account, data_slice_config);
124			UiAccountData::Binary(data, encoding)
125		}
126		UiAccountEncoding::Base64 => {
127			UiAccountData::Binary(
128				BASE64_STANDARD.encode(slice_data(account.data(), data_slice_config)),
129				encoding,
130			)
131		}
132		#[cfg(not(feature = "zstd"))]
133		UiAccountEncoding::Base64Zstd => todo!("zstd not supported without the zstd feature flag"),
134		#[cfg(feature = "zstd")]
135		UiAccountEncoding::Base64Zstd => {
136			use std::io::Write;
137
138			let mut encoder = zstd::stream::write::Encoder::new(Vec::new(), 0).unwrap();
139			match encoder
140				.write_all(slice_data(account.data(), data_slice_config))
141				.and_then(|()| encoder.finish())
142			{
143				Ok(zstd_data) => UiAccountData::Binary(BASE64_STANDARD.encode(zstd_data), encoding),
144				Err(_) => {
145					UiAccountData::Binary(
146						BASE64_STANDARD.encode(slice_data(account.data(), data_slice_config)),
147						UiAccountEncoding::Base64,
148					)
149				}
150			}
151		}
152		UiAccountEncoding::JsonParsed => {
153			if let Ok(parsed_data) =
154				parse_account_data_v3(pubkey, account.owner(), account.data(), additional_data)
155			{
156				UiAccountData::Json(parsed_data)
157			} else {
158				UiAccountData::Binary(
159					BASE64_STANDARD.encode(slice_data(account.data(), data_slice_config)),
160					UiAccountEncoding::Base64,
161				)
162			}
163		}
164	};
165	UiAccount {
166		lamports: account.lamports(),
167		data,
168		owner: *account.owner(),
169		executable: account.executable(),
170		rent_epoch: account.rent_epoch(),
171		space: Some(space as u64),
172	}
173}
174
175#[derive(Clone, Copy, Default)]
176pub struct AccountAdditionalDataV3 {
177	pub spl_token_additional_data: Option<SplTokenAdditionalDataV2>,
178}
179
180#[derive(Clone, Copy, Default)]
181pub struct SplTokenAdditionalData {
182	pub decimals: u8,
183	pub interest_bearing_config: Option<(InterestBearingConfig, UnixTimestamp)>,
184}
185
186impl SplTokenAdditionalData {
187	pub fn with_decimals(decimals: u8) -> Self {
188		Self {
189			decimals,
190			..Default::default()
191		}
192	}
193}
194
195#[derive(Clone, Copy, Default)]
196pub struct SplTokenAdditionalDataV2 {
197	pub decimals: u8,
198	pub interest_bearing_config: Option<(InterestBearingConfig, UnixTimestamp)>,
199	pub scaled_ui_amount_config: Option<(ScaledUiAmountConfig, UnixTimestamp)>,
200}
201
202impl From<SplTokenAdditionalData> for SplTokenAdditionalDataV2 {
203	fn from(v: SplTokenAdditionalData) -> Self {
204		Self {
205			decimals: v.decimals,
206			interest_bearing_config: v.interest_bearing_config,
207			scaled_ui_amount_config: None,
208		}
209	}
210}
211
212impl SplTokenAdditionalDataV2 {
213	pub fn with_decimals(decimals: u8) -> Self {
214		Self {
215			decimals,
216			..Default::default()
217		}
218	}
219}
220
221pub fn parse_account_data_v3(
222	pubkey: &Pubkey,
223	program_id: &Pubkey,
224	data: &[u8],
225	additional_data: Option<AccountAdditionalDataV3>,
226) -> Result<ParsedAccount, ParseAccountError> {
227	let program_name = PARSABLE_PROGRAM_IDS
228		.get(program_id)
229		.ok_or(ParseAccountError::ProgramNotParsable)?;
230	let additional_data = additional_data.unwrap_or_default();
231	let parsed_json = match program_name {
232		ParsableAccount::AddressLookupTable => {
233			serde_json::to_value(parse_address_lookup_table(data)?)?
234		}
235		ParsableAccount::BpfUpgradeableLoader => {
236			serde_json::to_value(parse_bpf_upgradeable_loader(data)?)?
237		}
238		ParsableAccount::Config => serde_json::to_value(parse_config(data, pubkey)?)?,
239		ParsableAccount::Nonce => serde_json::to_value(parse_nonce(data)?)?,
240		ParsableAccount::SplToken | ParsableAccount::SplToken2022 => {
241			serde_json::to_value(parse_token_v3(
242				data,
243				additional_data.spl_token_additional_data.as_ref(),
244			)?)?
245		}
246		ParsableAccount::Stake => serde_json::to_value(parse_stake(data)?)?,
247		ParsableAccount::Sysvar => serde_json::to_value(parse_sysvar(data, pubkey)?)?,
248		ParsableAccount::Vote => serde_json::to_value(parse_vote(data)?)?,
249	};
250	Ok(ParsedAccount {
251		program: format!("{program_name:?}").to_kebab_case(),
252		parsed: parsed_json,
253		space: data.len() as u64,
254	})
255}
256
257#[cfg(test)]
258mod test {
259	use solana_nonce::state::Data;
260	use solana_nonce::state::State;
261	use solana_nonce::versions::Versions;
262	use solana_vote_interface::program::id as vote_program_id;
263	use solana_vote_interface::state::VoteStateV3;
264	use solana_vote_interface::state::VoteStateVersions;
265
266	use super::*;
267
268	#[test]
269	fn test_parse_account_data() {
270		let account_pubkey = solana_pubkey::new_rand();
271		let other_program = solana_pubkey::new_rand();
272		let data = vec![0; 4];
273		assert!(parse_account_data_v3(&account_pubkey, &other_program, &data, None).is_err());
274
275		let vote_state = VoteStateV3::default();
276		let mut vote_account_data: Vec<u8> = vec![0; VoteStateV3::size_of()];
277		let versioned = VoteStateVersions::new_v3(vote_state);
278		VoteStateV3::serialize(&versioned, &mut vote_account_data).unwrap();
279		let parsed = parse_account_data_v3(
280			&account_pubkey,
281			&vote_program_id(),
282			&vote_account_data,
283			None,
284		)
285		.unwrap();
286		assert_eq!(parsed.program, "vote".to_string());
287		assert_eq!(parsed.space, VoteStateV3::size_of() as u64);
288
289		let nonce_data = Versions::new(State::Initialized(Data::default()));
290		let nonce_account_data = bincode::serialize(&nonce_data).unwrap();
291		let parsed = parse_account_data_v3(
292			&account_pubkey,
293			&system_program::id(),
294			&nonce_account_data,
295			None,
296		)
297		.unwrap();
298		assert_eq!(parsed.program, "nonce".to_string());
299		assert_eq!(parsed.space, State::size() as u64);
300	}
301}