1use melin_app::auth::Permission;
9use melin_app::decoder::{Decoded, RequestDecoder as RequestDecoderTrait};
10use melin_protocol::codec;
11use melin_protocol::message::Request;
12use melin_trading::trading_event::TradingEvent;
13use melin_wire_protocol::error::ProtocolError;
14
15#[derive(Debug, Clone, Copy)]
20pub struct RequestDecoder;
21
22impl RequestDecoderTrait for RequestDecoder {
23 type Event = TradingEvent;
24
25 fn decode(&self, bytes: &[u8], permission: Permission) -> Decoded<TradingEvent> {
26 let (request_seq, request) = match codec::decode_request(bytes) {
27 Ok(pair) => pair,
28 Err(e) => return Decoded::DecodeError(protocol_error_reason(&e)),
29 };
30
31 if should_filter(&request) {
32 return Decoded::Filter;
33 }
34
35 if let Err(reason) = check_permission(&request, permission) {
36 return Decoded::PermissionDenied(reason);
37 }
38
39 Decoded::Permitted {
40 request_seq,
41 event: to_trading_event(&request),
42 }
43 }
44}
45
46#[inline]
51fn protocol_error_reason(e: &ProtocolError) -> &'static str {
52 match e {
53 ProtocolError::Truncated => "truncated frame",
54 ProtocolError::UnknownTag(_) => "unknown variant tag",
55 ProtocolError::InvalidField(_) => "invalid field",
56 ProtocolError::MessageTooLarge(_) => "message too large",
57 ProtocolError::Io(_) => "io error",
58 }
59}
60
61#[inline]
64fn should_filter(request: &Request) -> bool {
65 matches!(
66 request,
67 Request::Heartbeat | Request::ChallengeResponse { .. } | Request::Subscribe { .. }
68 )
69}
70
71#[inline]
80fn check_permission(request: &Request, permission: Permission) -> Result<(), &'static str> {
81 if request.requires_operator() && !permission.is_operator() {
82 return Err("non-operator attempted operator command");
83 }
84 if request.is_fund_management() && !permission.can_manage_funds() {
85 return Err("non-custodian attempted fund management");
86 }
87 if !request.requires_operator() && !request.is_fund_management() && !permission.can_trade() {
88 return Err("connection lacks trading permission");
89 }
90 Ok(())
91}
92
93#[inline]
97fn to_trading_event(request: &Request) -> TradingEvent {
98 match *request {
99 Request::SubmitOrder { symbol, order } => TradingEvent::SubmitOrder { symbol, order },
100 Request::CancelOrder {
101 symbol,
102 account,
103 order_id,
104 } => TradingEvent::CancelOrder {
105 symbol,
106 account,
107 order_id,
108 },
109 Request::CancelAll { account } => TradingEvent::CancelAll { account },
110 Request::AddInstrument { spec } => TradingEvent::AddInstrument { spec },
111 Request::Deposit {
112 account,
113 currency,
114 amount,
115 } => TradingEvent::Deposit {
116 account,
117 currency,
118 amount,
119 },
120 Request::Withdraw {
121 account,
122 currency,
123 amount,
124 } => TradingEvent::Withdraw {
125 account,
126 currency,
127 amount,
128 },
129 Request::SetRiskLimits { symbol, limits } => TradingEvent::SetRiskLimits { symbol, limits },
130 Request::SetCircuitBreaker { symbol, config } => {
131 TradingEvent::SetCircuitBreaker { symbol, config }
132 }
133 Request::CancelReplace {
134 symbol,
135 account,
136 order_id,
137 new_price,
138 new_quantity,
139 } => TradingEvent::CancelReplace {
140 symbol,
141 account,
142 order_id,
143 new_price,
144 new_quantity,
145 },
146 Request::SetFeeSchedule { symbol, schedule } => {
147 TradingEvent::SetFeeSchedule { symbol, schedule }
148 }
149 Request::QueryStats => TradingEvent::QueryStats,
150 Request::QueryPosition { account } => TradingEvent::QueryPosition { account },
151 Request::QueryRequestSeq => TradingEvent::QueryRequestSeq,
152 Request::EndOfDay => TradingEvent::EndOfDay,
153 Request::DisableInstrument { symbol } => TradingEvent::DisableInstrument { symbol },
154 Request::EnableInstrument { symbol } => TradingEvent::EnableInstrument { symbol },
155 Request::RemoveInstrument { symbol } => TradingEvent::RemoveInstrument { symbol },
156 Request::Heartbeat | Request::ChallengeResponse { .. } | Request::Subscribe { .. } => {
157 unreachable!("filtered before to_trading_event")
158 }
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use std::num::NonZeroU64;
166
167 use melin_app::AppEvent;
168 use melin_types::types::*;
169
170 fn encode(request: &Request, seq: u64) -> Vec<u8> {
174 let mut buf = vec![0u8; 256];
175 let total = codec::encode_request(request, seq, &mut buf).unwrap();
176 buf[4..total].to_vec()
177 }
178
179 fn order() -> Order {
180 Order {
181 id: OrderId(1),
182 account: AccountId(1),
183 side: Side::Buy,
184 order_type: OrderType::Market,
185 quantity: Quantity(NonZeroU64::new(10).unwrap()),
186 time_in_force: TimeInForce::GTC,
187 stp: SelfTradeProtection::Allow,
188 expiry_ns: 0,
189 }
190 }
191
192 #[test]
193 fn heartbeat_is_filtered() {
194 let bytes = encode(&Request::Heartbeat, 0);
195 assert!(matches!(
196 RequestDecoder.decode(&bytes, Permission::Trader),
197 Decoded::Filter
198 ));
199 }
200
201 #[test]
202 fn subscribe_is_filtered() {
203 let bytes = encode(
204 &Request::Subscribe {
205 symbols: [Symbol(0); 8],
206 count: 0,
207 },
208 0,
209 );
210 assert!(matches!(
211 RequestDecoder.decode(&bytes, Permission::Trader),
212 Decoded::Filter
213 ));
214 }
215
216 #[test]
217 fn challenge_response_is_filtered() {
218 let bytes = encode(
219 &Request::ChallengeResponse {
220 signature: [0u8; 64],
221 public_key: [0u8; 32],
222 },
223 0,
224 );
225 assert!(matches!(
226 RequestDecoder.decode(&bytes, Permission::Trader),
227 Decoded::Filter
228 ));
229 }
230
231 #[test]
232 fn submit_order_as_trader_is_permitted() {
233 let bytes = encode(
234 &Request::SubmitOrder {
235 symbol: Symbol(1),
236 order: order(),
237 },
238 42,
239 );
240 match RequestDecoder.decode(&bytes, Permission::Trader) {
241 Decoded::Permitted { request_seq, event } => {
242 assert_eq!(request_seq, 42);
243 assert!(matches!(event, TradingEvent::SubmitOrder { .. }));
244 assert!(!event.is_query());
246 }
247 other => panic!("expected Permitted, got {:?}", debug_variant(&other)),
248 }
249 }
250
251 #[test]
252 fn submit_order_as_readonly_is_denied() {
253 let bytes = encode(
254 &Request::SubmitOrder {
255 symbol: Symbol(1),
256 order: order(),
257 },
258 0,
259 );
260 assert!(matches!(
261 RequestDecoder.decode(&bytes, Permission::ReadOnly),
262 Decoded::PermissionDenied(_)
263 ));
264 }
265
266 #[test]
267 fn add_instrument_as_operator_is_permitted() {
268 let bytes = encode(
269 &Request::AddInstrument {
270 spec: InstrumentSpec {
271 symbol: Symbol(1),
272 base: CurrencyId(1),
273 quote: CurrencyId(2),
274 },
275 },
276 7,
277 );
278 assert!(matches!(
279 RequestDecoder.decode(&bytes, Permission::Operator),
280 Decoded::Permitted { .. }
281 ));
282 }
283
284 #[test]
285 fn add_instrument_as_trader_is_denied() {
286 let bytes = encode(
287 &Request::AddInstrument {
288 spec: InstrumentSpec {
289 symbol: Symbol(1),
290 base: CurrencyId(1),
291 quote: CurrencyId(2),
292 },
293 },
294 0,
295 );
296 assert!(matches!(
297 RequestDecoder.decode(&bytes, Permission::Trader),
298 Decoded::PermissionDenied(_)
299 ));
300 }
301
302 #[test]
303 fn deposit_as_custodian_is_permitted() {
304 let bytes = encode(
305 &Request::Deposit {
306 account: AccountId(1),
307 currency: CurrencyId(1),
308 amount: 100,
309 },
310 3,
311 );
312 assert!(matches!(
313 RequestDecoder.decode(&bytes, Permission::Custodian),
314 Decoded::Permitted { .. }
315 ));
316 }
317
318 #[test]
319 fn deposit_as_trader_is_denied() {
320 let bytes = encode(
321 &Request::Deposit {
322 account: AccountId(1),
323 currency: CurrencyId(1),
324 amount: 100,
325 },
326 0,
327 );
328 assert!(matches!(
329 RequestDecoder.decode(&bytes, Permission::Trader),
330 Decoded::PermissionDenied(_)
331 ));
332 }
333
334 #[test]
335 fn query_stats_is_permitted_and_flagged() {
336 let bytes = encode(&Request::QueryStats, 1);
339 match RequestDecoder.decode(&bytes, Permission::Operator) {
340 Decoded::Permitted { event, .. } => {
341 assert!(matches!(event, TradingEvent::QueryStats));
342 assert!(event.is_query());
343 }
344 other => panic!("expected Permitted, got {:?}", debug_variant(&other)),
345 }
346 }
347
348 #[test]
349 fn malformed_frame_yields_decode_error() {
350 assert!(matches!(
352 RequestDecoder.decode(&[], Permission::Trader),
353 Decoded::DecodeError(_)
354 ));
355 }
356
357 fn debug_variant<E: AppEvent>(d: &Decoded<E>) -> &'static str {
358 match d {
359 Decoded::Filter => "Filter",
360 Decoded::Permitted { .. } => "Permitted",
361 Decoded::PermissionDenied(_) => "PermissionDenied",
362 Decoded::DecodeError(_) => "DecodeError",
363 }
364 }
365
366 fn full_order(id: u64, account: u32, side: Side) -> Order {
378 Order {
379 id: OrderId(id),
380 account: AccountId(account),
381 side,
382 order_type: OrderType::Limit {
383 price: Price(NonZeroU64::new(100).unwrap()),
384 post_only: false,
385 },
386 quantity: Quantity(NonZeroU64::new(10).unwrap()),
387 time_in_force: TimeInForce::GTC,
388 stp: SelfTradeProtection::CancelNewest,
389 expiry_ns: 0,
390 }
391 }
392
393 #[test]
394 fn maps_submit_order() {
395 let req = Request::SubmitOrder {
396 symbol: Symbol(1),
397 order: full_order(1, 1, Side::Buy),
398 };
399 assert!(matches!(
400 to_trading_event(&req),
401 TradingEvent::SubmitOrder { symbol, .. } if symbol == Symbol(1)
402 ));
403 }
404
405 #[test]
406 fn maps_cancel_order() {
407 let req = Request::CancelOrder {
408 symbol: Symbol(2),
409 account: AccountId(5),
410 order_id: OrderId(42),
411 };
412 assert!(matches!(
413 to_trading_event(&req),
414 TradingEvent::CancelOrder { symbol, account, order_id }
415 if symbol == Symbol(2) && account == AccountId(5) && order_id == OrderId(42)
416 ));
417 }
418
419 #[test]
420 fn maps_cancel_all() {
421 let req = Request::CancelAll {
422 account: AccountId(7),
423 };
424 assert!(matches!(
425 to_trading_event(&req),
426 TradingEvent::CancelAll { account } if account == AccountId(7)
427 ));
428 }
429
430 #[test]
431 fn maps_deposit() {
432 let req = Request::Deposit {
433 account: AccountId(1),
434 currency: CurrencyId(2),
435 amount: 1000,
436 };
437 assert!(matches!(
438 to_trading_event(&req),
439 TradingEvent::Deposit { account, currency, amount }
440 if account == AccountId(1) && currency == CurrencyId(2) && amount == 1000
441 ));
442 }
443
444 #[test]
445 fn maps_add_instrument() {
446 let spec = InstrumentSpec {
447 symbol: Symbol(10),
448 base: CurrencyId(1),
449 quote: CurrencyId(2),
450 };
451 let req = Request::AddInstrument { spec };
452 assert!(matches!(
453 to_trading_event(&req),
454 TradingEvent::AddInstrument { spec: s } if s.symbol == Symbol(10)
455 ));
456 }
457
458 #[test]
459 fn maps_cancel_replace() {
460 let req = Request::CancelReplace {
461 symbol: Symbol(1),
462 account: AccountId(1),
463 order_id: OrderId(5),
464 new_price: Price(NonZeroU64::new(200).unwrap()),
465 new_quantity: Quantity(NonZeroU64::new(50).unwrap()),
466 };
467 assert!(matches!(
468 to_trading_event(&req),
469 TradingEvent::CancelReplace { order_id, .. } if order_id == OrderId(5)
470 ));
471 }
472
473 #[test]
474 fn maps_set_risk_limits() {
475 let req = Request::SetRiskLimits {
476 symbol: Symbol(1),
477 limits: RiskLimits::default(),
478 };
479 assert!(matches!(
480 to_trading_event(&req),
481 TradingEvent::SetRiskLimits { symbol, .. } if symbol == Symbol(1)
482 ));
483 }
484
485 #[test]
486 fn maps_set_circuit_breaker() {
487 let req = Request::SetCircuitBreaker {
488 symbol: Symbol(1),
489 config: CircuitBreakerConfig::default(),
490 };
491 assert!(matches!(
492 to_trading_event(&req),
493 TradingEvent::SetCircuitBreaker { symbol, .. } if symbol == Symbol(1)
494 ));
495 }
496
497 #[test]
498 fn maps_set_fee_schedule() {
499 let req = Request::SetFeeSchedule {
500 symbol: Symbol(3),
501 schedule: FeeSchedule::default(),
502 };
503 assert!(matches!(
504 to_trading_event(&req),
505 TradingEvent::SetFeeSchedule { symbol, .. } if symbol == Symbol(3)
506 ));
507 }
508
509 #[test]
510 fn maps_query_stats() {
511 assert!(matches!(
512 to_trading_event(&Request::QueryStats),
513 TradingEvent::QueryStats
514 ));
515 }
516
517 #[test]
518 #[should_panic(expected = "filtered before to_trading_event")]
519 fn heartbeat_panics_if_not_filtered() {
520 to_trading_event(&Request::Heartbeat);
521 }
522
523 #[test]
524 #[should_panic(expected = "filtered before to_trading_event")]
525 fn challenge_response_panics_if_not_filtered() {
526 to_trading_event(&Request::ChallengeResponse {
527 signature: [0u8; 64],
528 public_key: [0u8; 32],
529 });
530 }
531}