1use std::collections::HashMap;
2
3use pot_o_core::TokenType;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum MarketAsset {
9 Token(TokenType),
11 Spl(String),
13 Sol,
15}
16
17impl std::fmt::Display for MarketAsset {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 match self {
20 MarketAsset::Token(t) => write!(f, "{t:?}"),
21 MarketAsset::Sol => write!(f, "SOL"),
22 MarketAsset::Spl(mint) => write!(f, "spl:{mint}"),
23 }
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29pub enum OrderSide {
30 Buy,
31 Sell,
32}
33
34impl std::fmt::Display for OrderSide {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 match self {
37 OrderSide::Buy => write!(f, "buy"),
38 OrderSide::Sell => write!(f, "sell"),
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45pub enum OrderStatus {
46 Open,
47 PartiallyFilled,
48 Filled,
49 Cancelled,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct Order {
55 pub id: String,
56 pub maker: String,
57 pub side: OrderSide,
58 pub sell_asset: MarketAsset,
59 pub buy_asset: MarketAsset,
60 pub amount: u64,
62 pub price: u64,
64 pub filled: u64,
65 pub status: OrderStatus,
66 pub timestamp: u64,
67}
68
69impl Order {
70 pub fn remaining(&self) -> u64 {
71 self.amount.saturating_sub(self.filled)
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct Trade {
78 pub buy_order_id: String,
79 pub sell_order_id: String,
80 pub maker_buy: String,
81 pub maker_sell: String,
82 pub sell_asset: MarketAsset,
83 pub buy_asset: MarketAsset,
84 pub amount: u64,
86 pub price: u64,
88 pub total: u64,
90 pub timestamp: u64,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct OrderBook {
96 pub sell_asset: MarketAsset,
97 pub buy_asset: MarketAsset,
98 pub bids: Vec<Order>,
99 pub asks: Vec<Order>,
100}
101
102pub struct Marketplace {
104 orders: HashMap<String, Order>,
105 trades: Vec<Trade>,
106 next_id: u64,
107 pub fee_bps: u64,
109 pub protocol_fee_address: String,
111}
112
113impl Marketplace {
114 pub fn new(fee_bps: u64, protocol_fee_address: String) -> Self {
115 Self {
116 orders: HashMap::new(),
117 trades: Vec::new(),
118 next_id: 1,
119 fee_bps,
120 protocol_fee_address,
121 }
122 }
123
124 fn generate_id(&mut self) -> String {
125 let id = self.next_id;
126 self.next_id += 1;
127 format!("order-{id}")
128 }
129
130 pub fn place_order(
132 &mut self,
133 maker: &str,
134 side: OrderSide,
135 sell_asset: MarketAsset,
136 buy_asset: MarketAsset,
137 amount: u64,
138 price: u64,
139 ) -> String {
140 let id = self.generate_id();
141 let order = Order {
142 id: id.clone(),
143 maker: maker.to_string(),
144 side,
145 sell_asset,
146 buy_asset,
147 amount,
148 price,
149 filled: 0,
150 status: OrderStatus::Open,
151 timestamp: chrono::Utc::now().timestamp() as u64,
152 };
153 self.orders.insert(id.clone(), order);
154 id
155 }
156
157 pub fn cancel_order(&mut self, order_id: &str) -> bool {
159 if let Some(order) = self.orders.get_mut(order_id) {
160 if order.status == OrderStatus::Open || order.status == OrderStatus::PartiallyFilled {
161 order.status = OrderStatus::Cancelled;
162 return true;
163 }
164 }
165 false
166 }
167
168 pub fn get_order(&self, order_id: &str) -> Option<&Order> {
170 self.orders.get(order_id)
171 }
172
173 pub fn order_book(&self, sell_asset: &MarketAsset, buy_asset: &MarketAsset) -> OrderBook {
175 let mut bids = Vec::new();
176 let mut asks = Vec::new();
177 for order in self.orders.values() {
178 if order.status == OrderStatus::Cancelled || order.status == OrderStatus::Filled {
179 continue;
180 }
181 if order.sell_asset != *sell_asset || order.buy_asset != *buy_asset {
182 continue;
183 }
184 match order.side {
185 OrderSide::Buy => bids.push(order.clone()),
186 OrderSide::Sell => asks.push(order.clone()),
187 }
188 }
189 bids.sort_by_key(|b| std::cmp::Reverse(b.price));
191 asks.sort_by_key(|a| a.price);
192 OrderBook {
193 sell_asset: sell_asset.clone(),
194 buy_asset: buy_asset.clone(),
195 bids,
196 asks,
197 }
198 }
199
200 pub fn orders_for_maker(&self, maker: &str) -> Vec<&Order> {
202 self.orders.values().filter(|o| o.maker == maker).collect()
203 }
204
205 pub fn try_match(
209 &mut self,
210 maker: &str,
211 side: OrderSide,
212 sell_asset: &MarketAsset,
213 buy_asset: &MarketAsset,
214 mut amount: u64,
215 price: u64,
216 ) -> Vec<Trade> {
217 let mut executed_trades = Vec::new();
218 let now = chrono::Utc::now().timestamp() as u64;
219
220 let mut counter_orders: Vec<String> = self
222 .orders
223 .iter()
224 .filter(|(_, o)| {
225 o.maker != maker
226 && (o.status == OrderStatus::Open || o.status == OrderStatus::PartiallyFilled)
227 && o.sell_asset == *sell_asset
228 && o.buy_asset == *buy_asset
229 && match (side, o.side) {
230 (OrderSide::Sell, OrderSide::Buy) => o.price >= price,
231 (OrderSide::Buy, OrderSide::Sell) => o.price <= price,
232 _ => false,
233 }
234 })
235 .map(|(id, _)| id.clone())
236 .collect();
237
238 match side {
240 OrderSide::Sell => {
241 counter_orders.sort_by(|a, b| {
243 let oa = &self.orders[a];
244 let ob = &self.orders[b];
245 ob.price
246 .cmp(&oa.price)
247 .then_with(|| oa.timestamp.cmp(&ob.timestamp))
248 });
249 }
250 OrderSide::Buy => {
251 counter_orders.sort_by(|a, b| {
253 let oa = &self.orders[a];
254 let ob = &self.orders[b];
255 oa.price
256 .cmp(&ob.price)
257 .then_with(|| oa.timestamp.cmp(&ob.timestamp))
258 });
259 }
260 }
261
262 for counter_id in counter_orders {
263 if amount == 0 {
264 break;
265 }
266 let filled_amount;
267 let exec_price;
268 {
269 let counter = self.orders.get(&counter_id).unwrap();
270 let available = counter.remaining();
271 if available == 0 {
272 continue;
273 }
274 filled_amount = amount.min(available);
275 exec_price = counter.price;
276 }
277
278 if let Some(counter) = self.orders.get_mut(&counter_id) {
280 counter.filled = counter.filled.saturating_add(filled_amount);
281 if counter.filled >= counter.amount {
282 counter.status = OrderStatus::Filled;
283 } else {
284 counter.status = OrderStatus::PartiallyFilled;
285 }
286 }
287
288 let total_cost = filled_amount.saturating_mul(exec_price);
289
290 let (buy_order_id, sell_order_id, maker_buy, maker_sell) = match side {
291 OrderSide::Sell => {
292 let co = &self.orders[&counter_id];
293 (
294 counter_id.clone(),
295 String::new(),
296 co.maker.clone(),
297 maker.to_string(),
298 )
299 }
300 OrderSide::Buy => {
301 let co = &self.orders[&counter_id];
302 (
303 String::new(),
304 counter_id.clone(),
305 maker.to_string(),
306 co.maker.clone(),
307 )
308 }
309 };
310
311 let trade = Trade {
312 buy_order_id,
313 sell_order_id,
314 maker_buy,
315 maker_sell,
316 sell_asset: sell_asset.clone(),
317 buy_asset: buy_asset.clone(),
318 amount: filled_amount,
319 price: exec_price,
320 total: total_cost,
321 timestamp: now,
322 };
323 executed_trades.push(trade);
324 amount = amount.saturating_sub(filled_amount);
325 }
326
327 self.trades.extend(executed_trades.clone());
328 executed_trades
329 }
330
331 pub fn place_and_match(
334 &mut self,
335 maker: &str,
336 side: OrderSide,
337 sell_asset: MarketAsset,
338 buy_asset: MarketAsset,
339 amount: u64,
340 price: u64,
341 ) -> (String, Vec<Trade>) {
342 let trades = self.try_match(maker, side, &sell_asset, &buy_asset, amount, price);
343 let filled_in_match: u64 = trades.iter().map(|t| t.amount).sum();
344 let remaining = amount.saturating_sub(filled_in_match);
345
346 let order_id = if remaining > 0 {
347 let id = self.generate_id();
348 let order = Order {
349 id: id.clone(),
350 maker: maker.to_string(),
351 side,
352 sell_asset,
353 buy_asset,
354 amount: remaining,
355 price,
356 filled: 0,
357 status: OrderStatus::Open,
358 timestamp: chrono::Utc::now().timestamp() as u64,
359 };
360 self.orders.insert(id.clone(), order);
361 id
362 } else {
363 String::new()
364 };
365
366 (order_id, trades)
367 }
368
369 pub fn trades(&self) -> &[Trade] {
371 &self.trades
372 }
373
374 pub fn open_orders(&self) -> Vec<&Order> {
376 self.orders
377 .values()
378 .filter(|o| o.status == OrderStatus::Open || o.status == OrderStatus::PartiallyFilled)
379 .collect()
380 }
381}
382
383pub fn parse_market_asset(s: &str) -> Result<MarketAsset, String> {
384 let lower = s.to_lowercase();
385 if lower == "sol" {
386 return Ok(MarketAsset::Sol);
387 }
388 if let Some(mint) = lower.strip_prefix("spl:") {
389 return Ok(MarketAsset::Spl(mint.to_string()));
390 }
391 match lower.as_str() {
392 "tribechain" | "native" => Ok(MarketAsset::Token(TokenType::TribeChain)),
393 "pttc" => Ok(MarketAsset::Token(TokenType::PTtC)),
394 "nmtc" => Ok(MarketAsset::Token(TokenType::NMTC)),
395 "stomp" => Ok(MarketAsset::Token(TokenType::STOMP)),
396 "aum" => Ok(MarketAsset::Token(TokenType::AUM)),
397 "ai3" => Ok(MarketAsset::Token(TokenType::AI3)),
398 _ => Err(format!("Unknown asset: {s}")),
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405
406 fn test_sell_asset() -> MarketAsset {
407 MarketAsset::Token(TokenType::TribeChain)
408 }
409
410 fn test_buy_asset() -> MarketAsset {
411 MarketAsset::Token(TokenType::PTtC)
412 }
413
414 #[test]
415 fn test_place_and_get_order() {
416 let mut m = Marketplace::new(25, "fee_addr".into());
417 let id = m.place_order(
418 "alice",
419 OrderSide::Sell,
420 test_sell_asset(),
421 test_buy_asset(),
422 100,
423 10,
424 );
425 let order = m.get_order(&id).unwrap();
426 assert_eq!(order.maker, "alice");
427 assert_eq!(order.amount, 100);
428 assert_eq!(order.remaining(), 100);
429 }
430
431 #[test]
432 fn test_cancel_order() {
433 let mut m = Marketplace::new(25, "fee_addr".into());
434 let id = m.place_order(
435 "alice",
436 OrderSide::Sell,
437 test_sell_asset(),
438 test_buy_asset(),
439 100,
440 10,
441 );
442 assert!(m.cancel_order(&id));
443 assert_eq!(m.get_order(&id).unwrap().status, OrderStatus::Cancelled);
444 assert!(!m.cancel_order(&id));
446 }
447
448 #[test]
449 fn test_order_book() {
450 let mut m = Marketplace::new(25, "fee_addr".into());
451 m.place_order(
452 "alice",
453 OrderSide::Sell,
454 test_sell_asset(),
455 test_buy_asset(),
456 100,
457 10,
458 );
459 m.place_order(
460 "bob",
461 OrderSide::Buy,
462 test_sell_asset(),
463 test_buy_asset(),
464 50,
465 9,
466 );
467 let ob = m.order_book(&test_sell_asset(), &test_buy_asset());
468 assert_eq!(ob.asks.len(), 1);
469 assert_eq!(ob.bids.len(), 1);
470 }
471
472 #[test]
473 fn test_orders_for_maker() {
474 let mut m = Marketplace::new(25, "".into());
475 m.place_order(
476 "alice",
477 OrderSide::Sell,
478 test_sell_asset(),
479 test_buy_asset(),
480 100,
481 10,
482 );
483 m.place_order(
484 "alice",
485 OrderSide::Buy,
486 test_sell_asset(),
487 test_buy_asset(),
488 50,
489 9,
490 );
491 m.place_order(
492 "bob",
493 OrderSide::Sell,
494 test_sell_asset(),
495 test_buy_asset(),
496 30,
497 8,
498 );
499 let alice_orders = m.orders_for_maker("alice");
500 assert_eq!(alice_orders.len(), 2);
501 let bob_orders = m.orders_for_maker("bob");
502 assert_eq!(bob_orders.len(), 1);
503 }
504
505 #[test]
506 fn test_matching_buy_sell() {
507 let mut m = Marketplace::new(25, "".into());
508 m.place_order(
511 "alice",
512 OrderSide::Buy,
513 test_sell_asset(),
514 test_buy_asset(),
515 50,
516 10,
517 );
518 let trades = m.try_match(
520 "bob",
521 OrderSide::Sell,
522 &test_sell_asset(),
523 &test_buy_asset(),
524 30,
525 10,
526 );
527 assert_eq!(trades.len(), 1);
528 assert_eq!(trades[0].amount, 30);
529 assert_eq!(m.orders_for_maker("alice").len(), 1);
531 let alice_order = m.orders_for_maker("alice")[0];
532 assert_eq!(alice_order.filled, 30);
533 assert_eq!(alice_order.status, OrderStatus::PartiallyFilled);
534 }
535
536 #[test]
537 fn test_matching_no_match_when_price_wrong() {
538 let mut m = Marketplace::new(25, "".into());
539 m.place_order(
540 "alice",
541 OrderSide::Buy,
542 test_sell_asset(),
543 test_buy_asset(),
544 50,
545 10,
546 );
547 let trades = m.try_match(
549 "bob",
550 OrderSide::Sell,
551 &test_sell_asset(),
552 &test_buy_asset(),
553 30,
554 11,
555 );
556 assert_eq!(trades.len(), 0);
557 }
558
559 #[test]
560 fn test_place_and_match_full_fill() {
561 let mut m = Marketplace::new(25, "".into());
562 m.place_order(
563 "alice",
564 OrderSide::Buy,
565 test_sell_asset(),
566 test_buy_asset(),
567 50,
568 10,
569 );
570 let (order_id, trades) = m.place_and_match(
571 "bob",
572 OrderSide::Sell,
573 test_sell_asset(),
574 test_buy_asset(),
575 50,
576 10,
577 );
578 assert_eq!(trades.len(), 1);
579 assert_eq!(trades[0].amount, 50);
580 assert!(order_id.is_empty());
581 }
582
583 #[test]
584 fn test_place_and_match_partial_fill() {
585 let mut m = Marketplace::new(25, "".into());
586 m.place_order(
587 "alice",
588 OrderSide::Buy,
589 test_sell_asset(),
590 test_buy_asset(),
591 50,
592 10,
593 );
594 let (order_id, trades) = m.place_and_match(
595 "bob",
596 OrderSide::Sell,
597 test_sell_asset(),
598 test_buy_asset(),
599 100,
600 10,
601 );
602 assert_eq!(trades.len(), 1);
603 assert_eq!(trades[0].amount, 50);
604 let order = m.get_order(&order_id).unwrap();
605 assert_eq!(order.amount, 50);
606 assert_eq!(order.status, OrderStatus::Open);
607 }
608
609 #[test]
610 fn test_parse_market_asset() {
611 assert!(matches!(
612 parse_market_asset("tribechain").unwrap(),
613 MarketAsset::Token(TokenType::TribeChain)
614 ));
615 assert!(matches!(
616 parse_market_asset("SOL").unwrap(),
617 MarketAsset::Sol
618 ));
619 assert!(
620 matches!(parse_market_asset("spl:abc123").unwrap(), MarketAsset::Spl(m) if m == "abc123")
621 );
622 assert!(parse_market_asset("unknown").is_err());
623 }
624
625 #[test]
626 fn test_parse_market_asset_case_insensitive() {
627 assert!(matches!(
628 parse_market_asset("TribeChain").unwrap(),
629 MarketAsset::Token(TokenType::TribeChain)
630 ));
631 assert!(matches!(
632 parse_market_asset("sol").unwrap(),
633 MarketAsset::Sol
634 ));
635 assert!(
636 matches!(parse_market_asset("SPL:XYZ").unwrap(), MarketAsset::Spl(m) if m == "xyz")
637 );
638 }
639}