mirror of
https://github.com/0xfnzero/solana-streamer.git
synced 2026-08-18 11:28:05 +00:00
rename jito mod
This commit is contained in:
Executable
+462
@@ -0,0 +1,462 @@
|
||||
// This file is @generated by prost-build.
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct PostSubmitRequest {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub transaction: ::core::option::Option<TransactionMessage>,
|
||||
#[prost(bool, tag = "2")]
|
||||
pub skip_pre_flight: bool,
|
||||
#[prost(bool, optional, tag = "3")]
|
||||
pub front_running_protection: ::core::option::Option<bool>,
|
||||
#[prost(bool, optional, tag = "8")]
|
||||
pub experimental_front_running_protection: ::core::option::Option<bool>,
|
||||
#[prost(bool, optional, tag = "9")]
|
||||
pub snipe_transaction: ::core::option::Option<bool>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct PostSubmitRequestEntry {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub transaction: ::core::option::Option<TransactionMessage>,
|
||||
#[prost(bool, tag = "2")]
|
||||
pub skip_pre_flight: bool,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct PostSubmitBatchRequest {
|
||||
#[prost(message, repeated, tag = "1")]
|
||||
pub entries: ::prost::alloc::vec::Vec<PostSubmitRequestEntry>,
|
||||
#[prost(enumeration = "SubmitStrategy", tag = "2")]
|
||||
pub submit_strategy: i32,
|
||||
#[prost(bool, optional, tag = "3")]
|
||||
pub use_bundle: ::core::option::Option<bool>,
|
||||
#[prost(bool, optional, tag = "4")]
|
||||
pub front_running_protection: ::core::option::Option<bool>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct PostSubmitBatchResponseEntry {
|
||||
#[prost(string, tag = "1")]
|
||||
pub signature: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "2")]
|
||||
pub error: ::prost::alloc::string::String,
|
||||
#[prost(bool, tag = "3")]
|
||||
pub submitted: bool,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct PostSubmitBatchResponse {
|
||||
#[prost(message, repeated, tag = "1")]
|
||||
pub transactions: ::prost::alloc::vec::Vec<PostSubmitBatchResponseEntry>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct PostSubmitResponse {
|
||||
#[prost(string, tag = "1")]
|
||||
pub signature: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct TransactionMessage {
|
||||
#[prost(string, tag = "1")]
|
||||
pub content: ::prost::alloc::string::String,
|
||||
#[prost(bool, tag = "2")]
|
||||
pub is_cleanup: bool,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct TransactionMessageV2 {
|
||||
#[prost(string, tag = "1")]
|
||||
pub content: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||
#[repr(i32)]
|
||||
pub enum SubmitStrategy {
|
||||
PUknown = 0,
|
||||
PSubmitAll = 1,
|
||||
PAbortOnFirstError = 2,
|
||||
PWaitForConfirmation = 3,
|
||||
}
|
||||
impl SubmitStrategy {
|
||||
/// String value of the enum field names used in the ProtoBuf definition.
|
||||
///
|
||||
/// The values are not transformed in any way and thus are considered stable
|
||||
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
|
||||
pub fn as_str_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::PUknown => "P_UKNOWN",
|
||||
Self::PSubmitAll => "P_SUBMIT_ALL",
|
||||
Self::PAbortOnFirstError => "P_ABORT_ON_FIRST_ERROR",
|
||||
Self::PWaitForConfirmation => "P_WAIT_FOR_CONFIRMATION",
|
||||
}
|
||||
}
|
||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||
match value {
|
||||
"P_UKNOWN" => Some(Self::PUknown),
|
||||
"P_SUBMIT_ALL" => Some(Self::PSubmitAll),
|
||||
"P_ABORT_ON_FIRST_ERROR" => Some(Self::PAbortOnFirstError),
|
||||
"P_WAIT_FOR_CONFIRMATION" => Some(Self::PWaitForConfirmation),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated client implementations.
|
||||
pub mod api_client {
|
||||
#![allow(
|
||||
unused_variables,
|
||||
dead_code,
|
||||
missing_docs,
|
||||
clippy::wildcard_imports,
|
||||
clippy::let_unit_value,
|
||||
)]
|
||||
use tonic::codegen::*;
|
||||
use tonic::codegen::http::Uri;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ApiClient<T> {
|
||||
inner: tonic::client::Grpc<T>,
|
||||
}
|
||||
impl ApiClient<tonic::transport::Channel> {
|
||||
/// Attempt to create a new client by connecting to a given endpoint.
|
||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
||||
where
|
||||
D: std::convert::TryInto<tonic::transport::Endpoint>,
|
||||
D::Error: Into<StdError>,
|
||||
{
|
||||
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
||||
Ok(Self::new(conn))
|
||||
}
|
||||
}
|
||||
impl<T> ApiClient<T>
|
||||
where
|
||||
T: tonic::client::GrpcService<tonic::body::BoxBody>,
|
||||
T::Error: Into<StdError>,
|
||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
||||
{
|
||||
pub fn new(inner: T) -> Self {
|
||||
let inner = tonic::client::Grpc::new(inner);
|
||||
Self { inner }
|
||||
}
|
||||
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
||||
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
||||
Self { inner }
|
||||
}
|
||||
pub fn with_interceptor<F>(
|
||||
inner: T,
|
||||
interceptor: F,
|
||||
) -> ApiClient<InterceptedService<T, F>>
|
||||
where
|
||||
F: tonic::service::Interceptor,
|
||||
T::ResponseBody: Default,
|
||||
T: tonic::codegen::Service<
|
||||
http::Request<tonic::body::BoxBody>,
|
||||
Response = http::Response<
|
||||
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
|
||||
>,
|
||||
>,
|
||||
<T as tonic::codegen::Service<
|
||||
http::Request<tonic::body::BoxBody>,
|
||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
||||
{
|
||||
ApiClient::new(InterceptedService::new(inner, interceptor))
|
||||
}
|
||||
/// Compress requests with the given encoding.
|
||||
///
|
||||
/// This requires the server to support it otherwise it might respond with an
|
||||
/// error.
|
||||
#[must_use]
|
||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.inner = self.inner.send_compressed(encoding);
|
||||
self
|
||||
}
|
||||
/// Enable decompressing responses.
|
||||
#[must_use]
|
||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.inner = self.inner.accept_compressed(encoding);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of a decoded message.
|
||||
///
|
||||
/// Default: `4MB`
|
||||
#[must_use]
|
||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.inner = self.inner.max_decoding_message_size(limit);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of an encoded message.
|
||||
///
|
||||
/// Default: `usize::MAX`
|
||||
#[must_use]
|
||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.inner = self.inner.max_encoding_message_size(limit);
|
||||
self
|
||||
}
|
||||
pub async fn post_submit_v2(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::PostSubmitRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::PostSubmitResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static("/api.Api/PostSubmitV2");
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut().insert(GrpcMethod::new("api.Api", "PostSubmitV2"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn post_submit_batch_v2(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::PostSubmitBatchRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::PostSubmitBatchResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/api.Api/PostSubmitBatchV2",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut().insert(GrpcMethod::new("api.Api", "PostSubmitBatchV2"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated server implementations.
|
||||
pub mod api_server {
|
||||
#![allow(
|
||||
unused_variables,
|
||||
dead_code,
|
||||
missing_docs,
|
||||
clippy::wildcard_imports,
|
||||
clippy::let_unit_value,
|
||||
)]
|
||||
use tonic::codegen::*;
|
||||
/// Generated trait containing gRPC methods that should be implemented for use with ApiServer.
|
||||
#[async_trait]
|
||||
pub trait Api: std::marker::Send + std::marker::Sync + 'static {
|
||||
async fn post_submit_v2(
|
||||
&self,
|
||||
request: tonic::Request<super::PostSubmitRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::PostSubmitResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn post_submit_batch_v2(
|
||||
&self,
|
||||
request: tonic::Request<super::PostSubmitBatchRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::PostSubmitBatchResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct ApiServer<T> {
|
||||
inner: Arc<T>,
|
||||
accept_compression_encodings: EnabledCompressionEncodings,
|
||||
send_compression_encodings: EnabledCompressionEncodings,
|
||||
max_decoding_message_size: Option<usize>,
|
||||
max_encoding_message_size: Option<usize>,
|
||||
}
|
||||
impl<T> ApiServer<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self::from_arc(Arc::new(inner))
|
||||
}
|
||||
pub fn from_arc(inner: Arc<T>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
accept_compression_encodings: Default::default(),
|
||||
send_compression_encodings: Default::default(),
|
||||
max_decoding_message_size: None,
|
||||
max_encoding_message_size: None,
|
||||
}
|
||||
}
|
||||
pub fn with_interceptor<F>(
|
||||
inner: T,
|
||||
interceptor: F,
|
||||
) -> InterceptedService<Self, F>
|
||||
where
|
||||
F: tonic::service::Interceptor,
|
||||
{
|
||||
InterceptedService::new(Self::new(inner), interceptor)
|
||||
}
|
||||
/// Enable decompressing requests with the given encoding.
|
||||
#[must_use]
|
||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.accept_compression_encodings.enable(encoding);
|
||||
self
|
||||
}
|
||||
/// Compress responses with the given encoding, if the client supports it.
|
||||
#[must_use]
|
||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.send_compression_encodings.enable(encoding);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of a decoded message.
|
||||
///
|
||||
/// Default: `4MB`
|
||||
#[must_use]
|
||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.max_decoding_message_size = Some(limit);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of an encoded message.
|
||||
///
|
||||
/// Default: `usize::MAX`
|
||||
#[must_use]
|
||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.max_encoding_message_size = Some(limit);
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<T, B> tonic::codegen::Service<http::Request<B>> for ApiServer<T>
|
||||
where
|
||||
T: Api,
|
||||
B: Body + std::marker::Send + 'static,
|
||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
||||
{
|
||||
type Response = http::Response<tonic::body::BoxBody>;
|
||||
type Error = std::convert::Infallible;
|
||||
type Future = BoxFuture<Self::Response, Self::Error>;
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<std::result::Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||
match req.uri().path() {
|
||||
"/api.Api/PostSubmitV2" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct PostSubmitV2Svc<T: Api>(pub Arc<T>);
|
||||
impl<T: Api> tonic::server::UnaryService<super::PostSubmitRequest>
|
||||
for PostSubmitV2Svc<T> {
|
||||
type Response = super::PostSubmitResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::PostSubmitRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as Api>::post_submit_v2(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = PostSubmitV2Svc(inner);
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/api.Api/PostSubmitBatchV2" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct PostSubmitBatchV2Svc<T: Api>(pub Arc<T>);
|
||||
impl<
|
||||
T: Api,
|
||||
> tonic::server::UnaryService<super::PostSubmitBatchRequest>
|
||||
for PostSubmitBatchV2Svc<T> {
|
||||
type Response = super::PostSubmitBatchResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::PostSubmitBatchRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as Api>::post_submit_batch_v2(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = PostSubmitBatchV2Svc(inner);
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
_ => {
|
||||
Box::pin(async move {
|
||||
let mut response = http::Response::new(empty_body());
|
||||
let headers = response.headers_mut();
|
||||
headers
|
||||
.insert(
|
||||
tonic::Status::GRPC_STATUS,
|
||||
(tonic::Code::Unimplemented as i32).into(),
|
||||
);
|
||||
headers
|
||||
.insert(
|
||||
http::header::CONTENT_TYPE,
|
||||
tonic::metadata::GRPC_CONTENT_TYPE,
|
||||
);
|
||||
Ok(response)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> Clone for ApiServer<T> {
|
||||
fn clone(&self) -> Self {
|
||||
let inner = self.inner.clone();
|
||||
Self {
|
||||
inner,
|
||||
accept_compression_encodings: self.accept_compression_encodings,
|
||||
send_compression_encodings: self.send_compression_encodings,
|
||||
max_decoding_message_size: self.max_decoding_message_size,
|
||||
max_encoding_message_size: self.max_encoding_message_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated gRPC service name
|
||||
pub const SERVICE_NAME: &str = "api.Api";
|
||||
impl<T> tonic::server::NamedService for ApiServer<T> {
|
||||
const NAME: &'static str = SERVICE_NAME;
|
||||
}
|
||||
}
|
||||
Executable
+127
@@ -0,0 +1,127 @@
|
||||
use bincode::serialize;
|
||||
use serde_json::json;
|
||||
use solana_client::rpc_client::SerializableTransaction;
|
||||
use solana_sdk::signature::Signature;
|
||||
use solana_sdk::transaction::Transaction;
|
||||
use solana_transaction_status::{TransactionConfirmationStatus, UiTransactionEncoding};
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time::sleep;
|
||||
use crate::common::types::SolanaRpcClient;
|
||||
use anyhow::Result;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use reqwest::Client;
|
||||
|
||||
pub async fn poll_transaction_confirmation(rpc: &SolanaRpcClient, txt_sig: Signature) -> Result<Signature> {
|
||||
// 15 second timeout
|
||||
let timeout: Duration = Duration::from_secs(15);
|
||||
// 5 second retry interval
|
||||
let interval: Duration = Duration::from_secs(5);
|
||||
let start: Instant = Instant::now();
|
||||
|
||||
loop {
|
||||
if start.elapsed() >= timeout {
|
||||
return Err(anyhow::anyhow!("Transaction {}'s confirmation timed out", txt_sig));
|
||||
}
|
||||
|
||||
let status = rpc.get_signature_statuses(&[txt_sig]).await?;
|
||||
|
||||
match status.value[0].clone() {
|
||||
Some(status) => {
|
||||
if status.err.is_none()
|
||||
&& (status.confirmation_status == Some(TransactionConfirmationStatus::Confirmed)
|
||||
|| status.confirmation_status == Some(TransactionConfirmationStatus::Finalized))
|
||||
{
|
||||
return Ok(txt_sig);
|
||||
}
|
||||
if status.err.is_some() {
|
||||
return Err(anyhow::anyhow!(status.err.unwrap()));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
sleep(interval).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_nb_transaction(client: Client, endpoint: &str, auth_token: &str, transaction: &Transaction) -> Result<Signature, anyhow::Error> {
|
||||
// 序列化交易
|
||||
let serialized = bincode::serialize(transaction)
|
||||
.map_err(|e| anyhow::anyhow!("序列化交易失败: {}", e))?;
|
||||
|
||||
// Base64编码
|
||||
let encoded = STANDARD.encode(serialized);
|
||||
|
||||
let request_data = json!({
|
||||
"transaction": {
|
||||
"content": encoded
|
||||
},
|
||||
"frontRunningProtection": true
|
||||
});
|
||||
|
||||
let url = format!("{}/api/v2/submit", endpoint);
|
||||
let response = client
|
||||
.post(url)
|
||||
.header("Authorization", auth_token)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&request_data)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("请求失败: {}", e))?;
|
||||
|
||||
let resp = response.json::<serde_json::Value>().await
|
||||
.map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?;
|
||||
|
||||
if let Some(reason) = resp["reason"].as_str() {
|
||||
return Err(anyhow::anyhow!(reason.to_string()));
|
||||
}
|
||||
|
||||
let signature = resp["signature"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("响应中缺少signature字段"))?;
|
||||
|
||||
let signature = Signature::from_str(signature)
|
||||
.map_err(|e| anyhow::anyhow!("无效的签名: {}", e))?;
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
pub async fn serialize_and_encode(
|
||||
transaction: &Vec<u8>,
|
||||
encoding: UiTransactionEncoding,
|
||||
) -> Result<String> {
|
||||
let serialized = match encoding {
|
||||
UiTransactionEncoding::Base58 => bs58::encode(transaction).into_string(),
|
||||
UiTransactionEncoding::Base64 => STANDARD.encode(transaction),
|
||||
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
||||
};
|
||||
Ok(serialized)
|
||||
}
|
||||
|
||||
pub async fn serialize_transaction_and_encode(
|
||||
transaction: &impl SerializableTransaction,
|
||||
encoding: UiTransactionEncoding,
|
||||
) -> Result<String> {
|
||||
let serialized_tx = serialize(transaction)?;
|
||||
let serialized = match encoding {
|
||||
UiTransactionEncoding::Base58 => bs58::encode(serialized_tx).into_string(),
|
||||
UiTransactionEncoding::Base64 => STANDARD.encode(serialized_tx),
|
||||
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
||||
};
|
||||
Ok(serialized)
|
||||
}
|
||||
|
||||
pub async fn serialize_smart_transaction_and_encode(
|
||||
transaction: &impl SerializableTransaction,
|
||||
encoding: UiTransactionEncoding,
|
||||
) -> Result<(String, Signature)> {
|
||||
let signature = transaction.get_signature();
|
||||
let serialized_tx = serialize(transaction)?;
|
||||
let serialized = match encoding {
|
||||
UiTransactionEncoding::Base58 => bs58::encode(serialized_tx).into_string(),
|
||||
UiTransactionEncoding::Base64 => STANDARD.encode(serialized_tx),
|
||||
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
||||
};
|
||||
Ok((serialized, *signature))
|
||||
}
|
||||
Executable
+171
@@ -0,0 +1,171 @@
|
||||
// This file is @generated by prost-build.
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Bundle {
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub header: ::core::option::Option<super::shared::Header>,
|
||||
#[prost(message, repeated, tag = "3")]
|
||||
pub packets: ::prost::alloc::vec::Vec<super::packet::Packet>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct BundleUuid {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub bundle: ::core::option::Option<Bundle>,
|
||||
#[prost(string, tag = "2")]
|
||||
pub uuid: ::prost::alloc::string::String,
|
||||
}
|
||||
/// Indicates the bundle was accepted and forwarded to a validator.
|
||||
/// NOTE: A single bundle may have multiple events emitted if forwarded to many validators.
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Accepted {
|
||||
/// Slot at which bundle was forwarded.
|
||||
#[prost(uint64, tag = "1")]
|
||||
pub slot: u64,
|
||||
/// Validator identity bundle was forwarded to.
|
||||
#[prost(string, tag = "2")]
|
||||
pub validator_identity: ::prost::alloc::string::String,
|
||||
}
|
||||
/// Indicates the bundle was dropped and therefore not forwarded to any validator.
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Rejected {
|
||||
#[prost(oneof = "rejected::Reason", tags = "1, 2, 3, 4, 5")]
|
||||
pub reason: ::core::option::Option<rejected::Reason>,
|
||||
}
|
||||
/// Nested message and enum types in `Rejected`.
|
||||
pub mod rejected {
|
||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
||||
pub enum Reason {
|
||||
#[prost(message, tag = "1")]
|
||||
StateAuctionBidRejected(super::StateAuctionBidRejected),
|
||||
#[prost(message, tag = "2")]
|
||||
WinningBatchBidRejected(super::WinningBatchBidRejected),
|
||||
#[prost(message, tag = "3")]
|
||||
SimulationFailure(super::SimulationFailure),
|
||||
#[prost(message, tag = "4")]
|
||||
InternalError(super::InternalError),
|
||||
#[prost(message, tag = "5")]
|
||||
DroppedBundle(super::DroppedBundle),
|
||||
}
|
||||
}
|
||||
/// Indicates the bundle's bid was high enough to win its state auction.
|
||||
/// However, not high enough relative to other state auction winners and therefore excluded from being forwarded.
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct WinningBatchBidRejected {
|
||||
/// Auction's unique identifier.
|
||||
#[prost(string, tag = "1")]
|
||||
pub auction_id: ::prost::alloc::string::String,
|
||||
/// Bundle's simulated bid.
|
||||
#[prost(uint64, tag = "2")]
|
||||
pub simulated_bid_lamports: u64,
|
||||
#[prost(string, optional, tag = "3")]
|
||||
pub msg: ::core::option::Option<::prost::alloc::string::String>,
|
||||
}
|
||||
/// Indicates the bundle's bid was __not__ high enough to be included in its state auction's set of winners.
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct StateAuctionBidRejected {
|
||||
/// Auction's unique identifier.
|
||||
#[prost(string, tag = "1")]
|
||||
pub auction_id: ::prost::alloc::string::String,
|
||||
/// Bundle's simulated bid.
|
||||
#[prost(uint64, tag = "2")]
|
||||
pub simulated_bid_lamports: u64,
|
||||
#[prost(string, optional, tag = "3")]
|
||||
pub msg: ::core::option::Option<::prost::alloc::string::String>,
|
||||
}
|
||||
/// Bundle dropped due to simulation failure.
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct SimulationFailure {
|
||||
/// Signature of the offending transaction.
|
||||
#[prost(string, tag = "1")]
|
||||
pub tx_signature: ::prost::alloc::string::String,
|
||||
#[prost(string, optional, tag = "2")]
|
||||
pub msg: ::core::option::Option<::prost::alloc::string::String>,
|
||||
}
|
||||
/// Bundle dropped due to an internal error.
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct InternalError {
|
||||
#[prost(string, tag = "1")]
|
||||
pub msg: ::prost::alloc::string::String,
|
||||
}
|
||||
/// Bundle dropped (e.g. because no leader upcoming)
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct DroppedBundle {
|
||||
#[prost(string, tag = "1")]
|
||||
pub msg: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||
pub struct Finalized {}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Processed {
|
||||
#[prost(string, tag = "1")]
|
||||
pub validator_identity: ::prost::alloc::string::String,
|
||||
#[prost(uint64, tag = "2")]
|
||||
pub slot: u64,
|
||||
/// / Index within the block.
|
||||
#[prost(uint64, tag = "3")]
|
||||
pub bundle_index: u64,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||
pub struct Dropped {
|
||||
#[prost(enumeration = "DroppedReason", tag = "1")]
|
||||
pub reason: i32,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct BundleResult {
|
||||
/// Bundle's Uuid.
|
||||
#[prost(string, tag = "1")]
|
||||
pub bundle_id: ::prost::alloc::string::String,
|
||||
#[prost(oneof = "bundle_result::Result", tags = "2, 3, 4, 5, 6")]
|
||||
pub result: ::core::option::Option<bundle_result::Result>,
|
||||
}
|
||||
/// Nested message and enum types in `BundleResult`.
|
||||
pub mod bundle_result {
|
||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
||||
pub enum Result {
|
||||
/// Indicated accepted by the block-engine and forwarded to a jito-solana validator.
|
||||
#[prost(message, tag = "2")]
|
||||
Accepted(super::Accepted),
|
||||
/// Rejected by the block-engine.
|
||||
#[prost(message, tag = "3")]
|
||||
Rejected(super::Rejected),
|
||||
/// Reached finalized commitment level.
|
||||
#[prost(message, tag = "4")]
|
||||
Finalized(super::Finalized),
|
||||
/// Reached a processed commitment level.
|
||||
#[prost(message, tag = "5")]
|
||||
Processed(super::Processed),
|
||||
/// Was accepted and forwarded by the block-engine but never landed on-chain.
|
||||
#[prost(message, tag = "6")]
|
||||
Dropped(super::Dropped),
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||
#[repr(i32)]
|
||||
pub enum DroppedReason {
|
||||
BlockhashExpired = 0,
|
||||
/// One or more transactions in the bundle landed on-chain, invalidating the bundle.
|
||||
PartiallyProcessed = 1,
|
||||
/// This indicates bundle was processed but not finalized. This could occur during forks.
|
||||
NotFinalized = 2,
|
||||
}
|
||||
impl DroppedReason {
|
||||
/// String value of the enum field names used in the ProtoBuf definition.
|
||||
///
|
||||
/// The values are not transformed in any way and thus are considered stable
|
||||
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
|
||||
pub fn as_str_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::BlockhashExpired => "BlockhashExpired",
|
||||
Self::PartiallyProcessed => "PartiallyProcessed",
|
||||
Self::NotFinalized => "NotFinalized",
|
||||
}
|
||||
}
|
||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||
match value {
|
||||
"BlockhashExpired" => Some(Self::BlockhashExpired),
|
||||
"PartiallyProcessed" => Some(Self::PartiallyProcessed),
|
||||
"NotFinalized" => Some(Self::NotFinalized),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+156
@@ -0,0 +1,156 @@
|
||||
use std::{
|
||||
cmp::min,
|
||||
net::{AddrParseError, IpAddr, Ipv4Addr, SocketAddr},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use bincode::serialize;
|
||||
use solana_perf::packet::{Packet, PacketBatch, PACKET_DATA_SIZE};
|
||||
use solana_sdk::{
|
||||
packet::{Meta, PacketFlags},
|
||||
transaction::VersionedTransaction,
|
||||
};
|
||||
|
||||
use crate::swqos::jito_grpc::packet::{
|
||||
Meta as ProtoMeta, Packet as ProtoPacket, PacketBatch as ProtoPacketBatch,
|
||||
PacketFlags as ProtoPacketFlags,
|
||||
};
|
||||
use crate::swqos::jito_grpc::shared::Socket;
|
||||
|
||||
/// Converts a Solana packet to a protobuf packet
|
||||
/// NOTE: the packet.data() function will filter packets marked for discard
|
||||
pub fn packet_to_proto_packet(p: &Packet) -> Option<ProtoPacket> {
|
||||
Some(ProtoPacket {
|
||||
data: p.data(..)?.to_vec(),
|
||||
meta: Some(ProtoMeta {
|
||||
size: p.meta().size as u64,
|
||||
addr: p.meta().addr.to_string(),
|
||||
port: p.meta().port as u32,
|
||||
flags: Some(ProtoPacketFlags {
|
||||
discard: p.meta().discard(),
|
||||
forwarded: p.meta().forwarded(),
|
||||
repair: p.meta().repair(),
|
||||
simple_vote_tx: p.meta().is_simple_vote_tx(),
|
||||
tracer_packet: p.meta().is_perf_track_packet(),
|
||||
from_staked_node: p.meta().is_from_staked_node(),
|
||||
}),
|
||||
sender_stake: 0,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn packet_batches_to_proto_packets(
|
||||
batches: &[PacketBatch],
|
||||
) -> impl Iterator<Item = ProtoPacket> + '_ {
|
||||
batches
|
||||
.iter()
|
||||
.flat_map(|b| b.iter().filter_map(packet_to_proto_packet))
|
||||
}
|
||||
|
||||
/// converts from a protobuf packet to packet
|
||||
pub fn proto_packet_to_packet(p: &ProtoPacket) -> Packet {
|
||||
let mut data = [0u8; PACKET_DATA_SIZE];
|
||||
let copy_len = min(data.len(), p.data.len());
|
||||
data[..copy_len].copy_from_slice(&p.data[..copy_len]);
|
||||
let mut packet = Packet::new(data, Meta::default());
|
||||
if let Some(meta) = &p.meta {
|
||||
packet.meta_mut().size = meta.size as usize;
|
||||
packet.meta_mut().addr = meta
|
||||
.addr
|
||||
.parse()
|
||||
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
|
||||
packet.meta_mut().port = meta.port as u16;
|
||||
if let Some(flags) = &meta.flags {
|
||||
if flags.simple_vote_tx {
|
||||
packet.meta_mut().flags.insert(PacketFlags::SIMPLE_VOTE_TX);
|
||||
}
|
||||
if flags.forwarded {
|
||||
packet.meta_mut().flags.insert(PacketFlags::FORWARDED);
|
||||
}
|
||||
if flags.tracer_packet {
|
||||
packet.meta_mut().flags.insert(PacketFlags::PERF_TRACK_PACKET);
|
||||
}
|
||||
if flags.repair {
|
||||
packet.meta_mut().flags.insert(PacketFlags::REPAIR);
|
||||
}
|
||||
if flags.discard {
|
||||
packet.meta_mut().flags.insert(PacketFlags::DISCARD);
|
||||
}
|
||||
}
|
||||
}
|
||||
packet
|
||||
}
|
||||
|
||||
pub fn proto_packet_batch_to_packets(
|
||||
packet_batch: ProtoPacketBatch,
|
||||
) -> impl Iterator<Item = Packet> {
|
||||
packet_batch
|
||||
.packets
|
||||
.into_iter()
|
||||
.map(|proto_packet| proto_packet_to_packet(&proto_packet))
|
||||
}
|
||||
|
||||
/// Converts a protobuf packet to a VersionedTransaction
|
||||
pub fn versioned_tx_from_packet(p: &ProtoPacket) -> Option<VersionedTransaction> {
|
||||
let mut data = [0; PACKET_DATA_SIZE];
|
||||
let copy_len = min(data.len(), p.data.len());
|
||||
data[..copy_len].copy_from_slice(&p.data[..copy_len]);
|
||||
let mut packet = Packet::new(data, Default::default());
|
||||
if let Some(meta) = &p.meta {
|
||||
packet.meta_mut().size = meta.size as usize;
|
||||
}
|
||||
packet.deserialize_slice(..).ok()
|
||||
}
|
||||
|
||||
/// Coverts a VersionedTransaction to packet
|
||||
pub fn packet_from_versioned_tx(tx: VersionedTransaction) -> Packet {
|
||||
let tx_data = serialize(&tx).expect("serializes");
|
||||
let mut data = [0; PACKET_DATA_SIZE];
|
||||
let copy_len = min(tx_data.len(), data.len());
|
||||
data[..copy_len].copy_from_slice(&tx_data[..copy_len]);
|
||||
let mut packet = Packet::new(data, Default::default());
|
||||
packet.meta_mut().size = copy_len;
|
||||
packet
|
||||
}
|
||||
|
||||
/// Converts a VersionedTransaction to a protobuf packet
|
||||
pub fn proto_packet_from_versioned_tx(tx: &VersionedTransaction) -> ProtoPacket {
|
||||
let data = serialize(tx).expect("serializes");
|
||||
let size = data.len() as u64;
|
||||
ProtoPacket {
|
||||
data,
|
||||
meta: Some(ProtoMeta {
|
||||
size,
|
||||
addr: "".to_string(),
|
||||
port: 0,
|
||||
flags: None,
|
||||
sender_stake: 0,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a GRPC Socket to stdlib SocketAddr
|
||||
impl TryFrom<&Socket> for SocketAddr {
|
||||
type Error = AddrParseError;
|
||||
|
||||
fn try_from(value: &Socket) -> Result<Self, Self::Error> {
|
||||
IpAddr::from_str(&value.ip).map(|ip| SocketAddr::new(ip, value.port as u16))
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use solana_perf::test_tx::test_tx;
|
||||
// use solana_sdk::transaction::VersionedTransaction;
|
||||
|
||||
// use crate::convert::{proto_packet_from_versioned_tx, versioned_tx_from_packet};
|
||||
|
||||
// #[test]
|
||||
// fn test_proto_to_packet() {
|
||||
// let tx_before = VersionedTransaction::from(test_tx());
|
||||
// let tx_after = versioned_tx_from_packet(&proto_packet_from_versioned_tx(&tx_before))
|
||||
// .expect("tx_after");
|
||||
|
||||
// assert_eq!(tx_before, tx_after);
|
||||
// }
|
||||
// }
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
pub mod bundle;
|
||||
pub mod packet;
|
||||
pub mod searcher;
|
||||
pub mod shared;
|
||||
pub mod convert;
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
// This file is @generated by prost-build.
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct PacketBatch {
|
||||
#[prost(message, repeated, tag = "1")]
|
||||
pub packets: ::prost::alloc::vec::Vec<Packet>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Packet {
|
||||
#[prost(bytes = "vec", tag = "1")]
|
||||
pub data: ::prost::alloc::vec::Vec<u8>,
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub meta: ::core::option::Option<Meta>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Meta {
|
||||
#[prost(uint64, tag = "1")]
|
||||
pub size: u64,
|
||||
#[prost(string, tag = "2")]
|
||||
pub addr: ::prost::alloc::string::String,
|
||||
#[prost(uint32, tag = "3")]
|
||||
pub port: u32,
|
||||
#[prost(message, optional, tag = "4")]
|
||||
pub flags: ::core::option::Option<PacketFlags>,
|
||||
#[prost(uint64, tag = "5")]
|
||||
pub sender_stake: u64,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||
pub struct PacketFlags {
|
||||
#[prost(bool, tag = "1")]
|
||||
pub discard: bool,
|
||||
#[prost(bool, tag = "2")]
|
||||
pub forwarded: bool,
|
||||
#[prost(bool, tag = "3")]
|
||||
pub repair: bool,
|
||||
#[prost(bool, tag = "4")]
|
||||
pub simple_vote_tx: bool,
|
||||
#[prost(bool, tag = "5")]
|
||||
pub tracer_packet: bool,
|
||||
#[prost(bool, tag = "6")]
|
||||
pub from_staked_node: bool,
|
||||
}
|
||||
Executable
+363
@@ -0,0 +1,363 @@
|
||||
// This file is @generated by prost-build.
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct SlotList {
|
||||
#[prost(uint64, repeated, tag = "1")]
|
||||
pub slots: ::prost::alloc::vec::Vec<u64>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ConnectedLeadersResponse {
|
||||
/// Mapping of validator pubkey to leader slots for the current epoch.
|
||||
#[prost(map = "string, message", tag = "1")]
|
||||
pub connected_validators: ::std::collections::HashMap<
|
||||
::prost::alloc::string::String,
|
||||
SlotList,
|
||||
>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct SendBundleRequest {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub bundle: ::core::option::Option<super::bundle::Bundle>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct SendBundleResponse {
|
||||
/// server uuid for the bundle
|
||||
#[prost(string, tag = "1")]
|
||||
pub uuid: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct NextScheduledLeaderRequest {
|
||||
/// Defaults to the currently connected region if no region provided.
|
||||
#[prost(string, repeated, tag = "1")]
|
||||
pub regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct NextScheduledLeaderResponse {
|
||||
/// the current slot the backend is on
|
||||
#[prost(uint64, tag = "1")]
|
||||
pub current_slot: u64,
|
||||
/// the slot of the next leader
|
||||
#[prost(uint64, tag = "2")]
|
||||
pub next_leader_slot: u64,
|
||||
/// the identity pubkey (base58) of the next leader
|
||||
#[prost(string, tag = "3")]
|
||||
pub next_leader_identity: ::prost::alloc::string::String,
|
||||
/// the block engine region of the next leader
|
||||
#[prost(string, tag = "4")]
|
||||
pub next_leader_region: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||
pub struct ConnectedLeadersRequest {}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ConnectedLeadersRegionedRequest {
|
||||
/// Defaults to the currently connected region if no region provided.
|
||||
#[prost(string, repeated, tag = "1")]
|
||||
pub regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ConnectedLeadersRegionedResponse {
|
||||
#[prost(map = "string, message", tag = "1")]
|
||||
pub connected_validators: ::std::collections::HashMap<
|
||||
::prost::alloc::string::String,
|
||||
ConnectedLeadersResponse,
|
||||
>,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||
pub struct GetTipAccountsRequest {}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct GetTipAccountsResponse {
|
||||
#[prost(string, repeated, tag = "1")]
|
||||
pub accounts: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||
pub struct SubscribeBundleResultsRequest {}
|
||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||
pub struct GetRegionsRequest {}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct GetRegionsResponse {
|
||||
/// The region the client is currently connected to
|
||||
#[prost(string, tag = "1")]
|
||||
pub current_region: ::prost::alloc::string::String,
|
||||
/// Regions that are online and ready for connections
|
||||
/// All regions: <https://jito-labs.gitbook.io/mev/systems/connecting/mainnet>
|
||||
#[prost(string, repeated, tag = "2")]
|
||||
pub available_regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
||||
}
|
||||
/// Generated client implementations.
|
||||
pub mod searcher_service_client {
|
||||
#![allow(
|
||||
unused_variables,
|
||||
dead_code,
|
||||
missing_docs,
|
||||
clippy::wildcard_imports,
|
||||
clippy::let_unit_value,
|
||||
)]
|
||||
use tonic::codegen::*;
|
||||
use tonic::codegen::http::Uri;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SearcherServiceClient<T> {
|
||||
inner: tonic::client::Grpc<T>,
|
||||
}
|
||||
impl SearcherServiceClient<tonic::transport::Channel> {
|
||||
/// Attempt to create a new client by connecting to a given endpoint.
|
||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
||||
where
|
||||
D: TryInto<tonic::transport::Endpoint>,
|
||||
D::Error: Into<StdError>,
|
||||
{
|
||||
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
||||
Ok(Self::new(conn))
|
||||
}
|
||||
}
|
||||
impl<T> SearcherServiceClient<T>
|
||||
where
|
||||
T: tonic::client::GrpcService<tonic::body::BoxBody>,
|
||||
T::Error: Into<StdError>,
|
||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
||||
{
|
||||
pub fn new(inner: T) -> Self {
|
||||
let inner = tonic::client::Grpc::new(inner);
|
||||
Self { inner }
|
||||
}
|
||||
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
||||
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
||||
Self { inner }
|
||||
}
|
||||
pub fn with_interceptor<F>(
|
||||
inner: T,
|
||||
interceptor: F,
|
||||
) -> SearcherServiceClient<InterceptedService<T, F>>
|
||||
where
|
||||
F: tonic::service::Interceptor,
|
||||
T::ResponseBody: Default,
|
||||
T: tonic::codegen::Service<
|
||||
http::Request<tonic::body::BoxBody>,
|
||||
Response = http::Response<
|
||||
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
|
||||
>,
|
||||
>,
|
||||
<T as tonic::codegen::Service<
|
||||
http::Request<tonic::body::BoxBody>,
|
||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
||||
{
|
||||
SearcherServiceClient::new(InterceptedService::new(inner, interceptor))
|
||||
}
|
||||
/// Compress requests with the given encoding.
|
||||
///
|
||||
/// This requires the server to support it otherwise it might respond with an
|
||||
/// error.
|
||||
#[must_use]
|
||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.inner = self.inner.send_compressed(encoding);
|
||||
self
|
||||
}
|
||||
/// Enable decompressing responses.
|
||||
#[must_use]
|
||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.inner = self.inner.accept_compressed(encoding);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of a decoded message.
|
||||
///
|
||||
/// Default: `4MB`
|
||||
#[must_use]
|
||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.inner = self.inner.max_decoding_message_size(limit);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of an encoded message.
|
||||
///
|
||||
/// Default: `usize::MAX`
|
||||
#[must_use]
|
||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.inner = self.inner.max_encoding_message_size(limit);
|
||||
self
|
||||
}
|
||||
/// Searchers can invoke this endpoint to subscribe to their respective bundle results.
|
||||
/// A success result would indicate the bundle won its state auction and was submitted to the validator.
|
||||
pub async fn subscribe_bundle_results(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::SubscribeBundleResultsRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<tonic::codec::Streaming<super::super::bundle::BundleResult>>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/searcher.SearcherService/SubscribeBundleResults",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new("searcher.SearcherService", "SubscribeBundleResults"),
|
||||
);
|
||||
self.inner.server_streaming(req, path, codec).await
|
||||
}
|
||||
pub async fn send_bundle(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::SendBundleRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::SendBundleResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/searcher.SearcherService/SendBundle",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("searcher.SearcherService", "SendBundle"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
/// Returns the next scheduled leader connected to the block engine.
|
||||
pub async fn get_next_scheduled_leader(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::NextScheduledLeaderRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::NextScheduledLeaderResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/searcher.SearcherService/GetNextScheduledLeader",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new("searcher.SearcherService", "GetNextScheduledLeader"),
|
||||
);
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
/// Returns leader slots for connected jito validators during the current epoch. Only returns data for this region.
|
||||
pub async fn get_connected_leaders(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::ConnectedLeadersRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::ConnectedLeadersResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/searcher.SearcherService/GetConnectedLeaders",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new("searcher.SearcherService", "GetConnectedLeaders"),
|
||||
);
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
/// Returns leader slots for connected jito validators during the current epoch.
|
||||
pub async fn get_connected_leaders_regioned(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::ConnectedLeadersRegionedRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::ConnectedLeadersRegionedResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/searcher.SearcherService/GetConnectedLeadersRegioned",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"searcher.SearcherService",
|
||||
"GetConnectedLeadersRegioned",
|
||||
),
|
||||
);
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
/// Returns the tip accounts searchers shall transfer funds to for the leader to claim.
|
||||
pub async fn get_tip_accounts(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::GetTipAccountsRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::GetTipAccountsResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/searcher.SearcherService/GetTipAccounts",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("searcher.SearcherService", "GetTipAccounts"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
/// Returns region the client is directly connected to, along with all available regions
|
||||
pub async fn get_regions(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::GetRegionsRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::GetRegionsResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/searcher.SearcherService/GetRegions",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("searcher.SearcherService", "GetRegions"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
// This file is @generated by prost-build.
|
||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||
pub struct Header {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub ts: ::core::option::Option<::prost_types::Timestamp>,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||
pub struct Heartbeat {
|
||||
#[prost(uint64, tag = "1")]
|
||||
pub count: u64,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Socket {
|
||||
#[prost(string, tag = "1")]
|
||||
pub ip: ::prost::alloc::string::String,
|
||||
#[prost(int64, tag = "2")]
|
||||
pub port: i64,
|
||||
}
|
||||
Executable
+335
@@ -0,0 +1,335 @@
|
||||
use api::api_client::ApiClient;
|
||||
use common::{poll_transaction_confirmation, serialize_smart_transaction_and_encode};
|
||||
use crate::swqos::jito_grpc::searcher::searcher_service_client::SearcherServiceClient;
|
||||
use reqwest::Client;
|
||||
use searcher_client::{get_searcher_client_no_auth, send_bundle_with_confirmation};
|
||||
use serde_json::json;
|
||||
use tonic::transport::Channel;
|
||||
use yellowstone_grpc_client::Interceptor;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
|
||||
use solana_sdk::signature::Signature;
|
||||
|
||||
use std::str::FromStr;
|
||||
use rustls::crypto::{ring::default_provider, CryptoProvider};
|
||||
|
||||
use tonic::{service::interceptor::InterceptedService, transport::Uri, Status};
|
||||
use std::time::Duration;
|
||||
use solana_transaction_status::UiTransactionEncoding;
|
||||
use tonic::transport::ClientTlsConfig;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use rand::{rng, seq::{IndexedRandom, IteratorRandom}};
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
|
||||
use crate::{common::SolanaRpcClient, constants::accounts::{JITO_TIP_ACCOUNTS, NEXTBLOCK_TIP_ACCOUNTS, ZEROSLOT_TIP_ACCOUNTS}};
|
||||
|
||||
pub mod common;
|
||||
pub mod searcher_client;
|
||||
pub mod api;
|
||||
pub mod jito_grpc;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref TIP_ACCOUNT_CACHE: RwLock<Vec<String>> = RwLock::new(Vec::new());
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ClientType {
|
||||
Jito,
|
||||
NextBlock,
|
||||
ZeroSlot,
|
||||
}
|
||||
|
||||
pub type FeeClient = dyn FeeClientTrait + Send + Sync + 'static;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait FeeClientTrait {
|
||||
async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature>;
|
||||
async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>>;
|
||||
async fn get_tip_account(&self) -> Result<String>;
|
||||
async fn get_client_type(&self) -> ClientType;
|
||||
}
|
||||
|
||||
pub struct JitoClient {
|
||||
pub rpc_client: Arc<SolanaRpcClient>,
|
||||
pub searcher_client: Arc<Mutex<SearcherServiceClient<Channel>>>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FeeClientTrait for JitoClient {
|
||||
async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
|
||||
self.send_bundle_with_confirmation(&vec![transaction.clone()]).await?.first().cloned().ok_or(anyhow!("Failed to send transaction"))
|
||||
}
|
||||
|
||||
async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
|
||||
self.send_bundle_with_confirmation(transactions).await
|
||||
}
|
||||
|
||||
async fn get_tip_account(&self) -> Result<String, anyhow::Error> {
|
||||
if let Some(acc) = JITO_TIP_ACCOUNTS.iter().choose(&mut rng()) {
|
||||
Ok(acc.to_string())
|
||||
} else {
|
||||
Err(anyhow!("no valid tip accounts found"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_client_type(&self) -> ClientType {
|
||||
ClientType::Jito
|
||||
}
|
||||
}
|
||||
|
||||
impl JitoClient {
|
||||
pub async fn new(rpc_url: String, block_engine_url: String) -> Result<Self> {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let searcher_client = get_searcher_client_no_auth(block_engine_url.as_str()).await?;
|
||||
Ok(Self { rpc_client: Arc::new(rpc_client), searcher_client: Arc::new(Mutex::new(searcher_client)) })
|
||||
}
|
||||
|
||||
pub async fn send_bundle_with_confirmation(
|
||||
&self,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
) -> Result<Vec<Signature>, anyhow::Error> {
|
||||
send_bundle_with_confirmation(self.rpc_client.clone(), &transactions, self.searcher_client.clone()).await
|
||||
}
|
||||
|
||||
pub async fn send_bundle_no_wait(
|
||||
&self,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
) -> Result<Vec<Signature>, anyhow::Error> {
|
||||
searcher_client::send_bundle_no_wait(&transactions, self.searcher_client.clone()).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MyInterceptor {
|
||||
auth_token: String,
|
||||
}
|
||||
|
||||
impl MyInterceptor {
|
||||
pub fn new(auth_token: String) -> Self {
|
||||
Self { auth_token }
|
||||
}
|
||||
}
|
||||
|
||||
impl Interceptor for MyInterceptor {
|
||||
fn call(&mut self, mut request: tonic::Request<()>) -> Result<tonic::Request<()>, Status> {
|
||||
request.metadata_mut().insert(
|
||||
"authorization",
|
||||
tonic::metadata::MetadataValue::from_str(&self.auth_token)
|
||||
.map_err(|_| Status::invalid_argument("Invalid auth token"))?
|
||||
);
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NextBlockClient {
|
||||
pub rpc_client: Arc<SolanaRpcClient>,
|
||||
pub client: ApiClient<InterceptedService<Channel, MyInterceptor>>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FeeClientTrait for NextBlockClient {
|
||||
async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
|
||||
self.send_transaction(transaction).await
|
||||
}
|
||||
|
||||
async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
|
||||
self.send_transactions(transactions).await
|
||||
}
|
||||
|
||||
async fn get_tip_account(&self) -> Result<String> {
|
||||
let tip_account = self.get_tip_account().await?;
|
||||
Ok(tip_account)
|
||||
}
|
||||
|
||||
async fn get_client_type(&self) -> ClientType {
|
||||
ClientType::NextBlock
|
||||
}
|
||||
}
|
||||
|
||||
impl NextBlockClient {
|
||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||
if CryptoProvider::get_default().is_none() {
|
||||
let _ = default_provider()
|
||||
.install_default()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e));
|
||||
}
|
||||
|
||||
let endpoint = endpoint.parse::<Uri>().unwrap();
|
||||
let tls = ClientTlsConfig::new().with_native_roots();
|
||||
let channel = Channel::builder(endpoint)
|
||||
.tls_config(tls).expect("Failed to create TLS config")
|
||||
.tcp_keepalive(Some(Duration::from_secs(60)))
|
||||
.http2_keep_alive_interval(Duration::from_secs(30))
|
||||
.keep_alive_while_idle(true)
|
||||
.timeout(Duration::from_secs(30))
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.connect_lazy();
|
||||
|
||||
let client = ApiClient::with_interceptor(channel, MyInterceptor::new(auth_token));
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
Self { rpc_client: Arc::new(rpc_client), client }
|
||||
}
|
||||
|
||||
pub async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
|
||||
let (content, signature) = serialize_smart_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
||||
|
||||
self.client.clone().post_submit_v2(api::PostSubmitRequest {
|
||||
transaction: Some(api::TransactionMessage {
|
||||
content,
|
||||
is_cleanup: false,
|
||||
}),
|
||||
skip_pre_flight: true,
|
||||
front_running_protection: Some(true),
|
||||
experimental_front_running_protection: Some(true),
|
||||
snipe_transaction: Some(true),
|
||||
}).await?;
|
||||
|
||||
let timeout: Duration = Duration::from_secs(10);
|
||||
let start_time: Instant = Instant::now();
|
||||
while Instant::now().duration_since(start_time) < timeout {
|
||||
match poll_transaction_confirmation(&self.rpc_client, signature).await {
|
||||
Ok(sig) => return Ok(sig),
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
pub async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
|
||||
let mut entries = Vec::new();
|
||||
let encoding = UiTransactionEncoding::Base64;
|
||||
|
||||
let mut signatures = Vec::new();
|
||||
for transaction in transactions {
|
||||
let (content, signature) = serialize_smart_transaction_and_encode(transaction, encoding).await?;
|
||||
entries.push(api::PostSubmitRequestEntry {
|
||||
transaction: Some(api::TransactionMessage {
|
||||
content,
|
||||
is_cleanup: false,
|
||||
}),
|
||||
skip_pre_flight: true,
|
||||
});
|
||||
signatures.push(signature);
|
||||
}
|
||||
|
||||
self.client.clone().post_submit_batch_v2(api::PostSubmitBatchRequest {
|
||||
entries,
|
||||
submit_strategy: api::SubmitStrategy::PSubmitAll as i32,
|
||||
use_bundle: Some(true),
|
||||
front_running_protection: Some(true),
|
||||
}).await?;
|
||||
|
||||
let timeout: Duration = Duration::from_secs(10);
|
||||
let start_time: Instant = Instant::now();
|
||||
while Instant::now().duration_since(start_time) < timeout {
|
||||
for signature in signatures.clone() {
|
||||
match poll_transaction_confirmation(&self.rpc_client, signature).await {
|
||||
Ok(sig) => signatures.push(sig),
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(signatures)
|
||||
}
|
||||
|
||||
async fn get_tip_account(&self) -> Result<String> {
|
||||
let tip_account = *NEXTBLOCK_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap();
|
||||
Ok(tip_account.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ZeroSlotClient {
|
||||
pub endpoint: String,
|
||||
pub auth_token: String,
|
||||
pub rpc_client: Arc<SolanaRpcClient>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FeeClientTrait for ZeroSlotClient {
|
||||
async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
|
||||
self.send_transaction(transaction).await
|
||||
}
|
||||
|
||||
async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
|
||||
self.send_transactions(transactions).await
|
||||
}
|
||||
|
||||
async fn get_tip_account(&self) -> Result<String> {
|
||||
let tip_account = self.get_tip_account().await?;
|
||||
Ok(tip_account)
|
||||
}
|
||||
|
||||
async fn get_client_type(&self) -> ClientType {
|
||||
ClientType::ZeroSlot
|
||||
}
|
||||
}
|
||||
|
||||
impl ZeroSlotClient {
|
||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token }
|
||||
}
|
||||
|
||||
pub async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
|
||||
let (content, signature) = serialize_smart_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
||||
|
||||
let client = Client::new();
|
||||
let request_body = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "sendTransaction",
|
||||
"params": [
|
||||
content,
|
||||
{
|
||||
"encoding": "base64",
|
||||
"skipPreflight": true,
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Send the request
|
||||
let response = client.post(format!("{}/?api-key={}", self.endpoint, self.auth_token))
|
||||
.json(&request_body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
// Parse the response
|
||||
let response_json: serde_json::Value = response.json().await?;
|
||||
if let Some(result) = response_json.get("result") {
|
||||
println!("Transaction sent successfully: {}", result);
|
||||
} else if let Some(error) = response_json.get("error") {
|
||||
eprintln!("Failed to send transaction: {}", error);
|
||||
}
|
||||
|
||||
let timeout: Duration = Duration::from_secs(10);
|
||||
let start_time: Instant = Instant::now();
|
||||
while Instant::now().duration_since(start_time) < timeout {
|
||||
match poll_transaction_confirmation(&self.rpc_client, signature).await {
|
||||
Ok(sig) => return Ok(sig),
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
pub async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
|
||||
let mut signatures = Vec::new();
|
||||
for transaction in transactions {
|
||||
let signature = self.send_transaction(transaction).await?;
|
||||
signatures.push(signature);
|
||||
}
|
||||
Ok(signatures)
|
||||
}
|
||||
|
||||
async fn get_tip_account(&self) -> Result<String> {
|
||||
let tip_account = *ZEROSLOT_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap();
|
||||
Ok(tip_account.to_string())
|
||||
}
|
||||
}
|
||||
Executable
+126
@@ -0,0 +1,126 @@
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::swqos::jito_grpc::{
|
||||
bundle::{
|
||||
Bundle, BundleResult,
|
||||
},
|
||||
convert::proto_packet_from_versioned_tx,
|
||||
searcher::{
|
||||
searcher_service_client::SearcherServiceClient, SendBundleRequest, SubscribeBundleResultsRequest,
|
||||
},
|
||||
};
|
||||
use solana_sdk::{
|
||||
signature::Signature,
|
||||
transaction::VersionedTransaction,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::Mutex;
|
||||
use tonic::{transport::{self, Channel, Endpoint}, Status};
|
||||
use yellowstone_grpc_client::ClientTlsConfig;
|
||||
|
||||
use crate::swqos::common::poll_transaction_confirmation;
|
||||
use crate::common::SolanaRpcClient;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BlockEngineConnectionError {
|
||||
#[error("transport error {0}")]
|
||||
TransportError(#[from] transport::Error),
|
||||
#[error("client error {0}")]
|
||||
ClientError(#[from] Status),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BundleRejectionError {
|
||||
#[error("bundle lost state auction, auction: {0}, tip {1} lamports")]
|
||||
StateAuctionBidRejected(String, u64),
|
||||
#[error("bundle won state auction but failed global auction, auction {0}, tip {1} lamports")]
|
||||
WinningBatchBidRejected(String, u64),
|
||||
#[error("bundle simulation failure on tx {0}, message: {1:?}")]
|
||||
SimulationFailure(String, Option<String>),
|
||||
#[error("internal error {0}")]
|
||||
InternalError(String),
|
||||
}
|
||||
|
||||
pub type BlockEngineConnectionResult<T> = Result<T, BlockEngineConnectionError>;
|
||||
|
||||
pub async fn get_searcher_client_no_auth(
|
||||
block_engine_url: &str,
|
||||
) -> BlockEngineConnectionResult<SearcherServiceClient<Channel>> {
|
||||
let searcher_channel = create_grpc_channel(block_engine_url).await?;
|
||||
let searcher_client = SearcherServiceClient::new(searcher_channel);
|
||||
Ok(searcher_client)
|
||||
}
|
||||
|
||||
pub async fn create_grpc_channel(url: &str) -> BlockEngineConnectionResult<Channel> {
|
||||
let mut endpoint = Endpoint::from_shared(url.to_string()).expect("invalid url");
|
||||
if url.starts_with("https") {
|
||||
endpoint = endpoint.tls_config(ClientTlsConfig::new().with_native_roots())?;
|
||||
}
|
||||
|
||||
endpoint = endpoint.tcp_nodelay(true);
|
||||
endpoint = endpoint.tcp_keepalive(Some(Duration::from_secs(10)));
|
||||
endpoint = endpoint.connect_timeout(Duration::from_secs(20));
|
||||
endpoint = endpoint.http2_keep_alive_interval(Duration::from_secs(10));
|
||||
|
||||
Ok(endpoint.connect().await?)
|
||||
}
|
||||
|
||||
pub async fn subscribe_bundle_results(
|
||||
searcher_client: Arc<Mutex<SearcherServiceClient<Channel>>>,
|
||||
request: impl tonic::IntoRequest<SubscribeBundleResultsRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<tonic::codec::Streaming<BundleResult>>,
|
||||
tonic::Status,
|
||||
> {
|
||||
let mut searcher = searcher_client.lock().await;
|
||||
searcher.subscribe_bundle_results(request).await
|
||||
}
|
||||
|
||||
pub async fn send_bundle_with_confirmation(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
searcher_client: Arc<Mutex<SearcherServiceClient<Channel>>>,
|
||||
) -> Result<Vec<Signature>, anyhow::Error> {
|
||||
let mut signatures = send_bundle_no_wait(transactions, searcher_client).await?;
|
||||
|
||||
let timeout: Duration = Duration::from_secs(10);
|
||||
let start_time: Instant = Instant::now();
|
||||
while Instant::now().duration_since(start_time) < timeout {
|
||||
for signature in signatures.clone() {
|
||||
match poll_transaction_confirmation(&rpc, signature).await {
|
||||
Ok(sig) => signatures.push(sig),
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(signatures)
|
||||
}
|
||||
|
||||
pub async fn send_bundle_no_wait(
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
searcher_client: Arc<Mutex<SearcherServiceClient<Channel>>>,
|
||||
) -> Result<Vec<Signature>, anyhow::Error> {
|
||||
let mut packets = vec![];
|
||||
let mut signatures = vec![];
|
||||
for transaction in transactions {
|
||||
let packet = proto_packet_from_versioned_tx(transaction);
|
||||
packets.push(packet);
|
||||
signatures.push(transaction.signatures[0]);
|
||||
}
|
||||
|
||||
let mut searcher = searcher_client.lock().await;
|
||||
searcher
|
||||
.send_bundle(SendBundleRequest {
|
||||
bundle: Some(Bundle {
|
||||
header: None,
|
||||
packets,
|
||||
}),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(signatures)
|
||||
}
|
||||
Reference in New Issue
Block a user