fuels_programs/
contract.rs

1mod storage;
2
3use std::fmt::Debug;
4
5use fuel_tx::{Bytes32, Contract as FuelContract, ContractId, Salt, StorageSlot};
6pub use storage::*;
7
8/// Represents a contract that can be deployed either directly ([`Contract::regular`]) or through a loader [`Contract::convert_to_loader`].
9/// Provides the ability to calculate the `ContractId` ([`Contract::contract_id`]) without needing to deploy the contract.
10/// This struct also manages contract code updates with `configurable`s
11/// ([`Contract::with_configurables`]) and can automatically
12/// load storage slots (via [`Contract::load_from`]).
13#[derive(Debug, Clone, PartialEq)]
14pub struct Contract<Code> {
15    code: Code,
16    salt: Salt,
17    storage_slots: Vec<StorageSlot>,
18}
19
20impl<T> Contract<T> {
21    pub fn salt(&self) -> Salt {
22        self.salt
23    }
24
25    pub fn with_salt(mut self, salt: impl Into<Salt>) -> Self {
26        self.salt = salt.into();
27        self
28    }
29
30    pub fn storage_slots(&self) -> &[StorageSlot] {
31        &self.storage_slots
32    }
33
34    pub fn with_storage_slots(mut self, storage_slots: Vec<StorageSlot>) -> Self {
35        self.storage_slots = storage_slots;
36        self
37    }
38}
39
40mod regular;
41pub use regular::*;
42
43mod loader;
44// reexported to avoid doing a breaking change
45pub use loader::*;
46
47pub use crate::assembly::contract_call::loader_contract_asm;
48
49fn compute_contract_id_and_state_root(
50    binary: &[u8],
51    salt: &Salt,
52    storage_slots: &[StorageSlot],
53) -> (ContractId, Bytes32, Bytes32) {
54    let fuel_contract = FuelContract::from(binary);
55    let code_root = fuel_contract.root();
56    let state_root = FuelContract::initial_state_root(storage_slots.iter());
57
58    let contract_id = fuel_contract.id(salt, &code_root, &state_root);
59
60    (contract_id, code_root, state_root)
61}
62
63#[cfg(test)]
64mod tests {
65    use std::path::Path;
66
67    use fuels_core::types::{
68        errors::{Error, Result},
69        transaction_builders::Blob,
70    };
71    use tempfile::tempdir;
72
73    use super::*;
74    use crate::assembly::contract_call::loader_contract_asm;
75
76    #[test]
77    fn autoload_storage_slots() {
78        // given
79        let temp_dir = tempdir().unwrap();
80        let contract_bin = temp_dir.path().join("my_contract.bin");
81        std::fs::write(&contract_bin, "").unwrap();
82
83        let storage_file = temp_dir.path().join("my_contract-storage_slots.json");
84
85        let expected_storage_slots = vec![StorageSlot::new([1; 32].into(), [2; 32].into())];
86        save_slots(&expected_storage_slots, &storage_file);
87
88        let storage_config = StorageConfiguration::new(true, vec![]);
89        let load_config = LoadConfiguration::default().with_storage_configuration(storage_config);
90
91        // when
92        let loaded_contract = Contract::load_from(&contract_bin, load_config).unwrap();
93
94        // then
95        assert_eq!(loaded_contract.storage_slots, expected_storage_slots);
96    }
97
98    #[test]
99    fn autoload_fails_if_file_missing() {
100        // given
101        let temp_dir = tempdir().unwrap();
102        let contract_bin = temp_dir.path().join("my_contract.bin");
103        std::fs::write(&contract_bin, "").unwrap();
104
105        let storage_config = StorageConfiguration::new(true, vec![]);
106        let load_config = LoadConfiguration::default().with_storage_configuration(storage_config);
107
108        // when
109        let error = Contract::load_from(&contract_bin, load_config)
110            .expect_err("should have failed because the storage slots file is missing");
111
112        // then
113        let storage_slots_path = temp_dir.path().join("my_contract-storage_slots.json");
114        let Error::Other(msg) = error else {
115            panic!("expected an error of type `Other`");
116        };
117        assert_eq!(
118            msg,
119            format!(
120                "could not autoload storage slots from file: {storage_slots_path:?}. Either provide the file or disable autoloading in `StorageConfiguration`"
121            )
122        );
123    }
124
125    fn save_slots(slots: &Vec<StorageSlot>, path: &Path) {
126        std::fs::write(
127            path,
128            serde_json::to_string::<Vec<StorageSlot>>(slots).unwrap(),
129        )
130        .unwrap()
131    }
132
133    #[test]
134    fn blob_size_must_be_greater_than_zero() {
135        // given
136        let contract = Contract::regular(vec![0x00], Salt::zeroed(), vec![]);
137
138        // when
139        let err = contract
140            .convert_to_loader(0)
141            .expect_err("should have failed because blob size is 0");
142
143        // then
144        assert_eq!(
145            err.to_string(),
146            "blob size must be greater than 0".to_string()
147        );
148    }
149
150    #[test]
151    fn contract_with_no_code_cannot_be_turned_into_a_loader() {
152        // given
153        let contract = Contract::regular(vec![], Salt::zeroed(), vec![]);
154
155        // when
156        let err = contract
157            .convert_to_loader(100)
158            .expect_err("should have failed because there is no code");
159
160        // then
161        assert_eq!(
162            err.to_string(),
163            "must provide at least one blob".to_string()
164        );
165    }
166
167    #[test]
168    fn loader_needs_at_least_one_blob() {
169        // given
170        let no_blobs = vec![];
171
172        // when
173        let err = Contract::loader_from_blobs(no_blobs, Salt::default(), vec![])
174            .expect_err("should have failed because there are no blobs");
175
176        // then
177        assert_eq!(
178            err.to_string(),
179            "must provide at least one blob".to_string()
180        );
181    }
182
183    #[test]
184    fn loader_requires_all_except_the_last_blob_to_be_word_sized() {
185        // given
186        let blobs = [vec![0; 9], vec![0; 8]].map(Blob::new).to_vec();
187
188        // when
189        let err = Contract::loader_from_blobs(blobs, Salt::default(), vec![])
190            .expect_err("should have failed because the first blob is not word-sized");
191
192        // then
193        assert_eq!(
194            err.to_string(),
195            "blob 1/2 has a size of 9 bytes, which is not a multiple of 8".to_string()
196        );
197    }
198
199    #[test]
200    fn last_blob_in_loader_can_be_unaligned() {
201        // given
202        let blobs = [vec![0; 8], vec![0; 9]].map(Blob::new).to_vec();
203
204        // when
205        let result = Contract::loader_from_blobs(blobs, Salt::default(), vec![]);
206
207        // then
208        let _ = result.unwrap();
209    }
210
211    #[test]
212    fn can_load_regular_contract() -> Result<()> {
213        // given
214        let tmp_dir = tempfile::tempdir()?;
215        let code_file = tmp_dir.path().join("contract.bin");
216        let code = b"some fake contract code";
217        std::fs::write(&code_file, code)?;
218
219        // when
220        let contract = Contract::load_from(
221            code_file,
222            LoadConfiguration::default()
223                .with_storage_configuration(StorageConfiguration::default().with_autoload(false)),
224        )?;
225
226        // then
227        assert_eq!(contract.code(), code);
228
229        Ok(())
230    }
231
232    #[test]
233    fn can_manually_create_regular_contract() -> Result<()> {
234        // given
235        let binary = b"some fake contract code";
236
237        // when
238        let contract = Contract::regular(binary.to_vec(), Salt::zeroed(), vec![]);
239
240        // then
241        assert_eq!(contract.code(), binary);
242
243        Ok(())
244    }
245
246    macro_rules! getters_work {
247        ($contract: ident, $contract_id: expr, $state_root: expr, $code_root: expr, $salt: expr, $code: expr) => {
248            assert_eq!($contract.contract_id(), $contract_id);
249            assert_eq!($contract.state_root(), $state_root);
250            assert_eq!($contract.code_root(), $code_root);
251            assert_eq!($contract.salt(), $salt);
252            assert_eq!($contract.code(), $code);
253        };
254    }
255
256    #[test]
257    fn regular_contract_has_expected_getters() -> Result<()> {
258        let contract_binary = b"some fake contract code";
259        let storage_slots = vec![StorageSlot::new([2; 32].into(), [1; 32].into())];
260        let contract = Contract::regular(contract_binary.to_vec(), Salt::zeroed(), storage_slots);
261
262        let expected_contract_id =
263            "93c9f1e61efb25458e3c56fdcfee62acb61c0533364eeec7ba61cb2957aa657b".parse()?;
264        let expected_state_root =
265            "852b7b7527124dbcd44302e52453b864dc6f4d9544851c729da666a430b84c97".parse()?;
266        let expected_code_root =
267            "69ca130191e9e469f1580229760b327a0729237f1aff65cf1d076b2dd8360031".parse()?;
268        let expected_salt = Salt::zeroed();
269
270        getters_work!(
271            contract,
272            expected_contract_id,
273            expected_state_root,
274            expected_code_root,
275            expected_salt,
276            contract_binary
277        );
278
279        Ok(())
280    }
281
282    #[test]
283    fn regular_can_be_turned_into_loader_and_back() -> Result<()> {
284        let contract_binary = b"some fake contract code";
285
286        let contract_original = Contract::regular(contract_binary.to_vec(), Salt::zeroed(), vec![]);
287
288        let loader_contract = contract_original.clone().convert_to_loader(1)?;
289
290        let regular_recreated = loader_contract.clone().revert_to_regular();
291
292        assert_eq!(regular_recreated, contract_original);
293
294        Ok(())
295    }
296
297    #[test]
298    fn unuploaded_loader_contract_has_expected_getters() -> Result<()> {
299        let contract_binary = b"some fake contract code";
300
301        let storage_slots = vec![StorageSlot::new([2; 32].into(), [1; 32].into())];
302        let original = Contract::regular(contract_binary.to_vec(), Salt::zeroed(), storage_slots);
303        let loader = original.clone().convert_to_loader(1024)?;
304
305        let loader_asm = loader_contract_asm(&loader.blob_ids()).unwrap();
306        let manual_loader = original.with_code(loader_asm);
307
308        getters_work!(
309            loader,
310            manual_loader.contract_id(),
311            manual_loader.state_root(),
312            manual_loader.code_root(),
313            manual_loader.salt(),
314            manual_loader.code()
315        );
316
317        Ok(())
318    }
319
320    #[test]
321    fn unuploaded_loader_requires_at_least_one_blob() -> Result<()> {
322        // given
323        let no_blob_ids = vec![];
324
325        // when
326        let loader = Contract::loader_from_blob_ids(no_blob_ids, Salt::default(), vec![])
327            .expect_err("should have failed because there are no blobs");
328
329        // then
330        assert_eq!(
331            loader.to_string(),
332            "must provide at least one blob".to_string()
333        );
334        Ok(())
335    }
336
337    #[test]
338    fn uploaded_loader_has_expected_getters() -> Result<()> {
339        let contract_binary = b"some fake contract code";
340        let original_contract = Contract::regular(contract_binary.to_vec(), Salt::zeroed(), vec![]);
341
342        let blob_ids = original_contract
343            .clone()
344            .convert_to_loader(1024)?
345            .blob_ids();
346
347        // we pretend we uploaded the blobs
348        let loader = Contract::loader_from_blob_ids(blob_ids.clone(), Salt::default(), vec![])?;
349
350        let loader_asm = loader_contract_asm(&blob_ids).unwrap();
351        let manual_loader = original_contract.with_code(loader_asm);
352
353        getters_work!(
354            loader,
355            manual_loader.contract_id(),
356            manual_loader.state_root(),
357            manual_loader.code_root(),
358            manual_loader.salt(),
359            manual_loader.code()
360        );
361
362        Ok(())
363    }
364}