pallet_governance/
application.rs

1use codec::{Decode, Encode, MaxEncodedLen};
2use polkadot_sdk::{
3    frame_election_provider_support::Get,
4    frame_support::{dispatch::DispatchResult, traits::Currency, DebugNoBound},
5    polkadot_sdk_frame::prelude::BlockNumberFor,
6    sp_runtime::{traits::Saturating, BoundedVec},
7    sp_std::vec::Vec,
8};
9use scale_info::TypeInfo;
10
11use crate::{
12    frame::traits::ExistenceRequirement, whitelist, AccountIdOf, AgentApplications, BalanceOf,
13};
14
15/// Decentralized autonomous organization application, it's used to do agent
16/// operations on the network, like creating or removing, and needs to be
17/// approved by other peers.
18#[derive(DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen)]
19#[scale_info(skip_type_params(T))]
20pub struct AgentApplication<T: crate::Config> {
21    pub id: u32,
22    pub payer_key: AccountIdOf<T>,
23    pub agent_key: AccountIdOf<T>,
24    pub data: BoundedVec<u8, T::MaxApplicationDataLength>,
25    pub cost: BalanceOf<T>,
26    pub expires_at: BlockNumberFor<T>,
27    pub action: ApplicationAction,
28    pub status: ApplicationStatus,
29}
30
31/// Possible operations are adding or removing applications.
32#[derive(DebugNoBound, Decode, Encode, TypeInfo, MaxEncodedLen, PartialEq, Eq)]
33pub enum ApplicationAction {
34    Add,
35    Remove,
36}
37
38#[derive(DebugNoBound, Decode, Encode, TypeInfo, MaxEncodedLen, PartialEq, Eq)]
39pub enum ApplicationStatus {
40    Open,
41    /// The application was processed before expiration, can be either accepted
42    /// or rejected.
43    Resolved {
44        accepted: bool,
45    },
46    Expired,
47}
48
49impl<T: crate::Config> AgentApplication<T> {
50    /// Returns true if the application is in the Open state, i.e. not Expired
51    /// or Resolved, meaning it can be acted upon.
52    pub fn is_open(&self) -> bool {
53        matches!(self.status, ApplicationStatus::Open)
54    }
55}
56
57/// Creates a new agent application if the key is not yet whitelisted. It
58/// withdraws a fee from the payer account, which is given back if the
59/// application is accepted. The fee avoids actors spamming applications.
60pub fn submit_application<T: crate::Config>(
61    payer: AccountIdOf<T>,
62    agent_key: AccountIdOf<T>,
63    data: Vec<u8>,
64    removing: bool,
65) -> DispatchResult {
66    if !removing && whitelist::is_whitelisted::<T>(&agent_key) {
67        return Err(crate::Error::<T>::AlreadyWhitelisted.into());
68    } else if removing && !whitelist::is_whitelisted::<T>(&agent_key) {
69        return Err(crate::Error::<T>::NotWhitelisted.into());
70    }
71
72    let action = if removing {
73        ApplicationAction::Remove
74    } else {
75        ApplicationAction::Add
76    };
77
78    if exists_for_agent_key::<T>(&agent_key, &action) {
79        return Err(crate::Error::<T>::ApplicationKeyAlreadyUsed.into());
80    }
81
82    let config = crate::GlobalGovernanceConfig::<T>::get();
83    let cost = config.agent_application_cost;
84
85    <T as crate::Config>::Currency::transfer(
86        &payer,
87        &crate::DaoTreasuryAddress::<T>::get(),
88        cost,
89        ExistenceRequirement::AllowDeath,
90    )
91    .map_err(|_| crate::Error::<T>::NotEnoughBalanceToApply)?;
92
93    let data_len: u32 = data
94        .len()
95        .try_into()
96        .map_err(|_| crate::Error::<T>::InvalidApplicationDataLength)?;
97
98    let data_range = T::MinApplicationDataLength::get()..T::MaxApplicationDataLength::get();
99    if !data_range.contains(&data_len) {
100        return Err(crate::Error::<T>::InvalidApplicationDataLength.into());
101    }
102
103    let expires_at = <polkadot_sdk::frame_system::Pallet<T>>::block_number()
104        .saturating_add(config.agent_application_expiration);
105
106    let next_id = AgentApplications::<T>::iter()
107        .count()
108        .try_into()
109        .map_err(|_| crate::Error::<T>::InternalError)?;
110
111    let application = AgentApplication::<T> {
112        id: next_id,
113        payer_key: payer,
114        agent_key,
115        data: BoundedVec::truncate_from(data),
116        cost,
117        expires_at,
118        status: ApplicationStatus::Open,
119        action,
120    };
121
122    crate::AgentApplications::<T>::insert(next_id, application);
123    crate::Pallet::<T>::deposit_event(crate::Event::<T>::ApplicationCreated(next_id));
124
125    Ok(())
126}
127
128/// Accepts an agent application and executes it if it's still open, fails
129/// otherwise.
130pub fn accept_application<T: crate::Config>(application_id: u32) -> DispatchResult {
131    let application = crate::AgentApplications::<T>::get(application_id)
132        .ok_or(crate::Error::<T>::ApplicationNotFound)?;
133
134    if !application.is_open() {
135        return Err(crate::Error::<T>::ApplicationNotOpen.into());
136    }
137
138    match application.action {
139        ApplicationAction::Add => {
140            crate::Whitelist::<T>::insert(application.agent_key.clone(), ());
141            crate::Pallet::<T>::deposit_event(crate::Event::<T>::WhitelistAdded(
142                application.agent_key,
143            ));
144        }
145        ApplicationAction::Remove => {
146            crate::Whitelist::<T>::remove(&application.agent_key);
147            crate::Pallet::<T>::deposit_event(crate::Event::<T>::WhitelistRemoved(
148                application.agent_key,
149            ));
150        }
151    }
152
153    crate::AgentApplications::<T>::mutate(application_id, |application| {
154        if let Some(app) = application {
155            app.status = ApplicationStatus::Resolved { accepted: true };
156        }
157    });
158
159    // Give the application fee back to the payer key.
160    let _ = <T as crate::Config>::Currency::transfer(
161        &crate::DaoTreasuryAddress::<T>::get(),
162        &application.payer_key,
163        application.cost,
164        ExistenceRequirement::AllowDeath,
165    );
166
167    crate::Pallet::<T>::deposit_event(crate::Event::<T>::ApplicationAccepted(application.id));
168
169    Ok(())
170}
171
172/// Rejects an open application.
173pub fn deny_application<T: crate::Config>(application_id: u32) -> DispatchResult {
174    let application = crate::AgentApplications::<T>::get(application_id)
175        .ok_or(crate::Error::<T>::ApplicationNotFound)?;
176
177    if !application.is_open() {
178        return Err(crate::Error::<T>::ApplicationNotOpen.into());
179    }
180
181    crate::AgentApplications::<T>::mutate(application_id, |application| {
182        if let Some(app) = application {
183            app.status = ApplicationStatus::Resolved { accepted: false };
184        }
185    });
186
187    crate::Pallet::<T>::deposit_event(crate::Event::<T>::ApplicationDenied(application.id));
188
189    Ok(())
190}
191
192/// Iterates through all open applications checking if the current block is
193/// greater or equal to the former's expiration block. If so, marks the
194/// application as Expired.
195pub(crate) fn resolve_expired_applications<T: crate::Config>(current_block: BlockNumberFor<T>) {
196    for application in crate::AgentApplications::<T>::iter_values() {
197        if current_block < application.expires_at
198            || !matches!(application.status, ApplicationStatus::Open)
199        {
200            continue;
201        }
202
203        crate::AgentApplications::<T>::mutate(application.id, |application| {
204            if let Some(app) = application {
205                if matches!(app.status, ApplicationStatus::Open) {
206                    app.status = ApplicationStatus::Expired;
207                }
208            }
209        });
210
211        crate::Pallet::<T>::deposit_event(crate::Event::<T>::ApplicationExpired(application.id));
212    }
213}
214
215/// If any applications for this agent and action are already pending.
216pub(crate) fn exists_for_agent_key<T: crate::Config>(
217    key: &AccountIdOf<T>,
218    action: &ApplicationAction,
219) -> bool {
220    crate::AgentApplications::<T>::iter().any(|(_, application)| {
221        application.agent_key == *key
222            && application.status == ApplicationStatus::Open
223            && application.action == *action
224    })
225}