diff --git a/Cargo.lock b/Cargo.lock index d3d4dd3c0..0160d3b26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9110,6 +9110,7 @@ dependencies = [ "pallet-ethereum", "pallet-evm", "pallet-evm-chain-id", + "pallet-evm-precompile-balances-erc20", "pallet-evm-precompile-blake2", "pallet-evm-precompile-bn128", "pallet-evm-precompile-curve25519", @@ -9117,6 +9118,7 @@ dependencies = [ "pallet-evm-precompile-modexp", "pallet-evm-precompile-sha3fips", "pallet-evm-precompile-simple", + "pallet-evm-precompileset-assets-erc20", "pallet-multi-asset-delegation", "pallet-services", "pallet-session", @@ -9828,6 +9830,7 @@ dependencies = [ "pallet-ethereum", "pallet-evm", "pallet-evm-chain-id", + "pallet-evm-precompile-balances-erc20", "pallet-evm-precompile-blake2", "pallet-evm-precompile-bn128", "pallet-evm-precompile-curve25519", @@ -9835,6 +9838,7 @@ dependencies = [ "pallet-evm-precompile-modexp", "pallet-evm-precompile-sha3fips", "pallet-evm-precompile-simple", + "pallet-evm-precompileset-assets-erc20", "pallet-session", "pallet-staking", "pallet-timestamp", diff --git a/pallets/multi-asset-delegation/src/extra.rs b/pallets/multi-asset-delegation/src/extra.rs new file mode 100644 index 000000000..32c6ff059 --- /dev/null +++ b/pallets/multi-asset-delegation/src/extra.rs @@ -0,0 +1,89 @@ +use frame_support::pallet_prelude::*; +use frame_system::pallet_prelude::*; +use mock::{AccountId, Runtime, RuntimeCall}; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_runtime::traits::{DispatchInfoOf, SignedExtension}; +use types::BalanceOf; + +use super::*; + +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub struct CheckNominatedRestaked(core::marker::PhantomData); + +impl sp_std::fmt::Debug for CheckNominatedRestaked { + #[cfg(feature = "std")] + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + write!(f, "CheckNominatedRestaked") + } + + #[cfg(not(feature = "std"))] + fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + Ok(()) + } +} + +impl CheckNominatedRestaked { + pub fn new() -> Self { + CheckNominatedRestaked(core::marker::PhantomData) + } +} + +impl CheckNominatedRestaked { + /// See [`crate::Pallet::can_unbound`] + pub fn can_unbound(who: &T::AccountId, amount: BalanceOf) -> bool { + crate::Pallet::::can_unbound(who, amount) + } +} + +impl Default for CheckNominatedRestaked { + fn default() -> Self { + CheckNominatedRestaked(core::marker::PhantomData) + } +} + +impl SignedExtension for CheckNominatedRestaked { + const IDENTIFIER: &'static str = "CheckNominatedRestaked"; + + type AccountId = AccountId; + + type Call = RuntimeCallFor; + + type AdditionalSigned = (); + + type Pre = (); + + fn additional_signed(&self) -> Result { + Ok(()) + } + + fn validate( + &self, + who: &Self::AccountId, + call: &Self::Call, + _info: &DispatchInfoOf, + _len: usize, + ) -> TransactionValidity { + match call { + RuntimeCall::Staking(pallet_staking::Call::unbond { value }) => { + if Self::can_unbound(who, *value) { + Ok(ValidTransaction::default()) + } else { + Err(TransactionValidityError::Invalid(InvalidTransaction::Custom(1))) + } + }, + _ => Ok(ValidTransaction::default()), + } + } + + fn pre_dispatch( + self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf, + len: usize, + ) -> Result { + self.validate(who, call, info, len).map(|_| ()) + } +} diff --git a/pallets/multi-asset-delegation/src/functions/delegate.rs b/pallets/multi-asset-delegation/src/functions/delegate.rs index 7f29acd40..79ed01967 100644 --- a/pallets/multi-asset-delegation/src/functions/delegate.rs +++ b/pallets/multi-asset-delegation/src/functions/delegate.rs @@ -13,38 +13,70 @@ // // You should have received a copy of the GNU General Public License // along with Tangle. If not, see . -use super::*; -use crate::{types::*, Pallet}; + +use crate::{types::*, Config, Delegators, Error, Event, Operators, Pallet}; use frame_support::{ ensure, pallet_prelude::DispatchResult, - traits::{fungibles::Mutate, tokens::Preservation, Get}, + traits::{Get, LockIdentifier, LockableCurrency, WithdrawReasons}, }; use sp_runtime::{ - traits::{CheckedAdd, CheckedSub, Zero}, - DispatchError, Percent, -}; -use sp_std::vec::Vec; -use tangle_primitives::{ - services::{Asset, EvmAddressMapping}, - BlueprintId, + traits::{CheckedAdd, Saturating, Zero}, + DispatchError, }; +use sp_staking::StakingInterface; +use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; +use tangle_primitives::{services::Asset, traits::MultiAssetDelegationInfo, RoundIndex}; + +pub const DELEGATION_LOCK_ID: LockIdentifier = *b"delegate"; impl Pallet { /// Processes the delegation of an amount of an asset to an operator. - /// Creates a new delegation for the delegator and updates their status to active, the deposit - /// of the delegator is moved to delegation. + /// + /// This function handles both creating new delegations and increasing existing ones. + /// It updates three main pieces of state: + /// 1. The delegator's deposit record (marking funds as delegated) + /// 2. The delegator's delegation list + /// 3. The operator's delegation records + /// + /// # Performance Considerations + /// + /// - Single storage read for operator verification + /// - Single storage write for delegation update + /// - Bounded by MaxDelegations for new delegations + /// /// # Arguments /// - /// * `who` - The account ID of the delegator. - /// * `operator` - The account ID of the operator. - /// * `asset_id` - The ID of the asset to be delegated. - /// * `amount` - The amount to be delegated. + /// * `who` - The account ID of the delegator + /// * `operator` - The account ID of the operator to delegate to + /// * `asset_id` - The asset being delegated + /// * `amount` - The amount to delegate + /// * `blueprint_selection` - Strategy for selecting which blueprints to work with: + /// - Fixed: Work with specific blueprints + /// - All: Work with all available blueprints /// /// # Errors /// - /// Returns an error if the delegator does not have enough deposited balance, - /// or if the operator is not found. + /// * `NotDelegator` - Account is not a delegator + /// * `NotAnOperator` - Target account is not an operator + /// * `InsufficientBalance` - Not enough deposited balance + /// * `MaxDelegationsExceeded` - Would exceed maximum allowed delegations + /// * `OverflowRisk` - Arithmetic overflow during calculations + /// * `OperatorNotActive` - Operator is not in active status + /// + /// # Example + /// + /// ```ignore + /// // Delegate 100 tokens to operator with Fixed blueprint selection + /// let blueprint_ids = vec![1, 2, 3]; + /// process_delegate( + /// delegator, + /// operator, + /// Asset::Custom(token_id), + /// 100, + /// DelegatorBlueprintSelection::Fixed(blueprint_ids) + /// )?; + /// ``` pub fn process_delegate( who: T::AccountId, operator: T::AccountId, @@ -52,406 +84,845 @@ impl Pallet { amount: BalanceOf, blueprint_selection: DelegatorBlueprintSelection, ) -> DispatchResult { + // Verify operator exists and is active + ensure!(Self::is_operator(&operator), Error::::NotAnOperator); + ensure!(Self::is_operator_active(&operator), Error::::NotActiveOperator); + ensure!(!amount.is_zero(), Error::::InvalidAmount); + Delegators::::try_mutate(&who, |maybe_metadata| { let metadata = maybe_metadata.as_mut().ok_or(Error::::NotDelegator)?; - // Ensure enough deposited balance + // Ensure enough deposited balance and update it let user_deposit = metadata.deposits.get_mut(&asset_id).ok_or(Error::::InsufficientBalance)?; - - // update the user deposit user_deposit .increase_delegated_amount(amount) .map_err(|_| Error::::InsufficientBalance)?; - // Check if the delegation exists and update it, otherwise create a new delegation - if let Some(delegation) = metadata + // Find existing delegation or create new one + let delegation_exists = metadata .delegations - .iter_mut() - .find(|d| d.operator == operator && d.asset_id == asset_id) - { - delegation.amount = - delegation.amount.checked_add(&amount).ok_or(Error::::OverflowRisk)?; - } else { - // Create the new delegation - let new_delegation = BondInfoDelegator { - operator: operator.clone(), - amount, - asset_id, - blueprint_selection, - }; - - // Create a mutable copy of delegations - let mut delegations = metadata.delegations.clone(); - delegations - .try_push(new_delegation) - .map_err(|_| Error::::MaxDelegationsExceeded)?; - metadata.delegations = delegations; - - // Update the status - metadata.status = DelegatorStatus::Active; - } - - // Update the operator's metadata - if let Some(mut operator_metadata) = Operators::::get(&operator) { - // Check if the operator has capacity for more delegations - ensure!( - operator_metadata.delegation_count < T::MaxDelegations::get(), - Error::::MaxDelegationsExceeded - ); - - // Create and push the new delegation bond - let delegation = DelegatorBond { delegator: who.clone(), amount, asset_id }; - - let mut delegations = operator_metadata.delegations.clone(); - - // Check if delegation already exists - if let Some(existing_delegation) = - delegations.iter_mut().find(|d| d.delegator == who && d.asset_id == asset_id) - { - existing_delegation.amount = existing_delegation - .amount - .checked_add(&amount) - .ok_or(Error::::OverflowRisk)?; - } else { - delegations - .try_push(delegation) + .iter() + .position(|d| d.operator == operator && d.asset_id == asset_id && !d.is_nomination); + + match delegation_exists { + Some(idx) => { + // Update existing delegation + let delegation = &mut metadata.delegations[idx]; + delegation.amount = + delegation.amount.checked_add(&amount).ok_or(Error::::OverflowRisk)?; + }, + None => { + // Create new delegation + metadata + .delegations + .try_push(BondInfoDelegator { + operator: operator.clone(), + amount, + asset_id, + blueprint_selection, + is_nomination: false, + }) .map_err(|_| Error::::MaxDelegationsExceeded)?; - operator_metadata.delegation_count = - operator_metadata.delegation_count.saturating_add(1); - } - - operator_metadata.delegations = delegations; - // Update storage - Operators::::insert(&operator, operator_metadata); - } else { - return Err(Error::::NotAnOperator.into()); + metadata.status = DelegatorStatus::Active; + }, } + // Update operator metadata + Self::update_operator_metadata(&operator, &who, asset_id, amount, true)?; + + // Emit event + Self::deposit_event(Event::Delegated { who: who.clone(), operator, amount, asset_id }); + Ok(()) }) } /// Schedules a stake reduction for a delegator. /// + /// Creates an unstake request that can be executed after the delegation bond less delay period. + /// The actual unstaking occurs when `execute_delegator_unstake` is called after the delay. + /// Multiple unstake requests for the same delegation are allowed, but each must be within + /// the available delegated amount. + /// + /// # Performance Considerations + /// + /// - Single storage read for delegation verification + /// - Single storage write for request creation + /// - Bounded by MaxUnstakeRequests + /// /// # Arguments /// - /// * `who` - The account ID of the delegator. - /// * `operator` - The account ID of the operator. - /// * `asset_id` - The ID of the asset to be reduced. - /// * `amount` - The amount to be reduced. + /// * `who` - The account ID of the delegator + /// * `operator` - The account ID of the operator + /// * `asset_id` - The asset to unstake + /// * `amount` - The amount to unstake /// /// # Errors /// - /// Returns an error if the delegator has no active delegation, - /// or if the unstake amount is greater than the current delegation amount. + /// * `NotDelegator` - Account is not a delegator + /// * `NoActiveDelegation` - No active delegation found for operator and asset + /// * `InsufficientBalance` - Trying to unstake more than delegated + /// * `MaxUnstakeRequestsExceeded` - Too many pending unstake requests + /// * `InvalidAmount` - Attempting to unstake zero tokens + /// + /// # Example + /// + /// ```ignore + /// // Schedule unstaking of 50 tokens from operator + /// process_schedule_delegator_unstake( + /// delegator, + /// operator, + /// Asset::Custom(token_id), + /// 50 + /// )?; + /// ``` pub fn process_schedule_delegator_unstake( who: T::AccountId, operator: T::AccountId, asset_id: Asset, amount: BalanceOf, ) -> DispatchResult { + ensure!(!amount.is_zero(), Error::::InvalidAmount); + Delegators::::try_mutate(&who, |maybe_metadata| { let metadata = maybe_metadata.as_mut().ok_or(Error::::NotDelegator)?; - // Ensure the delegator has an active delegation with the operator for the given asset - let delegation_index = metadata + // Find and validate delegation in a single pass + let delegation = metadata .delegations .iter() - .position(|d| d.operator == operator && d.asset_id == asset_id) + .find(|d| d.operator == operator && d.asset_id == asset_id && !d.is_nomination) .ok_or(Error::::NoActiveDelegation)?; - // Get the delegation and clone necessary data - let blueprint_selection = - metadata.delegations[delegation_index].blueprint_selection.clone(); - let delegation = &mut metadata.delegations[delegation_index]; - ensure!(delegation.amount >= amount, Error::::InsufficientBalance); + // Verify sufficient delegation amount considering existing unstake requests + let pending_unstake_amount: BalanceOf = metadata + .delegator_unstake_requests + .iter() + .filter(|r| r.operator == operator && r.asset_id == asset_id) + .fold(Zero::zero(), |acc, r| acc.saturating_add(r.amount)); - delegation.amount = - delegation.amount.checked_sub(&amount).ok_or(Error::::InsufficientBalance)?; + let available_amount = delegation.amount.saturating_sub(pending_unstake_amount); + ensure!(available_amount >= amount, Error::::InsufficientBalance); // Create the unstake request - let current_round = Self::current_round(); - let mut unstake_requests = metadata.delegator_unstake_requests.clone(); - unstake_requests - .try_push(BondLessRequest { - operator: operator.clone(), - asset_id, - amount, - requested_round: current_round, - blueprint_selection, - }) - .map_err(|_| Error::::MaxUnstakeRequestsExceeded)?; - metadata.delegator_unstake_requests = unstake_requests; + Self::create_unstake_request( + metadata, + operator.clone(), + asset_id, + amount, + delegation.blueprint_selection.clone(), + false, // is_nomination = false for regular delegations + )?; - // Remove the delegation if the remaining amount is zero - if delegation.amount.is_zero() { - metadata.delegations.remove(delegation_index); - } + Ok(()) + }) + } - // Update the operator's metadata - Operators::::try_mutate(&operator, |maybe_operator_metadata| -> DispatchResult { - let operator_metadata = - maybe_operator_metadata.as_mut().ok_or(Error::::NotAnOperator)?; + /// Cancels a scheduled stake reduction for a delegator. + /// + /// This function removes a pending unstake request without modifying any actual delegations. + /// It performs a simple lookup and removal of the matching request. + /// + /// # Performance Considerations + /// + /// - Single storage read for request verification + /// - Single storage write for request removal + /// - O(n) search through unstake requests + /// + /// # Arguments + /// + /// * `who` - The account ID of the delegator + /// * `operator` - The operator whose unstake request to cancel + /// * `asset_id` - The asset of the unstake request + /// * `amount` - The exact amount of the unstake request to cancel + /// + /// # Errors + /// + /// * `NotDelegator` - Account is not a delegator + /// * `NoBondLessRequest` - No matching unstake request found + /// * `InvalidAmount` - Amount specified is zero + /// + /// # Example + /// + /// ```ignore + /// // Cancel an unstake request for 50 tokens + /// process_cancel_delegator_unstake( + /// delegator, + /// operator, + /// Asset::Custom(token_id), + /// 50 + /// )?; + /// ``` + pub fn process_cancel_delegator_unstake( + who: T::AccountId, + operator: T::AccountId, + asset_id: Asset, + amount: BalanceOf, + ) -> DispatchResult { + ensure!(!amount.is_zero(), Error::::InvalidAmount); - // Ensure the operator has a matching delegation - let operator_delegation_index = operator_metadata - .delegations - .iter() - .position(|d| d.delegator == who && d.asset_id == asset_id) - .ok_or(Error::::NoActiveDelegation)?; + Delegators::::try_mutate(&who, |maybe_metadata| { + let metadata = maybe_metadata.as_mut().ok_or(Error::::NotDelegator)?; - let operator_delegation = - &mut operator_metadata.delegations[operator_delegation_index]; - - // Reduce the amount in the operator's delegation - ensure!(operator_delegation.amount >= amount, Error::::InsufficientBalance); - operator_delegation.amount = operator_delegation - .amount - .checked_sub(&amount) - .ok_or(Error::::InsufficientBalance)?; - - // Remove the delegation if the remaining amount is zero - if operator_delegation.amount.is_zero() { - operator_metadata.delegations.remove(operator_delegation_index); - operator_metadata.delegation_count = operator_metadata - .delegation_count - .checked_sub(1u32) - .ok_or(Error::::InsufficientBalance)?; - } + // Find and remove the matching unstake request + let request_index = metadata + .delegator_unstake_requests + .iter() + .position(|r| { + r.asset_id == asset_id + && r.amount == amount + && r.operator == operator + && !r.is_nomination + }) + .ok_or(Error::::NoBondLessRequest)?; - Ok(()) - })?; + // Remove the request and emit event + metadata.delegator_unstake_requests.remove(request_index); Ok(()) }) } - /// Executes scheduled stake reductions for a delegator. + /// Executes all ready unstake requests for a delegator. + /// + /// This function processes multiple unstake requests in a batched manner for efficiency: + /// 1. Aggregates all ready requests by asset and operator to minimize storage operations + /// 2. Updates deposits, delegations, and operator metadata in batches + /// 3. Removes zero-amount delegations and processed requests + /// + /// # Performance Considerations + /// + /// - Uses batch processing to minimize storage reads/writes + /// - Aggregates updates by asset and operator + /// - Removes items in reverse order to avoid unnecessary shifting /// /// # Arguments /// - /// * `who` - The account ID of the delegator. + /// * `who` - The account ID of the delegator /// /// # Errors /// - /// Returns an error if the delegator has no unstake requests or if none of the unstake requests - /// are ready. - pub fn process_execute_delegator_unstake(who: T::AccountId) -> DispatchResult { - Delegators::::try_mutate(&who, |maybe_metadata| { + /// * `NotDelegator` - Account is not a delegator + /// * `NoBondLessRequest` - No unstake requests exist + /// * `BondLessNotReady` - No requests are ready for execution + /// * `NoActiveDelegation` - Referenced delegation not found + /// * `InsufficientBalance` - Insufficient balance for unstaking + pub fn process_execute_delegator_unstake( + who: T::AccountId, + ) -> Result, BalanceOf)>, DispatchError> { + Delegators::::try_mutate(&who, |maybe_metadata| -> Result, BalanceOf)>, DispatchError> { let metadata = maybe_metadata.as_mut().ok_or(Error::::NotDelegator)?; - - // Ensure there are outstanding unstake requests ensure!(!metadata.delegator_unstake_requests.is_empty(), Error::::NoBondLessRequest); let current_round = Self::current_round(); let delay = T::DelegationBondLessDelay::get(); - // First, collect all ready requests and process them - let ready_requests: Vec<_> = metadata - .delegator_unstake_requests - .iter() - .filter(|request| current_round >= delay + request.requested_round) - .cloned() - .collect(); + // Aggregate all updates from ready requests + let (deposit_updates, delegation_updates, operator_updates, indices_to_remove) = + Self::aggregate_unstake_requests(metadata, current_round, delay)?; - // If no requests are ready, return an error - ensure!(!ready_requests.is_empty(), Error::::BondLessNotReady); + // Create a map to aggregate amounts by operator and asset + let mut event_aggregates = BTreeMap::<(T::AccountId, Asset), BalanceOf>::new(); - // Process each ready request - for request in ready_requests.iter() { - let deposit_record = metadata - .deposits - .get_mut(&request.asset_id) - .ok_or(Error::::InsufficientBalance)?; + // Sum up amounts by operator and asset + for &idx in &indices_to_remove { + if let Some(request) = metadata.delegator_unstake_requests.get(idx) { + let key = (request.operator.clone(), request.asset_id); + let entry = event_aggregates.entry(key).or_insert(Zero::zero()); + *entry = entry.saturating_add(request.amount); + } + } - deposit_record - .decrease_delegated_amount(request.amount) + // Apply updates in batches + // 1. Update deposits + for (asset_id, amount) in deposit_updates { + metadata + .deposits + .get_mut(&asset_id) + .ok_or(Error::::InsufficientBalance)? + .decrease_delegated_amount(amount) .map_err(|_| Error::::InsufficientBalance)?; } - // Remove the processed requests - metadata - .delegator_unstake_requests - .retain(|request| current_round < delay + request.requested_round); + // 2. Update delegations + let mut delegations_to_remove = Vec::new(); + for ((_, _), (idx, amount)) in delegation_updates { + let delegation = + metadata.delegations.get_mut(idx).ok_or(Error::::NoActiveDelegation)?; + ensure!(delegation.amount >= amount, Error::::InsufficientBalance); - Ok(()) + delegation.amount = delegation.amount.saturating_sub(amount); + if delegation.amount.is_zero() { + delegations_to_remove.push(idx); + } + } + + // 3. Remove zero-amount delegations + delegations_to_remove.sort_unstable_by(|a, b| b.cmp(a)); + for idx in delegations_to_remove { + metadata.delegations.remove(idx); + } + + // 4. Update operator metadata + for ((operator, asset_id), amount) in operator_updates { + Self::update_operator_metadata(&operator, &who, asset_id, amount, false)?; + } + + // 5. Remove processed requests + let mut indices = indices_to_remove; + indices.sort_unstable_by(|a, b| b.cmp(a)); + for idx in indices { + metadata.delegator_unstake_requests.remove(idx); + } + + // Convert the aggregates map into a vector for return + Ok(event_aggregates + .into_iter() + .map(|((operator, asset_id), amount)| (operator, asset_id, amount)) + .collect()) }) } - /// Cancels a scheduled stake reduction for a delegator. + /// Processes the delegation of nominated tokens to an operator. + /// + /// This function allows delegators to utilize their nominated (staked) tokens in the delegation system. + /// It differs from regular delegation in that: + /// 1. It uses nominated tokens instead of deposited assets + /// 2. It maintains a lock on the nominated tokens + /// 3. It tracks total nomination delegations to prevent over-delegation + /// + /// # Performance Considerations + /// + /// - External call to staking system for verification + /// - Single storage read for delegation lookup + /// - Single storage write for delegation update + /// - Additional storage write for token locking /// /// # Arguments /// - /// * `who` - The account ID of the delegator. - /// * `asset_id` - The ID of the asset for which to cancel the unstake request. - /// * `amount` - The amount of the unstake request to cancel. + /// * `who` - The account ID of the delegator + /// * `operator` - The operator to delegate to + /// * `amount` - The amount of nominated tokens to delegate + /// * `blueprint_selection` - Strategy for selecting which blueprints to work with /// /// # Errors /// - /// Returns an error if the delegator has no matching unstake request or if there is no active - /// delegation. - pub fn process_cancel_delegator_unstake( + /// * `NotDelegator` - Account is not a delegator + /// * `NotNominator` - Account has no nominated tokens + /// * `InsufficientBalance` - Not enough nominated tokens available + /// * `MaxDelegationsExceeded` - Would exceed maximum allowed delegations + /// * `OverflowRisk` - Arithmetic overflow during calculations + /// * `InvalidAmount` - Amount specified is zero + /// + /// # Example + /// + /// ```ignore + /// // Delegate 1000 nominated tokens to operator + /// process_delegate_nominations( + /// delegator, + /// operator, + /// 1000, + /// DelegatorBlueprintSelection::All + /// )?; + /// ``` + pub(crate) fn process_delegate_nominations( who: T::AccountId, operator: T::AccountId, - asset_id: Asset, amount: BalanceOf, + blueprint_selection: DelegatorBlueprintSelection, ) -> DispatchResult { - Delegators::::try_mutate(&who, |maybe_metadata| { + ensure!(!amount.is_zero(), Error::::InvalidAmount); + ensure!(Self::is_operator(&operator), Error::::NotAnOperator); + ensure!(Self::is_operator_active(&operator), Error::::NotActiveOperator); + + // Verify nomination amount in the staking system + let nominated_amount = Self::verify_nomination_amount(&who, amount)?; + + Delegators::::try_mutate(&who, |maybe_metadata| -> DispatchResult { + let metadata = maybe_metadata.get_or_insert_with(Default::default); + + // Calculate new total after this delegation + let current_total = metadata.total_nomination_delegations(); + let new_total = current_total.checked_add(&amount).ok_or(Error::::OverflowRisk)?; + + // Ensure total delegations don't exceed nominated amount + ensure!(new_total <= nominated_amount, Error::::InsufficientBalance); + + // Find existing nomination delegation or create new one + let delegation_exists = metadata + .delegations + .iter() + .position(|d| d.operator == operator && d.is_nomination); + + match delegation_exists { + Some(idx) => { + // Update existing delegation + let delegation = &mut metadata.delegations[idx]; + let new_amount = + delegation.amount.checked_add(&amount).ok_or(Error::::OverflowRisk)?; + + delegation.amount = new_amount; + T::Currency::set_lock( + DELEGATION_LOCK_ID, + &who, + new_amount, + WithdrawReasons::TRANSFER, + ); + }, + None => { + // Create new delegation + metadata + .delegations + .try_push(BondInfoDelegator { + operator: operator.clone(), + amount, + asset_id: Asset::Custom(Zero::zero()), + blueprint_selection, + is_nomination: true, + }) + .map_err(|_| Error::::MaxDelegationsExceeded)?; + + T::Currency::set_lock( + DELEGATION_LOCK_ID, + &who, + amount, + WithdrawReasons::TRANSFER, + ); + }, + } + + // Update operator metadata + Self::update_operator_metadata( + &operator, + &who, + Asset::Custom(Zero::zero()), + amount, + true, // is_increase = true for delegation + )?; + + // Emit event + Self::deposit_event(Event::NominationDelegated { + who: who.clone(), + operator: operator.clone(), + amount, + }); + + Ok(()) + })?; + + Ok(()) + } + + /// Schedules an unstake request for nomination delegations. + /// + /// Similar to regular unstaking but specifically for nominated tokens. This function: + /// 1. Verifies the nomination delegation exists + /// 2. Checks if there's enough balance to unstake + /// 3. Creates an unstake request that can be executed after the delay period + /// + /// # Performance Considerations + /// + /// - Single storage read for delegation verification + /// - Single storage write for request creation + /// - O(n) search through delegations + /// + /// # Arguments + /// + /// * `who` - The account ID of the delegator + /// * `operator` - The operator to unstake from + /// * `amount` - The amount of nominated tokens to unstake + /// * `blueprint_selection` - The blueprint selection to use after unstaking + /// + /// # Errors + /// + /// * `NotDelegator` - Account is not a delegator + /// * `NoActiveDelegation` - No active nomination delegation found + /// * `InsufficientBalance` - Trying to unstake more than delegated + /// * `MaxUnstakeRequestsExceeded` - Too many pending unstake requests + /// * `InvalidAmount` - Amount specified is zero + /// * `AssetNotWhitelisted` - Invalid asset type for nominations + /// + /// # Example + /// + /// ```ignore + /// // Schedule unstaking of 500 nominated tokens + /// process_schedule_delegator_nomination_unstake( + /// &delegator, + /// operator, + /// 500, + /// DelegatorBlueprintSelection::All + /// )?; + /// ``` + pub fn process_schedule_delegator_nomination_unstake( + who: &T::AccountId, + operator: T::AccountId, + amount: BalanceOf, + blueprint_selection: DelegatorBlueprintSelection, + ) -> Result { + ensure!(!amount.is_zero(), Error::::InvalidAmount); + ensure!(Self::is_operator(&operator), Error::::NotAnOperator); + + Delegators::::try_mutate(who, |maybe_metadata| -> Result { let metadata = maybe_metadata.as_mut().ok_or(Error::::NotDelegator)?; - // Find and remove the matching unstake request - let request_index = metadata + // Find the nomination delegation and verify amount + let (_, current_amount) = + Self::find_nomination_delegation(&metadata.delegations, &operator)? + .ok_or(Error::::NoActiveDelegation)?; + + // Calculate total pending unstakes + let pending_unstake_amount: BalanceOf = metadata .delegator_unstake_requests .iter() - .position(|r| { - r.asset_id == asset_id && r.amount == amount && r.operator == operator - }) + .filter(|r| r.operator == operator && r.is_nomination) + .fold(Zero::zero(), |acc, r| acc.saturating_add(r.amount)); + + let available_amount = current_amount.saturating_sub(pending_unstake_amount); + ensure!(available_amount >= amount, Error::::InsufficientBalance); + + // Create the unstake request + Self::create_unstake_request( + metadata, + operator.clone(), + Asset::Custom(Zero::zero()), + amount, + blueprint_selection, + true, // is_nomination = true for nomination delegations + )?; + + let when = Self::current_round() + T::DelegationBondLessDelay::get(); + Ok(when) + }) + } + + /// Cancels a scheduled unstake request for nomination delegations. + /// + /// Similar to regular unstake cancellation but specifically for nominated tokens. + /// This function removes a pending unstake request without modifying any actual delegations. + /// + /// # Performance Considerations + /// + /// - Single storage read for request verification + /// - Single storage write for request removal + /// - O(n) search through unstake requests + /// + /// # Arguments + /// + /// * `who` - The account ID of the delegator + /// * `operator` - The operator whose unstake request to cancel + /// + /// # Errors + /// + /// * `NotDelegator` - Account is not a delegator + /// * `NoBondLessRequest` - No matching unstake request found + /// + /// # Example + /// + /// ```ignore + /// // Cancel nomination unstake request from operator + /// process_cancel_delegator_nomination_unstake( + /// &delegator, + /// operator + /// )?; + /// ``` + pub(crate) fn process_cancel_delegator_nomination_unstake( + who: &T::AccountId, + operator: T::AccountId, + ) -> Result< + BondLessRequest, T::MaxDelegatorBlueprints>, + DispatchError, + > { + Delegators::::try_mutate( + who, + |maybe_metadata| -> Result< + BondLessRequest, T::MaxDelegatorBlueprints>, + DispatchError, + > { + let metadata = maybe_metadata.as_mut().ok_or(Error::::NotDelegator)?; + + // Find and remove the unstake request + let request_index = metadata + .delegator_unstake_requests + .iter() + .position(|r| r.operator == operator && r.is_nomination) + .ok_or(Error::::NoBondLessRequest)?; + + // Remove the request + let request = metadata.delegator_unstake_requests.remove(request_index); + + Ok(request.clone()) + }, + ) + } + + /// Execute an unstake request for nomination delegations + pub fn process_execute_delegator_nomination_unstake( + who: &T::AccountId, + operator: T::AccountId, + ) -> Result, DispatchError> { + Delegators::::try_mutate(who, |maybe_metadata| -> Result, DispatchError> { + let metadata = maybe_metadata.as_mut().ok_or(Error::::NotDelegator)?; + + // Find and validate the unstake request + let (request_index, request) = metadata + .delegator_unstake_requests + .iter() + .enumerate() + .find(|(_, r)| r.operator == operator && r.is_nomination) .ok_or(Error::::NoBondLessRequest)?; - let unstake_request = metadata.delegator_unstake_requests.remove(request_index); - - // Update the operator's metadata - Operators::::try_mutate( - &unstake_request.operator, - |maybe_operator_metadata| -> DispatchResult { - let operator_metadata = - maybe_operator_metadata.as_mut().ok_or(Error::::NotAnOperator)?; - - // Find the matching delegation and increase its amount, or insert a new - // delegation if not found - let mut delegations = operator_metadata.delegations.clone(); - if let Some(delegation) = delegations - .iter_mut() - .find(|d| d.asset_id == asset_id && d.delegator == who.clone()) - { - delegation.amount = delegation - .amount - .checked_add(&amount) - .ok_or(Error::::OverflowRisk)?; - } else { - delegations - .try_push(DelegatorBond { delegator: who.clone(), amount, asset_id }) - .map_err(|_| Error::::MaxDelegationsExceeded)?; - - // Increase the delegation count only when a new delegation is added - operator_metadata.delegation_count = operator_metadata - .delegation_count - .checked_add(1) - .ok_or(Error::::OverflowRisk)?; - } - operator_metadata.delegations = delegations; + let delay = T::DelegationBondLessDelay::get(); + ensure!( + request.requested_round + delay <= Self::current_round(), + Error::::BondLessNotReady + ); + + // Store the amount before removing the request + let unstake_amount = request.amount; + // Find the nomination delegation + let (delegation_index, current_amount) = + Self::find_nomination_delegation(&metadata.delegations, &operator)? + .ok_or(Error::::NoActiveDelegation)?; - Ok(()) - }, + // Verify the unstake amount is still valid + ensure!(current_amount >= unstake_amount, Error::::InsufficientBalance); + + // Update the delegation + let delegation = &mut metadata.delegations[delegation_index]; + delegation.amount = delegation.amount.saturating_sub(unstake_amount); + + // Update operator metadata during execution + Self::update_operator_metadata( + &operator, + who, + Asset::Custom(Zero::zero()), + unstake_amount, + false, // is_increase = false for unstaking )?; - // Update the delegator's metadata - let mut delegations = metadata.delegations.clone(); + // Remove the unstake request + metadata.delegator_unstake_requests.remove(request_index); - // If a similar delegation exists, increase the amount - if let Some(delegation) = delegations.iter_mut().find(|d| { - d.operator == unstake_request.operator && d.asset_id == unstake_request.asset_id - }) { - delegation.amount = delegation - .amount - .checked_add(&unstake_request.amount) - .ok_or(Error::::OverflowRisk)?; + // Set the lock to the new amount or remove it if zero + if delegation.amount.is_zero() { + T::Currency::remove_lock(DELEGATION_LOCK_ID, who); + metadata.delegations.remove(delegation_index); } else { - // Create a new delegation - delegations - .try_push(BondInfoDelegator { - operator: unstake_request.operator.clone(), - amount: unstake_request.amount, - asset_id: unstake_request.asset_id, - blueprint_selection: unstake_request.blueprint_selection, - }) - .map_err(|_| Error::::MaxDelegationsExceeded)?; + T::Currency::set_lock( + DELEGATION_LOCK_ID, + who, + delegation.amount, + WithdrawReasons::TRANSFER, + ); + } + + Ok(unstake_amount) + }) + } + + /// Helper function to update operator metadata for a delegation change + fn update_operator_metadata( + operator: &T::AccountId, + who: &T::AccountId, + asset_id: Asset, + amount: BalanceOf, + is_increase: bool, + ) -> DispatchResult { + Operators::::try_mutate(operator, |maybe_operator_metadata| -> DispatchResult { + let operator_metadata = + maybe_operator_metadata.as_mut().ok_or(Error::::NotAnOperator)?; + + let mut delegations = operator_metadata.delegations.clone(); + + if is_increase { + // Adding or increasing delegation + ensure!( + operator_metadata.delegation_count < T::MaxDelegations::get(), + Error::::MaxDelegationsExceeded + ); + + if let Some(existing_delegation) = + delegations.iter_mut().find(|d| d.delegator == *who && d.asset_id == asset_id) + { + existing_delegation.amount = existing_delegation + .amount + .checked_add(&amount) + .ok_or(Error::::OverflowRisk)?; + } else { + delegations + .try_push(DelegatorBond { delegator: who.clone(), amount, asset_id }) + .map_err(|_| Error::::MaxDelegationsExceeded)?; + operator_metadata.delegation_count = + operator_metadata.delegation_count.saturating_add(1); + } + } else { + // Decreasing or removing delegation + if let Some(index) = + delegations.iter().position(|d| d.delegator == *who && d.asset_id == asset_id) + { + let delegation = &mut delegations[index]; + ensure!(delegation.amount >= amount, Error::::InsufficientBalance); + + delegation.amount = delegation.amount.saturating_sub(amount); + if delegation.amount.is_zero() { + delegations.remove(index); + operator_metadata.delegation_count = operator_metadata + .delegation_count + .checked_sub(1) + .ok_or(Error::::InsufficientBalance)?; + } + } } - metadata.delegations = delegations; + operator_metadata.delegations = delegations; Ok(()) }) } - /// Slashes a delegator's stake. + /// Checks if an account can unbond a specified amount of tokens. + /// + /// This function verifies whether unbonding a certain amount would leave sufficient + /// tokens to cover existing nomination delegations. It: + /// 1. Checks if the account is a nominator + /// 2. Calculates remaining stake after unbonding + /// 3. Verifies nominated delegations can still be covered /// /// # Arguments /// - /// * `delegator` - The account ID of the delegator. - /// * `operator` - The account ID of the operator. - /// * `blueprint_id` - The ID of the blueprint. - /// * `percentage` - The percentage of the stake to slash. + /// * `who` - The account to check + /// * `amount` - The amount to potentially unbond /// - /// # Errors + /// # Returns /// - /// Returns an error if the delegator is not found, or if the delegation is not active. - pub fn slash_delegator( - delegator: &T::AccountId, + /// * `true` if unbonding is allowed + /// * `false` if unbonding would leave insufficient stake for delegations + pub fn can_unbound(who: &T::AccountId, amount: BalanceOf) -> bool { + let Ok(stake) = T::StakingInterface::stake(who) else { + // not a nominator + return true; + }; + // Simulate the unbound operation + let remaining_stake = stake.active.saturating_sub(amount); + let delegator = Self::delegators(who).unwrap_or_default(); + let nominated_amount = + delegator.delegations.iter().fold(BalanceOf::::zero(), |acc, d| { + if d.is_nomination { + acc.saturating_add(d.amount) + } else { + acc + } + }); + let restake_amount = remaining_stake.saturating_sub(nominated_amount); + !restake_amount.is_zero() + } + + /// Helper function to verify and get nomination amount + fn verify_nomination_amount( + who: &T::AccountId, + required_amount: BalanceOf, + ) -> Result, Error> { + let stake = T::StakingInterface::stake(who).map_err(|_| Error::::NotNominator)?; + ensure!(stake.total >= required_amount, Error::::InsufficientBalance); + Ok(stake.active) + } + + /// Helper function to find and validate a nomination delegation + fn find_nomination_delegation( + delegations: &[BondInfoDelegator< + T::AccountId, + BalanceOf, + T::AssetId, + T::MaxDelegatorBlueprints, + >], operator: &T::AccountId, - blueprint_id: BlueprintId, - percentage: Percent, - ) -> Result<(), DispatchError> { - Delegators::::try_mutate(delegator, |maybe_metadata| { - let metadata = maybe_metadata.as_mut().ok_or(Error::::NotDelegator)?; + ) -> Result)>, Error> { + if let Some((index, delegation)) = delegations + .iter() + .enumerate() + .find(|(_, d)| d.operator == *operator && d.is_nomination) + { + ensure!( + delegation.asset_id == Asset::Custom(Zero::zero()), + Error::::AssetNotWhitelisted + ); + Ok(Some((index, delegation.amount.clone()))) + } else { + Ok(None) + } + } - let delegation = metadata - .delegations - .iter_mut() - .find(|d| &d.operator == operator) - .ok_or(Error::::NoActiveDelegation)?; + /// Helper function to create an unstake request + fn create_unstake_request( + metadata: &mut DelegatorMetadataOf, + operator: T::AccountId, + asset_id: Asset, + amount: BalanceOf, + blueprint_selection: DelegatorBlueprintSelection, + is_nomination: bool, + ) -> DispatchResult { + let unstake_request = BondLessRequest { + operator, + asset_id, + amount, + requested_round: Self::current_round(), + blueprint_selection, + is_nomination, + }; + + metadata + .delegator_unstake_requests + .try_push(unstake_request) + .map_err(|_| Error::::MaxUnstakeRequestsExceeded)?; + + Ok(()) + } - // Check delegation type and blueprint_id - match &delegation.blueprint_selection { - DelegatorBlueprintSelection::Fixed(blueprints) => { - // For fixed delegation, ensure the blueprint_id is in the list - ensure!(blueprints.contains(&blueprint_id), Error::::BlueprintNotSelected); - }, - DelegatorBlueprintSelection::All => { - // For "All" type, no need to check blueprint_id - }, + /// Helper function to process a batch of unstake requests + /// Returns aggregated updates for deposits, delegations, and operators + fn aggregate_unstake_requests( + metadata: &DelegatorMetadataOf, + current_round: RoundIndex, + delay: RoundIndex, + ) -> Result< + ( + BTreeMap, BalanceOf>, // deposit_updates + BTreeMap<(T::AccountId, Asset), (usize, BalanceOf)>, // delegation_updates + BTreeMap<(T::AccountId, Asset), BalanceOf>, // operator_updates + Vec, // indices_to_remove + ), + Error, + > { + let mut indices_to_remove = Vec::new(); + let mut delegation_updates = BTreeMap::new(); + let mut deposit_updates = BTreeMap::new(); + let mut operator_updates = BTreeMap::new(); + + for (idx, request) in metadata.delegator_unstake_requests.iter().enumerate() { + if current_round < delay + request.requested_round { + continue; } - // Calculate and apply slash - let slash_amount = percentage.mul_floor(delegation.amount); - delegation.amount = delegation - .amount - .checked_sub(&slash_amount) - .ok_or(Error::::InsufficientStakeRemaining)?; - - match delegation.asset_id { - Asset::Custom(asset_id) => { - // Transfer slashed amount to the treasury - let _ = T::Fungibles::transfer( - asset_id, - &Self::pallet_account(), - &T::SlashedAmountRecipient::get(), - slash_amount, - Preservation::Expendable, - ); - }, - Asset::Erc20(address) => { - let slashed_amount_recipient_evm = - T::EvmAddressMapping::into_address(T::SlashedAmountRecipient::get()); - let (success, _weight) = Self::erc20_transfer( - address, - &Self::pallet_evm_account(), - slashed_amount_recipient_evm, - slash_amount, - ) - .map_err(|_| Error::::ERC20TransferFailed)?; - ensure!(success, Error::::ERC20TransferFailed); - }, - } + *deposit_updates.entry(request.asset_id).or_default() += request.amount; - // emit event - Self::deposit_event(Event::DelegatorSlashed { - who: delegator.clone(), - amount: slash_amount, - }); + let delegation_key = (request.operator.clone(), request.asset_id); + if let Some(delegation_idx) = metadata.delegations.iter().position(|d| { + d.operator == request.operator && d.asset_id == request.asset_id && !d.is_nomination + }) { + let (_, total_unstake) = delegation_updates + .entry(delegation_key.clone()) + .or_insert((delegation_idx, BalanceOf::::zero())); + *total_unstake += request.amount; + } else { + return Err(Error::::NoActiveDelegation); + } - Ok(()) - }) + *operator_updates.entry(delegation_key).or_default() += request.amount; + indices_to_remove.push(idx); + } + println!("indices_to_remove: {:?}", indices_to_remove); + ensure!(!indices_to_remove.is_empty(), Error::::BondLessNotReady); + Ok((deposit_updates, delegation_updates, operator_updates, indices_to_remove)) } } diff --git a/pallets/multi-asset-delegation/src/functions/deposit.rs b/pallets/multi-asset-delegation/src/functions/deposit.rs index 7058d9817..46ef7800c 100644 --- a/pallets/multi-asset-delegation/src/functions/deposit.rs +++ b/pallets/multi-asset-delegation/src/functions/deposit.rs @@ -13,8 +13,8 @@ // // You should have received a copy of the GNU General Public License // along with Tangle. If not, see . -use super::*; -use crate::{types::*, Pallet}; + +use crate::{types::*, Config, Delegators, Error, Pallet}; use frame_support::{ ensure, pallet_prelude::DispatchResult, @@ -204,7 +204,7 @@ impl Pallet { let metadata = maybe_metadata.as_mut().ok_or(Error::::NotDelegator)?; // Ensure there are outstanding withdraw requests - ensure!(!metadata.withdraw_requests.is_empty(), Error::::NowithdrawRequests); + ensure!(!metadata.withdraw_requests.is_empty(), Error::::NoWithdrawRequests); let current_round = Self::current_round(); let delay = T::LeaveDelegatorsDelay::get(); diff --git a/pallets/multi-asset-delegation/src/functions/evm.rs b/pallets/multi-asset-delegation/src/functions/evm.rs index fe3471664..e5e229c30 100644 --- a/pallets/multi-asset-delegation/src/functions/evm.rs +++ b/pallets/multi-asset-delegation/src/functions/evm.rs @@ -1,5 +1,20 @@ -use super::*; -use crate::types::BalanceOf; +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use crate::{types::*, Config, Error, Event, Pallet}; use ethabi::{Function, StateMutability, Token}; use frame_support::{ dispatch::{DispatchErrorWithPostInfo, PostDispatchInfo}, diff --git a/pallets/multi-asset-delegation/src/functions.rs b/pallets/multi-asset-delegation/src/functions/mod.rs similarity index 96% rename from pallets/multi-asset-delegation/src/functions.rs rename to pallets/multi-asset-delegation/src/functions/mod.rs index 42dd7c25a..926c4630f 100644 --- a/pallets/multi-asset-delegation/src/functions.rs +++ b/pallets/multi-asset-delegation/src/functions/mod.rs @@ -14,16 +14,16 @@ // You should have received a copy of the GNU General Public License // along with Tangle. If not, see . +use crate::{Config, Pallet}; use frame_system::RawOrigin; use sp_runtime::traits::BadOrigin; -use super::*; - pub mod delegate; pub mod deposit; pub mod evm; pub mod operator; pub mod session_manager; +pub mod slash; /// Ensure that the origin `o` represents the current pallet (i.e. transaction). /// Returns `Ok` if the origin is the current pallet, `Err` otherwise. diff --git a/pallets/multi-asset-delegation/src/functions/operator.rs b/pallets/multi-asset-delegation/src/functions/operator.rs index ef603e778..5eb43878a 100644 --- a/pallets/multi-asset-delegation/src/functions/operator.rs +++ b/pallets/multi-asset-delegation/src/functions/operator.rs @@ -14,21 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Tangle. If not, see . -/// Functions for the pallet. -use super::*; -use crate::{types::*, Pallet}; +use crate::{types::*, Config, Error, Operators, Pallet}; use frame_support::{ ensure, pallet_prelude::DispatchResult, - traits::{Currency, ExistenceRequirement, Get, ReservableCurrency}, + traits::{Get, ReservableCurrency}, BoundedVec, }; use sp_runtime::{ traits::{CheckedAdd, CheckedSub}, - DispatchError, Percent, + DispatchError, }; use tangle_primitives::traits::ServiceManager; -use tangle_primitives::BlueprintId; impl Pallet { /// Handles the deposit of stake amount and creation of an operator. @@ -304,43 +301,4 @@ impl Pallet { Ok(()) } - - pub fn slash_operator( - operator: &T::AccountId, - blueprint_id: BlueprintId, - percentage: Percent, - ) -> Result<(), DispatchError> { - Operators::::try_mutate(operator, |maybe_operator| { - let operator_data = maybe_operator.as_mut().ok_or(Error::::NotAnOperator)?; - ensure!(operator_data.status == OperatorStatus::Active, Error::::NotActiveOperator); - - // Slash operator stake - let amount = percentage.mul_floor(operator_data.stake); - operator_data.stake = operator_data - .stake - .checked_sub(&amount) - .ok_or(Error::::InsufficientStakeRemaining)?; - - // Slash each delegator - for delegator in operator_data.delegations.iter() { - // Ignore errors from individual delegator slashing - let _ = - Self::slash_delegator(&delegator.delegator, operator, blueprint_id, percentage); - } - - // transfer the slashed amount to the treasury - T::Currency::unreserve(operator, amount); - let _ = T::Currency::transfer( - operator, - &T::SlashedAmountRecipient::get(), - amount, - ExistenceRequirement::AllowDeath, - ); - - // emit event - Self::deposit_event(Event::OperatorSlashed { who: operator.clone(), amount }); - - Ok(()) - }) - } } diff --git a/pallets/multi-asset-delegation/src/functions/session_manager.rs b/pallets/multi-asset-delegation/src/functions/session_manager.rs index a7024ed05..8d48af9c6 100644 --- a/pallets/multi-asset-delegation/src/functions/session_manager.rs +++ b/pallets/multi-asset-delegation/src/functions/session_manager.rs @@ -13,8 +13,8 @@ // // You should have received a copy of the GNU General Public License // along with Tangle. If not, see . -use super::*; -use crate::{types::*, Pallet}; + +use crate::{types::*, AtStake, Config, CurrentRound, Operators, Pallet}; impl Pallet { pub fn handle_round_change(i: u32) { diff --git a/pallets/multi-asset-delegation/src/functions/slash.rs b/pallets/multi-asset-delegation/src/functions/slash.rs new file mode 100644 index 000000000..fb4bb189d --- /dev/null +++ b/pallets/multi-asset-delegation/src/functions/slash.rs @@ -0,0 +1,123 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use crate::{types::*, Config, Delegators, Error, Event, Operators, Pallet}; +use frame_support::{dispatch::DispatchResult, ensure, traits::Get, weights::Weight}; +use sp_runtime::{traits::CheckedSub, DispatchError}; +use tangle_primitives::{ + services::{Asset, UnappliedSlash}, + traits::SlashManager, +}; + +impl Pallet { + /// Helper function to update operator storage for a slash + pub(crate) fn do_slash_operator( + unapplied_slash: &UnappliedSlash, T::AssetId>, + ) -> Result { + let mut weight = T::DbWeight::get().reads(1); + + Operators::::try_mutate( + &unapplied_slash.operator, + |maybe_operator| -> DispatchResult { + let operator_data = maybe_operator.as_mut().ok_or(Error::::NotAnOperator)?; + ensure!( + operator_data.status == OperatorStatus::Active, + Error::::NotActiveOperator + ); + + // Update operator's stake + operator_data.stake = operator_data + .stake + .checked_sub(&unapplied_slash.own) + .ok_or(Error::::InsufficientStakeRemaining)?; + + weight += T::DbWeight::get().writes(1); + Ok(()) + }, + )?; + + // Emit event for operator slash + Self::deposit_event(Event::OperatorSlashed { + who: unapplied_slash.operator.clone(), + amount: unapplied_slash.own, + }); + + Ok(weight) + } + + /// Helper function to update delegator storage for a slash + pub(crate) fn do_slash_delegator( + operator: &T::AccountId, + delegator: &T::AccountId, + asset_id: Asset, + slash_amount: BalanceOf, + ) -> Result { + let mut weight = T::DbWeight::get().reads(1); + + Delegators::::try_mutate(delegator, |maybe_metadata| -> DispatchResult { + let metadata = maybe_metadata.as_mut().ok_or(Error::::NotDelegator)?; + + // Find the delegation to the slashed operator + let delegation = metadata + .delegations + .iter_mut() + .find(|d| &d.operator == operator && d.asset_id == asset_id) + .ok_or(Error::::NoActiveDelegation)?; + + // Update delegator's stake + delegation.amount = delegation + .amount + .checked_sub(&slash_amount) + .ok_or(Error::::InsufficientStakeRemaining)?; + + weight += T::DbWeight::get().writes(1); + Ok(()) + })?; + + // Emit event for delegator slash + Self::deposit_event(Event::DelegatorSlashed { + who: delegator.clone(), + amount: slash_amount, + }); + + Ok(weight) + } +} + +impl SlashManager, T::AssetId> for Pallet { + /// Updates operator storage to reflect a slash. + /// This only updates the storage items and does not handle asset transfers. + /// + /// # Arguments + /// * `unapplied_slash` - The unapplied slash record containing slash details + fn slash_operator( + unapplied_slash: &UnappliedSlash, T::AssetId>, + ) -> Result { + let mut total_weight = Self::do_slash_operator(unapplied_slash)?; + + // Also slash all delegators in the unapplied_slash.others list + for (delegator, asset_id, amount) in &unapplied_slash.others { + total_weight = total_weight.saturating_add(Self::do_slash_delegator( + &unapplied_slash.operator, + delegator, + *asset_id, + *amount, + )?); + } + + Ok(total_weight) + } +} diff --git a/pallets/multi-asset-delegation/src/lib.rs b/pallets/multi-asset-delegation/src/lib.rs index c83a00c82..1ca433ad8 100644 --- a/pallets/multi-asset-delegation/src/lib.rs +++ b/pallets/multi-asset-delegation/src/lib.rs @@ -66,6 +66,9 @@ pub mod mock_evm; #[cfg(test)] mod tests; +#[cfg(any(test, feature = "fuzzing"))] +mod extra; + pub mod weights; // #[cfg(feature = "runtime-benchmarks")] @@ -74,7 +77,6 @@ pub mod weights; pub mod functions; pub mod traits; pub mod types; -pub use functions::*; #[frame_support::pallet] pub mod pallet { @@ -89,8 +91,8 @@ pub mod pallet { use pallet_session::SessionManager; use scale_info::TypeInfo; use sp_core::H160; - use sp_runtime::traits::{MaybeSerializeDeserialize, Member}; - use sp_staking::SessionIndex; + use sp_runtime::traits::{MaybeSerializeDeserialize, Member, Zero}; + use sp_staking::{OnStakingUpdate, SessionIndex, Stake, StakingInterface}; use sp_std::{fmt::Debug, prelude::*, vec::Vec}; use tangle_primitives::traits::RewardsManager; use tangle_primitives::types::rewards::LockMultiplier; @@ -120,7 +122,8 @@ pub mod pallet { + MaxEncodedLen + Encode + Decode - + TypeInfo; + + TypeInfo + + Zero; /// The maximum number of blueprints a delegator can have in Fixed mode. #[pallet::constant] @@ -208,6 +211,16 @@ pub mod pallet { /// A type representing the weights required by the dispatchables of this pallet. type WeightInfo: crate::weights::WeightInfo; + + /// Currency to vote conversion + type CurrencyToVote: sp_staking::currency_to_vote::CurrencyToVote>; + + /// Interface to the staking system for nomination information + type StakingInterface: StakingInterface< + AccountId = Self::AccountId, + Balance = BalanceOf, + CurrencyToVote = Self::CurrencyToVote, + >; } /// The pallet struct. @@ -272,11 +285,11 @@ pub mod pallet { /// A deposit has been made. Deposited { who: T::AccountId, amount: BalanceOf, asset_id: Asset }, /// An withdraw has been scheduled. - Scheduledwithdraw { who: T::AccountId, amount: BalanceOf, asset_id: Asset }, + ScheduledWithdraw { who: T::AccountId, amount: BalanceOf, asset_id: Asset }, /// An withdraw has been executed. - Executedwithdraw { who: T::AccountId }, + ExecutedWithdraw { who: T::AccountId }, /// An withdraw has been cancelled. - Cancelledwithdraw { who: T::AccountId }, + CancelledWithdraw { who: T::AccountId }, /// A delegation has been made. Delegated { who: T::AccountId, @@ -285,22 +298,54 @@ pub mod pallet { asset_id: Asset, }, /// A delegator unstake request has been scheduled. - ScheduledDelegatorBondLess { + DelegatorUnstakeScheduled { who: T::AccountId, operator: T::AccountId, - amount: BalanceOf, asset_id: Asset, + amount: BalanceOf, + when: RoundIndex, }, /// A delegator unstake request has been executed. - ExecutedDelegatorBondLess { who: T::AccountId }, + DelegatorUnstakeExecuted { + who: T::AccountId, + operator: T::AccountId, + asset_id: Asset, + amount: BalanceOf, + }, /// A delegator unstake request has been cancelled. - CancelledDelegatorBondLess { who: T::AccountId }, + DelegatorUnstakeCancelled { + who: T::AccountId, + operator: T::AccountId, + asset_id: Asset, + amount: BalanceOf, + }, /// Operator has been slashed OperatorSlashed { who: T::AccountId, amount: BalanceOf }, /// Delegator has been slashed DelegatorSlashed { who: T::AccountId, amount: BalanceOf }, /// EVM execution reverted with a reason. EvmReverted { from: H160, to: H160, data: Vec, reason: Vec }, + /// A nomination has been delegated + NominationDelegated { who: T::AccountId, operator: T::AccountId, amount: BalanceOf }, + /// A nomination unstake request has been scheduled. + NominationUnstakeScheduled { + who: T::AccountId, + operator: T::AccountId, + amount: BalanceOf, + when: RoundIndex, + }, + /// A nomination unstake request has been executed. + NominationUnstakeExecuted { + who: T::AccountId, + operator: T::AccountId, + amount: BalanceOf, + }, + /// A nomination unstake request has been cancelled. + NominationUnstakeCancelled { + who: T::AccountId, + operator: T::AccountId, + amount: BalanceOf, + }, } /// Errors emitted by the pallet. @@ -310,6 +355,8 @@ pub mod pallet { AlreadyOperator, /// The stake amount is too low. BondTooLow, + /// Amount is invalid + InvalidAmount, /// The account is not an operator. NotAnOperator, /// The account cannot exit. @@ -359,7 +406,7 @@ pub mod pallet { /// The blueprint ID is already whitelisted BlueprintAlreadyWhitelisted, /// No withdraw requests found - NowithdrawRequests, + NoWithdrawRequests, /// No matching withdraw reqests found NoMatchingwithdrawRequest, /// Asset already exists in a reward vault @@ -410,6 +457,8 @@ pub mod pallet { DepositExceedsCapForAsset, /// Overflow from math OverflowRisk, + /// Delegator is not a nominator + NotNominator, } /// Hooks for the pallet. @@ -735,7 +784,7 @@ pub mod pallet { ) -> DispatchResult { let who = ensure_signed(origin)?; Self::process_schedule_withdraw(who.clone(), asset_id, amount)?; - Self::deposit_event(Event::Scheduledwithdraw { who, amount, asset_id }); + Self::deposit_event(Event::ScheduledWithdraw { who, amount, asset_id }); Ok(()) } @@ -765,7 +814,7 @@ pub mod pallet { None => ensure_signed(origin)?, }; Self::process_execute_withdraw(who.clone())?; - Self::deposit_event(Event::Executedwithdraw { who }); + Self::deposit_event(Event::ExecutedWithdraw { who }); Ok(()) } @@ -793,7 +842,7 @@ pub mod pallet { ) -> DispatchResult { let who = ensure_signed(origin)?; Self::process_cancel_withdraw(who.clone(), asset_id, amount)?; - Self::deposit_event(Event::Cancelledwithdraw { who }); + Self::deposit_event(Event::CancelledWithdraw { who }); Ok(()) } @@ -870,11 +919,12 @@ pub mod pallet { asset_id, amount, )?; - Self::deposit_event(Event::ScheduledDelegatorBondLess { + Self::deposit_event(Event::DelegatorUnstakeScheduled { who, - asset_id, operator, + asset_id, amount, + when: Self::current_round() + T::DelegationBondLessDelay::get(), }); Ok(()) } @@ -898,8 +948,17 @@ pub mod pallet { #[pallet::weight(Weight::from_parts(10_000, 0) + T::DbWeight::get().writes(1))] pub fn execute_delegator_unstake(origin: OriginFor) -> DispatchResult { let who = ensure_signed(origin)?; - Self::process_execute_delegator_unstake(who.clone())?; - Self::deposit_event(Event::ExecutedDelegatorBondLess { who }); + let unstake_results = Self::process_execute_delegator_unstake(who.clone())?; + + // Emit an event for each operator/asset combination that was unstaked + for (operator, asset_id, amount) in unstake_results { + Self::deposit_event(Event::DelegatorUnstakeExecuted { + who: who.clone(), + operator, + asset_id, + amount, + }); + } Ok(()) } @@ -929,8 +988,145 @@ pub mod pallet { amount: BalanceOf, ) -> DispatchResult { let who = ensure_signed(origin)?; - Self::process_cancel_delegator_unstake(who.clone(), operator, asset_id, amount)?; - Self::deposit_event(Event::CancelledDelegatorBondLess { who }); + Self::process_cancel_delegator_unstake( + who.clone(), + operator.clone(), + asset_id, + amount, + )?; + Self::deposit_event(Event::DelegatorUnstakeCancelled { + who, + operator, + asset_id, + amount, + }); + Ok(()) + } + + /// Delegates nominated tokens to an operator. + /// + /// # Arguments + /// * `origin` - Origin of the call + /// * `operator` - The operator to delegate to + /// * `amount` - Amount of nominated tokens to delegate + /// * `blueprint_selection` - Strategy for selecting which blueprints to work with + /// + /// # Errors + /// * `NotDelegator` - Account is not a delegator + /// * `NotNominator` - Account has no nominated tokens + /// * `InsufficientBalance` - Not enough nominated tokens available + /// * `MaxDelegationsExceeded` - Would exceed maximum allowed delegations + /// * `OverflowRisk` - Arithmetic overflow during calculations + /// * `InvalidAmount` - Amount specified is zero + #[pallet::call_index(18)] + #[pallet::weight(Weight::from_parts(10_000, 0) + T::DbWeight::get().writes(1))] + pub fn delegate_nomination( + origin: OriginFor, + operator: T::AccountId, + amount: BalanceOf, + blueprint_selection: DelegatorBlueprintSelection, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::process_delegate_nominations( + who.clone(), + operator.clone(), + amount, + blueprint_selection, + )?; + Self::deposit_event(Event::NominationDelegated { who, operator, amount }); + Ok(()) + } + + /// Schedules an unstake request for nomination delegations. + /// + /// # Arguments + /// * `origin` - Origin of the call + /// * `operator` - The operator to unstake from + /// * `amount` - Amount of nominated tokens to unstake + /// * `blueprint_selection` - The blueprint selection to use after unstaking + /// + /// # Errors + /// * `NotDelegator` - Account is not a delegator + /// * `NoActiveDelegation` - No active nomination delegation found + /// * `InsufficientBalance` - Trying to unstake more than delegated + /// * `MaxUnstakeRequestsExceeded` - Too many pending unstake requests + /// * `InvalidAmount` - Amount specified is zero + #[pallet::call_index(19)] + #[pallet::weight(Weight::from_parts(10_000, 0) + T::DbWeight::get().writes(1))] + pub fn schedule_nomination_unstake( + origin: OriginFor, + operator: T::AccountId, + amount: BalanceOf, + blueprint_selection: DelegatorBlueprintSelection, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::process_schedule_delegator_nomination_unstake( + &who, + operator.clone(), + amount, + blueprint_selection, + )?; + Self::deposit_event(Event::NominationUnstakeScheduled { + who: who.clone(), + operator, + amount, + when: Self::current_round() + T::DelegationBondLessDelay::get(), + }); + Ok(()) + } + + /// Executes a scheduled unstake request for nomination delegations. + /// + /// # Arguments + /// * `origin` - Origin of the call + /// * `operator` - The operator to execute unstake from + /// + /// # Errors + /// * `NotDelegator` - Account is not a delegator + /// * `NoBondLessRequest` - No matching unstake request found + /// * `BondLessNotReady` - Unstake request not ready for execution + /// * `NoActiveDelegation` - No active nomination delegation found + /// * `InsufficientBalance` - Insufficient balance for unstaking + #[pallet::call_index(20)] + #[pallet::weight(Weight::from_parts(10_000, 0) + T::DbWeight::get().writes(1))] + pub fn execute_nomination_unstake( + origin: OriginFor, + operator: T::AccountId, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let amount = + Self::process_execute_delegator_nomination_unstake(&who, operator.clone())?; + Self::deposit_event(Event::NominationUnstakeExecuted { + who: who.clone(), + operator, + amount, + }); + Ok(()) + } + + /// Cancels a scheduled unstake request for nomination delegations. + /// + /// # Arguments + /// * `origin` - Origin of the call + /// * `operator` - The operator whose unstake request to cancel + /// + /// # Errors + /// * `NotDelegator` - Account is not a delegator + /// * `NoBondLessRequest` - No matching unstake request found + #[pallet::call_index(21)] + #[pallet::weight(Weight::from_parts(10_000, 0) + T::DbWeight::get().writes(1))] + pub fn cancel_nomination_unstake( + origin: OriginFor, + operator: T::AccountId, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let request = + Self::process_cancel_delegator_nomination_unstake(&who, operator.clone())?; + Self::deposit_event(Event::NominationUnstakeCancelled { + who: who.clone(), + operator, + amount: request.amount, + }); Ok(()) } diff --git a/pallets/multi-asset-delegation/src/mock.rs b/pallets/multi-asset-delegation/src/mock.rs index 26deb9af4..53e13fe4d 100644 --- a/pallets/multi-asset-delegation/src/mock.rs +++ b/pallets/multi-asset-delegation/src/mock.rs @@ -17,6 +17,7 @@ use super::*; use crate::{self as pallet_multi_asset_delegation}; use ethabi::Uint; +use fp_self_contained::SelfContainedCall; use frame_election_provider_support::{ bounds::{ElectionBounds, ElectionBoundsBuilder}, onchain, SequentialPhragmen, @@ -25,7 +26,9 @@ use frame_support::{ construct_runtime, derive_impl, pallet_prelude::{Hooks, Weight}, parameter_types, - traits::{AsEnsureOriginWithArg, ConstU128, ConstU32, OneSessionHandler}, + traits::{ + AsEnsureOriginWithArg, ConstU128, ConstU32, Contains, GetCallMetadata, OneSessionHandler, + }, PalletId, }; use frame_system::pallet_prelude::BlockNumberFor; @@ -39,10 +42,12 @@ use sp_core::{sr25519, H160}; use sp_keyring::AccountKeyring; use sp_keystore::{testing::MemoryKeystore, KeystoreExt, KeystorePtr}; use sp_runtime::{ + generic, testing::UintAuthorityId, traits::{ConvertInto, IdentityLookup, OpaqueKeys}, AccountId32, BoundToRuntimeAppPublic, BuildStorage, DispatchError, Perbill, }; +use sp_staking::currency_to_vote::U128CurrencyToVote; use std::cell::RefCell; use tangle_primitives::{ services::{EvmAddressMapping, EvmGasWeightMapping, EvmRunner}, @@ -199,7 +204,7 @@ impl pallet_staking::Config for Runtime { type Currency = Balances; type CurrencyBalance = ::Balance; type UnixTime = pallet_timestamp::Pallet; - type CurrencyToVote = (); + type CurrencyToVote = U128CurrencyToVote; type RewardRemainder = (); type RuntimeEvent = RuntimeEvent; type Slash = (); @@ -391,6 +396,8 @@ impl pallet_multi_asset_delegation::Config for Runtime { type Currency = Balances; type MinOperatorBondAmount = MinOperatorBondAmount; type BondDuration = BondDuration; + type CurrencyToVote = U128CurrencyToVote; + type StakingInterface = Staking; type ServiceManager = MockServiceManager; type LeaveOperatorsDelay = ConstU32<10>; type OperatorBondLessDelay = ConstU32<1>; @@ -414,7 +421,17 @@ impl pallet_multi_asset_delegation::Config for Runtime { type WeightInfo = (); } -type Block = frame_system::mocking::MockBlock; +/// An unchecked extrinsic type to be used in tests. +pub type MockUncheckedExtrinsic = generic::UncheckedExtrinsic< + AccountId, + RuntimeCall, + u32, + extra::CheckNominatedRestaked, +>; + +/// An implementation of `sp_runtime::traits::Block` to be used in tests. +type Block = + generic::Block, MockUncheckedExtrinsic>; construct_runtime!( pub enum Runtime @@ -458,6 +475,9 @@ pub fn new_test_ext() -> sp_io::TestExternalities { ext } +pub const TNT: AssetId = 0; +pub const USDC: AssetId = 1; +pub const WETH: AssetId = 2; pub const USDC_ERC20: H160 = H160([0x23; 20]); pub const VDOT: AssetId = 4; @@ -516,6 +536,14 @@ pub fn new_test_ext_raw_authorities() -> sp_io::TestExternalities { evm_config.assimilate_storage(&mut t).unwrap(); + let staking_config = pallet_staking::GenesisConfig:: { + validator_count: 3, + invulnerables: authorities.clone(), + ..Default::default() + }; + + staking_config.assimilate_storage(&mut t).unwrap(); + // assets_config.assimilate_storage(&mut t).unwrap(); let mut ext = sp_io::TestExternalities::new(t); ext.register_extension(KeystoreExt(Arc::new(MemoryKeystore::new()) as KeystorePtr)); diff --git a/pallets/multi-asset-delegation/src/tests.rs b/pallets/multi-asset-delegation/src/tests.rs index 376a1b43b..1e9fb07db 100644 --- a/pallets/multi-asset-delegation/src/tests.rs +++ b/pallets/multi-asset-delegation/src/tests.rs @@ -18,6 +18,7 @@ use crate::{mock::*, tests::RuntimeEvent}; pub mod delegate; pub mod deposit; +pub mod native_restaking; pub mod operator; pub mod session_manager; diff --git a/pallets/multi-asset-delegation/src/tests/delegate.rs b/pallets/multi-asset-delegation/src/tests/delegate.rs index ed2e60043..4732c6e59 100644 --- a/pallets/multi-asset-delegation/src/tests/delegate.rs +++ b/pallets/multi-asset-delegation/src/tests/delegate.rs @@ -17,7 +17,7 @@ use super::*; use crate::{CurrentRound, Error}; use frame_support::{assert_noop, assert_ok}; -use sp_keyring::AccountKeyring::{Alice, Bob}; +use sp_keyring::AccountKeyring::{Alice, Bob, Charlie}; use tangle_primitives::services::Asset; #[test] @@ -119,6 +119,17 @@ fn schedule_delegator_unstake_should_work() { assert_eq!(request.asset_id, asset_id); assert_eq!(request.amount, amount); + // Check the operator metadata + let operator_metadata = MultiAssetDelegation::operator_info(operator.clone()).unwrap(); + assert_eq!(operator_metadata.delegation_count, 1); + assert_eq!(operator_metadata.delegations.len(), 1); + // Move to next round + CurrentRound::::put(10); + // Execute the unstake + assert_ok!(MultiAssetDelegation::execute_delegator_unstake(RuntimeOrigin::signed( + who.clone() + ),)); + // Check the operator metadata let operator_metadata = MultiAssetDelegation::operator_info(operator.clone()).unwrap(); assert_eq!(operator_metadata.delegation_count, 0); @@ -231,8 +242,8 @@ fn cancel_delegator_unstake_should_work() { // Check the operator metadata let operator_metadata = MultiAssetDelegation::operator_info(operator.clone()).unwrap(); - assert_eq!(operator_metadata.delegation_count, 0); - assert_eq!(operator_metadata.delegations.len(), 0); + assert_eq!(operator_metadata.delegation_count, 1); + assert_eq!(operator_metadata.delegations.len(), 1); assert_ok!(MultiAssetDelegation::cancel_delegator_unstake( RuntimeOrigin::signed(who.clone()), @@ -310,7 +321,7 @@ fn cancel_delegator_unstake_should_update_already_existing() { assert_eq!(operator_metadata.delegations.len(), 1); let operator_delegation = &operator_metadata.delegations[0]; assert_eq!(operator_delegation.delegator, who.clone()); - assert_eq!(operator_delegation.amount, amount - 10); + assert_eq!(operator_delegation.amount, amount); assert_eq!(operator_delegation.asset_id, asset_id); assert_ok!(MultiAssetDelegation::cancel_delegator_unstake( @@ -539,3 +550,330 @@ fn delegate_should_not_create_multiple_on_repeat_delegation() { assert_eq!(updated_operator_delegation.asset_id, asset_id); }); } + +#[test] +fn delegate_exceeds_max_delegations() { + new_test_ext().execute_with(|| { + let who: AccountId = Bob.into(); + let operator: AccountId = Alice.into(); + let amount = 100; + + // Setup max number of operators + let mut operators = vec![]; + for i in 0..MaxDelegations::get() { + let operator_account: AccountId = AccountId::new([i as u8; 32]); + // Give operator enough balance to join + Balances::force_set_balance(RuntimeOrigin::root(), operator_account.clone(), 100_000); + operators.push(operator_account.clone()); + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator_account), + 10_000 + )); + } + + // Create max number of delegations with same asset + let asset_id = Asset::Custom(999); + create_and_mint_tokens(999, who.clone(), amount * MaxDelegations::get() as u128); + assert_ok!(MultiAssetDelegation::deposit( + RuntimeOrigin::signed(who.clone()), + asset_id.clone(), + amount * MaxDelegations::get() as u128, + None, + None, + )); + + println!("Max delegations: {}", MaxDelegations::get()); + for i in 0..MaxDelegations::get() { + assert_ok!(MultiAssetDelegation::delegate( + RuntimeOrigin::signed(who.clone()), + operators[i as usize].clone(), + asset_id.clone(), + 1u128, + Default::default(), + )); + } + + let operator: AccountId = Charlie.into(); + // Give operator enough balance to join + Balances::force_set_balance(RuntimeOrigin::root(), operator.clone(), 100_000); + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + assert_noop!( + MultiAssetDelegation::delegate( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + asset_id, + amount, + Default::default(), + ), + Error::::MaxDelegationsExceeded + ); + + // Verify state + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + assert_eq!(metadata.delegations.len() as u32, MaxDelegations::get()); + }); +} + +#[test] +fn delegate_insufficient_deposit() { + new_test_ext().execute_with(|| { + let who: AccountId = Bob.into(); + let operator: AccountId = Alice.into(); + let deposit_amount = 100; + let delegate_amount = deposit_amount + 1; + let asset_id = Asset::Custom(USDC); + + // Setup operator + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + + create_and_mint_tokens(USDC, who.clone(), deposit_amount); + + // Make deposit + assert_ok!(MultiAssetDelegation::deposit( + RuntimeOrigin::signed(who.clone()), + asset_id.clone(), + deposit_amount, + None, + None, + )); + + // Try to delegate more than deposited + assert_noop!( + MultiAssetDelegation::delegate( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + asset_id.clone(), + delegate_amount, + Default::default(), + ), + Error::::InsufficientBalance + ); + + // Verify state remains unchanged + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + assert_eq!(metadata.delegations.len(), 0); + assert_eq!(metadata.deposits.len(), 1); + assert_eq!(metadata.deposits.get(&asset_id).unwrap().amount, deposit_amount); + }); +} + +#[test] +fn delegate_to_inactive_operator() { + new_test_ext().execute_with(|| { + let who: AccountId = Bob.into(); + let operator: AccountId = Alice.into(); + let amount = 100; + + // Setup operator but make them inactive + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + assert_ok!(MultiAssetDelegation::go_offline(RuntimeOrigin::signed(operator.clone()))); + + // Make deposit + create_and_mint_tokens(USDC, who.clone(), amount); + assert_ok!(MultiAssetDelegation::deposit( + RuntimeOrigin::signed(who.clone()), + Asset::Custom(USDC), + amount, + None, + None, + )); + + // Try to delegate to inactive operator + assert_noop!( + MultiAssetDelegation::delegate( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + Asset::Custom(USDC), + amount, + Default::default(), + ), + Error::::NotActiveOperator + ); + + // Verify state remains unchanged + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + assert_eq!(metadata.delegations.len(), 0); + }); +} + +#[test] +fn delegate_repeated_same_asset() { + new_test_ext().execute_with(|| { + let who: AccountId = Bob.into(); + let operator: AccountId = Alice.into(); + let initial_amount = 100; + let additional_amount = 50; + + // Setup operator + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + + // Make deposit + create_and_mint_tokens(USDC, who.clone(), initial_amount + additional_amount); + assert_ok!(MultiAssetDelegation::deposit( + RuntimeOrigin::signed(who.clone()), + Asset::Custom(USDC), + initial_amount + additional_amount, + None, + None, + )); + + // First delegation + assert_ok!(MultiAssetDelegation::delegate( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + Asset::Custom(USDC), + initial_amount, + Default::default(), + )); + + // Second delegation with same asset + assert_ok!(MultiAssetDelegation::delegate( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + Asset::Custom(USDC), + additional_amount, + Default::default(), + )); + + // Verify state + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + assert_eq!(metadata.delegations.len(), 1); + let delegation = &metadata.delegations[0]; + assert_eq!(delegation.amount, initial_amount + additional_amount); + assert_eq!(delegation.operator, operator); + assert_eq!(delegation.asset_id, Asset::Custom(USDC)); + + // Verify operator state + let operator_metadata = MultiAssetDelegation::operator_info(operator.clone()).unwrap(); + assert_eq!(operator_metadata.delegation_count, 1); + let operator_delegation = &operator_metadata.delegations[0]; + assert_eq!(operator_delegation.amount, initial_amount + additional_amount); + assert_eq!(operator_delegation.delegator, who); + assert_eq!(operator_delegation.asset_id, Asset::Custom(USDC)); + }); +} + +#[test] +fn delegate_multiple_assets_same_operator() { + new_test_ext().execute_with(|| { + let who: AccountId = Bob.into(); + let operator: AccountId = Alice.into(); + let amount = 100; + + // Setup operator + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + + // Make deposits for different assets + for asset_id in [USDC, WETH].iter() { + create_and_mint_tokens(*asset_id, who.clone(), amount); + assert_ok!(MultiAssetDelegation::deposit( + RuntimeOrigin::signed(who.clone()), + Asset::Custom(*asset_id), + amount, + None, + None, + )); + assert_ok!(MultiAssetDelegation::delegate( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + Asset::Custom(*asset_id), + amount, + Default::default(), + )); + } + + // Verify state + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + assert_eq!(metadata.delegations.len(), 2); + + // Verify each delegation + for (i, asset_id) in [USDC, WETH].iter().enumerate() { + let delegation = &metadata.delegations[i]; + assert_eq!(delegation.amount, amount); + assert_eq!(delegation.operator, operator); + assert_eq!(delegation.asset_id, Asset::Custom(*asset_id)); + } + + // Verify operator state + let operator_metadata = MultiAssetDelegation::operator_info(operator.clone()).unwrap(); + assert_eq!(operator_metadata.delegation_count, 2); + for (i, asset_id) in [USDC, WETH].iter().enumerate() { + let operator_delegation = &operator_metadata.delegations[i]; + assert_eq!(operator_delegation.amount, amount); + assert_eq!(operator_delegation.delegator, who); + assert_eq!(operator_delegation.asset_id, Asset::Custom(*asset_id)); + } + }); +} + +#[test] +fn delegate_zero_amount() { + new_test_ext().execute_with(|| { + let who: AccountId = Bob.into(); + let operator: AccountId = Alice.into(); + + // Setup operator + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + + // Try to delegate zero amount + assert_noop!( + MultiAssetDelegation::delegate( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + Asset::Custom(USDC), + 0, + Default::default(), + ), + Error::::InvalidAmount + ); + }); +} + +#[test] +fn delegate_with_no_deposit() { + new_test_ext().execute_with(|| { + let who: AccountId = Bob.into(); + let operator: AccountId = Alice.into(); + let amount = 100; + + // Setup operator + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + + // Try to delegate without deposit + assert_noop!( + MultiAssetDelegation::delegate( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + Asset::Custom(USDC), + amount, + Default::default(), + ), + Error::::NotDelegator + ); + + // Verify state remains unchanged + let metadata = MultiAssetDelegation::delegators(who.clone()); + assert_eq!(metadata.is_none(), true); + }); +} diff --git a/pallets/multi-asset-delegation/src/tests/deposit.rs b/pallets/multi-asset-delegation/src/tests/deposit.rs index efa65e5c0..c5898ea43 100644 --- a/pallets/multi-asset-delegation/src/tests/deposit.rs +++ b/pallets/multi-asset-delegation/src/tests/deposit.rs @@ -367,7 +367,7 @@ fn execute_withdraw_should_work() { // Check event System::assert_last_event(RuntimeEvent::MultiAssetDelegation( - crate::Event::Executedwithdraw { who: who.clone() }, + crate::Event::ExecutedWithdraw { who: who.clone() }, )); }); } @@ -406,7 +406,7 @@ fn execute_withdraw_should_fail_if_no_withdraw_request() { assert_noop!( MultiAssetDelegation::execute_withdraw(RuntimeOrigin::signed(who.clone()), None), - Error::::NowithdrawRequests + Error::::NoWithdrawRequests ); }); } @@ -548,7 +548,7 @@ fn cancel_withdraw_should_work() { // Check event System::assert_last_event(RuntimeEvent::MultiAssetDelegation( - crate::Event::Cancelledwithdraw { who: who.clone() }, + crate::Event::CancelledWithdraw { who: who.clone() }, )); }); } diff --git a/pallets/multi-asset-delegation/src/tests/native_restaking.rs b/pallets/multi-asset-delegation/src/tests/native_restaking.rs new file mode 100644 index 000000000..54af15429 --- /dev/null +++ b/pallets/multi-asset-delegation/src/tests/native_restaking.rs @@ -0,0 +1,623 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use super::*; +use crate::{CurrentRound, Error}; +use extra::CheckNominatedRestaked; +use frame_support::{ + assert_err, assert_noop, assert_ok, + dispatch::DispatchInfo, + pallet_prelude::{InvalidTransaction, TransactionValidityError}, + traits::{Hooks, OnFinalize, OnInitialize}, +}; +use sp_keyring::AccountKeyring::{Alice, Bob, Charlie, Dave, Eve}; +use sp_runtime::traits::SignedExtension; +use tangle_primitives::services::Asset; + +#[test] +fn native_restaking_should_work() { + new_test_ext().execute_with(|| { + // Arrange + let who: AccountId = Dave.into(); + let validator = Staking::invulnerables()[0].clone(); + let operator: AccountId = Alice.into(); + let amount = 100_000; + let delegate_amount = amount / 2; + // Bond Some TNT + assert_ok!(Staking::bond( + RuntimeOrigin::signed(who.clone()), + amount, + pallet_staking::RewardDestination::Staked + )); + // Nominate the validator + assert_ok!(Staking::nominate(RuntimeOrigin::signed(who.clone()), vec![validator.clone()])); + + System::set_block_number(2); + >::on_initialize(2); + >::on_initialize(2); + >::on_finalize(2); + >::on_finalize(2); + // Assert + let ledger = Staking::ledger(sp_staking::StakingAccount::Stash(who.clone())).unwrap(); + assert_eq!(ledger.active, amount); + assert_eq!(ledger.total, amount); + assert_eq!(ledger.unlocking.len(), 0); + + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + + // Restake + assert_ok!(MultiAssetDelegation::delegate_nomination( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + delegate_amount, + Default::default(), + )); + // Assert + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + assert_eq!(metadata.delegations.len(), 1); + let delegation = &metadata.delegations[0]; + assert_eq!(delegation.operator, operator.clone()); + assert_eq!(delegation.amount, delegate_amount); + assert_eq!(delegation.asset_id, Asset::Custom(TNT)); + // Check the locks + let locks = pallet_balances::Pallet::::locks(&who); + // 1 lock for the staking + // 1 lock for the delegation + assert_eq!(locks.len(), 2); + assert_eq!(&locks[0].id, b"staking "); + assert_eq!(locks[0].amount, amount); + assert_eq!(&locks[1].id, b"delegate"); + assert_eq!(locks[1].amount, delegate_amount); + }); +} + +#[test] +fn unbond_should_fail_if_delegated_nomination() { + new_test_ext().execute_with(|| { + // Arrange + let who: AccountId = Dave.into(); + let validator = Staking::invulnerables()[0].clone(); + let operator: AccountId = Alice.into(); + let amount = 100_000; + let delegate_amount = amount / 2; + // Bond Some TNT + assert_ok!(Staking::bond( + RuntimeOrigin::signed(who.clone()), + amount, + pallet_staking::RewardDestination::Staked + )); + // Nominate the validator + assert_ok!(Staking::nominate(RuntimeOrigin::signed(who.clone()), vec![validator.clone()])); + + System::set_block_number(2); + >::on_initialize(2); + >::on_initialize(2); + >::on_finalize(2); + >::on_finalize(2); + + // Verify initial staking state + let ledger = Staking::ledger(sp_staking::StakingAccount::Stash(who.clone())).unwrap(); + assert_eq!(ledger.active, amount); + assert_eq!(ledger.total, amount); + assert_eq!(ledger.unlocking.len(), 0); + + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + + // Restake + assert_ok!(MultiAssetDelegation::delegate_nomination( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + delegate_amount, + Default::default(), + )); + + // Verify delegation state + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + assert_eq!(metadata.delegations.len(), 1); + let delegation = &metadata.delegations[0]; + assert_eq!(delegation.operator, operator); + assert_eq!(delegation.amount, delegate_amount); + assert_eq!(delegation.is_nomination, true); + assert_eq!(delegation.asset_id, Asset::Custom(TNT)); + + // Check operator metadata + let operator_metadata = MultiAssetDelegation::operator_info(operator.clone()).unwrap(); + assert_eq!(operator_metadata.delegation_count, 1); + let operator_delegation = &operator_metadata.delegations[0]; + assert_eq!(operator_delegation.delegator, who.clone()); + assert_eq!(operator_delegation.amount, delegate_amount); + + // Check locks before unbond attempt + let locks = pallet_balances::Pallet::::locks(&who); + assert_eq!(locks.len(), 2); + assert_eq!(&locks[0].id, b"staking "); + assert_eq!(locks[0].amount, amount); + assert_eq!(&locks[1].id, b"delegate"); + assert_eq!(locks[1].amount, delegate_amount); + let call = RuntimeCall::Staking(pallet_staking::Call::unbond { value: amount }); + + // Try to unbond from the staking pallet - should fail + assert_err!( + CheckNominatedRestaked::::new().validate( + &who, + &call, + &DispatchInfo::default(), + 0 + ), + TransactionValidityError::Invalid(InvalidTransaction::Custom(1)) + ); + + // Verify state remains unchanged after failed unbond + let ledger = Staking::ledger(sp_staking::StakingAccount::Stash(who.clone())).unwrap(); + assert_eq!(ledger.active, amount); + assert_eq!(ledger.total, amount); + assert_eq!(ledger.unlocking.len(), 0); + + // Verify delegation state remains unchanged + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + assert_eq!(metadata.delegations.len(), 1); + let delegation = &metadata.delegations[0]; + assert_eq!(delegation.operator, operator); + assert_eq!(delegation.amount, delegate_amount); + assert_eq!(delegation.is_nomination, true); + + // Verify locks remain unchanged + let locks = pallet_balances::Pallet::::locks(&who); + assert_eq!(locks.len(), 2); + assert_eq!(&locks[0].id, b"staking "); + assert_eq!(locks[0].amount, amount); + assert_eq!(&locks[1].id, b"delegate"); + assert_eq!(locks[1].amount, delegate_amount); + }); +} + +#[test] +fn successful_multiple_native_restaking() { + new_test_ext().execute_with(|| { + // Arrange + let who: AccountId = Bob.into(); + let operator: AccountId = Alice.into(); + let total_nomination = 100; + let first_restake = 40; + let second_restake = 30; + + // Setup operator + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + + // Setup nomination + assert_ok!(Staking::bond( + RuntimeOrigin::signed(who.clone()), + total_nomination, + pallet_staking::RewardDestination::Staked + )); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(who.clone()), vec![operator.clone()])); + + // First restake + assert_ok!(MultiAssetDelegation::delegate_nomination( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + first_restake, + Default::default(), + )); + + // Second restake + assert_ok!(MultiAssetDelegation::delegate_nomination( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + second_restake, + Default::default(), + )); + + // Assert + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + assert_eq!(metadata.delegations.len(), 1); + let delegation = &metadata.delegations[0]; + assert_eq!(delegation.operator, operator.clone()); + assert_eq!(delegation.amount, first_restake + second_restake); + + // Check operator metadata + let operator_metadata = MultiAssetDelegation::operator_info(operator.clone()).unwrap(); + assert_eq!(operator_metadata.delegation_count, 1); + let operator_delegation = &operator_metadata.delegations[0]; + assert_eq!(operator_delegation.delegator, who.clone()); + assert_eq!(operator_delegation.amount, first_restake + second_restake); + }); +} + +#[test] +fn native_restake_exceeding_nomination_amount() { + new_test_ext().execute_with(|| { + let who: AccountId = Bob.into(); + let operator: AccountId = Alice.into(); + let nomination_amount = 100; + let excessive_amount = 150; + + // Setup operator + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + + // Setup nomination + assert_ok!(Staking::bond( + RuntimeOrigin::signed(who.clone()), + nomination_amount, + pallet_staking::RewardDestination::Staked + )); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(who.clone()), vec![operator.clone()])); + + // Try to restake more than nominated + assert_noop!( + MultiAssetDelegation::delegate_nomination( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + excessive_amount, + Default::default(), + ), + Error::::InsufficientBalance + ); + }); +} + +#[test] +fn native_restake_with_no_active_nomination() { + new_test_ext().execute_with(|| { + let who: AccountId = Bob.into(); + let operator: AccountId = Alice.into(); + let amount = 100; + + // Setup operator + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + + // Try to restake without nomination + assert_noop!( + MultiAssetDelegation::delegate_nomination( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + amount, + Default::default(), + ), + Error::::NotNominator + ); + }); +} + +#[test] +fn native_restake_to_non_operator() { + new_test_ext().execute_with(|| { + let who: AccountId = Bob.into(); + let non_operator: AccountId = Charlie.into(); + let amount = 100; + + // Setup nomination + assert_ok!(Staking::bond( + RuntimeOrigin::signed(who.clone()), + amount, + pallet_staking::RewardDestination::Staked + )); + assert_ok!(Staking::nominate( + RuntimeOrigin::signed(who.clone()), + vec![non_operator.clone()] + )); + + // Try to restake to non-operator + assert_noop!( + MultiAssetDelegation::delegate_nomination( + RuntimeOrigin::signed(who.clone()), + non_operator.clone(), + amount, + Default::default(), + ), + Error::::NotAnOperator + ); + }); +} + +#[test] +fn native_restake_and_unstake_flow() { + new_test_ext().execute_with(|| { + let who: AccountId = Bob.into(); + let operator: AccountId = Alice.into(); + let amount = 100; + let unstake_amount = 40; + + // Setup + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + assert_ok!(Staking::bond( + RuntimeOrigin::signed(who.clone()), + amount, + pallet_staking::RewardDestination::Staked + )); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(who.clone()), vec![operator.clone()])); + + // Restake + assert_ok!(MultiAssetDelegation::delegate_nomination( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + amount, + Default::default(), + )); + + // Schedule unstake + assert_ok!(MultiAssetDelegation::schedule_nomination_unstake( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + unstake_amount, + Default::default(), + )); + + // Verify unstake request + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + assert_eq!(metadata.delegator_unstake_requests.len(), 1); + let request = &metadata.delegator_unstake_requests[0]; + assert_eq!(request.operator, operator.clone()); + assert_eq!(request.amount, unstake_amount); + + // Move to next round + CurrentRound::::put(10); + + // Execute unstake + assert_ok!(MultiAssetDelegation::execute_nomination_unstake( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + )); + + // Verify final state + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + assert_eq!(metadata.delegations.len(), 1); + let delegation = &metadata.delegations[0]; + assert_eq!(delegation.amount, amount - unstake_amount); + }); +} + +#[test] +fn native_restake_zero_amount() { + new_test_ext().execute_with(|| { + let who: AccountId = Bob.into(); + let operator: AccountId = Alice.into(); + let amount = 100; + + // Setup + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + assert_ok!(Staking::bond( + RuntimeOrigin::signed(who.clone()), + amount, + pallet_staking::RewardDestination::Staked + )); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(who.clone()), vec![operator.clone()])); + + // Try to restake zero amount + assert_noop!( + MultiAssetDelegation::delegate_nomination( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + 0, + Default::default(), + ), + Error::::InvalidAmount + ); + }); +} + +#[test] +fn native_restake_concurrent_operations() { + new_test_ext().execute_with(|| { + let who: AccountId = Bob.into(); + let operator: AccountId = Alice.into(); + let amount = 100; + + // Setup + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + assert_ok!(Staking::bond( + RuntimeOrigin::signed(who.clone()), + amount, + pallet_staking::RewardDestination::Staked + )); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(who.clone()), vec![operator.clone()])); + + // Perform multiple operations in same block + assert_ok!(MultiAssetDelegation::delegate_nomination( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + 50, + Default::default(), + )); + assert_ok!(MultiAssetDelegation::schedule_nomination_unstake( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + 20, + Default::default(), + )); + assert_ok!(MultiAssetDelegation::delegate_nomination( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + 30, + Default::default(), + )); + + // Verify final state + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + let delegation = &metadata.delegations[0]; + assert_eq!(delegation.amount, 80); // 50 + 30 + assert_eq!(metadata.delegator_unstake_requests.len(), 1); + }); +} + +#[test] +fn native_restake_early_unstake_execution_fails() { + new_test_ext().execute_with(|| { + let who: AccountId = Bob.into(); + let operator: AccountId = Alice.into(); + let amount = 100; + let unstake_amount = 40; + + // Setup + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + assert_ok!(Staking::bond( + RuntimeOrigin::signed(who.clone()), + amount, + pallet_staking::RewardDestination::Staked + )); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(who.clone()), vec![operator.clone()])); + + // Restake + assert_ok!(MultiAssetDelegation::delegate_nomination( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + amount, + Default::default(), + )); + + // Verify delegation state after restaking + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + assert_eq!(metadata.delegations.len(), 1); + let delegation = &metadata.delegations[0]; + assert_eq!(delegation.operator, operator); + assert_eq!(delegation.amount, amount); + assert_eq!(delegation.is_nomination, true); + assert_eq!(metadata.delegator_unstake_requests.len(), 0); + + // Schedule unstake + assert_ok!(MultiAssetDelegation::schedule_nomination_unstake( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + unstake_amount, + Default::default(), + )); + + // Verify unstake request state + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + assert_eq!(metadata.delegator_unstake_requests.len(), 1); + let request = &metadata.delegator_unstake_requests[0]; + assert_eq!(request.operator, operator); + assert_eq!(request.amount, unstake_amount); + assert_eq!(request.is_nomination, true); + + // Try to execute unstake immediately - should fail + assert_noop!( + MultiAssetDelegation::execute_nomination_unstake( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + ), + Error::::BondLessNotReady + ); + + // Verify state remains unchanged after failed execution + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + assert_eq!(metadata.delegations.len(), 1); + let delegation = &metadata.delegations[0]; + assert_eq!(delegation.operator, operator); + assert_eq!(delegation.amount, amount); + assert_eq!(delegation.is_nomination, true); + assert_eq!(metadata.delegator_unstake_requests.len(), 1); + let request = &metadata.delegator_unstake_requests[0]; + assert_eq!(request.operator, operator); + assert_eq!(request.amount, unstake_amount); + assert_eq!(request.is_nomination, true); + }); +} + +#[test] +fn native_restake_cancel_unstake() { + new_test_ext().execute_with(|| { + let who: AccountId = Bob.into(); + let operator: AccountId = Alice.into(); + let amount = 100; + let unstake_amount = 40; + + // Setup + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator.clone()), + 10_000 + )); + assert_ok!(Staking::bond( + RuntimeOrigin::signed(who.clone()), + amount, + pallet_staking::RewardDestination::Staked + )); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(who.clone()), vec![operator.clone()])); + + // Restake + assert_ok!(MultiAssetDelegation::delegate_nomination( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + amount, + Default::default(), + )); + + // Schedule unstake + assert_ok!(MultiAssetDelegation::schedule_nomination_unstake( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + unstake_amount, + Default::default(), + )); + + // Verify unstake request exists + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + assert_eq!(metadata.delegator_unstake_requests.len(), 1); + let request = &metadata.delegator_unstake_requests[0]; + assert_eq!(request.operator, operator.clone()); + assert_eq!(request.amount, unstake_amount); + + // Cancel unstake request + assert_ok!(MultiAssetDelegation::cancel_nomination_unstake( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + )); + + // Verify unstake request is removed + let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + assert_eq!(metadata.delegator_unstake_requests.len(), 0); + + // Verify delegation amount remains unchanged + let delegation = &metadata.delegations[0]; + assert_eq!(delegation.amount, amount); + + // Try to execute cancelled unstake - should fail + assert_noop!( + MultiAssetDelegation::execute_nomination_unstake( + RuntimeOrigin::signed(who.clone()), + operator.clone(), + ), + Error::::NoBondLessRequest + ); + }); +} diff --git a/pallets/multi-asset-delegation/src/tests/operator.rs b/pallets/multi-asset-delegation/src/tests/operator.rs index 08cba2757..62e3c1f31 100644 --- a/pallets/multi-asset-delegation/src/tests/operator.rs +++ b/pallets/multi-asset-delegation/src/tests/operator.rs @@ -20,8 +20,10 @@ use crate::{ }; use frame_support::{assert_noop, assert_ok}; use sp_keyring::AccountKeyring::{Alice, Bob, Eve}; -use sp_runtime::Percent; -use tangle_primitives::services::Asset; +use tangle_primitives::{ + services::{Asset, UnappliedSlash}, + traits::SlashManager, +}; #[test] fn join_operator_success() { @@ -625,19 +627,22 @@ fn slash_operator_success() { operator_stake )); - // Setup delegators - let delegator_stake = 5_000; - let asset_id = Asset::Custom(1); + // Setup delegators with different assets and blueprint selections + let delegator1_stake = 5_000; + let delegator2_stake = 3_000; + let asset1 = Asset::Custom(1); + let asset2 = Asset::Custom(2); let blueprint_id = 1; + let service_id = 42; - create_and_mint_tokens(1, Bob.to_account_id(), delegator_stake); - mint_tokens(Bob.to_account_id(), 1, Bob.to_account_id(), delegator_stake); + // Setup first delegator with asset1 and selected blueprint + create_and_mint_tokens(1, Bob.to_account_id(), delegator1_stake); + mint_tokens(Bob.to_account_id(), 1, Bob.to_account_id(), delegator1_stake); - // Setup delegator with fixed blueprint selection assert_ok!(MultiAssetDelegation::deposit( RuntimeOrigin::signed(Bob.to_account_id()), - asset_id, - delegator_stake, + asset1, + delegator1_stake, None, None )); @@ -650,36 +655,79 @@ fn slash_operator_success() { assert_ok!(MultiAssetDelegation::delegate( RuntimeOrigin::signed(Bob.to_account_id()), Alice.to_account_id(), - asset_id, - delegator_stake, + asset1, + delegator1_stake, Fixed(vec![blueprint_id].try_into().unwrap()), )); - // Slash 50% of stakes - let slash_percentage = Percent::from_percent(50); - assert_ok!(MultiAssetDelegation::slash_operator( - &Alice.to_account_id(), - blueprint_id, - slash_percentage + // Setup second delegator with asset2 but without selecting the blueprint + create_and_mint_tokens(2, Eve.to_account_id(), delegator2_stake); + mint_tokens(Eve.to_account_id(), 2, Eve.to_account_id(), delegator2_stake); + + assert_ok!(MultiAssetDelegation::deposit( + RuntimeOrigin::signed(Eve.to_account_id()), + asset2, + delegator2_stake, + None, + None )); + assert_ok!(MultiAssetDelegation::delegate( + RuntimeOrigin::signed(Eve.to_account_id()), + Alice.to_account_id(), + asset2, + delegator2_stake, + Fixed(vec![].try_into().unwrap()), + )); + + // Create UnappliedSlash with 50% slash for operator and first delegator only + let exposed_stake = operator_stake / 2; // 50% of operator stake + let exposed_delegation = delegator1_stake / 2; // 50% of delegator1 stake + + let unapplied_slash = UnappliedSlash { + era: 1, + blueprint_id, + service_id, + operator: Alice.to_account_id(), + own: exposed_stake, + others: vec![(Bob.to_account_id(), asset1, exposed_delegation)], + reporters: vec![Eve.to_account_id()], + }; + + // Apply the slash + assert_ok!(MultiAssetDelegation::slash_operator(&unapplied_slash)); + // Verify operator stake was slashed let operator_info = MultiAssetDelegation::operator_info(Alice.to_account_id()).unwrap(); - assert_eq!(operator_info.stake, operator_stake / 2); + assert_eq!(operator_info.stake, operator_stake - exposed_stake); - // Verify delegator stake was slashed - let delegator = MultiAssetDelegation::delegators(Bob.to_account_id()).unwrap(); - let delegation = delegator + // Verify first delegator (Bob) was slashed + let delegator1 = MultiAssetDelegation::delegators(Bob.to_account_id()).unwrap(); + let delegation1 = delegator1 .delegations .iter() - .find(|d| d.operator == Alice.to_account_id()) + .find(|d| d.operator == Alice.to_account_id() && d.asset_id == asset1) .unwrap(); - assert_eq!(delegation.amount, delegator_stake / 2); + assert_eq!(delegation1.amount, delegator1_stake - exposed_delegation); - // Verify event + // Verify second delegator (Eve) was NOT slashed since they didn't select the blueprint + let delegator2 = MultiAssetDelegation::delegators(Eve.to_account_id()).unwrap(); + let delegation2 = delegator2 + .delegations + .iter() + .find(|d| d.operator == Alice.to_account_id() && d.asset_id == asset2) + .unwrap(); + assert_eq!(delegation2.amount, delegator2_stake); // Amount unchanged + + // Verify events System::assert_has_event(RuntimeEvent::MultiAssetDelegation(Event::OperatorSlashed { who: Alice.to_account_id(), - amount: operator_stake / 2, + amount: exposed_stake, + })); + + System::assert_has_event(RuntimeEvent::MultiAssetDelegation(Event::DelegatorSlashed { + who: Bob.to_account_id(), + amount: exposed_delegation, })); }); } @@ -687,12 +735,18 @@ fn slash_operator_success() { #[test] fn slash_operator_not_an_operator() { new_test_ext().execute_with(|| { + let unapplied_slash = UnappliedSlash { + era: 1, + blueprint_id: 1, + service_id: 42, + operator: Alice.to_account_id(), + own: 5_000, + others: vec![], + reporters: vec![Eve.to_account_id()], + }; + assert_noop!( - MultiAssetDelegation::slash_operator( - &Alice.to_account_id(), - 1, - Percent::from_percent(50) - ), + MultiAssetDelegation::slash_operator(&unapplied_slash), Error::::NotAnOperator ); }); @@ -708,12 +762,18 @@ fn slash_operator_not_active() { )); assert_ok!(MultiAssetDelegation::go_offline(RuntimeOrigin::signed(Alice.to_account_id()))); + let unapplied_slash = UnappliedSlash { + era: 1, + blueprint_id: 1, + service_id: 42, + operator: Alice.to_account_id(), + own: 5_000, + others: vec![], + reporters: vec![Eve.to_account_id()], + }; + assert_noop!( - MultiAssetDelegation::slash_operator( - &Alice.to_account_id(), - 1, - Percent::from_percent(50) - ), + MultiAssetDelegation::slash_operator(&unapplied_slash), Error::::NotActiveOperator ); }); @@ -728,13 +788,15 @@ fn slash_delegator_fixed_blueprint_not_selected() { 10_000 )); - create_and_mint_tokens(1, Bob.to_account_id(), 10_000); - // Setup delegator with fixed blueprint selection + let delegator_stake = 5_000; + let asset_id = Asset::Custom(1); + create_and_mint_tokens(1, Bob.to_account_id(), delegator_stake); + assert_ok!(MultiAssetDelegation::deposit( RuntimeOrigin::signed(Bob.to_account_id()), - Asset::Custom(1), - 5_000, + asset_id, + delegator_stake, None, None )); @@ -747,20 +809,30 @@ fn slash_delegator_fixed_blueprint_not_selected() { assert_ok!(MultiAssetDelegation::delegate( RuntimeOrigin::signed(Bob.to_account_id()), Alice.to_account_id(), - Asset::Custom(1), - 5_000, - Fixed(vec![2].try_into().unwrap()), - )); - - // Try to slash with unselected blueprint - assert_noop!( - MultiAssetDelegation::slash_delegator( - &Bob.to_account_id(), - &Alice.to_account_id(), - 5, - Percent::from_percent(50) - ), - Error::::BlueprintNotSelected - ); + asset_id, + delegator_stake, + Fixed(vec![2].try_into().unwrap()), // Selected blueprint 2, not 1 + )); + + // Create UnappliedSlash for blueprint 1 + let unapplied_slash = UnappliedSlash { + era: 1, + blueprint_id: 1, + service_id: 42, + operator: Alice.to_account_id(), + own: 5_000, + others: vec![], + reporters: vec![Eve.to_account_id()], + }; + + // Verify delegator is not slashed since they didn't select blueprint 1 + assert_ok!(MultiAssetDelegation::slash_operator(&unapplied_slash)); + let delegator = MultiAssetDelegation::delegators(Bob.to_account_id()).unwrap(); + let delegation = delegator + .delegations + .iter() + .find(|d| d.operator == Alice.to_account_id()) + .unwrap(); + assert_eq!(delegation.amount, delegator_stake); // Amount unchanged }); } diff --git a/pallets/multi-asset-delegation/src/tests/session_manager.rs b/pallets/multi-asset-delegation/src/tests/session_manager.rs index 970375cbd..5418e07ba 100644 --- a/pallets/multi-asset-delegation/src/tests/session_manager.rs +++ b/pallets/multi-asset-delegation/src/tests/session_manager.rs @@ -160,7 +160,7 @@ fn handle_round_change_with_unstake_should_work() { assert_eq!(snapshot1.stake, 10_000); assert_eq!(snapshot1.delegations.len(), 1); assert_eq!(snapshot1.delegations[0].delegator, delegator1.clone()); - assert_eq!(snapshot1.delegations[0].amount, amount1 - unstake_amount); // Amount reduced by unstake_amount + assert_eq!(snapshot1.delegations[0].amount, amount1); // Amount should be the same assert_eq!(snapshot1.delegations[0].asset_id, asset_id); // Check the snapshot for operator2 diff --git a/pallets/multi-asset-delegation/src/traits.rs b/pallets/multi-asset-delegation/src/traits.rs index f5576ef4f..f639d69ff 100644 --- a/pallets/multi-asset-delegation/src/traits.rs +++ b/pallets/multi-asset-delegation/src/traits.rs @@ -14,20 +14,19 @@ // You should have received a copy of the GNU General Public License // along with Tangle. If not, see . use super::*; -use crate::types::{BalanceOf, OperatorStatus}; +use crate::types::{BalanceOf, DelegatorBlueprintSelection, OperatorStatus}; use frame_system::pallet_prelude::BlockNumberFor; -use sp_runtime::{traits::Zero, Percent}; +use sp_runtime::traits::Zero; use sp_std::prelude::*; use tangle_primitives::types::rewards::UserDepositWithLocks; use tangle_primitives::{ services::Asset, traits::MultiAssetDelegationInfo, BlueprintId, RoundIndex, }; -impl MultiAssetDelegationInfo, BlockNumberFor> +impl + MultiAssetDelegationInfo, BlockNumberFor, T::AssetId> for crate::Pallet { - type AssetId = T::AssetId; - fn get_current_round() -> RoundIndex { Self::current_round() } @@ -60,7 +59,7 @@ impl MultiAssetDelegationInfo, Bloc fn get_delegators_for_operator( operator: &T::AccountId, - ) -> Vec<(T::AccountId, BalanceOf, Asset)> { + ) -> Vec<(T::AccountId, BalanceOf, Asset)> { Operators::::get(operator).map_or(Vec::new(), |metadata| { metadata .delegations @@ -70,8 +69,26 @@ impl MultiAssetDelegationInfo, Bloc }) } - fn slash_operator(operator: &T::AccountId, blueprint_id: BlueprintId, percentage: Percent) { - let _ = Pallet::::slash_operator(operator, blueprint_id, percentage); + fn has_delegator_selected_blueprint( + delegator: &T::AccountId, + operator: &T::AccountId, + blueprint_id: BlueprintId, + ) -> bool { + // Get delegator metadata + if let Some(metadata) = Delegators::::get(delegator) { + // Find delegation to specific operator and check its blueprint selection + metadata.delegations.iter().any(|delegation| { + delegation.operator == *operator + && match &delegation.blueprint_selection { + DelegatorBlueprintSelection::Fixed(blueprints) => { + blueprints.contains(&blueprint_id) + }, + DelegatorBlueprintSelection::All => true, + } + }) + } else { + false + } } fn get_user_deposit_with_locks( diff --git a/pallets/multi-asset-delegation/src/types/delegator.rs b/pallets/multi-asset-delegation/src/types/delegator.rs index 84aaaabd7..13d3f0722 100644 --- a/pallets/multi-asset-delegation/src/types/delegator.rs +++ b/pallets/multi-asset-delegation/src/types/delegator.rs @@ -16,7 +16,7 @@ use super::*; use frame_support::{ensure, pallet_prelude::Get, BoundedVec}; -use sp_runtime::traits::CheckedAdd; +use sp_runtime::traits::{CheckedAdd, Saturating}; use sp_std::{fmt::Debug, vec}; use tangle_primitives::{ services::Asset, @@ -54,7 +54,7 @@ pub enum DelegatorStatus { pub struct WithdrawRequest { /// The ID of the asset to be withdrawd. pub asset_id: Asset, - /// The amount of the asset to be withdrawd. + /// The amount of the asset to be withdrawn. pub amount: Balance, /// The round in which the withdraw was requested. pub requested_round: RoundIndex, @@ -73,6 +73,24 @@ pub struct BondLessRequest, + /// Whether this unstake request is for a nomination delegation + pub is_nomination: bool, +} + +/// Represents a delegation bond from a delegator to an operator. +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo, Eq, PartialEq)] +pub struct BondInfoDelegator> +{ + /// The operator being delegated to. + pub operator: AccountId, + /// The amount being delegated. + pub amount: Balance, + /// The asset being delegated. + pub asset_id: Asset, + /// The blueprint selection for this delegation. + pub blueprint_selection: DelegatorBlueprintSelection, + /// Whether this delegation is from nominated tokens + pub is_nomination: bool, } /// Stores the state of a delegator, including deposits, delegations, and requests. @@ -185,7 +203,6 @@ impl< /// Calculates the total delegation amount for a specific asset. pub fn calculate_delegation_by_asset(&self, asset_id: Asset) -> Balance - // Asset) -> Balance where Balance: Default + core::ops::AddAssign + Clone + CheckedAdd, AssetId: Eq + PartialEq, @@ -209,6 +226,32 @@ impl< { self.delegations.iter().filter(|&stake| stake.operator == operator).collect() } + + /// Calculate total nomination delegations + pub fn total_nomination_delegations(&self) -> Balance + where + Balance: Default + core::ops::AddAssign + Clone + Saturating, + AssetId: Eq + PartialEq, + { + self.delegations + .iter() + .filter(|d| d.is_nomination) + .fold(Balance::default(), |acc, delegation| { + acc.saturating_add(delegation.amount.clone()) + }) + } + + /// Find nomination delegation by operator + pub fn get_nomination_delegation_by_operator( + &self, + operator: &AccountId, + ) -> Option<&BondInfoDelegator> + where + AccountId: PartialEq, + AssetId: Eq, + { + self.delegations.iter().find(|d| &d.operator == operator && d.is_nomination) + } } /// Represents a deposit of a specific asset. @@ -333,17 +376,3 @@ impl< Ok(()) } } - -/// Represents a stake between a delegator and an operator. -#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo, Eq, PartialEq)] -pub struct BondInfoDelegator> -{ - /// The account ID of the operator. - pub operator: AccountId, - /// The amount bonded. - pub amount: Balance, - /// The ID of the bonded asset. - pub asset_id: Asset, - /// The blueprint selection mode for this delegator. - pub blueprint_selection: DelegatorBlueprintSelection, -} diff --git a/pallets/rewards/src/functions/mod.rs b/pallets/rewards/src/functions/mod.rs index c7d9fa947..3e2447f16 100644 --- a/pallets/rewards/src/functions/mod.rs +++ b/pallets/rewards/src/functions/mod.rs @@ -23,6 +23,7 @@ use sp_std::vec::Vec; use tangle_primitives::services::Asset; pub mod rewards; +pub mod services; impl Pallet { pub fn remove_asset_from_vault( diff --git a/pallets/rewards/src/functions/rewards.rs b/pallets/rewards/src/functions/rewards.rs index 3a7d175df..e933b38bf 100644 --- a/pallets/rewards/src/functions/rewards.rs +++ b/pallets/rewards/src/functions/rewards.rs @@ -13,6 +13,7 @@ // // You should have received a copy of the GNU General Public License // along with Tangle. If not, see . + use crate::{ ApyBlocks, AssetLookupRewardVaults, BalanceOf, Config, DecayRate, DecayStartPeriod, Error, Event, Pallet, RewardConfigForAssetVault, RewardConfigStorage, RewardVaultsPotAccount, diff --git a/pallets/rewards/src/functions/services.rs b/pallets/rewards/src/functions/services.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/pallets/rewards/src/functions/services.rs @@ -0,0 +1 @@ + diff --git a/pallets/rewards/src/lib.rs b/pallets/rewards/src/lib.rs index f4593419d..6b17a1936 100644 --- a/pallets/rewards/src/lib.rs +++ b/pallets/rewards/src/lib.rs @@ -124,7 +124,7 @@ pub mod pallet { Self::AccountId, BalanceOf, BlockNumberFor, - AssetId = Self::AssetId, + Self::AssetId, >; /// The origin that can manage reward assets diff --git a/pallets/rewards/src/mock.rs b/pallets/rewards/src/mock.rs index 8e887a6ef..53546977c 100644 --- a/pallets/rewards/src/mock.rs +++ b/pallets/rewards/src/mock.rs @@ -36,7 +36,7 @@ use sp_runtime::{ traits::{ConvertInto, IdentityLookup}, AccountId32, BuildStorage, Perbill, }; -use tangle_primitives::{services::Asset, types::rewards::UserDepositWithLocks}; +use tangle_primitives::{services::Asset, types::rewards::UserDepositWithLocks, BlueprintId}; use core::ops::Mul; use std::{cell::RefCell, collections::BTreeMap, sync::Arc}; @@ -257,11 +257,9 @@ pub struct MockDelegationData { } pub struct MockDelegationManager; -impl tangle_primitives::traits::MultiAssetDelegationInfo +impl tangle_primitives::traits::MultiAssetDelegationInfo for MockDelegationManager { - type AssetId = AssetId; - fn get_current_round() -> tangle_primitives::types::RoundIndex { Default::default() } @@ -288,27 +286,29 @@ impl tangle_primitives::traits::MultiAssetDelegationInfo, + _asset_id: &Asset, ) -> Balance { Default::default() } fn get_delegators_for_operator( _operator: &AccountId, - ) -> Vec<(AccountId, Balance, Asset)> { + ) -> Vec<(AccountId, Balance, Asset)> { Default::default() } - fn slash_operator( + fn has_delegator_selected_blueprint( + _delegator: &AccountId, _operator: &AccountId, - _blueprint_id: tangle_primitives::BlueprintId, - _percentage: sp_runtime::Percent, - ) { + _blueprint_id: BlueprintId, + ) -> bool { + // For mock implementation, always return true + true } fn get_user_deposit_with_locks( who: &AccountId, - asset_id: Asset, + asset_id: Asset, ) -> Option> { MOCK_DELEGATION_INFO.with(|delegation_info| { delegation_info.borrow().deposits.get(&(who.clone(), asset_id)).cloned() diff --git a/pallets/services/Cargo.toml b/pallets/services/Cargo.toml index 3d5140594..a5efa3739 100644 --- a/pallets/services/Cargo.toml +++ b/pallets/services/Cargo.toml @@ -65,6 +65,9 @@ pallet-evm-precompile-modexp = { workspace = true } pallet-evm-precompile-sha3fips = { workspace = true } pallet-evm-precompile-simple = { workspace = true } +pallet-evm-precompile-balances-erc20 = { workspace = true } +pallet-evm-precompileset-assets-erc20 = { workspace = true } + precompile-utils = { workspace = true } pallet-session = { workspace = true } diff --git a/pallets/services/prompt.md b/pallets/services/prompt.md new file mode 100644 index 000000000..8993337e5 --- /dev/null +++ b/pallets/services/prompt.md @@ -0,0 +1,56 @@ +# Service Marketplace System Design Prompt + +## Core Components + +1. **Service Request Types** + +- Direct requests to specific operators +- Open market with dynamic participation +- Time-bounded auctions +- Standing orderbook mechanics + +2. **Dynamic Security Model** + +- Security pools for flexible collateral +- Dynamic operator participation +- Asset-specific security requirements +- Join/leave mechanics for operators + +3. **Market Mechanisms** + +- Continuous orderbook for standard services +- Auctions for specialized requirements +- Price discovery through market forces +- Automated matching and service creation + +## Key Abstractions + +```rust +// Market mechanisms for service creation +enum MarketMechanism { + Direct { ... } // Direct operator selection + OrderBook { ... } // Standing orders with price matching + TimedAuction { ... } // Time-bounded price discovery +} +// Dynamic security management +struct SecurityPool { + asset: Asset, + participants: Map, + requirements: SecurityRequirements +} +// Market order representation +struct MarketOrder { + operator: AccountId, + price: Balance, + security_commitment: SecurityCommitment, + expiry: BlockNumber +} +``` + +## Design Principles + +1. Support multiple service creation patterns +2. Enable market-driven pricing +3. Maintain security and reliability +4. Allow dynamic participation +5. Automate matching where possible diff --git a/pallets/services/rpc/runtime-api/src/lib.rs b/pallets/services/rpc/runtime-api/src/lib.rs index 606d0b93a..63e221283 100644 --- a/pallets/services/rpc/runtime-api/src/lib.rs +++ b/pallets/services/rpc/runtime-api/src/lib.rs @@ -21,7 +21,7 @@ use parity_scale_codec::Codec; use sp_runtime::{traits::MaybeDisplay, Serialize}; use sp_std::vec::Vec; -use tangle_primitives::services::{Constraints, RpcServicesWithBlueprint}; +use tangle_primitives::services::{AssetIdT, Constraints, RpcServicesWithBlueprint}; pub type BlockNumberOf = <::HeaderT as sp_runtime::traits::Header>::Number; @@ -31,7 +31,7 @@ sp_api::decl_runtime_apis! { where C: Constraints, AccountId: Codec + MaybeDisplay + Serialize, - AssetId: Codec + MaybeDisplay + Serialize, + AssetId: AssetIdT, { /// Query all the services that this operator is providing along with their blueprints. /// diff --git a/pallets/services/rpc/src/lib.rs b/pallets/services/rpc/src/lib.rs index ce67fb1b7..f5b749fb7 100644 --- a/pallets/services/rpc/src/lib.rs +++ b/pallets/services/rpc/src/lib.rs @@ -29,7 +29,7 @@ use sp_runtime::{ DispatchError, Serialize, }; use std::sync::Arc; -use tangle_primitives::services::{Constraints, RpcServicesWithBlueprint}; +use tangle_primitives::services::{AssetIdT, Constraints, RpcServicesWithBlueprint}; type BlockNumberOf = <::HeaderT as sp_runtime::traits::Header>::Number; @@ -41,7 +41,7 @@ where X: Constraints, AccountId: Codec + MaybeDisplay + core::fmt::Debug + Send + Sync + 'static + Serialize, BlockNumber: Codec + MaybeDisplay + core::fmt::Debug + Send + Sync + 'static + Serialize, - AssetId: Codec + MaybeDisplay + core::fmt::Debug + Send + Sync + 'static + Serialize, + AssetId: AssetIdT, { #[method(name = "services_queryServicesWithBlueprintsByOperator")] fn query_services_with_blueprints_by_operator( @@ -70,7 +70,7 @@ impl where Block: BlockT, AccountId: Codec + MaybeDisplay + core::fmt::Debug + Send + Sync + 'static + Serialize, - AssetId: Codec + MaybeDisplay + core::fmt::Debug + Send + Sync + 'static + Serialize, + AssetId: AssetIdT, X: Constraints, C: HeaderBackend + ProvideRuntimeApi + Send + Sync + 'static, C::Api: ServicesRuntimeApi, diff --git a/pallets/services/src/functions/approve.rs b/pallets/services/src/functions/approve.rs new file mode 100644 index 000000000..4a0d0cb81 --- /dev/null +++ b/pallets/services/src/functions/approve.rs @@ -0,0 +1,336 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use crate::{ + types::*, Config, Error, Event, Instances, NextInstanceId, OperatorsProfile, Pallet, + ServiceRequests, StagingServicePayments, UserServices, +}; +use frame_support::{ + pallet_prelude::*, + traits::{fungibles::Mutate, tokens::Preservation, Currency, ExistenceRequirement}, +}; +use frame_system::pallet_prelude::*; +use sp_runtime::{traits::Zero, Percent}; +use sp_std::vec::Vec; +use tangle_primitives::{ + services::{ + ApprovalState, Asset, AssetSecurityCommitment, Constraints, EvmAddressMapping, Service, + ServiceRequest, StagingServicePayment, + }, + BlueprintId, +}; + +impl Pallet { + /// Process an operator's approval for a service request. + /// + /// This function handles the approval workflow for a service request, including: + /// 1. Validating the operator's eligibility to approve + /// 2. Updating the approval state with security commitments + /// 3. Checking if all operators have approved + /// 4. Initializing the service if fully approved + /// 5. Processing payments to the MBSM + /// + /// # Arguments + /// + /// * `operator` - The account ID of the approving operator + /// * `request_id` - The ID of the service request being approved + /// * `native_exposure_percent` - Percentage of native token stake to expose + /// * `asset_exposure` - Vector of asset-specific exposure commitments + /// + /// # Returns + /// + /// Returns a DispatchResult indicating success or the specific error that occurred + pub fn do_approve( + operator: T::AccountId, + request_id: u64, + native_exposure_percent: Percent, + asset_exposures: Vec>, + ) -> DispatchResult { + // Retrieve and validate the service request + let mut request = Self::service_requests(request_id)?; + + // Ensure asset exposures don't exceed max assets per service + ensure!( + asset_exposures.len() <= T::MaxAssetsPerService::get() as usize, + Error::::MaxAssetsPerServiceExceeded + ); + // Ensure asset exposures length matches requested assets length + ensure!( + asset_exposures.len() == request.non_native_asset_security.len(), + Error::::InvalidAssetMatching + ); + // Ensure no duplicate assets in exposures + let mut seen_assets = sp_std::collections::btree_set::BTreeSet::new(); + for exposure in asset_exposures.iter() { + ensure!(seen_assets.insert(&exposure.asset), Error::::DuplicateAsset); + } + + // Ensure all assets in request have matching exposures in same order + for (i, required_asset) in request.non_native_asset_security.iter().enumerate() { + ensure!( + asset_exposures[i].asset == required_asset.asset, + Error::::InvalidAssetMatching + ); + } + + // Find and update operator's approval state + let updated = request + .operators_with_approval_state + .iter_mut() + .find(|(op, _)| op == &operator) + .map(|(_, state)| { + *state = ApprovalState::Approved { + native_exposure_percent, + asset_exposure: asset_exposures.clone(), + } + }); + ensure!(updated.is_some(), Error::::ApprovalNotRequested); + + let blueprint_id = request.blueprint; + let (_, blueprint) = Self::blueprints(blueprint_id)?; + let preferences = Self::operators(blueprint_id, operator.clone())?; + + // Validate operator commitments against service requirements + ensure!( + native_exposure_percent >= T::NativeExposureMinimum::get(), + Error::::InvalidRequestInput + ); + ensure!(request.validate_commitments(&asset_exposures), Error::::InvalidRequestInput); + + // Call approval hook + let (allowed, _weight) = Self::on_approve_hook( + &blueprint, + blueprint_id, + &preferences, + request_id, + native_exposure_percent.deconstruct(), + ) + .map_err(|_| Error::::OnApproveFailure)?; + ensure!(allowed, Error::::ApprovalInterrupted); + + // Get lists of approved and pending operators + let approved = request + .operators_with_approval_state + .iter() + .filter_map(|(op, state)| { + if matches!(state, ApprovalState::Approved { .. }) { + Some(op.clone()) + } else { + None + } + }) + .collect::>(); + + let pending_approvals = request + .operators_with_approval_state + .iter() + .filter_map(|(op, state)| { + if matches!(state, ApprovalState::Pending) { + Some(op.clone()) + } else { + None + } + }) + .collect::>(); + + // Emit approval event + Self::deposit_event(Event::ServiceRequestApproved { + operator: operator.clone(), + request_id, + blueprint_id, + pending_approvals, + approved: approved.clone(), + }); + + // If all operators have approved, initialize the service + if request.is_approved() { + Self::initialize_approved_service(request_id, request)?; + } else { + // Update the service request if still pending approvals + ServiceRequests::::insert(request_id, request); + } + + Ok(()) + } + + /// Initialize a service after all operators have approved. + /// + /// This is a helper function that handles the service initialization process including: + /// - Creating the service instance + /// - Processing payments + /// - Updating operator profiles + /// - Emitting events + /// + /// # Arguments + /// + /// * `request` - The approved service request to initialize + fn initialize_approved_service( + request_id: u64, + request: ServiceRequest, T::AssetId>, + ) -> DispatchResult { + // Remove the service request since it's now approved + ServiceRequests::::remove(request_id); + + let service_id = Self::next_instance_id(); + + // Collect operator commitments + let (native_exposures, non_native_exposures): ( + Vec<(T::AccountId, Percent)>, + Vec<( + T::AccountId, + BoundedVec< + AssetSecurityCommitment, + // TODO: Verify this doesn't cause issues. Constraints and `T::MaxAssetsPerService` as conflicting. + ::MaxAssetsPerService, + >, + )>, + ) = request + .operators_with_approval_state + .into_iter() + .filter_map(|(op, state)| match state { + ApprovalState::Approved { native_exposure_percent, asset_exposure } => { + // This is okay because we assert that each operators approval state contains + // a bounded list of asset exposures in the initial `do_approve` call. + let bounded_asset_exposure = BoundedVec::try_from(asset_exposure).unwrap(); + Some(((op.clone(), native_exposure_percent), (op, bounded_asset_exposure))) + }, + _ => None, + }) + .unzip(); + + // Update operator profiles + for (operator, _) in &native_exposures { + OperatorsProfile::::try_mutate_exists(operator, |profile| { + profile + .as_mut() + .and_then(|p| p.services.try_insert(service_id).ok()) + .ok_or(Error::::NotRegistered) + })?; + } + + // Create bounded vectors for service instance + let native_exposures = BoundedVec::try_from(native_exposures) + .map_err(|_| Error::::MaxServiceProvidersExceeded)?; + let non_native_exposures = BoundedVec::try_from(non_native_exposures) + .map_err(|_| Error::::MaxServiceProvidersExceeded)?; + + // Create the service instance + let service = Service { + id: service_id, + blueprint: request.blueprint, + owner: request.owner.clone(), + non_native_asset_security: non_native_exposures, + native_asset_security: native_exposures, + permitted_callers: request.permitted_callers.clone(), + ttl: request.ttl, + membership_model: request.membership_model, + }; + + // Update storage + UserServices::::try_mutate(&request.owner, |service_ids| { + Instances::::insert(service_id, service.clone()); + NextInstanceId::::set(service_id.saturating_add(1)); + service_ids + .try_insert(service_id) + .map_err(|_| Error::::MaxServicesPerUserExceeded) + })?; + + // Process payment if it exists + if let Some(payment) = Self::service_payment(request_id) { + Self::process_service_payment(request.blueprint, &payment)?; + StagingServicePayments::::remove(request_id); + } + + // Call service initialization hook + let (_, blueprint) = Self::blueprints(request.blueprint)?; + let (allowed, _weight) = Self::on_service_init_hook( + &blueprint, + request.blueprint, + request_id, + service_id, + &request.owner, + &request.permitted_callers, + request.ttl, + ) + .map_err(|_| Error::::OnServiceInitHook)?; + ensure!(allowed, Error::::ServiceInitializationInterrupted); + + // Emit service initiated event + Self::deposit_event(Event::ServiceInitiated { + owner: request.owner, + request_id, + service_id, + blueprint_id: request.blueprint, + assets: request.non_native_asset_security.iter().map(|a| a.asset.clone()).collect(), + }); + + Ok(()) + } + + /// Process a service payment by transferring funds to the MBSM. + /// + /// This function handles transferring payment from the pallet account to the MBSM account + /// based on the payment asset type (native, custom, or ERC20). + /// + /// # Arguments + /// + /// * `payment` - The payment details including asset type and amount + /// + /// # Returns + /// + /// Returns a DispatchResult indicating success or the specific error that occurred + pub(crate) fn process_service_payment( + blueprint_id: BlueprintId, + payment: &StagingServicePayment>, + ) -> DispatchResult { + let (_, blueprint) = Self::blueprints(blueprint_id)?; + + // send payments to the MBSM + let mbsm_address = Self::mbsm_address_of(&blueprint)?; + let mbsm_account_id = T::EvmAddressMapping::into_account_id(mbsm_address); + match payment.asset.clone() { + Asset::Custom(asset_id) if asset_id == Zero::zero() => { + T::Currency::transfer( + &Self::pallet_account(), + &mbsm_account_id, + payment.amount, + ExistenceRequirement::AllowDeath, + )?; + }, + Asset::Custom(asset_id) => { + T::Fungibles::transfer( + asset_id, + &Self::pallet_account(), + &mbsm_account_id, + payment.amount, + Preservation::Expendable, + )?; + }, + Asset::Erc20(token) => { + let (success, _weight) = Self::erc20_transfer( + token, + Self::pallet_evm_account(), + mbsm_address, + payment.amount, + ) + .map_err(|_| Error::::OnErc20TransferFailure)?; + ensure!(success, Error::::ERC20TransferFailed); + }, + } + + Ok(()) + } +} diff --git a/pallets/services/src/functions.rs b/pallets/services/src/functions/evm_hooks.rs similarity index 79% rename from pallets/services/src/functions.rs rename to pallets/services/src/functions/evm_hooks.rs index f85bfdec9..a49098dc5 100644 --- a/pallets/services/src/functions.rs +++ b/pallets/services/src/functions/evm_hooks.rs @@ -1,40 +1,38 @@ -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, string::String, vec, vec::Vec}; - -#[cfg(feature = "std")] -use std::{boxed::Box, string::String, vec::Vec}; - +use crate::types::BalanceOf; +use crate::{Config, Error, Event, MasterBlueprintServiceManagerRevisions, Pallet, Pays, Weight}; use ethabi::{Function, StateMutability, Token}; use frame_support::dispatch::{DispatchErrorWithPostInfo, PostDispatchInfo}; -use sp_core::{H160, U256}; -use sp_runtime::traits::{UniqueSaturatedInto, Zero}; +use frame_system::pallet_prelude::BlockNumberFor; +use parity_scale_codec::Encode; +use sp_core::{Get, H160, U256}; +use sp_runtime::traits::{AccountIdConversion, UniqueSaturatedInto, Zero}; +use sp_std::{boxed::Box, vec, vec::Vec}; use tangle_primitives::services::{ Asset, BlueprintServiceManager, EvmAddressMapping, EvmGasWeightMapping, EvmRunner, Field, MasterBlueprintServiceManagerRevision, OperatorPreferences, Service, ServiceBlueprint, }; -use super::*; -use crate::types::BalanceOf; +#[cfg(not(feature = "std"))] +use alloc::string::String; +#[cfg(feature = "std")] +use std::string::String; #[allow(clippy::too_many_arguments)] impl Pallet { - /// Returns the account id of the pallet. + /// Returns the account ID of the pallet. + pub fn pallet_account() -> T::AccountId { + T::PalletId::get().into_account_truncating() + } + + /// Returns the EVM account id of the pallet. /// /// This function retrieves the account id associated with the pallet by converting /// the pallet evm address to an account id. /// /// # Returns /// * `T::AccountId` - The account id of the pallet. - pub fn account_id() -> T::AccountId { - T::EvmAddressMapping::into_account_id(Self::address()) - } - - /// Returns the EVM address of the pallet. - /// - /// # Returns - /// * `H160` - The address of the pallet. - pub fn address() -> H160 { - T::PalletEVMAddress::get() + pub fn pallet_evm_account() -> H160 { + T::EvmAddressMapping::into_address(Self::pallet_account()) } /// Get the address of the master blueprint service manager at a given revision. @@ -138,7 +136,7 @@ impl Pallet { let data = f.encode_input(args).map_err(|_| Error::::EVMAbiEncode)?; let gas_limit = 300_000; let value = U256::zero(); - let info = Self::evm_call(Self::address(), bsm, value, data, gas_limit)?; + let info = Self::evm_call(Self::pallet_evm_account(), bsm, value, data, gas_limit)?; let weight = Self::weight_from_call_info(&info); log::debug!( target: "evm", @@ -200,7 +198,7 @@ impl Pallet { /// # Parameters /// * `blueprint` - The service blueprint. /// * `blueprint_id` - The blueprint ID. - /// * `prefrences` - The operator preferences. + /// * `preferences` - The operator preferences. /// * `registration_args` - The registration arguments. /// * `value` - The value to be sent with the call. /// @@ -210,7 +208,7 @@ impl Pallet { pub fn on_register_hook( blueprint: &ServiceBlueprint, blueprint_id: u64, - prefrences: &OperatorPreferences, + preferences: &OperatorPreferences, registration_args: &[Field], value: BalanceOf, ) -> Result<(bool, Weight), DispatchErrorWithPostInfo> { @@ -238,7 +236,7 @@ impl Pallet { }, &[ Token::Uint(ethabi::Uint::from(blueprint_id)), - prefrences.to_ethabi(), + preferences.to_ethabi(), Token::Bytes(Field::encode_to_ethabi(registration_args)), ], value, @@ -253,7 +251,7 @@ impl Pallet { /// # Parameters /// * `blueprint` - The service blueprint. /// * `blueprint_id` - The blueprint ID. - /// * `prefrences` - The operator preferences. + /// * `preferences` - The operator preferences. /// /// # Returns /// * `Result<(bool, Weight), DispatchErrorWithPostInfo>` - A tuple containing a boolean @@ -261,7 +259,7 @@ impl Pallet { pub fn on_unregister_hook( blueprint: &ServiceBlueprint, blueprint_id: u64, - prefrences: &OperatorPreferences, + preferences: &OperatorPreferences, ) -> Result<(bool, Weight), DispatchErrorWithPostInfo> { #[allow(deprecated)] Self::dispatch_hook( @@ -280,7 +278,7 @@ impl Pallet { constant: None, state_mutability: StateMutability::NonPayable, }, - &[Token::Uint(ethabi::Uint::from(blueprint_id)), prefrences.to_ethabi()], + &[Token::Uint(ethabi::Uint::from(blueprint_id)), preferences.to_ethabi()], Zero::zero(), ) } @@ -292,7 +290,7 @@ impl Pallet { /// # Parameters /// * `blueprint` - The service blueprint. /// * `blueprint_id` - The blueprint ID. - /// * `prefrences` - The operator preferences. + /// * `preferences` - The operator preferences. /// /// # Returns /// @@ -301,7 +299,7 @@ impl Pallet { pub fn on_update_price_targets( blueprint: &ServiceBlueprint, blueprint_id: u64, - prefrences: &OperatorPreferences, + preferences: &OperatorPreferences, ) -> Result<(bool, Weight), DispatchErrorWithPostInfo> { #[allow(deprecated)] Self::dispatch_hook( @@ -320,7 +318,7 @@ impl Pallet { constant: None, state_mutability: StateMutability::Payable, }, - &[Token::Uint(ethabi::Uint::from(blueprint_id)), prefrences.to_ethabi()], + &[Token::Uint(ethabi::Uint::from(blueprint_id)), preferences.to_ethabi()], Zero::zero(), ) } @@ -333,7 +331,7 @@ impl Pallet { /// # Parameters /// * `blueprint` - The service blueprint. /// * `blueprint_id` - The blueprint ID. - /// * `prefrences` - The operator preferences. + /// * `preferences` - The operator preferences. /// * `request_id` - The request id. /// * `restaking_percent` - The restaking percent. /// @@ -343,7 +341,7 @@ impl Pallet { pub fn on_approve_hook( blueprint: &ServiceBlueprint, blueprint_id: u64, - prefrences: &OperatorPreferences, + preferences: &OperatorPreferences, request_id: u64, restaking_percent: u8, ) -> Result<(bool, Weight), DispatchErrorWithPostInfo> { @@ -376,7 +374,7 @@ impl Pallet { }, &[ Token::Uint(ethabi::Uint::from(blueprint_id)), - prefrences.to_ethabi(), + preferences.to_ethabi(), Token::Uint(ethabi::Uint::from(request_id)), Token::Uint(ethabi::Uint::from(restaking_percent)), ], @@ -391,7 +389,7 @@ impl Pallet { /// # Parameters /// * `blueprint` - The service blueprint. /// * `blueprint_id` - The blueprint ID. - /// * `prefrences` - The operator preferences. + /// * `preferences` - The operator preferences. /// * `request_id` - The request id. /// /// # Returns @@ -400,7 +398,7 @@ impl Pallet { pub fn on_reject_hook( blueprint: &ServiceBlueprint, blueprint_id: u64, - prefrences: &OperatorPreferences, + preferences: &OperatorPreferences, request_id: u64, ) -> Result<(bool, Weight), DispatchErrorWithPostInfo> { #[allow(deprecated)] @@ -427,7 +425,7 @@ impl Pallet { }, &[ Token::Uint(ethabi::Uint::from(blueprint_id)), - prefrences.to_ethabi(), + preferences.to_ethabi(), Token::Uint(ethabi::Uint::from(request_id)), ], Zero::zero(), @@ -462,7 +460,6 @@ impl Pallet { operators: &[OperatorPreferences], request_args: &[Field], permitted_callers: &[T::AccountId], - _assets: &[T::AssetId], ttl: BlockNumberFor, paymet_asset: Asset, value: BalanceOf, @@ -524,7 +521,6 @@ impl Pallet { }) .collect(), ), - // Token::Array(vec![]), Token::Uint(ethabi::Uint::from(ttl.into())), paymet_asset.to_ethabi(), Token::Uint(ethabi::Uint::from(value.using_encoded(U256::from_little_endian))), @@ -557,7 +553,6 @@ impl Pallet { service_id: u64, owner: &T::AccountId, permitted_callers: &[T::AccountId], - _assets: &[T::AssetId], ttl: BlockNumberFor, ) -> Result<(bool, Weight), DispatchErrorWithPostInfo> { #[allow(deprecated)] @@ -761,7 +756,7 @@ impl Pallet { /// * `service_id` - The service ID. /// * `job` - The job index. /// * `job_call_id` - The job call ID. - /// * `prefrences` - The operator preferences. + /// * `preferences` - The operator preferences. /// * `inputs` - The input fields. /// * `outputs` - The output fields. /// @@ -774,7 +769,7 @@ impl Pallet { service_id: u64, job: u8, job_call_id: u64, - prefrences: &OperatorPreferences, + preferences: &OperatorPreferences, inputs: &[Field], outputs: &[Field], ) -> Result<(bool, Weight), DispatchErrorWithPostInfo> { @@ -825,7 +820,7 @@ impl Pallet { Token::Uint(ethabi::Uint::from(service_id)), Token::Uint(ethabi::Uint::from(job)), Token::Uint(ethabi::Uint::from(job_call_id)), - prefrences.to_ethabi(), + preferences.to_ethabi(), Token::Bytes(Field::encode_to_ethabi(inputs)), Token::Bytes(Field::encode_to_ethabi(outputs)), ], @@ -833,6 +828,238 @@ impl Pallet { ) } + /// Checks if an operator can join a service instance by calling the blueprint's EVM contract. + /// + /// This function dispatches a call to the `canJoin` function of the service blueprint's manager contract + /// to determine if an operator is allowed to join a service instance. + /// + /// # Parameters + /// * `blueprint` - The service blueprint containing the contract details + /// * `blueprint_id` - The ID of the service blueprint + /// * `instance_id` - The ID of the service instance + /// * `operator` - The account ID of the operator trying to join + /// * `preferences` - The operator's preferences for joining the service + /// + /// # Returns + /// * `Result<(bool, Weight), DispatchErrorWithPostInfo>` - A tuple containing: + /// - A boolean indicating if the operator can join + /// - The weight of the EVM operation + pub fn can_join_hook( + blueprint: &ServiceBlueprint, + blueprint_id: u64, + instance_id: u64, + operator: &T::AccountId, + preferences: &OperatorPreferences, + ) -> Result<(bool, Weight), DispatchErrorWithPostInfo> { + #[allow(deprecated)] + Self::dispatch_hook( + blueprint, + Function { + name: String::from("canJoin"), + inputs: vec![ + ethabi::Param { + name: String::from("blueprintId"), + kind: ethabi::ParamType::Uint(64), + internal_type: None, + }, + ethabi::Param { + name: String::from("instanceId"), + kind: ethabi::ParamType::Uint(64), + internal_type: None, + }, + ethabi::Param { + name: String::from("operator"), + kind: ethabi::ParamType::Address, + internal_type: None, + }, + OperatorPreferences::to_ethabi_param(), + ], + outputs: Default::default(), + constant: None, + state_mutability: StateMutability::NonPayable, + }, + &[ + Token::Uint(ethabi::Uint::from(blueprint_id)), + Token::Uint(ethabi::Uint::from(instance_id)), + Token::Address(T::EvmAddressMapping::into_address(operator.clone())), + preferences.to_ethabi(), + ], + Zero::zero(), + ) + } + + /// Notifies the blueprint's EVM contract that an operator has joined a service instance. + /// + /// This function dispatches a call to the `onOperatorJoined` function of the service blueprint's + /// manager contract after an operator successfully joins a service instance. + /// + /// # Parameters + /// * `blueprint` - The service blueprint containing the contract details + /// * `blueprint_id` - The ID of the service blueprint + /// * `instance_id` - The ID of the service instance + /// * `operator` - The account ID of the operator that joined + /// * `preferences` - The operator's preferences used when joining + /// + /// # Returns + /// * `Result<(bool, Weight), DispatchErrorWithPostInfo>` - A tuple containing: + /// - A boolean indicating if the notification was successful + /// - The weight of the EVM operation + pub fn on_operator_joined_hook( + blueprint: &ServiceBlueprint, + blueprint_id: u64, + instance_id: u64, + operator: &T::AccountId, + preferences: &OperatorPreferences, + ) -> Result<(bool, Weight), DispatchErrorWithPostInfo> { + #[allow(deprecated)] + Self::dispatch_hook( + blueprint, + Function { + name: String::from("onOperatorJoined"), + inputs: vec![ + ethabi::Param { + name: String::from("blueprintId"), + kind: ethabi::ParamType::Uint(64), + internal_type: None, + }, + ethabi::Param { + name: String::from("instanceId"), + kind: ethabi::ParamType::Uint(64), + internal_type: None, + }, + ethabi::Param { + name: String::from("operator"), + kind: ethabi::ParamType::Address, + internal_type: None, + }, + OperatorPreferences::to_ethabi_param(), + ], + outputs: Default::default(), + constant: None, + state_mutability: StateMutability::NonPayable, + }, + &[ + Token::Uint(ethabi::Uint::from(blueprint_id)), + Token::Uint(ethabi::Uint::from(instance_id)), + Token::Address(T::EvmAddressMapping::into_address(operator.clone())), + preferences.to_ethabi(), + ], + Zero::zero(), + ) + } + + /// Checks if an operator can leave a service instance by calling the blueprint's EVM contract. + /// + /// This function dispatches a call to the `canLeave` function of the service blueprint's manager contract + /// to determine if an operator is allowed to leave a service instance. + /// + /// # Parameters + /// * `blueprint` - The service blueprint containing the contract details + /// * `blueprint_id` - The ID of the service blueprint + /// * `instance_id` - The ID of the service instance + /// * `operator` - The account ID of the operator trying to leave + /// + /// # Returns + /// * `Result<(bool, Weight), DispatchErrorWithPostInfo>` - A tuple containing: + /// - A boolean indicating if the operator can leave + /// - The weight of the EVM operation + pub fn can_leave_hook( + blueprint: &ServiceBlueprint, + blueprint_id: u64, + instance_id: u64, + operator: &T::AccountId, + ) -> Result<(bool, Weight), DispatchErrorWithPostInfo> { + #[allow(deprecated)] + Self::dispatch_hook( + blueprint, + Function { + name: String::from("canLeave"), + inputs: vec![ + ethabi::Param { + name: String::from("blueprintId"), + kind: ethabi::ParamType::Uint(64), + internal_type: None, + }, + ethabi::Param { + name: String::from("instanceId"), + kind: ethabi::ParamType::Uint(64), + internal_type: None, + }, + ethabi::Param { + name: String::from("operator"), + kind: ethabi::ParamType::Address, + internal_type: None, + }, + ], + outputs: Default::default(), + constant: None, + state_mutability: StateMutability::NonPayable, + }, + &[ + Token::Uint(ethabi::Uint::from(blueprint_id)), + Token::Uint(ethabi::Uint::from(instance_id)), + Token::Address(T::EvmAddressMapping::into_address(operator.clone())), + ], + Zero::zero(), + ) + } + + /// Notifies the blueprint's EVM contract that an operator has left a service instance. + /// + /// This function dispatches a call to the `onOperatorLeft` function of the service blueprint's + /// manager contract after an operator successfully leaves a service instance. + /// + /// # Parameters + /// * `blueprint` - The service blueprint containing the contract details + /// * `blueprint_id` - The ID of the service blueprint + /// * `instance_id` - The ID of the service instance + /// * `operator` - The account ID of the operator that left + /// + /// # Returns + /// * `Result<(bool, Weight), DispatchErrorWithPostInfo>` - A tuple containing: + /// - A boolean indicating if the notification was successful + /// - The weight of the EVM operation + pub fn on_operator_left_hook( + blueprint: &ServiceBlueprint, + blueprint_id: u64, + instance_id: u64, + operator: &T::AccountId, + ) -> Result<(bool, Weight), DispatchErrorWithPostInfo> { + #[allow(deprecated)] + Self::dispatch_hook( + blueprint, + Function { + name: String::from("onOperatorLeft"), + inputs: vec![ + ethabi::Param { + name: String::from("blueprintId"), + kind: ethabi::ParamType::Uint(64), + internal_type: None, + }, + ethabi::Param { + name: String::from("instanceId"), + kind: ethabi::ParamType::Uint(64), + internal_type: None, + }, + ethabi::Param { + name: String::from("operator"), + kind: ethabi::ParamType::Address, + internal_type: None, + }, + ], + outputs: Default::default(), + constant: None, + state_mutability: StateMutability::NonPayable, + }, + &[ + Token::Uint(ethabi::Uint::from(blueprint_id)), + Token::Uint(ethabi::Uint::from(instance_id)), + Token::Address(T::EvmAddressMapping::into_address(operator.clone())), + ], + Zero::zero(), + ) + } + /// Queries the slashing origin of a service. /// /// This function performs an EVM call to the `querySlashingOrigin` function of the @@ -1051,7 +1278,8 @@ impl Pallet { log::debug!(target: "evm", "Dispatching EVM call(0x{}): {}", hex::encode(transfer_fn.short_signature()), transfer_fn.signature()); let data = transfer_fn.encode_input(&args).map_err(|_| Error::::EVMAbiEncode)?; let gas_limit = 300_000; - let info = Self::evm_call(Self::address(), erc20, U256::zero(), data, gas_limit)?; + let info = + Self::evm_call(Self::pallet_evm_account(), erc20, U256::zero(), data, gas_limit)?; let weight = Self::weight_from_call_info(&info); // decode the result and return it @@ -1092,9 +1320,9 @@ impl Pallet { ) -> Result<(fp_evm::CallInfo, Weight), DispatchErrorWithPostInfo> { log::debug!(target: "evm", "Dispatching EVM call(0x{}): {}", hex::encode(f.short_signature()), f.signature()); let data = f.encode_input(args).map_err(|_| Error::::EVMAbiEncode)?; - let gas_limit = 300_000; + let gas_limit = 1_000_000; let value = value.using_encoded(U256::from_little_endian); - let info = Self::evm_call(Self::address(), contract, value, data, gas_limit)?; + let info = Self::evm_call(Self::pallet_evm_account(), contract, value, data, gas_limit)?; let weight = Self::weight_from_call_info(&info); Ok((info, weight)) } diff --git a/pallets/services/src/functions/membership.rs b/pallets/services/src/functions/membership.rs new file mode 100644 index 000000000..234e8dc05 --- /dev/null +++ b/pallets/services/src/functions/membership.rs @@ -0,0 +1,143 @@ +use crate::{Config, Error, Instances, Pallet}; +use frame_support::pallet_prelude::*; +use sp_runtime::Percent; +use sp_std::vec::Vec; +use tangle_primitives::services::{ + AssetSecurityCommitment, MembershipModel, OperatorPreferences, ServiceBlueprint, +}; + +impl Pallet { + /// Implementation of join_service extrinsic + pub(crate) fn do_join_service( + blueprint: &ServiceBlueprint, + blueprint_id: u64, + instance_id: u64, + operator: &T::AccountId, + preferences: &OperatorPreferences, + native_asset_exposure: Percent, + non_native_asset_exposures: Vec>, + ) -> DispatchResult { + // Get service instance + let instance = Instances::::get(instance_id)?; + + // Validate membership model + match instance.membership_model { + MembershipModel::Fixed { .. } => { + return Err(Error::::DynamicMembershipNotSupported.into()) + }, + MembershipModel::Dynamic { min_operators, max_operators } => { + ensure!(min_operators > 0, Error::::InvalidMinOperators); + // Check max operators if set + if let Some(max) = max_operators { + ensure!(min_operators < max, Error::::InvalidMinOperators); + ensure!( + instance.native_asset_security.len() < max as usize, + Error::::MaxOperatorsReached + ); + } + }, + } + + // Check if operator can join via blueprint contract + let (can_join, _) = + Self::can_join_hook(blueprint, blueprint_id, instance_id, operator, preferences) + .map_err(|e| { + log::error!("Can join hook failed: {:?}", e); + Error::::OnCanJoinFailure + })?; + ensure!(can_join, Error::::JoinRejected); + + // Add operator to instance + Instances::::try_mutate(instance_id, |maybe_instance| -> DispatchResult { + let instance = maybe_instance.as_mut().map_err(|e| { + log::error!("Service not found: {:?}", e); + Error::::ServiceNotFound + })?; + instance + .native_asset_security + .try_push((operator.clone(), native_asset_exposure)) + .map_err(|e| { + log::error!("Failed to push native asset security: {:?}", e); + Error::::MaxOperatorsReached + })?; + instance + .non_native_asset_security + .try_push(( + operator.clone(), + BoundedVec::try_from(non_native_asset_exposures.clone()).map_err(|e| { + log::error!("Failed to convert non-native asset exposures: {:?}", e); + Error::::MaxOperatorsReached + })?, + )) + .map_err(|e| { + log::error!("Failed to push non-native asset security: {:?}", e); + Error::::MaxOperatorsReached + })?; + + Ok(()) + })?; + + // Notify blueprint + Self::on_operator_joined_hook(blueprint, blueprint_id, instance_id, operator, preferences) + .map_err(|e| { + log::error!("Operator joined hook failed: {:?}", e); + Error::::OnOperatorJoinFailure + })?; + + Ok(()) + } + + /// Implementation of leave_service extrinsic + pub(crate) fn do_leave_service( + blueprint: &ServiceBlueprint, + blueprint_id: u64, + instance_id: u64, + operator: &T::AccountId, + ) -> DispatchResult { + // Get service instance + let instance = Instances::::get(instance_id)?; + + // Validate membership model + match instance.membership_model { + MembershipModel::Fixed { .. } => { + return Err(Error::::DynamicMembershipNotSupported.into()) + }, + MembershipModel::Dynamic { min_operators, .. } => { + // Ensure minimum operators maintained + ensure!( + instance.native_asset_security.len() > min_operators as usize, + Error::::InsufficientOperators + ); + }, + } + + // Check if operator can leave via blueprint contract + let (can_leave, _) = Self::can_leave_hook(blueprint, blueprint_id, instance_id, operator) + .map_err(|e| { + log::error!("Can leave hook failed: {:?}", e); + Error::::OnCanLeaveFailure + })?; + ensure!(can_leave, Error::::LeaveRejected); + + // Remove operator from instance + Instances::::try_mutate(instance_id, |maybe_instance| -> DispatchResult { + let instance = maybe_instance.as_mut().map_err(|e| { + log::error!("Service not found: {:?}", e); + Error::::ServiceNotFound + })?; + instance.native_asset_security.retain(|(op, _)| op != operator); + instance.non_native_asset_security.retain(|(op, _)| op != operator); + Ok(()) + })?; + + // Notify blueprint + Self::on_operator_left_hook(blueprint, blueprint_id, instance_id, operator).map_err( + |e| { + log::error!("Operator left hook failed: {:?}", e); + Error::::OnOperatorLeaveFailure + }, + )?; + + Ok(()) + } +} diff --git a/pallets/services/src/functions/mod.rs b/pallets/services/src/functions/mod.rs new file mode 100644 index 000000000..2c2f7eb72 --- /dev/null +++ b/pallets/services/src/functions/mod.rs @@ -0,0 +1,7 @@ +pub mod approve; +pub mod evm_hooks; +pub mod membership; +pub mod register; +pub mod reject; +pub mod request; +pub mod slash; diff --git a/pallets/services/src/functions/register.rs b/pallets/services/src/functions/register.rs new file mode 100644 index 000000000..d0ee03655 --- /dev/null +++ b/pallets/services/src/functions/register.rs @@ -0,0 +1,101 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use crate::{BalanceOf, Config, Error, Event, Operators, OperatorsProfile, Pallet}; +use frame_support::{ + dispatch::DispatchResult, + pallet_prelude::*, + traits::{Currency, ExistenceRequirement}, +}; +use sp_std::vec::Vec; +use tangle_primitives::{ + services::{Field, OperatorPreferences, OperatorProfile}, + traits::MultiAssetDelegationInfo, +}; + +impl Pallet { + pub fn do_register( + operator: &T::AccountId, + blueprint_id: u64, + preferences: OperatorPreferences, + registration_args: Vec>, + value: BalanceOf, + ) -> DispatchResult { + let (_, blueprint) = Self::blueprints(blueprint_id)?; + + ensure!( + T::OperatorDelegationManager::is_operator_active(&operator), + Error::::OperatorNotActive + ); + + let already_registered = Operators::::contains_key(blueprint_id, &operator); + ensure!(!already_registered, Error::::AlreadyRegistered); + blueprint + .type_check_registration(®istration_args) + .map_err(Error::::TypeCheck)?; + + // Transfer the registration value to the pallet + T::Currency::transfer( + &operator, + &Self::pallet_account(), + value, + ExistenceRequirement::KeepAlive, + )?; + + let (allowed, _weight) = Self::on_register_hook( + &blueprint, + blueprint_id, + &preferences, + ®istration_args, + value, + ) + .map_err(|e| { + log::error!("Error in on_register_hook: {:?}", e); + Error::::OnRegisterHookFailed + })?; + + ensure!(allowed, Error::::InvalidRegistrationInput); + + Operators::::insert(blueprint_id, &operator, preferences); + + OperatorsProfile::::try_mutate(&operator, |profile| { + match profile { + Ok(p) => { + p.blueprints + .try_insert(blueprint_id) + .map_err(|_| Error::::MaxServicesPerProviderExceeded)?; + }, + Err(_) => { + let mut blueprints = BoundedBTreeSet::new(); + blueprints + .try_insert(blueprint_id) + .map_err(|_| Error::::MaxServicesPerProviderExceeded)?; + *profile = Ok(OperatorProfile { blueprints, ..Default::default() }); + }, + }; + Result::<_, Error>::Ok(()) + })?; + + Self::deposit_event(Event::Registered { + provider: operator.clone(), + blueprint_id, + preferences, + registration_args, + }); + + Ok(()) + } +} diff --git a/pallets/services/src/functions/reject.rs b/pallets/services/src/functions/reject.rs new file mode 100644 index 000000000..56be1e77d --- /dev/null +++ b/pallets/services/src/functions/reject.rs @@ -0,0 +1,117 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use crate::{Config, Error, Event, Pallet, StagingServicePayments}; +use frame_support::{ + pallet_prelude::*, + traits::{fungibles::Mutate, tokens::Preservation, Currency, ExistenceRequirement}, +}; +use sp_runtime::traits::Zero; +use tangle_primitives::services::{ApprovalState, Asset}; + +impl Pallet { + /// Process a rejection of a service request by an operator. + /// + /// This function handles the rejection workflow including: + /// - Updating the operator's approval state to rejected + /// - Refunding any staged payments + /// - Emitting appropriate events + /// + /// # Arguments + /// + /// * `operator` - The account ID of the operator rejecting the request + /// * `request_id` - The ID of the service request being rejected + /// + /// # Returns + /// + /// Returns a DispatchResult indicating success or the specific error that occurred + pub fn do_reject(operator: T::AccountId, request_id: u64) -> DispatchResult { + let mut request = Self::service_requests(request_id)?; + let updated = + request.operators_with_approval_state.iter_mut().find_map(|(v, ref mut s)| { + if v == &operator { + *s = ApprovalState::Rejected; + Some(()) + } else { + None + } + }); + + ensure!(updated.is_some(), Error::::ApprovalNotRequested); + + let blueprint_id = request.blueprint; + let (_, blueprint) = Self::blueprints(blueprint_id)?; + let prefs = Self::operators(blueprint_id, operator.clone())?; + + let (allowed, _weight) = Self::on_reject_hook(&blueprint, blueprint_id, &prefs, request_id) + .map_err(|_| Error::::OnRejectFailure)?; + ensure!(allowed, Error::::RejectionInterrupted); + + Self::deposit_event(Event::ServiceRequestRejected { + operator, + blueprint_id: request.blueprint, + request_id, + }); + + // Refund the payment if it exists + if let Some(payment) = Self::service_payment(request_id) { + match payment.asset { + Asset::Custom(asset_id) if asset_id == Zero::zero() => { + let refund_to = payment + .refund_to + .try_into_account_id() + .map_err(|_| Error::::ExpectedAccountId)?; + T::Currency::transfer( + &Self::pallet_account(), + &refund_to, + payment.amount, + ExistenceRequirement::AllowDeath, + )?; + }, + Asset::Custom(asset_id) => { + let refund_to = payment + .refund_to + .try_into_account_id() + .map_err(|_| Error::::ExpectedAccountId)?; + T::Fungibles::transfer( + asset_id, + &Self::pallet_account(), + &refund_to, + payment.amount, + Preservation::Expendable, + )?; + }, + Asset::Erc20(token) => { + let refund_to = payment + .refund_to + .try_into_address() + .map_err(|_| Error::::ExpectedEVMAddress)?; + let (success, _weight) = Self::erc20_transfer( + token, + Self::pallet_evm_account(), + refund_to, + payment.amount, + ) + .map_err(|_| Error::::OnErc20TransferFailure)?; + ensure!(success, Error::::ERC20TransferFailed); + }, + } + StagingServicePayments::::remove(request_id); + } + + Ok(()) + } +} diff --git a/pallets/services/src/functions/request.rs b/pallets/services/src/functions/request.rs new file mode 100644 index 000000000..a7fb69b7e --- /dev/null +++ b/pallets/services/src/functions/request.rs @@ -0,0 +1,192 @@ +use crate::{ + BalanceOf, Config, Error, Event, MaxAssetsPerServiceOf, MaxFieldsOf, MaxOperatorsPerServiceOf, + MaxPermittedCallersOf, NextServiceRequestId, Pallet, ServiceRequests, StagingServicePayments, +}; +use frame_support::{ + pallet_prelude::*, + traits::{fungibles::Mutate, tokens::Preservation, Currency, ExistenceRequirement}, + BoundedVec, +}; +use frame_system::pallet_prelude::*; +use sp_core::H160; +use sp_runtime::traits::Zero; +use sp_std::vec::Vec; +use tangle_primitives::{ + services::{ + ApprovalState, Asset, AssetSecurityRequirement, EvmAddressMapping, Field, MembershipModel, + ServiceRequest, StagingServicePayment, + }, + Account, +}; + +impl Pallet { + /// Request a new service using a blueprint and specified operators. + /// + /// # Arguments + /// + /// * `caller` - The account requesting the service + /// * `evm_origin` - Optional EVM address for ERC20 payments + /// * `blueprint_id` - The identifier of the blueprint to use + /// * `permitted_callers` - Accounts allowed to call the service + /// * `operators` - List of operators that will run the service + /// * `request_args` - Blueprint initialization arguments + /// * `assets` - Required assets for the service + /// * `ttl` - Time-to-live in blocks for the service request + /// * `payment_asset` - Asset used for payment (native, custom or ERC20) + /// * `value` - Payment amount for the service + pub(crate) fn do_request( + caller: T::AccountId, + evm_origin: Option, + blueprint_id: u64, + permitted_callers: Vec, + operators: Vec, + request_args: Vec>, + asset_security_requirements: Vec>, + ttl: BlockNumberFor, + payment_asset: Asset, + value: BalanceOf, + membership_model: MembershipModel, + ) -> Result { + let (_, blueprint) = Self::blueprints(blueprint_id)?; + + blueprint.type_check_request(&request_args).map_err(Error::::TypeCheck)?; + // ensure we at least have one asset and all assets are unique + ensure!(!asset_security_requirements.is_empty(), Error::::NoAssetsProvided); + ensure!( + asset_security_requirements + .iter() + .map(|req| &req.asset) + .collect::>() + .len() == asset_security_requirements.len(), + Error::::DuplicateAsset + ); + + let assets = asset_security_requirements + .clone() + .into_iter() + .map(|req| req.asset) + .collect::>(); + let bounded_requirements = BoundedVec::try_from(asset_security_requirements) + .map_err(|_| Error::::MaxAssetsPerServiceExceeded)?; + + let mut preferences = Vec::new(); + let mut pending_approvals = Vec::new(); + for provider in &operators { + let prefs = Self::operators(blueprint_id, provider)?; + pending_approvals.push(provider.clone()); + preferences.push(prefs); + } + + let mut native_value = Zero::zero(); + let request_id = Self::next_service_request_id(); + + if value != Zero::zero() { + // Payment transfer + let refund_to = match payment_asset.clone() { + // Handle the case of native currency. + Asset::Custom(asset_id) if asset_id == Zero::zero() => { + T::Currency::transfer( + &caller, + &Self::pallet_account(), + value, + ExistenceRequirement::KeepAlive, + )?; + native_value = value; + Account::id(caller.clone()) + }, + Asset::Custom(asset_id) => { + T::Fungibles::transfer( + asset_id, + &caller, + &Self::pallet_account(), + value, + Preservation::Preserve, + )?; + Account::id(caller.clone()) + }, + Asset::Erc20(token) => { + // origin check. + let evm_origin = evm_origin.ok_or(Error::::MissingEVMOrigin)?; + let mapped_origin = T::EvmAddressMapping::into_account_id(evm_origin); + ensure!(mapped_origin == caller, DispatchError::BadOrigin); + let (success, _weight) = + Self::erc20_transfer(token, evm_origin, Self::pallet_evm_account(), value) + .map_err(|_| Error::::OnErc20TransferFailure)?; + ensure!(success, Error::::ERC20TransferFailed); + Account::from(evm_origin) + }, + }; + + // Save the payment information for the service request. + let payment = StagingServicePayment { + request_id, + refund_to, + asset: payment_asset.clone(), + amount: value, + }; + + StagingServicePayments::::insert(request_id, payment); + } + + let (allowed, _weight) = Self::on_request_hook( + &blueprint, + blueprint_id, + &caller, + request_id, + &preferences, + &request_args, + &permitted_callers, + ttl, + payment_asset, + value, + native_value, + ) + .map_err(|_| Error::::OnRequestFailure)?; + + ensure!(allowed, Error::::InvalidRequestInput); + + let permitted_callers = + BoundedVec::<_, MaxPermittedCallersOf>::try_from(permitted_callers) + .map_err(|_| Error::::MaxPermittedCallersExceeded)?; + let asset_security = BoundedVec::<_, MaxAssetsPerServiceOf>::try_from(assets.clone()) + .map_err(|_| Error::::MaxAssetsPerServiceExceeded)?; + let operators = pending_approvals + .iter() + .cloned() + .map(|v| (v, ApprovalState::Pending)) + .collect::>(); + + let args = BoundedVec::<_, MaxFieldsOf>::try_from(request_args) + .map_err(|_| Error::::MaxFieldsExceeded)?; + + let operators_with_approval_state = + BoundedVec::<_, MaxOperatorsPerServiceOf>::try_from(operators) + .map_err(|_| Error::::MaxServiceProvidersExceeded)?; + + let service_request = ServiceRequest { + blueprint: blueprint_id, + owner: caller.clone(), + non_native_asset_security: bounded_requirements, + ttl, + args, + permitted_callers, + operators_with_approval_state, + membership_model, + }; + + ensure!(allowed, Error::::InvalidRequestInput); + ServiceRequests::::insert(request_id, service_request); + NextServiceRequestId::::set(request_id.saturating_add(1)); + + Self::deposit_event(Event::ServiceRequested { + owner: caller, + request_id, + blueprint_id, + pending_approvals, + approved: Default::default(), + asset_security: asset_security.into_iter().map(|asset| (asset, Vec::new())).collect(), + }); + + Ok(request_id) + } +} diff --git a/pallets/services/src/functions/slash.rs b/pallets/services/src/functions/slash.rs new file mode 100644 index 000000000..fd7b5234b --- /dev/null +++ b/pallets/services/src/functions/slash.rs @@ -0,0 +1,195 @@ +use crate::{types::BalanceOf, Config, Error, Event, Pallet}; +use frame_support::{ + ensure, + pallet_prelude::*, + traits::{ + fungibles::Mutate, tokens::Preservation, Currency, ExistenceRequirement, Get, + ReservableCurrency, + }, +}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_runtime::{traits::Zero, Percent}; +use sp_std::{vec, vec::Vec}; +use tangle_primitives::{ + services::{Asset, EvmAddressMapping, Service, UnappliedSlash}, + traits::{MultiAssetDelegationInfo, SlashManager}, +}; + +impl Pallet { + /// Calculates and creates an unapplied slash for an operator and their delegators. + /// + /// This function: + /// 1. Calculates the operator's native currency slash based on their exposure + /// 2. For each asset required by the service: + /// - Identifies delegators who selected this blueprint + /// - Calculates slashes based on delegator exposure and asset requirements + /// 3. Creates an UnappliedSlash record for later processing + /// + /// # Arguments + /// * `reporter` - The account ID of the reporter + /// * `service` - The service instance where the slash occurred + /// * `offender` - The operator being slashed + /// * `slash_percent` - The percentage of exposed stake to slash + pub(crate) fn calculate_slash( + reporter: &T::AccountId, + service: &Service, T::AssetId>, + offender: &T::AccountId, + slash_percent: Percent, + ) -> Result, T::AssetId>, DispatchError> { + // Get operator's total stake and calculate their native currency slash + let total_stake = T::OperatorDelegationManager::get_operator_stake(offender); + + // Find operator's exposure percentage for this service + let operator_exposure = service + .native_asset_security + .iter() + .find(|(op, _)| op == offender) + .map(|(_, exposure)| *exposure) + .ok_or(Error::::OffenderNotOperator)?; + + // Calculate operator's own slash in native currency + let exposed_stake = operator_exposure.mul_floor(total_stake); + let own_slash = slash_percent.mul_floor(exposed_stake); + + // Get all delegators for this operator and filter by blueprint selection upfront + let all_delegators = T::OperatorDelegationManager::get_delegators_for_operator(offender); + let eligible_delegators: Vec<_> = all_delegators + .into_iter() + .filter(|(delegator, _, _)| { + T::OperatorDelegationManager::has_delegator_selected_blueprint( + delegator, + offender, + service.blueprint, + ) + }) + .collect(); + + // Get the asset commitments for the offending operator + let offender_commitments = service + .non_native_asset_security + .iter() + .find(|(op, _)| op == offender) + .map(|(_, commitments)| commitments) + .ok_or(Error::::OffenderNotOperator)?; + + // Calculate delegator slashes per asset + let mut delegator_slashes = Vec::new(); + + // For each asset commitment of the offending operator + for commitment in offender_commitments { + let asset = &commitment.asset; + let asset_exposure = commitment.exposure_percent; + + // Process only delegators who delegated this specific asset + let asset_delegators = eligible_delegators.iter().filter(|(_, _, delegator_asset)| { + match (delegator_asset, asset) { + (Asset::Custom(d_asset), Asset::Custom(s_asset)) => d_asset == s_asset, + (Asset::Erc20(d_asset), Asset::Erc20(s_asset)) => d_asset.0 == s_asset.0, + _ => false, + } + }); + + // Calculate slashes for eligible delegators + for (delegator, stake, _) in asset_delegators { + let exposed_delegation = asset_exposure.mul_floor(*stake); + let slash_amount = slash_percent.mul_floor(exposed_delegation); + + if !slash_amount.is_zero() { + delegator_slashes.push((delegator.clone(), asset.clone(), slash_amount)); + } + } + } + + Ok(UnappliedSlash { + era: T::OperatorDelegationManager::get_current_round(), + blueprint_id: service.blueprint, + service_id: service.id, + operator: offender.clone(), + own: own_slash, + others: delegator_slashes, + reporters: vec![reporter.clone()], + }) + } + + /// Slash an operator and their delegators for a specific service instance. + /// The slashing is applied according to the operator's security commitments in the service. + /// + /// # Arguments + /// * `unapplied_slash` - The unapplied slash record to apply + pub fn apply_slash( + unapplied_slash: UnappliedSlash, T::AssetId>, + ) -> Result { + let mut weight: Weight = Weight::zero(); + // Notify the multi-asset delegation system about the operator slash + // This call will also slash all delegators for the operator. Note, that + // the `SlashManager`` is solely responsible for updating the storage of + // the operator and delegators. The asset transfers are handled by this function. + let slash_operator_weight = T::SlashManager::slash_operator(&unapplied_slash)?; + weight += slash_operator_weight; + + // Transfer native slashed amount to treasury + T::Currency::unreserve(&unapplied_slash.operator, unapplied_slash.own); + weight += T::DbWeight::get().reads(1_u64); + weight += T::DbWeight::get().writes(1_u64); + + T::Currency::transfer( + &unapplied_slash.operator, + &T::SlashRecipient::get(), + unapplied_slash.own, + ExistenceRequirement::AllowDeath, + )?; + weight += T::DbWeight::get().reads(1_u64); + weight += T::DbWeight::get().writes(1_u64); + + // Emit event for native token slash + Self::deposit_event(Event::OperatorSlashed { + operator: unapplied_slash.operator.clone(), + amount: unapplied_slash.own, + service_id: unapplied_slash.service_id, + blueprint_id: unapplied_slash.blueprint_id, + era: unapplied_slash.era, + }); + + // Process all delegator slashes + for (delegator, asset, slash_amount) in unapplied_slash.clone().others { + // Transfer slashed assets to treasury + match asset { + Asset::Custom(asset_id) => { + T::Fungibles::transfer( + asset_id, + &Self::pallet_account(), + &T::SlashRecipient::get(), + slash_amount, + Preservation::Expendable, + )?; + weight += T::DbWeight::get().reads(1_u64); + weight += T::DbWeight::get().writes(1_u64); + }, + Asset::Erc20(token) => { + let treasury_evm = T::EvmAddressMapping::into_address(T::SlashRecipient::get()); + let (success, _) = Self::erc20_transfer( + token, + Self::pallet_evm_account(), + treasury_evm, + slash_amount, + ) + .map_err(|_| Error::::ERC20TransferFailed)?; + ensure!(success, Error::::ERC20TransferFailed); + weight += T::DbWeight::get().reads(1_u64); + weight += T::DbWeight::get().writes(1_u64); + }, + } + + // Emit event for delegator slash + Self::deposit_event(Event::DelegatorSlashed { + delegator: delegator.clone(), + amount: slash_amount, + service_id: unapplied_slash.service_id, + blueprint_id: unapplied_slash.blueprint_id, + era: unapplied_slash.era, + }); + } + + Ok(weight) + } +} diff --git a/pallets/services/src/impls.rs b/pallets/services/src/impls.rs index e4b1f5649..d0f3cd2b3 100644 --- a/pallets/services/src/impls.rs +++ b/pallets/services/src/impls.rs @@ -1,10 +1,6 @@ use super::*; use crate::types::BalanceOf; -#[cfg(not(feature = "std"))] -use alloc::vec::Vec; -use sp_std::vec; -#[cfg(feature = "std")] -use std::vec::Vec; +use sp_std::{vec, vec::Vec}; use tangle_primitives::{services::Constraints, traits::ServiceManager, BlueprintId}; impl Constraints for types::ConstraintsOf { @@ -73,17 +69,19 @@ impl ServiceManager> for crate::Pal } #[cfg(feature = "runtime-benchmarks")] -pub struct BenchmarkingOperatorDelegationManager( - core::marker::PhantomData<(T, Balance, AssetId)>, +pub struct BenchmarkingOperatorDelegationManager( + core::marker::PhantomData<(T, Balance)>, ); #[cfg(feature = "runtime-benchmarks")] -impl - tangle_primitives::traits::MultiAssetDelegationInfo - for BenchmarkingOperatorDelegationManager +impl + tangle_primitives::traits::MultiAssetDelegationInfo< + T::AccountId, + Balance, + BlockNumberFor, + T::AssetId, + > for BenchmarkingOperatorDelegationManager { - type AssetId = AssetId; - fn get_current_round() -> tangle_primitives::types::RoundIndex { Default::default() } @@ -100,10 +98,28 @@ impl Default::default() } - fn get_total_delegation_by_asset_id( - _operator: &T::AccountId, - _asset_id: &Self::AssetId, - ) -> Balance { + fn get_total_delegation_by_asset_id(_operator: &T::AccountId, _asset_id: &AssetId) -> Balance { Default::default() } + + fn get_delegators_for_operator( + _operator: &T::AccountId, + ) -> Vec<(T::AccountId, Balance, Asset)> { + Vec::new() + } + + fn has_delegator_selected_blueprint( + _delegator: &T::AccountId, + _operator: &T::AccountId, + _blueprint_id: BlueprintId, + ) -> bool { + true // For benchmarking, always return true + } + + fn get_user_deposit_with_locks( + _who: &T::AccountId, + _asset_id: Asset, + ) -> Option>> { + None + } } diff --git a/pallets/services/src/lib.rs b/pallets/services/src/lib.rs index c1693a1cd..d9e959206 100644 --- a/pallets/services/src/lib.rs +++ b/pallets/services/src/lib.rs @@ -22,16 +22,24 @@ extern crate alloc; use frame_support::{ pallet_prelude::*, - traits::{Currency, ExistenceRequirement, ReservableCurrency}, + traits::{Currency, ReservableCurrency}, }; use frame_system::pallet_prelude::*; -use sp_runtime::{traits::Get, DispatchResult}; -use tangle_primitives::traits::MultiAssetDelegationInfo; +use sp_runtime::{ + traits::{Get, Zero}, + DispatchResult, +}; +use tangle_primitives::{ + services::{AssetSecurityCommitment, AssetSecurityRequirement, MembershipModel}, + traits::MultiAssetDelegationInfo, + BlueprintId, InstanceId, JobCallId, ServiceRequestId, +}; -mod functions; +pub mod functions; mod impls; mod rpc; pub mod types; +use types::*; #[cfg(test)] mod mock; @@ -46,7 +54,6 @@ mod benchmarking; pub mod weights; pub use module::*; -use tangle_primitives::BlueprintId; pub use weights::WeightInfo; #[cfg(feature = "runtime-benchmarks")] @@ -58,22 +65,13 @@ pub mod module { use super::*; use frame_support::{ dispatch::PostDispatchInfo, - traits::{ - fungibles::{Inspect, Mutate}, - tokens::Preservation, - }, + traits::fungibles::{Inspect, Mutate}, + PalletId, }; use sp_core::H160; - use sp_runtime::{ - traits::{AtLeast32BitUnsigned, MaybeSerializeDeserialize, Zero}, - Percent, - }; + use sp_runtime::{traits::MaybeSerializeDeserialize, Percent}; use sp_std::vec::Vec; - use tangle_primitives::{ - services::{MasterBlueprintServiceManagerRevision, *}, - Account, - }; - use types::*; + use tangle_primitives::services::*; #[pallet::config] pub trait Config: frame_system::Config { @@ -87,9 +85,13 @@ pub mod module { type Fungibles: Inspect> + Mutate; - /// `Pallet` EVM Address. + /// PalletId used for deriving the AccountId and EVM address. + /// This account receives slashed assets upon slash event processing. #[pallet::constant] - type PalletEVMAddress: Get; + type PalletId: Get; + + #[pallet::constant] + type SlashRecipient: Get; /// A type that implements the `EvmRunner` trait for the execution of EVM /// transactions. @@ -103,14 +105,7 @@ pub mod module { type EvmAddressMapping: tangle_primitives::services::EvmAddressMapping; /// The asset ID type. - type AssetId: AtLeast32BitUnsigned - + Parameter - + Member - + MaybeSerializeDeserialize - + Clone - + Copy - + PartialOrd - + MaxEncodedLen; + type AssetId: AssetIdT; /// Maximum number of fields in a job call. #[pallet::constant] @@ -188,6 +183,14 @@ pub mod module { Self::AccountId, BalanceOf, BlockNumberFor, + Self::AssetId, + >; + + /// Manager for slashing that dispatches slash operations to `pallet-multi-asset-delegation`. + type SlashManager: tangle_primitives::traits::SlashManager< + Self::AccountId, + BalanceOf, + Self::AssetId, >; /// Number of eras that slashes are deferred by, after computation. @@ -200,6 +203,10 @@ pub mod module { /// The origin which can manage Add a new Master Blueprint Service Manager revision. type MasterBlueprintServiceManagerUpdateOrigin: EnsureOrigin; + /// The minimum percentage of native token stake that operators must expose for slashing. + #[pallet::constant] + type NativeExposureMinimum: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Weight information for the extrinsics in this module. type WeightInfo: WeightInfo; } @@ -208,10 +215,41 @@ pub mod module { impl Hooks> for Pallet { fn integrity_test() { // Ensure that the pallet's configuration is valid. - // 1. Make sure that pallet's associated AccountId value maps correctly to the EVM - // address. - let account_id = T::EvmAddressMapping::into_account_id(Self::address()); - assert_eq!(account_id, Self::account_id(), "Services: AccountId mapping is incorrect."); + // 1. Make sure that pallet's substrate address maps correctly back to the EVM address + let evm_address = T::EvmAddressMapping::into_address(Self::pallet_account()); + assert_eq!( + evm_address, + Self::pallet_evm_account(), + "Services: EVM address mapping is incorrect." + ); + } + + /// On initialize, we should check for any unapplied slashes and apply them. + fn on_initialize(_n: BlockNumberFor) -> Weight { + let mut weight = Zero::zero(); + let current_era = T::OperatorDelegationManager::get_current_round(); + let slash_defer_duration = T::SlashDeferDuration::get(); + + // Only process slashes from eras that have completed their deferral period + let process_era = current_era.saturating_sub(slash_defer_duration); + + // Get all unapplied slashes for this era + let prefix_iter = UnappliedSlashes::::iter_prefix(process_era); + for (index, slash) in prefix_iter { + // TODO: This call must be all or nothing. + // TODO: If fail then revert all storage changes + match Self::apply_slash(slash) { + Ok(weight_used) => { + weight = weight_used.checked_add(&weight).unwrap_or_else(Zero::zero); + // Remove the slash from storage after successful application + UnappliedSlashes::::remove(process_era, index); + }, + Err(_) => { + log::error!("Failed to apply slash for index: {:?}", index); + }, + } + } + weight } } @@ -281,8 +319,12 @@ pub mod module { OperatorNotActive, /// No assets provided for the service, at least one asset is required. NoAssetsProvided, + /// Duplicate assets provided + DuplicateAsset, /// The maximum number of assets per service has been exceeded. MaxAssetsPerServiceExceeded, + /// Assets don't match + InvalidAssetMatching, /// Offender is not a registered operator. OffenderNotOperator, /// Offender is not an active operator. @@ -305,6 +347,42 @@ pub mod module { ExpectedEVMAddress, /// Expected the account to be an account ID. ExpectedAccountId, + /// Request hook failure + OnRequestFailure, + /// Register hook failure + OnRegisterHookFailed, + /// ERC20 transfer hook failure + OnErc20TransferFailure, + /// Approve service request hook failure + OnApproveFailure, + /// Reject service request hook failure + OnRejectFailure, + /// Service init hook + OnServiceInitHook, + /// Membership model not supported by blueprint + UnsupportedMembershipModel, + /// Service does not support dynamic membership + DynamicMembershipNotSupported, + /// Cannot join service - rejected by blueprint + JoinRejected, + /// Cannot leave service - rejected by blueprint + LeaveRejected, + /// Invalid minimum # of operators (zero) or greater than max + InvalidMinOperators, + /// Maximum operators reached + MaxOperatorsReached, + /// Insufficient # of operators + InsufficientOperators, + /// Can join hook failure + OnCanJoinFailure, + /// Can leave hook failure + OnCanLeaveFailure, + /// Operator join hook failure + OnOperatorJoinFailure, + /// Operator leave hook failure + OnOperatorLeaveFailure, + /// Operator is a member or has already joined the service + AlreadyJoined, } #[pallet::event] @@ -363,10 +441,10 @@ pub mod module { blueprint_id: u64, /// The list of operators that need to approve the service. pending_approvals: Vec, - /// The list of operators that atomaticaly approved the service. + /// The list of operators that automatically approved the service. approved: Vec, - /// The list of asset IDs that are being used to secure the service. - assets: Vec, + /// The list of asset security requirements for the service. + asset_security: Vec<(Asset, Vec<(T::AccountId, Percent)>)>, }, /// A service request has been approved. ServiceRequestApproved { @@ -400,8 +478,8 @@ pub mod module { service_id: u64, /// The ID of the service blueprint. blueprint_id: u64, - /// The list of asset IDs that are being used to secure the service. - assets: Vec, + /// The list of assets that are being used to secure the service. + assets: Vec>, }, /// A service has been terminated. @@ -473,7 +551,32 @@ pub mod module { /// Era index era: u32, }, - + /// An Operator has been slashed. + OperatorSlashed { + /// The account that has been slashed. + operator: T::AccountId, + /// The amount of the slash. + amount: BalanceOf, + /// Service ID + service_id: u64, + /// Blueprint ID + blueprint_id: u64, + /// Era index + era: u32, + }, + /// A Delegator has been slashed. + DelegatorSlashed { + /// The account that has been slashed. + delegator: T::AccountId, + /// The amount of the slash. + amount: BalanceOf, + /// Service ID + service_id: u64, + /// Blueprint ID + blueprint_id: u64, + /// Era index + era: u32, + }, /// The Master Blueprint Service Manager has been revised. MasterBlueprintServiceManagerRevised { /// The revision number of the Master Blueprint Service Manager. @@ -496,17 +599,17 @@ pub mod module { /// The next free ID for a service request. #[pallet::storage] #[pallet::getter(fn next_service_request_id)] - pub type NextServiceRequestId = StorageValue<_, u64, ValueQuery>; + pub type NextServiceRequestId = StorageValue<_, ServiceRequestId, ValueQuery>; /// The next free ID for a service Instance. #[pallet::storage] #[pallet::getter(fn next_instance_id)] - pub type NextInstanceId = StorageValue<_, u64, ValueQuery>; + pub type NextInstanceId = StorageValue<_, InstanceId, ValueQuery>; /// The next free ID for a service call. #[pallet::storage] #[pallet::getter(fn next_job_call_id)] - pub type NextJobCallId = StorageValue<_, u64, ValueQuery>; + pub type NextJobCallId = StorageValue<_, JobCallId, ValueQuery>; /// The next free ID for a unapplied slash. #[pallet::storage] @@ -614,7 +717,7 @@ pub mod module { u32, Identity, u32, - UnappliedSlash>, + UnappliedSlash, T::AssetId>, ResultQuery::UnappliedSlashNotFound>, >; @@ -702,7 +805,6 @@ pub mod module { let (allowed, _weight) = Self::on_blueprint_created_hook(&blueprint, blueprint_id, &owner)?; - ensure!(allowed, Error::::BlueprintCreationInterrupted); Blueprints::::insert(blueprint_id, (owner.clone(), blueprint)); @@ -794,64 +896,7 @@ pub mod module { #[pallet::compact] value: BalanceOf, ) -> DispatchResultWithPostInfo { let caller = ensure_signed(origin)?; - let (_, blueprint) = Self::blueprints(blueprint_id)?; - - ensure!( - T::OperatorDelegationManager::is_operator_active(&caller), - Error::::OperatorNotActive - ); - - let already_registered = Operators::::contains_key(blueprint_id, &caller); - ensure!(!already_registered, Error::::AlreadyRegistered); - blueprint - .type_check_registration(®istration_args) - .map_err(Error::::TypeCheck)?; - - // Transfer the registration value to the pallet - T::Currency::transfer( - &caller, - &Self::account_id(), - value, - ExistenceRequirement::KeepAlive, - )?; - - let (allowed, _weight) = Self::on_register_hook( - &blueprint, - blueprint_id, - &preferences, - ®istration_args, - value, - )?; - - ensure!(allowed, Error::::InvalidRegistrationInput); - - Operators::::insert(blueprint_id, &caller, preferences); - - OperatorsProfile::::try_mutate(&caller, |profile| { - match profile { - Ok(p) => { - p.blueprints - .try_insert(blueprint_id) - .map_err(|_| Error::::MaxServicesPerProviderExceeded)?; - }, - Err(_) => { - let mut blueprints = BoundedBTreeSet::new(); - blueprints - .try_insert(blueprint_id) - .map_err(|_| Error::::MaxServicesPerProviderExceeded)?; - *profile = Ok(OperatorProfile { blueprints, ..Default::default() }); - }, - }; - Result::<_, Error>::Ok(()) - })?; - - Self::deposit_event(Event::Registered { - provider: caller.clone(), - blueprint_id, - preferences, - registration_args, - }); - + Self::do_register(&caller, blueprint_id, preferences, registration_args, value)?; Ok(PostDispatchInfo { actual_weight: None, pays_fee: Pays::Yes }) } @@ -988,132 +1033,28 @@ pub mod module { permitted_callers: Vec, operators: Vec, request_args: Vec>, - assets: Vec, + asset_security_requirements: Vec>, #[pallet::compact] ttl: BlockNumberFor, payment_asset: Asset, #[pallet::compact] value: BalanceOf, + membership_model: MembershipModel, ) -> DispatchResultWithPostInfo { let caller = ensure_signed(origin)?; - let (_, blueprint) = Self::blueprints(blueprint_id)?; - - blueprint.type_check_request(&request_args).map_err(Error::::TypeCheck)?; - // ensure we at least have one asset - ensure!(!assets.is_empty(), Error::::NoAssetsProvided); - - let mut preferences = Vec::new(); - let mut pending_approvals = Vec::new(); - for provider in &operators { - let prefs = Self::operators(blueprint_id, provider)?; - pending_approvals.push(provider.clone()); - preferences.push(prefs); - } - - let mut native_value = Zero::zero(); - let request_id = NextServiceRequestId::::get(); - - if value != Zero::zero() { - // Payment transfer - let refund_to = match payment_asset { - // Handle the case of native currency. - Asset::Custom(asset_id) if asset_id == Zero::zero() => { - T::Currency::transfer( - &caller, - &Self::account_id(), - value, - ExistenceRequirement::KeepAlive, - )?; - native_value = value; - Account::id(caller.clone()) - }, - Asset::Custom(asset_id) => { - T::Fungibles::transfer( - asset_id, - &caller, - &Self::account_id(), - value, - Preservation::Preserve, - )?; - Account::id(caller.clone()) - }, - Asset::Erc20(token) => { - // origin check. - let evm_origin = evm_origin.ok_or(Error::::MissingEVMOrigin)?; - let mapped_origin = T::EvmAddressMapping::into_account_id(evm_origin); - ensure!(mapped_origin == caller, DispatchError::BadOrigin); - let (success, _weight) = - Self::erc20_transfer(token, evm_origin, Self::address(), value)?; - ensure!(success, Error::::ERC20TransferFailed); - Account::from(evm_origin) - }, - }; - // Save the payment information for the service request. - let payment = StagingServicePayment { - request_id, - refund_to, - asset: payment_asset, - amount: value, - }; - - StagingServicePayments::::insert(request_id, payment); - } - - let (allowed, _weight) = Self::on_request_hook( - &blueprint, + Self::do_request( + caller, + evm_origin, blueprint_id, - &caller, - request_id, - &preferences, - &request_args, - &permitted_callers, - &assets, + permitted_callers, + operators, + request_args, + asset_security_requirements, ttl, payment_asset, value, - native_value, + membership_model, )?; - let permitted_callers = - BoundedVec::<_, MaxPermittedCallersOf>::try_from(permitted_callers) - .map_err(|_| Error::::MaxPermittedCallersExceeded)?; - let assets = BoundedVec::<_, MaxAssetsPerServiceOf>::try_from(assets) - .map_err(|_| Error::::MaxAssetsPerServiceExceeded)?; - let operators = pending_approvals - .iter() - .cloned() - .map(|v| (v, ApprovalState::Pending)) - .collect::>(); - - let args = BoundedVec::<_, MaxFieldsOf>::try_from(request_args) - .map_err(|_| Error::::MaxFieldsExceeded)?; - - let operators_with_approval_state = - BoundedVec::<_, MaxOperatorsPerServiceOf>::try_from(operators) - .map_err(|_| Error::::MaxServiceProvidersExceeded)?; - - let service_request = ServiceRequest { - blueprint: blueprint_id, - owner: caller.clone(), - assets: assets.clone(), - ttl, - args, - permitted_callers, - operators_with_approval_state, - }; - - ensure!(allowed, Error::::InvalidRequestInput); - ServiceRequests::::insert(request_id, service_request); - NextServiceRequestId::::set(request_id.saturating_add(1)); - - Self::deposit_event(Event::ServiceRequested { - owner: caller.clone(), - request_id, - blueprint_id, - pending_approvals, - approved: Default::default(), - assets: assets.to_vec(), - }); - Ok(PostDispatchInfo { actual_weight: None, pays_fee: Pays::Yes }) } @@ -1128,177 +1069,28 @@ pub mod module { /// /// * `origin` - The origin of the call, must be a signed account /// * `request_id` - The ID of the service request to approve - /// * `restaking_percent` - Percentage of staked tokens to expose to this service (0-100) + /// * `native_exposure_percent` - Percentage of native token stake to expose + /// * `asset_exposure` - Vector of asset-specific exposure commitments /// /// # Errors /// /// * [`Error::ApprovalNotRequested`] - Caller is not in the pending approvals list /// * [`Error::ApprovalInterrupted`] - Approval was rejected by blueprint hook + /// * [`Error::InvalidRequestInput`] - Asset exposure commitments don't meet requirements #[pallet::weight(T::WeightInfo::approve())] pub fn approve( origin: OriginFor, #[pallet::compact] request_id: u64, - #[pallet::compact] restaking_percent: Percent, + #[pallet::compact] native_asset_exposure: Percent, + non_native_asset_exposures: Vec>, ) -> DispatchResultWithPostInfo { let caller = ensure_signed(origin)?; - let mut request = Self::service_requests(request_id)?; - let updated = request - .operators_with_approval_state - .iter_mut() - .find(|(v, _)| v == &caller) - .map(|(_, s)| *s = ApprovalState::Approved { restaking_percent }); - ensure!(updated.is_some(), Error::::ApprovalNotRequested); - - let blueprint_id = request.blueprint; - let (_, blueprint) = Self::blueprints(blueprint_id)?; - let preferences = Operators::::get(blueprint_id, caller.clone())?; - let approved = request - .operators_with_approval_state - .iter() - .filter_map(|(v, s)| { - if matches!(*s, ApprovalState::Approved { .. }) { - Some(v.clone()) - } else { - None - } - }) - .collect::>(); - let pending_approvals = request - .operators_with_approval_state - .iter() - .filter_map( - |(v, s)| if *s == ApprovalState::Pending { Some(v.clone()) } else { None }, - ) - .collect::>(); - - let (allowed, _weight) = Self::on_approve_hook( - &blueprint, - blueprint_id, - &preferences, + Self::do_approve( + caller, request_id, - restaking_percent.deconstruct(), + native_asset_exposure, + non_native_asset_exposures, )?; - - ensure!(allowed, Error::::ApprovalInterrupted); - // we emit this event regardless of the outcome of the approval. - Self::deposit_event(Event::ServiceRequestApproved { - operator: caller.clone(), - request_id, - blueprint_id: request.blueprint, - pending_approvals, - approved, - }); - - if request.is_approved() { - // remove the service request. - ServiceRequests::::remove(request_id); - - let service_id = Self::next_instance_id(); - let operators = request - .operators_with_approval_state - .into_iter() - .filter_map(|(v, state)| match state { - ApprovalState::Approved { restaking_percent } => { - Some((v, restaking_percent)) - }, - // N.B: this should not happen, as all operators are approved and checked - // above. - _ => None, - }) - .collect::>(); - - // add the service id to the list of services for each operator's profile. - for (operator, _) in &operators { - OperatorsProfile::::try_mutate_exists(operator, |profile| { - profile - .as_mut() - .and_then(|p| p.services.try_insert(service_id).ok()) - .ok_or(Error::::NotRegistered) - })?; - } - let operators = BoundedVec::<_, MaxOperatorsPerServiceOf>::try_from(operators) - .map_err(|_| Error::::MaxServiceProvidersExceeded)?; - let service = Service { - id: service_id, - blueprint: request.blueprint, - owner: request.owner.clone(), - assets: request.assets.clone(), - permitted_callers: request.permitted_callers.clone(), - operators, - ttl: request.ttl, - }; - - UserServices::::try_mutate(&request.owner, |service_ids| { - Instances::::insert(service_id, service); - NextInstanceId::::set(service_id.saturating_add(1)); - service_ids - .try_insert(service_id) - .map_err(|_| Error::::MaxServicesPerUserExceeded) - })?; - - // Payment - if let Some(payment) = Self::service_payment(request_id) { - // send payments to the MBSM - let mbsm_address = Self::mbsm_address_of(&blueprint)?; - let mbsm_account_id = T::EvmAddressMapping::into_account_id(mbsm_address); - match payment.asset { - Asset::Custom(asset_id) if asset_id == Zero::zero() => { - T::Currency::transfer( - &Self::account_id(), - &mbsm_account_id, - payment.amount, - ExistenceRequirement::AllowDeath, - )?; - }, - Asset::Custom(asset_id) => { - T::Fungibles::transfer( - asset_id, - &Self::account_id(), - &mbsm_account_id, - payment.amount, - Preservation::Expendable, - )?; - }, - Asset::Erc20(token) => { - let (success, _weight) = Self::erc20_transfer( - token, - Self::address(), - mbsm_address, - payment.amount, - )?; - ensure!(success, Error::::ERC20TransferFailed); - }, - } - - // Remove the payment information. - StagingServicePayments::::remove(request_id); - } - - let (allowed, _weight) = Self::on_service_init_hook( - &blueprint, - blueprint_id, - request_id, - service_id, - &request.owner, - &request.permitted_callers, - &request.assets, - request.ttl, - )?; - - ensure!(allowed, Error::::ServiceInitializationInterrupted); - - Self::deposit_event(Event::ServiceInitiated { - owner: request.owner, - request_id, - assets: request.assets.to_vec(), - service_id, - blueprint_id: request.blueprint, - }); - } else { - // Update the service request. - ServiceRequests::::insert(request_id, request); - } - Ok(PostDispatchInfo { actual_weight: None, pays_fee: Pays::Yes }) } @@ -1328,77 +1120,7 @@ pub mod module { #[pallet::compact] request_id: u64, ) -> DispatchResultWithPostInfo { let caller = ensure_signed(origin)?; - let mut request = Self::service_requests(request_id)?; - let updated = request.operators_with_approval_state.iter_mut().find_map(|(v, s)| { - if v == &caller { - *s = ApprovalState::Rejected; - Some(()) - } else { - None - } - }); - - ensure!(updated.is_some(), Error::::ApprovalNotRequested); - - let blueprint_id = request.blueprint; - let (_, blueprint) = Self::blueprints(blueprint_id)?; - let prefs = Operators::::get(blueprint_id, caller.clone())?; - - let (allowed, _weight) = - Self::on_reject_hook(&blueprint, blueprint_id, &prefs, request_id)?; - - ensure!(allowed, Error::::RejectionInterrupted); - Self::deposit_event(Event::ServiceRequestRejected { - operator: caller, - blueprint_id: request.blueprint, - request_id, - }); - - // Refund the payment - if let Some(payment) = Self::service_payment(request_id) { - match payment.asset { - Asset::Custom(asset_id) if asset_id == Zero::zero() => { - let refund_to = payment - .refund_to - .try_into_account_id() - .map_err(|_| Error::::ExpectedAccountId)?; - T::Currency::transfer( - &Self::account_id(), - &refund_to, - payment.amount, - ExistenceRequirement::AllowDeath, - )?; - }, - Asset::Custom(asset_id) => { - let refund_to = payment - .refund_to - .try_into_account_id() - .map_err(|_| Error::::ExpectedAccountId)?; - T::Fungibles::transfer( - asset_id, - &Self::account_id(), - &refund_to, - payment.amount, - Preservation::Expendable, - )?; - }, - Asset::Erc20(token) => { - let refund_to = payment - .refund_to - .try_into_address() - .map_err(|_| Error::::ExpectedEVMAddress)?; - let (success, _weight) = Self::erc20_transfer( - token, - Self::address(), - refund_to, - payment.amount, - )?; - ensure!(success, Error::::ERC20TransferFailed); - }, - } - StagingServicePayments::::remove(request_id); - } - + Self::do_reject(caller, request_id)?; Ok(PostDispatchInfo { actual_weight: None, pays_fee: Pays::Yes }) } @@ -1443,7 +1165,7 @@ pub mod module { ensure!(allowed, Error::::TerminationInterrupted); // Remove the service from the operator's profile. - for (operator, _) in &service.operators { + for (operator, _) in &service.native_asset_security { OperatorsProfile::::try_mutate_exists(operator, |profile| { profile .as_mut() @@ -1556,7 +1278,7 @@ pub mod module { let blueprint_id = service.blueprint; let (_, blueprint) = Self::blueprints(blueprint_id)?; - let is_operator = service.operators.iter().any(|(v, _)| v == &caller); + let is_operator = service.native_asset_security.iter().any(|(v, _)| v == &caller); ensure!(is_operator, DispatchError::BadOrigin); let operator_preferences = Operators::::get(blueprint_id, &caller)?; @@ -1632,48 +1354,33 @@ pub mod module { let slashing_origin = maybe_slashing_origin.ok_or(Error::::NoSlashingOrigin)?; ensure!(slashing_origin == caller, DispatchError::BadOrigin); - let (operator, restake_percent) = - match service.operators.iter().find(|(operator, _)| operator == &offender) { - Some((operator, restake_percent)) => (operator, restake_percent), - None => return Err(Error::::OffenderNotOperator.into()), - }; - let operator_is_active = T::OperatorDelegationManager::is_operator_active(&offender); - ensure!(operator_is_active, Error::::OffenderNotActiveOperator); - - let total_own_stake = T::OperatorDelegationManager::get_operator_stake(operator); - // Only take the exposed restake percentage for this service. - let own_stake = restake_percent.mul_floor(total_own_stake); - let delegators = T::OperatorDelegationManager::get_delegators_for_operator(operator); - let exposed_stake = percent.mul_floor(own_stake); - let others_slash = delegators - .into_iter() - .map(|(delegator, stake, _asset_id)| (delegator, percent.mul_floor(stake))) - .collect::>(); - let total_slash = - others_slash.iter().fold(exposed_stake, |acc, (_, slash)| acc + *slash); - - // for now, we treat all assets equally, which is not the case in reality. - let unapplied_slash = UnappliedSlash { - service_id, - operator: offender.clone(), - own: exposed_stake, - others: others_slash, - reporters: Vec::from([caller]), - payout: total_slash, - }; + // Verify offender is an operator for this service + ensure!( + service.native_asset_security.iter().any(|(op, _)| op == &offender), + Error::::OffenderNotOperator + ); + + // Verify operator is active in delegation system + ensure!( + T::OperatorDelegationManager::is_operator_active(&offender), + Error::::OffenderNotActiveOperator + ); + + // Calculate the slash amounts for operator and delegators + let unapplied_slash = Self::calculate_slash(&caller, &service, &offender, percent)?; + // Store the slash for later processing let index = Self::next_unapplied_slash_index(); - let era = T::OperatorDelegationManager::get_current_round(); - UnappliedSlashes::::insert(era, index, unapplied_slash); + UnappliedSlashes::::insert(unapplied_slash.era, index, unapplied_slash.clone()); NextUnappliedSlashIndex::::set(index.saturating_add(1)); Self::deposit_event(Event::::UnappliedSlash { index, - operator: offender.clone(), + operator: offender, blueprint_id: service.blueprint, service_id, - amount: total_slash, - era, + amount: unapplied_slash.own, + era: unapplied_slash.era, }); Ok(PostDispatchInfo { actual_weight: None, pays_fee: Pays::Yes }) @@ -1718,7 +1425,7 @@ pub mod module { operator: unapplied_slash.operator, blueprint_id: service.blueprint, service_id: unapplied_slash.service_id, - amount: unapplied_slash.payout, + amount: unapplied_slash.own, era, }); @@ -1756,5 +1463,61 @@ pub mod module { Ok(PostDispatchInfo { actual_weight: None, pays_fee: Pays::Yes }) } + + /// Join a service instance as an operator + #[pallet::call_index(15)] + #[pallet::weight(10_000)] + pub fn join_service( + origin: OriginFor, + instance_id: u64, + native_asset_exposure: Percent, + non_native_asset_exposures: Vec>, + ) -> DispatchResult { + let operator = ensure_signed(origin)?; + + // Get service instance + let instance = Instances::::get(instance_id)?; + + // Check if operator is already in the set + ensure!( + !instance.native_asset_security.iter().any(|(op, _)| op == &operator), + Error::::AlreadyJoined + ); + + let (_, blueprint) = Self::blueprints(instance.blueprint)?; + let preferences = Self::operators(instance.blueprint, operator.clone())?; + + // Call membership implementation + Self::do_join_service( + &blueprint, + instance.blueprint, + instance_id, + &operator, + &preferences, + native_asset_exposure, + non_native_asset_exposures, + )?; + + Ok(()) + } + + /// Leave a service instance as an operator + #[pallet::call_index(16)] + #[pallet::weight(10_000)] + pub fn leave_service(origin: OriginFor, instance_id: u64) -> DispatchResult { + let operator = ensure_signed(origin)?; + + // Get service instance + let instance = Instances::::get(instance_id)?; + + // Get blueprint + let (_, blueprint) = Self::blueprints(instance.blueprint)?; + let _ = Self::operators(instance.blueprint, operator.clone())?; + + // Call membership implementation + Self::do_leave_service(&blueprint, instance.blueprint, instance_id, &operator)?; + + Ok(()) + } } } diff --git a/pallets/services/src/mock.rs b/pallets/services/src/mock.rs index 7ba34ad80..c9145896b 100644 --- a/pallets/services/src/mock.rs +++ b/pallets/services/src/mock.rs @@ -24,6 +24,7 @@ use frame_election_provider_support::{ use frame_support::{ construct_runtime, derive_impl, parameter_types, traits::{AsEnsureOriginWithArg, ConstU128, ConstU32, OneSessionHandler}, + PalletId, }; use frame_system::EnsureRoot; use mock_evm::MockedEvmRunner; @@ -35,7 +36,7 @@ use sp_keystore::{testing::MemoryKeystore, KeystoreExt, KeystorePtr}; use sp_runtime::{ testing::UintAuthorityId, traits::{ConvertInto, IdentityLookup}, - AccountId32, BuildStorage, Perbill, + AccountId32, BuildStorage, Perbill, Percent, }; use tangle_primitives::rewards::UserDepositWithLocks; use tangle_primitives::services::{Asset, EvmAddressMapping, EvmGasWeightMapping, EvmRunner}; @@ -209,7 +210,9 @@ impl pallet_staking::Config for Runtime { } parameter_types! { - pub const ServicesEVMAddress: H160 = H160([0x11; 20]); + pub const ServicePalletAccountId: PalletId = PalletId(*b"Services"); + + pub const SlashRecipient: AccountId = AccountId32::new([9u8; 32]); } pub struct PalletEVMGasWeightMapping; @@ -241,7 +244,7 @@ impl pallet_assets::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = u128; type AssetId = AssetId; - type AssetIdParameter = u32; + type AssetIdParameter = u128; type Currency = Balances; type CreateOrigin = AsEnsureOriginWithArg>; type ForceOrigin = frame_system::EnsureRoot; @@ -258,14 +261,12 @@ impl pallet_assets::Config for Runtime { type RemoveItemsLimit = ConstU32<5>; } -pub type AssetId = u32; +pub type AssetId = u128; pub struct MockDelegationManager; -impl tangle_primitives::traits::MultiAssetDelegationInfo +impl tangle_primitives::traits::MultiAssetDelegationInfo for MockDelegationManager { - type AssetId = AssetId; - fn get_current_round() -> tangle_primitives::types::RoundIndex { Default::default() } @@ -292,27 +293,29 @@ impl tangle_primitives::traits::MultiAssetDelegationInfo, + _asset_id: &Asset, ) -> Balance { Default::default() } fn get_delegators_for_operator( _operator: &AccountId, - ) -> Vec<(AccountId, Balance, Asset)> { + ) -> Vec<(AccountId, Balance, Asset)> { Default::default() } - fn slash_operator( + fn has_delegator_selected_blueprint( + _delegator: &AccountId, _operator: &AccountId, _blueprint_id: tangle_primitives::BlueprintId, - _percentage: sp_runtime::Percent, - ) { + ) -> bool { + // For mock implementation, always return true + true } fn get_user_deposit_with_locks( _who: &AccountId, - _asset_id: Asset, + _asset_id: Asset, ) -> Option> { None } @@ -406,6 +409,10 @@ parameter_types! { #[derive(Default, Copy, Clone, Eq, PartialEq, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] pub const MaxMasterBlueprintServiceManagerRevisions: u32 = u32::MAX; + + #[derive(Default, Copy, Clone, Eq, PartialEq, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] + #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] + pub const NativeExposureMinimum: Percent = Percent::from_percent(10); } impl Config for Runtime { @@ -413,7 +420,9 @@ impl Config for Runtime { type ForceOrigin = frame_system::EnsureRoot; type Currency = Balances; type Fungibles = Assets; - type PalletEVMAddress = ServicesEVMAddress; + type PalletId = ServicePalletAccountId; + type SlashRecipient = SlashRecipient; + type SlashManager = (); type AssetId = AssetId; type EvmRunner = MockedEvmRunner; type EvmGasWeightMapping = PalletEVMGasWeightMapping; @@ -439,6 +448,7 @@ impl Config for Runtime { type MaxContainerImageTagLength = MaxContainerImageTagLength; type MaxAssetsPerService = MaxAssetsPerService; type MaxMasterBlueprintServiceManagerVersions = MaxMasterBlueprintServiceManagerRevisions; + type NativeExposureMinimum = NativeExposureMinimum; type Constraints = pallet_services::types::ConstraintsOf; type OperatorDelegationManager = MockDelegationManager; type SlashDeferDuration = SlashDeferDuration; @@ -500,7 +510,7 @@ pub fn new_test_ext(ids: Vec) -> sp_io::TestExternalities { pub const MBSM: H160 = H160([0x12; 20]); pub const CGGMP21_BLUEPRINT: H160 = H160([0x21; 20]); pub const HOOKS_TEST: H160 = H160([0x22; 20]); -pub const USDC_ERC20: H160 = H160([0x23; 20]); +pub const USDC_ERC20: H160 = H160(hex_literal::hex!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")); pub const TNT: AssetId = 0; pub const USDC: AssetId = 1; @@ -636,7 +646,7 @@ pub fn new_test_ext_raw_authorities(authorities: Vec) -> sp_io::TestE >::on_initialize(1); let call = ::EvmRunner::call( - Services::address(), + Services::pallet_evm_account(), USDC_ERC20, serde_json::from_value::(json!({ "name": "initialize", @@ -677,7 +687,7 @@ pub fn new_test_ext_raw_authorities(authorities: Vec) -> sp_io::TestE // Mint for i in 1..=authorities.len() { let call = ::EvmRunner::call( - Services::address(), + Services::pallet_evm_account(), USDC_ERC20, serde_json::from_value::(json!({ "name": "mint", diff --git a/pallets/services/src/mock_evm.rs b/pallets/services/src/mock_evm.rs index c4600040d..393e6e282 100644 --- a/pallets/services/src/mock_evm.rs +++ b/pallets/services/src/mock_evm.rs @@ -16,7 +16,7 @@ #![allow(clippy::all)] use crate as pallet_services; use crate::mock::{ - AccountId, Balances, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, Timestamp, + AccountId, AssetId, Balances, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, Timestamp, }; use fp_evm::FeeCalculator; use frame_support::{ @@ -36,19 +36,55 @@ use sp_runtime::{ ConsensusEngineId, }; +use pallet_evm_precompile_balances_erc20::{Erc20BalancesPrecompile, Erc20Metadata}; use pallet_evm_precompile_blake2::Blake2F; use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; use pallet_evm_precompile_modexp::Modexp; use pallet_evm_precompile_sha3fips::Sha3FIPS256; use pallet_evm_precompile_simple::{ECRecover, ECRecoverPublicKey, Identity, Ripemd160, Sha256}; +use pallet_evm_precompileset_assets_erc20::{AddressToAssetId, Erc20AssetsPrecompileSet}; use precompile_utils::precompile_set::{ AcceptDelegateCall, AddressU64, CallableByContract, CallableByPrecompile, PrecompileAt, - PrecompileSetBuilder, PrecompilesInRangeInclusive, + PrecompileSetBuilder, PrecompileSetStartingWith, PrecompilesInRangeInclusive, }; type EthereumPrecompilesChecks = (AcceptDelegateCall, CallableByContract, CallableByPrecompile); +pub struct NativeErc20Metadata; + +/// ERC20 metadata for the native token. +impl Erc20Metadata for NativeErc20Metadata { + /// Returns the name of the token. + fn name() -> &'static str { + "Tangle Testnet Network Token" + } + + /// Returns the symbol of the token. + fn symbol() -> &'static str { + "tTNT" + } + + /// Returns the decimals places of the token. + fn decimals() -> u8 { + 18 + } + + /// Must return `true` only if it represents the main native currency of + /// the network. It must be the currency used in `pallet_evm`. + fn is_native_currency() -> bool { + true + } +} + +/// The asset precompile address prefix. Addresses that match against this prefix will be routed +/// to Erc20AssetsPrecompileSet being marked as foreign +pub const ASSET_PRECOMPILE_ADDRESS_PREFIX: &[u8] = &[255u8; 4]; + +parameter_types! { + pub ForeignAssetPrefix: &'static [u8] = ASSET_PRECOMPILE_ADDRESS_PREFIX; +} + #[precompile_utils::precompile_name_from_address] pub type DefaultPrecompiles = ( // Ethereum precompiles: @@ -67,7 +103,20 @@ pub type DefaultPrecompiles = ( pub type TanglePrecompiles = PrecompileSetBuilder< R, - (PrecompilesInRangeInclusive<(AddressU64<1>, AddressU64<2095>), DefaultPrecompiles>,), + ( + PrecompilesInRangeInclusive<(AddressU64<1>, AddressU64<2095>), DefaultPrecompiles>, + PrecompileAt< + AddressU64<2050>, + Erc20BalancesPrecompile, + (CallableByContract, CallableByPrecompile), + >, + // Prefixed precompile sets (XC20) + PrecompileSetStartingWith< + ForeignAssetPrefix, + Erc20AssetsPrecompileSet, + CallableByContract, + >, + ), >; parameter_types! { @@ -83,6 +132,28 @@ impl pallet_timestamp::Config for Runtime { type WeightInfo = (); } +const ASSET_ID_SIZE: usize = core::mem::size_of::(); + +impl AddressToAssetId for Runtime { + fn address_to_asset_id(address: H160) -> Option { + let mut data = [0u8; ASSET_ID_SIZE]; + let address_bytes: [u8; 20] = address.into(); + if ASSET_PRECOMPILE_ADDRESS_PREFIX.eq(&address_bytes[0..4]) { + data.copy_from_slice(&address_bytes[4..ASSET_ID_SIZE + 4]); + Some(AssetId::from_be_bytes(data)) + } else { + None + } + } + + fn asset_id_to_address(asset_id: AssetId) -> H160 { + let mut data = [0u8; 20]; + data[0..4].copy_from_slice(ASSET_PRECOMPILE_ADDRESS_PREFIX); + data[4..ASSET_ID_SIZE + 4].copy_from_slice(&asset_id.to_be_bytes()); + H160::from(data) + } +} + pub struct FixedGasPrice; impl FeeCalculator for FixedGasPrice { fn min_gas_price() -> (U256, Weight) { @@ -160,7 +231,7 @@ impl OnChargeEVMTransaction for CustomEVMCurrencyAdapter { who: &H160, fee: U256, ) -> Result> { - let pallet_services_address = pallet_services::Pallet::::address(); + let pallet_services_address = pallet_services::Pallet::::pallet_evm_account(); // Make pallet services account free to use if who == &pallet_services_address { return Ok(None); @@ -177,7 +248,7 @@ impl OnChargeEVMTransaction for CustomEVMCurrencyAdapter { base_fee: U256, already_withdrawn: Self::LiquidityInfo, ) -> Self::LiquidityInfo { - let pallet_services_address = pallet_services::Pallet::::address(); + let pallet_services_address = pallet_services::Pallet::::pallet_evm_account(); // Make pallet services account free to use if who == &pallet_services_address { return already_withdrawn; diff --git a/pallets/services/src/test-artifacts/CGGMP21Blueprint.hex b/pallets/services/src/test-artifacts/CGGMP21Blueprint.hex index 84bb7d1ea..8bab31c97 100644 --- a/pallets/services/src/test-artifacts/CGGMP21Blueprint.hex +++ b/pallets/services/src/test-artifacts/CGGMP21Blueprint.hex @@ -1 +1 @@ -0x6080604052600436106101355760003560e01c80638b248065116100ab578063a4d91fe91161006f578063a4d91fe914610314578063bb43abd914610332578063d7deb48214610340578063e926cbd114610182578063f840766214610360578063fe0dd3711461038057600080fd5b80638b248065146102a35780639838caa3146102b1578063987ab9db146102bf5780639d0410ee146102e6578063a24e8a90146102f957600080fd5b806337c29662116100fd57806337c2966214610204578063434698bb146102175780635d79ea291461023757806374ceeb55146101e4578063821c7be21461025b578063884673ac1461027b57600080fd5b806308179f351461013a5780630af7d743146101825780630b6535d7146101a45780630d0dd399146101c457806314b4df4c146101e4575b600080fd5b34801561014657600080fd5b506101656101553660046105e3565b506002546001600160a01b031690565b6040516001600160a01b0390911681526020015b60405180910390f35b34801561018e57600080fd5b506101a261019d36600461065f565b61038e565b005b3480156101b057600080fd5b506101a26101bf3660046106e3565b6103da565b3480156101d057600080fd5b50600054610165906001600160a01b031681565b3480156101f057600080fd5b506101656101ff3660046105e3565b503090565b6101a261021236600461073e565b610468565b34801561022357600080fd5b506101a2610232366004610804565b6104b2565b34801561024357600080fd5b5061024d60015481565b604051908152602001610179565b34801561026757600080fd5b506101a2610276366004610841565b6104f5565b34801561028757600080fd5b5061016573111111111111111111111111111111111111111181565b6101a261023236600461088f565b6101a261019d3660046108cb565b3480156102cb57600080fd5b50731111111111111111111111111111111111111111610165565b6101a26102f4366004610941565b610539565b34801561030557600080fd5b506101a26102763660046109aa565b34801561032057600080fd5b506000546001600160a01b0316610165565b6101a26102f43660046109d4565b34801561034c57600080fd5b506101a261035b366004610a29565b61057e565b34801561036c57600080fd5b50600254610165906001600160a01b031681565b6101a2610232366004610804565b6000546001600160a01b031633146103d357600054604051630c423fcf60e01b81523360048201526001600160a01b0390911660248201526044015b60405180910390fd5b5050505050565b337311111111111111111111111111111111111111111461042a576040516359afe8af60e11b815233600482015273111111111111111111111111111111111111111160248201526044016103ca565b67ffffffffffffffff92909216600155600280546001600160a01b039283166001600160a01b03199182161790915560008054929093169116179055565b6000546001600160a01b031633146104a857600054604051630c423fcf60e01b81523360048201526001600160a01b0390911660248201526044016103ca565b5050505050505050565b6000546001600160a01b031633146104f257600054604051630c423fcf60e01b81523360048201526001600160a01b0390911660248201526044016103ca565b50565b6000546001600160a01b0316331461053557600054604051630c423fcf60e01b81523360048201526001600160a01b0390911660248201526044016103ca565b5050565b6000546001600160a01b0316331461057957600054604051630c423fcf60e01b81523360048201526001600160a01b0390911660248201526044016103ca565b505050565b6000546001600160a01b031633146105be57600054604051630c423fcf60e01b81523360048201526001600160a01b0390911660248201526044016103ca565b505050505050565b803567ffffffffffffffff811681146105de57600080fd5b919050565b6000602082840312156105f557600080fd5b6105fe826105c6565b9392505050565b60008083601f84011261061757600080fd5b50813567ffffffffffffffff81111561062f57600080fd5b60208301915083602082850101111561064757600080fd5b9250929050565b803560ff811681146105de57600080fd5b60008060008060006080868803121561067757600080fd5b610680866105c6565b9450602086013567ffffffffffffffff81111561069c57600080fd5b6106a888828901610605565b90955093506106bb90506040870161064e565b949793965091946060013592915050565b80356001600160a01b03811681146105de57600080fd5b6000806000606084860312156106f857600080fd5b610701846105c6565b925061070f602085016106cc565b915061071d604085016106cc565b90509250925092565b600060c0828403121561073857600080fd5b50919050565b60008060008060008060008060c0898b03121561075a57600080fd5b610763896105c6565b975061077160208a0161064e565b965061077f60408a016105c6565b9550606089013567ffffffffffffffff8082111561079c57600080fd5b6107a88c838d01610726565b965060808b01359150808211156107be57600080fd5b6107ca8c838d01610605565b909650945060a08b01359150808211156107e357600080fd5b506107f08b828c01610605565b999c989b5096995094979396929594505050565b60006020828403121561081657600080fd5b813567ffffffffffffffff81111561082d57600080fd5b61083984828501610726565b949350505050565b6000806040838503121561085457600080fd5b823567ffffffffffffffff81111561086b57600080fd5b61087785828601610726565b925050610886602084016105c6565b90509250929050565b6000602082840312156108a157600080fd5b813567ffffffffffffffff8111156108b857600080fd5b820161012081850312156105fe57600080fd5b6000806000806000608086880312156108e357600080fd5b6108ec866105c6565b94506108fa6020870161064e565b9350610908604087016105c6565b9250606086013567ffffffffffffffff81111561092457600080fd5b61093088828901610605565b969995985093965092949392505050565b60008060006040848603121561095657600080fd5b833567ffffffffffffffff8082111561096e57600080fd5b61097a87838801610726565b9450602086013591508082111561099057600080fd5b5061099d86828701610605565b9497909650939450505050565b600080604083850312156109bd57600080fd5b6109c6836105c6565b9150610886602084016106cc565b6000806000606084860312156109e957600080fd5b833567ffffffffffffffff811115610a0057600080fd5b610a0c86828701610726565b935050610a1b602085016105c6565b915061071d6040850161064e565b60008060008060008060a08789031215610a4257600080fd5b610a4b876105c6565b9550610a59602088016105c6565b9450610a67604088016106cc565b9350606087013567ffffffffffffffff80821115610a8457600080fd5b818901915089601f830112610a9857600080fd5b813581811115610aa757600080fd5b8a60208260051b8501011115610abc57600080fd5b602083019550809450505050610ad4608088016105c6565b9050929550929550929556fea164736f6c6343000814000a +0x6080604052600436106101c25760003560e01c806375af2f58116100f7578063a24e8a9011610095578063d7deb48211610064578063d7deb4821461045b578063e926cbd11461020f578063f84076621461047b578063fe0dd3711461049b57600080fd5b8063a24e8a9014610414578063a4d91fe91461042f578063ada6038214610321578063bb43abd91461044d57600080fd5b80638b248065116100d15780638b248065146103c35780639838caa3146103d1578063987ab9db146103df5780639d0410ee1461040157600080fd5b806375af2f5814610365578063821c7be214610385578063884673ac146103a057600080fd5b806337c296621161016457806346c578a51161013e57806346c578a5146103215780635d79ea291461034157806361545fdd1461036557806374ceeb551461027157600080fd5b806337c29662146102be5780633c3aa64c146102d1578063434698bb1461030157600080fd5b80630d0dd399116101a05780630d0dd3991461025157806314b4df4c146102715780631948fdbc14610291578063216e8042146102a857600080fd5b806308179f35146101c75780630af7d7431461020f5780630b6535d714610231575b600080fd5b3480156101d357600080fd5b506101f26101e2366004610893565b506002546001600160a01b031690565b6040516001600160a01b0390911681526020015b60405180910390f35b34801561021b57600080fd5b5061022f61022a366004610907565b6104a9565b005b34801561023d57600080fd5b5061022f61024c36600461098a565b6104f5565b34801561025d57600080fd5b506000546101f2906001600160a01b031681565b34801561027d57600080fd5b506101f261028c366004610893565b503090565b34801561029d57600080fd5b506101f2627e87d581565b3480156102b457600080fd5b50627e87d56101f2565b61022f6102cc3660046109e5565b610574565b3480156102dd57600080fd5b506102f16102ec366004610aaa565b6105ba565b6040519015158152602001610206565b34801561030d57600080fd5b5061022f61031c366004610ae9565b6105cf565b34801561032d57600080fd5b5061022f61033c366004610b25565b61060e565b34801561034d57600080fd5b5061035760015481565b604051908152602001610206565b34801561037157600080fd5b506102f1610380366004610b25565b61064e565b34801561039157600080fd5b5061022f61033c366004610b72565b3480156103ac57600080fd5b506101f26b6d6f646c536572766963657360401b81565b61022f61031c366004610bbf565b61022f61022a366004610c01565b3480156103eb57600080fd5b506b6d6f646c536572766963657360401b6101f2565b61022f61040f366004610c76565b610694565b34801561042057600080fd5b5061022f61033c366004610cde565b34801561043b57600080fd5b506000546001600160a01b03166101f2565b61022f61040f366004610d08565b34801561046757600080fd5b5061022f610476366004610d5c565b6106d5565b34801561048757600080fd5b506002546101f2906001600160a01b031681565b61022f61031c366004610ae9565b6000546001600160a01b031633146104ee57600054604051630c423fcf60e01b81526104e59133916001600160a01b0390911690600401610e12565b60405180910390fd5b5050505050565b336b6d6f646c536572766963657360401b1461053757336b6d6f646c536572766963657360401b6040516359afe8af60e11b81526004016104e5929190610e12565b6001600160401b0392909216600155600280546001600160a01b039283166001600160a01b03199182161790915560008054929093169116179055565b6000546001600160a01b031633146105b057600054604051630c423fcf60e01b81526104e59133916001600160a01b0390911690600401610e12565b5050505050505050565b60006105c68383610719565b90505b92915050565b6000546001600160a01b0316331461060b57600054604051630c423fcf60e01b81526104e59133916001600160a01b0390911690600401610e12565b50565b6000546001600160a01b0316331461064a57600054604051630c423fcf60e01b81526104e59133916001600160a01b0390911690600401610e12565b5050565b600080546001600160a01b0316331461068b57600054604051630c423fcf60e01b81526104e59133916001600160a01b0390911690600401610e12565b50600092915050565b6000546001600160a01b031633146106d057600054604051630c423fcf60e01b81526104e59133916001600160a01b0390911690600401610e12565b505050565b6000546001600160a01b0316331461071157600054604051630c423fcf60e01b81526104e59133916001600160a01b0390911690600401610e12565b505050505050565b600061073261072d36849003840184610e2c565b610785565b1561073f575060016105c9565b600061075861075336859003850185610e2c565b6107cb565b6001600160401b038516600090815260036020526040902090915061077d9082610833565b9150506105c9565b600061079082610855565b156107a75750602001516001600160a01b03161590565b6107b082610874565b156107be5750602001511590565b506000919050565b919050565b60006107d682610855565b156107e357506020015190565b6107ec82610874565b1561080457602082015163ffffffff60801b176105c9565b8151600181111561081757610817610e95565b6040516375458b5d60e11b81526004016104e591815260200190565b6001600160a01b038116600090815260018301602052604081205415156105c6565b600060015b8251600181111561086d5761086d610e95565b1492915050565b60008061085a565b80356001600160401b03811681146107c657600080fd5b6000602082840312156108a557600080fd5b6105c68261087c565b60008083601f8401126108c057600080fd5b5081356001600160401b038111156108d757600080fd5b6020830191508360208285010111156108ef57600080fd5b9250929050565b803560ff811681146107c657600080fd5b60008060008060006080868803121561091f57600080fd5b6109288661087c565b945060208601356001600160401b0381111561094357600080fd5b61094f888289016108ae565b90955093506109629050604087016108f6565b949793965091946060013592915050565b80356001600160a01b03811681146107c657600080fd5b60008060006060848603121561099f57600080fd5b6109a88461087c565b92506109b660208501610973565b91506109c460408501610973565b90509250925092565b600060c082840312156109df57600080fd5b50919050565b60008060008060008060008060c0898b031215610a0157600080fd5b610a0a8961087c565b9750610a1860208a016108f6565b9650610a2660408a0161087c565b955060608901356001600160401b0380821115610a4257600080fd5b610a4e8c838d016109cd565b965060808b0135915080821115610a6457600080fd5b610a708c838d016108ae565b909650945060a08b0135915080821115610a8957600080fd5b50610a968b828c016108ae565b999c989b5096995094979396929594505050565b6000808284036060811215610abe57600080fd5b610ac78461087c565b92506040601f1982011215610adb57600080fd5b506020830190509250929050565b600060208284031215610afb57600080fd5b81356001600160401b03811115610b1157600080fd5b610b1d848285016109cd565b949350505050565b60008060408385031215610b3857600080fd5b610b418361087c565b915060208301356001600160401b03811115610b5c57600080fd5b610b68858286016109cd565b9150509250929050565b60008060408385031215610b8557600080fd5b82356001600160401b03811115610b9b57600080fd5b610ba7858286016109cd565b925050610bb66020840161087c565b90509250929050565b600060208284031215610bd157600080fd5b81356001600160401b03811115610be757600080fd5b82016101208185031215610bfa57600080fd5b9392505050565b600080600080600060808688031215610c1957600080fd5b610c228661087c565b9450610c30602087016108f6565b9350610c3e6040870161087c565b925060608601356001600160401b03811115610c5957600080fd5b610c65888289016108ae565b969995985093965092949392505050565b600080600060408486031215610c8b57600080fd5b83356001600160401b0380821115610ca257600080fd5b610cae878388016109cd565b94506020860135915080821115610cc457600080fd5b50610cd1868287016108ae565b9497909650939450505050565b60008060408385031215610cf157600080fd5b610cfa8361087c565b9150610bb660208401610973565b600080600060608486031215610d1d57600080fd5b83356001600160401b03811115610d3357600080fd5b610d3f868287016109cd565b935050610d4e6020850161087c565b91506109c4604085016108f6565b60008060008060008060a08789031215610d7557600080fd5b610d7e8761087c565b9550610d8c6020880161087c565b9450610d9a60408801610973565b935060608701356001600160401b0380821115610db657600080fd5b818901915089601f830112610dca57600080fd5b813581811115610dd957600080fd5b8a60208260051b8501011115610dee57600080fd5b602083019550809450505050610e066080880161087c565b90509295509295509295565b6001600160a01b0392831681529116602082015260400190565b600060408284031215610e3e57600080fd5b604051604081018181106001600160401b0382111715610e6e57634e487b7160e01b600052604160045260246000fd5b604052823560028110610e8057600080fd5b81526020928301359281019290925250919050565b634e487b7160e01b600052602160045260246000fdfea164736f6c6343000814000a diff --git a/pallets/services/src/test-artifacts/HookTestBlueprintServiceManager.hex b/pallets/services/src/test-artifacts/HookTestBlueprintServiceManager.hex index f405bb353..bbbf00ae7 100644 --- a/pallets/services/src/test-artifacts/HookTestBlueprintServiceManager.hex +++ b/pallets/services/src/test-artifacts/HookTestBlueprintServiceManager.hex @@ -1 +1 @@ -0x6080604052600436106101355760003560e01c80638b248065116100ab578063a4d91fe91161006f578063a4d91fe914610323578063bb43abd914610341578063d7deb48214610354578063e926cbd114610374578063f840766214610394578063fe0dd371146103b457600080fd5b80638b248065146102a35780639838caa3146102b6578063987ab9db146102c95780639d0410ee146102f0578063a24e8a901461030357600080fd5b806337c29662116100fd57806337c2966214610204578063434698bb146102175780635d79ea291461023757806374ceeb55146101e4578063821c7be21461025b578063884673ac1461027b57600080fd5b806308179f351461013a5780630af7d743146101825780630b6535d7146101a45780630d0dd399146101c457806314b4df4c146101e4575b600080fd5b34801561014657600080fd5b50610165610155366004610965565b506002546001600160a01b031690565b6040516001600160a01b0390911681526020015b60405180910390f35b34801561018e57600080fd5b506101a261019d3660046109e1565b6103c7565b005b3480156101b057600080fd5b506101a26101bf366004610a65565b6103f7565b3480156101d057600080fd5b50600054610165906001600160a01b031681565b3480156101f057600080fd5b506101656101ff366004610965565b503090565b6101a2610212366004610ac0565b6104b6565b34801561022357600080fd5b506101a2610232366004610b86565b610525565b34801561024357600080fd5b5061024d60015481565b604051908152602001610179565b34801561026757600080fd5b506101a2610276366004610bc3565b61058d565b34801561028757600080fd5b5061016573111111111111111111111111111111111111111181565b6101a26102b1366004610c11565b6105f6565b6101a26102c4366004610c4d565b61065e565b3480156102d557600080fd5b50731111111111111111111111111111111111111111610165565b6101a26102fe366004610cc3565b6106ca565b34801561030f57600080fd5b506101a261031e366004610d2c565b610734565b34801561032f57600080fd5b506000546001600160a01b0316610165565b6101a261034f366004610d56565b61079d565b34801561036057600080fd5b506101a261036f366004610dab565b610807565b34801561038057600080fd5b506101a261038f3660046109e1565b610874565b3480156103a057600080fd5b50600254610165906001600160a01b031681565b6101a26103c2366004610b86565b6108e0565b6040517fd38931c95654e0c6958f3e427ad7c53f3a131634d634c86732f8655a28c119eb90600090a15050505050565b337311111111111111111111111111111111111111111461044c57337311111111111111111111111111111111111111116040516359afe8af60e11b8152600401610443929190610e62565b60405180910390fd5b67ffffffffffffffff8316600155600280546001600160a01b038085166001600160a01b03199283161790925560008054928416929091169190911781556040517fcf7e69bd1c708d6d282a3b1525cd1c89aa8bf25ec133174b4a7299223ceb3c4f9190a1505050565b6000546001600160a01b031633146104f257600054604051630c423fcf60e01b81526104439133916001600160a01b0390911690600401610e62565b6040517f025cf7c6b8939ff50c796fa1df561a4a8d4e0de035caa3d184d5929e47e33d3590600090a15050505050505050565b6000546001600160a01b0316331461056157600054604051630c423fcf60e01b81526104439133916001600160a01b0390911690600401610e62565b6040517f82b9cd36e76e3328de8315cb87dd42c248cdb31558184543b45ffd52ba422f2b90600090a150565b6000546001600160a01b031633146105c957600054604051630c423fcf60e01b81526104439133916001600160a01b0390911690600401610e62565b6040517f537b387222a90168d719f1d554df67d479e37cb59651378e6d4e4939030352a790600090a15050565b6000546001600160a01b0316331461063257600054604051630c423fcf60e01b81526104439133916001600160a01b0390911690600401610e62565b6040517f3582472ca72fc6a04530f08cfa31f71919e91e34971f4743873db0dbdf02b76090600090a150565b6000546001600160a01b0316331461069a57600054604051630c423fcf60e01b81526104439133916001600160a01b0390911690600401610e62565b6040517f9de6d7077ebf9fc111bfdc873d12a9d37d80b3657797fa91476f82695c942a3490600090a15050505050565b6000546001600160a01b0316331461070657600054604051630c423fcf60e01b81526104439133916001600160a01b0390911690600401610e62565b6040517f9add4e5824603d31f2170069522539a6971d31dc77d08e24588e3b68ce077ee690600090a1505050565b6000546001600160a01b0316331461077057600054604051630c423fcf60e01b81526104439133916001600160a01b0390911690600401610e62565b6040517fbdd650f74e0fff138cdfa7cac980a24bf480423cba6de8d8daf53042b6240a0890600090a15050565b6000546001600160a01b031633146107d957600054604051630c423fcf60e01b81526104439133916001600160a01b0390911690600401610e62565b6040517fd4162ed8e9bc92ece7ef39b8f199293e403fbea78e95ce1fcbfce0153eb86bb590600090a1505050565b6000546001600160a01b0316331461084357600054604051630c423fcf60e01b81526104439133916001600160a01b0390911690600401610e62565b6040517fdd03b583a4b2c88c19891a8aee92431d65f349d08a744f72297b4f25b28095a890600090a1505050505050565b6000546001600160a01b031633146108b057600054604051630c423fcf60e01b81526104439133916001600160a01b0390911690600401610e62565b6040517f95bfc812251a7cb44ca7b529c8854d13c0ae581729a7d2142231a3258ecd774390600090a15050505050565b6000546001600160a01b0316331461091c57600054604051630c423fcf60e01b81526104439133916001600160a01b0390911690600401610e62565b6040517f7e8e44afe7f3a9205df3a9e5ffc0c6e96006905bc7ca73b2a55eeb177ec63ff590600090a150565b803567ffffffffffffffff8116811461096057600080fd5b919050565b60006020828403121561097757600080fd5b61098082610948565b9392505050565b60008083601f84011261099957600080fd5b50813567ffffffffffffffff8111156109b157600080fd5b6020830191508360208285010111156109c957600080fd5b9250929050565b803560ff8116811461096057600080fd5b6000806000806000608086880312156109f957600080fd5b610a0286610948565b9450602086013567ffffffffffffffff811115610a1e57600080fd5b610a2a88828901610987565b9095509350610a3d9050604087016109d0565b949793965091946060013592915050565b80356001600160a01b038116811461096057600080fd5b600080600060608486031215610a7a57600080fd5b610a8384610948565b9250610a9160208501610a4e565b9150610a9f60408501610a4e565b90509250925092565b600060c08284031215610aba57600080fd5b50919050565b60008060008060008060008060c0898b031215610adc57600080fd5b610ae589610948565b9750610af360208a016109d0565b9650610b0160408a01610948565b9550606089013567ffffffffffffffff80821115610b1e57600080fd5b610b2a8c838d01610aa8565b965060808b0135915080821115610b4057600080fd5b610b4c8c838d01610987565b909650945060a08b0135915080821115610b6557600080fd5b50610b728b828c01610987565b999c989b5096995094979396929594505050565b600060208284031215610b9857600080fd5b813567ffffffffffffffff811115610baf57600080fd5b610bbb84828501610aa8565b949350505050565b60008060408385031215610bd657600080fd5b823567ffffffffffffffff811115610bed57600080fd5b610bf985828601610aa8565b925050610c0860208401610948565b90509250929050565b600060208284031215610c2357600080fd5b813567ffffffffffffffff811115610c3a57600080fd5b8201610120818503121561098057600080fd5b600080600080600060808688031215610c6557600080fd5b610c6e86610948565b9450610c7c602087016109d0565b9350610c8a60408701610948565b9250606086013567ffffffffffffffff811115610ca657600080fd5b610cb288828901610987565b969995985093965092949392505050565b600080600060408486031215610cd857600080fd5b833567ffffffffffffffff80821115610cf057600080fd5b610cfc87838801610aa8565b94506020860135915080821115610d1257600080fd5b50610d1f86828701610987565b9497909650939450505050565b60008060408385031215610d3f57600080fd5b610d4883610948565b9150610c0860208401610a4e565b600080600060608486031215610d6b57600080fd5b833567ffffffffffffffff811115610d8257600080fd5b610d8e86828701610aa8565b935050610d9d60208501610948565b9150610a9f604085016109d0565b60008060008060008060a08789031215610dc457600080fd5b610dcd87610948565b9550610ddb60208801610948565b9450610de960408801610a4e565b9350606087013567ffffffffffffffff80821115610e0657600080fd5b818901915089601f830112610e1a57600080fd5b813581811115610e2957600080fd5b8a60208260051b8501011115610e3e57600080fd5b602083019550809450505050610e5660808801610948565b90509295509295509295565b6001600160a01b039283168152911660208201526040019056fea164736f6c6343000814000a +0x6080604052600436106101c25760003560e01c806375af2f58116100f7578063a24e8a9011610095578063d7deb48211610064578063d7deb48214610474578063e926cbd114610494578063f8407662146104b4578063fe0dd371146104d457600080fd5b8063a24e8a9014610423578063a4d91fe914610443578063ada6038214610321578063bb43abd91461046157600080fd5b80638b248065116100d15780638b248065146103c85780639838caa3146103db578063987ab9db146103ee5780639d0410ee1461041057600080fd5b806375af2f5814610365578063821c7be214610385578063884673ac146103a557600080fd5b806337c296621161016457806346c578a51161013e57806346c578a5146103215780635d79ea291461034157806361545fdd1461036557806374ceeb551461027157600080fd5b806337c29662146102be5780633c3aa64c146102d1578063434698bb1461030157600080fd5b80630d0dd399116101a05780630d0dd3991461025157806314b4df4c146102715780631948fdbc14610291578063216e8042146102a857600080fd5b806308179f35146101c75780630af7d7431461020f5780630b6535d714610231575b600080fd5b3480156101d357600080fd5b506101f26101e2366004610c72565b506002546001600160a01b031690565b6040516001600160a01b0390911681526020015b60405180910390f35b34801561021b57600080fd5b5061022f61022a366004610ce6565b6104e7565b005b34801561023d57600080fd5b5061022f61024c366004610d69565b610517565b34801561025d57600080fd5b506000546101f2906001600160a01b031681565b34801561027d57600080fd5b506101f261028c366004610c72565b503090565b34801561029d57600080fd5b506101f2627e87d581565b3480156102b457600080fd5b50627e87d56101f2565b61022f6102cc366004610dc4565b6105cb565b3480156102dd57600080fd5b506102f16102ec366004610e89565b61063a565b6040519015158152602001610206565b34801561030d57600080fd5b5061022f61031c366004610ec8565b61064f565b34801561032d57600080fd5b5061022f61033c366004610f04565b6106b7565b34801561034d57600080fd5b5061035760015481565b604051908152602001610206565b34801561037157600080fd5b506102f1610380366004610f04565b6106f7565b34801561039157600080fd5b5061022f6103a0366004610f51565b61073d565b3480156103b157600080fd5b506101f26b6d6f646c536572766963657360401b81565b61022f6103d6366004610f9e565b6107a6565b61022f6103e9366004610fe0565b61080e565b3480156103fa57600080fd5b506b6d6f646c536572766963657360401b6101f2565b61022f61041e366004611055565b61087a565b34801561042f57600080fd5b5061022f61043e3660046110bd565b6108e4565b34801561044f57600080fd5b506000546001600160a01b03166101f2565b61022f61046f3660046110e7565b61094d565b34801561048057600080fd5b5061022f61048f36600461113b565b6109b7565b3480156104a057600080fd5b5061022f6104af366004610ce6565b610a24565b3480156104c057600080fd5b506002546101f2906001600160a01b031681565b61022f6104e2366004610ec8565b610a90565b6040517fd38931c95654e0c6958f3e427ad7c53f3a131634d634c86732f8655a28c119eb90600090a15050505050565b336b6d6f646c536572766963657360401b1461056257336b6d6f646c536572766963657360401b6040516359afe8af60e11b81526004016105599291906111f1565b60405180910390fd5b6001600160401b038316600155600280546001600160a01b038085166001600160a01b03199283161790925560008054928416929091169190911781556040517fcf7e69bd1c708d6d282a3b1525cd1c89aa8bf25ec133174b4a7299223ceb3c4f9190a1505050565b6000546001600160a01b0316331461060757600054604051630c423fcf60e01b81526105599133916001600160a01b03909116906004016111f1565b6040517f025cf7c6b8939ff50c796fa1df561a4a8d4e0de035caa3d184d5929e47e33d3590600090a15050505050505050565b60006106468383610af8565b90505b92915050565b6000546001600160a01b0316331461068b57600054604051630c423fcf60e01b81526105599133916001600160a01b03909116906004016111f1565b6040517f82b9cd36e76e3328de8315cb87dd42c248cdb31558184543b45ffd52ba422f2b90600090a150565b6000546001600160a01b031633146106f357600054604051630c423fcf60e01b81526105599133916001600160a01b03909116906004016111f1565b5050565b600080546001600160a01b0316331461073457600054604051630c423fcf60e01b81526105599133916001600160a01b03909116906004016111f1565b50600092915050565b6000546001600160a01b0316331461077957600054604051630c423fcf60e01b81526105599133916001600160a01b03909116906004016111f1565b6040517f537b387222a90168d719f1d554df67d479e37cb59651378e6d4e4939030352a790600090a15050565b6000546001600160a01b031633146107e257600054604051630c423fcf60e01b81526105599133916001600160a01b03909116906004016111f1565b6040517f3582472ca72fc6a04530f08cfa31f71919e91e34971f4743873db0dbdf02b76090600090a150565b6000546001600160a01b0316331461084a57600054604051630c423fcf60e01b81526105599133916001600160a01b03909116906004016111f1565b6040517f9de6d7077ebf9fc111bfdc873d12a9d37d80b3657797fa91476f82695c942a3490600090a15050505050565b6000546001600160a01b031633146108b657600054604051630c423fcf60e01b81526105599133916001600160a01b03909116906004016111f1565b6040517f9add4e5824603d31f2170069522539a6971d31dc77d08e24588e3b68ce077ee690600090a1505050565b6000546001600160a01b0316331461092057600054604051630c423fcf60e01b81526105599133916001600160a01b03909116906004016111f1565b6040517fbdd650f74e0fff138cdfa7cac980a24bf480423cba6de8d8daf53042b6240a0890600090a15050565b6000546001600160a01b0316331461098957600054604051630c423fcf60e01b81526105599133916001600160a01b03909116906004016111f1565b6040517fd4162ed8e9bc92ece7ef39b8f199293e403fbea78e95ce1fcbfce0153eb86bb590600090a1505050565b6000546001600160a01b031633146109f357600054604051630c423fcf60e01b81526105599133916001600160a01b03909116906004016111f1565b6040517fdd03b583a4b2c88c19891a8aee92431d65f349d08a744f72297b4f25b28095a890600090a1505050505050565b6000546001600160a01b03163314610a6057600054604051630c423fcf60e01b81526105599133916001600160a01b03909116906004016111f1565b6040517f95bfc812251a7cb44ca7b529c8854d13c0ae581729a7d2142231a3258ecd774390600090a15050505050565b6000546001600160a01b03163314610acc57600054604051630c423fcf60e01b81526105599133916001600160a01b03909116906004016111f1565b6040517f7e8e44afe7f3a9205df3a9e5ffc0c6e96006905bc7ca73b2a55eeb177ec63ff590600090a150565b6000610b11610b0c3684900384018461120b565b610b64565b15610b1e57506001610649565b6000610b37610b323685900385018561120b565b610baa565b6001600160401b0385166000908152600360205260409020909150610b5c9082610c12565b915050610649565b6000610b6f82610c34565b15610b865750602001516001600160a01b03161590565b610b8f82610c53565b15610b9d5750602001511590565b506000919050565b919050565b6000610bb582610c34565b15610bc257506020015190565b610bcb82610c53565b15610be357602082015163ffffffff60801b17610649565b81516001811115610bf657610bf6611274565b6040516375458b5d60e11b815260040161055991815260200190565b6001600160a01b03811660009081526001830160205260408120541515610646565b600060015b82516001811115610c4c57610c4c611274565b1492915050565b600080610c39565b80356001600160401b0381168114610ba557600080fd5b600060208284031215610c8457600080fd5b61064682610c5b565b60008083601f840112610c9f57600080fd5b5081356001600160401b03811115610cb657600080fd5b602083019150836020828501011115610cce57600080fd5b9250929050565b803560ff81168114610ba557600080fd5b600080600080600060808688031215610cfe57600080fd5b610d0786610c5b565b945060208601356001600160401b03811115610d2257600080fd5b610d2e88828901610c8d565b9095509350610d41905060408701610cd5565b949793965091946060013592915050565b80356001600160a01b0381168114610ba557600080fd5b600080600060608486031215610d7e57600080fd5b610d8784610c5b565b9250610d9560208501610d52565b9150610da360408501610d52565b90509250925092565b600060c08284031215610dbe57600080fd5b50919050565b60008060008060008060008060c0898b031215610de057600080fd5b610de989610c5b565b9750610df760208a01610cd5565b9650610e0560408a01610c5b565b955060608901356001600160401b0380821115610e2157600080fd5b610e2d8c838d01610dac565b965060808b0135915080821115610e4357600080fd5b610e4f8c838d01610c8d565b909650945060a08b0135915080821115610e6857600080fd5b50610e758b828c01610c8d565b999c989b5096995094979396929594505050565b6000808284036060811215610e9d57600080fd5b610ea684610c5b565b92506040601f1982011215610eba57600080fd5b506020830190509250929050565b600060208284031215610eda57600080fd5b81356001600160401b03811115610ef057600080fd5b610efc84828501610dac565b949350505050565b60008060408385031215610f1757600080fd5b610f2083610c5b565b915060208301356001600160401b03811115610f3b57600080fd5b610f4785828601610dac565b9150509250929050565b60008060408385031215610f6457600080fd5b82356001600160401b03811115610f7a57600080fd5b610f8685828601610dac565b925050610f9560208401610c5b565b90509250929050565b600060208284031215610fb057600080fd5b81356001600160401b03811115610fc657600080fd5b82016101208185031215610fd957600080fd5b9392505050565b600080600080600060808688031215610ff857600080fd5b61100186610c5b565b945061100f60208701610cd5565b935061101d60408701610c5b565b925060608601356001600160401b0381111561103857600080fd5b61104488828901610c8d565b969995985093965092949392505050565b60008060006040848603121561106a57600080fd5b83356001600160401b038082111561108157600080fd5b61108d87838801610dac565b945060208601359150808211156110a357600080fd5b506110b086828701610c8d565b9497909650939450505050565b600080604083850312156110d057600080fd5b6110d983610c5b565b9150610f9560208401610d52565b6000806000606084860312156110fc57600080fd5b83356001600160401b0381111561111257600080fd5b61111e86828701610dac565b93505061112d60208501610c5b565b9150610da360408501610cd5565b60008060008060008060a0878903121561115457600080fd5b61115d87610c5b565b955061116b60208801610c5b565b945061117960408801610d52565b935060608701356001600160401b038082111561119557600080fd5b818901915089601f8301126111a957600080fd5b8135818111156111b857600080fd5b8a60208260051b85010111156111cd57600080fd5b6020830195508094505050506111e560808801610c5b565b90509295509295509295565b6001600160a01b0392831681529116602082015260400190565b60006040828403121561121d57600080fd5b604051604081018181106001600160401b038211171561124d57634e487b7160e01b600052604160045260246000fd5b60405282356002811061125f57600080fd5b81526020928301359281019290925250919050565b634e487b7160e01b600052602160045260246000fdfea164736f6c6343000814000a diff --git a/pallets/services/src/test-artifacts/MasterBlueprintServiceManager.hex b/pallets/services/src/test-artifacts/MasterBlueprintServiceManager.hex index a6bc701ef..38821878b 100644 --- a/pallets/services/src/test-artifacts/MasterBlueprintServiceManager.hex +++ b/pallets/services/src/test-artifacts/MasterBlueprintServiceManager.hex @@ -1 +1 @@ -0x60806040526004361061019c5760003560e01c806382a1ece4116100ec578063a6a785381161008a578063b9315d3a11610064578063b9315d3a1461047c578063c97008e01461048f578063d547741f146104af578063f9e13845146104cf57600080fd5b8063a6a7853814610443578063a8f7c5ed14610456578063b89af9041461046957600080fd5b806391d14854116100c657806391d14854146103cf578063987ab9db146103ef578063a217fddf14610410578063a4d91fe91461042557600080fd5b806382a1ece41461036d578063884673ac1461038d5780638e6f8c60146103af57600080fd5b80632f2ff15d11610159578063410fdec811610133578063410fdec81461030f5780634984ee6b1461032f5780635c975abb14610342578063727391f21461035a57600080fd5b80632f2ff15d146102af57806333db3920146102cf57806336568abe146102ef57600080fd5b806301ffc9a7146101a15780630633da77146101d65780630d0dd399146101f85780631fb27b3414610230578063248a9ca314610250578063274ef0151461028f575b600080fd5b3480156101ad57600080fd5b506101c16101bc366004611707565b6104e2565b60405190151581526020015b60405180910390f35b3480156101e257600080fd5b506101f66101f136600461176d565b610519565b005b34801561020457600080fd5b50600054610218906001600160a01b031681565b6040516001600160a01b0390911681526020016101cd565b34801561023c57600080fd5b506101f661024b3660046117cc565b610640565b34801561025c57600080fd5b5061028161026b366004611819565b6000908152600160208190526040909120015490565b6040519081526020016101cd565b34801561029b57600080fd5b506102186102aa366004611832565b610745565b3480156102bb57600080fd5b506101f66102ca366004611865565b6107d9565b3480156102db57600080fd5b506101f66102ea3660046118ee565b610805565b3480156102fb57600080fd5b506101f661030a366004611865565b610926565b34801561031b57600080fd5b506101f661032a36600461196b565b61095e565b6101f661033d3660046119c8565b610a62565b34801561034e57600080fd5b5060025460ff166101c1565b6101f6610368366004611a4d565b610b72565b34801561037957600080fd5b506101f6610388366004611ac3565b610c7d565b34801561039957600080fd5b5061021860008051602061256a83398151915281565b3480156103bb57600080fd5b506102186103ca366004611832565b610da6565b3480156103db57600080fd5b506101c16103ea366004611865565b610df5565b3480156103fb57600080fd5b5060008051602061256a833981519152610218565b34801561041c57600080fd5b50610281600081565b34801561043157600080fd5b506000546001600160a01b0316610218565b6101f66104513660046117cc565b610e20565b6101f6610464366004611b8d565b610f18565b6101f6610477366004611c65565b611040565b6101f661048a366004611cc0565b611115565b34801561049b57600080fd5b506101f66104aa3660046118ee565b61122c565b3480156104bb57600080fd5b506101f66104ca366004611865565b61133c565b6101f66104dd366004611d2e565b611362565b60006001600160e01b03198216637965db0b60e01b148061051357506301ffc9a760e01b6001600160e01b03198316145b92915050565b3360008051602061256a83398151915214610562573360008051602061256a8339815191526040516359afe8af60e11b8152600401610559929190611d78565b60405180910390fd5b61056a6114ad565b600061058060036001600160401b0386166114d3565b604051630a24e8a960e41b81526001600160401b03851660048201526001600160a01b0384811660248301529192509082169063a24e8a9090604401600060405180830381600087803b1580156105d657600080fd5b505af11580156105ea573d6000803e3d6000fd5b50506040516001600160a01b03851681526001600160401b038087169350871691507f5267932e6c1b678e74efba1e4d6c8b3fd7504cf0fa8eea462efac2c590a21d56906020015b60405180910390a350505050565b3360008051602061256a83398151915214610680573360008051602061256a8339815191526040516359afe8af60e11b8152600401610559929190611d78565b6106886114ad565b600061069e60036001600160401b0385166114d3565b60405163434698bb60e01b81529091506001600160a01b0382169063434698bb906106cd908590600401611e93565b600060405180830381600087803b1580156106e757600080fd5b505af11580156106fb573d6000803e3d6000fd5b50505050826001600160401b03167fde25c2b146db36cbc91715fdf29cb0eb556eed7678e0bf13563bf7050e8f0bbd836040516107389190611e93565b60405180910390a2505050565b60008061075c60036001600160401b0386166114d3565b6040516374ceeb5560e01b81526001600160401b03851660048201529091506001600160a01b038216906374ceeb55906024015b602060405180830381865afa1580156107ad573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107d19190611ea6565b949350505050565b600082815260016020819052604090912001546107f5816114e6565b6107ff83836114f3565b50505050565b3360008051602061256a83398151915214610845573360008051602061256a8339815191526040516359afe8af60e11b8152600401610559929190611d78565b61084d6114ad565b600061086360036001600160401b0389166114d3565b604051630af7d74360e01b81529091506001600160a01b03821690630af7d7439061089a9089908990899089908990600401611ec3565b600060405180830381600087803b1580156108b457600080fd5b505af11580156108c8573d6000803e3d6000fd5b50505050856001600160401b0316876001600160401b03167f7d2ac4c1544e07516def74ce3ada12ff791868ed07d6da82694ae2c0342c8371878787876040516109159493929190611efd565b60405180910390a350505050505050565b6001600160a01b038116331461094f5760405163334bd91960e11b815260040160405180910390fd5b610959828261156c565b505050565b3360008051602061256a8339815191521461099e573360008051602061256a8339815191526040516359afe8af60e11b8152600401610559929190611d78565b6109a66114ad565b60006109bc60036001600160401b0386166114d3565b60405163410e3df160e11b81529091506001600160a01b0382169063821c7be2906109ed9086908690600401611f27565b600060405180830381600087803b158015610a0757600080fd5b505af1158015610a1b573d6000803e3d6000fd5b50505050816001600160401b0316846001600160401b03167f4ca48d7cb411035a931da7a686ea77bc413069de12d844c47daf9242b765cba4856040516106329190611e93565b3360008051602061256a83398151915214610aa2573360008051602061256a8339815191526040516359afe8af60e11b8152600401610559929190611d78565b610aaa6114ad565b6000610ac060036001600160401b0389166114d3565b604051639838caa360e01b81529091506001600160a01b03821690639838caa390610af79089908990899089908990600401611f52565b600060405180830381600087803b158015610b1157600080fd5b505af1158015610b25573d6000803e3d6000fd5b50506040805160ff891681526001600160401b038881166020830152808b1694508b1692507fa9aaaef624d196d961d053fc29d4331e5cb45d62195d99814d29a94f46fa9d4b9101610915565b3360008051602061256a83398151915214610bb2573360008051602061256a8339815191526040516359afe8af60e11b8152600401610559929190611d78565b610bba6114ad565b6000610bd060036001600160401b0387166114d3565b604051634e82087760e11b81529091506001600160a01b03821690639d0410ee90610c0390879087908790600401611f93565b600060405180830381600087803b158015610c1d57600080fd5b505af1158015610c31573d6000803e3d6000fd5b50505050846001600160401b03167f7814301100cb38a35027dd63a29aa14c3f73188f15cf9f027655257c45016e8785604051610c6e9190611e93565b60405180910390a25050505050565b3360008051602061256a83398151915214610cbd573360008051602061256a8339815191526040516359afe8af60e11b8152600401610559929190611d78565b610cc56114ad565b6000610cdb60036001600160401b038a166114d3565b604051636bef5a4160e11b81529091506001600160a01b0382169063d7deb48290610d14908a908a908a908a908a908a90600401611fc3565b600060405180830381600087803b158015610d2e57600080fd5b505af1158015610d42573d6000803e3d6000fd5b5050604080516001600160a01b03891681526001600160401b038681166020830152808b1694508b811693508c16917f93927390d124732c66289c6e22370514ddd612a9cc6bff5671cbf2cbc15ddfe3910160405180910390a45050505050505050565b600080610dbd60036001600160401b0386166114d3565b60405163052d37d360e21b81526001600160401b03851660048201529091506001600160a01b038216906314b4df4c90602401610790565b60009182526001602090815260408084206001600160a01b0393909316845291905290205460ff1690565b3360008051602061256a83398151915214610e60573360008051602061256a8339815191526040516359afe8af60e11b8152600401610559929190611d78565b610e686114ad565b6000610e7e60036001600160401b0385166114d3565b60405163fe0dd37160e01b81529091506001600160a01b0382169063fe0dd37190610ead908590600401611e93565b600060405180830381600087803b158015610ec757600080fd5b505af1158015610edb573d6000803e3d6000fd5b50505050826001600160401b03167febc442018e78570634a88dbac83d4677ae1a5cf8491fb0fc166a7306ba1ff051836040516107389190611e93565b3360008051602061256a83398151915214610f58573360008051602061256a8339815191526040516359afe8af60e11b8152600401610559929190611d78565b610f606114ad565b6000610f7660036001600160401b038c166114d3565b604051631be14b3160e11b81529091506001600160a01b038216906337c2966290610fb3908c908c908c908c908c908c908c908c9060040161204a565b600060405180830381600087803b158015610fcd57600080fd5b505af1158015610fe1573d6000803e3d6000fd5b50505050886001600160401b03168a6001600160401b03167f08e18861659838cad8b04f4687aca1dfb93336e750acf5c3699637ed8ca4c27a8a8a8a60405161102c939291906120b6565b60405180910390a350505050505050505050565b3360008051602061256a83398151915214611080573360008051602061256a8339815191526040516359afe8af60e11b8152600401610559929190611d78565b6110886114ad565b6110ae6001600160401b0384166110a560408401602085016120ea565b600391906115d9565b506110c460066001600160401b038516846115d9565b50826001600160401b0316826001600160a01b03167f278018182b7c51e2da72c1f465c26c12d9606099af0f08db420378344b35222383604051611108919061211b565b60405180910390a3505050565b3360008051602061256a83398151915214611155573360008051602061256a8339815191526040516359afe8af60e11b8152600401610559929190611d78565b61115d6114ad565b600061117360036001600160401b0387166114d3565b60405163bb43abd960e01b81529091506001600160a01b0382169063bb43abd9906111a6908790879087906004016122be565b600060405180830381600087803b1580156111c057600080fd5b505af11580156111d4573d6000803e3d6000fd5b50505050826001600160401b0316856001600160401b03167f219c2fdfbe4f111dda584eefbcb4eb2370cec850e2a79e4fed3a8f708006e7a9868560405161121d9291906122f3565b60405180910390a35050505050565b3360008051602061256a8339815191521461126c573360008051602061256a8339815191526040516359afe8af60e11b8152600401610559929190611d78565b6112746114ad565b600061128a60036001600160401b0389166114d3565b60405163e926cbd160e01b81529091506001600160a01b0382169063e926cbd1906112c19089908990899089908990600401611ec3565b600060405180830381600087803b1580156112db57600080fd5b505af11580156112ef573d6000803e3d6000fd5b50505050856001600160401b0316876001600160401b03167f0145a8acdb5e19e431ea924e2c8997b01f0869cad2a2c6927c8645a5938cf0c3878787876040516109159493929190611efd565b60008281526001602081905260409091200154611358816114e6565b6107ff838361156c565b3360008051602061256a833981519152146113a2573360008051602061256a8339815191526040516359afe8af60e11b8152600401610559929190611d78565b6113aa6114ad565b60006113c060036001600160401b0385166114d3565b604051638b24806560e01b81529091506001600160a01b03821690638b248065906113ef90859060040161242d565b600060405180830381600087803b15801561140957600080fd5b505af115801561141d573d6000803e3d6000fd5b506114329250505060408301602084016120ea565b6001600160a01b03166114486020840184612523565b6001600160401b039081169085167f4def5362750954077cbc9dad1ab88058dd6d0ed894ba05f8f147a7a2fe204b2261148760c0870160a08801612523565b8660c0018761010001356040516114a09392919061253e565b60405180910390a4505050565b60025460ff16156114d15760405163d93c066560e01b815260040160405180910390fd5b565b60006114df83836115ef565b9392505050565b6114f08133611636565b50565b60006114ff8383610df5565b6115645760008381526001602081815260408084206001600160a01b0387168086529252808420805460ff19169093179092559051339286917f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d9190a4506001610513565b506000610513565b60006115788383610df5565b156115645760008381526001602090815260408083206001600160a01b0386168085529252808320805460ff1916905551339286917ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9190a4506001610513565b60006107d184846001600160a01b038516611673565b60008181526002830160205260408120548015801561161557506116138484611690565b155b156114df5760405163015ab34360e11b815260048101849052602401610559565b6116408282610df5565b61166f5760405163e2517d3f60e01b81526001600160a01b038216600482015260248101839052604401610559565b5050565b600082815260028401602052604081208290556107d1848461169c565b60006114df83836116a8565b60006114df83836116c0565b600081815260018301602052604081205415156114df565b600081815260018301602052604081205461156457508154600181810184556000848152602080822090930184905584548482528286019093526040902091909155610513565b60006020828403121561171957600080fd5b81356001600160e01b0319811681146114df57600080fd5b80356001600160401b038116811461174857600080fd5b919050565b6001600160a01b03811681146114f057600080fd5b80356117488161174d565b60008060006060848603121561178257600080fd5b61178b84611731565b925061179960208501611731565b915060408401356117a98161174d565b809150509250925092565b600060c082840312156117c657600080fd5b50919050565b600080604083850312156117df57600080fd5b6117e883611731565b915060208301356001600160401b0381111561180357600080fd5b61180f858286016117b4565b9150509250929050565b60006020828403121561182b57600080fd5b5035919050565b6000806040838503121561184557600080fd5b61184e83611731565b915061185c60208401611731565b90509250929050565b6000806040838503121561187857600080fd5b82359150602083013561188a8161174d565b809150509250929050565b60008083601f8401126118a757600080fd5b5081356001600160401b038111156118be57600080fd5b6020830191508360208285010111156118d657600080fd5b9250929050565b803560ff8116811461174857600080fd5b60008060008060008060a0878903121561190757600080fd5b61191087611731565b955061191e60208801611731565b945060408701356001600160401b0381111561193957600080fd5b61194589828a01611895565b90955093506119589050606088016118dd565b9150608087013590509295509295509295565b60008060006060848603121561198057600080fd5b61198984611731565b925060208401356001600160401b038111156119a457600080fd5b6119b0868287016117b4565b9250506119bf60408501611731565b90509250925092565b60008060008060008060a087890312156119e157600080fd5b6119ea87611731565b95506119f860208801611731565b9450611a06604088016118dd565b9350611a1460608801611731565b925060808701356001600160401b03811115611a2f57600080fd5b611a3b89828a01611895565b979a9699509497509295939492505050565b60008060008060608587031215611a6357600080fd5b611a6c85611731565b935060208501356001600160401b0380821115611a8857600080fd5b611a94888389016117b4565b94506040870135915080821115611aaa57600080fd5b50611ab787828801611895565b95989497509550505050565b600080600080600080600060c0888a031215611ade57600080fd5b611ae788611731565b9650611af560208901611731565b9550611b0360408901611731565b94506060880135611b138161174d565b935060808801356001600160401b0380821115611b2f57600080fd5b818a0191508a601f830112611b4357600080fd5b813581811115611b5257600080fd5b8b60208260051b8501011115611b6757600080fd5b602083019550809450505050611b7f60a08901611731565b905092959891949750929550565b600080600080600080600080600060e08a8c031215611bab57600080fd5b611bb48a611731565b9850611bc260208b01611731565b9750611bd060408b016118dd565b9650611bde60608b01611731565b955060808a01356001600160401b0380821115611bfa57600080fd5b611c068d838e016117b4565b965060a08c0135915080821115611c1c57600080fd5b611c288d838e01611895565b909650945060c08c0135915080821115611c4157600080fd5b50611c4e8c828d01611895565b915080935050809150509295985092959850929598565b600080600060608486031215611c7a57600080fd5b611c8384611731565b92506020840135611c938161174d565b915060408401356001600160401b03811115611cae57600080fd5b8401606081870312156117a957600080fd5b60008060008060808587031215611cd657600080fd5b611cdf85611731565b935060208501356001600160401b03811115611cfa57600080fd5b611d06878288016117b4565b935050611d1560408601611731565b9150611d23606086016118dd565b905092959194509250565b60008060408385031215611d4157600080fd5b611d4a83611731565b915060208301356001600160401b03811115611d6557600080fd5b8301610120818603121561188a57600080fd5b6001600160a01b0392831681529116602082015260400190565b6000808335601e19843603018112611da957600080fd5b83016020810192503590506001600160401b03811115611dc857600080fd5b8036038213156118d657600080fd5b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b6000611e0c8283611d92565b60c08552611e1e60c086018284611dd7565b9150506001600160401b0380611e3660208601611731565b16602086015280611e4960408601611731565b16604086015280611e5c60608601611731565b16606086015280611e6f60808601611731565b16608086015280611e8260a08601611731565b1660a0860152508091505092915050565b6020815260006114df6020830184611e00565b600060208284031215611eb857600080fd5b81516114df8161174d565b6001600160401b0386168152608060208201526000611ee6608083018688611dd7565b60ff94909416604083015250606001529392505050565b606081526000611f11606083018688611dd7565b60ff949094166020830152506040015292915050565b604081526000611f3a6040830185611e00565b90506001600160401b03831660208301529392505050565b60006001600160401b03808816835260ff8716602084015280861660408401525060806060830152611f88608083018486611dd7565b979650505050505050565b604081526000611fa66040830186611e00565b8281036020840152611fb9818587611dd7565b9695505050505050565b6001600160401b0387811682528681166020808401919091526001600160a01b03878116604085015260a06060850181905284018690526000928792909160c08601855b8981101561202e57853561201a8161174d565b831682529483019490830190600101612007565b5080955050505080851660808501525050979650505050505050565b60006001600160401b03808b16835260ff8a16602084015280891660408401525060c0606083015261207f60c0830188611e00565b8281036080840152612092818789611dd7565b905082810360a08401526120a7818587611dd7565b9b9a5050505050505050505050565b60ff841681526001600160401b03831660208201526060604082015260006120e16060830184611e00565b95945050505050565b6000602082840312156120fc57600080fd5b81356114df8161174d565b803563ffffffff8116811461174857600080fd5b602081526000823560fe1984360301811261213557600080fd5b6060602084015283016121488180611d92565b61010080608087015261216061018087018385611dd7565b925061216f6020850185611d92565b9250607f19808886030160a0890152612189858584611dd7565b94506121986040870187611d92565b94509150808886030160c08901526121b1858584611dd7565b94506121c06060870187611d92565b94509150808886030160e08901526121d9858584611dd7565b94506121e86080870187611d92565b94509150808886030183890152612200858584611dd7565b945061220f60a0870187611d92565b945092508088860301610120890152612229858585611dd7565b945061223860c0870187611d92565b945092508088860301610140890152612252858585611dd7565b945061226160e0870187611d92565b96509350808886030161016089015250505061227e828483611dd7565b9250505061228e60208501611762565b6001600160a01b0381166040850152506122aa60408501612107565b63ffffffff81166060850152509392505050565b6060815260006122d16060830186611e00565b90506001600160401b038416602083015260ff83166040830152949350505050565b6040815260006123066040830185611e00565b905060ff831660208301529392505050565b6000808335601e1984360301811261232f57600080fd5b83016020810192503590506001600160401b0381111561234e57600080fd5b8060051b36038213156118d657600080fd5b81835260006020808501808196508560051b81019150846000805b888110156123ba578385038a52823560be1989360301811261239b578283fd5b6123a7868a8301611e00565b9a87019a9550509185019160010161237b565b509298975050505050505050565b8183526000602080850194508260005b858110156124065781356123eb8161174d565b6001600160a01b0316875295820195908201906001016123d8565b509495945050505050565b80356002811061242057600080fd5b8252602090810135910152565b602081526001600160401b0361244283611731565b166020820152600061245660208401611762565b6001600160a01b0381166040840152506124736040840184612318565b61012080606086015261248b61014086018385612360565b925061249a6060870187611d92565b9250601f19808786030160808801526124b4858584611dd7565b94506124c36080890189612318565b94509150808786030160a0880152506124dd8484836123c8565b9350506124ec60a08701611731565b6001600160401b03811660c0870152915061250d60e0860160c08801612411565b6101008601358186015250508091505092915050565b60006020828403121561253557600080fd5b6114df82611731565b6001600160401b03841681526080810161255b6020830185612411565b82606083015294935050505056fe0000000000000000000000001111111111111111111111111111111111111111a164736f6c6343000814000a +0x6080604052600436106102465760003560e01c80637e098ee311610139578063a6a78538116100b6578063d547741f1161007a578063d547741f1461067c578063d90a65471461069c578063e32916d0146106d0578063e63ab1e9146106f0578063ea1f48fa14610724578063f9e138451461074457600080fd5b8063a6a7853814610610578063a8f7c5ed14610623578063b89af90414610636578063b9315d3a14610649578063c97008e01461065c57600080fd5b80638e6f8c60116100fd5780638e6f8c601461057b57806391d148541461059b578063987ab9db146105bb578063a217fddf146105dd578063a4d91fe9146105f257600080fd5b80637e098ee3146104f057806382a14aad1461051057806382a1ece4146105305780638456cb5914610543578063884673ac1461055857600080fd5b80632f2ff15d116101c7578063459ba22c1161018b578063459ba22c146104695780634984ee6b146104895780635c975abb1461049c57806362f3765e146104b4578063727391f2146104dd57600080fd5b80632f2ff15d146103d457806333db3920146103f457806336568abe146104145780633f4ba83a14610434578063410fdec81461044957600080fd5b80631fb27b341161020e5780631fb27b3414610311578063216e804214610331578063248a9ca31461034757806326c2596214610386578063274ef015146103b457600080fd5b806301ffc9a71461024b5780630633da77146102805780630d0dd399146102a257806319413268146102da5780631948fdbc146102fa575b600080fd5b34801561025757600080fd5b5061026b610266366004612738565b610757565b60405190151581526020015b60405180910390f35b34801561028c57600080fd5b506102a061029b3660046127a2565b61078e565b005b3480156102ae57600080fd5b506000546102c2906001600160a01b031681565b6040516001600160a01b039091168152602001610277565b3480156102e657600080fd5b5061026b6102f5366004612805565b6108b7565b34801561030657600080fd5b506102c2627e87d581565b34801561031d57600080fd5b506102a061032c366004612866565b610949565b34801561033d57600080fd5b50627e87d56102c2565b34801561035357600080fd5b506103786103623660046128b5565b6000908152600160208190526040909120015490565b604051908152602001610277565b34801561039257600080fd5b506103a66103a13660046128b5565b610a50565b6040516102779291906128e4565b3480156103c057600080fd5b506102c26103cf36600461290b565b610a7e565b3480156103e057600080fd5b506102a06103ef366004612944565b610b12565b34801561040057600080fd5b506102a061040f3660046129c2565b610b3e565b34801561042057600080fd5b506102a061042f366004612944565b610c61565b34801561044057600080fd5b506102a0610c99565b34801561045557600080fd5b506102a0610464366004612a43565b610cd6565b34801561047557600080fd5b506102a0610484366004612805565b610e58565b6102a0610497366004612a9b565b610ed7565b3480156104a857600080fd5b5060025460ff1661026b565b3480156104c057600080fd5b506104ca61271081565b60405161ffff9091168152602001610277565b6102a06104eb366004612b26565b610fe9565b3480156104fc57600080fd5b50600b546102c2906001600160a01b031681565b34801561051c57600080fd5b506102a061052b366004612b9e565b6110f6565b6102a061053e366004612c12565b61112a565b34801561054f57600080fd5b506102a0611555565b34801561056457600080fd5b506102c26b6d6f646c536572766963657360401b81565b34801561058757600080fd5b506102c261059636600461290b565b61158f565b3480156105a757600080fd5b5061026b6105b6366004612944565b6115de565b3480156105c757600080fd5b506b6d6f646c536572766963657360401b6102c2565b3480156105e957600080fd5b50610378600081565b3480156105fe57600080fd5b506000546001600160a01b03166102c2565b6102a061061e366004612866565b611609565b6102a0610631366004612ce2565b611703565b6102a0610644366004612dbe565b61182d565b6102a0610657366004612e1b565b611904565b34801561066857600080fd5b506102a06106773660046129c2565b611a1d565b34801561068857600080fd5b506102a0610697366004612944565b611b2f565b3480156106a857600080fd5b506103787f682886e4aa036ce8b1761850f768c4604676f1856f596f453ecc49ee4d73545481565b3480156106dc57600080fd5b506102a06106eb366004612805565b611b55565b3480156106fc57600080fd5b506103787f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a81565b34801561073057600080fd5b5061026b61073f366004612805565b611b9c565b6102a0610752366004612e8d565b611be4565b60006001600160e01b03198216637965db0b60e01b148061078857506301ffc9a760e01b6001600160e01b03198316145b92915050565b336b6d6f646c536572766963657360401b146107d957336b6d6f646c536572766963657360401b6040516359afe8af60e11b81526004016107d0929190612ed9565b60405180910390fd5b6107e1611d6b565b60006107f760036001600160401b038616611d91565b604051630a24e8a960e41b81526001600160401b03851660048201526001600160a01b0384811660248301529192509082169063a24e8a9090604401600060405180830381600087803b15801561084d57600080fd5b505af1158015610861573d6000803e3d6000fd5b50506040516001600160a01b03851681526001600160401b038087169350871691507f5267932e6c1b678e74efba1e4d6c8b3fd7504cf0fa8eea462efac2c590a21d56906020015b60405180910390a350505050565b6000806108ce60036001600160401b038716611d91565b6040516361545fdd60e01b81529091506001600160a01b038216906361545fdd906108ff9087908790600401613006565b602060405180830381865afa15801561091c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109409190613028565b95945050505050565b336b6d6f646c536572766963657360401b1461098b57336b6d6f646c536572766963657360401b6040516359afe8af60e11b81526004016107d0929190612ed9565b610993611d6b565b60006109a960036001600160401b038516611d91565b60405163434698bb60e01b81529091506001600160a01b0382169063434698bb906109d890859060040161304a565b600060405180830381600087803b1580156109f257600080fd5b505af1158015610a06573d6000803e3d6000fd5b50505050826001600160401b03167fde25c2b146db36cbc91715fdf29cb0eb556eed7678e0bf13563bf7050e8f0bbd83604051610a43919061304a565b60405180910390a2505050565b600a8181548110610a6057600080fd5b60009182526020909120015460ff81169150610100900461ffff1682565b600080610a9560036001600160401b038616611d91565b6040516374ceeb5560e01b81526001600160401b03851660048201529091506001600160a01b038216906374ceeb55906024015b602060405180830381865afa158015610ae6573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b0a919061305d565b949350505050565b60008281526001602081905260409091200154610b2e81611da4565b610b388383611dae565b50505050565b336b6d6f646c536572766963657360401b14610b8057336b6d6f646c536572766963657360401b6040516359afe8af60e11b81526004016107d0929190612ed9565b610b88611d6b565b6000610b9e60036001600160401b038916611d91565b604051630af7d74360e01b81529091506001600160a01b03821690630af7d74390610bd5908990899089908990899060040161307a565b600060405180830381600087803b158015610bef57600080fd5b505af1158015610c03573d6000803e3d6000fd5b50505050856001600160401b0316876001600160401b03167f7d2ac4c1544e07516def74ce3ada12ff791868ed07d6da82694ae2c0342c837187878787604051610c5094939291906130b4565b60405180910390a350505050505050565b6001600160a01b0381163314610c8a5760405163334bd91960e11b815260040160405180910390fd5b610c948282611e27565b505050565b7f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a610cc381611da4565b610ccb611e94565b610cd3611eb7565b50565b336b6d6f646c536572766963657360401b14610d1857336b6d6f646c536572766963657360401b6040516359afe8af60e11b81526004016107d0929190612ed9565b610d20611d6b565b6000610d3660036001600160401b038616611d91565b60405163410e3df160e11b81529091506001600160a01b0382169063821c7be290610d6790869086906004016130de565b600060405180830381600087803b158015610d8157600080fd5b505af1158015610d95573d6000803e3d6000fd5b5050506001600160401b038316600090815260096020526040812080546001600160e01b03191681559150610dcd6001830182612636565b610ddb600283016000612657565b610de9600383016000612691565b5060048101805467ffffffffffffffff1916905560058101805460ff191690556000600682018190556007909101556040516001600160401b0383811691908616907f4ca48d7cb411035a931da7a686ea77bc413069de12d844c47daf9242b765cba4906108a990879061304a565b6000610e6e60036001600160401b038616611d91565b6040516356d301c160e11b81529091506001600160a01b0382169063ada6038290610e9f9086908690600401613006565b600060405180830381600087803b158015610eb957600080fd5b505af1158015610ecd573d6000803e3d6000fd5b5050505050505050565b336b6d6f646c536572766963657360401b14610f1957336b6d6f646c536572766963657360401b6040516359afe8af60e11b81526004016107d0929190612ed9565b610f21611d6b565b6000610f3760036001600160401b038916611d91565b604051639838caa360e01b81529091506001600160a01b03821690639838caa390610f6e9089908990899089908990600401613109565b600060405180830381600087803b158015610f8857600080fd5b505af1158015610f9c573d6000803e3d6000fd5b50506040805160ff891681526001600160401b038881166020830152808b1694508b1692507fa9aaaef624d196d961d053fc29d4331e5cb45d62195d99814d29a94f46fa9d4b9101610c50565b336b6d6f646c536572766963657360401b1461102b57336b6d6f646c536572766963657360401b6040516359afe8af60e11b81526004016107d0929190612ed9565b611033611d6b565b600061104960036001600160401b038716611d91565b604051634e82087760e11b81529091506001600160a01b03821690639d0410ee9061107c9087908790879060040161314a565b600060405180830381600087803b15801561109657600080fd5b505af11580156110aa573d6000803e3d6000fd5b50505050846001600160401b03167f7814301100cb38a35027dd63a29aa14c3f73188f15cf9f027655257c45016e87856040516110e7919061304a565b60405180910390a25050505050565b7f682886e4aa036ce8b1761850f768c4604676f1856f596f453ecc49ee4d73545461112081611da4565b610c948383611f09565b336b6d6f646c536572766963657360401b1461116c57336b6d6f646c536572766963657360401b6040516359afe8af60e11b81526004016107d0929190612ed9565b611174611d6b565b600061118a60036001600160401b038a16611d91565b6001600160401b03888116600090815260096020908152604080832081516101008101835281549586168152600160401b9095046001600160a01b0316858401526001810180548351818602810186018552818152979850949691949286019391929091879084015b8282101561131857838290600052602060002090600302016040518060400160405290816000820180546112269061317a565b80601f01602080910402602001604051908101604052809291908181526020018280546112529061317a565b801561129f5780601f106112745761010080835404028352916020019161129f565b820191906000526020600020905b81548152906001019060200180831161128257829003601f168201915b50505091835250506040805160a0810182526001848101546001600160401b038082168452600160401b82048116602085810191909152600160801b8304821695850195909552600160c01b909104811660608401526002909501549094166080820152918101919091529183529290920191016111f3565b5050505081526020016002820180546113309061317a565b80601f016020809104026020016040519081016040528092919081815260200182805461135c9061317a565b80156113a95780601f1061137e576101008083540402835291602001916113a9565b820191906000526020600020905b81548152906001019060200180831161138c57829003601f168201915b505050505081526020016003820180548060200260200160405190810160405280929190818152602001828054801561140b57602002820191906000526020600020905b81546001600160a01b031681526001909101906020018083116113ed575b505050918352505060048201546001600160401b0316602082015260408051808201825260058401805492909301929091829060ff166001811115611452576114526128ce565b6001811115611463576114636128ce565b81526020016001820154815250508152602001600782015481525050905061148c828883611fd0565b604051636bef5a4160e11b81526001600160a01b0383169063d7deb482906114c2908b908b908b908b908b908b906004016131ae565b600060405180830381600087803b1580156114dc57600080fd5b505af11580156114f0573d6000803e3d6000fd5b5050604080516001600160a01b038a1681526001600160401b038781166020830152808c1694508c811693508d16917f93927390d124732c66289c6e22370514ddd612a9cc6bff5671cbf2cbc15ddfe3910160405180910390a4505050505050505050565b7f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a61157f81611da4565b611587611d6b565b610cd36122c2565b6000806115a660036001600160401b038616611d91565b60405163052d37d360e21b81526001600160401b03851660048201529091506001600160a01b038216906314b4df4c90602401610ac9565b60009182526001602090815260408084206001600160a01b0393909316845291905290205460ff1690565b336b6d6f646c536572766963657360401b1461164b57336b6d6f646c536572766963657360401b6040516359afe8af60e11b81526004016107d0929190612ed9565b611653611d6b565b600061166960036001600160401b038516611d91565b60405163fe0dd37160e01b81529091506001600160a01b0382169063fe0dd3719061169890859060040161304a565b600060405180830381600087803b1580156116b257600080fd5b505af11580156116c6573d6000803e3d6000fd5b50505050826001600160401b03167febc442018e78570634a88dbac83d4677ae1a5cf8491fb0fc166a7306ba1ff05183604051610a43919061304a565b336b6d6f646c536572766963657360401b1461174557336b6d6f646c536572766963657360401b6040516359afe8af60e11b81526004016107d0929190612ed9565b61174d611d6b565b600061176360036001600160401b038c16611d91565b604051631be14b3160e11b81529091506001600160a01b038216906337c29662906117a0908c908c908c908c908c908c908c908c90600401613235565b600060405180830381600087803b1580156117ba57600080fd5b505af11580156117ce573d6000803e3d6000fd5b50505050886001600160401b03168a6001600160401b03167f08e18861659838cad8b04f4687aca1dfb93336e750acf5c3699637ed8ca4c27a8a8a8a604051611819939291906132a1565b60405180910390a350505050505050505050565b336b6d6f646c536572766963657360401b1461186f57336b6d6f646c536572766963657360401b6040516359afe8af60e11b81526004016107d0929190612ed9565b611877611d6b565b61189d6001600160401b03841661189460408401602085016132cc565b600391906122ff565b506118b360066001600160401b038516846122ff565b50826001600160401b0316826001600160a01b03167f278018182b7c51e2da72c1f465c26c12d9606099af0f08db420378344b352223836040516118f791906132fd565b60405180910390a3505050565b336b6d6f646c536572766963657360401b1461194657336b6d6f646c536572766963657360401b6040516359afe8af60e11b81526004016107d0929190612ed9565b61194e611d6b565b600061196460036001600160401b038716611d91565b60405163bb43abd960e01b81529091506001600160a01b0382169063bb43abd990611997908790879087906004016134a0565b600060405180830381600087803b1580156119b157600080fd5b505af11580156119c5573d6000803e3d6000fd5b50505050826001600160401b0316856001600160401b03167f219c2fdfbe4f111dda584eefbcb4eb2370cec850e2a79e4fed3a8f708006e7a98685604051611a0e9291906134d5565b60405180910390a35050505050565b336b6d6f646c536572766963657360401b14611a5f57336b6d6f646c536572766963657360401b6040516359afe8af60e11b81526004016107d0929190612ed9565b611a67611d6b565b6000611a7d60036001600160401b038916611d91565b60405163e926cbd160e01b81529091506001600160a01b0382169063e926cbd190611ab4908990899089908990899060040161307a565b600060405180830381600087803b158015611ace57600080fd5b505af1158015611ae2573d6000803e3d6000fd5b50505050856001600160401b0316876001600160401b03167f0145a8acdb5e19e431ea924e2c8997b01f0869cad2a2c6927c8645a5938cf0c387878787604051610c5094939291906130b4565b60008281526001602081905260409091200154611b4b81611da4565b610b388383611e27565b6000611b6b60036001600160401b038616611d91565b6040516346c578a560e01b81529091506001600160a01b038216906346c578a590610e9f9086908690600401613006565b600080611bb360036001600160401b038716611d91565b604051630eb5e5eb60e31b81529091506001600160a01b038216906375af2f58906108ff9087908790600401613006565b336b6d6f646c536572766963657360401b14611c2657336b6d6f646c536572766963657360401b6040516359afe8af60e11b81526004016107d0929190612ed9565b611c2e611d6b565b6000611c4460036001600160401b038516611d91565b604051638b24806560e01b81529091506001600160a01b03821690638b24806590611c73908590600401613628565b600060405180830381600087803b158015611c8d57600080fd5b505af1158015611ca1573d6000803e3d6000fd5b508492506009915060009050611cba6020840184613722565b6001600160401b031681526020810191909152604001600020611cdd8282613d15565b50611cf0905060408301602084016132cc565b6001600160a01b0316611d066020840184613722565b6001600160401b039081169085167f4def5362750954077cbc9dad1ab88058dd6d0ed894ba05f8f147a7a2fe204b22611d4560c0870160a08801613722565b8660c001876101000135604051611d5e93929190613e1b565b60405180910390a4505050565b60025460ff1615611d8f5760405163d93c066560e01b815260040160405180910390fd5b565b6000611d9d8383612315565b9392505050565b610cd3813361235c565b6000611dba83836115de565b611e1f5760008381526001602081815260408084206001600160a01b0387168086529252808420805460ff19169093179092559051339286917f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d9190a4506001610788565b506000610788565b6000611e3383836115de565b15611e1f5760008381526001602090815260408083206001600160a01b0386168085529252808320805460ff1916905551339286917ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9190a4506001610788565b60025460ff16611d8f57604051638dfc202b60e01b815260040160405180910390fd5b611ebf611e94565b6002805460ff191690557f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b6040516001600160a01b03909116815260200160405180910390a1565b611f648282808060200260200160405190810160405280939291908181526020016000905b82821015611f5a57611f4b60408302860136819003810190613e63565b81526020019060010190611f2e565b5050505050612399565b611f70600a60006126af565b60005b81811015610c9457600a838383818110611f8f57611f8f613ec1565b83546001810185556000948552602090942060409091029290920192919091019050611fbb8282613ed7565b50508080611fc890613f2d565b915050611f73565b60e08101516000819003611fe45750505050565b60008060008060005b600a5481101561211e576000600a828154811061200c5761200c613ec1565b600091825260209091206040805180820190915291018054829060ff16600381111561203a5761203a6128ce565b600381111561204b5761204b6128ce565b8152905461ffff6101009091048116602092830152908201519192506000916127109161207991168a6137e1565b6120839190613f46565b905060008251600381111561209a5761209a6128ce565b036120a757809650612109565b6001825160038111156120bc576120bc6128ce565b036120c957809550612109565b6002825160038111156120de576120de6128ce565b036120eb57809450612109565b600382516003811115612100576121006128ce565b03612109578093505b5050808061211690613f2d565b915050611fed565b50600061212b8284613f68565b6040516308179f3560e01b81526001600160401b038a1660048201529091506000906001600160a01b038b16906308179f3590602401602060405180830381865afa15801561217e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906121a2919061305d565b90506121b18860c0015161240a565b1561225e576040516001600160a01b0382169087156108fc029088906000818181858888f193505050501580156121ec573d6000803e3d6000fd5b50600b546040516001600160a01b039091169086156108fc029087906000818181858888f19350505050158015612227573d6000803e3d6000fd5b50604051627e87d59083156108fc029084906000818181858888f19350505050158015612258573d6000803e3d6000fd5b506122b6565b600061226d8960c00151612450565b90506122836001600160a01b03821683896124b8565b600b5461229d906001600160a01b038381169116886124b8565b6122b46001600160a01b038216627e87d5856124b8565b505b50505050505050505050565b6122ca611d6b565b6002805460ff191660011790557f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a258611eec3390565b6000610b0a84846001600160a01b03851661250a565b60008181526002830160205260408120548015801561233b57506123398484612527565b155b15611d9d5760405163015ab34360e11b8152600481018490526024016107d0565b61236682826115de565b6123955760405163e2517d3f60e01b81526001600160a01b0382166004820152602481018390526044016107d0565b5050565b6000805b82518110156123e3578281815181106123b8576123b8613ec1565b602002602001015160200151826123cf9190613f7b565b9150806123db81613f2d565b91505061239d565b5061ffff811661271014612395576040516304be90a960e51b815260040160405180910390fd5b600061241582612533565b1561242c5750602001516001600160a01b03161590565b61243582612552565b156124435750602001511590565b506000919050565b919050565b600061245b82612533565b1561246857506020015190565b61247182612552565b1561248957602082015163ffffffff60801b17610788565b8151600181111561249c5761249c6128ce565b6040516375458b5d60e11b81526004016107d091815260200190565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b179052610c9490849061255a565b60008281526002840160205260408120829055610b0a84846125cb565b6000611d9d83836125d7565b600060015b8251600181111561254b5761254b6128ce565b1492915050565b600080612538565b600080602060008451602086016000885af18061257d576040513d6000823e3d81fd5b50506000513d915081156125955780600114156125a2565b6001600160a01b0384163b155b15610b3857604051635274afe760e01b81526001600160a01b03851660048201526024016107d0565b6000611d9d83836125ef565b60008181526001830160205260408120541515611d9d565b6000818152600183016020526040812054611e1f57508154600181810184556000848152602080822090930184905584548482528286019093526040902091909155610788565b5080546000825560030290600052602060002090810190610cd391906126cd565b5080546126639061317a565b6000825580601f10612673575050565b601f016020900490600052602060002090810190610cd39190612708565b5080546000825590600052602060002090810190610cd39190612708565b5080546000825590600052602060002090810190610cd3919061271d565b808211156127045760006126e18282612657565b506000600182015560028101805467ffffffffffffffff191690556003016126cd565b5090565b5b808211156127045760008155600101612709565b5b8082111561270457805462ffffff1916815560010161271e565b60006020828403121561274a57600080fd5b81356001600160e01b031981168114611d9d57600080fd5b6001600160401b0381168114610cd357600080fd5b803561244b81612762565b6001600160a01b0381168114610cd357600080fd5b803561244b81612782565b6000806000606084860312156127b757600080fd5b83356127c281612762565b925060208401356127d281612762565b915060408401356127e281612782565b809150509250925092565b600060c082840312156127ff57600080fd5b50919050565b60008060006060848603121561281a57600080fd5b833561282581612762565b9250602084013561283581612762565b915060408401356001600160401b0381111561285057600080fd5b61285c868287016127ed565b9150509250925092565b6000806040838503121561287957600080fd5b823561288481612762565b915060208301356001600160401b0381111561289f57600080fd5b6128ab858286016127ed565b9150509250929050565b6000602082840312156128c757600080fd5b5035919050565b634e487b7160e01b600052602160045260246000fd5b60408101600484106128f8576128f86128ce565b92815261ffff9190911660209091015290565b6000806040838503121561291e57600080fd5b823561292981612762565b9150602083013561293981612762565b809150509250929050565b6000806040838503121561295757600080fd5b82359150602083013561293981612782565b60008083601f84011261297b57600080fd5b5081356001600160401b0381111561299257600080fd5b6020830191508360208285010111156129aa57600080fd5b9250929050565b803560ff8116811461244b57600080fd5b60008060008060008060a087890312156129db57600080fd5b86356129e681612762565b955060208701356129f681612762565b945060408701356001600160401b03811115612a1157600080fd5b612a1d89828a01612969565b9095509350612a309050606088016129b1565b9150608087013590509295509295509295565b600080600060608486031215612a5857600080fd5b8335612a6381612762565b925060208401356001600160401b03811115612a7e57600080fd5b612a8a868287016127ed565b92505060408401356127e281612762565b60008060008060008060a08789031215612ab457600080fd5b8635612abf81612762565b95506020870135612acf81612762565b9450612add604088016129b1565b93506060870135612aed81612762565b925060808701356001600160401b03811115612b0857600080fd5b612b1489828a01612969565b979a9699509497509295939492505050565b60008060008060608587031215612b3c57600080fd5b8435612b4781612762565b935060208501356001600160401b0380821115612b6357600080fd5b612b6f888389016127ed565b94506040870135915080821115612b8557600080fd5b50612b9287828801612969565b95989497509550505050565b60008060208385031215612bb157600080fd5b82356001600160401b0380821115612bc857600080fd5b818501915085601f830112612bdc57600080fd5b813581811115612beb57600080fd5b8660208260061b8501011115612c0057600080fd5b60209290920196919550909350505050565b600080600080600080600060c0888a031215612c2d57600080fd5b8735612c3881612762565b96506020880135612c4881612762565b95506040880135612c5881612762565b94506060880135612c6881612782565b935060808801356001600160401b0380821115612c8457600080fd5b818a0191508a601f830112612c9857600080fd5b813581811115612ca757600080fd5b8b60208260051b8501011115612cbc57600080fd5b602083019550809450505050612cd460a08901612777565b905092959891949750929550565b600080600080600080600080600060e08a8c031215612d0057600080fd5b8935612d0b81612762565b985060208a0135612d1b81612762565b9750612d2960408b016129b1565b9650612d3760608b01612777565b955060808a01356001600160401b0380821115612d5357600080fd5b612d5f8d838e016127ed565b965060a08c0135915080821115612d7557600080fd5b612d818d838e01612969565b909650945060c08c0135915080821115612d9a57600080fd5b50612da78c828d01612969565b915080935050809150509295985092959850929598565b600080600060608486031215612dd357600080fd5b8335612dde81612762565b92506020840135612dee81612782565b915060408401356001600160401b03811115612e0957600080fd5b8401606081870312156127e257600080fd5b60008060008060808587031215612e3157600080fd5b8435612e3c81612762565b935060208501356001600160401b03811115612e5757600080fd5b612e63878288016127ed565b9350506040850135612e7481612762565b9150612e82606086016129b1565b905092959194509250565b60008060408385031215612ea057600080fd5b8235612eab81612762565b915060208301356001600160401b03811115612ec657600080fd5b8301610120818603121561293957600080fd5b6001600160a01b0392831681529116602082015260400190565b6000808335601e19843603018112612f0a57600080fd5b83016020810192503590506001600160401b03811115612f2957600080fd5b8036038213156129aa57600080fd5b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b6000612f6d8283612ef3565b60c08552612f7f60c086018284612f38565b9150506020830135612f9081612762565b6001600160401b039081166020860152604084013590612faf82612762565b9081166040860152606084013590612fc682612762565b9081166060860152608084013590612fdd82612762565b908116608086015260a084013590612ff482612762565b1660a094909401939093525090919050565b6001600160401b0383168152604060208201526000610b0a6040830184612f61565b60006020828403121561303a57600080fd5b81518015158114611d9d57600080fd5b602081526000611d9d6020830184612f61565b60006020828403121561306f57600080fd5b8151611d9d81612782565b6001600160401b038616815260806020820152600061309d608083018688612f38565b60ff94909416604083015250606001529392505050565b6060815260006130c8606083018688612f38565b60ff949094166020830152506040015292915050565b6040815260006130f16040830185612f61565b90506001600160401b03831660208301529392505050565b60006001600160401b03808816835260ff871660208401528086166040840152506080606083015261313f608083018486612f38565b979650505050505050565b60408152600061315d6040830186612f61565b8281036020840152613170818587612f38565b9695505050505050565b600181811c9082168061318e57607f821691505b6020821081036127ff57634e487b7160e01b600052602260045260246000fd5b6001600160401b0387811682528681166020808401919091526001600160a01b03878116604085015260a06060850181905284018690526000928792909160c08601855b8981101561321957853561320581612782565b8316825294830194908301906001016131f2565b5080955050505080851660808501525050979650505050505050565b60006001600160401b03808b16835260ff8a16602084015280891660408401525060c0606083015261326a60c0830188612f61565b828103608084015261327d818789612f38565b905082810360a0840152613292818587612f38565b9b9a5050505050505050505050565b60ff841681526001600160401b03831660208201526060604082015260006109406060830184612f61565b6000602082840312156132de57600080fd5b8135611d9d81612782565b803563ffffffff8116811461244b57600080fd5b602081526000823560fe1984360301811261331757600080fd5b60606020840152830161332a8180612ef3565b61010080608087015261334261018087018385612f38565b92506133516020850185612ef3565b9250607f19808886030160a089015261336b858584612f38565b945061337a6040870187612ef3565b94509150808886030160c0890152613393858584612f38565b94506133a26060870187612ef3565b94509150808886030160e08901526133bb858584612f38565b94506133ca6080870187612ef3565b945091508088860301838901526133e2858584612f38565b94506133f160a0870187612ef3565b94509250808886030161012089015261340b858585612f38565b945061341a60c0870187612ef3565b945092508088860301610140890152613434858585612f38565b945061344360e0870187612ef3565b965093508088860301610160890152505050613460828483612f38565b9250505061347060208501612797565b6001600160a01b03811660408501525061348c604085016132e9565b63ffffffff81166060850152509392505050565b6060815260006134b36060830186612f61565b90506001600160401b038416602083015260ff83166040830152949350505050565b6040815260006134e86040830185612f61565b905060ff831660208301529392505050565b6000808335601e1984360301811261351157600080fd5b83016020810192503590506001600160401b0381111561353057600080fd5b8060051b36038213156129aa57600080fd5b81835260006020808501808196508560051b81019150846000805b8881101561359c578385038a52823560be1989360301811261357d578283fd5b613589868a8301612f61565b9a87019a9550509185019160010161355d565b509298975050505050505050565b8183526000602080850194508260005b858110156135e85781356135cd81612782565b6001600160a01b0316875295820195908201906001016135ba565b509495945050505050565b60028110610cd357600080fd5b803561360b816135f3565b6002811061361b5761361b6128ce565b8252602090810135910152565b602081526000823561363981612762565b6001600160401b03811660208401525061365560208401612797565b6001600160a01b03811660408401525061367260408401846134fa565b61012080606086015261368a61014086018385613542565b92506136996060870187612ef3565b9250601f19808786030160808801526136b3858584612f38565b94506136c260808901896134fa565b94509150808786030160a0880152506136dc8484836135aa565b9350506136eb60a08701612777565b6001600160401b03811660c0870152915061370c60e0860160c08801613600565b6101008601358186015250508091505092915050565b60006020828403121561373457600080fd5b8135611d9d81612762565b6000813561078881612762565b6000808335601e1984360301811261376357600080fd5b8301803591506001600160401b0382111561377d57600080fd5b6020019150600581901b36038213156129aa57600080fd5b6000823560be198336030181126137ab57600080fd5b9190910192915050565b634e487b7160e01b600052604160045260246000fd5b634e487b7160e01b600052601160045260246000fd5b8082028115828204841417610788576107886137cb565b5b8181101561239557600081556001016137f9565b6000808335601e1984360301811261382457600080fd5b8301803591506001600160401b0382111561383e57600080fd5b6020019150368190038213156129aa57600080fd5b601f821115610c9457806000526020600020601f840160051c8101602085101561387a5750805b61388c601f850160051c8301826137f8565b5050505050565b6001600160401b038311156138aa576138aa6137b5565b6138be836138b8835461317a565b83613853565b6000601f8411600181146138f257600085156138da5750838201355b600019600387901b1c1916600186901b17835561388c565b600083815260209020601f19861690835b828110156139235786850135825560209485019460019092019101613903565b50868210156139405760001960f88860031b161c19848701351681555b505060018560011b0183555050505050565b813561395d81612762565b815467ffffffffffffffff19166001600160401b03821617825550602082013561398681612762565b81546fffffffffffffffff0000000000000000604092831b166fffffffffffffffff000000000000000019821681178455918401356139c481612762565b67ffffffffffffffff60801b60809190911b1677ffffffffffffffffffffffffffffffff00000000000000001982168317811784556060850135613a0781612762565b6001600160401b0360c01b8160c01b16846001600160401b03851617831717855550505050612395613a3b6080840161373f565b600183016001600160401b0382166001600160401b03198254161781555050565b613a66828361380d565b6001600160401b03811115613a7d57613a7d6137b5565b613a9181613a8b855461317a565b85613853565b6000601f821160018114613ac55760008315613aad5750838201355b600019600385901b1c1916600184901b178555613b1f565b600085815260209020601f19841690835b82811015613af65786850135825560209485019460019092019101613ad6565b5084821015613b135760001960f88660031b161c19848701351681555b505060018360011b0185555b505050506123956020830160018301613952565b600160401b831115613b4757613b476137b5565b805483825580841015613c0a5760038181028181048314613b6a57613b6a6137cb565b8582028281048714613b7e57613b7e6137cb565b6000858152602081209283019291909101905b82821015613c0557613ba3825461317a565b8015613bed57601f80821160018114613bbe57838555613bea565b600085815260209020613bdb83850160051c8201600183016137f8565b50600085815260208120818755555b50505b50600060018301819055600283015590830190613b91565b505050505b5060008181526020812083915b85811015613c4857613c32613c2c8487613795565b83613a5c565b6020929092019160039190910190600101613c17565b505050505050565b6001600160401b03831115613c6757613c676137b5565b600160401b831115613c7b57613c7b6137b5565b805483825580841015613ca157816000526020600020613c9f8282018683016137f8565b505b50818160005260208060002060005b86811015613cd3578335613cc381612782565b8282015592820192600101613cb0565b50505050505050565b8135613ce7816135f3565b60028110613cf757613cf76128ce565b60ff1982541660ff8216811783555050602082013560018201555050565b8135613d2081612762565b815467ffffffffffffffff19166001600160401b038216178255506020820135613d4981612782565b815468010000000000000000600160e01b031916604091821b68010000000000000000600160e01b0316178255613d829083018361374c565b613d90818360018601613b33565b5050613d9f606083018361380d565b613dad818360028601613893565b5050613dbc608083018361374c565b613dca818360038601613c50565b5050613dfc613ddb60a0840161373f565b600483016001600160401b0382166001600160401b03198254161781555050565b613e0c60c0830160058301613cdc565b61010082013560078201555050565b6001600160401b038416815260808101613e386020830185613600565b826060830152949350505050565b60048110610cd357600080fd5b61ffff81168114610cd357600080fd5b600060408284031215613e7557600080fd5b604051604081018181106001600160401b0382111715613e9757613e976137b5565b6040528235613ea581613e46565b81526020830135613eb581613e53565b60208201529392505050565b634e487b7160e01b600052603260045260246000fd5b8135613ee281613e46565b60048110613ef257613ef26128ce565b815460ff821691508160ff1982161783556020840135613f1181613e53565b62ffff008160081b168362ffffff198416171784555050505050565b600060018201613f3f57613f3f6137cb565b5060010190565b600082613f6357634e487b7160e01b600052601260045260246000fd5b500490565b80820180821115610788576107886137cb565b61ffff818116838216019080821115613f9657613f966137cb565b509291505056fea164736f6c6343000814000a diff --git a/pallets/services/src/tests.rs b/pallets/services/src/tests.rs deleted file mode 100644 index cbea3bde7..000000000 --- a/pallets/services/src/tests.rs +++ /dev/null @@ -1,1387 +0,0 @@ -// This file is part of Tangle. -// Copyright (C) 2022-2024 Tangle Foundation. -// -// Tangle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Tangle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Tangle. If not, see . -use crate::types::ConstraintsOf; - -use super::*; -use frame_support::{assert_err, assert_ok}; -use k256::ecdsa::{SigningKey, VerifyingKey}; -use mock::*; -use sp_core::{bounded_vec, ecdsa, ByteArray, Pair, U256}; -use sp_runtime::{KeyTypeId, Percent}; -use tangle_primitives::{services::*, traits::MultiAssetDelegationInfo}; - -const ALICE: u8 = 1; -const BOB: u8 = 2; -const CHARLIE: u8 = 3; -const DAVE: u8 = 4; -const EVE: u8 = 5; - -const KEYGEN_JOB_ID: u8 = 0; -const SIGN_JOB_ID: u8 = 1; - -fn test_ecdsa_key() -> [u8; 65] { - let (ecdsa_key, _) = ecdsa::Pair::generate(); - let secret = SigningKey::from_slice(&ecdsa_key.seed()) - .expect("Should be able to create a secret key from a seed"); - let verifying_key = VerifyingKey::from(secret); - let public_key = verifying_key.to_encoded_point(false); - public_key.to_bytes().to_vec().try_into().unwrap() -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum MachineKind { - Large, - Medium, - Small, -} - -/// All prices are specified in USD/hr (in u64, so 1e6 = 1$) -fn price_targets(kind: MachineKind) -> PriceTargets { - match kind { - MachineKind::Large => PriceTargets { - cpu: 2_000, - mem: 1_000, - storage_hdd: 100, - storage_ssd: 200, - storage_nvme: 300, - }, - MachineKind::Medium => PriceTargets { - cpu: 1_000, - mem: 500, - storage_hdd: 50, - storage_ssd: 100, - storage_nvme: 150, - }, - MachineKind::Small => { - PriceTargets { cpu: 500, mem: 250, storage_hdd: 25, storage_ssd: 50, storage_nvme: 75 } - }, - } -} - -fn cggmp21_blueprint() -> ServiceBlueprint> { - #[allow(deprecated)] - ServiceBlueprint { - metadata: ServiceMetadata { name: "CGGMP21 TSS".try_into().unwrap(), ..Default::default() }, - manager: BlueprintServiceManager::Evm(CGGMP21_BLUEPRINT), - master_manager_revision: MasterBlueprintServiceManagerRevision::Latest, - jobs: bounded_vec![ - JobDefinition { - metadata: JobMetadata { name: "keygen".try_into().unwrap(), ..Default::default() }, - params: bounded_vec![FieldType::Uint8], - result: bounded_vec![FieldType::List(Box::new(FieldType::Uint8))], - }, - JobDefinition { - metadata: JobMetadata { name: "sign".try_into().unwrap(), ..Default::default() }, - params: bounded_vec![ - FieldType::Uint64, - FieldType::List(Box::new(FieldType::Uint8)) - ], - result: bounded_vec![FieldType::List(Box::new(FieldType::Uint8))], - }, - ], - registration_params: bounded_vec![], - request_params: bounded_vec![], - gadget: Default::default(), - } -} - -#[test] -fn update_mbsm() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - - assert_eq!(Pallet::::mbsm_latest_revision(), 0); - assert_eq!(Pallet::::mbsm_address(0).unwrap(), MBSM); - - // Add a new revision - let new_mbsm = { - let mut v = MBSM; - v.randomize(); - v - }; - - assert_ok!(Services::update_master_blueprint_service_manager( - RuntimeOrigin::root(), - new_mbsm - )); - - assert_eq!(Pallet::::mbsm_latest_revision(), 1); - assert_eq!(Pallet::::mbsm_address(1).unwrap(), new_mbsm); - // Old one should still be there - assert_eq!(Pallet::::mbsm_address(0).unwrap(), MBSM); - // Doesn't exist - assert!(Pallet::::mbsm_address(2).is_err()); - }); -} - -#[test] -fn update_mbsm_not_root() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - let alice = mock_pub_key(ALICE); - assert_err!( - Services::update_master_blueprint_service_manager(RuntimeOrigin::signed(alice), MBSM), - DispatchError::BadOrigin - ); - }); -} - -#[test] -fn create_service_blueprint() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - - let alice = mock_pub_key(ALICE); - - let blueprint = cggmp21_blueprint(); - - assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint,)); - - let next_id = Services::next_blueprint_id(); - assert_eq!(next_id, 1); - assert_events(vec![RuntimeEvent::Services(crate::Event::BlueprintCreated { - owner: alice, - blueprint_id: next_id - 1, - })]); - - let (_, blueprint) = Services::blueprints(next_id - 1).unwrap(); - - // The MBSM should be set on the blueprint - assert_eq!(Pallet::::mbsm_address_of(&blueprint).unwrap(), MBSM); - // The master manager revision should pinned to a specific revision that is equal to the - // latest revision of the MBSM. - assert_eq!( - blueprint.master_manager_revision, - MasterBlueprintServiceManagerRevision::Specific(0) - ); - }); -} - -#[test] -fn register_on_blueprint() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - let alice = mock_pub_key(ALICE); - - let blueprint = cggmp21_blueprint(); - - assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); - - let bob = mock_pub_key(BOB); - let bob_ecdsa_key = test_ecdsa_key(); - - let registration_call = Services::register( - RuntimeOrigin::signed(bob.clone()), - 0, - OperatorPreferences { - key: bob_ecdsa_key, - price_targets: price_targets(MachineKind::Large), - }, - Default::default(), - 0, - ); - assert_ok!(registration_call); - - assert_events(vec![RuntimeEvent::Services(crate::Event::Registered { - provider: bob.clone(), - blueprint_id: 0, - preferences: OperatorPreferences { - key: bob_ecdsa_key, - price_targets: price_targets(MachineKind::Large), - }, - registration_args: Default::default(), - })]); - - // The blueprint should be added to my blueprints in my profile. - let profile = OperatorsProfile::::get(bob.clone()).unwrap(); - assert!(profile.blueprints.contains(&0)); - - // if we try to register again, it should fail. - assert_err!( - Services::register( - RuntimeOrigin::signed(bob), - 0, - OperatorPreferences { key: bob_ecdsa_key, price_targets: Default::default() }, - Default::default(), - 0, - ), - crate::Error::::AlreadyRegistered - ); - - // if we try to register with a non active operator, should fail - assert_err!( - Services::register( - RuntimeOrigin::signed(mock_pub_key(10)), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - ), - crate::Error::::OperatorNotActive - ); - }); -} - -#[test] -fn pre_register_on_blueprint() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - let alice = mock_pub_key(ALICE); - - let blueprint = cggmp21_blueprint(); - - assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); - - let bob = mock_pub_key(BOB); - - let pre_registration_call = Services::pre_register(RuntimeOrigin::signed(bob.clone()), 0); - assert_ok!(pre_registration_call); - - assert_events(vec![RuntimeEvent::Services(crate::Event::PreRegistration { - operator: bob.clone(), - blueprint_id: 0, - })]); - }); -} - -#[test] -fn update_price_targets() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - let alice = mock_pub_key(ALICE); - - let blueprint = cggmp21_blueprint(); - - assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); - - let bob = mock_pub_key(BOB); - let bob_operator_ecdsa_key = test_ecdsa_key(); - assert_ok!(Services::register( - RuntimeOrigin::signed(bob.clone()), - 0, - OperatorPreferences { - key: bob_operator_ecdsa_key, - price_targets: price_targets(MachineKind::Small) - }, - Default::default(), - 0, - )); - - assert_eq!( - Operators::::get(0, &bob).unwrap(), - OperatorPreferences { - key: bob_operator_ecdsa_key, - price_targets: price_targets(MachineKind::Small) - } - ); - - assert_events(vec![RuntimeEvent::Services(crate::Event::Registered { - provider: bob.clone(), - blueprint_id: 0, - preferences: OperatorPreferences { - key: bob_operator_ecdsa_key, - price_targets: price_targets(MachineKind::Small), - }, - registration_args: Default::default(), - })]); - - // update price targets - assert_ok!(Services::update_price_targets( - RuntimeOrigin::signed(bob.clone()), - 0, - price_targets(MachineKind::Medium), - )); - - assert_eq!( - Operators::::get(0, &bob).unwrap().price_targets, - price_targets(MachineKind::Medium) - ); - - assert_events(vec![RuntimeEvent::Services(crate::Event::PriceTargetsUpdated { - operator: bob, - blueprint_id: 0, - price_targets: price_targets(MachineKind::Medium), - })]); - - // try to update price targets when not registered - let charlie = mock_pub_key(CHARLIE); - assert_err!( - Services::update_price_targets( - RuntimeOrigin::signed(charlie), - 0, - price_targets(MachineKind::Medium) - ), - crate::Error::::NotRegistered - ); - }); -} - -#[test] -fn unregister_from_blueprint() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - let alice = mock_pub_key(ALICE); - let blueprint = cggmp21_blueprint(); - assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); - - let bob = mock_pub_key(BOB); - assert_ok!(Services::register( - RuntimeOrigin::signed(bob.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - assert_ok!(Services::unregister(RuntimeOrigin::signed(bob.clone()), 0)); - assert!(!Operators::::contains_key(0, &bob)); - - // The blueprint should be removed from my blueprints in my profile. - let profile = OperatorsProfile::::get(bob.clone()).unwrap(); - assert!(!profile.blueprints.contains(&0)); - - assert_events(vec![RuntimeEvent::Services(crate::Event::Unregistered { - operator: bob, - blueprint_id: 0, - })]); - - // try to deregister when not registered - let charlie = mock_pub_key(CHARLIE); - assert_err!( - Services::unregister(RuntimeOrigin::signed(charlie), 0), - crate::Error::::NotRegistered - ); - }); -} - -#[test] -fn request_service() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - let alice = mock_pub_key(ALICE); - let blueprint = cggmp21_blueprint(); - assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); - let bob = mock_pub_key(BOB); - assert_ok!(Services::register( - RuntimeOrigin::signed(bob.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - let charlie = mock_pub_key(CHARLIE); - assert_ok!(Services::register( - RuntimeOrigin::signed(charlie.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - let dave = mock_pub_key(DAVE); - assert_ok!(Services::register( - RuntimeOrigin::signed(dave.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - - let eve = mock_pub_key(EVE); - assert_ok!(Services::request( - RuntimeOrigin::signed(eve.clone()), - None, - 0, - vec![alice.clone()], - vec![bob.clone(), charlie.clone(), dave.clone()], - Default::default(), - vec![USDC, WETH], - 100, - Asset::Custom(USDC), - 0, - )); - - assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); - - // Bob approves the request - assert_ok!(Services::approve( - RuntimeOrigin::signed(bob.clone()), - 0, - Percent::from_percent(10) - )); - - assert_events(vec![RuntimeEvent::Services(crate::Event::ServiceRequestApproved { - operator: bob.clone(), - request_id: 0, - blueprint_id: 0, - approved: vec![bob.clone()], - pending_approvals: vec![charlie.clone(), dave.clone()], - })]); - // Charlie approves the request - assert_ok!(Services::approve( - RuntimeOrigin::signed(charlie.clone()), - 0, - Percent::from_percent(20) - )); - - assert_events(vec![RuntimeEvent::Services(crate::Event::ServiceRequestApproved { - operator: charlie.clone(), - request_id: 0, - blueprint_id: 0, - approved: vec![bob.clone(), charlie.clone()], - pending_approvals: vec![dave.clone()], - })]); - - // Dave approves the request - assert_ok!(Services::approve( - RuntimeOrigin::signed(dave.clone()), - 0, - Percent::from_percent(30) - )); - - assert_events(vec![ - RuntimeEvent::Services(crate::Event::ServiceRequestApproved { - operator: dave.clone(), - request_id: 0, - blueprint_id: 0, - approved: vec![bob.clone(), charlie.clone(), dave.clone()], - pending_approvals: vec![], - }), - RuntimeEvent::Services(crate::Event::ServiceInitiated { - owner: eve, - request_id: 0, - service_id: 0, - blueprint_id: 0, - assets: vec![USDC, WETH], - }), - ]); - - // The request is now fully approved - assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 0); - - // Now the service should be initiated - assert!(Instances::::contains_key(0)); - - // The service should also be added to the services for each operator. - let profile = OperatorsProfile::::get(bob).unwrap(); - assert!(profile.services.contains(&0)); - let profile = OperatorsProfile::::get(charlie).unwrap(); - assert!(profile.services.contains(&0)); - let profile = OperatorsProfile::::get(dave).unwrap(); - assert!(profile.services.contains(&0)); - }); -} - -#[test] -fn request_service_with_no_assets() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - let alice = mock_pub_key(ALICE); - let blueprint = cggmp21_blueprint(); - assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); - let bob = mock_pub_key(BOB); - assert_ok!(Services::register( - RuntimeOrigin::signed(bob.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - let eve = mock_pub_key(EVE); - assert_err!( - Services::request( - RuntimeOrigin::signed(eve.clone()), - None, - 0, - vec![alice.clone()], - vec![bob.clone()], - Default::default(), - vec![], // no assets - 100, - Asset::Custom(USDC), - 0, - ), - Error::::NoAssetsProvided - ); - }); -} - -#[test] -fn request_service_with_payment_asset() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - let alice = mock_pub_key(ALICE); - let blueprint = cggmp21_blueprint(); - - assert_ok!(Services::create_blueprint( - RuntimeOrigin::signed(alice.clone()), - blueprint.clone() - )); - let bob = mock_pub_key(BOB); - assert_ok!(Services::register( - RuntimeOrigin::signed(bob.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - - let payment = 5 * 10u128.pow(6); // 5 USDC - let charlie = mock_pub_key(CHARLIE); - assert_ok!(Services::request( - RuntimeOrigin::signed(charlie.clone()), - None, - 0, - vec![], - vec![bob.clone()], - Default::default(), - vec![TNT, USDC, WETH], - 100, - Asset::Custom(USDC), - payment, - )); - - assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); - - // The Pallet account now has 5 USDC - assert_eq!(Assets::balance(USDC, Services::account_id()), payment); - - // Bob approves the request - assert_ok!(Services::approve( - RuntimeOrigin::signed(bob.clone()), - 0, - Percent::from_percent(10) - )); - - // The request is now fully approved - assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 0); - - // The Payment should be now transferred to the MBSM. - let mbsm_address = Pallet::::mbsm_address_of(&blueprint).unwrap(); - let mbsm_account_id = address_to_account_id(mbsm_address); - assert_eq!(Assets::balance(USDC, mbsm_account_id), payment); - // Pallet account should have 0 USDC - assert_eq!(Assets::balance(USDC, Services::account_id()), 0); - - // Now the service should be initiated - assert!(Instances::::contains_key(0)); - }); -} - -#[test] -fn request_service_with_payment_token() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - let alice = mock_pub_key(ALICE); - let blueprint = cggmp21_blueprint(); - - assert_ok!(Services::create_blueprint( - RuntimeOrigin::signed(alice.clone()), - blueprint.clone() - )); - let bob = mock_pub_key(BOB); - assert_ok!(Services::register( - RuntimeOrigin::signed(bob.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - - let payment = 5 * 10u128.pow(6); // 5 USDC - let charlie = mock_pub_key(CHARLIE); - assert_ok!(Services::request( - RuntimeOrigin::signed(address_to_account_id(mock_address(CHARLIE))), - Some(account_id_to_address(charlie.clone())), - 0, - vec![], - vec![bob.clone()], - Default::default(), - vec![TNT, USDC, WETH], - 100, - Asset::Erc20(USDC_ERC20), - payment, - )); - - assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); - - // The Pallet address now has 5 USDC - assert_ok!( - Services::query_erc20_balance_of(USDC_ERC20, Services::address()).map(|(b, _)| b), - U256::from(payment) - ); - - // Bob approves the request - assert_ok!(Services::approve( - RuntimeOrigin::signed(bob.clone()), - 0, - Percent::from_percent(10) - )); - - // The request is now fully approved - assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 0); - - // The Payment should be now transferred to the MBSM. - let mbsm_address = Pallet::::mbsm_address_of(&blueprint).unwrap(); - assert_ok!( - Services::query_erc20_balance_of(USDC_ERC20, mbsm_address).map(|(b, _)| b), - U256::from(payment) - ); - // Pallet account should have 0 USDC - assert_ok!( - Services::query_erc20_balance_of(USDC_ERC20, Services::address()).map(|(b, _)| b), - U256::from(0) - ); - - // Now the service should be initiated - assert!(Instances::::contains_key(0)); - }); -} - -#[test] -fn reject_service_with_payment_token() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - let alice = mock_pub_key(ALICE); - let blueprint = cggmp21_blueprint(); - - assert_ok!(Services::create_blueprint( - RuntimeOrigin::signed(alice.clone()), - blueprint.clone() - )); - let bob = mock_pub_key(BOB); - assert_ok!(Services::register( - RuntimeOrigin::signed(bob.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - - let payment = 5 * 10u128.pow(6); // 5 USDC - let charlie_address = mock_address(CHARLIE); - let charlie_evm_account_id = address_to_account_id(charlie_address); - let before_balance = Services::query_erc20_balance_of(USDC_ERC20, charlie_address) - .map(|(b, _)| b) - .unwrap_or_default(); - assert_ok!(Services::request( - RuntimeOrigin::signed(charlie_evm_account_id), - Some(charlie_address), - 0, - vec![], - vec![bob.clone()], - Default::default(), - vec![TNT, USDC, WETH], - 100, - Asset::Erc20(USDC_ERC20), - payment, - )); - - assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); - - // The Pallet address now has 5 USDC - assert_ok!( - Services::query_erc20_balance_of(USDC_ERC20, Services::address()).map(|(b, _)| b), - U256::from(payment) - ); - // Charlie Balance should be decreased by 5 USDC - assert_ok!( - Services::query_erc20_balance_of(USDC_ERC20, charlie_address).map(|(b, _)| b), - before_balance - U256::from(payment) - ); - - // Bob rejects the request - assert_ok!(Services::reject(RuntimeOrigin::signed(bob.clone()), 0)); - - // The Payment should be now refunded to the requester. - // Pallet account should have 0 USDC - assert_ok!( - Services::query_erc20_balance_of(USDC_ERC20, Services::address()).map(|(b, _)| b), - U256::from(0) - ); - // Charlie Balance should be back to the original - assert_ok!( - Services::query_erc20_balance_of(USDC_ERC20, charlie_address).map(|(b, _)| b), - before_balance - ); - }); -} - -#[test] -fn reject_service_with_payment_asset() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - let alice = mock_pub_key(ALICE); - let blueprint = cggmp21_blueprint(); - - assert_ok!(Services::create_blueprint( - RuntimeOrigin::signed(alice.clone()), - blueprint.clone() - )); - let bob = mock_pub_key(BOB); - assert_ok!(Services::register( - RuntimeOrigin::signed(bob.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - - let payment = 5 * 10u128.pow(6); // 5 USDC - let charlie = mock_pub_key(CHARLIE); - let before_balance = Assets::balance(USDC, charlie.clone()); - assert_ok!(Services::request( - RuntimeOrigin::signed(charlie.clone()), - None, - 0, - vec![], - vec![bob.clone()], - Default::default(), - vec![TNT, USDC, WETH], - 100, - Asset::Custom(USDC), - payment, - )); - - assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); - - // The Pallet account now has 5 USDC - assert_eq!(Assets::balance(USDC, Services::account_id()), payment); - // Charlie Balance should be decreased by 5 USDC - assert_eq!(Assets::balance(USDC, charlie.clone()), before_balance - payment); - - // Bob rejects the request - assert_ok!(Services::reject(RuntimeOrigin::signed(bob.clone()), 0)); - - // The Payment should be now refunded to the requester. - // Pallet account should have 0 USDC - assert_eq!(Assets::balance(USDC, Services::account_id()), 0); - // Charlie Balance should be back to the original - assert_eq!(Assets::balance(USDC, charlie), before_balance); - }); -} - -#[test] -fn job_calls() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - let alice = mock_pub_key(ALICE); - let blueprint = cggmp21_blueprint(); - assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); - let bob = mock_pub_key(BOB); - assert_ok!(Services::register( - RuntimeOrigin::signed(bob.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - let charlie = mock_pub_key(CHARLIE); - assert_ok!(Services::register( - RuntimeOrigin::signed(charlie.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - let dave = mock_pub_key(DAVE); - assert_ok!(Services::register( - RuntimeOrigin::signed(dave.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - - let eve = mock_pub_key(EVE); - assert_ok!(Services::request( - RuntimeOrigin::signed(eve.clone()), - None, - 0, - vec![alice.clone()], - vec![bob.clone(), charlie.clone(), dave.clone()], - Default::default(), - vec![WETH], - 100, - Asset::Custom(USDC), - 0, - )); - - assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); - assert_ok!(Services::approve( - RuntimeOrigin::signed(bob.clone()), - 0, - Percent::from_percent(10) - )); - - assert_ok!(Services::approve( - RuntimeOrigin::signed(charlie.clone()), - 0, - Percent::from_percent(10) - )); - - assert_ok!(Services::approve( - RuntimeOrigin::signed(dave.clone()), - 0, - Percent::from_percent(10) - )); - assert!(Instances::::contains_key(0)); - assert_events(vec![RuntimeEvent::Services(crate::Event::ServiceInitiated { - owner: eve.clone(), - request_id: 0, - service_id: 0, - blueprint_id: 0, - assets: vec![WETH], - })]); - - // now we can call the jobs - let job_call_id = 0; - assert_ok!(Services::call( - RuntimeOrigin::signed(eve.clone()), - 0, - 0, - bounded_vec![Field::Uint8(2)], - )); - - assert!(JobCalls::::contains_key(0, job_call_id)); - assert_events(vec![RuntimeEvent::Services(crate::Event::JobCalled { - caller: eve, - service_id: 0, - job: 0, - call_id: job_call_id, - args: vec![Field::Uint8(2)], - })]); - }); -} - -#[test] -fn job_result() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - let alice = mock_pub_key(ALICE); - let blueprint = cggmp21_blueprint(); - assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); - let bob = mock_pub_key(BOB); - assert_ok!(Services::register( - RuntimeOrigin::signed(bob.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - let charlie = mock_pub_key(CHARLIE); - assert_ok!(Services::register( - RuntimeOrigin::signed(charlie.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - let dave = mock_pub_key(DAVE); - assert_ok!(Services::register( - RuntimeOrigin::signed(dave.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - - let eve = mock_pub_key(EVE); - assert_ok!(Services::request( - RuntimeOrigin::signed(eve.clone()), - None, - 0, - vec![alice.clone()], - vec![bob.clone(), charlie.clone(), dave.clone()], - Default::default(), - vec![WETH], - 100, - Asset::Custom(USDC), - 0, - )); - - assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); - assert_ok!(Services::approve( - RuntimeOrigin::signed(bob.clone()), - 0, - Percent::from_percent(10) - )); - - assert_ok!(Services::approve( - RuntimeOrigin::signed(charlie.clone()), - 0, - Percent::from_percent(10) - )); - - assert_ok!(Services::approve( - RuntimeOrigin::signed(dave.clone()), - 0, - Percent::from_percent(10) - )); - assert!(Instances::::contains_key(0)); - assert_events(vec![RuntimeEvent::Services(crate::Event::ServiceInitiated { - owner: eve.clone(), - request_id: 0, - service_id: 0, - blueprint_id: 0, - assets: vec![WETH], - })]); - - // now we can call the jobs - let keygen_job_call_id = 0; - - assert_ok!(Services::call( - RuntimeOrigin::signed(eve.clone()), - 0, - 0, - bounded_vec![Field::Uint8(2)] - )); - - assert!(JobCalls::::contains_key(0, keygen_job_call_id)); - // now we can set the job result - let key_type = KeyTypeId(*b"mdkg"); - let dkg = sp_io::crypto::ecdsa_generate(key_type, None); - assert_ok!(Services::submit_result( - RuntimeOrigin::signed(bob.clone()), - 0, - keygen_job_call_id, - bounded_vec![Field::from(BoundedVec::try_from(dkg.to_raw_vec()).unwrap())], - )); - - // submit signing job - let _signing_job_call_id = 1; - let data_hash = sp_core::keccak_256(&[1; 32]); - - assert_ok!(Services::call( - RuntimeOrigin::signed(eve.clone()), - 0, - SIGN_JOB_ID, - bounded_vec![ - Field::Uint64(keygen_job_call_id), - Field::from(BoundedVec::try_from(data_hash.to_vec()).unwrap()) - ], - )); - - // now we can set the job result - let signature = sp_io::crypto::ecdsa_sign_prehashed(key_type, &dkg, &data_hash).unwrap(); - let mut signature_bytes = signature.to_raw_vec(); - // fix the v value (it should be 27 or 28). - signature_bytes[64] += 27u8; - // For some reason, the signature is not being verified. - // in EVM, ecrecover is used to verify the signature, but it returns - // 0x000000000000000000000000000000000000000 as the address of the signer. - // even though the signature is correct, and we have the precomiles in the runtime. - // - // assert_ok!(Services::submit_result( - // RuntimeOrigin::signed(bob.clone()), - // 0, - // signing_job_call_id, - // bounded_vec![Field::Bytes(signature_bytes.try_into().unwrap())], - // )); - }); -} - -struct Deployment { - blueprint_id: u64, - service_id: u64, - bob_exposed_restake_percentage: Percent, -} - -/// A Helper function that creates a blueprint and service instance -fn deploy() -> Deployment { - let alice = mock_pub_key(ALICE); - let blueprint = cggmp21_blueprint(); - let blueprint_id = Services::next_blueprint_id(); - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); - - let bob = mock_pub_key(BOB); - assert_ok!(Services::register( - RuntimeOrigin::signed(bob.clone()), - blueprint_id, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - - let eve = mock_pub_key(EVE); - let service_id = Services::next_instance_id(); - assert_ok!(Services::request( - RuntimeOrigin::signed(eve.clone()), - None, - blueprint_id, - vec![alice.clone()], - vec![bob.clone()], - Default::default(), - vec![WETH], - 100, - Asset::Custom(USDC), - 0, - )); - - assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); - - let bob_exposed_restake_percentage = Percent::from_percent(10); - assert_ok!(Services::approve( - RuntimeOrigin::signed(bob.clone()), - service_id, - bob_exposed_restake_percentage, - )); - - assert!(Instances::::contains_key(service_id)); - - Deployment { blueprint_id, service_id, bob_exposed_restake_percentage } -} - -#[test] -fn unapplied_slash() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - let Deployment { blueprint_id, service_id, bob_exposed_restake_percentage } = deploy(); - let eve = mock_pub_key(EVE); - let bob = mock_pub_key(BOB); - // now we can call the jobs - let job_call_id = Services::next_job_call_id(); - assert_ok!(Services::call( - RuntimeOrigin::signed(eve.clone()), - service_id, - KEYGEN_JOB_ID, - bounded_vec![Field::Uint8(1)], - )); - // sumbit an invalid result - let mut dkg = vec![0u8; 33]; - dkg[32] = 1; - assert_ok!(Services::submit_result( - RuntimeOrigin::signed(bob.clone()), - 0, - job_call_id, - bounded_vec![Field::from(BoundedVec::try_from(dkg).unwrap())], - )); - - let slash_percent = Percent::from_percent(50); - let service = Services::services(service_id).unwrap(); - let slashing_origin = - Services::query_slashing_origin(&service).map(|(o, _)| o.unwrap()).unwrap(); - - // Slash the operator for the invalid result - assert_ok!(Services::slash( - RuntimeOrigin::signed(slashing_origin.clone()), - bob.clone(), - service_id, - slash_percent - )); - - assert_eq!(UnappliedSlashes::::iter_keys().collect::>().len(), 1); - - let bob_slash = ::OperatorDelegationManager::get_operator_stake(&bob); - let expected_slash_amount = - (slash_percent * bob_exposed_restake_percentage).mul_floor(bob_slash); - - assert_events(vec![RuntimeEvent::Services(crate::Event::UnappliedSlash { - era: 0, - index: 0, - operator: bob.clone(), - blueprint_id, - service_id, - amount: expected_slash_amount, - })]); - }); -} - -#[test] -fn unapplied_slash_with_invalid_origin() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - let Deployment { service_id, .. } = deploy(); - let eve = mock_pub_key(EVE); - let bob = mock_pub_key(BOB); - let slash_percent = Percent::from_percent(50); - // Try to slash with an invalid origin - assert_err!( - Services::slash( - RuntimeOrigin::signed(eve.clone()), - bob.clone(), - service_id, - slash_percent - ), - DispatchError::BadOrigin - ); - }); -} - -#[test] -fn slash_account_not_an_operator() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - let Deployment { service_id, .. } = deploy(); - let karen = mock_pub_key(23); - - let service = Services::services(service_id).unwrap(); - let slashing_origin = - Services::query_slashing_origin(&service).map(|(o, _)| o.unwrap()).unwrap(); - let slash_percent = Percent::from_percent(50); - // Try to slash an operator that is not active in this service - assert_err!( - Services::slash( - RuntimeOrigin::signed(slashing_origin.clone()), - karen.clone(), - service_id, - slash_percent - ), - Error::::OffenderNotOperator - ); - }); -} - -#[test] -fn dispute() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - let Deployment { blueprint_id, service_id, bob_exposed_restake_percentage } = deploy(); - let bob = mock_pub_key(BOB); - let slash_percent = Percent::from_percent(50); - let service = Services::services(service_id).unwrap(); - let slashing_origin = - Services::query_slashing_origin(&service).map(|(o, _)| o.unwrap()).unwrap(); - - // Slash the operator for the invalid result - assert_ok!(Services::slash( - RuntimeOrigin::signed(slashing_origin.clone()), - bob.clone(), - service_id, - slash_percent - )); - - assert_eq!(UnappliedSlashes::::iter_keys().collect::>().len(), 1); - - let era = 0; - let slash_index = 0; - - // Dispute the slash - let dispute_origin = - Services::query_dispute_origin(&service).map(|(o, _)| o.unwrap()).unwrap(); - - assert_ok!(Services::dispute( - RuntimeOrigin::signed(dispute_origin.clone()), - era, - slash_index - )); - - assert_eq!(UnappliedSlashes::::iter_keys().collect::>().len(), 0); - - let bob_slash = ::OperatorDelegationManager::get_operator_stake(&bob); - let expected_slash_amount = - (slash_percent * bob_exposed_restake_percentage).mul_floor(bob_slash); - - assert_events(vec![RuntimeEvent::Services(crate::Event::SlashDiscarded { - era: 0, - index: 0, - operator: bob.clone(), - blueprint_id, - service_id, - amount: expected_slash_amount, - })]); - }); -} - -#[test] -fn dispute_with_unauthorized_origin() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - let Deployment { service_id, .. } = deploy(); - let eve = mock_pub_key(EVE); - let bob = mock_pub_key(BOB); - let slash_percent = Percent::from_percent(50); - let service = Services::services(service_id).unwrap(); - let slashing_origin = - Services::query_slashing_origin(&service).map(|(o, _)| o.unwrap()).unwrap(); - - // Slash the operator for the invalid result - assert_ok!(Services::slash( - RuntimeOrigin::signed(slashing_origin.clone()), - bob.clone(), - service_id, - slash_percent - )); - - assert_eq!(UnappliedSlashes::::iter_keys().collect::>().len(), 1); - - let era = 0; - let slash_index = 0; - - // Try to dispute with an invalid origin - assert_err!( - Services::dispute(RuntimeOrigin::signed(eve.clone()), era, slash_index), - DispatchError::BadOrigin - ); - }); -} - -#[test] -fn dispute_an_already_applied_slash() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - System::set_block_number(1); - let Deployment { service_id, .. } = deploy(); - let eve = mock_pub_key(EVE); - let bob = mock_pub_key(BOB); - let slash_percent = Percent::from_percent(50); - let service = Services::services(service_id).unwrap(); - let slashing_origin = - Services::query_slashing_origin(&service).map(|(o, _)| o.unwrap()).unwrap(); - - // Slash the operator for the invalid result - assert_ok!(Services::slash( - RuntimeOrigin::signed(slashing_origin.clone()), - bob.clone(), - service_id, - slash_percent - )); - - assert_eq!(UnappliedSlashes::::iter_keys().collect::>().len(), 1); - - let era = 0; - let slash_index = 0; - // Simulate a slash happening - UnappliedSlashes::::remove(era, slash_index); - - // Try to dispute an already applied slash - assert_err!( - Services::dispute(RuntimeOrigin::signed(eve.clone()), era, slash_index), - Error::::UnappliedSlashNotFound - ); - }); -} - -#[test] -fn hooks() { - new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - - let alice = mock_pub_key(ALICE); - let bob = mock_pub_key(BOB); - let charlie = mock_pub_key(CHARLIE); - let blueprint = ServiceBlueprint { - metadata: ServiceMetadata { - name: "Hooks Tests".try_into().unwrap(), - ..Default::default() - }, - manager: BlueprintServiceManager::Evm(HOOKS_TEST), - master_manager_revision: MasterBlueprintServiceManagerRevision::Latest, - jobs: bounded_vec![JobDefinition { - metadata: JobMetadata { name: "foo".try_into().unwrap(), ..Default::default() }, - params: bounded_vec![], - result: bounded_vec![], - },], - registration_params: bounded_vec![], - request_params: bounded_vec![], - gadget: Default::default(), - }; - - // OnBlueprintCreated hook should be called - assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); - assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnBlueprintCreated()")]); - - // OnRegister hook should be called - assert_ok!(Services::register( - RuntimeOrigin::signed(bob.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnRegister()")]); - - // OnUnregister hook should be called - assert_ok!(Services::unregister(RuntimeOrigin::signed(bob.clone()), 0)); - assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnUnregister()")]); - - // Register again to continue testing - assert_ok!(Services::register( - RuntimeOrigin::signed(bob.clone()), - 0, - OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, - Default::default(), - 0, - )); - - // OnUpdatePriceTargets hook should be called - assert_ok!(Services::update_price_targets( - RuntimeOrigin::signed(bob.clone()), - 0, - price_targets(MachineKind::Medium), - )); - assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnUpdatePriceTargets()")]); - - // OnRequest hook should be called - assert_ok!(Services::request( - RuntimeOrigin::signed(charlie.clone()), - None, - 0, - vec![alice.clone()], - vec![bob.clone()], - Default::default(), - vec![USDC, WETH], - 100, - Asset::Custom(USDC), - 0, - )); - assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnRequest()")]); - - // OnReject hook should be called - assert_ok!(Services::reject(RuntimeOrigin::signed(bob.clone()), 0)); - assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnReject()")]); - - // OnApprove hook should be called - // OnServiceInitialized is also called - assert_ok!(Services::approve( - RuntimeOrigin::signed(bob.clone()), - 0, - Percent::from_percent(10) - )); - assert_evm_logs(&[ - evm_log!(HOOKS_TEST, b"OnApprove()"), - evm_log!(HOOKS_TEST, b"OnServiceInitialized()"), - ]); - - // OnJobCall hook should be called - assert_ok!(Services::call(RuntimeOrigin::signed(charlie.clone()), 0, 0, bounded_vec![],)); - assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnJobCall()")]); - - // OnJobResult hook should be called - assert_ok!(Services::submit_result( - RuntimeOrigin::signed(bob.clone()), - 0, - 0, - bounded_vec![], - )); - assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnJobResult()")]); - // OnServiceTermination hook should be called - assert_ok!(Services::terminate(RuntimeOrigin::signed(charlie.clone()), 0)); - assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnServiceTermination()")]); - }); -} diff --git a/pallets/services/src/tests/blueprint.rs b/pallets/services/src/tests/blueprint.rs new file mode 100644 index 000000000..6aa82993b --- /dev/null +++ b/pallets/services/src/tests/blueprint.rs @@ -0,0 +1,91 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use super::*; +use frame_support::{assert_err, assert_ok}; + +#[test] +fn update_mbsm() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + + assert_eq!(Pallet::::mbsm_latest_revision(), 0); + assert_eq!(Pallet::::mbsm_address(0).unwrap(), MBSM); + + // Add a new revision + let new_mbsm = { + let mut v = MBSM; + v.randomize(); + v + }; + + assert_ok!(Services::update_master_blueprint_service_manager( + RuntimeOrigin::root(), + new_mbsm + )); + + assert_eq!(Pallet::::mbsm_latest_revision(), 1); + assert_eq!(Pallet::::mbsm_address(1).unwrap(), new_mbsm); + // Old one should still be there + assert_eq!(Pallet::::mbsm_address(0).unwrap(), MBSM); + // Doesn't exist + assert!(Pallet::::mbsm_address(2).is_err()); + }); +} + +#[test] +fn update_mbsm_not_root() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + let alice = mock_pub_key(ALICE); + assert_err!( + Services::update_master_blueprint_service_manager(RuntimeOrigin::signed(alice), MBSM), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn create_service_blueprint() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + + assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint,)); + + let next_id = Services::next_blueprint_id(); + assert_eq!(next_id, 1); + assert_events(vec![RuntimeEvent::Services(crate::Event::BlueprintCreated { + owner: alice, + blueprint_id: next_id - 1, + })]); + + let (_, blueprint) = Services::blueprints(next_id - 1).unwrap(); + + // The MBSM should be set on the blueprint + assert_eq!(Pallet::::mbsm_address_of(&blueprint).unwrap(), MBSM); + // The master manager revision should pinned to a specific revision that is equal to the + // latest revision of the MBSM. + assert_eq!( + blueprint.master_manager_revision, + MasterBlueprintServiceManagerRevision::Specific(0) + ); + }); +} diff --git a/pallets/services/src/tests/hooks.rs b/pallets/services/src/tests/hooks.rs new file mode 100644 index 000000000..37e22d8a8 --- /dev/null +++ b/pallets/services/src/tests/hooks.rs @@ -0,0 +1,156 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use super::*; +use frame_support::assert_ok; +use sp_runtime::Percent; + +#[test] +fn hooks() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + + let alice = mock_pub_key(ALICE); + let bob = mock_pub_key(BOB); + let charlie = mock_pub_key(CHARLIE); + let blueprint = ServiceBlueprint { + metadata: ServiceMetadata { + name: "Hooks Tests".try_into().unwrap(), + ..Default::default() + }, + manager: BlueprintServiceManager::Evm(HOOKS_TEST), + master_manager_revision: MasterBlueprintServiceManagerRevision::Latest, + jobs: bounded_vec![JobDefinition { + metadata: JobMetadata { name: "foo".try_into().unwrap(), ..Default::default() }, + params: bounded_vec![], + result: bounded_vec![], + },], + registration_params: bounded_vec![], + request_params: bounded_vec![], + gadget: Default::default(), + supported_membership_models: bounded_vec![ + MembershipModel::Fixed { min_operators: 1 }, + MembershipModel::Dynamic { min_operators: 1, max_operators: None }, + ], + }; + + // OnBlueprintCreated hook should be called + assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); + assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnBlueprintCreated()")]); + + // OnRegister hook should be called + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnRegister()")]); + + // OnUnregister hook should be called + assert_ok!(Services::unregister(RuntimeOrigin::signed(bob.clone()), 0)); + assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnUnregister()")]); + + // Register again to continue testing + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + // OnUpdatePriceTargets hook should be called + assert_ok!(Services::update_price_targets( + RuntimeOrigin::signed(bob.clone()), + 0, + price_targets(MachineKind::Medium), + )); + assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnUpdatePriceTargets()")]); + + // OnRequest hook should be called + assert_ok!(Services::request( + RuntimeOrigin::signed(charlie.clone()), + None, + 0, + vec![alice.clone()], + vec![bob.clone()], + Default::default(), + vec![ + get_security_requirement(USDC, &[10, 20]), + get_security_requirement(WETH, &[10, 20]) + ], + 100, + Asset::Custom(USDC), + 0, + MembershipModel::Fixed { min_operators: 1 }, + )); + assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnRequest()")]); + + // OnReject hook should be called + assert_ok!(Services::reject(RuntimeOrigin::signed(bob.clone()), 0)); + assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnReject()")]); + + // Create another request to test remaining hooks + assert_ok!(Services::request( + RuntimeOrigin::signed(charlie.clone()), + None, + 0, + vec![alice.clone()], + vec![bob.clone()], + Default::default(), + vec![ + get_security_requirement(USDC, &[10, 20]), + get_security_requirement(WETH, &[10, 20]) + ], + 100, + Asset::Custom(USDC), + 0, + MembershipModel::Fixed { min_operators: 1 }, + )); + + // OnApprove hook should be called + // OnServiceInitialized is also called + assert_ok!(Services::approve( + RuntimeOrigin::signed(bob.clone()), + 1, + Percent::from_percent(10), + vec![get_security_commitment(USDC, 10), get_security_commitment(WETH, 10)], + )); + assert_evm_logs(&[ + evm_log!(HOOKS_TEST, b"OnApprove()"), + evm_log!(HOOKS_TEST, b"OnServiceInitialized()"), + ]); + + // OnJobCall hook should be called + assert_ok!(Services::call(RuntimeOrigin::signed(charlie.clone()), 0, 0, bounded_vec![],)); + assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnJobCall()")]); + + // OnJobResult hook should be called + assert_ok!(Services::submit_result( + RuntimeOrigin::signed(bob.clone()), + 0, + 0, + bounded_vec![], + )); + assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnJobResult()")]); + + // OnServiceTermination hook should be called + assert_ok!(Services::terminate(RuntimeOrigin::signed(charlie.clone()), 0)); + assert_evm_logs(&[evm_log!(HOOKS_TEST, b"OnServiceTermination()")]); + }); +} diff --git a/pallets/services/src/tests/jobs.rs b/pallets/services/src/tests/jobs.rs new file mode 100644 index 000000000..2a212be5e --- /dev/null +++ b/pallets/services/src/tests/jobs.rs @@ -0,0 +1,267 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use super::*; +use frame_support::assert_ok; +use sp_core::{offchain::KeyTypeId, ByteArray}; +use sp_runtime::Percent; + +#[test] +fn job_calls() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); + + // Register multiple operators + let bob = mock_pub_key(BOB); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let charlie = mock_pub_key(CHARLIE); + assert_ok!(Services::register( + RuntimeOrigin::signed(charlie.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let dave = mock_pub_key(DAVE); + assert_ok!(Services::register( + RuntimeOrigin::signed(dave.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let eve = mock_pub_key(EVE); + assert_ok!(Services::request( + RuntimeOrigin::signed(eve.clone()), + None, + 0, + vec![alice.clone()], + vec![bob.clone(), charlie.clone(), dave.clone()], + Default::default(), + vec![get_security_requirement(WETH, &[10, 20])], + 100, + Asset::Custom(USDC), + 0, + MembershipModel::Fixed { min_operators: 3 }, + )); + + assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); + + // All operators approve with security commitments + assert_ok!(Services::approve( + RuntimeOrigin::signed(bob.clone()), + 0, + Percent::from_percent(10), + vec![get_security_commitment(WETH, 10)], + )); + + assert_ok!(Services::approve( + RuntimeOrigin::signed(charlie.clone()), + 0, + Percent::from_percent(10), + vec![get_security_commitment(WETH, 10)], + )); + + assert_ok!(Services::approve( + RuntimeOrigin::signed(dave.clone()), + 0, + Percent::from_percent(10), + vec![get_security_commitment(WETH, 10)], + )); + + assert!(Instances::::contains_key(0)); + assert_events(vec![RuntimeEvent::Services(crate::Event::ServiceInitiated { + owner: eve.clone(), + request_id: 0, + service_id: 0, + blueprint_id: 0, + assets: vec![Asset::Custom(WETH)], + })]); + + // now we can call the jobs + let job_call_id = 0; + assert_ok!(Services::call( + RuntimeOrigin::signed(eve.clone()), + 0, + 0, + bounded_vec![Field::Uint8(2)], + )); + + assert!(JobCalls::::contains_key(0, job_call_id)); + assert_events(vec![RuntimeEvent::Services(crate::Event::JobCalled { + caller: eve, + service_id: 0, + job: 0, + call_id: job_call_id, + args: vec![Field::Uint8(2)], + })]); + }); +} + +#[test] +fn job_result() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); + + // Register multiple operators + let bob = mock_pub_key(BOB); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let charlie = mock_pub_key(CHARLIE); + assert_ok!(Services::register( + RuntimeOrigin::signed(charlie.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let dave = mock_pub_key(DAVE); + assert_ok!(Services::register( + RuntimeOrigin::signed(dave.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let eve = mock_pub_key(EVE); + assert_ok!(Services::request( + RuntimeOrigin::signed(eve.clone()), + None, + 0, + vec![alice.clone()], + vec![bob.clone(), charlie.clone(), dave.clone()], + Default::default(), + vec![get_security_requirement(WETH, &[10, 20])], + 100, + Asset::Custom(USDC), + 0, + MembershipModel::Fixed { min_operators: 3 }, + )); + + assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); + + // All operators approve with security commitments + assert_ok!(Services::approve( + RuntimeOrigin::signed(bob.clone()), + 0, + Percent::from_percent(10), + vec![get_security_commitment(WETH, 10)], + )); + + assert_ok!(Services::approve( + RuntimeOrigin::signed(charlie.clone()), + 0, + Percent::from_percent(10), + vec![get_security_commitment(WETH, 10)], + )); + + assert_ok!(Services::approve( + RuntimeOrigin::signed(dave.clone()), + 0, + Percent::from_percent(10), + vec![get_security_commitment(WETH, 10)], + )); + + assert!(Instances::::contains_key(0)); + assert_events(vec![RuntimeEvent::Services(crate::Event::ServiceInitiated { + owner: eve.clone(), + request_id: 0, + service_id: 0, + blueprint_id: 0, + assets: vec![Asset::Custom(WETH)], + })]); + + // now we can call the jobs + let keygen_job_call_id = 0; + + assert_ok!(Services::call( + RuntimeOrigin::signed(eve.clone()), + 0, + 0, + bounded_vec![Field::Uint8(2)] + )); + + assert!(JobCalls::::contains_key(0, keygen_job_call_id)); + + // now we can set the job result + let key_type = KeyTypeId(*b"mdkg"); + let dkg = sp_io::crypto::ecdsa_generate(key_type, None); + assert_ok!(Services::submit_result( + RuntimeOrigin::signed(bob.clone()), + 0, + keygen_job_call_id, + bounded_vec![Field::from(BoundedVec::try_from(dkg.to_raw_vec()).unwrap())], + )); + + // submit signing job + + let data_hash = sp_core::keccak_256(&[1; 32]); + + assert_ok!(Services::call( + RuntimeOrigin::signed(eve.clone()), + 0, + SIGN_JOB_ID, + bounded_vec![ + Field::Uint64(keygen_job_call_id), + Field::from(BoundedVec::try_from(data_hash.to_vec()).unwrap()) + ], + )); + + // now we can set the job result + let signature = sp_io::crypto::ecdsa_sign_prehashed(key_type, &dkg, &data_hash).unwrap(); + let mut signature_bytes = signature.to_raw_vec(); + // fix the v value (it should be 27 or 28). + signature_bytes[64] += 27u8; + + // For some reason, the signature is not being verified. + // in EVM, ecrecover is used to verify the signature, but it returns + // 0x000000000000000000000000000000000000000 as the address of the signer. + // even though the signature is correct, and we have the precomiles in the runtime. + // + // let signing_job_call_id = 1; + // assert_ok!(Services::submit_result( + // RuntimeOrigin::signed(bob.clone()), + // 0, + // signing_job_call_id, + // bounded_vec![Field::Bytes(signature_bytes.try_into().unwrap())], + // )); + }); +} diff --git a/pallets/services/src/tests/mod.rs b/pallets/services/src/tests/mod.rs new file mode 100644 index 000000000..ebb8bdfed --- /dev/null +++ b/pallets/services/src/tests/mod.rs @@ -0,0 +1,180 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use super::*; +use crate::mock::*; +use frame_support::assert_ok; +use sp_core::bounded_vec; +use sp_core::Pair; +use sp_runtime::Percent; +use tangle_primitives::services::*; + +mod blueprint; +mod hooks; +mod jobs; +mod registration; +mod service; +mod slashing; + +pub const ALICE: u8 = 1; +pub const BOB: u8 = 2; +pub const CHARLIE: u8 = 3; +pub const DAVE: u8 = 4; +pub const EVE: u8 = 5; + +pub const KEYGEN_JOB_ID: u8 = 0; +pub const SIGN_JOB_ID: u8 = 1; + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MachineKind { + Large, + Medium, + Small, +} + +/// All prices are specified in USD/hr (in u64, so 1e6 = 1$) +fn price_targets(kind: MachineKind) -> PriceTargets { + match kind { + MachineKind::Large => PriceTargets { + cpu: 2_000, + mem: 1_000, + storage_hdd: 100, + storage_ssd: 200, + storage_nvme: 300, + }, + MachineKind::Medium => PriceTargets { + cpu: 1_000, + mem: 500, + storage_hdd: 50, + storage_ssd: 100, + storage_nvme: 150, + }, + MachineKind::Small => { + PriceTargets { cpu: 500, mem: 250, storage_hdd: 25, storage_ssd: 50, storage_nvme: 75 } + }, + } +} + +// Common test utilities and setup +pub(crate) fn cggmp21_blueprint() -> ServiceBlueprint> { + #[allow(deprecated)] + ServiceBlueprint { + metadata: ServiceMetadata { name: "CGGMP21 TSS".try_into().unwrap(), ..Default::default() }, + manager: BlueprintServiceManager::Evm(CGGMP21_BLUEPRINT), + master_manager_revision: MasterBlueprintServiceManagerRevision::Latest, + jobs: bounded_vec![ + JobDefinition { + metadata: JobMetadata { name: "keygen".try_into().unwrap(), ..Default::default() }, + params: bounded_vec![FieldType::Uint8], + result: bounded_vec![FieldType::List(Box::new(FieldType::Uint8))], + }, + JobDefinition { + metadata: JobMetadata { name: "sign".try_into().unwrap(), ..Default::default() }, + params: bounded_vec![ + FieldType::Uint64, + FieldType::List(Box::new(FieldType::Uint8)) + ], + result: bounded_vec![FieldType::List(Box::new(FieldType::Uint8))], + }, + ], + registration_params: bounded_vec![], + request_params: bounded_vec![], + gadget: Default::default(), + supported_membership_models: bounded_vec![ + MembershipModel::Fixed { min_operators: 1 }, + MembershipModel::Dynamic { min_operators: 1, max_operators: None }, + ], + } +} + +pub(crate) fn test_ecdsa_key() -> [u8; 65] { + let (ecdsa_key, _) = sp_core::ecdsa::Pair::generate(); + let secret = k256::ecdsa::SigningKey::from_slice(&ecdsa_key.seed()) + .expect("Should be able to create a secret key from a seed"); + let verifying_key = k256::ecdsa::VerifyingKey::from(secret); + let public_key = verifying_key.to_encoded_point(false); + public_key.to_bytes().to_vec().try_into().unwrap() +} + +pub(crate) fn get_security_requirement( + a: AssetId, + p: &[u8; 2], +) -> AssetSecurityRequirement { + AssetSecurityRequirement { + asset: Asset::Custom(a), + min_exposure_percent: Percent::from_percent(p[0]), + max_exposure_percent: Percent::from_percent(p[1]), + } +} + +pub(crate) fn get_security_commitment(a: AssetId, p: u8) -> AssetSecurityCommitment { + AssetSecurityCommitment { asset: Asset::Custom(a), exposure_percent: Percent::from_percent(p) } +} + +struct Deployment { + blueprint_id: u64, + service_id: u64, + bob_exposed_restake_percentage: Percent, +} + +/// A Helper function that creates a blueprint and service instance +fn deploy() -> Deployment { + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + let blueprint_id = Services::next_blueprint_id(); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); + + let bob = mock_pub_key(BOB); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + blueprint_id, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let eve = mock_pub_key(EVE); + let service_id = Services::next_instance_id(); + assert_ok!(Services::request( + RuntimeOrigin::signed(eve.clone()), + None, + blueprint_id, + vec![alice.clone()], + vec![bob.clone()], + Default::default(), + vec![get_security_requirement(WETH, &[10, 20])], + 100, + Asset::Custom(USDC), + 0, + MembershipModel::Fixed { min_operators: 1 }, + )); + + assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); + + let bob_exposed_restake_percentage = Percent::from_percent(10); + assert_ok!(Services::approve( + RuntimeOrigin::signed(bob.clone()), + service_id, + bob_exposed_restake_percentage, + vec![get_security_commitment(WETH, 10)], + )); + + assert!(Instances::::contains_key(service_id)); + + Deployment { blueprint_id, service_id, bob_exposed_restake_percentage } +} diff --git a/pallets/services/src/tests/registration.rs b/pallets/services/src/tests/registration.rs new file mode 100644 index 000000000..d0c9da270 --- /dev/null +++ b/pallets/services/src/tests/registration.rs @@ -0,0 +1,214 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use super::*; +use frame_support::{assert_err, assert_ok}; + +#[test] +fn register_on_blueprint() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + + assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); + + let bob = mock_pub_key(BOB); + let bob_ecdsa_key = test_ecdsa_key(); + + let registration_call = Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { + key: bob_ecdsa_key, + price_targets: price_targets(MachineKind::Large), + }, + Default::default(), + 0, + ); + assert_ok!(registration_call); + + assert_events(vec![RuntimeEvent::Services(crate::Event::Registered { + provider: bob.clone(), + blueprint_id: 0, + preferences: OperatorPreferences { + key: bob_ecdsa_key, + price_targets: price_targets(MachineKind::Large), + }, + registration_args: Default::default(), + })]); + + // The blueprint should be added to my blueprints in my profile. + let profile = OperatorsProfile::::get(bob.clone()).unwrap(); + assert!(profile.blueprints.contains(&0)); + + // if we try to register again, it should fail. + assert_err!( + Services::register( + RuntimeOrigin::signed(bob), + 0, + OperatorPreferences { key: bob_ecdsa_key, price_targets: Default::default() }, + Default::default(), + 0, + ), + crate::Error::::AlreadyRegistered + ); + + // if we try to register with a non active operator, should fail + assert_err!( + Services::register( + RuntimeOrigin::signed(mock_pub_key(10)), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + ), + crate::Error::::OperatorNotActive + ); + }); +} + +#[test] +fn pre_register_on_blueprint() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + + assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); + + let bob = mock_pub_key(BOB); + let pre_registration_call = Services::pre_register(RuntimeOrigin::signed(bob.clone()), 0); + assert_ok!(pre_registration_call); + + assert_events(vec![RuntimeEvent::Services(crate::Event::PreRegistration { + operator: bob.clone(), + blueprint_id: 0, + })]); + }); +} + +#[test] +fn update_price_targets() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + + assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); + + let bob = mock_pub_key(BOB); + let bob_operator_ecdsa_key = test_ecdsa_key(); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { + key: bob_operator_ecdsa_key, + price_targets: price_targets(MachineKind::Small) + }, + Default::default(), + 0, + )); + + assert_eq!( + Operators::::get(0, &bob).unwrap(), + OperatorPreferences { + key: bob_operator_ecdsa_key, + price_targets: price_targets(MachineKind::Small) + } + ); + + assert_events(vec![RuntimeEvent::Services(crate::Event::Registered { + provider: bob.clone(), + blueprint_id: 0, + preferences: OperatorPreferences { + key: bob_operator_ecdsa_key, + price_targets: price_targets(MachineKind::Small), + }, + registration_args: Default::default(), + })]); + + // update price targets + assert_ok!(Services::update_price_targets( + RuntimeOrigin::signed(bob.clone()), + 0, + price_targets(MachineKind::Medium), + )); + + assert_eq!( + Operators::::get(0, &bob).unwrap().price_targets, + price_targets(MachineKind::Medium) + ); + + assert_events(vec![RuntimeEvent::Services(crate::Event::PriceTargetsUpdated { + operator: bob, + blueprint_id: 0, + price_targets: price_targets(MachineKind::Medium), + })]); + + // try to update price targets when not registered + let charlie = mock_pub_key(CHARLIE); + assert_err!( + Services::update_price_targets( + RuntimeOrigin::signed(charlie), + 0, + price_targets(MachineKind::Medium) + ), + crate::Error::::NotRegistered + ); + }); +} + +#[test] +fn unregister_from_blueprint() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); + + let bob = mock_pub_key(BOB); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + assert_ok!(Services::unregister(RuntimeOrigin::signed(bob.clone()), 0)); + assert!(!Operators::::contains_key(0, &bob)); + + // The blueprint should be removed from my blueprints in my profile. + let profile = OperatorsProfile::::get(bob.clone()).unwrap(); + assert!(!profile.blueprints.contains(&0)); + + assert_events(vec![RuntimeEvent::Services(crate::Event::Unregistered { + operator: bob, + blueprint_id: 0, + })]); + + // try to deregister when not registered + let charlie = mock_pub_key(CHARLIE); + assert_err!( + Services::unregister(RuntimeOrigin::signed(charlie), 0), + crate::Error::::NotRegistered + ); + }); +} diff --git a/pallets/services/src/tests/service.rs b/pallets/services/src/tests/service.rs new file mode 100644 index 000000000..c80587428 --- /dev/null +++ b/pallets/services/src/tests/service.rs @@ -0,0 +1,482 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use super::*; +use frame_support::{assert_err, assert_ok}; +use sp_core::U256; +use sp_runtime::Percent; + +#[test] +fn request_service() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); + + // Register multiple operators + let bob = mock_pub_key(BOB); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let charlie = mock_pub_key(CHARLIE); + assert_ok!(Services::register( + RuntimeOrigin::signed(charlie.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let dave = mock_pub_key(DAVE); + assert_ok!(Services::register( + RuntimeOrigin::signed(dave.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let eve = mock_pub_key(EVE); + assert_ok!(Services::request( + RuntimeOrigin::signed(eve.clone()), + None, + 0, + vec![alice.clone()], + vec![bob.clone(), charlie.clone(), dave.clone()], + Default::default(), + vec![ + get_security_requirement(USDC, &[10, 20]), + get_security_requirement(WETH, &[10, 20]) + ], + 100, + Asset::Custom(USDC), + 0, + MembershipModel::Fixed { min_operators: 3 }, + )); + + assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); + + // Bob approves the request with security commitments + assert_ok!(Services::approve( + RuntimeOrigin::signed(bob.clone()), + 0, + Percent::from_percent(10), + vec![get_security_commitment(USDC, 10), get_security_commitment(WETH, 10)], + )); + + assert_events(vec![RuntimeEvent::Services(crate::Event::ServiceRequestApproved { + operator: bob.clone(), + request_id: 0, + blueprint_id: 0, + approved: vec![bob.clone()], + pending_approvals: vec![charlie.clone(), dave.clone()], + })]); + + // Charlie approves the request with security commitments + assert_ok!(Services::approve( + RuntimeOrigin::signed(charlie.clone()), + 0, + Percent::from_percent(20), + vec![get_security_commitment(USDC, 15), get_security_commitment(WETH, 15)], + )); + + assert_events(vec![RuntimeEvent::Services(crate::Event::ServiceRequestApproved { + operator: charlie.clone(), + request_id: 0, + blueprint_id: 0, + approved: vec![bob.clone(), charlie.clone()], + pending_approvals: vec![dave.clone()], + })]); + + // Dave approves the request with security commitments + assert_ok!(Services::approve( + RuntimeOrigin::signed(dave.clone()), + 0, + Percent::from_percent(30), + vec![get_security_commitment(USDC, 20), get_security_commitment(WETH, 20)], + )); + + assert_events(vec![ + RuntimeEvent::Services(crate::Event::ServiceRequestApproved { + operator: dave.clone(), + request_id: 0, + blueprint_id: 0, + approved: vec![bob.clone(), charlie.clone(), dave.clone()], + pending_approvals: vec![], + }), + RuntimeEvent::Services(crate::Event::ServiceInitiated { + owner: eve, + request_id: 0, + service_id: 0, + blueprint_id: 0, + assets: vec![Asset::Custom(USDC), Asset::Custom(WETH)], + }), + ]); + + // The request is now fully approved + assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 0); + + // Now the service should be initiated + assert!(Instances::::contains_key(0)); + + // The service should also be added to the services for each operator. + let profile = OperatorsProfile::::get(bob).unwrap(); + assert!(profile.services.contains(&0)); + let profile = OperatorsProfile::::get(charlie).unwrap(); + assert!(profile.services.contains(&0)); + let profile = OperatorsProfile::::get(dave).unwrap(); + assert!(profile.services.contains(&0)); + }); +} + +#[test] +fn request_service_with_no_assets() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); + let bob = mock_pub_key(BOB); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + let eve = mock_pub_key(EVE); + assert_err!( + Services::request( + RuntimeOrigin::signed(eve.clone()), + None, + 0, + vec![alice.clone()], + vec![bob.clone()], + Default::default(), + vec![], // no assets + 100, + Asset::Custom(USDC), + 0, + MembershipModel::Fixed { min_operators: 1 }, + ), + Error::::NoAssetsProvided + ); + }); +} + +#[test] +fn request_service_with_payment_asset() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + + assert_ok!(Services::create_blueprint( + RuntimeOrigin::signed(alice.clone()), + blueprint.clone() + )); + let bob = mock_pub_key(BOB); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let payment = 5 * 10u128.pow(6); // 5 USDC + let charlie = mock_pub_key(CHARLIE); + let before_balance = Assets::balance(USDC, charlie.clone()); + assert_ok!(Services::request( + RuntimeOrigin::signed(charlie.clone()), + None, + 0, + vec![], + vec![bob.clone()], + Default::default(), + vec![ + get_security_requirement(TNT, &[10, 20]), + get_security_requirement(USDC, &[10, 20]), + get_security_requirement(WETH, &[10, 20]) + ], + 100, + Asset::Custom(USDC), + payment, + MembershipModel::Fixed { min_operators: 1 }, + )); + + assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); + + // The Pallet account now has 5 USDC + assert_eq!(Assets::balance(USDC, Services::pallet_account()), payment); + // Charlie Balance should be decreased by 5 USDC + assert_eq!(Assets::balance(USDC, charlie.clone()), before_balance - payment); + + // Bob approves the request with security commitments + assert_ok!(Services::approve( + RuntimeOrigin::signed(bob.clone()), + 0, + Percent::from_percent(10), + vec![ + get_security_commitment(TNT, 10), + get_security_commitment(USDC, 10), + get_security_commitment(WETH, 10) + ], + )); + + // The request is now fully approved + assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 0); + + // The Payment should be now transferred to the MBSM. + let mbsm_address = Pallet::::mbsm_address_of(&blueprint).unwrap(); + let mbsm_account_id = address_to_account_id(mbsm_address); + assert_eq!(Assets::balance(USDC, mbsm_account_id), payment); + // Pallet account should have 0 USDC + assert_eq!(Assets::balance(USDC, Services::pallet_account()), 0); + + // Now the service should be initiated + assert!(Instances::::contains_key(0)); + }); +} + +#[test] +fn request_service_with_payment_token() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + + assert_ok!(Services::create_blueprint( + RuntimeOrigin::signed(alice.clone()), + blueprint.clone() + )); + let bob = mock_pub_key(BOB); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let payment = 5 * 10u128.pow(6); // 5 USDC + let charlie = mock_pub_key(CHARLIE); + assert_ok!(Services::request( + RuntimeOrigin::signed(address_to_account_id(mock_address(CHARLIE))), + Some(account_id_to_address(charlie.clone())), + 0, + vec![], + vec![bob.clone()], + Default::default(), + vec![ + get_security_requirement(TNT, &[10, 20]), + get_security_requirement(USDC, &[10, 20]), + get_security_requirement(WETH, &[10, 20]) + ], + 100, + Asset::Erc20(USDC_ERC20), + payment, + MembershipModel::Fixed { min_operators: 1 }, + )); + + assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); + + // The Pallet address now has 5 USDC + assert_ok!( + Services::query_erc20_balance_of(USDC_ERC20, Services::pallet_evm_account()) + .map(|(b, _)| b), + U256::from(payment) + ); + + // Bob approves the request with security commitments + assert_ok!(Services::approve( + RuntimeOrigin::signed(bob.clone()), + 0, + Percent::from_percent(10), + vec![ + get_security_commitment(TNT, 10), + get_security_commitment(USDC, 10), + get_security_commitment(WETH, 10) + ], + )); + + // The request is now fully approved + assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 0); + + // The Payment should be now transferred to the MBSM. + let mbsm_address = Pallet::::mbsm_address_of(&blueprint).unwrap(); + assert_ok!( + Services::query_erc20_balance_of(USDC_ERC20, mbsm_address).map(|(b, _)| b), + U256::from(payment) + ); + // Pallet account should have 0 USDC + assert_ok!( + Services::query_erc20_balance_of(USDC_ERC20, Services::pallet_evm_account()) + .map(|(b, _)| b), + U256::from(0) + ); + + // Now the service should be initiated + assert!(Instances::::contains_key(0)); + }); +} + +#[test] +fn reject_service_with_payment_token() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + + assert_ok!(Services::create_blueprint( + RuntimeOrigin::signed(alice.clone()), + blueprint.clone() + )); + let bob = mock_pub_key(BOB); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let payment = 5 * 10u128.pow(6); // 5 USDC + let charlie_address = mock_address(CHARLIE); + let charlie_evm_account_id = address_to_account_id(charlie_address); + let before_balance = Services::query_erc20_balance_of(USDC_ERC20, charlie_address) + .map(|(b, _)| b) + .unwrap_or_default(); + assert_ok!(Services::request( + RuntimeOrigin::signed(charlie_evm_account_id), + Some(charlie_address), + 0, + vec![], + vec![bob.clone()], + Default::default(), + vec![ + get_security_requirement(TNT, &[10, 20]), + get_security_requirement(USDC, &[10, 20]), + get_security_requirement(WETH, &[10, 20]) + ], + 100, + Asset::Erc20(USDC_ERC20), + payment, + MembershipModel::Fixed { min_operators: 1 }, + )); + + assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); + + // The Pallet address now has 5 USDC + assert_ok!( + Services::query_erc20_balance_of(USDC_ERC20, Services::pallet_evm_account()) + .map(|(b, _)| b), + U256::from(payment) + ); + // Charlie Balance should be decreased by 5 USDC + assert_ok!( + Services::query_erc20_balance_of(USDC_ERC20, charlie_address).map(|(b, _)| b), + before_balance - U256::from(payment) + ); + + // Bob rejects the request + assert_ok!(Services::reject(RuntimeOrigin::signed(bob.clone()), 0)); + + // The Payment should be now refunded to the requester. + // Pallet account should have 0 USDC + assert_ok!( + Services::query_erc20_balance_of(USDC_ERC20, Services::pallet_evm_account()) + .map(|(b, _)| b), + U256::from(0) + ); + // Charlie Balance should be back to the original + assert_ok!( + Services::query_erc20_balance_of(USDC_ERC20, charlie_address).map(|(b, _)| b), + before_balance + ); + }); +} + +#[test] +fn reject_service_with_payment_asset() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + + assert_ok!(Services::create_blueprint( + RuntimeOrigin::signed(alice.clone()), + blueprint.clone() + )); + let bob = mock_pub_key(BOB); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { key: test_ecdsa_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let payment = 5 * 10u128.pow(6); // 5 USDC + let charlie = mock_pub_key(CHARLIE); + let before_balance = Assets::balance(USDC, charlie.clone()); + assert_ok!(Services::request( + RuntimeOrigin::signed(charlie.clone()), + None, + 0, + vec![], + vec![bob.clone()], + Default::default(), + vec![ + get_security_requirement(TNT, &[10, 20]), + get_security_requirement(USDC, &[10, 20]), + get_security_requirement(WETH, &[10, 20]) + ], + 100, + Asset::Custom(USDC), + payment, + MembershipModel::Fixed { min_operators: 1 }, + )); + + assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); + + // The Pallet account now has 5 USDC + assert_eq!(Assets::balance(USDC, Services::pallet_account()), payment); + // Charlie Balance should be decreased by 5 USDC + assert_eq!(Assets::balance(USDC, charlie.clone()), before_balance - payment); + + // Bob rejects the request + assert_ok!(Services::reject(RuntimeOrigin::signed(bob.clone()), 0)); + + // The Payment should be now refunded to the requester. + // Pallet account should have 0 USDC + assert_eq!(Assets::balance(USDC, Services::pallet_account()), 0); + // Charlie Balance should be back to the original + assert_eq!(Assets::balance(USDC, charlie), before_balance); + }); +} diff --git a/pallets/services/src/tests/slashing.rs b/pallets/services/src/tests/slashing.rs new file mode 100644 index 000000000..de2fc0437 --- /dev/null +++ b/pallets/services/src/tests/slashing.rs @@ -0,0 +1,225 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use super::*; +use frame_support::{assert_err, assert_ok}; +use sp_runtime::Percent; + +#[test] +fn unapplied_slash() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + let Deployment { blueprint_id, service_id, bob_exposed_restake_percentage } = deploy(); + let eve = mock_pub_key(EVE); + let bob = mock_pub_key(BOB); + + // Set up a job call that will result in an invalid submission + let job_call_id = Services::next_job_call_id(); + assert_ok!(Services::call( + RuntimeOrigin::signed(eve.clone()), + service_id, + KEYGEN_JOB_ID, + bounded_vec![Field::Uint8(1)], + )); + + // Submit an invalid result that should trigger slashing + let mut dkg = vec![0u8; 33]; + dkg[32] = 1; + assert_ok!(Services::submit_result( + RuntimeOrigin::signed(bob.clone()), + 0, + job_call_id, + bounded_vec![Field::from(BoundedVec::try_from(dkg).unwrap())], + )); + + let slash_percent = Percent::from_percent(50); + let service = Services::services(service_id).unwrap(); + let slashing_origin = + Services::query_slashing_origin(&service).map(|(o, _)| o.unwrap()).unwrap(); + + // Slash the operator for the invalid result + assert_ok!(Services::slash( + RuntimeOrigin::signed(slashing_origin.clone()), + bob.clone(), + service_id, + slash_percent + )); + + // Verify the slash was recorded but not yet applied + assert_eq!(UnappliedSlashes::::iter_keys().collect::>().len(), 1); + + // Verify the correct event was emitted + System::assert_has_event(RuntimeEvent::Services(crate::Event::UnappliedSlash { + era: 0, + index: 0, + operator: bob.clone(), + blueprint_id, + service_id, + amount: (slash_percent * bob_exposed_restake_percentage).mul_floor( + ::OperatorDelegationManager::get_operator_stake(&bob), + ), + })); + }); +} + +#[test] +fn slash_account_not_an_operator() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + let Deployment { service_id, .. } = deploy(); + let karen = mock_pub_key(23); + + let service = Services::services(service_id).unwrap(); + let slashing_origin = + Services::query_slashing_origin(&service).map(|(o, _)| o.unwrap()).unwrap(); + + let slash_percent = Percent::from_percent(50); + + // Try to slash an operator that is not active in this service + assert_err!( + Services::slash( + RuntimeOrigin::signed(slashing_origin.clone()), + karen.clone(), + service_id, + slash_percent + ), + Error::::OffenderNotOperator + ); + }); +} + +#[test] +fn dispute() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + let Deployment { blueprint_id, service_id, bob_exposed_restake_percentage } = deploy(); + let bob = mock_pub_key(BOB); + let slash_percent = Percent::from_percent(50); + let service = Services::services(service_id).unwrap(); + let slashing_origin = + Services::query_slashing_origin(&service).map(|(o, _)| o.unwrap()).unwrap(); + + // Create a slash + assert_ok!(Services::slash( + RuntimeOrigin::signed(slashing_origin.clone()), + bob.clone(), + service_id, + slash_percent + )); + + assert_eq!(UnappliedSlashes::::iter_keys().collect::>().len(), 1); + + let era = 0; + let slash_index = 0; + + // Dispute the slash + let dispute_origin = + Services::query_dispute_origin(&service).map(|(o, _)| o.unwrap()).unwrap(); + + assert_ok!(Services::dispute( + RuntimeOrigin::signed(dispute_origin.clone()), + era, + slash_index + )); + + // Verify the slash was removed + assert_eq!(UnappliedSlashes::::iter_keys().collect::>().len(), 0); + + // Calculate expected slash amount + let bob_stake = ::OperatorDelegationManager::get_operator_stake(&bob); + let expected_slash_amount = + (slash_percent * bob_exposed_restake_percentage).mul_floor(bob_stake); + + // Verify the correct event was emitted + System::assert_has_event(RuntimeEvent::Services(crate::Event::SlashDiscarded { + era, + index: slash_index, + operator: bob.clone(), + blueprint_id, + service_id, + amount: expected_slash_amount, + })); + }); +} + +#[test] +fn dispute_with_unauthorized_origin() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + let Deployment { service_id, .. } = deploy(); + let eve = mock_pub_key(EVE); + let bob = mock_pub_key(BOB); + let slash_percent = Percent::from_percent(50); + let service = Services::services(service_id).unwrap(); + let slashing_origin = + Services::query_slashing_origin(&service).map(|(o, _)| o.unwrap()).unwrap(); + + // Create a slash + assert_ok!(Services::slash( + RuntimeOrigin::signed(slashing_origin.clone()), + bob.clone(), + service_id, + slash_percent + )); + + assert_eq!(UnappliedSlashes::::iter_keys().collect::>().len(), 1); + + let era = 0; + let slash_index = 0; + + // Try to dispute with an invalid origin + assert_err!( + Services::dispute(RuntimeOrigin::signed(eve.clone()), era, slash_index), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn dispute_an_already_applied_slash() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + let Deployment { service_id, .. } = deploy(); + let eve = mock_pub_key(EVE); + let bob = mock_pub_key(BOB); + let slash_percent = Percent::from_percent(50); + let service = Services::services(service_id).unwrap(); + let slashing_origin = + Services::query_slashing_origin(&service).map(|(o, _)| o.unwrap()).unwrap(); + + // Create a slash + assert_ok!(Services::slash( + RuntimeOrigin::signed(slashing_origin.clone()), + bob.clone(), + service_id, + slash_percent + )); + + assert_eq!(UnappliedSlashes::::iter_keys().collect::>().len(), 1); + + let era = 0; + let slash_index = 0; + + // Simulate a slash being applied by removing it + UnappliedSlashes::::remove(era, slash_index); + + // Try to dispute an already applied slash + assert_err!( + Services::dispute(RuntimeOrigin::signed(eve.clone()), era, slash_index), + Error::::UnappliedSlashNotFound + ); + }); +} diff --git a/pallets/services/src/types.rs b/pallets/services/src/types.rs index 0b9cc2a12..754419a58 100644 --- a/pallets/services/src/types.rs +++ b/pallets/services/src/types.rs @@ -15,8 +15,6 @@ // along with Tangle. If not, see . use super::*; -use parity_scale_codec::HasCompact; -use sp_std::prelude::*; use tangle_primitives::services::Constraints; pub type BalanceOf = @@ -42,22 +40,3 @@ pub type MaxAssetsPerServiceOf = as Constraints>::MaxAsset #[codec(mel_bound(skip_type_params(T)))] #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] pub struct ConstraintsOf(sp_std::marker::PhantomData); - -/// A pending slash record. The value of the slash has been computed but not applied yet, -/// rather deferred for several eras. -#[derive(Encode, Decode, RuntimeDebug, TypeInfo)] -#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] -pub struct UnappliedSlash { - /// The Service Instance Id on which the slash is applied. - pub service_id: u64, - /// The account ID of the offending operator. - pub operator: AccountId, - /// The operator's own slash. - pub own: Balance, - /// All other slashed restakers and amounts. - pub others: Vec<(AccountId, Balance)>, - /// Reporters of the offence; bounty payout recipients. - pub reporters: Vec, - /// The amount of payout. - pub payout: Balance, -} diff --git a/precompiles/multi-asset-delegation/src/mock.rs b/precompiles/multi-asset-delegation/src/mock.rs index 2f68c7c19..a41b6e9ac 100644 --- a/precompiles/multi-asset-delegation/src/mock.rs +++ b/precompiles/multi-asset-delegation/src/mock.rs @@ -43,6 +43,7 @@ use sp_runtime::{ traits::{IdentifyAccount, Verify}, AccountId32, BuildStorage, }; +use sp_staking::StakingInterface; use tangle_primitives::services::EvmRunner; use tangle_primitives::services::{EvmAddressMapping, EvmGasWeightMapping}; use tangle_primitives::traits::{RewardsManager, ServiceManager}; @@ -365,6 +366,8 @@ impl pallet_multi_asset_delegation::Config for Runtime { type Currency = Balances; type MinOperatorBondAmount = MinOperatorBondAmount; type BondDuration = BondDuration; + type CurrencyToVote = (); + type StakingInterface = MockStakingInterface; type ServiceManager = MockServiceManager; type LeaveOperatorsDelay = ConstU32<10>; type EvmRunner = MockedEvmRunner; @@ -563,3 +566,106 @@ impl ExtBuilder { ext } } + +pub struct MockStakingInterface; + +impl StakingInterface for MockStakingInterface { + type CurrencyToVote = (); + type AccountId = AccountId; + type Balance = Balance; + + fn minimum_nominator_bond() -> Self::Balance { + unimplemented!() + } + + fn minimum_validator_bond() -> Self::Balance { + unimplemented!() + } + + fn stash_by_ctrl(_controller: &Self::AccountId) -> Result { + unimplemented!() + } + + fn bonding_duration() -> sp_staking::EraIndex { + unimplemented!() + } + + fn current_era() -> sp_staking::EraIndex { + unimplemented!() + } + + fn stake(_who: &Self::AccountId) -> Result, DispatchError> { + unimplemented!() + } + + fn bond( + _who: &Self::AccountId, + _value: Self::Balance, + _payee: &Self::AccountId, + ) -> sp_runtime::DispatchResult { + unimplemented!() + } + + fn nominate( + _who: &Self::AccountId, + _validators: Vec, + ) -> sp_runtime::DispatchResult { + unimplemented!() + } + + fn chill(_who: &Self::AccountId) -> sp_runtime::DispatchResult { + unimplemented!() + } + + fn bond_extra(_who: &Self::AccountId, _extra: Self::Balance) -> sp_runtime::DispatchResult { + unimplemented!() + } + + fn unbond(_stash: &Self::AccountId, _value: Self::Balance) -> sp_runtime::DispatchResult { + unimplemented!() + } + + fn update_payee( + _stash: &Self::AccountId, + _reward_acc: &Self::AccountId, + ) -> sp_runtime::DispatchResult { + unimplemented!() + } + + fn withdraw_unbonded( + _stash: Self::AccountId, + _num_slashing_spans: u32, + ) -> Result { + unimplemented!() + } + + fn desired_validator_count() -> u32 { + unimplemented!() + } + + fn election_ongoing() -> bool { + unimplemented!() + } + + fn force_unstake(_who: Self::AccountId) -> sp_runtime::DispatchResult { + unimplemented!() + } + + fn is_exposed_in_era(_who: &Self::AccountId, _era: &sp_staking::EraIndex) -> bool { + unimplemented!() + } + + fn status( + _who: &Self::AccountId, + ) -> Result, DispatchError> { + unimplemented!() + } + + fn is_virtual_staker(_who: &Self::AccountId) -> bool { + unimplemented!() + } + + fn slash_reward_fraction() -> sp_runtime::Perbill { + unimplemented!() + } +} diff --git a/precompiles/multi-asset-delegation/src/tests.rs b/precompiles/multi-asset-delegation/src/tests.rs index d228be74f..639bf4bfd 100644 --- a/precompiles/multi-asset-delegation/src/tests.rs +++ b/precompiles/multi-asset-delegation/src/tests.rs @@ -56,7 +56,7 @@ fn test_delegate_assets_invalid_operator() { token_address: Default::default(), }, ) - .execute_reverts(|output| output == b"Dispatched call failed with error: Module(ModuleError { index: 6, error: [2, 0, 0, 0], message: Some(\"NotAnOperator\") })"); + .execute_reverts(|output| output == b"Dispatched call failed with error: Module(ModuleError { index: 6, error: [3, 0, 0, 0], message: Some(\"NotAnOperator\") })"); assert_eq!(Balances::free_balance(delegator_account), 500); }); @@ -263,7 +263,7 @@ fn test_delegate_assets_insufficient_balance() { token_address: Default::default(), }, ) - .execute_reverts(|output| output == b"Dispatched call failed with error: Module(ModuleError { index: 6, error: [14, 0, 0, 0], message: Some(\"InsufficientBalance\") })"); + .execute_reverts(|output| output == b"Dispatched call failed with error: Module(ModuleError { index: 6, error: [15, 0, 0, 0], message: Some(\"InsufficientBalance\") })"); assert_eq!(Balances::free_balance(delegator_account), 500); }); diff --git a/precompiles/services/Cargo.toml b/precompiles/services/Cargo.toml index 835643041..d7241d36a 100644 --- a/precompiles/services/Cargo.toml +++ b/precompiles/services/Cargo.toml @@ -74,6 +74,8 @@ pallet-evm-precompile-ed25519 = { workspace = true } pallet-evm-precompile-modexp = { workspace = true } pallet-evm-precompile-sha3fips = { workspace = true } pallet-evm-precompile-simple = { workspace = true } +pallet-evm-precompile-balances-erc20 = { workspace = true } +pallet-evm-precompileset-assets-erc20 = { workspace = true } pallet-session = { workspace = true } pallet-staking = { workspace = true } diff --git a/precompiles/services/Services.sol b/precompiles/services/Services.sol index 8950e545f..32baa74d9 100644 --- a/precompiles/services/Services.sol +++ b/precompiles/services/Services.sol @@ -17,55 +17,34 @@ interface Services { /// @param blueprint_data The blueprint data in SCALE-encoded format. function createBlueprint(bytes calldata blueprint_data) external; - /// @dev Register as an operator for a specific blueprint. - /// @param blueprint_id The blueprint ID. - /// @param preferences The operator preferences in SCALE-encoded format. - /// @param registration_args The registration arguments in SCALE-encoded format. - function registerOperator(uint256 blueprint_id, bytes calldata preferences, bytes calldata registration_args) external payable; - - /// @dev Pre-register as an operator for a specific blueprint. - /// @param blueprint_id The blueprint ID. - function preRegister(uint256 blueprint_id) external; - - /// @dev Unregister as an operator from a blueprint. - /// @param blueprint_id The blueprint ID. - function unregisterOperator(uint256 blueprint_id) external; - - /// @dev Request a new service. - /// @param blueprint_id The blueprint ID. - /// @param assets The list of asset IDs. - /// @param permitted_callers The permitted callers in SCALE-encoded format. - /// @param service_providers The service providers in SCALE-encoded format. - /// @param request_args The request arguments in SCALE-encoded format. - /// @param ttl The time-to-live for the request. - /// @param payment_asset_id The payment asset ID. - /// @param payment_token_address The payment token address. - /// @param amount The payment amount. - function requestService( - uint256 blueprint_id, - uint256[] calldata assets, - bytes calldata permitted_callers, - bytes calldata service_providers, - bytes calldata request_args, - uint256 ttl, - uint256 payment_asset_id, - address payment_token_address, - uint256 amount - ) external payable; + /// @notice Request a service from a specific blueprint + /// @param blueprint_id The ID of the blueprint + /// @param assets The list of assets to use for the service + /// @param permitted_callers_data The permitted callers for the service encoded as bytes + /// @param service_providers_data The service providers encoded as bytes + /// @param request_args_data The request arguments encoded as bytes + /// @param ttl The time-to-live of the service. + /// @param payment_asset_id The ID of the asset to use for payment (0 for native asset) + /// @param payment_token_address The address of the token to use for payment (0x0 for using the value of payment_asset_id) + /// @param payment_amount The amount to pay for the service (use msg.value if payment_asset_id is 0) + function requestService( + uint256 blueprint_id, + uint256[] calldata assets, + bytes calldata permitted_callers_data, + bytes calldata service_providers_data, + bytes calldata request_args_data, + uint256 ttl, + uint256 payment_asset_id, + address payment_token_address, + uint256 payment_amount, + uint32 min_operators, + uint32 max_operators + ) external payable; /// @dev Terminate a service. /// @param service_id The service ID. function terminateService(uint256 service_id) external; - /// @dev Approve a request. - /// @param request_id The request ID. - /// @param restaking_percent The restaking percentage. - function approve(uint256 request_id, uint8 restaking_percent) external; - - /// @dev Reject a service request. - /// @param request_id The request ID. - function reject(uint256 request_id) external; - /// @dev Call a job in the service. /// @param service_id The service ID. /// @param job The job ID. @@ -89,11 +68,6 @@ interface Services { /// @param index The index of the slash. function dispute(uint32 era, uint32 index) external; - /// @dev Update price targets for a blueprint. - /// @param blueprint_id The blueprint ID. - /// @param price_targets The new price targets. - function updatePriceTargets(uint256 blueprint_id, uint256[] calldata price_targets) external; - /// @dev Custom errors for the Services precompile error InvalidPermittedCallers(); error InvalidOperatorsList(); diff --git a/precompiles/services/src/lib.rs b/precompiles/services/src/lib.rs index f18b77a80..0230f7920 100644 --- a/precompiles/services/src/lib.rs +++ b/precompiles/services/src/lib.rs @@ -12,7 +12,7 @@ use sp_core::U256; use sp_runtime::{traits::Dispatchable, Percent}; use sp_std::{marker::PhantomData, vec::Vec}; use tangle_primitives::services::{ - Asset, Field, OperatorPreferences, PriceTargets, ServiceBlueprint, + Asset, AssetSecurityRequirement, Field, MembershipModel, OperatorPreferences, ServiceBlueprint, }; #[cfg(test)] @@ -137,13 +137,13 @@ where /// Request a new service. #[precompile::public( - "requestService(uint256,uint256[],bytes,bytes,bytes,uint256,uint256,address,uint256)" + "requestService(uint256,bytes[],bytes,bytes,bytes,uint256,uint256,address,uint256,uint32,int32)" )] #[precompile::payable] fn request_service( handle: &mut impl PrecompileHandle, blueprint_id: U256, - assets: Vec, + asset_security_requirements: Vec, permitted_callers_data: UnboundedBytes, service_providers_data: UnboundedBytes, request_args_data: UnboundedBytes, @@ -151,12 +151,16 @@ where payment_asset_id: U256, payment_token_address: Address, amount: U256, + min_operators: u32, + max_operators: u32, ) -> EvmResult { handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; let msg_sender = handle.context().caller; let origin = Runtime::AddressMapping::into_account_id(msg_sender); let blueprint_id: u64 = blueprint_id.as_u64(); + let asset_security_requirements_data: Vec> = + asset_security_requirements.into_iter().map(|x| x.into()).collect(); let permitted_callers_data: Vec = permitted_callers_data.into(); let service_providers_data: Vec = service_providers_data.into(); let request_args_data: Vec = request_args_data.into(); @@ -173,8 +177,12 @@ where Decode::decode(&mut &request_args_data[..]) .map_err(|_| revert_custom_error(Self::INVALID_REQUEST_ARGUMENTS))?; - let assets: Vec = - assets.into_iter().map(|asset| asset.as_u32().into()).collect(); + let asset_security_requirements: Vec> = + asset_security_requirements_data + .into_iter() + .map(|req| Decode::decode(&mut &req[..])) + .collect::>() + .map_err(|_| revert_custom_error(Self::INVALID_REQUEST_ARGUMENTS))?; let value_bytes = { let value = handle.context().apparent_value; @@ -223,16 +231,25 @@ where }, }; + let membership_model = if max_operators == 0 { + MembershipModel::Fixed { min_operators } + } else if max_operators == u32::MAX { + MembershipModel::Dynamic { min_operators, max_operators: None } + } else { + MembershipModel::Dynamic { min_operators, max_operators: Some(max_operators) } + }; + let call = pallet_services::Call::::request { evm_origin: Some(msg_sender), blueprint_id, permitted_callers, operators, ttl, - assets, + asset_security_requirements, request_args, payment_asset, value: amount, + membership_model, }; RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; @@ -255,39 +272,6 @@ where Ok(()) } - /// Approve a request. - #[precompile::public("approve(uint256,uint8)")] - fn approve( - handle: &mut impl PrecompileHandle, - request_id: U256, - restaking_percent: u8, - ) -> EvmResult { - handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; - let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); - let request_id: u64 = request_id.as_u64(); - let restaking_percent: Percent = Percent::from_percent(restaking_percent); - - let call = pallet_services::Call::::approve { request_id, restaking_percent }; - - RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; - - Ok(()) - } - - /// Reject a service request. - #[precompile::public("reject(uint256)")] - fn reject(handle: &mut impl PrecompileHandle, request_id: U256) -> EvmResult { - handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; - let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); - let request_id: u64 = request_id.as_u64(); - - let call = pallet_services::Call::::reject { request_id }; - - RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; - - Ok(()) - } - /// Call a job in the service. #[precompile::public("callJob(uint256,uint8,bytes)")] fn call_job( @@ -312,34 +296,6 @@ where Ok(()) } - /// Submit the result for a job call. - #[precompile::public("submitResult(uint256,uint256,bytes)")] - fn submit_result( - handle: &mut impl PrecompileHandle, - service_id: U256, - call_id: U256, - result_data: UnboundedBytes, - ) -> EvmResult { - handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; - let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); - let service_id: u64 = service_id.as_u64(); - let call_id: u64 = call_id.as_u64(); - let result: Vec = result_data.into(); - - let decoded_result: Vec> = - Decode::decode(&mut &result[..]).map_err(|_| revert("Invalid job result data"))?; - - let call = pallet_services::Call::::submit_result { - service_id, - call_id, - result: decoded_result, - }; - - RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; - - Ok(()) - } - /// Slash an operator (offender) for a service id with a given percent of their exposed stake /// for that service. /// @@ -384,52 +340,6 @@ where Ok(()) } - - /// Update price targets for a blueprint. - #[precompile::public("updatePriceTargets(uint256,uint256[])")] - fn update_price_targets( - handle: &mut impl PrecompileHandle, - blueprint_id: U256, - price_targets: Vec, - ) -> EvmResult { - handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; - let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); - - let blueprint_id: u64 = blueprint_id.as_u64(); - - // Convert price targets into the correct struct - let price_targets = { - let mut targets = price_targets.into_iter(); - PriceTargets { - cpu: targets.next().map_or(0, |v| v.as_u64()), - mem: targets.next().map_or(0, |v| v.as_u64()), - storage_hdd: targets.next().map_or(0, |v| v.as_u64()), - storage_ssd: targets.next().map_or(0, |v| v.as_u64()), - storage_nvme: targets.next().map_or(0, |v| v.as_u64()), - } - }; - - let call = - pallet_services::Call::::update_price_targets { blueprint_id, price_targets }; - - RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; - - Ok(()) - } - - /// Pre-register as an operator for a specific blueprint. - #[precompile::public("preRegister(uint256)")] - fn pre_register(handle: &mut impl PrecompileHandle, blueprint_id: U256) -> EvmResult { - handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; - let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); - - let blueprint_id: u64 = blueprint_id.as_u64(); - let call = pallet_services::Call::::pre_register { blueprint_id }; - - RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; - - Ok(()) - } } /// Revert with Custom Error Selector diff --git a/precompiles/services/src/mock.rs b/precompiles/services/src/mock.rs index 9fcdb89a9..5e1abd0db 100644 --- a/precompiles/services/src/mock.rs +++ b/precompiles/services/src/mock.rs @@ -26,6 +26,7 @@ use frame_support::{ pallet_prelude::{Hooks, Weight}, parameter_types, traits::{AsEnsureOriginWithArg, ConstU128, OneSessionHandler}, + PalletId, }; use frame_system::EnsureRoot; use mock_evm::MockedEvmRunner; @@ -215,7 +216,8 @@ impl pallet_staking::Config for Runtime { } parameter_types! { - pub const ServicesEVMAddress: H160 = H160([0x11; 20]); + pub const ServicesPalletId: PalletId = PalletId(*b"Services"); + pub const DummySlashRecipient: AccountId = AccountId32::new([0u8; 32]); } pub struct PalletEVMGasWeightMapping; @@ -251,7 +253,7 @@ impl pallet_assets::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = u128; type AssetId = AssetId; - type AssetIdParameter = u32; + type AssetIdParameter = u128; type Currency = Balances; type CreateOrigin = AsEnsureOriginWithArg>; type ForceOrigin = frame_system::EnsureRoot; @@ -389,14 +391,12 @@ impl From for sp_core::sr25519::Public { } } -pub type AssetId = u32; +pub type AssetId = u128; pub struct MockDelegationManager; -impl tangle_primitives::traits::MultiAssetDelegationInfo +impl tangle_primitives::traits::MultiAssetDelegationInfo for MockDelegationManager { - type AssetId = AssetId; - fn get_current_round() -> tangle_primitives::types::RoundIndex { Default::default() } @@ -419,30 +419,31 @@ impl tangle_primitives::traits::MultiAssetDelegationInfo, + _asset_id: &Asset, ) -> Balance { Default::default() } fn get_delegators_for_operator( _operator: &AccountId, - ) -> Vec<(AccountId, Balance, Asset)> { + ) -> Vec<(AccountId, Balance, Asset)> { Default::default() } - fn slash_operator( - _operator: &AccountId, - _blueprint_id: tangle_primitives::BlueprintId, - _percentage: sp_runtime::Percent, - ) { - } - fn get_user_deposit_with_locks( _who: &AccountId, - _asset_id: Asset, + _asset_id: Asset, ) -> Option> { None } + + fn has_delegator_selected_blueprint( + _delegator: &AccountId, + _operator: &AccountId, + _blueprint_id: tangle_primitives::BlueprintId, + ) -> bool { + true + } } parameter_types! { @@ -533,6 +534,10 @@ parameter_types! { #[derive(Default, Copy, Clone, Eq, PartialEq, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] pub const SlashDeferDuration: u32 = 7; + + #[derive(Default, Copy, Clone, Eq, PartialEq, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] + #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] + pub const NativeExposureMinimum: Percent = Percent::from_percent(10); } impl pallet_services::Config for Runtime { @@ -541,7 +546,9 @@ impl pallet_services::Config for Runtime { type Currency = Balances; type Fungibles = Assets; type AssetId = AssetId; - type PalletEVMAddress = ServicesEVMAddress; + type PalletId = ServicesPalletId; + type SlashRecipient = DummySlashRecipient; + type SlashManager = (); type EvmRunner = MockedEvmRunner; type EvmAddressMapping = PalletEVMAddressMapping; type EvmGasWeightMapping = PalletEVMGasWeightMapping; @@ -566,6 +573,7 @@ impl pallet_services::Config for Runtime { type MaxContainerImageTagLength = MaxContainerImageTagLength; type MaxAssetsPerService = MaxAssetsPerService; type MaxMasterBlueprintServiceManagerVersions = MaxMasterBlueprintServiceManagerRevisions; + type NativeExposureMinimum = NativeExposureMinimum; type Constraints = pallet_services::types::ConstraintsOf; type OperatorDelegationManager = MockDelegationManager; type SlashDeferDuration = SlashDeferDuration; @@ -603,6 +611,7 @@ pub const MBSM: H160 = H160([0x12; 20]); pub const CGGMP21_BLUEPRINT: H160 = H160([0x21; 20]); pub const USDC_ERC20: H160 = H160([0x23; 20]); +#[allow(dead_code)] pub const TNT: AssetId = 0; pub const USDC: AssetId = 1; pub const WETH: AssetId = 2; @@ -747,7 +756,7 @@ pub fn new_test_ext_raw_authorities(authorities: Vec) -> sp_io::TestE >::on_initialize(1); let call = ::EvmRunner::call( - Services::address(), + Services::pallet_evm_account(), USDC_ERC20, serde_json::from_value::(json!({ "name": "initialize", @@ -788,7 +797,7 @@ pub fn new_test_ext_raw_authorities(authorities: Vec) -> sp_io::TestE // Mint for a in authorities { let call = ::EvmRunner::call( - Services::address(), + Services::pallet_evm_account(), USDC_ERC20, serde_json::from_value::(json!({ "name": "mint", diff --git a/precompiles/services/src/mock_evm.rs b/precompiles/services/src/mock_evm.rs index 9d0878282..75995bc2a 100644 --- a/precompiles/services/src/mock_evm.rs +++ b/precompiles/services/src/mock_evm.rs @@ -15,7 +15,9 @@ // along with Tangle. If not, see . #![allow(clippy::all)] use crate::{ - mock::{AccountId, Balances, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, Timestamp}, + mock::{ + AccountId, AssetId, Balances, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, Timestamp, + }, ServicesPrecompile, ServicesPrecompileCall, }; use fp_evm::FeeCalculator; @@ -27,7 +29,10 @@ use frame_support::{ }; use pallet_ethereum::{EthereumBlockHashMapping, IntermediateStateRoot, PostLogContent, RawOrigin}; use pallet_evm::{EnsureAddressNever, EnsureAddressOrigin, OnChargeEVMTransaction}; -use precompile_utils::precompile_set::{AddressU64, PrecompileAt, PrecompileSetBuilder}; +use precompile_utils::precompile_set::{ + AddressU64, CallableByContract, CallableByPrecompile, PrecompileAt, PrecompileSetBuilder, + PrecompileSetStartingWith, +}; use sp_core::{keccak_256, ConstU32, H160, H256, U256}; use sp_runtime::{ traits::{DispatchInfoOf, Dispatchable}, @@ -36,8 +41,60 @@ use sp_runtime::{ }; use tangle_primitives::services::EvmRunner; -pub type Precompiles = - PrecompileSetBuilder, ServicesPrecompile>,)>; +use pallet_evm_precompile_balances_erc20::{Erc20BalancesPrecompile, Erc20Metadata}; +use pallet_evm_precompileset_assets_erc20::{AddressToAssetId, Erc20AssetsPrecompileSet}; + +pub struct NativeErc20Metadata; + +/// ERC20 metadata for the native token. +impl Erc20Metadata for NativeErc20Metadata { + /// Returns the name of the token. + fn name() -> &'static str { + "Tangle Testnet Network Token" + } + + /// Returns the symbol of the token. + fn symbol() -> &'static str { + "tTNT" + } + + /// Returns the decimals places of the token. + fn decimals() -> u8 { + 18 + } + + /// Must return `true` only if it represents the main native currency of + /// the network. It must be the currency used in `pallet_evm`. + fn is_native_currency() -> bool { + true + } +} + +/// The asset precompile address prefix. Addresses that match against this prefix will be routed +/// to Erc20AssetsPrecompileSet being marked as foreign +pub const ASSET_PRECOMPILE_ADDRESS_PREFIX: &[u8] = &[255u8; 4]; + +parameter_types! { + pub ForeignAssetPrefix: &'static [u8] = ASSET_PRECOMPILE_ADDRESS_PREFIX; +} + +pub type Precompiles = PrecompileSetBuilder< + R, + ( + PrecompileAt, ServicesPrecompile>, + PrecompileAt< + AddressU64<2050>, + Erc20BalancesPrecompile, + (CallableByContract, CallableByPrecompile), + >, + // Prefixed precompile sets (XC20) + PrecompileSetStartingWith< + ForeignAssetPrefix, + Erc20AssetsPrecompileSet, + CallableByContract, + >, + ), +>; pub type PCall = ServicesPrecompileCall; @@ -52,6 +109,28 @@ impl pallet_timestamp::Config for Runtime { type WeightInfo = (); } +const ASSET_ID_SIZE: usize = core::mem::size_of::(); + +impl AddressToAssetId for Runtime { + fn address_to_asset_id(address: H160) -> Option { + let mut data = [0u8; ASSET_ID_SIZE]; + let address_bytes: [u8; 20] = address.into(); + if ASSET_PRECOMPILE_ADDRESS_PREFIX.eq(&address_bytes[0..4]) { + data.copy_from_slice(&address_bytes[4..ASSET_ID_SIZE + 4]); + Some(AssetId::from_be_bytes(data)) + } else { + None + } + } + + fn asset_id_to_address(asset_id: AssetId) -> H160 { + let mut data = [0u8; 20]; + data[0..4].copy_from_slice(ASSET_PRECOMPILE_ADDRESS_PREFIX); + data[4..ASSET_ID_SIZE + 4].copy_from_slice(&asset_id.to_be_bytes()); + H160::from(data) + } +} + pub struct FixedGasPrice; impl FeeCalculator for FixedGasPrice { fn min_gas_price() -> (U256, Weight) { @@ -149,7 +228,7 @@ impl OnChargeEVMTransaction for CustomEVMCurrencyAdapter { who: &H160, fee: U256, ) -> Result> { - let pallet_services_address = pallet_services::Pallet::::address(); + let pallet_services_address = pallet_services::Pallet::::pallet_evm_account(); // Make pallet services account free to use if who == &pallet_services_address { return Ok(None); @@ -166,7 +245,7 @@ impl OnChargeEVMTransaction for CustomEVMCurrencyAdapter { base_fee: U256, already_withdrawn: Self::LiquidityInfo, ) -> Self::LiquidityInfo { - let pallet_services_address = pallet_services::Pallet::::address(); + let pallet_services_address = pallet_services::Pallet::::pallet_evm_account(); // Make pallet services account free to use if who == &pallet_services_address { return already_withdrawn; diff --git a/precompiles/services/src/tests.rs b/precompiles/services/src/tests.rs index 1eecb814f..ec334a097 100644 --- a/precompiles/services/src/tests.rs +++ b/precompiles/services/src/tests.rs @@ -6,17 +6,29 @@ use crate::{ }; use frame_support::assert_ok; use k256::ecdsa::{SigningKey, VerifyingKey}; -use pallet_services::{types::ConstraintsOf, Instances, Operators, OperatorsProfile}; +use pallet_services::{types::ConstraintsOf, Instances}; use parity_scale_codec::Encode; use precompile_utils::{prelude::UnboundedBytes, testing::*}; use sp_core::{ecdsa, Pair, H160, U256}; -use sp_runtime::{bounded_vec, AccountId32}; +use sp_runtime::{bounded_vec, AccountId32, Percent}; use tangle_primitives::services::{ - BlueprintServiceManager, FieldType, JobDefinition, JobMetadata, - MasterBlueprintServiceManagerRevision, OperatorPreferences, PriceTargets, ServiceBlueprint, - ServiceMetadata, + Asset, AssetSecurityCommitment, AssetSecurityRequirement, BlueprintServiceManager, FieldType, + JobDefinition, JobMetadata, MasterBlueprintServiceManagerRevision, MembershipModel, + OperatorPreferences, PriceTargets, ServiceBlueprint, ServiceMetadata, }; +fn get_security_requirement(a: AssetId, p: &[u8; 2]) -> AssetSecurityRequirement { + AssetSecurityRequirement { + asset: Asset::Custom(a), + min_exposure_percent: Percent::from_percent(p[0]), + max_exposure_percent: Percent::from_percent(p[1]), + } +} + +fn get_security_commitment(a: AssetId, p: u8) -> AssetSecurityCommitment { + AssetSecurityCommitment { asset: Asset::Custom(a), exposure_percent: Percent::from_percent(p) } +} + fn test_ecdsa_key() -> [u8; 65] { let (ecdsa_key, _) = ecdsa::Pair::generate(); let secret = SigningKey::from_slice(&ecdsa_key.seed()) @@ -67,19 +79,32 @@ fn cggmp21_blueprint() -> ServiceBlueprint> { JobDefinition { metadata: JobMetadata { name: "keygen".try_into().unwrap(), ..Default::default() }, params: bounded_vec![FieldType::Uint8], - result: bounded_vec![FieldType::Bytes], + result: bounded_vec![FieldType::List(Box::new(FieldType::Uint8))], }, JobDefinition { metadata: JobMetadata { name: "sign".try_into().unwrap(), ..Default::default() }, - params: bounded_vec![FieldType::Uint64, FieldType::Bytes], - result: bounded_vec![FieldType::Bytes], + params: bounded_vec![ + FieldType::Uint64, + FieldType::List(Box::new(FieldType::Uint8)) + ], + result: bounded_vec![FieldType::List(Box::new(FieldType::Uint8))], }, ], registration_params: bounded_vec![], request_params: bounded_vec![], gadget: Default::default(), + supported_membership_models: bounded_vec![ + MembershipModel::Fixed { min_operators: 1 }, + MembershipModel::Dynamic { min_operators: 1, max_operators: None }, + ], } } + +#[test] +fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() { + check_precompile_implements_solidity_interfaces(&["Services.sol"], PCall::supports_selector) +} + #[test] fn test_create_blueprint() { ExtBuilder.build().execute_with(|| { @@ -102,53 +127,10 @@ fn test_create_blueprint() { }); } -#[test] -fn test_register_operator() { - ExtBuilder.build().execute_with(|| { - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - // First create the blueprint - let blueprint_data = cggmp21_blueprint(); - - PrecompilesValue::get() - .prepare_test( - TestAccount::Alex, - H160::from_low_u64_be(1), - PCall::create_blueprint { - blueprint_data: UnboundedBytes::from(blueprint_data.encode()), - }, - ) - .execute_returns(()); - - // Now register operator - let preferences_data = OperatorPreferences { - key: test_ecdsa_key(), - price_targets: price_targets(MachineKind::Large), - } - .encode(); - - PrecompilesValue::get() - .prepare_test( - TestAccount::Bob, - H160::from_low_u64_be(1), - PCall::register_operator { - blueprint_id: U256::from(0), // We use the first blueprint - preferences: UnboundedBytes::from(preferences_data), - registration_args: UnboundedBytes::from(Vec::new()), - }, - ) - .execute_returns(()); - - // Check that the operator profile exists - let account: AccountId32 = TestAccount::Bob.into(); - assert!(OperatorsProfile::::get(account).is_ok()); - }); -} - #[test] fn test_request_service() { ExtBuilder.build().execute_with(|| { assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - // First create the blueprint let blueprint_data = cggmp21_blueprint(); PrecompilesValue::get() @@ -161,28 +143,22 @@ fn test_request_service() { ) .execute_returns(()); - // Now register operator - let preferences_data = OperatorPreferences { - key: test_ecdsa_key(), - price_targets: price_targets(MachineKind::Large), - } - .encode(); - - PrecompilesValue::get() - .prepare_test( - TestAccount::Bob, - H160::from_low_u64_be(1), - PCall::register_operator { - blueprint_id: U256::from(0), - preferences: UnboundedBytes::from(preferences_data), - registration_args: UnboundedBytes::from(vec![0u8]), - }, - ) - .execute_returns(()); + // Register operator using pallet function + let bob: AccountId32 = TestAccount::Bob.into(); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { + key: test_ecdsa_key(), + price_targets: price_targets(MachineKind::Large), + }, + Default::default(), + 0, + )); - // Finally, request the service + // Request service from EVM let permitted_callers_data: Vec = vec![TestAccount::Alex.into()]; - let service_providers_data: Vec = vec![TestAccount::Bob.into()]; + let service_providers_data: Vec = vec![bob.clone()]; let request_args_data = vec![0u8]; PrecompilesValue::get() @@ -190,27 +166,31 @@ fn test_request_service() { TestAccount::Alex, H160::from_low_u64_be(1), PCall::request_service { - blueprint_id: U256::from(0), // Use the first blueprint + blueprint_id: U256::from(0), permitted_callers_data: UnboundedBytes::from(permitted_callers_data.encode()), service_providers_data: UnboundedBytes::from(service_providers_data.encode()), request_args_data: UnboundedBytes::from(request_args_data), - assets: [WETH].into_iter().map(Into::into).collect(), + asset_security_requirements: vec![get_security_requirement(WETH, &[10, 20])] + .into_iter() + .map(|r| r.encode().into()) + .collect(), ttl: U256::from(1000), payment_asset_id: U256::from(0), payment_token_address: Default::default(), amount: U256::from(0), + min_operators: 1, + max_operators: u32::MAX, }, ) .execute_returns(()); - // Approve the service request by the operator(s) - PrecompilesValue::get() - .prepare_test( - TestAccount::Bob, - H160::from_low_u64_be(1), - PCall::approve { request_id: U256::from(0), restaking_percent: 10 }, - ) - .execute_returns(()); + // Approve using pallet function + assert_ok!(Services::approve( + RuntimeOrigin::signed(bob.clone()), + 0, + Percent::from_percent(10), + vec![get_security_commitment(WETH, 10)], + )); // Ensure the service instance is created assert!(Instances::::contains_key(0)); @@ -221,7 +201,6 @@ fn test_request_service() { fn test_request_service_with_erc20() { ExtBuilder.build().execute_with(|| { assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - // First create the blueprint let blueprint_data = cggmp21_blueprint(); PrecompilesValue::get() @@ -234,33 +213,27 @@ fn test_request_service_with_erc20() { ) .execute_returns(()); - // Now register operator - let preferences_data = OperatorPreferences { - key: test_ecdsa_key(), - price_targets: price_targets(MachineKind::Large), - } - .encode(); - - PrecompilesValue::get() - .prepare_test( - TestAccount::Bob, - H160::from_low_u64_be(1), - PCall::register_operator { - blueprint_id: U256::from(0), - preferences: UnboundedBytes::from(preferences_data), - registration_args: UnboundedBytes::from(vec![0u8]), - }, - ) - .execute_returns(()); + // Register operator using pallet function + let bob: AccountId32 = TestAccount::Bob.into(); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { + key: test_ecdsa_key(), + price_targets: price_targets(MachineKind::Large), + }, + Default::default(), + 0, + )); assert_ok!( - Services::query_erc20_balance_of(USDC_ERC20, Services::address()) + Services::query_erc20_balance_of(USDC_ERC20, Services::pallet_evm_account()) .map(|(balance, _)| balance), U256::zero(), ); - // Finally, request the service + let permitted_callers_data: Vec = vec![TestAccount::Alex.into()]; - let service_providers_data: Vec = vec![TestAccount::Bob.into()]; + let service_providers_data: Vec = vec![bob.clone()]; let request_args_data = vec![0u8]; let payment_amount = U256::from(5).mul(U256::from(10).pow(6.into())); // 5 USDC @@ -270,34 +243,38 @@ fn test_request_service_with_erc20() { TestAccount::Alex, H160::from_low_u64_be(1), PCall::request_service { - blueprint_id: U256::from(0), // Use the first blueprint + blueprint_id: U256::from(0), permitted_callers_data: UnboundedBytes::from(permitted_callers_data.encode()), service_providers_data: UnboundedBytes::from(service_providers_data.encode()), request_args_data: UnboundedBytes::from(request_args_data), - assets: [TNT, WETH].into_iter().map(Into::into).collect(), + asset_security_requirements: vec![get_security_requirement(WETH, &[10, 20])] + .into_iter() + .map(|r| r.encode().into()) + .collect(), ttl: U256::from(1000), payment_asset_id: U256::from(0), payment_token_address: USDC_ERC20.into(), amount: payment_amount, + min_operators: 1, + max_operators: u32::MAX, }, ) .execute_returns(()); // Services pallet address now should have 5 USDC assert_ok!( - Services::query_erc20_balance_of(USDC_ERC20, Services::address()) + Services::query_erc20_balance_of(USDC_ERC20, Services::pallet_evm_account()) .map(|(balance, _)| balance), payment_amount ); - // Approve the service request by the operator(s) - PrecompilesValue::get() - .prepare_test( - TestAccount::Bob, - H160::from_low_u64_be(1), - PCall::approve { request_id: U256::from(0), restaking_percent: 10 }, - ) - .execute_returns(()); + // Approve using pallet function + assert_ok!(Services::approve( + RuntimeOrigin::signed(bob.clone()), + 0, + Percent::from_percent(10), + vec![get_security_commitment(WETH, 10)], + )); // Ensure the service instance is created assert!(Instances::::contains_key(0)); @@ -308,7 +285,6 @@ fn test_request_service_with_erc20() { fn test_request_service_with_asset() { ExtBuilder.build().execute_with(|| { assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - // First create the blueprint let blueprint_data = cggmp21_blueprint(); PrecompilesValue::get() @@ -321,30 +297,23 @@ fn test_request_service_with_asset() { ) .execute_returns(()); - // Now register operator - let preferences_data = OperatorPreferences { - key: test_ecdsa_key(), - price_targets: price_targets(MachineKind::Large), - } - .encode(); - - PrecompilesValue::get() - .prepare_test( - TestAccount::Bob, - H160::from_low_u64_be(1), - PCall::register_operator { - blueprint_id: U256::from(0), - preferences: UnboundedBytes::from(preferences_data), - registration_args: UnboundedBytes::from(vec![0u8]), - }, - ) - .execute_returns(()); + // Register operator using pallet function + let bob: AccountId32 = TestAccount::Bob.into(); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { + key: test_ecdsa_key(), + price_targets: price_targets(MachineKind::Large), + }, + Default::default(), + 0, + )); - assert_eq!(Assets::balance(USDC, Services::account_id()), 0); + assert_eq!(Assets::balance(USDC, Services::pallet_account()), 0); - // Finally, request the service let permitted_callers_data: Vec = vec![TestAccount::Alex.into()]; - let service_providers_data: Vec = vec![TestAccount::Bob.into()]; + let service_providers_data: Vec = vec![bob.clone()]; let request_args_data = vec![0u8]; let payment_amount = U256::from(5).mul(U256::from(10).pow(6.into())); // 5 USDC @@ -354,91 +323,44 @@ fn test_request_service_with_asset() { TestAccount::Alex, H160::from_low_u64_be(1), PCall::request_service { - blueprint_id: U256::from(0), // Use the first blueprint + blueprint_id: U256::from(0), permitted_callers_data: UnboundedBytes::from(permitted_callers_data.encode()), service_providers_data: UnboundedBytes::from(service_providers_data.encode()), request_args_data: UnboundedBytes::from(request_args_data), - assets: [TNT, WETH].into_iter().map(Into::into).collect(), + asset_security_requirements: vec![get_security_requirement(WETH, &[10, 20])] + .into_iter() + .map(|r| r.encode().into()) + .collect(), ttl: U256::from(1000), payment_asset_id: U256::from(USDC), payment_token_address: Default::default(), amount: payment_amount, + min_operators: 1, + max_operators: u32::MAX, }, ) .execute_returns(()); // Services pallet address now should have 5 USDC - assert_eq!(Assets::balance(USDC, Services::account_id()), payment_amount.as_u128()); + assert_eq!(Assets::balance(USDC, Services::pallet_account()), payment_amount.as_u128()); - // Approve the service request by the operator(s) - PrecompilesValue::get() - .prepare_test( - TestAccount::Bob, - H160::from_low_u64_be(1), - PCall::approve { request_id: U256::from(0), restaking_percent: 10 }, - ) - .execute_returns(()); + // Approve using pallet function + assert_ok!(Services::approve( + RuntimeOrigin::signed(bob.clone()), + 0, + Percent::from_percent(10), + vec![get_security_commitment(WETH, 10)], + )); // Ensure the service instance is created assert!(Instances::::contains_key(0)); }); } -#[test] -fn test_unregister_operator() { - ExtBuilder.build().execute_with(|| { - assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - // First register operator (after blueprint creation) - let blueprint_data = cggmp21_blueprint(); - - PrecompilesValue::get() - .prepare_test( - TestAccount::Alex, - H160::from_low_u64_be(1), - PCall::create_blueprint { - blueprint_data: UnboundedBytes::from(blueprint_data.encode()), - }, - ) - .execute_returns(()); - - let preferences_data = OperatorPreferences { - key: test_ecdsa_key(), - price_targets: price_targets(MachineKind::Large), - } - .encode(); - - PrecompilesValue::get() - .prepare_test( - TestAccount::Bob, - H160::from_low_u64_be(1), - PCall::register_operator { - blueprint_id: U256::from(0), - preferences: UnboundedBytes::from(preferences_data), - registration_args: UnboundedBytes::from(vec![0u8]), - }, - ) - .execute_returns(()); - - // Now unregister operator - PrecompilesValue::get() - .prepare_test( - TestAccount::Bob, - H160::from_low_u64_be(1), - PCall::unregister_operator { blueprint_id: U256::from(0) }, - ) - .execute_returns(()); - - // Ensure the operator is removed - let bob_account: AccountId32 = TestAccount::Bob.into(); - assert!(!Operators::::contains_key(0, bob_account)); - }); -} - #[test] fn test_terminate_service() { ExtBuilder.build().execute_with(|| { assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); - // First request a service let blueprint_data = cggmp21_blueprint(); PrecompilesValue::get() @@ -451,26 +373,21 @@ fn test_terminate_service() { ) .execute_returns(()); - let preferences_data = OperatorPreferences { - key: test_ecdsa_key(), - price_targets: price_targets(MachineKind::Large), - } - .encode(); - - PrecompilesValue::get() - .prepare_test( - TestAccount::Bob, - H160::from_low_u64_be(1), - PCall::register_operator { - blueprint_id: U256::from(0), - preferences: UnboundedBytes::from(preferences_data), - registration_args: UnboundedBytes::from(vec![0u8]), - }, - ) - .execute_returns(()); + // Register operator using pallet function + let bob: AccountId32 = TestAccount::Bob.into(); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { + key: test_ecdsa_key(), + price_targets: price_targets(MachineKind::Large), + }, + Default::default(), + 0, + )); let permitted_callers_data: Vec = vec![TestAccount::Alex.into()]; - let service_providers_data: Vec = vec![TestAccount::Bob.into()]; + let service_providers_data: Vec = vec![bob.clone()]; let request_args_data = vec![0u8]; PrecompilesValue::get() @@ -482,23 +399,27 @@ fn test_terminate_service() { permitted_callers_data: UnboundedBytes::from(permitted_callers_data.encode()), service_providers_data: UnboundedBytes::from(service_providers_data.encode()), request_args_data: UnboundedBytes::from(request_args_data), - assets: [WETH].into_iter().map(Into::into).collect(), + asset_security_requirements: vec![get_security_requirement(WETH, &[10, 20])] + .into_iter() + .map(|r| r.encode().into()) + .collect(), ttl: U256::from(1000), payment_asset_id: U256::from(0), payment_token_address: Default::default(), amount: U256::from(0), + min_operators: 1, + max_operators: u32::MAX, }, ) .execute_returns(()); - // Approve the service request by the operator(s) - PrecompilesValue::get() - .prepare_test( - TestAccount::Bob, - H160::from_low_u64_be(1), - PCall::approve { request_id: U256::from(0), restaking_percent: 10 }, - ) - .execute_returns(()); + // Approve using pallet function + assert_ok!(Services::approve( + RuntimeOrigin::signed(bob.clone()), + 0, + Percent::from_percent(10), + vec![get_security_commitment(WETH, 10)], + )); assert!(Instances::::contains_key(0)); diff --git a/primitives/rpc/debug/src/lib.rs b/primitives/rpc/debug/src/lib.rs index e518d19b1..3099af758 100644 --- a/primitives/rpc/debug/src/lib.rs +++ b/primitives/rpc/debug/src/lib.rs @@ -17,6 +17,7 @@ #![cfg_attr(not(feature = "std"), no_std)] +#[warn(unused_imports)] use ethereum::{TransactionV0 as LegacyTransaction, TransactionV2 as Transaction}; use ethereum_types::{H160, H256, U256}; use parity_scale_codec::{Decode, Encode}; diff --git a/primitives/src/services/constraints.rs b/primitives/src/services/constraints.rs new file mode 100644 index 000000000..eaf0d4112 --- /dev/null +++ b/primitives/src/services/constraints.rs @@ -0,0 +1,61 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use frame_support::pallet_prelude::*; + +/// A Higher level abstraction of all the constraints. +pub trait Constraints { + /// Maximum number of fields in a job call. + type MaxFields: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Maximum size of a field in a job call. + type MaxFieldsSize: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Maximum length of metadata string length. + type MaxMetadataLength: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Maximum number of jobs per service. + type MaxJobsPerService: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Maximum number of Operators per service. + type MaxOperatorsPerService: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Maximum number of permitted callers per service. + type MaxPermittedCallers: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Maximum number of services per operator. + type MaxServicesPerOperator: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Maximum number of blueprints per operator. + type MaxBlueprintsPerOperator: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Maximum number of services per user. + type MaxServicesPerUser: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Maximum number of binaries per gadget. + type MaxBinariesPerGadget: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Maximum number of sources per gadget. + type MaxSourcesPerGadget: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Git owner maximum length. + type MaxGitOwnerLength: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Git repository maximum length. + type MaxGitRepoLength: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Git tag maximum length. + type MaxGitTagLength: Get + Default + Parameter + MaybeSerializeDeserialize; + /// binary name maximum length. + type MaxBinaryNameLength: Get + Default + Parameter + MaybeSerializeDeserialize; + /// IPFS hash maximum length. + type MaxIpfsHashLength: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Container registry maximum length. + type MaxContainerRegistryLength: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Container image name maximum length. + type MaxContainerImageNameLength: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Container image tag maximum length. + type MaxContainerImageTagLength: Get + Default + Parameter + MaybeSerializeDeserialize; + /// Maximum number of assets per service. + type MaxAssetsPerService: Get + Default + Parameter + MaybeSerializeDeserialize; +} diff --git a/primitives/src/services/evm.rs b/primitives/src/services/evm.rs new file mode 100644 index 000000000..85192a293 --- /dev/null +++ b/primitives/src/services/evm.rs @@ -0,0 +1,68 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use crate::Weight; +use fp_evm::CallInfo; +use frame_system::Config; +use sp_core::{H160, U256}; +use sp_std::vec::Vec; + +#[derive(Debug)] +pub struct RunnerError> { + pub error: E, + pub weight: Weight, +} + +#[allow(clippy::too_many_arguments)] +pub trait EvmRunner { + type Error: Into; + + fn call( + source: H160, + target: H160, + input: Vec, + value: U256, + gas_limit: u64, + is_transactional: bool, + validate: bool, + ) -> Result>; +} + +/// A mapping function that converts EVM gas to Substrate weight and vice versa +pub trait EvmGasWeightMapping { + /// Convert EVM gas to Substrate weight + fn gas_to_weight(gas: u64, without_base_weight: bool) -> Weight; + /// Convert Substrate weight to EVM gas + fn weight_to_gas(weight: Weight) -> u64; +} + +impl EvmGasWeightMapping for () { + fn gas_to_weight(_gas: u64, _without_base_weight: bool) -> Weight { + Default::default() + } + fn weight_to_gas(_weight: Weight) -> u64 { + Default::default() + } +} + +/// Trait to be implemented for evm address mapping. +pub trait EvmAddressMapping { + /// Convert an address to an account id. + fn into_account_id(address: H160) -> A; + + /// Convert an account id to an address. + fn into_address(account_id: A) -> H160; +} diff --git a/primitives/src/services/field.rs b/primitives/src/services/field.rs index 087328c06..fa46c54fa 100644 --- a/primitives/src/services/field.rs +++ b/primitives/src/services/field.rs @@ -275,9 +275,6 @@ pub enum FieldType { /// A Field of `String` type. #[codec(index = 10)] String, - /// A Field of `Vec` type. - #[codec(index = 11)] - Bytes, /// A Field of `Option` type. #[codec(index = 12)] Optional(Box), diff --git a/primitives/src/services/gadget.rs b/primitives/src/services/gadget.rs new file mode 100644 index 000000000..b605dd7b5 --- /dev/null +++ b/primitives/src/services/gadget.rs @@ -0,0 +1,299 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use super::{constraints::Constraints, BoundedString}; +use educe::Educe; +use frame_support::pallet_prelude::*; + +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; + +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] +pub enum Gadget { + /// A Gadget that is a WASM binary that will be executed. + /// inside the shell using the wasm runtime. + Wasm(WasmGadget), + /// A Gadget that is a native binary that will be executed. + /// inside the shell using the OS. + Native(NativeGadget), + /// A Gadget that is a container that will be executed. + /// inside the shell using the container runtime (e.g. Docker, Podman, etc.) + Container(ContainerGadget), +} + +impl Default for Gadget { + fn default() -> Self { + Gadget::Wasm(WasmGadget { runtime: WasmRuntime::Wasmtime, sources: Default::default() }) + } +} + +/// A binary that is stored in the Github release. +/// this will constuct the URL to the release and download the binary. +/// The URL will be in the following format: +/// https://github.com///releases/download/v/ +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] +pub struct GithubFetcher { + /// The owner of the repository. + pub owner: BoundedString, + /// The repository name. + pub repo: BoundedString, + /// The release tag of the repository. + /// NOTE: The tag should be a valid semver tag. + pub tag: BoundedString, + /// The names of the binary in the release by the arch and the os. + pub binaries: BoundedVec, C::MaxBinariesPerGadget>, +} + +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] +pub struct TestFetcher { + /// The cargo package name that contains the blueprint logic + pub cargo_package: BoundedString, + /// The specific binary name that contains the blueprint logic. + /// Should match up what is in the Cargo.toml file under [[bin]]/name + pub cargo_bin: BoundedString, + /// The base path to the workspace/crate + pub base_path: BoundedString, +} + +/// The CPU or System architecture. +#[derive( + PartialEq, + PartialOrd, + Ord, + Eq, + Encode, + Decode, + RuntimeDebug, + TypeInfo, + Clone, + Copy, + MaxEncodedLen, +)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum Architecture { + /// WebAssembly architecture (32-bit). + #[codec(index = 0)] + Wasm, + /// WebAssembly architecture (64-bit). + #[codec(index = 1)] + Wasm64, + /// WASI architecture (32-bit). + #[codec(index = 2)] + Wasi, + /// WASI architecture (64-bit). + #[codec(index = 3)] + Wasi64, + /// Amd architecture (32-bit). + #[codec(index = 4)] + Amd, + /// Amd64 architecture (x86_64). + #[codec(index = 5)] + Amd64, + /// Arm architecture (32-bit). + #[codec(index = 6)] + Arm, + /// Arm64 architecture (64-bit). + #[codec(index = 7)] + Arm64, + /// Risc-V architecture (32-bit). + #[codec(index = 8)] + RiscV, + /// Risc-V architecture (64-bit). + #[codec(index = 9)] + RiscV64, +} + +/// Operating System that the binary is compiled for. +#[derive( + Default, + PartialEq, + PartialOrd, + Ord, + Eq, + Encode, + Decode, + RuntimeDebug, + TypeInfo, + Clone, + Copy, + MaxEncodedLen, +)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum OperatingSystem { + /// Unknown operating system. + /// This is used when the operating system is not known + /// for example, for WASM, where the OS is not relevant. + #[default] + #[codec(index = 0)] + Unknown, + /// Linux operating system. + #[codec(index = 1)] + Linux, + /// Windows operating system. + #[codec(index = 2)] + Windows, + /// MacOS operating system. + #[codec(index = 3)] + MacOS, + /// BSD operating system. + #[codec(index = 4)] + BSD, +} + +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] +pub struct GadgetBinary { + /// CPU or System architecture. + pub arch: Architecture, + /// Operating System that the binary is compiled for. + pub os: OperatingSystem, + /// The name of the binary. + pub name: BoundedString, + /// The sha256 hash of the binary. + /// used to verify the downloaded binary. + pub sha256: [u8; 32], +} + +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] +pub struct GadgetSource { + /// The fetcher that will fetch the gadget from a remote source. + fetcher: GadgetSourceFetcher, +} + +/// A Gadget Source Fetcher is a fetcher that will fetch the gadget +/// from a remote source. +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] +pub enum GadgetSourceFetcher { + /// A Gadget that will be fetched from the IPFS. + #[codec(index = 0)] + IPFS(BoundedVec), + /// A Gadget that will be fetched from the Github release. + #[codec(index = 1)] + Github(GithubFetcher), + /// A Gadgets that will be fetched from the container registry. + #[codec(index = 2)] + ContainerImage(ImageRegistryFetcher), + /// For tests only + #[codec(index = 3)] + Testing(TestFetcher), +} + +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] +pub struct ImageRegistryFetcher { + /// The URL of the container registry. + registry: BoundedString, + /// The name of the image. + image: BoundedString, + /// The tag of the image. + tag: BoundedString, +} + +/// A WASM binary that contains all the compiled gadget code. +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] +pub struct WasmGadget { + /// Which runtime to use to execute the WASM binary. + pub runtime: WasmRuntime, + /// Where the WASM binary is stored. + pub sources: BoundedVec, C::MaxSourcesPerGadget>, +} + +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] +pub enum WasmRuntime { + /// The WASM binary will be executed using the WASMtime runtime. + #[codec(index = 0)] + Wasmtime, + /// The WASM binary will be executed using the Wasmer runtime. + #[codec(index = 1)] + Wasmer, +} + +/// A Native binary that contains all the gadget code. +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] +pub struct NativeGadget { + /// Where the WASM binary is stored. + pub sources: BoundedVec, C::MaxSourcesPerGadget>, +} + +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] +pub struct ContainerGadget { + /// Where the Image of the gadget binary is stored. + pub sources: BoundedVec, C::MaxSourcesPerGadget>, +} diff --git a/primitives/src/services/jobs.rs b/primitives/src/services/jobs.rs new file mode 100644 index 000000000..c279bdd73 --- /dev/null +++ b/primitives/src/services/jobs.rs @@ -0,0 +1,171 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use crate::services::{constraints::Constraints, types::TypeCheckError}; +use educe::Educe; +use frame_support::pallet_prelude::*; +use parity_scale_codec::Encode; +use sp_core::H160; + +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; + +use super::{ + field::{Field, FieldType}, + BoundedString, +}; + +/// A Job Definition is a definition of a job that can be called. +/// It contains the input and output fields of the job with the permitted caller. +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Default(bound()), Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] +pub struct JobDefinition { + /// The metadata of the job. + pub metadata: JobMetadata, + /// These are parameters that are required for this job. + /// i.e. the input. + pub params: BoundedVec, + /// These are the result, the return values of this job. + /// i.e. the output. + pub result: BoundedVec, +} + +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Default(bound()), Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] +pub struct JobMetadata { + /// The Job name. + pub name: BoundedString, + /// The Job description. + pub description: Option>, +} + +/// A Job Call is a call to execute a job using it's job definition. +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe( + Default(bound(AccountId: Default)), + Clone(bound(AccountId: Clone)), + PartialEq(bound(AccountId: PartialEq)), + Eq +)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(not(feature = "std"), derive(RuntimeDebugNoBound))] +#[cfg_attr( + feature = "std", + derive(Serialize, Deserialize), + serde(bound(serialize = "AccountId: Serialize", deserialize = "AccountId: Deserialize<'de>")), + educe(Debug(bound(AccountId: core::fmt::Debug))) +)] +pub struct JobCall { + /// The Service ID that this call is for. + pub service_id: u64, + /// The job definition index in the service that this call is for. + pub job: u8, + /// The supplied arguments for this job call. + pub args: BoundedVec, C::MaxFields>, +} + +/// A Job Call Result is the result of a job call. +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe( + Default(bound(AccountId: Default)), + Clone(bound(AccountId: Clone)), + PartialEq(bound(AccountId: PartialEq)), + Eq +)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(not(feature = "std"), derive(RuntimeDebugNoBound))] +#[cfg_attr( + feature = "std", + derive(Serialize, Deserialize), + serde(bound(serialize = "AccountId: Serialize", deserialize = "AccountId: Deserialize<'de>")), + educe(Debug(bound(AccountId: core::fmt::Debug))) +)] +pub struct JobCallResult { + /// The id of the service. + pub service_id: u64, + /// The id of the job call. + pub call_id: u64, + /// The result of the job call. + pub result: BoundedVec, C::MaxFields>, +} + +/// A Job Result verifier is a verifier that will verify the result of a job call +/// using different verification methods. +#[derive(Default, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Clone, MaxEncodedLen)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum JobResultVerifier { + /// No verification is needed. + #[default] + None, + /// An EVM Contract Address that will verify the result. + Evm(H160), + // NOTE(@shekohex): Add more verification methods here. +} + +/// Type checks the supplied arguments against the parameters. +pub fn type_checker( + params: &[FieldType], + args: &[Field], +) -> Result<(), TypeCheckError> { + if params.len() != args.len() { + return Err(TypeCheckError::NotEnoughArguments { + expected: params.len() as u8, + actual: args.len() as u8, + }); + } + for i in 0..args.len() { + let arg = &args[i]; + let expected = ¶ms[i]; + if arg != expected { + return Err(TypeCheckError::ArgumentTypeMismatch { + index: i as u8, + expected: expected.clone(), + actual: arg.clone().into(), + }); + } + } + Ok(()) +} + +impl JobCall { + /// Check if the supplied arguments match the job definition types. + pub fn type_check(&self, job_def: &JobDefinition) -> Result<(), TypeCheckError> { + type_checker(&job_def.params, &self.args) + } +} + +impl JobCallResult { + /// Check if the supplied result match the job definition types. + pub fn type_check(&self, job_def: &JobDefinition) -> Result<(), TypeCheckError> { + type_checker(&job_def.result, &self.result) + } +} diff --git a/primitives/src/services/mod.rs b/primitives/src/services/mod.rs index 115f4c262..f25c34642 100644 --- a/primitives/src/services/mod.rs +++ b/primitives/src/services/mod.rs @@ -15,1223 +15,19 @@ // along with Tangle. If not, see . //! Services primitives. -use crate::Weight; -use educe::Educe; -use fp_evm::CallInfo; -use frame_support::pallet_prelude::*; -use serde::Deserializer; -#[cfg(feature = "std")] -use serde::{Deserialize, Serialize}; -use sp_core::{ByteArray, RuntimeDebug, H160, U256}; -use sp_runtime::Percent; - -#[cfg(not(feature = "std"))] -use alloc::{string::String, vec, vec::Vec}; +pub mod constraints; +pub mod evm; pub mod field; -pub use field::*; - -use super::Account; - -/// A Higher level abstraction of all the constraints. -pub trait Constraints { - /// Maximum number of fields in a job call. - type MaxFields: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Maximum size of a field in a job call. - type MaxFieldsSize: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Maximum length of metadata string length. - type MaxMetadataLength: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Maximum number of jobs per service. - type MaxJobsPerService: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Maximum number of Operators per service. - type MaxOperatorsPerService: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Maximum number of permitted callers per service. - type MaxPermittedCallers: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Maximum number of services per operator. - type MaxServicesPerOperator: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Maximum number of blueprints per operator. - type MaxBlueprintsPerOperator: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Maximum number of services per user. - type MaxServicesPerUser: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Maximum number of binaries per gadget. - type MaxBinariesPerGadget: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Maximum number of sources per gadget. - type MaxSourcesPerGadget: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Git owner maximum length. - type MaxGitOwnerLength: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Git repository maximum length. - type MaxGitRepoLength: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Git tag maximum length. - type MaxGitTagLength: Get + Default + Parameter + MaybeSerializeDeserialize; - /// binary name maximum length. - type MaxBinaryNameLength: Get + Default + Parameter + MaybeSerializeDeserialize; - /// IPFS hash maximum length. - type MaxIpfsHashLength: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Container registry maximum length. - type MaxContainerRegistryLength: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Container image name maximum length. - type MaxContainerImageNameLength: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Container image tag maximum length. - type MaxContainerImageTagLength: Get + Default + Parameter + MaybeSerializeDeserialize; - /// Maximum number of assets per service. - type MaxAssetsPerService: Get + Default + Parameter + MaybeSerializeDeserialize; -} - -/// A Job Definition is a definition of a job that can be called. -/// It contains the input and output fields of the job with the permitted caller. - -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe(Default(bound()), Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] -pub struct JobDefinition { - /// The metadata of the job. - pub metadata: JobMetadata, - /// These are parameters that are required for this job. - /// i.e. the input. - pub params: BoundedVec, - /// These are the result, the return values of this job. - /// i.e. the output. - pub result: BoundedVec, -} - -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe(Default(bound()), Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] -pub struct JobMetadata { - /// The Job name. - pub name: BoundedString, - /// The Job description. - pub description: Option>, -} - -/// A Job Call is a call to execute a job using it's job definition. -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe( - Default(bound(AccountId: Default)), - Clone(bound(AccountId: Clone)), - PartialEq(bound(AccountId: PartialEq)), - Eq -)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(not(feature = "std"), derive(RuntimeDebugNoBound))] -#[cfg_attr( - feature = "std", - derive(Serialize, Deserialize), - serde(bound(serialize = "AccountId: Serialize", deserialize = "AccountId: Deserialize<'de>")), - educe(Debug(bound(AccountId: core::fmt::Debug))) -)] -pub struct JobCall { - /// The Service ID that this call is for. - pub service_id: u64, - /// The job definition index in the service that this call is for. - pub job: u8, - /// The supplied arguments for this job call. - pub args: BoundedVec, C::MaxFields>, -} - -/// Type checks the supplied arguments against the parameters. -pub fn type_checker( - params: &[FieldType], - args: &[Field], -) -> Result<(), TypeCheckError> { - if params.len() != args.len() { - return Err(TypeCheckError::NotEnoughArguments { - expected: params.len() as u8, - actual: args.len() as u8, - }); - } - for i in 0..args.len() { - let arg = &args[i]; - let expected = ¶ms[i]; - if arg != expected { - return Err(TypeCheckError::ArgumentTypeMismatch { - index: i as u8, - expected: expected.clone(), - actual: arg.clone().into(), - }); - } - } - Ok(()) -} - -impl JobCall { - /// Check if the supplied arguments match the job definition types. - pub fn type_check(&self, job_def: &JobDefinition) -> Result<(), TypeCheckError> { - type_checker(&job_def.params, &self.args) - } -} - -/// A Job Call Result is the result of a job call. -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe( - Default(bound(AccountId: Default)), - Clone(bound(AccountId: Clone)), - PartialEq(bound(AccountId: PartialEq)), - Eq -)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(not(feature = "std"), derive(RuntimeDebugNoBound))] -#[cfg_attr( - feature = "std", - derive(Serialize, Deserialize), - serde(bound(serialize = "AccountId: Serialize", deserialize = "AccountId: Deserialize<'de>")), - educe(Debug(bound(AccountId: core::fmt::Debug))) -)] -pub struct JobCallResult { - /// The id of the service. - pub service_id: u64, - /// The id of the job call. - pub call_id: u64, - /// The result of the job call. - pub result: BoundedVec, C::MaxFields>, -} - -impl JobCallResult { - /// Check if the supplied result match the job definition types. - pub fn type_check(&self, job_def: &JobDefinition) -> Result<(), TypeCheckError> { - type_checker(&job_def.result, &self.result) - } -} - -/// A Job Result verifier is a verifier that will verify the result of a job call -/// using different verification methods. -#[derive(Default, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Clone, MaxEncodedLen)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub enum JobResultVerifier { - /// No verification is needed. - #[default] - None, - /// An EVM Contract Address that will verify the result. - Evm(sp_core::H160), - // NOTE(@shekohex): Add more verification methods here. -} - -/// An error that can occur during type checking. -#[derive(PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Clone, MaxEncodedLen)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub enum TypeCheckError { - /// The argument type does not match the expected type. - ArgumentTypeMismatch { - /// The index of the argument. - index: u8, - /// The expected type. - expected: FieldType, - /// The actual type. - actual: FieldType, - }, - /// Not enough arguments were supplied. - NotEnoughArguments { - /// The number of arguments that were expected. - expected: u8, - /// The number of arguments that were supplied. - actual: u8, - }, - /// The result type does not match the expected type. - ResultTypeMismatch { - /// The index of the argument. - index: u8, - /// The expected type. - expected: FieldType, - /// The actual type. - actual: FieldType, - }, -} - -impl frame_support::traits::PalletError for TypeCheckError { - const MAX_ENCODED_SIZE: usize = 2; -} - -// -*** Service ***- - -/// Blueprint Service Manager is a smart contract that will manage the service lifecycle. -#[derive(PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Clone, Copy, MaxEncodedLen)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -#[non_exhaustive] -pub enum BlueprintServiceManager { - /// A Smart contract that will manage the service lifecycle. - Evm(sp_core::H160), -} - -impl BlueprintServiceManager { - pub fn try_into_evm(self) -> Result { - match self { - Self::Evm(addr) => Ok(addr), - } - } -} - -impl Default for BlueprintServiceManager { - fn default() -> Self { - Self::Evm(Default::default()) - } -} - -/// Master Blueprint Service Manager Revision. -#[derive( - Default, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Clone, Copy, MaxEncodedLen, -)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -#[non_exhaustive] -pub enum MasterBlueprintServiceManagerRevision { - /// Use Whatever the latest revision available on-chain. - /// - /// This is the default value. - #[default] - #[codec(index = 0)] - Latest, - - /// Use a specific revision number. - /// - /// Note: Must be already deployed on-chain. - #[codec(index = 1)] - Specific(u32), -} - -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe(Default(bound()), Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] -pub struct ServiceMetadata { - /// The Service name. - pub name: BoundedString, - /// The Service description. - pub description: Option>, - /// The Service author. - /// Could be a company or a person. - pub author: Option>, - /// The Job category. - pub category: Option>, - /// Code Repository URL. - /// Could be a github, gitlab, or any other code repository. - pub code_repository: Option>, - /// Service Logo URL. - pub logo: Option>, - /// Service Website URL. - pub website: Option>, - /// Service License. - pub license: Option>, -} - -/// A Service Blueprint is a the main definition of a service. -/// it contains the metadata of the service, the job definitions, and other hooks, along with the -/// gadget that will be executed when one of the jobs is calling this service. -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe(Default(bound()), Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] -pub struct ServiceBlueprint { - /// The metadata of the service. - pub metadata: ServiceMetadata, - /// The job definitions that are available in this service. - pub jobs: BoundedVec, C::MaxJobsPerService>, - /// The parameters that are required for the service registration. - pub registration_params: BoundedVec, - /// The request hook that will be called before creating a service from the service blueprint. - /// The parameters that are required for the service request. - pub request_params: BoundedVec, - /// A Blueprint Manager is a smart contract that implements the `IBlueprintServiceManager` - /// interface. - pub manager: BlueprintServiceManager, - /// The Revision number of the Master Blueprint Service Manager. - /// - /// If not sure what to use, use `MasterBlueprintServiceManagerRevision::default()` which will - /// use the latest revision available. - pub master_manager_revision: MasterBlueprintServiceManagerRevision, - /// The gadget that will be executed for the service. - pub gadget: Gadget, -} - -impl ServiceBlueprint { - /// Check if the supplied arguments match the registration parameters. - pub fn type_check_registration( - &self, - args: &[Field], - ) -> Result<(), TypeCheckError> { - type_checker(&self.registration_params, args) - } - - /// Check if the supplied arguments match the request parameters. - pub fn type_check_request( - &self, - args: &[Field], - ) -> Result<(), TypeCheckError> { - type_checker(&self.request_params, args) - } - - /// Converts the struct to ethabi ParamType. - pub fn to_ethabi_param_type() -> ethabi::ParamType { - ethabi::ParamType::Tuple(vec![ - // Service Metadata - ethabi::ParamType::Tuple(vec![ - // Service Name - ethabi::ParamType::String, - // Service Description - ethabi::ParamType::String, - // Service Author - ethabi::ParamType::String, - // Service Category - ethabi::ParamType::String, - // Code Repository - ethabi::ParamType::String, - // Service Logo - ethabi::ParamType::String, - // Service Website - ethabi::ParamType::String, - // Service License - ethabi::ParamType::String, - ]), - // Job Definitions ? - // Registration Parameters ? - // Request Parameters ? - // Blueprint Manager - ethabi::ParamType::Address, - // Master Manager Revision - ethabi::ParamType::Uint(32), - // Gadget ? - ]) - } - - /// Converts the struct to ethabi Param. - pub fn to_ethabi_param() -> ethabi::Param { - ethabi::Param { - name: String::from("blueprint"), - kind: Self::to_ethabi_param_type(), - internal_type: Some(String::from("struct MasterBlueprintServiceManager.Blueprint")), - } - } - - /// Converts the struct to ethabi Token. - pub fn to_ethabi(&self) -> ethabi::Token { - ethabi::Token::Tuple(vec![ - // Service Metadata - ethabi::Token::Tuple(vec![ - // Service Name - ethabi::Token::String(self.metadata.name.as_str().into()), - // Service Description - ethabi::Token::String( - self.metadata - .description - .as_ref() - .map(|v| v.as_str().into()) - .unwrap_or_default(), - ), - // Service Author - ethabi::Token::String( - self.metadata.author.as_ref().map(|v| v.as_str().into()).unwrap_or_default(), - ), - // Service Category - ethabi::Token::String( - self.metadata.category.as_ref().map(|v| v.as_str().into()).unwrap_or_default(), - ), - // Code Repository - ethabi::Token::String( - self.metadata - .code_repository - .as_ref() - .map(|v| v.as_str().into()) - .unwrap_or_default(), - ), - // Service Logo - ethabi::Token::String( - self.metadata.logo.as_ref().map(|v| v.as_str().into()).unwrap_or_default(), - ), - // Service Website - ethabi::Token::String( - self.metadata.website.as_ref().map(|v| v.as_str().into()).unwrap_or_default(), - ), - // Service License - ethabi::Token::String( - self.metadata.license.as_ref().map(|v| v.as_str().into()).unwrap_or_default(), - ), - ]), - // Job Definitions ? - // Registration Parameters ? - // Request Parameters ? - // Blueprint Manager - match self.manager { - BlueprintServiceManager::Evm(addr) => ethabi::Token::Address(addr), - }, - // Master Manager Revision - match self.master_manager_revision { - MasterBlueprintServiceManagerRevision::Latest => { - ethabi::Token::Uint(ethabi::Uint::MAX) - }, - MasterBlueprintServiceManagerRevision::Specific(rev) => { - ethabi::Token::Uint(rev.into()) - }, - }, - // Gadget ? - ]) - } -} - -/// A service request is a request to create a service from a service blueprint. -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe( - Default(bound(AccountId: Default, BlockNumber: Default, AssetId: Default)), - Clone(bound(AccountId: Clone, BlockNumber: Clone, AssetId: Clone)), - PartialEq(bound(AccountId: PartialEq, BlockNumber: PartialEq, AssetId: PartialEq)), - Eq -)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(not(feature = "std"), derive(RuntimeDebugNoBound))] -#[cfg_attr( - feature = "std", - derive(Serialize, Deserialize), - serde(bound( - serialize = "AccountId: Serialize, BlockNumber: Serialize, AssetId: Serialize", - deserialize = "AccountId: Deserialize<'de>, BlockNumber: Deserialize<'de>, AssetId: Deserialize<'de>" - )), - educe(Debug(bound(AccountId: core::fmt::Debug, BlockNumber: core::fmt::Debug, AssetId: core::fmt::Debug))) -)] -pub struct ServiceRequest { - /// The service blueprint ID. - pub blueprint: u64, - /// The owner of the service. - pub owner: AccountId, - /// The permitted caller(s) of the service. - pub permitted_callers: BoundedVec, - /// Asset(s) used to secure the service instance. - pub assets: BoundedVec, - /// The Lifetime of the service. - pub ttl: BlockNumber, - /// The supplied arguments for the service request. - pub args: BoundedVec, C::MaxFields>, - /// The Selected Operator(s) with their approval state. - pub operators_with_approval_state: - BoundedVec<(AccountId, ApprovalState), C::MaxOperatorsPerService>, -} - -impl - ServiceRequest -{ - /// Returns true if all the operators are [ApprovalState::Approved]. - pub fn is_approved(&self) -> bool { - self.operators_with_approval_state - .iter() - .all(|(_, state)| matches!(state, ApprovalState::Approved { .. })) - } - - /// Returns true if any the operators are [ApprovalState::Pending]. - pub fn is_pending(&self) -> bool { - self.operators_with_approval_state - .iter() - .any(|(_, state)| state == &ApprovalState::Pending) - } - - /// Returns true if any the operators are [ApprovalState::Rejected]. - pub fn is_rejected(&self) -> bool { - self.operators_with_approval_state - .iter() - .any(|(_, state)| state == &ApprovalState::Rejected) - } -} - -/// A staging service payment is a payment that is made for a service request -/// but will be paid when the service is created or refunded if the service is rejected. -#[derive(PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Copy, Clone, MaxEncodedLen)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct StagingServicePayment { - /// The service request ID. - pub request_id: u64, - /// Where the refund should go. - pub refund_to: Account, - /// The Asset used in the payment. - pub asset: Asset, - /// The amount of the asset that is paid. - pub amount: Balance, -} - -impl Default for StagingServicePayment -where - AccountId: ByteArray, - AssetId: sp_runtime::traits::Zero, - Balance: Default, -{ - fn default() -> Self { - Self { - request_id: Default::default(), - refund_to: Account::default(), - asset: Asset::default(), - amount: Default::default(), - } - } -} - -/// A Service is an instance of a service blueprint. -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe( - Default(bound(AccountId: Default, BlockNumber: Default, AssetId: Default)), - Clone(bound(AccountId: Clone, BlockNumber: Clone, AssetId: Clone)), - PartialEq(bound(AccountId: PartialEq, BlockNumber: PartialEq, AssetId: PartialEq)), - Eq -)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(not(feature = "std"), derive(RuntimeDebugNoBound))] -#[cfg_attr( - feature = "std", - derive(Serialize, Deserialize), - serde(bound( - serialize = "AccountId: Serialize, BlockNumber: Serialize, AssetId: Serialize", - deserialize = "AccountId: Deserialize<'de>, BlockNumber: Deserialize<'de>, AssetId: Deserialize<'de>", - )), - educe(Debug(bound(AccountId: core::fmt::Debug, BlockNumber: core::fmt::Debug, AssetId: core::fmt::Debug))) -)] -pub struct Service { - /// The service ID. - pub id: u64, - /// The Blueprint ID of the service. - pub blueprint: u64, - /// The owner of the service. - pub owner: AccountId, - /// The Permitted caller(s) of the service. - pub permitted_callers: BoundedVec, - /// The Selected operators(s) for this service with their restaking Percentage. - // This a Vec instead of a BTreeMap because the number of operators is expected to be small - // (smaller than 512) and the overhead of a BTreeMap is not worth it, plus BoundedBTreeMap is - // not serde compatible. - pub operators: BoundedVec<(AccountId, Percent), C::MaxOperatorsPerService>, - /// Asset(s) used to secure the service instance. - pub assets: BoundedVec, - /// The Lifetime of the service. - pub ttl: BlockNumber, -} - -/// Operator's Approval State. -#[derive( - Default, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Copy, Clone, MaxEncodedLen, -)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub enum ApprovalState { - /// The operator is pending approval. - #[codec(index = 0)] - #[default] - Pending, - /// The operator is approved to provide the service. - #[codec(index = 1)] - Approved { - /// The restaking percentage of the operator. - restaking_percent: Percent, - }, - /// The operator is rejected to provide the service. - #[codec(index = 2)] - Rejected, -} - -/// Different types of assets that can be used. -#[derive( - PartialEq, - Eq, - Encode, - Decode, - RuntimeDebug, - TypeInfo, - Copy, - Clone, - MaxEncodedLen, - Ord, - PartialOrd, -)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub enum Asset { - /// Use the specified AssetId. - #[codec(index = 0)] - Custom(AssetId), - - /// Use an ERC20-like token with the specified contract address. - #[codec(index = 1)] - Erc20(sp_core::H160), -} - -impl Default for Asset { - fn default() -> Self { - Asset::Custom(sp_runtime::traits::Zero::zero()) - } -} - -impl Asset { - pub fn to_ethabi_param_type() -> ethabi::ParamType { - ethabi::ParamType::Tuple(vec![ - // Kind of the Asset - ethabi::ParamType::Uint(8), - // Data of the Asset (Contract Address or AssetId) - ethabi::ParamType::FixedBytes(32), - ]) - } +pub mod gadget; +pub mod jobs; +pub mod service; +pub mod types; - pub fn to_ethabi_param() -> ethabi::Param { - ethabi::Param { - name: String::from("asset"), - kind: Self::to_ethabi_param_type(), - internal_type: Some(String::from("struct ServiceOperators.Asset")), - } - } - - pub fn to_ethabi(&self) -> ethabi::Token { - match self { - Asset::Custom(asset_id) => { - let asset_id = asset_id.using_encoded(ethabi::Uint::from_little_endian); - let mut asset_id_bytes = [0u8; core::mem::size_of::()]; - asset_id.to_big_endian(&mut asset_id_bytes); - ethabi::Token::Tuple(vec![ - ethabi::Token::Uint(0.into()), - ethabi::Token::FixedBytes(asset_id_bytes.into()), - ]) - }, - Asset::Erc20(addr) => ethabi::Token::Tuple(vec![ - ethabi::Token::Uint(1.into()), - ethabi::Token::FixedBytes(addr.to_fixed_bytes().into()), - ]), - } - } -} - -/// Represents the pricing structure for various hardware resources. -/// All prices are specified in USD/hr, calculated based on the average block time. -#[derive( - PartialEq, Eq, Default, Encode, Decode, RuntimeDebug, TypeInfo, Copy, Clone, MaxEncodedLen, -)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct PriceTargets { - /// Price per vCPU per hour - pub cpu: u64, - /// Price per MB of memory per hour - pub mem: u64, - /// Price per GB of HDD storage per hour - pub storage_hdd: u64, - /// Price per GB of SSD storage per hour - pub storage_ssd: u64, - /// Price per GB of NVMe storage per hour - pub storage_nvme: u64, -} - -impl PriceTargets { - /// Converts the struct to ethabi ParamType. - pub fn to_ethabi_param_type() -> ethabi::ParamType { - ethabi::ParamType::Tuple(vec![ - // Price per vCPU per hour - ethabi::ParamType::Uint(64), - // Price per MB of memory per hour - ethabi::ParamType::Uint(64), - // Price per GB of HDD storage per hour - ethabi::ParamType::Uint(64), - // Price per GB of SSD storage per hour - ethabi::ParamType::Uint(64), - // Price per GB of NVMe storage per hour - ethabi::ParamType::Uint(64), - ]) - } - - /// Converts the struct to ethabi Param. - pub fn to_ethabi_param() -> ethabi::Param { - ethabi::Param { - name: String::from("priceTargets"), - kind: Self::to_ethabi_param_type(), - internal_type: Some(String::from("struct ServiceOperators.PriceTargets")), - } - } - - /// Converts the struct to ethabi Token. - pub fn to_ethabi(&self) -> ethabi::Token { - ethabi::Token::Tuple(vec![ - ethabi::Token::Uint(self.cpu.into()), - ethabi::Token::Uint(self.mem.into()), - ethabi::Token::Uint(self.storage_hdd.into()), - ethabi::Token::Uint(self.storage_ssd.into()), - ethabi::Token::Uint(self.storage_nvme.into()), - ]) - } -} - -#[derive(PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Copy, Clone, MaxEncodedLen)] -pub struct OperatorPreferences { - /// The operator ECDSA public key. - pub key: [u8; 65], - /// The pricing targets for the operator's resources. - pub price_targets: PriceTargets, -} - -#[cfg(feature = "std")] -impl Serialize for OperatorPreferences { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - use serde::ser::SerializeTuple; - let mut tup = serializer.serialize_tuple(2)?; - tup.serialize_element(&self.key[..])?; - tup.serialize_element(&self.price_targets)?; - tup.end() - } -} - -#[cfg(feature = "std")] -struct OperatorPreferencesVisitor; - -#[cfg(feature = "std")] -impl<'de> serde::de::Visitor<'de> for OperatorPreferencesVisitor { - type Value = OperatorPreferences; - - fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { - formatter.write_str("a tuple of 2 elements") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: serde::de::SeqAccess<'de>, - { - let key = seq - .next_element::>()? - .ok_or_else(|| serde::de::Error::custom("key is missing"))?; - let price_targets = seq - .next_element::()? - .ok_or_else(|| serde::de::Error::custom("price_targets is missing"))?; - let key_arr: [u8; 65] = key.try_into().map_err(|_| { - serde::de::Error::custom( - "key must be in the uncompressed format with length of 65 bytes", - ) - })?; - Ok(OperatorPreferences { key: key_arr, price_targets }) - } -} - -#[cfg(feature = "std")] -impl<'de> Deserialize<'de> for OperatorPreferences { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_tuple(2, OperatorPreferencesVisitor) - } -} - -impl OperatorPreferences { - /// Returns the ethabi ParamType for OperatorPreferences. - pub fn to_ethabi_param_type() -> ethabi::ParamType { - ethabi::ParamType::Tuple(vec![ - // Operator's ECDSA Public Key (33 bytes) - ethabi::ParamType::Bytes, - // Operator's price targets - PriceTargets::to_ethabi_param_type(), - ]) - } - /// Returns the ethabi Param for OperatorPreferences. - pub fn to_ethabi_param() -> ethabi::Param { - ethabi::Param { - name: String::from("operatorPreferences"), - kind: Self::to_ethabi_param_type(), - internal_type: Some(String::from( - "struct IBlueprintServiceManager.OperatorPreferences", - )), - } - } - - /// Encode the fields to ethabi bytes. - pub fn to_ethabi(&self) -> ethabi::Token { - ethabi::Token::Tuple(vec![ - // operator public key - ethabi::Token::Bytes(self.key.to_vec()), - // price targets - self.price_targets.to_ethabi(), - ]) - } -} - -/// Operator Profile is a profile of an operator that -/// contains metadata about the services that the operator is providing. -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe(Default(bound()), Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] -pub struct OperatorProfile { - /// The Service IDs that I'm currently providing. - pub services: BoundedBTreeSet, - /// The Blueprint IDs that I'm currently registered for. - pub blueprints: BoundedBTreeSet, -} - -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] -pub enum Gadget { - /// A Gadget that is a WASM binary that will be executed. - /// inside the shell using the wasm runtime. - Wasm(WasmGadget), - /// A Gadget that is a native binary that will be executed. - /// inside the shell using the OS. - Native(NativeGadget), - /// A Gadget that is a container that will be executed. - /// inside the shell using the container runtime (e.g. Docker, Podman, etc.) - Container(ContainerGadget), -} - -impl Default for Gadget { - fn default() -> Self { - Gadget::Wasm(WasmGadget { runtime: WasmRuntime::Wasmtime, sources: Default::default() }) - } -} - -/// A binary that is stored in the Github release. -/// this will constuct the URL to the release and download the binary. -/// The URL will be in the following format: -/// https://github.com///releases/download/v/ -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] -pub struct GithubFetcher { - /// The owner of the repository. - pub owner: BoundedString, - /// The repository name. - pub repo: BoundedString, - /// The release tag of the repository. - /// NOTE: The tag should be a valid semver tag. - pub tag: BoundedString, - /// The names of the binary in the release by the arch and the os. - pub binaries: BoundedVec, C::MaxBinariesPerGadget>, -} - -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] -pub struct TestFetcher { - /// The cargo package name that contains the blueprint logic - pub cargo_package: BoundedString, - /// The specific binary name that contains the blueprint logic. - /// Should match up what is in the Cargo.toml file under [[bin]]/name - pub cargo_bin: BoundedString, - /// The base path to the workspace/crate - pub base_path: BoundedString, -} - -/// The CPU or System architecture. -#[derive( - PartialEq, - PartialOrd, - Ord, - Eq, - Encode, - Decode, - RuntimeDebug, - TypeInfo, - Clone, - Copy, - MaxEncodedLen, -)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub enum Architecture { - /// WebAssembly architecture (32-bit). - #[codec(index = 0)] - Wasm, - /// WebAssembly architecture (64-bit). - #[codec(index = 1)] - Wasm64, - /// WASI architecture (32-bit). - #[codec(index = 2)] - Wasi, - /// WASI architecture (64-bit). - #[codec(index = 3)] - Wasi64, - /// Amd architecture (32-bit). - #[codec(index = 4)] - Amd, - /// Amd64 architecture (x86_64). - #[codec(index = 5)] - Amd64, - /// Arm architecture (32-bit). - #[codec(index = 6)] - Arm, - /// Arm64 architecture (64-bit). - #[codec(index = 7)] - Arm64, - /// Risc-V architecture (32-bit). - #[codec(index = 8)] - RiscV, - /// Risc-V architecture (64-bit). - #[codec(index = 9)] - RiscV64, -} - -/// Operating System that the binary is compiled for. -#[derive( - Default, - PartialEq, - PartialOrd, - Ord, - Eq, - Encode, - Decode, - RuntimeDebug, - TypeInfo, - Clone, - Copy, - MaxEncodedLen, -)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub enum OperatingSystem { - /// Unknown operating system. - /// This is used when the operating system is not known - /// for example, for WASM, where the OS is not relevant. - #[default] - #[codec(index = 0)] - Unknown, - /// Linux operating system. - #[codec(index = 1)] - Linux, - /// Windows operating system. - #[codec(index = 2)] - Windows, - /// MacOS operating system. - #[codec(index = 3)] - MacOS, - /// BSD operating system. - #[codec(index = 4)] - BSD, -} - -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] -pub struct GadgetBinary { - /// CPU or System architecture. - pub arch: Architecture, - /// Operating System that the binary is compiled for. - pub os: OperatingSystem, - /// The name of the binary. - pub name: BoundedString, - /// The sha256 hash of the binary. - /// used to verify the downloaded binary. - pub sha256: [u8; 32], -} - -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] -pub struct GadgetSource { - /// The fetcher that will fetch the gadget from a remote source. - fetcher: GadgetSourceFetcher, -} - -/// A Gadget Source Fetcher is a fetcher that will fetch the gadget -/// from a remote source. -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] -pub enum GadgetSourceFetcher { - /// A Gadget that will be fetched from the IPFS. - #[codec(index = 0)] - IPFS(BoundedVec), - /// A Gadget that will be fetched from the Github release. - #[codec(index = 1)] - Github(GithubFetcher), - /// A Gadgets that will be fetched from the container registry. - #[codec(index = 2)] - ContainerImage(ImageRegistryFetcher), - /// For tests only - #[codec(index = 3)] - Testing(TestFetcher), -} - -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] -pub struct ImageRegistryFetcher { - /// The URL of the container registry. - registry: BoundedString, - /// The name of the image. - image: BoundedString, - /// The tag of the image. - tag: BoundedString, -} - -/// A WASM binary that contains all the compiled gadget code. -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] -pub struct WasmGadget { - /// Which runtime to use to execute the WASM binary. - pub runtime: WasmRuntime, - /// Where the WASM binary is stored. - pub sources: BoundedVec, C::MaxSourcesPerGadget>, -} - -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] -pub enum WasmRuntime { - /// The WASM binary will be executed using the WASMtime runtime. - #[codec(index = 0)] - Wasmtime, - /// The WASM binary will be executed using the Wasmer runtime. - #[codec(index = 1)] - Wasmer, -} - -/// A Native binary that contains all the gadget code. -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] -pub struct NativeGadget { - /// Where the WASM binary is stored. - pub sources: BoundedVec, C::MaxSourcesPerGadget>, -} - -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe(Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] -pub struct ContainerGadget { - /// Where the Image of the gadget binary is stored. - pub sources: BoundedVec, C::MaxSourcesPerGadget>, -} - -// -***- RPC -***- - -/// RPC Response for query the blueprint along with the services instances of that blueprint. -#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[educe( - Default(bound(AccountId: Default, BlockNumber: Default, AssetId: Default)), - Clone(bound(AccountId: Clone, BlockNumber: Clone, AssetId: Clone)), - PartialEq(bound(AccountId: PartialEq, BlockNumber: PartialEq, AssetId: PartialEq)), - Eq -)] -#[scale_info(skip_type_params(C))] -#[codec(encode_bound(skip_type_params(C)))] -#[codec(decode_bound(skip_type_params(C)))] -#[codec(mel_bound(skip_type_params(C)))] -#[cfg_attr(not(feature = "std"), derive(RuntimeDebugNoBound))] -#[cfg_attr( - feature = "std", - derive(Serialize, Deserialize), - serde(bound( - serialize = "AccountId: Serialize, BlockNumber: Serialize, AssetId: Serialize", - deserialize = "AccountId: Deserialize<'de>, BlockNumber: Deserialize<'de>, AssetId: Deserialize<'de>", - )), - educe(Debug(bound(AccountId: core::fmt::Debug, BlockNumber: core::fmt::Debug, AssetId: core::fmt::Debug))) -)] -pub struct RpcServicesWithBlueprint { - /// The blueprint ID. - pub blueprint_id: u64, - /// The service blueprint. - pub blueprint: ServiceBlueprint, - /// The services instances of that blueprint. - pub services: Vec>, -} - -#[derive(Debug)] -pub struct RunnerError> { - pub error: E, - pub weight: Weight, -} - -#[allow(clippy::too_many_arguments)] -pub trait EvmRunner { - type Error: Into; - - fn call( - source: H160, - target: H160, - input: Vec, - value: U256, - gas_limit: u64, - is_transactional: bool, - validate: bool, - ) -> Result>; -} - -/// A mapping function that converts EVM gas to Substrate weight and vice versa -pub trait EvmGasWeightMapping { - /// Convert EVM gas to Substrate weight - fn gas_to_weight(gas: u64, without_base_weight: bool) -> Weight; - /// Convert Substrate weight to EVM gas - fn weight_to_gas(weight: Weight) -> u64; -} - -impl EvmGasWeightMapping for () { - fn gas_to_weight(_gas: u64, _without_base_weight: bool) -> Weight { - Default::default() - } - fn weight_to_gas(_weight: Weight) -> u64 { - Default::default() - } -} - -/// Trait to be implemented for evm address mapping. -pub trait EvmAddressMapping { - /// Convert an address to an account id. - fn into_account_id(address: H160) -> A; - - /// Convert an account id to an address. - fn into_address(account_id: A) -> H160; -} +pub use constraints::*; +pub use evm::*; +pub use field::*; +pub use gadget::*; +pub use jobs::*; +pub use service::*; +pub use types::*; diff --git a/primitives/src/services/service.rs b/primitives/src/services/service.rs new file mode 100644 index 000000000..1dedbd152 --- /dev/null +++ b/primitives/src/services/service.rs @@ -0,0 +1,465 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use super::{ + constraints::Constraints, + jobs::{type_checker, JobDefinition}, + types::{ApprovalState, Asset, MembershipModel}, + AssetIdT, AssetSecurityCommitment, AssetSecurityRequirement, BoundedString, Gadget, + TypeCheckError, +}; +use crate::{Account, BlueprintId}; +use educe::Educe; +use frame_support::pallet_prelude::*; +use sp_core::H160; +use sp_runtime::Percent; +use sp_std::{vec, vec::Vec}; + +#[cfg(not(feature = "std"))] +use alloc::string::String; +#[cfg(feature = "std")] +use std::string::String; + +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; + +use super::field::{Field, FieldType}; + +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Default(bound()), Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize), serde(bound = ""))] +pub struct ServiceMetadata { + /// The Service name. + pub name: BoundedString, + /// The Service description. + pub description: Option>, + /// The Service author. + /// Could be a company or a person. + pub author: Option>, + /// The Job category. + pub category: Option>, + /// Code Repository URL. + /// Could be a github, gitlab, or any other code repository. + pub code_repository: Option>, + /// Service Logo URL. + pub logo: Option>, + /// Service Website URL. + pub website: Option>, + /// Service License. + pub license: Option>, +} + +/// Blueprint Service Manager is a smart contract that will manage the service lifecycle. +#[derive(PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Clone, Copy, MaxEncodedLen)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +#[non_exhaustive] +pub enum BlueprintServiceManager { + /// A Smart contract that will manage the service lifecycle. + Evm(H160), +} + +impl BlueprintServiceManager { + pub fn try_into_evm(self) -> Result { + match self { + Self::Evm(addr) => Ok(addr), + } + } +} + +impl Default for BlueprintServiceManager { + fn default() -> Self { + Self::Evm(Default::default()) + } +} + +/// Master Blueprint Service Manager Revision. +#[derive( + Default, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Clone, Copy, MaxEncodedLen, +)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +#[non_exhaustive] +pub enum MasterBlueprintServiceManagerRevision { + /// Use Whatever the latest revision available on-chain. + /// + /// This is the default value. + #[default] + #[codec(index = 0)] + Latest, + + /// Use a specific revision number. + /// + /// Note: Must be already deployed on-chain. + #[codec(index = 1)] + Specific(u32), +} + +/// A Service Blueprint is a the main definition of a service. +/// it contains the metadata of the service, the job definitions, and other hooks, along with the +/// gadget that will be executed when one of the jobs is calling this service. +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Default(bound()), Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize), serde(bound = ""))] +pub struct ServiceBlueprint { + /// The metadata of the service. + pub metadata: ServiceMetadata, + /// The job definitions that are available in this service. + pub jobs: BoundedVec, C::MaxJobsPerService>, + /// The parameters that are required for the service registration. + pub registration_params: BoundedVec, + /// The request hook that will be called before creating a service from the service blueprint. + /// The parameters that are required for the service request. + pub request_params: BoundedVec, + /// A Blueprint Manager is a smart contract that implements the `IBlueprintServiceManager` + /// interface. + pub manager: BlueprintServiceManager, + /// The Revision number of the Master Blueprint Service Manager. + /// + /// If not sure what to use, use `MasterBlueprintServiceManagerRevision::default()` which will + /// use the latest revision available. + pub master_manager_revision: MasterBlueprintServiceManagerRevision, + /// The gadget that will be executed for the service. + pub gadget: Gadget, + /// The membership models supported by this blueprint + pub supported_membership_models: BoundedVec>, +} + +impl ServiceBlueprint { + /// Check if the supplied arguments match the registration parameters. + pub fn type_check_registration( + &self, + args: &[Field], + ) -> Result<(), TypeCheckError> { + type_checker(&self.registration_params, args) + } + + /// Check if the supplied arguments match the request parameters. + pub fn type_check_request( + &self, + args: &[Field], + ) -> Result<(), TypeCheckError> { + type_checker(&self.request_params, args) + } + + /// Converts the struct to ethabi ParamType. + pub fn to_ethabi_param_type() -> ethabi::ParamType { + ethabi::ParamType::Tuple(vec![ + // Service Metadata + ethabi::ParamType::Tuple(vec![ + // Service Name + ethabi::ParamType::String, + // Service Description + ethabi::ParamType::String, + // Service Author + ethabi::ParamType::String, + // Service Category + ethabi::ParamType::String, + // Code Repository + ethabi::ParamType::String, + // Service Logo + ethabi::ParamType::String, + // Service Website + ethabi::ParamType::String, + // Service License + ethabi::ParamType::String, + ]), + // Job Definitions ? + // Registration Parameters ? + // Request Parameters ? + // Blueprint Manager + ethabi::ParamType::Address, + // Master Manager Revision + ethabi::ParamType::Uint(32), + // Gadget ? + ]) + } + + /// Converts the struct to ethabi Param. + pub fn to_ethabi_param() -> ethabi::Param { + ethabi::Param { + name: String::from("blueprint"), + kind: Self::to_ethabi_param_type(), + internal_type: Some(String::from("struct MasterBlueprintServiceManager.Blueprint")), + } + } + + /// Converts the struct to ethabi Token. + pub fn to_ethabi(&self) -> ethabi::Token { + ethabi::Token::Tuple(vec![ + // Service Metadata + ethabi::Token::Tuple(vec![ + // Service Name + ethabi::Token::String(self.metadata.name.as_str().into()), + // Service Description + ethabi::Token::String( + self.metadata + .description + .as_ref() + .map(|v| v.as_str().into()) + .unwrap_or_default(), + ), + // Service Author + ethabi::Token::String( + self.metadata.author.as_ref().map(|v| v.as_str().into()).unwrap_or_default(), + ), + // Service Category + ethabi::Token::String( + self.metadata.category.as_ref().map(|v| v.as_str().into()).unwrap_or_default(), + ), + // Code Repository + ethabi::Token::String( + self.metadata + .code_repository + .as_ref() + .map(|v| v.as_str().into()) + .unwrap_or_default(), + ), + // Service Logo + ethabi::Token::String( + self.metadata.logo.as_ref().map(|v| v.as_str().into()).unwrap_or_default(), + ), + // Service Website + ethabi::Token::String( + self.metadata.website.as_ref().map(|v| v.as_str().into()).unwrap_or_default(), + ), + // Service License + ethabi::Token::String( + self.metadata.license.as_ref().map(|v| v.as_str().into()).unwrap_or_default(), + ), + ]), + // Job Definitions ? + // Registration Parameters ? + // Request Parameters ? + // Blueprint Manager + match self.manager { + BlueprintServiceManager::Evm(addr) => ethabi::Token::Address(addr), + }, + // Master Manager Revision + match self.master_manager_revision { + MasterBlueprintServiceManagerRevision::Latest => { + ethabi::Token::Uint(ethabi::Uint::MAX) + }, + MasterBlueprintServiceManagerRevision::Specific(rev) => { + ethabi::Token::Uint(rev.into()) + }, + }, + // Gadget ? + ]) + } +} + +/// Represents a request for service with specific security requirements for each asset. +/// The security requirements define the minimum and maximum exposure percentages that +/// operators must commit to be eligible for the service. +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe( + Default(bound(AccountId: Default, BlockNumber: Default, AssetId: Default)), + Clone(bound(AccountId: Clone, BlockNumber: Clone, AssetId: Clone)), + PartialEq(bound(AccountId: PartialEq, BlockNumber: PartialEq, AssetId: PartialEq)), + Eq +)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(not(feature = "std"), derive(RuntimeDebugNoBound))] +#[cfg_attr( + feature = "std", + derive(serde::Serialize, serde::Deserialize), + serde(bound( + serialize = "AccountId: Serialize, BlockNumber: Serialize, AssetId: Serialize", + deserialize = "AccountId: Deserialize<'de>, BlockNumber: Deserialize<'de>, AssetId: AssetIdT" + )), + educe(Debug(bound(AccountId: core::fmt::Debug, BlockNumber: core::fmt::Debug, AssetId: AssetIdT))) +)] + +pub struct ServiceRequest { + /// The blueprint ID this request is for + pub blueprint: BlueprintId, + /// The account that requested the service + pub owner: AccountId, + /// The assets required for this service along with their security requirements. + /// This defines both which assets are needed and how much security backing is required. + pub non_native_asset_security: + BoundedVec, C::MaxAssetsPerService>, + /// Time-to-live for this request in blocks + pub ttl: BlockNumber, + /// Arguments for service initialization + pub args: BoundedVec, C::MaxFields>, + /// Accounts permitted to call service functions + pub permitted_callers: BoundedVec, + /// Operators and their approval states + pub operators_with_approval_state: + BoundedVec<(AccountId, ApprovalState), C::MaxOperatorsPerService>, + /// The membership model to use for this service instance + pub membership_model: MembershipModel, +} + +impl + ServiceRequest +{ + /// Returns true if all the operators are [ApprovalState::Approved]. + pub fn is_approved(&self) -> bool { + self.operators_with_approval_state + .iter() + .all(|(_, state)| matches!(state, ApprovalState::Approved { .. })) + } + + /// Returns true if any the operators are [ApprovalState::Pending]. + pub fn is_pending(&self) -> bool { + self.operators_with_approval_state + .iter() + .any(|(_, state)| state == &ApprovalState::Pending) + } + + /// Returns true if any the operators are [ApprovalState::Rejected]. + pub fn is_rejected(&self) -> bool { + self.operators_with_approval_state + .iter() + .any(|(_, state)| state == &ApprovalState::Rejected) + } + + /// Validates that an operator's security commitments meet the requirements + pub fn validate_commitments( + &self, + asset_commitments: &[AssetSecurityCommitment], + ) -> bool + where + AssetId: PartialEq, + { + // Ensure commitments exist for all required assets + self.non_native_asset_security.iter().all(|req| { + asset_commitments.iter().any(|commit| { + commit.asset == req.asset + && commit.exposure_percent >= req.min_exposure_percent + && commit.exposure_percent <= req.max_exposure_percent + }) + }) + } +} + +/// A staging service payment is a payment that is made for a service request +/// but will be paid when the service is created or refunded if the service is rejected. +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen, Copy)] +#[educe( + Default(bound(AccountId: Default, Balance: Default, AssetId: Default)), + Clone(bound(AccountId: Clone, Balance: Clone, AssetId: Clone)), + PartialEq(bound(AccountId: PartialEq, Balance: PartialEq, AssetId: PartialEq)), + Eq +)] +#[cfg_attr(not(feature = "std"), derive(RuntimeDebugNoBound))] +#[cfg_attr( + feature = "std", + derive(serde::Serialize, serde::Deserialize), + serde(bound( + serialize = "AccountId: Serialize, Balance: Serialize, AssetId: Serialize", + deserialize = "AccountId: Deserialize<'de>, Balance: Deserialize<'de>, AssetId: AssetIdT", + )), + educe(Debug(bound(AccountId: core::fmt::Debug, Balance: core::fmt::Debug, AssetId: AssetIdT))) +)] +pub struct StagingServicePayment { + /// The service request ID. + pub request_id: u64, + /// Where the refund should go. + pub refund_to: Account, + /// The Asset used in the payment. + pub asset: Asset, + /// The amount of the asset that is paid. + pub amount: Balance, +} + +/// A Service is an instance of a service blueprint. +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe( + Default(bound(AccountId: Default, BlockNumber: Default, AssetId: Default)), + Clone(bound(AccountId: Clone, BlockNumber: Clone, AssetId: Clone)), + PartialEq(bound(AccountId: PartialEq, BlockNumber: PartialEq, AssetId: PartialEq)), + Eq +)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(not(feature = "std"), derive(RuntimeDebugNoBound))] +#[cfg_attr( + feature = "std", + derive(serde::Serialize, serde::Deserialize), + serde(bound( + serialize = "AccountId: Serialize, BlockNumber: Serialize, AssetId: Serialize", + deserialize = "AccountId: Deserialize<'de>, BlockNumber: Deserialize<'de>, AssetId: AssetIdT", + )), + educe(Debug(bound(AccountId: core::fmt::Debug, BlockNumber: core::fmt::Debug, AssetId: AssetIdT))) +)] +pub struct Service { + /// Unique identifier for this service instance + pub id: u64, + /// The blueprint this service was created from + pub blueprint: BlueprintId, + /// The account that owns this service + pub owner: AccountId, + /// The assets and their security commitments from operators. + /// This represents the actual security backing the service. + pub non_native_asset_security: BoundedVec< + (AccountId, BoundedVec, C::MaxAssetsPerService>), + C::MaxOperatorsPerService, + >, + /// Active operators and their native currency exposure percentages + pub native_asset_security: BoundedVec<(AccountId, Percent), C::MaxOperatorsPerService>, + /// Accounts permitted to call service functions + pub permitted_callers: BoundedVec, + /// Time-to-live in blocks + pub ttl: BlockNumber, + /// The membership model of the service + pub membership_model: MembershipModel, +} + +/// RPC Response for query the blueprint along with the services instances of that blueprint. +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe( + Default(bound(AccountId: Default, BlockNumber: Default, AssetId: Default)), + Clone(bound(AccountId: Clone, BlockNumber: Clone, AssetId: Clone)), + PartialEq(bound(AccountId: PartialEq, BlockNumber: PartialEq, AssetId: PartialEq)), + Eq +)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(not(feature = "std"), derive(RuntimeDebugNoBound))] +#[cfg_attr( + feature = "std", + derive(serde::Serialize, serde::Deserialize), + serde(bound( + serialize = "AccountId: Serialize, BlockNumber: Serialize, AssetId: Serialize", + deserialize = "AccountId: Deserialize<'de>, BlockNumber: Deserialize<'de>, AssetId: AssetIdT", + )), + educe(Debug(bound(AccountId: core::fmt::Debug, BlockNumber: core::fmt::Debug, AssetId: core::fmt::Debug))) +)] +pub struct RpcServicesWithBlueprint { + /// The blueprint ID. + pub blueprint_id: u64, + /// The service blueprint. + pub blueprint: ServiceBlueprint, + /// The services instances of that blueprint. + pub services: Vec>, +} diff --git a/primitives/src/services/types.rs b/primitives/src/services/types.rs new file mode 100644 index 000000000..613f5f519 --- /dev/null +++ b/primitives/src/services/types.rs @@ -0,0 +1,440 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use educe::Educe; +use frame_support::pallet_prelude::*; +#[cfg(feature = "std")] +use serde::{Deserialize, Deserializer, Serialize}; +use sp_core::{RuntimeDebug, H160}; +use sp_runtime::{traits::AtLeast32BitUnsigned, Percent}; +use sp_staking::EraIndex; +use sp_std::fmt::Display; + +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec, vec::Vec}; + +use super::{field::FieldType, Constraints}; + +/// An error that can occur during type checking. +#[derive(PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Clone, MaxEncodedLen)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum TypeCheckError { + /// The argument type does not match the expected type. + ArgumentTypeMismatch { + /// The index of the argument. + index: u8, + /// The expected type. + expected: FieldType, + /// The actual type. + actual: FieldType, + }, + /// Not enough arguments were supplied. + NotEnoughArguments { + /// The number of arguments that were expected. + expected: u8, + /// The number of arguments that were supplied. + actual: u8, + }, + /// The result type does not match the expected type. + ResultTypeMismatch { + /// The index of the argument. + index: u8, + /// The expected type. + expected: FieldType, + /// The actual type. + actual: FieldType, + }, +} + +impl frame_support::traits::PalletError for TypeCheckError { + const MAX_ENCODED_SIZE: usize = 2; +} + +/// Different types of assets that can be used. +#[derive( + PartialEq, + Eq, + Encode, + Decode, + RuntimeDebug, + TypeInfo, + Copy, + Clone, + MaxEncodedLen, + Ord, + PartialOrd, +)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum Asset { + /// Use the specified AssetId. + #[codec(index = 0)] + Custom(AssetId), + + /// Use an ERC20-like token with the specified contract address. + #[codec(index = 1)] + Erc20(H160), +} + +impl Default for Asset { + fn default() -> Self { + Asset::Custom(AssetId::default()) + } +} + +impl Asset { + pub fn to_ethabi_param_type() -> ethabi::ParamType { + ethabi::ParamType::Tuple(vec![ + // Kind of the Asset + ethabi::ParamType::Uint(8), + // Data of the Asset (Contract Address or AssetId) + ethabi::ParamType::FixedBytes(32), + ]) + } + + pub fn to_ethabi_param() -> ethabi::Param { + ethabi::Param { + name: String::from("asset"), + kind: Self::to_ethabi_param_type(), + internal_type: Some(String::from("struct ServiceOperators.Asset")), + } + } + + pub fn to_ethabi(&self) -> ethabi::Token { + match self { + Asset::Custom(asset_id) => { + let asset_id = asset_id.using_encoded(ethabi::Uint::from_little_endian); + let mut asset_id_bytes = [0u8; core::mem::size_of::()]; + asset_id.to_big_endian(&mut asset_id_bytes); + ethabi::Token::Tuple(vec![ + ethabi::Token::Uint(0.into()), + ethabi::Token::FixedBytes(asset_id_bytes.into()), + ]) + }, + Asset::Erc20(addr) => { + let mut addr_bytes = [0u8; 32]; + addr_bytes[12..].copy_from_slice(addr.as_fixed_bytes()); + ethabi::Token::Tuple(vec![ + ethabi::Token::Uint(1.into()), + ethabi::Token::FixedBytes(addr_bytes.into()), + ]) + }, + } + } +} + +/// Represents the pricing structure for various hardware resources. +/// All prices are specified in USD/hr, calculated based on the average block time. +#[derive( + PartialEq, Eq, Default, Encode, Decode, RuntimeDebug, TypeInfo, Copy, Clone, MaxEncodedLen, +)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct PriceTargets { + /// Price per vCPU per hour + pub cpu: u64, + /// Price per MB of memory per hour + pub mem: u64, + /// Price per GB of HDD storage per hour + pub storage_hdd: u64, + /// Price per GB of SSD storage per hour + pub storage_ssd: u64, + /// Price per GB of NVMe storage per hour + pub storage_nvme: u64, +} + +impl PriceTargets { + /// Converts the struct to ethabi ParamType. + pub fn to_ethabi_param_type() -> ethabi::ParamType { + ethabi::ParamType::Tuple(vec![ + // Price per vCPU per hour + ethabi::ParamType::Uint(64), + // Price per MB of memory per hour + ethabi::ParamType::Uint(64), + // Price per GB of HDD storage per hour + ethabi::ParamType::Uint(64), + // Price per GB of SSD storage per hour + ethabi::ParamType::Uint(64), + // Price per GB of NVMe storage per hour + ethabi::ParamType::Uint(64), + ]) + } + + /// Converts the struct to ethabi Param. + pub fn to_ethabi_param() -> ethabi::Param { + ethabi::Param { + name: String::from("priceTargets"), + kind: Self::to_ethabi_param_type(), + internal_type: Some(String::from("struct ServiceOperators.PriceTargets")), + } + } + + /// Converts the struct to ethabi Token. + pub fn to_ethabi(&self) -> ethabi::Token { + ethabi::Token::Tuple(vec![ + ethabi::Token::Uint(self.cpu.into()), + ethabi::Token::Uint(self.mem.into()), + ethabi::Token::Uint(self.storage_hdd.into()), + ethabi::Token::Uint(self.storage_ssd.into()), + ethabi::Token::Uint(self.storage_nvme.into()), + ]) + } +} + +/// Trait for asset identifiers +pub trait AssetIdT: + Default + + Clone + + Parameter + + Member + + PartialEq + + Eq + + PartialOrd + + Ord + + AtLeast32BitUnsigned + + parity_scale_codec::FullCodec + + MaxEncodedLen + + TypeInfo + + core::fmt::Debug + + MaybeSerializeDeserialize + + Display +{ +} + +impl AssetIdT for T where + T: Default + + Clone + + Parameter + + Member + + PartialEq + + Eq + + PartialOrd + + Ord + + AtLeast32BitUnsigned + + parity_scale_codec::FullCodec + + MaxEncodedLen + + TypeInfo + + core::fmt::Debug + + MaybeSerializeDeserialize + + Display +{ +} + +/// The approval state of an operator for a service request +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Clone(bound()), PartialEq(bound()), Eq, PartialOrd, Ord(bound()))] +#[cfg_attr(not(feature = "std"), derive(RuntimeDebugNoBound))] +#[cfg_attr( + feature = "std", + derive(serde::Serialize, serde::Deserialize), + serde(bound = ""), + educe(Debug(bound())) +)] +pub enum ApprovalState { + /// The operator has not yet responded to the request + Pending, + /// The operator has approved the request with specific asset commitments + Approved { + /// The percentage of native currency stake to expose + native_exposure_percent: Percent, + /// Asset-specific exposure commitments + asset_exposure: Vec>, + }, + /// The operator has rejected the request + Rejected, +} + +/// Asset-specific security requirements for a service request +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Default(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[cfg_attr(not(feature = "std"), derive(RuntimeDebugNoBound))] +#[cfg_attr( + feature = "std", + derive(serde::Serialize, serde::Deserialize), + serde(bound = ""), + educe(Debug(bound())) +)] +pub struct AssetSecurityRequirement { + /// The asset that needs to be secured + pub asset: Asset, + /// The minimum percentage of the asset that needs to be exposed for slashing + pub min_exposure_percent: Percent, + /// The maximum percentage of the asset that can be exposed for slashing + pub max_exposure_percent: Percent, +} + +/// Asset-specific security commitment from an operator +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Default(bound()), Clone(bound()), PartialEq(bound()), Eq, PartialOrd, Ord(bound()))] +#[cfg_attr(not(feature = "std"), derive(RuntimeDebugNoBound))] +#[cfg_attr( + feature = "std", + derive(serde::Serialize, serde::Deserialize), + serde(bound = ""), + educe(Debug(bound())) +)] +pub struct AssetSecurityCommitment { + /// The asset being secured + pub asset: Asset, + /// The percentage of the asset exposed for slashing + pub exposure_percent: Percent, +} + +#[derive(PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Copy, Clone, MaxEncodedLen)] +pub struct OperatorPreferences { + /// The operator ECDSA public key. + pub key: [u8; 65], + /// The pricing targets for the operator's resources. + pub price_targets: PriceTargets, +} + +#[cfg(feature = "std")] +impl Serialize for OperatorPreferences { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeTuple; + let mut tup = serializer.serialize_tuple(2)?; + tup.serialize_element(&self.key[..])?; + tup.serialize_element(&self.price_targets)?; + tup.end() + } +} + +#[cfg(feature = "std")] +struct OperatorPreferencesVisitor; + +#[cfg(feature = "std")] +impl<'de> serde::de::Visitor<'de> for OperatorPreferencesVisitor { + type Value = OperatorPreferences; + + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + formatter.write_str("a tuple of 2 elements") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let key = seq + .next_element::>()? + .ok_or_else(|| serde::de::Error::custom("key is missing"))?; + let price_targets = seq + .next_element::()? + .ok_or_else(|| serde::de::Error::custom("price_targets is missing"))?; + let key_arr: [u8; 65] = key.try_into().map_err(|_| { + serde::de::Error::custom( + "key must be in the uncompressed format with length of 65 bytes", + ) + })?; + Ok(OperatorPreferences { key: key_arr, price_targets }) + } +} + +#[cfg(feature = "std")] +impl<'de> Deserialize<'de> for OperatorPreferences { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_tuple(2, OperatorPreferencesVisitor) + } +} + +impl OperatorPreferences { + /// Returns the ethabi ParamType for OperatorPreferences. + pub fn to_ethabi_param_type() -> ethabi::ParamType { + ethabi::ParamType::Tuple(vec![ + // Operator's ECDSA Public Key (33 bytes) + ethabi::ParamType::Bytes, + // Operator's price targets + PriceTargets::to_ethabi_param_type(), + ]) + } + /// Returns the ethabi Param for OperatorPreferences. + pub fn to_ethabi_param() -> ethabi::Param { + ethabi::Param { + name: String::from("operatorPreferences"), + kind: Self::to_ethabi_param_type(), + internal_type: Some(String::from( + "struct IBlueprintServiceManager.OperatorPreferences", + )), + } + } + + /// Encode the fields to ethabi bytes. + pub fn to_ethabi(&self) -> ethabi::Token { + ethabi::Token::Tuple(vec![ + // operator public key + ethabi::Token::Bytes(self.key.to_vec()), + // price targets + self.price_targets.to_ethabi(), + ]) + } +} + +/// Operator Profile is a profile of an operator that +/// contains metadata about the services that the operator is providing. +#[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[educe(Default(bound()), Debug(bound()), Clone(bound()), PartialEq(bound()), Eq)] +#[scale_info(skip_type_params(C))] +#[codec(encode_bound(skip_type_params(C)))] +#[codec(decode_bound(skip_type_params(C)))] +#[codec(mel_bound(skip_type_params(C)))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] +pub struct OperatorProfile { + /// The Service IDs that I'm currently providing. + pub services: BoundedBTreeSet, + /// The Blueprint IDs that I'm currently registered for. + pub blueprints: BoundedBTreeSet, +} + +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize), serde(bound = ""))] +pub enum MembershipModel { + /// Fixed set of operators defined at service creation + Fixed { min_operators: u32 }, + /// Operators can join/leave subject to blueprint rules + Dynamic { min_operators: u32, max_operators: Option }, +} + +impl Default for MembershipModel { + fn default() -> Self { + MembershipModel::Fixed { min_operators: 1 } + } +} + +/// A pending slash record. The value of the slash has been computed but not applied yet, +/// rather deferred for several eras. +#[derive(PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Clone, MaxEncodedLen)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[scale_info(skip_type_params(Balance))] +pub struct UnappliedSlash { + /// The era the slash was reported. + pub era: EraIndex, + /// The Blueprint Id of the service being slashed. + pub blueprint_id: u64, + /// The Service Instance Id on which the slash is applied. + pub service_id: u64, + /// The account ID of the offending operator. + pub operator: AccountId, + /// The operator's own slash in native currency + pub own: Balance, + /// All other slashed restakers and amounts per asset. + /// (delegator, asset, amount) + pub others: Vec<(AccountId, Asset, Balance)>, + /// Reporters of the offence; bounty payout recipients. + pub reporters: Vec, +} diff --git a/primitives/src/traits/mod.rs b/primitives/src/traits/mod.rs index 0fb9c6b99..c32c09ca3 100644 --- a/primitives/src/traits/mod.rs +++ b/primitives/src/traits/mod.rs @@ -3,9 +3,11 @@ pub mod data_provider; pub mod multi_asset_delegation; pub mod rewards; pub mod services; +pub mod slash; pub use assets::*; pub use data_provider::*; pub use multi_asset_delegation::*; pub use rewards::*; pub use services::*; +pub use slash::*; diff --git a/primitives/src/traits/multi_asset_delegation.rs b/primitives/src/traits/multi_asset_delegation.rs index f9639e532..c0eda15e5 100644 --- a/primitives/src/traits/multi_asset_delegation.rs +++ b/primitives/src/traits/multi_asset_delegation.rs @@ -14,9 +14,7 @@ use sp_std::prelude::*; /// * `AssetId`: The type representing an asset identifier. /// * `Balance`: The type representing a balance or amount. /// * `BlockNumber`: The type representing a block number. -pub trait MultiAssetDelegationInfo { - type AssetId; - +pub trait MultiAssetDelegationInfo { /// Get the current round index. /// /// This method returns the current round index, which may be used to track @@ -81,10 +79,8 @@ pub trait MultiAssetDelegationInfo { /// # Returns /// /// The total delegation amount as a `Balance`. - fn get_total_delegation_by_asset_id( - operator: &AccountId, - asset_id: &Asset, - ) -> Balance; + fn get_total_delegation_by_asset_id(operator: &AccountId, asset_id: &Asset) + -> Balance; /// Get all delegators for a specific operator. /// @@ -101,16 +97,45 @@ pub trait MultiAssetDelegationInfo { /// delegator account identifier, delegation amount, and asset identifier. fn get_delegators_for_operator( operator: &AccountId, - ) -> Vec<(AccountId, Balance, Asset)>; + ) -> Vec<(AccountId, Balance, Asset)>; - fn slash_operator( + /// Check if a delegator has selected a specific blueprint for a given operator. + /// + /// This method checks whether the delegator has included the specified blueprint + /// in their blueprint selection for their delegation to the given operator. + /// + /// # Parameters + /// + /// * `delegator`: A reference to the account identifier of the delegator. + /// * `operator`: A reference to the account identifier of the operator. + /// * `blueprint_id`: The blueprint ID to check. + /// + /// # Returns + /// + /// `true` if the delegator has selected the blueprint for the operator, otherwise `false`. + fn has_delegator_selected_blueprint( + delegator: &AccountId, operator: &AccountId, blueprint_id: crate::BlueprintId, - percentage: sp_runtime::Percent, - ); + ) -> bool; + /// Get a user's deposit and associated locks for a specific asset. + /// + /// This method retrieves information about a user's deposit for a given asset, + /// including both the unlocked amount and any time-locked portions. + /// + /// # Parameters + /// + /// * `who`: A reference to the account identifier of the user. + /// * `asset_id`: The asset identifier for which to get deposit information. + /// + /// # Returns + /// + /// An `Option` containing the user's deposit information if it exists: + /// - `Some(UserDepositWithLocks)` containing the unlocked amount and any time-locks + /// - `None` if no deposit exists for this user and asset fn get_user_deposit_with_locks( who: &AccountId, - asset_id: Asset, + asset_id: Asset, ) -> Option>; } diff --git a/primitives/src/traits/slash.rs b/primitives/src/traits/slash.rs new file mode 100644 index 000000000..b5ddc21d5 --- /dev/null +++ b/primitives/src/traits/slash.rs @@ -0,0 +1,39 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Tangle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Tangle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Tangle. If not, see . + +use crate::services::UnappliedSlash; +use frame_support::weights::Weight; +use sp_runtime::DispatchError; + +/// Trait for managing slashing in the Tangle network. +/// This trait provides functionality to slash operators and delegators. +pub trait SlashManager { + /// Slash an operator's stake for an offense. + /// + /// # Parameters + /// * `unapplied_slash` - The unapplied slash record containing slash details + fn slash_operator( + unapplied_slash: &UnappliedSlash, + ) -> Result; +} + +impl SlashManager for () { + fn slash_operator( + _unapplied_slash: &UnappliedSlash, + ) -> Result { + Ok(Weight::zero()) + } +} diff --git a/primitives/src/types.rs b/primitives/src/types.rs index a78d88eab..a8779389f 100644 --- a/primitives/src/types.rs +++ b/primitives/src/types.rs @@ -19,7 +19,7 @@ pub mod rewards; use frame_support::pallet_prelude::*; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; -use sp_core::{ByteArray, RuntimeDebug}; +use sp_core::RuntimeDebug; use sp_runtime::{generic, AccountId32, OpaqueExtrinsic}; /// Block header type as expected by this runtime. @@ -59,6 +59,15 @@ pub type RoundIndex = u32; /// Blueprint ID pub type BlueprintId = u64; +/// Service request ID +pub type ServiceRequestId = u64; + +/// Service instance ID +pub type InstanceId = u64; + +/// Job call ID +pub type JobCallId = u64; + /// The address format for describing accounts. pub type Address = MultiAddress; @@ -99,16 +108,9 @@ pub enum Account { Address(sp_core::H160), } -impl Default for Account -where - AccountId: ByteArray, -{ +impl Default for Account { fn default() -> Self { - // This should be good enough to make the account for any account id type. - let empty = [0u8; 64]; - let account_id = - AccountId::from_slice(&empty[0..AccountId::LEN]).expect("never fails; qed"); - Account::Id(account_id) + Account::::Address(sp_core::H160([0u8; 20])) } } diff --git a/primitives/src/types/rewards.rs b/primitives/src/types/rewards.rs index 19912cb77..918bf3b29 100644 --- a/primitives/src/types/rewards.rs +++ b/primitives/src/types/rewards.rs @@ -1,11 +1,14 @@ use super::*; use crate::services::Asset; use frame_system::Config; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use services::AssetIdT; use sp_std::vec::Vec; /// Represents different types of rewards a user can earn #[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo, PartialEq, Eq)] -pub struct UserRewards> { +pub struct UserRewards> { /// Rewards earned from restaking (in TNT) pub restaking_rewards: Balance, /// Boost rewards information @@ -15,14 +18,14 @@ pub struct UserRewards { +pub struct UserRestakeUpdate { pub asset: Asset, pub amount: Balance, pub multiplier: LockMultiplier, } #[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo, PartialEq, Eq)] -pub struct ServiceRewards { +pub struct ServiceRewards { asset_id: Asset, amount: Balance, } @@ -48,7 +51,7 @@ impl Default for BoostInfo> Default +impl> Default for UserRewards { fn default() -> Self { diff --git a/runtime/mainnet/src/lib.rs b/runtime/mainnet/src/lib.rs index e01df5bf6..45051d770 100644 --- a/runtime/mainnet/src/lib.rs +++ b/runtime/mainnet/src/lib.rs @@ -1259,6 +1259,8 @@ impl pallet_multi_asset_delegation::Config for Runtime { type Currency = Balances; type MinOperatorBondAmount = MinOperatorBondAmount; type BondDuration = BondDuration; + type CurrencyToVote = U128CurrencyToVote; + type StakingInterface = Staking; type ServiceManager = Services; type LeaveOperatorsDelay = ConstU32<10>; type OperatorBondLessDelay = ConstU32<1>; diff --git a/runtime/mainnet/src/tangle_services.rs b/runtime/mainnet/src/tangle_services.rs index a6cbcf267..326a35b7c 100644 --- a/runtime/mainnet/src/tangle_services.rs +++ b/runtime/mainnet/src/tangle_services.rs @@ -4,7 +4,7 @@ use pallet_evm::GasWeightMapping; use scale_info::TypeInfo; parameter_types! { - pub const ServicesEVMAddress: H160 = H160([0x11; 20]); + pub const ServicesPalletId: PalletId = PalletId(*b"Services"); } pub struct PalletEvmRunner; @@ -143,6 +143,9 @@ parameter_types! { #[derive(Default, Copy, Clone, Eq, PartialEq, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo, Serialize, Deserialize)] pub const MaxMasterBlueprintServiceManagerVersions: u32 = u32::MAX; + + #[derive(Default, Copy, Clone, Eq, PartialEq, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo, Serialize, Deserialize)] + pub const NativeExposureMinimum: Percent = Percent::from_percent(10); } pub type PalletServicesConstraints = pallet_services::types::ConstraintsOf; @@ -152,7 +155,9 @@ impl pallet_services::Config for Runtime { type ForceOrigin = EnsureRootOrHalfCouncil; type Currency = Balances; type Fungibles = Assets; - type PalletEVMAddress = ServicesEVMAddress; + type PalletId = ServicesPalletId; + type SlashRecipient = TreasuryAccount; + type SlashManager = (); type EvmRunner = PalletEvmRunner; type EvmGasWeightMapping = PalletEVMGasWeightMapping; type EvmAddressMapping = PalletEVMAddressMapping; @@ -178,6 +183,7 @@ impl pallet_services::Config for Runtime { type MaxContainerImageTagLength = MaxContainerImageTagLength; type MaxAssetsPerService = MaxAssetsPerService; type MaxMasterBlueprintServiceManagerVersions = MaxMasterBlueprintServiceManagerVersions; + type NativeExposureMinimum = NativeExposureMinimum; type Constraints = PalletServicesConstraints; type SlashDeferDuration = SlashDeferDuration; type MasterBlueprintServiceManagerUpdateOrigin = EnsureRootOrHalfCouncil; diff --git a/runtime/testnet/src/frontier_evm.rs b/runtime/testnet/src/frontier_evm.rs index 23fb1f1a3..2358248dc 100644 --- a/runtime/testnet/src/frontier_evm.rs +++ b/runtime/testnet/src/frontier_evm.rs @@ -96,7 +96,7 @@ impl OnChargeEVMTransaction for CustomEVMCurrencyAdapter { who: &H160, fee: U256, ) -> Result> { - let pallet_services_address = pallet_services::Pallet::::address(); + let pallet_services_address = pallet_services::Pallet::::pallet_evm_account(); // Make pallet services account free to use if who == &pallet_services_address { return Ok(None); @@ -113,7 +113,7 @@ impl OnChargeEVMTransaction for CustomEVMCurrencyAdapter { base_fee: U256, already_withdrawn: Self::LiquidityInfo, ) -> Self::LiquidityInfo { - let pallet_services_address = pallet_services::Pallet::::address(); + let pallet_services_address = pallet_services::Pallet::::pallet_evm_account(); // Make pallet services account free to use if who == &pallet_services_address { return already_withdrawn; diff --git a/runtime/testnet/src/lib.rs b/runtime/testnet/src/lib.rs index 504ef777b..a98f33073 100644 --- a/runtime/testnet/src/lib.rs +++ b/runtime/testnet/src/lib.rs @@ -1522,6 +1522,8 @@ impl pallet_multi_asset_delegation::Config for Runtime { type Currency = Balances; type MinOperatorBondAmount = MinOperatorBondAmount; type BondDuration = BondDuration; + type CurrencyToVote = U128CurrencyToVote; + type StakingInterface = Staking; type ServiceManager = Services; type LeaveOperatorsDelay = LeaveOperatorsDelay; type OperatorBondLessDelay = OperatorBondLessDelay; diff --git a/runtime/testnet/src/tangle_services.rs b/runtime/testnet/src/tangle_services.rs index a2beafd87..6696f43d7 100644 --- a/runtime/testnet/src/tangle_services.rs +++ b/runtime/testnet/src/tangle_services.rs @@ -1,7 +1,7 @@ use super::*; parameter_types! { - pub const ServicesEVMAddress: H160 = H160([0x11; 20]); + pub const ServicesPalletId: PalletId = PalletId(*b"Services"); } pub struct PalletEvmRunner; @@ -140,6 +140,9 @@ parameter_types! { #[derive(Default, Copy, Clone, Eq, PartialEq, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo, Serialize, Deserialize)] pub const MaxMasterBlueprintServiceManagerVersions: u32 = u32::MAX; + + #[derive(Default, Copy, Clone, Eq, PartialEq, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo, Serialize, Deserialize)] + pub const NativeExposureMinimum: Percent = Percent::from_percent(10); } pub type PalletServicesConstraints = pallet_services::types::ConstraintsOf; @@ -149,7 +152,9 @@ impl pallet_services::Config for Runtime { type ForceOrigin = EnsureRootOrHalfCouncil; type Currency = Balances; type Fungibles = Assets; - type PalletEVMAddress = ServicesEVMAddress; + type PalletId = ServicesPalletId; + type SlashRecipient = TreasuryAccount; + type SlashManager = (); type EvmRunner = PalletEvmRunner; type EvmGasWeightMapping = PalletEVMGasWeightMapping; type EvmAddressMapping = PalletEVMAddressMapping; @@ -178,6 +183,7 @@ impl pallet_services::Config for Runtime { type SlashDeferDuration = SlashDeferDuration; type MaxMasterBlueprintServiceManagerVersions = MaxMasterBlueprintServiceManagerVersions; type MasterBlueprintServiceManagerUpdateOrigin = EnsureRootOrHalfCouncil; + type NativeExposureMinimum = NativeExposureMinimum; #[cfg(not(feature = "runtime-benchmarks"))] type OperatorDelegationManager = MultiAssetDelegation; #[cfg(feature = "runtime-benchmarks")]