1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
//! Utilities for mocking project workspaces
use crate::{
    artifacts::Settings,
    config::ProjectPathsConfigBuilder,
    error::{bail, Result, SolcError},
    hh::HardhatArtifacts,
    project_util::mock::{MockProjectGenerator, MockProjectSettings},
    remappings::Remapping,
    utils,
    utils::tempdir,
    Artifact, ArtifactOutput, Artifacts, ConfigurableArtifacts, ConfigurableContractArtifact,
    FileFilter, PathStyle, Project, ProjectCompileOutput, ProjectPathsConfig, SolFilesCache,
    SolcIoError,
};
use fs_extra::{dir, file};
use std::{
    fmt,
    path::{Path, PathBuf},
    process,
    process::Command,
};
use tempfile::TempDir;

pub mod mock;

/// A [`Project`] wrapper that lives in a new temporary directory
///
/// Once `TempProject` is dropped, the temp dir is automatically removed, see [`TempDir::drop()`]
pub struct TempProject<T: ArtifactOutput = ConfigurableArtifacts> {
    /// temporary workspace root
    _root: TempDir,
    /// actual project workspace with the `root` tempdir as its root
    inner: Project<T>,
}

impl<T: ArtifactOutput> TempProject<T> {
    /// Makes sure all resources are created
    pub fn create_new(root: TempDir, inner: Project<T>) -> std::result::Result<Self, SolcIoError> {
        let mut project = Self { _root: root, inner };
        project.paths().create_all()?;
        // ignore license warnings
        project.inner.ignored_error_codes.push(1878);
        Ok(project)
    }

    /// Creates a new temp project using the provided paths and artifacts handler.
    /// sets the project root to a temp dir
    pub fn with_artifacts(paths: ProjectPathsConfigBuilder, artifacts: T) -> Result<Self> {
        Self::prefixed_with_artifacts("temp-project", paths, artifacts)
    }

    /// Creates a new temp project inside a tempdir with a prefixed directory and the given
    /// artifacts handler
    pub fn prefixed_with_artifacts(
        prefix: &str,
        paths: ProjectPathsConfigBuilder,
        artifacts: T,
    ) -> Result<Self> {
        let tmp_dir = tempdir(prefix)?;
        let paths = paths.build_with_root(tmp_dir.path());
        let inner = Project::builder().artifacts(artifacts).paths(paths).build()?;
        Ok(Self::create_new(tmp_dir, inner)?)
    }

    /// Overwrites the settings to pass to `solc`
    pub fn with_settings(mut self, settings: impl Into<Settings>) -> Self {
        self.inner.solc_config.settings = settings.into();
        self
    }

    /// Explicitly sets the solc version for the project
    #[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
    pub fn set_solc(&mut self, solc: impl AsRef<str>) -> &mut Self {
        self.inner.solc = crate::Solc::find_or_install_svm_version(solc).unwrap();
        self.inner.auto_detect = false;
        self
    }

    pub fn project(&self) -> &Project<T> {
        &self.inner
    }

    pub fn compile(&self) -> Result<ProjectCompileOutput<T>> {
        self.project().compile()
    }

    pub fn compile_sparse<F: FileFilter + 'static>(
        &self,
        filter: F,
    ) -> Result<ProjectCompileOutput<T>> {
        self.project().compile_sparse(filter)
    }

    pub fn flatten(&self, target: &Path) -> Result<String> {
        self.project().flatten(target)
    }

    pub fn project_mut(&mut self) -> &mut Project<T> {
        &mut self.inner
    }

    /// The configured paths of the project
    pub fn paths(&self) -> &ProjectPathsConfig {
        &self.project().paths
    }

    /// The configured paths of the project
    pub fn paths_mut(&mut self) -> &mut ProjectPathsConfig {
        &mut self.project_mut().paths
    }

    /// Returns the path to the artifacts directory
    pub fn artifacts_path(&self) -> &PathBuf {
        &self.paths().artifacts
    }

    /// Returns the path to the sources directory
    pub fn sources_path(&self) -> &PathBuf {
        &self.paths().sources
    }

    /// Returns the path to the cache file
    pub fn cache_path(&self) -> &PathBuf {
        &self.paths().cache
    }

    /// The root path of the temporary workspace
    pub fn root(&self) -> &Path {
        self.project().paths.root.as_path()
    }

    /// Copies a single file into the projects source
    pub fn copy_source(&self, source: impl AsRef<Path>) -> Result<()> {
        copy_file(source, &self.paths().sources)
    }

    pub fn copy_sources<I, S>(&self, sources: I) -> Result<()>
    where
        I: IntoIterator<Item = S>,
        S: AsRef<Path>,
    {
        for path in sources {
            self.copy_source(path)?;
        }
        Ok(())
    }

    fn get_lib(&self) -> Result<PathBuf> {
        self.paths()
            .libraries
            .first()
            .cloned()
            .ok_or_else(|| SolcError::msg("No libraries folders configured"))
    }

    /// Copies a single file into the project's main library directory
    pub fn copy_lib(&self, lib: impl AsRef<Path>) -> Result<()> {
        let lib_dir = self.get_lib()?;
        copy_file(lib, lib_dir)
    }

    /// Copy a series of files into the main library dir
    pub fn copy_libs<I, S>(&self, libs: I) -> Result<()>
    where
        I: IntoIterator<Item = S>,
        S: AsRef<Path>,
    {
        for path in libs {
            self.copy_lib(path)?;
        }
        Ok(())
    }

    /// Adds a new library file
    pub fn add_lib(&self, name: impl AsRef<str>, content: impl AsRef<str>) -> Result<PathBuf> {
        let name = contract_file_name(name);
        let lib_dir = self.get_lib()?;
        let lib = lib_dir.join(name);
        create_contract_file(lib, content)
    }

    /// Adds a basic lib contract `contract <name> {}` as a new file
    pub fn add_basic_lib(
        &self,
        name: impl AsRef<str>,
        version: impl AsRef<str>,
    ) -> Result<PathBuf> {
        let name = name.as_ref();
        let name = name.strip_suffix(".sol").unwrap_or(name);
        self.add_lib(
            name,
            format!(
                r#"
// SPDX-License-Identifier: UNLICENSED
pragma solidity {};
contract {} {{}}
            "#,
                version.as_ref(),
                name,
            ),
        )
    }

    /// Adds a new test file inside the project's test dir
    pub fn add_test(&self, name: impl AsRef<str>, content: impl AsRef<str>) -> Result<PathBuf> {
        let name = contract_file_name(name);
        let tests = self.paths().tests.join(name);
        create_contract_file(tests, content)
    }

    /// Adds a new script file inside the project's script dir
    pub fn add_script(&self, name: impl AsRef<str>, content: impl AsRef<str>) -> Result<PathBuf> {
        let name = contract_file_name(name);
        let script = self.paths().scripts.join(name);
        create_contract_file(script, content)
    }

    /// Adds a new source file inside the project's source dir
    pub fn add_source(&self, name: impl AsRef<str>, content: impl AsRef<str>) -> Result<PathBuf> {
        let name = contract_file_name(name);
        let source = self.paths().sources.join(name);
        create_contract_file(source, content)
    }

    /// Adds a basic source contract `contract <name> {}` as a new file
    pub fn add_basic_source(
        &self,
        name: impl AsRef<str>,
        version: impl AsRef<str>,
    ) -> Result<PathBuf> {
        let name = name.as_ref();
        let name = name.strip_suffix(".sol").unwrap_or(name);
        self.add_source(
            name,
            format!(
                r#"
// SPDX-License-Identifier: UNLICENSED
pragma solidity {};
contract {} {{}}
            "#,
                version.as_ref(),
                name,
            ),
        )
    }

    /// Adds a solidity contract in the project's root dir.
    /// This will also create all intermediary dirs.
    pub fn add_contract(&self, name: impl AsRef<str>, content: impl AsRef<str>) -> Result<PathBuf> {
        let name = contract_file_name(name);
        let source = self.root().join(name);
        create_contract_file(source, content)
    }

    /// Returns a snapshot of all cached artifacts
    pub fn artifacts_snapshot(&self) -> Result<ArtifactsSnapshot<T::Artifact>> {
        let cache = self.project().read_cache_file()?;
        let artifacts = cache.read_artifacts::<T::Artifact>()?;
        Ok(ArtifactsSnapshot { cache, artifacts })
    }

    /// Populate the project with mock files
    pub fn mock(&self, gen: &MockProjectGenerator, version: impl AsRef<str>) -> Result<()> {
        gen.write_to(self.paths(), version)
    }

    /// Compiles the project and ensures that the output does not contain errors
    pub fn ensure_no_errors(&self) -> Result<&Self> {
        let compiled = self.compile().unwrap();
        if compiled.has_compiler_errors() {
            bail!("Compiled with errors {}", compiled)
        }
        Ok(self)
    }

    /// Compiles the project and ensures that the output is __unchanged__
    pub fn ensure_unchanged(&self) -> Result<&Self> {
        let compiled = self.compile().unwrap();
        if !compiled.is_unchanged() {
            bail!("Compiled with detected changes {}", compiled)
        }
        Ok(self)
    }

    /// Compiles the project and ensures that the output has __changed__
    pub fn ensure_changed(&self) -> Result<&Self> {
        let compiled = self.compile().unwrap();
        if compiled.is_unchanged() {
            bail!("Compiled without detecting changes {}", compiled)
        }
        Ok(self)
    }

    /// Compiles the project and ensures that the output does not contain errors and no changes
    /// exists on recompiled.
    ///
    /// This is a convenience function for
    ///
    /// ```no_run
    /// use ethers_solc::project_util::TempProject;
    /// let project = TempProject::dapptools().unwrap();
    //  project.ensure_no_errors().unwrap();
    //  project.ensure_unchanged().unwrap();
    /// ```
    pub fn ensure_no_errors_recompile_unchanged(&self) -> Result<&Self> {
        self.ensure_no_errors()?.ensure_unchanged()
    }

    /// Compiles the project and asserts that the output does not contain errors and no changes
    /// exists on recompiled.
    ///
    /// This is a convenience function for
    ///
    /// ```no_run
    /// use ethers_solc::project_util::TempProject;
    /// let project = TempProject::dapptools().unwrap();
    //  project.assert_no_errors();
    //  project.assert_unchanged();
    /// ```
    pub fn assert_no_errors_recompile_unchanged(&self) -> &Self {
        self.assert_no_errors().assert_unchanged()
    }

    /// Compiles the project and asserts that the output does not contain errors
    pub fn assert_no_errors(&self) -> &Self {
        let compiled = self.compile().unwrap();
        compiled.assert_success();
        self
    }

    /// Compiles the project and asserts that the output is unchanged
    pub fn assert_unchanged(&self) -> &Self {
        let compiled = self.compile().unwrap();
        assert!(compiled.is_unchanged());
        self
    }

    /// Compiles the project and asserts that the output is _changed_
    pub fn assert_changed(&self) -> &Self {
        let compiled = self.compile().unwrap();
        assert!(!compiled.is_unchanged());
        self
    }

    /// Returns a list of all source files in the project's `src` directory
    pub fn list_source_files(&self) -> Vec<PathBuf> {
        utils::source_files(self.project().sources_path())
    }
}

impl<T: ArtifactOutput + Default> TempProject<T> {
    /// Creates a new temp project inside a tempdir with a prefixed directory
    pub fn prefixed(prefix: &str, paths: ProjectPathsConfigBuilder) -> Result<Self> {
        Self::prefixed_with_artifacts(prefix, paths, T::default())
    }

    /// Creates a new temp project for the given `PathStyle`
    pub fn with_style(prefix: &str, style: PathStyle) -> Result<Self> {
        let tmp_dir = tempdir(prefix)?;
        let paths = style.paths(tmp_dir.path())?;
        let inner = Project::builder().artifacts(T::default()).paths(paths).build()?;
        Ok(Self::create_new(tmp_dir, inner)?)
    }

    /// Creates a new temp project using the provided paths and setting the project root to a temp
    /// dir
    pub fn new(paths: ProjectPathsConfigBuilder) -> Result<Self> {
        Self::prefixed("temp-project", paths)
    }
}

impl<T: ArtifactOutput> fmt::Debug for TempProject<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("TempProject").field("paths", self.paths()).finish()
    }
}

pub(crate) fn create_contract_file(path: PathBuf, content: impl AsRef<str>) -> Result<PathBuf> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .map_err(|err| SolcIoError::new(err, parent.to_path_buf()))?;
    }
    std::fs::write(&path, content.as_ref()).map_err(|err| SolcIoError::new(err, path.clone()))?;
    Ok(path)
}

fn contract_file_name(name: impl AsRef<str>) -> String {
    let name = name.as_ref().trim();
    if name.ends_with(".sol") {
        name.to_string()
    } else {
        format!("{name}.sol")
    }
}

impl TempProject<HardhatArtifacts> {
    /// Creates an empty new hardhat style workspace in a new temporary dir
    pub fn hardhat() -> Result<Self> {
        let tmp_dir = tempdir("tmp_hh")?;

        let paths = ProjectPathsConfig::hardhat(tmp_dir.path())?;

        let inner =
            Project::builder().artifacts(HardhatArtifacts::default()).paths(paths).build()?;
        Ok(Self::create_new(tmp_dir, inner)?)
    }
}

impl TempProject<ConfigurableArtifacts> {
    /// Creates an empty new dapptools style workspace in a new temporary dir
    pub fn dapptools() -> Result<Self> {
        let tmp_dir = tempdir("tmp_dapp")?;
        let paths = ProjectPathsConfig::dapptools(tmp_dir.path())?;

        let inner = Project::builder().paths(paths).build()?;
        Ok(Self::create_new(tmp_dir, inner)?)
    }

    /// Creates an initialized dapptools style workspace in a new temporary dir
    pub fn dapptools_init() -> Result<Self> {
        let mut project = Self::dapptools()?;
        let orig_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/dapp-sample");
        copy_dir(orig_root, project.root())?;
        project.project_mut().paths.remappings = Remapping::find_many(project.root());

        Ok(project)
    }

    /// Clones the given repo into a temp dir, initializes it recursively and configures it.
    ///
    /// # Example
    ///
    /// ```
    /// use ethers_solc::project_util::TempProject;
    /// # fn t() {
    /// let project = TempProject::checkout("transmissions11/solmate").unwrap();
    /// # }
    /// ```
    pub fn checkout(repo: impl AsRef<str>) -> Result<Self> {
        let tmp_dir = tempdir("tmp_checkout")?;
        clone_remote(&format!("https://github.com/{}", repo.as_ref()), tmp_dir.path())
            .map_err(|err| SolcIoError::new(err, tmp_dir.path()))?;
        let paths = ProjectPathsConfig::dapptools(tmp_dir.path())?;

        let inner = Project::builder().paths(paths).build()?;
        Ok(Self::create_new(tmp_dir, inner)?)
    }

    /// Create a new temporary project and populate it with mock files
    ///
    /// ```no_run
    /// use ethers_solc::project_util::mock::MockProjectSettings;
    /// use ethers_solc::project_util::TempProject;
    /// let tmp = TempProject::mocked(&MockProjectSettings::default(), "^0.8.10").unwrap();
    /// ```
    pub fn mocked(settings: &MockProjectSettings, version: impl AsRef<str>) -> Result<Self> {
        let mut tmp = Self::dapptools()?;
        let gen = MockProjectGenerator::new(settings);
        tmp.mock(&gen, version)?;
        let remappings = gen.remappings_at(tmp.root());
        tmp.paths_mut().remappings.extend(remappings);
        Ok(tmp)
    }

    /// Create a new temporary project and populate it with a random layout
    ///
    /// ```no_run
    /// use ethers_solc::project_util::TempProject;
    /// let tmp = TempProject::mocked_random("^0.8.10").unwrap();
    /// ```
    ///
    /// This is a convenience function for:
    ///
    /// ```no_run
    /// use ethers_solc::project_util::mock::MockProjectSettings;
    /// use ethers_solc::project_util::TempProject;
    /// let tmp = TempProject::mocked(&MockProjectSettings::random(), "^0.8.10").unwrap();
    /// ```
    pub fn mocked_random(version: impl AsRef<str>) -> Result<Self> {
        Self::mocked(&MockProjectSettings::random(), version)
    }
}

impl<T: ArtifactOutput> AsRef<Project<T>> for TempProject<T> {
    fn as_ref(&self) -> &Project<T> {
        self.project()
    }
}

/// The cache file and all the artifacts it references
#[derive(Debug, Clone)]
pub struct ArtifactsSnapshot<T> {
    pub cache: SolFilesCache,
    pub artifacts: Artifacts<T>,
}

impl ArtifactsSnapshot<ConfigurableContractArtifact> {
    /// Ensures that all artifacts have abi, bytecode, deployedbytecode
    pub fn assert_artifacts_essentials_present(&self) {
        for artifact in self.artifacts.artifact_files() {
            let c = artifact.artifact.clone().into_compact_contract();
            assert!(c.abi.is_some());
            assert!(c.bin.is_some());
            assert!(c.bin_runtime.is_some());
        }
    }
}

/// commonly used options for copying entire folders
fn dir_copy_options() -> dir::CopyOptions {
    dir::CopyOptions {
        overwrite: true,
        skip_exist: false,
        buffer_size: 64000, //64kb
        copy_inside: true,
        content_only: true,
        depth: 0,
    }
}

/// commonly used options for copying files
fn file_copy_options() -> file::CopyOptions {
    file::CopyOptions {
        overwrite: true,
        skip_exist: false,
        buffer_size: 64000, //64kb
    }
}

/// Copies a single file into the given dir
pub fn copy_file(source: impl AsRef<Path>, target_dir: impl AsRef<Path>) -> Result<()> {
    let source = source.as_ref();
    let target = target_dir.as_ref().join(
        source
            .file_name()
            .ok_or_else(|| SolcError::msg(format!("No file name for {}", source.display())))?,
    );

    fs_extra::file::copy(source, target, &file_copy_options())?;
    Ok(())
}

/// Copies all content of the source dir into the target dir
pub fn copy_dir(source: impl AsRef<Path>, target_dir: impl AsRef<Path>) -> Result<()> {
    fs_extra::dir::copy(source, target_dir, &dir_copy_options())?;
    Ok(())
}

/// Clones a remote repository into the specified directory.
pub fn clone_remote(
    repo_url: &str,
    target_dir: impl AsRef<Path>,
) -> std::io::Result<process::Output> {
    Command::new("git")
        .args(["clone", "--depth", "1", "--recursive", repo_url])
        .arg(target_dir.as_ref())
        .output()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn can_mock_project() {
        let _prj = TempProject::mocked(&Default::default(), "^0.8.11").unwrap();
        let _prj = TempProject::mocked_random("^0.8.11").unwrap();
    }
}