pallet_sidechain_rpc/
lib.rs

1//! Json RPC for the Sidechain pallet
2//!
3//! # Usage
4//!
5//! ## Implementing runtime APIs
6//!
7//! Your runtime should implement the [GetGenesisUtxo] and [GetEpochDurationApi] runtime APIs.
8//! For example, if your chain uses Aura for consensus, they may be implemented similar to this:
9//! ```rust,ignore
10//! impl sp_sidechain::GetGenesisUtxo<Block> for Runtime {
11//!   fn genesis_utxo() -> UtxoId {
12//!     Sidechain::genesis_utxo()
13//!   }
14//! }
15//!
16//! impl sp_sidechain::GetEpochDurationApi<Block> for Runtime {
17//! 	fn get_epoch_duration_millis() -> u64 {
18//! 		BLOCKS_PER_EPOCH * MILLISECS_PER_BLOCK
19//! 	}
20//! }
21//! ```
22//!
23//! ## Adding to the RPC stack
24//!
25//! Once the runtime APIs are in place, the RPC can be added to the node:
26//!
27//! ```rust
28//! # use jsonrpsee::RpcModule;
29//! # use pallet_sidechain_rpc::{*, types::GetBestHash};
30//! # use sidechain_domain::mainchain_epoch::MainchainEpochConfig;
31//! # use sp_api::{ CallApiAt, ProvideRuntimeApi };
32//! # use sp_runtime::traits::Block as BlockT;
33//! # use sp_sidechain::{ GetEpochDurationApi, GetGenesisUtxo };
34//! # use std::sync::Arc;
35//! # use time_source::TimeSource;
36//! fn create_rpc<B: BlockT, C: Send + Sync + 'static>(
37//!    client: Arc<C>,
38//!    time_source: Arc<dyn TimeSource + Send + Sync>,
39//!    data_source: Arc<dyn SidechainRpcDataSource + Send + Sync>,
40//! ) -> Result<RpcModule<()>, Box<dyn std::error::Error + Send + Sync>>
41//! where
42//!     C: ProvideRuntimeApi<B> + GetBestHash<B> + CallApiAt<B>,
43//!     C::Api: GetGenesisUtxo<B> + GetEpochDurationApi<B>
44//! {
45//!
46//!     let mut module = RpcModule::new(());
47//!     module.merge(
48//!         SidechainRpc::new(
49//!             client.clone(),
50//!             MainchainEpochConfig::read_from_env().unwrap(),
51//!             data_source,
52//!             time_source,
53//!         )
54//!         .into_rpc(),
55//!     )?;
56//!
57//!     // ... other RPCs
58//!     Ok(module)
59//! }
60//! ```
61//!
62//! Note that your node should already have necessary time and data sources wired in. A Db-Sync-based
63//! data source is provided by the Partner Chain toolkit in the `partner_chains_db_sync_data_sources`
64//! crate.
65//!
66//! ## Legacy compatibility mode
67//!
68//! In previous versions of the Partner Chains toolkit, the RPC services in this crate relied on now
69//! deprecated runtime api `sp_sidechain::SlotApi` to compute Partner Chain epochs and current slot.
70//! For Partner Chains that started before this dependency was removed, a compatibility mode is
71//! provided behind the `legacy-slotapi-compat` feature flag. Enabling this flag will cause the nodes to
72//! use this runtime API when present and optionally include information about current Patner Chain
73//! slot in `sidechain.slot` field of `sidechain_getStatus` response.
74//!
75//! [GetGenesisUtxo]: sp_sidechain::GetGenesisUtxo
76//! [GetEpochDurationApi]: sp_sidechain::GetEpochDurationApi
77#![deny(missing_docs)]
78use derive_new::new;
79use jsonrpsee::{
80	core::{RpcResult, async_trait},
81	proc_macros::rpc,
82	types::{ErrorObject, ErrorObjectOwned, error::ErrorCode},
83};
84#[cfg(feature = "legacy-slotapi-compat")]
85use legacy_compat::slots::*;
86use sidechain_domain::mainchain_epoch::{MainchainEpochConfig, MainchainEpochDerivation};
87use sidechain_domain::{MainchainBlock, UtxoId};
88use sp_api::ProvideRuntimeApi;
89use sp_core::offchain::{Duration, Timestamp};
90use sp_runtime::traits::Block as BlockT;
91use sp_sidechain::{GetEpochDurationApi, GetGenesisUtxo};
92use std::sync::Arc;
93use time_source::*;
94use types::*;
95
96/// Response types returned by RPC endpoints for Sidechain pallet
97pub mod types;
98
99#[cfg(feature = "legacy-slotapi-compat")]
100mod legacy_compat;
101
102#[cfg(test)]
103mod tests;
104
105#[cfg(any(test))]
106mod mock;
107
108/// Response type of the [crate::SidechainRpcApiServer::get_params] method
109#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Debug)]
110pub struct GetParamsOutput {
111	/// Genesis UTXO of the queried Partner Chain
112	pub genesis_utxo: UtxoId,
113}
114
115/// Json RPC API for querying basic information about a Partner Chain
116#[rpc(client, server, namespace = "sidechain")]
117pub trait SidechainRpcApi {
118	/// Gets the genesis UTXO of the Partner Chain
119	///
120	/// note: the legacy name `get_params` comes from the times when there were more parameters that
121	///       defined a Partner Chain than a single genesis UTXO
122	#[method(name = "getParams")]
123	fn get_params(&self) -> RpcResult<GetParamsOutput>;
124
125	/// Gets information about current Partner Chain and Cardano slot and epoch number
126	#[method(name = "getStatus")]
127	async fn get_status(&self) -> RpcResult<GetStatusResponse>;
128}
129
130/// Data source used by [SidechainRpc] for querying latest block
131#[async_trait]
132pub trait SidechainRpcDataSource {
133	/// Returns the latest Partner Chain block info
134	async fn get_latest_block_info(
135		&self,
136	) -> Result<MainchainBlock, Box<dyn std::error::Error + Send + Sync>>;
137}
138
139/// Json RPC service implementing [SidechainRpcApiServer]
140#[derive(new)]
141pub struct SidechainRpc<C, Block> {
142	client: Arc<C>,
143	mc_epoch_config: MainchainEpochConfig,
144	data_source: Arc<dyn SidechainRpcDataSource + Send + Sync>,
145	time_source: Arc<dyn TimeSource + Send + Sync>,
146	_marker: std::marker::PhantomData<Block>,
147}
148
149impl<C, B> SidechainRpc<C, B> {
150	fn get_current_timestamp(&self) -> Timestamp {
151		Timestamp::from_unix_millis(self.time_source.get_current_time_millis())
152	}
153}
154
155/// Runtime client that can serve data to sidechain RPC
156trait SidechainRpcClient<Block: BlockT> {
157	/// Returns Partner Chain epoch duration
158	fn get_epoch_duration(
159		&self,
160		best_block: Block::Hash,
161	) -> Result<Duration, Box<dyn std::error::Error + Send + Sync>>;
162
163	/// Returns the Partner Chain's genesis UTXO
164	fn get_genesis_utxo(
165		&self,
166		best_block: Block::Hash,
167	) -> Result<UtxoId, Box<dyn std::error::Error + Send + Sync>>;
168
169	#[cfg(feature = "legacy-slotapi-compat")]
170	/// Returns slot duration
171	fn get_maybe_slot_duration(&self, best_block: Block::Hash) -> Option<u64>;
172}
173
174impl<Block, Client> SidechainRpcClient<Block> for Client
175where
176	Block: BlockT,
177	Client: sp_api::CallApiAt<Block> + Send + Sync + 'static,
178	Client: ProvideRuntimeApi<Block>,
179	Client::Api: GetEpochDurationApi<Block>,
180	Client::Api: GetGenesisUtxo<Block>,
181{
182	fn get_epoch_duration(
183		&self,
184		best_block: Block::Hash,
185	) -> Result<Duration, Box<dyn std::error::Error + Send + Sync>> {
186		#[cfg(feature = "legacy-slotapi-compat")]
187		if let Some(slot_config) = self.get_sc_slot_config(best_block) {
188			return Ok(Duration::from_millis(
189				u64::from(slot_config.slots_per_epoch) * slot_config.slot_duration_millis,
190			));
191		}
192
193		Ok(Duration::from_millis(self.runtime_api().get_epoch_duration_millis(best_block)?))
194	}
195
196	fn get_genesis_utxo(
197		&self,
198		best_block: Block::Hash,
199	) -> Result<UtxoId, Box<dyn std::error::Error + Send + Sync>> {
200		Ok(self.runtime_api().genesis_utxo(best_block)?)
201	}
202
203	#[cfg(feature = "legacy-slotapi-compat")]
204	fn get_maybe_slot_duration(&self, best_block: Block::Hash) -> Option<u64> {
205		let slot_config = self.get_sc_slot_config(best_block)?;
206		Some(slot_config.slot_duration_millis)
207	}
208}
209
210#[async_trait]
211impl<C, Block> SidechainRpcApiServer for SidechainRpc<C, Block>
212where
213	Block: BlockT,
214	C: Send + Sync + 'static,
215	C: SidechainRpcClient<Block>,
216	C: GetBestHash<Block>,
217{
218	fn get_params(&self) -> RpcResult<GetParamsOutput> {
219		let best_block = self.client.best_hash();
220
221		let genesis_utxo = self.client.get_genesis_utxo(best_block).map_err(error_object_from)?;
222
223		Ok(GetParamsOutput { genesis_utxo })
224	}
225
226	async fn get_status(&self) -> RpcResult<GetStatusResponse> {
227		let best_block = self.client.best_hash();
228
229		let current_timestamp = self.get_current_timestamp();
230
231		let sc_epoch_duration: Duration =
232			self.client.get_epoch_duration(best_block).map_err(error_object_from)?;
233
234		let current_sidechain_epoch = current_timestamp.unix_millis() / sc_epoch_duration.millis();
235
236		let next_sidechain_epoch_timestamp =
237			Timestamp::from((current_sidechain_epoch + 1) * sc_epoch_duration.millis());
238
239		let latest_mainchain_block =
240			self.data_source.get_latest_block_info().await.map_err(|err| {
241				ErrorObject::owned(
242					ErrorCode::InternalError.code(),
243					format!("Internal error: GetLatestBlockResponse error '{:?}", err),
244					None::<u8>,
245				)
246			})?;
247		let next_mainchain_epoch_timestamp = self
248			.mc_epoch_config
249			.mainchain_epoch_to_timestamp(latest_mainchain_block.epoch.next());
250
251		Ok(GetStatusResponse {
252			sidechain: SidechainData {
253				epoch: current_sidechain_epoch,
254				#[cfg(feature = "legacy-slotapi-compat")]
255				slot: (self.client.get_maybe_slot_duration(best_block))
256					.map(|duration| current_timestamp.unix_millis() / duration),
257				next_epoch_timestamp: next_sidechain_epoch_timestamp,
258			},
259			mainchain: MainchainData {
260				epoch: latest_mainchain_block.epoch.0,
261				slot: latest_mainchain_block.slot.0,
262				next_epoch_timestamp: next_mainchain_epoch_timestamp,
263			},
264		})
265	}
266}
267
268fn error_object_from<T: std::fmt::Debug>(err: T) -> ErrorObjectOwned {
269	ErrorObject::owned::<u8>(-1, format!("{err:?}"), None)
270}