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