Skip to main content

o2_tools/
trade_account_deploy.rs

1use crate::{
2    CallOption,
3    call_handler_ext::CallHandlerExt,
4    contract_ext::ContractExt,
5};
6use anyhow::Result;
7use fuels::{
8    prelude::*,
9    tx::StorageSlot,
10    types::{
11        ContractId,
12        Identity,
13    },
14};
15
16abigen!(
17    Contract(
18        name = "TradingAccountOracle",
19        abi = "artifacts/trade-account-oracle/trade-account-oracle-abi.json"
20    ),
21    Contract(
22        name = "TradingAccount",
23        abi = "artifacts/trade-account/trade-account-abi.json"
24    ),
25    Contract(
26        name = "TradingAccountProxy",
27        abi = "artifacts/trade-account-proxy/trade-account-proxy-abi.json",
28    ),
29    Contract(
30        name = "TradingAccountProxy0_69_1",
31        abi = "artifacts/0.69.1/trade-account-proxy/artifacts/release/trade-account-proxy-abi.json",
32    ),
33);
34pub const TRADE_ACCOUNT_BYTECODE: &[u8] =
35    include_bytes!("../artifacts/trade-account/trade-account.bin");
36pub const TRADE_ACCOUNT_STORAGE: &[u8] =
37    include_bytes!("../artifacts/trade-account/trade-account-storage_slots.json");
38pub const TRADE_ACCOUNT_PROXY_BYTECODE: &[u8] =
39    include_bytes!("../artifacts/trade-account-proxy/trade-account-proxy.bin");
40pub const TRADE_ACCOUNT_PROXY_STORAGE: &[u8] = include_bytes!(
41    "../artifacts/trade-account-proxy/trade-account-proxy-storage_slots.json"
42);
43pub const TRADE_ACCOUNT_ORACLE_BYTECODE: &[u8] =
44    include_bytes!("../artifacts/trade-account-oracle/trade-account-oracle.bin");
45pub const TRADE_ACCOUNT_ORACLE_STORAGE: &[u8] = include_bytes!(
46    "../artifacts/trade-account-oracle/trade-account-oracle-storage_slots.json"
47);
48
49/// Configuration for deploying trade account contracts.
50/// Contains all bytecode and storage slot information needed for deployment.
51#[derive(Clone)]
52pub struct TradeAccountDeployConfig {
53    /// Bytecode for the trade account implementation contract
54    pub trade_account_bytecode: Vec<u8>,
55    /// Bytecode for the oracle contract that manages implementation addresses
56    pub oracle_bytecode: Vec<u8>,
57    /// Bytecode for the proxy contract that delegates to implementations
58    pub proxy_bytecode: Vec<u8>,
59    /// Storage slots configuration for the trade account contract
60    pub trade_account_storage_slots: Vec<StorageSlot>,
61    /// Storage slots configuration for the oracle contract
62    pub oracle_storage_slots: Vec<StorageSlot>,
63    /// Storage slots configuration for the proxy contract
64    pub proxy_storage_slots: Vec<StorageSlot>,
65    /// Maximum words per blob for large contract deployment
66    pub max_words_per_blob: usize,
67    /// Salt for contract deployment
68    pub salt: Salt,
69}
70
71impl TradeAccountDeployConfig {
72    pub fn old_0_69_1() -> Self {
73        pub const TRADE_ACCOUNT_BYTECODE_0_69_1: &[u8] = include_bytes!(
74            "../artifacts/0.69.1/trade-account/artifacts/release/trade-account.bin"
75        );
76        pub const TRADE_ACCOUNT_STORAGE_0_69_1: &[u8] = include_bytes!(
77            "../artifacts/0.69.1/trade-account/artifacts/release/trade-account-storage_slots.json"
78        );
79        pub const TRADE_ACCOUNT_PROXY_BYTECODE_0_69_1: &[u8] = include_bytes!(
80            "../artifacts/0.69.1/trade-account-proxy/artifacts/release/trade-account-proxy.bin"
81        );
82        pub const TRADE_ACCOUNT_PROXY_STORAGE_0_69_1: &[u8] = include_bytes!(
83            "../artifacts/0.69.1/trade-account-proxy/artifacts/release/trade-account-proxy-storage_slots.json"
84        );
85        pub const TRADE_ACCOUNT_ORACLE_BYTECODE_0_69_1: &[u8] = include_bytes!(
86            "../artifacts/0.69.1/trade-account-oracle/artifacts/release/trade-account-oracle.bin"
87        );
88        pub const TRADE_ACCOUNT_ORACLE_STORAGE_0_69_1: &[u8] = include_bytes!(
89            "../artifacts/0.69.1/trade-account-oracle/artifacts/release/trade-account-oracle-storage_slots.json"
90        );
91
92        Self {
93            trade_account_bytecode: TRADE_ACCOUNT_BYTECODE_0_69_1.to_vec(),
94            oracle_bytecode: TRADE_ACCOUNT_ORACLE_BYTECODE_0_69_1.to_vec(),
95            proxy_bytecode: TRADE_ACCOUNT_PROXY_BYTECODE_0_69_1.to_vec(),
96            trade_account_storage_slots: serde_json::from_slice(
97                TRADE_ACCOUNT_STORAGE_0_69_1,
98            )
99            .unwrap(),
100            oracle_storage_slots: serde_json::from_slice(
101                TRADE_ACCOUNT_ORACLE_STORAGE_0_69_1,
102            )
103            .unwrap(),
104            proxy_storage_slots: serde_json::from_slice(
105                TRADE_ACCOUNT_PROXY_STORAGE_0_69_1,
106            )
107            .unwrap(),
108            max_words_per_blob: 10_000,
109            salt: Salt::default(),
110        }
111    }
112}
113
114impl Default for TradeAccountDeployConfig {
115    fn default() -> Self {
116        Self {
117            trade_account_bytecode: TRADE_ACCOUNT_BYTECODE.to_vec(),
118            oracle_bytecode: TRADE_ACCOUNT_ORACLE_BYTECODE.to_vec(),
119            proxy_bytecode: TRADE_ACCOUNT_PROXY_BYTECODE.to_vec(),
120            trade_account_storage_slots: serde_json::from_slice(TRADE_ACCOUNT_STORAGE)
121                .unwrap(),
122            oracle_storage_slots: serde_json::from_slice(TRADE_ACCOUNT_ORACLE_STORAGE)
123                .unwrap(),
124            proxy_storage_slots: serde_json::from_slice(TRADE_ACCOUNT_PROXY_STORAGE)
125                .unwrap(),
126            max_words_per_blob: 100_000,
127            salt: Salt::default(),
128        }
129    }
130}
131
132/// Result of a complete trade account deployment.
133/// Contains all deployed contract instances and their IDs for easy access.
134#[derive(Clone)]
135pub struct TradeAccountDeploy<W> {
136    /// The deployed oracle contract instance
137    pub oracle: TradingAccountOracle<W>,
138    /// Contract ID of the deployed oracle
139    pub oracle_id: ContractId,
140    /// Blob ID of the trade account implementation
141    pub trade_account_blob_id: BlobId,
142    /// The wallet to use for deployment (pays all gas fees)
143    pub deployer_wallet: W,
144    /// The deployed proxy contract instance
145    pub proxy: Option<TradingAccountProxy<W>>,
146    /// Contract ID of the deployed proxy
147    pub proxy_id: Option<ContractId>,
148}
149
150pub struct TradeAccountBlob {
151    /// The ID of the deployed blob
152    pub id: BlobId,
153    /// Whether the blob already exists
154    pub exists: bool,
155    /// The blob data containing the contract bytecode
156    pub blob: Blob,
157}
158
159impl<W> TradeAccountDeploy<W>
160where
161    W: Account + Clone,
162{
163    pub fn change_wallet(mut self, wallet: &W) -> Self {
164        self.deployer_wallet = wallet.clone();
165        self.oracle = self.oracle.with_account(wallet.clone());
166
167        if let Some(proxy) = self.proxy {
168            self.proxy = Some(proxy.with_account(wallet.clone()));
169        }
170
171        self
172    }
173
174    pub async fn from_oracle_id(
175        deployer_wallet: &W,
176        oracle_id: ContractId,
177    ) -> Result<Self>
178    where
179        W: Account + Clone,
180    {
181        let oracle: TradingAccountOracle<W> =
182            TradingAccountOracle::new(oracle_id, deployer_wallet.clone());
183        let trade_account_blob_id = oracle
184            .methods()
185            .get_trade_account_impl()
186            .simulate(Execution::state_read_only())
187            .await?
188            .value
189            .ok_or_else(|| anyhow::anyhow!("Trade account implementation not set"))?
190            .into();
191        Ok(Self {
192            oracle,
193            oracle_id,
194            trade_account_blob_id,
195            deployer_wallet: deployer_wallet.clone(),
196            proxy: None,
197            proxy_id: None,
198        })
199    }
200
201    pub fn trade_account_blob_from_config(
202        config: &TradeAccountDeployConfig,
203    ) -> Result<Blob> {
204        let blobs = Contract::regular(
205            config.trade_account_bytecode.clone(),
206            config.salt,
207            config.trade_account_storage_slots.clone(),
208        )
209        .convert_to_loader(config.max_words_per_blob)?
210        .blobs()
211        .to_vec();
212        let blob = blobs[0].clone();
213        Ok(blob)
214    }
215
216    pub fn trade_account_proxy_blob_from_config(
217        config: &TradeAccountDeployConfig,
218    ) -> Result<Blob> {
219        let blobs = Contract::regular(
220            config.proxy_bytecode.clone(),
221            config.salt,
222            config.proxy_storage_slots.clone(),
223        )
224        .convert_to_loader(config.max_words_per_blob)?
225        .blobs()
226        .to_vec();
227        let blob = blobs[0].clone();
228        Ok(blob)
229    }
230
231    pub async fn trade_account_blob(
232        deployer_wallet: &W,
233        config: &TradeAccountDeployConfig,
234    ) -> Result<TradeAccountBlob> {
235        let blob = Self::trade_account_blob_from_config(config)?;
236        let blob_id = blob.id();
237        let blob_exists = deployer_wallet.try_provider()?.blob_exists(blob_id).await?;
238
239        Ok(TradeAccountBlob {
240            id: blob_id,
241            exists: blob_exists,
242            blob: blob.clone(),
243        })
244    }
245
246    /// Deploys the trade account implementation as a blob.
247    /// Large contracts are deployed as blobs to handle size limitations.
248    ///
249    /// # Arguments
250    /// * `deployer_wallet` - The wallet to use for deployment (pays gas fees)
251    /// * `config` - Deployment configuration containing bytecode and settings
252    ///
253    /// # Returns
254    /// * `Ok(BlobId)` - The ID of the deployed blob
255    /// * `Err(anyhow::Error)` - If deployment fails
256    pub async fn deploy_trade_account_blob(
257        deployer_wallet: &W,
258        config: &DeployConfig,
259    ) -> Result<BlobId> {
260        match config {
261            DeployConfig::Old0_69_1(config) | DeployConfig::Latest(config) => {
262                let trade_account_blob =
263                    Self::trade_account_blob(deployer_wallet, config).await?;
264                if !trade_account_blob.exists {
265                    let mut builder = BlobTransactionBuilder::default()
266                        .with_blob(trade_account_blob.blob.clone());
267
268                    deployer_wallet.adjust_for_fee(&mut builder, 0).await?;
269                    deployer_wallet.add_witnesses(&mut builder)?;
270
271                    let tx = builder.build(&deployer_wallet.try_provider()?).await?;
272
273                    deployer_wallet
274                        .try_provider()?
275                        .send_transaction_and_await_commit(tx)
276                        .await?
277                        .check(None)?;
278                }
279                Ok(trade_account_blob.id)
280            }
281        }
282    }
283
284    /// Deploys the oracle contract that manages trade account implementation addresses.
285    /// The oracle is initialized with the deployer as owner and the blob ID as the current implementation.
286    ///
287    /// # Arguments
288    /// * `deployer_wallet` - The wallet to use for deployment and set as initial owner
289    /// * `trade_account_blob_id` - The blob ID of the trade account implementation
290    /// * `config` - Deployment configuration containing oracle bytecode and settings
291    ///
292    /// # Returns
293    /// * `Ok((TradingAccountOracle, ContractId))` - The oracle instance and its contract ID
294    /// * `Err(anyhow::Error)` - If deployment or initialization fails
295    pub async fn deploy_oracle(
296        deployer_wallet: &W,
297        trade_account_blob_id: &BlobId,
298        config: &DeployConfig,
299    ) -> Result<(TradingAccountOracle<W>, ContractId)> {
300        match config {
301            DeployConfig::Old0_69_1(_) => Err(anyhow::anyhow!(
302                "Deployment with old 0.69.1 config is not supported for oracle"
303            )),
304            DeployConfig::Latest(config) => {
305                let contract = Contract::regular(
306                    config.oracle_bytecode.clone(),
307                    config.salt,
308                    config.oracle_storage_slots.clone(),
309                )
310                .with_configurables(TradingAccountOracleConfigurables::default())
311                .with_salt(config.salt);
312                let contract_id = contract.contract_id();
313                let instance =
314                    TradingAccountOracle::new(contract_id, deployer_wallet.clone());
315                let contract_exists = deployer_wallet
316                    .try_provider()?
317                    .contract_exists(&contract_id)
318                    .await?;
319
320                if !contract_exists {
321                    contract
322                        .deploy(deployer_wallet, TxPolicies::default())
323                        .await?;
324                    instance
325                        .methods()
326                        .initialize(
327                            Identity::Address(deployer_wallet.address()),
328                            ContractId::from(*trade_account_blob_id),
329                        )
330                        .call()
331                        .await?;
332                }
333
334                Ok((instance, contract_id))
335            }
336        }
337    }
338
339    pub fn trade_account_contract(
340        oracle_id: &ContractId,
341        owner_identity: &Identity,
342        config: &DeployConfig,
343    ) -> Result<Contract<fuels::programs::contract::Regular>> {
344        match config {
345            DeployConfig::Old0_69_1(config) => {
346                let configurables = TradingAccountProxy0_69_1Configurables::default()
347                    .with_ORACLE_CONTRACT_ID(*oracle_id)?
348                    .with_INITIAL_OWNER(State::Initialized(*owner_identity))?;
349                let contract = Contract::regular(
350                    config.proxy_bytecode.clone(),
351                    config.salt,
352                    config.proxy_storage_slots.clone(),
353                )
354                .with_configurables(configurables);
355                Ok(contract)
356            }
357            DeployConfig::Latest(config) => {
358                let configurables = TradingAccountProxyConfigurables::default()
359                    .with_ORACLE_CONTRACT_ID(*oracle_id)?
360                    .with_INITIAL_OWNER(State::Initialized(*owner_identity))?;
361                let contract = Contract::regular(
362                    config.proxy_bytecode.clone(),
363                    config.salt,
364                    config.proxy_storage_slots.clone(),
365                )
366                .with_configurables(configurables);
367                Ok(contract)
368            }
369        }
370    }
371
372    /// Deploys the proxy contract that delegates calls to the current implementation.
373    /// The proxy is configured with the oracle contract ID and initialized with the specified owner.
374    ///
375    /// # Arguments
376    /// * `deployer_wallet` - The wallet to use for deployment (pays gas fees)
377    /// * `owner_wallet` - The wallet to set as the proxy owner
378    /// * `oracle_id` - The contract ID of the deployed oracle
379    /// * `config` - Deployment configuration containing proxy bytecode and settings
380    ///
381    /// # Returns
382    /// * `Ok((TradingAccountProxy, ContractId))` - The proxy instance and its contract ID
383    /// * `Err(anyhow::Error)` - If deployment or initialization fails
384    pub async fn deploy_proxy(
385        deployer_wallet: &Wallet,
386        owner_identity: &Identity,
387        oracle_id: ContractId,
388        config: &DeployConfig,
389        call_option: &CallOption,
390    ) -> Result<(TradingAccountProxy<Wallet>, ContractId)>
391    where
392        W: Account + Clone,
393    {
394        let contract = Self::trade_account_contract(&oracle_id, owner_identity, config)?;
395        let result = deployer_wallet
396            .try_provider()?
397            .contract_exists(&contract.contract_id())
398            .await?;
399
400        let id = if !result {
401            match call_option {
402                CallOption::AwaitBlock => {
403                    contract
404                        .deploy(deployer_wallet, TxPolicies::default())
405                        .await?
406                        .contract_id
407                }
408                CallOption::AwaitPreconfirmation(ops) => {
409                    let result = contract
410                        .almost_sync_deploy(
411                            deployer_wallet,
412                            &ops.data_builder,
413                            &ops.utxo_manager,
414                            &ops.tx_config,
415                        )
416                        .await?;
417
418                    // We need to wait for the block to be produced in order to submit next transaction
419                    // that will initialize the proxy.
420                    if let Some(tx_id) = result.tx_id {
421                        deployer_wallet
422                            .try_provider()?
423                            .client()
424                            .await_transaction_commit(&tx_id)
425                            .await?;
426                    }
427                    result.contract_id
428                }
429            }
430        } else {
431            contract.contract_id()
432        };
433
434        let proxy = TradingAccountProxy::new(id, deployer_wallet.clone());
435
436        let response = proxy
437            .methods()
438            .proxy_owner()
439            .simulate(Execution::state_read_only())
440            .await?;
441
442        if response.value == State::Uninitialized {
443            let call_handler =
444                proxy.methods().initialize().with_contract_ids(&[oracle_id]);
445
446            match call_option {
447                CallOption::AwaitBlock => {
448                    call_handler.call().await?;
449                }
450                CallOption::AwaitPreconfirmation(ops) => {
451                    call_handler
452                        .almost_sync_call(
453                            &ops.data_builder,
454                            &ops.utxo_manager,
455                            &ops.tx_config,
456                        )
457                        .await?
458                        .tx_status?;
459                }
460            }
461        }
462
463        Ok((proxy, id))
464    }
465
466    /// Deploys a complete trade account system including implementation blob, oracle, and proxy.
467    /// This is the main deployment function that orchestrates the entire process.
468    ///
469    /// # Arguments
470    /// * `deployer_wallet` - The wallet to use for deployment (pays all gas fees)
471    /// * `owner_wallet` - The wallet to set as the trade account owner
472    /// * `config` - Deployment configuration containing all bytecode and settings
473    ///
474    /// # Returns
475    /// * `Ok(TradeAccountDeploy)` - Complete deployment result with all contract instances
476    /// * `Err(anyhow::Error)` - If any part of the deployment fails
477    ///
478    /// # Process
479    /// 1. Deploys the trade account implementation as a blob
480    /// 2. Deploys the oracle contract and registers the blob ID
481    /// 3. Deploys the proxy contract configured with the oracle
482    pub async fn deploy(
483        deployer_wallet: &W,
484        config: &DeployConfig,
485    ) -> Result<TradeAccountDeploy<W>> {
486        let trade_account_blob_id =
487            Self::deploy_trade_account_blob(deployer_wallet, config).await?;
488        let (oracle, oracle_id) =
489            Self::deploy_oracle(deployer_wallet, &trade_account_blob_id, config).await?;
490        let trade_account_blob_id = oracle
491            .methods()
492            .get_trade_account_impl()
493            .simulate(Execution::state_read_only())
494            .await?
495            .value
496            .unwrap();
497
498        Ok(TradeAccountDeploy {
499            oracle,
500            oracle_id,
501            trade_account_blob_id: trade_account_blob_id.into(),
502            deployer_wallet: deployer_wallet.clone(),
503            proxy: None,
504            proxy_id: None,
505        })
506    }
507}
508
509pub enum DeployConfig {
510    // TODO: Remove, when mainnet is upgraded to latest version of the trade account contracts.
511    Old0_69_1(TradeAccountDeployConfig),
512    Latest(TradeAccountDeployConfig),
513}
514
515impl DeployConfig {
516    pub fn config(&self) -> &TradeAccountDeployConfig {
517        match self {
518            DeployConfig::Old0_69_1(config) | DeployConfig::Latest(config) => config,
519        }
520    }
521}
522
523impl TradeAccountDeploy<Wallet> {
524    pub async fn deploy_with_account(
525        &self,
526        owner_identity: &Identity,
527        config: &DeployConfig,
528        call_option: &CallOption,
529    ) -> Result<Self> {
530        let (proxy, proxy_id) = Self::deploy_proxy(
531            &self.deployer_wallet,
532            owner_identity,
533            self.oracle_id,
534            config,
535            call_option,
536        )
537        .await?;
538        Ok(Self {
539            proxy: Some(proxy),
540            proxy_id: Some(proxy_id),
541            oracle: self.oracle.clone(),
542            oracle_id: self.oracle_id,
543            trade_account_blob_id: self.trade_account_blob_id,
544            deployer_wallet: self.deployer_wallet.clone(),
545        })
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552    use fuels::test_helpers::{
553        WalletsConfig,
554        launch_custom_provider_and_get_wallets,
555    };
556
557    #[tokio::test]
558    async fn test_trade_account_sdk() {
559        // Start fuel-core
560        let mut wallets = launch_custom_provider_and_get_wallets(
561            WalletsConfig::new(Some(1), Some(1), Some(1_000_000_000)),
562            None,
563            None,
564        )
565        .await
566        .unwrap();
567        let wallet = wallets.pop().unwrap();
568
569        // Deploy contracts
570        let config = DeployConfig::Latest(TradeAccountDeployConfig::default());
571        let deployment = TradeAccountDeploy::deploy(&wallet, &config)
572            .await
573            .unwrap()
574            .deploy_with_account(
575                &wallet.address().into(),
576                &config,
577                &CallOption::AwaitBlock,
578            )
579            .await
580            .unwrap();
581
582        // Check if IDs exist by querying the deployed contracts
583        let provider = wallet.try_provider().unwrap();
584
585        // Check oracle contract exists
586        let oracle_contract_info = provider
587            .contract_exists(&deployment.oracle_id)
588            .await
589            .unwrap();
590        assert!(oracle_contract_info, "Oracle contract should exist");
591
592        // Check proxy contract exists
593        let proxy_contract_info = provider
594            .contract_exists(&deployment.proxy_id.unwrap())
595            .await
596            .unwrap();
597        assert!(proxy_contract_info, "Proxy contract should exist");
598
599        // Verify blob exists by checking the oracle's stored blob ID
600        let stored_blob_id = deployment
601            .oracle
602            .methods()
603            .get_trade_account_impl()
604            .simulate(Execution::state_read_only())
605            .await
606            .unwrap()
607            .value;
608
609        assert_eq!(
610            deployment.trade_account_blob_id,
611            BlobId::from(stored_blob_id.unwrap()),
612            "Trade account blob ID should match"
613        );
614    }
615}