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