1use crate::prelude::*;
18
19#[derive(thiserror::Error, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone)]
21#[serde(rename_all = "snake_case", tag = "error_id")]
22#[allow(missing_docs)]
23pub enum MarketError {
24 #[error(
25 "Infinite max gains can only be used on long positions for collateral-is-base markets"
26 )]
27 InvalidInfiniteMaxGains {
28 market_type: MarketType,
29 direction: DirectionToBase,
30 },
31 #[error(
32 "Infinite take profit price can only be used on long positions for collateral-is-base markets"
33 )]
34 InvalidInfiniteTakeProfitPrice {
35 market_type: MarketType,
36 direction: DirectionToBase,
37 },
38 #[error("Max gains are too large")]
39 MaxGainsTooLarge {},
40 #[error("Unable to withdraw {requested}. Only {available} LP tokens held.")]
41 WithdrawTooMuch {
42 requested: NonZero<LpToken>,
43 available: NonZero<LpToken>,
44 },
45 #[error("Insufficient unlocked liquidity for withdrawal. Requested {requested_collateral} ({requested_lp} LP tokens), only {unlocked} liquidity available until min liquidity.")]
46 InsufficientLiquidityForWithdrawal {
47 requested_lp: NonZero<LpToken>,
48 requested_collateral: NonZero<Collateral>,
49 unlocked: Collateral,
50 },
51 #[error("Missing position: {id}")]
52 MissingPosition { id: String },
53 #[error("Trader leverage {new_leverage} is out of range ({low_allowed}..{high_allowed}]")]
54 TraderLeverageOutOfRange {
55 low_allowed: Decimal256,
56 high_allowed: Decimal256,
57 new_leverage: Decimal256,
58 current_leverage: Option<Decimal256>,
59 },
60 #[error("Deposit collateral is too small. Deposited {deposit_collateral}, or {deposit_usd} USD. Minimum is {minimum_usd} USD")]
61 MinimumDeposit {
62 deposit_collateral: Collateral,
63 deposit_usd: Usd,
64 minimum_usd: Usd,
65 },
66 #[error("Cannot open or update positions currently, the position queue size is {current_queue}, while the allowed size is {max_size}. Please try again later")]
67 Congestion {
68 current_queue: u32,
69 max_size: u32,
70 reason: CongestionReason,
71 },
72 #[error("Deposit would exceed maximum liquidity allowed. Current liquidity: {current} USD. Deposit size: {deposit} USD. Maximum allowed: {max} USD.")]
73 MaxLiquidity {
74 price_collateral_in_usd: PriceCollateralInUsd,
75 current: Usd,
76 deposit: Usd,
77 max: Usd,
78 },
79 #[error("Cannot perform this action since it would exceed delta neutrality limits - protocol is already too long")]
80 DeltaNeutralityFeeAlreadyLong {
81 cap: Number,
82 sensitivity: Number,
83 instant_before: Number,
84 net_notional_before: Signed<Notional>,
85 net_notional_after: Signed<Notional>,
86 },
87 #[error("Cannot perform this action since it would exceed delta neutrality limits - protocol is already too short")]
88 DeltaNeutralityFeeAlreadyShort {
89 cap: Number,
90 sensitivity: Number,
91 instant_before: Number,
92 net_notional_before: Signed<Notional>,
93 net_notional_after: Signed<Notional>,
94 },
95 #[error("Cannot perform this action since it would exceed delta neutrality limits - protocol would become too long")]
96 DeltaNeutralityFeeNewlyLong {
97 cap: Number,
98 sensitivity: Number,
99 instant_after: Number,
100 net_notional_before: Signed<Notional>,
101 net_notional_after: Signed<Notional>,
102 },
103 #[error( "Cannot perform this action since it would exceed delta neutrality limits - protocol would become too short")]
104 DeltaNeutralityFeeNewlyShort {
105 cap: Number,
106 sensitivity: Number,
107 instant_after: Number,
108 net_notional_before: Signed<Notional>,
109 net_notional_after: Signed<Notional>,
110 },
111 #[error("Cannot perform this action since it would exceed delta neutrality limits - protocol would go from too long to too short")]
112 DeltaNeutralityFeeLongToShort {
113 cap: Number,
114 sensitivity: Number,
115 instant_before: Number,
116 instant_after: Number,
117 net_notional_before: Signed<Notional>,
118 net_notional_after: Signed<Notional>,
119 },
120 #[error("Cannot perform this action since it would exceed delta neutrality limits - protocol would go from too short to too long")]
121 DeltaNeutralityFeeShortToLong {
122 cap: Number,
123 sensitivity: Number,
124 instant_before: Number,
125 instant_after: Number,
126 net_notional_before: Signed<Notional>,
127 net_notional_after: Signed<Notional>,
128 },
129 #[error("Liquidity cooldown in effect, will end in {seconds_remaining} seconds.")]
130 LiquidityCooldown {
131 ends_at: Timestamp,
132 seconds_remaining: u64,
133 },
134 #[error("Cannot perform the given action while a pending action is waiting for the position")]
135 PendingDeferredExec {},
136 #[error("The difference between oldest and newest publish timestamp is too large. Oldest: {oldest}. Newest: {newest}.")]
137 VolatilePriceFeedTimeDelta {
138 oldest: Timestamp,
139 newest: Timestamp,
140 },
141 #[error("Limit order {order_id} is already canceling")]
142 LimitOrderAlreadyCanceling { order_id: Uint64 },
143 #[error("Position {position_id} is already closing")]
144 PositionAlreadyClosing { position_id: Uint64 },
145 #[error(
146 "No price publish time found, there is likely a spot price config error for this market"
147 )]
148 NoPricePublishTimeFound,
149 #[error("Cannot close position {id}, it was already closed at {close_time}. Close reason: {reason}.")]
150 PositionAlreadyClosed {
151 id: Uint64,
152 close_time: Timestamp,
153 reason: String,
154 },
155 #[error("Insufficient locked liquidity in protocol to perform the given unlock. Requested: {requested}. Total locked: {total_locked}.")]
156 InsufficientLiquidityForUnlock {
157 requested: NonZero<Collateral>,
158 total_locked: Collateral,
159 },
160 #[error("Insufficient unlocked liquidity in the protocol. Requested: {requested}. Total available: {total_unlocked}. Total allowed with carry leverage restrictions: {allowed}.")]
161 Liquidity {
162 requested: NonZero<Collateral>,
164 total_unlocked: Collateral,
166 allowed: Collateral,
172 },
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
177#[serde(rename_all = "snake_case")]
178pub enum TriggerPriceMustBe {
179 Less,
181 Greater,
183}
184
185impl Display for TriggerPriceMustBe {
186 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
187 write!(
188 f,
189 "{}",
190 match self {
191 TriggerPriceMustBe::Greater => "greater",
192 TriggerPriceMustBe::Less => "less",
193 }
194 )
195 }
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
200#[serde(rename_all = "snake_case")]
201pub enum TriggerType {
202 StopLoss,
204 TakeProfit,
206}
207
208impl Display for TriggerType {
209 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
210 write!(
211 f,
212 "{}",
213 match self {
214 TriggerType::StopLoss => "stop loss",
215 TriggerType::TakeProfit => "take profit",
216 }
217 )
218 }
219}
220
221#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, serde::Serialize, serde::Deserialize)]
223#[serde(rename_all = "snake_case")]
224pub enum CongestionReason {
225 OpenMarket,
227 PlaceLimit,
229 Update,
231 SetTrigger,
233}
234
235impl MarketError {
236 pub fn into_anyhow(self) -> anyhow::Error {
241 let description = format!("{self}");
242 self.into_perp_error(description).into()
243 }
244
245 pub fn try_from_anyhow(err: &anyhow::Error) -> Result<Self> {
247 (|| {
248 let err = err
249 .downcast_ref::<PerpError<MarketError>>()
250 .context("Not a PerpError<MarketError>")?;
251 err.data
252 .clone()
253 .context("PerpError<MarketError> without a data field")
254 })()
255 .with_context(|| format!("try_from_anyhow failed on: {err:?}"))
256 }
257
258 fn into_perp_error(self, description: String) -> PerpError<MarketError> {
260 let id = self.get_error_id();
261 PerpError {
262 id,
263 domain: ErrorDomain::Market,
264 description,
265 data: Some(self),
266 }
267 }
268
269 fn get_error_id(&self) -> ErrorId {
271 match self {
272 MarketError::InvalidInfiniteMaxGains { .. } => ErrorId::InvalidInfiniteMaxGains,
273 MarketError::InvalidInfiniteTakeProfitPrice { .. } => {
274 ErrorId::InvalidInfiniteTakeProfitPrice
275 }
276 MarketError::MaxGainsTooLarge {} => ErrorId::MaxGainsTooLarge,
277 MarketError::WithdrawTooMuch { .. } => ErrorId::WithdrawTooMuch,
278 MarketError::InsufficientLiquidityForWithdrawal { .. } => {
279 ErrorId::InsufficientLiquidityForWithdrawal
280 }
281 MarketError::MissingPosition { .. } => ErrorId::MissingPosition,
282 MarketError::TraderLeverageOutOfRange { .. } => ErrorId::TraderLeverageOutOfRange,
283 MarketError::MinimumDeposit { .. } => ErrorId::MinimumDeposit,
284 MarketError::Congestion { .. } => ErrorId::Congestion,
285 MarketError::MaxLiquidity { .. } => ErrorId::MaxLiquidity,
286 MarketError::DeltaNeutralityFeeAlreadyLong { .. } => {
287 ErrorId::DeltaNeutralityFeeAlreadyLong
288 }
289 MarketError::DeltaNeutralityFeeAlreadyShort { .. } => {
290 ErrorId::DeltaNeutralityFeeAlreadyShort
291 }
292 MarketError::DeltaNeutralityFeeNewlyLong { .. } => ErrorId::DeltaNeutralityFeeNewlyLong,
293 MarketError::DeltaNeutralityFeeNewlyShort { .. } => {
294 ErrorId::DeltaNeutralityFeeNewlyShort
295 }
296 MarketError::DeltaNeutralityFeeLongToShort { .. } => {
297 ErrorId::DeltaNeutralityFeeLongToShort
298 }
299 MarketError::DeltaNeutralityFeeShortToLong { .. } => {
300 ErrorId::DeltaNeutralityFeeShortToLong
301 }
302 MarketError::LiquidityCooldown { .. } => ErrorId::LiquidityCooldown,
303 MarketError::PendingDeferredExec {} => ErrorId::PendingDeferredExec,
304 MarketError::VolatilePriceFeedTimeDelta { .. } => ErrorId::VolatilePriceFeedTimeDelta,
305 MarketError::LimitOrderAlreadyCanceling { .. } => ErrorId::LimitOrderAlreadyCanceling,
306 MarketError::PositionAlreadyClosing { .. } => ErrorId::PositionAlreadyClosing,
307 MarketError::NoPricePublishTimeFound => ErrorId::NoPricePublishTimeFound,
308 MarketError::PositionAlreadyClosed { .. } => ErrorId::PositionAlreadyClosed,
309 MarketError::InsufficientLiquidityForUnlock { .. } => {
310 ErrorId::InsufficientLiquidityForUnlock
311 }
312 MarketError::Liquidity { .. } => ErrorId::Liquidity,
313 }
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn into_perp_error() {
323 let market_error = MarketError::WithdrawTooMuch {
324 requested: "100".parse().unwrap(),
325 available: "50".parse().unwrap(),
326 };
327 let expected = PerpError {
328 id: ErrorId::WithdrawTooMuch,
329 domain: ErrorDomain::Market,
330 description: "Unable to withdraw 100. Only 50 LP tokens held.".to_owned(),
331 data: Some(market_error.clone()),
332 };
333 let anyhow_error = market_error.clone().into_anyhow();
334 let actual = anyhow_error.downcast_ref::<PerpError<_>>().unwrap();
335 assert_eq!(&expected, actual);
336
337 let market_error2 = MarketError::try_from_anyhow(&anyhow_error).unwrap();
338 assert_eq!(market_error, market_error2);
339 }
340}