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 std::io::Write;
19
20use base64::Engine;
21use base64::prelude::BASE64_STANDARD;
22use solana_account::ReadableAccount;
23pub use solana_account_decoder_client_types_wasm::UiAccount;
24pub use solana_account_decoder_client_types_wasm::UiAccountData;
25pub use solana_account_decoder_client_types_wasm::UiAccountEncoding;
26pub use solana_account_decoder_client_types_wasm::UiDataSliceConfig;
27use solana_fee_calculator::FeeCalculator;
28use solana_pubkey::Pubkey;
29
30use crate::parse_account_data::AccountAdditionalDataV3;
31use crate::parse_account_data::parse_account_data_v3;
32
33pub type StringAmount = String;
34pub type StringDecimals = String;
35pub const MAX_BASE58_BYTES: usize = 128;
36
37fn encode_bs58<T: ReadableAccount>(
38	account: &T,
39	data_slice_config: Option<UiDataSliceConfig>,
40) -> String {
41	let slice = slice_data(account.data(), data_slice_config);
42	if slice.len() <= MAX_BASE58_BYTES {
43		bs58::encode(slice).into_string()
44	} else {
45		"error: data too large for bs58 encoding".to_string()
46	}
47}
48
49pub fn encode_ui_account<T: ReadableAccount>(
50	pubkey: &Pubkey,
51	account: &T,
52	encoding: UiAccountEncoding,
53	additional_data: Option<AccountAdditionalDataV3>,
54	data_slice_config: Option<UiDataSliceConfig>,
55) -> UiAccount {
56	let space = account.data().len();
57	let data = match encoding {
58		UiAccountEncoding::Binary => {
59			let data = encode_bs58(account, data_slice_config);
60			UiAccountData::LegacyBinary(data)
61		}
62		UiAccountEncoding::Base58 => {
63			let data = encode_bs58(account, data_slice_config);
64			UiAccountData::Binary(data, encoding)
65		}
66		UiAccountEncoding::Base64 => {
67			UiAccountData::Binary(
68				BASE64_STANDARD.encode(slice_data(account.data(), data_slice_config)),
69				encoding,
70			)
71		}
72		#[cfg(not(feature = "zstd"))]
73		UiAccountEncoding::Base64Zstd => todo!("Currently zstd is not supported unless specified"),
74		#[cfg(feature = "zstd")]
75		UiAccountEncoding::Base64Zstd => {
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}