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:
+4
-5
@@ -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(
|
||||
|
||||
Executable
+260
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+19
@@ -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,
|
||||
}
|
||||
Executable
+462
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Executable
+178
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+279
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+13
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Executable
+167
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user