1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
//! This module attempts to make a little bit of sense when dealing with Sui
//! types.
//!
//! This way we can use, for example `sui::types::Address` in our code.
pub mod types {
pub use sui_sdk_types::*;
}
pub mod crypto {
pub use sui_crypto::{ed25519::Ed25519PrivateKey, *};
}
pub mod observation;
mod request_budget;
mod rpc_observation;
pub mod grpc {
use std::{
collections::HashMap,
num::NonZeroU32,
sync::{Arc, LazyLock, Mutex},
time::Duration,
};
pub use sui_rpc::{field::FieldMask, proto::sui::rpc::v2::*, Client};
/// Metadata used by nodes that can return execution only after the
/// transaction checkpoint is locally visible.
pub mod checkpoint_wait {
const HEADER: &str = "x-sui-checkpoint-wait";
/// Return whether a service response advertises checkpoint waiting.
pub fn is_supported<T>(response: &tonic::Response<T>) -> bool {
response
.metadata()
.get(HEADER)
.is_some_and(|value| value == "true")
}
/// Build an execution request that waits for local checkpoint
/// visibility on a node which advertised support.
pub fn execution_request<T>(message: T) -> tonic::Request<T> {
let mut request = tonic::Request::new(message);
request
.metadata_mut()
.insert(HEADER, tonic::metadata::MetadataValue::from_static("true"));
request
}
}
/// Maximum independent HTTP/2 connections retained for one RPC endpoint.
///
/// A Sui client and all of its clones multiplex responses through one
/// connection. A burst of small responses can therefore exhaust h2's
/// framing budget before callers consume them. Independent clients are
/// spread across this bounded set so one busy connection cannot fail the
/// entire process.
const MAX_CHANNELS_PER_ENDPOINT: usize = 32;
/// Maximum wait for an RPC response to begin.
///
/// Streaming responses remain open after their headers arrive. This bound
/// prevents a failed or saturated connection from parking an ordinary RPC
/// forever before the server starts its response.
const RESPONSE_HEADERS_TIMEOUT: Duration = Duration::from_secs(30);
/// Request metadata for finalized causal simulation.
#[cfg(feature = "events")]
pub mod causal {
use super::super::types::Digest;
const PARENT_HEADER: &str = "x-sui-causal-parent";
const RECORD_HEADER: &str = "x-sui-causal-record";
const APPLIED_HEADER: &str = "x-sui-causal-applied";
const STREAM_HEADER: &str = "x-sui-causal-stream";
/// Build an execution request whose finalized state is retained.
///
/// `parent` links the new state to a previously finalized transaction.
/// An absent parent starts a new bounded causal view.
pub fn execution_request<T>(
message: T,
parent: Option<Digest>,
) -> anyhow::Result<tonic::Request<T>> {
let mut request = tonic::Request::new(message);
request.metadata_mut().insert(
RECORD_HEADER,
tonic::metadata::MetadataValue::from_static("true"),
);
if let Some(parent) = parent {
request.metadata_mut().insert(
PARENT_HEADER,
tonic::metadata::MetadataValue::try_from(parent.to_string())?,
);
}
Ok(request)
}
/// Build a simulation request that must include `parent` state.
pub fn simulation_request<T>(
message: T,
parent: Digest,
) -> anyhow::Result<tonic::Request<T>> {
let mut request = tonic::Request::new(message);
request.metadata_mut().insert(
PARENT_HEADER,
tonic::metadata::MetadataValue::try_from(parent.to_string())?,
);
Ok(request)
}
/// Return whether the server honored the requested causal view.
pub fn was_applied<T>(response: &tonic::Response<T>) -> bool {
response
.metadata()
.get(APPLIED_HEADER)
.is_some_and(|value| value == "true")
}
/// Build an opt in stream request for quorum finalized receipts.
///
/// The stream is opportunistic and has no replay cursor. Consumers
/// must retain the canonical checkpoint stream as their recovery path.
pub fn finality_stream_request<T>(message: T) -> tonic::Request<T> {
let mut request = tonic::Request::new(message);
request.metadata_mut().insert(
STREAM_HEADER,
tonic::metadata::MetadataValue::from_static("true"),
);
request
}
/// Return whether the server accepted the causal finality stream.
pub fn finality_stream_was_applied<T>(response: &tonic::Response<T>) -> bool {
response
.metadata()
.get(STREAM_HEADER)
.is_some_and(|value| value == "true")
}
}
static TRANSPORT_RUNTIME: LazyLock<tokio::runtime::Runtime> = LazyLock::new(|| {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.thread_name("nexus-sui-grpc")
.enable_all()
.build()
.expect("Sui gRPC transport runtime should start")
});
#[derive(Default)]
struct EndpointPool {
clients: Vec<Client>,
next: usize,
budget: Arc<Mutex<super::request_budget::RequestBudget>>,
}
#[derive(Default)]
struct ClientPool {
endpoints: Mutex<HashMap<String, EndpointPool>>,
}
impl ClientPool {
fn client(&self, rpc_url: impl AsRef<str>) -> anyhow::Result<Client> {
let rpc_url = rpc_url.as_ref();
let mut endpoints = self
.endpoints
.lock()
.map_err(|_| anyhow::anyhow!("Sui gRPC client pool lock was poisoned"))?;
let endpoint = endpoints.entry(rpc_url.to_owned()).or_default();
if endpoint.clients.len() < MAX_CHANNELS_PER_ENDPOINT {
let budget = Arc::clone(&endpoint.budget);
let client = {
let _runtime = TRANSPORT_RUNTIME.enter();
Client::new(rpc_url)
.map_err(anyhow::Error::new)?
.with_response_headers_timeout(RESPONSE_HEADERS_TIMEOUT)
.request_layer(tower::layer::layer_fn(move |service| {
let service = super::rpc_observation::ObservedService(service);
super::request_budget::BudgetService::new(service, Arc::clone(&budget))
}))
};
endpoint.clients.push(client.clone());
return Ok(client);
}
let index = endpoint.next;
endpoint.next = (endpoint.next + 1) % endpoint.clients.len();
Ok(endpoint.clients[index].clone())
}
}
static CLIENT_POOL: LazyLock<ClientPool> = LazyLock::new(ClientPool::default);
/// Returns a [`Client`] backed by a bounded process wide connection pool.
///
/// # Errors
///
/// Returns an error when the endpoint is invalid or the client pool is
/// unavailable.
pub fn client(rpc_url: impl AsRef<str>) -> anyhow::Result<Client> {
CLIENT_POOL.client(rpc_url)
}
/// Bound recovery RPC attempts to this URL across pooled clients and their clones.
///
/// Calls inside [`with_retry_budget`] include reads, submissions, and stream establishment. Existing
/// streams keep receiving without consuming tokens. No request is retried by
/// this layer, so callers retain control of transaction identity and deadlines.
/// Repeating the same configuration does not replenish the shared budget.
/// Ordinary calls and endpoints without a configured budget preserve unrestricted admission.
pub fn set_retry_request_budget(
rpc_url: &str,
requests_per_second: NonZeroU32,
burst: NonZeroU32,
) -> anyhow::Result<()> {
client(rpc_url)?;
let endpoints = CLIENT_POOL
.endpoints
.lock()
.map_err(|_| anyhow::anyhow!("Sui gRPC client pool lock was poisoned"))?;
endpoints[rpc_url]
.budget
.lock()
.map_err(|_| anyhow::anyhow!("Sui RPC budget lock was poisoned"))?
.configure(requests_per_second, burst);
Ok(())
}
/// Apply the configured shared budget to every RPC made by a recovery attempt.
///
/// This task scope leaves ordinary parallel execution unrestricted. It does
/// not spawn work, repeat effects, or change the identity of a submission.
pub async fn with_retry_budget<F: std::future::Future>(attempt: F) -> F::Output {
super::request_budget::scope(attempt).await
}
/// Retain concurrent preparation until its observation deadline.
///
/// Crawler reads retry only transient transport failures, retaining completed
/// sibling reads. Dropping the scope cancels reads and waits. Use this only
/// for preparation before an external effect can occur.
pub async fn with_read_retry_until<F: std::future::Future>(
deadline: tokio::time::Instant,
preparation: F,
) -> F::Output {
super::request_budget::read_scope(Some(deadline), preparation).await
}
/// End preparation retry semantics before performing or processing effects.
pub async fn without_read_retry<F: std::future::Future>(operation: F) -> F::Output {
super::request_budget::read_scope(None, operation).await
}
pub(crate) async fn retry_read<T, F, Fut>(operation: F) -> Result<T, tonic::Status>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<T, tonic::Status>>,
{
super::request_budget::read(operation).await
}
#[cfg(test)]
mod tests {
use super::{ClientPool, MAX_CHANNELS_PER_ENDPOINT};
#[test]
fn client_pool_bounds_channels_per_endpoint() {
let pool = ClientPool::default();
for _ in 0..MAX_CHANNELS_PER_ENDPOINT + 1 {
let _client = pool.client("http://127.0.0.1:1").unwrap();
}
let endpoints = pool.endpoints.lock().unwrap();
assert_eq!(endpoints.len(), 1);
assert_eq!(
endpoints["http://127.0.0.1:1"].clients.len(),
MAX_CHANNELS_PER_ENDPOINT
);
assert_eq!(endpoints["http://127.0.0.1:1"].next, 1);
}
#[test]
fn checkpoint_wait_metadata_is_explicit() {
let request = super::checkpoint_wait::execution_request(());
assert_eq!(
request.metadata().get("x-sui-checkpoint-wait").unwrap(),
"true"
);
let mut response = tonic::Response::new(());
assert!(!super::checkpoint_wait::is_supported(&response));
response.metadata_mut().insert(
"x-sui-checkpoint-wait",
tonic::metadata::MetadataValue::from_static("true"),
);
assert!(super::checkpoint_wait::is_supported(&response));
}
#[cfg(feature = "events")]
#[test]
fn causal_requests_carry_the_parent_digest() {
let parent = super::super::types::Digest::ZERO;
let request = super::causal::simulation_request((), parent).unwrap();
assert_eq!(
request.metadata().get("x-sui-causal-parent").unwrap(),
parent.to_string().as_str(),
);
let stream = super::causal::finality_stream_request(());
assert_eq!(
stream.metadata().get("x-sui-causal-stream").unwrap(),
"true",
);
}
}
}
/// Generic Sui event queries and ingestion.
#[cfg(feature = "events")]
pub mod events;
/// Sui traits re-exported so that we can `use sui::traits::*` in our code.
pub mod traits {
pub use {sui_crypto::SuiSigner, sui_rpc::field::FieldMaskUtil, sui_sdk_types::bcs::ToBcs};
}
pub const MIST_PER_SUI: u64 = 1_000_000_000;
/// Compiled Move package data accepted by package publishing APIs.
///
/// This value contains only transaction input data. Move project compilation
/// and file system access remain outside the registry compatible SDK.
/// Existing TAP publish artifacts describe skill metadata after deployment,
/// so they cannot carry the module bytes required by a Sui publish command.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MovePackageArtifact {
/// Serialized Move modules in dependency order.
pub modules: Vec<Vec<u8>>,
/// Published storage package IDs for package dependencies.
pub dependency_ids: Vec<types::Address>,
}