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 crate::assembly::contract_call::loader_contract_asm;
46pub use loader::*;
47
48fn compute_contract_id_and_state_root(
49    binary: &[u8],
50    salt: &Salt,
51    storage_slots: &[StorageSlot],
52) -> (ContractId, Bytes32, Bytes32) {
53    let fuel_contract = FuelContract::from(binary);
54    let code_root = fuel_contract.root();
55    let state_root = FuelContract::initial_state_root(storage_slots.iter());
56
57    let contract_id = fuel_contract.id(salt, &code_root, &state_root);
58
59    (contract_id, code_root, state_root)
60}
61
62#[cfg(test)]
63mod tests {
64    use std::path::Path;
65
66    use fuels_core::types::{
67        errors::{Error, Result},
68        transaction_builders::Blob,
69    };
70    use tempfile::tempdir;
71
72    use crate::assembly::contract_call::loader_contract_asm;
73
74    use super::*;
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!(msg, format!("could not autoload storage slots from file: {storage_slots_path:?}. Either provide the file or disable autoloading in `StorageConfiguration`"));
118    }
119
120    fn save_slots(slots: &Vec<StorageSlot>, path: &Path) {
121        std::fs::write(
122            path,
123            serde_json::to_string::<Vec<StorageSlot>>(slots).unwrap(),
124        )
125        .unwrap()
126    }
127
128    #[test]
129    fn blob_size_must_be_greater_than_zero() {
130        // given
131        let contract = Contract::regular(vec![0x00], Salt::zeroed(), vec![]);
132
133        // when
134        let err = contract
135            .convert_to_loader(0)
136            .expect_err("should have failed because blob size is 0");
137
138        // then
139        assert_eq!(
140            err.to_string(),
141            "blob size must be greater than 0".to_string()
142        );
143    }
144
145    #[test]
146    fn contract_with_no_code_cannot_be_turned_into_a_loader() {
147        // given
148        let contract = Contract::regular(vec![], Salt::zeroed(), vec![]);
149
150        // when
151        let err = contract
152            .convert_to_loader(100)
153            .expect_err("should have failed because there is no code");
154
155        // then
156        assert_eq!(
157            err.to_string(),
158            "must provide at least one blob".to_string()
159        );
160    }
161
162    #[test]
163    fn loader_needs_at_least_one_blob() {
164        // given
165        let no_blobs = vec![];
166
167        // when
168        let err = Contract::loader_from_blobs(no_blobs, Salt::default(), vec![])
169            .expect_err("should have failed because there are no blobs");
170
171        // then
172        assert_eq!(
173            err.to_string(),
174            "must provide at least one blob".to_string()
175        );
176    }
177
178    #[test]
179    fn loader_requires_all_except_the_last_blob_to_be_word_sized() {
180        // given
181        let blobs = [vec![0; 9], vec![0; 8]].map(Blob::new).to_vec();
182
183        // when
184        let err = Contract::loader_from_blobs(blobs, Salt::default(), vec![])
185            .expect_err("should have failed because the first blob is not word-sized");
186
187        // then
188        assert_eq!(
189            err.to_string(),
190            "blob 1/2 has a size of 9 bytes, which is not a multiple of 8".to_string()
191        );
192    }
193
194    #[test]
195    fn last_blob_in_loader_can_be_unaligned() {
196        // given
197        let blobs = [vec![0; 8], vec![0; 9]].map(Blob::new).to_vec();
198
199        // when
200        let result = Contract::loader_from_blobs(blobs, Salt::default(), vec![]);
201
202        // then
203        let _ = result.unwrap();
204    }
205
206    #[test]
207    fn can_load_regular_contract() -> Result<()> {
208        // given
209        let tmp_dir = tempfile::tempdir()?;
210        let code_file = tmp_dir.path().join("contract.bin");
211        let code = b"some fake contract code";
212        std::fs::write(&code_file, code)?;
213
214        // when
215        let contract = Contract::load_from(
216            code_file,
217            LoadConfiguration::default()
218                .with_storage_configuration(StorageConfiguration::default().with_autoload(false)),
219        )?;
220
221        // then
222        assert_eq!(contract.code(), code);
223
224        Ok(())
225    }
226
227    #[test]
228    fn can_manually_create_regular_contract() -> Result<()> {
229        // given
230        let binary = b"some fake contract code";
231
232        // when
233        let contract = Contract::regular(binary.to_vec(), Salt::zeroed(), vec![]);
234
235        // then
236        assert_eq!(contract.code(), binary);
237
238        Ok(())
239    }
240
241    macro_rules! getters_work {
242        ($contract: ident, $contract_id: expr, $state_root: expr, $code_root: expr, $salt: expr, $code: expr) => {
243            assert_eq!($contract.contract_id(), $contract_id);
244            assert_eq!($contract.state_root(), $state_root);
245            assert_eq!($contract.code_root(), $code_root);
246            assert_eq!($contract.salt(), $salt);
247            assert_eq!($contract.code(), $code);
248        };
249    }
250
251    #[test]
252    fn regular_contract_has_expected_getters() -> Result<()> {
253        let contract_binary = b"some fake contract code";
254        let storage_slots = vec![StorageSlot::new([2; 32].into(), [1; 32].into())];
255        let contract = Contract::regular(contract_binary.to_vec(), Salt::zeroed(), storage_slots);
256
257        let expected_contract_id =
258            "93c9f1e61efb25458e3c56fdcfee62acb61c0533364eeec7ba61cb2957aa657b".parse()?;
259        let expected_state_root =
260            "852b7b7527124dbcd44302e52453b864dc6f4d9544851c729da666a430b84c97".parse()?;
261        let expected_code_root =
262            "69ca130191e9e469f1580229760b327a0729237f1aff65cf1d076b2dd8360031".parse()?;
263        let expected_salt = Salt::zeroed();
264
265        getters_work!(
266            contract,
267            expected_contract_id,
268            expected_state_root,
269            expected_code_root,
270            expected_salt,
271            contract_binary
272        );
273
274        Ok(())
275    }
276
277    #[test]
278    fn regular_can_be_turned_into_loader_and_back() -> Result<()> {
279        let contract_binary = b"some fake contract code";
280
281        let contract_original = Contract::regular(contract_binary.to_vec(), Salt::zeroed(), vec![]);
282
283        let loader_contract = contract_original.clone().convert_to_loader(1)?;
284
285        let regular_recreated = loader_contract.clone().revert_to_regular();
286
287        assert_eq!(regular_recreated, contract_original);
288
289        Ok(())
290    }
291
292    #[test]
293    fn unuploaded_loader_contract_has_expected_getters() -> Result<()> {
294        let contract_binary = b"some fake contract code";
295
296        let storage_slots = vec![StorageSlot::new([2; 32].into(), [1; 32].into())];
297        let original = Contract::regular(contract_binary.to_vec(), Salt::zeroed(), storage_slots);
298        let loader = original.clone().convert_to_loader(1024)?;
299
300        let loader_asm = loader_contract_asm(&loader.blob_ids()).unwrap();
301        let manual_loader = original.with_code(loader_asm);
302
303        getters_work!(
304            loader,
305            manual_loader.contract_id(),
306            manual_loader.state_root(),
307            manual_loader.code_root(),
308            manual_loader.salt(),
309            manual_loader.code()
310        );
311
312        Ok(())
313    }
314
315    #[test]
316    fn unuploaded_loader_requires_at_least_one_blob() -> Result<()> {
317        // given
318        let no_blob_ids = vec![];
319
320        // when
321        let loader = Contract::loader_from_blob_ids(no_blob_ids, Salt::default(), vec![])
322            .expect_err("should have failed because there are no blobs");
323
324        // then
325        assert_eq!(
326            loader.to_string(),
327            "must provide at least one blob".to_string()
328        );
329        Ok(())
330    }
331
332    #[test]
333    fn uploaded_loader_has_expected_getters() -> Result<()> {
334        let contract_binary = b"some fake contract code";
335        let original_contract = Contract::regular(contract_binary.to_vec(), Salt::zeroed(), vec![]);
336
337        let blob_ids = original_contract
338            .clone()
339            .convert_to_loader(1024)?
340            .blob_ids();
341
342        // we pretend we uploaded the blobs
343        let loader = Contract::loader_from_blob_ids(blob_ids.clone(), Salt::default(), vec![])?;
344
345        let loader_asm = loader_contract_asm(&blob_ids).unwrap();
346        let manual_loader = original_contract.with_code(loader_asm);
347
348        getters_work!(
349            loader,
350            manual_loader.contract_id(),
351            manual_loader.state_root(),
352            manual_loader.code_root(),
353            manual_loader.salt(),
354            manual_loader.code()
355        );
356
357        Ok(())
358    }
359}