1use std::net::SocketAddr;
5use std::sync::Arc;
6use std::time::Duration;
7
8use anyhow::Context as _;
9use jsonrpsee::server::BatchRequestConfig;
10use jsonrpsee::server::RpcServiceBuilder;
11use jsonrpsee::server::ServerBuilder;
12use prometheus::Registry;
13use serde_json::json;
14use sui_futures::service::Service;
15use sui_indexer_alt_reader::consistent_reader::ConsistentReaderArgs;
16use sui_indexer_alt_reader::fullnode_client::FullnodeArgs;
17use sui_indexer_alt_reader::fullnode_client::FullnodeClient;
18use sui_indexer_alt_reader::kv_loader::KvArgs;
19use sui_indexer_alt_reader::pg_reader::db::DbArgs;
20use sui_indexer_alt_reader::system_package_task::SystemPackageTask;
21use sui_indexer_alt_reader::system_package_task::SystemPackageTaskArgs;
22use sui_open_rpc::Project;
23use tower_http::catch_panic;
24use tower_layer::Identity;
25use tracing::info;
26use tracing::warn;
27use url::Url;
28
29use crate::api::checkpoints::Checkpoints;
30use crate::api::coin::Coins;
31use crate::api::dynamic_fields::DynamicFields;
32use crate::api::governance::Governance;
33use crate::api::move_utils::MoveUtils;
34use crate::api::name_service::NameService;
35use crate::api::objects::Objects;
36use crate::api::objects::QueryObjects;
37use crate::api::protocol::Protocol;
38use crate::api::rpc_module::RpcModule;
39use crate::api::transactions::QueryTransactions;
40use crate::api::transactions::Transactions;
41use crate::api::write::Write;
42use crate::config::RpcConfig;
43use crate::context::Context;
44use crate::error::PanicHandler;
45use crate::metrics::RpcMetrics;
46use crate::metrics::middleware::MetricsLayer;
47use crate::timeout::TimeoutLayer;
48
49pub mod api;
50pub mod args;
51pub mod config;
52mod context;
53pub mod data;
54mod error;
55mod metrics;
56mod paginate;
57mod timeout;
58
59#[derive(clap::Args, Debug, Clone)]
60pub struct RpcArgs {
61 #[clap(long, default_value_t = Self::default().rpc_listen_address)]
63 pub rpc_listen_address: SocketAddr,
64
65 #[clap(long, default_value_t = Self::default().max_in_flight_requests)]
68 pub max_in_flight_requests: u32,
69
70 #[clap(long, default_value_t = Self::default().request_timeout_ms)]
73 pub request_timeout_ms: u64,
74
75 #[clap(long, default_value_t = Self::default().slow_request_threshold_ms)]
78 pub slow_request_threshold_ms: u64,
79}
80
81pub struct RpcService {
82 rpc_listen_address: SocketAddr,
84
85 server: ServerBuilder<Identity, Identity>,
87
88 metrics: Arc<RpcMetrics>,
90
91 request_timeout: Duration,
93
94 slow_request_threshold: Duration,
96
97 modules: jsonrpsee::RpcModule<()>,
99
100 schema: Project,
102}
103
104impl RpcArgs {
105 fn request_timeout(&self) -> Duration {
107 Duration::from_millis(self.request_timeout_ms)
108 }
109
110 fn slow_request_threshold(&self) -> Duration {
113 Duration::from_millis(self.slow_request_threshold_ms)
114 }
115}
116
117impl RpcService {
118 pub fn new(rpc_args: RpcArgs, registry: &Registry) -> anyhow::Result<Self> {
121 let metrics = RpcMetrics::new(registry);
122
123 let server = ServerBuilder::new()
124 .http_only()
125 .max_connections(rpc_args.max_in_flight_requests)
128 .max_response_body_size(u32::MAX)
129 .set_batch_request_config(BatchRequestConfig::Disabled);
130
131 let schema = Project::new(
132 env!("CARGO_PKG_VERSION"),
133 "Sui JSON-RPC",
134 "A JSON-RPC API for interacting with the Sui blockchain.",
135 "Mysten Labs",
136 "https://mystenlabs.com",
137 "build@mystenlabs.com",
138 "Apache-2.0",
139 "https://raw.githubusercontent.com/MystenLabs/sui/main/LICENSE",
140 );
141
142 Ok(Self {
143 rpc_listen_address: rpc_args.rpc_listen_address,
144 server,
145 metrics,
146 request_timeout: rpc_args.request_timeout(),
147 slow_request_threshold: rpc_args.slow_request_threshold(),
148 modules: jsonrpsee::RpcModule::new(()),
149 schema,
150 })
151 }
152
153 pub fn metrics(&self) -> Arc<RpcMetrics> {
155 self.metrics.clone()
156 }
157
158 pub fn add_module(&mut self, module: impl RpcModule) -> anyhow::Result<()> {
161 self.schema.add_module(module.schema());
162 self.modules
163 .merge(module.into_impl().remove_context())
164 .context("Failed to add module because of a name conflict")
165 }
166
167 pub async fn run(self) -> anyhow::Result<Service> {
170 let Self {
171 rpc_listen_address,
172 server,
173 metrics,
174 request_timeout,
175 slow_request_threshold,
176 mut modules,
177 schema,
178 } = self;
179
180 info!("Starting JSON-RPC service on {rpc_listen_address}",);
181 info!("Serving schema: {}", serde_json::to_string_pretty(&schema)?);
182
183 modules
185 .register_method("rpc.discover", move |_, _, _| json!(schema.clone()))
186 .context("Failed to add schema discovery method")?;
187
188 let middleware = RpcServiceBuilder::new()
189 .layer(TimeoutLayer::new(request_timeout))
190 .layer(MetricsLayer::new(
191 metrics.clone(),
192 modules.method_names().map(|n| n.to_owned()).collect(),
193 slow_request_threshold,
194 ));
195
196 let handle = server
197 .set_rpc_middleware(middleware)
198 .set_http_middleware(
199 tower::builder::ServiceBuilder::new()
200 .layer(
201 tower_http::cors::CorsLayer::new()
202 .allow_methods([http::Method::GET, http::Method::POST])
203 .allow_origin(tower_http::cors::Any)
204 .allow_headers(tower_http::cors::Any),
205 )
206 .layer(catch_panic::CatchPanicLayer::custom(PanicHandler::new(
207 metrics,
208 ))),
209 )
210 .build(rpc_listen_address)
211 .await
212 .context("Failed to bind JSON-RPC service")?
213 .start(modules);
214
215 let signal = handle.clone();
216 Ok(Service::new()
217 .with_shutdown_signal(async move {
218 let _ = signal.stop();
219 })
220 .spawn(async move {
221 handle.stopped().await;
222 Ok(())
223 }))
224 }
225}
226
227impl Default for RpcArgs {
228 fn default() -> Self {
229 Self {
230 rpc_listen_address: "0.0.0.0:6000".parse().unwrap(),
231 max_in_flight_requests: 2000,
232 request_timeout_ms: 60_000,
233 slow_request_threshold_ms: 15_000,
234 }
235 }
236}
237
238#[derive(clap::Args, Debug, Clone, Default)]
240pub struct NodeArgs {
241 #[arg(long)]
243 pub fullnode_grpc_url: Option<String>,
244}
245
246pub async fn start_rpc(
261 database_url: Option<Url>,
262 db_args: DbArgs,
263 kv_args: KvArgs,
264 consistent_reader_args: ConsistentReaderArgs,
265 rpc_args: RpcArgs,
266 node_args: NodeArgs,
267 system_package_task_args: SystemPackageTaskArgs,
268 rpc_config: RpcConfig,
269 registry: &Registry,
270) -> anyhow::Result<Service> {
271 let mut rpc = RpcService::new(rpc_args, registry).context("Failed to create RPC service")?;
272
273 let fullnode_args = node_args
274 .fullnode_grpc_url
275 .as_deref()
276 .map(Url::parse)
277 .transpose()
278 .context("Invalid fullnode gRPC URL")?
279 .map(FullnodeArgs::new)
280 .unwrap_or_default();
281
282 let fullnode_client =
283 FullnodeClient::new(Some("jsonrpc_alt_fullnode"), fullnode_args, registry)
284 .await
285 .context("Failed to create fullnode gRPC client")?;
286
287 let context = Context::new(
288 database_url,
289 db_args,
290 kv_args,
291 consistent_reader_args,
292 fullnode_client.clone(),
293 rpc_config,
294 rpc.metrics(),
295 registry,
296 )
297 .await?;
298
299 let system_package_task = SystemPackageTask::new(
300 system_package_task_args,
301 context.pg_reader().clone(),
302 context.package_resolver().package_store().clone(),
303 );
304
305 rpc.add_module(Checkpoints(context.clone()))?;
306 rpc.add_module(Coins(context.clone()))?;
307 rpc.add_module(DynamicFields(context.clone()))?;
308 rpc.add_module(MoveUtils(context.clone()))?;
309 rpc.add_module(NameService(context.clone()))?;
310 rpc.add_module(Objects(context.clone()))?;
311 rpc.add_module(Protocol(context.clone()))?;
312 rpc.add_module(QueryObjects(context.clone()))?;
313 rpc.add_module(QueryTransactions(context.clone()))?;
314 rpc.add_module(Transactions(context.clone()))?;
315
316 if let Some(_fullnode_client) = fullnode_client {
317 rpc.add_module(Governance::new(context.clone()))?;
318 rpc.add_module(Write::new(context.clone()))?;
319 } else {
320 warn!("No fullnode grpc url provided, Write and Governance modules will not be added.");
321 }
322
323 let s_rpc = rpc.run().await.context("Failed to start RPC service")?;
324 let s_system_package_task = system_package_task.run();
325
326 Ok(s_rpc.attach(s_system_package_task))
327}
328
329#[cfg(test)]
330mod tests {
331 use std::collections::BTreeSet;
332 use std::net::IpAddr;
333 use std::net::Ipv4Addr;
334 use std::net::SocketAddr;
335 use std::time::Duration;
336
337 use jsonrpsee::core::RpcResult;
338 use jsonrpsee::proc_macros::rpc;
339 use jsonrpsee::types::error::INTERNAL_ERROR_CODE;
340 use jsonrpsee::types::error::METHOD_NOT_FOUND_CODE;
341 use reqwest::Client;
342 use serde_json::Value;
343 use serde_json::json;
344 use sui_open_rpc::Module;
345 use sui_open_rpc_macros::open_rpc;
346 use sui_pg_db::temp::get_available_port;
347
348 use super::*;
349
350 #[tokio::test]
351 async fn test_add_module() {
352 let mut rpc = test_service().await;
353
354 rpc.add_module(Foo).unwrap();
355
356 assert_eq!(
357 BTreeSet::from_iter(rpc.modules.method_names()),
358 BTreeSet::from_iter(["test_bar"]),
359 )
360 }
361
362 #[tokio::test]
363 async fn test_add_module_multiple_methods() {
364 let mut rpc = test_service().await;
365
366 rpc.add_module(Bar).unwrap();
367
368 assert_eq!(
369 BTreeSet::from_iter(rpc.modules.method_names()),
370 BTreeSet::from_iter(["test_bar", "test_baz"]),
371 )
372 }
373
374 #[tokio::test]
375 async fn test_add_multiple_modules() {
376 let mut rpc = test_service().await;
377
378 rpc.add_module(Foo).unwrap();
379 rpc.add_module(Baz).unwrap();
380
381 assert_eq!(
382 BTreeSet::from_iter(rpc.modules.method_names()),
383 BTreeSet::from_iter(["test_bar", "test_baz"]),
384 )
385 }
386
387 #[tokio::test]
388 async fn test_add_module_conflict() {
389 let mut rpc = test_service().await;
390
391 rpc.add_module(Foo).unwrap();
392 assert!(rpc.add_module(Bar).is_err(),)
393 }
394
395 #[tokio::test]
396 async fn test_graceful_shutdown() {
397 let rpc = test_service().await;
398 let svc = rpc.run().await.unwrap();
399
400 tokio::time::timeout(Duration::from_millis(500), svc.shutdown())
401 .await
402 .expect("Shutdown should not timeout")
403 .expect("Shutdown should succeed");
404 }
405
406 #[tokio::test]
407 async fn test_rpc_discovery() {
408 let rpc_listen_address = test_listen_address();
409 let mut rpc = RpcService::new(
410 RpcArgs {
411 rpc_listen_address,
412 ..Default::default()
413 },
414 &Registry::new(),
415 )
416 .unwrap();
417
418 rpc.add_module(Foo).unwrap();
419 rpc.add_module(Baz).unwrap();
420
421 let svc = rpc.run().await.unwrap();
422
423 let url = format!("http://{rpc_listen_address}/");
424 let client = Client::new();
425
426 let resp: Value = client
427 .post(&url)
428 .json(&json!({
429 "jsonrpc": "2.0",
430 "method": "rpc.discover",
431 "id": 1,
432 }))
433 .send()
434 .await
435 .expect("Request should succeed")
436 .json()
437 .await
438 .expect("Deserialization should succeed");
439
440 assert_eq!(resp["result"]["info"]["title"], "Sui JSON-RPC");
441 assert_eq!(
442 resp["result"]["methods"],
443 json!([
444 {
445 "name": "test_bar",
446 "tags": [{
447 "name": "Test API"
448 }],
449 "params": [],
450 "result": {
451 "name": "u64",
452 "required": true,
453 "schema": {
454 "type": "integer",
455 "format": "uint64",
456 "minimum": 0.0
457 }
458 }
459 },
460 {
461 "name": "test_baz",
462 "tags": [{
463 "name": "Test API"
464 }],
465 "params": [],
466 "result": {
467 "name": "u64",
468 "required": true,
469 "schema": {
470 "type": "integer",
471 "format": "uint64",
472 "minimum": 0.0
473 }
474 }
475 }
476 ])
477 );
478
479 tokio::time::timeout(Duration::from_millis(500), svc.shutdown())
480 .await
481 .expect("Shutdown should not timeout")
482 .expect("Shutdown should succeed");
483 }
484
485 #[tokio::test]
486 async fn test_request_metrics() {
487 let rpc_listen_address = test_listen_address();
488 let mut rpc = RpcService::new(
489 RpcArgs {
490 rpc_listen_address,
491 ..Default::default()
492 },
493 &Registry::new(),
494 )
495 .unwrap();
496
497 rpc.add_module(Foo).unwrap();
498
499 let metrics = rpc.metrics();
500 let svc = rpc.run().await.unwrap();
501
502 let url = format!("http://{rpc_listen_address}/");
503 let client = Client::new();
504
505 client
506 .post(&url)
507 .json(&json!({
508 "jsonrpc": "2.0",
509 "method": "test_bar",
510 "id": 1,
511 }))
512 .send()
513 .await
514 .expect("Request should succeed");
515
516 client
517 .post(&url)
518 .json(&json!({
519 "jsonrpc": "2.0",
520 "method": "test_baz",
521 "id": 1,
522 }))
523 .send()
524 .await
525 .expect("Request should succeed");
526
527 assert_eq!(
528 metrics
529 .requests_received
530 .with_label_values(&["test_bar"])
531 .get(),
532 1
533 );
534
535 assert_eq!(
536 metrics
537 .requests_succeeded
538 .with_label_values(&["test_bar"])
539 .get(),
540 1
541 );
542
543 assert_eq!(
544 metrics
545 .requests_received
546 .with_label_values(&["<UNKNOWN>"])
547 .get(),
548 1
549 );
550
551 assert_eq!(
552 metrics
553 .requests_succeeded
554 .with_label_values(&["<UNKNOWN>"])
555 .get(),
556 0
557 );
558
559 assert_eq!(
560 metrics
561 .requests_failed
562 .with_label_values(&["<UNKNOWN>", &format!("{METHOD_NOT_FOUND_CODE}")])
563 .get(),
564 1
565 );
566
567 tokio::time::timeout(Duration::from_millis(500), svc.shutdown())
568 .await
569 .expect("Shutdown should not timeout")
570 .expect("Shutdown should succeed");
571 }
572
573 #[tokio::test]
574 async fn test_panic_handling() {
575 let rpc_listen_address = test_listen_address();
576 let mut rpc = RpcService::new(
577 RpcArgs {
578 rpc_listen_address,
579 ..Default::default()
580 },
581 &Registry::new(),
582 )
583 .unwrap();
584
585 rpc.add_module(Panic).unwrap();
586
587 let metrics = rpc.metrics();
588 let svc = rpc.run().await.unwrap();
589
590 let url = format!("http://{rpc_listen_address}/");
591 let client = Client::new();
592
593 let resp = client
594 .post(&url)
595 .json(&json!({
596 "jsonrpc": "2.0",
597 "method": "test_panic",
598 "id": 1,
599 }))
600 .send()
601 .await
602 .expect("Request should succeed");
603
604 let body: Value = resp.json().await.expect("Response should be JSON");
605
606 assert_eq!(body["jsonrpc"], "2.0");
608 assert_eq!(body["error"]["code"], INTERNAL_ERROR_CODE);
609 assert!(body["error"]["message"].as_str().unwrap().contains("Boom!"));
610
611 assert_eq!(metrics.requests_panicked.get(), 1);
613
614 tokio::time::timeout(Duration::from_millis(500), svc.shutdown())
615 .await
616 .expect("Shutdown should not timeout")
617 .expect("Shutdown should succeed");
618 }
619
620 #[open_rpc(namespace = "test", tag = "Test API")]
623 #[rpc(server, namespace = "test")]
624 trait FooApi {
625 #[method(name = "bar")]
626 fn bar(&self) -> RpcResult<u64>;
627 }
628
629 #[open_rpc(namespace = "test", tag = "Test API")]
630 #[rpc(server, namespace = "test")]
631 trait BarApi {
632 #[method(name = "bar")]
633 fn bar(&self) -> RpcResult<u64>;
634
635 #[method(name = "baz")]
636 fn baz(&self) -> RpcResult<u64>;
637 }
638
639 #[open_rpc(namespace = "test", tag = "Test API")]
640 #[rpc(server, namespace = "test")]
641 trait BazApi {
642 #[method(name = "baz")]
643 fn baz(&self) -> RpcResult<u64>;
644 }
645
646 #[open_rpc(namespace = "test", tag = "Test API")]
647 #[rpc(server, namespace = "test")]
648 trait PanicApi {
649 #[method(name = "panic")]
650 fn panic(&self) -> RpcResult<u64>;
651 }
652
653 struct Foo;
654 struct Bar;
655 struct Baz;
656 struct Panic;
657
658 impl FooApiServer for Foo {
659 fn bar(&self) -> RpcResult<u64> {
660 Ok(42)
661 }
662 }
663
664 impl BarApiServer for Bar {
665 fn bar(&self) -> RpcResult<u64> {
666 Ok(43)
667 }
668
669 fn baz(&self) -> RpcResult<u64> {
670 Ok(44)
671 }
672 }
673
674 impl BazApiServer for Baz {
675 fn baz(&self) -> RpcResult<u64> {
676 Ok(45)
677 }
678 }
679
680 impl PanicApiServer for Panic {
681 fn panic(&self) -> RpcResult<u64> {
682 panic!("Boom!");
683 }
684 }
685
686 impl RpcModule for Foo {
687 fn schema(&self) -> Module {
688 FooApiOpenRpc::module_doc()
689 }
690
691 fn into_impl(self) -> jsonrpsee::RpcModule<Self> {
692 self.into_rpc()
693 }
694 }
695
696 impl RpcModule for Bar {
697 fn schema(&self) -> Module {
698 BarApiOpenRpc::module_doc()
699 }
700
701 fn into_impl(self) -> jsonrpsee::RpcModule<Self> {
702 self.into_rpc()
703 }
704 }
705
706 impl RpcModule for Baz {
707 fn schema(&self) -> Module {
708 BazApiOpenRpc::module_doc()
709 }
710
711 fn into_impl(self) -> jsonrpsee::RpcModule<Self> {
712 self.into_rpc()
713 }
714 }
715
716 impl RpcModule for Panic {
717 fn schema(&self) -> Module {
718 PanicApiOpenRpc::module_doc()
719 }
720
721 fn into_impl(self) -> jsonrpsee::RpcModule<Self> {
722 self.into_rpc()
723 }
724 }
725
726 fn test_listen_address() -> SocketAddr {
727 let port = get_available_port();
728 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)
729 }
730
731 async fn test_service() -> RpcService {
732 RpcService::new(
733 RpcArgs {
734 rpc_listen_address: test_listen_address(),
735 ..Default::default()
736 },
737 &Registry::new(),
738 )
739 .expect("Failed to create test JSON-RPC service")
740 }
741}