perpl-sdk 0.2.5

Rust SDK for the Perpl decentralized perpetuals exchange
Documentation
use std::time::{Duration, Instant};

use fastnum::{UD64, udec64, udec128};
use perpl_sdk::{state, testing, types};

/// Tests the creation of exchange snapshot when perpetual order book is full.
#[tokio::test]
async fn test_full_book_snapshot() {
    let exchange = testing::TestExchange::new().await;
    let maker = exchange.account(0, 1_000_000).await;
    let taker = exchange.account(1, 100_000).await;
    let btc_perp = exchange.btc_perp().await;

    // Fill the whole book with orders
    let started_at = Instant::now();
    let size = udec64!(0.001);
    let leverage = udec64!(10);
    let mut pending_txs = vec![];
    for (chunk, levels) in (1..32768)
        .map(|offs| {
            (UD64::from(100100u64 + offs / 100 * 100), UD64::from(99900u64 - offs / 100 * 100))
        })
        .collect::<Vec<_>>()
        .chunks(50)
        .enumerate()
    {
        let orders = levels
            .iter()
            .enumerate()
            .flat_map(|(level, (ask, bid))| {
                vec![
                    types::OrderRequest::new(
                        chunk as u64 * 100 + level as u64,
                        btc_perp.id,
                        types::RequestType::OpenShort,
                        None,
                        *ask,
                        size,
                        None,
                        true,
                        false,
                        false,
                        None,
                        leverage,
                        None,
                        None,
                        1000,
                    ),
                    types::OrderRequest::new(
                        chunk as u64 * 100 + level as u64 + 1,
                        btc_perp.id,
                        types::RequestType::OpenLong,
                        None,
                        *bid,
                        size,
                        None,
                        true,
                        false,
                        false,
                        None,
                        leverage,
                        None,
                        None,
                        1000,
                    ),
                ]
            })
            .collect();

        pending_txs.push(btc_perp.orders(maker.id, orders).await);
        if chunk % 200 == 0 {
            pending_txs.push(btc_perp.set_mark_price(udec64!(100000)).await);
        }
        tokio::time::sleep(Duration::from_millis(100)).await;
    }
    let receipts =
        futures::future::join_all(pending_txs.into_iter().map(|ptx| ptx.get_receipt())).await;
    for (i, r) in receipts.iter().enumerate() {
        assert!(r.as_ref().unwrap().status(), "#{}: {:#?}", i, r);
    }
    println!("book filled in: {:?}", started_at.elapsed());

    // Do some trades
    btc_perp
        .set_mark_price(udec64!(100000))
        .await
        .get_receipt()
        .await
        .unwrap();
    let receipt = btc_perp
        .orders(
            taker.id,
            vec![
                types::OrderRequest::new(
                    1,
                    btc_perp.id,
                    types::RequestType::OpenLong,
                    None,
                    udec64!(101000),
                    udec64!(0.05),
                    None,
                    false,
                    false,
                    false,
                    None,
                    leverage,
                    None,
                    None,
                    1000,
                ),
                types::OrderRequest::new(
                    2,
                    btc_perp.id,
                    types::RequestType::OpenLong,
                    None,
                    udec64!(101000),
                    udec64!(0.05),
                    None,
                    false,
                    false,
                    false,
                    None,
                    leverage,
                    None,
                    None,
                    1000,
                ),
                types::OrderRequest::new(
                    2,
                    btc_perp.id,
                    types::RequestType::CloseLong,
                    None,
                    udec64!(99500),
                    udec64!(0.01),
                    None,
                    false,
                    false,
                    false,
                    None,
                    leverage,
                    None,
                    None,
                    1000,
                ),
            ],
        )
        .await
        .get_receipt()
        .await
        .unwrap();
    assert!(receipt.status(), "{:#?}", receipt);

    // Take the snapshot
    let started_at = Instant::now();
    let snap = state::SnapshotBuilder::new(&exchange.chain(), exchange.provider.clone())
        .with_accounts(vec![
            types::AccountAddressOrID::Address(maker.address),
            types::AccountAddressOrID::Address(taker.address),
        ])
        .build()
        .await
        .unwrap();
    println!("snapshot taken in: {:?}", started_at.elapsed());

    assert!(
        snap.instant().block_number() > 180,
        "actual block num: {}",
        snap.instant().block_number()
    );
    assert!(!snap.is_halted());
    assert_eq!(snap.perpetuals().len(), 1);
    assert_eq!(snap.accounts().len(), 2);

    let perp = snap.perpetuals().get(&btc_perp.id).unwrap();
    assert!(perp.instant().block_number() > 200);
    assert_eq!(perp.id(), btc_perp.id);
    assert_eq!(perp.name(), "BTC".to_string());
    assert_eq!(perp.symbol(), "BTC".to_string());
    assert!(!perp.is_paused());
    // Base (tier 0) rates of the exchange-wide default schedule, seeded at
    // deployment by `Exchange::_seedDefaultFeeSchedule` - a listing does not set
    // its own fees
    assert_eq!(perp.maker_fee(), udec64!(0.000045));
    assert_eq!(perp.taker_fee(), udec64!(0.000345));
    assert_eq!(perp.initial_margin(), udec64!(10));
    assert_eq!(perp.maintenance_margin(), udec64!(20));
    assert_eq!(perp.last_price(), udec64!(99900));
    assert_eq!(perp.mark_price(), udec64!(100000));
    assert_eq!(perp.funding_start_block(), 8571);
    assert_eq!(perp.open_interest(), udec128!(0.09));

    assert_eq!(perp.total_orders(), 65424);
    // Verify all orders belong to maker account with expected size
    let all_asks_valid = perp
        .l3_book()
        .ask_orders()
        .all(|o| o.account_id() == maker.id && o.size() == udec64!(0.001));
    let all_bids_valid = perp
        .l3_book()
        .bid_orders()
        .all(|o| o.account_id() == maker.id && o.size() == udec64!(0.001));
    assert!(all_asks_valid && all_bids_valid);

    assert_eq!(perp.l3_book().best_ask(), Some((udec64!(100200), udec64!(0.099))));
    assert_eq!(perp.l3_book().best_bid(), Some((udec64!(99900), udec64!(0.089))));

    assert_eq!(
        perp.l3_book().ask_impact(udec64!(1)),
        Some((udec64!(101200), udec64!(1), udec64!(100651)))
    );
    assert_eq!(
        perp.l3_book().bid_impact(udec64!(1)),
        Some((udec64!(98900), udec64!(1), udec64!(99439)))
    );
}