Skip to main content

sui_indexer_alt_jsonrpc/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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    /// Address to listen to for incoming JSON-RPC connections.
62    #[clap(long, default_value_t = Self::default().rpc_listen_address)]
63    pub rpc_listen_address: SocketAddr,
64
65    /// The maximum number of concurrent requests to accept. If the service receives more than this
66    /// many requests, it will start responding with 429.
67    #[clap(long, default_value_t = Self::default().max_in_flight_requests)]
68    pub max_in_flight_requests: u32,
69
70    /// Requests that take longer than this (in milliseconds) to respond to will be terminated, and
71    /// the query itself will be logged as a warning.
72    #[clap(long, default_value_t = Self::default().request_timeout_ms)]
73    pub request_timeout_ms: u64,
74
75    /// Requests that take longer than this (in milliseconds) will be logged even if they succeed.
76    /// This should be shorter than `request_timeout_ms`.
77    #[clap(long, default_value_t = Self::default().slow_request_threshold_ms)]
78    pub slow_request_threshold_ms: u64,
79}
80
81pub struct RpcService {
82    /// The address that the server will start listening for requests on, when it is run.
83    rpc_listen_address: SocketAddr,
84
85    /// A partially built/configured JSON-RPC server.
86    server: ServerBuilder<Identity, Identity>,
87
88    /// Metrics for the RPC service.
89    metrics: Arc<RpcMetrics>,
90
91    /// Maximum time a request can take to complete.
92    request_timeout: Duration,
93
94    /// Threshold for logging slow requests.
95    slow_request_threshold: Duration,
96
97    /// All the methods added to the server so far.
98    modules: jsonrpsee::RpcModule<()>,
99
100    /// Description of the schema served by this service.
101    schema: Project,
102}
103
104impl RpcArgs {
105    /// Requests that take longer than this are terminated and logged for debugging.
106    fn request_timeout(&self) -> Duration {
107        Duration::from_millis(self.request_timeout_ms)
108    }
109
110    /// Requests that take longer than this are logged for debugging even if they succeed.
111    /// This threshold should be lower than the request timeout threshold.
112    fn slow_request_threshold(&self) -> Duration {
113        Duration::from_millis(self.slow_request_threshold_ms)
114    }
115}
116
117impl RpcService {
118    /// Create a new instance of the JSON-RPC service, configured by `rpc_args`. The service will
119    /// not accept connections until [Self::run] is called.
120    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            // `jsonrpsee` calls this a limit on connections, but it is implemented as a limit on
126            // requests.
127            .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    /// Return a copy of the metrics.
154    pub fn metrics(&self) -> Arc<RpcMetrics> {
155        self.metrics.clone()
156    }
157
158    /// Add an `RpcModule` to the service. The module's methods are combined with the existing
159    /// methods registered on the service, and the operation will fail if there is any overlap.
160    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    /// Start the service (it will accept connections) and return a handle that tracks the
168    /// lifecycle of the service.
169    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        // Add a method to serve the schema to clients.
184        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/// Configuration for the fullnode RPC that this service will connect to.
239#[derive(clap::Args, Debug, Clone, Default)]
240pub struct NodeArgs {
241    /// The URL of the fullnode gRPC service, used for transaction execution and dry-running.
242    #[arg(long)]
243    pub fullnode_grpc_url: Option<String>,
244}
245
246/// Set-up and run the RPC service, using the provided arguments (expected to be extracted from the
247/// command-line).
248///
249/// Access to most reads is controlled by the `database_url` -- if it is `None`, reads will not
250/// work.
251///
252/// KV queries can optionally be served by a Ledger gRPC service, if `kv_args.ledger_grpc_url` is
253/// provided. Otherwise these requests are served by the database.
254///
255/// Access to writes (executing and dry-running transactions) is controlled by
256/// `node_args.fullnode_grpc_url`, which can be omitted to disable writes from this RPC.
257///
258/// The service may spin up auxiliary services (such as the system package task) to support itself,
259/// and will clean these up on shutdown as well.
260pub 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        // Verify the response is a JSON-RPC error
607        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        // Verify the panic is recorded in metrics
612        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    // Test Helpers
621
622    #[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}