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