feat: Add address lookup, caching system and Jito GRPC integration

- Implement address lookup tables and multi-level caching

- Add GRPC stream processing and YellowStone connections

- Extend PumpFun functionality including token creation

- Integrate Jito GRPC services (auth, block engine, relayer)

- Optimize log processing and event system
This commit is contained in:
sgxiang
2025-05-29 18:53:58 +08:00
parent f261b8a66d
commit bfe76fcddc
37 changed files with 4168 additions and 694 deletions
+4 -5
View File
@@ -14,10 +14,8 @@ 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(5);
// 5 second retry interval
let interval: Duration = Duration::from_millis(300);
let interval: Duration = Duration::from_millis(1000);
let start: Instant = Instant::now();
loop {
@@ -102,14 +100,15 @@ pub async fn serialize_and_encode(
pub async fn serialize_transaction_and_encode(
transaction: &impl SerializableTransaction,
encoding: UiTransactionEncoding,
) -> Result<String> {
) -> 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)
Ok((serialized, *signature))
}
pub async fn serialize_smart_transaction_and_encode(
+260
View File
@@ -0,0 +1,260 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GenerateAuthChallengeRequest {
/// / Role the client is attempting to generate tokens for.
#[prost(enumeration = "Role", tag = "1")]
pub role: i32,
/// / Client's 32 byte pubkey.
#[prost(bytes = "vec", tag = "2")]
pub pubkey: ::prost::alloc::vec::Vec<u8>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GenerateAuthChallengeResponse {
#[prost(string, tag = "1")]
pub challenge: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GenerateAuthTokensRequest {
/// / The pre-signed challenge.
#[prost(string, tag = "1")]
pub challenge: ::prost::alloc::string::String,
/// / The signing keypair's corresponding 32 byte pubkey.
#[prost(bytes = "vec", tag = "2")]
pub client_pubkey: ::prost::alloc::vec::Vec<u8>,
/// / The 64 byte signature of the challenge signed by the client's private key. The private key must correspond to
/// the pubkey passed in the \[GenerateAuthChallenge\] method. The client is expected to sign the challenge token
/// prepended with their pubkey. For example sign(pubkey, challenge).
#[prost(bytes = "vec", tag = "3")]
pub signed_challenge: ::prost::alloc::vec::Vec<u8>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Token {
/// / The token.
#[prost(string, tag = "1")]
pub value: ::prost::alloc::string::String,
/// / When the token will expire.
#[prost(message, optional, tag = "2")]
pub expires_at_utc: ::core::option::Option<::prost_types::Timestamp>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GenerateAuthTokensResponse {
/// / The token granting access to resources.
#[prost(message, optional, tag = "1")]
pub access_token: ::core::option::Option<Token>,
/// / The token used to refresh the access_token. This has a longer TTL than the access_token.
#[prost(message, optional, tag = "2")]
pub refresh_token: ::core::option::Option<Token>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct RefreshAccessTokenRequest {
/// / Non-expired refresh token obtained from the \[GenerateAuthTokens\] method.
#[prost(string, tag = "1")]
pub refresh_token: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct RefreshAccessTokenResponse {
/// / Fresh access_token.
#[prost(message, optional, tag = "1")]
pub access_token: ::core::option::Option<Token>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum Role {
Relayer = 0,
Searcher = 1,
Validator = 2,
ShredstreamSubscriber = 3,
}
impl Role {
/// 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::Relayer => "RELAYER",
Self::Searcher => "SEARCHER",
Self::Validator => "VALIDATOR",
Self::ShredstreamSubscriber => "SHREDSTREAM_SUBSCRIBER",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"RELAYER" => Some(Self::Relayer),
"SEARCHER" => Some(Self::Searcher),
"VALIDATOR" => Some(Self::Validator),
"SHREDSTREAM_SUBSCRIBER" => Some(Self::ShredstreamSubscriber),
_ => None,
}
}
}
/// Generated client implementations.
pub mod auth_service_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
/// / This service is responsible for issuing auth tokens to clients for API access.
#[derive(Debug, Clone)]
pub struct AuthServiceClient<T> {
inner: tonic::client::Grpc<T>,
}
impl AuthServiceClient<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> AuthServiceClient<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,
) -> AuthServiceClient<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,
{
AuthServiceClient::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
}
/// / Returns a challenge, client is expected to sign this challenge with an appropriate keypair in order to obtain access tokens.
pub async fn generate_auth_challenge(
&mut self,
request: impl tonic::IntoRequest<super::GenerateAuthChallengeRequest>,
) -> std::result::Result<
tonic::Response<super::GenerateAuthChallengeResponse>,
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(
"/auth.AuthService/GenerateAuthChallenge",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("auth.AuthService", "GenerateAuthChallenge"));
self.inner.unary(req, path, codec).await
}
/// / Provides the client with the initial pair of auth tokens for API access.
pub async fn generate_auth_tokens(
&mut self,
request: impl tonic::IntoRequest<super::GenerateAuthTokensRequest>,
) -> std::result::Result<
tonic::Response<super::GenerateAuthTokensResponse>,
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(
"/auth.AuthService/GenerateAuthTokens",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("auth.AuthService", "GenerateAuthTokens"));
self.inner.unary(req, path, codec).await
}
/// / Call this method with a non-expired refresh token to obtain a new access token.
pub async fn refresh_access_token(
&mut self,
request: impl tonic::IntoRequest<super::RefreshAccessTokenRequest>,
) -> std::result::Result<
tonic::Response<super::RefreshAccessTokenResponse>,
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(
"/auth.AuthService/RefreshAccessToken",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("auth.AuthService", "RefreshAccessToken"));
self.inner.unary(req, path, codec).await
}
}
}
+19
View File
@@ -0,0 +1,19 @@
// This file is @generated by prost-build.
/// Condensed block helpful for getting data around efficiently internal to our system.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct CondensedBlock {
#[prost(message, optional, tag = "1")]
pub header: ::core::option::Option<super::shared::Header>,
#[prost(string, tag = "2")]
pub previous_blockhash: ::prost::alloc::string::String,
#[prost(string, tag = "3")]
pub blockhash: ::prost::alloc::string::String,
#[prost(uint64, tag = "4")]
pub parent_slot: u64,
#[prost(bytes = "vec", repeated, tag = "5")]
pub versioned_transactions: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec<u8>>,
#[prost(uint64, tag = "6")]
pub slot: u64,
#[prost(string, tag = "7")]
pub commitment: ::prost::alloc::string::String,
}
+462
View File
@@ -0,0 +1,462 @@
// This file is @generated by prost-build.
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct SubscribePacketsRequest {}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SubscribePacketsResponse {
#[prost(message, optional, tag = "1")]
pub header: ::core::option::Option<super::shared::Header>,
#[prost(message, optional, tag = "2")]
pub batch: ::core::option::Option<super::packet::PacketBatch>,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct SubscribeBundlesRequest {}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SubscribeBundlesResponse {
#[prost(message, repeated, tag = "1")]
pub bundles: ::prost::alloc::vec::Vec<super::bundle::BundleUuid>,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct BlockBuilderFeeInfoRequest {}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct BlockBuilderFeeInfoResponse {
#[prost(string, tag = "1")]
pub pubkey: ::prost::alloc::string::String,
/// commission (0-100)
#[prost(uint64, tag = "2")]
pub commission: u64,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct AccountsOfInterest {
/// use * for all accounts
#[prost(string, repeated, tag = "1")]
pub accounts: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct AccountsOfInterestRequest {}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct AccountsOfInterestUpdate {
#[prost(string, repeated, tag = "1")]
pub accounts: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProgramsOfInterestRequest {}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProgramsOfInterestUpdate {
#[prost(string, repeated, tag = "1")]
pub programs: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
}
/// A series of packets with an expiration attached to them.
/// The header contains a timestamp for when this packet was generated.
/// The expiry is how long the packet batches have before they expire and are forwarded to the validator.
/// This provides a more censorship resistant method to MEV than block engines receiving packets directly.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ExpiringPacketBatch {
#[prost(message, optional, tag = "1")]
pub header: ::core::option::Option<super::shared::Header>,
#[prost(message, optional, tag = "2")]
pub batch: ::core::option::Option<super::packet::PacketBatch>,
#[prost(uint32, tag = "3")]
pub expiry_ms: u32,
}
/// Packets and heartbeats are sent over the same stream.
/// ExpiringPacketBatches have an expiration attached to them so the block engine can track
/// how long it has until the relayer forwards the packets to the validator.
/// Heartbeats contain a timestamp from the system and is used as a simple and naive time-sync mechanism
/// so the block engine has some idea on how far their clocks are apart.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PacketBatchUpdate {
#[prost(oneof = "packet_batch_update::Msg", tags = "1, 2")]
pub msg: ::core::option::Option<packet_batch_update::Msg>,
}
/// Nested message and enum types in `PacketBatchUpdate`.
pub mod packet_batch_update {
#[derive(Clone, PartialEq, ::prost::Oneof)]
pub enum Msg {
#[prost(message, tag = "1")]
Batches(super::ExpiringPacketBatch),
#[prost(message, tag = "2")]
Heartbeat(super::super::shared::Heartbeat),
}
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct StartExpiringPacketStreamResponse {
#[prost(message, optional, tag = "1")]
pub heartbeat: ::core::option::Option<super::shared::Heartbeat>,
}
/// Generated client implementations.
pub mod block_engine_validator_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
/// / Validators can connect to Block Engines to receive packets and bundles.
#[derive(Debug, Clone)]
pub struct BlockEngineValidatorClient<T> {
inner: tonic::client::Grpc<T>,
}
impl BlockEngineValidatorClient<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> BlockEngineValidatorClient<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,
) -> BlockEngineValidatorClient<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,
{
BlockEngineValidatorClient::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
}
/// / Validators can subscribe to the block engine to receive a stream of packets
pub async fn subscribe_packets(
&mut self,
request: impl tonic::IntoRequest<super::SubscribePacketsRequest>,
) -> std::result::Result<
tonic::Response<tonic::codec::Streaming<super::SubscribePacketsResponse>>,
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(
"/block_engine.BlockEngineValidator/SubscribePackets",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"block_engine.BlockEngineValidator",
"SubscribePackets",
),
);
self.inner.server_streaming(req, path, codec).await
}
/// / Validators can subscribe to the block engine to receive a stream of simulated and profitable bundles
pub async fn subscribe_bundles(
&mut self,
request: impl tonic::IntoRequest<super::SubscribeBundlesRequest>,
) -> std::result::Result<
tonic::Response<tonic::codec::Streaming<super::SubscribeBundlesResponse>>,
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(
"/block_engine.BlockEngineValidator/SubscribeBundles",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"block_engine.BlockEngineValidator",
"SubscribeBundles",
),
);
self.inner.server_streaming(req, path, codec).await
}
/// Block builders can optionally collect fees. This returns fee information if a block builder wants to
/// collect one.
pub async fn get_block_builder_fee_info(
&mut self,
request: impl tonic::IntoRequest<super::BlockBuilderFeeInfoRequest>,
) -> std::result::Result<
tonic::Response<super::BlockBuilderFeeInfoResponse>,
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(
"/block_engine.BlockEngineValidator/GetBlockBuilderFeeInfo",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"block_engine.BlockEngineValidator",
"GetBlockBuilderFeeInfo",
),
);
self.inner.unary(req, path, codec).await
}
}
}
/// Generated client implementations.
pub mod block_engine_relayer_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
/// / Relayers can forward packets to Block Engines.
/// / Block Engines provide an AccountsOfInterest field to only send transactions that are of interest.
#[derive(Debug, Clone)]
pub struct BlockEngineRelayerClient<T> {
inner: tonic::client::Grpc<T>,
}
impl BlockEngineRelayerClient<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> BlockEngineRelayerClient<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,
) -> BlockEngineRelayerClient<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,
{
BlockEngineRelayerClient::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
}
/// / The block engine feeds accounts of interest (AOI) updates to the relayer periodically.
/// / For all transactions the relayer receives, it forwards transactions to the block engine which write-lock
/// / any of the accounts in the AOI.
pub async fn subscribe_accounts_of_interest(
&mut self,
request: impl tonic::IntoRequest<super::AccountsOfInterestRequest>,
) -> std::result::Result<
tonic::Response<tonic::codec::Streaming<super::AccountsOfInterestUpdate>>,
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(
"/block_engine.BlockEngineRelayer/SubscribeAccountsOfInterest",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"block_engine.BlockEngineRelayer",
"SubscribeAccountsOfInterest",
),
);
self.inner.server_streaming(req, path, codec).await
}
pub async fn subscribe_programs_of_interest(
&mut self,
request: impl tonic::IntoRequest<super::ProgramsOfInterestRequest>,
) -> std::result::Result<
tonic::Response<tonic::codec::Streaming<super::ProgramsOfInterestUpdate>>,
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(
"/block_engine.BlockEngineRelayer/SubscribeProgramsOfInterest",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"block_engine.BlockEngineRelayer",
"SubscribeProgramsOfInterest",
),
);
self.inner.server_streaming(req, path, codec).await
}
/// Validators can subscribe to packets from the relayer and receive a multiplexed signal that contains a mixture
/// of packets and heartbeats.
/// NOTE: This is a bi-directional stream due to a bug with how Envoy handles half closed client-side streams.
/// The issue is being tracked here: https://github.com/envoyproxy/envoy/issues/22748. In the meantime, the
/// server will stream heartbeats to clients at some reasonable cadence.
pub async fn start_expiring_packet_stream(
&mut self,
request: impl tonic::IntoStreamingRequest<Message = super::PacketBatchUpdate>,
) -> std::result::Result<
tonic::Response<
tonic::codec::Streaming<super::StartExpiringPacketStreamResponse>,
>,
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(
"/block_engine.BlockEngineRelayer/StartExpiringPacketStream",
);
let mut req = request.into_streaming_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"block_engine.BlockEngineRelayer",
"StartExpiringPacketStream",
),
);
self.inner.streaming(req, path, codec).await
}
}
}
+6 -4
View File
@@ -11,11 +11,13 @@ use solana_sdk::{
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::{
packet::{
Meta as ProtoMeta, Packet as ProtoPacket, PacketBatch as ProtoPacketBatch,
PacketFlags as ProtoPacketFlags,
},
shared::Socket,
};
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
+7 -1
View File
@@ -1,5 +1,11 @@
pub mod auth;
pub mod block;
pub mod block_engine;
pub mod bundle;
pub mod packet;
pub mod relayer;
pub mod searcher;
pub mod shared;
pub mod convert;
pub mod shredstream;
pub mod trace_shred;
pub mod convert;
+178
View File
@@ -0,0 +1,178 @@
// This file is @generated by prost-build.
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct GetTpuConfigsRequest {}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GetTpuConfigsResponse {
#[prost(message, optional, tag = "1")]
pub tpu: ::core::option::Option<super::shared::Socket>,
#[prost(message, optional, tag = "2")]
pub tpu_forward: ::core::option::Option<super::shared::Socket>,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct SubscribePacketsRequest {}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SubscribePacketsResponse {
#[prost(message, optional, tag = "1")]
pub header: ::core::option::Option<super::shared::Header>,
#[prost(oneof = "subscribe_packets_response::Msg", tags = "2, 3")]
pub msg: ::core::option::Option<subscribe_packets_response::Msg>,
}
/// Nested message and enum types in `SubscribePacketsResponse`.
pub mod subscribe_packets_response {
#[derive(Clone, PartialEq, ::prost::Oneof)]
pub enum Msg {
#[prost(message, tag = "2")]
Heartbeat(super::super::shared::Heartbeat),
#[prost(message, tag = "3")]
Batch(super::super::packet::PacketBatch),
}
}
/// Generated client implementations.
pub mod relayer_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
/// / Relayers offer a TPU and TPU forward proxy for Solana validators.
/// / Validators can connect and fetch the TPU configuration for the relayer and start to advertise the
/// / relayer's information in gossip.
/// / They can also subscribe to packets which arrived on the TPU ports at the relayer
#[derive(Debug, Clone)]
pub struct RelayerClient<T> {
inner: tonic::client::Grpc<T>,
}
impl RelayerClient<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> RelayerClient<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,
) -> RelayerClient<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,
{
RelayerClient::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
}
/// The relayer has TPU and TPU forward sockets that validators can leverage.
/// A validator can fetch this config and change its TPU and TPU forward port in gossip.
pub async fn get_tpu_configs(
&mut self,
request: impl tonic::IntoRequest<super::GetTpuConfigsRequest>,
) -> std::result::Result<
tonic::Response<super::GetTpuConfigsResponse>,
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(
"/relayer.Relayer/GetTpuConfigs",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("relayer.Relayer", "GetTpuConfigs"));
self.inner.unary(req, path, codec).await
}
/// Validators can subscribe to packets from the relayer and receive a multiplexed signal that contains a mixture
/// of packets and heartbeats
pub async fn subscribe_packets(
&mut self,
request: impl tonic::IntoRequest<super::SubscribePacketsRequest>,
) -> std::result::Result<
tonic::Response<tonic::codec::Streaming<super::SubscribePacketsResponse>>,
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(
"/relayer.Relayer/SubscribePackets",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("relayer.Relayer", "SubscribePackets"));
self.inner.server_streaming(req, path, codec).await
}
}
}
+279
View File
@@ -0,0 +1,279 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Heartbeat {
/// don't trust IP:PORT from tcp header since it can be tampered over the wire
/// `socket.ip` must match incoming packet's ip. this prevents spamming an unwitting destination
#[prost(message, optional, tag = "1")]
pub socket: ::core::option::Option<super::shared::Socket>,
/// regions for shredstream proxy to receive shreds from
/// list of valid regions: <https://docs.jito.wtf/lowlatencytxnsend/#api>
#[prost(string, repeated, tag = "2")]
pub regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct HeartbeatResponse {
/// client must respond within `ttl_ms` to keep stream alive
#[prost(uint32, tag = "1")]
pub ttl_ms: u32,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TraceShred {
/// source region, one of: <https://docs.jito.wtf/lowlatencytxnsend/#api>
#[prost(string, tag = "1")]
pub region: ::prost::alloc::string::String,
/// timestamp of creation
#[prost(message, optional, tag = "2")]
pub created_at: ::core::option::Option<::prost_types::Timestamp>,
/// monotonically increases, resets upon service restart
#[prost(uint32, tag = "3")]
pub seq_num: u32,
}
/// tbd: we may want to add filters here
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct SubscribeEntriesRequest {}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Entry {
/// the slot that the entry is from
#[prost(uint64, tag = "1")]
pub slot: u64,
/// Serialized bytes of Vec<Entry>: <https://docs.rs/solana-entry/latest/solana_entry/entry/struct.Entry.html>
#[prost(bytes = "vec", tag = "2")]
pub entries: ::prost::alloc::vec::Vec<u8>,
}
/// Generated client implementations.
pub mod shredstream_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 ShredstreamClient<T> {
inner: tonic::client::Grpc<T>,
}
impl ShredstreamClient<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> ShredstreamClient<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,
) -> ShredstreamClient<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,
{
ShredstreamClient::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
}
/// RPC endpoint to send heartbeats to keep shreds flowing
pub async fn send_heartbeat(
&mut self,
request: impl tonic::IntoRequest<super::Heartbeat>,
) -> std::result::Result<
tonic::Response<super::HeartbeatResponse>,
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(
"/shredstream.Shredstream/SendHeartbeat",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("shredstream.Shredstream", "SendHeartbeat"));
self.inner.unary(req, path, codec).await
}
}
}
/// Generated client implementations.
pub mod shredstream_proxy_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 ShredstreamProxyClient<T> {
inner: tonic::client::Grpc<T>,
}
impl ShredstreamProxyClient<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> ShredstreamProxyClient<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,
) -> ShredstreamProxyClient<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,
{
ShredstreamProxyClient::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 subscribe_entries(
&mut self,
request: impl tonic::IntoRequest<super::SubscribeEntriesRequest>,
) -> std::result::Result<
tonic::Response<tonic::codec::Streaming<super::Entry>>,
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(
"/shredstream.ShredstreamProxy/SubscribeEntries",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new("shredstream.ShredstreamProxy", "SubscribeEntries"),
);
self.inner.server_streaming(req, path, codec).await
}
}
}
+13
View File
@@ -0,0 +1,13 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TraceShred {
/// source region, one of: <https://jito-labs.gitbook.io/mev/systems/connecting/mainnet>
#[prost(string, tag = "1")]
pub region: ::prost::alloc::string::String,
/// timestamp of creation
#[prost(message, optional, tag = "2")]
pub created_at: ::core::option::Option<::prost_types::Timestamp>,
/// monotonically increases, resets upon service restart
#[prost(uint32, tag = "3")]
pub seq_num: u32,
}
+264 -79
View File
@@ -1,5 +1,6 @@
use api::api_client::ApiClient;
use common::{poll_transaction_confirmation, serialize_smart_transaction_and_encode};
use common::{poll_transaction_confirmation, serialize_smart_transaction_and_encode, serialize_transaction_and_encode};
use solana_client::rpc_config::RpcSendTransactionConfig;
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};
@@ -9,7 +10,7 @@ use yellowstone_grpc_client::Interceptor;
use std::{sync::Arc, time::Instant};
use tokio::sync::{Mutex, RwLock};
use solana_sdk::signature::Signature;
use solana_sdk::{commitment_config::CommitmentLevel, signature::Signature};
use std::str::FromStr;
use rustls::crypto::{ring::default_provider, CryptoProvider};
@@ -23,11 +24,11 @@ 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}};
use crate::{common::SolanaRpcClient, constants::accounts::{JITO_TIP_ACCOUNTS, NEXTBLOCK_TIP_ACCOUNTS, ZEROSLOT_TIP_ACCOUNTS, NOZOMI_TIP_ACCOUNTS}};
pub mod api;
pub mod common;
pub mod searcher_client;
pub mod api;
pub mod jito_grpc;
lazy_static::lazy_static! {
@@ -35,20 +36,92 @@ lazy_static::lazy_static! {
}
#[derive(Debug, Clone, Copy)]
pub enum TradeType {
Create,
CreateAndBuy,
Buy,
Sell,
}
impl std::fmt::Display for TradeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
TradeType::Create => "创建",
TradeType::CreateAndBuy => "创建并买入",
TradeType::Buy => "买入",
TradeType::Sell => "卖出",
};
write!(f, "{}", s)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ClientType {
Jito,
NextBlock,
ZeroSlot,
Nozomi,
Rpc,
}
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;
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result<Signature>;
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>>;
fn get_tip_account(&self) -> Result<String>;
fn get_client_type(&self) -> ClientType;
}
#[derive(Clone)]
pub struct SolRpcClient {
pub rpc_client: Arc<SolanaRpcClient>,
}
#[async_trait::async_trait]
impl FeeClientTrait for SolRpcClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result<Signature> {
let signature = self.rpc_client.send_transaction_with_config(transaction, RpcSendTransactionConfig{
skip_preflight: true,
preflight_commitment: Some(CommitmentLevel::Processed),
encoding: Some(UiTransactionEncoding::Base64),
max_retries: Some(3),
min_context_slot: Some(0),
}).await?;
let start_time = Instant::now();
match poll_transaction_confirmation(&self.rpc_client, signature).await {
Ok(_) => (),
Err(_) => (),
}
println!(" rpc{}确认: {:?}", trade_type, start_time.elapsed());
Ok(signature)
}
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>> {
let mut signatures = Vec::new();
for transaction in transactions {
let signature = self.send_transaction(trade_type, transaction).await?;
signatures.push(signature);
}
Ok(signatures)
}
fn get_tip_account(&self) -> Result<String> {
Ok("".to_string())
}
fn get_client_type(&self) -> ClientType {
ClientType::Rpc
}
}
impl SolRpcClient {
pub fn new(rpc_client: Arc<SolanaRpcClient>) -> Self {
Self { rpc_client }
}
}
pub struct JitoClient {
@@ -58,15 +131,15 @@ pub struct JitoClient {
#[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_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
self.send_bundle_with_confirmation(trade_type, &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 send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
self.send_bundle_with_confirmation(trade_type, transactions).await
}
async fn get_tip_account(&self) -> Result<String, anyhow::Error> {
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 {
@@ -74,7 +147,7 @@ impl FeeClientTrait for JitoClient {
}
}
async fn get_client_type(&self) -> ClientType {
fn get_client_type(&self) -> ClientType {
ClientType::Jito
}
}
@@ -88,9 +161,10 @@ impl JitoClient {
pub async fn send_bundle_with_confirmation(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
) -> Result<Vec<Signature>, anyhow::Error> {
send_bundle_with_confirmation(self.rpc_client.clone(), &transactions, self.searcher_client.clone()).await
send_bundle_with_confirmation(self.rpc_client.clone(), trade_type, &transactions, self.searcher_client.clone()).await
}
pub async fn send_bundle_no_wait(
@@ -131,20 +205,20 @@ pub struct NextBlockClient {
#[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_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
self.send_transaction(trade_type, transaction).await
}
async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
self.send_transactions(transactions).await
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
self.send_transactions(trade_type, transactions).await
}
async fn get_tip_account(&self) -> Result<String> {
let tip_account = self.get_tip_account().await?;
Ok(tip_account)
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())
}
async fn get_client_type(&self) -> ClientType {
fn get_client_type(&self) -> ClientType {
ClientType::NextBlock
}
}
@@ -173,7 +247,8 @@ impl NextBlockClient {
Self { rpc_client: Arc::new(rpc_client), client }
}
pub async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
let start_time = Instant::now();
let (content, signature) = serialize_smart_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
self.client.clone().post_submit_v2(api::PostSubmitRequest {
@@ -187,19 +262,23 @@ impl NextBlockClient {
snipe_transaction: Some(true),
}).await?;
let timeout: Duration = Duration::from_secs(10);
println!(" nextblock{}提交: {:?}", trade_type, start_time.elapsed());
let start_time: Instant = Instant::now();
let timeout: Duration = Duration::from_secs(10);
while Instant::now().duration_since(start_time) < timeout {
match poll_transaction_confirmation(&self.rpc_client, signature).await {
Ok(sig) => return Ok(sig),
Ok(_) => break,
Err(_) => continue,
}
}
println!(" nextblock{}确认: {:?}", trade_type, start_time.elapsed());
Ok(signature)
}
pub async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
let mut entries = Vec::new();
let encoding = UiTransactionEncoding::Base64;
@@ -223,24 +302,18 @@ impl NextBlockClient {
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,
}
for signature in signatures.clone() {
match poll_transaction_confirmation(&self.rpc_client, signature).await {
Ok(_) => continue,
Err(_) => continue,
}
}
println!(" nextblock{}确认: {:?}", trade_type, start_time.elapsed());
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)]
@@ -248,24 +321,25 @@ pub struct ZeroSlotClient {
pub endpoint: String,
pub auth_token: String,
pub rpc_client: Arc<SolanaRpcClient>,
pub http_client: Client,
}
#[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_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
self.send_transaction(trade_type, transaction).await
}
async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
self.send_transactions(transactions).await
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
self.send_transactions(trade_type, transactions).await
}
async fn get_tip_account(&self) -> Result<String> {
let tip_account = self.get_tip_account().await?;
Ok(tip_account)
fn get_tip_account(&self) -> Result<String> {
let tip_account = *ZEROSLOT_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| ZEROSLOT_TIP_ACCOUNTS.first()).unwrap();
Ok(tip_account.to_string())
}
async fn get_client_type(&self) -> ClientType {
fn get_client_type(&self) -> ClientType {
ClientType::ZeroSlot
}
}
@@ -273,63 +347,174 @@ impl FeeClientTrait for ZeroSlotClient {
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 }
let http_client = Client::builder()
.pool_idle_timeout(Duration::from_secs(60))
.pool_max_idle_per_host(64)
.tcp_keepalive(Some(Duration::from_secs(1200)))
.http2_keep_alive_interval(Duration::from_secs(15))
.timeout(Duration::from_secs(10))
.connect_timeout(Duration::from_secs(5))
.build()
.unwrap();
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_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?;
let client = Client::new();
let request_body = json!({
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
println!(" 交易编码base64: {:?}", start_time.elapsed());
let request_body = serde_json::to_string(&json!({
"jsonrpc": "2.0",
"id": 1,
"method": "sendTransaction",
"params": [
content,
{
"encoding": "base64",
"skipPreflight": true,
}
{ "encoding": "base64", "skipPreflight": true }
]
});
}))?;
// Send the request
let response = client.post(format!("{}/?api-key={}", self.endpoint, self.auth_token))
.json(&request_body)
let mut url = String::with_capacity(self.endpoint.len() + self.auth_token.len() + 20);
url.push_str(&self.endpoint);
url.push_str("/?api-key=");
url.push_str(&self.auth_token);
// 4. 直接使用 `text().await?`,避免 `json().await?` 的异步 JSON 解析
let response_text = self.http_client.post(&url)
.body(request_body) // 直接传字符串,避免 `json()` 开销
.header("Content-Type", "application/json") // 显式指定 JSON 头
.send()
.await?
.text()
.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,
// 5. 用 `serde_json::from_str()` 解析 JSON,减少 `.json().await?` 额外等待
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() {
println!(" 0slot{}提交: {:?}", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" 0slot{}提交失败: {:?}", trade_type, _error);
}
}
let start_time: Instant = Instant::now();
match poll_transaction_confirmation(&self.rpc_client, signature).await {
Ok(_) => (),
Err(_) => (),
}
println!(" 0slot{}确认: {:?}", trade_type, start_time.elapsed());
Ok(signature)
}
pub async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
let mut signatures = Vec::new();
for transaction in transactions {
let signature = self.send_transaction(transaction).await?;
let signature = self.send_transaction(trade_type, 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();
#[derive(Clone)]
pub struct NozomiClient {
pub rpc_client: Arc<SolanaRpcClient>,
pub endpoint: String,
pub auth_token: String,
pub http_client: Client,
}
#[async_trait::async_trait]
impl FeeClientTrait for NozomiClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
self.send_transaction(trade_type, transaction).await
}
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
self.send_transactions(trade_type, transactions).await
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *NOZOMI_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NOZOMI_TIP_ACCOUNTS.first()).unwrap();
Ok(tip_account.to_string())
}
fn get_client_type(&self) -> ClientType {
ClientType::Nozomi
}
}
impl NozomiClient {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = Client::builder()
.pool_idle_timeout(Duration::from_secs(60))
.pool_max_idle_per_host(64)
.tcp_keepalive(Some(Duration::from_secs(1200)))
.http2_keep_alive_interval(Duration::from_secs(15))
.timeout(Duration::from_secs(10))
.connect_timeout(Duration::from_secs(5))
.build()
.unwrap();
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
println!(" 交易编码base64: {:?}", start_time.elapsed());
// 按照 Nozomi 文档要求构建请求体
let request_body = serde_json::to_string(&json!({
"jsonrpc": "2.0",
"id": 1,
"method": "sendTransaction",
"params": [
content,
{ "encoding": "base64" }
]
}))?;
let mut url = String::with_capacity(self.endpoint.len() + self.auth_token.len() + 20);
url.push_str(&self.endpoint);
url.push_str("/?c=");
url.push_str(&self.auth_token);
let response_text = self.http_client.post(&url)
.body(request_body)
.header("Content-Type", "application/json")
.send()
.await?
.text()
.await?;
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() {
println!(" nozomi{}提交: {:?}", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
// eprintln!("nozomi交易提交失败: {:?}", _error);
}
}
let start_time: Instant = Instant::now();
match poll_transaction_confirmation(&self.rpc_client, signature).await {
Ok(_) => (),
Err(_) => (),
}
println!(" nozomi{}确认: {:?}", trade_type, start_time.elapsed());
Ok(signature)
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
let mut signatures = Vec::new();
for transaction in transactions {
let signature = self.send_transaction(trade_type, transaction).await?;
signatures.push(signature);
}
Ok(signatures)
}
}
+15 -9
View File
@@ -18,12 +18,16 @@ use solana_sdk::{
};
use thiserror::Error;
use tokio::sync::Mutex;
use tonic::{transport::{self, Channel, Endpoint}, Status};
use tonic::{
transport::{self, Channel, Endpoint}, Status
};
use yellowstone_grpc_client::ClientTlsConfig;
use crate::swqos::common::poll_transaction_confirmation;
use crate::common::SolanaRpcClient;
use super::TradeType;
#[derive(Debug, Error)]
pub enum BlockEngineConnectionError {
#[error("transport error {0}")]
@@ -81,21 +85,23 @@ pub async fn subscribe_bundle_results(
pub async fn send_bundle_with_confirmation(
rpc: Arc<SolanaRpcClient>,
trade_type: TradeType,
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 start_time = Instant::now();
let signatures = send_bundle_no_wait(transactions, searcher_client).await?;
println!(" Jito{}提交: {:?}", trade_type, start_time.elapsed());
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,
}
for signature in signatures.clone() {
match poll_transaction_confirmation(&rpc, signature).await {
Ok(_) => continue,
Err(_) => continue,
}
}
println!(" Jito{}确认: {:?}", trade_type, start_time.elapsed());
Ok(signatures)
}
+167
View File
@@ -0,0 +1,167 @@
use std::{
sync::{Arc, RwLock},
time::{Duration, SystemTime},
};
use jito_protos::auth::{
auth_service_client::AuthServiceClient, GenerateAuthChallengeRequest,
GenerateAuthTokensRequest, RefreshAccessTokenRequest, Role, Token,
};
use prost_types::Timestamp;
use solana_metrics::datapoint_info;
use solana_sdk::signature::{Keypair, Signer};
use tokio::{task::JoinHandle, time::sleep};
use tonic::{service::Interceptor, transport::Channel, Request, Status};
use super::searcher_client::BlockEngineConnectionResult;
const AUTHORIZATION_HEADER: &str = "authorization";
const BEARER: &str = "Bearer ";
/// Adds the token to each requests' authorization header.
/// Manages refreshing the token in a separate thread.
#[derive(Clone)]
pub struct ClientInterceptor {
/// The token added to each request header.
bearer_token: Arc<RwLock<String>>,
}
impl ClientInterceptor {
pub async fn new(
mut auth_service_client: AuthServiceClient<Channel>,
keypair: &Arc<Keypair>,
role: Role,
) -> BlockEngineConnectionResult<Self> {
let (access_token, refresh_token) =
Self::auth(&mut auth_service_client, keypair, role).await?;
let bearer_token = Arc::new(RwLock::new(access_token.value.clone()));
let _refresh_token_thread = Self::spawn_token_refresh_thread(
auth_service_client,
bearer_token.clone(),
refresh_token,
access_token.expires_at_utc.unwrap(),
keypair.clone(),
role,
);
Ok(Self { bearer_token })
}
async fn auth(
auth_service_client: &mut AuthServiceClient<Channel>,
keypair: &Keypair,
role: Role,
) -> BlockEngineConnectionResult<(Token, Token)> {
let challenge_resp = auth_service_client
.generate_auth_challenge(GenerateAuthChallengeRequest {
role: role as i32,
pubkey: keypair.pubkey().as_ref().to_vec(),
})
.await?
.into_inner();
let challenge = format!("{}-{}", keypair.pubkey(), challenge_resp.challenge);
let signed_challenge = keypair.sign_message(challenge.as_bytes()).as_ref().to_vec();
let tokens = auth_service_client
.generate_auth_tokens(GenerateAuthTokensRequest {
challenge,
client_pubkey: keypair.pubkey().as_ref().to_vec(),
signed_challenge,
})
.await?
.into_inner();
Ok((tokens.access_token.unwrap(), tokens.refresh_token.unwrap()))
}
fn spawn_token_refresh_thread(
mut auth_service_client: AuthServiceClient<Channel>,
bearer_token: Arc<RwLock<String>>,
refresh_token: Token,
access_token_expiration: Timestamp,
keypair: Arc<Keypair>,
role: Role,
) -> JoinHandle<BlockEngineConnectionResult<()>> {
tokio::spawn(async move {
let mut refresh_token = refresh_token;
let mut access_token_expiration = access_token_expiration;
loop {
let access_token_ttl = SystemTime::try_from(access_token_expiration.clone())
.unwrap()
.duration_since(SystemTime::now())
.unwrap_or_else(|_| Duration::from_secs(0));
let refresh_token_ttl =
SystemTime::try_from(refresh_token.expires_at_utc.as_ref().unwrap().clone())
.unwrap()
.duration_since(SystemTime::now())
.unwrap_or_else(|_| Duration::from_secs(0));
let does_access_token_expire_soon = access_token_ttl < Duration::from_secs(5 * 60);
let does_refresh_token_expire_soon =
refresh_token_ttl < Duration::from_secs(5 * 60);
match (
does_refresh_token_expire_soon,
does_access_token_expire_soon,
) {
// re-run entire auth workflow is refresh token expiring soon
(true, _) => {
let is_error = {
if let Ok((new_access_token, new_refresh_token)) =
Self::auth(&mut auth_service_client, &keypair, role).await
{
*bearer_token.write().unwrap() = new_access_token.value.clone();
access_token_expiration = new_access_token.expires_at_utc.unwrap();
refresh_token = new_refresh_token;
false
} else {
true
}
};
datapoint_info!("searcher-full-auth", ("is_error", is_error, bool));
}
// re-up the access token if it expires soon
(_, true) => {
let is_error = {
if let Ok(refresh_resp) = auth_service_client
.refresh_access_token(RefreshAccessTokenRequest {
refresh_token: refresh_token.value.clone(),
})
.await
{
let access_token = refresh_resp.into_inner().access_token.unwrap();
*bearer_token.write().unwrap() = access_token.value.clone();
access_token_expiration = access_token.expires_at_utc.unwrap();
false
} else {
true
}
};
datapoint_info!("searcher-refresh-auth", ("is_error", is_error, bool));
}
_ => {
sleep(Duration::from_secs(60)).await;
}
}
}
})
}
}
impl Interceptor for ClientInterceptor {
fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
let l_token = self.bearer_token.read().unwrap();
if !l_token.is_empty() {
request.metadata_mut().insert(
AUTHORIZATION_HEADER,
format!("{BEARER}{l_token}").parse().unwrap(),
);
}
Ok(request)
}
}