fuels_programs/contract/
loader.rs

1use std::collections::HashSet;
2
3use fuel_tx::{Bytes32, ContractId, Salt, StorageSlot};
4use fuels_accounts::Account;
5use fuels_core::{
6    constants::WORD_SIZE,
7    types::{
8        bech32::Bech32ContractId,
9        errors::{error, Result},
10        transaction::TxPolicies,
11        transaction_builders::{Blob, BlobId, BlobTransactionBuilder, TransactionBuilder},
12    },
13};
14
15use crate::{assembly::contract_call::loader_contract_asm, DEFAULT_MAX_FEE_ESTIMATION_TOLERANCE};
16
17use super::{compute_contract_id_and_state_root, Contract, Regular};
18
19#[derive(Debug, Clone)]
20pub struct BlobsUploaded {
21    blob_ids: Vec<BlobId>,
22}
23
24#[derive(Debug, Clone)]
25pub struct BlobsNotUploaded {
26    blobs: Vec<Blob>,
27}
28
29#[derive(Debug, Clone)]
30pub struct Loader<Blobs> {
31    as_blobs: Blobs,
32}
33
34impl Contract<Loader<BlobsNotUploaded>> {
35    pub fn code(&self) -> Vec<u8> {
36        let ids: Vec<_> = self.blob_ids();
37        loader_contract_asm(&ids)
38            .expect("a contract to be creatable due to the check done in loader_from_blobs")
39    }
40
41    pub fn contract_id(&self) -> ContractId {
42        self.compute_roots().0
43    }
44
45    pub fn code_root(&self) -> Bytes32 {
46        self.compute_roots().1
47    }
48
49    pub fn state_root(&self) -> Bytes32 {
50        self.compute_roots().2
51    }
52
53    fn compute_roots(&self) -> (ContractId, Bytes32, Bytes32) {
54        compute_contract_id_and_state_root(&self.code(), &self.salt, &self.storage_slots)
55    }
56
57    /// Creates a loader contract for the code found in `blobs`. Calling `deploy` on this contract
58    /// does two things:
59    /// 1. Uploads the code blobs.
60    /// 2. Deploys the loader contract.
61    ///
62    /// The loader contract, when executed, will load all the given blobs into memory and delegate the call to the original contract code contained in the blobs.
63    pub fn loader_from_blobs(
64        blobs: Vec<Blob>,
65        salt: Salt,
66        storage_slots: Vec<StorageSlot>,
67    ) -> Result<Self> {
68        if blobs.is_empty() {
69            return Err(error!(Other, "must provide at least one blob"));
70        }
71
72        let idx_of_last_blob = blobs.len().saturating_sub(1);
73        let idx_of_offender = blobs.iter().enumerate().find_map(|(idx, blob)| {
74            (blob.len() % WORD_SIZE != 0 && idx != idx_of_last_blob).then_some(idx)
75        });
76
77        if let Some(idx) = idx_of_offender {
78            return Err(error!(
79                Other,
80                "blob {}/{} has a size of {} bytes, which is not a multiple of {WORD_SIZE}",
81                idx.saturating_add(1),
82                blobs.len(),
83                blobs[idx].len()
84            ));
85        }
86
87        let ids = blobs.iter().map(|blob| blob.id()).collect::<Vec<_>>();
88
89        // Validate that the loader contract can be created.
90        loader_contract_asm(&ids)?;
91
92        Ok(Self {
93            code: Loader {
94                as_blobs: BlobsNotUploaded { blobs },
95            },
96            salt,
97            storage_slots,
98        })
99    }
100
101    pub fn blobs(&self) -> &[Blob] {
102        self.code.as_blobs.blobs.as_slice()
103    }
104
105    pub fn blob_ids(&self) -> Vec<BlobId> {
106        self.code
107            .as_blobs
108            .blobs
109            .iter()
110            .map(|blob| blob.id())
111            .collect()
112    }
113
114    /// Uploads the blobs associated with this contract. Calling `deploy` on the result will only
115    /// deploy the loader contract.
116    pub async fn upload_blobs(
117        self,
118        account: &impl Account,
119        tx_policies: TxPolicies,
120    ) -> Result<Contract<Loader<BlobsUploaded>>> {
121        let provider = account.try_provider()?;
122
123        let all_blob_ids = self.blob_ids();
124        let mut already_uploaded = HashSet::new();
125
126        for blob in self.code.as_blobs.blobs {
127            let id = blob.id();
128
129            if already_uploaded.contains(&id) {
130                continue;
131            }
132
133            if provider.blob_exists(id).await? {
134                already_uploaded.insert(id);
135                continue;
136            }
137
138            let mut tb = BlobTransactionBuilder::default()
139                .with_blob(blob)
140                .with_tx_policies(tx_policies)
141                .with_max_fee_estimation_tolerance(DEFAULT_MAX_FEE_ESTIMATION_TOLERANCE);
142
143            account.adjust_for_fee(&mut tb, 0).await?;
144            account.add_witnesses(&mut tb)?;
145
146            let tx = tb.build(provider).await?;
147
148            let tx_status_response = provider.send_transaction_and_await_commit(tx).await;
149            tx_status_response.and_then(|response| response.check(None))?;
150
151            already_uploaded.insert(id);
152        }
153
154        Contract::loader_from_blob_ids(all_blob_ids, self.salt, self.storage_slots)
155    }
156
157    /// Deploys the loader contract after uploading the code blobs.
158    pub async fn deploy(
159        self,
160        account: &impl Account,
161        tx_policies: TxPolicies,
162    ) -> Result<Bech32ContractId> {
163        self.upload_blobs(account, tx_policies)
164            .await?
165            .deploy(account, tx_policies)
166            .await
167    }
168
169    /// Deploys the loader contract after uploading the code blobs,
170    /// if there is no contract with this ContractId Already.
171    pub async fn deploy_if_not_exists(
172        self,
173        account: &impl Account,
174        tx_policies: TxPolicies,
175    ) -> Result<Bech32ContractId> {
176        self.upload_blobs(account, tx_policies)
177            .await?
178            .deploy_if_not_exists(account, tx_policies)
179            .await
180    }
181    /// Reverts the contract from a loader contract back to a regular contract.
182    pub fn revert_to_regular(self) -> Contract<Regular> {
183        let code = self
184            .code
185            .as_blobs
186            .blobs
187            .into_iter()
188            .flat_map(Vec::from)
189            .collect();
190
191        Contract::regular(code, self.salt, self.storage_slots)
192    }
193}
194
195impl Contract<Loader<BlobsUploaded>> {
196    pub fn code(&self) -> Vec<u8> {
197        loader_contract_asm(&self.code.as_blobs.blob_ids)
198            .expect("a contract to be creatable due to the check done in loader_for_blobs")
199    }
200
201    pub fn contract_id(&self) -> ContractId {
202        self.compute_roots().0
203    }
204
205    pub fn code_root(&self) -> Bytes32 {
206        self.compute_roots().1
207    }
208
209    pub fn state_root(&self) -> Bytes32 {
210        self.compute_roots().2
211    }
212
213    pub fn compute_roots(&self) -> (ContractId, Bytes32, Bytes32) {
214        compute_contract_id_and_state_root(&self.code(), &self.salt, &self.storage_slots)
215    }
216
217    /// Creates a loader contract using previously uploaded blobs.
218    ///
219    /// The contract code has been uploaded in blobs with [`BlobId`]s specified in `blob_ids`.
220    /// This will create a loader contract that, when deployed and executed, will load all the specified blobs into memory and delegate the call to the code contained in the blobs.
221    pub fn loader_from_blob_ids(
222        blob_ids: Vec<BlobId>,
223        salt: Salt,
224        storage_slots: Vec<StorageSlot>,
225    ) -> Result<Self> {
226        if blob_ids.is_empty() {
227            return Err(error!(Other, "must provide at least one blob"));
228        }
229
230        // Validate that the loader contract can be created.
231        loader_contract_asm(&blob_ids)?;
232
233        Ok(Self {
234            code: Loader {
235                as_blobs: BlobsUploaded { blob_ids },
236            },
237            salt,
238            storage_slots,
239        })
240    }
241
242    pub fn blob_ids(&self) -> &[BlobId] {
243        &self.code.as_blobs.blob_ids
244    }
245
246    /// Deploys the loader contract.
247    pub async fn deploy(
248        self,
249        account: &impl Account,
250        tx_policies: TxPolicies,
251    ) -> Result<Bech32ContractId> {
252        Contract::regular(self.code(), self.salt, self.storage_slots)
253            .deploy(account, tx_policies)
254            .await
255    }
256
257    pub async fn deploy_if_not_exists(
258        self,
259        account: &impl Account,
260        tx_policies: TxPolicies,
261    ) -> Result<Bech32ContractId> {
262        Contract::regular(self.code(), self.salt, self.storage_slots)
263            .deploy_if_not_exists(account, tx_policies)
264            .await
265    }
266}