avail_rust_client/subscription/
sub.rs

1use super::should_retry;
2use crate::{BlockInfo, Client, H256, RpcError, platform::sleep};
3use std::time::Duration;
4
5/// The [Sub] subscription behaves as follows by default:
6///
7/// **Defaults**
8/// - Tracks **finalized blocks**.  
9///   → To track best (non-finalized) blocks instead, call: `sub.use_best_block(true)`
10/// - Starts from the **latest** finalized (or best) block.  
11///   → To start from a specific height, call: `sub.set_block_height(height)`
12/// - **Retries** failed RPC calls automatically.  
13///   → To disable retries, call: `sub.set_retry_on_error(false)`
14/// - Polls for new block information every **3 seconds**.  
15///   → To change the interval, call: `sub.set_pool_rate(Duration)`
16///
17/// **Fetching methods**
18/// - `sub.next()` → Returns the **next block reference** `(hash, height)`.  
19///   - If you’ve already fetched a block, this moves forward.  
20///   - If you set a starting height, it begins from there.  
21///   - Otherwise, it starts at the latest finalized (or best) block.
22/// - `sub.prev()` → Returns the **previous block reference** `(hash, height)`.  
23///   - If you set a starting height, it begins from `(height - 1)`.  
24///   - Otherwise, it starts from `(latest finalized/best height - 1)`.
25///
26/// **State**
27/// - The initial state is `UnInit`.  
28/// - After the first call to `next()` or `prev()`, the state changes to either:  
29///   - `FinalizedBlock` (default), or  
30///   - `BestBlock` (if `sub.use_best_block(true)` was called).   
31/// - Once initialized, calling `use_best_block(...)` has **no effect**.
32///
33/// # Example
34/// ```rust
35#[doc = include_str!("../../examples/sub_doc.rs")]
36/// ```
37#[derive(Clone)]
38pub enum Sub {
39	UnInit(UnInitSub),
40	BestBlock(BestBlockSub),
41	FinalizedBlock(FinalizedBlockSub),
42}
43
44impl Sub {
45	/// Creates a new lazy subscription using the provided `client`.
46	pub fn new(client: Client) -> Self {
47		Self::UnInit(UnInitSub::new(client))
48	}
49
50	/// Returns the **next block reference** `(hash, height)`.
51	///
52	/// - If you’ve already called [`Sub::next`] or [`Sub::prev`] once, this moves forward.
53	/// - If you set a starting height via [`Sub::set_block_height`], it begins from there.
54	/// - Otherwise, it starts at the latest finalized (or best) block depending on
55	///   [`Sub::use_best_block`].
56	///
57	/// # Errors
58	/// Returns `Err(RpcError)` when the underlying RPC requests fail; the internal cursor remains
59	/// unchanged so the next call can retry.
60	pub async fn next(&mut self) -> Result<BlockInfo, RpcError> {
61		if let Self::UnInit(u) = self {
62			let concrete = u.build().await?;
63			*self = concrete;
64		};
65
66		match self {
67			Self::BestBlock(s) => s.next().await,
68			Self::FinalizedBlock(s) => s.next().await,
69			_ => unreachable!("We cannot be here."),
70		}
71	}
72
73	/// Returns the **previous block reference** `(hash, height)`.
74	///
75	/// - If you’ve already called [`Sub::next`] or [`Sub::prev`] once, this moves backwards.
76	/// - If you set a starting height via [`Sub::set_block_height`], it begins from `(height - 1)`.
77	/// - Otherwise, it starts from `(latest finalized/best height - 1)`.
78	/// # Errors
79	/// Returns `Err(RpcError)` when the underlying RPC interaction fails; the cursor is left at the
80	/// same position so the call can be retried.
81	pub async fn prev(&mut self) -> Result<BlockInfo, RpcError> {
82		if let Self::UnInit(u) = self {
83			let concrete = u.build().await?;
84			*self = concrete;
85		};
86
87		match self {
88			Self::BestBlock(s) => s.prev().await,
89			Self::FinalizedBlock(s) => s.prev().await,
90			_ => unreachable!("We cannot be here."),
91		}
92	}
93
94	/// Returns `true` when RPC calls should be retried after failures.
95	///
96	/// The decision honors any explicit override configured via [`Sub::set_retry_on_error`]
97	/// and falls back to the client's default retry policy when no override is provided.
98	pub fn should_retry_on_error(&self) -> bool {
99		let value = match self {
100			Self::UnInit(u) => u.retry_on_error,
101			Self::BestBlock(s) => s.retry_on_error,
102			Self::FinalizedBlock(s) => s.retry_on_error,
103		};
104
105		should_retry(self.client_ref(), value)
106	}
107
108	/// Switches the subscription mode based on `value`.
109	///
110	/// - `true` → Track best (non-finalized) blocks instead of finalized ones.
111	/// - `false` → Stick with the default of finalized blocks.
112	///
113	/// This configuration must be applied before the subscription is initialized by a call to
114	/// [`Sub::next`] or [`Sub::prev`]; later calls have no effect.
115	pub fn use_best_block(&mut self, value: bool) {
116		if let Self::UnInit(u) = self {
117			u.use_best_block = value;
118		}
119	}
120
121	/// Sets the starting block height according to `value`.
122	///
123	/// Subsequent calls to [`Sub::next`] and [`Sub::prev`] honour this height, rewinding or
124	/// fast-forwarding the internal cursor so iteration resumes relative to `value`.
125	pub fn set_block_height(&mut self, value: u32) {
126		match self {
127			Self::UnInit(u) => u.block_height = Some(value),
128			Self::BestBlock(x) => {
129				x.current_block_height = value;
130				x.block_processed.clear();
131			},
132			Self::FinalizedBlock(x) => {
133				x.next_block_height = value;
134				x.processed_previous_block = false;
135			},
136		}
137	}
138
139	/// Updates the polling interval using the provided `value`.
140	///
141	/// The change takes effect immediately and drives how often the subscription waits before
142	/// attempting to fetch fresh block data.
143	pub fn set_pool_rate(&mut self, value: Duration) {
144		match self {
145			Self::UnInit(u) => u.poll_rate = value,
146			Self::BestBlock(x) => x.poll_rate = value,
147			Self::FinalizedBlock(x) => x.poll_rate = value,
148		}
149	}
150
151	/// Controls retry behaviour for this subscription based on `value`.
152	///
153	/// - `Some(true)` → Always retry failed RPC calls.
154	/// - `Some(false)` → Never retry failed RPC calls.
155	/// - `None` → Defer to the [`Client`]'s global retry setting.
156	pub fn set_retry_on_error(&mut self, value: Option<bool>) {
157		match self {
158			Self::UnInit(u) => u.retry_on_error = value,
159			Self::BestBlock(x) => x.retry_on_error = value,
160			Self::FinalizedBlock(x) => x.retry_on_error = value,
161		}
162	}
163
164	pub(crate) fn client_ref(&self) -> &Client {
165		match self {
166			Sub::UnInit(x) => &x.client,
167			Sub::BestBlock(x) => &x.client,
168			Sub::FinalizedBlock(x) => &x.client,
169		}
170	}
171
172	#[cfg(test)]
173	pub(crate) fn as_finalized(&self) -> &FinalizedBlockSub {
174		if let Self::FinalizedBlock(f) = self {
175			return f;
176		}
177		panic!("Not Finalized Sub");
178	}
179}
180
181/// Dummy subscription. Not meant to be used directly.
182///
183/// Use [`Sub`] instead.
184#[derive(Clone)]
185pub struct UnInitSub {
186	client: Client,
187	use_best_block: bool,
188	block_height: Option<u32>,
189	poll_rate: Duration,
190	retry_on_error: Option<bool>,
191}
192
193impl UnInitSub {
194	/// Creates an uninitialised subscription placeholder.
195	pub fn new(client: Client) -> Self {
196		Self {
197			client,
198			use_best_block: false,
199			block_height: Default::default(),
200			poll_rate: Duration::from_secs(3),
201			retry_on_error: None,
202		}
203	}
204
205	/// Materializes the concrete subscription based on the collected settings.
206	///
207	/// # Returns
208	/// - `Ok(Sub)` wrapping either a `BestBlock` or `FinalizedBlock` variant when the node responds
209	///   successfully.
210	/// - `Err(RpcError)` if the initial height lookup fails.
211	pub async fn build(&self) -> Result<Sub, RpcError> {
212		let block_height = match self.block_height {
213			Some(x) => x,
214			None => match self.use_best_block {
215				true => self.client.best().block_height().await?,
216				false => self.client.finalized().block_height().await?,
217			},
218		};
219
220		let sub = match self.use_best_block {
221			true => Sub::BestBlock(BestBlockSub {
222				client: self.client.clone(),
223				poll_rate: self.poll_rate,
224				current_block_height: block_height,
225				block_processed: Vec::new(),
226				retry_on_error: self.retry_on_error,
227				latest_finalized_height: None,
228			}),
229			false => Sub::FinalizedBlock(FinalizedBlockSub {
230				client: self.client.clone(),
231				poll_rate: self.poll_rate,
232				next_block_height: block_height,
233				retry_on_error: self.retry_on_error,
234				latest_finalized_height: None,
235				processed_previous_block: false,
236			}),
237		};
238
239		Ok(sub)
240	}
241}
242
243/// Subscription to fetch finalized block. Not meant to be used directly.
244///
245/// Use [`Sub`] instead.
246#[derive(Clone)]
247pub struct FinalizedBlockSub {
248	client: Client,
249	poll_rate: Duration,
250	pub(crate) next_block_height: u32,
251	retry_on_error: Option<bool>,
252	latest_finalized_height: Option<u32>,
253	processed_previous_block: bool,
254}
255
256impl FinalizedBlockSub {
257	/// Moves forward to the next finalized block.
258	///
259	/// # Returns
260	/// - `Ok(BlockInfo)` containing the next finalized block reference.
261	/// - `Err(RpcError)` if any RPC request fails.
262	pub async fn next(&mut self) -> Result<BlockInfo, RpcError> {
263		let latest_finalized_height = self.fetch_latest_finalized_height().await?;
264
265		let result = if latest_finalized_height > self.next_block_height {
266			self.run_historical().await?
267		} else {
268			self.run_head().await?
269		};
270
271		self.next_block_height = result.height + 1;
272		self.processed_previous_block = true;
273		Ok(result)
274	}
275
276	/// Steps back to the previous finalized block.
277	///
278	/// # Returns
279	/// Behaves like [`FinalizedBlockSub::next`] after adjusting the internal cursor backward.
280	pub async fn prev(&mut self) -> Result<BlockInfo, RpcError> {
281		self.next_block_height = self.next_block_height.saturating_sub(1);
282		if self.processed_previous_block {
283			self.next_block_height = self.next_block_height.saturating_sub(1);
284		}
285		self.processed_previous_block = false;
286
287		self.next().await
288	}
289
290	/// Fetches and caches the latest finalized height, respecting retry configuration.
291	async fn fetch_latest_finalized_height(&mut self) -> Result<u32, RpcError> {
292		if let Some(height) = self.latest_finalized_height.as_ref() {
293			return Ok(*height);
294		}
295
296		let retry_on_error = Some(should_retry(&self.client, self.retry_on_error));
297		let latest_finalized_height = self.client.finalized().retry_on(retry_on_error).block_height().await?;
298		self.latest_finalized_height = Some(latest_finalized_height);
299		Ok(latest_finalized_height)
300	}
301
302	/// Fetches historical blocks below the current head.
303	async fn run_historical(&mut self) -> Result<BlockInfo, RpcError> {
304		let height = self.next_block_height;
305		let hash = self
306			.client
307			.chain()
308			.retry_on(self.retry_on_error, None)
309			.block_hash(Some(height))
310			.await?;
311		let hash = hash.ok_or(RpcError::ExpectedData("Expected to fetch block hash".into()))?;
312
313		Ok(BlockInfo { hash, height })
314	}
315
316	/// Polls for new finalized blocks when caught up with the head.
317	/// Polls for new best blocks once historical replay has caught up.
318	async fn run_head(&mut self) -> Result<BlockInfo, RpcError> {
319		loop {
320			let head = self.client.finalized().block_info().await?;
321
322			let is_past_block = self.next_block_height > head.height;
323			if is_past_block {
324				sleep(self.poll_rate).await;
325				continue;
326			}
327
328			if self.next_block_height == head.height {
329				return Ok(head);
330			}
331
332			let height = self.next_block_height;
333			let hash = self
334				.client
335				.chain()
336				.retry_on(self.retry_on_error, Some(true))
337				.block_hash(Some(height))
338				.await?;
339			let hash = hash.ok_or(RpcError::ExpectedData("Expected to fetch block hash".into()))?;
340
341			return Ok(BlockInfo { hash, height });
342		}
343	}
344}
345
346/// Subscription to fetch best block. Not meant to be used directly.
347///
348/// Use [`Sub`] instead.
349#[derive(Clone)]
350pub struct BestBlockSub {
351	client: Client,
352	poll_rate: Duration,
353	pub(crate) current_block_height: u32,
354	block_processed: Vec<H256>,
355	retry_on_error: Option<bool>,
356	latest_finalized_height: Option<u32>,
357}
358
359impl BestBlockSub {
360	/// Moves forward to the next best (head) block.
361	///
362	/// # Returns
363	/// - `Ok(BlockInfo)` pointing to the next best block.
364	/// - `Err(RpcError)` when RPC calls fail.
365	pub async fn next(&mut self) -> Result<BlockInfo, RpcError> {
366		let latest_finalized_height = self.fetch_latest_finalized_height().await?;
367
368		// Dealing with historical blocks
369		if latest_finalized_height > self.current_block_height {
370			let info = self.run_historical().await?;
371			self.block_processed.clear();
372			self.block_processed.push(info.hash);
373			self.current_block_height = info.height;
374			return Ok(info);
375		}
376
377		let info = self.run_head().await?;
378		if info.height == self.current_block_height {
379			self.block_processed.push(info.hash);
380		} else {
381			self.block_processed.clear();
382			self.block_processed.push(info.hash);
383			self.current_block_height = info.height;
384		}
385
386		Ok(info)
387	}
388
389	/// Steps back to the previous best (head) block.
390	///
391	/// # Returns
392	/// Behaves like [`BestBlockSub::next`] after adjusting the internal cursor backward.
393	pub async fn prev(&mut self) -> Result<BlockInfo, RpcError> {
394		self.current_block_height = self.current_block_height.saturating_sub(1);
395		self.block_processed.clear();
396		self.next().await
397	}
398
399	/// Fetches and caches the latest finalized height, respecting retry configuration.
400	async fn fetch_latest_finalized_height(&mut self) -> Result<u32, RpcError> {
401		if let Some(height) = self.latest_finalized_height.as_ref() {
402			return Ok(*height);
403		}
404
405		let latest_finalized_height = self
406			.client
407			.finalized()
408			.retry_on(self.retry_on_error)
409			.block_height()
410			.await?;
411		self.latest_finalized_height = Some(latest_finalized_height);
412		Ok(latest_finalized_height)
413	}
414
415	/// Fetches historical best blocks when replaying past heights.
416	async fn run_historical(&mut self) -> Result<BlockInfo, RpcError> {
417		let mut height = self.current_block_height;
418		if !self.block_processed.is_empty() {
419			height += 1;
420		}
421
422		let hash = self
423			.client
424			.chain()
425			.retry_on(self.retry_on_error, None)
426			.block_hash(Some(height))
427			.await?;
428		let hash = hash.ok_or(RpcError::ExpectedData("Expected to fetch block hash".into()))?;
429
430		Ok(BlockInfo { hash, height })
431	}
432
433	async fn run_head(&mut self) -> Result<BlockInfo, RpcError> {
434		loop {
435			let head = self.client.best().retry_on(self.retry_on_error).block_info().await?;
436
437			let is_past_block = self.current_block_height > head.height;
438			let block_already_processed = self.block_processed.contains(&head.hash);
439			if is_past_block || block_already_processed {
440				sleep(self.poll_rate).await;
441				continue;
442			}
443
444			let no_block_processed_yet = self.block_processed.is_empty();
445			if no_block_processed_yet {
446				let hash = self
447					.client
448					.chain()
449					.retry_on(self.retry_on_error, Some(true))
450					.block_hash(Some(self.current_block_height))
451					.await?;
452				let hash = hash.ok_or(RpcError::ExpectedData("Expected to fetch block hash".into()))?;
453
454				return Ok(BlockInfo { hash, height: self.current_block_height });
455			}
456
457			let is_current_block = self.current_block_height == head.height;
458			let is_next_block = self.current_block_height + 1 == head.height;
459			if is_current_block || is_next_block {
460				return Ok(head);
461			}
462
463			let height = self.current_block_height + 1;
464			let hash = self
465				.client
466				.chain()
467				.retry_on(Some(true), Some(true))
468				.block_hash(Some(height))
469				.await?;
470			let hash = hash.ok_or(RpcError::ExpectedData("Expected to fetch block hash".into()))?;
471
472			return Ok(BlockInfo { hash, height });
473		}
474	}
475}
476
477#[cfg(test)]
478mod tests {
479	use super::*;
480	use crate::{error::Error, prelude::*};
481
482	#[tokio::test]
483	async fn sub_test() -> Result<(), Error> {
484		let client = Client::new(TURING_ENDPOINT).await?;
485		let mut sub = Sub::new(client.clone());
486
487		//
488		//	Test Case 1: By default retires should be based around the global setting
489		//
490		client.set_global_retries_enabled(true);
491		assert_eq!(sub.should_retry_on_error(), true);
492
493		client.set_global_retries_enabled(false);
494		assert_eq!(sub.should_retry_on_error(), false);
495
496		//
497		//	Test Case 2: Forcefully setting it to false should always yield false
498		//
499		sub.set_retry_on_error(Some(false));
500
501		client.set_global_retries_enabled(true);
502		assert_eq!(sub.should_retry_on_error(), false);
503
504		client.set_global_retries_enabled(false);
505		assert_eq!(sub.should_retry_on_error(), false);
506
507		//
508		//	Test Case 2: Forcefully setting it to true should always yield true
509		//
510		sub.set_retry_on_error(Some(true));
511
512		client.set_global_retries_enabled(true);
513		assert_eq!(sub.should_retry_on_error(), true);
514
515		client.set_global_retries_enabled(false);
516		assert_eq!(sub.should_retry_on_error(), true);
517
518		Ok(())
519	}
520
521	// This test will be by flaky and that is OK.
522	#[tokio::test]
523	async fn best_sub_test() -> Result<(), Error> {
524		let client = Client::new(TURING_ENDPOINT).await?;
525
526		//
527		// Test Case 1: Latest Block Height + Next
528		//
529		let mut sub = Sub::new(client.clone());
530		sub.use_best_block(true);
531
532		let block_height = client.best().block_height().await?;
533		let value = sub.next().await?;
534		assert_eq!(value.height, block_height);
535
536		//
537		// Test Case 2: Latest Block Height + Prev
538		//
539		let mut sub = Sub::new(client.clone());
540		sub.use_best_block(true);
541
542		let block_height = client.best().block_height().await?;
543		let value = sub.prev().await?;
544		assert_eq!(value.height, block_height - 1);
545
546		//
547		// Test Case 3: Set Block Height + Next + Next + Next
548		//
549		let block_height = 1900000u32;
550		let mut sub = Sub::new(client.clone());
551		sub.use_best_block(true);
552		sub.set_block_height(block_height);
553		for i in 0..3 {
554			let value = sub.next().await?;
555			assert_eq!(value.height, block_height + i);
556		}
557
558		//
559		// Test Case 4: Set Block Height + Prev + Prev + Prev
560		//
561		let block_height = 1900000u32;
562		let mut sub = Sub::new(client.clone());
563		sub.use_best_block(true);
564		sub.set_block_height(block_height);
565		for i in 0..3 {
566			let value = sub.prev().await?;
567			assert_eq!(value.height, block_height - i - 1);
568		}
569
570		//
571		// Test Case 5: Set Block Height + Next + Prev
572		//
573		let block_height = 1900000u32;
574		let mut sub = Sub::new(client.clone());
575		sub.use_best_block(true);
576		sub.set_block_height(block_height);
577
578		let value = sub.next().await?;
579		assert_eq!(value.height, block_height);
580
581		let value = sub.prev().await?;
582		assert_eq!(value.height, block_height - 1);
583
584		//
585		// Test Case 6: Set Block Height + Prev + Next
586		//
587		let block_height = 1900000u32;
588		let mut sub = Sub::new(client.clone());
589		sub.use_best_block(true);
590		sub.set_block_height(block_height);
591
592		let value = sub.prev().await?;
593		assert_eq!(value.height, block_height - 1);
594
595		let value = sub.next().await?;
596		assert_eq!(value.height, block_height);
597
598		Ok(())
599	}
600
601	// This test will be by flaky and that is OK.
602	#[tokio::test]
603	async fn finalized_sub_test() -> Result<(), Error> {
604		let client = Client::new(TURING_ENDPOINT).await?;
605
606		//
607		// Test Case 1: Latest Block Height + Next
608		//
609		let mut sub = Sub::new(client.clone());
610
611		let block_height = client.finalized().block_height().await?;
612		let value = sub.next().await?;
613		assert_eq!(value.height, block_height);
614
615		//
616		// Test Case 2: Latest Block Height + Prev
617		//
618		let mut sub = Sub::new(client.clone());
619
620		let block_height = client.finalized().block_height().await?;
621		let value = sub.prev().await?;
622		assert_eq!(value.height, block_height - 1);
623
624		//
625		// Test Case 3: Set Block Height + Next + Next + Next
626		//
627		let block_height = 1900000u32;
628		let mut sub = Sub::new(client.clone());
629		sub.set_block_height(block_height);
630		for i in 0..3 {
631			let value = sub.next().await?;
632			assert_eq!(value.height, block_height + i);
633		}
634
635		//
636		// Test Case 4: Set Block Height + Prev + Prev + Prev
637		//
638		let block_height = 1900000u32;
639		let mut sub = Sub::new(client.clone());
640		sub.set_block_height(block_height);
641		for i in 0..3 {
642			let value = sub.prev().await?;
643			assert_eq!(value.height, block_height - i - 1);
644		}
645
646		//
647		// Test Case 5: Set Block Height + Next + Prev
648		//
649		let block_height = 1900000u32;
650		let mut sub = Sub::new(client.clone());
651		sub.set_block_height(block_height);
652
653		let value = sub.next().await?;
654		assert_eq!(value.height, block_height);
655
656		let value = sub.prev().await?;
657		assert_eq!(value.height, block_height - 1);
658
659		//
660		// Test Case 6: Set Block Height + Prev + Next
661		//
662		let block_height = 1900000u32;
663		let mut sub = Sub::new(client.clone());
664		sub.set_block_height(block_height);
665
666		let value = sub.prev().await?;
667		assert_eq!(value.height, block_height - 1);
668
669		let value = sub.next().await?;
670		assert_eq!(value.height, block_height);
671
672		Ok(())
673	}
674}