1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
//! Special error types for the market contract.
//!
//! This module is intended as a stop-gap measure. The perps protocol overall
//! uses `anyhow` for error handling, and then uses a `PerpError` type to
//! represent known error cases that require special handling by consumers of
//! the contracts.
//!
//! Generally we would like to move `PerpError` over to using `thiserror`, and
//! then have a duality of error handling: `anyhow::Error` for general purpose
//! errors (like serialization issues) that do not require special handling, and
//! `PerpError` for well described error types with known payloads. However,
//! making such a change would be an invasive change to the codebase.
//!
//! Instead, in the short term, we use this module to provide well-typed
//! `thiserror` error values that can be converted to `PerpError` values.

use crate::prelude::*;

/// An error type for known market errors with potentially special error handling.
#[derive(thiserror::Error, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "snake_case", tag = "error_id")]
#[allow(missing_docs)]
pub enum MarketError {
    #[error(
        "Infinite max gains can only be used on long positions for collateral-is-base markets"
    )]
    InvalidInfiniteMaxGains {
        market_type: MarketType,
        direction: DirectionToBase,
    },
    #[error(
        "Infinite take profit price can only be used on long positions for collateral-is-base markets"
    )]
    InvalidInfiniteTakeProfitPrice {
        market_type: MarketType,
        direction: DirectionToBase,
    },
    #[error("Max gains are too large")]
    MaxGainsTooLarge {},
    #[error("Unable to withdraw {requested}. Only {available} LP tokens held.")]
    WithdrawTooMuch {
        requested: NonZero<LpToken>,
        available: NonZero<LpToken>,
    },
    #[error("Insufficient unlocked liquidity for withdrawal. Requested {requested_collateral} ({requested_lp} LP tokens), only {unlocked} liquidity available until min liquidity.")]
    InsufficientLiquidityForWithdrawal {
        requested_lp: NonZero<LpToken>,
        requested_collateral: NonZero<Collateral>,
        unlocked: Collateral,
    },
    #[error("Missing position: {id}")]
    MissingPosition { id: String },
    #[error("Trader leverage {new_leverage} is out of range ({low_allowed}..{high_allowed}]")]
    TraderLeverageOutOfRange {
        low_allowed: Decimal256,
        high_allowed: Decimal256,
        new_leverage: Decimal256,
        current_leverage: Option<Decimal256>,
    },
    #[error("Counter leverage {new_leverage} is out of range ({low_allowed}..{high_allowed}]")]
    CounterLeverageOutOfRange {
        low_allowed: Decimal256,
        high_allowed: Decimal256,
        new_leverage: Decimal256,
        current_leverage: Option<Decimal256>,
    },
    #[error("Deposit collateral is too small. Deposited {deposit_collateral}, or {deposit_usd} USD. Minimum is {minimum_usd} USD")]
    MinimumDeposit {
        deposit_collateral: Collateral,
        deposit_usd: Usd,
        minimum_usd: Usd,
    },
    #[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")]
    Congestion {
        current_queue: u32,
        max_size: u32,
        reason: CongestionReason,
    },
    #[error("Deposit would exceed maximum liquidity allowed. Current liquidity: {current} USD. Deposit size: {deposit} USD. Maximum allowed: {max} USD.")]
    MaxLiquidity {
        price_collateral_in_usd: PriceCollateralInUsd,
        current: Usd,
        deposit: Usd,
        max: Usd,
    },
    #[error("Cannot perform this action since it would exceed delta neutrality limits - protocol is already too long")]
    DeltaNeutralityFeeAlreadyLong {
        cap: Number,
        sensitivity: Number,
        instant_before: Number,
        net_notional_before: Signed<Notional>,
        net_notional_after: Signed<Notional>,
    },
    #[error("Cannot perform this action since it would exceed delta neutrality limits - protocol is already too short")]
    DeltaNeutralityFeeAlreadyShort {
        cap: Number,
        sensitivity: Number,
        instant_before: Number,
        net_notional_before: Signed<Notional>,
        net_notional_after: Signed<Notional>,
    },
    #[error("Cannot perform this action since it would exceed delta neutrality limits - protocol would become too long")]
    DeltaNeutralityFeeNewlyLong {
        cap: Number,
        sensitivity: Number,
        instant_after: Number,
        net_notional_before: Signed<Notional>,
        net_notional_after: Signed<Notional>,
    },
    #[error( "Cannot perform this action since it would exceed delta neutrality limits - protocol would become too short")]
    DeltaNeutralityFeeNewlyShort {
        cap: Number,
        sensitivity: Number,
        instant_after: Number,
        net_notional_before: Signed<Notional>,
        net_notional_after: Signed<Notional>,
    },
    #[error("Cannot perform this action since it would exceed delta neutrality limits - protocol would go from too long to too short")]
    DeltaNeutralityFeeLongToShort {
        cap: Number,
        sensitivity: Number,
        instant_before: Number,
        instant_after: Number,
        net_notional_before: Signed<Notional>,
        net_notional_after: Signed<Notional>,
    },
    #[error("Cannot perform this action since it would exceed delta neutrality limits - protocol would go from too short to too long")]
    DeltaNeutralityFeeShortToLong {
        cap: Number,
        sensitivity: Number,
        instant_before: Number,
        instant_after: Number,
        net_notional_before: Signed<Notional>,
        net_notional_after: Signed<Notional>,
    },
    #[error("Liquidity cooldown in effect, will end in {seconds_remaining} seconds.")]
    LiquidityCooldown {
        ends_at: Timestamp,
        seconds_remaining: u64,
    },
    #[error("Cannot perform the given action while a pending action is waiting for the position")]
    PendingDeferredExec {},
    #[error("The difference between oldest and newest publish timestamp is too large. Oldest: {oldest}. Newest: {newest}.")]
    VolatilePriceFeedTimeDelta {
        oldest: Timestamp,
        newest: Timestamp,
    },
    #[error("Limit order {order_id} is already canceling")]
    LimitOrderAlreadyCanceling { order_id: Uint64 },
    #[error("Position {position_id} is already closing")]
    PositionAlreadyClosing { position_id: Uint64 },
    #[error(
        "No price publish time found, there is likely a spot price config error for this market"
    )]
    NoPricePublishTimeFound,
    #[error("Cannot close position {id}, it was already closed at {close_time}. Close reason: {reason}.")]
    PositionAlreadyClosed {
        id: Uint64,
        close_time: Timestamp,
        reason: String,
    },
    #[error("Insufficient locked liquidity in protocol to perform the given unlock. Requested: {requested}. Total locked: {total_locked}.")]
    InsufficientLiquidityForUnlock {
        requested: NonZero<Collateral>,
        total_locked: Collateral,
    },
    #[error("Insufficient unlocked liquidity in the protocol. Requested: {requested}. Total available: {total_unlocked}. Total allowed with carry leverage restrictions: {allowed}.")]
    Liquidity {
        /// Total amount of liquidity requested to take from unlocked pool.
        requested: NonZero<Collateral>,
        /// Total amount of liquidity available in the unlocked pool.
        total_unlocked: Collateral,
        /// Liquidity allowed to be taken for this action.
        ///
        /// In particular, carry leverage may restrict the total amount of
        /// liquidity that can be used to ensure sufficient funds for cash-and-carry
        /// balancing operations.
        allowed: Collateral,
    },
}

/// Was the price provided by the trader too high or too low?
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TriggerPriceMustBe {
    /// Specified price must be less than the bound
    Less,
    /// Specified price must be greater than the bound
    Greater,
}

impl Display for TriggerPriceMustBe {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(
            f,
            "{}",
            match self {
                TriggerPriceMustBe::Greater => "greater",
                TriggerPriceMustBe::Less => "less",
            }
        )
    }
}

/// What type of price trigger occurred?
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TriggerType {
    /// A stop loss
    StopLoss,
    /// A take profit
    TakeProfit,
}

impl Display for TriggerType {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(
            f,
            "{}",
            match self {
                TriggerType::StopLoss => "stop loss",
                TriggerType::TakeProfit => "take profit",
            }
        )
    }
}

/// What was the user doing when they hit the congestion error message?
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CongestionReason {
    /// Opening a new position via market order
    OpenMarket,
    /// Placing a new limit order
    PlaceLimit,
    /// Updating an existing position
    Update,
    /// Setting a trigger price on an existing position
    SetTrigger,
}

impl MarketError {
    /// Convert into an `anyhow::Error`.
    ///
    /// This method will first convert into a `PerpError` and then wrap that
    /// in `anyhow::Error`.
    pub fn into_anyhow(self) -> anyhow::Error {
        let description = format!("{self}");
        self.into_perp_error(description).into()
    }

    /// Try to convert from an `anyhow::Error`.
    pub fn try_from_anyhow(err: &anyhow::Error) -> Result<Self> {
        (|| {
            let err = err
                .downcast_ref::<PerpError<MarketError>>()
                .context("Not a PerpError<MarketError>")?;
            err.data
                .clone()
                .context("PerpError<MarketError> without a data field")
        })()
        .with_context(|| format!("try_from_anyhow failed on: {err:?}"))
    }

    /// Convert into a `PerpError`.
    fn into_perp_error(self, description: String) -> PerpError<MarketError> {
        let id = self.get_error_id();
        PerpError {
            id,
            domain: ErrorDomain::Market,
            description,
            data: Some(self),
        }
    }

    /// Get the [ErrorId] for this value.
    fn get_error_id(&self) -> ErrorId {
        match self {
            MarketError::InvalidInfiniteMaxGains { .. } => ErrorId::InvalidInfiniteMaxGains,
            MarketError::InvalidInfiniteTakeProfitPrice { .. } => {
                ErrorId::InvalidInfiniteTakeProfitPrice
            }
            MarketError::MaxGainsTooLarge {} => ErrorId::MaxGainsTooLarge,
            MarketError::WithdrawTooMuch { .. } => ErrorId::WithdrawTooMuch,
            MarketError::InsufficientLiquidityForWithdrawal { .. } => {
                ErrorId::InsufficientLiquidityForWithdrawal
            }
            MarketError::MissingPosition { .. } => ErrorId::MissingPosition,
            MarketError::TraderLeverageOutOfRange { .. } => ErrorId::TraderLeverageOutOfRange,
            MarketError::CounterLeverageOutOfRange { .. } => ErrorId::CounterLeverageOutOfRange,
            MarketError::MinimumDeposit { .. } => ErrorId::MinimumDeposit,
            MarketError::Congestion { .. } => ErrorId::Congestion,
            MarketError::MaxLiquidity { .. } => ErrorId::MaxLiquidity,
            MarketError::DeltaNeutralityFeeAlreadyLong { .. } => {
                ErrorId::DeltaNeutralityFeeAlreadyLong
            }
            MarketError::DeltaNeutralityFeeAlreadyShort { .. } => {
                ErrorId::DeltaNeutralityFeeAlreadyShort
            }
            MarketError::DeltaNeutralityFeeNewlyLong { .. } => ErrorId::DeltaNeutralityFeeNewlyLong,
            MarketError::DeltaNeutralityFeeNewlyShort { .. } => {
                ErrorId::DeltaNeutralityFeeNewlyShort
            }
            MarketError::DeltaNeutralityFeeLongToShort { .. } => {
                ErrorId::DeltaNeutralityFeeLongToShort
            }
            MarketError::DeltaNeutralityFeeShortToLong { .. } => {
                ErrorId::DeltaNeutralityFeeShortToLong
            }
            MarketError::LiquidityCooldown { .. } => ErrorId::LiquidityCooldown,
            MarketError::PendingDeferredExec {} => ErrorId::PendingDeferredExec,
            MarketError::VolatilePriceFeedTimeDelta { .. } => ErrorId::VolatilePriceFeedTimeDelta,
            MarketError::LimitOrderAlreadyCanceling { .. } => ErrorId::LimitOrderAlreadyCanceling,
            MarketError::PositionAlreadyClosing { .. } => ErrorId::PositionAlreadyClosing,
            MarketError::NoPricePublishTimeFound => ErrorId::NoPricePublishTimeFound,
            MarketError::PositionAlreadyClosed { .. } => ErrorId::PositionAlreadyClosed,
            MarketError::InsufficientLiquidityForUnlock { .. } => {
                ErrorId::InsufficientLiquidityForUnlock
            }
            MarketError::Liquidity { .. } => ErrorId::Liquidity,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn into_perp_error() {
        let market_error = MarketError::WithdrawTooMuch {
            requested: "100".parse().unwrap(),
            available: "50".parse().unwrap(),
        };
        let expected = PerpError {
            id: ErrorId::WithdrawTooMuch,
            domain: ErrorDomain::Market,
            description: "Unable to withdraw 100. Only 50 LP tokens held.".to_owned(),
            data: Some(market_error.clone()),
        };
        let anyhow_error = market_error.clone().into_anyhow();
        let actual = anyhow_error.downcast_ref::<PerpError<_>>().unwrap();
        assert_eq!(&expected, actual);

        let market_error2 = MarketError::try_from_anyhow(&anyhow_error).unwrap();
        assert_eq!(market_error, market_error2);
    }
}