pallet_torus0/
agent.rs

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