Skip to main content

o2_tools/
trade_account_deploy.rs

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