Skip to main content

o2_tools/
trial_trade_account_deploy.rs

1use crate::{
2    CallOption,
3    blob_loader,
4    contract_ext::ContractExt,
5};
6use anyhow::Result;
7use fuel_core_client::client::FuelClient;
8use fuels::{
9    core::Configurables,
10    prelude::*,
11    tx::StorageSlot,
12    types::{
13        Address,
14        AssetId,
15        ContractId,
16        Identity,
17    },
18};
19
20abigen!(
21    Contract(
22        name = "TrialTradingAccountOracle",
23        abi = "artifacts/trial-trade-account-oracle/trial-trade-account-oracle-abi.json"
24    ),
25    Contract(
26        name = "TrialTradingAccount",
27        abi = "artifacts/trial-trade-account/trial-trade-account-abi.json"
28    ),
29    Contract(
30        name = "TrialTradingAccountProxy",
31        abi = "artifacts/trial-trade-account-proxy/trial-trade-account-proxy-abi.json",
32    ),
33);
34
35pub const TRIAL_TRADE_ACCOUNT_BYTECODE: &[u8] =
36    include_bytes!("../artifacts/trial-trade-account/trial-trade-account.bin");
37pub const TRIAL_TRADE_ACCOUNT_STORAGE: &[u8] = include_bytes!(
38    "../artifacts/trial-trade-account/trial-trade-account-storage_slots.json"
39);
40pub const TRIAL_TRADE_ACCOUNT_PROXY_BYTECODE: &[u8] = include_bytes!(
41    "../artifacts/trial-trade-account-proxy/trial-trade-account-proxy.bin"
42);
43pub const TRIAL_TRADE_ACCOUNT_PROXY_STORAGE: &[u8] = include_bytes!(
44    "../artifacts/trial-trade-account-proxy/trial-trade-account-proxy-storage_slots.json"
45);
46pub const TRIAL_TRADE_ACCOUNT_ORACLE_BYTECODE: &[u8] = include_bytes!(
47    "../artifacts/trial-trade-account-oracle/trial-trade-account-oracle.bin"
48);
49pub const TRIAL_TRADE_ACCOUNT_ORACLE_STORAGE: &[u8] = include_bytes!(
50    "../artifacts/trial-trade-account-oracle/trial-trade-account-oracle-storage_slots.json"
51);
52pub const DEFAULT_TRIAL_DURATION: u64 = 604800;
53pub const DEFAULT_LIQUIDATION_THRESHOLD_AMOUNT: u64 = 0;
54pub const DEFAULT_TRIAL_USER_PROFIT_SHARE_BPS: u64 = 0;
55
56#[derive(Clone)]
57pub struct TrialTradeAccountDeployConfig {
58    pub trial_trade_account_bytecode: Vec<u8>,
59    pub oracle_bytecode: Vec<u8>,
60    pub oracle_storage_slots: Vec<StorageSlot>,
61    pub proxy_bytecode: Vec<u8>,
62    pub trial_trade_account_storage_slots: Vec<StorageSlot>,
63    pub proxy_storage_slots: Vec<StorageSlot>,
64    pub trial_trade_account_config: TrialTradingAccountConfigurables,
65    pub proxy_config: TrialTradingAccountProxyConfigurables,
66    pub cosigner: Option<Address>,
67    pub trial_duration: u64,
68    pub liquidation_threshold_amount: u64,
69    pub liquidation_threshold_asset_id: AssetId,
70    pub trial_allowed_order_book_ids: Vec<ContractId>,
71    pub trial_user_profit_share_bps: u64,
72    pub trial_platform_payout_identity: Identity,
73    pub max_words_per_blob: usize,
74    pub salt: Salt,
75}
76
77impl Default for TrialTradeAccountDeployConfig {
78    fn default() -> Self {
79        Self {
80            trial_trade_account_bytecode: TRIAL_TRADE_ACCOUNT_BYTECODE.to_vec(),
81            oracle_bytecode: TRIAL_TRADE_ACCOUNT_ORACLE_BYTECODE.to_vec(),
82            oracle_storage_slots: serde_json::from_slice(
83                TRIAL_TRADE_ACCOUNT_ORACLE_STORAGE,
84            )
85            .unwrap(),
86            proxy_bytecode: TRIAL_TRADE_ACCOUNT_PROXY_BYTECODE.to_vec(),
87            trial_trade_account_storage_slots: serde_json::from_slice(
88                TRIAL_TRADE_ACCOUNT_STORAGE,
89            )
90            .unwrap(),
91            proxy_storage_slots: serde_json::from_slice(
92                TRIAL_TRADE_ACCOUNT_PROXY_STORAGE,
93            )
94            .unwrap(),
95            trial_trade_account_config: TrialTradingAccountConfigurables::default(),
96            proxy_config: TrialTradingAccountProxyConfigurables::default(),
97            cosigner: None,
98            trial_duration: DEFAULT_TRIAL_DURATION,
99            liquidation_threshold_amount: DEFAULT_LIQUIDATION_THRESHOLD_AMOUNT,
100            liquidation_threshold_asset_id: AssetId::zeroed(),
101            trial_allowed_order_book_ids: Vec::new(),
102            trial_user_profit_share_bps: DEFAULT_TRIAL_USER_PROFIT_SHARE_BPS,
103            trial_platform_payout_identity: Identity::Address(Address::zeroed()),
104            max_words_per_blob: 10_000,
105            salt: Salt::default(),
106        }
107    }
108}
109
110impl TrialTradeAccountDeployConfig {
111    pub fn with_oracle_id(mut self, oracle_id: ContractId) -> Result<Self> {
112        self.trial_trade_account_config = self
113            .trial_trade_account_config
114            .with_ORACLE_CONTRACT_ID(oracle_id)?;
115        self.proxy_config = self.proxy_config.with_ORACLE_CONTRACT_ID(oracle_id)?;
116        Ok(self)
117    }
118
119    pub fn with_cosigner(mut self, cosigner: Address) -> Self {
120        self.cosigner = Some(cosigner);
121        self
122    }
123
124    pub fn with_trial_duration(mut self, trial_duration: u64) -> Self {
125        self.trial_duration = trial_duration;
126        self
127    }
128
129    pub fn with_liquidation_threshold_amount(
130        mut self,
131        liquidation_threshold_amount: u64,
132    ) -> Self {
133        self.liquidation_threshold_amount = liquidation_threshold_amount;
134        self
135    }
136
137    pub fn with_liquidation_threshold_asset_id(
138        mut self,
139        liquidation_threshold_asset_id: AssetId,
140    ) -> Self {
141        self.liquidation_threshold_asset_id = liquidation_threshold_asset_id;
142        self
143    }
144
145    pub fn with_trial_allowed_order_book_ids(
146        mut self,
147        trial_allowed_order_book_ids: Vec<ContractId>,
148    ) -> Self {
149        self.trial_allowed_order_book_ids = trial_allowed_order_book_ids;
150        self
151    }
152
153    pub fn with_trial_user_profit_share_bps(
154        mut self,
155        trial_user_profit_share_bps: u64,
156    ) -> Self {
157        self.trial_user_profit_share_bps = trial_user_profit_share_bps;
158        self
159    }
160
161    pub fn with_trial_platform_payout_identity(
162        mut self,
163        trial_platform_payout_identity: Identity,
164    ) -> Self {
165        self.trial_platform_payout_identity = trial_platform_payout_identity;
166        self
167    }
168
169    pub fn with_initial_session_id(mut self, session_id: Identity) -> Result<Self> {
170        self.proxy_config = self.proxy_config.with_INITIAL_SESSION_ID(session_id)?;
171        Ok(self)
172    }
173
174    pub fn with_trial_trade_account_registry(
175        mut self,
176        registry_id: ContractId,
177    ) -> Result<Self> {
178        self.proxy_config = self
179            .proxy_config
180            .with_TRIAL_TRADE_ACCOUNT_REGISTRY(registry_id)?;
181        Ok(self)
182    }
183}
184
185#[derive(Clone)]
186pub struct TrialTradeAccountDeploy<W> {
187    pub oracle: TrialTradingAccountOracle<W>,
188    pub oracle_id: ContractId,
189    pub trial_trade_account_blob_id: BlobId,
190    pub deployer_wallet: W,
191    pub proxy: Option<TrialTradingAccountProxy<W>>,
192    pub proxy_id: Option<ContractId>,
193}
194
195pub struct TrialTradeAccountBlob {
196    pub id: BlobId,
197    pub exists: bool,
198    pub blob: Blob,
199    pub data_blobs: Vec<Blob>,
200}
201
202impl<W> TrialTradeAccountDeploy<W>
203where
204    W: Account + Clone,
205{
206    pub fn change_wallet(mut self, wallet: &W) -> Self {
207        self.deployer_wallet = wallet.clone();
208        self.oracle = self.oracle.with_account(wallet.clone());
209
210        if let Some(proxy) = self.proxy {
211            self.proxy = Some(proxy.with_account(wallet.clone()));
212        }
213
214        self
215    }
216
217    pub async fn from_oracle_id(
218        deployer_wallet: &W,
219        oracle_id: ContractId,
220    ) -> Result<Self> {
221        let oracle = TrialTradingAccountOracle::new(oracle_id, deployer_wallet.clone());
222        let trial_trade_account_blob_id = oracle
223            .methods()
224            .get_trial_account_impl()
225            .simulate(Execution::state_read_only())
226            .await?
227            .value
228            .ok_or_else(|| anyhow::anyhow!("Trial trade account implementation not set"))?
229            .into();
230
231        Ok(Self {
232            oracle,
233            oracle_id,
234            trial_trade_account_blob_id,
235            deployer_wallet: deployer_wallet.clone(),
236            proxy: None,
237            proxy_id: None,
238        })
239    }
240
241    pub fn trial_trade_account_blob_from_config(
242        config: &TrialTradeAccountDeployConfig,
243    ) -> Result<(Vec<Blob>, Blob)> {
244        blob_loader::build_loader_blobs(
245            config.trial_trade_account_bytecode.clone(),
246            config.salt,
247            config.trial_trade_account_storage_slots.clone(),
248            config.trial_trade_account_config.clone(),
249            config.max_words_per_blob,
250        )
251    }
252
253    pub fn trial_trade_account_proxy_blob_from_config(
254        config: &TrialTradeAccountDeployConfig,
255    ) -> Result<(Vec<Blob>, Blob)> {
256        blob_loader::build_loader_blobs(
257            config.proxy_bytecode.clone(),
258            config.salt,
259            config.proxy_storage_slots.clone(),
260            Configurables::from(config.proxy_config.clone()),
261            config.max_words_per_blob,
262        )
263    }
264
265    pub async fn trial_trade_account_blob(
266        deployer_wallet: &W,
267        config: &TrialTradeAccountDeployConfig,
268    ) -> Result<TrialTradeAccountBlob> {
269        let (data_blobs, loader_blob) =
270            Self::trial_trade_account_blob_from_config(config)?;
271        let loader_blob_id = loader_blob.id();
272        let loader_blob_exists = deployer_wallet
273            .try_provider()?
274            .blob_exists(loader_blob_id)
275            .await?;
276
277        Ok(TrialTradeAccountBlob {
278            id: loader_blob_id,
279            exists: loader_blob_exists,
280            blob: loader_blob,
281            data_blobs,
282        })
283    }
284
285    pub async fn deploy_trial_trade_account_blob(
286        deployer_wallet: &W,
287        config: &DeployConfig,
288    ) -> Result<BlobId> {
289        match config {
290            DeployConfig::Latest(config) => {
291                let trial_trade_account_blob =
292                    Self::trial_trade_account_blob(deployer_wallet, config).await?;
293                blob_loader::upload_loader_blobs(
294                    deployer_wallet,
295                    trial_trade_account_blob.data_blobs,
296                    trial_trade_account_blob.blob,
297                )
298                .await
299            }
300        }
301    }
302
303    /// Deploy the dedicated trial oracle (salted, so the id is deterministic
304    /// per config) and initialize it with the deployer as owner. Idempotent:
305    /// an already-deployed oracle is loaded instead.
306    pub async fn deploy_oracle(
307        deployer_wallet: &W,
308        config: &DeployConfig,
309    ) -> Result<(TrialTradingAccountOracle<W>, ContractId)> {
310        match config {
311            DeployConfig::Latest(config) => {
312                let contract = Contract::regular(
313                    config.oracle_bytecode.clone(),
314                    config.salt,
315                    config.oracle_storage_slots.clone(),
316                )
317                .with_configurables(TrialTradingAccountOracleConfigurables::default())
318                .with_salt(config.salt);
319                let contract_id = contract.contract_id();
320                let instance =
321                    TrialTradingAccountOracle::new(contract_id, deployer_wallet.clone());
322                let contract_exists = deployer_wallet
323                    .try_provider()?
324                    .contract_exists(&contract_id)
325                    .await?;
326
327                if !contract_exists {
328                    contract
329                        .deploy(deployer_wallet, TxPolicies::default())
330                        .await?;
331                    instance
332                        .methods()
333                        .initialize(Identity::Address(deployer_wallet.address()))
334                        .call()
335                        .await?;
336                }
337
338                Ok((instance, contract_id))
339            }
340        }
341    }
342
343    /// Deploy the full trial oracle stack: the trial oracle itself, the trial
344    /// implementation blob, and the oracle state (implementation id and,
345    /// when configured, the cosigner).
346    pub async fn deploy(
347        deployer_wallet: &W,
348        config: &DeployConfig,
349    ) -> Result<TrialTradeAccountDeploy<W>> {
350        let (_, oracle_id) = Self::deploy_oracle(deployer_wallet, config).await?;
351        Self::deploy_to_oracle(deployer_wallet, oracle_id, config).await
352    }
353
354    pub async fn deploy_to_oracle(
355        deployer_wallet: &W,
356        oracle_id: ContractId,
357        config: &DeployConfig,
358    ) -> Result<TrialTradeAccountDeploy<W>> {
359        let config = match config {
360            DeployConfig::Latest(config) => {
361                DeployConfig::Latest(config.clone().with_oracle_id(oracle_id)?)
362            }
363        };
364        let trial_trade_account_blob_id =
365            Self::deploy_trial_trade_account_blob(deployer_wallet, &config).await?;
366        let oracle = TrialTradingAccountOracle::new(oracle_id, deployer_wallet.clone());
367        let implementation = ContractId::from(trial_trade_account_blob_id);
368        let current_impl = oracle
369            .methods()
370            .get_trial_account_impl()
371            .simulate(Execution::state_read_only())
372            .await?
373            .value;
374
375        if current_impl != Some(implementation) {
376            oracle
377                .methods()
378                .set_trial_account_impl(implementation)
379                .call()
380                .await?;
381        }
382
383        if let Some(cosigner) = config.config().cosigner {
384            let current_cosigner = oracle
385                .methods()
386                .get_cosigner()
387                .simulate(Execution::state_read_only())
388                .await?
389                .value;
390
391            if current_cosigner != Some(cosigner) {
392                oracle.methods().set_cosigner(cosigner).call().await?;
393            }
394        }
395
396        Ok(TrialTradeAccountDeploy {
397            oracle,
398            oracle_id,
399            trial_trade_account_blob_id,
400            deployer_wallet: deployer_wallet.clone(),
401            proxy: None,
402            proxy_id: None,
403        })
404    }
405}
406
407impl TrialTradeAccountDeploy<Wallet> {
408    pub fn trial_trade_account_contract(
409        oracle_id: &ContractId,
410        config: &DeployConfig,
411    ) -> Result<Contract<fuels::programs::contract::Regular>> {
412        match config {
413            DeployConfig::Latest(config) => {
414                let configurables = config
415                    .proxy_config
416                    .clone()
417                    .with_ORACLE_CONTRACT_ID(*oracle_id)?;
418                let contract = Contract::regular(
419                    config.proxy_bytecode.clone(),
420                    config.salt,
421                    config.proxy_storage_slots.clone(),
422                )
423                .with_configurables(configurables);
424                Ok(contract)
425            }
426        }
427    }
428
429    pub async fn deploy_proxy(
430        deployer_wallet: &Wallet,
431        oracle_id: ContractId,
432        config: &DeployConfig,
433        call_option: &CallOption,
434        dry_run_client: Option<&FuelClient>,
435        submit_clients: &[FuelClient],
436    ) -> Result<(TrialTradingAccountProxy<Wallet>, ContractId, Option<u64>)> {
437        let contract = Self::trial_trade_account_contract(&oracle_id, config)?;
438        let already_deployed = deployer_wallet
439            .try_provider()?
440            .contract_exists(&contract.contract_id())
441            .await?;
442
443        let (id, total_gas) = if !already_deployed {
444            match call_option {
445                CallOption::AwaitBlock => {
446                    let id = contract
447                        .deploy(deployer_wallet, TxPolicies::default())
448                        .await?
449                        .contract_id;
450                    (id, None)
451                }
452                CallOption::AwaitPreconfirmation(ops) => {
453                    let result = contract
454                        .almost_sync_deploy(
455                            deployer_wallet,
456                            &ops.data_builder,
457                            &ops.utxo_manager,
458                            &ops.tx_config,
459                            submit_clients,
460                        )
461                        .await?;
462
463                    let mut total_gas = None;
464                    if let Some(tx_id) = result.tx_id {
465                        let status = deployer_wallet
466                            .try_provider()?
467                            .client()
468                            .await_transaction_commit(&tx_id)
469                            .await?;
470                        if let fuel_core_client::client::types::TransactionStatus::Success {
471                            total_gas: gas,
472                            ..
473                        } = status
474                        {
475                            total_gas = Some(gas);
476                        }
477                    }
478                    (result.contract_id, total_gas)
479                }
480            }
481        } else {
482            (contract.contract_id(), None)
483        };
484
485        let proxy = TrialTradingAccountProxy::new(id, deployer_wallet.clone());
486
487        let _ = dry_run_client;
488
489        Ok((proxy, id, total_gas))
490    }
491
492    /// Deploys the trial account proxy. Returns the deploy handle and, when a
493    /// fresh preconfirmation deploy happened, the deploy transaction's total
494    /// gas (None when the contract already existed or gas is unavailable).
495    pub async fn deploy_with_account(
496        &self,
497        config: &DeployConfig,
498        call_option: &CallOption,
499        dry_run_client: Option<&FuelClient>,
500        submit_clients: &[FuelClient],
501    ) -> Result<(Self, Option<u64>)> {
502        let (proxy, proxy_id, total_gas) = Self::deploy_proxy(
503            &self.deployer_wallet,
504            self.oracle_id,
505            config,
506            call_option,
507            dry_run_client,
508            submit_clients,
509        )
510        .await?;
511        Ok((
512            Self {
513                proxy: Some(proxy),
514                proxy_id: Some(proxy_id),
515                oracle: self.oracle.clone(),
516                oracle_id: self.oracle_id,
517                trial_trade_account_blob_id: self.trial_trade_account_blob_id,
518                deployer_wallet: self.deployer_wallet.clone(),
519            },
520            total_gas,
521        ))
522    }
523}
524
525#[derive(Clone)]
526pub enum DeployConfig {
527    Latest(TrialTradeAccountDeployConfig),
528}
529
530impl DeployConfig {
531    pub fn config(&self) -> &TrialTradeAccountDeployConfig {
532        match self {
533            DeployConfig::Latest(config) => config,
534        }
535    }
536}