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