1mod storage;
2
3use std::fmt::Debug;
4
5use fuel_tx::{Bytes32, Contract as FuelContract, ContractId, Salt, StorageSlot};
6pub use storage::*;
7
8#[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;
44pub 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 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 let loaded_contract = Contract::load_from(&contract_bin, load_config).unwrap();
93
94 assert_eq!(loaded_contract.storage_slots, expected_storage_slots);
96 }
97
98 #[test]
99 fn autoload_fails_if_file_missing() {
100 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 let error = Contract::load_from(&contract_bin, load_config)
110 .expect_err("should have failed because the storage slots file is missing");
111
112 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 let contract = Contract::regular(vec![0x00], Salt::zeroed(), vec![]);
137
138 let err = contract
140 .convert_to_loader(0)
141 .expect_err("should have failed because blob size is 0");
142
143 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 let contract = Contract::regular(vec![], Salt::zeroed(), vec![]);
154
155 let err = contract
157 .convert_to_loader(100)
158 .expect_err("should have failed because there is no code");
159
160 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 let no_blobs = vec![];
171
172 let err = Contract::loader_from_blobs(no_blobs, Salt::default(), vec![])
174 .expect_err("should have failed because there are no blobs");
175
176 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 let blobs = [vec![0; 9], vec![0; 8]].map(Blob::new).to_vec();
187
188 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 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 let blobs = [vec![0; 8], vec![0; 9]].map(Blob::new).to_vec();
203
204 let result = Contract::loader_from_blobs(blobs, Salt::default(), vec![]);
206
207 let _ = result.unwrap();
209 }
210
211 #[test]
212 fn can_load_regular_contract() -> Result<()> {
213 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 let contract = Contract::load_from(
221 code_file,
222 LoadConfiguration::default()
223 .with_storage_configuration(StorageConfiguration::default().with_autoload(false)),
224 )?;
225
226 assert_eq!(contract.code(), code);
228
229 Ok(())
230 }
231
232 #[test]
233 fn can_manually_create_regular_contract() -> Result<()> {
234 let binary = b"some fake contract code";
236
237 let contract = Contract::regular(binary.to_vec(), Salt::zeroed(), vec![]);
239
240 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 let no_blob_ids = vec![];
324
325 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 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 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}