templar_curator_primitives/policy/withdraw_route/
mod.rs1use alloc::vec::Vec;
4use templar_vault_kernel::TargetId;
5use typed_builder::TypedBuilder;
6
7use super::target_set::find_first_duplicate;
8
9#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
11#[derive(Clone, PartialEq, Eq, TypedBuilder)]
12#[builder(field_defaults(setter(into)))]
13pub struct WithdrawRouteEntry {
14 pub target_id: TargetId,
15 pub max_amount: u128,
16 #[builder(default)]
17 pub available_liquidity: Option<u128>,
18}
19
20impl WithdrawRouteEntry {
21 #[must_use]
22 pub fn new(target_id: TargetId, max_amount: u128) -> Self {
23 Self {
24 target_id,
25 max_amount,
26 available_liquidity: None,
27 }
28 }
29
30 #[must_use]
31 pub fn with_liquidity(mut self, available_liquidity: u128) -> Self {
32 self.available_liquidity = Some(available_liquidity);
33 self
34 }
35}
36
37impl From<(TargetId, u128)> for WithdrawRouteEntry {
38 fn from(value: (TargetId, u128)) -> Self {
39 Self::new(value.0, value.1)
40 }
41}
42
43#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
45#[derive(Clone, Default)]
46pub struct WithdrawRoute {
47 pub entries: Vec<WithdrawRouteEntry>,
48 pub target_amount: u128,
49}
50
51impl WithdrawRoute {
52 #[must_use]
53 pub fn from_entries(entries: Vec<WithdrawRouteEntry>, target_amount: u128) -> Self {
54 Self {
55 entries,
56 target_amount,
57 }
58 }
59
60 #[must_use]
61 pub fn is_empty(&self) -> bool {
62 self.entries.is_empty()
63 }
64
65 #[must_use]
66 pub fn len(&self) -> usize {
67 self.entries.len()
68 }
69
70 #[must_use]
71 pub fn total(&self) -> u128 {
72 self.entries
73 .iter()
74 .fold(0u128, |acc, e| acc.checked_add(e.max_amount).unwrap())
75 }
76
77 #[must_use]
78 pub fn available_liquidity(&self) -> u128 {
79 self.entries
80 .iter()
81 .filter_map(|e| e.available_liquidity)
82 .fold(0u128, |acc, l| acc.checked_add(l).unwrap())
83 }
84
85 #[must_use]
86 pub fn can_satisfy(&self) -> bool {
87 self.total() >= self.target_amount
88 }
89
90 pub fn validate(&self) -> Result<(), WithdrawRouteError> {
99 if self.target_amount == 0 {
100 return Err(WithdrawRouteError::ZeroTargetAmount);
101 }
102
103 if self.is_empty() {
104 return Err(WithdrawRouteError::EmptyRoute);
105 }
106
107 for entry in &self.entries {
109 if entry.max_amount == 0 {
110 return Err(WithdrawRouteError::ZeroMaxAmount {
111 target_id: entry.target_id,
112 });
113 }
114 }
115
116 let targets: Vec<TargetId> = self.entries.iter().map(|e| e.target_id).collect();
118 if let Some(target_id) = find_first_duplicate(&targets) {
119 return Err(WithdrawRouteError::DuplicateTarget { target_id });
120 }
121
122 if !self.can_satisfy() {
124 return Err(WithdrawRouteError::InsufficientRouteTotal {
125 route_total: self.total(),
126 target_amount: self.target_amount,
127 });
128 }
129
130 Ok(())
131 }
132
133 #[must_use]
137 pub fn to_withdrawal_plan(&self) -> Vec<(TargetId, u128)> {
138 self.entries
139 .iter()
140 .map(|e| (e.target_id, e.max_amount))
141 .collect()
142 }
143
144 #[must_use]
146 pub fn get_entry(&self, target_id: TargetId) -> Option<&WithdrawRouteEntry> {
147 self.entries.iter().find(|e| e.target_id == target_id)
148 }
149
150 #[must_use]
152 pub fn has_target(&self, target_id: TargetId) -> bool {
153 self.entries.iter().any(|e| e.target_id == target_id)
154 }
155}
156
157impl From<(Vec<WithdrawRouteEntry>, u128)> for WithdrawRoute {
158 fn from(value: (Vec<WithdrawRouteEntry>, u128)) -> Self {
159 Self::from_entries(value.0, value.1)
160 }
161}
162
163#[templar_vault_macros::vault_derive]
165#[derive(Clone, PartialEq, Eq)]
166pub enum WithdrawRouteError {
167 ZeroTargetAmount,
169 EmptyRoute,
171 InsufficientRouteTotal {
173 route_total: u128,
174 target_amount: u128,
175 },
176 DuplicateTarget { target_id: TargetId },
178 ZeroMaxAmount { target_id: TargetId },
180 AmountOverflow,
182}
183
184fn checked_total_amount<I>(amounts: I) -> Result<u128, WithdrawRouteError>
185where
186 I: IntoIterator<Item = u128>,
187{
188 amounts.into_iter().try_fold(0u128, |acc, amount| {
189 acc.checked_add(amount)
190 .ok_or(WithdrawRouteError::AmountOverflow)
191 })
192}
193
194pub fn build_withdraw_route(
206 principals: &[(TargetId, u128)],
207 target_amount: u128,
208) -> Result<WithdrawRoute, WithdrawRouteError> {
209 if target_amount == 0 {
210 return Err(WithdrawRouteError::ZeroTargetAmount);
211 }
212
213 let total_principal = checked_total_amount(principals.iter().map(|(_, p)| *p))?;
214
215 if total_principal < target_amount {
216 return Err(WithdrawRouteError::InsufficientRouteTotal {
217 route_total: total_principal,
218 target_amount,
219 });
220 }
221
222 let mut sorted: Vec<(TargetId, u128)> =
224 principals.iter().filter(|(_, p)| *p > 0).cloned().collect();
225 sorted.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
226
227 let entries: Vec<WithdrawRouteEntry> = sorted
228 .into_iter()
229 .map(|(target_id, principal)| WithdrawRouteEntry::new(target_id, principal))
230 .collect();
231
232 if entries.is_empty() {
233 return Err(WithdrawRouteError::EmptyRoute);
234 }
235
236 Ok(WithdrawRoute::from_entries(entries, target_amount))
237}
238
239pub fn build_withdraw_route_with_liquidity(
251 market_data: &[(TargetId, u128, u128)],
252 target_amount: u128,
253) -> Result<WithdrawRoute, WithdrawRouteError> {
254 if target_amount == 0 {
255 return Err(WithdrawRouteError::ZeroTargetAmount);
256 }
257
258 let mut sorted: Vec<(TargetId, u128, u128)> = market_data
260 .iter()
261 .filter(|(_, p, _)| *p > 0)
262 .cloned()
263 .collect();
264 sorted.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.0.cmp(&b.0)));
265
266 let entries: Vec<WithdrawRouteEntry> = sorted
268 .into_iter()
269 .map(|(target_id, principal, liquidity)| {
270 let max_amount = principal.min(liquidity);
271 WithdrawRouteEntry::new(target_id, max_amount).with_liquidity(liquidity)
272 })
273 .filter(|e| e.max_amount > 0)
274 .collect();
275
276 if entries.is_empty() {
277 return Err(WithdrawRouteError::EmptyRoute);
278 }
279
280 let route_total = checked_total_amount(entries.iter().map(|entry| entry.max_amount))?;
281 let route = WithdrawRoute::from_entries(entries, target_amount);
282
283 if route_total < target_amount {
284 return Err(WithdrawRouteError::InsufficientRouteTotal {
285 route_total,
286 target_amount,
287 });
288 }
289
290 Ok(route)
291}