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