1use std::collections::HashMap;
7use std::time::Duration;
8
9#[derive(Debug, Clone, serde::Serialize)]
11pub struct FinalityConfig {
12 pub chain: String,
14 pub safe_confirmations: u64,
16 pub finalized_confirmations: u64,
18 pub block_time: Duration,
20 pub reorg_window: usize,
22 pub allows_slot_skipping: bool,
24 pub has_sequencer: bool,
26 pub settlement_chain: Option<String>,
28}
29
30impl FinalityConfig {
31 pub fn safe_time(&self) -> Duration {
33 self.block_time * self.safe_confirmations as u32
34 }
35
36 pub fn finalized_time(&self) -> Duration {
38 self.block_time * self.finalized_confirmations as u32
39 }
40}
41
42pub struct FinalityRegistry {
44 configs: HashMap<String, FinalityConfig>,
45}
46
47impl FinalityRegistry {
48 pub fn new() -> Self {
50 let mut configs = HashMap::new();
51
52 configs.insert(
54 "ethereum".into(),
55 FinalityConfig {
56 chain: "ethereum".into(),
57 safe_confirmations: 32, finalized_confirmations: 64, block_time: Duration::from_secs(12),
60 reorg_window: 128,
61 allows_slot_skipping: false,
62 has_sequencer: false,
63 settlement_chain: None,
64 },
65 );
66
67 configs.insert(
69 "polygon".into(),
70 FinalityConfig {
71 chain: "polygon".into(),
72 safe_confirmations: 128,
73 finalized_confirmations: 256,
74 block_time: Duration::from_secs(2),
75 reorg_window: 512,
76 allows_slot_skipping: false,
77 has_sequencer: false,
78 settlement_chain: Some("ethereum".into()),
79 },
80 );
81
82 configs.insert(
84 "arbitrum".into(),
85 FinalityConfig {
86 chain: "arbitrum".into(),
87 safe_confirmations: 0, finalized_confirmations: 1, block_time: Duration::from_millis(250),
90 reorg_window: 64,
91 allows_slot_skipping: false,
92 has_sequencer: true,
93 settlement_chain: Some("ethereum".into()),
94 },
95 );
96
97 configs.insert(
99 "optimism".into(),
100 FinalityConfig {
101 chain: "optimism".into(),
102 safe_confirmations: 0,
103 finalized_confirmations: 1,
104 block_time: Duration::from_secs(2),
105 reorg_window: 64,
106 allows_slot_skipping: false,
107 has_sequencer: true,
108 settlement_chain: Some("ethereum".into()),
109 },
110 );
111
112 configs.insert(
113 "base".into(),
114 FinalityConfig {
115 chain: "base".into(),
116 safe_confirmations: 0,
117 finalized_confirmations: 1,
118 block_time: Duration::from_secs(2),
119 reorg_window: 64,
120 allows_slot_skipping: false,
121 has_sequencer: true,
122 settlement_chain: Some("ethereum".into()),
123 },
124 );
125
126 configs.insert(
128 "bsc".into(),
129 FinalityConfig {
130 chain: "bsc".into(),
131 safe_confirmations: 15,
132 finalized_confirmations: 15,
133 block_time: Duration::from_secs(3),
134 reorg_window: 64,
135 allows_slot_skipping: false,
136 has_sequencer: false,
137 settlement_chain: None,
138 },
139 );
140
141 configs.insert(
143 "avalanche".into(),
144 FinalityConfig {
145 chain: "avalanche".into(),
146 safe_confirmations: 1, finalized_confirmations: 1,
148 block_time: Duration::from_secs(2),
149 reorg_window: 32,
150 allows_slot_skipping: false,
151 has_sequencer: false,
152 settlement_chain: None,
153 },
154 );
155
156 configs.insert(
158 "solana".into(),
159 FinalityConfig {
160 chain: "solana".into(),
161 safe_confirmations: 1, finalized_confirmations: 32, block_time: Duration::from_millis(400),
164 reorg_window: 256,
165 allows_slot_skipping: true,
166 has_sequencer: false,
167 settlement_chain: None,
168 },
169 );
170
171 configs.insert(
173 "fantom".into(),
174 FinalityConfig {
175 chain: "fantom".into(),
176 safe_confirmations: 1,
177 finalized_confirmations: 1,
178 block_time: Duration::from_secs(1),
179 reorg_window: 32,
180 allows_slot_skipping: false,
181 has_sequencer: false,
182 settlement_chain: None,
183 },
184 );
185
186 configs.insert(
188 "scroll".into(),
189 FinalityConfig {
190 chain: "scroll".into(),
191 safe_confirmations: 0,
192 finalized_confirmations: 1,
193 block_time: Duration::from_secs(3),
194 reorg_window: 64,
195 allows_slot_skipping: false,
196 has_sequencer: true,
197 settlement_chain: Some("ethereum".into()),
198 },
199 );
200
201 configs.insert(
203 "zksync".into(),
204 FinalityConfig {
205 chain: "zksync".into(),
206 safe_confirmations: 0,
207 finalized_confirmations: 1,
208 block_time: Duration::from_secs(1),
209 reorg_window: 64,
210 allows_slot_skipping: false,
211 has_sequencer: true,
212 settlement_chain: Some("ethereum".into()),
213 },
214 );
215
216 configs.insert(
218 "linea".into(),
219 FinalityConfig {
220 chain: "linea".into(),
221 safe_confirmations: 0,
222 finalized_confirmations: 1,
223 block_time: Duration::from_secs(12),
224 reorg_window: 64,
225 allows_slot_skipping: false,
226 has_sequencer: true,
227 settlement_chain: Some("ethereum".into()),
228 },
229 );
230
231 configs.insert(
233 "cosmoshub".into(),
234 FinalityConfig {
235 chain: "cosmoshub".into(),
236 safe_confirmations: 1, finalized_confirmations: 1,
238 block_time: Duration::from_secs(6),
239 reorg_window: 32,
240 allows_slot_skipping: false,
241 has_sequencer: false,
242 settlement_chain: None,
243 },
244 );
245
246 configs.insert(
248 "osmosis".into(),
249 FinalityConfig {
250 chain: "osmosis".into(),
251 safe_confirmations: 1,
252 finalized_confirmations: 1,
253 block_time: Duration::from_secs(6),
254 reorg_window: 32,
255 allows_slot_skipping: false,
256 has_sequencer: false,
257 settlement_chain: None,
258 },
259 );
260
261 configs.insert(
263 "polkadot".into(),
264 FinalityConfig {
265 chain: "polkadot".into(),
266 safe_confirmations: 1, finalized_confirmations: 1,
268 block_time: Duration::from_secs(6),
269 reorg_window: 32,
270 allows_slot_skipping: true, has_sequencer: false,
272 settlement_chain: None,
273 },
274 );
275
276 configs.insert(
278 "kusama".into(),
279 FinalityConfig {
280 chain: "kusama".into(),
281 safe_confirmations: 1,
282 finalized_confirmations: 1,
283 block_time: Duration::from_secs(6),
284 reorg_window: 32,
285 allows_slot_skipping: true,
286 has_sequencer: false,
287 settlement_chain: None,
288 },
289 );
290
291 configs.insert(
293 "bitcoin".into(),
294 FinalityConfig {
295 chain: "bitcoin".into(),
296 safe_confirmations: 3,
297 finalized_confirmations: 6, block_time: Duration::from_secs(600), reorg_window: 12,
300 allows_slot_skipping: false,
301 has_sequencer: false,
302 settlement_chain: None,
303 },
304 );
305
306 configs.insert(
308 "aptos".into(),
309 FinalityConfig {
310 chain: "aptos".into(),
311 safe_confirmations: 1, finalized_confirmations: 1,
313 block_time: Duration::from_secs(4),
314 reorg_window: 32,
315 allows_slot_skipping: false,
316 has_sequencer: false,
317 settlement_chain: None,
318 },
319 );
320
321 configs.insert(
323 "sui".into(),
324 FinalityConfig {
325 chain: "sui".into(),
326 safe_confirmations: 1, finalized_confirmations: 1,
328 block_time: Duration::from_millis(500), reorg_window: 64,
330 allows_slot_skipping: false,
331 has_sequencer: false,
332 settlement_chain: None,
333 },
334 );
335
336 Self { configs }
337 }
338
339 pub fn get(&self, chain: &str) -> Option<&FinalityConfig> {
341 self.configs.get(chain)
342 }
343
344 pub fn register(&mut self, config: FinalityConfig) {
346 self.configs.insert(config.chain.clone(), config);
347 }
348
349 pub fn chains(&self) -> Vec<&str> {
351 self.configs.keys().map(|s| s.as_str()).collect()
352 }
353
354 pub fn confirmation_depth(&self, chain: &str) -> u64 {
357 self.configs
358 .get(chain)
359 .map(|c| c.finalized_confirmations)
360 .unwrap_or(12)
361 }
362
363 pub fn reorg_window(&self, chain: &str) -> usize {
366 self.configs
367 .get(chain)
368 .map(|c| c.reorg_window)
369 .unwrap_or(128)
370 }
371
372 pub fn is_l2(&self, chain: &str) -> bool {
374 self.configs
375 .get(chain)
376 .map(|c| c.settlement_chain.is_some())
377 .unwrap_or(false)
378 }
379}
380
381impl Default for FinalityRegistry {
382 fn default() -> Self {
383 Self::new()
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test]
392 fn known_chains() {
393 let reg = FinalityRegistry::new();
394
395 let eth = reg.get("ethereum").expect("ethereum must exist");
397 assert_eq!(eth.safe_confirmations, 32);
398 assert_eq!(eth.finalized_confirmations, 64);
399 assert_eq!(eth.block_time, Duration::from_secs(12));
400 assert_eq!(eth.reorg_window, 128);
401 assert!(!eth.allows_slot_skipping);
402 assert!(!eth.has_sequencer);
403 assert!(eth.settlement_chain.is_none());
404
405 let poly = reg.get("polygon").expect("polygon must exist");
407 assert_eq!(poly.safe_confirmations, 128);
408 assert_eq!(poly.finalized_confirmations, 256);
409 assert_eq!(poly.block_time, Duration::from_secs(2));
410 assert_eq!(poly.settlement_chain.as_deref(), Some("ethereum"));
411
412 let arb = reg.get("arbitrum").expect("arbitrum must exist");
414 assert_eq!(arb.safe_confirmations, 0);
415 assert_eq!(arb.finalized_confirmations, 1);
416 assert!(arb.has_sequencer);
417 assert_eq!(arb.settlement_chain.as_deref(), Some("ethereum"));
418
419 let sol = reg.get("solana").expect("solana must exist");
421 assert_eq!(sol.safe_confirmations, 1);
422 assert_eq!(sol.finalized_confirmations, 32);
423 assert!(sol.allows_slot_skipping);
424 assert_eq!(sol.block_time, Duration::from_millis(400));
425 }
426
427 #[test]
428 fn confirmation_depth_known() {
429 let reg = FinalityRegistry::new();
430 assert_eq!(reg.confirmation_depth("ethereum"), 64);
431 }
432
433 #[test]
434 fn confirmation_depth_unknown() {
435 let reg = FinalityRegistry::new();
436 assert_eq!(reg.confirmation_depth("unknown_chain"), 12);
437 }
438
439 #[test]
440 fn custom_chain() {
441 let mut reg = FinalityRegistry::new();
442 reg.register(FinalityConfig {
443 chain: "mychain".into(),
444 safe_confirmations: 5,
445 finalized_confirmations: 10,
446 block_time: Duration::from_secs(6),
447 reorg_window: 50,
448 allows_slot_skipping: false,
449 has_sequencer: false,
450 settlement_chain: None,
451 });
452 let cfg = reg.get("mychain").expect("custom chain must exist");
453 assert_eq!(cfg.safe_confirmations, 5);
454 assert_eq!(cfg.finalized_confirmations, 10);
455 assert_eq!(cfg.block_time, Duration::from_secs(6));
456 assert_eq!(cfg.reorg_window, 50);
457 }
458
459 #[test]
460 fn is_l2() {
461 let reg = FinalityRegistry::new();
462 assert!(reg.is_l2("arbitrum"));
463 assert!(reg.is_l2("optimism"));
464 assert!(reg.is_l2("base"));
465 assert!(!reg.is_l2("ethereum"));
466 assert!(!reg.is_l2("solana"));
467 assert!(!reg.is_l2("unknown"));
468 }
469
470 #[test]
471 fn safe_time_calculation() {
472 let reg = FinalityRegistry::new();
473
474 let eth = reg.get("ethereum").unwrap();
476 assert_eq!(eth.safe_time(), Duration::from_secs(32 * 12));
477
478 assert_eq!(eth.finalized_time(), Duration::from_secs(64 * 12));
480
481 let arb = reg.get("arbitrum").unwrap();
483 assert_eq!(arb.safe_time(), Duration::ZERO);
484
485 let sol = reg.get("solana").unwrap();
487 assert_eq!(sol.safe_time(), Duration::from_millis(400));
488 }
489}