From 07e45d136f7467fd1045ff5649c4961eef3b4cbf Mon Sep 17 00:00:00 2001 From: Wood Date: Wed, 18 Mar 2026 21:32:23 +0800 Subject: [PATCH] Release v4.0.0: SWQoS transport improvements and Binary-Tx response handling Major changes: - Switch BlockRazor default transport from gRPC to HTTP to avoid FRAME_SIZE_ERROR - gRPC mode still available via explicit SwqosTransport configuration - Fix ZeroSlot Binary-Tx JSON-RPC 2.0 response parsing - Properly handle success responses with "result" field - Properly handle error responses with "error" field containing code and message - Update version to 4.0.0 - Update README files to reflect new version Technical details: - BlockRazor: HTTP mode is now the default (SwqosTransport::Http or None) - ZeroSlot: Parse JSON-RPC 2.0 format responses instead of plain text - Proto code: Pre-generated gRPC code for BlockRazor (no protoc required) Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 36 +--- Cargo.toml | 6 +- README.md | 4 +- README_CN.md | 4 +- RELEASE_v3.6.4.md | 8 - src/swqos/blockrazor.rs | 126 +++++++------ src/swqos/mod.rs | 6 +- src/swqos/pb/README.md | 42 +++++ src/swqos/pb/serverpb.rs | 383 +++++++++++++++++++++++++++++++++++++++ src/swqos/zeroslot.rs | 27 ++- 10 files changed, 530 insertions(+), 112 deletions(-) delete mode 100644 RELEASE_v3.6.4.md create mode 100644 src/swqos/pb/README.md create mode 100644 src/swqos/pb/serverpb.rs diff --git a/.gitignore b/.gitignore index 0016389..9aa5191 100755 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,12 @@ -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ +# Proto 生成工具和生成的代码(用户不需要) +/proto/gen/ -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +# 预生成的 proto Rust 代码(提交到仓库,但用户不应修改) +# /src/swqos/pb/serverpb.rs <- 这个文件已经提交,用户不应修改 + +# Build artifacts +/target/ Cargo.lock -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb - -# RustRover -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -.cargo/ - -tmp_*.rs -tmp_*.log - - -.claude/ -.serena/ \ No newline at end of file +# Proto sources +/proto/ diff --git a/Cargo.toml b/Cargo.toml index 9b15e0c..45373dd 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sol-trade-sdk" -version = "3.6.5" +version = "4.0.0" edition = "2021" authors = [ "William ", @@ -135,6 +135,10 @@ incremental = true # 增量编译 - 大幅加速重新编译 opt-level = 1 # 开发时适度优化 overflow-checks = true # 开发时启用溢出检查 +# 🚀 构建依赖 +# 注意:proto 代码已预生成在 src/swqos/pb/serverpb.rs +# 开发者如需重新生成代码,请运行 gen_proto 目录下的工具 + # 🚀 性能关键依赖的特殊优化 [profile.release.package.solana-sdk] opt-level = 3 diff --git a/README.md b/README.md index 3d869f9..2b9441d 100644 --- a/README.md +++ b/README.md @@ -90,14 +90,14 @@ Add the dependency to your `Cargo.toml`: ```toml # Add to your Cargo.toml -sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.6.5" } +sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.0" } ``` ### Use crates.io ```toml # Add to your Cargo.toml -sol-trade-sdk = "3.6.5" +sol-trade-sdk = "4.0.0" ``` ## 🛠️ Usage Examples diff --git a/README_CN.md b/README_CN.md index 67c1741..9d857f6 100755 --- a/README_CN.md +++ b/README_CN.md @@ -90,14 +90,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk ```toml # 添加到您的 Cargo.toml -sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.6.5" } +sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.0" } ``` ### 使用 crates.io ```toml # 添加到您的 Cargo.toml -sol-trade-sdk = "3.6.5" +sol-trade-sdk = "4.0.0" ``` ## 🛠️ 使用示例 diff --git a/RELEASE_v3.6.4.md b/RELEASE_v3.6.4.md deleted file mode 100644 index 7902f0c..0000000 --- a/RELEASE_v3.6.4.md +++ /dev/null @@ -1,8 +0,0 @@ -# Release v3.6.4 - -## Changes from v3.6.3 - -- **Examples**: All workspace examples are verified to build successfully (`cargo build --workspace`). -- **Docs**: Version references in README.md and README_CN.md updated to 3.6.4. - -No API or behavior changes in this release. diff --git a/src/swqos/blockrazor.rs b/src/swqos/blockrazor.rs index 9929bde..5c65909 100644 --- a/src/swqos/blockrazor.rs +++ b/src/swqos/blockrazor.rs @@ -17,60 +17,66 @@ use crate::{common::SolanaRpcClient, constants::swqos::BLOCKRAZOR_TIP_ACCOUNTS}; use std::sync::atomic::{AtomicBool, Ordering}; use tokio::task::JoinHandle; -use tonic::metadata::AsciiMetadataValue; use tonic::transport::Channel; +use tonic::metadata::AsciiMetadataValue; -// Manual gRPC message types for BlockRazor serverpb.proto -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct HealthRequest {} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct HealthResponse { - pub status: String, +// Include pre-generated gRPC code +pub mod serverpb { + include!("pb/serverpb.rs"); } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SendRequest { - pub transaction: String, - pub mode: String, - pub safe_window: Option, - pub revert_protection: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SendResponse { - pub signature: String, -} - -// Mock gRPC client using tonic +// gRPC client wrapper #[derive(Clone)] pub struct BlockRazorGrpcClient { channel: Channel, + auth_token: String, } impl BlockRazorGrpcClient { - pub fn new(channel: Channel) -> Self { - Self { channel } + pub fn new(channel: Channel, auth_token: String) -> Self { + Self { channel, auth_token } } - pub async fn get_health(&self) -> Result { - // For now, use a simple HTTP request for health check - let http_client = Client::new(); - let response = http_client - .get("http://health.example.com") // Placeholder - .send() - .await; - Ok(HealthResponse { - status: "ok".to_string(), - }) + pub async fn get_health(&self) -> Result { + let mut client = serverpb::server_client::ServerClient::new(self.channel.clone()); + let apikey = AsciiMetadataValue::try_from(self.auth_token.as_str()) + .map_err(|e| anyhow::anyhow!("Invalid API key format: {}", e))?; + + let mut request = tonic::Request::new(serverpb::HealthRequest {}); + request.metadata_mut().insert("apikey", apikey); + + let response = client.get_health(request).await + .map_err(|e| anyhow::anyhow!("gRPC health check failed: {}", e))?; + Ok(response.into_inner().status) } - pub async fn send_transaction(&self, _request: SendRequest) -> Result { - // For now, this is a placeholder implementation - // Real implementation would use tonic-generated client - Ok(SendResponse { - signature: "placeholder".to_string(), - }) + pub async fn send_transaction( + &self, + transaction: String, + mode: String, + safe_window: Option, + revert_protection: bool, + ) -> Result { + // 检查交易数据大小 + if crate::common::sdk_log::sdk_log_enabled() { + eprintln!("BlockRazor transaction size: {} bytes", transaction.len()); + } + + let mut client = serverpb::server_client::ServerClient::new(self.channel.clone()); + let apikey = AsciiMetadataValue::try_from(self.auth_token.as_str()) + .map_err(|e| anyhow::anyhow!("Invalid API key format: {}", e))?; + + let mut request = tonic::Request::new(serverpb::SendRequest { + transaction, + mode: String::from(mode), + safe_window, + revert_protection, + }); + request.metadata_mut().insert("apikey", apikey); + + let response = client.send_transaction(request).await + .map_err(|e| anyhow::anyhow!("gRPC send transaction failed: {}", e))?; + Ok(response.into_inner().signature) } } @@ -135,22 +141,23 @@ impl SwqosClientTrait for BlockRazorClient { } impl BlockRazorClient { - /// 使用 gRPC 提交(默认方式)。 pub async fn new(rpc_url: String, endpoint: String, auth_token: String) -> Result { - Self::new_grpc(rpc_url, endpoint, auth_token).await + // 默认使用 HTTP 模式,避免 gRPC FRAME_SIZE_ERROR + Ok(Self::new_http(rpc_url, endpoint, auth_token)) } - /// 使用 gRPC 提交。 pub async fn new_grpc(rpc_url: String, endpoint: String, auth_token: String) -> Result { let rpc_client = SolanaRpcClient::new(rpc_url); + // 配置 Channel,增加连接超时 let channel = tonic::transport::Channel::from_shared(endpoint.clone()) .map_err(|e| anyhow::anyhow!("Invalid gRPC endpoint: {}", e))? + .timeout(Duration::from_secs(30)) .connect() .await .map_err(|e| anyhow::anyhow!("Failed to connect to gRPC endpoint: {}", e))?; - let grpc_client = Arc::new(BlockRazorGrpcClient::new(channel)); + let grpc_client = Arc::new(BlockRazorGrpcClient::new(channel, auth_token.clone())); let ping_handle = Arc::new(tokio::sync::Mutex::new(None)); let stop_ping = Arc::new(AtomicBool::new(false)); @@ -173,10 +180,8 @@ impl BlockRazorClient { Ok(client) } - /// 使用 HTTP 提交。 pub fn new_http(rpc_url: String, endpoint: String, auth_token: String) -> Self { let rpc_client = SolanaRpcClient::new(rpc_url); - // 官方文档:請求中唯一允許的 header 是 Content-Type: text/plain;避免默认 User-Agent 等导致 500 let http_client = default_http_client_builder().user_agent("").build().unwrap(); let ping_handle = Arc::new(tokio::sync::Mutex::new(None)); let stop_ping = Arc::new(AtomicBool::new(false)); @@ -213,13 +218,12 @@ impl BlockRazorClient { let stop_ping = stop_ping.clone(); let handle = tokio::spawn(async move { - // Immediate first ping to warm connection and reduce first-submit cold start latency if let Err(e) = grpc_client.get_health().await { if crate::common::sdk_log::sdk_log_enabled() { eprintln!("BlockRazor gRPC ping request failed: {}", e); } } - let mut interval = tokio::time::interval(Duration::from_secs(30)); // 30s keepalive to avoid server ~5min idle close + let mut interval = tokio::time::interval(Duration::from_secs(30)); loop { interval.tick().await; if stop_ping.load(Ordering::Relaxed) { @@ -253,13 +257,12 @@ impl BlockRazorClient { let stop_ping = stop_ping.clone(); let handle = tokio::spawn(async move { - // Immediate first ping to warm connection and reduce first-submit cold start latency if let Err(e) = Self::send_http_ping(&http_client, &endpoint, &auth_token).await { if crate::common::sdk_log::sdk_log_enabled() { eprintln!("BlockRazor HTTP ping request failed: {}", e); } } - let mut interval = tokio::time::interval(Duration::from_secs(30)); // 30s keepalive to avoid server ~5min idle close + let mut interval = tokio::time::interval(Duration::from_secs(30)); loop { interval.tick().await; if stop_ping.load(Ordering::Relaxed) { @@ -282,7 +285,6 @@ impl BlockRazorClient { } } - /// Send HTTP ping request: POST /v2/health?auth=... (Keep Alive). Only required param: auth. async fn send_http_ping( http_client: &Client, endpoint: &str, @@ -315,24 +317,21 @@ impl BlockRazorClient { match &self.backend { BlockRazorBackend::Grpc { - auth_token, grpc_client, .. } => { let (content, _signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?; - let request = SendRequest { - transaction: content, - mode: "fast".to_string(), - safe_window: None, - revert_protection: false, - }; - - let response = grpc_client.send_transaction(request).await; - match response { - Ok(resp) => { - if !resp.signature.is_empty() { + let signature = grpc_client.send_transaction( + content, + "fast".to_string(), + None, + false, + ).await; + match signature { + Ok(sig) => { + if !sig.is_empty() { if crate::common::sdk_log::sdk_log_enabled() { crate::common::sdk_log::log_swqos_submitted("BlockRazor", trade_type, start_time.elapsed()); } @@ -389,7 +388,6 @@ impl BlockRazorClient { } let start_time = Instant::now(); - // Get signature from transaction let signature = transaction.signatures[0]; match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await { diff --git a/src/swqos/mod.rs b/src/swqos/mod.rs index e71aa0e..44d29b2 100755 --- a/src/swqos/mod.rs +++ b/src/swqos/mod.rs @@ -352,14 +352,16 @@ impl SwqosConfig { Ok(Arc::new(flashblock_client)) } SwqosConfig::BlockRazor(auth_token, region, url, transport) => { - let use_http = transport.map_or(false, |t| t == SwqosTransport::Http); + // BlockRazor: transport=None 或 transport=Http 时使用 HTTP,否则使用 gRPC + // 默认使用 HTTP,避免 gRPC FRAME_SIZE_ERROR + let use_http = transport.map_or(true, |t| t == SwqosTransport::Http); if use_http { let endpoint = SwqosConfig::get_endpoint(SwqosType::BlockRazor, region, url); let blockrazor_client = BlockRazorClient::new_http(rpc_url.clone(), endpoint.to_string(), auth_token); Ok(Arc::new(blockrazor_client)) } else { - // Default to gRPC + // 使用 gRPC 模式(用户明确指定了非 Http 的 transport) let endpoint = SwqosConfig::get_endpoint(SwqosType::BlockRazor, region, url); let blockrazor_client = BlockRazorClient::new_grpc(rpc_url.clone(), endpoint.to_string(), auth_token).await?; diff --git a/src/swqos/pb/README.md b/src/swqos/pb/README.md new file mode 100644 index 0000000..26a96be --- /dev/null +++ b/src/swqos/pb/README.md @@ -0,0 +1,42 @@ +# Proto 生成的代码说明 + +## 概述 + +这个目录包含了从 `.proto` 文件预生成的 Rust 代码。 + +## 文件说明 + +- `serverpb.rs` - 从 `blockrazor.proto` 生成的 gRPC 代码 + - 消息类型: `SendRequest`, `SendResponse`, `HealthRequest`, `HealthResponse` + - gRPC 客户端: `server_client::ServerClient` + - gRPC 服务端: `server_server::Server` + +## 用户使用 + +用户**不需要**安装 `protoc` 或编译 proto 文件。这些代码已经预生成好了,可以直接使用。 + +在 `blockrazor.rs` 中使用: +```rust +pub mod serverpb { + include!("pb/serverpb.rs"); +} +``` + +## 开发者如何重新生成代码 + +如果你修改了 `.proto` 文件并需要重新生成代码: + +```bash +cd sol-trade-sdk/proto/gen +cargo run +``` + +这会在 `src/swqos/pb/serverpb.rs` 生成新的代码。 + +## 技术细节 + +生成工具使用 `tonic-prost-build` crate: +- 输出目录: `src/swqos/pb` +- Proto 文件: `proto/blockrazor.proto` +- 生成工具: `proto/gen/` +- 包含完整的客户端和服务端代码 diff --git a/src/swqos/pb/serverpb.rs b/src/swqos/pb/serverpb.rs new file mode 100644 index 0000000..c3e5d7c --- /dev/null +++ b/src/swqos/pb/serverpb.rs @@ -0,0 +1,383 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct SendRequest { + #[prost(string, tag = "1")] + pub transaction: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub mode: ::prost::alloc::string::String, + /// only take effect in sandwichMitigation mode + #[prost(int32, optional, tag = "3")] + pub safe_window: ::core::option::Option, + #[prost(bool, tag = "4")] + pub revert_protection: bool, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct SendResponse { + #[prost(string, tag = "1")] + pub signature: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct HealthRequest {} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct HealthResponse { + #[prost(string, tag = "1")] + pub status: ::prost::alloc::string::String, +} +/// Generated client implementations. +pub mod server_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 ServerClient { + inner: tonic::client::Grpc, + } + impl ServerClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl ServerClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + 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( + inner: T, + interceptor: F, + ) -> ServerClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + ServerClient::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 send_transaction( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/serverpb.Server/SendTransaction", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("serverpb.Server", "SendTransaction")); + self.inner.unary(req, path, codec).await + } + pub async fn get_health( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/serverpb.Server/GetHealth", + ); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("serverpb.Server", "GetHealth")); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod server_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with ServerServer. + #[async_trait] + pub trait Server: std::marker::Send + std::marker::Sync + 'static { + async fn send_transaction( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + async fn get_health( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + } + #[derive(Debug)] + pub struct ServerServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl ServerServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for ServerServer + where + T: Server, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/serverpb.Server/SendTransaction" => { + #[allow(non_camel_case_types)] + struct SendTransactionSvc(pub Arc); + impl tonic::server::UnaryService + for SendTransactionSvc { + type Response = super::SendResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::send_transaction(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = SendTransactionSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/serverpb.Server/GetHealth" => { + #[allow(non_camel_case_types)] + struct GetHealthSvc(pub Arc); + impl tonic::server::UnaryService + for GetHealthSvc { + type Response = super::HealthResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_health(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetHealthSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new( + tonic::body::Body::default(), + ); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for ServerServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "serverpb.Server"; + impl tonic::server::NamedService for ServerServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/src/swqos/zeroslot.rs b/src/swqos/zeroslot.rs index cb93d6d..6503803 100755 --- a/src/swqos/zeroslot.rs +++ b/src/swqos/zeroslot.rs @@ -167,18 +167,33 @@ impl ZeroSlotClient { let status = response.status(); let response_text = response.text().await?; - // Binary-Tx returns plain text responses with specific status codes - // 200: ok - transaction submitted successfully + // Binary-Tx returns JSON-RPC 2.0 format responses + // 200: success with result field containing signature, or error field with code/message // 403: api-key error (null, doesn't exist, or expired) // 419: rate limit exceeded // 500: submission failed match status.as_u16() { 200 => { - if response_text.trim() == "ok" { - crate::common::sdk_log::log_swqos_submitted("0slot", trade_type, start_time.elapsed()); + if let Ok(json_value) = serde_json::from_str::(&response_text) { + if json_value.get("result").is_some() { + crate::common::sdk_log::log_swqos_submitted("0slot", trade_type, start_time.elapsed()); + } else if let Some(error) = json_value.get("error") { + let code = error.get("code") + .and_then(|c| c.as_i64()) + .map(|c| c.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + let message = error.get("message") + .and_then(|m| m.as_str()) + .unwrap_or("unknown error"); + crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), format!("code {}: {}", code, message)); + return Err(anyhow::anyhow!("0slot Binary-Tx error: {}", message)); + } else { + crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), format!("unexpected JSON: {}", response_text)); + return Err(anyhow::anyhow!("0slot Binary-Tx unexpected JSON: {}", response_text)); + } } else { - crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), format!("unexpected response: {}", response_text)); - return Err(anyhow::anyhow!("0slot Binary-Tx unexpected response: {}", response_text)); + crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), format!("invalid JSON: {}", response_text)); + return Err(anyhow::anyhow!("0slot Binary-Tx invalid JSON: {}", response_text)); } } 403 => {