1use anyhow::{anyhow, Result};
2use rand::seq::SliceRandom;
3use reqwest::Client;
4use serde_json::{json, Value};
5use std::fmt;
6use tracing::{debug, trace};
7
8pub struct JitoJsonRpcSDK {
9 base_url: String,
10 uuid: Option<String>,
11 client: Client,
12}
13
14#[derive(Debug)]
15pub struct PrettyJsonValue(pub Value);
16
17impl fmt::Display for PrettyJsonValue {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 write!(f, "{}", serde_json::to_string_pretty(&self.0).unwrap())
20 }
21}
22
23impl From<Value> for PrettyJsonValue {
24 fn from(value: Value) -> Self {
25 PrettyJsonValue(value)
26 }
27}
28
29impl JitoJsonRpcSDK {
30 pub fn new(base_url: &str, uuid: Option<String>) -> Self {
31 Self {
32 base_url: base_url.to_string(),
33 uuid,
34 client: Client::new(),
35 }
36 }
37
38 async fn send_request(
39 &self,
40 endpoint: &str,
41 method: &str,
42 params: Option<Value>,
43 ) -> Result<Value, reqwest::Error> {
44 let url = format!("{}{}", self.base_url, endpoint);
45
46 let data = json!({
47 "jsonrpc": "2.0",
48 "id": 1,
49 "method": method,
50 "params": params.unwrap_or(json!([]))
51 });
52
53 trace!("Sending request to: {}", url);
54 trace!(
55 "Request body: {}",
56 serde_json::to_string_pretty(&data).unwrap()
57 );
58
59 let response = self
60 .client
61 .post(&url)
62 .header("Content-Type", "application/json")
63 .json(&data)
64 .send()
65 .await?;
66
67 let status = response.status();
68 debug!("Response status: {}", status);
69
70 let body = response.json::<Value>().await?;
71 trace!(
72 "Response body: {}",
73 serde_json::to_string_pretty(&body).unwrap()
74 );
75
76 Ok(body)
77 }
78
79 pub async fn get_tip_accounts(&self) -> Result<Value, reqwest::Error> {
80 let endpoint = if let Some(uuid) = &self.uuid {
81 format!("/bundles?uuid={}", uuid)
82 } else {
83 "/bundles".to_string()
84 };
85
86 self.send_request(&endpoint, "getTipAccounts", None).await
87 }
88
89 pub async fn get_random_tip_account(&self) -> Result<String> {
91 let tip_accounts_response = self.get_tip_accounts().await?;
92
93 let tip_accounts = tip_accounts_response["result"]
94 .as_array()
95 .ok_or_else(|| anyhow!("Failed to parse tip accounts as array"))?;
96
97 if tip_accounts.is_empty() {
98 return Err(anyhow!("No tip accounts available"));
99 }
100
101 let random_account = tip_accounts
102 .choose(&mut rand::thread_rng())
103 .ok_or_else(|| anyhow!("Failed to choose random tip account"))?;
104
105 random_account
106 .as_str()
107 .ok_or_else(|| anyhow!("Failed to parse tip account as string"))
108 .map(String::from)
109 }
110
111 pub async fn get_bundle_statuses(&self, bundle_uuids: Vec<String>) -> Result<Value> {
112 let endpoint = if let Some(uuid) = &self.uuid {
113 format!("/getBundleStatuses?uuid={}", uuid)
114 } else {
115 "/getBundleStatuses".to_string()
116 };
117
118 let params = json!([bundle_uuids]);
120
121 self.send_request(&endpoint, "getBundleStatuses", Some(params))
122 .await
123 .map_err(|e| anyhow!("Request error: {}", e))
124 }
125
126 pub async fn send_bundle(&self, params: Option<Value>, uuid: Option<&str>) -> Result<Value, anyhow::Error> {
127 let mut endpoint = "/bundles".to_string();
128
129 if let Some(uuid) = uuid {
130 endpoint = format!("{}?uuid={}", endpoint, uuid);
131 }
132
133 let request_params = match params {
135 Some(ref value) if value.is_array() && value.as_array().unwrap().len() == 2 => {
137 value.clone()
139 },
140 Some(Value::Array(transactions)) => {
141 if transactions.is_empty() {
143 return Err(anyhow!("Bundle must contain at least one transaction"));
144 }
145 if transactions.len() > 5 {
146 return Err(anyhow!("Bundle can contain at most 5 transactions"));
147 }
148
149 json!([
150 transactions,
151 {
152 "encoding": "base64"
153 }
154 ])
155 },
156 _ => return Err(anyhow!("Invalid bundle format: expected an array of transactions")),
157 };
158
159 self.send_request(&endpoint, "sendBundle", Some(request_params))
160 .await
161 .map_err(|e| anyhow!("Request error: {}", e))
162 }
163
164 pub async fn send_txn(&self, params: Option<Value>, bundle_only: bool) -> Result<Value, reqwest::Error> {
165 let mut query_params = Vec::new();
166
167 if bundle_only {
168 query_params.push("bundleOnly=true".to_string());
169 }
170
171 let endpoint = if query_params.is_empty() {
172 "/transactions".to_string()
173 } else {
174 format!("/transactions?{}", query_params.join("&"))
175 };
176
177 let params = match params {
178 Some(Value::Object(map)) => {
179 let tx = map.get("tx").and_then(Value::as_str).unwrap_or_default();
180 let skip_preflight = map.get("skipPreflight").and_then(Value::as_bool).unwrap_or(false);
181 json!([
182 tx,
183 {
184 "encoding": "base64",
185 "skipPreflight": skip_preflight
186 }
187 ])
188 },
189 _ => json!([]),
190 };
191
192 self.send_request(&endpoint, "sendTransaction", Some(params)).await
193 }
194
195 pub async fn get_in_flight_bundle_statuses(&self, bundle_uuids: Vec<String>) -> Result<Value> {
196 let endpoint = if let Some(uuid) = &self.uuid {
197 format!("/getInflightBundleStatuses?uuid={}", uuid)
198 } else {
199 "/getInflightBundleStatuses".to_string()
200 };
201
202 let params = json!([bundle_uuids]);
203
204 self.send_request(&endpoint, "getInflightBundleStatuses", Some(params))
205 .await
206 .map_err(|e| anyhow!("Request error: {}", e))
207 }
208
209 pub fn prettify(value: Value) -> PrettyJsonValue {
211 PrettyJsonValue(value)
212 }
213}