Skip to main content

light_client/rpc/
rpc_trait.rs

1use std::fmt::Debug;
2
3use async_trait::async_trait;
4use borsh::BorshDeserialize;
5use light_event::event::{BatchPublicTransactionEvent, PublicTransactionEvent};
6use solana_account::Account;
7use solana_clock::Slot;
8use solana_commitment_config::CommitmentConfig;
9use solana_hash::Hash;
10use solana_instruction::Instruction;
11use solana_keypair::Keypair;
12use solana_message::AddressLookupTableAccount;
13use solana_pubkey::Pubkey;
14use solana_rpc_client_api::config::RpcSendTransactionConfig;
15use solana_signature::Signature;
16use solana_transaction::Transaction;
17use solana_transaction_status_client_types::TransactionStatus;
18
19use super::client::RpcUrl;
20use crate::{
21    indexer::{Indexer, IndexerRpcConfig, Response, TreeInfo},
22    interface::{AccountInterface, AccountToFetch, MintInterface, TokenAccountInterface},
23    rpc::errors::RpcError,
24};
25
26#[derive(Debug, Clone)]
27pub struct LightClientConfig {
28    pub url: String,
29    pub commitment_config: Option<CommitmentConfig>,
30    /// Photon indexer URL. To pass an API key, include it in the URL:
31    /// `https://photon.helius.com?api-key=YOUR_KEY`
32    pub photon_url: Option<String>,
33    pub fetch_active_tree: bool,
34}
35
36impl LightClientConfig {
37    pub fn new(url: String, photon_url: Option<String>) -> Self {
38        Self {
39            url,
40            photon_url,
41            commitment_config: Some(CommitmentConfig::confirmed()),
42            fetch_active_tree: true,
43        }
44    }
45    pub fn local_no_indexer() -> Self {
46        Self {
47            url: RpcUrl::Localnet.to_string(),
48            commitment_config: Some(CommitmentConfig::confirmed()),
49            photon_url: None,
50            fetch_active_tree: false,
51        }
52    }
53
54    pub fn local() -> Self {
55        Self {
56            url: RpcUrl::Localnet.to_string(),
57            commitment_config: Some(CommitmentConfig::processed()),
58            photon_url: Some("http://127.0.0.1:8784".to_string()),
59            fetch_active_tree: false,
60        }
61    }
62
63    pub fn devnet(photon_url: Option<String>) -> Self {
64        Self {
65            url: RpcUrl::Devnet.to_string(),
66            photon_url,
67            commitment_config: Some(CommitmentConfig::confirmed()),
68            fetch_active_tree: true,
69        }
70    }
71}
72
73#[async_trait]
74pub trait Rpc: Send + Sync + Debug + 'static {
75    async fn new(config: LightClientConfig) -> Result<Self, RpcError>
76    where
77        Self: Sized;
78
79    fn should_retry(&self, error: &RpcError) -> bool {
80        match error {
81            // Do not retry transaction errors.
82            RpcError::ClientError(error) => error.kind.get_transaction_error().is_none(),
83            // Do not retry signing errors.
84            RpcError::SigningError(_) => false,
85            _ => true,
86        }
87    }
88
89    fn get_payer(&self) -> &Keypair;
90    fn get_url(&self) -> String;
91
92    async fn health(&self) -> Result<(), RpcError>;
93
94    async fn get_program_accounts(
95        &self,
96        program_id: &Pubkey,
97    ) -> Result<Vec<(Pubkey, Account)>, RpcError>;
98
99    async fn get_program_accounts_with_discriminator(
100        &self,
101        program_id: &Pubkey,
102        discriminator: &[u8],
103    ) -> Result<Vec<(Pubkey, Account)>, RpcError>;
104
105    // TODO: add send transaction with config
106
107    async fn confirm_transaction(&self, signature: Signature) -> Result<bool, RpcError>;
108
109    /// Returns an account struct.
110    async fn get_account(&self, address: Pubkey) -> Result<Option<Account>, RpcError>;
111
112    /// Returns multiple account structs.
113    async fn get_multiple_accounts(
114        &self,
115        addresses: &[Pubkey],
116    ) -> Result<Vec<Option<Account>>, RpcError>;
117
118    /// Returns an a borsh deserialized account.
119    /// Deserialization skips the discriminator.
120    async fn get_anchor_account<T: BorshDeserialize>(
121        &self,
122        pubkey: &Pubkey,
123    ) -> Result<Option<T>, RpcError> {
124        match self.get_account(*pubkey).await? {
125            Some(account) => {
126                let data = T::deserialize(&mut &account.data[8..]).map_err(RpcError::from)?;
127                Ok(Some(data))
128            }
129            None => Ok(None),
130        }
131    }
132
133    async fn get_minimum_balance_for_rent_exemption(
134        &self,
135        data_len: usize,
136    ) -> Result<u64, RpcError>;
137
138    async fn airdrop_lamports(&mut self, to: &Pubkey, lamports: u64)
139        -> Result<Signature, RpcError>;
140
141    async fn get_balance(&self, pubkey: &Pubkey) -> Result<u64, RpcError>;
142    async fn get_latest_blockhash(&mut self) -> Result<(Hash, u64), RpcError>;
143    async fn get_slot(&self) -> Result<u64, RpcError>;
144    async fn get_transaction_slot(&self, signature: &Signature) -> Result<u64, RpcError>;
145    async fn get_signature_statuses(
146        &self,
147        signatures: &[Signature],
148    ) -> Result<Vec<Option<TransactionStatus>>, RpcError>;
149
150    async fn send_transaction(&self, transaction: &Transaction) -> Result<Signature, RpcError>;
151
152    async fn send_transaction_with_config(
153        &self,
154        transaction: &Transaction,
155        config: RpcSendTransactionConfig,
156    ) -> Result<Signature, RpcError>;
157
158    async fn process_transaction(
159        &mut self,
160        transaction: Transaction,
161    ) -> Result<Signature, RpcError>;
162
163    async fn process_transaction_with_context(
164        &mut self,
165        transaction: Transaction,
166    ) -> Result<(Signature, Slot), RpcError>;
167
168    async fn create_and_send_transaction_with_event<T>(
169        &mut self,
170        instructions: &[Instruction],
171        authority: &Pubkey,
172        signers: &[&Keypair],
173    ) -> Result<Option<(T, Signature, Slot)>, RpcError>
174    where
175        T: BorshDeserialize + Send + Debug;
176
177    async fn create_and_send_transaction<'a>(
178        &'a mut self,
179        instructions: &'a [Instruction],
180        payer: &'a Pubkey,
181        signers: &'a [&'a Keypair],
182    ) -> Result<Signature, RpcError> {
183        let blockhash = self.get_latest_blockhash().await?.0;
184        let mut transaction = Transaction::new_with_payer(instructions, Some(payer));
185        transaction
186            .try_sign(signers, blockhash)
187            .map_err(|e| RpcError::SigningError(e.to_string()))?;
188        self.process_transaction(transaction).await
189    }
190
191    async fn create_and_send_versioned_transaction<'a>(
192        &'a mut self,
193        instructions: &'a [Instruction],
194        payer: &'a Pubkey,
195        signers: &'a [&'a Keypair],
196        address_lookup_tables: &'a [AddressLookupTableAccount],
197    ) -> Result<Signature, RpcError>;
198
199    async fn create_and_send_transaction_with_public_event(
200        &mut self,
201        instruction: &[Instruction],
202        payer: &Pubkey,
203        signers: &[&Keypair],
204    ) -> Result<Option<(PublicTransactionEvent, Signature, Slot)>, RpcError>;
205
206    async fn create_and_send_transaction_with_batched_event(
207        &mut self,
208        instruction: &[Instruction],
209        payer: &Pubkey,
210        signers: &[&Keypair],
211    ) -> Result<Option<(Vec<BatchPublicTransactionEvent>, Signature, Slot)>, RpcError>;
212
213    fn indexer(&self) -> Result<&impl Indexer, RpcError>;
214    fn indexer_mut(&mut self) -> Result<&mut impl Indexer, RpcError>;
215
216    /// Fetch the latest state tree addresses from the cluster.
217    async fn get_latest_active_state_trees(&mut self) -> Result<Vec<TreeInfo>, RpcError>;
218
219    /// Gets state tree infos.
220    /// State trees are cached and have to be fetched or set.
221    fn get_state_tree_infos(&self) -> Vec<TreeInfo>;
222
223    /// Gets a random state tree info.
224    /// State trees are cached and have to be fetched or set.
225    /// Returns v1 state trees by default, v2 state trees when v2 feature is enabled.
226    fn get_random_state_tree_info(&self) -> Result<TreeInfo, RpcError>;
227
228    /// Gets a random v1 state tree info.
229    /// State trees are cached and have to be fetched or set.
230    fn get_random_state_tree_info_v1(&self) -> Result<TreeInfo, RpcError>;
231
232    fn get_address_tree_v1(&self) -> TreeInfo;
233
234    fn get_address_tree_v2(&self) -> TreeInfo;
235
236    // ============ Interface Methods ============
237    // These race hot (on-chain) and cold (compressed) lookups in the indexer.
238
239    /// Get account data from either on-chain or compressed sources.
240    ///
241    /// Looks up by on-chain Solana pubkey. For cold accounts, searches by
242    /// onchain_pubkey stored in the compressed account data.
243    async fn get_account_interface(
244        &self,
245        address: &Pubkey,
246        config: Option<IndexerRpcConfig>,
247    ) -> Result<Response<Option<AccountInterface>>, RpcError>;
248
249    /// Get token account data from either on-chain or compressed sources.
250    async fn get_token_account_interface(
251        &self,
252        address: &Pubkey,
253        config: Option<IndexerRpcConfig>,
254    ) -> Result<Response<Option<TokenAccountInterface>>, RpcError>;
255
256    /// Get ATA data from either on-chain or compressed sources.
257    async fn get_associated_token_account_interface(
258        &self,
259        owner: &Pubkey,
260        mint: &Pubkey,
261        config: Option<IndexerRpcConfig>,
262    ) -> Result<Response<Option<TokenAccountInterface>>, RpcError>;
263
264    /// Get multiple account interfaces in a batch.
265    async fn get_multiple_account_interfaces(
266        &self,
267        addresses: Vec<&Pubkey>,
268        config: Option<IndexerRpcConfig>,
269    ) -> Result<Response<Vec<Option<AccountInterface>>>, RpcError>;
270
271    /// Get mint interface from either on-chain or compressed sources.
272    ///
273    /// This method:
274    /// 1. First checks if the mint exists on-chain (hot)
275    /// 2. Falls back to compressed account lookup (cold) using derived address
276    /// 3. Parses mint data locally from the account data
277    async fn get_mint_interface(
278        &self,
279        address: &Pubkey,
280        config: Option<IndexerRpcConfig>,
281    ) -> Result<Response<Option<MintInterface>>, RpcError>;
282
283    /// Fetch multiple accounts using `AccountToFetch` descriptors.
284    ///
285    /// Routes each account to the correct method based on its variant:
286    /// - `Pda` -> `get_account_interface`
287    /// - `Token` -> `get_token_account_interface`
288    /// - `Ata` -> `get_associated_token_account_interface`
289    /// - `Mint` -> `get_mint_interface`
290    async fn fetch_accounts(
291        &self,
292        accounts: &[AccountToFetch],
293        config: Option<IndexerRpcConfig>,
294    ) -> Result<Vec<AccountInterface>, RpcError> {
295        let mut results = Vec::with_capacity(accounts.len());
296        for account in accounts {
297            let interface = match account {
298                AccountToFetch::Pda { address, .. } => self
299                    .get_account_interface(address, config.clone())
300                    .await?
301                    .value
302                    .ok_or_else(|| {
303                        RpcError::CustomError(format!("PDA account not found: {}", address))
304                    })?,
305                AccountToFetch::Token { address } => {
306                    let tai = self
307                        .get_token_account_interface(address, config.clone())
308                        .await?
309                        .value
310                        .ok_or_else(|| {
311                            RpcError::CustomError(format!("Token account not found: {}", address))
312                        })?;
313                    tai.into()
314                }
315                AccountToFetch::Ata { wallet_owner, mint } => {
316                    let tai = self
317                        .get_associated_token_account_interface(wallet_owner, mint, config.clone())
318                        .await?
319                        .value
320                        .ok_or_else(|| {
321                            RpcError::CustomError(format!(
322                                "ATA not found for owner {} mint {}",
323                                wallet_owner, mint
324                            ))
325                        })?;
326                    tai.into()
327                }
328                AccountToFetch::Mint { address } => {
329                    let mi = self
330                        .get_mint_interface(address, config.clone())
331                        .await?
332                        .value
333                        .ok_or_else(|| {
334                            RpcError::CustomError(format!("Mint not found: {}", address))
335                        })?;
336                    mi.into()
337                }
338            };
339            results.push(interface);
340        }
341        Ok(results)
342    }
343}