Skip to main content

pot_o_extensions/
marketplace.rs

1use std::collections::HashMap;
2
3use pot_o_core::TokenType;
4use serde::{Deserialize, Serialize};
5
6/// Asset identifier — either a ledger token or a bridged Solana mint.
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum MarketAsset {
9    /// A native ledger token.
10    Token(TokenType),
11    /// A bridged Solana SPL token mint address.
12    Spl(String),
13    /// Native SOL (bridged).
14    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/// Buy or sell side.
28#[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/// Order status.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45pub enum OrderStatus {
46    Open,
47    PartiallyFilled,
48    Filled,
49    Cancelled,
50}
51
52/// A single order in the marketplace.
53#[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    /// Total amount of sell_asset the maker wants to sell.
61    pub amount: u64,
62    /// Price in buy_asset per unit of sell_asset (fixed-price limit order).
63    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/// A filled trade record.
76#[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    /// Amount of sell_asset filled.
85    pub amount: u64,
86    /// Price at execution.
87    pub price: u64,
88    /// Total cost in buy_asset.
89    pub total: u64,
90    pub timestamp: u64,
91}
92
93/// Order book for a specific asset pair (sell_asset → buy_asset).
94#[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
102/// The marketplace engine.
103pub struct Marketplace {
104    orders: HashMap<String, Order>,
105    trades: Vec<Trade>,
106    next_id: u64,
107    /// Fee in basis points (e.g. 25 = 0.25%) charged to the filled side.
108    pub fee_bps: u64,
109    /// Protocol address that collects marketplace fees.
110    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    /// Place a limit order. Returns the order ID.
131    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    /// Cancel an open order. Returns `true` if found and cancelled.
158    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    /// Get a single order.
169    pub fn get_order(&self, order_id: &str) -> Option<&Order> {
170        self.orders.get(order_id)
171    }
172
173    /// Get the order book for a given asset pair.
174    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        // Sort bids descending by price, asks ascending by price
190        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    /// Get all orders for a maker.
201    pub fn orders_for_maker(&self, maker: &str) -> Vec<&Order> {
202        self.orders.values().filter(|o| o.maker == maker).collect()
203    }
204
205    /// Try to match a new sell order against existing buy orders (and vice versa).
206    /// Returns the trades that were executed.
207    /// This is a simple price-time priority matching engine.
208    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        // Collect matching counter-orders from the same asset pair
221        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        // Sort by price priority, then timestamp (older first)
239        match side {
240            OrderSide::Sell => {
241                // Counter is Buy orders: highest price first, then oldest
242                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 is Sell orders: lowest price first, then oldest
252                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            // Deduct from counter order
279            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    /// Place an order and immediately attempt to match it.
332    /// Returns the order ID and any trades that were executed.
333    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    /// Get all trades.
370    pub fn trades(&self) -> &[Trade] {
371        &self.trades
372    }
373
374    /// Get all open orders.
375    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        // Second cancel should fail
445        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        // Pair: TribeChain/PTtC (sell_asset=TribeChain=base, buy_asset=PTtC=quote)
509        // alice buys 50 TribeChain at price 10 (pays 10 PTtC per TribeChain)
510        m.place_order(
511            "alice",
512            OrderSide::Buy,
513            test_sell_asset(),
514            test_buy_asset(),
515            50,
516            10,
517        );
518        // bob sells 30 TribeChain at price 10 (receives 10 PTtC per TribeChain)
519        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        // alice's order should be partially filled (bought 30 of her 50)
530        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        // bob wants to sell at 11, but highest buy is 10 — no match
548        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}