use crate::{
MarketsConfigPartial,
OwnershipTransferOptions,
parse_margin_identity,
};
use anyhow::{
Context,
Result,
ensure,
};
use fuels::{
accounts::{
Account,
ViewOnlyAccount,
},
prelude::Execution,
types::{
ContractId,
Identity,
Salt,
},
};
use o2_api_types::domain::book::OrderBookConfig;
use o2_tools::{
house_order_book::{
HOUSE_ROLE,
HouseOrderBookDeploy,
HouseOrderBookDeployConfig,
},
order_book::OrderBookManager,
order_book_deploy::{
HouseOrderBookConfigurables,
OrderBook,
State,
},
};
use std::collections::BTreeSet;
pub(crate) fn validate_config(pairs: &[OrderBookConfig]) -> Result<()> {
for pair in pairs {
for identity in &pair.house_identities {
parse_margin_identity(identity).with_context(|| {
format!(
"{}/{}: invalid House identity",
pair.base.symbol, pair.quote.symbol
)
})?;
}
if let Some(sidecar) = &pair.sidecar {
ensure!(
sidecar.maker_fee <= 1_000_000 && sidecar.taker_fee <= 1_000_000,
"{}/{}: sidecar fees must be at most 1000000 millionths",
pair.base.symbol,
pair.quote.symbol
);
}
}
Ok(())
}
pub(crate) async fn grant_house_roles<W>(
canonical: &OrderBookManager<W>,
identities: &[String],
) -> Result<()>
where
W: Account + ViewOnlyAccount + Clone + 'static,
{
for identity in identities {
let account = parse_margin_identity(identity)?;
if !canonical
.contract
.methods()
.has_role(HOUSE_ROLE, account)
.simulate(Execution::state_read_only())
.await?
.value
{
tracing::info!(canonical = %canonical.contract.contract_id(), ?account,
"Granting HOUSE_ROLE");
canonical
.contract
.methods()
.owner_grant_role(HOUSE_ROLE, account)
.call()
.await?;
}
}
Ok(())
}
pub(crate) async fn deploy_sidecar<W>(
wallet: &W,
canonical: &OrderBookManager<W>,
pair: &mut OrderBookConfig,
upgrade: bool,
ownership: &OwnershipTransferOptions,
) -> Result<()>
where
W: Account + ViewOnlyAccount + Clone + 'static,
{
let Some(settings) = pair.sidecar.as_mut() else {
return Ok(())
};
let canonical_id = canonical.contract.contract_id();
let attached = canonical.contract.methods().get_sidecar_state()
.simulate(Execution::state_read_only()).await
.context("Reading sidecar configuration; upgrade canonical to a sidecar-capable implementation first")?.value;
if let (Some(recorded), Some(active)) = (settings.contract_id, attached) {
ensure!(
recorded == active,
"Canonical {canonical_id} is attached to {active}, but the config records {recorded}"
);
}
let canonical_owner = canonical
.contract
.methods()
.owner()
.simulate(Execution::state_read_only())
.await?
.value;
let owner = ownership
.new_contract_owner
.map(Identity::Address)
.or_else(|| initialized_owner(canonical_owner))
.context("Canonical contract owner is not initialized")?;
let proxy_owner = canonical
.proxy
.methods()
.proxy_owner()
.simulate(Execution::state_read_only())
.await?
.value;
let proxy_owner = ownership
.new_proxy_owner
.map(Identity::Address)
.or_else(|| initialized_owner(proxy_owner))
.context("Canonical proxy owner is not initialized")?;
let mut config = HouseOrderBookDeployConfig {
owner: Some(owner),
proxy_owner: Some(proxy_owner),
salt: Salt::from(*canonical_id),
configurables: HouseOrderBookConfigurables::default()
.with_MAKER_FEE(settings.maker_fee.into())?
.with_TAKER_FEE(settings.taker_fee.into())?,
..Default::default()
};
let house = match settings.contract_id.or(attached) {
Some(id) => HouseOrderBookDeploy::load(wallet, &canonical.contract, id).await?,
None => {
HouseOrderBookDeploy::deploy(wallet, &canonical.contract, &config).await?
}
};
let id = house.book.contract_id;
let blob = if upgrade {
if let Some(current_owner) = initialized_owner(
house
.house
.methods()
.owner()
.simulate(Execution::state_read_only())
.await?
.value,
) {
config.owner = Some(current_owner);
}
house.upgrade(&config).await?
} else {
let actual_maker = house
.house
.methods()
.get_maker_fee()
.simulate(Execution::state_read_only())
.await?
.value;
let actual_taker = house
.house
.methods()
.get_taker_fee()
.simulate(Execution::state_read_only())
.await?
.value;
ensure!(
actual_maker == settings.maker_fee && actual_taker == settings.taker_fee,
"House book {id} has different fees; use --upgrade-bytecode to apply the configured fees"
);
house
.book
.order_book_proxy
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await?
.value
.context("House proxy target is not initialized")?
};
settings.contract_id = Some(id);
settings.blob_id = Some(blob);
tracing::info!(canonical = %canonical_id, sidecar = %id, blob = %blob,
"Sidecar deployment ready; attachment is a separate operation");
Ok(())
}
fn initialized_owner(state: State) -> Option<Identity> {
match state {
State::Initialized(owner) => Some(owner),
State::Uninitialized | State::Revoked => None,
}
}
fn selected_pairs<'a>(
pairs: &'a [OrderBookConfig],
markets: &[String],
) -> Result<Vec<&'a OrderBookConfig>> {
ensure!(
!markets.is_empty(),
"Select at least one market for sidecar activation"
);
let mut selected = BTreeSet::new();
for market in markets {
let wanted = market.trim();
ensure!(
!wanted.is_empty(),
"Sidecar activation market cannot be empty"
);
let id = wanted.strip_prefix("0x").unwrap_or(wanted);
let matches = pairs
.iter()
.enumerate()
.filter(|(_, pair)| {
let symbol = format!("{}/{}", pair.base.symbol, pair.quote.symbol);
symbol.eq_ignore_ascii_case(wanted)
|| hex::encode(*pair.market_id).eq_ignore_ascii_case(id)
})
.map(|(index, _)| index)
.collect::<Vec<_>>();
ensure!(
matches.len() == 1,
"Sidecar activation market `{market}` must identify exactly one configured pair"
);
selected.insert(matches[0]);
}
Ok(selected.into_iter().map(|index| &pairs[index]).collect())
}
pub async fn activate_sidecars<W>(
wallet: &W,
config: &MarketsConfigPartial,
markets: &[String],
) -> Result<Vec<ContractId>>
where
W: Account + ViewOnlyAccount + Clone + 'static,
{
let selected = selected_pairs(&config.pairs, markets)?;
let mut calls = Vec::with_capacity(selected.len());
for pair in selected {
let market = format!("{}/{}", pair.base.symbol, pair.quote.symbol);
let canonical_id = pair
.contract_id
.with_context(|| format!("{market}: no recorded canonical deployment"))?;
let sidecar_id = pair
.sidecar
.as_ref()
.and_then(|s| s.contract_id)
.with_context(|| {
format!("{market}: deploy and record the sidecar before activation")
})?;
let canonical = OrderBook::new(canonical_id, wallet.clone());
let house = HouseOrderBookDeploy::load(wallet, &canonical, sidecar_id).await?;
ensure!(
house.book.base_asset == pair.base.asset
&& house.book.quote_asset == pair.quote.asset,
"{market}: recorded canonical book has different assets"
);
let active = canonical
.methods()
.get_sidecar_state()
.simulate(Execution::state_read_only())
.await?
.value;
ensure!(
active.is_none() || active == Some(sidecar_id),
"{market}: canonical is already attached to another sidecar"
);
if active != Some(sidecar_id) {
canonical
.methods()
.set_sidecar(Some(sidecar_id))
.with_contracts(&[&house.house])
.simulate(Execution::realistic())
.await
.with_context(|| format!("{market}: activation preflight failed"))?;
}
calls.push((market, canonical, house, active == Some(sidecar_id)));
}
let mut activated = Vec::with_capacity(calls.len());
for (market, canonical, house, already_active) in calls {
let id = house.book.contract_id;
if !already_active {
canonical
.methods()
.set_sidecar(Some(id))
.with_contracts(&[&house.house])
.call()
.await
.with_context(|| format!("{market}: sidecar activation failed"))?;
}
tracing::info!(%market, canonical = %canonical.contract_id(), sidecar = %id,
already_active, "Sidecar attached");
activated.push(id);
}
Ok(activated)
}