Skip to main content

surfpool_core/rpc/
utils.rs

1#![allow(dead_code)]
2
3use std::any::type_name;
4
5use base64::prelude::*;
6use bincode::Options;
7use jsonrpc_core::{Error, Result};
8use litesvm::types::TransactionMetadata;
9use solana_client::{
10    rpc_config::{RpcTokenAccountsFilter, RpcTransactionConfig},
11    rpc_filter::RpcFilterType,
12    rpc_request::{MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT, TokenAccountsFilter},
13};
14use solana_commitment_config::CommitmentConfig;
15use solana_hash::Hash;
16use solana_message::{AccountKeys, VersionedMessage};
17use solana_packet::PACKET_DATA_SIZE;
18use solana_pubkey::{ParsePubkeyError, Pubkey};
19use solana_signature::Signature;
20use solana_transaction_status::{
21    InnerInstruction, InnerInstructions, TransactionBinaryEncoding, UiInnerInstructions,
22    UiTransactionEncoding, parse_ui_inner_instructions,
23};
24
25use crate::error::{SurfpoolError, SurfpoolResult};
26
27pub fn convert_transaction_metadata_from_canonical(
28    transaction_metadata: &TransactionMetadata,
29) -> surfpool_types::TransactionMetadata {
30    surfpool_types::TransactionMetadata {
31        signature: transaction_metadata.signature,
32        logs: transaction_metadata.logs.clone(),
33        inner_instructions: transaction_metadata.inner_instructions.clone(),
34        compute_units_consumed: transaction_metadata.compute_units_consumed,
35        return_data: transaction_metadata.return_data.clone(),
36        fee: transaction_metadata.fee,
37    }
38}
39
40fn optimize_filters(filters: &mut [RpcFilterType]) {
41    filters.iter_mut().for_each(|filter_type| {
42        if let RpcFilterType::Memcmp(compare) = filter_type {
43            if let Err(err) = compare.convert_to_raw_bytes() {
44                // All filters should have been previously verified
45                warn!("Invalid filter: bytes could not be decoded, {err}");
46            }
47        }
48    })
49}
50
51fn verify_filter(input: &RpcFilterType) -> Result<()> {
52    input
53        .verify()
54        .map_err(|e| Error::invalid_params(format!("Invalid param: {e:?}")))
55}
56
57pub fn verify_pubkey(input: &str) -> SurfpoolResult<Pubkey> {
58    input
59        .parse()
60        .map_err(|e: ParsePubkeyError| SurfpoolError::invalid_pubkey(input, e.to_string()))
61}
62
63pub fn verify_pubkeys(input: &[String]) -> SurfpoolResult<Vec<Pubkey>> {
64    input
65        .iter()
66        .enumerate()
67        .map(|(i, s)| {
68            verify_pubkey(s)
69                .map_err(|e| SurfpoolError::invalid_pubkey_at_index(s, i, e.to_string()))
70        })
71        .collect::<SurfpoolResult<Vec<_>>>()
72}
73
74fn verify_hash(input: &str) -> Result<Hash> {
75    input
76        .parse()
77        .map_err(|e| Error::invalid_params(format!("Invalid param: {e:?}")))
78}
79
80fn verify_signature(input: &str) -> Result<Signature> {
81    input
82        .parse()
83        .map_err(|e| Error::invalid_params(format!("Invalid param: {e:?}")))
84}
85
86fn verify_token_account_filter(
87    token_account_filter: RpcTokenAccountsFilter,
88) -> Result<TokenAccountsFilter> {
89    match token_account_filter {
90        RpcTokenAccountsFilter::Mint(mint_str) => {
91            let mint = verify_pubkey(&mint_str)?;
92            Ok(TokenAccountsFilter::Mint(mint))
93        }
94        RpcTokenAccountsFilter::ProgramId(program_id_str) => {
95            let program_id = verify_pubkey(&program_id_str)?;
96            Ok(TokenAccountsFilter::ProgramId(program_id))
97        }
98    }
99}
100
101fn verify_and_parse_signatures_for_address_params(
102    address: String,
103    before: Option<String>,
104    until: Option<String>,
105    limit: Option<usize>,
106) -> Result<(Pubkey, Option<Signature>, Option<Signature>, usize)> {
107    let address = verify_pubkey(&address)?;
108    let before = before
109        .map(|ref before| verify_signature(before))
110        .transpose()?;
111    let until = until.map(|ref until| verify_signature(until)).transpose()?;
112    let limit = limit.unwrap_or(MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT);
113
114    if limit == 0 || limit > MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT {
115        return Err(Error::invalid_params(format!(
116            "Invalid limit; max {MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT}"
117        )));
118    }
119    Ok((address, before, until, limit))
120}
121
122const MAX_BASE58_SIZE: usize = 1683; // Golden, bump if PACKET_DATA_SIZE changes
123const MAX_BASE64_SIZE: usize = 1644; // Golden, bump if PACKET_DATA_SIZE changes
124pub fn decode_and_deserialize<T>(
125    encoded: String,
126    encoding: TransactionBinaryEncoding,
127) -> Result<(Vec<u8>, T)>
128where
129    T: serde::de::DeserializeOwned,
130{
131    let wire_output = match encoding {
132        TransactionBinaryEncoding::Base58 => {
133            if encoded.len() > MAX_BASE58_SIZE {
134                return Err(Error::invalid_params(format!(
135                    "base58 encoded {} too large: {} bytes (max: encoded/raw {}/{})",
136                    type_name::<T>(),
137                    encoded.len(),
138                    MAX_BASE58_SIZE,
139                    PACKET_DATA_SIZE,
140                )));
141            }
142            bs58::decode(encoded)
143                .into_vec()
144                .map_err(|e| Error::invalid_params(format!("invalid base58 encoding: {e:?}")))?
145        }
146        TransactionBinaryEncoding::Base64 => {
147            if encoded.len() > MAX_BASE64_SIZE {
148                return Err(Error::invalid_params(format!(
149                    "base64 encoded {} too large: {} bytes (max: encoded/raw {}/{})",
150                    type_name::<T>(),
151                    encoded.len(),
152                    MAX_BASE64_SIZE,
153                    PACKET_DATA_SIZE,
154                )));
155            }
156            BASE64_STANDARD
157                .decode(encoded)
158                .map_err(|e| Error::invalid_params(format!("invalid base64 encoding: {e:?}")))?
159        }
160    };
161    if wire_output.len() > PACKET_DATA_SIZE {
162        return Err(Error::invalid_params(format!(
163            "decoded {} too large: {} bytes (max: {} bytes)",
164            type_name::<T>(),
165            wire_output.len(),
166            PACKET_DATA_SIZE
167        )));
168    }
169    bincode::options()
170        .with_limit(PACKET_DATA_SIZE as u64)
171        .with_fixint_encoding()
172        .allow_trailing_bytes()
173        .deserialize_from(&wire_output[..])
174        .map_err(|err| {
175            Error::invalid_params(format!(
176                "failed to deserialize {}: {}",
177                type_name::<T>(),
178                &err.to_string()
179            ))
180        })
181        .map(|output| (wire_output, output))
182}
183
184/// Decode the RPC `data` parameter of `sendTransaction` and
185/// `simulateTransaction` into a `solana_transaction::VersionedTransaction`.
186///
187/// Both methods accept a `UiTransactionEncoding` on their config that defaults
188/// to `Base58`, map it to the internal `TransactionBinaryEncoding`, and feed
189/// the result into `decode_and_deserialize`. The mapping can fail (the RPC
190/// only accepts base58 and base64), which is reported back as an
191/// `Error::invalid_params` listing the supported encodings.
192pub fn decode_rpc_versioned_transaction(
193    data: String,
194    encoding: Option<UiTransactionEncoding>,
195) -> Result<solana_transaction::versioned::VersionedTransaction> {
196    let tx_encoding = encoding.unwrap_or(UiTransactionEncoding::Base58);
197    let binary_encoding = tx_encoding.into_binary_encoding().ok_or_else(|| {
198        Error::invalid_params(format!(
199            "unsupported encoding: {tx_encoding}. Supported encodings: base58, base64"
200        ))
201    })?;
202    let (_, unsanitized_tx) = decode_and_deserialize::<
203        solana_transaction::versioned::VersionedTransaction,
204    >(data, binary_encoding)?;
205    Ok(unsanitized_tx)
206}
207
208pub fn transform_tx_metadata_to_ui_accounts(
209    meta: TransactionMetadata,
210    message: &VersionedMessage,
211    loaded_addresses: Option<&solana_message::v0::LoadedAddresses>,
212) -> Vec<UiInnerInstructions> {
213    // Create AccountKeys from the transaction message with loaded addresses from ALTs
214    let account_keys = AccountKeys::new(message.static_account_keys(), loaded_addresses);
215
216    meta.inner_instructions
217        .into_iter()
218        .enumerate()
219        .filter_map(|(i, ixs)| {
220            let instructions: Vec<InnerInstruction> = ixs
221                .iter()
222                .map(|ix| InnerInstruction {
223                    instruction: ix.instruction.clone(),
224                    stack_height: Some(ix.stack_height as u32),
225                })
226                .collect();
227            if instructions.is_empty() {
228                None
229            } else {
230                // Create InnerInstructions and then parse it into UiInnerInstructions
231                // This will properly convert CompiledInstruction to UiInstruction format
232                let inner_instructions = InnerInstructions {
233                    index: i as u8,
234                    instructions,
235                };
236                Some(parse_ui_inner_instructions(
237                    inner_instructions,
238                    &account_keys,
239                ))
240            }
241        })
242        .collect()
243}
244
245/// Substrings that, when present in a lowercased error message, indicate the
246/// remote RPC method is not supported by the upstream (often a public endpoint
247/// that has gated methods behind a 410 Gone response or a custom refusal).
248const METHOD_NOT_SUPPORTED_NEEDLES: &[&str] = &[
249    "not supported",
250    "unsupported",
251    "unavailable",
252    "method blocked",
253    "invalid request",
254    "is blocked",
255    "if you need this method",
256    "client error 410",
257    "410 gone",
258    "(410 gone)",
259    " status 410",
260    "http 410",
261    "client error (410",
262];
263
264/// Returns true if the error indicates the remote method is not supported.
265pub fn is_method_not_supported_error<E: std::fmt::Display>(err: &E) -> bool {
266    let msg = err.to_string().to_lowercase();
267    METHOD_NOT_SUPPORTED_NEEDLES
268        .iter()
269        .any(|needle| msg.contains(needle))
270}
271
272pub fn get_default_transaction_config() -> RpcTransactionConfig {
273    RpcTransactionConfig {
274        encoding: Some(UiTransactionEncoding::Json),
275        commitment: Some(CommitmentConfig::default()),
276        max_supported_transaction_version: Some(0),
277    }
278}
279
280pub fn adjust_default_transaction_config(config: &mut RpcTransactionConfig) {
281    if config.encoding.is_none() {
282        config.encoding = Some(UiTransactionEncoding::Json);
283    }
284    if config.max_supported_transaction_version.is_none() {
285        config.max_supported_transaction_version = Some(0);
286    }
287    if config.commitment.is_none() {
288        config.commitment = Some(CommitmentConfig::default());
289    }
290}