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
//! Data types and events for cranking.
use super::deferred_execution::{DeferredExecId, DeferredExecTarget};
use super::position::PositionId;
use crate::contracts::market::order::OrderId;
use crate::contracts::market::position::LiquidationReason;
use shared::prelude::*;

/// What work is currently available for the crank.
#[cw_serde]
pub enum CrankWorkInfo {
    /// Closing all open positions
    CloseAllPositions {
        /// Next position to be closed
        position: PositionId,
    },
    /// Resetting all LP balances to 0 after all liquidity is drained
    ResetLpBalances {},
    /// Liquifund a position
    Liquifunding {
        /// Next position to be liquifunded
        position: PositionId,
    },
    /// Liquidate a position.
    ///
    /// Includes max gains, take profit, and stop loss.
    Liquidation {
        /// Position to liquidate
        position: PositionId,
        /// Reason for the liquidation
        liquidation_reason: LiquidationReason,
    },
    /// Deferred execution (open/update/closed) can be executed.
    DeferredExec {
        /// ID to be processed
        deferred_exec_id: DeferredExecId,
        /// Target of the action
        target: DeferredExecTarget,
    },
    /// Limit order can be opened
    LimitOrder {
        /// ID of the order to be opened
        order_id: OrderId,
    },
    /// Finished all processing for a given price update
    Completed {},
}

impl CrankWorkInfo {
    /// Should a cranker receive rewards for performing this action?
    ///
    /// We generally want to give out rewards for actions that are directly
    /// user initiated and will be receiving a crank fee paid into the system. Actions
    /// which are overall protocol maintenance without a specific user action may be
    /// unfunded. A simple "attack" we want to avoid is a cranker flooding the system
    /// with unnecessary price updates + cranks to continue making a profit off of
    /// "Completed" items.
    pub fn receives_crank_rewards(&self) -> bool {
        match self {
            CrankWorkInfo::CloseAllPositions { .. }
            | CrankWorkInfo::ResetLpBalances {}
            | CrankWorkInfo::Completed { .. } => false,
            CrankWorkInfo::Liquifunding { .. }
            | CrankWorkInfo::Liquidation { .. }
            | CrankWorkInfo::DeferredExec { .. }
            | CrankWorkInfo::LimitOrder { .. } => true,
        }
    }
}

/// Events related to the crank
pub mod events {
    use std::borrow::Cow;

    use super::*;
    use cosmwasm_std::Event;

    /// Batch processing of multiple cranks
    pub struct CrankExecBatchEvent {
        /// How many cranks were requested
        pub requested: u64,
        /// How many paying cranks were processed
        pub paying: u64,
        /// How many cranks were actually processed
        pub actual: Vec<(CrankWorkInfo, PricePoint)>,
    }

    impl From<CrankExecBatchEvent> for Event {
        fn from(
            CrankExecBatchEvent {
                requested,
                paying,
                actual,
            }: CrankExecBatchEvent,
        ) -> Self {
            let mut event = Event::new("crank-batch-exec")
                .add_attribute("requested", requested.to_string())
                .add_attribute("actual", actual.len().to_string())
                .add_attribute("paying", paying.to_string());

            for (idx, (work, price_point)) in actual.into_iter().enumerate() {
                event = event.add_attribute(
                    format!("work-{}", idx + 1),
                    match work {
                        CrankWorkInfo::CloseAllPositions { .. } => {
                            Cow::Borrowed("close-all-positions")
                        }
                        CrankWorkInfo::ResetLpBalances {} => "reset-lp-balances".into(),
                        CrankWorkInfo::Liquifunding { position, .. } => {
                            format!("liquifund {position}").into()
                        }
                        CrankWorkInfo::Liquidation { position, .. } => {
                            format!("liquidation {position}").into()
                        }
                        CrankWorkInfo::DeferredExec {
                            deferred_exec_id, ..
                        } => format!("deferred exec {deferred_exec_id}").into(),
                        CrankWorkInfo::LimitOrder { order_id, .. } => {
                            format!("limit order {order_id}").into()
                        }
                        CrankWorkInfo::Completed {} => {
                            format!("completed {}", price_point.timestamp).into()
                        }
                    },
                )
            }

            event
        }
    }

    /// CrankWorkInfo with a given price point
    pub struct CrankWorkInfoEvent {
        /// The work info itself
        pub work_info: CrankWorkInfo,
        /// A price point, i.e. the price point that triggered the work
        pub price_point: PricePoint,
    }

    impl From<CrankWorkInfoEvent> for Event {
        fn from(
            CrankWorkInfoEvent {
                work_info,
                price_point,
            }: CrankWorkInfoEvent,
        ) -> Self {
            let mut event = Event::new("crank-work")
                .add_attribute(
                    "kind",
                    match work_info {
                        CrankWorkInfo::CloseAllPositions { .. } => "close-all-positions",
                        CrankWorkInfo::ResetLpBalances { .. } => "reset-lp-balances",
                        CrankWorkInfo::Completed { .. } => "completed",
                        CrankWorkInfo::Liquidation { .. } => "liquidation",
                        CrankWorkInfo::Liquifunding { .. } => "liquifunding",
                        CrankWorkInfo::DeferredExec { .. } => "deferred-exec",
                        CrankWorkInfo::LimitOrder { .. } => "limit-order",
                    },
                )
                // Keeping this for backwards-compat with indexer, though it should be deprecated
                // and deserialized from price-point itself
                .add_attribute("price-point-timestamp", price_point.timestamp.to_string())
                .add_attribute("price-point", serde_json::to_string(&price_point).unwrap());

            let (position_id, order_id) = match work_info {
                CrankWorkInfo::CloseAllPositions { position } => (Some(position), None),
                CrankWorkInfo::ResetLpBalances {} => (None, None),
                CrankWorkInfo::Completed {} => (None, None),
                CrankWorkInfo::Liquidation {
                    position,
                    liquidation_reason: _,
                } => (Some(position), None),
                CrankWorkInfo::Liquifunding { position } => (Some(position), None),
                CrankWorkInfo::DeferredExec {
                    deferred_exec_id: _,
                    target,
                } => (target.position_id(), target.order_id()),
                CrankWorkInfo::LimitOrder { order_id } => (None, Some(order_id)),
            };

            if let Some(position_id) = position_id {
                event = event.add_attribute("pos-id", position_id.to_string());
            }

            if let CrankWorkInfo::Liquidation {
                liquidation_reason, ..
            } = work_info
            {
                event = event.add_attribute("liquidation-reason", liquidation_reason.to_string());
            }

            if let CrankWorkInfo::DeferredExec {
                deferred_exec_id,
                target,
            } = work_info
            {
                event = event
                    .add_attribute("deferred-exec-id", deferred_exec_id.to_string())
                    .add_attribute(
                        "deferred-exec-target",
                        match target {
                            DeferredExecTarget::DoesNotExist => "not-exist",
                            DeferredExecTarget::Position { .. } => "position",
                            DeferredExecTarget::Order { .. } => "order",
                        },
                    );
            }

            if let Some(order_id) = order_id {
                event = event.add_attribute("order-id", order_id.to_string());
            }

            event
        }
    }

    impl TryFrom<Event> for CrankWorkInfoEvent {
        type Error = anyhow::Error;

        fn try_from(evt: Event) -> anyhow::Result<Self> {
            let get_position_id =
                || -> anyhow::Result<PositionId> { Ok(PositionId::new(evt.u64_attr("pos-id")?)) };
            let get_order_id =
                || -> anyhow::Result<OrderId> { Ok(OrderId::new(evt.u64_attr("order-id")?)) };

            let get_liquidation_reason = || -> anyhow::Result<LiquidationReason> {
                match evt.string_attr("liquidation-reason")?.as_str() {
                    "liquidated" => Ok(LiquidationReason::Liquidated),
                    "take-profit" => Ok(LiquidationReason::TakeProfit),
                    _ => Err(PerpError::unimplemented().into()),
                }
            };

            let work_info = evt.map_attr_result("kind", |s| match s {
                "completed" => Ok(CrankWorkInfo::Completed {}),
                "liquifunding" => Ok(CrankWorkInfo::Liquifunding {
                    position: get_position_id()?,
                }),
                "liquidation" => Ok(CrankWorkInfo::Liquidation {
                    position: get_position_id()?,
                    liquidation_reason: get_liquidation_reason()?,
                }),
                "limit-order" => Ok(CrankWorkInfo::LimitOrder {
                    order_id: get_order_id()?,
                }),
                "close-all-positions" => Ok(CrankWorkInfo::CloseAllPositions {
                    position: get_position_id()?,
                }),
                "reset-lp-balances" => Ok(CrankWorkInfo::ResetLpBalances {}),
                "deferred-exec" => Ok(CrankWorkInfo::DeferredExec {
                    deferred_exec_id: DeferredExecId::from_u64(evt.u64_attr("deferred-exec-id")?),
                    target: evt.map_attr_result(
                        "deferred-exec-target",
                        |x| -> Result<DeferredExecTarget> {
                            match x {
                                "not-exist" => Ok(DeferredExecTarget::DoesNotExist),
                                "position" => get_position_id().map(DeferredExecTarget::Position),
                                "order" => get_order_id().map(DeferredExecTarget::Order),
                                _ => Err(PerpError::unimplemented().into()),
                            }
                        },
                    )?,
                }),

                _ => Err(PerpError::unimplemented().into()),
            })?;

            Ok(Self {
                work_info,
                price_point: evt.json_attr("price-point")?,
            })
        }
    }

    // Not sure if this is needed anymore, but keeping it in for backwards compat at least
    impl TryFrom<Event> for CrankWorkInfo {
        type Error = anyhow::Error;

        fn try_from(evt: Event) -> anyhow::Result<Self> {
            CrankWorkInfoEvent::try_from(evt).map(|x| x.work_info)
        }
    }
}