#![doc = include_str!("../README.md")]
#![allow(clippy::expect_used)]
#![cfg_attr(not(feature = "std"), no_std)]
#![deny(
rustdoc::broken_intra_doc_links,
rustdoc::missing_crate_level_docs,
rustdoc::invalid_codeblock_attributes,
missing_docs
)]
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod migration;
#[cfg(test)]
mod tests;
pub mod weights;
mod types;
use frame_support::{ensure, pallet_prelude::Weight, traits::Get, BoundedVec};
use sp_runtime::DispatchError;
use sp_std::{convert::TryInto, fmt::Debug, prelude::*};
use common_primitives::{
messages::*,
msa::{
DelegatorId, MessageSourceId, MsaLookup, MsaValidator, ProviderId, SchemaGrantValidator,
},
schema::*,
};
use frame_support::dispatch::DispatchResult;
use parity_scale_codec::Encode;
#[cfg(feature = "runtime-benchmarks")]
use common_primitives::benchmarks::{MsaBenchmarkHelper, SchemaBenchmarkHelper};
pub use pallet::*;
pub use types::*;
pub use weights::*;
use cid::Cid;
use frame_system::pallet_prelude::*;
const LOG_TARGET: &str = "runtime::messages";
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type WeightInfo: WeightInfo;
type MsaInfoProvider: MsaLookup + MsaValidator<AccountId = Self::AccountId>;
type SchemaGrantValidator: SchemaGrantValidator<BlockNumberFor<Self>>;
type SchemaProvider: SchemaProvider<SchemaId>;
#[pallet::constant]
type MessagesMaxPayloadSizeBytes: Get<u32> + Clone + Debug + MaxEncodedLen;
#[cfg(feature = "runtime-benchmarks")]
type MsaBenchmarkHelper: MsaBenchmarkHelper<Self::AccountId>;
#[cfg(feature = "runtime-benchmarks")]
type SchemaBenchmarkHelper: SchemaBenchmarkHelper;
}
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::storage]
#[pallet::whitelist_storage]
pub(super) type BlockMessageIndex<T: Config> = StorageValue<_, MessageIndex, ValueQuery>;
#[pallet::storage]
pub(super) type MessagesV2<T: Config> = StorageNMap<
_,
(
storage::Key<Twox64Concat, BlockNumberFor<T>>,
storage::Key<Twox64Concat, SchemaId>,
storage::Key<Twox64Concat, MessageIndex>,
),
Message<T::MessagesMaxPayloadSizeBytes>,
OptionQuery,
>;
#[pallet::error]
pub enum Error<T> {
TooManyMessagesInBlock,
ExceedsMaxMessagePayloadSizeBytes,
TypeConversionOverflow,
InvalidMessageSourceAccount,
InvalidSchemaId,
UnAuthorizedDelegate,
InvalidPayloadLocation,
UnsupportedCidVersion,
InvalidCid,
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
MessagesStored {
schema_id: SchemaId,
block_number: BlockNumberFor<T>,
},
MessagesInBlock,
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(_current: BlockNumberFor<T>) -> Weight {
<BlockMessageIndex<T>>::set(0u16);
T::DbWeight::get().reads(1u64).saturating_add(T::DbWeight::get().writes(1u64))
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::add_ipfs_message())]
pub fn add_ipfs_message(
origin: OriginFor<T>,
#[pallet::compact] schema_id: SchemaId,
cid: Vec<u8>,
#[pallet::compact] payload_length: u32,
) -> DispatchResult {
let provider_key = ensure_signed(origin)?;
let cid_binary = Self::validate_cid(&cid)?;
let payload_tuple: OffchainPayloadType = (cid_binary, payload_length);
let bounded_payload: BoundedVec<u8, T::MessagesMaxPayloadSizeBytes> = payload_tuple
.encode()
.try_into()
.map_err(|_| Error::<T>::ExceedsMaxMessagePayloadSizeBytes)?;
if let Some(schema) = T::SchemaProvider::get_schema_info_by_id(schema_id) {
ensure!(
schema.payload_location == PayloadLocation::IPFS,
Error::<T>::InvalidPayloadLocation
);
let provider_msa_id = Self::find_msa_id(&provider_key)?;
let current_block = frame_system::Pallet::<T>::block_number();
if Self::add_message(
provider_msa_id,
None,
bounded_payload,
schema_id,
current_block,
)? {
Self::deposit_event(Event::MessagesInBlock);
}
Ok(())
} else {
Err(Error::<T>::InvalidSchemaId.into())
}
}
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::add_onchain_message(payload.len() as u32))]
pub fn add_onchain_message(
origin: OriginFor<T>,
on_behalf_of: Option<MessageSourceId>,
#[pallet::compact] schema_id: SchemaId,
payload: Vec<u8>,
) -> DispatchResult {
let provider_key = ensure_signed(origin)?;
let bounded_payload: BoundedVec<u8, T::MessagesMaxPayloadSizeBytes> =
payload.try_into().map_err(|_| Error::<T>::ExceedsMaxMessagePayloadSizeBytes)?;
if let Some(schema) = T::SchemaProvider::get_schema_info_by_id(schema_id) {
ensure!(
schema.payload_location == PayloadLocation::OnChain,
Error::<T>::InvalidPayloadLocation
);
let provider_msa_id = Self::find_msa_id(&provider_key)?;
let provider_id = ProviderId(provider_msa_id);
let current_block = frame_system::Pallet::<T>::block_number();
let maybe_delegator = match on_behalf_of {
Some(delegator_msa_id) => {
let delegator_id = DelegatorId(delegator_msa_id);
T::SchemaGrantValidator::ensure_valid_schema_grant(
provider_id,
delegator_id,
schema_id,
current_block,
)
.map_err(|_| Error::<T>::UnAuthorizedDelegate)?;
delegator_id
},
None => DelegatorId(provider_msa_id), };
if Self::add_message(
provider_msa_id,
Some(maybe_delegator.into()),
bounded_payload,
schema_id,
current_block,
)? {
Self::deposit_event(Event::MessagesInBlock);
}
Ok(())
} else {
Err(Error::<T>::InvalidSchemaId.into())
}
}
}
}
impl<T: Config> Pallet<T> {
pub fn add_message(
provider_msa_id: MessageSourceId,
msa_id: Option<MessageSourceId>,
payload: BoundedVec<u8, T::MessagesMaxPayloadSizeBytes>,
schema_id: SchemaId,
current_block: BlockNumberFor<T>,
) -> Result<bool, DispatchError> {
let index = BlockMessageIndex::<T>::get();
let first = index == 0;
let msg = Message {
payload, provider_msa_id,
msa_id,
};
<MessagesV2<T>>::insert((current_block, schema_id, index), msg);
BlockMessageIndex::<T>::set(index.saturating_add(1));
Ok(first)
}
pub fn find_msa_id(key: &T::AccountId) -> Result<MessageSourceId, DispatchError> {
Ok(T::MsaInfoProvider::ensure_valid_msa_key(key)
.map_err(|_| Error::<T>::InvalidMessageSourceAccount)?)
}
pub fn get_messages_by_schema_and_block(
schema_id: SchemaId,
schema_payload_location: PayloadLocation,
block_number: BlockNumberFor<T>,
) -> Vec<MessageResponse> {
let block_number_value: u32 = block_number.try_into().unwrap_or_default();
match schema_payload_location {
PayloadLocation::Itemized | PayloadLocation::Paginated => return Vec::new(),
_ => {
let mut messages: Vec<_> = <MessagesV2<T>>::iter_prefix((block_number, schema_id))
.map(|(index, msg)| {
msg.map_to_response(block_number_value, schema_payload_location, index)
})
.collect();
messages.sort_by(|a, b| a.index.cmp(&b.index));
return messages
},
}
}
pub fn validate_cid(in_cid: &Vec<u8>) -> Result<Vec<u8>, DispatchError> {
let cid_str: &str =
sp_std::str::from_utf8(&in_cid[..]).map_err(|_| Error::<T>::InvalidCid)?;
ensure!(cid_str.len() > 2, Error::<T>::InvalidCid);
ensure!(!cid_str.starts_with("Qm"), Error::<T>::UnsupportedCidVersion);
let cid_b = multibase::decode(cid_str).map_err(|_| Error::<T>::InvalidCid)?.1;
ensure!(Cid::read_bytes(&cid_b[..]).is_ok(), Error::<T>::InvalidCid);
Ok(cid_b)
}
}