o2-tools 0.4.7-rc4

Reusable tooling for trade account and order book contract interactions on Fuel
//! Actual Turbo accounts routed through a House-only tier, using production pool code.

use super::*;
use crate::{
    house_order_book::{
        HOUSE_ROLE,
        HouseOrderBookDeploy,
        HouseOrderBookDeployConfig,
    },
    prop::PropOrderBookCleanup,
};

const UNIT: u64 = 1_000_000_000;
const PRICE: u64 = 2_000_000;

async fn house_pair(fixture: &PropFixture) -> HouseOrderBookDeploy<Wallet> {
    let house = HouseOrderBookDeploy::deploy(
        &fixture.deployer,
        &fixture.order_book.order_book,
        &HouseOrderBookDeployConfig::default(),
    )
    .await
    .unwrap();
    house
        .canonical
        .methods()
        .set_sidecar(Some(house.book.contract_id))
        .with_contracts(&[&house.house])
        .call()
        .await
        .unwrap();
    house
        .canonical
        .methods()
        .grant_role(HOUSE_ROLE, Identity::Address(fixture.deployer.address()))
        .call()
        .await
        .unwrap();
    house
}

async fn source_order(
    fixture: &PropFixture,
    house: &HouseOrderBookDeploy<Wallet>,
    price: u64,
    quantity: u64,
    buy: bool,
) {
    let (asset, coins) = if buy {
        (
            fixture.collateral_asset,
            (quantity as u128 * price as u128 / UNIT as u128) as u64,
        )
    } else {
        (fixture.base_asset, quantity)
    };
    house
        .canonical
        .methods()
        .create_order(OrderArgs {
            price,
            quantity,
            order_type: OrderType::TurboSharedPostOnly,
        })
        .with_contract_ids(&house.contract_ids())
        .call_params(CallParameters::new(coins, asset, 30_000_000))
        .unwrap()
        .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
        .call()
        .await
        .unwrap();
}

fn dependencies(
    fixture: &PropFixture,
    house: &HouseOrderBookDeploy<Wallet>,
) -> Vec<ContractId> {
    vec![
        fixture.deployment.oracle_id,
        fixture.deployment.pool_id,
        fixture.deployment.registry_id,
        fixture.deployment.price_feed_id,
        fixture.child_id,
        house.book.contract_id,
        house.canonical.contract_id(),
    ]
}

#[tokio::test]
async fn house_turbo_account_cannot_bypass_route_and_pool_values_only_its_own_assets() {
    let mut fixture = PropFixture::new().await;
    let house = house_pair(&fixture).await;
    fixture.close_session().await;
    fixture.order_book = house.book.clone();
    publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
    start_turbo_session(&fixture).await;
    let deps = dependencies(&fixture, &house);
    source_order(&fixture, &house, PRICE, 10 * UNIT, false).await;
    let draw = fixture.draw_call(6_000_000);
    fixture
        .call_account_with_contract_ids(vec![draw], &deps)
        .await
        .unwrap();
    let initial_value = fixture.session_value().await;
    let mut bypass = fixture.book_call(PRICE, UNIT, fixture.collateral_asset, PRICE);
    bypass.contract_id = house.canonical.contract_id();
    let error = fixture
        .call_account_with_contract_ids(vec![bypass], &deps)
        .await
        .unwrap_err();
    assert!(error.to_string().contains("TargetNotAllowed"), "{error:#}");
    let account_base_before = fixture.account_balance(fixture.base_asset).await;
    let buy = fixture.book_call(PRICE, UNIT, fixture.collateral_asset, PRICE);
    let bought = fixture
        .call_account_with_contract_ids(vec![buy], &deps)
        .await
        .unwrap();
    let fills = house
        .decode_shared_executions(&bought.tx_status.receipts)
        .unwrap();
    assert_eq!(fills.len(), 1);
    assert_eq!(fills[0].quantity, UNIT);
    assert_eq!(fills[0].price, PRICE);
    assert_eq!(
        house
            .house
            .methods()
            .get_total_balance_of(Identity::ContractId(fixture.child_id))
            .call()
            .await
            .unwrap()
            .value,
        (0, 0)
    );
    assert_eq!(
        fixture.account_balance(fixture.base_asset).await,
        account_base_before + UNIT
    );
    assert_eq!(
        house
            .canonical
            .methods()
            .get_total_balance_of(Identity::ContractId(fixture.child_id))
            .call()
            .await
            .unwrap()
            .value,
        (0, 0)
    );
    assert_eq!(fixture.session_value().await, initial_value);
    // A Turbo bid contributes no sellable House depth, even though it is funded.
    let rest = fixture.book_call(1_900_000, UNIT, fixture.collateral_asset, 1_900_000);
    fixture
        .call_account_with_contract_ids(vec![rest], &deps)
        .await
        .unwrap();
    let error = fixture
        .deployment
        .pool
        .methods()
        .min_sellable(fixture.child_id, fixture.base_asset)
        .with_contract_ids(&deps)
        .call()
        .await
        .unwrap_err();
    assert!(error.to_string().contains("NoBestBid"), "{error}");
    source_order(&fixture, &house, 1_800_000, UNIT, true).await;
    let minimum = fixture
        .deployment
        .pool
        .methods()
        .min_sellable(fixture.child_id, fixture.base_asset)
        .with_contract_ids(&deps)
        .call()
        .await
        .unwrap()
        .value;
    assert_eq!(minimum, Some(556));
    assert_eq!(fixture.session_value().await, initial_value);

    let resting = house
        .house
        .methods()
        .get_orders_at_price(crate::order_book_deploy::Side::Buy, 1_900_000)
        .with_contract_ids(&house.contract_ids())
        .call()
        .await
        .unwrap()
        .value[0]
        .order_id;
    let cleanup = PropOrderBookCleanup {
        order_book: house.book.contract_id,
        order_ids: vec![resting],
    };
    // Clear the pool's cash loan while keeping the funded book position for cleanup.
    fixture.fund_collateral(6_000_000).await;
    let repay = fixture.repay_from_collateral_call(6_000_000);
    fixture
        .call_account_with_contract_ids(vec![repay], &deps)
        .await
        .unwrap();
    house.house.methods().pause().call().await.unwrap();
    let error = fixture
        .account
        .methods()
        .close_session(vec![cleanup.clone()])
        .with_contracts(&[&house.house, &fixture.deployment.pool])
        .with_contract_ids(&deps)
        .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
        .call()
        .await
        .unwrap_err();
    assert!(error.to_string().contains("Paused"), "{error}");
    assert!(fixture.session_opt().await.is_some());
    house.house.methods().unpause().call().await.unwrap();
    fixture
        .account
        .methods()
        .close_session(vec![cleanup])
        .with_contracts(&[&house.house, &fixture.deployment.pool])
        .with_contract_ids(&deps)
        .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
        .call()
        .await
        .unwrap();
    assert!(fixture.session_opt().await.is_none());
    assert_eq!(
        house
            .house
            .methods()
            .get_total_balance_of(Identity::ContractId(fixture.child_id))
            .call()
            .await
            .unwrap()
            .value,
        (0, 0)
    );
    assert_eq!(
        house
            .canonical
            .methods()
            .get_total_balance_of(Identity::Address(fixture.deployer.address()))
            .call()
            .await
            .unwrap()
            .value,
        (9 * UNIT, 3_800_000)
    );
}

#[tokio::test]
async fn house_tier_publication_preserves_existing_session_cleanup_until_reopening() {
    let mut fixture = PropFixture::new().await;
    publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
    open_turbo_session(&fixture).await;
    let canonical = fixture.order_book.clone();
    let draw = fixture.draw_call(1_000_000);
    let rest = fixture.book_call(1_000_000, UNIT, fixture.collateral_asset, 1_000_000);
    fixture.call_account(vec![draw, rest], 0).await.unwrap();
    let order = canonical
        .order_book
        .methods()
        .get_orders_at_price(crate::order_book_deploy::Side::Buy, 1_000_000)
        .call()
        .await
        .unwrap()
        .value[0]
        .order_id;
    let house = house_pair(&fixture).await;
    fixture.order_book = house.book.clone();
    let version = publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
    let books = fixture
        .deployment
        .pool
        .methods()
        .session_books(fixture.child_id)
        .call()
        .await
        .unwrap()
        .value;
    assert_eq!(books, vec![canonical.contract_id]);
    assert!(fixture.session().await.tier_version < version);
    let deps = dependencies(&fixture, &house);
    fixture.fund_collateral(1_000_000).await;
    let repay = fixture.repay_from_collateral_call(1_000_000);
    fixture
        .call_account_with_contract_ids(vec![repay], &deps)
        .await
        .unwrap();
    fixture
        .account
        .methods()
        .close_session(vec![PropOrderBookCleanup {
            order_book: canonical.contract_id,
            order_ids: vec![order],
        }])
        .with_contracts(&[&canonical.order_book, &fixture.deployment.pool])
        .with_contract_ids(&deps)
        .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
        .call()
        .await
        .unwrap();
    assert_eq!(
        canonical
            .order_book
            .methods()
            .get_total_balance_of(Identity::ContractId(fixture.child_id))
            .call()
            .await
            .unwrap()
            .value,
        (0, 0)
    );
    start_turbo_session(&fixture).await;
    assert_eq!(fixture.session().await.tier_version, version);
    assert_eq!(
        fixture
            .deployment
            .pool
            .methods()
            .session_books(fixture.child_id)
            .call()
            .await
            .unwrap()
            .value,
        vec![house.book.contract_id]
    );
    assert!(
        !fixture
            .deployment
            .pool
            .methods()
            .is_call_allowed(fixture.child_id, canonical.contract_id)
            .call()
            .await
            .unwrap()
            .value
    );
}