use anyhow::Result;
use fuels::{
prelude::*,
tx::StorageSlot,
types::{
AssetId,
ContractId,
Identity,
},
};
use crate::blob_loader;
abigen!(
Contract(
name = "SharedEventsV1",
abi = r#"{"programType":"contract","specVersion":"1.2","encodingVersion":"1","concreteTypes":[{"type":"()","concreteTypeId":"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d"},{"type":"b256","concreteTypeId":"7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b"},{"type":"struct contract_schema::sidecar::SharedOrderMatchedEvent","concreteTypeId":"b67e97f64320e8ebb08209f9a5e4f55f8ff63d26fd34efedb1224ffd2cafbaa6","metadataTypeId":59},{"type":"u64","concreteTypeId":"1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"}],"metadataTypes":[{"type":"enum contract_schema::sidecar::HouseExecutionRole","metadataTypeId":18,"components":[{"name":"Maker","typeId":"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d"},{"name":"Taker","typeId":"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d"}]},{"type":"struct contract_schema::sidecar::SharedOrderMatchedEvent","metadataTypeId":59,"components":[{"name":"source_order_id","typeId":"7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b"},{"name":"turbo_order_id","typeId":"7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b"},{"name":"house_execution_role","typeId":18},{"name":"quantity","typeId":"1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"},{"name":"price","typeId":"1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"},{"name":"timestamp","typeId":72}]},{"type":"struct std::time::Time","metadataTypeId":72,"components":[{"name":"unix","typeId":"1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"}]}],"functions":[],"loggedTypes":[{"logId":"13150115045912406251","concreteTypeId":"b67e97f64320e8ebb08209f9a5e4f55f8ff63d26fd34efedb1224ffd2cafbaa6"}],"messagesTypes":[],"configurables":[],"errorCodes":{},"panickingCalls":{}}"#
),
Contract(
name = "OrderBook",
abi = "artifacts/order-book/order-book-abi.json"
),
Contract(
name = "HouseOrderBook",
abi = "artifacts/house-order-book/house-order-book-abi.json"
),
Contract(
name = "OrderBookProxy",
abi = "artifacts/order-book-proxy/order-book-proxy-abi.json"
),
Contract(
name = "OrderBookWhitelist",
abi = "artifacts/order-book-whitelist/order-book-whitelist-abi.json",
),
Contract(
name = "OrderBookBlacklist",
abi = "artifacts/order-book-blacklist/order-book-blacklist-abi.json",
),
);
pub const ORDER_BOOK_BYTECODE: &[u8] =
include_bytes!("../artifacts/order-book/order-book.bin");
pub const ORDER_BOOK_STORAGE: &[u8] =
include_bytes!("../artifacts/order-book/order-book-storage_slots.json");
pub const ORDER_BOOK_PROXY_BYTECODE: &[u8] =
include_bytes!("../artifacts/order-book-proxy/order-book-proxy.bin");
pub const ORDER_BOOK_PROXY_STORAGE: &[u8] =
include_bytes!("../artifacts/order-book-proxy/order-book-proxy-storage_slots.json");
pub const ORDER_BOOK_WHITELIST_BYTECODE: &[u8] =
include_bytes!("../artifacts/order-book-whitelist/order-book-whitelist.bin");
pub const ORDER_BOOK_WHITELIST_STORAGE: &[u8] = include_bytes!(
"../artifacts/order-book-whitelist/order-book-whitelist-storage_slots.json"
);
pub const ORDER_BOOK_BLACKLIST_BYTECODE: &[u8] =
include_bytes!("../artifacts/order-book-blacklist/order-book-blacklist.bin");
pub const ORDER_BOOK_BLACKLIST_STORAGE: &[u8] = include_bytes!(
"../artifacts/order-book-blacklist/order-book-blacklist-storage_slots.json"
);
/// Configuration for deploying OrderBook contracts.
/// Contains bytecode and storage slot information needed for deployment.
#[derive(Clone)]
pub struct OrderBookDeployConfig {
/// Bytecode for the OrderBook contract
pub order_book_bytecode: Vec<u8>,
/// Storage slots configuration for the OrderBook contract
pub order_book_storage_slots: Vec<StorageSlot>,
/// Configurables for the OrderBook contract
pub order_book_configurables: OrderBookConfigurables,
/// Bytecode for the OrderBookProxy contract
pub order_book_proxy_bytecode: Vec<u8>,
/// Storage slots configuration for the OrderBookProxy contract
pub order_book_proxy_storage_slots: Vec<StorageSlot>,
/// Configurables for the OrderBookProxy contract
pub order_book_proxy_configurables: OrderBookProxyConfigurables,
/// Bytecode for the OrderBookWhitelist contract
pub order_book_whitelist_bytecode: Vec<u8>,
/// Storage slots configuration for the OrderBookWhitelist contract
pub order_book_whitelist_storage_slots: Vec<StorageSlot>,
/// Bytecode for the OrderBookBlacklist contract
pub order_book_blacklist_bytecode: Vec<u8>,
/// Storage slots configuration for the OrderBookBlacklist contract
pub order_book_blacklist_storage_slots: Vec<StorageSlot>,
/// Maximum words per blob for deployment
pub max_words_per_blob: usize,
/// Owner of the OrderBook proxy
pub proxy_owner: Option<Identity>,
/// Owner of the OrderBook
pub order_book_owner: Option<Identity>,
/// Salt for contract deployment
pub salt: Salt,
}
/// Result of building the blob payload for an OrderBook contract.
///
/// The proxy fallback (`run_external_blob`) can only target a *single*
/// blob, so for any contract bytecode that doesn't fit in one
/// blob-upload transaction we split it into multiple `data_blobs` and
/// generate a tiny **loader blob** that, when LDC'd by the proxy,
/// loads every data blob in order and then jumps into the
/// reconstituted code. `id` / `blob` are that loader blob — the value
/// the proxy will target as `INITIAL_TARGET`. `data_blobs` are the
/// chunked code blobs the loader references; they must be uploaded
/// before (or in the same set of transactions as) the loader blob.
pub struct OrderBookBlob {
/// `BlobId` of the loader blob — this is what the proxy points at.
pub id: BlobId,
/// Whether the loader blob is already on chain (cheap dedup).
pub exists: bool,
/// The loader blob: a small piece of bytecode that, when executed
/// by the proxy, LDCs `data_blobs` in order and jumps into them.
pub blob: Blob,
/// The chunked code blobs the loader references. Empty for tiny
/// contracts that fit in a single blob (in that case `blob` IS the
/// chunk and the loader stub round-trips trivially).
pub data_blobs: Vec<Blob>,
}
impl Default for OrderBookDeployConfig {
fn default() -> Self {
Self {
order_book_bytecode: ORDER_BOOK_BYTECODE.to_vec(),
order_book_storage_slots: serde_json::from_slice(ORDER_BOOK_STORAGE).unwrap(),
order_book_whitelist_bytecode: ORDER_BOOK_WHITELIST_BYTECODE.to_vec(),
order_book_configurables: OrderBookConfigurables::default(),
order_book_proxy_bytecode: ORDER_BOOK_PROXY_BYTECODE.to_vec(),
order_book_proxy_storage_slots: serde_json::from_slice(
ORDER_BOOK_PROXY_STORAGE,
)
.unwrap(),
order_book_proxy_configurables: OrderBookProxyConfigurables::default(),
order_book_whitelist_storage_slots: serde_json::from_slice(
ORDER_BOOK_WHITELIST_STORAGE,
)
.unwrap(),
order_book_blacklist_bytecode: ORDER_BOOK_BLACKLIST_BYTECODE.to_vec(),
order_book_blacklist_storage_slots: serde_json::from_slice(
ORDER_BOOK_BLACKLIST_STORAGE,
)
.unwrap(),
max_words_per_blob: 10_000,
proxy_owner: None,
order_book_owner: None,
salt: Salt::default(),
}
}
}
impl OrderBookDeployConfig {
pub fn with_configurables(order_book_configurables: OrderBookConfigurables) -> Self {
Self {
order_book_configurables,
..OrderBookDeployConfig::default()
}
}
}
/// Result of an OrderBook deployment.
/// Contains the deployed contract instance and configuration.
#[derive(Clone)]
pub struct OrderBookDeploy<W: Account + Clone> {
/// The deployed OrderBook contract instance
pub order_book_proxy: OrderBookProxy<W>,
/// The deployed OrderBook contract instance
pub order_book: OrderBook<W>,
/// Contract ID of the deployed OrderBook
pub contract_id: ContractId,
/// Base asset ID for the trading pair
pub base_asset: AssetId,
/// Quote asset ID for the trading pair
pub quote_asset: AssetId,
/// The deployed trade account whitelist contract instance
pub whitelist: Option<OrderBookWhitelist<W>>,
/// Contract ID of the deployed trade account whitelist
pub whitelist_id: Option<ContractId>,
}
impl<W: Account + Clone> OrderBookDeploy<W> {
pub fn new(
w: W,
contract_id: ContractId,
base_asset: AssetId,
quote_asset: AssetId,
) -> Self {
let order_book = OrderBook::new(contract_id, w.clone());
let order_book_proxy = OrderBookProxy::new(contract_id, w.clone());
Self {
order_book,
order_book_proxy,
contract_id,
base_asset,
quote_asset,
whitelist: None,
whitelist_id: None,
}
}
pub async fn initialize(&self) -> Result<()> {
// Initialize the OrderBookProxy with the configured owner via configurables
self.order_book_proxy
.methods()
.initialize_proxy()
.call()
.await?;
// Initialize the OrderBook contract with the specified owner
self.order_book.methods().initialize().call().await?;
Ok(())
}
pub async fn deploy_order_book_blacklist(
deployer_wallet: &W,
owner_identity: &Identity,
config: &OrderBookDeployConfig,
) -> Result<OrderBookBlacklist<W>> {
let contract = Contract::regular(
config.order_book_blacklist_bytecode.clone(),
config.salt,
config.order_book_blacklist_storage_slots.clone(),
)
.with_configurables(
OrderBookBlacklistConfigurables::default()
.with_INITIAL_OWNER(State::Initialized(*owner_identity))?,
)
.with_salt(config.salt);
let contract_id = contract.contract_id();
let instance = OrderBookBlacklist::new(contract_id, deployer_wallet.clone());
let contract_exists = deployer_wallet
.try_provider()?
.contract_exists(&contract_id)
.await?;
if !contract_exists {
contract
.deploy(deployer_wallet, TxPolicies::default())
.await?;
instance.methods().initialize().call().await?;
}
Ok(instance)
}
pub async fn deploy_order_book_whitelist(
deployer_wallet: &W,
owner_identity: &Identity,
config: &OrderBookDeployConfig,
) -> Result<OrderBookWhitelist<W>> {
let contract = Contract::regular(
config.order_book_whitelist_bytecode.clone(),
config.salt,
config.order_book_whitelist_storage_slots.clone(),
)
.with_configurables(
OrderBookWhitelistConfigurables::default()
.with_INITIAL_OWNER(State::Initialized(*owner_identity))?,
)
.with_salt(config.salt);
let contract_id = contract.contract_id();
let instance = OrderBookWhitelist::new(contract_id, deployer_wallet.clone());
let contract_exists = deployer_wallet
.try_provider()?
.contract_exists(&contract_id)
.await?;
if !contract_exists {
contract
.deploy(deployer_wallet, TxPolicies::default())
.await?;
instance.methods().initialize().call().await?;
}
Ok(instance)
}
pub async fn order_book_blob(
deployer_wallet: &W,
base_asset: AssetId,
quote_asset: AssetId,
config: &OrderBookDeployConfig,
) -> Result<OrderBookBlob> {
let order_book_owner = config
.order_book_owner
.unwrap_or(Identity::Address(deployer_wallet.address()));
// Configure the contract with the trading pair assets
let configurables = config
.order_book_configurables
.clone()
.with_BASE_ASSET(base_asset)?
.with_QUOTE_ASSET(quote_asset)?
.with_INITIAL_OWNER(State::Initialized(order_book_owner))?;
// See `blob_loader` for the why: proxy LDCs one blob, so we
// need a loader stub blob in front of N data blobs.
let (data_blobs, loader_blob) = blob_loader::build_loader_blobs(
config.order_book_bytecode.clone(),
config.salt,
config.order_book_storage_slots.clone(),
configurables.clone(),
config.max_words_per_blob,
)?;
let loader_blob_id = loader_blob.id();
let loader_blob_exists = deployer_wallet
.try_provider()?
.blob_exists(loader_blob_id)
.await?;
Ok(OrderBookBlob {
id: loader_blob_id,
exists: loader_blob_exists,
blob: loader_blob,
data_blobs,
})
}
/// Deploys the trade account implementation as a blob.
/// Large contracts are deployed as blobs to handle size limitations.
///
/// # Arguments
/// * `deployer_wallet` - The wallet to use for deployment (pays gas fees)
/// * `config` - Deployment configuration containing bytecode and settings
///
/// # Returns
/// * `Ok(BlobId)` - The ID of the deployed blob
/// * `Err(anyhow::Error)` - If deployment fails
pub async fn deploy_order_book_blob(
deployer_wallet: &W,
base_asset: AssetId,
quote_asset: AssetId,
config: &OrderBookDeployConfig,
) -> Result<BlobId> {
let order_book_blob =
Self::order_book_blob(deployer_wallet, base_asset, quote_asset, config)
.await?;
blob_loader::upload_loader_blobs(
deployer_wallet,
order_book_blob.data_blobs,
order_book_blob.blob,
)
.await
}
pub async fn deploy_order_book_proxy(
deployer_wallet: &W,
order_book_blob_id: &BlobId,
config: &OrderBookDeployConfig,
) -> Result<(OrderBookProxy<W>, bool)> {
let proxy_owner = config
.proxy_owner
.unwrap_or(Identity::Address(deployer_wallet.address()));
let blob_id = ContractId::new(*order_book_blob_id);
let configurables = config
.order_book_proxy_configurables
.clone()
.with_INITIAL_TARGET(blob_id)?
.with_INITIAL_OWNER(State::Initialized(proxy_owner))?;
let contract = Contract::regular(
config.order_book_proxy_bytecode.clone(),
config.salt,
config.order_book_proxy_storage_slots.clone(),
)
.with_configurables(configurables);
let contract_id = contract.contract_id();
let already_deployed = deployer_wallet
.try_provider()?
.contract_exists(&contract_id)
.await?;
let order_book_proxy = OrderBookProxy::new(contract_id, deployer_wallet.clone());
if !already_deployed {
contract
.deploy(deployer_wallet, TxPolicies::default())
.await?;
}
let requires_initialization = !already_deployed;
Ok((order_book_proxy, requires_initialization))
}
/// Deploys an OrderBook contract with the specified trading pair.
///
/// # Arguments
/// * `deployer_wallet` - The wallet to use for deployment (pays gas fees)
/// * `owner` - The identity to set as the OrderBook owner
/// * `base_asset` - The base asset ID for the trading pair
/// * `quote_asset` - The quote asset ID for the trading pair
/// * `config` - Deployment configuration containing bytecode and settings
///
/// # Returns
/// * `Ok(OrderBookDeploy)` - Complete deployment result with contract instance
/// * `Err(anyhow::Error)` - If deployment or initialization fails
///
/// # Process
/// 1. Deploys the OrderBook contract with asset configurables
/// 2. Initializes the contract with the specified owner
pub async fn deploy(
deployer_wallet: &W,
base_asset: AssetId,
quote_asset: AssetId,
config: &OrderBookDeployConfig,
) -> Result<OrderBookDeploy<W>> {
// Configure the contract with the trading pair assets
let (deploy, requires_initialization) = Self::deploy_without_initialization(
deployer_wallet,
base_asset,
quote_asset,
config,
)
.await?;
if requires_initialization {
// Initialize the OrderBookProxy with the configured owner via configurables
deploy.initialize().await?;
}
Ok(deploy)
}
/// Deploys an OrderBook contract like [`Self::deploy`], but without initializing it.
pub async fn deploy_without_initialization(
deployer_wallet: &W,
base_asset: AssetId,
quote_asset: AssetId,
config: &OrderBookDeployConfig,
) -> Result<(OrderBookDeploy<W>, bool)> {
let order_book_blob_id = Self::deploy_order_book_blob(
deployer_wallet,
base_asset,
quote_asset,
config,
)
.await?;
let (deploy_order_book_proxy, requires_initialization) =
Self::deploy_order_book_proxy(deployer_wallet, &order_book_blob_id, config)
.await?;
let contract_id = deploy_order_book_proxy.contract_id();
let deploy = OrderBookDeploy::new(
deployer_wallet.clone(),
contract_id,
base_asset,
quote_asset,
);
Ok((deploy, requires_initialization))
}
}
#[cfg(test)]
mod tests {
use super::*;
use fuels::test_helpers::{
WalletsConfig,
launch_custom_provider_and_get_wallets,
};
#[tokio::test]
async fn test_deploy_order_book_blob() {
// Start fuel-core
let mut 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_wallet = wallets.pop().unwrap();
let contract_owner = wallets.pop().unwrap();
// Deploy OrderBook
let base_asset = AssetId::from([1u8; 32]);
let quote_asset = AssetId::from([2u8; 32]);
let config = OrderBookDeployConfig {
proxy_owner: Some(Identity::Address(contract_owner.address())),
order_book_owner: Some(Identity::Address(contract_owner.address())),
..Default::default()
};
let order_book_blob_id = OrderBookDeploy::deploy_order_book_blob(
&deployer_wallet,
base_asset,
quote_asset,
&config,
)
.await
.unwrap();
let (order_book_deploy, requires_initialization) =
OrderBookDeploy::deploy_without_initialization(
&deployer_wallet,
base_asset,
quote_asset,
&config,
)
.await
.unwrap();
let order_book = order_book_deploy.order_book;
let order_book_proxy = order_book_deploy.order_book_proxy;
if requires_initialization {
order_book_proxy
.methods()
.initialize_proxy()
.call()
.await
.unwrap();
order_book.methods().initialize().call().await.unwrap();
}
assert_eq!(
order_book_proxy
.methods()
.proxy_owner()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
State::Initialized(Identity::Address(contract_owner.address()))
);
assert_eq!(
order_book_proxy
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
Some(ContractId::new(order_book_blob_id))
);
assert_eq!(
order_book
.methods()
.owner()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
State::Initialized(Identity::Address(contract_owner.address()))
);
assert_eq!(
order_book
.methods()
.get_base_asset()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
base_asset,
);
assert_eq!(
order_book
.methods()
.get_quote_asset()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
quote_asset,
);
// Test upgrading contract configuration
let new_base_asset = AssetId::from([3u8; 32]);
let order_book_blob_id = OrderBookDeploy::deploy_order_book_blob(
&deployer_wallet,
new_base_asset,
quote_asset,
&config,
)
.await
.unwrap();
order_book_proxy
.with_account(contract_owner)
.methods()
.set_proxy_target(ContractId::new(order_book_blob_id))
.call()
.await
.unwrap();
assert_eq!(
order_book
.methods()
.get_base_asset()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
new_base_asset
);
}
#[tokio::test]
async fn test_order_book_deployment() {
// Start fuel-core
let mut 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_wallet = wallets.pop().unwrap();
// Deploy OrderBook
let base_asset = AssetId::new([1u8; 32]);
let quote_asset = AssetId::new([2u8; 32]);
let config = OrderBookDeployConfig::default();
let deployment =
OrderBookDeploy::deploy(&deployer_wallet, base_asset, quote_asset, &config)
.await
.unwrap();
// Check if contract exists
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");
// Verify configuration
assert_eq!(deployment.base_asset, base_asset);
assert_eq!(deployment.quote_asset, quote_asset);
}
}