templar_curator_primitives/policy/market_lock/
mod.rs

1//! Market locks for preventing concurrent operations on the same market.
2
3use 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/// A lock on a specific market/target.
16#[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    /// Optional expiry timestamp (nanoseconds). `None` means no expiry.
25    #[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    /// Fluent method: set time-to-live from locked_at timestamp.
46    /// This computes `expires_at_ns = locked_at_ns + ttl_ns`.
47    #[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/// A set of market locks.
70#[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    /// Iterator over active (non-expired) locks.
83    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    /// Acquire a lock, returning an updated lock set or the existing lock on conflict.
126    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        // Remove any expired locks for this target
136        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    /// Release a lock held by a specific operation.
151    #[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    /// Release all locks held by a specific operation.
161    #[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    /// Clear all locks (emergency reset).
169    #[must_use]
170    pub fn clear(&self) -> Self {
171        Self::default()
172    }
173
174    /// Clean up expired locks.
175    #[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    /// Get all currently locked target IDs.
183    #[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    /// Check if any of the targets in a list are locked.
189    #[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}