templar_curator_primitives/policy/state/
mod.rs

1//! Policy state container for curator executors.
2//!
3//! This module defines a lightweight, chain-agnostic policy state that
4//! executors can persist alongside the vault kernel state.
5
6use alloc::vec::Vec;
7
8use templar_vault_kernel::TargetId;
9
10use super::cap_group::{CapGroupError, CapGroupId, CapGroupRecord};
11use super::market_lock::MarketLeaseRegistry;
12use super::supply_queue::{SupplyQueue, SupplyQueueError};
13
14#[templar_vault_macros::vault_derive(borsh, borsh_schema, serde, postcard, schemars)]
15#[derive(Clone, PartialEq, Eq)]
16pub struct OrderedMap<K, V> {
17    entries: Vec<(K, V)>,
18}
19
20impl<K, V> Default for OrderedMap<K, V> {
21    fn default() -> Self {
22        Self {
23            entries: Vec::new(),
24        }
25    }
26}
27
28impl<K: PartialEq, V> OrderedMap<K, V> {
29    #[must_use]
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    #[must_use]
35    pub fn len(&self) -> usize {
36        self.entries.len()
37    }
38
39    #[must_use]
40    pub fn is_empty(&self) -> bool {
41        self.entries.is_empty()
42    }
43
44    pub fn clear(&mut self) {
45        self.entries.clear();
46    }
47
48    pub fn insert(&mut self, key: K, value: V) -> Option<V> {
49        if let Some((_, existing)) = self
50            .entries
51            .iter_mut()
52            .find(|(candidate, _)| candidate == &key)
53        {
54            return Some(core::mem::replace(existing, value));
55        }
56        self.entries.push((key, value));
57        None
58    }
59
60    #[must_use]
61    pub fn get(&self, key: &K) -> Option<&V> {
62        self.entries
63            .iter()
64            .find(|(candidate, _)| candidate == key)
65            .map(|(_, value)| value)
66    }
67
68    #[must_use]
69    pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
70        self.entries
71            .iter_mut()
72            .find(|(candidate, _)| candidate == key)
73            .map(|(_, value)| value)
74    }
75
76    #[must_use]
77    pub fn contains_key(&self, key: &K) -> bool {
78        self.entries.iter().any(|(candidate, _)| candidate == key)
79    }
80
81    pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
82        self.entries.iter().map(|(key, value)| (key, value))
83    }
84
85    pub fn retain(&mut self, mut f: impl FnMut(&K, &V) -> bool) {
86        self.entries.retain(|(key, value)| f(key, value));
87    }
88
89    pub fn remove(&mut self, key: &K) -> Option<V> {
90        let index = self
91            .entries
92            .iter()
93            .position(|(candidate, _)| candidate == key)?;
94        Some(self.entries.remove(index).1)
95    }
96
97    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&K, &mut V)> {
98        self.entries.iter_mut().map(|(key, value)| (&*key, value))
99    }
100
101    pub fn keys(&self) -> impl Iterator<Item = &K> {
102        self.entries.iter().map(|(key, _)| key)
103    }
104
105    pub fn values(&self) -> impl Iterator<Item = &V> {
106        self.entries.iter().map(|(_, value)| value)
107    }
108}
109
110impl<K: PartialEq, V> FromIterator<(K, V)> for OrderedMap<K, V> {
111    fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
112        let mut map = Self::default();
113        for (key, value) in iter {
114            let _ = map.insert(key, value);
115        }
116        map
117    }
118}
119
120impl<K, V> IntoIterator for OrderedMap<K, V> {
121    type Item = (K, V);
122    type IntoIter = alloc::vec::IntoIter<(K, V)>;
123
124    fn into_iter(self) -> Self::IntoIter {
125        self.entries.into_iter()
126    }
127}
128
129#[templar_vault_macros::vault_derive(borsh, serde)]
130#[derive(Clone, PartialEq, Eq)]
131pub struct MarketConfig {
132    pub enabled: bool,
133    pub cap: u128,
134    pub cap_group_id: Option<CapGroupId>,
135}
136
137impl MarketConfig {
138    #[must_use]
139    pub fn new(enabled: bool, cap: u128, cap_group_id: Option<CapGroupId>) -> Self {
140        Self {
141            enabled,
142            cap,
143            cap_group_id,
144        }
145    }
146}
147
148impl Default for MarketConfig {
149    fn default() -> Self {
150        Self {
151            enabled: true,
152            cap: 0,
153            cap_group_id: None,
154        }
155    }
156}
157
158/// Curator policy state used by executors.
159#[templar_vault_macros::vault_derive(borsh, serde)]
160#[derive(Clone, Default)]
161pub struct PolicyState {
162    markets: OrderedMap<TargetId, MarketConfig>,
163    principals: OrderedMap<TargetId, u128>,
164    cap_groups: OrderedMap<CapGroupId, CapGroupRecord>,
165    supply_queue: SupplyQueue,
166    leases: MarketLeaseRegistry,
167}
168
169#[templar_vault_macros::vault_derive]
170#[derive(Clone, PartialEq, Eq)]
171pub enum PolicyStateError {
172    UnknownMarket { target_id: TargetId },
173    UnknownCapGroup { id: CapGroupId },
174    CapGroupInUse { id: CapGroupId },
175    PrincipalOverflow { target_id: TargetId },
176    InvalidSupplyQueue { source: SupplyQueueError },
177    SupplyQueueUnknownMarket { target_id: TargetId },
178    SupplyQueueDisabledMarket { target_id: TargetId },
179    SupplyQueueUnauthorizedMarket { target_id: TargetId },
180}
181
182impl PolicyState {
183    pub fn from_parts(
184        markets: OrderedMap<TargetId, MarketConfig>,
185        principals: OrderedMap<TargetId, u128>,
186        cap_groups: OrderedMap<CapGroupId, CapGroupRecord>,
187        leases: MarketLeaseRegistry,
188        supply_queue: SupplyQueue,
189    ) -> Result<Self, PolicyStateError> {
190        let mut state = Self {
191            markets,
192            principals,
193            cap_groups,
194            supply_queue,
195            leases,
196        };
197        state
198            .supply_queue
199            .validate()
200            .map_err(|source| PolicyStateError::InvalidSupplyQueue { source })?;
201        state.validate_supply_queue_targets()?;
202        state.prune_orphan_principals();
203        state.initialize_missing_principals();
204        state.recompute_cap_group_principals()?;
205        Ok(state)
206    }
207
208    #[must_use]
209    pub fn markets(&self) -> &OrderedMap<TargetId, MarketConfig> {
210        &self.markets
211    }
212
213    #[must_use]
214    pub fn principals(&self) -> &OrderedMap<TargetId, u128> {
215        &self.principals
216    }
217
218    #[must_use]
219    pub fn cap_groups(&self) -> &OrderedMap<CapGroupId, CapGroupRecord> {
220        &self.cap_groups
221    }
222
223    #[must_use]
224    pub fn supply_queue(&self) -> &SupplyQueue {
225        &self.supply_queue
226    }
227
228    #[must_use]
229    pub fn leases(&self) -> &MarketLeaseRegistry {
230        &self.leases
231    }
232
233    #[must_use]
234    pub fn market_config(&self, target_id: TargetId) -> Option<&MarketConfig> {
235        self.markets.get(&target_id)
236    }
237
238    #[must_use]
239    pub fn principal_entry(&self, target_id: TargetId) -> Option<u128> {
240        self.principals.get(&target_id).copied()
241    }
242
243    #[must_use]
244    pub fn cap_group(&self, cap_group_id: &CapGroupId) -> Option<&CapGroupRecord> {
245        self.cap_groups.get(cap_group_id)
246    }
247
248    pub fn replace_supply_queue(
249        &mut self,
250        supply_queue: SupplyQueue,
251    ) -> Result<(), PolicyStateError> {
252        supply_queue
253            .validate()
254            .map_err(|source| PolicyStateError::InvalidSupplyQueue { source })?;
255        self.validate_supply_queue_targets_with(&supply_queue)?;
256        self.supply_queue = supply_queue;
257        Ok(())
258    }
259
260    pub fn set_market_config(
261        &mut self,
262        target_id: TargetId,
263        config: MarketConfig,
264    ) -> Result<(), PolicyStateError> {
265        self.ensure_known_cap_group(config.cap_group_id.as_ref())?;
266        let should_purge_queue = !config.enabled || config.cap == 0;
267        self.markets.insert(target_id, config);
268        let _ = self
269            .principals
270            .insert(target_id, self.principal_entry(target_id).unwrap_or(0));
271        if should_purge_queue {
272            self.supply_queue.remove_target(target_id);
273        }
274        self.recompute_cap_group_principals()
275    }
276
277    pub fn set_market_cap(
278        &mut self,
279        target_id: TargetId,
280        cap: u128,
281    ) -> Result<(), PolicyStateError> {
282        let config = self
283            .markets
284            .get_mut(&target_id)
285            .ok_or(PolicyStateError::UnknownMarket { target_id })?;
286        config.cap = cap;
287        if cap == 0 {
288            self.supply_queue.remove_target(target_id);
289        }
290        Ok(())
291    }
292
293    pub fn set_market_enabled(
294        &mut self,
295        target_id: TargetId,
296        enabled: bool,
297    ) -> Result<(), PolicyStateError> {
298        let config = self
299            .markets
300            .get_mut(&target_id)
301            .ok_or(PolicyStateError::UnknownMarket { target_id })?;
302        config.enabled = enabled;
303        if !enabled {
304            self.supply_queue.remove_target(target_id);
305        }
306        Ok(())
307    }
308
309    pub fn set_market_cap_group(
310        &mut self,
311        target_id: TargetId,
312        cap_group_id: Option<CapGroupId>,
313    ) -> Result<(), PolicyStateError> {
314        self.ensure_known_cap_group(cap_group_id.as_ref())?;
315        let config = self
316            .markets
317            .get_mut(&target_id)
318            .ok_or(PolicyStateError::UnknownMarket { target_id })?;
319        config.cap_group_id = cap_group_id;
320        self.recompute_cap_group_principals()
321    }
322
323    pub fn set_principal(
324        &mut self,
325        target_id: TargetId,
326        principal: u128,
327    ) -> Result<(), PolicyStateError> {
328        if !self.markets.contains_key(&target_id) {
329            return Err(PolicyStateError::UnknownMarket { target_id });
330        }
331        let _ = self.principals.insert(target_id, principal);
332        self.recompute_cap_group_principals()
333    }
334
335    pub fn remove_market(
336        &mut self,
337        target_id: TargetId,
338    ) -> Result<Option<MarketConfig>, PolicyStateError> {
339        let removed = self.markets.remove(&target_id);
340        if removed.is_some() {
341            let _ = self.principals.remove(&target_id);
342            self.supply_queue.remove_target(target_id);
343            self.recompute_cap_group_principals()?;
344        }
345        Ok(removed)
346    }
347
348    pub fn remove_cap_group(
349        &mut self,
350        cap_group_id: &CapGroupId,
351    ) -> Result<Option<CapGroupRecord>, PolicyStateError> {
352        if self
353            .markets
354            .values()
355            .any(|config| config.cap_group_id.as_ref() == Some(cap_group_id))
356        {
357            return Err(PolicyStateError::CapGroupInUse {
358                id: cap_group_id.clone(),
359            });
360        }
361
362        Ok(self.cap_groups.remove(cap_group_id))
363    }
364
365    #[must_use]
366    pub fn principal_for(&self, target_id: TargetId) -> Option<u128> {
367        self.principal_entry(target_id)
368    }
369
370    /// Compute total external assets from all principals.
371    pub fn external_assets(&self) -> Result<u128, PolicyStateError> {
372        self.principals
373            .iter()
374            .try_fold(0u128, |acc, (target_id, principal)| {
375                acc.checked_add(*principal)
376                    .ok_or(PolicyStateError::PrincipalOverflow {
377                        target_id: *target_id,
378                    })
379            })
380    }
381
382    /// Compute principal totals per cap group.
383    ///
384    /// Aggregates principals for all markets assigned to each cap group.
385    pub fn compute_cap_group_totals(&self) -> Result<Vec<(CapGroupId, u128)>, PolicyStateError> {
386        let mut totals: Vec<(CapGroupId, u128)> = Vec::new();
387
388        for (target_id, config) in self.markets.iter() {
389            let group_id = match &config.cap_group_id {
390                Some(id) => id.clone(),
391                None => continue,
392            };
393            let principal = self.principal_entry(*target_id).unwrap_or(0);
394            if let Some((_, sum)) = totals
395                .iter_mut()
396                .find(|(existing_group_id, _)| *existing_group_id == group_id)
397            {
398                *sum = sum
399                    .checked_add(principal)
400                    .ok_or(PolicyStateError::PrincipalOverflow {
401                        target_id: *target_id,
402                    })?;
403            } else {
404                totals.push((group_id, principal));
405            }
406        }
407
408        Ok(totals)
409    }
410
411    pub fn ensure_cap_group(&mut self, cap_group_id: CapGroupId) {
412        if !self.cap_groups.contains_key(&cap_group_id) {
413            let _ = self
414                .cap_groups
415                .insert(cap_group_id, CapGroupRecord::default());
416        }
417    }
418
419    pub fn set_cap_group_absolute_cap(&mut self, cap_group_id: CapGroupId, new_cap: Option<u128>) {
420        self.ensure_cap_group(cap_group_id.clone());
421        if let Some(record) = self.cap_groups.get_mut(&cap_group_id) {
422            record.cap.set_absolute_cap(new_cap);
423        }
424    }
425
426    pub fn set_cap_group_relative_cap(
427        &mut self,
428        cap_group_id: CapGroupId,
429        new_relative_cap: Option<templar_vault_kernel::Wad>,
430    ) {
431        self.ensure_cap_group(cap_group_id.clone());
432        if let Some(record) = self.cap_groups.get_mut(&cap_group_id) {
433            record.cap.set_relative_cap(new_relative_cap);
434        }
435    }
436
437    pub fn recompute_cap_group_principals(&mut self) -> Result<(), PolicyStateError> {
438        self.validate_cap_group_memberships()?;
439        let totals = self.compute_cap_group_totals()?;
440
441        for (group_id, record) in self.cap_groups.iter_mut() {
442            let total = totals
443                .iter()
444                .find(|(candidate, _)| candidate == group_id)
445                .map(|(_, sum)| *sum)
446                .unwrap_or(0);
447            record.principal = total;
448        }
449
450        Ok(())
451    }
452
453    pub fn prune_unused_cap_groups(&mut self) {
454        self.cap_groups.retain(|group_id, _| {
455            self.markets
456                .values()
457                .any(|config| config.cap_group_id.as_ref() == Some(group_id))
458        });
459    }
460
461    pub fn prune_zero_principals(&mut self) {
462        self.principals
463            .retain(|target_id, principal| *principal != 0 && self.markets.contains_key(target_id));
464    }
465
466    fn initialize_missing_principals(&mut self) {
467        let market_ids: Vec<TargetId> = self.markets.keys().copied().collect();
468        for target_id in market_ids {
469            if !self.principals.contains_key(&target_id) {
470                let _ = self.principals.insert(target_id, 0);
471            }
472        }
473    }
474
475    fn validate_supply_queue_targets(&self) -> Result<(), PolicyStateError> {
476        self.validate_supply_queue_targets_with(&self.supply_queue)
477    }
478
479    fn validate_supply_queue_targets_with(
480        &self,
481        supply_queue: &SupplyQueue,
482    ) -> Result<(), PolicyStateError> {
483        for entry in supply_queue.entries() {
484            let target_id = entry.target_id;
485            let config = self
486                .market_config(target_id)
487                .ok_or(PolicyStateError::SupplyQueueUnknownMarket { target_id })?;
488
489            if !config.enabled {
490                return Err(PolicyStateError::SupplyQueueDisabledMarket { target_id });
491            }
492
493            if config.cap == 0 {
494                return Err(PolicyStateError::SupplyQueueUnauthorizedMarket { target_id });
495            }
496        }
497
498        Ok(())
499    }
500
501    fn prune_orphan_principals(&mut self) {
502        self.principals
503            .retain(|target_id, _| self.markets.contains_key(target_id));
504    }
505
506    fn ensure_known_cap_group(
507        &self,
508        cap_group_id: Option<&CapGroupId>,
509    ) -> Result<(), PolicyStateError> {
510        match cap_group_id {
511            Some(id) if !self.cap_groups.contains_key(id) => {
512                Err(PolicyStateError::UnknownCapGroup { id: id.clone() })
513            }
514            _ => Ok(()),
515        }
516    }
517
518    fn validate_cap_group_memberships(&self) -> Result<(), PolicyStateError> {
519        for (_, config) in self.markets.iter() {
520            self.ensure_known_cap_group(config.cap_group_id.as_ref())?;
521        }
522
523        Ok(())
524    }
525
526    #[must_use]
527    pub fn is_empty(&self) -> bool {
528        self.leases.is_empty()
529            && self.markets.is_empty()
530            && self.principals.is_empty()
531            && self.cap_groups.is_empty()
532            && self.supply_queue.is_empty()
533    }
534}
535
536impl From<PolicyStateError> for CapGroupError {
537    fn from(err: PolicyStateError) -> Self {
538        match err {
539            PolicyStateError::UnknownCapGroup { id } => Self::NotFound { id },
540            PolicyStateError::CapGroupInUse { id } => Self::InconsistentRecord { id },
541            PolicyStateError::PrincipalOverflow { target_id: _ }
542            | PolicyStateError::UnknownMarket { target_id: _ }
543            | PolicyStateError::InvalidSupplyQueue { source: _ }
544            | PolicyStateError::SupplyQueueUnknownMarket { target_id: _ }
545            | PolicyStateError::SupplyQueueDisabledMarket { target_id: _ }
546            | PolicyStateError::SupplyQueueUnauthorizedMarket { target_id: _ } => {
547                Self::InconsistentRecord {
548                    id: CapGroupId::policy_state_sentinel(),
549                }
550            }
551        }
552    }
553}