0xPolygonMiden / examples

Examples of using Polygon Miden
MIT License
18 stars 18 forks source link

CLI-based CLOB using the Miden client #206

Open Dominik1999 opened 1 month ago

Dominik1999 commented 1 month ago

Let's build a CLI-based order book

Do some research and tests first - Scripts to set up the order book

The program - Wrapper around the Miden client with a CLI

Collect feedback

Dominik1999 commented 1 month ago

We don't need to focus too much on UX. It is mostly a showcase for Composability Labs on how to build a simple order book.

In my mind, a user would simply type the order type buy/sell and the amount (and price) 50 Asset1 to 1000 Asset2.

So in the CLI, I would expect:

An example of a full command would be order buy 50 0xa0e61d8a3f8b50be 1000 0xb1d43d1h5d6z46gj

Ideally, we would replace the <fauvet ud> with a shorter "unique" identifier like POL - at least for the showcase.

Dominik1999 commented 1 month ago

@phklive for the matching algorithm:

So the user should by able to type the CLI command: buy X ETH for Y BTC.

Then the program starts creating a sub-set of the sorted ETH order book (by price). Assuming that the order book is sorted ascending, so the cheapest price is on top, the order book looks like this

| Note ID    | Requested Asset | Amount Requested | Containing Asset | Amount Containing | Price   |
|------------|-----------------|------------------|------------------|-------------------|---------|
| 0x7y8z9a   | ETH             | 199              | BTC              | 10                | 19.90   |
| 0x1o2p3q   | ETH             | 786              | BTC              | 20                | 39.30   |
| 0x4x5y6z   | ETH             | 814              | BTC              | 20                | 40.70   |
| 0x4q5r6s   | ETH             | 320              | BTC              | 8                 | 40.00   |
| 0x1m2n3o   | ETH             | 585              | BTC              | 14                | 41.79   |
| 0x7r8s9t   | ETH             | 720              | BTC              | 17                | 42.35   |
| 0x1w2x3y   | ETH             | 127              | BTC              | 3                 | 42.33   |
| 0x7t8u9v   | ETH             | 590              | BTC              | 13                | 45.38   |
| 0x7g8h9i   | ETH             | 965              | BTC              | 21                | 45.95   |
| 0x4y5z6a   | ETH             | 992              | BTC              | 22                | 45.09   |
| 0x1l2m3n   | ETH             | 394              | BTC              | 8                 | 49.25   |
| 0x7l8m9n   | ETH             | 523              | BTC              | 11                | 47.55   |
| 0x4r5s6t   | ETH             | 619              | BTC              | 14                | 44.21   |
| 0x1n2o3p   | ETH             | 762              | BTC              | 17                | 44.82   |
| 0x4w5x6y   | ETH             | 671              | BTC              | 15                | 44.73   |
| 0x4d5e6f   | ETH             | 312              | BTC              | 7                 | 44.57   |
| 0x4o5p6q   | ETH             | 275              | BTC              | 6                 | 45.83   |
| 0x7p8q9r   | ETH             | 234              | BTC              | 3                 | 78.00   |
| 0x7s8t9u   | ETH             | 369              | BTC              | 5                 | 73.80   |
| 0x1s2t3u   | ETH             | 405              | BTC              | 9                 | 45.00   |
| 0x4v5w6x   | ETH             | 582              | BTC              | 19                | 30.63   |
| 0x4e5f6g   | ETH             | 150              | BTC              | 5                 | 30.00   |
| 0x7h8i9j   | ETH             | 926              | BTC              | 16                | 57.88   |
| 0x1f2g3h   | ETH             | 800              | BTC              | 14                | 57.14   |
| 0x1k2l3m   | ETH             | 354              | BTC              | 6                 | 59.00   |
| 0x7j8k9l   | ETH             | 763              | BTC              | 15                | 50.87   |
| 0x1b2c3d   | ETH             | 633              | BTC              | 12                | 52.75   |
| 0x7a8b9c   | ETH             | 151              | BTC              | 3                 | 50.33   |
| 0x1d2e3f   | ETH             | 972              | BTC              | 19                | 51.16   |
| 0x1u2v3w   | ETH             | 509              | BTC              | 9                 | 56.56   |
| 0x4f5g6h   | ETH             | 984              | BTC              | 18                | 54.67   |
| 0x1d2e4f   | ETH             | 523              | BTC              | 10                | 51.30   |
| 0x7b8c9d   | ETH             | 991              | BTC              | 18                | 55.06   |
| 0x4z5a6b   | ETH             | 991              | BTC              | 18                | 55.06   |
| 0x1l3m5o   | ETH             | 750              | BTC              | 21                | 35.71   |
| 0x7r3p7m   | ETH             | 850              | BTC              | 29                | 31.45   |
| 0x1e3f3t   | ETH             | 120              | BTC              | 17                | 12.50   |
| 

So our simple matching algorithm starts on the top of the list and sees if it can fulfill the order for the requested price or cheaper.

use std::collections::HashMap;

#[derive(Debug, Clone)]
struct Order {
    note_id: String,
    amount_requested: f64,  // ETH
    amount_contained: f64,  // BTC
    price: f64,             // Price = ETH/BTC
}

impl Order {
    fn new(note_id: &str, amount_requested: f64, amount_contained: f64) -> Self {
        let price = amount_requested / amount_contained;
        Order {
            note_id: note_id.to_string(),
            amount_requested,
            amount_contained,
            price,
        }
    }
}

fn match_order(order_book: &[Order], buy_eth: f64, btc_offered: f64) -> Result<(Vec<String>, f64), bool> {
    let mut remaining_eth = buy_eth;
    let mut total_btc_needed = 0.0;
    let mut matched_orders: Vec<String> = Vec::new();
    let mut total_price = 0.0;

    // Sort the order book by price in ascending order
    let mut sorted_orders = order_book.to_vec();
    sorted_orders.sort_by(|a, b| a.price.partial_cmp(&b.price).unwrap());

    for order in sorted_orders.iter() {
        if remaining_eth <= 0.0 {
            break;
        }
        let eth_to_buy = remaining_eth.min(order.amount_requested);
        let btc_needed = eth_to_buy / order.price;

        if total_btc_needed + btc_needed > btc_offered {
            return Err(false);
        }

        remaining_eth -= eth_to_buy;
        total_btc_needed += btc_needed;
        total_price += order.price * eth_to_buy;
        matched_orders.push(order.note_id.clone());
    }

    if remaining_eth > 0.0 {
        Err(false)  // Not enough orders to fulfill the request
    } else {
        let average_price = total_price / buy_eth;
        Ok((matched_orders, average_price))
    }
}

fn main() {
    // Sample order book
    let order_book = vec![
        Order::new("0x1a2b3c", 100.0, 2.0),
        Order::new("0x1d2e3f", 200.0, 4.0),
        Order::new("0x1g2h3i", 150.0, 3.5),
        Order::new("0x1j2k3l", 50.0, 1.0),
    ];

    // Example: Buying 250 ETH for 7 BTC
    let buy_eth = 250.0;
    let btc_offered = 7.0;

    match match_order(&order_book, buy_eth, btc_offered) {
        Ok((matched_orders, avg_price)) => {
            println!("Order fulfilled with NoteIDs: {:?} at an average price of {:.2}", matched_orders, avg_price);
        },
        Err(_) => {
            println!("Order cannot be fulfilled.");
        }
    }
}

I created this with ChatGTP and reviewed, it looks correct. You probably need u64 instead of f64.

partylikeits1983 commented 1 month ago

SWAPp note currently works and has basic tests in this repository here: https://github.com/compolabs/spark-miden-v1/tree/main/tests/mock_integration

The CLI currently computes the expected P2ID of the SWAP note before it is consumed. This is fine, but will only work for the standard SWAP note, not the partially fillable SWAP note.

Example: User 1 is selling 10 tokens A for 10 tokens B. We don't know how many tokens A User 2 wants in the future. When User 2 builds the transaction, "I want 5 tokens A", then we will know the output of the SWAPp note.

Right now, this is a peer to peer order book. This is fine for the first PoC version, but in the future, we will need to make some sort of "clearing house" or "matcher" contract that can consume multiple SWAPp orders and fill them.

Dominik1999 commented 1 month ago

Let's change the computation of the expected P2ID then. Let's work in parallel. @phklive can make it work end-to-end with the SWAP note. @part can start integrating the SWAPp note, first in the order book creation.

When we integrate the SWAPp note, we must adjust the matching algorithm as well.