Skip to main content

surfpool_core/rpc/
utils.rs

1#![allow(dead_code)]
2
3use std::any::type_name;
4
5use base64::prelude::*;
6use jsonrpc_core::{Error, Result};
7use litesvm::types::TransactionMetadata;
8use solana_client::{
9    rpc_config::{RpcTokenAccountsFilter, RpcTransactionConfig},
10    rpc_filter::RpcFilterType,
11    rpc_request::{MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT, TokenAccountsFilter},
12};
13use solana_commitment_config::CommitmentConfig;
14use solana_hash::Hash;
15use solana_message::{
16    AccountKeys, VersionedMessage,
17    v1::{MAX_TRANSACTION_SIZE, V1_PREFIX},
18};
19use solana_packet::PACKET_DATA_SIZE;
20use solana_pubkey::{ParsePubkeyError, Pubkey};
21use solana_signature::Signature;
22use solana_transaction_status::{
23    InnerInstruction, InnerInstructions, TransactionBinaryEncoding, UiInnerInstructions,
24    UiTransactionEncoding, parse_ui_inner_instructions,
25};
26
27use crate::error::{SurfpoolError, SurfpoolResult};
28
29pub fn convert_transaction_metadata_from_canonical(
30    transaction_metadata: &TransactionMetadata,
31) -> surfpool_types::TransactionMetadata {
32    surfpool_types::TransactionMetadata {
33        signature: transaction_metadata.signature,
34        logs: transaction_metadata.logs.clone(),
35        inner_instructions: transaction_metadata.inner_instructions.clone(),
36        compute_units_consumed: transaction_metadata.compute_units_consumed,
37        return_data: transaction_metadata.return_data.clone(),
38        fee: transaction_metadata.fee,
39    }
40}
41
42fn optimize_filters(filters: &mut [RpcFilterType]) {
43    filters.iter_mut().for_each(|filter_type| {
44        if let RpcFilterType::Memcmp(compare) = filter_type {
45            if let Err(err) = compare.convert_to_raw_bytes() {
46                // All filters should have been previously verified
47                warn!("Invalid filter: bytes could not be decoded, {err}");
48            }
49        }
50    })
51}
52
53fn verify_filter(input: &RpcFilterType) -> Result<()> {
54    input
55        .verify()
56        .map_err(|e| Error::invalid_params(format!("Invalid param: {e:?}")))
57}
58
59pub fn verify_pubkey(input: &str) -> SurfpoolResult<Pubkey> {
60    input
61        .parse()
62        .map_err(|e: ParsePubkeyError| SurfpoolError::invalid_pubkey(input, e.to_string()))
63}
64
65pub fn verify_pubkeys(input: &[String]) -> SurfpoolResult<Vec<Pubkey>> {
66    input
67        .iter()
68        .enumerate()
69        .map(|(i, s)| {
70            verify_pubkey(s)
71                .map_err(|e| SurfpoolError::invalid_pubkey_at_index(s, i, e.to_string()))
72        })
73        .collect::<SurfpoolResult<Vec<_>>>()
74}
75
76fn verify_hash(input: &str) -> Result<Hash> {
77    input
78        .parse()
79        .map_err(|e| Error::invalid_params(format!("Invalid param: {e:?}")))
80}
81
82fn verify_signature(input: &str) -> Result<Signature> {
83    input
84        .parse()
85        .map_err(|e| Error::invalid_params(format!("Invalid param: {e:?}")))
86}
87
88fn verify_token_account_filter(
89    token_account_filter: RpcTokenAccountsFilter,
90) -> Result<TokenAccountsFilter> {
91    match token_account_filter {
92        RpcTokenAccountsFilter::Mint(mint_str) => {
93            let mint = verify_pubkey(&mint_str)?;
94            Ok(TokenAccountsFilter::Mint(mint))
95        }
96        RpcTokenAccountsFilter::ProgramId(program_id_str) => {
97            let program_id = verify_pubkey(&program_id_str)?;
98            Ok(TokenAccountsFilter::ProgramId(program_id))
99        }
100    }
101}
102
103fn verify_and_parse_signatures_for_address_params(
104    address: String,
105    before: Option<String>,
106    until: Option<String>,
107    limit: Option<usize>,
108) -> Result<(Pubkey, Option<Signature>, Option<Signature>, usize)> {
109    let address = verify_pubkey(&address)?;
110    let before = before
111        .map(|ref before| verify_signature(before))
112        .transpose()?;
113    let until = until.map(|ref until| verify_signature(until)).transpose()?;
114    let limit = limit.unwrap_or(MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT);
115
116    if limit == 0 || limit > MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT {
117        return Err(Error::invalid_params(format!(
118            "Invalid limit; max {MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT}"
119        )));
120    }
121    Ok((address, before, until, limit))
122}
123
124const MAX_BASE58_SIZE: usize = 5594; // Golden, bump if MAX_TRANSACTION_SIZE changes
125const MAX_BASE64_SIZE: usize = 5464; // Golden, bump if MAX_TRANSACTION_SIZE changes
126
127/// Highest transaction version supported by this Surfpool release.
128pub const MAX_SUPPORTED_TRANSACTION_VERSION: u8 = 1;
129
130fn wire_size_limit(wire_output: &[u8]) -> usize {
131    // V1 messages and transactions both start with the V1 message prefix. Legacy and V0
132    // transactions start with their short-vec signature count instead.
133    if wire_output.first().copied() == Some(V1_PREFIX) {
134        MAX_TRANSACTION_SIZE
135    } else {
136        PACKET_DATA_SIZE
137    }
138}
139
140pub fn decode_and_deserialize<T>(
141    encoded: String,
142    encoding: TransactionBinaryEncoding,
143) -> Result<(Vec<u8>, T)>
144where
145    T: wincode::DeserializeOwned<Dst = T>,
146{
147    let wire_output = match encoding {
148        TransactionBinaryEncoding::Base58 => {
149            if encoded.len() > MAX_BASE58_SIZE {
150                return Err(Error::invalid_params(format!(
151                    "base58 encoded {} too large: {} bytes (max: encoded/raw {}/{})",
152                    type_name::<T>(),
153                    encoded.len(),
154                    MAX_BASE58_SIZE,
155                    MAX_TRANSACTION_SIZE,
156                )));
157            }
158            bs58::decode(encoded)
159                .into_vec()
160                .map_err(|e| Error::invalid_params(format!("invalid base58 encoding: {e:?}")))?
161        }
162        TransactionBinaryEncoding::Base64 => {
163            if encoded.len() > MAX_BASE64_SIZE {
164                return Err(Error::invalid_params(format!(
165                    "base64 encoded {} too large: {} bytes (max: encoded/raw {}/{})",
166                    type_name::<T>(),
167                    encoded.len(),
168                    MAX_BASE64_SIZE,
169                    MAX_TRANSACTION_SIZE,
170                )));
171            }
172            BASE64_STANDARD
173                .decode(encoded)
174                .map_err(|e| Error::invalid_params(format!("invalid base64 encoding: {e:?}")))?
175        }
176    };
177    let size_limit = wire_size_limit(&wire_output);
178    if wire_output.len() > size_limit {
179        return Err(Error::invalid_params(format!(
180            "decoded {} too large: {} bytes (max: {} bytes)",
181            type_name::<T>(),
182            wire_output.len(),
183            size_limit
184        )));
185    }
186    wincode::deserialize(&wire_output)
187        .map_err(|err| {
188            Error::invalid_params(format!(
189                "failed to deserialize {}: {}",
190                type_name::<T>(),
191                &err.to_string()
192            ))
193        })
194        .map(|output| (wire_output, output))
195}
196
197/// Decode the RPC `data` parameter of `sendTransaction` and
198/// `simulateTransaction` into a `solana_transaction::VersionedTransaction`.
199///
200/// Both methods accept a `UiTransactionEncoding` on their config that defaults
201/// to `Base58`, map it to the internal `TransactionBinaryEncoding`, and feed
202/// the result into `decode_and_deserialize`. The mapping can fail (the RPC
203/// only accepts base58 and base64), which is reported back as an
204/// `Error::invalid_params` listing the supported encodings.
205pub fn decode_rpc_versioned_transaction(
206    data: String,
207    encoding: Option<UiTransactionEncoding>,
208) -> Result<solana_transaction::versioned::VersionedTransaction> {
209    let tx_encoding = encoding.unwrap_or(UiTransactionEncoding::Base58);
210    let binary_encoding = tx_encoding.into_binary_encoding().ok_or_else(|| {
211        Error::invalid_params(format!(
212            "unsupported encoding: {tx_encoding}. Supported encodings: base58, base64"
213        ))
214    })?;
215    let (_, unsanitized_tx) = decode_and_deserialize::<
216        solana_transaction::versioned::VersionedTransaction,
217    >(data, binary_encoding)?;
218    Ok(unsanitized_tx)
219}
220
221pub fn transform_tx_metadata_to_ui_accounts(
222    meta: TransactionMetadata,
223    message: &VersionedMessage,
224    loaded_addresses: Option<&solana_message::v0::LoadedAddresses>,
225) -> Vec<UiInnerInstructions> {
226    // Create AccountKeys from the transaction message with loaded addresses from ALTs
227    let account_keys = AccountKeys::new(message.static_account_keys(), loaded_addresses);
228
229    meta.inner_instructions
230        .into_iter()
231        .enumerate()
232        .filter_map(|(i, ixs)| {
233            let instructions: Vec<InnerInstruction> = ixs
234                .iter()
235                .map(|ix| InnerInstruction {
236                    instruction: ix.instruction.clone(),
237                    stack_height: Some(ix.stack_height as u32),
238                })
239                .collect();
240            if instructions.is_empty() {
241                None
242            } else {
243                // Create InnerInstructions and then parse it into UiInnerInstructions
244                // This will properly convert CompiledInstruction to UiInstruction format
245                let inner_instructions = InnerInstructions {
246                    index: i as u8,
247                    instructions,
248                };
249                Some(parse_ui_inner_instructions(
250                    inner_instructions,
251                    &account_keys,
252                ))
253            }
254        })
255        .collect()
256}
257
258/// Substrings that, when present in a lowercased error message, indicate the
259/// remote RPC method is not supported by the upstream (often a public endpoint
260/// that has gated methods behind a 410 Gone response or a custom refusal).
261const METHOD_NOT_SUPPORTED_NEEDLES: &[&str] = &[
262    "not supported",
263    "unsupported",
264    "unavailable",
265    "method blocked",
266    "invalid request",
267    "is blocked",
268    "if you need this method",
269    "client error 410",
270    "410 gone",
271    "(410 gone)",
272    " status 410",
273    "http 410",
274    "client error (410",
275];
276
277/// Returns true if the error indicates the remote method is not supported.
278pub fn is_method_not_supported_error<E: std::fmt::Display>(err: &E) -> bool {
279    let msg = err.to_string().to_lowercase();
280    METHOD_NOT_SUPPORTED_NEEDLES
281        .iter()
282        .any(|needle| msg.contains(needle))
283}
284
285pub fn get_default_transaction_config() -> RpcTransactionConfig {
286    RpcTransactionConfig {
287        encoding: Some(UiTransactionEncoding::Json),
288        commitment: Some(CommitmentConfig::default()),
289        max_supported_transaction_version: Some(MAX_SUPPORTED_TRANSACTION_VERSION),
290    }
291}
292
293pub fn adjust_default_transaction_config(config: &mut RpcTransactionConfig) {
294    if config.encoding.is_none() {
295        config.encoding = Some(UiTransactionEncoding::Json);
296    }
297    if config.max_supported_transaction_version.is_none() {
298        config.max_supported_transaction_version = Some(MAX_SUPPORTED_TRANSACTION_VERSION);
299    }
300    if config.commitment.is_none() {
301        config.commitment = Some(CommitmentConfig::default());
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use solana_keypair::Keypair;
308    use solana_message::{MessageHeader, compiled_instruction::CompiledInstruction, v1};
309    use solana_signer::Signer;
310    use solana_transaction::versioned::VersionedTransaction;
311
312    use super::*;
313
314    fn signed_v1_transaction(data_len: usize) -> VersionedTransaction {
315        let payer = Keypair::new();
316        let message = VersionedMessage::V1(v1::Message::new(
317            MessageHeader {
318                num_required_signatures: 1,
319                num_readonly_signed_accounts: 0,
320                num_readonly_unsigned_accounts: 1,
321            },
322            v1::TransactionConfig::empty(),
323            Hash::default(),
324            vec![payer.pubkey(), Pubkey::new_unique()],
325            vec![CompiledInstruction {
326                program_id_index: 1,
327                accounts: vec![0],
328                data: vec![0; data_len],
329            }],
330        ));
331        let transaction = VersionedTransaction::try_new(message, &[&payer]).unwrap();
332        assert!(
333            transaction.signatures[0]
334                .as_ref()
335                .iter()
336                .any(|byte| *byte != 0)
337        );
338        transaction
339    }
340
341    #[test]
342    fn decodes_signed_v1_transaction_larger_than_legacy_packet() {
343        let transaction = signed_v1_transaction(PACKET_DATA_SIZE);
344        let wire = wincode::serialize(&transaction).unwrap();
345        assert!(wire.len() > PACKET_DATA_SIZE);
346        assert!(wire.len() <= MAX_TRANSACTION_SIZE);
347        assert_eq!(wire[0], V1_PREFIX);
348
349        let encoded = BASE64_STANDARD.encode(&wire);
350        let (_, decoded) = decode_and_deserialize::<VersionedTransaction>(
351            encoded,
352            TransactionBinaryEncoding::Base64,
353        )
354        .unwrap();
355
356        assert_eq!(decoded, transaction);
357    }
358
359    #[test]
360    fn rejects_oversized_v1_and_legacy_wire_payloads() {
361        let mut oversized_v1 = vec![0; MAX_TRANSACTION_SIZE + 1];
362        oversized_v1[0] = V1_PREFIX;
363        let v1_error = decode_and_deserialize::<VersionedTransaction>(
364            BASE64_STANDARD.encode(oversized_v1),
365            TransactionBinaryEncoding::Base64,
366        )
367        .unwrap_err();
368        assert!(v1_error.message.contains("max: 4096 bytes"));
369
370        let oversized_legacy = vec![0; PACKET_DATA_SIZE + 1];
371        let legacy_error = decode_and_deserialize::<VersionedTransaction>(
372            BASE64_STANDARD.encode(oversized_legacy),
373            TransactionBinaryEncoding::Base64,
374        )
375        .unwrap_err();
376        assert!(legacy_error.message.contains("max: 1232 bytes"));
377    }
378
379    #[test]
380    fn worst_case_v1_encoded_size_goldens() {
381        let wire = vec![0xff; MAX_TRANSACTION_SIZE];
382        assert_eq!(bs58::encode(&wire).into_string().len(), MAX_BASE58_SIZE);
383        assert_eq!(BASE64_STANDARD.encode(&wire).len(), MAX_BASE64_SIZE);
384    }
385
386    #[test]
387    fn transaction_config_defaults_support_v1() {
388        assert_eq!(
389            get_default_transaction_config().max_supported_transaction_version,
390            Some(MAX_SUPPORTED_TRANSACTION_VERSION)
391        );
392
393        let mut config = RpcTransactionConfig::default();
394        adjust_default_transaction_config(&mut config);
395        assert_eq!(
396            config.max_supported_transaction_version,
397            Some(MAX_SUPPORTED_TRANSACTION_VERSION)
398        );
399    }
400}