1use std::net::SocketAddr;
5use std::time::Duration;
6
7#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
8#[serde(rename_all = "kebab-case")]
9pub struct RpcConfig {
10 #[serde(skip_serializing_if = "Option::is_none")]
20 pub enable_indexing: Option<bool>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
26 pub https_address: Option<SocketAddr>,
27
28 #[serde(skip_serializing_if = "Option::is_none")]
32 pub tls: Option<RpcTlsConfig>,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
41 pub max_json_move_value_size: Option<usize>,
42
43 #[serde(skip_serializing_if = "Option::is_none")]
52 pub max_json_move_value_response_size: Option<usize>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub index_initialization: Option<RpcIndexInitConfig>,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
63 pub ledger_history: Option<LedgerHistoryConfig>,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
70 pub subscription_watermark_interval: Option<u32>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub subscription_max_subscribers: Option<usize>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
81 pub subscription_shards: Option<u32>,
82
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub display: Option<DisplayConfig>,
86
87 #[serde(skip_serializing_if = "Option::is_none")]
97 pub max_connection_age_secs: Option<u64>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
108 pub max_connection_age_grace_secs: Option<u64>,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
118 pub grpc_timeout_ms: Option<u64>,
119}
120
121const DEFAULT_MAX_CONNECTION_AGE: Duration = Duration::from_secs(4 * 60 * 60);
122const DEFAULT_MAX_CONNECTION_AGE_GRACE: Duration = Duration::from_secs(10 * 60);
123const DEFAULT_GRPC_TIMEOUT: Duration = Duration::from_secs(60);
124
125impl RpcConfig {
126 pub fn enable_indexing(&self) -> bool {
127 self.enable_indexing.unwrap_or(false)
128 }
129
130 pub fn https_address(&self) -> SocketAddr {
131 self.https_address
132 .unwrap_or_else(|| SocketAddr::from(([0, 0, 0, 0], 9443)))
133 }
134
135 pub fn tls_config(&self) -> Option<&RpcTlsConfig> {
136 self.tls.as_ref()
137 }
138
139 pub fn max_json_move_value_size(&self) -> usize {
140 self.max_json_move_value_size.unwrap_or(1024 * 1024)
141 }
142
143 pub fn max_json_move_value_response_size(&self) -> usize {
144 self.max_json_move_value_response_size
145 .unwrap_or(16 * 1024 * 1024)
146 }
147
148 pub fn index_initialization_config(&self) -> Option<&RpcIndexInitConfig> {
149 self.index_initialization.as_ref()
150 }
151
152 pub fn ledger_history(&self) -> &LedgerHistoryConfig {
153 const DEFAULT_LEDGER_HISTORY_CONFIG: LedgerHistoryConfig = LedgerHistoryConfig {
154 list_transactions: None,
155 list_events: None,
156 list_checkpoints: None,
157 bitmap_bucket_scan_budget: None,
158 chunk_bucket_scan_budget: None,
159 max_bitmap_filter_literals: None,
160 };
161
162 self.ledger_history
163 .as_ref()
164 .unwrap_or(&DEFAULT_LEDGER_HISTORY_CONFIG)
165 }
166
167 pub fn max_connection_age(&self) -> Option<Duration> {
169 match self.max_connection_age_secs {
170 Some(0) => None,
171 Some(secs) => Some(Duration::from_secs(secs)),
172 None => Some(DEFAULT_MAX_CONNECTION_AGE),
173 }
174 }
175
176 pub fn max_connection_age_grace(&self) -> Duration {
179 self.max_connection_age_grace_secs
180 .map(Duration::from_secs)
181 .unwrap_or(DEFAULT_MAX_CONNECTION_AGE_GRACE)
182 }
183
184 pub fn grpc_timeout(&self) -> Option<Duration> {
187 match self.grpc_timeout_ms {
188 Some(0) => None,
189 Some(ms) => Some(Duration::from_millis(ms)),
190 None => Some(DEFAULT_GRPC_TIMEOUT),
191 }
192 }
193
194 pub fn validate(&self) -> anyhow::Result<()> {
197 self.ledger_history().validate()
198 }
199
200 pub fn display(&self) -> &DisplayConfig {
201 const DEFAULT_DISPLAY_CONFIG: DisplayConfig = DisplayConfig {
202 max_field_depth: None,
203 max_format_nodes: None,
204 max_object_loads: None,
205 max_move_value_depth: None,
206 max_output_size: None,
207 };
208
209 self.display.as_ref().unwrap_or(&DEFAULT_DISPLAY_CONFIG)
210 }
211}
212
213#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
214#[serde(rename_all = "kebab-case")]
215pub struct RpcTlsConfig {
216 cert: String,
218 key: String,
220}
221
222impl RpcTlsConfig {
223 pub fn cert(&self) -> &str {
224 &self.cert
225 }
226
227 pub fn key(&self) -> &str {
228 &self.key
229 }
230}
231
232#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
234#[serde(rename_all = "kebab-case")]
235pub struct RpcIndexInitConfig {
236 #[serde(skip_serializing_if = "Option::is_none")]
241 pub db_write_buffer_size: Option<usize>,
242
243 #[serde(skip_serializing_if = "Option::is_none")]
247 pub cf_write_buffer_size: Option<usize>,
248
249 #[serde(skip_serializing_if = "Option::is_none")]
254 pub cf_max_write_buffer_number: Option<i32>,
255
256 #[serde(skip_serializing_if = "Option::is_none")]
260 pub max_background_jobs: Option<i32>,
261
262 #[serde(skip_serializing_if = "Option::is_none")]
267 pub batch_size_limit: Option<usize>,
268}
269
270#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
271#[serde(rename_all = "kebab-case")]
272pub struct DisplayConfig {
273 #[serde(skip_serializing_if = "Option::is_none")]
278 max_field_depth: Option<usize>,
279
280 #[serde(skip_serializing_if = "Option::is_none")]
285 max_format_nodes: Option<usize>,
286
287 #[serde(skip_serializing_if = "Option::is_none")]
291 max_object_loads: Option<usize>,
292
293 #[serde(skip_serializing_if = "Option::is_none")]
297 max_move_value_depth: Option<usize>,
298
299 #[serde(skip_serializing_if = "Option::is_none")]
306 max_output_size: Option<usize>,
307}
308
309impl DisplayConfig {
310 pub fn max_field_depth(&self) -> usize {
311 self.max_field_depth.unwrap_or(32)
312 }
313
314 pub fn max_format_nodes(&self) -> usize {
315 self.max_format_nodes.unwrap_or(32768)
316 }
317
318 pub fn max_object_loads(&self) -> usize {
319 self.max_object_loads.unwrap_or(8)
320 }
321
322 pub fn max_move_value_depth(&self) -> usize {
323 self.max_move_value_depth.unwrap_or(32)
324 }
325
326 pub fn max_output_size(&self) -> usize {
327 self.max_output_size.unwrap_or(1024 * 1024)
328 }
329}
330
331#[cfg(test)]
332mod connection_lifecycle_tests {
333 use super::*;
334
335 #[test]
341 fn connection_lifecycle_defaults_are_enabled() {
342 let config = RpcConfig::default();
343 assert_eq!(
344 config.max_connection_age(),
345 Some(Duration::from_secs(4 * 60 * 60))
346 );
347 assert_eq!(
348 config.max_connection_age_grace(),
349 Duration::from_secs(10 * 60)
350 );
351 assert_eq!(config.grpc_timeout(), Some(Duration::from_secs(60)));
352 }
353
354 #[test]
355 fn zero_disables_age_and_timeout() {
356 let config = RpcConfig {
357 max_connection_age_secs: Some(0),
358 max_connection_age_grace_secs: Some(0),
359 grpc_timeout_ms: Some(0),
360 ..Default::default()
361 };
362 assert_eq!(config.max_connection_age(), None);
363 assert_eq!(config.max_connection_age_grace(), Duration::ZERO);
365 assert_eq!(config.grpc_timeout(), None);
366 }
367
368 #[test]
369 fn explicit_values_are_used() {
370 let config = RpcConfig {
371 max_connection_age_secs: Some(60),
372 max_connection_age_grace_secs: Some(5),
373 grpc_timeout_ms: Some(1_500),
374 ..Default::default()
375 };
376 assert_eq!(config.max_connection_age(), Some(Duration::from_secs(60)));
377 assert_eq!(config.max_connection_age_grace(), Duration::from_secs(5));
378 assert_eq!(config.grpc_timeout(), Some(Duration::from_millis(1_500)));
379 }
380}
381
382const DEFAULT_LEDGER_HISTORY_METHOD_TIMEOUT_MS: u64 = 5_000;
383const DEFAULT_BITMAP_BUCKET_SCAN_BUDGET: usize = 1_024;
384const DEFAULT_CHUNK_BUCKET_SCAN_BUDGET: usize = 256;
385const DEFAULT_MAX_BITMAP_FILTER_LITERALS: usize = 10;
386const _: () = assert!(DEFAULT_CHUNK_BUCKET_SCAN_BUDGET <= DEFAULT_BITMAP_BUCKET_SCAN_BUDGET);
390
391struct LedgerHistoryMethodDefaults {
394 default_limit_items: u32,
395 max_limit_items: u32,
396 chunk_max: usize,
397}
398
399const LIST_TRANSACTIONS_DEFAULTS: LedgerHistoryMethodDefaults = LedgerHistoryMethodDefaults {
400 default_limit_items: 50,
401 max_limit_items: 500,
402 chunk_max: 32,
403};
404const LIST_EVENTS_DEFAULTS: LedgerHistoryMethodDefaults = LedgerHistoryMethodDefaults {
405 default_limit_items: 50,
406 max_limit_items: 1_000,
407 chunk_max: 32,
408};
409const LIST_CHECKPOINTS_DEFAULTS: LedgerHistoryMethodDefaults = LedgerHistoryMethodDefaults {
410 default_limit_items: 10,
411 max_limit_items: 50,
412 chunk_max: 16,
413};
414
415#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
418#[serde(rename_all = "kebab-case")]
419pub struct LedgerHistoryMethodConfig {
420 #[serde(skip_serializing_if = "Option::is_none")]
422 pub timeout_ms: Option<u64>,
423
424 #[serde(skip_serializing_if = "Option::is_none")]
426 pub default_limit_items: Option<u32>,
427
428 #[serde(skip_serializing_if = "Option::is_none")]
430 pub max_limit_items: Option<u32>,
431
432 #[serde(skip_serializing_if = "Option::is_none")]
434 pub chunk_max: Option<usize>,
435}
436
437#[derive(Clone, Copy, Debug)]
439pub struct ResolvedLedgerHistoryMethodConfig {
440 pub timeout: Duration,
441 pub default_limit_items: u32,
442 pub max_limit_items: u32,
443 pub chunk_max: usize,
444}
445
446impl LedgerHistoryMethodConfig {
447 fn resolve(
448 this: Option<&LedgerHistoryMethodConfig>,
449 defaults: LedgerHistoryMethodDefaults,
450 ) -> ResolvedLedgerHistoryMethodConfig {
451 ResolvedLedgerHistoryMethodConfig {
452 timeout: Duration::from_millis(
453 this.and_then(|c| c.timeout_ms)
454 .unwrap_or(DEFAULT_LEDGER_HISTORY_METHOD_TIMEOUT_MS),
455 ),
456 default_limit_items: this
457 .and_then(|c| c.default_limit_items)
458 .unwrap_or(defaults.default_limit_items),
459 max_limit_items: this
460 .and_then(|c| c.max_limit_items)
461 .unwrap_or(defaults.max_limit_items),
462 chunk_max: this.and_then(|c| c.chunk_max).unwrap_or(defaults.chunk_max),
463 }
464 }
465}
466
467#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
471#[serde(rename_all = "kebab-case")]
472pub struct LedgerHistoryConfig {
473 #[serde(skip_serializing_if = "Option::is_none")]
475 pub list_transactions: Option<LedgerHistoryMethodConfig>,
476
477 #[serde(skip_serializing_if = "Option::is_none")]
479 pub list_events: Option<LedgerHistoryMethodConfig>,
480
481 #[serde(skip_serializing_if = "Option::is_none")]
483 pub list_checkpoints: Option<LedgerHistoryMethodConfig>,
484
485 #[serde(skip_serializing_if = "Option::is_none")]
491 pub bitmap_bucket_scan_budget: Option<usize>,
492
493 #[serde(skip_serializing_if = "Option::is_none")]
500 pub chunk_bucket_scan_budget: Option<usize>,
501
502 #[serde(skip_serializing_if = "Option::is_none")]
509 pub max_bitmap_filter_literals: Option<usize>,
510}
511
512impl LedgerHistoryConfig {
513 pub fn list_transactions(&self) -> ResolvedLedgerHistoryMethodConfig {
514 LedgerHistoryMethodConfig::resolve(
515 self.list_transactions.as_ref(),
516 LIST_TRANSACTIONS_DEFAULTS,
517 )
518 }
519
520 pub fn list_events(&self) -> ResolvedLedgerHistoryMethodConfig {
521 LedgerHistoryMethodConfig::resolve(self.list_events.as_ref(), LIST_EVENTS_DEFAULTS)
522 }
523
524 pub fn list_checkpoints(&self) -> ResolvedLedgerHistoryMethodConfig {
525 LedgerHistoryMethodConfig::resolve(
526 self.list_checkpoints.as_ref(),
527 LIST_CHECKPOINTS_DEFAULTS,
528 )
529 }
530
531 pub fn bitmap_bucket_scan_budget(&self) -> usize {
532 self.bitmap_bucket_scan_budget
533 .unwrap_or(DEFAULT_BITMAP_BUCKET_SCAN_BUDGET)
534 }
535
536 pub fn chunk_bucket_scan_budget(&self) -> usize {
537 self.chunk_bucket_scan_budget
538 .unwrap_or(DEFAULT_CHUNK_BUCKET_SCAN_BUDGET)
539 .min(self.bitmap_bucket_scan_budget())
540 }
541
542 pub fn max_bitmap_filter_literals(&self) -> usize {
543 self.max_bitmap_filter_literals
544 .unwrap_or(DEFAULT_MAX_BITMAP_FILTER_LITERALS)
545 }
546
547 pub fn validate(&self) -> anyhow::Result<()> {
554 anyhow::ensure!(
555 self.bitmap_bucket_scan_budget() >= self.max_bitmap_filter_literals(),
556 "ledger_history.bitmap_bucket_scan_budget ({}) must be >= \
557 max_bitmap_filter_literals ({}) so every filter leaf gets at least one \
558 bucket before SCAN_LIMIT",
559 self.bitmap_bucket_scan_budget(),
560 self.max_bitmap_filter_literals(),
561 );
562 Ok(())
563 }
564}