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