o2-deploy 0.3.8-rc

Contract deployment logic for Fuel O2 exchange
Documentation
//! Integration test that spins up a local fuel-core node and deploys the full
//! set of O2 contracts via `o2_deploy::deploy`.
//!
//! Run with:
//! ```bash
//! cargo test --features integration-tests -p o2-deploy -- --nocapture
//! ```
#![cfg(feature = "integration-tests")]

use fuel_core::{
    chain_config::{
        ChainConfig,
        StateConfig,
        coin_config_helpers::CoinConfigGenerator,
    },
    service::{
        Config,
        FuelService,
        config::Trigger,
    },
};
use fuel_core_types::fuel_types::AssetId;
use fuels::prelude::{
    Provider,
    Wallet,
    private_key::PrivateKeySigner,
};
use o2_api_types::domain::book::{
    AssetConfig,
    MarketIdAssets,
    OrderBookConfig,
};
use o2_deploy::{
    DeployParams,
    MarketsConfigPartial,
    deploy,
};

/// Fixed secret key for the test wallet (matches fuel-o2 test setup).
const SECRET_KEY: &str =
    "0xde97d8624a438121b86a1956544bd72ed68cd69f2c99555b08b1e8c51ffd511c";

/// Fake asset IDs for BTC and USDT in tests.
fn btc_asset() -> AssetId {
    AssetId::new([0xBB; 32])
}
fn usdt_asset() -> AssetId {
    AssetId::new([0xCC; 32])
}

fn secret_key() -> fuels::crypto::SecretKey {
    SECRET_KEY.parse().unwrap()
}

fn default_book_config() -> OrderBookConfig {
    let base = AssetConfig {
        symbol: "BTC".into(),
        asset: btc_asset(),
        decimals: 9,
        min_precision: 0,
        max_precision: 9,
    };
    let quote = AssetConfig {
        symbol: "USDT".into(),
        asset: usdt_asset(),
        decimals: 6,
        min_precision: 0,
        max_precision: 6,
    };
    let ids = MarketIdAssets {
        base_asset: btc_asset(),
        quote_asset: usdt_asset(),
    };
    OrderBookConfig {
        contract_id: None,
        blob_id: None,
        market_id: ids.market_id(),
        taker_fee: 30,
        maker_fee: 10,
        min_order: 10_000,
        dust: 1_000,
        price_window: 20,
        allow_fractional_price: false,
        base,
        quote,
    }
}

/// Start a local fuel-core node with coins for our test wallet.
async fn setup_node() -> FuelService {
    let mut coin_generator = CoinConfigGenerator::new();
    // Skip first 100 to avoid genesis conflicts
    for _ in 0..100 {
        coin_generator.generate();
    }

    let base_asset_id = AssetId::zeroed();
    let assets = [
        (btc_asset(), u64::MAX),
        (usdt_asset(), u64::MAX),
        (base_asset_id, u64::MAX >> 4),
    ];

    let coins: Vec<_> = assets
        .iter()
        .map(|(asset_id, amount)| {
            let mut coin = coin_generator.generate_with(secret_key(), *amount);
            coin.asset_id = *asset_id;
            coin
        })
        .collect();

    let state = StateConfig {
        coins,
        ..StateConfig::default()
    };

    let chain_config = ChainConfig::local_testnet();
    let mut config = Config::local_node_with_configs(chain_config, state);
    config.debug = true;
    config.utxo_validation = true;
    config.gas_price_config.min_exec_gas_price = 1000;
    config.block_production = Trigger::Instant;

    FuelService::new_node(config).await.unwrap()
}

#[tokio::test]
async fn deploy_fresh_contracts_on_local_node() {
    // Start local node
    let node = setup_node().await;
    let provider = Provider::from(node.bound_address).await.unwrap();

    // Create wallet
    let wallet = Wallet::new(PrivateKeySigner::new(secret_key()), provider.clone());

    // Deploy
    let params = DeployParams {
        deploy_config: MarketsConfigPartial {
            starting_height: 0,
            trade_account_registry_id: None,
            order_book_registry_id: None,
            trade_account_oracle_id: None,
            trial_trade_account_oracle_id: None,
            order_book_whitelist_id: None,
            order_book_blacklist_id: None,
            fast_bridge_asset_registry_proxy_id: None,
            pairs: vec![default_book_config()],
        },
        output: None,
        deploy_whitelist: true,
        deploy_blacklist: false,
        upgrade_bytecode: false,
        new_proxy_owner: None,
        new_contract_owner: None,
        trial_cosigner: None,
        trial_creator: None,
        revoke_orderbook_maintainers: Vec::new(),
        new_orderbook_maintainers: Vec::new(),
    };

    let result = deploy(wallet.clone(), params).await.unwrap();

    // Assert non-zero contract IDs
    let zero = fuel_core_types::fuel_types::ContractId::zeroed();
    assert_ne!(
        result.trade_account_oracle_id, zero,
        "oracle should be deployed"
    );
    assert_ne!(
        result.trade_account_registry_id, zero,
        "trade account registry should be deployed"
    );
    assert_ne!(
        result.order_book_registry_id, zero,
        "order book registry should be deployed"
    );
    assert!(
        !result.pairs.is_empty(),
        "should have at least one market pair"
    );
    assert!(
        result.pairs[0].contract_id.is_some(),
        "first pair should have a contract_id"
    );

    // Verify contracts are actually on-chain
    use fuel_core_client::client::FuelClient;
    let client = FuelClient::new(node.bound_address.to_string()).unwrap();

    let oracle_contract = client
        .contract(&result.trade_account_oracle_id)
        .await
        .expect("should query oracle contract");
    assert!(
        oracle_contract.is_some(),
        "oracle contract should exist on-chain"
    );

    let registry_contract = client
        .contract(&result.order_book_registry_id)
        .await
        .expect("should query registry contract");
    assert!(
        registry_contract.is_some(),
        "order book registry should exist on-chain"
    );

    println!("✅ Deploy succeeded!");
    println!("  Oracle:              {}", result.trade_account_oracle_id);
    println!(
        "  Trade Acct Registry: {}",
        result.trade_account_registry_id
    );
    println!("  OB Registry:        {}", result.order_book_registry_id);
    println!("  Pairs:              {}", result.pairs.len());
    if let Some(id) = &result.pairs[0].contract_id {
        println!("  First pair contract: {}", id);
    }
}