solana_account_decoder/
lib.rs

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