1use std::time::Duration;
4
5use async_trait::async_trait;
6use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
7use reqwest::Url;
8use serde::{Deserialize, Serialize};
9
10use super::{JitoSubmitConfig, JitoSubmitTransport, SubmitTransportError};
11
12const DEFAULT_JITO_BLOCK_ENGINE_URL: &str = "https://mainnet.block-engine.jito.wtf";
14
15#[derive(Debug, Clone, Eq, PartialEq)]
17pub struct JitoAuthToken(String);
18
19impl JitoAuthToken {
20 #[must_use]
22 pub fn new(token: impl Into<String>) -> Self {
23 Self(token.into())
24 }
25
26 #[must_use]
28 pub fn as_str(&self) -> &str {
29 &self.0
30 }
31}
32
33#[derive(Debug, Clone, Eq, PartialEq)]
35pub enum JitoBlockEngineEndpoint {
36 Mainnet,
38 Custom(Url),
40}
41
42impl JitoBlockEngineEndpoint {
43 #[must_use]
45 pub const fn mainnet() -> Self {
46 Self::Mainnet
47 }
48
49 #[must_use]
51 pub const fn custom(url: Url) -> Self {
52 Self::Custom(url)
53 }
54
55 #[must_use]
57 pub fn as_url(&self) -> &str {
58 match self {
59 Self::Mainnet => DEFAULT_JITO_BLOCK_ENGINE_URL,
60 Self::Custom(url) => url.as_str(),
61 }
62 }
63}
64
65impl Default for JitoBlockEngineEndpoint {
66 fn default() -> Self {
67 Self::mainnet()
68 }
69}
70
71#[derive(Debug, Clone, Eq, PartialEq)]
73pub struct JitoTransportConfig {
74 pub endpoint: JitoBlockEngineEndpoint,
76 pub request_timeout: Duration,
78 pub auth_token: Option<JitoAuthToken>,
80}
81
82impl Default for JitoTransportConfig {
83 fn default() -> Self {
84 Self {
85 endpoint: JitoBlockEngineEndpoint::default(),
86 request_timeout: Duration::from_secs(10),
87 auth_token: None,
88 }
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct JitoJsonRpcTransport {
95 client: reqwest::Client,
97 transport_config: JitoTransportConfig,
99}
100
101impl JitoJsonRpcTransport {
102 pub fn new() -> Result<Self, SubmitTransportError> {
108 Self::with_config(JitoTransportConfig::default())
109 }
110
111 pub fn with_endpoint(endpoint: JitoBlockEngineEndpoint) -> Result<Self, SubmitTransportError> {
117 Self::with_config(JitoTransportConfig {
118 endpoint,
119 ..JitoTransportConfig::default()
120 })
121 }
122
123 pub fn with_config(
129 transport_config: JitoTransportConfig,
130 ) -> Result<Self, SubmitTransportError> {
131 let client = reqwest::Client::builder()
132 .timeout(transport_config.request_timeout)
133 .build()
134 .map_err(|error| SubmitTransportError::Config {
135 message: error.to_string(),
136 })?;
137 Ok(Self {
138 client,
139 transport_config,
140 })
141 }
142
143 fn request_url(&self, config: &JitoSubmitConfig) -> String {
145 let mut url = self
146 .transport_config
147 .endpoint
148 .as_url()
149 .trim_end_matches('/')
150 .to_owned();
151 url.push_str("/api/v1/transactions");
152 if config.bundle_only {
153 url.push_str("?bundleOnly=true");
154 }
155 url
156 }
157}
158
159#[derive(Debug, Deserialize)]
161struct JsonRpcResponse {
162 result: Option<String>,
164 error: Option<JsonRpcError>,
166}
167
168#[derive(Debug, Deserialize)]
170struct JsonRpcError {
171 code: i64,
173 message: String,
175}
176
177#[async_trait]
178impl JitoSubmitTransport for JitoJsonRpcTransport {
179 async fn submit_jito(
180 &self,
181 tx_bytes: &[u8],
182 config: &JitoSubmitConfig,
183 ) -> Result<String, SubmitTransportError> {
184 #[derive(Debug, Serialize)]
185 struct JitoRpcConfig<'config> {
186 encoding: &'config str,
188 }
189
190 let encoded_tx = BASE64_STANDARD.encode(tx_bytes);
191 let payload = serde_json::json!({
192 "jsonrpc": "2.0",
193 "id": 1,
194 "method": "sendTransaction",
195 "params": [
196 encoded_tx,
197 JitoRpcConfig { encoding: "base64" }
198 ]
199 });
200
201 let mut request = self.client.post(self.request_url(config)).json(&payload);
202 if let Some(auth_token) = &self.transport_config.auth_token {
203 request = request.header("x-jito-auth", auth_token.as_str());
204 }
205
206 let response = request
207 .send()
208 .await
209 .map_err(|error| SubmitTransportError::Failure {
210 message: error.to_string(),
211 })?;
212
213 let response =
214 response
215 .error_for_status()
216 .map_err(|error| SubmitTransportError::Failure {
217 message: error.to_string(),
218 })?;
219
220 let parsed: JsonRpcResponse =
221 response
222 .json()
223 .await
224 .map_err(|error| SubmitTransportError::Failure {
225 message: error.to_string(),
226 })?;
227
228 if let Some(signature) = parsed.result {
229 return Ok(signature);
230 }
231 if let Some(error) = parsed.error {
232 return Err(SubmitTransportError::Failure {
233 message: format!("jito error {}: {}", error.code, error.message),
234 });
235 }
236
237 Err(SubmitTransportError::Failure {
238 message: "jito returned neither result nor error".to_owned(),
239 })
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn request_url_uses_transactions_path() {
249 let transport_result = JitoJsonRpcTransport::new();
250 assert!(transport_result.is_ok());
251 let Some(transport) = transport_result.ok() else {
252 return;
253 };
254
255 let url = transport.request_url(&JitoSubmitConfig::default());
256
257 assert_eq!(
258 url,
259 "https://mainnet.block-engine.jito.wtf/api/v1/transactions"
260 );
261 }
262
263 #[test]
264 fn request_url_appends_bundle_only_query() {
265 let parsed_url_result = Url::parse("https://mainnet.block-engine.jito.wtf/");
266 assert!(parsed_url_result.is_ok());
267 let Some(parsed_url) = parsed_url_result.ok() else {
268 return;
269 };
270 let transport_result =
271 JitoJsonRpcTransport::with_endpoint(JitoBlockEngineEndpoint::custom(parsed_url));
272 assert!(transport_result.is_ok());
273 let Some(transport) = transport_result.ok() else {
274 return;
275 };
276
277 let url = transport.request_url(&JitoSubmitConfig { bundle_only: true });
278
279 assert_eq!(
280 url,
281 "https://mainnet.block-engine.jito.wtf/api/v1/transactions?bundleOnly=true"
282 );
283 }
284
285 #[test]
286 fn transport_config_defaults_are_stable() {
287 let config = JitoTransportConfig::default();
288
289 assert_eq!(config.endpoint, JitoBlockEngineEndpoint::mainnet());
290 assert_eq!(config.request_timeout, Duration::from_secs(10));
291 assert_eq!(config.auth_token, None);
292 }
293}