feat: add global MEV protection and refactor TradeConfig to builder pattern

- Add `mev_protection: bool` to `TradeConfig` and `InfrastructureConfig` (default: false)
  - Astralane QUIC: switches to port 9000 (MEV-protected endpoint) when enabled
  - BlockRazor HTTP: uses `mode=sandwichMitigation` query param when enabled
  - BlockRazor gRPC: uses `mode=sandwichMitigation` when enabled
- Add `SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV` constants (port 9000) to `constants/swqos.rs`
- Fix `astralane_quic.rs` IP candidates to use the actual port from the address (supports both 7000 and 9000)
- Refactor `TradeConfig` to builder pattern via `TradeConfig::builder()`
  - Introduce `TradeConfigBuilder` with all optional fields and clear defaults
  - `TradeConfig::new()` kept as a shortcut (calls `builder().build()`) for backward compatibility
  - Remove old `with_wsol_ata_config` / `with_check_min_tip` / `with_swqos_cores_from_end` / `with_mev_protection` chain methods
- Update all 16 examples to use `TradeConfig::builder()` with commented-out options so users can discover all available settings at a glance
- Update README.md and README_CN.md code snippets to use builder pattern

🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
0xfnzero
2026-04-08 02:13:37 +08:00
parent 35bfa93516
commit 971ef41fad
44 changed files with 6783 additions and 74 deletions
+10 -10
View File
@@ -52,32 +52,32 @@ impl AstralaneQuicClient {
fn astralane_quic_ip_candidates(host: &str, port: u16) -> Vec<SocketAddr> {
// Official recommended direct-IP list (faster/more stable than DNS-only for QUIC).
// We intentionally avoid fr2/ams2 per prior guidance.
// Both port 7000 (standard) and port 9000 (MEV-protected) use the same IPs.
match host {
"fr.gateway.astralane.io" => vec![
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(185, 191, 117, 97)), 7000),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(45, 139, 132, 160)), 7000),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(185, 191, 117, 97)), port),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(45, 139, 132, 160)), port),
],
"ny.gateway.astralane.io" => {
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(64, 130, 45, 19)), 7000)]
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(64, 130, 45, 19)), port)]
}
"ams.gateway.astralane.io" => vec![
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(64, 130, 43, 43)), 7000),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 186, 73)), 7000),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(64, 130, 43, 43)), port),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 186, 73)), port),
],
"la.gateway.astralane.io" => {
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(74, 118, 142, 151)), 7000)]
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(74, 118, 142, 151)), port)]
}
"lim.gateway.astralane.io" => {
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(162, 19, 222, 232)), 7000)]
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(162, 19, 222, 232)), port)]
}
"sg.gateway.astralane.io" => {
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(67, 209, 54, 176)), 7000)]
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(67, 209, 54, 176)), port)]
}
"lit.gateway.astralane.io" => {
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 97, 47)), 7000)]
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 97, 47)), port)]
}
_ => {
let _ = port; // keep signature consistent; fall back to DNS below.
Vec::new()
}
}
+24 -5
View File
@@ -89,6 +89,8 @@ pub enum BlockRazorBackend {
grpc_client: Arc<ArcSwap<BlockRazorGrpcClient>>,
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
stop_ping: Arc<AtomicBool>,
/// When true, gRPC send_transaction sets revert_protection=true for MEV protection.
mev_protection: bool,
},
Http {
endpoint: String,
@@ -96,6 +98,8 @@ pub enum BlockRazorBackend {
http_client: Client,
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
stop_ping: Arc<AtomicBool>,
/// When true, HTTP request adds revertProtection=true query param for MEV protection.
mev_protection: bool,
},
}
@@ -144,10 +148,10 @@ impl SwqosClientTrait for BlockRazorClient {
impl BlockRazorClient {
pub async fn new(rpc_url: String, endpoint: String, auth_token: String) -> Result<Self> {
// 默认使用 HTTP 模式,避免 gRPC FRAME_SIZE_ERROR
Ok(Self::new_http(rpc_url, endpoint, auth_token))
Ok(Self::new_http(rpc_url, endpoint, auth_token, false))
}
pub async fn new_grpc(rpc_url: String, endpoint: String, auth_token: String) -> Result<Self> {
pub async fn new_grpc(rpc_url: String, endpoint: String, auth_token: String, mev_protection: bool) -> Result<Self> {
let rpc_client = SolanaRpcClient::new(rpc_url);
// 配置 Channel,增加连接超时
@@ -173,6 +177,7 @@ impl BlockRazorClient {
grpc_client,
ping_handle,
stop_ping,
mev_protection,
},
};
@@ -184,7 +189,7 @@ impl BlockRazorClient {
Ok(client)
}
pub fn new_http(rpc_url: String, endpoint: String, auth_token: String) -> Self {
pub fn new_http(rpc_url: String, endpoint: String, auth_token: String, mev_protection: bool) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = default_http_client_builder().user_agent("").build().unwrap();
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
@@ -198,6 +203,7 @@ impl BlockRazorClient {
http_client,
ping_handle,
stop_ping,
mev_protection,
},
};
@@ -217,6 +223,7 @@ impl BlockRazorClient {
stop_ping,
endpoint,
auth_token,
..
} => {
let grpc_client = grpc_client.clone();
let ping_handle = ping_handle.clone();
@@ -293,6 +300,7 @@ impl BlockRazorClient {
http_client,
ping_handle,
stop_ping,
..
} => {
let endpoint = endpoint.clone();
let auth_token = auth_token.clone();
@@ -374,6 +382,7 @@ impl BlockRazorClient {
match &self.backend {
BlockRazorBackend::Grpc {
grpc_client,
mev_protection,
..
} => {
let (content, _signature) =
@@ -383,7 +392,9 @@ impl BlockRazorClient {
let client = grpc_client.load();
let signature = client.send_transaction(
content,
"fast".to_string(),
// mev_protection=true: sandwichMitigation mode skips blacklisted Leader slots (MEV protection).
// revert_protection is unrelated to MEV; keep false.
if *mev_protection { "sandwichMitigation".to_string() } else { "fast".to_string() },
None,
false,
).await;
@@ -412,14 +423,22 @@ impl BlockRazorClient {
endpoint,
auth_token,
http_client,
mev_protection,
..
} => {
let (content, _signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let mut query_params: Vec<(&str, &str)> = vec![
("auth", auth_token.as_str()),
// mev_protection=true: sandwichMitigation mode skips blacklisted Leader slots (MEV protection).
// revertProtection is unrelated to MEV; not set.
("mode", if *mev_protection { "sandwichMitigation" } else { "fast" }),
];
let response = http_client
.post(endpoint)
.query(&[("auth", auth_token.as_str())])
.query(&query_params)
.header("Content-Type", "text/plain")
.body(content)
.send()
+20 -7
View File
@@ -29,7 +29,8 @@ use anyhow::Result;
use crate::{
common::SolanaRpcClient,
constants::swqos::{
SWQOS_ENDPOINTS_ASTRALANE, SWQOS_ENDPOINTS_ASTRALANE_QUIC, SWQOS_ENDPOINTS_BLOCKRAZOR,
SWQOS_ENDPOINTS_ASTRALANE, SWQOS_ENDPOINTS_ASTRALANE_QUIC,
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV, SWQOS_ENDPOINTS_BLOCKRAZOR,
SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, SWQOS_ENDPOINTS_BLOX, SWQOS_ENDPOINTS_FLASHBLOCK,
SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO, SWQOS_ENDPOINTS_NEXTBLOCK,
SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_NODE1_QUIC, SWQOS_ENDPOINTS_SOYAS,
@@ -303,6 +304,7 @@ impl SwqosConfig {
region: SwqosRegion,
url: Option<String>,
transport: Option<SwqosTransport>,
mev_protection: bool,
) -> String {
if let Some(custom_url) = url {
return custom_url;
@@ -329,12 +331,17 @@ impl SwqosConfig {
SwqosType::Astralane => {
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
if use_quic {
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
// MEV protection: port 9000; standard: port 7000
if mev_protection {
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[region as usize].to_string()
} else {
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
}
} else {
SWQOS_ENDPOINTS_ASTRALANE[region as usize].to_string()
}
}
_ => Self::get_endpoint(swqos_type, region, url),
_ => Self::get_endpoint(swqos_type, region, None),
}
}
@@ -342,6 +349,7 @@ impl SwqosConfig {
rpc_url: String,
commitment: CommitmentConfig,
swqos_config: SwqosConfig,
mev_protection: bool,
) -> Result<Arc<SwqosClient>> {
match swqos_config {
SwqosConfig::Jito(auth_token, region, url) => {
@@ -398,15 +406,15 @@ impl SwqosConfig {
SwqosConfig::BlockRazor(auth_token, region, url, transport) => {
// BlockRazor: transport=None 或 transport=Grpc 时使用 gRPCtransport=Http 时使用 HTTP
let use_http = transport.map_or(false, |t| t == SwqosTransport::Http);
let endpoint = SwqosConfig::get_endpoint_with_transport(SwqosType::BlockRazor, region, url, transport);
let endpoint = SwqosConfig::get_endpoint_with_transport(SwqosType::BlockRazor, region, url, transport, mev_protection);
if use_http {
let blockrazor_client =
BlockRazorClient::new_http(rpc_url.clone(), endpoint.to_string(), auth_token);
BlockRazorClient::new_http(rpc_url.clone(), endpoint.to_string(), auth_token, mev_protection);
Ok(Arc::new(blockrazor_client))
} else {
// 使用 gRPC 模式(默认或用户明确指定了 gRPC)
let blockrazor_client =
BlockRazorClient::new_grpc(rpc_url.clone(), endpoint.to_string(), auth_token).await?;
BlockRazorClient::new_grpc(rpc_url.clone(), endpoint.to_string(), auth_token, mev_protection).await?;
Ok(Arc::new(blockrazor_client))
}
}
@@ -414,7 +422,12 @@ impl SwqosConfig {
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
if use_quic {
let quic_endpoint = url.unwrap_or_else(|| {
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
// MEV protection: port 9000; standard: port 7000
if mev_protection {
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[region as usize].to_string()
} else {
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
}
});
let astralane_client =
AstralaneClient::new_quic(rpc_url.clone(), &quic_endpoint, auth_token)