diff --git a/.helix/languages.toml b/.helix/languages.toml index 0703039..41d708c 100644 --- a/.helix/languages.toml +++ b/.helix/languages.toml @@ -1,4 +1,5 @@ [language-server.rust-analyzer.config] cargo.extraEnv = { SKIP_WASM_BUILD = "true" } cargo.features = ["runtime-benchmarks"] - +check.extraEnv = { SKIP_WASM_BUILD = "true" } +check.overrideCommand = ["cargo", "check", "--message-format=json"] diff --git a/pallets/permission0/api/src/lib.rs b/pallets/permission0/api/src/lib.rs index 2a96231..ae2c28b 100644 --- a/pallets/permission0/api/src/lib.rs +++ b/pallets/permission0/api/src/lib.rs @@ -161,6 +161,29 @@ pub trait Permission0NamespacesApi { fn is_delegating_namespace(delegator: &AccountId, path: &NamespacePath) -> bool; } +pub struct WalletPermission { + pub recipient: AccountId, + pub r#type: WalletScopeType, +} + +pub enum WalletScopeType { + Stake { + /// If true, allows the recipient to perform transfer of stake between staked accounts. + can_transfer_stake: bool, + /// If true, this permission holds exclusive access to the delegator stake, meaning that + /// the delegator has no right to perform operations over stake (including unstaking) + /// while this permission is active. + exclusive_stake_access: bool, + }, +} + +pub trait Permission0WalletApi { + /// Lists all active wallet permissions, regardless of the type. + fn find_active_wallet_permission( + delegator: &AccountId, + ) -> impl Iterator)>; +} + polkadot_sdk::sp_api::decl_runtime_apis! { /// A set of helper functions for permission and streams /// queries. diff --git a/pallets/permission0/src/ext.rs b/pallets/permission0/src/ext.rs index 45e126a..eb2b99f 100644 --- a/pallets/permission0/src/ext.rs +++ b/pallets/permission0/src/ext.rs @@ -17,6 +17,7 @@ use polkadot_sdk::{ pub mod curator_impl; pub mod namespace_impl; pub mod stream_impl; +pub mod wallet_impl; /// Implementation of the Permission0Api trait to be used externally impl Permission0Api> for pallet::Pallet { @@ -146,6 +147,7 @@ pub(crate) fn execute_permission_impl( } PermissionScope::Curator(_) => curator_impl::execute_permission_impl::(permission_id), PermissionScope::Namespace(_) => Ok(()), + PermissionScope::Wallet(_) => Ok(()), } } @@ -208,6 +210,7 @@ pub fn enforcement_execute_permission_impl( return curator_impl::execute_permission_impl::(&permission_id); } PermissionScope::Namespace(_) => return Ok(()), + PermissionScope::Wallet(_) => return Ok(()), } EnforcementTracking::::remove(permission_id, EnforcementReferendum::Execution); diff --git a/pallets/permission0/src/ext/wallet_impl.rs b/pallets/permission0/src/ext/wallet_impl.rs new file mode 100644 index 0000000..73a3fd6 --- /dev/null +++ b/pallets/permission0/src/ext/wallet_impl.rs @@ -0,0 +1,164 @@ +use codec::{Decode, Encode, MaxEncodedLen}; +use pallet_permission0_api::Permission0WalletApi; +use pallet_torus0_api::Torus0Api; +use polkadot_sdk::{ + frame_support::{ + CloneNoBound, DebugNoBound, EqNoBound, PartialEqNoBound, dispatch::DispatchResult, ensure, + }, + frame_system::ensure_signed, + polkadot_sdk_frame::prelude::OriginFor, +}; +use scale_info::TypeInfo; + +use crate::{ + BalanceOf, Config, Error, Event, Pallet, PermissionContract, PermissionDuration, PermissionId, + PermissionScope, Permissions, PermissionsByDelegator, RevocationTerms, generate_permission_id, + permission::{ + add_permission_indices, + wallet::{WalletScope, WalletScopeType, WalletStake}, + }, +}; + +impl Permission0WalletApi for Pallet { + fn find_active_wallet_permission( + delegator: &T::AccountId, + ) -> impl Iterator< + Item = ( + PermissionId, + pallet_permission0_api::WalletPermission, + ), + > { + PermissionsByDelegator::::get(delegator) + .into_iter() + .filter_map(|pid| { + let permission = Permissions::::get(pid)?; + let PermissionScope::Wallet(wallet) = permission.scope else { + return None; + }; + + Some(( + pid, + pallet_permission0_api::WalletPermission { + recipient: wallet.recipient, + r#type: match wallet.r#type { + WalletScopeType::Stake(stake) => { + pallet_permission0_api::WalletScopeType::Stake { + can_transfer_stake: stake.can_transfer_stake, + exclusive_stake_access: stake.exclusive_stake_access, + } + } + }, + }, + )) + }) + } +} +pub(crate) fn delegate_wallet_stake_permission( + origin: OriginFor, + recipient: T::AccountId, + stake_details: WalletStake, + duration: PermissionDuration, + revocation: RevocationTerms, +) -> DispatchResult { + let delegator = ensure_signed(origin)?; + ensure!(delegator != recipient, Error::::SelfPermissionNotAllowed); + + for (_, perm) in Pallet::::find_active_wallet_permission(&delegator) { + if stake_details.exclusive_stake_access + || matches!( + perm.r#type, + pallet_permission0_api::WalletScopeType::Stake { + exclusive_stake_access: true, + .. + } + ) + { + return Err(Error::::DuplicatePermission.into()); + } + } + + let scope = PermissionScope::Wallet(WalletScope { + recipient: recipient.clone(), + r#type: WalletScopeType::Stake(stake_details), + }); + let permission_id = generate_permission_id::(&delegator, &scope)?; + + let contract = PermissionContract::::new( + delegator, + scope, + duration, + revocation, + crate::EnforcementAuthority::None, + ); + + Permissions::::insert(permission_id, &contract); + add_permission_indices::( + &contract.delegator, + core::iter::once(&recipient), + permission_id, + )?; + + >::deposit_event(Event::PermissionDelegated { + delegator: contract.delegator, + permission_id, + }); + + Ok(()) +} + +pub(crate) fn execute_wallet_stake_permission( + caller: OriginFor, + permission_id: PermissionId, + op: WalletStakeOperation, +) -> DispatchResult { + let caller = ensure_signed(caller)?; + let Some(permission) = Permissions::::get(permission_id) else { + return Err(Error::::PermissionNotFound.into()); + }; + let PermissionScope::Wallet(wallet) = &permission.scope else { + return Err(Error::::UnsupportedPermissionType.into()); + }; + #[allow(irrefutable_let_patterns)] + let WalletScopeType::Stake(stake) = &wallet.r#type else { + return Err(Error::::UnsupportedPermissionType.into()); + }; + + ensure!( + caller == wallet.recipient, + Error::::NotPermissionRecipient + ); + + let staker = &permission.delegator; + + match op { + WalletStakeOperation::Unstake { staked, amount } => { + ::remove_stake(staker, &staked, amount)?; + } + WalletStakeOperation::Transfer { from, to, amount } => { + ensure!(stake.can_transfer_stake, Error::::PermissionNotFound); + ::transfer_stake(staker, &from, &to, amount)?; + } + } + + Ok(()) +} + +#[derive( + CloneNoBound, DebugNoBound, Encode, Decode, MaxEncodedLen, TypeInfo, PartialEqNoBound, EqNoBound, +)] +#[scale_info(skip_type_params(T))] +pub enum WalletStakeOperation { + /// Unstakes the balance from the staked account, yielding control of the + /// balance back to the delegator. + Unstake { + staked: T::AccountId, + amount: BalanceOf, + }, + /// Transfers stake from one staked agent to another staked agent, + /// related to the `transfer_stake` extrinsic in Torus0. + Transfer { + from: T::AccountId, + to: T::AccountId, + amount: BalanceOf, + }, +} diff --git a/pallets/permission0/src/lib.rs b/pallets/permission0/src/lib.rs index 5a99ce1..5c43fca 100644 --- a/pallets/permission0/src/lib.rs +++ b/pallets/permission0/src/lib.rs @@ -34,6 +34,7 @@ use polkadot_sdk::{ #[frame::pallet] pub mod pallet { use pallet_torus0_api::NamespacePathInner; + use permission::wallet::WalletStake; use polkadot_sdk::{frame_support::PalletId, sp_core::TryCollect}; use super::*; @@ -491,6 +492,27 @@ pub mod pallet { Ok(()) } + /// Delegate a permission over namespaces + #[pallet::call_index(11)] + #[pallet::weight(T::WeightInfo::delegate_namespace_permission())] + pub fn delegate_wallet_stake_permission( + origin: OriginFor, + recipient: T::AccountId, + stake_details: WalletStake, + duration: PermissionDuration, + revocation: RevocationTerms, + ) -> DispatchResult { + ext::wallet_impl::delegate_wallet_stake_permission::( + origin, + recipient, + stake_details, + duration, + revocation, + )?; + + Ok(()) + } + /// Delegate a permission over namespaces to multiple recipients. /// Note: this extrinsic creates _multiple_ permissions with the same /// properties. @@ -568,6 +590,16 @@ pub mod pallet { Ok(()) } + + #[pallet::call_index(12)] + #[pallet::weight(T::WeightInfo::update_namespace_permission())] + pub fn execute_wallet_stake_permission( + caller: OriginFor, + permission_id: PermissionId, + op: ext::wallet_impl::WalletStakeOperation, + ) -> DispatchResult { + ext::wallet_impl::execute_wallet_stake_permission(caller, permission_id, op) + } } } diff --git a/pallets/permission0/src/migrations.rs b/pallets/permission0/src/migrations.rs index e41d2a6..8b13789 100644 --- a/pallets/permission0/src/migrations.rs +++ b/pallets/permission0/src/migrations.rs @@ -1,465 +1 @@ -pub mod v7 { - use polkadot_sdk::{ - frame_support::{migrations::VersionedMigration, traits::UncheckedOnRuntimeUpgrade}, - sp_runtime::BoundedBTreeSet, - sp_std::vec::Vec, - sp_tracing::{error, info, warn}, - sp_weights::Weight, - }; - use crate::{ - Config, Pallet, PermissionContract, PermissionScope, Permissions, PermissionsByDelegator, - PermissionsByParticipants, PermissionsByRecipient, StreamScope, - permission::{ - CuratorScope, NamespaceScope, add_permission_indices, remove_permission_from_indices, - }, - }; - - pub type Migration = VersionedMigration<5, 7, MigrateToV7, Pallet, W>; - pub struct MigrateToV7(core::marker::PhantomData); - - mod old_storage { - use codec::{Decode, Encode, MaxEncodedLen}; - use pallet_torus0_api::NamespacePath; - use polkadot_sdk::{ - frame_support::Identity, - frame_support_procedural::storage_alias, - polkadot_sdk_frame::prelude::{BlockNumberFor, ValueQuery}, - sp_runtime::{BoundedBTreeMap, BoundedBTreeSet, BoundedVec}, - }; - use scale_info::TypeInfo; - - use crate::{ - AccountIdOf, Config, CuratorPermissions, DistributionControl, EnforcementAuthority, - Pallet, PermissionDuration, PermissionId, RevocationTerms, StreamAllocation, - }; - - #[storage_alias] - pub type Permissions = - StorageMap, Identity, PermissionId, OldPermissionContract>; - - #[storage_alias] - pub type PermissionsByParticipants = StorageMap< - Pallet, - Identity, - (AccountIdOf, AccountIdOf), - BoundedVec::MaxRecipientsPerPermission>, - ValueQuery, - >; - - #[storage_alias] - pub type PermissionsByDelegator = StorageMap< - Pallet, - Identity, - AccountIdOf, - BoundedVec::MaxRecipientsPerPermission>, - ValueQuery, - >; - - #[storage_alias] - pub type PermissionsByRecipient = StorageMap< - Pallet, - Identity, - AccountIdOf, - BoundedVec::MaxRecipientsPerPermission>, - ValueQuery, - >; - - #[derive(Encode, Decode, TypeInfo, MaxEncodedLen)] - #[scale_info(skip_type_params(T))] - pub struct OldEmissionScope { - pub allocation: StreamAllocation, - pub distribution: DistributionControl, - pub targets: BoundedBTreeMap, - pub accumulating: bool, - } - - #[derive(Encode, Decode, TypeInfo, MaxEncodedLen)] - #[scale_info(skip_type_params(T))] - pub struct OldCuratorScope { - pub flags: BoundedBTreeMap< - Option, - CuratorPermissions, - T::MaxCuratorSubpermissionsPerPermission, - >, - pub cooldown: Option>, - } - - #[derive(Encode, Decode, TypeInfo, MaxEncodedLen)] - #[scale_info(skip_type_params(T))] - pub struct OldNamespaceScope { - pub paths: BoundedBTreeMap< - Option, - BoundedBTreeSet, - T::MaxNamespacesPerPermission, - >, - } - - #[derive(Encode, Decode, TypeInfo, MaxEncodedLen)] - #[scale_info(skip_type_params(T))] - pub enum OldPermissionScope { - Emission(OldEmissionScope), - Curator(OldCuratorScope), - Namespace(OldNamespaceScope), - } - - #[derive(Encode, Decode, TypeInfo, MaxEncodedLen)] - #[scale_info(skip_type_params(T))] - pub struct OldPermissionContract { - pub delegator: T::AccountId, - pub recipient: T::AccountId, - pub scope: OldPermissionScope, - pub duration: PermissionDuration, - pub revocation: RevocationTerms, - /// Enforcement authority that can toggle the permission - pub enforcement: EnforcementAuthority, - /// Last execution block - #[doc(hidden)] - pub last_execution: Option>, - /// Number of times the permission was executed - #[doc(hidden)] - pub execution_count: u32, - pub max_instances: u32, - pub children: polkadot_sdk::sp_runtime::BoundedBTreeSet< - PermissionId, - T::MaxChildrenPerPermission, - >, - pub created_at: BlockNumberFor, - } - } - - impl UncheckedOnRuntimeUpgrade for MigrateToV7 { - fn on_runtime_upgrade() -> Weight { - let _ = old_storage::PermissionsByDelegator::::clear(u32::MAX, None); - let _ = old_storage::PermissionsByRecipient::::clear(u32::MAX, None); - let _ = old_storage::PermissionsByParticipants::::clear(u32::MAX, None); - - let _ = crate::PermissionsByDelegator::::clear(u32::MAX, None); - let _ = crate::PermissionsByRecipient::::clear(u32::MAX, None); - let _ = crate::PermissionsByParticipants::::clear(u32::MAX, None); - - for (permission_id, old_contract) in old_storage::Permissions::::iter() { - let new_scope = match old_contract.scope { - old_storage::OldPermissionScope::Emission(old_emission) => { - // For emission permissions, we need to update storage indices: - // 1. Remove the old recipient index (permission recipient) - // 2. Add indices for all the emission targets - - // Remove old recipient index - remove_permission_from_indices::( - &old_contract.delegator, - core::iter::once(&old_contract.recipient), - permission_id, - ); - - let mut managers = BoundedBTreeSet::new(); - let _ = managers.try_insert(old_contract.delegator.clone()); - - let stream = StreamScope:: { - recipients: old_emission.targets, // Field renamed from targets to recipients - allocation: old_emission.allocation, - distribution: old_emission.distribution, - accumulating: old_emission.accumulating, - // New manager fields introduced in v6 - recipient_managers: managers.clone(), - weight_setters: managers, - }; - - // Add new indices for all emission targets - if let Err(e) = add_permission_indices::( - &old_contract.delegator, - stream.recipients.keys(), - permission_id, - ) { - error!( - "Failed to add permission indices for emission permission {permission_id:?}: {e:?}" - ); - } - - PermissionScope::Stream(stream) - } - old_storage::OldPermissionScope::Curator(old_curator) => { - let new_curator = crate::permission::CuratorScope:: { - recipient: old_contract.recipient, - flags: old_curator.flags, - cooldown: old_curator.cooldown, - max_instances: old_contract.max_instances, - children: old_contract.children, - }; - - if let Err(e) = add_permission_indices::( - &old_contract.delegator, - core::iter::once(&new_curator.recipient), - permission_id, - ) { - error!( - "Failed to add permission indices for curator permission {permission_id:?}: {e:?}" - ); - } - - PermissionScope::Curator(new_curator) - } - old_storage::OldPermissionScope::Namespace(old_namespace) => { - let new_namespace = crate::permission::NamespaceScope:: { - recipient: old_contract.recipient, - paths: old_namespace.paths, - max_instances: old_contract.max_instances, - children: old_contract.children, - }; - - if let Err(e) = add_permission_indices::( - &old_contract.delegator, - core::iter::once(&new_namespace.recipient), - permission_id, - ) { - error!( - "Failed to add permission indices for namespace permission {permission_id:?}: {e:?}" - ); - } - - PermissionScope::Namespace(new_namespace) - } - }; - - let new_contract = PermissionContract:: { - delegator: old_contract.delegator, - scope: new_scope, - duration: old_contract.duration, - revocation: old_contract.revocation, - enforcement: old_contract.enforcement, - last_update: old_contract.created_at, - last_execution: old_contract.last_execution, - execution_count: old_contract.execution_count, - created_at: old_contract.created_at, - }; - - crate::Permissions::::set(permission_id, Some(new_contract)); - } - - check_all_indices_consistency::(); - - Weight::zero() - } - } - - /// Check consistency of all permission indices after migration - fn check_all_indices_consistency() { - info!("Starting permission0 index consistency checks..."); - - let mut total_inconsistencies = 0u32; - total_inconsistencies = - total_inconsistencies.saturating_add(check_delegator_indices_consistency::()); - total_inconsistencies = - total_inconsistencies.saturating_add(check_recipient_indices_consistency::()); - total_inconsistencies = - total_inconsistencies.saturating_add(check_participant_indices_consistency::()); - total_inconsistencies = - total_inconsistencies.saturating_add(check_permissions_are_indexed::()); - - if total_inconsistencies == 0 { - info!("All permission0 indices are consistent!"); - } else { - error!("Found {total_inconsistencies} total index inconsistencies in permission0!"); - } - } - - /// Check that all permissions in delegator indices exist and have correct delegator - fn check_delegator_indices_consistency() -> u32 { - let mut inconsistencies = 0u32; - - for (delegator, permission_ids) in PermissionsByDelegator::::iter() { - for permission_id in permission_ids.iter() { - if let Some(contract) = Permissions::::get(permission_id) { - if contract.delegator != delegator { - error!( - "Delegator index inconsistency: Permission {permission_id:?} \ - indexed under delegator {delegator:?} but actual delegator is {:?}", - contract.delegator - ); - inconsistencies = inconsistencies.saturating_add(1); - } - } else { - error!( - "Delegator index inconsistency: Permission {permission_id:?} \ - indexed under delegator {delegator:?} but permission doesn't exist" - ); - inconsistencies = inconsistencies.saturating_add(1); - } - } - } - - if inconsistencies > 0 { - warn!("Found {inconsistencies} delegator index inconsistencies"); - } else { - info!("Delegator indices are consistent"); - } - - inconsistencies - } - - /// Check that all permissions in recipient indices exist and recipient is listed in the permission scope - fn check_recipient_indices_consistency() -> u32 { - let mut inconsistencies = 0u32; - - for (recipient, permission_ids) in PermissionsByRecipient::::iter() { - for permission_id in permission_ids.iter() { - if let Some(contract) = Permissions::::get(permission_id) { - let is_valid_recipient = match &contract.scope { - PermissionScope::Stream(StreamScope { recipients, .. }) => { - recipients.contains_key(&recipient) - } - PermissionScope::Curator(CuratorScope { - recipient: scope_recipient, - .. - }) - | PermissionScope::Namespace(NamespaceScope { - recipient: scope_recipient, - .. - }) => scope_recipient == &recipient, - }; - - if !is_valid_recipient { - error!( - "Recipient index inconsistency: Permission {permission_id:?} \ - indexed under recipient {recipient:?} but recipient is not in the permission scope" - ); - inconsistencies = inconsistencies.saturating_add(1); - } - } else { - error!( - "Recipient index inconsistency: Permission {permission_id:?} \ - indexed under recipient {recipient:?} but permission doesn't exist" - ); - inconsistencies = inconsistencies.saturating_add(1); - } - } - } - - if inconsistencies > 0 { - warn!("Found {inconsistencies} recipient index inconsistencies"); - } else { - info!("Recipient indices are consistent"); - } - - inconsistencies - } - - /// Check that all permissions in participant indices exist and both participants are correct - fn check_participant_indices_consistency() -> u32 { - let mut inconsistencies = 0u32; - - for ((delegator, recipient), permission_ids) in PermissionsByParticipants::::iter() { - for permission_id in permission_ids.iter() { - if let Some(contract) = Permissions::::get(permission_id) { - if contract.delegator != delegator { - error!( - "Participant index inconsistency: Permission {permission_id:?} \ - indexed under delegator {delegator:?} but actual delegator is {:?}", - contract.delegator - ); - inconsistencies = inconsistencies.saturating_add(1); - continue; - } - - let is_valid_recipient = match &contract.scope { - PermissionScope::Stream(StreamScope { recipients, .. }) => { - recipients.contains_key(&recipient) - } - PermissionScope::Curator(CuratorScope { - recipient: scope_recipient, - .. - }) - | PermissionScope::Namespace(NamespaceScope { - recipient: scope_recipient, - .. - }) => scope_recipient == &recipient, - }; - - if !is_valid_recipient { - error!( - "Participant index inconsistency: Permission {permission_id:?} \ - indexed under participant pair ({delegator:?}, {recipient:?}) \ - but recipient is not in the permission scope" - ); - inconsistencies = inconsistencies.saturating_add(1); - } - } else { - error!( - "Participant index inconsistency: Permission {permission_id:?} \ - indexed under participant pair ({delegator:?}, {recipient:?}) \ - but permission doesn't exist" - ); - inconsistencies = inconsistencies.saturating_add(1); - } - } - } - - if inconsistencies > 0 { - warn!("Found {inconsistencies} participant index inconsistencies"); - } else { - info!("Participant indices are consistent"); - } - - inconsistencies - } - - /// Check that all permissions in storage are properly indexed - /// This is the counterpart function that verifies all permissions have correct index entries - pub fn check_permissions_are_indexed() -> u32 { - let mut inconsistencies = 0u32; - - info!("Checking that all permissions in storage are properly indexed..."); - - for (permission_id, contract) in Permissions::::iter() { - let delegator = &contract.delegator; - - let delegator_indices = PermissionsByDelegator::::get(delegator); - if !delegator_indices.contains(&permission_id) { - error!( - "Missing delegator index: Permission {permission_id:?} with delegator {delegator:?} \ - is not in PermissionsByDelegator index" - ); - inconsistencies = inconsistencies.saturating_add(1); - } - - let recipients: Vec = match &contract.scope { - PermissionScope::Stream(StreamScope { recipients, .. }) => { - recipients.keys().cloned().collect() - } - PermissionScope::Curator(CuratorScope { recipient, .. }) - | PermissionScope::Namespace(NamespaceScope { recipient, .. }) => { - polkadot_sdk::sp_std::vec![recipient.clone()] - } - }; - - for recipient in recipients.iter() { - let recipient_indices = PermissionsByRecipient::::get(recipient); - if !recipient_indices.contains(&permission_id) { - error!( - "Missing recipient index: Permission {permission_id:?} with recipient {recipient:?} \ - is not in PermissionsByRecipient index" - ); - inconsistencies = inconsistencies.saturating_add(1); - } - - let participant_indices = - PermissionsByParticipants::::get((delegator.clone(), recipient.clone())); - if !participant_indices.contains(&permission_id) { - error!( - "Missing participant index: Permission {permission_id:?} with participants \ - ({delegator:?}, {recipient:?}) is not in PermissionsByParticipants index" - ); - inconsistencies = inconsistencies.saturating_add(1); - } - } - } - - if inconsistencies > 0 { - warn!("Found {inconsistencies} missing index entries for existing permissions"); - } else { - info!("All permissions are properly indexed"); - } - - inconsistencies - } -} diff --git a/pallets/permission0/src/permission.rs b/pallets/permission0/src/permission.rs index 6e6051c..7544d5d 100644 --- a/pallets/permission0/src/permission.rs +++ b/pallets/permission0/src/permission.rs @@ -11,10 +11,11 @@ use polkadot_sdk::{ BoundedBTreeMap, BoundedVec, DispatchError, Percent, traits::{BlakeTwo256, Hash}, }, - sp_std::vec::Vec, + sp_std::{vec, vec::Vec}, sp_tracing::{error, info, trace}, }; use scale_info::TypeInfo; +use wallet::WalletScope; use crate::*; @@ -25,6 +26,7 @@ pub use stream::{DistributionControl, StreamAllocation, StreamScope}; pub mod curator; pub mod namespace; pub mod stream; +pub mod wallet; /// Type for permission ID pub type PermissionId = H256; @@ -246,7 +248,8 @@ impl PermissionContract { let delegator = self.delegator.clone(); let recipients = match &self.scope { PermissionScope::Curator(CuratorScope { recipient, .. }) - | PermissionScope::Namespace(NamespaceScope { recipient, .. }) => { + | PermissionScope::Namespace(NamespaceScope { recipient, .. }) + | PermissionScope::Wallet(WalletScope { recipient, .. }) => { vec![recipient.clone()] } PermissionScope::Stream(StreamScope { recipients, .. }) => { @@ -292,7 +295,7 @@ impl PermissionContract { Error::::NotAuthorizedToRevoke ); - if recipients.len() > 1 { + if recipients.len() > 1usize { remove_recipient_from_indices::(&delegator, caller, permission_id); Permissions::::mutate(permission_id, |permission| { @@ -347,7 +350,8 @@ impl PermissionContract { fn cleanup(self, permission_id: H256) -> DispatchResult { match &self.scope { PermissionScope::Curator(CuratorScope { recipient, .. }) - | PermissionScope::Namespace(NamespaceScope { recipient, .. }) => { + | PermissionScope::Namespace(NamespaceScope { recipient, .. }) + | PermissionScope::Wallet(WalletScope { recipient, .. }) => { remove_permission_from_indices::( &self.delegator, core::iter::once(recipient), @@ -377,6 +381,9 @@ impl PermissionContract { PermissionScope::Namespace(namespace) => { namespace.cleanup(permission_id, &self.last_execution, &self.delegator); } + PermissionScope::Wallet(wallet) => { + wallet.cleanup(permission_id, &self.last_execution, &self.delegator); + } } Ok(()) @@ -394,6 +401,7 @@ pub enum PermissionScope { Stream(StreamScope), Curator(CuratorScope), Namespace(NamespaceScope), + Wallet(WalletScope), } #[derive( diff --git a/pallets/permission0/src/permission/wallet.rs b/pallets/permission0/src/permission/wallet.rs new file mode 100644 index 0000000..d249057 --- /dev/null +++ b/pallets/permission0/src/permission/wallet.rs @@ -0,0 +1,41 @@ +use codec::{Decode, Encode, MaxEncodedLen}; +use polkadot_sdk::frame_support::{CloneNoBound, DebugNoBound, EqNoBound, PartialEqNoBound}; +use scale_info::TypeInfo; + +use crate::Config; + +#[derive(CloneNoBound, DebugNoBound, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub struct WalletScope { + pub recipient: T::AccountId, + pub r#type: WalletScopeType, +} + +impl WalletScope { + /// Cleanup operations when permission is revoked or expired + pub(crate) fn cleanup( + &self, + _permission_id: polkadot_sdk::sp_core::H256, + _last_execution: &Option>, + _delegator: &T::AccountId, + ) { + // No actions to perform + } +} + +#[derive(CloneNoBound, DebugNoBound, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub enum WalletScopeType { + Stake(WalletStake), +} + +#[derive( + CloneNoBound, DebugNoBound, Encode, Decode, MaxEncodedLen, TypeInfo, PartialEqNoBound, EqNoBound, +)] +pub struct WalletStake { + /// If true, allows the recipient to perform transfer of stake between staked accounts. + pub can_transfer_stake: bool, + /// If true, this permission holds exclusive access to the delegator stake, meaning that + /// the delegator has no right to perform operations over stake (including unstaking) + /// while this permission is active. + pub exclusive_stake_access: bool, +} diff --git a/pallets/permission0/tests/wallet.rs b/pallets/permission0/tests/wallet.rs new file mode 100644 index 0000000..a071722 --- /dev/null +++ b/pallets/permission0/tests/wallet.rs @@ -0,0 +1,463 @@ +use pallet_permission0::{ + PermissionDuration, PermissionId, PermissionScope, Permissions, RevocationTerms, + ext::wallet_impl::WalletStakeOperation, + permission::wallet::{WalletScopeType, WalletStake}, +}; +use polkadot_sdk::frame_support::{assert_err, assert_ok}; +use test_utils::*; + +pub fn new_test_ext() -> polkadot_sdk::sp_io::TestExternalities { + new_test_ext_with_block(1) +} + +fn setup_agents_with_stake() -> (AccountId, AccountId, AccountId) { + zero_min_burn(); + + let staker = 1; + let validator1 = 2; + let validator2 = 3; + + register_empty_agent(staker); + register_empty_agent(validator1); + register_empty_agent(validator2); + + add_balance(staker, as_tors(1000)); + + assert_ok!(Torus0::add_stake( + RuntimeOrigin::signed(staker), + validator1, + as_tors(100) + )); + + (staker, validator1, validator2) +} + +fn get_last_delegated_permission_id(delegator: AccountId) -> PermissionId { + System::events() + .into_iter() + .filter_map(|record| { + if let RuntimeEvent::Permission0(pallet_permission0::Event::PermissionDelegated { + delegator: event_delegator, + permission_id, + }) = record.event + { + if event_delegator == delegator { + Some(permission_id) + } else { + None + } + } else { + None + } + }) + .next_back() + .expect("No PermissionDelegated event found") +} + +// ============ Delegation Rules Tests ============ + +#[test] +fn basic_stake_permission_delegation() { + new_test_ext().execute_with(|| { + let (staker, _, _) = setup_agents_with_stake(); + let recipient = 10; + register_empty_agent(recipient); + + assert_err!( + Permission0::delegate_wallet_stake_permission( + RuntimeOrigin::signed(staker), + staker, + WalletStake { + can_transfer_stake: false, + exclusive_stake_access: false, + }, + PermissionDuration::Indefinite, + RevocationTerms::RevocableByDelegator, + ), + pallet_permission0::Error::::SelfPermissionNotAllowed + ); + + assert_ok!(Permission0::delegate_wallet_stake_permission( + RuntimeOrigin::signed(staker), + recipient, + WalletStake { + can_transfer_stake: false, + exclusive_stake_access: false, + }, + PermissionDuration::Indefinite, + RevocationTerms::RevocableByDelegator, + )); + + let permission_id = get_last_delegated_permission_id(staker); + let permission = Permissions::::get(permission_id).unwrap(); + + assert_eq!(permission.delegator, staker); + if let PermissionScope::Wallet(wallet) = permission.scope { + assert_eq!(wallet.recipient, recipient); + #[allow(irrefutable_let_patterns)] + if let WalletScopeType::Stake(stake) = wallet.r#type { + assert!(!stake.can_transfer_stake); + assert!(!stake.exclusive_stake_access); + } else { + panic!("Expected Stake type"); + } + } else { + panic!("Expected Wallet scope"); + } + }); +} + +#[test] +fn exclusive_permission_blocks_new_delegations() { + new_test_ext().execute_with(|| { + let (staker, _, _) = setup_agents_with_stake(); + let recipient1 = 10; + let recipient2 = 11; + register_empty_agent(recipient1); + register_empty_agent(recipient2); + + assert_ok!(Permission0::delegate_wallet_stake_permission( + RuntimeOrigin::signed(staker), + recipient1, + WalletStake { + can_transfer_stake: true, + exclusive_stake_access: true, + }, + PermissionDuration::Indefinite, + RevocationTerms::RevocableByDelegator, + )); + + assert_err!( + Permission0::delegate_wallet_stake_permission( + RuntimeOrigin::signed(staker), + recipient2, + WalletStake { + can_transfer_stake: false, + exclusive_stake_access: false, + }, + PermissionDuration::Indefinite, + RevocationTerms::RevocableByDelegator, + ), + pallet_permission0::Error::::DuplicatePermission + ); + }); +} + +#[test] +fn cannot_delegate_exclusive_after_non_exclusive() { + new_test_ext().execute_with(|| { + let (staker, _, _) = setup_agents_with_stake(); + let recipient1 = 10; + let recipient2 = 11; + register_empty_agent(recipient1); + register_empty_agent(recipient2); + + assert_ok!(Permission0::delegate_wallet_stake_permission( + RuntimeOrigin::signed(staker), + recipient1, + WalletStake { + can_transfer_stake: false, + exclusive_stake_access: false, + }, + PermissionDuration::Indefinite, + RevocationTerms::RevocableByDelegator, + )); + + assert_err!( + Permission0::delegate_wallet_stake_permission( + RuntimeOrigin::signed(staker), + recipient2, + WalletStake { + can_transfer_stake: true, + exclusive_stake_access: true, + }, + PermissionDuration::Indefinite, + RevocationTerms::RevocableByDelegator, + ), + pallet_permission0::Error::::DuplicatePermission + ); + }); +} + +#[test] +fn multiple_non_exclusive_permissions_allowed() { + new_test_ext().execute_with(|| { + let (staker, _, _) = setup_agents_with_stake(); + let recipient1 = 10; + let recipient2 = 11; + register_empty_agent(recipient1); + register_empty_agent(recipient2); + + // Delegate first non-exclusive stake permission + assert_ok!(Permission0::delegate_wallet_stake_permission( + RuntimeOrigin::signed(staker), + recipient1, + WalletStake { + can_transfer_stake: false, + exclusive_stake_access: false, + }, + PermissionDuration::Indefinite, + RevocationTerms::RevocableByDelegator, + )); + + // Delegate second non-exclusive permission (should succeed) + assert_ok!(Permission0::delegate_wallet_stake_permission( + RuntimeOrigin::signed(staker), + recipient2, + WalletStake { + can_transfer_stake: true, + exclusive_stake_access: false, + }, + PermissionDuration::Indefinite, + RevocationTerms::RevocableByDelegator, + )); + + // Verify both permissions exist + let permissions: Vec<_> = pallet_permission0::PermissionsByDelegator::::get(staker) + .into_iter() + .collect(); + assert_eq!(permissions.len(), 2); + }); +} + +// ============ Execution Tests ============ + +#[test] +fn unstake_operation_through_permission() { + new_test_ext().execute_with(|| { + let (staker, validator, _) = setup_agents_with_stake(); + let recipient = 10; + register_empty_agent(recipient); + + assert_ok!(Permission0::delegate_wallet_stake_permission( + RuntimeOrigin::signed(staker), + recipient, + WalletStake { + can_transfer_stake: false, + exclusive_stake_access: false, + }, + PermissionDuration::Indefinite, + RevocationTerms::RevocableByDelegator, + )); + + let permission_id = get_last_delegated_permission_id(staker); + + assert_ok!(Permission0::execute_wallet_stake_permission( + RuntimeOrigin::signed(recipient), + permission_id, + WalletStakeOperation::Unstake { + staked: validator, + amount: as_tors(50), + } + )); + + let stake_amount = pallet_torus0::StakingTo::::get(staker, validator).unwrap_or(0); + assert_eq!(stake_amount, as_tors(50)); + }); +} + +#[test] +fn transfer_stake_operation_requires_permission() { + new_test_ext().execute_with(|| { + let (staker, validator1, validator2) = setup_agents_with_stake(); + let recipient = 10; + register_empty_agent(recipient); + + assert_ok!(Permission0::delegate_wallet_stake_permission( + RuntimeOrigin::signed(staker), + recipient, + WalletStake { + can_transfer_stake: false, + exclusive_stake_access: false, + }, + PermissionDuration::Indefinite, + RevocationTerms::RevocableByDelegator, + )); + let permission_id = get_last_delegated_permission_id(staker); + + assert_err!( + Permission0::execute_wallet_stake_permission( + RuntimeOrigin::signed(recipient), + permission_id, + WalletStakeOperation::Transfer { + from: validator1, + to: validator2, + amount: as_tors(30), + } + ), + pallet_permission0::Error::::PermissionNotFound + ); + + assert_ok!(Permission0::delegate_wallet_stake_permission( + RuntimeOrigin::signed(staker), + recipient, + WalletStake { + can_transfer_stake: true, + exclusive_stake_access: false, + }, + PermissionDuration::Indefinite, + RevocationTerms::RevocableByDelegator, + )); + let permission_id = get_last_delegated_permission_id(staker); + + assert_ok!(Permission0::execute_wallet_stake_permission( + RuntimeOrigin::signed(recipient), + permission_id, + WalletStakeOperation::Transfer { + from: validator1, + to: validator2, + amount: as_tors(30), + } + )); + }); +} + +#[test] +fn transfer_stake_with_permission() { + new_test_ext().execute_with(|| { + let (staker, validator1, validator2) = setup_agents_with_stake(); + let recipient = 10; + register_empty_agent(recipient); + + assert_ok!(Permission0::delegate_wallet_stake_permission( + RuntimeOrigin::signed(staker), + recipient, + WalletStake { + can_transfer_stake: true, + exclusive_stake_access: false, + }, + PermissionDuration::Indefinite, + RevocationTerms::RevocableByDelegator, + )); + + let permission_id = get_last_delegated_permission_id(staker); + + assert_ok!(Permission0::execute_wallet_stake_permission( + RuntimeOrigin::signed(recipient), + permission_id, + WalletStakeOperation::Transfer { + from: validator1, + to: validator2, + amount: as_tors(30), + } + )); + + let stake1 = pallet_torus0::StakingTo::::get(staker, validator1).unwrap_or(0); + let stake2 = pallet_torus0::StakingTo::::get(staker, validator2).unwrap_or(0); + assert_eq!(stake1, as_tors(70)); + assert_eq!(stake2, as_tors(30)); + }); +} + +#[test] +fn only_recipient_can_execute_permission() { + new_test_ext().execute_with(|| { + let (staker, validator, _) = setup_agents_with_stake(); + let recipient = 10; + let other_user = 11; + register_empty_agent(recipient); + register_empty_agent(other_user); + + // Delegate stake permission + assert_ok!(Permission0::delegate_wallet_stake_permission( + RuntimeOrigin::signed(staker), + recipient, + WalletStake { + can_transfer_stake: false, + exclusive_stake_access: false, + }, + PermissionDuration::Indefinite, + RevocationTerms::RevocableByDelegator, + )); + + let permission_id = get_last_delegated_permission_id(staker); + + // Try to execute with wrong account + assert_err!( + Permission0::execute_wallet_stake_permission( + RuntimeOrigin::signed(other_user), + permission_id, + WalletStakeOperation::Unstake { + staked: validator, + amount: as_tors(50), + } + ), + pallet_permission0::Error::::NotPermissionRecipient + ); + + // Execute with correct recipient + assert_ok!(Permission0::execute_wallet_stake_permission( + RuntimeOrigin::signed(recipient), + permission_id, + WalletStakeOperation::Unstake { + staked: validator, + amount: as_tors(50), + } + )); + }); +} + +#[test] +fn permission_not_found_error() { + new_test_ext().execute_with(|| { + let (_, validator, _) = setup_agents_with_stake(); + let recipient = 10; + register_empty_agent(recipient); + + let invalid_permission_id = [1u8; 32].into(); + + assert_err!( + Permission0::execute_wallet_stake_permission( + RuntimeOrigin::signed(recipient), + invalid_permission_id, + WalletStakeOperation::Unstake { + staked: validator, + amount: as_tors(50), + } + ), + pallet_permission0::Error::::PermissionNotFound + ); + }); +} + +#[test] +fn revoke_wallet_permission() { + new_test_ext().execute_with(|| { + let (staker, validator, _) = setup_agents_with_stake(); + let recipient = 10; + register_empty_agent(recipient); + + // Delegate stake permission + assert_ok!(Permission0::delegate_wallet_stake_permission( + RuntimeOrigin::signed(staker), + recipient, + WalletStake { + can_transfer_stake: false, + exclusive_stake_access: false, + }, + PermissionDuration::Indefinite, + RevocationTerms::RevocableByDelegator, + )); + + let permission_id = get_last_delegated_permission_id(staker); + + // Revoke permission + assert_ok!(Permission0::revoke_permission( + RuntimeOrigin::signed(staker), + permission_id + )); + + // Try to use revoked permission + assert_err!( + Permission0::execute_wallet_stake_permission( + RuntimeOrigin::signed(recipient), + permission_id, + WalletStakeOperation::Unstake { + staked: validator, + amount: as_tors(50), + } + ), + pallet_permission0::Error::::PermissionNotFound + ); + }); +} diff --git a/pallets/torus0/api/src/lib.rs b/pallets/torus0/api/src/lib.rs index 7be483d..16ff5eb 100644 --- a/pallets/torus0/api/src/lib.rs +++ b/pallets/torus0/api/src/lib.rs @@ -35,6 +35,13 @@ pub trait Torus0Api { fn staked_by(staked: &AccountId) -> alloc::vec::Vec<(AccountId, Balance)>; fn stake_to(staker: &AccountId, staked: &AccountId, amount: Balance) -> DispatchResult; + fn remove_stake(staker: &AccountId, staked: &AccountId, amount: Balance) -> DispatchResult; + fn transfer_stake( + staker: &AccountId, + from: &AccountId, + to: &AccountId, + amount: Balance, + ) -> DispatchResult; fn agent_ids() -> impl Iterator; fn find_agent_by_name(name: &[u8]) -> Option; diff --git a/pallets/torus0/src/lib.rs b/pallets/torus0/src/lib.rs index 212f42a..86f58b3 100644 --- a/pallets/torus0/src/lib.rs +++ b/pallets/torus0/src/lib.rs @@ -39,7 +39,7 @@ pub mod pallet { use frame::prelude::BlockNumberFor; use pallet_emission0_api::Emission0Api; use pallet_governance_api::GovernanceApi; - use pallet_permission0_api::Permission0NamespacesApi; + use pallet_permission0_api::{Permission0NamespacesApi, Permission0WalletApi, WalletScopeType}; use pallet_torus0_api::NamespacePathInner; use polkadot_sdk::frame_support::traits::{NamedReservableCurrency, ReservableCurrency}; use weights::WeightInfo; @@ -265,7 +265,8 @@ pub mod pallet { type Governance: GovernanceApi; type Emission: Emission0Api; - type Permission0: Permission0NamespacesApi; + type Permission0: Permission0NamespacesApi + + Permission0WalletApi; type WeightInfo: WeightInfo; } @@ -301,6 +302,20 @@ pub mod pallet { amount: BalanceOf, ) -> DispatchResult { let key = ensure_signed(origin)?; + + ensure!( + !::find_active_wallet_permission(&key).any(|(_, perm)| { + matches!( + perm.r#type, + WalletScopeType::Stake { + exclusive_stake_access: true, + .. + } + ) + }), + Error::::StakeIsDelegated + ); + stake::remove_stake::(key, agent_key, amount) } @@ -314,6 +329,20 @@ pub mod pallet { amount: BalanceOf, ) -> DispatchResult { let key = ensure_signed(origin)?; + + ensure!( + !::find_active_wallet_permission(&key).any(|(_, perm)| { + matches!( + perm.r#type, + WalletScopeType::Stake { + exclusive_stake_access: true, + .. + } + ) + }), + Error::::StakeIsDelegated + ); + stake::transfer_stake::(key, agent_key, new_agent_key, amount) } @@ -525,6 +554,8 @@ pub mod pallet { AgentsFrozen, /// Namespace Creation was disabled by a curator. NamespacesFrozen, + /// The stake is being delegated exclusively. + StakeIsDelegated, } } @@ -583,6 +614,23 @@ impl stake::add_stake::(staker.clone(), staked.clone(), amount) } + fn remove_stake( + staker: &T::AccountId, + staked: &T::AccountId, + amount: >::Balance, + ) -> DispatchResult { + stake::remove_stake::(staker.clone(), staked.clone(), amount) + } + + fn transfer_stake( + staker: &T::AccountId, + from: &T::AccountId, + to: &T::AccountId, + amount: >::Balance, + ) -> DispatchResult { + stake::transfer_stake::(staker.clone(), from.clone(), to.clone(), amount) + } + fn agent_ids() -> impl Iterator { Agents::::iter_keys() } diff --git a/pallets/torus0/src/stake.rs b/pallets/torus0/src/stake.rs index ab4ff52..be7ddb0 100644 --- a/pallets/torus0/src/stake.rs +++ b/pallets/torus0/src/stake.rs @@ -89,12 +89,12 @@ fn remove_stake0( /// [`add_stake`]). pub fn transfer_stake( staker: AccountIdOf, - old_staked: AccountIdOf, - new_staked: AccountIdOf, + from: AccountIdOf, + to: AccountIdOf, amount: BalanceOf, ) -> DispatchResult { - remove_stake::(staker.clone(), old_staked, amount)?; - add_stake::(staker, new_staked, amount)?; + remove_stake::(staker.clone(), from, amount)?; + add_stake::(staker, to, amount)?; Ok(()) } diff --git a/pallets/torus0/tests/stake.rs b/pallets/torus0/tests/stake.rs index 1e3776b..6ac2dec 100644 --- a/pallets/torus0/tests/stake.rs +++ b/pallets/torus0/tests/stake.rs @@ -5,7 +5,10 @@ use polkadot_sdk::frame_support::{ assert_err, traits::{Currency, NamedReservableCurrency}, }; -use test_utils::{Balances, Test, as_tors, assert_ok, get_origin, pallet_governance}; +use test_utils::{ + Balances, Permission0, Test, as_tors, assert_ok, get_origin, pallet_governance, + pallet_permission0, +}; #[test] fn add_stake_correctly() { @@ -286,3 +289,198 @@ fn remove_stake_with_deregistered_agent() { assert_eq!(TotalStake::::get(), 0); }); } + +#[test] +fn exclusive_wallet_permission_blocks_stake_operations() { + test_utils::new_test_ext().execute_with(|| { + let staker = 0; + let validator1 = 1; + let validator2 = 2; + let recipient = 3; + let stake_amount = MinAllowedStake::::get(); + let total_balance = stake_amount * 3; + + assert_ok!(pallet_governance::whitelist::add_to_whitelist::( + validator1 + )); + assert_ok!(pallet_governance::whitelist::add_to_whitelist::( + validator2 + )); + assert_ok!(pallet_governance::whitelist::add_to_whitelist::( + staker + )); + assert_ok!(pallet_governance::whitelist::add_to_whitelist::( + recipient + )); + + assert_ok!(pallet_torus0::agent::register::( + validator1, + "validator1".as_bytes().to_vec(), + "val1://url".as_bytes().to_vec(), + "meta1".as_bytes().to_vec() + )); + + assert_ok!(pallet_torus0::agent::register::( + validator2, + "validator2".as_bytes().to_vec(), + "val2://url".as_bytes().to_vec(), + "meta2".as_bytes().to_vec() + )); + + assert_ok!(pallet_torus0::agent::register::( + staker, + "staker".as_bytes().to_vec(), + "staker://url".as_bytes().to_vec(), + "meta".as_bytes().to_vec() + )); + + assert_ok!(pallet_torus0::agent::register::( + recipient, + "recipient".as_bytes().to_vec(), + "recipient://url".as_bytes().to_vec(), + "meta".as_bytes().to_vec() + )); + + let _ = Balances::deposit_creating(&staker, total_balance); + assert_ok!(pallet_torus0::Pallet::::add_stake( + get_origin(staker), + validator1, + stake_amount + )); + + assert_ok!(Permission0::delegate_wallet_stake_permission( + get_origin(staker), + recipient, + pallet_permission0::permission::wallet::WalletStake { + can_transfer_stake: true, + exclusive_stake_access: true, + }, + pallet_permission0::PermissionDuration::Indefinite, + pallet_permission0::RevocationTerms::RevocableByDelegator, + )); + + assert_err!( + pallet_torus0::Pallet::::remove_stake( + get_origin(staker), + validator1, + stake_amount / 2 + ), + Error::::StakeIsDelegated + ); + + assert_err!( + pallet_torus0::Pallet::::transfer_stake( + get_origin(staker), + validator1, + validator2, + stake_amount / 2 + ), + Error::::StakeIsDelegated + ); + + assert_eq!( + StakingTo::::get(staker, validator1), + Some(stake_amount) + ); + assert_eq!(StakingTo::::get(staker, validator2), None); + }); +} + +#[test] +fn non_exclusive_wallet_permission_allows_stake_operations() { + test_utils::new_test_ext().execute_with(|| { + let staker = 0; + let validator1 = 1; + let validator2 = 2; + let recipient = 3; + let stake_amount = MinAllowedStake::::get(); + let total_balance = stake_amount * 3; + + // Setup agents + assert_ok!(pallet_governance::whitelist::add_to_whitelist::( + validator1 + )); + assert_ok!(pallet_governance::whitelist::add_to_whitelist::( + validator2 + )); + assert_ok!(pallet_governance::whitelist::add_to_whitelist::( + staker + )); + assert_ok!(pallet_governance::whitelist::add_to_whitelist::( + recipient + )); + + assert_ok!(pallet_torus0::agent::register::( + validator1, + "validator1".as_bytes().to_vec(), + "val1://url".as_bytes().to_vec(), + "meta1".as_bytes().to_vec() + )); + + assert_ok!(pallet_torus0::agent::register::( + validator2, + "validator2".as_bytes().to_vec(), + "val2://url".as_bytes().to_vec(), + "meta2".as_bytes().to_vec() + )); + + assert_ok!(pallet_torus0::agent::register::( + staker, + "staker".as_bytes().to_vec(), + "staker://url".as_bytes().to_vec(), + "meta".as_bytes().to_vec() + )); + + assert_ok!(pallet_torus0::agent::register::( + recipient, + "recipient".as_bytes().to_vec(), + "recipient://url".as_bytes().to_vec(), + "meta".as_bytes().to_vec() + )); + + let _ = Balances::deposit_creating(&staker, total_balance); + assert_ok!(pallet_torus0::Pallet::::add_stake( + get_origin(staker), + validator1, + stake_amount + )); + + assert_ok!(Permission0::delegate_wallet_stake_permission( + get_origin(staker), + recipient, + pallet_permission0::permission::wallet::WalletStake { + can_transfer_stake: false, + exclusive_stake_access: false, + }, + pallet_permission0::PermissionDuration::Indefinite, + pallet_permission0::RevocationTerms::RevocableByDelegator, + )); + + assert_ok!(pallet_torus0::Pallet::::remove_stake( + get_origin(staker), + validator1, + stake_amount / 2 + )); + + assert_eq!( + StakingTo::::get(staker, validator1), + Some(stake_amount / 2) + ); + + assert_ok!(pallet_torus0::Pallet::::transfer_stake( + get_origin(staker), + validator1, + validator2, + stake_amount / 4 + )); + + assert_eq!( + StakingTo::::get(staker, validator1), + Some(stake_amount / 4) + ); + assert_eq!( + StakingTo::::get(staker, validator2), + Some(stake_amount / 4) + ); + }); +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index e193376..bc3742f 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -37,7 +37,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("torus-runtime"), impl_name: create_runtime_str!("torus-runtime"), authoring_version: 1, - spec_version: 25, + spec_version: 26, impl_version: 1, apis: apis::RUNTIME_API_VERSIONS, transaction_version: 1, @@ -82,8 +82,7 @@ pub type SignedPayload = sp_runtime::generic::SignedPayload,); +type Migrations = (); /// Executive: handles dispatch to the various modules. pub type RuntimeExecutive = frame_executive::Executive<