Skip to content

Commit b3696b1

Browse files
feat(permission0): new wallet scope (#140)
Introduces a new permission type for controlling wallet stake. Closes CHAIN-129.
2 parents 8ccfed1 + 73d4ca8 commit b3696b1

File tree

14 files changed

+1002
-479
lines changed

14 files changed

+1002
-479
lines changed

.helix/languages.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[language-server.rust-analyzer.config]
22
cargo.extraEnv = { SKIP_WASM_BUILD = "true" }
33
cargo.features = ["runtime-benchmarks"]
4-
4+
check.extraEnv = { SKIP_WASM_BUILD = "true" }
5+
check.overrideCommand = ["cargo", "check", "--message-format=json"]

pallets/permission0/api/src/lib.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,29 @@ pub trait Permission0NamespacesApi<AccountId, NamespacePath> {
161161
fn is_delegating_namespace(delegator: &AccountId, path: &NamespacePath) -> bool;
162162
}
163163

164+
pub struct WalletPermission<AccountId> {
165+
pub recipient: AccountId,
166+
pub r#type: WalletScopeType,
167+
}
168+
169+
pub enum WalletScopeType {
170+
Stake {
171+
/// If true, allows the recipient to perform transfer of stake between staked accounts.
172+
can_transfer_stake: bool,
173+
/// If true, this permission holds exclusive access to the delegator stake, meaning that
174+
/// the delegator has no right to perform operations over stake (including unstaking)
175+
/// while this permission is active.
176+
exclusive_stake_access: bool,
177+
},
178+
}
179+
180+
pub trait Permission0WalletApi<AccountId> {
181+
/// Lists all active wallet permissions, regardless of the type.
182+
fn find_active_wallet_permission(
183+
delegator: &AccountId,
184+
) -> impl Iterator<Item = (PermissionId, WalletPermission<AccountId>)>;
185+
}
186+
164187
polkadot_sdk::sp_api::decl_runtime_apis! {
165188
/// A set of helper functions for permission and streams
166189
/// queries.

pallets/permission0/src/ext.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use polkadot_sdk::{
1717
pub mod curator_impl;
1818
pub mod namespace_impl;
1919
pub mod stream_impl;
20+
pub mod wallet_impl;
2021

2122
/// Implementation of the Permission0Api trait to be used externally
2223
impl<T: Config> Permission0Api<OriginFor<T>> for pallet::Pallet<T> {
@@ -146,6 +147,7 @@ pub(crate) fn execute_permission_impl<T: Config>(
146147
}
147148
PermissionScope::Curator(_) => curator_impl::execute_permission_impl::<T>(permission_id),
148149
PermissionScope::Namespace(_) => Ok(()),
150+
PermissionScope::Wallet(_) => Ok(()),
149151
}
150152
}
151153

@@ -208,6 +210,7 @@ pub fn enforcement_execute_permission_impl<T: Config>(
208210
return curator_impl::execute_permission_impl::<T>(&permission_id);
209211
}
210212
PermissionScope::Namespace(_) => return Ok(()),
213+
PermissionScope::Wallet(_) => return Ok(()),
211214
}
212215

213216
EnforcementTracking::<T>::remove(permission_id, EnforcementReferendum::Execution);
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
use codec::{Decode, Encode, MaxEncodedLen};
2+
use pallet_permission0_api::Permission0WalletApi;
3+
use pallet_torus0_api::Torus0Api;
4+
use polkadot_sdk::{
5+
frame_support::{
6+
CloneNoBound, DebugNoBound, EqNoBound, PartialEqNoBound, dispatch::DispatchResult, ensure,
7+
},
8+
frame_system::ensure_signed,
9+
polkadot_sdk_frame::prelude::OriginFor,
10+
};
11+
use scale_info::TypeInfo;
12+
13+
use crate::{
14+
BalanceOf, Config, Error, Event, Pallet, PermissionContract, PermissionDuration, PermissionId,
15+
PermissionScope, Permissions, PermissionsByDelegator, RevocationTerms, generate_permission_id,
16+
permission::{
17+
add_permission_indices,
18+
wallet::{WalletScope, WalletScopeType, WalletStake},
19+
},
20+
};
21+
22+
impl<T: Config> Permission0WalletApi<T::AccountId> for Pallet<T> {
23+
fn find_active_wallet_permission(
24+
delegator: &T::AccountId,
25+
) -> impl Iterator<
26+
Item = (
27+
PermissionId,
28+
pallet_permission0_api::WalletPermission<T::AccountId>,
29+
),
30+
> {
31+
PermissionsByDelegator::<T>::get(delegator)
32+
.into_iter()
33+
.filter_map(|pid| {
34+
let permission = Permissions::<T>::get(pid)?;
35+
let PermissionScope::Wallet(wallet) = permission.scope else {
36+
return None;
37+
};
38+
39+
Some((
40+
pid,
41+
pallet_permission0_api::WalletPermission {
42+
recipient: wallet.recipient,
43+
r#type: match wallet.r#type {
44+
WalletScopeType::Stake(stake) => {
45+
pallet_permission0_api::WalletScopeType::Stake {
46+
can_transfer_stake: stake.can_transfer_stake,
47+
exclusive_stake_access: stake.exclusive_stake_access,
48+
}
49+
}
50+
},
51+
},
52+
))
53+
})
54+
}
55+
}
56+
pub(crate) fn delegate_wallet_stake_permission<T: Config>(
57+
origin: OriginFor<T>,
58+
recipient: T::AccountId,
59+
stake_details: WalletStake,
60+
duration: PermissionDuration<T>,
61+
revocation: RevocationTerms<T>,
62+
) -> DispatchResult {
63+
let delegator = ensure_signed(origin)?;
64+
ensure!(delegator != recipient, Error::<T>::SelfPermissionNotAllowed);
65+
66+
for (_, perm) in Pallet::<T>::find_active_wallet_permission(&delegator) {
67+
if stake_details.exclusive_stake_access
68+
|| matches!(
69+
perm.r#type,
70+
pallet_permission0_api::WalletScopeType::Stake {
71+
exclusive_stake_access: true,
72+
..
73+
}
74+
)
75+
{
76+
return Err(Error::<T>::DuplicatePermission.into());
77+
}
78+
}
79+
80+
let scope = PermissionScope::Wallet(WalletScope {
81+
recipient: recipient.clone(),
82+
r#type: WalletScopeType::Stake(stake_details),
83+
});
84+
let permission_id = generate_permission_id::<T>(&delegator, &scope)?;
85+
86+
let contract = PermissionContract::<T>::new(
87+
delegator,
88+
scope,
89+
duration,
90+
revocation,
91+
crate::EnforcementAuthority::None,
92+
);
93+
94+
Permissions::<T>::insert(permission_id, &contract);
95+
add_permission_indices::<T>(
96+
&contract.delegator,
97+
core::iter::once(&recipient),
98+
permission_id,
99+
)?;
100+
101+
<Pallet<T>>::deposit_event(Event::PermissionDelegated {
102+
delegator: contract.delegator,
103+
permission_id,
104+
});
105+
106+
Ok(())
107+
}
108+
109+
pub(crate) fn execute_wallet_stake_permission<T: Config>(
110+
caller: OriginFor<T>,
111+
permission_id: PermissionId,
112+
op: WalletStakeOperation<T>,
113+
) -> DispatchResult {
114+
let caller = ensure_signed(caller)?;
115+
let Some(permission) = Permissions::<T>::get(permission_id) else {
116+
return Err(Error::<T>::PermissionNotFound.into());
117+
};
118+
let PermissionScope::Wallet(wallet) = &permission.scope else {
119+
return Err(Error::<T>::UnsupportedPermissionType.into());
120+
};
121+
#[allow(irrefutable_let_patterns)]
122+
let WalletScopeType::Stake(stake) = &wallet.r#type else {
123+
return Err(Error::<T>::UnsupportedPermissionType.into());
124+
};
125+
126+
ensure!(
127+
caller == wallet.recipient,
128+
Error::<T>::NotPermissionRecipient
129+
);
130+
131+
let staker = &permission.delegator;
132+
133+
match op {
134+
WalletStakeOperation::Unstake { staked, amount } => {
135+
<T::Torus>::remove_stake(staker, &staked, amount)?;
136+
}
137+
WalletStakeOperation::Transfer { from, to, amount } => {
138+
ensure!(stake.can_transfer_stake, Error::<T>::PermissionNotFound);
139+
<T::Torus>::transfer_stake(staker, &from, &to, amount)?;
140+
}
141+
}
142+
143+
Ok(())
144+
}
145+
146+
#[derive(
147+
CloneNoBound, DebugNoBound, Encode, Decode, MaxEncodedLen, TypeInfo, PartialEqNoBound, EqNoBound,
148+
)]
149+
#[scale_info(skip_type_params(T))]
150+
pub enum WalletStakeOperation<T: Config> {
151+
/// Unstakes the balance from the staked account, yielding control of the
152+
/// balance back to the delegator.
153+
Unstake {
154+
staked: T::AccountId,
155+
amount: BalanceOf<T>,
156+
},
157+
/// Transfers stake from one staked agent to another staked agent,
158+
/// related to the `transfer_stake` extrinsic in Torus0.
159+
Transfer {
160+
from: T::AccountId,
161+
to: T::AccountId,
162+
amount: BalanceOf<T>,
163+
},
164+
}

pallets/permission0/src/lib.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use polkadot_sdk::{
3434
#[frame::pallet]
3535
pub mod pallet {
3636
use pallet_torus0_api::NamespacePathInner;
37+
use permission::wallet::WalletStake;
3738
use polkadot_sdk::{frame_support::PalletId, sp_core::TryCollect};
3839

3940
use super::*;
@@ -491,6 +492,27 @@ pub mod pallet {
491492
Ok(())
492493
}
493494

495+
/// Delegate a permission over namespaces
496+
#[pallet::call_index(11)]
497+
#[pallet::weight(T::WeightInfo::delegate_namespace_permission())]
498+
pub fn delegate_wallet_stake_permission(
499+
origin: OriginFor<T>,
500+
recipient: T::AccountId,
501+
stake_details: WalletStake,
502+
duration: PermissionDuration<T>,
503+
revocation: RevocationTerms<T>,
504+
) -> DispatchResult {
505+
ext::wallet_impl::delegate_wallet_stake_permission::<T>(
506+
origin,
507+
recipient,
508+
stake_details,
509+
duration,
510+
revocation,
511+
)?;
512+
513+
Ok(())
514+
}
515+
494516
/// Delegate a permission over namespaces to multiple recipients.
495517
/// Note: this extrinsic creates _multiple_ permissions with the same
496518
/// properties.
@@ -568,6 +590,16 @@ pub mod pallet {
568590

569591
Ok(())
570592
}
593+
594+
#[pallet::call_index(12)]
595+
#[pallet::weight(T::WeightInfo::update_namespace_permission())]
596+
pub fn execute_wallet_stake_permission(
597+
caller: OriginFor<T>,
598+
permission_id: PermissionId,
599+
op: ext::wallet_impl::WalletStakeOperation<T>,
600+
) -> DispatchResult {
601+
ext::wallet_impl::execute_wallet_stake_permission(caller, permission_id, op)
602+
}
571603
}
572604
}
573605

0 commit comments

Comments
 (0)