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 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 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!(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 let contract = Contract::regular(vec![0x00], Salt::zeroed(), vec![]);
132
133 let err = contract
135 .convert_to_loader(0)
136 .expect_err("should have failed because blob size is 0");
137
138 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 let contract = Contract::regular(vec![], Salt::zeroed(), vec![]);
149
150 let err = contract
152 .convert_to_loader(100)
153 .expect_err("should have failed because there is no code");
154
155 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 let no_blobs = vec![];
166
167 let err = Contract::loader_from_blobs(no_blobs, Salt::default(), vec![])
169 .expect_err("should have failed because there are no blobs");
170
171 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 let blobs = [vec![0; 9], vec![0; 8]].map(Blob::new).to_vec();
182
183 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 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 let blobs = [vec![0; 8], vec![0; 9]].map(Blob::new).to_vec();
198
199 let result = Contract::loader_from_blobs(blobs, Salt::default(), vec![]);
201
202 let _ = result.unwrap();
204 }
205
206 #[test]
207 fn can_load_regular_contract() -> Result<()> {
208 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 let contract = Contract::load_from(
216 code_file,
217 LoadConfiguration::default()
218 .with_storage_configuration(StorageConfiguration::default().with_autoload(false)),
219 )?;
220
221 assert_eq!(contract.code(), code);
223
224 Ok(())
225 }
226
227 #[test]
228 fn can_manually_create_regular_contract() -> Result<()> {
229 let binary = b"some fake contract code";
231
232 let contract = Contract::regular(binary.to_vec(), Salt::zeroed(), vec![]);
234
235 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 let no_blob_ids = vec![];
319
320 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 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 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}