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