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