templar_curator_primitives/policy/market_lock/
mod.rs1use alloc::vec::Vec;
4use templar_vault_kernel::{TargetId, TimeGate, TimestampNs};
5use typed_builder::TypedBuilder;
6
7pub fn validate_lock_expiry(current_ns: u64, expiry_ns: u64, max_duration_ns: u64) -> bool {
8 let max_expiry_ns =
9 TimeGate::schedule_from(TimestampNs(current_ns), TimestampNs(max_duration_ns))
10 .ready_at_ns()
11 .unwrap_or(TimestampNs(current_ns));
12 expiry_ns > current_ns && expiry_ns <= u64::from(max_expiry_ns)
13}
14
15#[templar_vault_macros::vault_derive(borsh, postcard, schemars, serde, std_borsh_schema)]
17#[derive(Clone, PartialEq, Eq, TypedBuilder)]
18#[builder(field_defaults(setter(into)))]
19pub struct MarketLock {
20 pub target_id: TargetId,
21 #[builder(default, setter(strip_option))]
22 pub op_id: Option<u64>,
23 pub locked_at_ns: u64,
24 #[builder(default, setter(strip_option))]
26 pub expires_at_ns: Option<u64>,
27}
28
29impl MarketLock {
30 fn expiry_gate(&self) -> Option<TimeGate> {
31 self.expires_at_ns
32 .map(|expiry_ns| TimeGate::from_ready_at(TimestampNs(expiry_ns)))
33 }
34
35 #[must_use]
36 pub fn new(target_id: TargetId, locked_at_ns: u64) -> Self {
37 Self {
38 target_id,
39 op_id: None,
40 locked_at_ns,
41 expires_at_ns: None,
42 }
43 }
44
45 #[must_use]
48 pub fn with_ttl(mut self, ttl_ns: u64) -> Self {
49 self.expires_at_ns =
50 TimeGate::schedule_from(TimestampNs(self.locked_at_ns), TimestampNs(ttl_ns))
51 .ready_at_ns()
52 .map(Into::into);
53 self
54 }
55
56 #[must_use]
57 pub fn is_expired(&self, current_ns: u64) -> bool {
58 self.expiry_gate()
59 .is_some_and(|gate| gate.is_ready(TimestampNs(current_ns)))
60 }
61
62 #[must_use]
63 pub fn remaining(&self, current_ns: u64) -> Option<u64> {
64 self.expiry_gate()
65 .map(|gate| u64::from(gate.remaining(TimestampNs(current_ns))))
66 }
67}
68
69#[templar_vault_macros::vault_derive(borsh, postcard, schemars, serde, std_borsh_schema)]
71#[derive(Clone, Default)]
72pub struct MarketLockSet {
73 pub locks: Vec<MarketLock>,
74}
75
76impl MarketLockSet {
77 #[must_use]
78 pub fn new() -> Self {
79 Self::default()
80 }
81
82 fn active_iter(&self, current_ns: u64) -> impl Iterator<Item = &MarketLock> + '_ {
84 self.locks.iter().filter(move |l| !l.is_expired(current_ns))
85 }
86
87 #[must_use]
88 pub fn is_empty(&self) -> bool {
89 self.locks.is_empty()
90 }
91
92 #[must_use]
93 pub fn len(&self) -> usize {
94 self.locks.len()
95 }
96
97 #[must_use]
98 pub fn active_count(&self, current_ns: u64) -> usize {
99 self.active_iter(current_ns).count()
100 }
101
102 #[must_use]
103 pub fn is_all_expired(&self, current_ns: u64) -> bool {
104 self.active_count(current_ns) == 0
105 }
106
107 #[must_use]
108 pub fn is_locked(&self, target_id: TargetId, current_ns: u64) -> bool {
109 self.active_iter(current_ns)
110 .any(|lock| lock.target_id == target_id)
111 }
112
113 #[must_use]
114 pub fn is_locked_by_op(&self, target_id: TargetId, op_id: u64, current_ns: u64) -> bool {
115 self.active_iter(current_ns)
116 .any(|lock| lock.target_id == target_id && lock.op_id == Some(op_id))
117 }
118
119 #[must_use]
120 pub fn get_lock(&self, target_id: TargetId, current_ns: u64) -> Option<&MarketLock> {
121 self.active_iter(current_ns)
122 .find(|l| l.target_id == target_id)
123 }
124
125 pub fn acquire(&self, lock: MarketLock, current_ns: u64) -> Result<Self, MarketLock> {
127 if let Some(existing) = self
128 .active_iter(current_ns)
129 .find(|l| l.target_id == lock.target_id)
130 {
131 return Err(existing.clone());
132 }
133
134 let mut new_set = self.clone();
135 new_set
137 .locks
138 .retain(|l| l.target_id != lock.target_id || !l.is_expired(current_ns));
139 new_set.locks.push(lock);
140 Ok(new_set)
141 }
142
143 #[must_use]
144 pub fn release(&self, target_id: TargetId) -> Self {
145 let mut new_set = self.clone();
146 new_set.locks.retain(|l| l.target_id != target_id);
147 new_set
148 }
149
150 #[must_use]
152 pub fn release_by_op(&self, target_id: TargetId, op_id: u64) -> Self {
153 let mut new_set = self.clone();
154 new_set
155 .locks
156 .retain(|l| l.target_id != target_id || l.op_id != Some(op_id));
157 new_set
158 }
159
160 #[must_use]
162 pub fn release_all_by_op(&self, op_id: u64) -> Self {
163 let mut new_set = self.clone();
164 new_set.locks.retain(|l| l.op_id != Some(op_id));
165 new_set
166 }
167
168 #[must_use]
170 pub fn clear(&self) -> Self {
171 Self::default()
172 }
173
174 #[must_use]
176 pub fn cleanup_expired(&self, current_ns: u64) -> Self {
177 let mut new_set = self.clone();
178 new_set.locks.retain(|l| !l.is_expired(current_ns));
179 new_set
180 }
181
182 #[must_use]
184 pub fn locked_targets(&self, current_ns: u64) -> Vec<TargetId> {
185 self.active_iter(current_ns).map(|l| l.target_id).collect()
186 }
187
188 #[must_use]
190 pub fn find_locked_targets(&self, targets: &[TargetId], current_ns: u64) -> Vec<TargetId> {
191 targets
192 .iter()
193 .copied()
194 .filter(|t| self.is_locked(*t, current_ns))
195 .collect()
196 }
197}
198
199impl From<Vec<MarketLock>> for MarketLockSet {
200 fn from(locks: Vec<MarketLock>) -> Self {
201 Self { locks }
202 }
203}