use fuels::{
prelude::*,
types::{
AssetId,
Identity,
},
};
use crate::order_book_deploy::{
OrderBookDeploy,
OrderBookDeployConfig,
OrderBookProxy,
};
use crate::{
order_book_deploy,
order_book_deploy::{
OrderArgs,
OrderBook,
Side as ContractSide,
Time,
},
trade_account_deploy::CallParams,
};
pub use o2_api_types::primitives::{
OrderType,
Side,
};
pub const DEFAULT_METHOD_GAS: u64 = 1_000_000;
pub fn order_type_from_contract(order_type: order_book_deploy::OrderType) -> OrderType {
match order_type {
order_book_deploy::OrderType::Spot => OrderType::Spot,
order_book_deploy::OrderType::Limit((price, timestamp)) => {
OrderType::Limit(price, timestamp.unix as u128)
}
order_book_deploy::OrderType::FillOrKill => OrderType::FillOrKill,
order_book_deploy::OrderType::PostOnly => OrderType::PostOnly,
order_book_deploy::OrderType::Market => OrderType::Market,
order_book_deploy::OrderType::BoundedMarket((max_price, min_price)) => {
OrderType::BoundedMarket {
max_price,
min_price,
}
}
}
}
pub fn order_type_to_contract(order_type: OrderType) -> order_book_deploy::OrderType {
match order_type {
OrderType::Spot => order_book_deploy::OrderType::Spot,
OrderType::Limit(price, timestamp) => order_book_deploy::OrderType::Limit((
price,
Time {
unix: timestamp as u64,
},
)),
OrderType::FillOrKill => order_book_deploy::OrderType::FillOrKill,
OrderType::PostOnly => order_book_deploy::OrderType::PostOnly,
OrderType::Market => order_book_deploy::OrderType::Market,
OrderType::BoundedMarket {
max_price,
min_price,
} => order_book_deploy::OrderType::BoundedMarket((max_price, min_price)),
}
}
pub fn side_from_contract(order_side: ContractSide) -> Side {
match order_side {
ContractSide::Buy => Side::Buy,
ContractSide::Sell => Side::Sell,
}
}
pub fn side_to_contract(side: Side) -> ContractSide {
match side {
Side::Buy => ContractSide::Buy,
Side::Sell => ContractSide::Sell,
}
}
#[derive(Debug, Clone)]
pub struct CreateOrderParams {
pub price: u64,
pub quantity: u64,
pub order_type: OrderType,
pub side: Side,
pub asset_id: AssetId,
}
impl CreateOrderParams {
pub fn random(
side: Side,
asset_id: AssetId,
min_price: u64,
max_price: u64,
min_quantity: u64,
max_quantity: u64,
) -> Self {
let price = (rand::random::<u64>() + min_price) % max_price;
let quantity = (rand::random::<u64>() + min_quantity) % max_quantity;
let order_type = OrderType::Spot;
Self {
price,
quantity,
order_type,
side,
asset_id,
}
}
pub fn to_order_args(&self) -> OrderArgs {
OrderArgs {
price: self.price,
quantity: self.quantity,
order_type: order_type_to_contract(self.order_type),
}
}
}
#[derive(Clone, Copy)]
pub struct OrderbookConfig {
pub base_asset: AssetId,
pub base_decimals: u64,
pub quote_asset: AssetId,
pub quote_decimals: u64,
}
impl OrderbookConfig {
pub fn get_order_side_asset(&self, order_side: &Side) -> AssetId {
if order_side == &Side::Buy {
self.quote_asset
} else {
self.base_asset
}
}
pub fn get_order_side_amount(&self, quantity: u64, price: u64, side: &Side) -> u64 {
if side == &Side::Buy {
((quantity as u128 * price as u128) / self.base_decimals as u128)
.try_into()
.unwrap()
} else {
quantity
}
}
pub fn create_call_params(
&self,
params: &CreateOrderParams,
gas: Option<u64>,
) -> CallParams {
let amount =
self.get_order_side_amount(params.quantity, params.price, ¶ms.side);
let asset_id = self.get_order_side_asset(¶ms.side);
CallParams::new(amount, asset_id, gas.unwrap_or(u64::MAX))
}
}
#[derive(Clone)]
pub struct OrderBookManager<W: Account + Clone> {
pub proxy: OrderBookProxy<W>,
pub contract: OrderBook<W>,
pub gas_payer_wallet: W,
pub config: OrderbookConfig,
}
impl<W: Account + Clone> OrderBookManager<W> {
pub fn new(
gas_payer_wallet: &W,
base_decimals: u64,
quote_decimals: u64,
order_book_deploy: &OrderBookDeploy<W>,
) -> Self {
let config = OrderbookConfig {
base_asset: order_book_deploy.base_asset,
base_decimals,
quote_asset: order_book_deploy.quote_asset,
quote_decimals,
};
Self {
proxy: order_book_deploy
.order_book_proxy
.clone()
.with_account(gas_payer_wallet.clone()),
contract: order_book_deploy
.order_book
.clone()
.with_account(gas_payer_wallet.clone()),
config,
gas_payer_wallet: gas_payer_wallet.clone(),
}
}
pub fn create_call_params(
&self,
params: &CreateOrderParams,
gas: Option<u64>,
) -> CallParams {
self.config().create_call_params(params, gas)
}
pub fn get_order_side_asset(&self, order_side: &Side) -> AssetId {
self.config().get_order_side_asset(order_side)
}
pub fn get_order_side_amount(&self, quantity: u64, price: u64, side: &Side) -> u64 {
self.config().get_order_side_amount(quantity, price, side)
}
pub async fn balances_of(&self, identity: &Identity) -> anyhow::Result<(u128, u128)> {
let result = self
.contract
.methods()
.get_settled_balance_of(*identity)
.simulate(Execution::state_read_only())
.await?;
Ok((result.value.0 as u128, result.value.1 as u128))
}
pub async fn emit_config(&self) -> anyhow::Result<()> {
self.contract
.methods()
.emit_orderbook_config()
.call()
.await?;
Ok(())
}
pub async fn upgrade(
&self,
deploy_config: &OrderBookDeployConfig,
) -> anyhow::Result<()> {
let base_asset_id = self.contract.methods().get_base_asset().call().await?.value;
let quote_asset_id = self
.contract
.methods()
.get_quote_asset()
.simulate(Execution::state_read_only())
.await?
.value;
let order_book_blob_id = OrderBookDeploy::deploy_order_book_blob(
&self.gas_payer_wallet,
base_asset_id,
quote_asset_id,
deploy_config,
)
.await?;
self.proxy
.methods()
.set_proxy_target(ContractId::new(order_book_blob_id))
.call()
.await?;
Ok(())
}
pub async fn accumulated_fees(&self) -> anyhow::Result<(u64, u64)> {
let fees = self
.contract
.methods()
.current_fees()
.simulate(Execution::state_read_only())
.await?
.value;
Ok(fees)
}
pub async fn get_whitelist_id(&self) -> anyhow::Result<Option<ContractId>> {
let result = self
.contract
.methods()
.get_whitelist_id()
.simulate(Execution::state_read_only())
.await?;
Ok(result.value)
}
pub async fn get_blacklist_id(&self) -> anyhow::Result<Option<ContractId>> {
let result = self
.contract
.methods()
.get_blacklist_id()
.simulate(Execution::state_read_only())
.await?;
Ok(result.value)
}
pub fn base_asset(&self) -> AssetId {
self.config().base_asset
}
pub fn base_decimals(&self) -> u64 {
self.config().base_decimals
}
pub fn quote_asset(&self) -> AssetId {
self.config().quote_asset
}
pub fn quote_decimals(&self) -> u64 {
self.config().quote_decimals
}
pub fn config(&self) -> OrderbookConfig {
self.config
}
pub async fn is_paused(&self) -> anyhow::Result<bool> {
let result = self
.contract
.methods()
.is_paused()
.simulate(Execution::state_read_only())
.await?;
Ok(result.value)
}
}
#[cfg(test)]
mod tests {
use crate::{
helpers::get_asset_balance,
order_book_deploy::{
OrderBookDeployConfig,
OrderCreatedEvent,
OrderMatchedEvent,
},
};
use super::*;
use fuels::test_helpers::{
WalletsConfig,
launch_custom_provider_and_get_wallets,
};
#[tokio::test]
async fn test_order_book_manager() {
let base_asset = AssetId::from([1; 32]);
let quote_asset = AssetId::from([2; 32]);
let initial_balance = 1_000_000_000_000_000u64;
let mut wallets = launch_custom_provider_and_get_wallets(
WalletsConfig::new_multiple_assets(
3,
vec![
AssetConfig {
id: AssetId::default(),
num_coins: 1,
coin_amount: initial_balance,
},
AssetConfig {
id: quote_asset,
num_coins: 1,
coin_amount: initial_balance,
},
AssetConfig {
id: base_asset,
num_coins: 1,
coin_amount: initial_balance,
},
],
),
None,
None,
)
.await
.unwrap();
let deployer_wallet = wallets.pop().unwrap();
let maker_wallet = wallets.pop().unwrap();
let taker_wallet = wallets.pop().unwrap();
let mut config = OrderBookDeployConfig::default();
config.order_book_configurables = config
.order_book_configurables
.with_MAKER_FEE(0.into())
.unwrap()
.with_TAKER_FEE(0.into())
.unwrap();
let deployment =
OrderBookDeploy::deploy(&deployer_wallet, base_asset, quote_asset, &config)
.await
.unwrap();
let provider = deployer_wallet.try_provider().unwrap();
let contract_exists = provider
.contract_exists(&deployment.contract_id)
.await
.unwrap();
assert!(contract_exists, "OrderBook contract should exist");
assert_eq!(deployment.base_asset, base_asset);
assert_eq!(deployment.quote_asset, quote_asset);
let order_book_manager = OrderBookManager::new(
&deployer_wallet,
10u64.pow(9),
10u64.pow(9),
&deployment,
);
let maker_order_params = CreateOrderParams {
price: 1_000_000_000,
quantity: 2_000_000_000,
order_type: OrderType::Spot,
side: Side::Buy,
asset_id: quote_asset,
};
let maker_call_params =
order_book_manager.create_call_params(&maker_order_params, None);
let maker_order_book_instance = order_book_manager
.contract
.clone()
.with_account(maker_wallet.clone());
let result = maker_order_book_instance
.methods()
.create_order(maker_order_params.to_order_args())
.with_tx_policies(TxPolicies::default())
.call_params(CallParameters::new(
maker_call_params.coins,
maker_call_params.asset_id,
maker_call_params.gas,
))
.unwrap()
.call()
.await
.unwrap();
let maker_order_created_events =
result.decode_logs_with_type::<OrderCreatedEvent>().unwrap();
let maker_order_created_event = maker_order_created_events.first().unwrap();
let taker_order_params = CreateOrderParams {
price: 1_000_000_000,
quantity: 1_000_000_000,
order_type: OrderType::Spot,
side: Side::Sell,
asset_id: base_asset,
};
let taker_call_params =
order_book_manager.create_call_params(&taker_order_params, None);
let result = order_book_manager
.contract
.clone()
.with_account(taker_wallet.clone())
.methods()
.create_order(taker_order_params.to_order_args())
.with_tx_policies(TxPolicies::default())
.call_params(CallParameters::new(
taker_call_params.coins,
taker_call_params.asset_id,
taker_call_params.gas,
))
.unwrap()
.with_variable_output_policy(VariableOutputPolicy::Exactly(10))
.call()
.await
.unwrap();
let maker_balances = maker_wallet.get_balances().await.unwrap();
let taker_balances = taker_wallet.get_balances().await.unwrap();
let (maker_balance_base, maker_balance_quote) = order_book_manager
.balances_of(&Identity::Address(maker_wallet.address()))
.await
.unwrap();
let (taker_balance_base, taker_balance_quote) = order_book_manager
.balances_of(&Identity::Address(taker_wallet.address()))
.await
.unwrap();
assert_eq!(
get_asset_balance(&maker_balances, &base_asset) + maker_balance_base,
initial_balance as u128 + taker_order_params.quantity as u128
);
assert_eq!(
get_asset_balance(&maker_balances, "e_asset) + maker_balance_quote,
initial_balance as u128 - maker_call_params.coins as u128
);
assert_eq!(
get_asset_balance(&taker_balances, &base_asset) + taker_balance_base,
initial_balance as u128 - taker_order_params.quantity as u128
);
assert_eq!(
get_asset_balance(&taker_balances, "e_asset) + taker_balance_quote,
initial_balance as u128 + 1_000_000_000u128
);
let matches = result.decode_logs_with_type::<OrderMatchedEvent>().unwrap();
assert_eq!(matches.len(), 1);
let match_event = matches.first().unwrap();
assert_eq!(match_event.price, maker_order_params.price);
assert_eq!(match_event.quantity, taker_order_params.quantity);
let _ = maker_order_book_instance
.methods()
.settle_balances(vec![
Identity::Address(maker_wallet.address()),
Identity::Address(taker_wallet.address()),
])
.with_variable_output_policy(VariableOutputPolicy::Exactly(5))
.call()
.await
.unwrap();
let maker_quote_balance_before_cancel =
get_asset_balance(&maker_wallet.get_balances().await.unwrap(), "e_asset);
let cancel_result = maker_order_book_instance
.methods()
.cancel_order(maker_order_created_event.order_id)
.with_tx_policies(TxPolicies::default())
.with_variable_output_policy(VariableOutputPolicy::Exactly(10))
.call()
.await
.unwrap();
let maker_quote_balance_after =
get_asset_balance(&maker_wallet.get_balances().await.unwrap(), "e_asset);
assert!(cancel_result.value);
assert_eq!(
maker_quote_balance_after,
maker_quote_balance_before_cancel + 1_000_000_000u128
);
}
}