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