1use fuels::{
2 prelude::*,
3 types::{
4 AssetId,
5 Identity,
6 },
7};
8
9use crate::order_book_deploy::{
10 OrderBookDeploy,
11 OrderBookDeployConfig,
12 OrderBookProxy,
13};
14
15use crate::{
17 order_book_deploy,
18 order_book_deploy::{
19 OrderArgs,
20 OrderBook,
21 Side as ContractSide,
22 Time,
23 },
24 trade_account_deploy::CallParams,
25};
26
27pub use o2_api_types::primitives::{
29 OrderType,
30 Side,
31};
32
33pub const DEFAULT_METHOD_GAS: u64 = 1_000_000;
34
35pub fn order_type_from_contract(order_type: order_book_deploy::OrderType) -> OrderType {
37 match order_type {
38 order_book_deploy::OrderType::Spot => OrderType::Spot,
39 order_book_deploy::OrderType::Limit((price, timestamp)) => {
40 OrderType::Limit(price, timestamp.unix as u128)
41 }
42 order_book_deploy::OrderType::FillOrKill => OrderType::FillOrKill,
43 order_book_deploy::OrderType::PostOnly => OrderType::PostOnly,
44 order_book_deploy::OrderType::Market => OrderType::Market,
45 order_book_deploy::OrderType::BoundedMarket((max_price, min_price)) => {
46 OrderType::BoundedMarket {
47 max_price,
48 min_price,
49 }
50 }
51 }
52}
53
54pub fn order_type_to_contract(order_type: OrderType) -> order_book_deploy::OrderType {
56 match order_type {
57 OrderType::Spot => order_book_deploy::OrderType::Spot,
58 OrderType::Limit(price, timestamp) => order_book_deploy::OrderType::Limit((
59 price,
60 Time {
61 unix: timestamp as u64,
62 },
63 )),
64 OrderType::FillOrKill => order_book_deploy::OrderType::FillOrKill,
65 OrderType::PostOnly => order_book_deploy::OrderType::PostOnly,
66 OrderType::Market => order_book_deploy::OrderType::Market,
67 OrderType::BoundedMarket {
68 max_price,
69 min_price,
70 } => order_book_deploy::OrderType::BoundedMarket((max_price, min_price)),
71 }
72}
73
74pub fn side_from_contract(order_side: ContractSide) -> Side {
76 match order_side {
77 ContractSide::Buy => Side::Buy,
78 ContractSide::Sell => Side::Sell,
79 }
80}
81
82pub fn side_to_contract(side: Side) -> ContractSide {
84 match side {
85 Side::Buy => ContractSide::Buy,
86 Side::Sell => ContractSide::Sell,
87 }
88}
89
90#[derive(Debug, Clone)]
93pub struct CreateOrderParams {
94 pub price: u64,
96 pub quantity: u64,
98 pub order_type: OrderType,
100 pub side: Side,
102 pub asset_id: AssetId,
104}
105
106impl CreateOrderParams {
107 pub fn random(
108 side: Side,
109 asset_id: AssetId,
110 min_price: u64,
111 max_price: u64,
112 min_quantity: u64,
113 max_quantity: u64,
114 ) -> Self {
115 let price = (rand::random::<u64>() + min_price) % max_price;
116 let quantity = (rand::random::<u64>() + min_quantity) % max_quantity;
117 let order_type = OrderType::Spot;
118 Self {
119 price,
120 quantity,
121 order_type,
122 side,
123 asset_id,
124 }
125 }
126
127 pub fn to_order_args(&self) -> OrderArgs {
129 OrderArgs {
130 price: self.price,
131 quantity: self.quantity,
132 order_type: order_type_to_contract(self.order_type),
133 }
134 }
135}
136
137#[derive(Clone, Copy)]
138pub struct OrderbookConfig {
139 pub base_asset: AssetId,
141 pub base_decimals: u64,
142 pub quote_asset: AssetId,
144 pub quote_decimals: u64,
145}
146
147impl OrderbookConfig {
148 pub fn get_order_side_asset(&self, order_side: &Side) -> AssetId {
149 if order_side == &Side::Buy {
150 self.quote_asset
151 } else {
152 self.base_asset
153 }
154 }
155
156 pub fn get_order_side_amount(&self, quantity: u64, price: u64, side: &Side) -> u64 {
157 if side == &Side::Buy {
158 ((quantity as u128 * price as u128) / self.base_decimals as u128)
159 .try_into()
160 .unwrap()
161 } else {
162 quantity
163 }
164 }
165
166 pub fn create_call_params(
167 &self,
168 params: &CreateOrderParams,
169 gas: Option<u64>,
170 ) -> CallParams {
171 let amount =
172 self.get_order_side_amount(params.quantity, params.price, ¶ms.side);
173 let asset_id = self.get_order_side_asset(¶ms.side);
174 CallParams::new(amount, asset_id, gas.unwrap_or(u64::MAX))
175 }
176}
177
178#[derive(Clone)]
179pub struct OrderBookManager<W: Account + Clone> {
180 pub proxy: OrderBookProxy<W>,
182 pub contract: OrderBook<W>,
184 pub gas_payer_wallet: W,
186 pub config: OrderbookConfig,
188}
189
190impl<W: Account + Clone> OrderBookManager<W> {
191 pub fn new(
199 gas_payer_wallet: &W,
200 base_decimals: u64,
201 quote_decimals: u64,
202 order_book_deploy: &OrderBookDeploy<W>,
203 ) -> Self {
204 let config = OrderbookConfig {
205 base_asset: order_book_deploy.base_asset,
206 base_decimals,
207 quote_asset: order_book_deploy.quote_asset,
208 quote_decimals,
209 };
210 Self {
211 proxy: order_book_deploy
212 .order_book_proxy
213 .clone()
214 .with_account(gas_payer_wallet.clone()),
215 contract: order_book_deploy
216 .order_book
217 .clone()
218 .with_account(gas_payer_wallet.clone()),
219 config,
220 gas_payer_wallet: gas_payer_wallet.clone(),
221 }
222 }
223
224 pub fn create_call_params(
225 &self,
226 params: &CreateOrderParams,
227 gas: Option<u64>,
228 ) -> CallParams {
229 self.config().create_call_params(params, gas)
230 }
231
232 pub fn get_order_side_asset(&self, order_side: &Side) -> AssetId {
233 self.config().get_order_side_asset(order_side)
234 }
235
236 pub fn get_order_side_amount(&self, quantity: u64, price: u64, side: &Side) -> u64 {
237 self.config().get_order_side_amount(quantity, price, side)
238 }
239
240 pub async fn balances_of(&self, identity: &Identity) -> anyhow::Result<(u128, u128)> {
241 let result = self
242 .contract
243 .methods()
244 .get_settled_balance_of(*identity)
245 .simulate(Execution::state_read_only())
246 .await?;
247 Ok((result.value.0 as u128, result.value.1 as u128))
248 }
249
250 pub async fn emit_config(&self) -> anyhow::Result<()> {
251 self.contract
252 .methods()
253 .emit_orderbook_config()
254 .call()
255 .await?;
256 Ok(())
257 }
258
259 pub async fn upgrade(
260 &self,
261 deploy_config: &OrderBookDeployConfig,
262 ) -> anyhow::Result<()> {
263 let base_asset_id = self.contract.methods().get_base_asset().call().await?.value;
264 let quote_asset_id = self
265 .contract
266 .methods()
267 .get_quote_asset()
268 .simulate(Execution::state_read_only())
269 .await?
270 .value;
271 let order_book_blob_id = OrderBookDeploy::deploy_order_book_blob(
272 &self.gas_payer_wallet,
273 base_asset_id,
274 quote_asset_id,
275 deploy_config,
276 )
277 .await?;
278 self.proxy
279 .methods()
280 .set_proxy_target(ContractId::new(order_book_blob_id))
281 .call()
282 .await?;
283 Ok(())
284 }
285
286 pub async fn accumulated_fees(&self) -> anyhow::Result<(u64, u64)> {
287 let fees = self
288 .contract
289 .methods()
290 .current_fees()
291 .simulate(Execution::state_read_only())
292 .await?
293 .value;
294 Ok(fees)
295 }
296
297 pub async fn get_whitelist_id(&self) -> anyhow::Result<Option<ContractId>> {
298 let result = self
299 .contract
300 .methods()
301 .get_whitelist_id()
302 .simulate(Execution::state_read_only())
303 .await?;
304 Ok(result.value)
305 }
306
307 pub async fn get_blacklist_id(&self) -> anyhow::Result<Option<ContractId>> {
308 let result = self
309 .contract
310 .methods()
311 .get_blacklist_id()
312 .simulate(Execution::state_read_only())
313 .await?;
314 Ok(result.value)
315 }
316
317 pub fn base_asset(&self) -> AssetId {
318 self.config().base_asset
319 }
320
321 pub fn base_decimals(&self) -> u64 {
322 self.config().base_decimals
323 }
324
325 pub fn quote_asset(&self) -> AssetId {
326 self.config().quote_asset
327 }
328
329 pub fn quote_decimals(&self) -> u64 {
330 self.config().quote_decimals
331 }
332
333 pub fn config(&self) -> OrderbookConfig {
334 self.config
335 }
336
337 pub async fn is_paused(&self) -> anyhow::Result<bool> {
338 let result = self
339 .contract
340 .methods()
341 .is_paused()
342 .simulate(Execution::state_read_only())
343 .await?;
344 Ok(result.value)
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use crate::{
351 helpers::get_asset_balance,
352 order_book_deploy::{
353 OrderBookDeployConfig,
354 OrderCreatedEvent,
355 OrderMatchedEvent,
356 },
357 };
358
359 use super::*;
360 use fuels::test_helpers::{
361 WalletsConfig,
362 launch_custom_provider_and_get_wallets,
363 };
364
365 #[tokio::test]
366 async fn test_order_book_manager() {
367 let base_asset = AssetId::from([1; 32]);
368 let quote_asset = AssetId::from([2; 32]);
369 let initial_balance = 1_000_000_000_000_000u64;
370
371 let mut wallets = launch_custom_provider_and_get_wallets(
373 WalletsConfig::new_multiple_assets(
374 3,
375 vec![
376 AssetConfig {
377 id: AssetId::default(),
378 num_coins: 1,
379 coin_amount: initial_balance,
380 },
381 AssetConfig {
382 id: quote_asset,
383 num_coins: 1,
384 coin_amount: initial_balance,
385 },
386 AssetConfig {
387 id: base_asset,
388 num_coins: 1,
389 coin_amount: initial_balance,
390 },
391 ],
392 ),
393 None,
394 None,
395 )
396 .await
397 .unwrap();
398 let deployer_wallet = wallets.pop().unwrap();
399 let maker_wallet = wallets.pop().unwrap();
400 let taker_wallet = wallets.pop().unwrap();
401
402 let mut config = OrderBookDeployConfig::default();
404
405 config.order_book_configurables = config
406 .order_book_configurables
407 .with_MAKER_FEE(0.into())
408 .unwrap()
409 .with_TAKER_FEE(0.into())
410 .unwrap();
411
412 let deployment =
413 OrderBookDeploy::deploy(&deployer_wallet, base_asset, quote_asset, &config)
414 .await
415 .unwrap();
416
417 let provider = deployer_wallet.try_provider().unwrap();
419 let contract_exists = provider
420 .contract_exists(&deployment.contract_id)
421 .await
422 .unwrap();
423 assert!(contract_exists, "OrderBook contract should exist");
424
425 assert_eq!(deployment.base_asset, base_asset);
427 assert_eq!(deployment.quote_asset, quote_asset);
428
429 let order_book_manager = OrderBookManager::new(
430 &deployer_wallet,
431 10u64.pow(9),
432 10u64.pow(9),
433 &deployment,
434 );
435 let maker_order_params = CreateOrderParams {
436 price: 1_000_000_000,
437 quantity: 2_000_000_000,
438 order_type: OrderType::Spot,
439 side: Side::Buy,
440 asset_id: quote_asset,
441 };
442 let maker_call_params =
443 order_book_manager.create_call_params(&maker_order_params, None);
444 let maker_order_book_instance = order_book_manager
445 .contract
446 .clone()
447 .with_account(maker_wallet.clone());
448 let result = maker_order_book_instance
449 .methods()
450 .create_order(maker_order_params.to_order_args())
451 .with_tx_policies(TxPolicies::default())
452 .call_params(CallParameters::new(
453 maker_call_params.coins,
454 maker_call_params.asset_id,
455 maker_call_params.gas,
456 ))
457 .unwrap()
458 .call()
459 .await
460 .unwrap();
461 let maker_order_created_events =
462 result.decode_logs_with_type::<OrderCreatedEvent>().unwrap();
463 let maker_order_created_event = maker_order_created_events.first().unwrap();
464 let taker_order_params = CreateOrderParams {
465 price: 1_000_000_000,
466 quantity: 1_000_000_000,
467 order_type: OrderType::Spot,
468 side: Side::Sell,
469 asset_id: base_asset,
470 };
471 let taker_call_params =
472 order_book_manager.create_call_params(&taker_order_params, None);
473 let result = order_book_manager
474 .contract
475 .clone()
476 .with_account(taker_wallet.clone())
477 .methods()
478 .create_order(taker_order_params.to_order_args())
479 .with_tx_policies(TxPolicies::default())
480 .call_params(CallParameters::new(
481 taker_call_params.coins,
482 taker_call_params.asset_id,
483 taker_call_params.gas,
484 ))
485 .unwrap()
486 .with_variable_output_policy(VariableOutputPolicy::Exactly(10))
487 .call()
488 .await
489 .unwrap();
490
491 let maker_balances = maker_wallet.get_balances().await.unwrap();
492 let taker_balances = taker_wallet.get_balances().await.unwrap();
493 let (maker_balance_base, maker_balance_quote) = order_book_manager
494 .balances_of(&Identity::Address(maker_wallet.address()))
495 .await
496 .unwrap();
497 let (taker_balance_base, taker_balance_quote) = order_book_manager
498 .balances_of(&Identity::Address(taker_wallet.address()))
499 .await
500 .unwrap();
501
502 assert_eq!(
503 get_asset_balance(&maker_balances, &base_asset) + maker_balance_base,
504 initial_balance as u128 + taker_order_params.quantity as u128
505 );
506 assert_eq!(
507 get_asset_balance(&maker_balances, "e_asset) + maker_balance_quote,
508 initial_balance as u128 - maker_call_params.coins as u128
509 );
510 assert_eq!(
511 get_asset_balance(&taker_balances, &base_asset) + taker_balance_base,
512 initial_balance as u128 - taker_order_params.quantity as u128
513 );
514 assert_eq!(
515 get_asset_balance(&taker_balances, "e_asset) + taker_balance_quote,
516 initial_balance as u128 + 1_000_000_000u128
517 );
518
519 let matches = result.decode_logs_with_type::<OrderMatchedEvent>().unwrap();
521 assert_eq!(matches.len(), 1);
523 let match_event = matches.first().unwrap();
524 assert_eq!(match_event.price, maker_order_params.price);
525 assert_eq!(match_event.quantity, taker_order_params.quantity);
526
527 let _ = maker_order_book_instance
528 .methods()
529 .settle_balances(vec![
530 Identity::Address(maker_wallet.address()),
531 Identity::Address(taker_wallet.address()),
532 ])
533 .with_variable_output_policy(VariableOutputPolicy::Exactly(5))
534 .call()
535 .await
536 .unwrap();
537 let maker_quote_balance_before_cancel =
538 get_asset_balance(&maker_wallet.get_balances().await.unwrap(), "e_asset);
539 let cancel_result = maker_order_book_instance
540 .methods()
541 .cancel_order(maker_order_created_event.order_id)
542 .with_tx_policies(TxPolicies::default())
543 .with_variable_output_policy(VariableOutputPolicy::Exactly(10))
544 .call()
545 .await
546 .unwrap();
547 let maker_quote_balance_after =
548 get_asset_balance(&maker_wallet.get_balances().await.unwrap(), "e_asset);
549
550 assert!(cancel_result.value);
551 assert_eq!(
552 maker_quote_balance_after,
553 maker_quote_balance_before_cancel + 1_000_000_000u128
554 );
555 }
556}