use anyhow::{
Context,
Result,
};
use fuels::{
prelude::*,
types::{
Identity,
transaction_builders::Blob,
},
};
pub use crate::order_book_deploy::{
DepthLevel,
HouseExecutionResult,
HouseOrderBook,
HouseOrderBookConfigurables,
SharedExecutionEvent,
SharedOrderMatchedEvent,
Side,
};
use crate::{
blob_loader,
order_book_deploy::{
OrderBook,
OrderBookDeploy,
OrderBookDeployConfig,
State,
},
};
pub const HOUSE_ROLE: u64 = 2;
const HOUSE_BYTECODE: &[u8] =
include_bytes!("../artifacts/house-order-book/house-order-book.bin");
const HOUSE_STORAGE: &[u8] =
include_bytes!("../artifacts/house-order-book/house-order-book-storage_slots.json");
#[derive(Clone)]
pub struct HouseOrderBookDeployConfig {
pub configurables: HouseOrderBookConfigurables,
pub owner: Option<Identity>,
pub proxy_owner: Option<Identity>,
pub salt: Salt,
pub max_words_per_blob: usize,
}
impl Default for HouseOrderBookDeployConfig {
fn default() -> Self {
Self {
configurables: HouseOrderBookConfigurables::default(),
owner: None,
proxy_owner: None,
salt: Salt::default(),
max_words_per_blob: 10_000,
}
}
}
#[derive(Clone)]
pub struct HouseOrderBookDeploy<W: Account + Clone> {
pub house: HouseOrderBook<W>,
pub canonical: OrderBook<W>,
pub book: OrderBookDeploy<W>,
}
impl<W: Account + Clone> HouseOrderBookDeploy<W> {
pub async fn deploy(
wallet: &W,
canonical: &OrderBook<W>,
config: &HouseOrderBookDeployConfig,
) -> Result<Self> {
let base = canonical
.methods()
.get_base_asset()
.simulate(Execution::state_read_only())
.await?
.value;
let quote = canonical
.methods()
.get_quote_asset()
.simulate(Execution::state_read_only())
.await?
.value;
let (data, loader) = Self::build_blobs(wallet, canonical, config)?;
let blob = blob_loader::upload_loader_blobs(wallet, data, loader).await?;
let proxy_config = OrderBookDeployConfig {
proxy_owner: config.proxy_owner,
salt: config.salt,
..Default::default()
};
let (proxy, _) =
OrderBookDeploy::deploy_order_book_proxy(wallet, &blob, &proxy_config)
.await?;
let house = HouseOrderBook::new(proxy.contract_id(), wallet.clone());
if proxy
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await?
.value
.is_none()
{
proxy.methods().initialize_proxy().call().await?;
}
Ok(Self {
house,
canonical: canonical.clone(),
book: OrderBookDeploy::new(wallet.clone(), proxy.contract_id(), base, quote),
})
}
pub async fn load(
wallet: &W,
canonical: &OrderBook<W>,
contract_id: ContractId,
) -> Result<Self> {
let house = HouseOrderBook::new(contract_id, wallet.clone());
let paired = house
.methods()
.get_canonical_orderbook()
.simulate(Execution::state_read_only())
.await?
.value;
anyhow::ensure!(
paired == canonical.contract_id(),
"House book {contract_id} belongs to {paired}, not {}",
canonical.contract_id()
);
let base = canonical
.methods()
.get_base_asset()
.simulate(Execution::state_read_only())
.await?
.value;
let quote = canonical
.methods()
.get_quote_asset()
.simulate(Execution::state_read_only())
.await?
.value;
Ok(Self {
house,
canonical: canonical.clone(),
book: OrderBookDeploy::new(wallet.clone(), contract_id, base, quote),
})
}
pub async fn upgrade(
&self,
config: &HouseOrderBookDeployConfig,
) -> Result<ContractId> {
let current = self
.book
.order_book_proxy
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await?
.value
.context("House proxy target is not initialized")?;
let wallet = self.house.account();
let (data, loader) = Self::build_blobs(wallet, &self.canonical, config)?;
let target = ContractId::new(loader.id());
if current != target {
blob_loader::upload_loader_blobs(wallet, data, loader).await?;
self.book
.order_book_proxy
.methods()
.set_proxy_target(target)
.call()
.await?;
if !matches!(
self.house
.methods()
.owner()
.simulate(Execution::state_read_only())
.await?
.value,
State::Uninitialized
) {
self.house.methods().emit_orderbook_config().call().await?;
}
}
Ok(target)
}
fn build_blobs(
wallet: &W,
canonical: &OrderBook<W>,
config: &HouseOrderBookDeployConfig,
) -> Result<(Vec<Blob>, Blob)> {
let owner = config.owner.unwrap_or(Identity::Address(wallet.address()));
let configurables = config
.configurables
.clone()
.with_CANONICAL_BOOK(canonical.contract_id())?
.with_INITIAL_OWNER(State::Initialized(owner))?;
blob_loader::build_loader_blobs(
HOUSE_BYTECODE.to_vec(),
config.salt,
serde_json::from_slice(HOUSE_STORAGE)?,
configurables,
config.max_words_per_blob,
)
}
pub fn contract_ids(&self) -> [ContractId; 2] {
[self.book.contract_id, self.canonical.contract_id()]
}
pub fn decode_shared_matches(
&self,
receipts: &[fuels::tx::Receipt],
) -> Result<Vec<SharedOrderMatchedEvent>> {
decode_legacy_shared_matches(self.canonical.contract_id(), receipts)
}
pub fn execute_turbo_orders(
&self,
source_order_id: fuels::types::Bits256,
max_base_quantity: u64,
max_fills: u64,
) -> fuels::programs::calls::CallHandler<
W,
fuels::programs::calls::ContractCall,
HouseExecutionResult,
> {
self.canonical
.methods()
.execute_turbo_orders(source_order_id, max_base_quantity, max_fills)
.with_contracts(&[&self.house])
}
pub fn decode_shared_executions(
&self,
receipts: &[fuels::tx::Receipt],
) -> Result<Vec<SharedExecutionEvent>> {
Ok(self
.canonical
.log_decoder()
.decode_logs_with_type::<SharedExecutionEvent>(receipts)?)
}
pub async fn is_execution_enabled(&self) -> Result<bool> {
if self
.canonical
.methods()
.get_sidecar_state()
.simulate(Execution::state_read_only())
.await?
.value
!= Some(self.book.contract_id)
{
return Ok(false);
}
if self
.house
.methods()
.is_paused()
.simulate(Execution::state_read_only())
.await?
.value
{
return Ok(false);
}
Ok(!self
.canonical
.methods()
.is_paused()
.simulate(Execution::state_read_only())
.await?
.value)
}
pub async fn executable_depth(
&self,
side: Side,
max_levels: u64,
) -> Result<Vec<DepthLevel>> {
Ok(self
.house
.methods()
.get_executable_depth(side, max_levels)
.with_contracts(&[&self.canonical])
.simulate(Execution::state_read_only())
.await?
.value)
}
}
pub fn decode_legacy_shared_matches(
canonical: ContractId,
receipts: &[fuels::tx::Receipt],
) -> Result<Vec<SharedOrderMatchedEvent>> {
use fuels::core::codec::{
Log,
LogDecoder,
LogFormatter,
log_formatters_lookup,
};
let decoder = LogDecoder::new(
log_formatters_lookup(
vec![(
SharedOrderMatchedEvent::LOG_ID_U64.to_string(),
LogFormatter::new_log::<SharedOrderMatchedEvent>(),
)],
canonical,
),
Default::default(),
);
Ok(decoder.decode_logs_with_type::<SharedOrderMatchedEvent>(receipts)?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
order_book_deploy::OrderBookConfigurables,
order_book_registry::{
MarketId,
OrderBookRegistryDeployConfig,
OrderBookRegistryManager,
},
};
#[tokio::test]
async fn house_deployment_preserves_owners_and_is_discovered_through_canonical() {
let wallets = launch_custom_provider_and_get_wallets(
WalletsConfig::new(Some(2), Some(1), Some(1_000_000_000)),
None,
Some(fuels::test_helpers::ChainConfig::local_testnet()),
)
.await
.unwrap();
let deployer = &wallets[0];
let owner = Identity::Address(wallets[1].address());
let base = AssetId::new([1; 32]);
let quote = AssetId::new([2; 32]);
let canonical = OrderBookDeploy::deploy(
deployer,
base,
quote,
&OrderBookDeployConfig::with_configurables(
OrderBookConfigurables::default()
.with_BASE_DECIMALS(1_000_000)
.unwrap()
.with_MIN_ORDER(50_000_000)
.unwrap()
.with_PRICE_PRECISION(10_000)
.unwrap(),
),
)
.await
.unwrap();
let config = HouseOrderBookDeployConfig {
owner: Some(owner),
proxy_owner: Some(owner),
configurables: HouseOrderBookConfigurables::default()
.with_MAKER_FEE(123u64.into())
.unwrap(),
..Default::default()
};
let house =
HouseOrderBookDeploy::deploy(deployer, &canonical.order_book, &config)
.await
.unwrap();
assert_eq!(
house.house.methods().owner().call().await.unwrap().value,
State::Uninitialized
);
canonical
.order_book
.methods()
.set_sidecar(Some(house.book.contract_id))
.with_contracts(&[&house.house])
.call()
.await
.unwrap();
assert_eq!(
house.house.methods().owner().call().await.unwrap().value,
State::Initialized(owner)
);
assert_eq!(
house
.book
.order_book_proxy
.methods()
.proxy_owner()
.call()
.await
.unwrap()
.value,
State::Initialized(owner)
);
assert_eq!(
house
.house
.methods()
.get_base_decimals()
.call()
.await
.unwrap()
.value,
1_000_000
);
assert_eq!(
house
.house
.methods()
.get_maker_fee()
.call()
.await
.unwrap()
.value,
123
);
assert_eq!(
house
.house
.methods()
.get_minimum_trade_amount()
.call()
.await
.unwrap()
.value,
50_000_000
);
assert_eq!(
house
.house
.methods()
.get_price_precision()
.call()
.await
.unwrap()
.value,
10_000
);
assert!(
!house
.house
.methods()
.get_allow_fractional_price()
.call()
.await
.unwrap()
.value
);
assert_eq!(
house
.house
.methods()
.get_canonical_orderbook()
.call()
.await
.unwrap()
.value,
canonical.contract_id
);
assert_eq!(
house.contract_ids(),
[house.book.contract_id, canonical.contract_id]
);
let repeated =
HouseOrderBookDeploy::deploy(deployer, &canonical.order_book, &config)
.await
.unwrap();
assert_eq!(repeated.book.contract_id, house.book.contract_id);
let registry = OrderBookRegistryManager::deploy(
deployer,
&OrderBookRegistryDeployConfig::default(),
)
.await
.unwrap();
let market = MarketId {
base_asset: base,
quote_asset: quote,
};
assert!(
registry
.get_house_order_book(market.clone())
.await
.unwrap()
.is_none()
);
registry
.register_order_book(market.clone(), canonical.contract_id)
.await
.unwrap();
assert_eq!(
registry.get_house_order_book(market.clone()).await.unwrap(),
Some(house.book.contract_id)
);
canonical
.order_book
.methods()
.set_sidecar(Some(house.book.contract_id))
.call()
.await
.unwrap();
assert!(house.is_execution_enabled().await.unwrap());
let house_owner = house.house.clone().with_account(wallets[1].clone());
house_owner.methods().pause().call().await.unwrap();
assert!(!house.is_execution_enabled().await.unwrap());
house_owner.methods().unpause().call().await.unwrap();
canonical.order_book.methods().pause().call().await.unwrap();
assert!(!house.is_execution_enabled().await.unwrap());
canonical
.order_book
.methods()
.unpause()
.call()
.await
.unwrap();
assert!(house.is_execution_enabled().await.unwrap());
assert_eq!(
registry.get_house_order_book(market.clone()).await.unwrap(),
Some(house.book.contract_id)
);
assert_eq!(
registry.get_order_book(market.clone()).await.unwrap(),
Some(canonical.contract_id)
);
assert!(
house
.executable_depth(Side::Sell, 10)
.await
.unwrap()
.is_empty()
);
canonical
.order_book
.methods()
.set_sidecar(None)
.call()
.await
.unwrap();
assert!(!house.is_execution_enabled().await.unwrap());
assert!(
registry
.get_house_order_book(market)
.await
.unwrap()
.is_none()
);
}
}