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::{CapGroupId, CapGroupRecord};
11use super::market_lock::MarketLockSet;
12use super::supply_queue::SupplyQueue;
13
14#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
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 iter_mut(&mut self) -> impl Iterator<Item = (&K, &mut V)> {
86        self.entries.iter_mut().map(|(key, value)| (&*key, value))
87    }
88
89    pub fn keys(&self) -> impl Iterator<Item = &K> {
90        self.entries.iter().map(|(key, _)| key)
91    }
92
93    pub fn values(&self) -> impl Iterator<Item = &V> {
94        self.entries.iter().map(|(_, value)| value)
95    }
96}
97
98impl<K: PartialEq, V> FromIterator<(K, V)> for OrderedMap<K, V> {
99    fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
100        let mut map = Self::default();
101        for (key, value) in iter {
102            let _ = map.insert(key, value);
103        }
104        map
105    }
106}
107
108impl<K, V> IntoIterator for OrderedMap<K, V> {
109    type Item = (K, V);
110    type IntoIter = alloc::vec::IntoIter<(K, V)>;
111
112    fn into_iter(self) -> Self::IntoIter {
113        self.entries.into_iter()
114    }
115}
116
117#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
118#[derive(Clone, PartialEq, Eq)]
119pub struct MarketConfig {
120    pub enabled: bool,
121    pub cap: u128,
122    pub cap_group_id: Option<CapGroupId>,
123}
124
125impl MarketConfig {
126    pub fn new(enabled: bool, cap_group_id: Option<CapGroupId>) -> Self {
127        Self {
128            enabled,
129            cap: 0,
130            cap_group_id,
131        }
132    }
133}
134
135impl Default for MarketConfig {
136    fn default() -> Self {
137        Self {
138            enabled: true,
139            cap: 0,
140            cap_group_id: None,
141        }
142    }
143}
144
145/// Curator policy state used by executors.
146#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
147#[derive(Clone, Default)]
148pub struct PolicyState {
149    pub markets: OrderedMap<TargetId, MarketConfig>,
150    pub principals: OrderedMap<TargetId, u128>,
151    pub cap_groups: OrderedMap<CapGroupId, CapGroupRecord>,
152    pub supply_queue: SupplyQueue,
153    pub locks: MarketLockSet,
154}
155
156impl PolicyState {
157    pub fn set_market_config(&mut self, target_id: TargetId, config: MarketConfig) {
158        self.markets.insert(target_id, config);
159        // Refresh cap group principals to keep them in sync with market config changes
160        self.refresh_cap_group_principals();
161    }
162
163    pub fn set_principal(&mut self, target_id: TargetId, principal: u128) {
164        self.principals.insert(target_id, principal);
165        // Refresh cap group principals to keep them in sync with principal changes
166        self.refresh_cap_group_principals();
167    }
168
169    /// Return the principal for a market (0 if missing).
170    pub fn principal_for(&self, target_id: TargetId) -> u128 {
171        self.principals.get(&target_id).copied().unwrap_or(0)
172    }
173
174    /// Compute total external assets from all principals.
175    #[must_use]
176    pub fn external_assets(&self) -> u128 {
177        self.principals
178            .values()
179            .fold(0u128, |acc, p| acc.saturating_add(*p))
180    }
181
182    /// Compute principal totals per cap group.
183    ///
184    /// Aggregates principals for all markets assigned to each cap group.
185    #[must_use]
186    pub fn compute_cap_group_totals(&self) -> Vec<(CapGroupId, u128)> {
187        let mut totals: Vec<(CapGroupId, u128)> = Vec::new();
188
189        for (target_id, config) in self.markets.iter() {
190            let group_id = match &config.cap_group_id {
191                Some(id) => id.clone(),
192                None => continue,
193            };
194            let principal = self.principal_for(*target_id);
195            if let Some((_, sum)) = totals
196                .iter_mut()
197                .find(|(existing_group_id, _)| *existing_group_id == group_id)
198            {
199                *sum = sum.saturating_add(principal);
200            } else {
201                totals.push((group_id, principal));
202            }
203        }
204
205        totals
206    }
207
208    /// Recompute and update cap group principals in-place.
209    ///
210    /// Note: This also creates missing cap group entries (with default caps)
211    /// for any cap groups referenced by markets but not yet in `cap_groups`.
212    /// This prevents silently dropping principals for unknown groups.
213    pub fn refresh_cap_group_principals(&mut self) {
214        let totals = self.compute_cap_group_totals();
215
216        // First, ensure all referenced cap groups exist (create missing ones with defaults)
217        for (group_id, principal) in &totals {
218            if !self.cap_groups.contains_key(group_id) {
219                // Create a default cap group record for the missing group
220                let record = CapGroupRecord {
221                    principal: *principal,
222                    ..Default::default()
223                };
224                self.cap_groups.insert(group_id.clone(), record);
225            }
226        }
227
228        // Now update all cap group principals
229        for (group_id, record) in self.cap_groups.iter_mut() {
230            let total = totals
231                .iter()
232                .find(|(candidate, _)| candidate == group_id)
233                .map(|(_, sum)| *sum)
234                .unwrap_or(0);
235            record.principal = total;
236        }
237    }
238}