Skip to main content

o2_tools/
order_book_deploy.rs

1use anyhow::Result;
2use fuels::{
3    prelude::*,
4    tx::StorageSlot,
5    types::{
6        AssetId,
7        ContractId,
8        Identity,
9    },
10};
11
12use crate::blob_loader;
13
14abigen!(
15    Contract(
16        name = "SharedEventsV1",
17        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":{}}"#
18    ),
19    Contract(
20        name = "OrderBook",
21        abi = "artifacts/order-book/order-book-abi.json"
22    ),
23    Contract(
24        name = "HouseOrderBook",
25        abi = "artifacts/house-order-book/house-order-book-abi.json"
26    ),
27    Contract(
28        name = "OrderBookProxy",
29        abi = "artifacts/order-book-proxy/order-book-proxy-abi.json"
30    ),
31    Contract(
32        name = "OrderBookWhitelist",
33        abi = "artifacts/order-book-whitelist/order-book-whitelist-abi.json",
34    ),
35    Contract(
36        name = "OrderBookBlacklist",
37        abi = "artifacts/order-book-blacklist/order-book-blacklist-abi.json",
38    ),
39);
40pub const ORDER_BOOK_BYTECODE: &[u8] =
41    include_bytes!("../artifacts/order-book/order-book.bin");
42pub const ORDER_BOOK_STORAGE: &[u8] =
43    include_bytes!("../artifacts/order-book/order-book-storage_slots.json");
44pub const ORDER_BOOK_PROXY_BYTECODE: &[u8] =
45    include_bytes!("../artifacts/order-book-proxy/order-book-proxy.bin");
46pub const ORDER_BOOK_PROXY_STORAGE: &[u8] =
47    include_bytes!("../artifacts/order-book-proxy/order-book-proxy-storage_slots.json");
48pub const ORDER_BOOK_WHITELIST_BYTECODE: &[u8] =
49    include_bytes!("../artifacts/order-book-whitelist/order-book-whitelist.bin");
50pub const ORDER_BOOK_WHITELIST_STORAGE: &[u8] = include_bytes!(
51    "../artifacts/order-book-whitelist/order-book-whitelist-storage_slots.json"
52);
53pub const ORDER_BOOK_BLACKLIST_BYTECODE: &[u8] =
54    include_bytes!("../artifacts/order-book-blacklist/order-book-blacklist.bin");
55pub const ORDER_BOOK_BLACKLIST_STORAGE: &[u8] = include_bytes!(
56    "../artifacts/order-book-blacklist/order-book-blacklist-storage_slots.json"
57);
58
59/// Configuration for deploying OrderBook contracts.
60/// Contains bytecode and storage slot information needed for deployment.
61#[derive(Clone)]
62pub struct OrderBookDeployConfig {
63    /// Bytecode for the OrderBook contract
64    pub order_book_bytecode: Vec<u8>,
65    /// Storage slots configuration for the OrderBook contract
66    pub order_book_storage_slots: Vec<StorageSlot>,
67    /// Configurables for the OrderBook contract
68    pub order_book_configurables: OrderBookConfigurables,
69    /// Bytecode for the OrderBookProxy contract
70    pub order_book_proxy_bytecode: Vec<u8>,
71    /// Storage slots configuration for the OrderBookProxy contract
72    pub order_book_proxy_storage_slots: Vec<StorageSlot>,
73    /// Configurables for the OrderBookProxy contract
74    pub order_book_proxy_configurables: OrderBookProxyConfigurables,
75    /// Bytecode for the OrderBookWhitelist contract
76    pub order_book_whitelist_bytecode: Vec<u8>,
77    /// Storage slots configuration for the OrderBookWhitelist contract
78    pub order_book_whitelist_storage_slots: Vec<StorageSlot>,
79    /// Bytecode for the OrderBookBlacklist contract
80    pub order_book_blacklist_bytecode: Vec<u8>,
81    /// Storage slots configuration for the OrderBookBlacklist contract
82    pub order_book_blacklist_storage_slots: Vec<StorageSlot>,
83    /// Maximum words per blob for deployment
84    pub max_words_per_blob: usize,
85    /// Owner of the OrderBook proxy
86    pub proxy_owner: Option<Identity>,
87    /// Owner of the OrderBook
88    pub order_book_owner: Option<Identity>,
89    /// Salt for contract deployment
90    pub salt: Salt,
91}
92
93/// Result of building the blob payload for an OrderBook contract.
94///
95/// The proxy fallback (`run_external_blob`) can only target a *single*
96/// blob, so for any contract bytecode that doesn't fit in one
97/// blob-upload transaction we split it into multiple `data_blobs` and
98/// generate a tiny **loader blob** that, when LDC'd by the proxy,
99/// loads every data blob in order and then jumps into the
100/// reconstituted code. `id` / `blob` are that loader blob — the value
101/// the proxy will target as `INITIAL_TARGET`. `data_blobs` are the
102/// chunked code blobs the loader references; they must be uploaded
103/// before (or in the same set of transactions as) the loader blob.
104pub struct OrderBookBlob {
105    /// `BlobId` of the loader blob — this is what the proxy points at.
106    pub id: BlobId,
107    /// Whether the loader blob is already on chain (cheap dedup).
108    pub exists: bool,
109    /// The loader blob: a small piece of bytecode that, when executed
110    /// by the proxy, LDCs `data_blobs` in order and jumps into them.
111    pub blob: Blob,
112    /// The chunked code blobs the loader references. Empty for tiny
113    /// contracts that fit in a single blob (in that case `blob` IS the
114    /// chunk and the loader stub round-trips trivially).
115    pub data_blobs: Vec<Blob>,
116}
117
118impl Default for OrderBookDeployConfig {
119    fn default() -> Self {
120        Self {
121            order_book_bytecode: ORDER_BOOK_BYTECODE.to_vec(),
122            order_book_storage_slots: serde_json::from_slice(ORDER_BOOK_STORAGE).unwrap(),
123            order_book_whitelist_bytecode: ORDER_BOOK_WHITELIST_BYTECODE.to_vec(),
124            order_book_configurables: OrderBookConfigurables::default(),
125            order_book_proxy_bytecode: ORDER_BOOK_PROXY_BYTECODE.to_vec(),
126            order_book_proxy_storage_slots: serde_json::from_slice(
127                ORDER_BOOK_PROXY_STORAGE,
128            )
129            .unwrap(),
130            order_book_proxy_configurables: OrderBookProxyConfigurables::default(),
131            order_book_whitelist_storage_slots: serde_json::from_slice(
132                ORDER_BOOK_WHITELIST_STORAGE,
133            )
134            .unwrap(),
135            order_book_blacklist_bytecode: ORDER_BOOK_BLACKLIST_BYTECODE.to_vec(),
136            order_book_blacklist_storage_slots: serde_json::from_slice(
137                ORDER_BOOK_BLACKLIST_STORAGE,
138            )
139            .unwrap(),
140            max_words_per_blob: 10_000,
141            proxy_owner: None,
142            order_book_owner: None,
143            salt: Salt::default(),
144        }
145    }
146}
147
148impl OrderBookDeployConfig {
149    pub fn with_configurables(order_book_configurables: OrderBookConfigurables) -> Self {
150        Self {
151            order_book_configurables,
152            ..OrderBookDeployConfig::default()
153        }
154    }
155}
156
157/// Result of an OrderBook deployment.
158/// Contains the deployed contract instance and configuration.
159#[derive(Clone)]
160pub struct OrderBookDeploy<W: Account + Clone> {
161    /// The deployed OrderBook contract instance
162    pub order_book_proxy: OrderBookProxy<W>,
163    /// The deployed OrderBook contract instance
164    pub order_book: OrderBook<W>,
165    /// Contract ID of the deployed OrderBook
166    pub contract_id: ContractId,
167    /// Base asset ID for the trading pair
168    pub base_asset: AssetId,
169    /// Quote asset ID for the trading pair
170    pub quote_asset: AssetId,
171    /// The deployed trade account whitelist contract instance
172    pub whitelist: Option<OrderBookWhitelist<W>>,
173    /// Contract ID of the deployed trade account whitelist
174    pub whitelist_id: Option<ContractId>,
175}
176
177impl<W: Account + Clone> OrderBookDeploy<W> {
178    pub fn new(
179        w: W,
180        contract_id: ContractId,
181        base_asset: AssetId,
182        quote_asset: AssetId,
183    ) -> Self {
184        let order_book = OrderBook::new(contract_id, w.clone());
185        let order_book_proxy = OrderBookProxy::new(contract_id, w.clone());
186        Self {
187            order_book,
188            order_book_proxy,
189            contract_id,
190            base_asset,
191            quote_asset,
192            whitelist: None,
193            whitelist_id: None,
194        }
195    }
196
197    pub async fn initialize(&self) -> Result<()> {
198        // Initialize the OrderBookProxy with the configured owner via configurables
199        self.order_book_proxy
200            .methods()
201            .initialize_proxy()
202            .call()
203            .await?;
204        // Initialize the OrderBook contract with the specified owner
205        self.order_book.methods().initialize().call().await?;
206        Ok(())
207    }
208
209    pub async fn deploy_order_book_blacklist(
210        deployer_wallet: &W,
211        owner_identity: &Identity,
212        config: &OrderBookDeployConfig,
213    ) -> Result<OrderBookBlacklist<W>> {
214        let contract = Contract::regular(
215            config.order_book_blacklist_bytecode.clone(),
216            config.salt,
217            config.order_book_blacklist_storage_slots.clone(),
218        )
219        .with_configurables(
220            OrderBookBlacklistConfigurables::default()
221                .with_INITIAL_OWNER(State::Initialized(*owner_identity))?,
222        )
223        .with_salt(config.salt);
224        let contract_id = contract.contract_id();
225        let instance = OrderBookBlacklist::new(contract_id, deployer_wallet.clone());
226        let contract_exists = deployer_wallet
227            .try_provider()?
228            .contract_exists(&contract_id)
229            .await?;
230        if !contract_exists {
231            contract
232                .deploy(deployer_wallet, TxPolicies::default())
233                .await?;
234            instance.methods().initialize().call().await?;
235        }
236        Ok(instance)
237    }
238
239    pub async fn deploy_order_book_whitelist(
240        deployer_wallet: &W,
241        owner_identity: &Identity,
242        config: &OrderBookDeployConfig,
243    ) -> Result<OrderBookWhitelist<W>> {
244        let contract = Contract::regular(
245            config.order_book_whitelist_bytecode.clone(),
246            config.salt,
247            config.order_book_whitelist_storage_slots.clone(),
248        )
249        .with_configurables(
250            OrderBookWhitelistConfigurables::default()
251                .with_INITIAL_OWNER(State::Initialized(*owner_identity))?,
252        )
253        .with_salt(config.salt);
254        let contract_id = contract.contract_id();
255        let instance = OrderBookWhitelist::new(contract_id, deployer_wallet.clone());
256        let contract_exists = deployer_wallet
257            .try_provider()?
258            .contract_exists(&contract_id)
259            .await?;
260
261        if !contract_exists {
262            contract
263                .deploy(deployer_wallet, TxPolicies::default())
264                .await?;
265            instance.methods().initialize().call().await?;
266        }
267
268        Ok(instance)
269    }
270
271    pub async fn order_book_blob(
272        deployer_wallet: &W,
273        base_asset: AssetId,
274        quote_asset: AssetId,
275        config: &OrderBookDeployConfig,
276    ) -> Result<OrderBookBlob> {
277        let order_book_owner = config
278            .order_book_owner
279            .unwrap_or(Identity::Address(deployer_wallet.address()));
280        // Configure the contract with the trading pair assets
281        let configurables = config
282            .order_book_configurables
283            .clone()
284            .with_BASE_ASSET(base_asset)?
285            .with_QUOTE_ASSET(quote_asset)?
286            .with_INITIAL_OWNER(State::Initialized(order_book_owner))?;
287        // See `blob_loader` for the why: proxy LDCs one blob, so we
288        // need a loader stub blob in front of N data blobs.
289        let (data_blobs, loader_blob) = blob_loader::build_loader_blobs(
290            config.order_book_bytecode.clone(),
291            config.salt,
292            config.order_book_storage_slots.clone(),
293            configurables.clone(),
294            config.max_words_per_blob,
295        )?;
296        let loader_blob_id = loader_blob.id();
297        let loader_blob_exists = deployer_wallet
298            .try_provider()?
299            .blob_exists(loader_blob_id)
300            .await?;
301
302        Ok(OrderBookBlob {
303            id: loader_blob_id,
304            exists: loader_blob_exists,
305            blob: loader_blob,
306            data_blobs,
307        })
308    }
309
310    /// Deploys the trade account implementation as a blob.
311    /// Large contracts are deployed as blobs to handle size limitations.
312    ///
313    /// # Arguments
314    /// * `deployer_wallet` - The wallet to use for deployment (pays gas fees)
315    /// * `config` - Deployment configuration containing bytecode and settings
316    ///
317    /// # Returns
318    /// * `Ok(BlobId)` - The ID of the deployed blob
319    /// * `Err(anyhow::Error)` - If deployment fails
320    pub async fn deploy_order_book_blob(
321        deployer_wallet: &W,
322        base_asset: AssetId,
323        quote_asset: AssetId,
324        config: &OrderBookDeployConfig,
325    ) -> Result<BlobId> {
326        let order_book_blob =
327            Self::order_book_blob(deployer_wallet, base_asset, quote_asset, config)
328                .await?;
329        blob_loader::upload_loader_blobs(
330            deployer_wallet,
331            order_book_blob.data_blobs,
332            order_book_blob.blob,
333        )
334        .await
335    }
336
337    pub async fn deploy_order_book_proxy(
338        deployer_wallet: &W,
339        order_book_blob_id: &BlobId,
340        config: &OrderBookDeployConfig,
341    ) -> Result<(OrderBookProxy<W>, bool)> {
342        let proxy_owner = config
343            .proxy_owner
344            .unwrap_or(Identity::Address(deployer_wallet.address()));
345        let blob_id = ContractId::new(*order_book_blob_id);
346        let configurables = config
347            .order_book_proxy_configurables
348            .clone()
349            .with_INITIAL_TARGET(blob_id)?
350            .with_INITIAL_OWNER(State::Initialized(proxy_owner))?;
351        let contract = Contract::regular(
352            config.order_book_proxy_bytecode.clone(),
353            config.salt,
354            config.order_book_proxy_storage_slots.clone(),
355        )
356        .with_configurables(configurables);
357
358        let contract_id = contract.contract_id();
359        let already_deployed = deployer_wallet
360            .try_provider()?
361            .contract_exists(&contract_id)
362            .await?;
363        let order_book_proxy = OrderBookProxy::new(contract_id, deployer_wallet.clone());
364
365        if !already_deployed {
366            contract
367                .deploy(deployer_wallet, TxPolicies::default())
368                .await?;
369        }
370        let requires_initialization = !already_deployed;
371
372        Ok((order_book_proxy, requires_initialization))
373    }
374
375    /// Deploys an OrderBook contract with the specified trading pair.
376    ///
377    /// # Arguments
378    /// * `deployer_wallet` - The wallet to use for deployment (pays gas fees)
379    /// * `owner` - The identity to set as the OrderBook owner
380    /// * `base_asset` - The base asset ID for the trading pair
381    /// * `quote_asset` - The quote asset ID for the trading pair
382    /// * `config` - Deployment configuration containing bytecode and settings
383    ///
384    /// # Returns
385    /// * `Ok(OrderBookDeploy)` - Complete deployment result with contract instance
386    /// * `Err(anyhow::Error)` - If deployment or initialization fails
387    ///
388    /// # Process
389    /// 1. Deploys the OrderBook contract with asset configurables
390    /// 2. Initializes the contract with the specified owner
391    pub async fn deploy(
392        deployer_wallet: &W,
393        base_asset: AssetId,
394        quote_asset: AssetId,
395        config: &OrderBookDeployConfig,
396    ) -> Result<OrderBookDeploy<W>> {
397        // Configure the contract with the trading pair assets
398        let (deploy, requires_initialization) = Self::deploy_without_initialization(
399            deployer_wallet,
400            base_asset,
401            quote_asset,
402            config,
403        )
404        .await?;
405
406        if requires_initialization {
407            // Initialize the OrderBookProxy with the configured owner via configurables
408            deploy.initialize().await?;
409        }
410
411        Ok(deploy)
412    }
413
414    /// Deploys an OrderBook contract like [`Self::deploy`], but without initializing it.
415    pub async fn deploy_without_initialization(
416        deployer_wallet: &W,
417        base_asset: AssetId,
418        quote_asset: AssetId,
419        config: &OrderBookDeployConfig,
420    ) -> Result<(OrderBookDeploy<W>, bool)> {
421        let order_book_blob_id = Self::deploy_order_book_blob(
422            deployer_wallet,
423            base_asset,
424            quote_asset,
425            config,
426        )
427        .await?;
428        let (deploy_order_book_proxy, requires_initialization) =
429            Self::deploy_order_book_proxy(deployer_wallet, &order_book_blob_id, config)
430                .await?;
431        let contract_id = deploy_order_book_proxy.contract_id();
432        let deploy = OrderBookDeploy::new(
433            deployer_wallet.clone(),
434            contract_id,
435            base_asset,
436            quote_asset,
437        );
438
439        Ok((deploy, requires_initialization))
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446    use fuels::test_helpers::{
447        WalletsConfig,
448        launch_custom_provider_and_get_wallets,
449    };
450
451    #[tokio::test]
452    async fn test_deploy_order_book_blob() {
453        // Start fuel-core
454        let mut wallets = launch_custom_provider_and_get_wallets(
455            WalletsConfig::new(Some(2), Some(1), Some(1_000_000_000)),
456            None,
457            Some(::fuels::test_helpers::ChainConfig::local_testnet()),
458        )
459        .await
460        .unwrap();
461        let deployer_wallet = wallets.pop().unwrap();
462        let contract_owner = wallets.pop().unwrap();
463
464        // Deploy OrderBook
465        let base_asset = AssetId::from([1u8; 32]);
466        let quote_asset = AssetId::from([2u8; 32]);
467        let config = OrderBookDeployConfig {
468            proxy_owner: Some(Identity::Address(contract_owner.address())),
469            order_book_owner: Some(Identity::Address(contract_owner.address())),
470            ..Default::default()
471        };
472
473        let order_book_blob_id = OrderBookDeploy::deploy_order_book_blob(
474            &deployer_wallet,
475            base_asset,
476            quote_asset,
477            &config,
478        )
479        .await
480        .unwrap();
481        let (order_book_deploy, requires_initialization) =
482            OrderBookDeploy::deploy_without_initialization(
483                &deployer_wallet,
484                base_asset,
485                quote_asset,
486                &config,
487            )
488            .await
489            .unwrap();
490        let order_book = order_book_deploy.order_book;
491        let order_book_proxy = order_book_deploy.order_book_proxy;
492
493        if requires_initialization {
494            order_book_proxy
495                .methods()
496                .initialize_proxy()
497                .call()
498                .await
499                .unwrap();
500            order_book.methods().initialize().call().await.unwrap();
501        }
502
503        assert_eq!(
504            order_book_proxy
505                .methods()
506                .proxy_owner()
507                .simulate(Execution::state_read_only())
508                .await
509                .unwrap()
510                .value,
511            State::Initialized(Identity::Address(contract_owner.address()))
512        );
513        assert_eq!(
514            order_book_proxy
515                .methods()
516                .proxy_target()
517                .simulate(Execution::state_read_only())
518                .await
519                .unwrap()
520                .value,
521            Some(ContractId::new(order_book_blob_id))
522        );
523        assert_eq!(
524            order_book
525                .methods()
526                .owner()
527                .simulate(Execution::state_read_only())
528                .await
529                .unwrap()
530                .value,
531            State::Initialized(Identity::Address(contract_owner.address()))
532        );
533        assert_eq!(
534            order_book
535                .methods()
536                .get_base_asset()
537                .simulate(Execution::state_read_only())
538                .await
539                .unwrap()
540                .value,
541            base_asset,
542        );
543        assert_eq!(
544            order_book
545                .methods()
546                .get_quote_asset()
547                .simulate(Execution::state_read_only())
548                .await
549                .unwrap()
550                .value,
551            quote_asset,
552        );
553
554        // Test upgrading contract configuration
555        let new_base_asset = AssetId::from([3u8; 32]);
556        let order_book_blob_id = OrderBookDeploy::deploy_order_book_blob(
557            &deployer_wallet,
558            new_base_asset,
559            quote_asset,
560            &config,
561        )
562        .await
563        .unwrap();
564
565        order_book_proxy
566            .with_account(contract_owner)
567            .methods()
568            .set_proxy_target(ContractId::new(order_book_blob_id))
569            .call()
570            .await
571            .unwrap();
572        assert_eq!(
573            order_book
574                .methods()
575                .get_base_asset()
576                .simulate(Execution::state_read_only())
577                .await
578                .unwrap()
579                .value,
580            new_base_asset
581        );
582    }
583
584    #[tokio::test]
585    async fn test_order_book_deployment() {
586        // Start fuel-core
587        let mut wallets = launch_custom_provider_and_get_wallets(
588            WalletsConfig::new(Some(2), Some(1), Some(1_000_000_000)),
589            None,
590            Some(::fuels::test_helpers::ChainConfig::local_testnet()),
591        )
592        .await
593        .unwrap();
594        let deployer_wallet = wallets.pop().unwrap();
595
596        // Deploy OrderBook
597        let base_asset = AssetId::new([1u8; 32]);
598        let quote_asset = AssetId::new([2u8; 32]);
599        let config = OrderBookDeployConfig::default();
600
601        let deployment =
602            OrderBookDeploy::deploy(&deployer_wallet, base_asset, quote_asset, &config)
603                .await
604                .unwrap();
605
606        // Check if contract exists
607        let provider = deployer_wallet.try_provider().unwrap();
608        let contract_exists = provider
609            .contract_exists(&deployment.contract_id)
610            .await
611            .unwrap();
612        assert!(contract_exists, "OrderBook contract should exist");
613
614        // Verify configuration
615        assert_eq!(deployment.base_asset, base_asset);
616        assert_eq!(deployment.quote_asset, quote_asset);
617    }
618}