Skip to main content

surfpool_core/surfnet/
remote.rs

1use std::{collections::HashMap, str::FromStr};
2
3use serde_json::json;
4use solana_account::Account;
5use solana_account_decoder::UiAccount;
6use solana_client::{
7    nonblocking::rpc_client::RpcClient,
8    rpc_client::{GetConfirmedSignaturesForAddress2Config, RpcClientConfig},
9    rpc_config::{
10        RpcAccountInfoConfig, RpcBlockConfig, RpcLargestAccountsConfig, RpcProgramAccountsConfig,
11        RpcSignaturesForAddressConfig, RpcTokenAccountsFilter, RpcTransactionConfig,
12    },
13    rpc_filter::RpcFilterType,
14    rpc_request::{RpcRequest, TokenAccountsFilter},
15    rpc_response::{
16        RpcAccountBalance, RpcConfirmedTransactionStatusWithSignature, RpcKeyedAccount, RpcResult,
17        RpcTokenAccountBalance,
18    },
19};
20use solana_clock::Slot;
21use solana_commitment_config::CommitmentConfig;
22use solana_epoch_info::EpochInfo;
23use solana_epoch_schedule::EpochSchedule;
24use solana_hash::Hash;
25use solana_loader_v3_interface::get_program_data_address;
26use solana_pubkey::Pubkey;
27use solana_signature::Signature;
28use solana_transaction_status::UiConfirmedBlock;
29
30use super::GetTransactionResult;
31use crate::{
32    error::{SurfpoolError, SurfpoolResult},
33    rpc::utils::is_method_not_supported_error,
34    surfnet::{GetAccountResult, locker::is_supported_token_program},
35    types::{RemoteRpcResult, TokenAccount},
36};
37
38pub struct SurfnetRemoteClient {
39    pub client: RpcClient,
40}
41impl Clone for SurfnetRemoteClient {
42    fn clone(&self) -> Self {
43        let remote_rpc_url = self.client.url();
44        SurfnetRemoteClient::new_unsafe(remote_rpc_url)
45            .expect("unable to clone SurfnetRemoteClient")
46    }
47}
48
49pub trait SomeRemoteCtx {
50    fn get_remote_ctx<T>(&self, input: T) -> Option<(SurfnetRemoteClient, T)>;
51}
52
53impl SomeRemoteCtx for Option<SurfnetRemoteClient> {
54    fn get_remote_ctx<T>(&self, input: T) -> Option<(SurfnetRemoteClient, T)> {
55        self.as_ref()
56            .map(|remote_rpc_client| (remote_rpc_client.clone(), input))
57    }
58}
59
60impl SurfnetRemoteClient {
61    pub fn new<U: ToString>(remote_rpc_url: U) -> Self {
62        let client = RpcClient::new(remote_rpc_url.to_string());
63        SurfnetRemoteClient { client }
64    }
65
66    pub fn new_unsafe<U: ToString>(remote_rpc_url: U) -> Option<Self> {
67        use reqwest;
68        use solana_rpc_client::http_sender::HttpSender;
69
70        // Retry HTTP client initialization to handle potential fork-related issues
71        let client = match reqwest::Client::builder()
72            .danger_accept_invalid_certs(true)
73            .tls_built_in_root_certs(false)
74            .tls_built_in_webpki_certs(false)
75            .timeout(std::time::Duration::from_secs(30))
76            .build()
77        {
78            Ok(client) => client,
79            Err(e) => {
80                error!(
81                    "unable to initialize datasource client after retries: {}",
82                    e
83                );
84                return None;
85            }
86        };
87        let http_sender = HttpSender::new_with_client(remote_rpc_url, client);
88        let client = RpcClient::new_sender(http_sender, RpcClientConfig::default());
89        Some(SurfnetRemoteClient { client })
90    }
91
92    pub async fn get_epoch_info(&self) -> SurfpoolResult<EpochInfo> {
93        self.client.get_epoch_info().await.map_err(Into::into)
94    }
95
96    pub async fn get_epoch_schedule(&self) -> SurfpoolResult<EpochSchedule> {
97        self.client.get_epoch_schedule().await.map_err(Into::into)
98    }
99
100    pub async fn get_account(
101        &self,
102        pubkey: &Pubkey,
103        commitment_config: CommitmentConfig,
104    ) -> SurfpoolResult<GetAccountResult> {
105        #[cfg(feature = "prometheus")]
106        let fetch_start = std::time::Instant::now();
107
108        let res = self
109            .client
110            .get_account_with_commitment(pubkey, commitment_config)
111            .await
112            .map_err(|e| SurfpoolError::get_account(*pubkey, e))?;
113
114        let result = match res.value {
115            Some(account) => {
116                let mut result = None;
117                if is_supported_token_program(&account.owner) {
118                    if let Ok(token_account) = TokenAccount::unpack(&account.data) {
119                        let mint = self
120                            .client
121                            .get_account_with_commitment(&token_account.mint(), commitment_config)
122                            .await
123                            .map_err(|e| SurfpoolError::get_account(*pubkey, e))?;
124
125                        result = Some(GetAccountResult::FoundTokenAccount(
126                            (*pubkey, account.clone()),
127                            (token_account.mint(), mint.value),
128                        ));
129                    };
130                } else if account.executable {
131                    let program_data_address = get_program_data_address(pubkey);
132
133                    let program_data = self
134                        .client
135                        .get_account_with_commitment(&program_data_address, commitment_config)
136                        .await
137                        .map_err(|e| SurfpoolError::get_account(*pubkey, e))?;
138
139                    result = Some(GetAccountResult::FoundProgramAccount(
140                        (*pubkey, account.clone()),
141                        (program_data_address, program_data.value),
142                    ));
143                }
144
145                result.unwrap_or(GetAccountResult::FoundAccount(
146                    *pubkey, account,
147                    // Mark this account as needing to be updated in the SVM, since we fetched it
148                    true,
149                ))
150            }
151            None => GetAccountResult::None(*pubkey),
152        };
153        #[cfg(feature = "prometheus")]
154        if let Some(m) = crate::telemetry::metrics() {
155            m.record_remote_fetch(fetch_start.elapsed().as_millis() as u64);
156        }
157        Ok(result)
158    }
159
160    pub async fn get_multiple_accounts(
161        &self,
162        pubkeys: &[Pubkey],
163        commitment_config: CommitmentConfig,
164    ) -> SurfpoolResult<Vec<GetAccountResult>> {
165        #[cfg(feature = "prometheus")]
166        let fetch_start = std::time::Instant::now();
167
168        let remote_accounts = self
169            .client
170            .get_multiple_accounts(pubkeys)
171            .await
172            .map_err(SurfpoolError::get_multiple_accounts)?;
173        debug!("Fetched {:?} accounts from remote", pubkeys);
174        debug!(
175            "Found accounts for pubkeys: {:#?}",
176            remote_accounts
177                .iter()
178                .zip(pubkeys)
179                .filter_map(|(account, pubkey)| if account.is_some() {
180                    Some(pubkey)
181                } else {
182                    None
183                })
184                .collect::<Vec<&Pubkey>>()
185        );
186        let mut results_map: HashMap<Pubkey, GetAccountResult> = HashMap::new();
187        let mut mint_accounts_src: Vec<(Pubkey, Account, Pubkey)> = vec![];
188        let mut program_accounts_src: Vec<(Pubkey, Account, Pubkey)> = vec![];
189        for (pubkey, remote_account) in pubkeys.iter().zip(remote_accounts) {
190            if let Some(remote_account) = remote_account {
191                if is_supported_token_program(&remote_account.owner) {
192                    if let Ok(token_account) = TokenAccount::unpack(&remote_account.data) {
193                        mint_accounts_src.push((*pubkey, remote_account, token_account.mint()));
194                    } else {
195                        results_map.insert(
196                            *pubkey,
197                            GetAccountResult::FoundAccount(
198                                *pubkey,
199                                remote_account,
200                                // Mark this account as needing to be updated in the SVM, since we fetched it
201                                true,
202                            ),
203                        );
204                    }
205                } else if remote_account.executable {
206                    let program_data_address = get_program_data_address(pubkey);
207                    program_accounts_src.push((*pubkey, remote_account, program_data_address));
208                } else {
209                    results_map.insert(
210                        *pubkey,
211                        GetAccountResult::FoundAccount(
212                            *pubkey,
213                            remote_account,
214                            // Mark this account as needing to be updated in the SVM, since we fetched it
215                            true,
216                        ),
217                    );
218                }
219            } else {
220                results_map.insert(*pubkey, GetAccountResult::None(*pubkey));
221            }
222        }
223
224        debug!(
225            "Identified {} mint accounts and {} program accounts to fetch for remote accounts",
226            mint_accounts_src.len(),
227            program_accounts_src.len()
228        );
229
230        if !(mint_accounts_src.is_empty() && program_accounts_src.is_empty()) {
231            let mint_acc_src_len = mint_accounts_src.len();
232            let mut account_buffer = mint_accounts_src.clone();
233            account_buffer.extend_from_slice(&program_accounts_src);
234
235            let account_pubkeys: Vec<Pubkey> = account_buffer.iter().map(|p| p.2).collect();
236
237            let binding_remote_accounts = self
238                .client
239                .get_multiple_accounts_with_commitment(&account_pubkeys, commitment_config)
240                .await
241                .map_err(SurfpoolError::get_multiple_accounts)?
242                .value;
243
244            debug!(
245                "Fetched {} additional accounts from remote",
246                binding_remote_accounts.len()
247            );
248            debug!(
249                "Found additional accounts for pubkeys: {:#?}",
250                binding_remote_accounts
251                    .iter()
252                    .zip(account_pubkeys)
253                    .filter_map(|(account, pubkey)| if account.is_some() {
254                        Some(pubkey)
255                    } else {
256                        None
257                    })
258                    .collect::<Vec<Pubkey>>()
259            );
260
261            for (index, remote_account) in binding_remote_accounts.iter().enumerate() {
262                if index < mint_acc_src_len {
263                    // mint accounts to be inserted
264                    results_map.insert(
265                        account_buffer[index].0,
266                        GetAccountResult::FoundTokenAccount(
267                            (account_buffer[index].0, account_buffer[index].1.clone()),
268                            (account_buffer[index].2, remote_account.clone()),
269                        ),
270                    );
271                } else {
272                    results_map.insert(
273                        account_buffer[index].0,
274                        GetAccountResult::FoundProgramAccount(
275                            (account_buffer[index].0, account_buffer[index].1.clone()),
276                            (account_buffer[index].2, remote_account.clone()),
277                        ),
278                    );
279                }
280            }
281        }
282        #[cfg(feature = "prometheus")]
283        if let Some(m) = crate::telemetry::metrics() {
284            m.record_remote_fetch(fetch_start.elapsed().as_millis() as u64);
285        }
286        Ok(pubkeys
287            .iter()
288            .map(|pk| {
289                results_map
290                    .remove(pk)
291                    .unwrap_or(GetAccountResult::None(*pk))
292            })
293            .collect())
294    }
295
296    pub async fn get_transaction(
297        &self,
298        signature: Signature,
299        config: RpcTransactionConfig,
300        latest_absolute_slot: u64,
301    ) -> GetTransactionResult {
302        match self
303            .client
304            .get_transaction_with_config(&signature, config)
305            .await
306        {
307            Ok(tx) => GetTransactionResult::found_transaction(signature, tx, latest_absolute_slot),
308            Err(_) => GetTransactionResult::None(signature),
309        }
310    }
311
312    pub async fn get_token_accounts_by_owner(
313        &self,
314        owner: Pubkey,
315        filter: &TokenAccountsFilter,
316        config: &RpcAccountInfoConfig,
317    ) -> SurfpoolResult<Vec<RpcKeyedAccount>> {
318        let token_account_filter = match filter {
319            TokenAccountsFilter::Mint(mint) => RpcTokenAccountsFilter::Mint(mint.to_string()),
320            TokenAccountsFilter::ProgramId(program_id) => {
321                RpcTokenAccountsFilter::ProgramId(program_id.to_string())
322            }
323        };
324
325        // the RPC client's default implementation of get_token_accounts_by_owner doesn't allow providing the config,
326        // so we need to use the send method directly
327        let res: RpcResult<Vec<RpcKeyedAccount>> = self
328            .client
329            .send(
330                RpcRequest::GetTokenAccountsByOwner,
331                json!([owner.to_string(), token_account_filter, config]),
332            )
333            .await;
334        res.map_err(|e| SurfpoolError::get_token_accounts(owner, filter, e))
335            .map(|res| res.value)
336    }
337
338    pub async fn get_token_largest_accounts(
339        &self,
340        mint: &Pubkey,
341        commitment_config: CommitmentConfig,
342    ) -> SurfpoolResult<Vec<RpcTokenAccountBalance>> {
343        self.client
344            .get_token_largest_accounts_with_commitment(mint, commitment_config)
345            .await
346            .map(|response| response.value)
347            .map_err(|e| SurfpoolError::get_token_largest_accounts(*mint, e))
348    }
349
350    pub async fn get_token_accounts_by_delegate(
351        &self,
352        delegate: Pubkey,
353        filter: &TokenAccountsFilter,
354        config: &RpcAccountInfoConfig,
355    ) -> SurfpoolResult<Vec<RpcKeyedAccount>> {
356        // validate that the program is supported if using ProgramId filter
357        if let TokenAccountsFilter::ProgramId(program_id) = &filter {
358            if !is_supported_token_program(program_id) {
359                return Err(SurfpoolError::unsupported_token_program(*program_id));
360            }
361        }
362
363        let token_account_filter = match &filter {
364            TokenAccountsFilter::Mint(mint) => RpcTokenAccountsFilter::Mint(mint.to_string()),
365            TokenAccountsFilter::ProgramId(program_id) => {
366                RpcTokenAccountsFilter::ProgramId(program_id.to_string())
367            }
368        };
369
370        let res: RpcResult<Vec<RpcKeyedAccount>> = self
371            .client
372            .send(
373                RpcRequest::GetTokenAccountsByDelegate,
374                json!([delegate.to_string(), token_account_filter, config]),
375            )
376            .await;
377
378        res.map_err(|e| SurfpoolError::get_token_accounts_by_delegate_error(delegate, filter, e))
379            .map(|res| res.value)
380    }
381
382    pub async fn get_program_accounts(
383        &self,
384        program_id: &Pubkey,
385        account_config: RpcAccountInfoConfig,
386        filters: Option<Vec<RpcFilterType>>,
387    ) -> SurfpoolResult<RemoteRpcResult<Vec<(Pubkey, UiAccount)>>> {
388        handle_remote_rpc(|| async {
389            self.client
390                .get_program_ui_accounts_with_config(
391                    program_id,
392                    RpcProgramAccountsConfig {
393                        filters,
394                        with_context: Some(false),
395                        account_config,
396                        ..Default::default()
397                    },
398                )
399                .await
400                .map_err(|e| SurfpoolError::get_program_accounts(*program_id, e))
401        })
402        .await
403    }
404
405    pub async fn get_largest_accounts(
406        &self,
407        config: Option<RpcLargestAccountsConfig>,
408    ) -> SurfpoolResult<RemoteRpcResult<Vec<RpcAccountBalance>>> {
409        handle_remote_rpc(|| async {
410            self.client
411                .get_largest_accounts_with_config(config.unwrap_or_default())
412                .await
413                .map(|res| res.value)
414                .map_err(SurfpoolError::get_largest_accounts)
415        })
416        .await
417    }
418
419    pub async fn get_genesis_hash(&self) -> SurfpoolResult<Hash> {
420        self.client.get_genesis_hash().await.map_err(Into::into)
421    }
422
423    pub async fn get_signatures_for_address(
424        &self,
425        pubkey: &Pubkey,
426        config: Option<&RpcSignaturesForAddressConfig>,
427    ) -> SurfpoolResult<Vec<RpcConfirmedTransactionStatusWithSignature>> {
428        let c = match config {
429            Some(c) => GetConfirmedSignaturesForAddress2Config {
430                before: c
431                    .before
432                    .as_deref()
433                    .and_then(|s| Signature::from_str(&s).ok()),
434                commitment: c.commitment,
435                limit: c.limit,
436                until: c
437                    .until
438                    .as_deref()
439                    .and_then(|s| Signature::from_str(&s).ok()),
440            },
441            _ => GetConfirmedSignaturesForAddress2Config::default(),
442        };
443        self.client
444            .get_signatures_for_address_with_config(pubkey, c)
445            .await
446            .map_err(SurfpoolError::get_signatures_for_address)
447    }
448
449    pub async fn get_block(
450        &self,
451        slot: &Slot,
452        config: RpcBlockConfig,
453    ) -> SurfpoolResult<UiConfirmedBlock> {
454        self.client
455            .get_block_with_config(*slot, config)
456            .await
457            .map_err(|e| SurfpoolError::get_block(e, *slot))
458    }
459}
460
461/// Handles remote RPC calls, returning a `RemoteRpcResult` indicating whether the method was supported.
462/// If the method is not supported, it returns `RemoteRpcResult::MethodNotSupported`.
463/// If the method is supported, it returns `RemoteRpcResult::Ok(T)`.
464/// If the method is supported but returns an error, it returns `Err(E)`.
465pub async fn handle_remote_rpc<T, E, F, Fut>(fut: F) -> Result<RemoteRpcResult<T>, E>
466where
467    F: FnOnce() -> Fut,
468    Fut: std::future::Future<Output = Result<T, E>>,
469    E: std::fmt::Display,
470{
471    match fut().await {
472        Ok(val) => Ok(RemoteRpcResult::Ok(val)),
473        Err(e) if is_method_not_supported_error(&e) => Ok(RemoteRpcResult::MethodNotSupported),
474        Err(e) => Err(e),
475    }
476}