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}