├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── feature.md │ └── work-item.md ├── PULL_REQUEST_TEMPLATE.md ├── release-drafter.yml └── workflows │ ├── codeql.yml │ ├── draft.yaml │ ├── prtitle.yaml │ ├── publish.yaml │ ├── stale-prs.yml │ └── test.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ape_solidity ├── __init__.py ├── _cli.py ├── _models.py ├── _utils.py ├── compiler.py ├── exceptions.py └── py.typed ├── pyproject.toml ├── setup.cfg ├── setup.py └── tests ├── BrownieProject ├── brownie-config.yaml └── contracts │ └── BrownieContract.sol ├── Dependency ├── ape-config.yaml └── contracts │ ├── Dependency.sol │ ├── OlderDependency.sol │ ├── subfolder │ └── InDependencySubfolder.sol │ └── subfolder_with_imports │ └── InDependencySubfolderWithImports.sol ├── DependencyOfDependency ├── ape-config.yaml └── contracts │ └── DependencyOfDependency.sol ├── NonCompilingDependency ├── ape-config.yaml └── contracts │ ├── CompilingContract.sol │ ├── NonCompilingContract.sol │ └── subdir │ └── SubCompilingContract.sol ├── ProjectWithinProject ├── ape-config.yaml └── contracts │ └── Contract.sol ├── VersionSpecifiedInConfig ├── ape-config.yaml └── contracts │ └── VersionSpecifiedInConfig.sol ├── __init__.py ├── ape-config.yaml ├── conftest.py ├── contracts ├── BuiltinErrorChecker.sol ├── CircularImport1.sol ├── CircularImport2.sol ├── CompilesOnce.sol ├── ContractUsingLibraryNotInSameSource.sol ├── DifferentNameThanFile.sol ├── DirectoryWithExtension.sol │ └── README.md ├── ExperimentalABIEncoderV2.sol ├── HasError.sol ├── ImportOlderDependency.sol ├── ImportSourceWithEqualSignVersion.sol ├── ImportSourceWithNoPrefixVersion.sol ├── ImportingLessConstrainedVersion.sol ├── Imports.sol ├── IndirectlyImportingMoreConstrainedVersion.sol ├── IndirectlyImportingMoreConstrainedVersionCompanion.sol ├── IndirectlyImportingMoreConstrainedVersionCompanionImport.sol ├── JustAStruct.sol ├── LibraryFun.sol ├── MissingPragma.sol ├── MultipleDefinitions.sol ├── NumerousDefinitions.sol ├── OlderVersion.sol ├── RandomVyperFile.vy ├── RangedVersion.sol ├── Source.extra.ext.sol ├── SpacesInPragma.sol ├── SpecificVersionNoPrefix.sol ├── SpecificVersionNoPrefix2.sol ├── SpecificVersionRange.sol ├── SpecificVersionWithEqualSign.sol ├── UseYearn.sol ├── VagueVersion.sol ├── safe │ ├── README.md │ └── ThisIsNotGnosisSafe.sol └── subfolder │ ├── Relativecontract.sol │ └── UsingDependencyWithinSubFolder.sol ├── data ├── ImportingLessConstrainedVersionFlat.sol ├── ImportsFlattened.sol └── packages │ ├── OpenZeppelin │ ├── v4.5.0 │ │ └── OpenZeppelin.json │ └── v4.7.1 │ │ └── OpenZeppelin.json │ └── vault │ ├── master │ └── vault_main.json │ └── v0.4.5 │ └── vault.json ├── package-lock.json ├── package.json ├── scripts └── clean.py ├── test_cli.py ├── test_compiler.py ├── test_exceptions.py └── test_integration.py /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report an error that you've encountered. 4 | labels: bug 5 | --- 6 | 7 | ### Environment information 8 | 9 | - `ape` and plugin versions: 10 | 11 | ``` 12 | $ ape --version 13 | # ...copy and paste result of above command here... 14 | 15 | $ ape plugins list 16 | # ...copy and paste result of above command here... 17 | ``` 18 | 19 | - Python Version: x.x.x 20 | - OS: macOS/linux/win 21 | 22 | ### What went wrong? 23 | 24 | Please include information like: 25 | 26 | - what command you ran 27 | - the code that caused the failure (see [this link](https://help.github.com/articles/basic-writing-and-formatting-syntax/) for help with formatting code) 28 | - full output of the error you received 29 | 30 | ### How can it be fixed? 31 | 32 | Fill this in if you have ideas on how the bug could be fixed. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request a new feature, or an improvement to existing functionality. 4 | labels: enhancement 5 | --- 6 | 7 | ### Overview 8 | 9 | Provide a simple overview of what you wish to see added. Please include: 10 | 11 | - What you are trying to do 12 | - Why Ape's current functionality is inadequate to address your goal 13 | 14 | ### Specification 15 | 16 | Describe the syntax and semantics of how you would like to see this feature implemented. The more detailed the better! 17 | 18 | Remember, your feature is much more likely to be included if it does not involve any breaking changes. 19 | 20 | ### Dependencies 21 | 22 | Include links to any open issues that must be resolved before this feature can be implemented. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/work-item.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Work item 3 | about: New work item for Ape team 4 | labels: backlog 5 | --- 6 | 7 | ### Elevator pitch: 8 | 9 | 10 | 11 | ### Value: 12 | 13 | 18 | 19 | ### Dependencies: 20 | 21 | 22 | 23 | ### Design approach: 24 | 25 | 29 | 30 | ### Task list: 31 | 32 | 33 | 34 | - [ ] Tasks go here 35 | 36 | ### Estimated completion date: 37 | 38 | ### Design review: 39 | 40 | 41 | 42 | Do not signoff unless: 43 | 44 | - 1. agreed the tasks and design approach will achieve acceptance, and 45 | - 2. the work can be completed by one person within the SLA. 46 | Design reviewers should consider simpler approaches to achieve goals. 47 | 48 | (Please leave a comment to sign off) 49 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What I did 2 | 3 | 4 | 5 | fixes: # 6 | 7 | ### How I did it 8 | 9 | ### How to verify it 10 | 11 | ### Checklist 12 | 13 | - [ ] Passes all linting checks (pre-commit and CI jobs) 14 | - [ ] New test cases have been added and are passing 15 | - [ ] Documentation has been updated 16 | - [ ] PR title follows [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) standard (will be automatically included in the changelog) 17 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: '$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | 4 | categories: 5 | - title: 'Features' 6 | labels: 7 | - 'feat' 8 | - title: 'Bug Fixes' 9 | labels: 10 | - 'fix' 11 | - title: 'Other updates' 12 | labels: 13 | - 'refactor' 14 | - 'chore' 15 | - 'docs' 16 | 17 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 18 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 19 | 20 | version-resolver: 21 | major: 22 | labels: 23 | - 'major' 24 | minor: 25 | labels: 26 | - 'minor' 27 | patch: 28 | labels: 29 | - 'patch' 30 | default: patch 31 | 32 | template: | 33 | ## Changes 34 | 35 | $CHANGES 36 | 37 | Special thanks to: $CONTRIBUTORS 38 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | permissions: 14 | actions: read 15 | contents: read 16 | security-events: write 17 | 18 | strategy: 19 | fail-fast: false 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Initialize CodeQL 26 | uses: github/codeql-action/init@v2 27 | with: 28 | languages: python 29 | 30 | - name: Autobuild 31 | uses: github/codeql-action/autobuild@v2 32 | 33 | - name: Perform CodeQL Analysis 34 | uses: github/codeql-action/analyze@v2 35 | -------------------------------------------------------------------------------- /.github/workflows/draft.yaml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update-draft: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | # Drafts your next Release notes as Pull Requests are merged into "master" 15 | - uses: release-drafter/release-drafter@v5 16 | with: 17 | disable-autolabeler: true 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/prtitle.yaml: -------------------------------------------------------------------------------- 1 | name: PR Title 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | check: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Setup Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: "3.10" 21 | 22 | - name: Install Dependencies 23 | run: pip install commitizen 24 | 25 | - name: Check PR Title 26 | env: 27 | TITLE: ${{ github.event.pull_request.title }} 28 | run: cz check --message "$TITLE" 29 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | deploy: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: "3.10" 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install -e .[release] 24 | 25 | - name: Build 26 | run: python setup.py sdist bdist_wheel 27 | 28 | - name: Publish 29 | env: 30 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 31 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 32 | run: twine upload dist/* --verbose 33 | -------------------------------------------------------------------------------- /.github/workflows/stale-prs.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale PRs" 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | pull-requests: write 11 | steps: 12 | - uses: actions/stale@v6 13 | with: 14 | stale-pr-message: 'This pull request is considered stale because it has been open 30 days with no activity. Remove stale label, add a comment, or make a new commit, otherwise this PR will be closed in 5 days.' 15 | close-pr-message: 'This PR was closed because it has been inactive for 35 days.' 16 | stale-pr-label: "stale" 17 | close-pr-label: "inactive" 18 | days-before-pr-stale: 30 19 | days-before-pr-close: 5 20 | repo-token: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: ["push", "pull_request"] 2 | 3 | name: Test 4 | 5 | concurrency: 6 | # Cancel older, in-progress jobs from the same PR, same workflow. 7 | # use run_id if the job is triggered by a push to ensure 8 | # push-triggered jobs to not get canceled. 9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | linting: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Setup Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: "3.10" 23 | 24 | - name: Install Dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install .[lint] 28 | 29 | - name: Run Black 30 | run: black --check . 31 | 32 | - name: Run isort 33 | run: isort --check-only . 34 | 35 | - name: Run flake8 36 | run: flake8 . 37 | 38 | - name: Run mdformat 39 | run: mdformat . --check 40 | 41 | type-check: 42 | runs-on: ubuntu-latest 43 | 44 | steps: 45 | - uses: actions/checkout@v4 46 | 47 | - name: Setup Python 48 | uses: actions/setup-python@v5 49 | with: 50 | python-version: "3.10" 51 | 52 | - name: Install Dependencies 53 | run: | 54 | python -m pip install --upgrade pip 55 | pip install .[lint,test] 56 | 57 | - name: Run MyPy 58 | run: mypy . 59 | 60 | functional: 61 | runs-on: ${{ matrix.os }} 62 | 63 | strategy: 64 | matrix: 65 | os: [ubuntu-latest, macos-latest] # eventually add `windows-latest` 66 | python-version: [3.9, '3.10', '3.11', '3.12', '3.13'] 67 | 68 | env: 69 | GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | 71 | steps: 72 | - uses: actions/checkout@v4 73 | 74 | - name: Setup Python 75 | uses: actions/setup-python@v5 76 | with: 77 | python-version: ${{ matrix.python-version }} 78 | 79 | - name: Install Dependencies 80 | run: | 81 | python -m pip install --upgrade pip 82 | pip uninstall eth-ape --yes 83 | pushd tests 84 | npm install 85 | popd 86 | pip install .[test] 87 | 88 | - name: Run Tests 89 | run: pytest -m "not fuzzing" 90 | 91 | # fuzzing: 92 | # runs-on: ubuntu-latest 93 | 94 | # strategy: 95 | # fail-fast: true 96 | 97 | # steps: 98 | # - uses: actions/checkout@v4 99 | 100 | # - name: Setup Python 101 | # uses: actions/setup-python@v5 102 | # with: 103 | # python-version: "3.10" 104 | 105 | # - name: Install Dependencies 106 | # run: pip install .[test] 107 | 108 | # - name: Run Tests 109 | # run: pytest -m "fuzzing" --no-cov -s 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tests/BrownieProject/ape-config.yaml 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # Environments 103 | .env 104 | .venv 105 | env/ 106 | venv/ 107 | ENV/ 108 | env.bak/ 109 | venv.bak/ 110 | 111 | # mkdocs documentation 112 | /site 113 | 114 | # mypy 115 | .mypy_cache/ 116 | .dmypy.json 117 | dmypy.json 118 | 119 | # setuptools-scm 120 | version.py 121 | 122 | # Ape stuff 123 | .build/ 124 | 125 | **/.DS_Store 126 | *.swp 127 | *.swo 128 | 129 | tests/node_modules 130 | .benchmarks/ 131 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-yaml 6 | 7 | - repo: https://github.com/PyCQA/isort 8 | rev: 6.0.0 9 | hooks: 10 | - id: isort 11 | 12 | - repo: https://github.com/psf/black 13 | rev: 25.1.0 14 | hooks: 15 | - id: black 16 | name: black 17 | 18 | - repo: https://github.com/pycqa/flake8 19 | rev: 7.1.2 20 | hooks: 21 | - id: flake8 22 | additional_dependencies: [flake8-breakpoint, flake8-print, flake8-pydantic, flake8-type-checking] 23 | 24 | - repo: https://github.com/pre-commit/mirrors-mypy 25 | rev: v1.15.0 26 | hooks: 27 | - id: mypy 28 | additional_dependencies: [types-requests, types-setuptools] 29 | 30 | - repo: https://github.com/executablebooks/mdformat 31 | rev: 0.7.22 32 | hooks: 33 | - id: mdformat 34 | additional_dependencies: [mdformat-gfm, mdformat-frontmatter, mdformat-pyproject] 35 | 36 | 37 | default_language_version: 38 | python: python3 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | To get started with working on the codebase, use the following steps prepare your local environment: 4 | 5 | ```bash 6 | # clone the github repo and navigate into the folder 7 | git clone https://github.com/ApeWorX/ape-solidity.git 8 | cd ape-solidity 9 | 10 | # create and load a virtual environment 11 | python3 -m venv venv 12 | source venv/bin/activate 13 | 14 | # install ape-solidity into the virtual environment 15 | python setup.py install 16 | 17 | # install the developer dependencies (-e is interactive mode) 18 | pip install -e .'[dev]' 19 | ``` 20 | 21 | ## Pre-Commit Hooks 22 | 23 | We use [`pre-commit`](https://pre-commit.com/) hooks to simplify linting and ensure consistent formatting among contributors. 24 | Use of `pre-commit` is not a requirement, but is highly recommended. 25 | 26 | Install `pre-commit` locally from the root folder: 27 | 28 | ```bash 29 | pip install pre-commit 30 | pre-commit install 31 | ``` 32 | 33 | Committing will now automatically run the local hooks and ensure that your commit passes all lint checks. 34 | 35 | ## Pull Requests 36 | 37 | Pull requests are welcomed! Please adhere to the following: 38 | 39 | - Ensure your pull request passes our linting checks 40 | - Include test cases for any new functionality 41 | - Include any relevant documentation updates 42 | 43 | It's a good idea to make pull requests early on. 44 | A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission. 45 | 46 | If you are opening a work-in-progress pull request to verify that it passes CI tests, please consider 47 | [marking it as a draft](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests). 48 | 49 | Join the ApeWorX [Discord](https://discord.gg/apeworx) if you have any questions. 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 ApeWorX Ltd. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | Compile Solidity contracts. 4 | 5 | ## Dependencies 6 | 7 | - [python3](https://www.python.org/downloads) version 3.9 up to 3.12. 8 | 9 | ## Installation 10 | 11 | ### via `pip` 12 | 13 | You can install the latest release via [`pip`](https://pypi.org/project/pip/): 14 | 15 | ```bash 16 | pip install ape-solidity 17 | ``` 18 | 19 | ### via `setuptools` 20 | 21 | You can clone the repository and use [`setuptools`](https://github.com/pypa/setuptools) for the most up-to-date version: 22 | 23 | ```bash 24 | git clone https://github.com/ApeWorX/ape-solidity.git 25 | cd ape-solidity 26 | python3 setup.py install 27 | ``` 28 | 29 | ## Quick Usage 30 | 31 | In your project, make sure you have a `contracts/` directory containing Solidity files (`.sol`). 32 | 33 | Then, while this plugin is installed, compile your contracts: 34 | 35 | ```bash 36 | ape compile 37 | ``` 38 | 39 | The byte-code and ABI for your contracts should now exist in a `__local__.json` file in a `.build/` directory. 40 | 41 | ### Solidity Versioning 42 | 43 | By default, `ape-solidity` tries to use the best versions of Solidity by looking at all the source files' pragma specifications. 44 | However, it is often better to specify a version directly. 45 | If you know the best version to use, set it in your `ape-config.yaml`, like this: 46 | 47 | ```yaml 48 | solidity: 49 | version: 0.8.14 50 | ``` 51 | 52 | ### EVM Versioning 53 | 54 | By default, `ape-solidity` will use whatever version of EVM rules are set as default in the compiler version that gets used. 55 | Sometimes, you might want to use a different version, such as deploying on Arbitrum or Optimism where new opcodes are not supported yet. 56 | If you want to require a different version of EVM rules to use in the configuration of the compiler, set it in your `ape-config.yaml` like this: 57 | 58 | ```yaml 59 | solidity: 60 | evm_version: paris 61 | ``` 62 | 63 | ### Dependency Mapping 64 | 65 | By default, `ape-solidity` knows to look at installed dependencies for potential remapping-values and will use those when it notices you are importing them. 66 | For example, if you are using dependencies like: 67 | 68 | ```yaml 69 | dependencies: 70 | - name: openzeppelin 71 | github: OpenZeppelin/openzeppelin-contracts 72 | version: 4.4.2 73 | ``` 74 | 75 | And your source files import from `openzeppelin` this way: 76 | 77 | ```solidity 78 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 79 | ``` 80 | 81 | Ape knows how to resolve the `@openzeppelin` value and find the correct source. 82 | 83 | If you want to override this behavior or add new remappings that are not dependencies, you can add them to your `ape-config.yaml` under the `solidity:` key. 84 | For example, let's say you have downloaded `openzeppelin` somewhere and do not have it installed in Ape. 85 | You can map to your local install of `openzeppelin` this way: 86 | 87 | ```yaml 88 | solidity: 89 | import_remapping: 90 | - "@openzeppelin=path/to/openzeppelin" 91 | ``` 92 | 93 | ### Library Linking 94 | 95 | To compile contracts that use libraries, you need to add the libraries first. 96 | Use the `add_library()` method from the `ape-solidity` compiler class to add the library. 97 | A typical flow is: 98 | 99 | 1. Deploy the library. 100 | 2. Call `add_library()` using the Solidity compiler plugin, which will also re-compile contracts that need the library. 101 | 3. Deploy and use contracts that require the library. 102 | 103 | For example: 104 | 105 | ```python 106 | import pytest 107 | 108 | 109 | @pytest.fixture 110 | def contract(accounts, project, compilers): 111 | # Deploy the library. 112 | account = accounts[0] 113 | library = project.Set.deploy(sender=account) 114 | 115 | # Add the library to Solidity (re-compiles contracts that use the library). 116 | compilers.solidity.add_library(library) 117 | 118 | # Deploy the contract that uses the library. 119 | return project.C.deploy(sender=account) 120 | ``` 121 | 122 | ### Compiler Settings 123 | 124 | When using `ape-solidity`, your project's manifest's compiler settings will include standard JSON output. 125 | You should have one listed `compiler` per `solc` version used in your project. 126 | You can view your current project manifest, including the compiler settings, by doing: 127 | 128 | ```python 129 | from ape import project 130 | 131 | manifest = project.extract_manifest() 132 | 133 | for compiler_entry in manifest.compilers: 134 | print(compiler_entry.version) 135 | print(compiler_entry.settings) 136 | ``` 137 | 138 | **NOTE**: These are the settings used during contract verification when using the [Etherscan plugin](https://github.com/ApeWorX/ape-etherscan). 139 | 140 | #### `--via-IR` Yul IR Compilation Pipeline 141 | 142 | You can enable `solc`'s `--via-IR` flag by adding the following values to your `ape-config.yaml` 143 | 144 | ```yaml 145 | solidity: 146 | via_ir: True 147 | ``` 148 | 149 | ### Contract Flattening 150 | 151 | `ape-solidity` has contract-flattening capabilities. 152 | If you are publishing contracts using Ape, Ape automatically detects and uses the flattened-contract approach if needed. 153 | 154 | To manually flatten a contract for your own benefit, use the following code: 155 | 156 | ```python 157 | from ape import compilers, project 158 | 159 | source_path = project.source_paths[0] # Replace with your path. 160 | flattened_src = compilers.flatten_contract(source_path) 161 | print(str(flattened_src)) 162 | ``` 163 | -------------------------------------------------------------------------------- /ape_solidity/__init__.py: -------------------------------------------------------------------------------- 1 | from ape import plugins 2 | 3 | 4 | @plugins.register(plugins.Config) 5 | def config_class(): 6 | from .compiler import SolidityConfig 7 | 8 | return SolidityConfig 9 | 10 | 11 | @plugins.register(plugins.CompilerPlugin) 12 | def register_compiler(): 13 | from ._utils import Extension 14 | from .compiler import SolidityCompiler 15 | 16 | return (Extension.SOL.value,), SolidityCompiler 17 | 18 | 19 | def __getattr__(name: str): 20 | if name == "Extension": 21 | from ._utils import Extension 22 | 23 | return Extension 24 | 25 | elif name == "SolidityCompiler": 26 | from .compiler import SolidityCompiler 27 | 28 | return SolidityCompiler 29 | 30 | elif name == "SolidityConfig": 31 | from .compiler import SolidityConfig 32 | 33 | return SolidityConfig 34 | 35 | else: 36 | raise AttributeError(name) 37 | 38 | 39 | __all__ = [ 40 | "Extension", 41 | "SolidityCompiler", 42 | "SolidityConfig", 43 | ] 44 | -------------------------------------------------------------------------------- /ape_solidity/_cli.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import ape 4 | import click 5 | from ape.cli.options import ape_cli_context, project_option 6 | 7 | 8 | @click.group 9 | def cli(): 10 | """`solidity` command group""" 11 | 12 | 13 | @cli.command(short_help="Flatten select contract source files") 14 | @ape_cli_context() 15 | @project_option() 16 | @click.argument("CONTRACT", type=click.Path(exists=True, resolve_path=True)) 17 | @click.argument("OUTFILE", type=click.Path(exists=False, resolve_path=True, writable=True)) 18 | def flatten(cli_ctx, project, contract: Path, outfile: Path): 19 | """ 20 | Flatten a contract into a single file 21 | """ 22 | with Path(outfile).open("w") as fout: 23 | content = ape.compilers.solidity.flatten_contract( 24 | Path(contract), 25 | base_path=ape.project.contracts_folder, 26 | project=project, 27 | ) 28 | fout.write(str(content)) 29 | -------------------------------------------------------------------------------- /ape_solidity/_models.py: -------------------------------------------------------------------------------- 1 | import os 2 | from collections.abc import Iterable 3 | from functools import singledispatchmethod 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING, Optional 6 | 7 | from ape.exceptions import CompilerError, ProjectError 8 | from ape.utils.basemodel import BaseModel, ManagerAccessMixin, classproperty 9 | from ape.utils.os import get_relative_path 10 | from pydantic import field_serializer 11 | 12 | from ape_solidity._utils import get_single_import_lines 13 | 14 | if TYPE_CHECKING: 15 | from ape.managers.project import ProjectManager 16 | 17 | from ape_solidity.compiler import SolidityCompiler 18 | 19 | 20 | class ApeSolidityMixin(ManagerAccessMixin): 21 | @classproperty 22 | def solidity(cls) -> "SolidityCompiler": 23 | return cls.compiler_manager.solidity 24 | 25 | 26 | class ApeSolidityModel(BaseModel, ApeSolidityMixin): 27 | pass 28 | 29 | 30 | def _create_import_remapping(project: "ProjectManager") -> dict[str, str]: 31 | prefix = f"{get_relative_path(project.contracts_folder, project.path)}" 32 | specified = project.dependencies.install() 33 | 34 | # Ensure .cache folder is ready-to-go. 35 | cache_folder = project.contracts_folder / ".cache" 36 | cache_folder.mkdir(exist_ok=True, parents=True) 37 | 38 | # Start with explicitly configured remappings. 39 | cfg_remappings: dict[str, str] = { 40 | m.key: m.value for m in project.config.solidity.import_remapping 41 | } 42 | key_map: dict[str, str] = {} 43 | 44 | def get_cache_id(dep) -> str: 45 | return os.path.sep.join((prefix, ".cache", dep.name, dep.version)) 46 | 47 | def unpack(dep): 48 | # Ensure the dependency is installed. 49 | try: 50 | dep.project 51 | except ProjectError: 52 | # Try to compile anyway. 53 | # Let the compiler fail on its own. 54 | return 55 | 56 | for unpacked_dep in dep.unpack(project.contracts_folder / ".cache"): 57 | main_key = key_map.get(unpacked_dep.name) 58 | keys = (main_key,) if main_key else (f"@{unpacked_dep.name}", unpacked_dep.name) 59 | for _key in keys: 60 | if _key not in remapping: 61 | remapping[_key] = get_cache_id(unpacked_dep) 62 | # else, was specified or configured more appropriately. 63 | 64 | remapping: dict[str, str] = {} 65 | for key, value in cfg_remappings.items(): 66 | # Check if legacy-style and still accept it. 67 | parts = value.split(os.path.sep) 68 | name = parts[0] 69 | _version = None 70 | if len(parts) > 2: 71 | # Clearly, not pointing at a dependency. 72 | remapping[key] = value 73 | continue 74 | 75 | elif len(parts) == 2: 76 | _version = parts[1] 77 | 78 | if _version is None: 79 | matching_deps = [d for d in project.dependencies.installed if d.name == name] 80 | if len(matching_deps) == 1: 81 | _version = matching_deps[0].version 82 | else: 83 | # Not obvious if it is pointing at one of these dependencies. 84 | remapping[key] = value 85 | continue 86 | 87 | # Dependency found. Map to it using the provider key. 88 | dependency = project.dependencies.get_dependency(name, _version) 89 | key_map[dependency.name] = key 90 | unpack(dependency) 91 | 92 | # Add auto-remapped dependencies. 93 | # (Meaning, the dependencies are specified but their remappings 94 | # are not, so we auto-generate default ones). 95 | for dependency in specified: 96 | unpack(dependency) 97 | 98 | return remapping 99 | 100 | 101 | class ImportRemappingCache(ApeSolidityMixin): 102 | def __init__(self): 103 | # Cache project paths to import remapping. 104 | self._cache: dict[str, dict[str, str]] = {} 105 | 106 | def __getitem__(self, project: "ProjectManager") -> dict[str, str]: 107 | if remapping := self._cache.get(f"{project.path}"): 108 | return remapping 109 | 110 | return self.add_project(project) 111 | 112 | def add_project(self, project: "ProjectManager") -> dict[str, str]: 113 | remapping = _create_import_remapping(project) 114 | return self.add(project, remapping) 115 | 116 | def add(self, project: "ProjectManager", remapping: dict[str, str]): 117 | self._cache[f"{project.path}"] = remapping 118 | return remapping 119 | 120 | @classmethod 121 | def get_import_remapping(cls, project: "ProjectManager"): 122 | return _create_import_remapping(project) 123 | 124 | 125 | class ImportStatementMetadata(ApeSolidityModel): 126 | quote_char: str 127 | sep_char: str 128 | raw_value: str 129 | 130 | # Only set when remappings are involved. 131 | import_remap_key: Optional[str] = None 132 | import_remap_value: Optional[str] = None 133 | 134 | # Only set when import-remapping resolves to a dependency. 135 | dependency_name: Optional[str] = None 136 | dependency_version: Optional[str] = None 137 | 138 | # Set once a source-file is located. This happens _after_ 139 | # dependency related properties. 140 | source_id: Optional[str] = None 141 | path: Optional[Path] = None 142 | 143 | @property 144 | def value(self) -> str: 145 | if self.import_remap_key and self.import_remap_value: 146 | return self.raw_value.replace(self.import_remap_key, self.import_remap_value) 147 | 148 | return self.raw_value 149 | 150 | @property 151 | def dependency(self) -> Optional["ProjectManager"]: 152 | if name := self.dependency_name: 153 | if version := self.dependency_version: 154 | return self.local_project.dependencies[name][version] 155 | 156 | return None 157 | 158 | @classmethod 159 | def parse_line( 160 | cls, 161 | value: str, 162 | reference: Path, 163 | project: "ProjectManager", 164 | dependency: Optional["ProjectManager"] = None, 165 | ) -> "ImportStatementMetadata": 166 | quote = '"' if '"' in value else "'" 167 | sep = "\\" if "\\" in value else "/" 168 | 169 | try: 170 | end_index = value.index(quote) + 1 171 | except ValueError as err: 172 | raise CompilerError( 173 | f"Error parsing import statement '{value}' in '{reference.name}'." 174 | ) from err 175 | 176 | import_str_prefix = value[end_index:] 177 | value = import_str_prefix[: import_str_prefix.index(quote)] 178 | result = cls(quote_char=quote, sep_char=sep, raw_value=value) 179 | result._resolve_source(reference, project, dependency=dependency) 180 | return result 181 | 182 | def __repr__(self) -> str: 183 | return self.raw_value 184 | 185 | def __hash__(self) -> int: 186 | path = self.path or Path(self.raw_value) 187 | return hash(path) 188 | 189 | def _resolve_source( 190 | self, 191 | reference: Path, 192 | project: "ProjectManager", 193 | dependency: Optional["ProjectManager"] = None, 194 | ): 195 | if not self._resolve_dependency(project, dependency=dependency): 196 | # Handle non-dependencies. 197 | self._resolve_import_remapping(project) 198 | self._resolve_path(reference, project) 199 | 200 | def _resolve_import_remapping(self, project: "ProjectManager"): 201 | if self.value.startswith("."): 202 | # Relative paths should not use import-remappings. 203 | return 204 | 205 | import_remapping = self.solidity._import_remapping_cache[project] 206 | 207 | # Get all matches. 208 | valid_matches: list[tuple[str, str]] = [] 209 | for check_remap_key, check_remap_value in import_remapping.items(): 210 | if check_remap_key not in self.value: 211 | continue 212 | 213 | valid_matches.append((check_remap_key, check_remap_value)) 214 | 215 | if valid_matches: 216 | self.import_remap_key, self.import_remap_value = max( 217 | valid_matches, key=lambda x: len(x[0]) 218 | ) 219 | 220 | def _resolve_path(self, reference: Path, project: "ProjectManager"): 221 | base_path = None 222 | if self.value.startswith("."): 223 | base_path = reference.parent 224 | elif (project.path / self.value).is_file(): 225 | base_path = project.path 226 | elif (project.contracts_folder / self.value).is_file(): 227 | base_path = project.contracts_folder 228 | elif self.import_remap_key is not None and self.import_remap_key.startswith("@"): 229 | nm = self.import_remap_key[1:] 230 | for cfg_dep in project.config.dependencies: 231 | if ( 232 | cfg_dep.get("name") == nm 233 | and "project" in cfg_dep 234 | and (Path(cfg_dep["project"]) / self.value).is_file() 235 | ): 236 | base_path = Path(cfg_dep["project"]) 237 | 238 | if base := base_path: 239 | self.path = (base / self.value).resolve().absolute() 240 | self.source_id = f"{get_relative_path(self.path, project.path)}" 241 | 242 | def _resolve_dependency( 243 | self, project: "ProjectManager", dependency: Optional["ProjectManager"] = None 244 | ) -> bool: 245 | config_project = dependency or project 246 | # NOTE: Dependency is set if we are getting dependencies of dependencies. 247 | # It is tricky because we still need the base (local) project along 248 | # with project defining this dependency, for separate pieces of data. 249 | # (need base project for relative .cache folder location and need dependency 250 | # for configuration). 251 | import_remapping = self.solidity._import_remapping_cache[config_project] 252 | parts = self.value.split(self.sep_char) 253 | pot_dep_names = {parts[0], parts[0].lstrip("@"), f"@{parts[0].lstrip('@')}"} 254 | matches = [] 255 | for nm in pot_dep_names: 256 | if nm not in import_remapping or nm not in self.value: 257 | continue 258 | 259 | matches.append(nm) 260 | 261 | if not matches: 262 | return False 263 | 264 | name = max(matches, key=lambda x: len(x)) 265 | resolved_import = import_remapping[name] 266 | resolved_path_parts = resolved_import.split(self.sep_char) 267 | if ".cache" not in resolved_path_parts: 268 | # Not a dependency 269 | return False 270 | 271 | cache_index = resolved_path_parts.index(".cache") 272 | nm_index = cache_index + 1 273 | version_index = nm_index + 1 274 | 275 | if version_index >= len(resolved_path_parts): 276 | # Not sure. 277 | return False 278 | 279 | cache_folder_name = resolved_path_parts[nm_index] 280 | cache_folder_version = resolved_path_parts[version_index] 281 | dependency_project = config_project.dependencies[cache_folder_name][cache_folder_version] 282 | if not dependency_project: 283 | return False 284 | 285 | self.import_remap_key = name 286 | self.import_remap_value = resolved_import 287 | self.dependency_name = dependency_project.name 288 | self.dependency_version = dependency_project.version 289 | path = project.path / self.value 290 | if path.is_file(): 291 | self.source_id = self.value 292 | self.path = project.path / self.source_id 293 | else: 294 | contracts_dir = dependency_project.contracts_folder 295 | dep_path = dependency_project.path 296 | contracts_folder_name = f"{get_relative_path(contracts_dir, dep_path)}" 297 | prefix_pth = dep_path / contracts_folder_name 298 | start_idx = version_index + 1 299 | suffix = self.sep_char.join(self.value.split(self.sep_char)[start_idx:]) 300 | new_path = prefix_pth / suffix 301 | if not new_path.is_file(): 302 | # No further resolution required (but still is a resolved dependency). 303 | return True 304 | 305 | adjusted_base_path = ( 306 | f"{self.sep_char.join(resolved_path_parts[:4])}" 307 | f"{self.sep_char}{contracts_folder_name}" 308 | ) 309 | adjusted_src_id = f"{adjusted_base_path}{self.sep_char}{suffix}" 310 | 311 | # Also, correct import remappings now, since it didn't work. 312 | if key := self.import_remap_key: 313 | # Base path will now included the missing contracts name. 314 | self.solidity._import_remapping_cache[project][key] = adjusted_base_path 315 | self.import_remap_value = adjusted_base_path 316 | 317 | self.path = project.path / adjusted_src_id 318 | self.source_id = adjusted_src_id 319 | 320 | return True 321 | 322 | 323 | class SourceTree(ApeSolidityModel): 324 | """ 325 | A model representing a source-tree, meaning given a sequence 326 | of base-sources, this is the tree of each of them with their imports. 327 | """ 328 | 329 | import_statements: dict[tuple[Path, str], set[ImportStatementMetadata]] = {} 330 | """ 331 | Mapping of each file to its import-statements. 332 | """ 333 | 334 | @field_serializer("import_statements") 335 | def _serialize_import_statements(self, statements, info): 336 | imports_by_source_id = {k[1]: v for k, v in statements.items()} 337 | keys = sorted(imports_by_source_id.keys()) 338 | return { 339 | k: sorted(list({i.source_id for i in imports_by_source_id[k] if i.source_id})) 340 | for k in keys 341 | } 342 | 343 | @classmethod 344 | def from_source_files( 345 | cls, 346 | source_files: Iterable[Path], 347 | project: "ProjectManager", 348 | statements: Optional[dict[tuple[Path, str], set[ImportStatementMetadata]]] = None, 349 | dependency: Optional["ProjectManager"] = None, 350 | ) -> "SourceTree": 351 | statements = statements or {} 352 | for path in source_files: 353 | key = (path, f"{get_relative_path(path.absolute(), project.path)}") 354 | if key in statements: 355 | # We have already captures all of the imports from the file. 356 | continue 357 | 358 | statements[key] = set() 359 | for line in get_single_import_lines(path): 360 | node_data = ImportStatementMetadata.parse_line( 361 | line, path, project, dependency=dependency 362 | ) 363 | statements[key].add(node_data) 364 | if sub_path := node_data.path: 365 | sub_source_id = f"{get_relative_path(sub_path.absolute(), project.path)}" 366 | sub_key = (sub_path, sub_source_id) 367 | 368 | if sub_key in statements: 369 | sub_statements = statements[sub_key] 370 | else: 371 | sub_tree = SourceTree.from_source_files( 372 | (sub_path,), 373 | project, 374 | statements=statements, 375 | dependency=node_data.dependency, 376 | ) 377 | statements = {**statements, **sub_tree.import_statements} 378 | sub_statements = statements[sub_key] 379 | 380 | for sub_stmt in sub_statements: 381 | statements[key].add(sub_stmt) 382 | 383 | return cls(import_statements=statements) 384 | 385 | @singledispatchmethod 386 | def __getitem__(self, key) -> set[ImportStatementMetadata]: 387 | return set() 388 | 389 | @__getitem__.register 390 | def __getitem_path(self, path: Path) -> set[ImportStatementMetadata]: 391 | return next((v for k, v in self.import_statements.items() if k[0] == path), set()) 392 | 393 | @__getitem__.register 394 | def __getitem_str(self, source_id: str) -> set[ImportStatementMetadata]: 395 | return next((v for k, v in self.import_statements.items() if k[1] == source_id), set()) 396 | 397 | @singledispatchmethod 398 | def __contains__(self, value) -> bool: 399 | return False 400 | 401 | @__contains__.register 402 | def __contains_path(self, path: Path) -> bool: 403 | return any(x[0] == path for x in self.import_statements) 404 | 405 | @__contains__.register 406 | def __contains_str(self, source_id: str) -> bool: 407 | return any(x[1] == source_id for x in self.import_statements) 408 | 409 | @__contains__.register 410 | def __contains_tuple(self, key: tuple) -> bool: 411 | return key in self.import_statements 412 | 413 | def __repr__(self) -> str: 414 | key_str = ", ".join([f"{k[1]}={v}" for k, v in self.import_statements.items() if v]) 415 | return f"" 416 | 417 | def get_imported_paths(self, path: Path) -> set[Path]: 418 | return {x.path for x in self[path] if x.path} 419 | 420 | def get_remappings_used(self, paths: Iterable[Path]) -> dict[str, str]: 421 | remappings = {} 422 | for path in paths: 423 | for metadata in self[path]: 424 | if not metadata.import_remap_key or not metadata.import_remap_value: 425 | continue 426 | 427 | remappings[metadata.import_remap_key] = metadata.import_remap_value 428 | 429 | return remappings 430 | -------------------------------------------------------------------------------- /ape_solidity/_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | from collections.abc import Iterable 4 | from enum import Enum 5 | from pathlib import Path 6 | from typing import TYPE_CHECKING, Optional, Union 7 | 8 | from ape.exceptions import CompilerError 9 | from ape.utils import pragma_str_to_specifier_set 10 | from packaging.version import Version 11 | from solcx.install import get_executable 12 | from solcx.wrapper import get_solc_version as get_solc_version_from_binary 13 | 14 | if TYPE_CHECKING: 15 | from packaging.specifiers import SpecifierSet 16 | 17 | 18 | OUTPUT_SELECTION = [ 19 | "abi", 20 | "bin-runtime", 21 | "devdoc", 22 | "userdoc", 23 | "evm.bytecode.object", 24 | "evm.bytecode.sourceMap", 25 | "evm.deployedBytecode.object", 26 | ] 27 | 28 | 29 | class Extension(Enum): 30 | SOL = ".sol" 31 | 32 | 33 | def get_import_lines(source_paths: Iterable[Path]) -> dict[Path, list[str]]: 34 | imports_dict: dict[Path, list[str]] = {} 35 | for filepath in source_paths: 36 | imports_dict[filepath] = get_single_import_lines(filepath) 37 | 38 | return imports_dict 39 | 40 | 41 | def get_single_import_lines(source_path: Path) -> list[str]: 42 | import_set = set() 43 | if not source_path.is_file(): 44 | return [] 45 | 46 | source_lines = source_path.read_text(encoding="utf8").splitlines() 47 | num_lines = len(source_lines) 48 | for line_number, ln in enumerate(source_lines): 49 | if not ln.startswith("import"): 50 | continue 51 | 52 | import_str = ln 53 | second_line_number = line_number 54 | while ";" not in import_str: 55 | second_line_number += 1 56 | if second_line_number >= num_lines: 57 | raise CompilerError("Import statement missing semicolon.") 58 | 59 | next_line = source_lines[second_line_number] 60 | import_str += f" {next_line.strip()}" 61 | 62 | import_set.add(import_str) 63 | line_number += 1 64 | 65 | return list(import_set) 66 | 67 | 68 | def get_pragma_spec_from_path(source_file_path: Union[Path, str]) -> Optional["SpecifierSet"]: 69 | """ 70 | Extracts pragma information from Solidity source code. 71 | 72 | Args: 73 | source_file_path (Union[Path, str]): Solidity source file path. 74 | 75 | Returns: 76 | ``Optional[packaging.specifiers.SpecifierSet]`` 77 | """ 78 | path = Path(source_file_path) 79 | if not path.is_file(): 80 | return None 81 | 82 | source_str = path.read_text(encoding="utf8") 83 | return get_pragma_spec_from_str(source_str) 84 | 85 | 86 | def get_pragma_spec_from_str(source_str: str) -> Optional["SpecifierSet"]: 87 | if not ( 88 | pragma_match := next( 89 | re.finditer(r"(?:\n|^)\s*pragma\s*solidity\s*([^;\n]*)", source_str), None 90 | ) 91 | ): 92 | return None # Try compiling with latest 93 | 94 | return pragma_str_to_specifier_set(pragma_match.groups()[0]) 95 | 96 | 97 | def load_dict(data: Union[str, dict]) -> dict: 98 | return data if isinstance(data, dict) else json.loads(data) 99 | 100 | 101 | def add_commit_hash(version: Union[str, Version]) -> Version: 102 | vers = Version(f"{version}") if isinstance(version, str) else version 103 | has_commit = len(f"{vers}") > len(vers.base_version) 104 | if has_commit: 105 | # Already added. 106 | return vers 107 | 108 | solc = get_executable(version=vers) 109 | return get_solc_version_from_binary(solc, with_commit_hash=True) 110 | 111 | 112 | def get_versions_can_use(pragma_spec: "SpecifierSet", options: Iterable[Version]) -> list[Version]: 113 | return sorted(list(pragma_spec.filter(options)), reverse=True) 114 | 115 | 116 | def select_version(pragma_spec: "SpecifierSet", options: Iterable[Version]) -> Optional[Version]: 117 | choices = get_versions_can_use(pragma_spec, options) 118 | return choices[0] if choices else None 119 | 120 | 121 | def strip_commit_hash(version: Union[str, Version]) -> Version: 122 | """ 123 | Version('0.8.21+commit.d9974bed') => Version('0.8.21')> the simple way. 124 | """ 125 | return Version(f"{str(version).split('+')[0].strip()}") 126 | -------------------------------------------------------------------------------- /ape_solidity/exceptions.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | from typing import TYPE_CHECKING, Union 3 | 4 | from ape.exceptions import CompilerError, ConfigError, ContractLogicError 5 | from ape.logging import LogLevel, logger 6 | 7 | if TYPE_CHECKING: 8 | from solcx.exceptions import SolcError 9 | 10 | 11 | class SolcInstallError(CompilerError): 12 | """ 13 | Raised when `solc` is not installed 14 | and unable to be installed. 15 | """ 16 | 17 | def __init__(self): 18 | super().__init__( 19 | "No versions of `solc` installed and unable to install latest `solc` version." 20 | ) 21 | 22 | 23 | class SolcCompileError(CompilerError): 24 | """ 25 | Specifically, only errors arising from ``solc`` compile methods. 26 | This error is a modified version of ``SolcError`` to take into 27 | account Ape's logging verbosity. 28 | """ 29 | 30 | def __init__(self, solc_error: "SolcError"): 31 | self.solc_error = solc_error 32 | 33 | def __str__(self) -> str: 34 | if logger.level <= LogLevel.DEBUG: 35 | # Show everything when in DEBUG mode. 36 | return str(self.solc_error) 37 | 38 | else: 39 | # Only show the error and line-number(s) where it occurred. 40 | return self.solc_error.message 41 | 42 | 43 | class IncorrectMappingFormatError(ConfigError, ValueError): 44 | def __init__(self): 45 | super().__init__( 46 | "Incorrectly formatted 'solidity.remapping' config property. " 47 | "Expected '@value_1=value2'." 48 | ) 49 | 50 | 51 | class RuntimeErrorType(IntEnum): 52 | ASSERTION_ERROR = 0x1 53 | ARITHMETIC_UNDER_OR_OVERFLOW = 0x11 54 | DIVISION_BY_ZERO_ERROR = 0x12 55 | ENUM_CONVERSION_OUT_OF_BOUNDS = 0x21 56 | INCORRECTLY_ENCODED_STORAGE_BYTE_ARRAY = 0x22 57 | POP_ON_EMPTY_ARRAY = 0x31 58 | INDEX_OUT_OF_BOUNDS_ERROR = 0x32 59 | MEMORY_OVERFLOW_ERROR = 0x41 60 | ZERO_INITIALIZED_VARIABLE = 0x51 61 | 62 | 63 | class SolidityRuntimeError(ContractLogicError): 64 | def __init__(self, error_type: RuntimeErrorType, message: str, **kwargs): 65 | self.error_type = error_type 66 | super().__init__(message, **kwargs) 67 | 68 | 69 | class SolidityArithmeticError(SolidityRuntimeError, ArithmeticError): 70 | """ 71 | Raised from math operations going wrong. 72 | """ 73 | 74 | def __init__(self, **kwargs): 75 | message = "Arithmetic operation underflowed or overflowed outside of an unchecked block." 76 | super().__init__(RuntimeErrorType.ARITHMETIC_UNDER_OR_OVERFLOW, message, **kwargs) 77 | 78 | 79 | class SolidityAssertionError(SolidityRuntimeError, AssertionError): 80 | """ 81 | Raised from Solidity ``assert`` statements. 82 | You typically should never see this error, as higher-level Contract Logic error 83 | handled in the framework should appear first (with the correct revert message). 84 | """ 85 | 86 | def __init__(self, **kwargs): 87 | message = "Assertion error." 88 | super().__init__(RuntimeErrorType.ASSERTION_ERROR, message, **kwargs) 89 | 90 | 91 | class DivisionByZeroError(SolidityRuntimeError, ZeroDivisionError): 92 | """ 93 | Raised when dividing goes wrong (such as using a 0 denominator). 94 | """ 95 | 96 | def __init__(self, **kwargs): 97 | message = "Division or modulo division by zero" 98 | super().__init__(RuntimeErrorType.DIVISION_BY_ZERO_ERROR, message, **kwargs) 99 | 100 | 101 | class EnumConversionError(SolidityRuntimeError): 102 | """ 103 | Raised when Solidity fails to convert an enum value to its primitive type. 104 | """ 105 | 106 | def __init__(self, **kwargs): 107 | message = "Tried to convert a value into an enum, but the value was too big or negative." 108 | super().__init__(RuntimeErrorType.ENUM_CONVERSION_OUT_OF_BOUNDS, message, **kwargs) 109 | 110 | 111 | class EncodeStorageError(SolidityRuntimeError): 112 | """ 113 | Raised when Solidity fails to encode a storage value. 114 | """ 115 | 116 | def __init__(self, **kwargs): 117 | message = "Incorrectly encoded storage byte array." 118 | super().__init__(RuntimeErrorType.INCORRECTLY_ENCODED_STORAGE_BYTE_ARRAY, message, **kwargs) 119 | 120 | 121 | class IndexOutOfBoundsError(SolidityRuntimeError, IndexError): 122 | """ 123 | Raised when accessing an index that is out of bounds in your contract. 124 | """ 125 | 126 | def __init__(self, **kwargs): 127 | message = "Array accessed at an out-of-bounds or negative index." 128 | super().__init__(RuntimeErrorType.INDEX_OUT_OF_BOUNDS_ERROR, message, **kwargs) 129 | 130 | 131 | class MemoryOverflowError(SolidityRuntimeError, OverflowError): 132 | """ 133 | Raised when exceeding the allocating memory for a data type 134 | in Solidity. 135 | """ 136 | 137 | def __init__(self, **kwargs): 138 | message = "Too much memory was allocated, or an array was created that is too large." 139 | super().__init__(RuntimeErrorType.MEMORY_OVERFLOW_ERROR, message, **kwargs) 140 | 141 | 142 | class PopOnEmptyArrayError(SolidityRuntimeError): 143 | """ 144 | Raised when popping from a data-structure fails in your contract. 145 | """ 146 | 147 | def __init__(self, **kwargs): 148 | message = ".pop() was called on an empty array." 149 | super().__init__(RuntimeErrorType.POP_ON_EMPTY_ARRAY, message, **kwargs) 150 | 151 | 152 | class ZeroInitializedVariableError(SolidityRuntimeError): 153 | """ 154 | Raised when calling a zero-initialized variable of internal function type. 155 | """ 156 | 157 | def __init__(self, **kwargs): 158 | message = "Called a zero-initialized variable of internal function type." 159 | super().__init__(RuntimeErrorType.ZERO_INITIALIZED_VARIABLE, message, **kwargs) 160 | 161 | 162 | RUNTIME_ERROR_CODE_PREFIX = "0x4e487b71" 163 | RuntimeErrorUnion = Union[ 164 | SolidityArithmeticError, 165 | SolidityAssertionError, 166 | DivisionByZeroError, 167 | EnumConversionError, 168 | EncodeStorageError, 169 | IndexOutOfBoundsError, 170 | MemoryOverflowError, 171 | PopOnEmptyArrayError, 172 | ZeroInitializedVariableError, 173 | ] 174 | RUNTIME_ERROR_MAP: dict[RuntimeErrorType, type[RuntimeErrorUnion]] = { 175 | RuntimeErrorType.ASSERTION_ERROR: SolidityAssertionError, 176 | RuntimeErrorType.ARITHMETIC_UNDER_OR_OVERFLOW: SolidityArithmeticError, 177 | RuntimeErrorType.DIVISION_BY_ZERO_ERROR: DivisionByZeroError, 178 | RuntimeErrorType.ENUM_CONVERSION_OUT_OF_BOUNDS: EnumConversionError, 179 | RuntimeErrorType.INCORRECTLY_ENCODED_STORAGE_BYTE_ARRAY: EncodeStorageError, 180 | RuntimeErrorType.INDEX_OUT_OF_BOUNDS_ERROR: IndexOutOfBoundsError, 181 | RuntimeErrorType.MEMORY_OVERFLOW_ERROR: MemoryOverflowError, 182 | RuntimeErrorType.POP_ON_EMPTY_ARRAY: PopOnEmptyArrayError, 183 | RuntimeErrorType.ZERO_INITIALIZED_VARIABLE: ZeroInitializedVariableError, 184 | } 185 | -------------------------------------------------------------------------------- /ape_solidity/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ApeWorX/ape-solidity/04b03fedd95c0ceb60fc0dbaddab5072dbf45b22/ape_solidity/py.typed -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=75.6.0", "wheel", "setuptools_scm[toml]>=5.0"] 3 | 4 | [tool.mypy] 5 | exclude = "build/" 6 | check_untyped_defs = true 7 | plugins = ["pydantic.mypy"] 8 | 9 | [tool.setuptools_scm] 10 | write_to = "ape_solidity/version.py" 11 | 12 | # NOTE: you have to use single-quoted strings in TOML for regular expressions. 13 | # It's the equivalent of r-strings in Python. Multiline strings are treated as 14 | # verbose regular expressions by Black. Use [ ] to denote a significant space 15 | # character. 16 | 17 | [tool.black] 18 | line-length = 100 19 | target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] 20 | include = '\.pyi?$' 21 | 22 | [tool.pytest.ini_options] 23 | addopts = """ 24 | -p no:ape_test 25 | --cov-branch 26 | --cov-report term 27 | --cov-report html 28 | --cov-report xml 29 | --cov=ape_solidity 30 | """ 31 | python_files = "test_*.py" 32 | testpaths = "tests" 33 | markers = """ 34 | fuzzing: Run Hypothesis fuzz test suite 35 | install: Tests that will install a solc version (slow) 36 | """ 37 | 38 | [tool.isort] 39 | line_length = 100 40 | force_grid_wrap = 0 41 | include_trailing_comma = true 42 | multi_line_output = 3 43 | use_parentheses = true 44 | 45 | [tool.mdformat] 46 | number = true 47 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | ignore = E704,W503,PYD002,TC003,TC006 4 | exclude = 5 | *.venv* 6 | venv* 7 | docs 8 | build 9 | tests/node_modules 10 | type-checking-pydantic-enabled = True 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from setuptools import find_packages, setup 4 | 5 | extras_require = { 6 | "test": [ # `test` GitHub Action jobs uses this 7 | "pytest>=6.0", # Core testing package 8 | "pytest-xdist", # Multi-process runner 9 | "pytest-cov", # Coverage analyzer plugin 10 | "pytest-mock", # For creating and using mocks 11 | "hypothesis>=6.2.0,<7.0", # Strategy-based fuzzer 12 | "pytest-benchmark", # For performance tests 13 | ], 14 | "lint": [ 15 | "black>=25.1.0,<26", # Auto-formatter and linter 16 | "mypy>=1.15.0,<2", # Static type analyzer 17 | "types-requests", # Needed for mypy type shed 18 | "types-setuptools", # Needed for mypy type shed 19 | "flake8>=7.1.2,<8", # Style linter 20 | "flake8-pydantic", # For detecting issues with Pydantic models 21 | "flake8-type-checking", # Detect imports to move in/out of type-checking blocks 22 | "isort>=5.13.2,<6", # Import sorting linter 23 | "mdformat>=0.7.22", # Auto-formatter for markdown 24 | "mdformat-gfm>=0.3.5", # Needed for formatting GitHub-flavored markdown 25 | "mdformat-frontmatter>=0.4.1", # Needed for frontmatters-style headers in issue templates 26 | "mdformat-pyproject>=0.0.2", # Allows configuring in pyproject.toml 27 | ], 28 | "doc": [ 29 | "Sphinx>=6.1.3,<7", # Documentation generator 30 | "sphinx_rtd_theme>=1.2.0,<2", # Readthedocs.org theme 31 | "towncrier>=19.2.0,<20", # Generate release notes 32 | ], 33 | "release": [ # `release` GitHub Action job uses this 34 | "setuptools>=75.6.0", # Installation tool 35 | "setuptools-scm", # Installation tool 36 | "wheel", # Packaging tool 37 | "twine==3.8", # Package upload tool 38 | ], 39 | "dev": [ 40 | "commitizen", # Manage commits and publishing releases 41 | "pytest-watch", # `ptw` test watcher/runner 42 | "IPython", # Console for interacting 43 | "ipdb", # Debugger (Must use `export PYTHONBREAKPOINT=ipdb.set_trace`) 44 | ], 45 | } 46 | 47 | # NOTE: `pip install -e .[dev]` to install package 48 | extras_require["dev"] = ( 49 | extras_require["test"] 50 | + extras_require["lint"] 51 | + extras_require["doc"] 52 | + extras_require["release"] 53 | + extras_require["dev"] 54 | ) 55 | 56 | with open("./README.md") as readme: 57 | long_description = readme.read() 58 | 59 | 60 | setup( 61 | name="ape-solidity", 62 | use_scm_version=True, 63 | setup_requires=["setuptools_scm"], 64 | description="Plugin for Ape Ethereum Framework for compiling Solidity contracts", 65 | long_description=long_description, 66 | long_description_content_type="text/markdown", 67 | author="ApeWorX Ltd.", 68 | author_email="admin@apeworx.io", 69 | url="https://github.com/ApeWorX/ape-solidity", 70 | include_package_data=True, 71 | install_requires=[ 72 | "py-solc-x>=2.0.2,<3", 73 | "eth-ape>=0.8.4,<0.9", 74 | "ethpm-types", # Use the version ape requires 75 | "eth-pydantic-types", # Use the version ape requires 76 | "packaging", # Use the version ape requires 77 | "requests", 78 | ], 79 | python_requires=">=3.9,<4", 80 | extras_require=extras_require, 81 | py_modules=["ape_solidity"], 82 | entry_points={ 83 | "ape_cli_subcommands": [ 84 | "ape_solidity=ape_solidity._cli:cli", 85 | ], 86 | }, 87 | license="Apache-2.0", 88 | zip_safe=False, 89 | keywords="ethereum", 90 | packages=find_packages(exclude=["tests", "tests.*"]), 91 | package_data={"ape_solidity": ["py.typed"]}, 92 | classifiers=[ 93 | "Development Status :: 5 - Production/Stable", 94 | "Intended Audience :: Developers", 95 | "License :: OSI Approved :: MIT License", 96 | "Natural Language :: English", 97 | "Operating System :: MacOS", 98 | "Operating System :: POSIX", 99 | "Programming Language :: Python :: 3", 100 | "Programming Language :: Python :: 3.9", 101 | "Programming Language :: Python :: 3.10", 102 | "Programming Language :: Python :: 3.11", 103 | "Programming Language :: Python :: 3.12", 104 | "Programming Language :: Python :: 3.13", 105 | ], 106 | ) 107 | -------------------------------------------------------------------------------- /tests/BrownieProject/brownie-config.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - OpenZeppelin/openzeppelin-contracts@4.5.0 3 | 4 | compiler: 5 | solc: 6 | version: 0.8.12 7 | remappings: 8 | - "@oz=OpenZeppelin/openzeppelin-contracts@4.5.0" 9 | -------------------------------------------------------------------------------- /tests/BrownieProject/contracts/BrownieContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.4; 4 | 5 | contract CompilingContract { 6 | function foo() pure public returns(bool) { 7 | return true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/Dependency/ape-config.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: dependencyofdependency 3 | local: ../DependencyOfDependency 4 | -------------------------------------------------------------------------------- /tests/Dependency/contracts/Dependency.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.4; 4 | 5 | import "@dependencyofdependency/contracts/DependencyOfDependency.sol"; 6 | 7 | struct DependencyStruct { 8 | string name; 9 | uint value; 10 | } 11 | 12 | contract Dependency { 13 | function foo() pure public returns(bool) { 14 | return true; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Dependency/contracts/OlderDependency.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | contract OlderDependency { 4 | function foo() pure public returns(bool) { 5 | return true; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/Dependency/contracts/subfolder/InDependencySubfolder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.4; 4 | 5 | contract InDependencySubfolder { 6 | function foo() pure public returns(bool) { 7 | return true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/Dependency/contracts/subfolder_with_imports/InDependencySubfolderWithImports.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.4; 4 | 5 | import "../Dependency.sol"; 6 | import "../subfolder/InDependencySubfolder.sol"; 7 | 8 | contract InDependencySubfolderWithImports { 9 | function foo() pure public returns(bool) { 10 | return true; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/DependencyOfDependency/ape-config.yaml: -------------------------------------------------------------------------------- 1 | contracts_folder: contracts 2 | name: TestDependencyOfDependency 3 | version: local 4 | -------------------------------------------------------------------------------- /tests/DependencyOfDependency/contracts/DependencyOfDependency.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.4; 4 | 5 | contract DependencyOfDependency { 6 | function foo() pure public returns(bool) { 7 | return true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/NonCompilingDependency/ape-config.yaml: -------------------------------------------------------------------------------- 1 | contracts_folder: contracts 2 | -------------------------------------------------------------------------------- /tests/NonCompilingDependency/contracts/CompilingContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.4; 4 | 5 | contract BrownieStyleDependency { 6 | function foo() pure public returns(bool) { 7 | return true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/NonCompilingDependency/contracts/NonCompilingContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | This file exists to show that a non-fully compiling dependency can contain a contract that does not compile. 5 | But the dependency is still usable (provided the project only uses working contract types) 6 | -------------------------------------------------------------------------------- /tests/NonCompilingDependency/contracts/subdir/SubCompilingContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.4; 4 | 5 | contract SubCompilingContract { 6 | function foo() pure public returns(bool) { 7 | return true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/ProjectWithinProject/ape-config.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: remapping 3 | local: ../Dependency 4 | 5 | - name: dependencyofdependency 6 | local: ../DependencyOfDependency 7 | -------------------------------------------------------------------------------- /tests/ProjectWithinProject/contracts/Contract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.4; 4 | 5 | import "@remapping/contracts/Dependency.sol"; 6 | 7 | contract Contract { 8 | function foo() pure public returns(bool) { 9 | return true; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/VersionSpecifiedInConfig/ape-config.yaml: -------------------------------------------------------------------------------- 1 | solidity: 2 | version: 0.8.12 3 | -------------------------------------------------------------------------------- /tests/VersionSpecifiedInConfig/contracts/VersionSpecifiedInConfig.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.8.0; 4 | 5 | contract VersionSpecifiedInConfig { 6 | function foo() pure public returns(bool) { 7 | return true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ApeWorX/ape-solidity/04b03fedd95c0ceb60fc0dbaddab5072dbf45b22/tests/__init__.py -------------------------------------------------------------------------------- /tests/ape-config.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: dependency 3 | local: ./Dependency 4 | 5 | # Make sure can use a Brownie project as a dependency 6 | - name: browniedependency 7 | local: ./BrownieProject 8 | 9 | # Make sure can use contracts from a non-fully compiling dependency. 10 | - name: noncompilingdependency 11 | local: ./NonCompilingDependency 12 | 13 | # Ensure we can build a realistic-brownie project with dependencies. 14 | - name: vault 15 | github: yearn/yearn-vaults 16 | ref: v0.4.5 17 | 18 | # Ensure dependencies using GitHub references work. 19 | - name: vaultmain 20 | github: yearn/yearn-vaults 21 | ref: master 22 | 23 | # Ensure NPM dependencies work. 24 | # NOTE: Do not change this dependency; it is also 25 | # part of other tests. See `./safe/ThisIsNotGnosisSafe.sol`. 26 | - name: safe 27 | npm: "@gnosis.pm/safe-contracts" 28 | version: 1.3.0 29 | 30 | solidity: 31 | # Using evm_version compatible with older and newer solidity versions. 32 | evm_version: constantinople 33 | 34 | import_remapping: 35 | # Legacy support test (missing contracts key in import test) 36 | - "@noncompilingdependency=noncompilingdependency" 37 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from contextlib import contextmanager 3 | from pathlib import Path 4 | from tempfile import mkdtemp 5 | from unittest import mock 6 | 7 | import ape 8 | import pytest 9 | import solcx 10 | from ape.utils.os import create_tempdir 11 | from click.testing import CliRunner 12 | 13 | from ape_solidity._utils import Extension 14 | 15 | 16 | @contextmanager 17 | def _tmp_solcx_path(monkeypatch): 18 | solcx_install_path = mkdtemp() 19 | 20 | monkeypatch.setenv( 21 | solcx.install.SOLCX_BINARY_PATH_VARIABLE, 22 | solcx_install_path, 23 | ) 24 | 25 | yield solcx_install_path 26 | 27 | if Path(solcx_install_path).is_dir(): 28 | shutil.rmtree(solcx_install_path, ignore_errors=True) 29 | 30 | 31 | @pytest.fixture 32 | def fake_no_installs(mocker): 33 | """ 34 | Tricks the tests into thinking there are no installed versions. 35 | This saves time because it won't actually need to install solc, 36 | and it should still work. 37 | """ 38 | patch = mocker.patch("ape_solidity.compiler.get_installed_solc_versions") 39 | patch.return_value = [] 40 | return patch 41 | 42 | 43 | @pytest.fixture 44 | def temp_solcx_path(monkeypatch): 45 | """ 46 | Creates a new, temporary installation path for solcx for a given test. 47 | """ 48 | with _tmp_solcx_path(monkeypatch) as path: 49 | yield path 50 | 51 | 52 | @pytest.fixture(scope="session") 53 | def project(config): 54 | _ = config # Ensure temp data folder gets set first. 55 | root = Path(__file__).parent 56 | 57 | # Delete .build / .cache that may exist pre-copy 58 | for path in ( 59 | root, 60 | root / "BrownieProject", 61 | root / "BrownieStyleDependency", 62 | root / "Dependency", 63 | root / "DependencyOfDependency", 64 | root / "ProjectWithinProject", 65 | root / "VersionSpecifiedInConfig", 66 | ): 67 | for cache in (path / ".build", path / "contracts" / ".cache"): 68 | if cache.is_dir(): 69 | shutil.rmtree(cache) 70 | 71 | root_project = ape.Project(root) 72 | with root_project.isolate_in_tempdir() as tmp_project: 73 | yield tmp_project 74 | 75 | 76 | @pytest.fixture(scope="session", autouse=True) 77 | def config(): 78 | cfg = ape.config 79 | 80 | # Uncomment to install dependencies in actual data folder. 81 | # This will save time running tests. 82 | # project = ape.Project(Path(__file__).parent) 83 | # project.dependencies.install() 84 | 85 | # Ensure we don't persist any .ape data. 86 | real_data_folder = cfg.DATA_FOLDER 87 | with create_tempdir() as path: 88 | cfg.DATA_FOLDER = path 89 | 90 | # Copy in existing packages to save test time 91 | # when running locally. 92 | packages = real_data_folder / "packages" 93 | packages.mkdir(parents=True, exist_ok=True) 94 | shutil.copytree(packages, path / "packages", dirs_exist_ok=True) 95 | 96 | yield cfg 97 | 98 | 99 | @pytest.fixture 100 | def compiler_manager(): 101 | return ape.compilers 102 | 103 | 104 | @pytest.fixture 105 | def compiler(compiler_manager): 106 | return compiler_manager.solidity 107 | 108 | 109 | @pytest.fixture(autouse=True) 110 | def ignore_other_compilers(mocker, compiler_manager, compiler): 111 | """ 112 | Having ape-vyper installed causes the random Vyper file 113 | (that exists for testing purposes) to get compiled and 114 | vyper to get repeatedly installed in a temporary directory. 115 | Avoid that by tricking Ape into thinking ape-vyper is not 116 | installed (if it is). 117 | """ 118 | existing_compilers = compiler_manager.registered_compilers 119 | ape_pm = compiler_manager.ethpm 120 | valid_compilers = { 121 | ext: c for ext, c in existing_compilers.items() if ext in [x.value for x in Extension] 122 | } 123 | path = "ape.managers.compilers.CompilerManager.registered_compilers" 124 | mock_registered_compilers = mocker.patch(path, new_callable=mock.PropertyMock) 125 | 126 | # Only ethpm (.json) and Solidity extensions allowed. 127 | mock_registered_compilers.return_value = {".json": ape_pm, **valid_compilers} 128 | 129 | 130 | @pytest.fixture 131 | def account(): 132 | return ape.accounts.test_accounts[0] 133 | 134 | 135 | @pytest.fixture 136 | def owner(): 137 | return ape.accounts.test_accounts[1] 138 | 139 | 140 | @pytest.fixture 141 | def not_owner(): 142 | return ape.accounts.test_accounts[2] 143 | 144 | 145 | @pytest.fixture 146 | def connection(): 147 | with ape.networks.ethereum.local.use_provider("test") as provider: 148 | yield provider 149 | 150 | 151 | @pytest.fixture 152 | def cli_runner(): 153 | return CliRunner() 154 | -------------------------------------------------------------------------------- /tests/contracts/BuiltinErrorChecker.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | contract BuiltinErrorChecker { 5 | uint256[] public _arr; 6 | 7 | function checkIndexOutOfBounds() view public returns(uint256) { 8 | return _arr[2]; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/contracts/CircularImport1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.4; 4 | 5 | import "contracts/CircularImport2.sol"; 6 | 7 | contract CircularImport1 { 8 | function foo() pure public returns(bool) { 9 | return true; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/contracts/CircularImport2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.4; 4 | 5 | import "contracts/CircularImport1.sol"; 6 | 7 | contract CircularImport2 { 8 | function foo() pure public returns(bool) { 9 | return true; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/contracts/CompilesOnce.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.8.0; 4 | 5 | struct MyStruct { 6 | string name; 7 | uint value; 8 | } 9 | 10 | contract CompilesOnce { 11 | // This contract tests the scenario when we have a contract with 12 | // a similar compiler version to more than one other contract's. 13 | // This ensures we don't compile the same contract more than once. 14 | 15 | function foo() pure public returns(bool) { 16 | return true; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/contracts/ContractUsingLibraryNotInSameSource.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | // Borrowed from Solidity documentation. 3 | pragma solidity >=0.6.0 <0.9.0; 4 | 5 | import "./LibraryFun.sol"; 6 | 7 | contract ContractUsingLibraryNotInSameSource { 8 | Data knownValues; 9 | 10 | function register(uint value) public { 11 | // The library functions can be called without a 12 | // specific instance of the library, since the 13 | // "instance" will be the current contract. 14 | require(ExampleLibrary.insert(knownValues, value)); 15 | } 16 | // In this contract, we can also directly access knownValues.flags, if we want. 17 | } 18 | -------------------------------------------------------------------------------- /tests/contracts/DifferentNameThanFile.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.8.0; 4 | 5 | contract ApeDifferentNameThanFile { 6 | function foo() pure public returns(bool) { 7 | return true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/contracts/DirectoryWithExtension.sol/README.md: -------------------------------------------------------------------------------- 1 | # Directory with Misleading Extension Test 2 | 3 | The existence of this directory is to test things still work when a directory with `.sol` in its name is present. 4 | -------------------------------------------------------------------------------- /tests/contracts/ExperimentalABIEncoderV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.4.0; 4 | pragma experimental ABIEncoderV2; 5 | 6 | contract ExperimentalABIEncoderV2 { 7 | function foo() pure public returns(bool) { 8 | return true; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/contracts/HasError.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.4; 3 | 4 | error Unauthorized(address addr, uint256 counter); 5 | 6 | contract HasError { 7 | address payable owner = payable(msg.sender); 8 | 9 | constructor(uint256 value) { 10 | if (value == 0) { 11 | revert Unauthorized(msg.sender, 123); 12 | } 13 | } 14 | 15 | function withdraw() public { 16 | if (msg.sender != owner) 17 | revert Unauthorized(msg.sender, 123); 18 | 19 | owner.transfer(address(this).balance); 20 | } 21 | // ... 22 | } 23 | -------------------------------------------------------------------------------- /tests/contracts/ImportOlderDependency.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | import "@dependency/contracts/OlderDependency.sol"; 4 | 5 | contract ImportOlderDependency { 6 | function foo() pure public returns(bool) { 7 | return true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/contracts/ImportSourceWithEqualSignVersion.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.4; 4 | 5 | // The file SpecificVersionWithEqualSign.sol has pragma spec '=0.8.12'. 6 | // This means that these files should all compile using that version. 7 | import "contracts/SpecificVersionWithEqualSign.sol"; 8 | import "contracts/CompilesOnce.sol"; 9 | 10 | contract ImportSourceWithEqualSignVersion { 11 | function foo() pure public returns(bool) { 12 | return true; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/contracts/ImportSourceWithNoPrefixVersion.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.4; 4 | 5 | // The file SpecificVersionWithEqualSign.sol has pragma spec '0.8.12'. 6 | // This means that these files should all compile using that version. 7 | import "contracts/SpecificVersionNoPrefix.sol"; 8 | import "contracts/CompilesOnce.sol"; 9 | 10 | contract ImportSourceWithNoPrefixVersion { 11 | function foo() pure public returns(bool) { 12 | return true; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/contracts/ImportingLessConstrainedVersion.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity =0.8.12; 4 | 5 | // The file we are importing specific range '>=0.8.12 <0.8.15'; 6 | // This means on its own, the plugin would use 0.8.14 if its installed. 7 | // However - it should use 0.8.12 because of this file's requirements. 8 | import "./SpecificVersionRange.sol"; 9 | 10 | contract ImportingLessConstrainedVersion { 11 | function foo() pure public returns(bool) { 12 | return true; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/contracts/Imports.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.4; 4 | 5 | import * as Depend from "@dependency/contracts/Dependency.sol"; 6 | import 7 | "./././././././././././././././././././././././././././././././././././MissingPragma.sol"; 8 | import { MyStruct } from "contracts/CompilesOnce.sol"; 9 | import "./subfolder/Relativecontract.sol"; 10 | import "@dependency/contracts/Dependency.sol" as Depend2; 11 | import "@browniedependency/contracts/BrownieContract.sol"; 12 | import { 13 | Struct0, 14 | Struct1, 15 | Struct2, 16 | Struct3, 17 | Struct4, 18 | Struct5 19 | } from "./NumerousDefinitions.sol"; 20 | import "@noncompilingdependency/CompilingContract.sol"; 21 | // Purposely repeat an import to test how the plugin handles that. 22 | import "@noncompilingdependency/CompilingContract.sol"; 23 | 24 | import "@safe/contracts/common/Enum.sol"; 25 | // Show we can import a local contract with the same name (sin-@) as a dependency. 26 | import "./safe/ThisIsNotGnosisSafe.sol"; 27 | 28 | // Purposely exclude the contracts folder to test older Ape-style project imports. 29 | import "@noncompilingdependency/subdir/SubCompilingContract.sol"; 30 | 31 | // Showing sources with extra extensions are by default excluded, 32 | // unless used as an import somewhere in a non-excluded source. 33 | import "./Source.extra.ext.sol"; 34 | 35 | contract Imports { 36 | function foo() pure public returns(bool) { 37 | return true; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/contracts/IndirectlyImportingMoreConstrainedVersion.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "./ImportSourceWithEqualSignVersion.sol"; 6 | import "./IndirectlyImportingMoreConstrainedVersionCompanion.sol"; 7 | 8 | contract IndirectlyImportingMoreConstrainedVersion { 9 | function foo() pure public returns(bool) { 10 | return true; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/contracts/IndirectlyImportingMoreConstrainedVersionCompanion.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | // We are testing this files imports are constrained by the version 6 | // of the file importing this file (which is constrained by another one of 7 | // its imports). 8 | import "./IndirectlyImportingMoreConstrainedVersionCompanionImport.sol"; 9 | 10 | contract IndirectlyImportingMoreConstrainedVersionCompanion { 11 | function foo() pure public returns(bool) { 12 | return true; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/contracts/IndirectlyImportingMoreConstrainedVersionCompanionImport.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | // This file exists to test that when we import a file with another more constraining import, 6 | // that the files other imports (e.g. IndirectlyImportingMoreContrainedVersionCompanion.sol) 7 | // uses the constrained version as well as that files imports, which is this file. 8 | 9 | contract IndirectlyImportingMoreConstrainedVersionCompanionImport { 10 | function foo() pure public returns(bool) { 11 | return true; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/contracts/JustAStruct.sol: -------------------------------------------------------------------------------- 1 | struct Data { 2 | mapping(uint => bool) flags; 3 | } 4 | -------------------------------------------------------------------------------- /tests/contracts/LibraryFun.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | // Borrowed from Solidity documentation. 3 | pragma solidity >=0.6.0 <0.9.0; 4 | 5 | 6 | // We define a new struct datatype that will be used to 7 | // hold its data in the calling contract. 8 | struct Data { 9 | mapping(uint => bool) flags; 10 | } 11 | 12 | library ExampleLibrary { 13 | // Note that the first parameter is of type "storage 14 | // reference" and thus only its storage address and not 15 | // its contents is passed as part of the call. This is a 16 | // special feature of library functions. It is idiomatic 17 | // to call the first parameter `self`, if the function can 18 | // be seen as a method of that object. 19 | function insert(Data storage self, uint value) 20 | public 21 | returns (bool) 22 | { 23 | if (self.flags[value]) 24 | return false; // already there 25 | self.flags[value] = true; 26 | return true; 27 | } 28 | 29 | function remove(Data storage self, uint value) 30 | public 31 | returns (bool) 32 | { 33 | if (!self.flags[value]) 34 | return false; // not there 35 | self.flags[value] = false; 36 | return true; 37 | } 38 | 39 | function contains(Data storage self, uint value) 40 | public 41 | view 42 | returns (bool) 43 | { 44 | return self.flags[value]; 45 | } 46 | } 47 | 48 | 49 | contract ContractUsingLibraryInSameSource { 50 | Data knownValues; 51 | 52 | function register(uint value) public { 53 | // The library functions can be called without a 54 | // specific instance of the library, since the 55 | // "instance" will be the current contract. 56 | require(ExampleLibrary.insert(knownValues, value)); 57 | } 58 | // In this contract, we can also directly access knownValues.flags, if we want. 59 | } 60 | -------------------------------------------------------------------------------- /tests/contracts/MissingPragma.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | contract MissingPragma { 4 | function foo() pure public returns(bool) { 5 | return true; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/contracts/MultipleDefinitions.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | interface IMultipleDefinitions { 6 | function foo() external; 7 | } 8 | 9 | contract MultipleDefinitions is IMultipleDefinitions { 10 | function foo() external {} 11 | } 12 | -------------------------------------------------------------------------------- /tests/contracts/NumerousDefinitions.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.8.0; 4 | 5 | struct Struct0 { 6 | string name; 7 | uint value; 8 | } 9 | 10 | struct Struct1 { 11 | string name; 12 | uint value; 13 | } 14 | 15 | struct Struct2 { 16 | string name; 17 | uint value; 18 | } 19 | 20 | struct Struct3 { 21 | string name; 22 | uint value; 23 | } 24 | 25 | struct Struct4 { 26 | string name; 27 | uint value; 28 | } 29 | 30 | struct Struct5 { 31 | string name; 32 | uint value; 33 | } 34 | 35 | contract NumerousDefinitions { 36 | function foo() pure public returns(bool) { 37 | return true; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/contracts/OlderVersion.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity =0.5.16; 4 | 5 | contract OlderVersion { 6 | function foo() pure public returns(bool) { 7 | return true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/contracts/RandomVyperFile.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.10 2 | 3 | # NOTE: This file only exists to prove it does not interfere 4 | # (we had found bugs related to this) 5 | 6 | myNumber: public(uint256) 7 | 8 | @external 9 | def setNumber(num: uint256): 10 | self.myNumber = num 11 | -------------------------------------------------------------------------------- /tests/contracts/RangedVersion.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >= 0.4.19 < 0.7.0; 3 | 4 | // Tests that we fixed a bug regarding vague versioned imports. 5 | import "./VagueVersion.sol"; 6 | 7 | library RangedVersion { 8 | } 9 | -------------------------------------------------------------------------------- /tests/contracts/Source.extra.ext.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | // Showing sources with extra extensions are by default excluded, 5 | // unless used as an import somewhere in a non-excluded source. 6 | contract SourceExtraExt { 7 | function foo() pure public returns(bool) { 8 | return true; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/contracts/SpacesInPragma.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // This file exists to test a bug that occurred when the user has a pragma 3 | // like this one. It was failing to register as a proper NpmSpec because 4 | // of the spaces between the operator and the version. 5 | pragma solidity >= 0.4.19 < 0.5.0; 6 | 7 | interface SpacesInPragma { 8 | // This syntax fails on version >= 5.0, thus we are testing 9 | // that we don't default to our latest solidity version. 10 | function foo(bytes32 node) public view returns (uint64); 11 | } 12 | -------------------------------------------------------------------------------- /tests/contracts/SpecificVersionNoPrefix.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "./CompilesOnce.sol"; 6 | 7 | contract SpecificVersionNoPrefix { 8 | function foo() pure public returns(bool) { 9 | return true; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/contracts/SpecificVersionNoPrefix2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // NOTE: This contract purposely is the same as `SpecificVersionNoPrefix` 4 | // except with a different specific version. 5 | pragma solidity 0.8.14; 6 | 7 | // Both specific versions import the same file. 8 | // This is an important test! 9 | import "contracts/CompilesOnce.sol"; 10 | 11 | contract SpecificVersionNoPrefix2 { 12 | function foo() pure public returns(bool) { 13 | return true; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/contracts/SpecificVersionRange.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.8.12 <0.8.15; 4 | 5 | contract SpecificVersionRange { 6 | function foo() pure public returns(bool) { 7 | return true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/contracts/SpecificVersionWithEqualSign.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity =0.8.12; 4 | 5 | import "contracts/CompilesOnce.sol"; 6 | 7 | contract SpecificVersionWithEqualSign { 8 | function foo() pure public returns(bool) { 9 | return true; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/contracts/UseYearn.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.17; 3 | 4 | import {VaultAPI} from "@vault/contracts/BaseStrategy.sol"; 5 | import {VaultAPI as VaultMain} from "@vaultmain/contracts/BaseStrategy.sol"; 6 | 7 | 8 | interface UseYearn is VaultAPI { 9 | function name() override external view returns (string calldata); 10 | } 11 | -------------------------------------------------------------------------------- /tests/contracts/VagueVersion.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >0.4.18; 4 | 5 | 6 | library VagueVersion { 7 | } 8 | -------------------------------------------------------------------------------- /tests/contracts/safe/README.md: -------------------------------------------------------------------------------- 1 | # Local folder named 'safe' 2 | 3 | Do not change the name of this folder. 4 | It is part of test where we ensure we can have local imports to folders that have the same name as dependencies. 5 | In this case, we also have a dependency with key `@safe`. 6 | -------------------------------------------------------------------------------- /tests/contracts/safe/ThisIsNotGnosisSafe.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // The point of this file is show we can have contracts in folders 3 | // where the folder is the same name as a dependency. 4 | pragma solidity ^0.8.0; 5 | 6 | contract ThisIsNotGnosisSafe { 7 | constructor(){ 8 | 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/contracts/subfolder/Relativecontract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.8.0; 4 | 5 | contract Relativecontract { 6 | 7 | function foo() pure public returns(bool) { 8 | return true; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/contracts/subfolder/UsingDependencyWithinSubFolder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.4; 4 | 5 | import "@dependency/contracts/Dependency.sol"; 6 | 7 | contract UsingDependencyWithinSubFolder { 8 | function foo() pure public returns(bool) { 9 | return true; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/data/ImportingLessConstrainedVersionFlat.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.8.12; 2 | // SPDX-License-Identifier: MIT 3 | 4 | // File: ./SpecificVersionRange.sol 5 | 6 | contract SpecificVersionRange { 7 | function foo() pure public returns(bool) { 8 | return true; 9 | } 10 | } 11 | 12 | // File: ImportingLessConstrainedVersion.sol 13 | 14 | // The file we are importing specific range '>=0.8.12 <0.8.15'; 15 | // This means on its own, the plugin would use 0.8.14 if its installed. 16 | // However - it should use 0.8.12 because of this file's requirements. 17 | 18 | contract ImportingLessConstrainedVersion { 19 | function foo() pure public returns(bool) { 20 | return true; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/data/ImportsFlattened.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.4; 2 | // SPDX-License-Identifier: MIT 3 | 4 | // File: @dependencyofdependency/contracts/DependencyOfDependency.sol 5 | 6 | contract DependencyOfDependency { 7 | function foo() pure public returns(bool) { 8 | return true; 9 | } 10 | } 11 | 12 | // File: * as Depend from "@dependency/contracts/Dependency.sol 13 | 14 | struct DependencyStruct { 15 | string name; 16 | uint value; 17 | } 18 | 19 | contract Dependency { 20 | function foo() pure public returns(bool) { 21 | return true; 22 | } 23 | } 24 | // File: { Struct0, Struct1, Struct2, Struct3, Struct4, Struct5 } from "./NumerousDefinitions.sol 25 | 26 | struct Struct0 { 27 | string name; 28 | uint value; 29 | } 30 | 31 | struct Struct1 { 32 | string name; 33 | uint value; 34 | } 35 | 36 | struct Struct2 { 37 | string name; 38 | uint value; 39 | } 40 | 41 | struct Struct3 { 42 | string name; 43 | uint value; 44 | } 45 | 46 | struct Struct4 { 47 | string name; 48 | uint value; 49 | } 50 | 51 | struct Struct5 { 52 | string name; 53 | uint value; 54 | } 55 | 56 | contract NumerousDefinitions { 57 | function foo() pure public returns(bool) { 58 | return true; 59 | } 60 | } 61 | // File: @noncompilingdependency/CompilingContract.sol 62 | 63 | contract BrownieStyleDependency { 64 | function foo() pure public returns(bool) { 65 | return true; 66 | } 67 | } 68 | // File: @browniedependency/contracts/BrownieContract.sol 69 | 70 | contract CompilingContract { 71 | function foo() pure public returns(bool) { 72 | return true; 73 | } 74 | } 75 | // File: ./subfolder/Relativecontract.sol 76 | 77 | contract Relativecontract { 78 | 79 | function foo() pure public returns(bool) { 80 | return true; 81 | } 82 | } 83 | // File: ./././././././././././././././././././././././././././././././././././MissingPragma.sol 84 | 85 | contract MissingPragma { 86 | function foo() pure public returns(bool) { 87 | return true; 88 | } 89 | } 90 | // File: @safe/contracts/common/Enum.sol 91 | 92 | /// @title Enum - Collection of enums 93 | /// @author Richard Meissner - 94 | contract Enum { 95 | enum Operation {Call, DelegateCall} 96 | } 97 | // File: ./Source.extra.ext.sol 98 | 99 | // Showing sources with extra extensions are by default excluded, 100 | // unless used as an import somewhere in a non-excluded source. 101 | contract SourceExtraExt { 102 | function foo() pure public returns(bool) { 103 | return true; 104 | } 105 | } 106 | // File: { MyStruct } from "contracts/CompilesOnce.sol 107 | 108 | struct MyStruct { 109 | string name; 110 | uint value; 111 | } 112 | 113 | contract CompilesOnce { 114 | // This contract tests the scenario when we have a contract with 115 | // a similar compiler version to more than one other contract's. 116 | // This ensures we don't compile the same contract more than once. 117 | 118 | function foo() pure public returns(bool) { 119 | return true; 120 | } 121 | } 122 | // File: @noncompilingdependency/subdir/SubCompilingContract.sol 123 | 124 | contract SubCompilingContract { 125 | function foo() pure public returns(bool) { 126 | return true; 127 | } 128 | } 129 | 130 | // File: Imports.sol 131 | 132 | import 133 | "./././././././././././././././././././././././././././././././././././MissingPragma.sol"; 134 | import { 135 | Struct0, 136 | Struct1, 137 | Struct2, 138 | Struct3, 139 | Struct4, 140 | Struct5 141 | } from "./NumerousDefinitions.sol"; 142 | // Purposely repeat an import to test how the plugin handles that. 143 | 144 | // Purposely exclude the contracts folder to test older Ape-style project imports. 145 | 146 | // Showing sources with extra extensions are by default excluded, 147 | // unless used as an import somewhere in a non-excluded source. 148 | 149 | contract Imports { 150 | function foo() pure public returns(bool) { 151 | return true; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /tests/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ape-solidity-tests", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "ape-solidity-tests", 9 | "version": "1.0.0", 10 | "dependencies": { 11 | "@gnosis.pm/safe-contracts": "^1.3.0" 12 | } 13 | }, 14 | "node_modules/@ethersproject/abi": { 15 | "version": "5.7.0", 16 | "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz", 17 | "integrity": "sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==", 18 | "funding": [ 19 | { 20 | "type": "individual", 21 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 22 | }, 23 | { 24 | "type": "individual", 25 | "url": "https://www.buymeacoffee.com/ricmoo" 26 | } 27 | ], 28 | "peer": true, 29 | "dependencies": { 30 | "@ethersproject/address": "^5.7.0", 31 | "@ethersproject/bignumber": "^5.7.0", 32 | "@ethersproject/bytes": "^5.7.0", 33 | "@ethersproject/constants": "^5.7.0", 34 | "@ethersproject/hash": "^5.7.0", 35 | "@ethersproject/keccak256": "^5.7.0", 36 | "@ethersproject/logger": "^5.7.0", 37 | "@ethersproject/properties": "^5.7.0", 38 | "@ethersproject/strings": "^5.7.0" 39 | } 40 | }, 41 | "node_modules/@ethersproject/abstract-provider": { 42 | "version": "5.7.0", 43 | "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz", 44 | "integrity": "sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==", 45 | "funding": [ 46 | { 47 | "type": "individual", 48 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 49 | }, 50 | { 51 | "type": "individual", 52 | "url": "https://www.buymeacoffee.com/ricmoo" 53 | } 54 | ], 55 | "peer": true, 56 | "dependencies": { 57 | "@ethersproject/bignumber": "^5.7.0", 58 | "@ethersproject/bytes": "^5.7.0", 59 | "@ethersproject/logger": "^5.7.0", 60 | "@ethersproject/networks": "^5.7.0", 61 | "@ethersproject/properties": "^5.7.0", 62 | "@ethersproject/transactions": "^5.7.0", 63 | "@ethersproject/web": "^5.7.0" 64 | } 65 | }, 66 | "node_modules/@ethersproject/abstract-signer": { 67 | "version": "5.7.0", 68 | "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz", 69 | "integrity": "sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==", 70 | "funding": [ 71 | { 72 | "type": "individual", 73 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 74 | }, 75 | { 76 | "type": "individual", 77 | "url": "https://www.buymeacoffee.com/ricmoo" 78 | } 79 | ], 80 | "peer": true, 81 | "dependencies": { 82 | "@ethersproject/abstract-provider": "^5.7.0", 83 | "@ethersproject/bignumber": "^5.7.0", 84 | "@ethersproject/bytes": "^5.7.0", 85 | "@ethersproject/logger": "^5.7.0", 86 | "@ethersproject/properties": "^5.7.0" 87 | } 88 | }, 89 | "node_modules/@ethersproject/address": { 90 | "version": "5.7.0", 91 | "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz", 92 | "integrity": "sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==", 93 | "funding": [ 94 | { 95 | "type": "individual", 96 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 97 | }, 98 | { 99 | "type": "individual", 100 | "url": "https://www.buymeacoffee.com/ricmoo" 101 | } 102 | ], 103 | "peer": true, 104 | "dependencies": { 105 | "@ethersproject/bignumber": "^5.7.0", 106 | "@ethersproject/bytes": "^5.7.0", 107 | "@ethersproject/keccak256": "^5.7.0", 108 | "@ethersproject/logger": "^5.7.0", 109 | "@ethersproject/rlp": "^5.7.0" 110 | } 111 | }, 112 | "node_modules/@ethersproject/base64": { 113 | "version": "5.7.0", 114 | "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.7.0.tgz", 115 | "integrity": "sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==", 116 | "funding": [ 117 | { 118 | "type": "individual", 119 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 120 | }, 121 | { 122 | "type": "individual", 123 | "url": "https://www.buymeacoffee.com/ricmoo" 124 | } 125 | ], 126 | "peer": true, 127 | "dependencies": { 128 | "@ethersproject/bytes": "^5.7.0" 129 | } 130 | }, 131 | "node_modules/@ethersproject/basex": { 132 | "version": "5.7.0", 133 | "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.7.0.tgz", 134 | "integrity": "sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw==", 135 | "funding": [ 136 | { 137 | "type": "individual", 138 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 139 | }, 140 | { 141 | "type": "individual", 142 | "url": "https://www.buymeacoffee.com/ricmoo" 143 | } 144 | ], 145 | "peer": true, 146 | "dependencies": { 147 | "@ethersproject/bytes": "^5.7.0", 148 | "@ethersproject/properties": "^5.7.0" 149 | } 150 | }, 151 | "node_modules/@ethersproject/bignumber": { 152 | "version": "5.7.0", 153 | "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.7.0.tgz", 154 | "integrity": "sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==", 155 | "funding": [ 156 | { 157 | "type": "individual", 158 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 159 | }, 160 | { 161 | "type": "individual", 162 | "url": "https://www.buymeacoffee.com/ricmoo" 163 | } 164 | ], 165 | "peer": true, 166 | "dependencies": { 167 | "@ethersproject/bytes": "^5.7.0", 168 | "@ethersproject/logger": "^5.7.0", 169 | "bn.js": "^5.2.1" 170 | } 171 | }, 172 | "node_modules/@ethersproject/bytes": { 173 | "version": "5.7.0", 174 | "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz", 175 | "integrity": "sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==", 176 | "funding": [ 177 | { 178 | "type": "individual", 179 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 180 | }, 181 | { 182 | "type": "individual", 183 | "url": "https://www.buymeacoffee.com/ricmoo" 184 | } 185 | ], 186 | "peer": true, 187 | "dependencies": { 188 | "@ethersproject/logger": "^5.7.0" 189 | } 190 | }, 191 | "node_modules/@ethersproject/constants": { 192 | "version": "5.7.0", 193 | "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.7.0.tgz", 194 | "integrity": "sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==", 195 | "funding": [ 196 | { 197 | "type": "individual", 198 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 199 | }, 200 | { 201 | "type": "individual", 202 | "url": "https://www.buymeacoffee.com/ricmoo" 203 | } 204 | ], 205 | "peer": true, 206 | "dependencies": { 207 | "@ethersproject/bignumber": "^5.7.0" 208 | } 209 | }, 210 | "node_modules/@ethersproject/contracts": { 211 | "version": "5.7.0", 212 | "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.7.0.tgz", 213 | "integrity": "sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg==", 214 | "funding": [ 215 | { 216 | "type": "individual", 217 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 218 | }, 219 | { 220 | "type": "individual", 221 | "url": "https://www.buymeacoffee.com/ricmoo" 222 | } 223 | ], 224 | "peer": true, 225 | "dependencies": { 226 | "@ethersproject/abi": "^5.7.0", 227 | "@ethersproject/abstract-provider": "^5.7.0", 228 | "@ethersproject/abstract-signer": "^5.7.0", 229 | "@ethersproject/address": "^5.7.0", 230 | "@ethersproject/bignumber": "^5.7.0", 231 | "@ethersproject/bytes": "^5.7.0", 232 | "@ethersproject/constants": "^5.7.0", 233 | "@ethersproject/logger": "^5.7.0", 234 | "@ethersproject/properties": "^5.7.0", 235 | "@ethersproject/transactions": "^5.7.0" 236 | } 237 | }, 238 | "node_modules/@ethersproject/hash": { 239 | "version": "5.7.0", 240 | "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.7.0.tgz", 241 | "integrity": "sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==", 242 | "funding": [ 243 | { 244 | "type": "individual", 245 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 246 | }, 247 | { 248 | "type": "individual", 249 | "url": "https://www.buymeacoffee.com/ricmoo" 250 | } 251 | ], 252 | "peer": true, 253 | "dependencies": { 254 | "@ethersproject/abstract-signer": "^5.7.0", 255 | "@ethersproject/address": "^5.7.0", 256 | "@ethersproject/base64": "^5.7.0", 257 | "@ethersproject/bignumber": "^5.7.0", 258 | "@ethersproject/bytes": "^5.7.0", 259 | "@ethersproject/keccak256": "^5.7.0", 260 | "@ethersproject/logger": "^5.7.0", 261 | "@ethersproject/properties": "^5.7.0", 262 | "@ethersproject/strings": "^5.7.0" 263 | } 264 | }, 265 | "node_modules/@ethersproject/hdnode": { 266 | "version": "5.7.0", 267 | "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.7.0.tgz", 268 | "integrity": "sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg==", 269 | "funding": [ 270 | { 271 | "type": "individual", 272 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 273 | }, 274 | { 275 | "type": "individual", 276 | "url": "https://www.buymeacoffee.com/ricmoo" 277 | } 278 | ], 279 | "peer": true, 280 | "dependencies": { 281 | "@ethersproject/abstract-signer": "^5.7.0", 282 | "@ethersproject/basex": "^5.7.0", 283 | "@ethersproject/bignumber": "^5.7.0", 284 | "@ethersproject/bytes": "^5.7.0", 285 | "@ethersproject/logger": "^5.7.0", 286 | "@ethersproject/pbkdf2": "^5.7.0", 287 | "@ethersproject/properties": "^5.7.0", 288 | "@ethersproject/sha2": "^5.7.0", 289 | "@ethersproject/signing-key": "^5.7.0", 290 | "@ethersproject/strings": "^5.7.0", 291 | "@ethersproject/transactions": "^5.7.0", 292 | "@ethersproject/wordlists": "^5.7.0" 293 | } 294 | }, 295 | "node_modules/@ethersproject/json-wallets": { 296 | "version": "5.7.0", 297 | "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz", 298 | "integrity": "sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g==", 299 | "funding": [ 300 | { 301 | "type": "individual", 302 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 303 | }, 304 | { 305 | "type": "individual", 306 | "url": "https://www.buymeacoffee.com/ricmoo" 307 | } 308 | ], 309 | "peer": true, 310 | "dependencies": { 311 | "@ethersproject/abstract-signer": "^5.7.0", 312 | "@ethersproject/address": "^5.7.0", 313 | "@ethersproject/bytes": "^5.7.0", 314 | "@ethersproject/hdnode": "^5.7.0", 315 | "@ethersproject/keccak256": "^5.7.0", 316 | "@ethersproject/logger": "^5.7.0", 317 | "@ethersproject/pbkdf2": "^5.7.0", 318 | "@ethersproject/properties": "^5.7.0", 319 | "@ethersproject/random": "^5.7.0", 320 | "@ethersproject/strings": "^5.7.0", 321 | "@ethersproject/transactions": "^5.7.0", 322 | "aes-js": "3.0.0", 323 | "scrypt-js": "3.0.1" 324 | } 325 | }, 326 | "node_modules/@ethersproject/keccak256": { 327 | "version": "5.7.0", 328 | "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.7.0.tgz", 329 | "integrity": "sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==", 330 | "funding": [ 331 | { 332 | "type": "individual", 333 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 334 | }, 335 | { 336 | "type": "individual", 337 | "url": "https://www.buymeacoffee.com/ricmoo" 338 | } 339 | ], 340 | "peer": true, 341 | "dependencies": { 342 | "@ethersproject/bytes": "^5.7.0", 343 | "js-sha3": "0.8.0" 344 | } 345 | }, 346 | "node_modules/@ethersproject/logger": { 347 | "version": "5.7.0", 348 | "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz", 349 | "integrity": "sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==", 350 | "funding": [ 351 | { 352 | "type": "individual", 353 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 354 | }, 355 | { 356 | "type": "individual", 357 | "url": "https://www.buymeacoffee.com/ricmoo" 358 | } 359 | ], 360 | "peer": true 361 | }, 362 | "node_modules/@ethersproject/networks": { 363 | "version": "5.7.1", 364 | "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.7.1.tgz", 365 | "integrity": "sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==", 366 | "funding": [ 367 | { 368 | "type": "individual", 369 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 370 | }, 371 | { 372 | "type": "individual", 373 | "url": "https://www.buymeacoffee.com/ricmoo" 374 | } 375 | ], 376 | "peer": true, 377 | "dependencies": { 378 | "@ethersproject/logger": "^5.7.0" 379 | } 380 | }, 381 | "node_modules/@ethersproject/pbkdf2": { 382 | "version": "5.7.0", 383 | "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz", 384 | "integrity": "sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw==", 385 | "funding": [ 386 | { 387 | "type": "individual", 388 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 389 | }, 390 | { 391 | "type": "individual", 392 | "url": "https://www.buymeacoffee.com/ricmoo" 393 | } 394 | ], 395 | "peer": true, 396 | "dependencies": { 397 | "@ethersproject/bytes": "^5.7.0", 398 | "@ethersproject/sha2": "^5.7.0" 399 | } 400 | }, 401 | "node_modules/@ethersproject/properties": { 402 | "version": "5.7.0", 403 | "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.7.0.tgz", 404 | "integrity": "sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==", 405 | "funding": [ 406 | { 407 | "type": "individual", 408 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 409 | }, 410 | { 411 | "type": "individual", 412 | "url": "https://www.buymeacoffee.com/ricmoo" 413 | } 414 | ], 415 | "peer": true, 416 | "dependencies": { 417 | "@ethersproject/logger": "^5.7.0" 418 | } 419 | }, 420 | "node_modules/@ethersproject/providers": { 421 | "version": "5.7.2", 422 | "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.2.tgz", 423 | "integrity": "sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==", 424 | "funding": [ 425 | { 426 | "type": "individual", 427 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 428 | }, 429 | { 430 | "type": "individual", 431 | "url": "https://www.buymeacoffee.com/ricmoo" 432 | } 433 | ], 434 | "peer": true, 435 | "dependencies": { 436 | "@ethersproject/abstract-provider": "^5.7.0", 437 | "@ethersproject/abstract-signer": "^5.7.0", 438 | "@ethersproject/address": "^5.7.0", 439 | "@ethersproject/base64": "^5.7.0", 440 | "@ethersproject/basex": "^5.7.0", 441 | "@ethersproject/bignumber": "^5.7.0", 442 | "@ethersproject/bytes": "^5.7.0", 443 | "@ethersproject/constants": "^5.7.0", 444 | "@ethersproject/hash": "^5.7.0", 445 | "@ethersproject/logger": "^5.7.0", 446 | "@ethersproject/networks": "^5.7.0", 447 | "@ethersproject/properties": "^5.7.0", 448 | "@ethersproject/random": "^5.7.0", 449 | "@ethersproject/rlp": "^5.7.0", 450 | "@ethersproject/sha2": "^5.7.0", 451 | "@ethersproject/strings": "^5.7.0", 452 | "@ethersproject/transactions": "^5.7.0", 453 | "@ethersproject/web": "^5.7.0", 454 | "bech32": "1.1.4", 455 | "ws": "7.4.6" 456 | } 457 | }, 458 | "node_modules/@ethersproject/random": { 459 | "version": "5.7.0", 460 | "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.7.0.tgz", 461 | "integrity": "sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ==", 462 | "funding": [ 463 | { 464 | "type": "individual", 465 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 466 | }, 467 | { 468 | "type": "individual", 469 | "url": "https://www.buymeacoffee.com/ricmoo" 470 | } 471 | ], 472 | "peer": true, 473 | "dependencies": { 474 | "@ethersproject/bytes": "^5.7.0", 475 | "@ethersproject/logger": "^5.7.0" 476 | } 477 | }, 478 | "node_modules/@ethersproject/rlp": { 479 | "version": "5.7.0", 480 | "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.7.0.tgz", 481 | "integrity": "sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==", 482 | "funding": [ 483 | { 484 | "type": "individual", 485 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 486 | }, 487 | { 488 | "type": "individual", 489 | "url": "https://www.buymeacoffee.com/ricmoo" 490 | } 491 | ], 492 | "peer": true, 493 | "dependencies": { 494 | "@ethersproject/bytes": "^5.7.0", 495 | "@ethersproject/logger": "^5.7.0" 496 | } 497 | }, 498 | "node_modules/@ethersproject/sha2": { 499 | "version": "5.7.0", 500 | "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.7.0.tgz", 501 | "integrity": "sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw==", 502 | "funding": [ 503 | { 504 | "type": "individual", 505 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 506 | }, 507 | { 508 | "type": "individual", 509 | "url": "https://www.buymeacoffee.com/ricmoo" 510 | } 511 | ], 512 | "peer": true, 513 | "dependencies": { 514 | "@ethersproject/bytes": "^5.7.0", 515 | "@ethersproject/logger": "^5.7.0", 516 | "hash.js": "1.1.7" 517 | } 518 | }, 519 | "node_modules/@ethersproject/signing-key": { 520 | "version": "5.7.0", 521 | "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.7.0.tgz", 522 | "integrity": "sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==", 523 | "funding": [ 524 | { 525 | "type": "individual", 526 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 527 | }, 528 | { 529 | "type": "individual", 530 | "url": "https://www.buymeacoffee.com/ricmoo" 531 | } 532 | ], 533 | "peer": true, 534 | "dependencies": { 535 | "@ethersproject/bytes": "^5.7.0", 536 | "@ethersproject/logger": "^5.7.0", 537 | "@ethersproject/properties": "^5.7.0", 538 | "bn.js": "^5.2.1", 539 | "elliptic": "6.5.4", 540 | "hash.js": "1.1.7" 541 | } 542 | }, 543 | "node_modules/@ethersproject/solidity": { 544 | "version": "5.7.0", 545 | "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.7.0.tgz", 546 | "integrity": "sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==", 547 | "funding": [ 548 | { 549 | "type": "individual", 550 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 551 | }, 552 | { 553 | "type": "individual", 554 | "url": "https://www.buymeacoffee.com/ricmoo" 555 | } 556 | ], 557 | "peer": true, 558 | "dependencies": { 559 | "@ethersproject/bignumber": "^5.7.0", 560 | "@ethersproject/bytes": "^5.7.0", 561 | "@ethersproject/keccak256": "^5.7.0", 562 | "@ethersproject/logger": "^5.7.0", 563 | "@ethersproject/sha2": "^5.7.0", 564 | "@ethersproject/strings": "^5.7.0" 565 | } 566 | }, 567 | "node_modules/@ethersproject/strings": { 568 | "version": "5.7.0", 569 | "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", 570 | "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==", 571 | "funding": [ 572 | { 573 | "type": "individual", 574 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 575 | }, 576 | { 577 | "type": "individual", 578 | "url": "https://www.buymeacoffee.com/ricmoo" 579 | } 580 | ], 581 | "peer": true, 582 | "dependencies": { 583 | "@ethersproject/bytes": "^5.7.0", 584 | "@ethersproject/constants": "^5.7.0", 585 | "@ethersproject/logger": "^5.7.0" 586 | } 587 | }, 588 | "node_modules/@ethersproject/transactions": { 589 | "version": "5.7.0", 590 | "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.7.0.tgz", 591 | "integrity": "sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==", 592 | "funding": [ 593 | { 594 | "type": "individual", 595 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 596 | }, 597 | { 598 | "type": "individual", 599 | "url": "https://www.buymeacoffee.com/ricmoo" 600 | } 601 | ], 602 | "peer": true, 603 | "dependencies": { 604 | "@ethersproject/address": "^5.7.0", 605 | "@ethersproject/bignumber": "^5.7.0", 606 | "@ethersproject/bytes": "^5.7.0", 607 | "@ethersproject/constants": "^5.7.0", 608 | "@ethersproject/keccak256": "^5.7.0", 609 | "@ethersproject/logger": "^5.7.0", 610 | "@ethersproject/properties": "^5.7.0", 611 | "@ethersproject/rlp": "^5.7.0", 612 | "@ethersproject/signing-key": "^5.7.0" 613 | } 614 | }, 615 | "node_modules/@ethersproject/units": { 616 | "version": "5.7.0", 617 | "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.7.0.tgz", 618 | "integrity": "sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==", 619 | "funding": [ 620 | { 621 | "type": "individual", 622 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 623 | }, 624 | { 625 | "type": "individual", 626 | "url": "https://www.buymeacoffee.com/ricmoo" 627 | } 628 | ], 629 | "peer": true, 630 | "dependencies": { 631 | "@ethersproject/bignumber": "^5.7.0", 632 | "@ethersproject/constants": "^5.7.0", 633 | "@ethersproject/logger": "^5.7.0" 634 | } 635 | }, 636 | "node_modules/@ethersproject/wallet": { 637 | "version": "5.7.0", 638 | "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.7.0.tgz", 639 | "integrity": "sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==", 640 | "funding": [ 641 | { 642 | "type": "individual", 643 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 644 | }, 645 | { 646 | "type": "individual", 647 | "url": "https://www.buymeacoffee.com/ricmoo" 648 | } 649 | ], 650 | "peer": true, 651 | "dependencies": { 652 | "@ethersproject/abstract-provider": "^5.7.0", 653 | "@ethersproject/abstract-signer": "^5.7.0", 654 | "@ethersproject/address": "^5.7.0", 655 | "@ethersproject/bignumber": "^5.7.0", 656 | "@ethersproject/bytes": "^5.7.0", 657 | "@ethersproject/hash": "^5.7.0", 658 | "@ethersproject/hdnode": "^5.7.0", 659 | "@ethersproject/json-wallets": "^5.7.0", 660 | "@ethersproject/keccak256": "^5.7.0", 661 | "@ethersproject/logger": "^5.7.0", 662 | "@ethersproject/properties": "^5.7.0", 663 | "@ethersproject/random": "^5.7.0", 664 | "@ethersproject/signing-key": "^5.7.0", 665 | "@ethersproject/transactions": "^5.7.0", 666 | "@ethersproject/wordlists": "^5.7.0" 667 | } 668 | }, 669 | "node_modules/@ethersproject/web": { 670 | "version": "5.7.1", 671 | "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz", 672 | "integrity": "sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==", 673 | "funding": [ 674 | { 675 | "type": "individual", 676 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 677 | }, 678 | { 679 | "type": "individual", 680 | "url": "https://www.buymeacoffee.com/ricmoo" 681 | } 682 | ], 683 | "peer": true, 684 | "dependencies": { 685 | "@ethersproject/base64": "^5.7.0", 686 | "@ethersproject/bytes": "^5.7.0", 687 | "@ethersproject/logger": "^5.7.0", 688 | "@ethersproject/properties": "^5.7.0", 689 | "@ethersproject/strings": "^5.7.0" 690 | } 691 | }, 692 | "node_modules/@ethersproject/wordlists": { 693 | "version": "5.7.0", 694 | "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.7.0.tgz", 695 | "integrity": "sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA==", 696 | "funding": [ 697 | { 698 | "type": "individual", 699 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 700 | }, 701 | { 702 | "type": "individual", 703 | "url": "https://www.buymeacoffee.com/ricmoo" 704 | } 705 | ], 706 | "peer": true, 707 | "dependencies": { 708 | "@ethersproject/bytes": "^5.7.0", 709 | "@ethersproject/hash": "^5.7.0", 710 | "@ethersproject/logger": "^5.7.0", 711 | "@ethersproject/properties": "^5.7.0", 712 | "@ethersproject/strings": "^5.7.0" 713 | } 714 | }, 715 | "node_modules/@gnosis.pm/safe-contracts": { 716 | "version": "1.3.0", 717 | "resolved": "https://registry.npmjs.org/@gnosis.pm/safe-contracts/-/safe-contracts-1.3.0.tgz", 718 | "integrity": "sha512-1p+1HwGvxGUVzVkFjNzglwHrLNA67U/axP0Ct85FzzH8yhGJb4t9jDjPYocVMzLorDoWAfKicGy1akPY9jXRVw==", 719 | "peerDependencies": { 720 | "ethers": "^5.1.4" 721 | } 722 | }, 723 | "node_modules/aes-js": { 724 | "version": "3.0.0", 725 | "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", 726 | "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", 727 | "peer": true 728 | }, 729 | "node_modules/bech32": { 730 | "version": "1.1.4", 731 | "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", 732 | "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", 733 | "peer": true 734 | }, 735 | "node_modules/bn.js": { 736 | "version": "5.2.1", 737 | "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", 738 | "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", 739 | "peer": true 740 | }, 741 | "node_modules/brorand": { 742 | "version": "1.1.0", 743 | "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", 744 | "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", 745 | "peer": true 746 | }, 747 | "node_modules/elliptic": { 748 | "version": "6.5.4", 749 | "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", 750 | "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", 751 | "peer": true, 752 | "dependencies": { 753 | "bn.js": "^4.11.9", 754 | "brorand": "^1.1.0", 755 | "hash.js": "^1.0.0", 756 | "hmac-drbg": "^1.0.1", 757 | "inherits": "^2.0.4", 758 | "minimalistic-assert": "^1.0.1", 759 | "minimalistic-crypto-utils": "^1.0.1" 760 | } 761 | }, 762 | "node_modules/elliptic/node_modules/bn.js": { 763 | "version": "4.12.0", 764 | "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", 765 | "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", 766 | "peer": true 767 | }, 768 | "node_modules/ethers": { 769 | "version": "5.7.2", 770 | "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", 771 | "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", 772 | "funding": [ 773 | { 774 | "type": "individual", 775 | "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" 776 | }, 777 | { 778 | "type": "individual", 779 | "url": "https://www.buymeacoffee.com/ricmoo" 780 | } 781 | ], 782 | "peer": true, 783 | "dependencies": { 784 | "@ethersproject/abi": "5.7.0", 785 | "@ethersproject/abstract-provider": "5.7.0", 786 | "@ethersproject/abstract-signer": "5.7.0", 787 | "@ethersproject/address": "5.7.0", 788 | "@ethersproject/base64": "5.7.0", 789 | "@ethersproject/basex": "5.7.0", 790 | "@ethersproject/bignumber": "5.7.0", 791 | "@ethersproject/bytes": "5.7.0", 792 | "@ethersproject/constants": "5.7.0", 793 | "@ethersproject/contracts": "5.7.0", 794 | "@ethersproject/hash": "5.7.0", 795 | "@ethersproject/hdnode": "5.7.0", 796 | "@ethersproject/json-wallets": "5.7.0", 797 | "@ethersproject/keccak256": "5.7.0", 798 | "@ethersproject/logger": "5.7.0", 799 | "@ethersproject/networks": "5.7.1", 800 | "@ethersproject/pbkdf2": "5.7.0", 801 | "@ethersproject/properties": "5.7.0", 802 | "@ethersproject/providers": "5.7.2", 803 | "@ethersproject/random": "5.7.0", 804 | "@ethersproject/rlp": "5.7.0", 805 | "@ethersproject/sha2": "5.7.0", 806 | "@ethersproject/signing-key": "5.7.0", 807 | "@ethersproject/solidity": "5.7.0", 808 | "@ethersproject/strings": "5.7.0", 809 | "@ethersproject/transactions": "5.7.0", 810 | "@ethersproject/units": "5.7.0", 811 | "@ethersproject/wallet": "5.7.0", 812 | "@ethersproject/web": "5.7.1", 813 | "@ethersproject/wordlists": "5.7.0" 814 | } 815 | }, 816 | "node_modules/hash.js": { 817 | "version": "1.1.7", 818 | "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", 819 | "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", 820 | "peer": true, 821 | "dependencies": { 822 | "inherits": "^2.0.3", 823 | "minimalistic-assert": "^1.0.1" 824 | } 825 | }, 826 | "node_modules/hmac-drbg": { 827 | "version": "1.0.1", 828 | "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", 829 | "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", 830 | "peer": true, 831 | "dependencies": { 832 | "hash.js": "^1.0.3", 833 | "minimalistic-assert": "^1.0.0", 834 | "minimalistic-crypto-utils": "^1.0.1" 835 | } 836 | }, 837 | "node_modules/inherits": { 838 | "version": "2.0.4", 839 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 840 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 841 | "peer": true 842 | }, 843 | "node_modules/js-sha3": { 844 | "version": "0.8.0", 845 | "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", 846 | "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", 847 | "peer": true 848 | }, 849 | "node_modules/minimalistic-assert": { 850 | "version": "1.0.1", 851 | "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", 852 | "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", 853 | "peer": true 854 | }, 855 | "node_modules/minimalistic-crypto-utils": { 856 | "version": "1.0.1", 857 | "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", 858 | "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", 859 | "peer": true 860 | }, 861 | "node_modules/scrypt-js": { 862 | "version": "3.0.1", 863 | "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", 864 | "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==", 865 | "peer": true 866 | }, 867 | "node_modules/ws": { 868 | "version": "7.4.6", 869 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", 870 | "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", 871 | "peer": true, 872 | "engines": { 873 | "node": ">=8.3.0" 874 | }, 875 | "peerDependencies": { 876 | "bufferutil": "^4.0.1", 877 | "utf-8-validate": "^5.0.2" 878 | }, 879 | "peerDependenciesMeta": { 880 | "bufferutil": { 881 | "optional": true 882 | }, 883 | "utf-8-validate": { 884 | "optional": true 885 | } 886 | } 887 | } 888 | } 889 | } 890 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ape-solidity-tests", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "@gnosis.pm/safe-contracts": "^1.3.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/scripts/clean.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from pathlib import Path 3 | 4 | 5 | def clean(): 6 | """ 7 | Delete all ``.cache/ and ``.build/`` folders in the project and local 8 | dependencies. 9 | """ 10 | project_path = Path(__file__).parent.parent 11 | for path in ( 12 | project_path, 13 | project_path / "BrownieProject", 14 | project_path / "Dependency", 15 | project_path / "DependencyOfDependency", 16 | project_path / "ProjectWithinProject", 17 | project_path / "VersionSpecifiedInConfig", 18 | ): 19 | for cache in (path / ".build", path / "contracts" / ".cache"): 20 | if cache.is_dir(): 21 | shutil.rmtree(cache) 22 | 23 | 24 | def main(): 25 | clean() 26 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from ape.utils import create_tempdir 4 | 5 | from ape_solidity._cli import cli 6 | 7 | EXPECTED_FLATTENED_CONTRACT = """ 8 | pragma solidity ^0.8.4; 9 | // SPDX-License-Identifier: MIT 10 | 11 | // File: ./././././././././././././././././././././././././././././././././././MissingPragma.sol 12 | 13 | contract MissingPragma { 14 | function foo() pure public returns(bool) { 15 | return true; 16 | } 17 | } 18 | // File: ./NumerousDefinitions.sol 19 | 20 | struct Struct0 { 21 | string name; 22 | uint value; 23 | } 24 | 25 | struct Struct1 { 26 | string name; 27 | uint value; 28 | } 29 | 30 | struct Struct2 { 31 | string name; 32 | uint value; 33 | } 34 | 35 | struct Struct3 { 36 | string name; 37 | uint value; 38 | } 39 | 40 | struct Struct4 { 41 | string name; 42 | uint value; 43 | } 44 | 45 | struct Struct5 { 46 | string name; 47 | uint value; 48 | } 49 | 50 | contract NumerousDefinitions { 51 | function foo() pure public returns(bool) { 52 | return true; 53 | } 54 | } 55 | // File: ./Source.extra.ext.sol 56 | 57 | // Showing sources with extra extensions are by default excluded, 58 | // unless used as an import somewhere in a non-excluded source. 59 | contract SourceExtraExt { 60 | function foo() pure public returns(bool) { 61 | return true; 62 | } 63 | } 64 | // File: ./safe/ThisIsNotGnosisSafe.sol 65 | // The point of this file is show we can have contracts in folders 66 | // where the folder is the same name as a dependency. 67 | 68 | contract ThisIsNotGnosisSafe { 69 | constructor(){ 70 | 71 | } 72 | } 73 | // File: ./subfolder/Relativecontract.sol 74 | 75 | contract Relativecontract { 76 | 77 | function foo() pure public returns(bool) { 78 | return true; 79 | } 80 | } 81 | // File: @browniedependency/contracts/BrownieContract.sol 82 | 83 | contract CompilingContract { 84 | function foo() pure public returns(bool) { 85 | return true; 86 | } 87 | } 88 | // File: @dependencyofdependency/contracts/DependencyOfDependency.sol 89 | 90 | contract DependencyOfDependency { 91 | function foo() pure public returns(bool) { 92 | return true; 93 | } 94 | } 95 | 96 | // File: @dependency/contracts/Dependency.sol 97 | 98 | struct DependencyStruct { 99 | string name; 100 | uint value; 101 | } 102 | 103 | contract Dependency { 104 | function foo() pure public returns(bool) { 105 | return true; 106 | } 107 | } 108 | // File: @noncompilingdependency/CompilingContract.sol 109 | 110 | contract BrownieStyleDependency { 111 | function foo() pure public returns(bool) { 112 | return true; 113 | } 114 | } 115 | // File: @noncompilingdependency/subdir/SubCompilingContract.sol 116 | 117 | contract SubCompilingContract { 118 | function foo() pure public returns(bool) { 119 | return true; 120 | } 121 | } 122 | // File: @safe/contracts/common/Enum.sol 123 | 124 | /// @title Enum - Collection of enums 125 | /// @author Richard Meissner - 126 | contract Enum { 127 | enum Operation {Call, DelegateCall} 128 | } 129 | // File: contracts/CompilesOnce.sol 130 | 131 | struct MyStruct { 132 | string name; 133 | uint value; 134 | } 135 | 136 | contract CompilesOnce { 137 | // This contract tests the scenario when we have a contract with 138 | // a similar compiler version to more than one other contract's. 139 | // This ensures we don't compile the same contract more than once. 140 | 141 | function foo() pure public returns(bool) { 142 | return true; 143 | } 144 | } 145 | 146 | // File: Imports.sol 147 | 148 | // Purposely repeat an import to test how the plugin handles that. 149 | 150 | // Show we can import a local contract with the same name (sin-@) as a dependency. 151 | 152 | // Purposely exclude the contracts folder to test older Ape-style project imports. 153 | 154 | // Showing sources with extra extensions are by default excluded, 155 | // unless used as an import somewhere in a non-excluded source. 156 | 157 | contract Imports { 158 | function foo() pure public returns(bool) { 159 | return true; 160 | } 161 | } 162 | """.strip() 163 | 164 | 165 | def test_cli_flatten(project, cli_runner): 166 | filename = "Imports.sol" 167 | path = project.contracts_folder / filename 168 | arguments = ["flatten", str(path)] 169 | end = ("--project", str(project.path)) 170 | with create_tempdir() as tmpdir: 171 | file = tmpdir / filename 172 | arguments.extend([str(file), *end]) 173 | result = cli_runner.invoke(cli, arguments, catch_exceptions=False) 174 | assert result.exit_code == 0, result.stderr_bytes 175 | output = file.read_text(encoding="utf8").strip() 176 | assert output == EXPECTED_FLATTENED_CONTRACT 177 | 178 | 179 | def test_compile(): 180 | """ 181 | Integration: Testing the CLI using an actual subprocess because 182 | it is the only way to test compiling the project such that it 183 | isn't treated as a tempdir project. 184 | """ 185 | # Use a couple contracts 186 | cmd_ls = ("ape", "compile", "subdir", "--force") 187 | completed_process = subprocess.run(cmd_ls, capture_output=True) 188 | output = completed_process.stdout.decode(encoding="utf8") 189 | assert completed_process.returncode == 0 190 | assert "SUCCESS" in output 191 | -------------------------------------------------------------------------------- /tests/test_compiler.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | import solcx 5 | from ape import Project, reverts 6 | from ape.exceptions import CompilerError 7 | from ape.utils import get_full_extension 8 | from ethpm_types import ContractType 9 | from packaging.version import Version 10 | 11 | from ape_solidity.exceptions import IndexOutOfBoundsError 12 | 13 | EXPECTED_NON_SOLIDITY_ERR_MSG = "Unable to compile 'RandomVyperFile.vy' using Solidity compiler." 14 | raises_because_not_sol = pytest.raises(CompilerError, match=EXPECTED_NON_SOLIDITY_ERR_MSG) 15 | 16 | 17 | def test_get_config(project, compiler): 18 | actual = compiler.get_config(project=project) 19 | assert actual.evm_version == "constantinople" 20 | 21 | 22 | def test_get_import_remapping(project, compiler): 23 | actual = compiler.get_import_remapping(project=project) 24 | expected = { 25 | "@browniedependency": "contracts/.cache/browniedependency/local", 26 | "@dependency": "contracts/.cache/dependency/local", 27 | "@dependencyofdependency": "contracts/.cache/dependencyofdependency/local", 28 | "@noncompilingdependency": "contracts/.cache/noncompilingdependency/local", 29 | "@openzeppelin": "contracts/.cache/openzeppelin/4.5.0", 30 | "@safe": "contracts/.cache/safe/1.3.0", 31 | "@vault": "contracts/.cache/vault/v0.4.5", 32 | "@vaultmain": "contracts/.cache/vaultmain/master", 33 | } 34 | for key, value in expected.items(): 35 | assert key in actual 36 | assert actual[key] == value 37 | 38 | 39 | def test_get_import_remapping_handles_config(project, compiler): 40 | """ 41 | Show you can override default remappings. 42 | Normally, these are deduced from dependencies, but you can change them 43 | and/or add new ones. 44 | """ 45 | new_value = "NEW_VALUE" 46 | cfg = { 47 | "solidity": { 48 | "import_remapping": [ 49 | "@dependency=dependency", # Backwards compat! 50 | "@dependencyofdependency=dependencyofdependency/local", 51 | f"@vaultmain={new_value}", # Changing a dependency 52 | f"@{new_value}={new_value}123", # Adding something new 53 | ] 54 | }, 55 | "dependencies": project.config.dependencies, 56 | } 57 | with project.temp_config(**cfg): 58 | actual = compiler.get_import_remapping(project=project) 59 | 60 | # Show it is backwards compatible (still works w/o changing cfg) 61 | assert actual["@dependency"] == "contracts/.cache/dependency/local" 62 | assert actual["@dependencyofdependency"] == "contracts/.cache/dependencyofdependency/local" 63 | # Show we can change a dependency. 64 | assert actual["@vaultmain"] == new_value 65 | # Show we can add a new remapping (quiet dependency). 66 | assert actual[f"@{new_value}"] == f"{new_value}123" 67 | # Show other dependency-deduced remappings still work. 68 | assert actual["@browniedependency"] == "contracts/.cache/browniedependency/local" 69 | 70 | # Clear remapping, to return to regular config values. 71 | compiler.__dict__.pop("_import_remapping_cache", None) 72 | 73 | 74 | def test_get_imports(project, compiler): 75 | source_id = "contracts/ImportSourceWithEqualSignVersion.sol" 76 | path = project.sources.lookup(source_id) 77 | # Source (total) only has these 2 imports. 78 | expected = ( 79 | "contracts/SpecificVersionWithEqualSign.sol", 80 | "contracts/CompilesOnce.sol", 81 | ) 82 | actual = compiler.get_imports((path,), project=project) 83 | assert source_id in actual 84 | assert all(e in actual[source_id] for e in expected) 85 | 86 | 87 | def test_get_imports_indirect(project, compiler): 88 | """ 89 | Show that twice-removed indirect imports show up. This is required 90 | for accurate version mapping. 91 | """ 92 | 93 | source_id = "contracts/IndirectlyImportingMoreConstrainedVersion.sol" 94 | path = project.sources.lookup(source_id) 95 | expected = ( 96 | # These 2 are directly imported. 97 | "contracts/ImportSourceWithEqualSignVersion.sol", 98 | "contracts/IndirectlyImportingMoreConstrainedVersionCompanion.sol", 99 | # These are 2 are imported by the imported. 100 | "contracts/SpecificVersionWithEqualSign.sol", 101 | "contracts/CompilesOnce.sol", 102 | ) 103 | actual = compiler.get_imports((path,), project=project) 104 | assert source_id in actual 105 | actual_str = ", ".join(list(actual[source_id])) 106 | for ex in expected: 107 | assert ex in actual[source_id], f"{ex} WAS NOT found in {actual_str}" 108 | 109 | 110 | def test_get_imports_complex(project, compiler): 111 | """ 112 | `contracts/Imports.sol` imports sources in every possible 113 | way. This test shows that we are able to detect all those 114 | unique ways of importing. 115 | """ 116 | path = project.sources.lookup("contracts/Imports.sol") 117 | assert path is not None, "Failed to find Imports test contract." 118 | 119 | actual = compiler.get_imports((path,), project=project) 120 | expected = { 121 | "contracts/CompilesOnce.sol": [], 122 | "contracts/Imports.sol": [ 123 | "contracts/.cache/browniedependency/local/contracts/BrownieContract.sol", 124 | "contracts/.cache/dependency/local/contracts/Dependency.sol", 125 | "contracts/.cache/dependencyofdependency/local/contracts/DependencyOfDependency.sol", 126 | "contracts/.cache/noncompilingdependency/local/contracts/CompilingContract.sol", 127 | "contracts/.cache/noncompilingdependency/local/contracts/subdir/SubCompilingContract.sol", # noqa: E501 128 | "contracts/.cache/safe/1.3.0/contracts/common/Enum.sol", 129 | "contracts/CompilesOnce.sol", 130 | "contracts/MissingPragma.sol", 131 | "contracts/NumerousDefinitions.sol", 132 | "contracts/Source.extra.ext.sol", 133 | "contracts/safe/ThisIsNotGnosisSafe.sol", 134 | "contracts/subfolder/Relativecontract.sol", 135 | ], 136 | "contracts/MissingPragma.sol": [], 137 | "contracts/NumerousDefinitions.sol": [], 138 | "contracts/subfolder/Relativecontract.sol": [], 139 | } 140 | for base, imports in expected.items(): 141 | assert base in actual 142 | assert actual[base] == imports 143 | 144 | 145 | def test_get_imports_dependencies(project, compiler): 146 | """ 147 | Show all the affected dependency contracts get included in the imports list. 148 | """ 149 | source_id = "contracts/UseYearn.sol" 150 | path = project.sources.lookup(source_id) 151 | import_ls = compiler.get_imports((path,), project=project) 152 | actual = import_ls[source_id] 153 | 154 | # NOTE: Both Yearn-vaults master branch and yearn-vaults 0.4.5 155 | # use OpenZeppelin 4.7.1. However, the root project for these 156 | # tests uses OpenZeppelin 4.5.0. This proves we are handling 157 | # dependencies-of-dependencies correctly. 158 | 159 | token_path = "contracts/.cache/openzeppelin/4.7.1/contracts/token" 160 | expected = [ 161 | f"{token_path}/ERC20/ERC20.sol", 162 | f"{token_path}/ERC20/IERC20.sol", 163 | f"{token_path}/ERC20/extensions/IERC20Metadata.sol", 164 | f"{token_path}/ERC20/extensions/draft-IERC20Permit.sol", 165 | f"{token_path}/ERC20/utils/SafeERC20.sol", 166 | "contracts/.cache/openzeppelin/4.7.1/contracts/utils/Address.sol", 167 | "contracts/.cache/openzeppelin/4.7.1/contracts/utils/Context.sol", 168 | "contracts/.cache/vault/v0.4.5/contracts/BaseStrategy.sol", 169 | "contracts/.cache/vaultmain/master/contracts/BaseStrategy.sol", 170 | ] 171 | assert actual == expected 172 | 173 | 174 | def test_get_imports_vyper_file(project, compiler): 175 | path = Path(__file__).parent / "contracts" / "RandomVyperFile.vy" 176 | assert path.is_file(), f"Setup failed - file not found {path}" 177 | with raises_because_not_sol: 178 | compiler.get_imports((path,)) 179 | 180 | 181 | def test_get_imports_full_project(project, compiler): 182 | paths = [x for x in project.sources.paths if x.suffix == ".sol"] 183 | actual = compiler.get_imports(paths, project=project) 184 | assert len(actual) > 0 185 | # Prove that every import source also is present in the import map. 186 | for imported_source_ids in actual.values(): 187 | for source_id in imported_source_ids: 188 | assert source_id in actual, f"{source_id}'s imports not present." 189 | 190 | 191 | def test_get_version_map(project, compiler): 192 | """ 193 | Test that a strict version pragma is recognized in the version map. 194 | """ 195 | path = project.sources.lookup("contracts/SpecificVersionWithEqualSign.sol") 196 | actual = compiler.get_version_map((path,), project=project) 197 | expected_version = Version("0.8.12+commit.f00d7308") 198 | expected_sources = ("SpecificVersionWithEqualSign",) 199 | assert expected_version in actual 200 | 201 | actual_ids = [x.stem for x in actual[expected_version]] 202 | assert all(e in actual_ids for e in expected_sources) 203 | 204 | 205 | def test_get_version_map_importing_more_constrained_version(project, compiler): 206 | """ 207 | Test that a strict version pragma in an imported source is recognized 208 | in the version map. 209 | """ 210 | # This file's version is not super constrained, but it imports 211 | # a different source that does have a strict constraint. 212 | path = project.sources.lookup("contracts/ImportSourceWithEqualSignVersion.sol") 213 | 214 | actual = compiler.get_version_map((path,), project=project) 215 | expected_version = Version("0.8.12+commit.f00d7308") 216 | expected_sources = ("ImportSourceWithEqualSignVersion", "SpecificVersionWithEqualSign") 217 | assert expected_version in actual 218 | 219 | actual_ids = [x.stem for x in actual[expected_version]] 220 | assert all(e in actual_ids for e in expected_sources) 221 | 222 | 223 | def test_get_version_map_indirectly_importing_more_constrained_version(project, compiler): 224 | """ 225 | Test that a strict version pragma in a source imported by an imported 226 | source (twice removed) is recognized in the version map. 227 | """ 228 | # This file's version is not super constrained, but it imports 229 | # a different source that imports another source that does have a constraint. 230 | path = project.sources.lookup("contracts/IndirectlyImportingMoreConstrainedVersion.sol") 231 | 232 | actual = compiler.get_version_map((path,), project=project) 233 | expected_version = Version("0.8.12+commit.f00d7308") 234 | expected_sources = ( 235 | "IndirectlyImportingMoreConstrainedVersion", 236 | "ImportSourceWithEqualSignVersion", 237 | "SpecificVersionWithEqualSign", 238 | ) 239 | assert expected_version in actual 240 | 241 | actual_ids = [x.stem for x in actual[expected_version]] 242 | assert all(e in actual_ids for e in expected_sources) 243 | 244 | 245 | def test_get_version_map_dependencies(project, compiler): 246 | """ 247 | Show all the affected dependency contracts get included in the version map. 248 | """ 249 | source_id = "contracts/UseYearn.sol" 250 | older_example = "contracts/ImportOlderDependency.sol" 251 | paths = [project.sources.lookup(x) for x in (source_id, older_example)] 252 | actual = compiler.get_version_map(paths, project=project) 253 | 254 | fail_msg = f"versions: {', '.join([str(x) for x in actual])}" 255 | actual_len = len(actual) 256 | 257 | # Expecting one old version for ImportOlderDependency and one version for Yearn stuff. 258 | expected_len = 2 259 | 260 | if actual_len > expected_len: 261 | # Weird anomaly in CI/CD tests sometimes (at least at the time of write). 262 | # Including additional debug information. 263 | alt_map: dict = {} 264 | for version, src_ids in actual.items(): 265 | for src_id in src_ids: 266 | if src_id in alt_map: 267 | other_version = alt_map[src_id] 268 | versions_str = ", ".join([str(other_version), str(version)]) 269 | pytest.fail(f"{src_id} in multiple version '{versions_str}'") 270 | else: 271 | alt_map[src_id] = version 272 | 273 | # No duplicated versions found but still have unexpected extras. 274 | pytest.fail(f"Unexpected number of versions. {fail_msg}") 275 | 276 | elif actual_len < expected_len: 277 | pytest.fail(fail_msg) 278 | 279 | versions = sorted(list(actual.keys())) 280 | older = versions[0] # Via ImportOlderDependency 281 | latest = versions[1] # via UseYearn 282 | 283 | oz_token = "contracts/.cache/openzeppelin/4.7.1/contracts/token" 284 | expected_latest_source_ids = [ 285 | f"{oz_token}/ERC20/ERC20.sol", 286 | f"{oz_token}/ERC20/IERC20.sol", 287 | f"{oz_token}/ERC20/extensions/IERC20Metadata.sol", 288 | f"{oz_token}/ERC20/extensions/draft-IERC20Permit.sol", 289 | f"{oz_token}/ERC20/utils/SafeERC20.sol", 290 | "contracts/.cache/openzeppelin/4.7.1/contracts/utils/Address.sol", 291 | "contracts/.cache/openzeppelin/4.7.1/contracts/utils/Context.sol", 292 | "contracts/.cache/vault/v0.4.5/contracts/BaseStrategy.sol", 293 | "contracts/.cache/vaultmain/master/contracts/BaseStrategy.sol", 294 | source_id, 295 | ] 296 | expected_older_source_ids = [ 297 | "contracts/.cache/dependency/local/contracts/OlderDependency.sol", 298 | older_example, 299 | ] 300 | expected_latest_source_paths = {project.path / e for e in expected_latest_source_ids} 301 | expected_oldest_source_paths = {project.path / e for e in expected_older_source_ids} 302 | assert len(actual[latest]) == len(expected_latest_source_paths) 303 | assert actual[latest] == expected_latest_source_paths 304 | assert actual[older] == expected_oldest_source_paths 305 | 306 | 307 | def test_get_version_map_picks_most_constrained_version(project, compiler): 308 | """ 309 | Test that if given both a file that can compile at the latest version 310 | and a file that requires a lesser version but also imports the same file 311 | that could compile at the latest version, that they are all designated 312 | to compile using the lesser version. 313 | """ 314 | source_ids = ( 315 | "contracts/CompilesOnce.sol", 316 | "contracts/IndirectlyImportingMoreConstrainedVersion.sol", 317 | ) 318 | paths = [project.sources.lookup(x) for x in source_ids] 319 | actual = compiler.get_version_map(paths, project=project) 320 | expected_version = Version("0.8.12+commit.f00d7308") 321 | assert expected_version in actual 322 | for path in paths: 323 | assert path in actual[expected_version], f"{path} is missing!" 324 | 325 | 326 | def test_get_version_map_version_specified_in_config_file(compiler): 327 | path = Path(__file__).parent / "VersionSpecifiedInConfig" 328 | project = Project(path) 329 | paths = [p for p in project.sources.paths if p.suffix == ".sol"] 330 | actual = compiler.get_version_map(paths, project=project) 331 | expected_version = Version("0.8.12+commit.f00d7308") 332 | assert len(actual) == 1 333 | assert expected_version in actual 334 | assert len(actual[expected_version]) > 0 335 | 336 | 337 | def test_get_version_map_raises_on_non_solidity_sources(project, compiler): 338 | path = project.contracts_folder / "RandomVyperFile.vy" 339 | with raises_because_not_sol: 340 | compiler.get_version_map((path,), project=project) 341 | 342 | 343 | def test_get_version_map_full_project(project, compiler): 344 | paths = [x for x in project.sources.paths if x.suffix == ".sol"] 345 | actual = compiler.get_version_map(paths, project=project) 346 | latest = sorted(list(actual.keys()), reverse=True)[0] 347 | v0812 = Version("0.8.12+commit.f00d7308") 348 | vold = Version("0.4.26+commit.4563c3fc") 349 | assert v0812 in actual 350 | assert vold in actual 351 | 352 | v0812_1 = project.path / "contracts/ImportSourceWithEqualSignVersion.sol" 353 | v0812_2 = project.path / "contracts/IndirectlyImportingMoreConstrainedVersion.sol" 354 | 355 | assert v0812_1 in actual[v0812], "Constrained version files missing" 356 | assert v0812_2 in actual[v0812], "Constrained version files missing" 357 | 358 | # TDD: This was happening during development of 0.8.0. 359 | assert v0812_1 not in actual[latest], f"{v0812_1.stem} ended up in latest" 360 | assert v0812_2 not in actual[latest], f"{v0812_2.stem} ended up in latest" 361 | 362 | # TDD: Old file ending up in multiple spots. 363 | older_file = project.path / "contracts/.cache/dependency/local/contracts/OlderDependency.sol" 364 | assert older_file in actual[vold] 365 | for vers, fileset in actual.items(): 366 | if vers == vold: 367 | continue 368 | 369 | assert older_file not in fileset, f"Oldest file also appears in version {vers}" 370 | 371 | 372 | def test_get_compiler_settings(project, compiler): 373 | path = project.sources.lookup("contracts/Imports.sol") 374 | 375 | # Set this setting using an adhoc approach. 376 | compiler.compiler_settings["optimization_runs"] = 190 377 | 378 | actual = compiler.get_compiler_settings((path,), project=project) 379 | # No reason (when alone) to not use 380 | assert len(actual) == 1 381 | 382 | # 0.8.12 is hardcoded in some files, but none of those files should be here. 383 | version = next(iter(actual.keys())) 384 | assert version > Version("0.8.12+commit.f00d7308") 385 | 386 | settings = actual[version] 387 | assert settings["optimizer"] == {"enabled": True, "runs": 190} 388 | 389 | # NOTE: These should be sorted! 390 | expected_remapping = [ 391 | "@browniedependency=contracts/.cache/browniedependency/local", 392 | "@dependency=contracts/.cache/dependency/local", 393 | "@dependencyofdependency=contracts/.cache/dependencyofdependency/local", 394 | # This remapping below was auto-corrected because imports were excluding contracts/ suffix. 395 | "@noncompilingdependency=contracts/.cache/noncompilingdependency/local/contracts", 396 | "@safe=contracts/.cache/safe/1.3.0", 397 | ] 398 | assert settings["remappings"] == expected_remapping 399 | 400 | # Set in config. 401 | assert settings["evmVersion"] == "constantinople" 402 | 403 | # Should be all files (imports of imports etc.) 404 | actual_files = sorted(list(settings["outputSelection"].keys())) 405 | expected_files = [ 406 | "contracts/.cache/browniedependency/local/contracts/BrownieContract.sol", 407 | "contracts/.cache/dependency/local/contracts/Dependency.sol", 408 | "contracts/.cache/dependencyofdependency/local/contracts/DependencyOfDependency.sol", 409 | "contracts/.cache/noncompilingdependency/local/contracts/CompilingContract.sol", 410 | "contracts/.cache/noncompilingdependency/local/contracts/subdir/SubCompilingContract.sol", 411 | "contracts/.cache/safe/1.3.0/contracts/common/Enum.sol", 412 | "contracts/CompilesOnce.sol", 413 | "contracts/Imports.sol", 414 | "contracts/MissingPragma.sol", 415 | "contracts/NumerousDefinitions.sol", 416 | "contracts/Source.extra.ext.sol", 417 | "contracts/safe/ThisIsNotGnosisSafe.sol", 418 | "contracts/subfolder/Relativecontract.sol", 419 | ] 420 | assert actual_files == expected_files 421 | 422 | # Output request is the same for all. 423 | expected_output_request = { 424 | "*": [ 425 | "abi", 426 | "bin-runtime", 427 | "devdoc", 428 | "userdoc", 429 | "evm.bytecode.object", 430 | "evm.bytecode.sourceMap", 431 | "evm.deployedBytecode.object", 432 | ], 433 | "": ["ast"], 434 | } 435 | for output in settings["outputSelection"].values(): 436 | assert output == expected_output_request 437 | 438 | 439 | def test_get_standard_input_json(project, compiler): 440 | paths = [x for x in project.sources.paths if x.suffix == ".sol"] 441 | actual = compiler.get_standard_input_json(paths, project=project) 442 | v0812 = Version("0.8.12+commit.f00d7308") 443 | v056 = Version("0.5.16+commit.9c3226ce") 444 | v0426 = Version("0.4.26+commit.4563c3fc") 445 | latest = sorted(list(actual.keys()), reverse=True)[0] 446 | 447 | fail_msg = f"Versions: {', '.join([str(v) for v in actual])}" 448 | assert v0812 in actual, fail_msg 449 | assert v056 in actual, fail_msg 450 | assert v0426 in actual, fail_msg 451 | assert latest in actual, fail_msg 452 | 453 | v0812_sources = list(actual[v0812]["sources"].keys()) 454 | v056_sources = list(actual[v056]["sources"].keys()) 455 | v0426_sources = list(actual[v0426]["sources"].keys()) 456 | latest_sources = list(actual[latest]["sources"].keys()) 457 | 458 | assert "contracts/ImportSourceWithEqualSignVersion.sol" not in latest_sources 459 | assert "contracts/IndirectlyImportingMoreConstrainedVersion.sol" not in latest_sources 460 | 461 | # Some source expectations. 462 | assert "contracts/CompilesOnce.sol" in v0812_sources 463 | assert "contracts/SpecificVersionRange.sol" in v0812_sources 464 | assert "contracts/ImportSourceWithNoPrefixVersion.sol" in v0812_sources 465 | 466 | assert "contracts/OlderVersion.sol" in v056_sources 467 | assert "contracts/ImportOlderDependency.sol" in v0426_sources 468 | assert "contracts/.cache/dependency/local/contracts/OlderDependency.sol" in v0426_sources 469 | 470 | 471 | def test_compile(project, compiler): 472 | path = project.sources.lookup("contracts/Imports.sol") 473 | actual = [c for c in compiler.compile((path,), project=project)] 474 | # We only get back the contracts we requested, even if it had to compile 475 | # others (like imports) to get it to work. 476 | assert len(actual) == 1 477 | assert isinstance(actual[0], ContractType) 478 | assert actual[0].name == "Imports" 479 | assert actual[0].source_id == "contracts/Imports.sol" 480 | assert actual[0].deployment_bytecode is not None 481 | assert actual[0].runtime_bytecode is not None 482 | assert len(actual[0].abi) > 0 483 | 484 | 485 | def test_compile_performance(benchmark, compiler, project): 486 | """ 487 | See https://pytest-benchmark.readthedocs.io/en/latest/ 488 | """ 489 | path = project.sources.lookup("contracts/MultipleDefinitions.sol") 490 | result = benchmark.pedantic( 491 | lambda *args, **kwargs: [x for x in compiler.compile(*args, **kwargs)], 492 | args=((path,),), 493 | kwargs={"project": project}, 494 | rounds=1, 495 | warmup_rounds=1, 496 | ) 497 | assert len(result) > 0 498 | 499 | # Currently seeing '~0.08; on macOS locally. 500 | # Was seeing '~0.68' before https://github.com/ApeWorX/ape-solidity/pull/151 501 | threshold = 0.2 502 | 503 | assert benchmark.stats["median"] < threshold 504 | 505 | 506 | def test_compile_multiple_definitions_in_source(project, compiler): 507 | """ 508 | Show that if multiple contracts / interfaces are defined in a single 509 | source, that we get all of them when compiling. 510 | """ 511 | source_id = "contracts/MultipleDefinitions.sol" 512 | path = project.sources.lookup(source_id) 513 | result = [c for c in compiler.compile((path,), project=project)] 514 | assert len(result) == 2 515 | assert [r.name for r in result] == ["IMultipleDefinitions", "MultipleDefinitions"] 516 | assert all(r.source_id == source_id for r in result) 517 | 518 | assert project.MultipleDefinitions 519 | assert project.IMultipleDefinitions 520 | 521 | 522 | def test_compile_contract_with_different_name_than_file(project, compiler): 523 | source_id = "contracts/DifferentNameThanFile.sol" 524 | path = project.sources.lookup(source_id) 525 | actual = [c for c in compiler.compile((path,), project=project)] 526 | assert len(actual) == 1 527 | assert actual[0].source_id == source_id 528 | 529 | 530 | def test_compile_only_returns_contract_types_for_inputs(project, compiler): 531 | """ 532 | Test showing only the requested contract types get returned. 533 | """ 534 | path = project.sources.lookup("contracts/Imports.sol") 535 | contract_types = [c for c in compiler.compile((path,), project=project)] 536 | assert len(contract_types) == 1 537 | assert contract_types[0].name == "Imports" 538 | 539 | 540 | def test_compile_vyper_contract(project, compiler): 541 | path = project.contracts_folder / "RandomVyperFile.vy" 542 | with raises_because_not_sol: 543 | _ = [c for c in compiler.compile((path,), project=project)] 544 | 545 | 546 | def test_compile_just_a_struct(compiler, project): 547 | """ 548 | Before, you would get a nasty index error, even though this is valid Solidity. 549 | The fix involved using nicer access to "contracts" in the standard output JSON. 550 | """ 551 | path = project.sources.lookup("contracts/JustAStruct.sol") 552 | contract_types = [c for c in compiler.compile((path,), project=project)] 553 | assert len(contract_types) == 0 554 | 555 | 556 | def test_compile_produces_source_map(project, compiler): 557 | path = project.sources.lookup("contracts/MultipleDefinitions.sol") 558 | result = [c for c in compiler.compile((path,), project=project)][-1] 559 | assert result.sourcemap.root == "124:87:0:-:0;;;;;;;;;;;;;;;;;;;" 560 | 561 | 562 | def test_compile_via_ir(project, compiler): 563 | path = project.contracts_folder / "StackTooDep.sol" 564 | source_code = """ 565 | // SPDX-License-Identifier: MIT 566 | 567 | pragma solidity >=0.8.0; 568 | 569 | contract StackTooDeep { 570 | // This contract tests the scenario when we have a contract with 571 | // too many local variables and the stack is too deep. 572 | // The compiler will throw an error when trying to compile this contract. 573 | // To get around the error, we can compile the contract with the 574 | // --via-ir flag 575 | 576 | function foo( 577 | uint256 a, 578 | uint256 b, 579 | uint256 c, 580 | uint256 d, 581 | uint256 e, 582 | uint256 f, 583 | uint256 g, 584 | uint256 h, 585 | uint256 i, 586 | uint256 j, 587 | uint256 k, 588 | uint256 l, 589 | uint256 m, 590 | uint256 n, 591 | uint256 o, 592 | uint256 p 593 | ) public pure returns (uint256) { 594 | 595 | uint256 sum = 0; 596 | 597 | for (uint256 index = 0; index < 16; index++) { 598 | uint256 innerSum = a + b + c + d + e + f + g + h + i + j + k + l + m + n + o + p; 599 | sum += innerSum; 600 | } 601 | 602 | return (sum); 603 | } 604 | 605 | } 606 | """ 607 | 608 | # write source code to file 609 | path.write_text(source_code) 610 | 611 | try: 612 | [c for c in compiler.compile((path,), project=project)] 613 | except Exception as e: 614 | assert "Stack too deep" in str(e) 615 | 616 | with project.temp_config(solidity={"via_ir": True}): 617 | _ = [c for c in compiler.compile((path,), project=project)] 618 | 619 | # delete source code file 620 | path.unlink() 621 | 622 | 623 | @pytest.mark.install 624 | def test_installs_from_compile(project, compiler, temp_solcx_path): 625 | """ 626 | Test the compilation of a contract with no defined pragma spec. 627 | 628 | The plugin should implicitly download the latest version to compile the 629 | contract with. `temp_solcx_path` is used to simulate an environment without 630 | compilers installed. 631 | """ 632 | assert not solcx.get_installed_solc_versions() 633 | path = project.sources.lookup("contracts/MissingPragma.sol") 634 | contract_types = [c for c in compiler.compile((path,), project=project)] 635 | assert len(contract_types) == 1 636 | installed_versions = solcx.get_installed_solc_versions() 637 | assert len(installed_versions) == 1 638 | assert installed_versions[0] == max(solcx.get_installable_solc_versions()) 639 | 640 | 641 | def test_compile_project(project, compiler): 642 | """ 643 | Simple test showing the full project indeed compiles. 644 | """ 645 | paths = [x for x in project.sources.paths if get_full_extension(x) == ".sol"] 646 | actual = [c for c in compiler.compile(paths, project=project)] 647 | assert len(actual) > 0 648 | 649 | 650 | def test_compile_outputs_compiler_data_to_manifest(project, compiler): 651 | project.update_manifest(compilers=[]) 652 | path = project.sources.lookup("contracts/CompilesOnce.sol") 653 | _ = [c for c in compiler.compile((path,), project=project)] 654 | assert len(project.manifest.compilers or []) == 1 655 | actual = project.manifest.compilers[0] 656 | assert actual.name == "solidity" 657 | assert "CompilesOnce" in actual.contractTypes 658 | assert actual.version == "0.8.28+commit.7893614a" 659 | # Compiling again should not add the same compiler again. 660 | _ = [c for c in compiler.compile((path,), project=project)] 661 | length_again = len(project.manifest.compilers or []) 662 | assert length_again == 1 663 | 664 | 665 | def test_add_library(project, account, compiler, connection): 666 | # Does not exist yet because library is not deployed or known. 667 | with pytest.raises(AttributeError): 668 | _ = project.ContractUsingLibraryInSameSource 669 | with pytest.raises(AttributeError): 670 | _ = project.ContractUsingLibraryNotInSameSource 671 | 672 | library = project.ExampleLibrary.deploy(sender=account) 673 | compiler.add_library(library, project=project) 674 | 675 | # After deploying and adding the library, we can use contracts that need it. 676 | assert project.ContractUsingLibraryNotInSameSource 677 | assert project.ContractUsingLibraryInSameSource 678 | 679 | 680 | def test_enrich_error_when_custom(compiler, project, owner, not_owner, connection): 681 | path = project.sources.lookup("contracts/HasError.sol") 682 | _ = [c for c in compiler.compile((path,), project=project)] 683 | 684 | # Deploy so Ape know about contract type. 685 | contract = owner.deploy(project.HasError, 1) 686 | with pytest.raises(contract.Unauthorized) as err: 687 | contract.withdraw(sender=not_owner) 688 | 689 | assert err.value.inputs == {"addr": not_owner.address, "counter": 123} 690 | 691 | 692 | def test_enrich_error_when_custom_in_constructor(compiler, project, owner, not_owner, connection): 693 | # Deploy so Ape know about contract type. 694 | with reverts(project.HasError.Unauthorized) as err: 695 | not_owner.deploy(project.HasError, 0) 696 | 697 | assert err.value.inputs == {"addr": not_owner.address, "counter": 123} 698 | 699 | 700 | def test_enrich_error_when_builtin(project, owner, connection): 701 | contract = project.BuiltinErrorChecker.deploy(sender=owner) 702 | with pytest.raises(IndexOutOfBoundsError): 703 | contract.checkIndexOutOfBounds(sender=owner) 704 | 705 | 706 | def test_flatten(mocker, project, compiler): 707 | path = project.contracts_folder / "Imports.sol" 708 | base_expected = Path(__file__).parent / "data" 709 | 710 | # NOTE: caplog for some reason is inconsistent and causes flakey tests. 711 | # Thus, we are using our own "logger_spy". 712 | logger_spy = mocker.patch("ape_solidity.compiler.logger") 713 | 714 | res = compiler.flatten_contract(path, project=project) 715 | call_args = logger_spy.warning.call_args 716 | actual_logs = call_args[0] if call_args else () 717 | assert actual_logs, f"Missing warning logs from dup-licenses, res: {res}" 718 | actual = actual_logs[-1] 719 | # NOTE: MIT coming from Imports.sol and LGPL-3.0-only coming from 720 | # @safe/contracts/common/Enum.sol. 721 | expected = ( 722 | "Conflicting licenses found: 'LGPL-3.0-only, MIT'. Using the root file's license 'MIT'." 723 | ) 724 | assert actual == expected 725 | 726 | path = project.contracts_folder / "ImportingLessConstrainedVersion.sol" 727 | flattened_source = compiler.flatten_contract(path, project=project) 728 | flattened_source_path = base_expected / "ImportingLessConstrainedVersionFlat.sol" 729 | 730 | actual = str(flattened_source) 731 | expected = str(flattened_source_path.read_text(encoding="utf8")) 732 | assert actual == expected 733 | 734 | 735 | def test_compile_code(project, compiler): 736 | code = """ 737 | contract Contract { 738 | function snakes() pure public returns(bool) { 739 | return true; 740 | } 741 | } 742 | """ 743 | actual = compiler.compile_code(code, project=project, contractName="TestContractName") 744 | assert actual.name == "TestContractName" 745 | assert len(actual.abi) > 0 746 | assert actual.ast is not None 747 | assert len(actual.runtime_bytecode.bytecode) > 0 748 | assert len(actual.deployment_bytecode.bytecode) > 0 749 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ape.logging import logger 3 | from solcx.exceptions import SolcError 4 | 5 | from ape_solidity.exceptions import SolcCompileError 6 | 7 | MESSAGE = "__message__" 8 | COMMAND = ["solc", "command"] 9 | RETURN_CODE = 123 10 | STDOUT_DATA = "" 11 | STDERR_DATA = "" 12 | 13 | 14 | @pytest.fixture(scope="module") 15 | def solc_error(): 16 | return SolcError( 17 | message=MESSAGE, 18 | command=COMMAND, 19 | return_code=RETURN_CODE, 20 | stdout_data=STDOUT_DATA, 21 | stderr_data=STDERR_DATA, 22 | ) 23 | 24 | 25 | def test_solc_compile_error(solc_error): 26 | error = SolcCompileError(solc_error) 27 | actual = str(error) 28 | assert MESSAGE in actual 29 | assert f"{RETURN_CODE}" not in actual 30 | assert " ".join(COMMAND) not in actual 31 | assert STDOUT_DATA not in actual 32 | assert STDERR_DATA not in actual 33 | 34 | 35 | def test_solc_compile_error_verbose(solc_error): 36 | logger.set_level("DEBUG") 37 | error = SolcCompileError(solc_error) 38 | actual = str(error) 39 | assert MESSAGE in actual 40 | assert f"{RETURN_CODE}" in actual 41 | assert " ".join(COMMAND) in actual 42 | assert STDOUT_DATA in actual 43 | assert STDERR_DATA in actual 44 | logger.set_level("INFO") 45 | -------------------------------------------------------------------------------- /tests/test_integration.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ape._cli import cli 3 | from click.testing import CliRunner 4 | 5 | 6 | @pytest.fixture 7 | def ape_cli(): 8 | return cli 9 | 10 | 11 | @pytest.fixture 12 | def runner(): 13 | return CliRunner() 14 | 15 | 16 | def test_compile_using_cli(ape_cli, runner, project): 17 | arguments = ["compile", "--project", f"{project.path}"] 18 | result = runner.invoke(ape_cli, [*arguments, "--force"], catch_exceptions=False) 19 | assert result.exit_code == 0 20 | assert "CompilesOnce" in result.output 21 | result = runner.invoke(ape_cli, arguments, catch_exceptions=False) 22 | 23 | # Already compiled so does not compile again. 24 | assert "CompilesOnce" not in result.output 25 | 26 | 27 | @pytest.mark.parametrize( 28 | "contract_path", 29 | ( 30 | "CompilesOnce", 31 | "CompilesOnce.sol", 32 | "contracts/CompilesOnce", 33 | "contracts/CompilesOnce.sol", 34 | ), 35 | ) 36 | def test_compile_specified_contracts(ape_cli, runner, contract_path, project): 37 | arguments = ("compile", "--project", f"{project.path}", contract_path, "--force") 38 | result = runner.invoke(ape_cli, arguments, catch_exceptions=False) 39 | assert result.exit_code == 0, result.output 40 | assert "contracts/CompilesOnce.sol" in result.output, f"Failed to compile {contract_path}." 41 | --------------------------------------------------------------------------------