Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .helix/languages.toml
Original file line number Diff line number Diff line change
@@ -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"]
23 changes: 23 additions & 0 deletions pallets/permission0/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,29 @@ pub trait Permission0NamespacesApi<AccountId, NamespacePath> {
fn is_delegating_namespace(delegator: &AccountId, path: &NamespacePath) -> bool;
}

pub struct WalletPermission<AccountId> {
pub recipient: AccountId,
pub r#type: WalletScopeType,
Copy link
Collaborator

@steinerkelvin steinerkelvin Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was converted by Polkadot.js to r_type, and in Serde, it would be just type. Even worse, Polkadot.js usually converts the fields to camel case, and in this case, there's this underscore _.

I think it's better for us to avoid using type or other keywords as identifiers if this is going to happen, to avoid inconsistencies in serialization/conversion at interfaces.

Alternatives: kind, variant, body.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed by: #148

}

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<AccountId> {
/// Lists all active wallet permissions, regardless of the type.
fn find_active_wallet_permission(
delegator: &AccountId,
) -> impl Iterator<Item = (PermissionId, WalletPermission<AccountId>)>;
}

polkadot_sdk::sp_api::decl_runtime_apis! {
/// A set of helper functions for permission and streams
/// queries.
Expand Down
3 changes: 3 additions & 0 deletions pallets/permission0/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Config> Permission0Api<OriginFor<T>> for pallet::Pallet<T> {
Expand Down Expand Up @@ -146,6 +147,7 @@ pub(crate) fn execute_permission_impl<T: Config>(
}
PermissionScope::Curator(_) => curator_impl::execute_permission_impl::<T>(permission_id),
PermissionScope::Namespace(_) => Ok(()),
PermissionScope::Wallet(_) => Ok(()),
}
}

Expand Down Expand Up @@ -208,6 +210,7 @@ pub fn enforcement_execute_permission_impl<T: Config>(
return curator_impl::execute_permission_impl::<T>(&permission_id);
}
PermissionScope::Namespace(_) => return Ok(()),
PermissionScope::Wallet(_) => return Ok(()),
}

EnforcementTracking::<T>::remove(permission_id, EnforcementReferendum::Execution);
Expand Down
164 changes: 164 additions & 0 deletions pallets/permission0/src/ext/wallet_impl.rs
Original file line number Diff line number Diff line change
@@ -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<T: Config> Permission0WalletApi<T::AccountId> for Pallet<T> {
fn find_active_wallet_permission(
delegator: &T::AccountId,
) -> impl Iterator<
Item = (
PermissionId,
pallet_permission0_api::WalletPermission<T::AccountId>,
),
> {
PermissionsByDelegator::<T>::get(delegator)
.into_iter()
.filter_map(|pid| {
let permission = Permissions::<T>::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<T: Config>(
origin: OriginFor<T>,
recipient: T::AccountId,
stake_details: WalletStake,
duration: PermissionDuration<T>,
revocation: RevocationTerms<T>,
) -> DispatchResult {
let delegator = ensure_signed(origin)?;
ensure!(delegator != recipient, Error::<T>::SelfPermissionNotAllowed);

for (_, perm) in Pallet::<T>::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::<T>::DuplicatePermission.into());
}
}

let scope = PermissionScope::Wallet(WalletScope {
recipient: recipient.clone(),
r#type: WalletScopeType::Stake(stake_details),
});
let permission_id = generate_permission_id::<T>(&delegator, &scope)?;

let contract = PermissionContract::<T>::new(
delegator,
scope,
duration,
revocation,
crate::EnforcementAuthority::None,
);

Permissions::<T>::insert(permission_id, &contract);
add_permission_indices::<T>(
&contract.delegator,
core::iter::once(&recipient),
permission_id,
)?;

<Pallet<T>>::deposit_event(Event::PermissionDelegated {
delegator: contract.delegator,
permission_id,
});

Ok(())
}

pub(crate) fn execute_wallet_stake_permission<T: Config>(
caller: OriginFor<T>,
permission_id: PermissionId,
op: WalletStakeOperation<T>,
) -> DispatchResult {
let caller = ensure_signed(caller)?;
let Some(permission) = Permissions::<T>::get(permission_id) else {
return Err(Error::<T>::PermissionNotFound.into());
};
let PermissionScope::Wallet(wallet) = &permission.scope else {
return Err(Error::<T>::UnsupportedPermissionType.into());
};
#[allow(irrefutable_let_patterns)]
let WalletScopeType::Stake(stake) = &wallet.r#type else {
return Err(Error::<T>::UnsupportedPermissionType.into());
};

ensure!(
caller == wallet.recipient,
Error::<T>::NotPermissionRecipient
);

let staker = &permission.delegator;

match op {
WalletStakeOperation::Unstake { staked, amount } => {
<T::Torus>::remove_stake(staker, &staked, amount)?;
}
WalletStakeOperation::Transfer { from, to, amount } => {
ensure!(stake.can_transfer_stake, Error::<T>::PermissionNotFound);
<T::Torus>::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<T: Config> {
/// Unstakes the balance from the staked account, yielding control of the
/// balance back to the delegator.
Unstake {
staked: T::AccountId,
amount: BalanceOf<T>,
},
/// 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<T>,
},
}
32 changes: 32 additions & 0 deletions pallets/permission0/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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<T>,
recipient: T::AccountId,
stake_details: WalletStake,
duration: PermissionDuration<T>,
revocation: RevocationTerms<T>,
) -> DispatchResult {
ext::wallet_impl::delegate_wallet_stake_permission::<T>(
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.
Expand Down Expand Up @@ -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<T>,
permission_id: PermissionId,
op: ext::wallet_impl::WalletStakeOperation<T>,
) -> DispatchResult {
ext::wallet_impl::execute_wallet_stake_permission(caller, permission_id, op)
}
}
}

Expand Down
Loading