solana_account_decoder/
lib.rs

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        // Whole account
192        assert_eq!(
193            encode_bs58(&account, None),
194            "error: data too large for bs58 encoding"
195        );
196
197        // Slice of account that's still too large
198        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        // Slice of account that fits inside `MAX_BASE58_BYTES`
210        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        // Slice of account that's too large, but whose intersection with the account still fits
222        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}