Skip to content

Commit f79ae9f

Browse files
authored
feat(gRPC): add accessors, google types and refactor client and grpc-types (#9355)
# Description of change This PR refactors the `iota-grpc-types` file / folder structure, adds auto-generated accessors and google types for error details. ## How the change has been tested - [x] Basic tests (linting, compilation, formatting, unit/integration tests) - [ ] Patch-specific tests (correctness, functionality coverage) - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have checked that new and existing unit tests pass locally with my changes
1 parent c210dd1 commit f79ae9f

File tree

89 files changed

+12401
-413
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+12401
-413
lines changed

.licenseignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# Generated protobuf files from Google
2-
crates/iota-grpc-types/src/proto_generated/google.*
2+
crates/iota-grpc-types/src/proto/generated/google.*

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/iota-grpc-client/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ workspace = true
1212
[dependencies]
1313
# external dependencies
1414
anyhow.workspace = true
15+
base64.workspace = true
1516
futures.workspace = true
17+
http.workspace = true
18+
iota-sdk2.workspace = true
1619
serde_json.workspace = true
17-
tonic.workspace = true
20+
tap.workspace = true
21+
tonic = { workspace = true, features = ["tls-ring"] }
1822

1923
# internal dependencies
2024
iota-grpc-types.workspace = true
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright (c) Mysten Labs, Inc.
2+
// Modifications Copyright (c) 2025 IOTA Stiftung
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
use std::{
6+
sync::{Arc, OnceLock},
7+
time::Duration,
8+
};
9+
10+
use iota_grpc_types::v0::ledger_service::ledger_service_client::LedgerServiceClient;
11+
use tap::Pipe;
12+
use tonic::{codec::CompressionEncoding, transport::channel::ClientTlsConfig};
13+
14+
use crate::interceptors::HeadersInterceptor;
15+
16+
type Result<T, E = tonic::Status> = std::result::Result<T, E>;
17+
type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
18+
19+
type InterceptedChannel =
20+
tonic::service::interceptor::InterceptedService<tonic::transport::Channel, HeadersInterceptor>;
21+
22+
/// gRPC client factory for IOTA gRPC operations.
23+
#[derive(Clone)]
24+
pub struct Client {
25+
/// Target URI of the gRPC server
26+
uri: http::Uri,
27+
/// Shared gRPC channel for all service clients
28+
channel: tonic::transport::Channel,
29+
/// Headers interceptor for adding custom headers to requests
30+
headers: HeadersInterceptor,
31+
/// Maximum decoding message size for responses
32+
max_decoding_message_size: Option<usize>,
33+
34+
/// Cached ledger client (singleton)
35+
ledger_client: Arc<OnceLock<LedgerServiceClient<InterceptedChannel>>>,
36+
}
37+
38+
impl Client {
39+
/// Connect to a gRPC server and create a new Client instance.
40+
#[allow(clippy::result_large_err)]
41+
pub async fn connect<T>(uri: T) -> Result<Self>
42+
where
43+
T: TryInto<http::Uri>,
44+
T::Error: Into<BoxError>,
45+
{
46+
let uri = uri
47+
.try_into()
48+
.map_err(Into::into)
49+
.map_err(tonic::Status::from_error)?;
50+
51+
let mut endpoint = tonic::transport::Endpoint::from(uri.clone());
52+
if uri.scheme() == Some(&http::uri::Scheme::HTTPS) {
53+
endpoint = endpoint
54+
.tls_config(ClientTlsConfig::new().with_enabled_roots())
55+
.map_err(Into::into)
56+
.map_err(tonic::Status::from_error)?;
57+
}
58+
59+
let channel = endpoint
60+
.connect_timeout(Duration::from_secs(5))
61+
.http2_keep_alive_interval(Duration::from_secs(5))
62+
.connect_lazy();
63+
64+
Ok(Self {
65+
uri,
66+
channel,
67+
headers: Default::default(),
68+
max_decoding_message_size: None,
69+
ledger_client: Arc::new(OnceLock::new()),
70+
})
71+
}
72+
73+
pub fn uri(&self) -> &http::Uri {
74+
&self.uri
75+
}
76+
77+
/// Get a reference to the underlying channel.
78+
///
79+
/// This can be useful for creating additional service clients that aren't
80+
/// yet integrated into Client.
81+
pub fn channel(&self) -> &tonic::transport::Channel {
82+
&self.channel
83+
}
84+
85+
pub fn headers(&self) -> &HeadersInterceptor {
86+
&self.headers
87+
}
88+
89+
pub fn max_decoding_message_size(&self) -> Option<usize> {
90+
self.max_decoding_message_size
91+
}
92+
93+
pub fn with_headers(mut self, headers: HeadersInterceptor) -> Self {
94+
self.headers = headers;
95+
self
96+
}
97+
98+
pub fn with_max_decoding_message_size(mut self, limit: usize) -> Self {
99+
self.max_decoding_message_size = Some(limit);
100+
self
101+
}
102+
103+
// ========================================
104+
// Service Client Factories
105+
// ========================================
106+
107+
/// Get a ledger service client.
108+
///
109+
/// Returns `Some(LedgerClient)` if the server supports ledger-related
110+
/// operations, `None` otherwise. The client is created only once and
111+
/// cached for subsequent calls.
112+
pub fn ledger_service_client(&self) -> Option<LedgerServiceClient<InterceptedChannel>> {
113+
// For now, always return Some since ledger service is always available
114+
// In the future, this could check server capabilities first
115+
Some(
116+
self.ledger_client
117+
.get_or_init(|| {
118+
LedgerServiceClient::with_interceptor(
119+
self.channel.clone(),
120+
self.headers.clone(),
121+
)
122+
.accept_compressed(CompressionEncoding::Zstd)
123+
.pipe(|client| {
124+
if let Some(limit) = self.max_decoding_message_size {
125+
client.max_decoding_message_size(limit)
126+
} else {
127+
client
128+
}
129+
})
130+
})
131+
.clone(),
132+
)
133+
}
134+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) Mysten Labs, Inc.
2+
// Modifications Copyright (c) 2025 IOTA Stiftung
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
/// Interceptor used to add additional headers to a Request
6+
#[derive(Clone, Debug, Default)]
7+
pub struct HeadersInterceptor {
8+
headers: tonic::metadata::MetadataMap,
9+
}
10+
11+
impl HeadersInterceptor {
12+
/// Create a new, empty `HeadersInterceptor`.
13+
pub fn new() -> Self {
14+
Self::default()
15+
}
16+
17+
/// Return reference to the internal `MetadataMap`.
18+
pub fn headers(&self) -> &tonic::metadata::MetadataMap {
19+
&self.headers
20+
}
21+
22+
/// Get mutable access to the internal `MetadataMap` for modification.
23+
pub fn headers_mut(&mut self) -> &mut tonic::metadata::MetadataMap {
24+
&mut self.headers
25+
}
26+
27+
/// Enable HTTP basic authentication with a username and optional password.
28+
pub fn basic_auth<U, P>(&mut self, username: U, password: Option<P>)
29+
where
30+
U: std::fmt::Display,
31+
P: std::fmt::Display,
32+
{
33+
use std::io::Write;
34+
35+
use base64::{prelude::BASE64_STANDARD, write::EncoderWriter};
36+
37+
let mut buf = b"Basic ".to_vec();
38+
{
39+
let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
40+
let _ = write!(encoder, "{username}:");
41+
if let Some(password) = password {
42+
let _ = write!(encoder, "{password}");
43+
}
44+
}
45+
let mut header = tonic::metadata::MetadataValue::try_from(buf)
46+
.expect("base64 is always valid HeaderValue");
47+
header.set_sensitive(true);
48+
49+
self.headers
50+
.insert(http::header::AUTHORIZATION.as_str(), header);
51+
}
52+
53+
/// Enable HTTP bearer authentication.
54+
pub fn bearer_auth<T>(&mut self, token: T)
55+
where
56+
T: std::fmt::Display,
57+
{
58+
let header_value = format!("Bearer {token}");
59+
let mut header = tonic::metadata::MetadataValue::try_from(header_value)
60+
.expect("token is always valid HeaderValue");
61+
header.set_sensitive(true);
62+
63+
self.headers
64+
.insert(http::header::AUTHORIZATION.as_str(), header);
65+
}
66+
}
67+
68+
impl tonic::service::Interceptor for &HeadersInterceptor {
69+
fn call(
70+
&mut self,
71+
mut request: tonic::Request<()>,
72+
) -> std::result::Result<tonic::Request<()>, tonic::Status> {
73+
if !self.headers.is_empty() {
74+
request
75+
.metadata_mut()
76+
.as_mut()
77+
.extend(self.headers.clone().into_headers());
78+
}
79+
Ok(request)
80+
}
81+
}
82+
83+
impl tonic::service::Interceptor for HeadersInterceptor {
84+
fn call(
85+
&mut self,
86+
request: tonic::Request<()>,
87+
) -> std::result::Result<tonic::Request<()>, tonic::Status> {
88+
(&*self).call(request)
89+
}
90+
}

crates/iota-grpc-client/src/ledger.rs

Lines changed: 0 additions & 23 deletions
This file was deleted.

crates/iota-grpc-client/src/lib.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33

44
//! gRPC client for IOTA node operations.
55
6-
mod ledger;
7-
mod node_client;
6+
mod client;
7+
pub use client::Client;
88

9-
pub use ledger::LedgerClient;
10-
pub use node_client::NodeClient;
9+
mod response_ext;
10+
pub use response_ext::ResponseExt;
11+
12+
mod interceptors;
13+
pub use interceptors::HeadersInterceptor;

crates/iota-grpc-client/src/node_client.rs

Lines changed: 0 additions & 55 deletions
This file was deleted.

0 commit comments

Comments
 (0)