use std::sync::Arc;
use std::time::Duration;
use tap::Pipe;
use tonic::body::Body;
use tonic::codec::CompressionEncoding;
use tonic::transport::channel::ClientTlsConfig;
use tower::Layer;
use tower::Service;
use tower::ServiceBuilder;
use tower::util::BoxLayer;
use tower::util::BoxService;
mod response_ext;
pub use response_ext::ResponseExt;
mod interceptors;
pub use interceptors::HeadersInterceptor;
mod watchdog;
pub use watchdog::BodyIdleTimeout;
use watchdog::DEFAULT_BODY_IDLE_TIMEOUT;
use watchdog::WatchdogLayer;
mod staking_rewards;
pub use staking_rewards::DelegatedStake;
mod coin_selection;
mod lists;
mod transaction_execution;
pub use transaction_execution::ExecuteAndWaitError;
use crate::proto::sui::rpc::v2::ledger_service_client::LedgerServiceClient;
use crate::proto::sui::rpc::v2::move_package_service_client::MovePackageServiceClient;
use crate::proto::sui::rpc::v2::signature_verification_service_client::SignatureVerificationServiceClient;
use crate::proto::sui::rpc::v2::state_service_client::StateServiceClient;
use crate::proto::sui::rpc::v2::subscription_service_client::SubscriptionServiceClient;
use crate::proto::sui::rpc::v2::transaction_execution_service_client::TransactionExecutionServiceClient;
#[cfg(feature = "unstable")]
use crate::proto::sui::rpc::v2alpha::proof_service_client::ProofServiceClient;
type Result<T, E = tonic::Status> = std::result::Result<T, E>;
type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
type BoxedChannel = BoxService<http::Request<Body>, http::Response<Body>, tonic::Status>;
type RequestLayer = BoxLayer<
BoxService<http::Request<Body>, http::Response<Body>, BoxError>,
http::Request<Body>,
http::Response<Body>,
BoxError,
>;
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
const DEFAULT_TCP_KEEPALIVE_IDLE: Duration = Duration::from_secs(15);
const DEFAULT_TCP_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(5);
const DEFAULT_TCP_KEEPALIVE_RETRIES: u32 = 3;
const DEFAULT_HTTP2_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(5);
const DEFAULT_HTTP2_KEEP_ALIVE_TIMEOUT: Duration = Duration::from_secs(20);
const DEFAULT_HTTP2_STREAM_WINDOW_SIZE: u32 = 2 * 1024 * 1024;
const DEFAULT_HTTP2_CONNECTION_WINDOW_SIZE: u32 = 64 * 1024 * 1024;
#[derive(Clone)]
pub struct Client {
channel: tonic::transport::Channel,
config: Arc<ClientConfig>,
}
#[derive(Clone)]
struct ClientConfig {
uri: http::Uri,
endpoint: tonic::transport::Endpoint,
headers: HeadersInterceptor,
max_decoding_message_size: Option<usize>,
body_idle_timeout: Option<Duration>,
request_layer: Option<RequestLayer>,
}
impl Client {
pub const MAINNET_FULLNODE: &str = "https://fullnode.mainnet.sui.io";
pub const TESTNET_FULLNODE: &str = "https://fullnode.testnet.sui.io";
pub const DEVNET_FULLNODE: &str = "https://fullnode.devnet.sui.io";
pub const MAINNET_ARCHIVE: &str = "https://archive.mainnet.sui.io";
pub const TESTNET_ARCHIVE: &str = "https://archive.testnet.sui.io";
pub fn from_endpoint(endpoint: &tonic::transport::Endpoint) -> Self {
let uri = endpoint.uri().clone();
let channel = endpoint.connect_lazy();
Self {
channel,
config: Arc::new(ClientConfig {
uri,
endpoint: endpoint.clone(),
headers: Default::default(),
max_decoding_message_size: None,
body_idle_timeout: Some(DEFAULT_BODY_IDLE_TIMEOUT),
request_layer: None,
}),
}
}
#[allow(clippy::result_large_err)]
pub fn new<T>(uri: T) -> Result<Self>
where
T: TryInto<http::Uri>,
T::Error: Into<BoxError>,
{
let uri = uri
.try_into()
.map_err(Into::into)
.map_err(status_from_error)?;
let mut endpoint = tonic::transport::Endpoint::from(uri.clone());
if uri.scheme() == Some(&http::uri::Scheme::HTTPS) {
endpoint = endpoint
.tls_config(ClientTlsConfig::new().with_enabled_roots())
.map_err(Into::into)
.map_err(status_from_error)?;
}
let endpoint = endpoint
.connect_timeout(DEFAULT_CONNECT_TIMEOUT)
.tcp_keepalive(Some(DEFAULT_TCP_KEEPALIVE_IDLE))
.tcp_keepalive_interval(Some(DEFAULT_TCP_KEEPALIVE_INTERVAL))
.tcp_keepalive_retries(Some(DEFAULT_TCP_KEEPALIVE_RETRIES))
.http2_keep_alive_interval(DEFAULT_HTTP2_KEEP_ALIVE_INTERVAL)
.keep_alive_timeout(DEFAULT_HTTP2_KEEP_ALIVE_TIMEOUT)
.initial_stream_window_size(DEFAULT_HTTP2_STREAM_WINDOW_SIZE)
.initial_connection_window_size(DEFAULT_HTTP2_CONNECTION_WINDOW_SIZE);
let channel = endpoint.connect_lazy();
Ok(Self {
channel,
config: Arc::new(ClientConfig {
uri,
endpoint,
headers: Default::default(),
max_decoding_message_size: None,
body_idle_timeout: Some(DEFAULT_BODY_IDLE_TIMEOUT),
request_layer: None,
}),
})
}
pub fn with_body_idle_timeout(mut self, timeout: Duration) -> Self {
Arc::make_mut(&mut self.config).body_idle_timeout = Some(timeout);
self
}
pub fn without_body_idle_timeout(mut self) -> Self {
Arc::make_mut(&mut self.config).body_idle_timeout = None;
self
}
pub fn with_response_headers_timeout(mut self, timeout: Duration) -> Self {
let config = Arc::make_mut(&mut self.config);
config.endpoint = config.endpoint.clone().timeout(timeout);
self.channel = config.endpoint.connect_lazy();
self
}
pub fn with_initial_stream_window_size(mut self, size: u32) -> Self {
let config = Arc::make_mut(&mut self.config);
config.endpoint = config.endpoint.clone().initial_stream_window_size(size);
self.channel = config.endpoint.connect_lazy();
self
}
pub fn with_initial_connection_window_size(mut self, size: u32) -> Self {
let config = Arc::make_mut(&mut self.config);
config.endpoint = config.endpoint.clone().initial_connection_window_size(size);
self.channel = config.endpoint.connect_lazy();
self
}
pub fn with_headers(mut self, headers: HeadersInterceptor) -> Self {
Arc::make_mut(&mut self.config).headers = headers;
self
}
pub fn request_layer<L, ResBody, E>(mut self, layer: L) -> Self
where
L: Layer<BoxService<http::Request<Body>, http::Response<Body>, BoxError>>
+ Send
+ Sync
+ 'static,
L::Service: Service<http::Request<Body>, Response = http::Response<ResBody>, Error = E>
+ Send
+ 'static,
<L::Service as Service<http::Request<Body>>>::Future: Send + 'static,
ResBody: http_body::Body<Data = bytes::Bytes> + Send + 'static,
ResBody::Error: Into<BoxError>,
E: Into<BoxError> + Send + 'static,
{
let layer = BoxLayer::new(
ServiceBuilder::new()
.map_response(|resp: http::Response<ResBody>| resp.map(Body::new))
.map_err(Into::<BoxError>::into)
.layer(layer),
);
Arc::make_mut(&mut self.config).request_layer = Some(layer);
self
}
pub fn with_max_decoding_message_size(mut self, limit: usize) -> Self {
Arc::make_mut(&mut self.config).max_decoding_message_size = Some(limit);
self
}
pub fn uri(&self) -> &http::Uri {
&self.config.uri
}
fn channel(&self) -> BoxedChannel {
let headers = self.config.headers.clone();
let base = BoxService::new(
ServiceBuilder::new()
.map_err(|e: tonic::transport::Error| -> BoxError { Box::new(e) })
.map_request(move |mut req: http::Request<Body>| {
if !headers.headers().is_empty() {
req.headers_mut()
.extend(headers.headers().clone().into_headers());
}
req
})
.service(self.channel.clone()),
);
let base = BoxService::new(WatchdogLayer::new(self.config.body_idle_timeout).layer(base));
let layered = if let Some(layer) = &self.config.request_layer {
layer.layer(base)
} else {
base
};
BoxService::new(
ServiceBuilder::new()
.map_err(status_from_error)
.service(layered),
)
}
pub fn ledger_client(&mut self) -> LedgerServiceClient<BoxedChannel> {
LedgerServiceClient::new(self.channel())
.accept_compressed(CompressionEncoding::Zstd)
.pipe(|client| {
if let Some(limit) = self.config.max_decoding_message_size {
client.max_decoding_message_size(limit)
} else {
client
}
})
}
pub fn state_client(&mut self) -> StateServiceClient<BoxedChannel> {
StateServiceClient::new(self.channel())
.accept_compressed(CompressionEncoding::Zstd)
.pipe(|client| {
if let Some(limit) = self.config.max_decoding_message_size {
client.max_decoding_message_size(limit)
} else {
client
}
})
}
pub fn execution_client(&mut self) -> TransactionExecutionServiceClient<BoxedChannel> {
TransactionExecutionServiceClient::new(self.channel())
.accept_compressed(CompressionEncoding::Zstd)
.pipe(|client| {
if let Some(limit) = self.config.max_decoding_message_size {
client.max_decoding_message_size(limit)
} else {
client
}
})
}
pub fn package_client(&mut self) -> MovePackageServiceClient<BoxedChannel> {
MovePackageServiceClient::new(self.channel())
.accept_compressed(CompressionEncoding::Zstd)
.pipe(|client| {
if let Some(limit) = self.config.max_decoding_message_size {
client.max_decoding_message_size(limit)
} else {
client
}
})
}
pub fn signature_verification_client(
&mut self,
) -> SignatureVerificationServiceClient<BoxedChannel> {
SignatureVerificationServiceClient::new(self.channel())
.accept_compressed(CompressionEncoding::Zstd)
.pipe(|client| {
if let Some(limit) = self.config.max_decoding_message_size {
client.max_decoding_message_size(limit)
} else {
client
}
})
}
pub fn subscription_client(&mut self) -> SubscriptionServiceClient<BoxedChannel> {
SubscriptionServiceClient::new(self.channel())
.accept_compressed(CompressionEncoding::Zstd)
.pipe(|client| {
if let Some(limit) = self.config.max_decoding_message_size {
client.max_decoding_message_size(limit)
} else {
client
}
})
}
#[cfg(feature = "unstable")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "unstable")))]
pub fn proof_client(&mut self) -> ProofServiceClient<BoxedChannel> {
ProofServiceClient::new(self.channel())
.accept_compressed(CompressionEncoding::Zstd)
.pipe(|client| {
if let Some(limit) = self.config.max_decoding_message_size {
client.max_decoding_message_size(limit)
} else {
client
}
})
}
}
fn status_from_error(error: BoxError) -> tonic::Status {
let mut source: Option<&(dyn std::error::Error + 'static)> = Some(error.as_ref());
while let Some(err) = source {
if err.is::<tonic::Status>() {
break;
}
if err.is::<tonic::TimeoutExpired>() {
return tonic::Status::deadline_exceeded(
"timeout expired before response headers were received",
);
}
source = err.source();
}
tonic::Status::from_error(error)
}