sp_native_token_management/
lib.rs

1//! Primitives and inherent data provider for the Native Token Management feature
2//!
3//! # Purpose and context
4//!
5//! This crate defines shared types used by components that implement the Native Token Management
6//! feature of the Partner Chains toolkit, along with an inherent data provider for token transfer
7//! data.
8//!
9//! The Native Token Management feature allows a Partner Chain to keep its token as a native asset
10//! on Cardano and have it be transferable to the Partner Chain. This is achieved by the native
11//! token being locked at an _illiquid supply_ address on Cardano, and the Partner Chain handling
12//! this locking event (a _transfer_) after it has been observed as part of a stable block.
13//!
14//! The inherent data provider defined in this crate is responsible for providing information about
15//! the transfers in form of inherent data, and handling them is the responsibility of the pallet,
16//! which allows the Partner Chain builders to define their own transfer handling logic to suit
17//! their needs.
18//!
19//! # Usage
20//!
21//! ## Prerequisites
22//!
23//! This features depends on the MC Reference Hash feature to provide Cardano block reference for
24//! querying the token transfers. See the documentation of `sidechain_mc_hash` crate for more information.
25//!
26//! ## Implementing runtime APIs
27//!
28//! [NativeTokenManagementInherentDataProvider] requires the runtime to implement the
29//! [NativeTokenManagementApi] runtime API. This only requires passing relevant values from the pallet:
30//! ```rust,ignore
31//! impl sp_native_token_management::NativeTokenManagementApi<Block> for Runtime {
32//! 	fn get_main_chain_scripts() -> Option<sp_native_token_management::MainChainScripts> {
33//! 		NativeTokenManagement::get_main_chain_scripts()
34//! 	}
35//! 	fn initialized() -> bool {
36//! 		NativeTokenManagement::initialized()
37//! 	}
38//! }
39//! ```
40//!
41//! ## Adding the inherent data provider
42//!
43//! The inherent data provider requires a data source implementing [NativeTokenManagementDataSource].
44//! A Db-Sync implementation is provided by the `partner_chains_db_sync_data_sources` crate.
45//!
46//! With the data source present, the IDP is straightfoward to create:
47//!
48//! ```rust
49//! use std::sync::Arc;
50//! use sp_runtime::traits::Block as BlockT;
51//! use sp_native_token_management::*;
52//!
53//! async fn create_idps<Block: BlockT, C>(
54//!     parent_hash: Block::Hash,
55//!     client: Arc<C>,
56//!     native_token_data_source: &(dyn NativeTokenManagementDataSource + Send + Sync)
57//! ) -> Result<(NativeTokenManagementInherentDataProvider /* other IDPs */), Box<dyn std::error::Error + Send + Sync>>
58//! where
59//!     C: sp_api::ProvideRuntimeApi<Block> + Send + Sync,
60//!     C::Api: NativeTokenManagementApi<Block>
61//! {
62//!     let (mc_hash, previous_mc_hash) = todo!("Should come from the MC Reference Hash feature");
63//!
64//!     let native_token_idp = NativeTokenManagementInherentDataProvider::new(
65//!     	client.clone(),
66//!     	native_token_data_source,
67//!     	mc_hash,
68//!     	previous_mc_hash,
69//!     	parent_hash,
70//!     )
71//!     .await?;
72//!     Ok((native_token_idp /* other IDPs */))
73//! }
74//! ```
75//!
76//! The same constructor can be used for both proposal and validation of blocks.
77//!
78//! [NativeTokenManagementApi]: sp_native_token_management::NativeTokenManagementApi
79#![cfg_attr(not(feature = "std"), no_std)]
80#![deny(missing_docs)]
81
82#[cfg(feature = "std")]
83use {core::str::FromStr, sp_runtime::traits::Block as BlockT};
84
85#[cfg(feature = "std")]
86pub use inherent_provider::*;
87
88use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
89use sidechain_domain::*;
90use sp_inherents::*;
91use sp_runtime::scale_info::TypeInfo;
92
93#[cfg(test)]
94mod tests;
95
96/// Inherent identifier used by the Native Token Management pallet
97pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"nattoken";
98
99/// Values identifying on-chain entities involved in the native token management system on Cardano.
100#[derive(Default, Debug, Clone, PartialEq, Eq, TypeInfo, Encode, Decode, MaxEncodedLen)]
101#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
102pub struct MainChainScripts {
103	/// Minting policy ID of the native token
104	pub native_token_policy_id: PolicyId,
105	/// Asset name of the native token
106	pub native_token_asset_name: AssetName,
107	/// Address of the illiquid supply validator. All tokens sent to that address are effectively locked
108	/// and considered "sent" to the Partner Chain.
109	pub illiquid_supply_validator_address: MainchainAddress,
110}
111
112#[cfg(feature = "std")]
113impl MainChainScripts {
114	/// Reads the main chain script values from local environment variables
115	///
116	/// It expects the following variables to be set:
117	/// - `NATIVE_TOKEN_POLICY_ID`
118	/// - `NATIVE_TOKEN_ASSET_NAME`
119	/// - `ILLIQUID_SUPPLY_VALIDATOR_ADDRESS`
120	pub fn read_from_env() -> Result<Self, envy::Error> {
121		#[derive(serde::Serialize, serde::Deserialize)]
122		struct MainChainScriptsEnvConfig {
123			pub native_token_policy_id: PolicyId,
124			pub native_token_asset_name: AssetName,
125			pub illiquid_supply_validator_address: String,
126		}
127
128		let MainChainScriptsEnvConfig {
129			native_token_policy_id,
130			native_token_asset_name,
131			illiquid_supply_validator_address,
132		} = envy::from_env()?;
133		let illiquid_supply_validator_address =
134			FromStr::from_str(&illiquid_supply_validator_address).map_err(|err| {
135				envy::Error::Custom(format!("Incorrect main chain address: {}", err))
136			})?;
137		Ok(Self {
138			native_token_policy_id,
139			native_token_asset_name,
140			illiquid_supply_validator_address,
141		})
142	}
143}
144
145sp_api::decl_runtime_apis! {
146	/// Runtime API exposing configuration and initialization status of the Native Token Management pallet
147	pub trait NativeTokenManagementApi {
148		/// Returns the current main chain scripts configured in the pallet or [None] if they are not set.
149		fn get_main_chain_scripts() -> Option<MainChainScripts>;
150		/// Gets current initializaion status and set it to `true` afterwards. This check is used to
151		/// determine whether historical data from the beginning of main chain should be queried.
152		fn initialized() -> bool;
153	}
154}
155
156/// Data about token transfers in some period of time
157#[derive(Decode, Encode)]
158pub struct TokenTransferData {
159	/// Aggregate number of tokens transfered
160	pub token_amount: NativeTokenAmount,
161}
162
163/// Error type returned by the Native Token Management pallet inherent logic
164#[derive(Encode, Debug, PartialEq)]
165#[cfg_attr(feature = "std", derive(Decode, thiserror::Error))]
166pub enum InherentError {
167	/// Signals that no inherent was submitted despite new token transfers being observed
168	#[cfg_attr(feature = "std", error("Inherent missing for token transfer of {0} tokens"))]
169	TokenTransferNotHandled(NativeTokenAmount),
170	/// Signals that the inherent registered an incorrect number of tokens transfered
171	#[cfg_attr(
172		feature = "std",
173		error("Incorrect token transfer amount: expected {0}, got {1} tokens")
174	)]
175	IncorrectTokenNumberTransfered(NativeTokenAmount, NativeTokenAmount),
176	/// Signals that an inherent was submitted when no token transfers were observed
177	#[cfg_attr(feature = "std", error("Unexpected transfer of {0} tokens"))]
178	UnexpectedTokenTransferInherent(NativeTokenAmount),
179}
180
181impl IsFatalError for InherentError {
182	fn is_fatal_error(&self) -> bool {
183		true
184	}
185}
186
187#[cfg(feature = "std")]
188mod inherent_provider {
189	use super::*;
190	use sp_api::{ApiError, ApiExt, ProvideRuntimeApi};
191	use std::error::Error;
192	use std::sync::Arc;
193
194	/// Interface for a data source serving native token transfer data compatible with [NativeTokenManagementInherentDataProvider].
195	#[async_trait::async_trait]
196	pub trait NativeTokenManagementDataSource {
197		/// Retrieves total of native token transfers into the illiquid supply in the range (after_block, to_block]
198		async fn get_total_native_token_transfer(
199			&self,
200			after_block: Option<McBlockHash>,
201			to_block: McBlockHash,
202			scripts: MainChainScripts,
203		) -> Result<NativeTokenAmount, Box<dyn std::error::Error + Send + Sync>>;
204	}
205
206	/// Inherent data provider that provides aggregate number of native token transfers to the illiquid supply on Cardano
207	/// in some time range.
208	///
209	/// This IDP will not provide any inherent data if `token_amount` is [None], but *will* provide data for `Some(0)`.
210	pub struct NativeTokenManagementInherentDataProvider {
211		/// Aggregate number of tokens transfered
212		pub token_amount: Option<NativeTokenAmount>,
213	}
214
215	/// Error type returned when creation of [NativeTokenManagementInherentDataProvider] fails
216	#[derive(thiserror::Error, sp_runtime::RuntimeDebug)]
217	pub enum IDPCreationError {
218		/// Signals that the data source used returned an error
219		#[error("Failed to read native token data from data source: {0:?}")]
220		DataSourceError(Box<dyn Error + Send + Sync>),
221		/// Signals that a runtime API call failed
222		#[error("Failed to call runtime API: {0:?}")]
223		ApiError(ApiError),
224	}
225
226	impl From<ApiError> for IDPCreationError {
227		fn from(err: ApiError) -> Self {
228			Self::ApiError(err)
229		}
230	}
231
232	impl NativeTokenManagementInherentDataProvider {
233		/// Creates a [NativeTokenManagementInherentDataProvider] that will provide as inherent data the aggregate number of
234		/// native token transfers into the illiquid supply on Cardano after the block with hash `parent_mc_hash` up to and
235		/// including the block with hash `mc_hash`.
236		///
237		/// This function is runtime-aware and will only create an active [NativeTokenManagementInherentDataProvider] instance
238		/// if the pallet is present and configured. Otherwise the returned inherent data provider will be inactive.
239		///
240		/// # Arguments
241		/// - `client`: runtime client exposing the [NativeTokenManagementApi] runtime API
242		/// - `data-source`: data source implementing [NativeTokenManagementDataSource]
243		/// - `mc_hash`: main chain block hash referenced by the currently produced or validated block
244		/// - `parent_mc_hash`: main chain block hash referenced by the parent of the currently producer or validated block.
245		///                     This argument should be [None] if the parent block was the genesis block or didn't reference
246		///                     any main chain block.
247		/// - `parent_hash`: block hash of the parent block of the currently produced or validated block
248		pub async fn new<Block, C>(
249			client: Arc<C>,
250			data_source: &(dyn NativeTokenManagementDataSource + Send + Sync),
251			mc_hash: McBlockHash,
252			parent_mc_hash: Option<McBlockHash>,
253			parent_hash: <Block as BlockT>::Hash,
254		) -> Result<Self, IDPCreationError>
255		where
256			Block: BlockT,
257			C: ProvideRuntimeApi<Block> + Send + Sync,
258			C::Api: NativeTokenManagementApi<Block>,
259		{
260			if client
261				.runtime_api()
262				.has_api::<dyn NativeTokenManagementApi<Block>>(parent_hash)?
263			{
264				let api = client.runtime_api();
265				let Some(scripts) = api.get_main_chain_scripts(parent_hash)? else {
266					return Ok(Self { token_amount: None });
267				};
268				let token_amount = data_source
269					.get_total_native_token_transfer(parent_mc_hash, mc_hash, scripts)
270					.await
271					.map_err(IDPCreationError::DataSourceError)?;
272
273				let token_amount = if token_amount.0 > 0 { Some(token_amount) } else { None };
274
275				Ok(Self { token_amount })
276			} else {
277				Ok(Self { token_amount: None })
278			}
279		}
280	}
281
282	#[async_trait::async_trait]
283	impl InherentDataProvider for NativeTokenManagementInherentDataProvider {
284		async fn provide_inherent_data(
285			&self,
286			inherent_data: &mut InherentData,
287		) -> Result<(), sp_inherents::Error> {
288			if let Some(token_amount) = self.token_amount {
289				inherent_data.put_data(INHERENT_IDENTIFIER, &TokenTransferData { token_amount })
290			} else {
291				Ok(())
292			}
293		}
294
295		async fn try_handle_error(
296			&self,
297			identifier: &InherentIdentifier,
298			mut error: &[u8],
299		) -> Option<Result<(), sp_inherents::Error>> {
300			if *identifier != INHERENT_IDENTIFIER {
301				return None;
302			}
303
304			let error = InherentError::decode(&mut error).ok()?;
305
306			Some(Err(sp_inherents::Error::Application(Box::from(error))))
307		}
308	}
309
310	/// Mock implementation of the data source
311	#[cfg(any(test, feature = "mock"))]
312	pub mod mock {
313		use crate::{MainChainScripts, NativeTokenManagementDataSource};
314		use async_trait::async_trait;
315		use derive_new::new;
316		use sidechain_domain::*;
317		use std::collections::HashMap;
318
319		/// Mock implementation of [NativeTokenManagementDataSource]
320		#[derive(new, Default)]
321		pub struct MockNativeTokenDataSource {
322			transfers: HashMap<(Option<McBlockHash>, McBlockHash), NativeTokenAmount>,
323		}
324
325		#[async_trait]
326		impl NativeTokenManagementDataSource for MockNativeTokenDataSource {
327			async fn get_total_native_token_transfer(
328				&self,
329				after_block: Option<McBlockHash>,
330				to_block: McBlockHash,
331				_scripts: MainChainScripts,
332			) -> Result<NativeTokenAmount, Box<dyn std::error::Error + Send + Sync>> {
333				Ok(self.transfers.get(&(after_block, to_block)).cloned().unwrap_or_default())
334			}
335		}
336	}
337}