Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI-based CLOB using the Miden client #206

Open
7 of 11 tasks
Dominik1999 opened this issue Oct 1, 2024 · 4 comments
Open
7 of 11 tasks

CLI-based CLOB using the Miden client #206

Dominik1999 opened this issue Oct 1, 2024 · 4 comments

Comments

@Dominik1999
Copy link
Contributor

Dominik1999 commented Oct 1, 2024

Let's build a CLI-based order book

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

  • Deploy a public faucet and mint assets for the 50 notes ASSET1
  • Deploy a second public faucet and mint some assets for the executing account ASSET2
  • Create a script that issues 50 public notes with the same TAG but different inputs. For the first version: use the normal SWAP script.
  • Alter the current script that creates 100 public notes such that we get per market (ETH/BTC or BTC/ETH) 50 different notes. Some can be the same, but not all should be the same.
  • Create SWAPp notes instead

The program - Wrapper around the Miden client with a CLI

  • Create a Rust program in which the user can enter a TAG in the CLI. The program then queries the Miden Node and gets all public notes with the same TAG. As a result, all notes should be listed in the interface.
  • Check if all the information is there to consume the notes and try to consume a note.
  • Be able to print the order book in the CLI. Orders should be ordered by price. This price is denominated in the containing asset and expressed in the requested asset - (requested_asset/asset).
  • The user should be able to enter an order into the CLI. The program returns either (1) the optimal set of orders, or if the order can not be fulfilled, (2) adds the order to the opposite order book - for example, buy 10 ETH for 1 BTC cannot be fulfilled, so a new order will be added to the BTC/ETH order book sell 1 BTC for 10 ETH. This will always result in a transaction.
  • Refactor the code to allow SWAPp note. The computation of the P2ID notes should be different.
  • Add the SWAPp script from Alex. Now we need to use the TransactionRequest, since it is not a standard script

Collect feedback

  • Note down any feedback you have
@Dominik1999
Copy link
Contributor Author

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:

  • order as the command to create a new order
  • buy or sell as the type of the order
  • <amount 1> <faucet id> as the first leg of the trade
  • <amount 2> <faucet id> as the second leg of the trade

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
Copy link
Contributor Author

@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
Copy link

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
Copy link
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants