pub mod constants;
pub mod types;
use constants::BEACON_PUBKEY;
use ink::{
env::{
hash::{Blake2x256, CryptoHash},
Error as EnvError,
},
xcm::lts::prelude::Weight,
};
#[cfg(not(test))]
use ink::{
prelude::vec,
xcm::{
lts::{
prelude::{
BuyExecution, DepositAsset, OriginKind as XcmOriginKind, RefundSurplus, Transact,
WithdrawAsset, Xcm,
},
Asset,
AssetFilter::Wild,
AssetId, Junctions,
WeightLimit::Unlimited,
WildAsset::AllOf,
WildFungibility,
},
VersionedLocation, VersionedXcm,
},
};
pub use bp_idn::{
types::{
QuoteRequest, QuoteSubParams, RequestReference, SubInfoRequest, Subscription,
SubscriptionDetails, SubscriptionState,
},
Call as RuntimeCall, IdnManagerCall,
};
use codec::{Compact, Decode, Encode};
use ink::xcm::lts::{Junction, Location};
#[cfg(not(test))]
use scale_info::prelude::boxed::Box;
use scale_info::prelude::vec::Vec;
use sp_idn_traits::pulse::Pulse as TPulse;
use types::{
AccountId, Balance, CallData, CreateSubParams, Credits, IdnBlockNumber, IdnXcm, Metadata,
OriginKind, PalletIndex, ParaId, Pulse, Quote, SubInfoResponse, SubscriptionId,
UpdateSubParams,
};
use crate::xcm::constants::{CONSUME_PULSE_SEL, CONSUME_QUOTE_SEL, CONSUME_SUB_INFO_SEL};
pub trait Hashable {
fn hash(&self, salt: &[u8]) -> [u8; 32];
}
#[derive(Encode, Decode)]
pub struct ContractsCall {
pub dest: MultiAddress,
#[codec(compact)]
pub value: Balance,
pub gas_limit: Weight,
pub storage_deposit_limit: Option<Compact<Balance>>,
pub data: Vec<u8>,
}
#[derive(Encode, Decode)]
pub enum MultiAddress {
Id(AccountId),
}
#[derive(Encode, Decode)]
pub struct ContractCallParams {
pub value: Balance,
pub gas_limit_ref_time: u64,
pub gas_limit_proof_size: u64,
pub storage_deposit_limit: Option<Balance>,
}
impl<T> Hashable for T
where
T: Encode,
{
fn hash(&self, salt: &[u8]) -> [u8; 32] {
let id_tuple = (self, salt);
let encoded = id_tuple.encode();
let mut output = [0u8; 32];
Blake2x256::hash(&encoded, &mut output);
output
}
}
#[allow(clippy::cast_possible_truncation)]
#[derive(Debug, PartialEq, Eq)]
#[ink::scale_derive(Encode, Decode, TypeInfo)]
pub enum Error {
XcmExecutionFailed,
XcmSendFailed,
NonXcmEnvError,
MethodNotImplemented,
ConsumePulseError,
ConsumeQuoteError,
ConsumeSubInfoError,
Unauthorized,
InvalidSubscriptionId,
CallDataTooLong,
InvalidParams,
Other,
}
impl From<EnvError> for Error {
fn from(env_error: EnvError) -> Self {
use ink::env::ReturnErrorCode;
match env_error {
EnvError::ReturnError(ReturnErrorCode::XcmExecutionFailed) => Error::XcmExecutionFailed,
EnvError::ReturnError(ReturnErrorCode::XcmSendFailed) => Error::XcmSendFailed,
_ => Error::NonXcmEnvError,
}
}
}
pub type Result<T> = core::result::Result<T, Error>;
#[ink::trait_definition]
pub trait IdnConsumer {
#[ink(message)]
fn consume_pulse(&mut self, pulse: Pulse, sub_id: SubscriptionId) -> Result<()>;
#[ink(message)]
fn consume_quote(&mut self, quote: Quote) -> Result<()>;
#[ink(message)]
fn consume_sub_info(&mut self, sub_info: SubInfoResponse) -> Result<()>;
}
#[derive(Clone, Copy, Encode, Decode, Debug)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))]
pub struct IdnClient {
pub idn_para_id: ParaId,
pub idn_manager_pallet_index: PalletIndex,
pub self_para_id: ParaId,
pub self_contracts_pallet_index: PalletIndex,
pub self_contract_call_index: u8,
pub max_idn_xcm_fees: u128,
}
impl IdnClient {
pub fn new(
idn_para_id: ParaId,
idn_manager_pallet_index: PalletIndex,
self_para_id: ParaId,
self_contracts_pallet_index: PalletIndex,
self_contract_call_index: u8,
max_idn_xcm_fees: u128,
) -> Self {
Self {
idn_para_id,
idn_manager_pallet_index,
self_para_id,
self_contracts_pallet_index,
self_contract_call_index,
max_idn_xcm_fees,
}
}
pub fn get_idn_manager_pallet_index(&self) -> PalletIndex {
self.idn_manager_pallet_index
}
pub fn get_idn_para_id(&self) -> ParaId {
self.idn_para_id
}
pub fn get_self_contracts_pallet_index(&self) -> PalletIndex {
self.self_contracts_pallet_index
}
pub fn get_self_contract_call_index(&self) -> u8 {
self.self_contract_call_index
}
pub fn get_self_para_id(&self) -> ParaId {
self.self_para_id
}
pub fn create_subscription(
&self,
credits: Credits,
frequency: IdnBlockNumber,
metadata: Option<Metadata>,
sub_id: Option<SubscriptionId>,
call_params: Option<ContractCallParams>,
origin_kind: Option<OriginKind>,
) -> Result<SubscriptionId> {
if credits == 0 || frequency == 0 {
return Err(Error::InvalidParams);
}
let dummy_pulse = Pulse::default();
let dummy_sub_id = SubscriptionId::default();
let dummy_params = (dummy_pulse, dummy_sub_id).encode();
let mut params = CreateSubParams {
credits,
target: self.self_para_sibling_location(),
call: self.create_callback_data(CONSUME_PULSE_SEL, dummy_params, call_params)?,
frequency,
metadata,
sub_id,
origin_kind: origin_kind.unwrap_or(OriginKind::Native),
};
let sub_id = match sub_id {
Some(sub_id) => sub_id,
None => {
let salt = ink::env::block_timestamp::<ink::env::DefaultEnvironment>().encode();
let sub_id = params.hash(&salt);
params.sub_id = Some(sub_id);
sub_id
},
};
let call = RuntimeCall::IdnManager(IdnManagerCall::create_subscription { params });
self.xcm_send(call)?;
Ok(sub_id)
}
pub fn pause_subscription(&self, sub_id: SubscriptionId) -> Result<()> {
let call = RuntimeCall::IdnManager(IdnManagerCall::pause_subscription { sub_id });
self.xcm_send(call)
}
pub fn reactivate_subscription(&self, sub_id: SubscriptionId) -> Result<()> {
let call = RuntimeCall::IdnManager(IdnManagerCall::reactivate_subscription { sub_id });
self.xcm_send(call)
}
pub fn update_subscription(
&mut self,
sub_id: SubscriptionId,
credits: Option<Credits>,
frequency: Option<IdnBlockNumber>,
metadata: Option<Option<Metadata>>,
) -> Result<()> {
let params = UpdateSubParams { sub_id, credits, frequency, metadata };
let call = RuntimeCall::IdnManager(IdnManagerCall::update_subscription { params });
self.xcm_send(call)
}
pub fn kill_subscription(&self, sub_id: SubscriptionId) -> Result<()> {
let call = RuntimeCall::IdnManager(IdnManagerCall::kill_subscription { sub_id });
self.xcm_send(call)
}
pub fn request_quote(
&self,
number_of_pulses: IdnBlockNumber,
frequency: IdnBlockNumber,
metadata: Option<Metadata>,
sub_id: Option<SubscriptionId>,
req_ref: Option<RequestReference>,
origin_kind: Option<OriginKind>,
) -> Result<()> {
let req_ref = match req_ref {
Some(req_ref) => req_ref,
None => {
let salt = ink::env::block_number::<ink::env::DefaultEnvironment>().encode();
frequency.hash(&salt)
},
};
let mut dummy_params = Vec::new();
let create_sub_params = CreateSubParams {
credits: 0,
target: self.self_para_sibling_location(),
call: self.create_callback_data(CONSUME_QUOTE_SEL, dummy_params.clone(), None)?,
origin_kind: origin_kind.clone().unwrap_or(OriginKind::Native),
frequency,
metadata,
sub_id,
};
let quote = Quote { req_ref, fees: u128::default(), deposit: u128::default() };
dummy_params = quote.encode();
let quote_request =
QuoteRequest { req_ref, create_sub_params, lifetime_pulses: number_of_pulses };
let req = QuoteSubParams {
quote_request,
call: self.create_callback_data(CONSUME_QUOTE_SEL, dummy_params, None)?,
origin_kind: origin_kind.unwrap_or(OriginKind::Native),
};
let call = RuntimeCall::IdnManager(IdnManagerCall::quote_subscription { params: req });
self.xcm_send(call)
}
pub fn request_sub_info(
&self,
sub_id: SubscriptionId,
metadata: Option<Metadata>,
req_ref: Option<RequestReference>,
call_params: Option<ContractCallParams>,
origin_kind: Option<OriginKind>,
) -> Result<()> {
let req_ref = match req_ref {
Some(req_ref) => req_ref,
None => {
let salt = ink::env::block_number::<ink::env::DefaultEnvironment>().encode();
sub_id.hash(&salt)
},
};
let dummy_sub_info_response =
self.create_dummy_sub_info_response(sub_id, req_ref, metadata, call_params)?;
let dummy_params = dummy_sub_info_response.encode();
let req = SubInfoRequest {
sub_id,
req_ref,
call: self.create_callback_data(CONSUME_SUB_INFO_SEL, dummy_params, None)?,
origin_kind: origin_kind.unwrap_or(OriginKind::Native),
};
let call = RuntimeCall::IdnManager(IdnManagerCall::get_subscription_info { req });
self.xcm_send(call)
}
pub fn create_dummy_sub_info_response(
&self,
sub_id: SubscriptionId,
req_ref: [u8; 32],
metadata: Option<Metadata>,
call_params: Option<ContractCallParams>,
) -> Result<SubInfoResponse> {
let dummy_pulse = Pulse::default();
let dummy_sub_id = SubscriptionId::default();
let dummy_details_parameters = (dummy_pulse, dummy_sub_id).encode();
let dummy_details = SubscriptionDetails {
subscriber: sub_id.into(),
target: self.self_para_sibling_location(),
origin_kind: OriginKind::Native,
call: self.create_callback_data(
CONSUME_PULSE_SEL,
dummy_details_parameters,
call_params,
)?,
};
let dummy_sub = Subscription {
id: sub_id,
state: SubscriptionState::Active,
metadata,
last_delivered: Some(u32::default()),
details: dummy_details,
credits_left: u64::default(),
created_at: u32::default(),
updated_at: u32::default(),
credits: u64::default(),
frequency: u32::default(),
};
let dummy_sub_response = SubInfoResponse { req_ref, sub: dummy_sub };
Ok(dummy_sub_response)
}
pub fn is_valid_pulse(&self, pulse: &Pulse) -> bool {
let pk = hex::decode(BEACON_PUBKEY).unwrap();
pulse.authenticate(pk.try_into().expect("The public key is well-defined; qed."))
}
fn self_para_sibling_location(&self) -> IdnXcm::Location {
IdnXcm::Location {
parents: 1, interior: IdnXcm::Junctions::X1(
[IdnXcm::Junction::Parachain(self.get_self_para_id()) ]
.into(),
),
}
}
#[cfg(not(test))]
fn xcm_send(&self, call: RuntimeCall) -> Result<()> {
let idn_fee_asset = Asset {
id: AssetId(Location { parents: 1, interior: Junctions::Here }),
fun: self.max_idn_xcm_fees.into(),
};
let xcm_call: Xcm<RuntimeCall> = Xcm(vec![
WithdrawAsset(idn_fee_asset.clone().into()),
BuyExecution { weight_limit: Unlimited, fees: idn_fee_asset.clone() },
Transact {
origin_kind: XcmOriginKind::Xcm,
require_weight_at_most: Weight::MAX,
call: call.encode().into(),
},
RefundSurplus,
DepositAsset {
assets: Wild(AllOf { id: idn_fee_asset.id, fun: WildFungibility::Fungible }),
beneficiary: self.contract_idn_location(),
},
]);
let versioned_target: Box<VersionedLocation> = Box::new(self.sibling_idn_location().into());
let versioned_msg: Box<VersionedXcm<()>> = Box::new(VersionedXcm::V4(xcm_call.into()));
ink::env::xcm_send::<ink::env::DefaultEnvironment, ()>(&versioned_target, &versioned_msg)
.map_err(|_err| Error::XcmSendFailed)?;
Ok(())
}
#[cfg(test)]
fn xcm_send(&self, _call: RuntimeCall) -> Result<()> {
Ok(())
}
fn sibling_idn_location(&self) -> Location {
Location::new(1, Junction::Parachain(self.get_idn_para_id()))
}
fn contract_idn_location(&self) -> Location {
Location::new(0, Junction::AccountId32 { network: None, id: *self.account_id().as_ref() })
}
#[cfg(not(test))]
fn account_id(&self) -> AccountId {
ink::env::account_id::<ink::env::DefaultEnvironment>()
}
#[cfg(test)]
fn account_id(&self) -> AccountId {
[88u8; 32].into()
}
fn create_callback_data(
&self,
selector: [u8; 4],
dummy_params: Vec<u8>,
call_params: Option<ContractCallParams>,
) -> Result<CallData> {
const DEF_VALUE: Balance = 0;
const DEF_REF_TIME: u64 = 4_000_000_000;
const DEF_PROOF_SIZE: u64 = 200_000;
const DEF_STORAGE_DEPOSIT: Option<Balance> = None;
let mut data = Vec::new();
data.extend_from_slice(&selector);
data.extend_from_slice(&dummy_params);
let mut call = self.generate_call(
call_params.as_ref().map(|p| p.value).unwrap_or(DEF_VALUE), call_params.as_ref().map(|p| p.gas_limit_ref_time).unwrap_or(DEF_REF_TIME), call_params.as_ref().map(|p| p.gas_limit_proof_size).unwrap_or(DEF_PROOF_SIZE), call_params
.as_ref()
.map(|p| p.storage_deposit_limit)
.unwrap_or(DEF_STORAGE_DEPOSIT), data, );
call.truncate(call.len().saturating_sub(dummy_params.len()));
CallData::try_from(call).map_err(|_| Error::CallDataTooLong)
}
#[cfg(not(test))]
fn generate_call(
&self,
value: Balance,
gas_limit_ref_time: u64,
gas_limit_proof_size: u64,
storage_deposit_limit: Option<Balance>,
data: Vec<u8>,
) -> Vec<u8> {
let mut encoded = Vec::new();
encoded.push(self.get_self_contracts_pallet_index());
encoded.push(self.get_self_contract_call_index());
let call = ContractsCall {
dest: MultiAddress::Id(ink::env::account_id::<ink::env::DefaultEnvironment>()),
value,
gas_limit: Weight::from_parts(gas_limit_ref_time, gas_limit_proof_size),
storage_deposit_limit: storage_deposit_limit.map(Compact),
data,
};
encoded.extend_from_slice(&call.encode());
encoded
}
#[cfg(test)]
fn generate_call(
&self,
value: Balance,
gas_limit_ref_time: u64,
gas_limit_proof_size: u64,
storage_deposit_limit: Option<Balance>,
data: Vec<u8>,
) -> Vec<u8> {
let mut encoded = Vec::new();
encoded.push(self.get_self_contracts_pallet_index());
encoded.push(self.get_self_contract_call_index());
let call = ContractsCall {
dest: MultiAddress::Id(AccountId::from([42u8; 32])), value,
gas_limit: Weight::from_parts(gas_limit_ref_time, gas_limit_proof_size),
storage_deposit_limit: storage_deposit_limit.map(Compact),
data,
};
encoded.extend_from_slice(&call.encode());
encoded
}
}
#[cfg(test)]
mod tests {
use super::{
constants::{
CONSUMER_PARA_ID_PASEO, CONTRACTS_CALL_INDEX, CONTRACTS_PALLET_INDEX_PASEO,
IDN_MANAGER_PALLET_INDEX_PASEO, IDN_PARA_ID_PASEO,
},
*,
};
fn mock_client() -> IdnClient {
IdnClient::new(
IDN_PARA_ID_PASEO,
IDN_MANAGER_PALLET_INDEX_PASEO,
CONSUMER_PARA_ID_PASEO,
CONTRACTS_PALLET_INDEX_PASEO,
CONTRACTS_CALL_INDEX,
1_000_000_000,
)
}
fn create_subscription(client: &IdnClient) -> Result<SubscriptionId> {
let credits = 100u64;
let frequency = 10u32;
let metadata: Option<types::Metadata> = None; let sub_id = Some([1u8; 32]);
client.create_subscription(credits, frequency, metadata, sub_id, None, None)
}
#[test]
fn test_client_basic_functionality() {
let client = mock_client();
assert_eq!(client.get_idn_manager_pallet_index(), IDN_MANAGER_PALLET_INDEX_PASEO);
assert_eq!(client.get_idn_para_id(), IDN_PARA_ID_PASEO);
assert_eq!(client.get_self_contracts_pallet_index(), CONTRACTS_PALLET_INDEX_PASEO);
assert_eq!(client.get_self_para_id(), CONSUMER_PARA_ID_PASEO);
assert!(client.request_sub_info([0; 32], None, None, None, None).is_ok());
assert!(client.request_quote(100, 4, None, None, None, None).is_ok());
}
#[test]
fn test_client_encoding_decoding() {
let client = mock_client();
let encoded = client.encode();
let decoded: IdnClient = Decode::decode(&mut &encoded[..]).unwrap();
assert_eq!(client.get_idn_manager_pallet_index(), decoded.get_idn_manager_pallet_index());
assert_eq!(client.get_idn_para_id(), decoded.get_idn_para_id());
assert_eq!(
client.get_self_contracts_pallet_index(),
decoded.get_self_contracts_pallet_index()
);
assert_eq!(client.get_self_para_id(), decoded.get_self_para_id());
assert_eq!(client.max_idn_xcm_fees, decoded.max_idn_xcm_fees);
}
#[test]
fn test_edge_cases() {
let edge_client = IdnClient::new(u32::MAX, u8::MAX, u32::MAX, u8::MAX, u8::MAX, u128::MAX);
assert_eq!(edge_client.get_idn_manager_pallet_index(), u8::MAX);
assert_eq!(edge_client.get_idn_para_id(), u32::MAX);
assert_eq!(edge_client.get_self_contracts_pallet_index(), u8::MAX);
assert_eq!(edge_client.get_self_para_id(), u32::MAX);
assert_eq!(edge_client.max_idn_xcm_fees, u128::MAX);
assert!(edge_client.request_sub_info([0; 32], None, None, None, None).is_ok());
assert!(edge_client.request_quote(100, 4, None, None, None, None).is_ok());
}
#[test]
fn test_error_handling() {
assert_ne!(Error::XcmExecutionFailed, Error::XcmSendFailed);
assert_ne!(Error::XcmExecutionFailed, Error::NonXcmEnvError);
}
#[test]
fn test_create_subscription_xcm_send_failure() {
let err = ink::env::Error::ReturnError(ink::env::ReturnErrorCode::XcmSendFailed);
let converted: Error = err.into();
assert_eq!(converted, Error::XcmSendFailed);
}
#[test]
fn test_subscription_management_api() {
let client = mock_client();
let quote_result = client.request_quote(1, 1, None, None, None, None);
assert!(quote_result.is_ok());
let sub_id = create_subscription(&client).unwrap();
let sub_info_result = client.request_sub_info(sub_id, None, None, None, None);
assert!(sub_info_result.is_ok());
let pause_result = client.pause_subscription(sub_id);
assert!(pause_result.is_ok());
let reactivate_result = client.reactivate_subscription(sub_id);
assert!(reactivate_result.is_ok());
let kill_result = client.kill_subscription(sub_id);
assert!(kill_result.is_ok());
}
#[test]
fn test_update_subscription_api() {
let mut client = mock_client();
let sub_id = create_subscription(&client).unwrap();
let update_result = client.update_subscription(sub_id, Some(100), Some(20), Some(None));
assert!(update_result.is_ok());
let credits_only_result = client.update_subscription(sub_id, Some(200), None, None);
assert!(credits_only_result.is_ok());
let frequency_only_result = client.update_subscription(sub_id, None, Some(5), None);
assert!(frequency_only_result.is_ok());
}
#[test]
fn test_create_subscription_edge_values() {
let client = mock_client();
let max_values_result = client.create_subscription(
u64::MAX, u32::MAX, None, Some([u8::MAX; 32]), None, None, );
assert!(max_values_result.is_ok());
let min_values_result = client.create_subscription(
1, 1, None, None, None, None, );
assert!(min_values_result.is_ok());
let zero_values_result = client.create_subscription(
0, 0, None, None, None, None, );
assert!(matches!(zero_values_result, Err(Error::InvalidParams)));
}
#[ink::test]
fn test_create_callback_data() {
let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
let client = mock_client();
let callback_data = client.create_callback_data(CONSUME_PULSE_SEL, Vec::new(), None);
assert!(callback_data.is_ok());
let callback_data_2 = client.create_callback_data(CONSUME_PULSE_SEL, Vec::new(), None);
assert_eq!(callback_data, callback_data_2);
let encoded_data = callback_data.unwrap();
assert!(!encoded_data.is_empty());
}
#[ink::test]
fn test_create_create_dummy_sub_info_response() {
let client = mock_client();
let sub_id = [1; 32];
let req_ref = [2; 32];
let dummy_sub_info_response =
client.create_dummy_sub_info_response(sub_id, req_ref, None, None);
assert!(dummy_sub_info_response.is_ok());
let dummy_sub_info_response2 =
client.create_dummy_sub_info_response(sub_id, req_ref, None, None);
assert_eq!(dummy_sub_info_response2, dummy_sub_info_response);
}
#[ink::test]
fn test_location_helper_api() {
let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
let client = mock_client();
let idn_location = client.sibling_idn_location();
assert_eq!(idn_location.parents, 1);
let self_location = client.self_para_sibling_location();
assert_eq!(self_location.parents, 1);
let contract_location = client.contract_idn_location();
assert_eq!(contract_location.parents, 0);
}
#[test]
fn test_pulse_encode_decode() {
use super::types::Pulse;
let result = std::panic::catch_unwind(|| {
let _pulse_type_exists = |_p: Pulse| {
true
};
});
assert!(result.is_ok());
}
}