solana_account_decoder_wasm/
lib.rs

1#![allow(clippy::arithmetic_side_effects)]
2
3use serde::Deserialize;
4use serde::Serialize;
5pub mod parse_account_data;
6pub mod parse_address_lookup_table;
7pub mod parse_bpf_loader;
8#[allow(deprecated)]
9pub mod parse_config;
10pub mod parse_nonce;
11pub mod parse_stake;
12pub mod parse_sysvar;
13pub mod parse_token;
14pub mod parse_token_extension;
15pub mod parse_vote;
16pub mod validator_info;
17
18use base64::Engine;
19use base64::prelude::BASE64_STANDARD;
20use solana_account::ReadableAccount;
21pub use solana_account_decoder_client_types_wasm::UiAccount;
22pub use solana_account_decoder_client_types_wasm::UiAccountData;
23pub use solana_account_decoder_client_types_wasm::UiAccountEncoding;
24pub use solana_account_decoder_client_types_wasm::UiDataSliceConfig;
25use solana_fee_calculator::FeeCalculator;
26use solana_pubkey::Pubkey;
27
28use crate::parse_account_data::AccountAdditionalDataV3;
29use crate::parse_account_data::parse_account_data_v3;
30
31pub type StringAmount = String;
32pub type StringDecimals = String;
33pub const MAX_BASE58_BYTES: usize = 128;
34
35fn encode_bs58<T: ReadableAccount>(
36	account: &T,
37	data_slice_config: Option<UiDataSliceConfig>,
38) -> String {
39	let slice = slice_data(account.data(), data_slice_config);
40	if slice.len() <= MAX_BASE58_BYTES {
41		bs58::encode(slice).into_string()
42	} else {
43		"error: data too large for bs58 encoding".to_string()
44	}
45}
46
47pub fn encode_ui_account<T: ReadableAccount>(
48	pubkey: &Pubkey,
49	account: &T,
50	encoding: UiAccountEncoding,
51	additional_data: Option<AccountAdditionalDataV3>,
52	data_slice_config: Option<UiDataSliceConfig>,
53) -> UiAccount {
54	let space = account.data().len();
55	let data = match encoding {
56		UiAccountEncoding::Binary => {
57			let data = encode_bs58(account, data_slice_config);
58			UiAccountData::LegacyBinary(data)
59		}
60		UiAccountEncoding::Base58 => {
61			let data = encode_bs58(account, data_slice_config);
62			UiAccountData::Binary(data, encoding)
63		}
64		UiAccountEncoding::Base64 => {
65			UiAccountData::Binary(
66				BASE64_STANDARD.encode(slice_data(account.data(), data_slice_config)),
67				encoding,
68			)
69		}
70		#[cfg(not(feature = "zstd"))]
71		UiAccountEncoding::Base64Zstd => todo!("Currently zstd is not supported unless specified"),
72		#[cfg(feature = "zstd")]
73		UiAccountEncoding::Base64Zstd => {
74			use std::io::Write;
75
76			let mut encoder = zstd::stream::write::Encoder::new(Vec::new(), 0).unwrap();
77			match encoder
78				.write_all(slice_data(account.data(), data_slice_config))
79				.and_then(|()| encoder.finish())
80			{
81				Ok(zstd_data) => UiAccountData::Binary(BASE64_STANDARD.encode(zstd_data), encoding),
82				Err(_) => {
83					UiAccountData::Binary(
84						BASE64_STANDARD.encode(slice_data(account.data(), data_slice_config)),
85						UiAccountEncoding::Base64,
86					)
87				}
88			}
89		}
90		UiAccountEncoding::JsonParsed => {
91			if let Ok(parsed_data) =
92				parse_account_data_v3(pubkey, account.owner(), account.data(), additional_data)
93			{
94				UiAccountData::Json(parsed_data)
95			} else {
96				UiAccountData::Binary(
97					BASE64_STANDARD.encode(slice_data(account.data(), data_slice_config)),
98					UiAccountEncoding::Base64,
99				)
100			}
101		}
102	};
103	UiAccount {
104		lamports: account.lamports(),
105		data,
106		owner: *account.owner(),
107		executable: account.executable(),
108		rent_epoch: account.rent_epoch(),
109		space: Some(space as u64),
110	}
111}
112
113#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
114#[serde(rename_all = "camelCase")]
115pub struct UiFeeCalculator {
116	pub lamports_per_signature: StringAmount,
117}
118
119impl From<FeeCalculator> for UiFeeCalculator {
120	fn from(fee_calculator: FeeCalculator) -> Self {
121		Self {
122			lamports_per_signature: fee_calculator.lamports_per_signature.to_string(),
123		}
124	}
125}
126
127impl Default for UiFeeCalculator {
128	fn default() -> Self {
129		Self {
130			lamports_per_signature: "0".to_string(),
131		}
132	}
133}
134
135fn slice_data(data: &[u8], data_slice_config: Option<UiDataSliceConfig>) -> &[u8] {
136	if let Some(UiDataSliceConfig { offset, length }) = data_slice_config {
137		if offset >= data.len() {
138			&[]
139		} else if length > data.len() - offset {
140			&data[offset..]
141		} else {
142			&data[offset..offset + length]
143		}
144	} else {
145		data
146	}
147}
148
149#[cfg(test)]
150mod test {
151	use assert_matches::assert_matches;
152	use solana_account::Account;
153	use solana_account::AccountSharedData;
154
155	use super::*;
156
157	#[test]
158	fn test_slice_data() {
159		let data = vec![1, 2, 3, 4, 5];
160		let slice_config = Some(UiDataSliceConfig {
161			offset: 0,
162			length: 5,
163		});
164		assert_eq!(slice_data(&data, slice_config), &data[..]);
165
166		let slice_config = Some(UiDataSliceConfig {
167			offset: 0,
168			length: 10,
169		});
170		assert_eq!(slice_data(&data, slice_config), &data[..]);
171
172		let slice_config = Some(UiDataSliceConfig {
173			offset: 1,
174			length: 2,
175		});
176		assert_eq!(slice_data(&data, slice_config), &data[1..3]);
177
178		let slice_config = Some(UiDataSliceConfig {
179			offset: 10,
180			length: 2,
181		});
182		assert_eq!(slice_data(&data, slice_config), &[] as &[u8]);
183	}
184
185	#[test]
186	fn test_encode_account_when_data_exceeds_base58_byte_limit() {
187		let data = vec![42; MAX_BASE58_BYTES + 2];
188		let account = AccountSharedData::from(Account {
189			data,
190			..Account::default()
191		});
192
193		// Whole account
194		assert_eq!(
195			encode_bs58(&account, None),
196			"error: data too large for bs58 encoding"
197		);
198
199		// Slice of account that's still too large
200		assert_eq!(
201			encode_bs58(
202				&account,
203				Some(UiDataSliceConfig {
204					length: MAX_BASE58_BYTES + 1,
205					offset: 1
206				})
207			),
208			"error: data too large for bs58 encoding"
209		);
210
211		// Slice of account that fits inside `MAX_BASE58_BYTES`
212		assert_ne!(
213			encode_bs58(
214				&account,
215				Some(UiDataSliceConfig {
216					length: MAX_BASE58_BYTES,
217					offset: 1
218				})
219			),
220			"error: data too large for bs58 encoding"
221		);
222
223		// Slice of account that's too large, but whose intersection with the account
224		// still fits
225		assert_ne!(
226			encode_bs58(
227				&account,
228				Some(UiDataSliceConfig {
229					length: MAX_BASE58_BYTES + 1,
230					offset: 2
231				})
232			),
233			"error: data too large for bs58 encoding"
234		);
235	}
236
237	#[test]
238	#[cfg(feature = "zstd")]
239	fn test_base64_zstd() {
240		let encoded_account = encode_ui_account(
241			&Pubkey::default(),
242			&AccountSharedData::from(Account {
243				data: vec![0; 1024],
244				..Account::default()
245			}),
246			UiAccountEncoding::Base64Zstd,
247			None,
248			None,
249		);
250		assert_matches!(
251			encoded_account.data,
252			UiAccountData::Binary(_, UiAccountEncoding::Base64Zstd)
253		);
254
255		let decoded_account = encoded_account.decode::<Account>().unwrap();
256		assert_eq!(decoded_account.data(), &vec![0; 1024]);
257		let decoded_account = encoded_account.decode::<AccountSharedData>().unwrap();
258		assert_eq!(decoded_account.data(), &vec![0; 1024]);
259	}
260}