openzeppelin_monitor/services/blockchain/transports/solana/
http.rs1use reqwest_middleware::ClientWithMiddleware;
8use serde::{Deserialize, Serialize};
9use serde_json::{json, Value};
10
11use crate::{
12 models::Network,
13 services::blockchain::transports::{
14 BlockchainTransport, HttpTransportClient, RotatingTransport, TransportError,
15 },
16};
17
18pub mod rpc_methods {
20 pub const GET_SLOT: &str = "getSlot";
22 pub const GET_BLOCK: &str = "getBlock";
24 pub const GET_BLOCKS: &str = "getBlocks";
26 pub const GET_TRANSACTION: &str = "getTransaction";
28 pub const GET_HEALTH: &str = "getHealth";
30 pub const GET_VERSION: &str = "getVersion";
32 pub const GET_ACCOUNT_INFO: &str = "getAccountInfo";
34 #[allow(dead_code)]
36 pub const GET_MULTIPLE_ACCOUNTS: &str = "getMultipleAccounts";
37 pub const GET_PROGRAM_ACCOUNTS: &str = "getProgramAccounts";
39 pub const GET_BLOCK_HEIGHT: &str = "getBlockHeight";
41 pub const GET_BLOCK_TIME: &str = "getBlockTime";
43 pub const GET_FIRST_AVAILABLE_BLOCK: &str = "getFirstAvailableBlock";
45 #[allow(dead_code)]
47 pub const GET_SLOT_LEADER: &str = "getSlotLeader";
48 pub const MINIMUM_LEDGER_SLOT: &str = "minimumLedgerSlot";
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, Default)]
54#[serde(rename_all = "camelCase")]
55pub struct GetBlockConfig {
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub encoding: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub transaction_details: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub rewards: Option<bool>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub commitment: Option<String>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub max_supported_transaction_version: Option<u8>,
71}
72
73impl GetBlockConfig {
74 pub fn full() -> Self {
76 Self {
77 encoding: Some("json".to_string()),
78 transaction_details: Some("full".to_string()),
79 rewards: Some(true),
80 commitment: Some("finalized".to_string()),
81 max_supported_transaction_version: Some(0),
82 }
83 }
84
85 pub fn signatures_only() -> Self {
87 Self {
88 encoding: Some("json".to_string()),
89 transaction_details: Some("signatures".to_string()),
90 rewards: Some(false),
91 commitment: Some("finalized".to_string()),
92 max_supported_transaction_version: Some(0),
93 }
94 }
95
96 pub fn none() -> Self {
98 Self {
99 encoding: Some("json".to_string()),
100 transaction_details: Some("none".to_string()),
101 rewards: Some(false),
102 commitment: Some("finalized".to_string()),
103 max_supported_transaction_version: Some(0),
104 }
105 }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, Default)]
110#[serde(rename_all = "camelCase")]
111pub struct GetTransactionConfig {
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub encoding: Option<String>,
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub commitment: Option<String>,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub max_supported_transaction_version: Option<u8>,
121}
122
123impl GetTransactionConfig {
124 pub fn json() -> Self {
126 Self {
127 encoding: Some("json".to_string()),
128 commitment: Some("finalized".to_string()),
129 max_supported_transaction_version: Some(0),
130 }
131 }
132
133 pub fn json_parsed() -> Self {
135 Self {
136 encoding: Some("jsonParsed".to_string()),
137 commitment: Some("finalized".to_string()),
138 max_supported_transaction_version: Some(0),
139 }
140 }
141}
142
143#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
145#[serde(rename_all = "lowercase")]
146pub enum Commitment {
147 #[default]
149 Finalized,
150 Confirmed,
152 Processed,
154}
155
156impl Commitment {
157 pub fn as_str(&self) -> &'static str {
159 match self {
160 Commitment::Finalized => "finalized",
161 Commitment::Confirmed => "confirmed",
162 Commitment::Processed => "processed",
163 }
164 }
165}
166
167#[derive(Clone, Debug)]
174pub struct SolanaTransportClient {
175 http_client: HttpTransportClient,
177}
178
179impl SolanaTransportClient {
180 pub async fn new(network: &Network) -> Result<Self, anyhow::Error> {
188 let test_connection_payload =
190 Some(r#"{"id":1,"jsonrpc":"2.0","method":"getHealth"}"#.to_string());
191 let http_client = HttpTransportClient::new(network, test_connection_payload).await?;
192 Ok(Self { http_client })
193 }
194
195 pub async fn with_test_payload(
204 network: &Network,
205 test_payload: Option<String>,
206 ) -> Result<Self, anyhow::Error> {
207 let http_client = HttpTransportClient::new(network, test_payload).await?;
208 Ok(Self { http_client })
209 }
210
211 pub fn http_client(&self) -> &HttpTransportClient {
213 &self.http_client
214 }
215
216 pub async fn get_slot(&self, commitment: Option<Commitment>) -> Result<Value, TransportError> {
226 let params = commitment.map(|c| json!([{ "commitment": c.as_str() }]));
227 self.http_client
228 .send_raw_request(rpc_methods::GET_SLOT, params)
229 .await
230 }
231
232 pub async fn get_block(
241 &self,
242 slot: u64,
243 config: Option<GetBlockConfig>,
244 ) -> Result<Value, TransportError> {
245 let config = config.unwrap_or_else(GetBlockConfig::full);
246 let params = json!([slot, config]);
247 self.http_client
248 .send_raw_request(rpc_methods::GET_BLOCK, Some(params))
249 .await
250 }
251
252 pub async fn get_blocks(
262 &self,
263 start_slot: u64,
264 end_slot: Option<u64>,
265 commitment: Option<Commitment>,
266 ) -> Result<Value, TransportError> {
267 let params = match (end_slot, commitment) {
268 (Some(end), Some(c)) => json!([start_slot, end, { "commitment": c.as_str() }]),
269 (Some(end), None) => json!([start_slot, end]),
270 (None, Some(c)) => json!([start_slot, null, { "commitment": c.as_str() }]),
271 (None, None) => json!([start_slot]),
272 };
273 self.http_client
274 .send_raw_request(rpc_methods::GET_BLOCKS, Some(params))
275 .await
276 }
277
278 pub async fn get_transaction(
287 &self,
288 signature: &str,
289 config: Option<GetTransactionConfig>,
290 ) -> Result<Value, TransportError> {
291 let config = config.unwrap_or_else(GetTransactionConfig::json);
292 let params = json!([signature, config]);
293 self.http_client
294 .send_raw_request(rpc_methods::GET_TRANSACTION, Some(params))
295 .await
296 }
297
298 pub async fn get_account_info(
307 &self,
308 pubkey: &str,
309 commitment: Option<Commitment>,
310 ) -> Result<Value, TransportError> {
311 let config = json!({
312 "encoding": "jsonParsed",
313 "commitment": commitment.unwrap_or_default().as_str()
314 });
315 let params = json!([pubkey, config]);
316 self.http_client
317 .send_raw_request(rpc_methods::GET_ACCOUNT_INFO, Some(params))
318 .await
319 }
320
321 pub async fn get_program_accounts(
331 &self,
332 program_id: &str,
333 filters: Option<Vec<Value>>,
334 commitment: Option<Commitment>,
335 ) -> Result<Value, TransportError> {
336 let mut config = json!({
337 "encoding": "jsonParsed",
338 "commitment": commitment.unwrap_or_default().as_str()
339 });
340
341 if let Some(f) = filters {
342 config["filters"] = json!(f);
343 }
344
345 let params = json!([program_id, config]);
346 self.http_client
347 .send_raw_request(rpc_methods::GET_PROGRAM_ACCOUNTS, Some(params))
348 .await
349 }
350
351 pub async fn get_block_height(
359 &self,
360 commitment: Option<Commitment>,
361 ) -> Result<Value, TransportError> {
362 let params = commitment.map(|c| json!([{ "commitment": c.as_str() }]));
363 self.http_client
364 .send_raw_request(rpc_methods::GET_BLOCK_HEIGHT, params)
365 .await
366 }
367
368 pub async fn get_block_time(&self, slot: u64) -> Result<Value, TransportError> {
376 let params = json!([slot]);
377 self.http_client
378 .send_raw_request(rpc_methods::GET_BLOCK_TIME, Some(params))
379 .await
380 }
381
382 pub async fn get_first_available_block(&self) -> Result<Value, TransportError> {
387 self.http_client
388 .send_raw_request::<Value>(rpc_methods::GET_FIRST_AVAILABLE_BLOCK, None)
389 .await
390 }
391
392 pub async fn minimum_ledger_slot(&self) -> Result<Value, TransportError> {
397 self.http_client
398 .send_raw_request::<Value>(rpc_methods::MINIMUM_LEDGER_SLOT, None)
399 .await
400 }
401
402 pub async fn get_version(&self) -> Result<Value, TransportError> {
407 self.http_client
408 .send_raw_request::<Value>(rpc_methods::GET_VERSION, None)
409 .await
410 }
411
412 pub async fn get_health(&self) -> Result<Value, TransportError> {
417 self.http_client
418 .send_raw_request::<Value>(rpc_methods::GET_HEALTH, None)
419 .await
420 }
421}
422
423#[async_trait::async_trait]
424impl BlockchainTransport for SolanaTransportClient {
425 async fn get_current_url(&self) -> String {
430 self.http_client.get_current_url().await
431 }
432
433 async fn send_raw_request<P>(
442 &self,
443 method: &str,
444 params: Option<P>,
445 ) -> Result<Value, TransportError>
446 where
447 P: Into<Value> + Send + Clone + Serialize,
448 {
449 self.http_client.send_raw_request(method, params).await
450 }
451
452 fn update_endpoint_manager_client(
457 &mut self,
458 client: ClientWithMiddleware,
459 ) -> Result<(), anyhow::Error> {
460 self.http_client.update_endpoint_manager_client(client)
461 }
462}
463
464#[async_trait::async_trait]
465impl RotatingTransport for SolanaTransportClient {
466 async fn try_connect(&self, url: &str) -> Result<(), anyhow::Error> {
474 self.http_client.try_connect(url).await
475 }
476
477 async fn update_client(&self, url: &str) -> Result<(), anyhow::Error> {
485 self.http_client.update_client(url).await
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492
493 #[test]
494 fn test_solana_transport_implements_traits() {
495 fn assert_blockchain_transport<T: BlockchainTransport>() {}
497 fn assert_rotating_transport<T: RotatingTransport>() {}
498 fn assert_send_sync<T: Send + Sync>() {}
499 fn assert_clone<T: Clone>() {}
500
501 assert_blockchain_transport::<SolanaTransportClient>();
502 assert_rotating_transport::<SolanaTransportClient>();
503 assert_send_sync::<SolanaTransportClient>();
504 assert_clone::<SolanaTransportClient>();
505 }
506}