pallet_torus0/
agent.rs

1use codec::{Decode, Encode, MaxEncodedLen};
2use pallet_emission0_api::Emission0Api;
3use pallet_governance_api::GovernanceApi;
4use pallet_torus0_api::NamespacePath;
5use polkadot_sdk::{
6    frame_election_provider_support::Get,
7    frame_support::{
8        dispatch::DispatchResult,
9        ensure,
10        traits::{Currency, ExistenceRequirement},
11        DebugNoBound,
12    },
13    polkadot_sdk_frame::prelude::BlockNumberFor,
14    sp_runtime::{traits::Saturating, BoundedVec, DispatchError, Percent},
15    sp_tracing::{debug_span, warn},
16};
17use scale_info::{prelude::vec::Vec, TypeInfo};
18
19use crate::AccountIdOf;
20
21/// Agents are one of the primitives in the Torus ecosystem which are bounded
22/// to modules in off-chain environment. They can receive weights by the
23/// allocators.
24///
25/// Agent registration needs approval from a curator. Registration applications
26/// are submitter at dao.torus.network.
27#[derive(DebugNoBound, Encode, Decode, MaxEncodedLen, TypeInfo)]
28#[scale_info(skip_type_params(T))]
29pub struct Agent<T: crate::Config> {
30    /// The key that bounds the agent to the module
31    pub key: AccountIdOf<T>,
32    pub name: BoundedVec<u8, T::MaxAgentNameLengthConstraint>,
33    pub url: BoundedVec<u8, T::MaxAgentUrlLengthConstraint>,
34    pub metadata: BoundedVec<u8, T::MaxAgentMetadataLengthConstraint>,
35    /// Penalities acts on agent's incentives and dividends of users who set
36    /// weights on them.
37    pub weight_penalty_factor: Percent,
38    pub registration_block: BlockNumberFor<T>,
39    pub fees: crate::fee::ValidatorFee<T>,
40    pub last_update_block: BlockNumberFor<T>,
41}
42
43/// Register an agent to the given key, payed by the payer key.
44///
45/// If the network is full, this function will drop enough agents until there's
46/// at least one slot (see [`find_agent_to_prune`]). Fails if no agents were
47/// eligible for pruning.
48///
49/// Registration fee is stored as [`crate::Burn`].
50pub fn register<T: crate::Config>(
51    payer: AccountIdOf<T>,
52    agent_key: AccountIdOf<T>,
53    name: Vec<u8>,
54    url: Vec<u8>,
55    metadata: Vec<u8>,
56) -> DispatchResult {
57    let span = debug_span!("register", agent.key = ?agent_key);
58    let _guard = span.enter();
59
60    ensure!(
61        <T as crate::pallet::Config>::Governance::can_register_agent(&payer),
62        crate::pallet::Error::<T>::AgentsFrozen
63    );
64
65    ensure!(
66        !exists::<T>(&agent_key),
67        crate::Error::<T>::AgentAlreadyRegistered
68    );
69
70    ensure!(
71        crate::RegistrationsThisBlock::<T>::get() < crate::MaxRegistrationsPerBlock::<T>::get(),
72        crate::Error::<T>::TooManyAgentRegistrationsThisBlock
73    );
74
75    let burn_config = crate::BurnConfig::<T>::get();
76    ensure!(
77        crate::RegistrationsThisInterval::<T>::get() < burn_config.max_registrations_per_interval,
78        crate::Error::<T>::TooManyAgentRegistrationsThisInterval
79    );
80
81    let namespace_path = NamespacePath::new_agent_root(&name).map_err(|err| {
82        warn!("{agent_key:?} tried using invalid name: {err:?}");
83        crate::Error::<T>::InvalidNamespacePath
84    })?;
85
86    validate_agent_url::<T>(&url[..])?;
87    validate_agent_metadata::<T>(&metadata[..])?;
88
89    let burn = crate::Burn::<T>::get();
90
91    // Registration cost is sent to treasury
92    <T as crate::Config>::Currency::transfer(
93        &payer,
94        &<T as crate::Config>::Governance::dao_treasury_address(),
95        burn,
96        ExistenceRequirement::AllowDeath,
97    )
98    .map_err(|_| crate::Error::<T>::NotEnoughBalanceToRegisterAgent)?;
99
100    let registration_block = <polkadot_sdk::frame_system::Pallet<T>>::block_number();
101    crate::Agents::<T>::insert(
102        agent_key.clone(),
103        Agent {
104            key: agent_key.clone(),
105            name: BoundedVec::truncate_from(name),
106            url: BoundedVec::truncate_from(url),
107            metadata: BoundedVec::truncate_from(metadata),
108            weight_penalty_factor: Percent::from_percent(0),
109            registration_block,
110            fees: Default::default(),
111            last_update_block: registration_block,
112        },
113    );
114
115    crate::namespace::create_namespace::<T>(
116        crate::namespace::NamespaceOwnership::Account(agent_key.clone()),
117        namespace_path,
118    )?;
119
120    crate::RegistrationsThisBlock::<T>::mutate(|value| value.saturating_add(1));
121    crate::RegistrationsThisInterval::<T>::mutate(|value| value.saturating_add(1));
122
123    crate::Pallet::<T>::deposit_event(crate::Event::<T>::AgentRegistered(agent_key.clone()));
124
125    if let Some(allocator) = <T::Governance>::get_allocators().next() {
126        let _ = <T::Emission>::delegate_weight_control(&agent_key, &allocator);
127    } else {
128        polkadot_sdk::sp_tracing::warn!("no allocators available to delegate to for {agent_key:?}");
129    }
130
131    Ok(())
132}
133
134/// Unregister an agent key from the network, erasing all its data and removing
135/// stakers.
136pub fn unregister<T: crate::Config>(agent_key: AccountIdOf<T>) -> DispatchResult {
137    let span = debug_span!("unregister", agent.key = ?agent_key);
138    let _guard = span.enter();
139
140    let agent = crate::Agents::<T>::get(&agent_key).ok_or(crate::Error::<T>::AgentDoesNotExist)?;
141
142    let namespace_path = NamespacePath::new_agent_root(&agent.name)
143        .map_err(|_| crate::Error::<T>::InvalidNamespacePath)?;
144    crate::namespace::delete_namespace::<T>(
145        crate::namespace::NamespaceOwnership::Account(agent_key.clone()),
146        namespace_path,
147    )?;
148
149    crate::stake::clear_key::<T>(&agent_key)?;
150
151    crate::Agents::<T>::remove(&agent_key);
152
153    crate::Pallet::<T>::deposit_event(crate::Event::<T>::AgentUnregistered(agent_key));
154
155    Ok(())
156}
157
158/// Updates the metadata of an existing agent.
159pub fn update<T: crate::Config>(
160    agent_key: AccountIdOf<T>,
161    url: Vec<u8>,
162    metadata: Option<Vec<u8>>,
163    staking_fee: Option<Percent>,
164    weight_control_fee: Option<Percent>,
165) -> DispatchResult {
166    let span = debug_span!("update", agent.key = ?agent_key);
167    let _guard = span.enter();
168
169    crate::Agents::<T>::try_mutate(&agent_key, |agent| {
170        let Some(agent) = agent else {
171            return Err(crate::Error::<T>::AgentDoesNotExist.into());
172        };
173
174        if is_in_update_cooldown::<T>(&agent_key)? {
175            return Err(crate::Error::<T>::AgentUpdateOnCooldown.into());
176        }
177
178        validate_agent_url::<T>(&url[..])?;
179        agent.url = BoundedVec::truncate_from(url);
180
181        if let Some(metadata) = metadata {
182            validate_agent_metadata::<T>(&metadata[..])?;
183            agent.metadata = BoundedVec::truncate_from(metadata);
184        }
185
186        let constraints = crate::FeeConstraints::<T>::get();
187
188        if let Some(staking_fee) = staking_fee {
189            ensure!(
190                staking_fee >= constraints.min_staking_fee,
191                crate::Error::<T>::InvalidStakingFee
192            );
193
194            agent.fees.staking_fee = staking_fee;
195        }
196
197        if let Some(weight_control_fee) = weight_control_fee {
198            ensure!(
199                weight_control_fee >= constraints.min_weight_control_fee,
200                crate::Error::<T>::InvalidWeightControlFee
201            );
202
203            agent.fees.weight_control_fee = weight_control_fee;
204        }
205
206        Ok::<(), DispatchError>(())
207    })?;
208
209    set_in_cooldown::<T>(&agent_key)?;
210    crate::Pallet::<T>::deposit_event(crate::Event::<T>::AgentUpdated(agent_key));
211
212    Ok(())
213}
214
215fn is_in_update_cooldown<T: crate::Config>(key: &AccountIdOf<T>) -> Result<bool, DispatchError> {
216    let current_block = <polkadot_sdk::frame_system::Pallet<T>>::block_number();
217    let cooldown = crate::AgentUpdateCooldown::<T>::get();
218
219    let last_update = crate::Agents::<T>::get(key)
220        .ok_or(crate::Error::<T>::AgentDoesNotExist)?
221        .last_update_block;
222
223    Ok(last_update.saturating_add(cooldown) > current_block)
224}
225
226fn set_in_cooldown<T: crate::Config>(key: &AccountIdOf<T>) -> DispatchResult {
227    crate::Agents::<T>::mutate(key, |agent| {
228        let Some(agent) = agent else {
229            return Err(crate::Error::<T>::AgentDoesNotExist.into());
230        };
231
232        agent.last_update_block = <polkadot_sdk::frame_system::Pallet<T>>::block_number();
233
234        Ok(())
235    })
236}
237
238pub fn exists<T: crate::Config>(key: &AccountIdOf<T>) -> bool {
239    crate::Agents::<T>::contains_key(key)
240}
241
242fn validate_agent_url<T: crate::Config>(bytes: &[u8]) -> DispatchResult {
243    let len: u32 = bytes
244        .len()
245        .try_into()
246        .map_err(|_| crate::Error::<T>::AgentUrlTooLong)?;
247
248    ensure!(len > 0, crate::Error::<T>::AgentUrlTooShort);
249
250    ensure!(
251        len <= (crate::MaxAgentUrlLength::<T>::get() as u32)
252            .min(T::MaxAgentUrlLengthConstraint::get()),
253        crate::Error::<T>::AgentUrlTooLong
254    );
255
256    ensure!(
257        core::str::from_utf8(bytes).is_ok(),
258        crate::Error::<T>::InvalidAgentUrl
259    );
260
261    Ok(())
262}
263
264fn validate_agent_metadata<T: crate::Config>(bytes: &[u8]) -> DispatchResult {
265    let len: u32 = bytes
266        .len()
267        .try_into()
268        .map_err(|_| crate::Error::<T>::AgentMetadataTooLong)?;
269
270    ensure!(len > 0, crate::Error::<T>::AgentMetadataTooShort);
271
272    ensure!(
273        len <= T::MaxAgentMetadataLengthConstraint::get(),
274        crate::Error::<T>::AgentMetadataTooLong
275    );
276
277    ensure!(
278        core::str::from_utf8(bytes).is_ok(),
279        crate::Error::<T>::InvalidAgentMetadata
280    );
281
282    Ok(())
283}
284
285#[doc(hidden)]
286pub enum PruningStrategy {
287    /// Finds the agent producing least dividends and incentives to
288    /// the network that is older than the current immunity period.
289    LeastProductive,
290    /// Like [`PruningStrategy::LeastProductive`] but ignoring the immunity
291    /// period.
292    IgnoreImmunity,
293}