Repository: EthTx/ethtx_ce Branch: master Commit: f1f36fd5f447 Files: 52 Total size: 148.4 KB Directory structure: gitextract_udd85lnu/ ├── .dockerignore ├── .env_sample ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── DEVELOPMENT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── NOTICE ├── Pipfile ├── README.md ├── docker-compose.override.yml ├── docker-compose.yaml ├── ethtx_ce/ │ ├── .flake8 │ ├── .gitignore │ ├── app/ │ │ ├── __init__.py │ │ ├── api/ │ │ │ ├── __init__.py │ │ │ ├── decorators.py │ │ │ ├── endpoints/ │ │ │ │ ├── __init__.py │ │ │ │ ├── info.py │ │ │ │ ├── semantics.py │ │ │ │ └── transactions.py │ │ │ ├── exceptions.py │ │ │ └── utils.py │ │ ├── config.py │ │ ├── exceptions.py │ │ ├── factory.py │ │ ├── frontend/ │ │ │ ├── __init__.py │ │ │ ├── deps.py │ │ │ ├── exceptions.py │ │ │ ├── semantics.py │ │ │ ├── static/ │ │ │ │ └── ethtx.new.css │ │ │ ├── static.py │ │ │ ├── templates/ │ │ │ │ ├── exception.html │ │ │ │ ├── index.html │ │ │ │ ├── partials/ │ │ │ │ │ └── headtags.html │ │ │ │ ├── semantics.html │ │ │ │ └── transaction.html │ │ │ └── transactions.py │ │ ├── helpers.py │ │ ├── logger.py │ │ └── wsgi.py │ ├── entrypoint.sh │ ├── gunicorn_conf.py │ ├── log_cfg.json │ ├── start-reload.sh │ ├── start.sh │ └── tests/ │ ├── flask_test.py │ └── mocks/ │ ├── __init__.py │ └── mocks.py └── scripts/ └── git_version_for_docker.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ .git .github .gitignore .pre-commit* .env .docker_env .env_sample docker-compose.yaml ================================================ FILE: .env_sample ================================================ # Proper nodes are required to run ethtx, provide connection strings for chains which will be used. MAINNET_NODE_URL=https://geth-erigon-node:8545 # KOVAN_NODE_URL= # RINKEBY_NODE_URL= # EthTx supports multiple nodes, if one is unavailable, it will use others. You only need to specify them with a comma. # Example: MAINNET_NODE_URL=https://geth-erigon-node:8545,https://geth1-erigon-node:8545 # Etherscan API is used to get contract source code, required for decoding process # You can get free key here https://etherscan.io/apis ETHERSCAN_KEY= # Optional. Those represent data required for connecting to mongoDB. It's used for caching semantics # used in decoding process. But, it's not neccessary for running, If you don't want to use permanent # db or setup mongo, leave those values, mongomock package is used to simulate in-memory mongo. MONGO_CONNECTION_STRING=mongomock://localhost/ethtx # Optional. Credentials for accessing semantics editor page, available under '/semantics/' ETHTX_ADMIN_USERNAME=admin ETHTX_ADMIN_PASSWORD=admin # Optional. Api key used for securing decoding API API_KEY= # Optional. Valid values are ['production', 'staging', 'development']. Those mainly # dictate what options are used for flask debugging and logging ENV=development ================================================ FILE: .gitignore ================================================ # Ignore pipenv's lock Pipfile.lock # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Editors .idea .vscode # env .env .docker_env tmp/ *.sqlite ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/psf/black rev: 22.3.0 hooks: - id: black language_version: python3.9 name: EthTx_ce:black alias: ethtx_ce-black - repo: https://gitlab.com/pycqa/flake8 rev: 4.0.1 hooks: - id: flake8 language_version: python3.9 name: EthTx_ce:flake8 alias: ethtx_ce-flake8 args: [ --config=ethtx_ce/.flake8 ] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks: - id: trailing-whitespace - id: check-ast - id: check-docstring-first - id: check-merge-conflict - repo: local hooks: - id: pytest files: ./ethtx_ce/tests/ name: pytest language: system entry: make test pass_filenames: false always_run: true ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. ## 0.2.16 - 2022-11-25 ### Changed - Removed `.js` files from the sources and pulling them instead from `cdnjs` [#123](https://github.com/EthTx/ethtx_ce/pull/123) - Removed `static` ressources from the frontend of the app [#129](https://github.com/EthTx/ethtx_ce/pull/129) - Removed `gthread` for better stability [#135](https://github.com/EthTx/ethtx_ce/pull/135) - Removed support of `Rinkeby` [#139](https://github.com/EthTx/ethtx_ce/pull/139) - Changed the `README.md` to include the `development` dependencies [#140](https://github.com/EthTx/ethtx_ce/pull/140) ### Added - Added flexible chains rendering [#139](https://github.com/EthTx/ethtx_ce/pull/139) - Bumped `EthTx` to `0.3.20` and the `web3` depencency to `5.28.0` [#143](https://github.com/EthTx/ethtx_ce/pull/143) ## 0.2.15 - 2022-07-06 ### Changed - Removed `Tokne Flow` branding ### Added - Added redecode semantics functionality - Bumped `EthTx` to `0.3.16` [#116](https://github.com/EthTx/ethtx_ce/pull/116) - From now on the value of `transfer.value` is formatted on the frontend [#116](https://github.com/EthTx/ethtx_ce/pull/116) ### Fixed - Fixed wrong function name (`get_semantics`) [#118](https://github.com/EthTx/ethtx_ce/pull/118) ## 0.2.14 - 2022-05-18 ### Added - Added `info` endpoint with ethtx/ethtx_ce version [#113](https://github.com/EthTx/ethtx_ce/pull/113) - added `get_latest_ethtx_version` function (get version from Pypi) [#113](https://github.com/EthTx/ethtx_ce/pull/113) ### Changed - Refactored `deps` [#113](https://github.com/EthTx/ethtx_ce/pull/113) - Updated `README.md` [#113](https://github.com/EthTx/ethtx_ce/pull/113) - Updated `black` version [#113](https://github.com/EthTx/ethtx_ce/pull/113) - Changed the application name for each component [#113](https://github.com/EthTx/ethtx_ce/pull/113) ## 0.2.13 - 2022-04-22 ### Changed - Extended *.gitignore* [#110](https://github.com/EthTx/ethtx_ce/pull/110) - Updated `black` pre-commit version [#110](https://github.com/EthTx/ethtx_ce/pull/110) ### Fixed - Fixed mongodb semantics remove [#110](https://github.com/EthTx/ethtx_ce/pull/110) ## 0.2.12 - 2022-04-06 ### Changed - Bumped `EthTx` to `0.3.14` [#105](https://github.com/EthTx/ethtx_ce/pull/105) ## 0.2.11 - 2022-03-16 ### Changed - New project structure [#97](https://github.com/EthTx/ethtx_ce/pull/97) - Updated docker, docker-compose [#97](https://github.com/EthTx/ethtx_ce/pull/97) - Removed logs, useless text [#99](https://github.com/EthTx/ethtx_ce/pull/99) - Changed space between *tx_hash* and *chain_id* (transaction page) [#99](https://github.com/EthTx/ethtx_ce/pull/99) ### Added - Added gunicorn configuration [#97](https://github.com/EthTx/ethtx_ce/pull/97) - Added `entrypoint.sh` and other scripts [#97](https://github.com/EthTx/ethtx_ce/pull/97) ## 0.2.10 - 2022-03-03 ### Changed - Bumped `EthTx` to `0.3.10` [#93](https://github.com/EthTx/ethtx_ce/pull/93) ## 0.2.9 - 2022-02-07 ### Fixed - Typo in `README.md` [#75](https://github.com/EthTx/ethtx_ce/pull/75) - API serialization [#75](https://github.com/EthTx/ethtx_ce/pull/75) - Fixed semantics editor [#75](https://github.com/EthTx/ethtx_ce/pull/75) - Fixed `.env_sample` mongo connection string [#83](https://github.com/EthTx/ethtx_ce/pull/83) ### Added - Added new route `reload` - Added `Reolad semantics` button, which allows to reload the semantics (removes from the database and downloads again) [#80](https://github.com/EthTx/ethtx_ce/pull/80) - Added `get_eth_price`. Transaction page displays current **ETH** price taken from *coinbase* API [#88](https://github.com/EthTx/ethtx_ce/pull/88) ### Changed - Removed duplicated environment variables from `docker-compose.yml` [#83](https://github.com/EthTx/ethtx_ce/pull/83) - Bumped python to `3.9` [#87](https://github.com/EthTx/ethtx_ce/pull/87) - From now on, `EthTx` will be used with a static version (due to dynamic development) [#87](https://github.com/EthTx/ethtx_ce/pull/87) - Updated requirements [#88](https://github.com/EthTx/ethtx_ce/pull/88) - Install dev dependencies [#89](https://github.com/EthTx/ethtx_ce/pull/89) ## 0.2.8 - 2021-10-29 ### Changed - Updated **README** and **.env_sample** [#67](https://github.com/EthTx/ethtx_ce/pull/67) - `Web3ConnectionException` is not supported anymore. From now on, a general exception `NodeConnectionException` is caught for node connection errors [#67](https://github.com/EthTx/ethtx_ce/pull/67) - Guessed functions and events are detected using the guessed variable in the model [#67](https://github.com/EthTx/ethtx_ce/pull/67) ## 0.2.7 - 2021-10-14 ### Changed - Changed [EthTx](https://github.com/EthTx/ethtx) version - >=0.3.0,< 0.4.0 [#62](https://github.com/EthTx/ethtx_ce/pull/62) - Deleted usage of mongodb variable [#61](https://github.com/EthTx/ethtx_ce/pull/61) ### Fixed - Fixed colored guessed events with tuple arg [#65](https://github.com/EthTx/ethtx_ce/pull/65) ## 0.2.6 - 2021-10-01 ### Changed - Changed the position of the logo [#59](https://github.com/EthTx/ethtx_ce/pull/59) ## 0.2.5 - 2021-09-30 ### Fixed - Fixed colored guessed functions with nested args [#58](https://github.com/EthTx/ethtx_ce/pull/58) ## 0.2.4 - 2021-09-29 ### Added - Added `.env_sample` file with example environment variables [#57](https://github.com/EthTx/ethtx_ce/pull/57) ### Fixed - Fixed `make run-local` [#57](https://github.com/EthTx/ethtx_ce/pull/57) ### Changed - Changed the docker configuration to make it easier to start [#57](https://github.com/EthTx/ethtx_ce/pull/57) - Updated **README** [#57](https://github.com/EthTx/ethtx_ce/pull/57) ## 0.2.3 - 2021-09-23 ### Added - Color guessed functions and events [#56](https://github.com/EthTx/ethtx_ce/pull/56) ## 0.2.2 - 2021-09-20 ### Fixed - Fixed `tx hash` regexp extracting from request [#53](https://github.com/EthTx/ethtx_ce/pull/53) ## 0.2.1 - 2021-09-17 ### Fixed - Fixed `Decode now` button state [#50](https://github.com/EthTx/ethtx_ce/pull/50) ## 0.2.0 - 2021-09-14 ### Added - [#44](https://github.com/EthTx/ethtx_ce/pull/44) - Added new error page. - Added [Token Flow](https://tokenflow.live) logo. - Added input hash validator. ### Changed - [#44](https://github.com/EthTx/ethtx_ce/pull/44) - Changed footer style. - Removed **ToS** and **PP** and replaced them with `Token Flow` pages. - Removed old tests. - Added **Fathom** analytics tool. - Updated links. ### Fixed - [#44](https://github.com/EthTx/ethtx_ce/pull/44) - Fixed frontend styles. ## 0.1.10 - 2021-08-20 ### Added - Added *preload* to links. ## 0.1.9 - 2021-08-18 ### Added - Added new footer. - Added `Rinkeby` support. ### Changed - Changed [EthTx](https://github.com/EthTx/ethtx) version - >=0.2.0,<0.3.0. ### Fixed - Etherscan links fixed for testnets. ## 0.1.8 - 2021-08-11 ### Added - Added `Goerli` support. ### Changed - Changed [EthTx](https://github.com/EthTx/ethtx) version - >=0.2.0,<0.3.0. ## 0.1.7 - 2021-08-05 ### Added - Added link to PyPi. ## 0.1.6 - 2021-08-04 ### Added - Added information about the `EthTx` and `EthTx Ce` version to the frontend. ### Changed - Removed `Pipfile.lock` ### Fixed - Fixed application dependencies. ## 0.1.5 - 2021-08-02 ### Changed - Removed the banner that was about the new version of `ethtx_ce`. ## 0.1.4 - 2021-07-29 ### Changed - Changed semantics save functions. - Changed [EthTx](https://github.com/EthTx/ethtx) version - 0.1.7. ## 0.1.3 - 2021-07-28 ### Changed - Changed [EthTx](https://github.com/EthTx/ethtx) version - 0.1.6. ## 0.1.2 - 2021-07-27 ### Changed - Changed [EthTx](https://github.com/EthTx/ethtx) version - 0.1.5. - Changed app Config. - Removed EthtxConfig defaults. ## 0.1.1 - 2021-07-26 ### Fixed - Fixed header on mobile devices. ### Changed - Changed Development.MD note. ### Added - Added configuration: AWS, Pipfile, pre-commit. ## 0.1.0 - 2021-07-23 ### Added - First version EthTx CE. ================================================ FILE: DEVELOPMENT.md ================================================ # Local Development This repository contains 2 basic applications: `frontend` & `api`. It is easy to manage, and you can easily add new local application(s). ## Basic structure Application is based on [blueprints](https://flask.palletsprojects.com/en/2.0.x/blueprints/). New extension requires: - new Python Package in ![ethtx_ce](ethtx_ce/app) subdirectory. - `create_app` function (created in new package in `init` file) which returns `Flask` object by calling ![app factory](ethtx_ce/app/factory.py) file. - calling a function above in a `wsgi.py` file with assigned url prefix. These simple steps allow you to add new extension and integrate with entire application. ================================================ FILE: Dockerfile ================================================ FROM python:3.9 WORKDIR /app/ # Upgrade pip, install pipenv RUN pip install --upgrade pip && pip install pipenv # Copy Pipfile* in case it doesn't exist in the repo COPY Pipfile* /app/ COPY ./ethtx_ce/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh COPY ./ethtx_ce/start.sh /start.sh RUN chmod +x /start.sh COPY ./ethtx_ce/start-reload.sh /start-reload.sh RUN chmod +x /start-reload.sh COPY ./ethtx_ce/gunicorn_conf.py /gunicorn_conf.py COPY Makefile /Makefile RUN bash -c "pipenv install --dev --deploy" ARG GIT_URL ENV GIT_URL=$GIT_URL ARG GIT_SHA ENV GIT_SHA=$GIT_SHA ARG CI=1 COPY ./ethtx_ce /app ENV PYTHONPATH=/app EXPOSE 5000 ENTRYPOINT ["/entrypoint.sh"] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ================================================ FILE: Makefile ================================================ help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' build-image: ## Build all docker images docker build -t ethtx_ce . get-git-version: ## Get git version ./scripts/git_version_for_docker.sh run-database: ## Run only a local database required for local development docker-compose up -d mongo mongo-express run-local: PYTHONPATH=./ethtx_ce FLASK_APP=ethtx_ce/app/wsgi.py FLASK_DEBUG=1 pipenv run flask run --host=0.0.0.0 --port 5555 run-prod: fuser -k 5000/tcp || true PYTHONPATH=./ethtx_ce pipenv run gunicorn --workers 4 --max-requests 4000 --timeout 600 --bind :5000 app.wsgi:app run-docker: fuser -k 5000/tcp || true docker-compose up -d run-test-docker: docker run -it ethtx_ce pipenv run python -m pytest . test: PYTHONPATH=./ethtx_ce pipenv run python -m pytest ethtx_ce/tests/ test-all: PYTHONPATH=./ethtx_ce pipenv run python -m pytest . setup: pipenv install --dev pipenv run pre-commit install ================================================ FILE: NOTICE ================================================ Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) The product contains trademarks and other branding elements of Token Flow Insights SA which are not licensed under the Apache 2.0 license. When using or reproducing the code, please remove the trademark and/or other branding elements. ================================================ FILE: Pipfile ================================================ [[source]] url = "https://pypi.org/simple" verify_ssl = true name = "pypi" [packages] ethtx = "==0.3.22" python-dotenv = "*" flask = ">=2.0.2" werkzeug = ">=2.0.2" gunicorn = {version = ">=20.1.0"} flask-httpauth = ">=4.5.0" gitpython = ">=3.1.24" jsonpickle = ">=3.0.0" simplejson = "*" pydantic = "<2.0.0" [dev-packages] black = "*" pytest = ">=6.2.5" pytest-cov =">=3.0.0" pytest-mock =">=3.6.1" pre-commit = "*" [requires] python_version = "3.9" ================================================ FILE: README.md ================================================

EthTx Community Edition


Community version of EthTx transaction decoder
https://ethtx.info

Python Black OpenSource Apache

--- # Description This project represents usage of [EthTx](https://github.com/ethtx/ethtx) decoding library in form of a website. If you are looking for implementation of the said decoding functionalities, please refer to [EthTx](https://github.com/ethtx/ethtx) repository. # Local environment Here is a list of steps to recreate local environment on Ubuntu distribution. 1. Install needed packages using `apt`: ```shell apt install docker-compose python3-pip python3-dev pipenv make apt install libxml2-dev libxslt1-dev gcc ``` 2. Run: ```shell pipenv install ``` 3. Copy `.env_sample` to `.env` and fill required field according to description ``` # Proper nodes are required to run ethtx, provide connection strings for chains which will be used. MAINNET_NODE_URL=https://geth-erigon-node:8545 # KOVAN_NODE_URL= # RINKEBY_NODE_URL= # EthTx supports multiple nodes, if one is unavailable, it will use others. You only need to specify them with a comma # Example: MAINNET_NODE_URL=https://geth-erigon-node:8545,https://geth1-erigon-node:8545 # Etherscan API is used to get contract source code, required for decoding process # You can get free key here https://etherscan.io/apis ETHERSCAN_KEY= # Optional. Those represent data required for connecting to mongoDB. It's used for caching semantics # used in decoding process. But, it's not neccessary for running, If you don't want to use permanent # db or setup mongo, leave those values, mongomock package is used to simulate in-memory mongo. MONGO_CONNECTION_STRING=mongomock://localhost/ethtx # Optional. Credentials for accessing semantics editor page, available under '/semantics/' ETHTX_ADMIN_USERNAME=admin ETHTX_ADMIN_PASSWORD=admin # Optional. Api key used for exposing API_KEY= # Optional. Valid values are ['production', 'staging', 'development']. Those mainly # dictate what options are used for flask debugging and logging ENV=development ``` 4. Run ```shell PYTHONPATH=./ethtx_ce FLASK_APP=ethtx_ce/app/wsgi.py pipenv run flask run --host=0.0.0.0 --port 5000 ``` or ```shell make run-local ``` This will setup new server on host 0.0.0.0 port 5000. 5. Now `ethtx_ce` should be accessible through link [http://localhost:5000](http://localhost:5000) Use can also provided `docker-compose` for running this locally: ```shell docker-compose up ``` Note, this also need proper `.env` file to function properly. # .env file For proper functioning, `.env` file is required containing all database and 3rd party providers configuration. `.env_sample` file is provided in repository with example values. Parameters `[CHAIN_ID]_NODE_URL` should hold valid urls to ethereum nodes; Parameter `ETHERSCAN_KEY` should be equal to Etherscan API key assigned to user. # API The EthTx APIs are provided as a community service and without warranty, so please use what you need and no more. We support `GET` requests. * **Decode transaction** Returns decoded EthTx transaction, based on `chain_id` and transaction hash `tx_hash` * **URL** ```shell /api/transactions/CHAIN_ID/TX_HASH ``` * **Method** `GET` * **Authorization** * Required: header: `x-api-key=[string]` **OR** query parameter: `api_key=[string]` * **URL Params** * Required: `chain_id=[string]`,`tx_hash=[string]` * **Example** ```shell curl --location --request GET 'http://0.0.0.0:5000/api/transactions/dsad/asd' \ --header 'x-api-key: 05a2212d-9985-48d2-b54f-0fbc5ba28766' ``` * **Get Raw Semantic** Returns raw semantic based on `chain_id` and sender/receiver `address` * **URL** ```shell /api/semantics/CHAIN_ID/ADDRESS ``` * **Method** `GET` * **Authorization** * Required: header: `x-api-key=[string]` **OR** query parameter: `api_key=[string]` * **URL Params** * Required:`chain_id=[string]`,`address=[string]` * **Example** ```shell curl --location --request GET 'http://0.0.0.0:5000/api/semantics/dsad/asd' \ --header 'x-api-key: 05a2212d-9985-48d2-b54f-0fbc5ba28766' ``` * **Info** Returns information about the `EthTx` * **URL** ```shell /api/info ``` * **Method** `GET` * **Authorization** * Required: header: `x-api-key=[string]` **OR** query parameter: `api_key=[string]` * **URL Params** * None * **Example** ```shell curl --location --request GET 'http://0.0.0.0:5000/api/info' \ --header 'x-api-key: 05a2212d-9985-48d2-b54f-0fbc5ba28766' ``` ================================================ FILE: docker-compose.override.yml ================================================ version: "3.6" services: ethtx_ce: ports: - "5000:5000" build: context: . dockerfile: Dockerfile command: /start.sh mongo: ports: - "27017:27017" mongo-express: depends_on: - mongo ports: - "8081:8081" ================================================ FILE: docker-compose.yaml ================================================ version: "3.6" services: ethtx_ce: image: 'ethtx_ce:${TAG-latest}' env_file: - .env depends_on: - mongo build: context: . dockerfile: Dockerfile mongo: image: mongo environment: - MONGO_INITDB_DATABASE=${MONGODB_DB} mongo-express: image: mongo-express environment: - ME_CONFIG_MONGODB_SERVER=mongo - ME_CONFIG_MONGODB_PORT=27017 - ME_CONFIG_MONGODB_ENABLE_ADMIN=false - ME_CONFIG_MONGODB_AUTH_DATABASE=${MONGODB_DB} - ME_CONFIG_BASICAUTH_USERNAME=${MONGOEXPRESS_LOGIN} - ME_CONFIG_BASICAUTH_PASSWORD=${MONGOEXPRESS_PASSWORD} depends_on: - mongo ================================================ FILE: ethtx_ce/.flake8 ================================================ [flake8] max-line-length = 130 exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache extend-ignore = # See https://github.com/PyCQA/pycodestyle/issues/373 E203, F401, F403, F405 ================================================ FILE: ethtx_ce/.gitignore ================================================ __pycache__ app.egg-info ================================================ FILE: ethtx_ce/app/__init__.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. ================================================ FILE: ethtx_ce/app/api/__init__.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. from functools import wraps from typing import Dict, Type, Callable, Union, Optional from ethtx import EthTx from flask import Blueprint from flask import Flask from .. import factory from .decorators import auth_required from ..helpers import read_ethtx_versions def create_app( engine: EthTx, settings_override: Optional[Union[Dict, Type]] = None ) -> Flask: """Returns API application instance.""" app = factory.create_app(__name__, __path__, settings_override) app.name = "ethtx_ce/api" app.ethtx = engine # init ethtx engine read_ethtx_versions(app) return app def api_route(bp: Blueprint, *args, **kwargs): kwargs.setdefault("strict_slashes", False) def decorator(f: Callable): @bp.route(*args, **kwargs) @auth_required @wraps(f) def wrapper(*args, **kwargs): sc = 200 rv = f(*args, **kwargs) if isinstance(rv, tuple): sc = rv[1] rv = rv[0] return rv, sc f.__name__ = str(id(f)) + f.__name__ return f return decorator # avoid circular from .endpoints import * from .exceptions import exceptions_bp ================================================ FILE: ethtx_ce/app/api/decorators.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. import logging from functools import wraps from typing import Callable, Optional import jsonpickle from flask import request, current_app, jsonify from ..exceptions import ( AuthorizationError, PayloadTooLarge, UnexpectedError, InternalError, ) from .utils import enable_direct, delete_bstrings log = logging.getLogger(__name__) jsonpickle.set_decoder_options('simplejson', use_decimal=True) def auth_required(func: Callable): """api key verification.""" @wraps(func) def check_auth(**kwargs): api_key = request.headers.get("x-api-key") or request.args.get("api_key") if api_key != current_app.config.get("API_KEY"): raise AuthorizationError(api_key) return func(**kwargs) return check_auth def response(status: Optional[int] = 200): """ Return response with: :param status: response status code, default: `200` """ def _response(f: Callable): @wraps(f) def wrapped(*args, **kwargs): func = f(*args, **kwargs) try: data = jsonify( delete_bstrings( jsonpickle.decode( jsonpickle.encode(func, make_refs=False, unpicklable=False, use_decimal=True) ) ) ) except TypeError as e: log.critical("Response cannot be serialized. %s", e) raise InternalError() except Exception as e: log.exception(e) raise UnexpectedError() return data, status return wrapped return _response @enable_direct def limit_content_length(max_length: Optional[int] = None): """ Limit content length. If not given: The priority has app MAX_CONTENT_LENGTH value. """ def decorator(f): @wraps(f) def wrapper(*args, **kwargs): cl = request.content_length app_max_length = current_app.config.get("MAX_CONTENT_LENGTH") max_content_length = max_length if max_length else app_max_length if cl is not None and cl > max_content_length: raise PayloadTooLarge( content_length=cl, max_content_length=max_content_length ) return f(*args, **kwargs) return wrapper return decorator ================================================ FILE: ethtx_ce/app/api/endpoints/__init__.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. from .info import info_bp from .semantics import semantics_bp from .transactions import transactions_bp ================================================ FILE: ethtx_ce/app/api/endpoints/info.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. from flask import Blueprint, current_app from .. import api_route from ..decorators import response from ...helpers import get_latest_ethtx_version info_bp = Blueprint("api_info", __name__) @api_route(info_bp, "/info") @response(200) def read_info(): """Get info.""" ethtx_version = current_app.config["ethtx_version"] latest_ethtx_version = get_latest_ethtx_version() ethtx_ce_version = current_app.config["ethtx_ce_version"] return { "ethtx": { "version": ethtx_version, "is_latest": ethtx_version == latest_ethtx_version, }, "ethtx_ce": { "version": ethtx_ce_version, }, } ================================================ FILE: ethtx_ce/app/api/endpoints/semantics.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. from typing import Optional from flask import Blueprint, current_app from .. import api_route from ..decorators import response semantics_bp = Blueprint("api_semantics", __name__) @api_route(semantics_bp, "/semantics/") @api_route(semantics_bp, "/semantics//") @response(200) def read_raw_semantic(address: str, chain_id: Optional[str] = None): """Get raw semantic.""" raw_semantics = current_app.ethtx.semantics.get_semantics( chain_id=chain_id, address=address ) return raw_semantics.dict() ================================================ FILE: ethtx_ce/app/api/endpoints/transactions.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. import logging from typing import Optional from flask import Blueprint, current_app from .. import api_route from ..decorators import response log = logging.getLogger(__name__) transactions_bp = Blueprint("api_transactions", __name__) @api_route(transactions_bp, "/transactions/") @api_route(transactions_bp, "/transactions//") @response(200) def read_decoded_transaction(tx_hash: str, chain_id: Optional[str] = None): """Decode transaction.""" tx_hash = tx_hash if tx_hash.startswith("0x") else "0x" + tx_hash chain_id = chain_id or current_app.ethtx.default_chain decoded_transaction = current_app.ethtx.decoders.decode_transaction( chain_id=chain_id, tx_hash=tx_hash ) decoded_transaction.metadata.timestamp = ( decoded_transaction.metadata.timestamp.strftime("%Y-%m-%d %H:%M:%S") ) return decoded_transaction.dict() ================================================ FILE: ethtx_ce/app/api/exceptions.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. import datetime import logging from dataclasses import dataclass from http.client import responses from typing import Tuple, TypeVar from ethtx import exceptions as ethtx_exceptions from flask import Blueprint, request from web3.exceptions import TransactionNotFound from werkzeug.exceptions import HTTPException from .utils import as_dict from ..exceptions import * log = logging.getLogger(__name__) exceptions_bp = Blueprint("exceptions", __name__) @as_dict @dataclass class BaseRequestException: """Base Request Exception""" status: int error: str path: str message: str = "" timestamp: datetime.datetime.utcnow = None def __post_init__(self): """Post init values.""" self.error = str(self.error) self.message = responses[self.status] self.timestamp = datetime.datetime.utcnow() BaseErrorType = TypeVar("BaseErrorType", bound=Tuple[BaseRequestException, int]) @exceptions_bp.app_errorhandler(HTTPException) def handle_all_http_exceptions(error: HTTPException) -> BaseErrorType: """All HTTP Exceptions handler.""" return BaseRequestException(error.code, error.description, request.path), error.code @exceptions_bp.app_errorhandler(ethtx_exceptions.NodeConnectionException) def node_connection_error(error) -> BaseErrorType: """EthTx - Node connection error.""" return BaseRequestException(500, error, request.path), 500 @exceptions_bp.app_errorhandler(ethtx_exceptions.ProcessingException) def processing_error(error) -> BaseErrorType: """EthTx - Processing error.""" return BaseRequestException(500, error, request.path), 500 @exceptions_bp.app_errorhandler(ethtx_exceptions.InvalidTransactionHash) def invalid_transaction_hash(error) -> BaseErrorType: """EthTx - Invalid transaction hash.""" return BaseRequestException(400, error, request.path), 400 @exceptions_bp.app_errorhandler(TransactionNotFound) def transaction_not_found(error) -> BaseErrorType: """Could not find transaction.""" return BaseRequestException(404, error, request.path), 404 @exceptions_bp.app_errorhandler(AuthorizationError) def authorization_error(error) -> BaseErrorType: """Unauthorized request.""" return BaseRequestException(401, error, request.path), 401 @exceptions_bp.app_errorhandler(MalformedRequest) def malformed_request(error) -> BaseErrorType: """Wrong request.""" return BaseRequestException(400, error, request.path), 400 @exceptions_bp.app_errorhandler(PayloadTooLarge) def payload_too_large(error) -> BaseErrorType: """Payload is too large.""" return BaseRequestException(413, error, request.path), 413 @exceptions_bp.app_errorhandler(ResourceLockedError) def resource_locked_error(error) -> BaseErrorType: """Resource is locked.""" return BaseRequestException(423, error, request.path), 423 @exceptions_bp.app_errorhandler(Exception) def unexpected_error(error) -> BaseErrorType: """Unexpected error.""" log.exception(str(error)) return BaseRequestException(500, str(UnexpectedError()), request.path), 500 ================================================ FILE: ethtx_ce/app/api/utils.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. from dataclasses import asdict from functools import wraps from typing import Dict from decimal import Decimal def enable_direct(decorator): """Decorator direct helper.""" @wraps(decorator) def wrapper(*args, **kwargs): f = args[0] if callable(f): return decorator()(f) # pass the function to be decorated else: return decorator(*args, **kwargs) # pass the specified params return wrapper def as_dict(cls): """Return object as dict.""" def wrapper(*args, **kwargs) -> Dict: instance = cls(*args, **kwargs) return asdict(instance) return wrapper def delete_bstrings(obj): primitive = (str, bool, float, type(None)) if isinstance(obj, primitive): return obj elif isinstance(obj, int): return str(obj) elif isinstance(obj, Decimal): if obj == obj.to_integral_value(): return str(obj) else: obj elif type(obj) == bytes: return obj.decode() elif type(obj) == list: for index, value in enumerate(obj): obj[index] = delete_bstrings(value) elif type(obj) == dict: for index, value in obj.items(): obj[index] = delete_bstrings(value) else: raise Exception("Unknown type:" + str(type(obj))) return obj ================================================ FILE: ethtx_ce/app/config.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. import os from dotenv import load_dotenv, find_dotenv BASE_DIR = os.path.dirname(os.path.abspath(__file__)) load_dotenv(find_dotenv(filename="../../.env")) class Config: """Base Config.""" LOGGING_CONFIG = os.environ.get( "LOGGING_CONFIG", os.path.join(BASE_DIR, "../log_cfg.json") ) LOGGING_LOG_PATH = os.environ.get( "LOGGING_CONFIG", os.path.join(BASE_DIR, "../../tmp") ) API_KEY = os.getenv("API_KEY", "") MAX_CONTENT_LENGTH = 10 * 1024 * 1024 ETHTX_ADMIN_USERNAME = os.getenv("ETHTX_ADMIN_USERNAME") ETHTX_ADMIN_PASSWORD = os.getenv("ETHTX_ADMIN_PASSWORD") class ProductionConfig(Config): """Production Config.""" ENV = "production" FLASK_DEBUG = False TESTING = False PROPAGATE_EXCEPTIONS = True class StagingConfig(Config): """Staging Config.""" ENV = "staging" FLASK_DEBUG = True TESTING = False PROPAGATE_EXCEPTIONS = True class DevelopmentConfig(Config): """Development Config.""" ENV = "development" FLASK_DEBUG = True TESTING = True PROPAGATE_EXCEPTIONS = True ================================================ FILE: ethtx_ce/app/exceptions.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. from typing import Optional, Union __all__ = [ "AuthorizationError", "MalformedRequest", "PayloadTooLarge", "MethodNotAllowed", "ResourceLockedError", "InternalError", "UnexpectedError", "FactoryAppException", "EmptyResponseError", ] class FactoryAppException(Exception): """Basic Factory App exception.""" class UnexpectedError(Exception): """Internal Server Error.""" def __init__(self): super().__init__("Unexpected Error") class RequestError(Exception): """Request Error - basic class.""" class AuthorizationError(RequestError): """Unauthorized requests.""" def __init__(self, msg: Optional[str] = None): super().__init__( f"The provided api key is invalid : {msg}." if msg else "Api key is missing." ) class MalformedRequest(RequestError): """Malformed Request Error.""" def __init__(self, msg): super().__init__(msg) class PayloadTooLarge(RequestError): """Payload too large Error.""" def __init__(self, content_length: Union[float, int], max_content_length: int): super().__init__( f"The request is larger than the server is willing or able to process." f" Request length: {content_length}, but allowed is: {max_content_length}." ) class MethodNotAllowed(RequestError): """Method not allowed.""" def __init__(self, method: str): super().__init__(f"Method: {method} not allowed.") class ResourceLockedError(RequestError): """Resource is locked.""" def __init__(self): super().__init__("The resource that is being accessed is locked.") class EmptyResponseError(RequestError): """Response is empty.""" def __init__(self, msg: str): super().__init__(msg) class InternalError(RequestError): """Validation Error""" def __init__(self): super().__init__( "The request was well-formed but server could not properly decode transaction." ) ================================================ FILE: ethtx_ce/app/factory.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. import os from typing import Optional, Dict from flask import Flask from .config import Config from .helpers import class_import, register_blueprints from .logger import setup_logging env = os.getenv("ENV", "development").capitalize() config_class = f"app.config.{env}Config" config: Config = class_import(config_class) def create_app( package_name: str, package_path: str, settings_override: Optional[Dict] = None, **app_kwargs, ) -> Flask: """ Returns a :class:`Flask` application instance :param package_name: application package name :param package_path: application package path :param settings_override: a dictionary of settings to override :param app_kwargs: additional app kwargs """ app = Flask(__name__, instance_relative_config=True, **app_kwargs) app.config.from_object(config) setup_logging(app=app) app.config.from_object(settings_override) register_blueprints(app, package_name, package_path) return app ================================================ FILE: ethtx_ce/app/frontend/__init__.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. from functools import wraps from typing import Callable, Dict, Optional, Union, Type from ethtx import EthTx from flask import Blueprint, Flask from .. import factory from ..helpers import read_ethtx_versions def create_app( engine: EthTx, settings_override: Optional[Union[Dict, Type]] = None ) -> Flask: """Returns Frontend app instance.""" app = factory.create_app( __name__, __path__, settings_override, template_folder="frontend/templates", static_folder="frontend/static", ) app.name = "ethtx_ce/frontend" app.jinja_env.trim_blocks = True app.jinja_env.lstrip_blocks = True app.ethtx = engine # init ethtx engine read_ethtx_versions(app) return app def frontend_route(bp: Blueprint, *args, **kwargs): """Route in blueprint context.""" def decorator(f: Callable): @bp.route(*args, **kwargs) @wraps(f) def wrapper(*args, **kwargs): return f(*args, **kwargs) f.__name__ = str(id(f)) + f.__name__ return f return decorator ================================================ FILE: ethtx_ce/app/frontend/deps.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. import json import logging import re import time from secrets import compare_digest from typing import Optional import requests from flask import request from flask_httpauth import HTTPBasicAuth from ..config import Config log = logging.getLogger(__name__) auth = HTTPBasicAuth() eth_price: Optional[float] = None eth_price_update: Optional[float] = None @auth.verify_password def verify_password(username: str, password: str) -> bool: """Verify user, return bool.""" return username == Config.ETHTX_ADMIN_USERNAME and compare_digest( password, Config.ETHTX_ADMIN_PASSWORD ) def get_eth_price() -> Optional[float]: """ Get current ETH price from coinbase.com Cache price for 60 seconds. """ global eth_price, eth_price_update current_time = time.time() if ( eth_price is None or eth_price_update is None or (current_time - eth_price_update) > 60 ): response = requests.get( "https://api.coinbase.com/v2/prices/ETH-USD/buy", timeout=2 ) if response.status_code == 200: eth_price = float(json.loads(response.content)["data"]["amount"]) eth_price_update = time.time() return eth_price def extract_tx_hash_from_req() -> str: """Extract tx hash from request url.""" hash_match = re.findall(r"(0x)?([A-Fa-f0-9]{64})", request.url) return ( f"{hash_match[0][0]}{hash_match[0][1]}" if hash_match and len(hash_match[0]) == 2 else "" ) ================================================ FILE: ethtx_ce/app/frontend/exceptions.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. import logging from functools import wraps from typing import Callable, Optional from ethtx import exceptions as ethtx_exceptions from flask import Blueprint, render_template from web3.exceptions import TransactionNotFound from werkzeug.exceptions import HTTPException from .deps import extract_tx_hash_from_req from ..exceptions import * log = logging.getLogger(__name__) exceptions_bp = Blueprint("exceptions", __name__) def render_error_page(status: Optional[int] = 500): """Render error page.""" def _render_error_page(f: Callable): @wraps(f) def wrapper(*args, **kwargs): error = f(*args, **kwargs) status_code = status if isinstance(error, HTTPException): error, status_code = error.description, error.code return ( render_template( "exception.html", status_code=status_code, error=error, tx_hash=extract_tx_hash_from_req(), ), status_code, ) return wrapper return _render_error_page @exceptions_bp.app_errorhandler(HTTPException) @render_error_page() def handle_all_http_exceptions(error: HTTPException) -> HTTPException: """All HTTP Exceptions handler.""" return error @exceptions_bp.app_errorhandler(ethtx_exceptions.NodeConnectionException) @render_error_page(500) def node_connection_error(error) -> str: """EthTx - Node connection error.""" return error @exceptions_bp.app_errorhandler(ethtx_exceptions.ProcessingException) @render_error_page(500) def processing_error(error) -> str: """EthTx - Processing error.""" return error @exceptions_bp.app_errorhandler(ethtx_exceptions.InvalidTransactionHash) @render_error_page(400) def invalid_transaction_hash(error) -> str: """EthTx - Invalid transaction hash.""" return error @exceptions_bp.app_errorhandler(TransactionNotFound) @render_error_page(404) def transaction_not_found(error) -> str: """Could not find transaction.""" return error @exceptions_bp.app_errorhandler(AuthorizationError) @render_error_page(401) def authorization_error(error) -> str: """Unauthorized request.""" return error @exceptions_bp.app_errorhandler(MalformedRequest) @render_error_page(400) def malformed_request(error) -> str: """Wrong request.""" return error @exceptions_bp.app_errorhandler(PayloadTooLarge) @render_error_page(413) def payload_too_large(error) -> str: """Payload is too large.""" return error @exceptions_bp.app_errorhandler(ResourceLockedError) @render_error_page(423) def resource_locked_error(error) -> str: """Resource is locked.""" return error @exceptions_bp.app_errorhandler(EmptyResponseError) @render_error_page(404) def empty_response(error) -> str: """Response is empty.""" return error @exceptions_bp.app_errorhandler(Exception) @render_error_page(500) def unexpected_error(error) -> str: """Unexpected error.""" log.exception(str(error)) return str(UnexpectedError()) ================================================ FILE: ethtx_ce/app/frontend/semantics.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. from __future__ import annotations import json import logging from typing import Optional, List, Dict from ethtx import EthTx from ethtx.models.semantics_model import ( AddressSemantics, ContractSemantics, ERC20Semantics, ) from ethtx.models.semantics_model import ( EventSemantics, FunctionSemantics, TransformationSemantics, ParameterSemantics, ) from flask import Blueprint, render_template, current_app, request, jsonify from web3 import Web3 from . import frontend_route from .deps import auth from ..exceptions import EmptyResponseError bp = Blueprint("semantics", __name__) log = logging.getLogger(__name__) @frontend_route(bp, "/semantics//") @frontend_route(bp, "/semantics///") @auth.login_required def semantics(address: str, chain_id: Optional[str] = None) -> show_semantics_page: raw_semantics = current_app.ethtx.semantics.get_semantics( chain_id=chain_id or current_app.ethtx._default_chain, address=address ) return show_semantics_page(raw_semantics) @frontend_route(bp, "/reload", methods=["POST"]) @auth.login_required def reload_semantics(): """Reload raw semantic.""" data = json.loads(request.data) ethtx: EthTx = current_app.ethtx ethtx.semantics.database._addresses.delete_one({"address": data["address"]}) ethtx.semantics.get_semantics.cache_clear() ethtx.semantics.get_semantics( data["chain_id"] if data.get("chain_id") else current_app.ethtx._default_chain, data["address"], ) return "ok" @frontend_route(bp, "/save", methods=["POST"]) @auth.login_required def semantics_save(): data = json.loads(request.data) return _semantics_save(data) @frontend_route(bp, "/poke", methods=["POST"]) @auth.login_required def poke_abi(): data = json.loads(request.data) return _poke_abi(data) def show_semantics_page(data: AddressSemantics) -> render_template: if data: data_dict = data.dict() address = data.address chain_id = data.chain_id name = data.name or address if data.is_contract: events = data_dict["contract"]["events"] or {} functions = data_dict["contract"]["functions"] or {} transformations = data_dict["contract"]["transformations"] or {} code_hash = data.contract.code_hash contract_name = data.contract.name else: events = {} functions = {} transformations = {} code_hash = "EOA" contract_name = address standard = data.standard # ToDo: make it more universal if standard == "ERC20": standard_info = data_dict["erc20"] or {} elif standard == "ERC721": standard_info = {} else: standard_info = {} metadata = dict( label=name, chain=chain_id, contract=dict( name=contract_name, code_hash=code_hash, standard=dict(name=standard, data=standard_info), ), ) return ( render_template( "semantics.html", address=address, events=events, functions=functions, transformations=transformations, metadata=metadata, ), 200, ) raise EmptyResponseError( "The semantics are empty. It probably means that the given address " "has not been decoded before or address is incorrect." ) def _parameters_semantics(parameters: List[Dict]) -> List[ParameterSemantics]: parameters_semantics_list = [] if parameters: for parameter in parameters: parameters_semantics_list.append( ParameterSemantics( parameter_name=parameter.get("parameter_name"), parameter_type=parameter.get("parameter_type"), components=parameter.get("components", []), indexed=parameter.get("indexed", False), dynamic=parameter.get("dynamic", False), ) ) return parameters_semantics_list def _semantics_save(data): try: address = data.get("address") metadata = data.get("metadata") events = data.get("events") functions = data.get("functions") transformations = data.get("transformations") standard_name = None erc20_semantics = None if metadata.get("contract"): events_semantics = dict() functions_semantics = dict() transformations_semantics = dict() for event in events.values(): events_semantics[event.get("signature")] = EventSemantics( signature=event.get("signature"), anonymous=event.get("anonymous"), name=event.get("name"), parameters=_parameters_semantics(event.get("parameters")), ) for function in functions.values(): functions_semantics[function.get("signature")] = FunctionSemantics( signature=function.get("signature"), name=function.get("name"), inputs=_parameters_semantics(function.get("inputs")), outputs=_parameters_semantics(function.get("outputs")), ) for signature, transformation in transformations.items(): transformations_semantics[signature] = dict() for parameter_name, parameter_transformation in transformation: transformations_semantics[signature][ parameter_name ] = TransformationSemantics( transformed_name=parameter_transformation.get( "transformed_name" ), transformed_type=parameter_transformation.get( "transformed_type" ), transformation=parameter_transformation.get("transformation"), ) standard_name = metadata["contract"]["standard"]["name"] if standard_name == "ERC20": erc20_data = metadata["contract"]["standard"].get("data") if erc20_data: erc20_semantics = ERC20Semantics( name=erc20_data.get("name"), symbol=erc20_data.get("symbol"), decimals=erc20_data.get("decimals"), ) contract_semantics = ContractSemantics( code_hash=metadata["contract"].get("code_hash"), name=metadata["contract"].get("name"), events=events_semantics, functions=functions_semantics, transformations=transformations_semantics, ) else: contract_semantics = None address_semantics = AddressSemantics( chain_id=metadata.get("chain"), address=address, name=metadata.get("label"), is_contract=contract_semantics is not None, contract=contract_semantics, standard=standard_name, erc20=erc20_semantics, ) current_app.ethtx.semantics.update_semantics(semantics=address_semantics) current_app.ethtx.semantics.get_semantics.cache_clear() current_app.ethtx.semantics.get_event_abi.cache_clear() current_app.ethtx.semantics.get_anonymous_event_abi.cache_clear() current_app.ethtx.semantics.get_transformations.cache_clear() current_app.ethtx.semantics.get_function_abi.cache_clear() current_app.ethtx.semantics.get_constructor_abi.cache_clear() current_app.ethtx.semantics.check_is_contract.cache_clear() current_app.ethtx.semantics.get_standard.cache_clear() result = "ok" except Exception as e: logging.exception("Semantics save error: %s" % e) result = "error" return jsonify(result=result) def _poke_abi(data): # helper function decoding contract ABI def _parse_abi(json_abi): # helper function to recursively parse parameters def _parse_parameters(parameters): comp_canonical = "(" comp_inputs = list() for i, parameter in enumerate(parameters): argument = dict( parameter_name=parameter["name"], parameter_type=parameter["type"] ) if parameter["type"][:5] == "tuple": sub_canonical, sub_components = _parse_parameters( parameter["components"] ) comp_canonical += sub_canonical + parameter["type"][5:] argument["components"] = sub_components else: comp_canonical += parameter["type"] sub_components = [] if i < len(parameters) - 1: comp_canonical += "," if ( parameter["type"] in ("string", "bytes") or parameter["type"][-2:] == "[]" ): argument["dynamic"] = True elif parameter["type"] == "tuple": argument["dynamic"] = any(c["dynamic"] for c in sub_components) else: argument["dynamic"] = False if "indexed" in parameter: argument["indexed"] = parameter["indexed"] comp_inputs.append(argument) comp_canonical += ")" return comp_canonical, comp_inputs functions = dict() events = dict() for item in json_abi: if "type" in item: # parse contract functions if item["type"] == "constructor": _, inputs = _parse_parameters(item["inputs"]) functions["constructor"] = dict( signature="constructor", name="constructor", inputs=inputs, outputs=[], ) elif item["type"] == "fallback": functions["fallback"] = {} elif item["type"] == "function": canonical, inputs = _parse_parameters(item["inputs"]) canonical = item["name"] + canonical function_hash = Web3.sha3(text=canonical).hex() signature = function_hash[0:10] _, outputs = _parse_parameters(item["outputs"]) functions[signature] = dict( signature=signature, name=item["name"], inputs=inputs, outputs=outputs, ) # parse contract events elif item["type"] == "event": canonical, parameters = _parse_parameters(item["inputs"]) canonical = item["name"] + canonical event_hash = Web3.sha3(text=canonical).hex() signature = event_hash events[signature] = dict( signature=signature, name=item["name"], anonymous=item["anonymous"], parameters=parameters, ) return functions, events try: address = data["address"] chash = data["chash"] network = data["network"] name = data["name"] standard = json.loads(data["standard"]) abi = json.loads(data["abi"]) if abi and abi != []: is_contract = True functions, events = _parse_abi(abi) events_semantics = dict() for event in events.values(): events_semantics[event.get("signature")] = EventSemantics( signature=event.get("signature"), anonymous=event.get("anonymous"), name=event.get("name"), parameters=_parameters_semantics(event.get("parameters")), ) functions_semantics = dict() for function in functions.values(): functions_semantics[function.get("signature")] = FunctionSemantics( signature=function.get("signature"), name=function.get("name"), inputs=_parameters_semantics(function.get("inputs")), outputs=_parameters_semantics(function.get("outputs")), ) contract_semantics = ContractSemantics( code_hash=chash, name=name, events=events_semantics, functions=functions_semantics, transformations=dict(), ) else: is_contract = False contract_semantics = ContractSemantics( code_hash="0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", name="EOA", events=dict(), functions=dict(), transformations=dict(), ) address_semantics = AddressSemantics( chain_id=network, address=address, name=name, is_contract=is_contract, contract=contract_semantics, standard=standard.get("name"), erc20=ERC20Semantics( name=standard["data"].get("name"), symbol=standard["data"].get("symbol"), decimals=standard["data"].get("decimals"), ) if standard.get("name") == "ERC20" else None, ) current_app.ethtx.semantics.update_semantics(semantics=address_semantics) current_app.ethtx.semantics.get_semantics.cache_clear() current_app.ethtx.semantics.get_event_abi.cache_clear() current_app.ethtx.semantics.get_anonymous_event_abi.cache_clear() current_app.ethtx.semantics.get_transformations.cache_clear() current_app.ethtx.semantics.get_function_abi.cache_clear() current_app.ethtx.semantics.get_constructor_abi.cache_clear() current_app.ethtx.semantics.check_is_contract.cache_clear() current_app.ethtx.semantics.get_standard.cache_clear() logging.info(f"ABI for {address} decoded.") result = "ok" except Exception as e: logging.exception("ABI retrieval error: %s" % e) result = "error" return jsonify(result=result) ================================================ FILE: ethtx_ce/app/frontend/static/ethtx.new.css ================================================ /*Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce)*/ /*Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded*/ /*in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md)*/ /*Licensed under the Apache License, Version 2.0 (the "License");*/ /*you may not use this file except in compliance with the License.*/ /*You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0*/ /*Unless required by applicable law or agreed to in writing, software distributed under the License is distributed*/ /*on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.*/ /*See the License for the specific language governing permissions and limitations under the License.*/ /*The product contains trademarks and other branding elements of Token Flow Insights SA which are*/ /*not licensed under the Apache 2.0 license. When using or reproducing the code, please remove*/ /*the trademark and/or other branding elements.*/ body { font-family: monospace; margin: 5px; background-color: whitesmoke; width: 100%; } * { margin: 0; padding: 0; } h3 { margin: 11px; } p { padding-bottom: 50px; } .my_p { margin: 2px 2px 2px 11px; border: 1px #ccc solid; border-radius: 5px; padding: 2px 5px; white-space: nowrap; display: inline-block; } a:link, a:visited { color: black; text-decoration: none; } .delegate a:link, .delegate a:visited { color: darkorange; text-decoration: none; } .table { margin-left: 11px; border: solid 1px #bbbbbb; border-collapse: collapse; border-spacing: 0; } .table thead th { background-color: #ddd; border: solid 1px #bbbbbb; color: #444444; padding: 3px 10px 3px 10px; text-align: left; text-shadow: 1px 1px 1px #fff; } .table tbody td { border: solid 1px #bbbbbb; color: #333; padding: 3px 10px 3px 10px; text-shadow: 1px 1px 1px #fff; white-space: nowrap; } .content { width: 500px; margin: auto; } .transaction-info { margin: 5px 0 4px 13px; } .transaction-hash { color: darkgreen; } .events tbody td { border: 1px #ccc solid; padding: 5px 10px 2px 10px; } .calls table { border: transparent; } .calls tbody td { border: transparent; padding: 5px 10px 2px 10px; } .l2txs table { border: transparent; } .l2txs tbody td { border: transparent; padding: 5px 10px 2px 10px; } #tree .ui-fancytree ul { padding-left: 20px; } #tree .ui-fancytree li { list-style-type: none; margin: 5px; position: relative; } #tree .ui-fancytree li::before { content: ""; position: absolute; top: -7px; left: 2px; border-left: 1px solid #ccc; border-bottom: 1px solid #ccc; border-radius: 0 0 0 0; width: 20px; height: 15px; } #tree .ui-fancytree li::after { position: absolute; content: ""; top: 8px; left: 2px; border-left: 1px solid #ccc; border-top: 1px solid #ccc; border-radius: 0 0 0 0; width: 20px; height: 100%; } #tree .ui-fancytree li:last-child::after { display: none; } #tree .ui-fancytree li:last-child:before { border-radius: 0 0 0 5px; } #tree ul.ui-fancytree > li:first-child::before { display: none; } #tree ul.ui-fancytree > li:first-child::after { border-radius: 5px 0 0 0; } #tree .ui-fancytree li p { border: 1px #ccc solid; border-radius: 5px; padding: 2px 5px; white-space: nowrap; display: inline-block; } #tree .ui-fancytree li p:hover, #tree .ui-fancytree li p:hover + ul li p, #tree .ui-fancytree li p:focus, #tree .ui-fancytree li p:focus + ul li p { background: #eee; color: #000; border: 1px solid #aaa; } #tree .ui-fancytree li p:hover + ul li::after, #tree .ui-fancytree li p:focus + ul li::after, #tree .ui-fancytree li p:hover + ul li::before, #tree .ui-fancytree li p:focus + ul li::before, #tree .ui-fancytree li p:hover + ul::before, #tree .ui-fancytree li p:focus + ul::before, #tree .ui-fancytree li p:hover + ul ul::before, #tree .ui-fancytree li p:focus + ul ul::before { border-color: #999; } #tree .ui-fancytree .fancytree-icon { display: none; } #tree .fancytree-plain.fancytree-container.fancytree-treefocus span.fancytree-focused span.fancytree-title { border-color: transparent; } #tree .fancytree-plain span.fancytree-node span.fancytree-title { background-color: transparent; border-color: transparent; } #tree ul.fancytree-container { background-color: transparent; border: none; outline: none; } #tree .fancytree-expander { position: relative; z-index: 2; top: -2px; left: -5px; } #tree * { font-family: monospace; } #tx_tree .ui-fancytree ul { padding-left: 20px; } #tx_tree .ui-fancytree li { list-style-type: none; margin: 5px; position: relative; } #tx_tree .ui-fancytree li::before { content: ""; position: absolute; top: -7px; left: 2px; border-left: 1px solid #ccc; border-bottom: 1px solid #ccc; border-radius: 0 0 0 0; width: 20px; height: 15px; } #tx_tree .ui-fancytree li::after { position: absolute; content: ""; top: 8px; left: 2px; border-left: 1px solid #ccc; border-top: 1px solid #ccc; border-radius: 0 0 0 0; width: 20px; height: 100%; } #tx_tree .ui-fancytree li:last-child::after { display: none; } #tx_tree .ui-fancytree li:last-child:before { border-radius: 0 0 0 5px; } #tx_tree ul.ui-fancytree > li:first-child::before { display: none; } #tx_tree ul.ui-fancytree > li:first-child::after { border-radius: 5px 0 0 0; } #tx_tree .ui-fancytree li p { border: 1px #ccc solid; border-radius: 5px; padding: 2px 5px; white-space: nowrap; display: inline-block; } #tx_tree .ui-fancytree li p:hover, #tx_tree .ui-fancytree li p:hover + ul li p, #tx_tree .ui-fancytree li p:focus, #tx_tree .ui-fancytree li p:focus + ul li p { background: #eee; color: #000; border: 1px solid #aaa; } #tx_tree .ui-fancytree li p:hover + ul li::after, #tx_tree .ui-fancytree li p:focus + ul li::after, #tx_tree .ui-fancytree li p:hover + ul li::before, #tx_tree .ui-fancytree li p:focus + ul li::before, #tx_tree .ui-fancytree li p:hover + ul::before, #tx_tree .ui-fancytree li p:focus + ul::before, #tx_tree .ui-fancytree li p:hover + ul ul::before, #tx_tree .ui-fancytree li p:focus + ul ul::before { border-color: #999; } #tx_tree .ui-fancytree .fancytree-icon { display: none; } #tx_tree .fancytree-plain.fancytree-container.fancytree-treefocus span.fancytree-focused span.fancytree-title { border-color: transparent; } #tx_tree .fancytree-plain span.fancytree-node span.fancytree-title { background-color: transparent; border-color: transparent; } #tx_tree ul.fancytree-container { background-color: transparent; border: none; outline: none; } #tx_tree .fancytree-expander { position: relative; z-index: 2; top: -2px; left: -5px; } #tx_tree * { font-family: monospace; } .back-button-container { display: flex; align-items: center; font-size: 16px; } .back-button-container img { margin-left: 20px; margin-right: 20px; } .with-back-button { display: flex; align-items: center; margin: 10px 11px 10px; } .with-back-button h3 { padding-bottom: 0; margin: 0; } .container-top { display: inline-block; } .container-info-logo { display: flex; justify-content: space-between; } @media screen and (max-width: 1281px) { .hosted-by { order: 1; margin: 0 0 0 13px; } .container-info-logo { flex-flow: column; } .transaction-info { order: 2; } } @media screen and (max-width: 1280px) { .with-back-button { flex-direction: column; align-items: flex-start; } .back-button-container { order: -1; margin-bottom: 20px; width: 100%; justify-content: space-between; } } ================================================ FILE: ethtx_ce/app/frontend/static.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. from flask import render_template, Blueprint, current_app from . import frontend_route bp = Blueprint("static", __name__) @frontend_route(bp, "/") def search_page() -> render_template: """Render search page - index.""" return ( render_template( "index.html", chains=current_app.ethtx.providers.web3provider.nodes.keys() if current_app.ethtx else [], ethtx_version=current_app.config["ethtx_version"], ethtx_ce_version=current_app.config["ethtx_ce_version"], ), 200, ) ================================================ FILE: ethtx_ce/app/frontend/templates/exception.html ================================================ {% include './partials/headtags.html' %} Ethtx error

{{ status_code }}

{{ error }}

{% if tx_hash %}

Tx hash: {{ tx_hash }}

{%- endif -%}
================================================ FILE: ethtx_ce/app/frontend/templates/index.html ================================================ {% include './partials/headtags.html' %} EthDecoder
EthTx Transaction Decoder
================================================ FILE: ethtx_ce/app/frontend/templates/partials/headtags.html ================================================ ================================================ FILE: ethtx_ce/app/frontend/templates/semantics.html ================================================ {% include './partials/headtags.html' %} Semantics editor

Semantics for: {{ address }} / {{ metadata.chain }}

================================================ FILE: ethtx_ce/app/frontend/templates/transaction.html ================================================ {% macro address_link(address, label, badge="") %} {%- if address and address != '0x0000000000000000000000000000000000000000' -%} {%- if badge -%} [{{ badge }}] {%- endif -%} {{- label -}} {%- else -%} {{- label -}} {%- endif -%} {% endmacro %} {% macro nft_link(address, label) %} {%- if address and address != '0x0000000000000000000000000000000000000000' -%} {{- label -}} {%- else -%} {{- label -}} {%- endif -%} {% endmacro %} {% macro print_event_arguments(arguments) %} {% for argument in arguments %} {% if argument.type != "ignore" %} {% if loop.index > 1 %}, {% endif %} {%- if argument.name == "[no ABI]" -%} no_ABI {%- else -%} {%- if argument.name -%} {{ argument.name }}= {%- endif -%} {%- if argument.type == "tuple" -%} ({{- print_event_arguments(argument.value) -}}) {%- elif argument.type == "tuple[]" -%} [ {%- for sub_arg in argument.value -%} {{- print_event_arguments(sub_arg) -}} {%- endfor -%} ] {%- elif argument.type == "address" -%} {{- address_link(argument.value.address, argument.value.name, argument.value.badge) -}} {%- elif argument.type == "nft" -%} {{- nft_link(argument.value.address, argument.value.name) -}} {%- elif argument.type == "call" -%} {{- address_link(argument.value.address, argument.value.name, argument.value.badge) -}}. {{- argument.value.function_name -}}({{ print_event_arguments(argument.value.arguments) }}) {%- else -%} {{- argument.value -}} {%- endif -%} {% endif %} {% endif %} {% endfor %} {% endmacro %} {% macro print_call_arguments(arguments) %} {% if arguments is not none %} {%- for argument in arguments -%} {% if argument.type != "ignore" %} {%- if loop.index > 1 -%}, {% endif %} {%- if argument.name == "[no ABI]" -%} no_ABI {%- else -%} {%- if argument.name %}{{- argument.name -}}={%- endif -%} {%- if argument.type == "tuple" -%} ({{- print_call_arguments(argument.value) -}}) {%- elif argument.type == "tuple[]" -%} [ {%- for sub_arg in argument.value -%} {%- if loop.index > 1 -%}, {% endif %} ({{- print_call_arguments(sub_arg) -}}) {%- endfor -%} ] {%- elif argument.type == "address" -%} {{- address_link(argument.value.address, argument.value.name, argument.value.badge) -}} {%- elif argument.type == "nft" -%} {{- nft_link(argument.value.address, argument.value.name) -}} {% elif argument.type == "call" %} {{- address_link(argument.value.address, argument.value.name, argument.value.badge) -}}. {{- argument.value.function_name -}}( {{- print_event_arguments(argument.value.arguments) -}}) {%- else -%} {{- argument.value -}} {%- endif -%} {%- endif -%} {%- endif -%} {%- endfor -%} {% endif %} {% endmacro %} {%- macro print_call_line(call) -%}
  • [{{- call.gas_used if call.gas_used != None else "N/A" -}}]: {% if call.error %} ({{ call.error }}) {% endif %} {% if call.call_type == "delegatecall" %} (delegate) {% endif %} {% if call.value and call.call_type != "selfdestruct" %} ETH {{ call.value -}} {% endif %} {%- if call.call_type == "selfdestruct" -%} {{- address_link(call.from_address.address, call.from_address.name, call.from_address.badge) -}} .{{ call.call_type }}({% if call.value > 0 %} ETH {{ call.value }} => {{ address_link(call.to_address.address, call.to_address.name, call.to_address.badge) -}}{% endif %} ) {%- elif call.call_type == "create" -%} {{- address_link(call.to_address.address, call.to_address.name, call.to_address.badge) -}}.New() {%- else -%} {%- if call.call_type == "delegatecall" -%} {{- address_link(call.from_address.address, call.from_address.name, call.from_address.badge) -}} {%- else -%} {{- address_link(call.to_address.address, call.to_address.name, call.to_address.badge) -}} {%- endif -%} {%- if call.call_type == "delegatecall" -%} [{{- address_link(call.to_address.address, call.to_address.name, call.to_address.badge) -}} {%- endif -%} {%- if call.function_guessed -%} .{{- call.function_name -}} {%- elif call.function_name != "0x" -%} .{{- call.function_name -}} {%- else -%} .fallback {%- endif -%} {%- if call.call_type == "delegatecall" -%} ] {%- endif -%} ({{- print_call_arguments(call.arguments) -}}) => ({{- print_call_arguments(call.outputs) -}}) {%- endif -%}

      {% for sub_call in call.subcalls %} {{- print_call_line(sub_call) -}} {% endfor %}
  • {%- endmacro -%} {% include './partials/headtags.html' %} Ethtx.info Analysis {{ transaction.tx_hash }}

    Analysis for: {{ transaction.tx_hash }} / {{ transaction.chain_id }}

    {% if events %}

    Emitted events:

    {% for event in events %} {% endfor %}
    [{{- event.index -}}] {{ address_link(event.contract.address, event.contract.name, event.contract.badge) }} {%- if event.event_guessed -%} .{{ event.event_name }} {%- else -%} .{{ event.event_name }} {%- endif -%} ({{ print_event_arguments(event.parameters) }})
    {% endif %} {% if balances %} {% endif %} {% if transfers %}

    Token transfers:

    {% for transfer in transfers %} {% if transfer.token_standard == 'ERC721' %} {% else %} {% endif %} {% endfor %}
    Sender Token Amount Receiver
    {{- address_link(transfer.from_address.address, transfer.from_address.name, transfer.from_address.badge) -}}{{- nft_link(transfer.token_address, transfer.token_symbol) -}}{{- address_link(transfer.token_address, transfer.token_symbol) -}} {{- "{:,.4f}".format(transfer.value) -}} {{- address_link(transfer.to_address.address, transfer.to_address.name, transfer.to_address.badge) -}}
    {% endif %} {% if call %}

    Execution trace:

    • [{{- transaction.gas_used -}}]: {{- address_link(transaction.sender.address, transaction.sender.name, 'sender') -}}

        {{- print_call_line(call) -}}
    {% else %}

    Trace decoding error...

    {% endif %} ================================================ FILE: ethtx_ce/app/frontend/transactions.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. import logging import os from typing import Optional from ethtx.models.decoded_model import DecodedTransaction from flask import Blueprint, render_template, current_app, request from . import frontend_route, deps log = logging.getLogger(__name__) bp = Blueprint("transactions", __name__) @frontend_route(bp, "//") @frontend_route(bp, "///") def read_decoded_transaction( tx_hash: str, chain_id: Optional[str] = None ) -> "show_transaction_page": tx_hash = tx_hash if tx_hash.startswith("0x") else "0x" + tx_hash refresh_semantics = "refresh" in request.args if refresh_semantics: refresh_secure_key = request.args["refresh"] if refresh_secure_key != os.environ["SEMANTIC_REFRESH_KEY"]: return "Invalid semantics refresh key" log.info(f"Decoding tx {tx_hash} with semantics refresh") chain_id = chain_id or current_app.ethtx.default_chain decoded_transaction = current_app.ethtx.decoders.decode_transaction( chain_id=chain_id, tx_hash=tx_hash, recreate_semantics=refresh_semantics ) decoded_transaction.metadata.timestamp = ( decoded_transaction.metadata.timestamp.strftime("%Y-%m-%d %H:%M:%S") ) return show_transaction_page(decoded_transaction) def show_transaction_page(data: DecodedTransaction) -> render_template: """Render transaction/exception page.""" return ( render_template( "transaction.html", eth_price=deps.get_eth_price(), transaction=data.metadata, events=data.events, call=data.calls, transfers=data.transfers, balances=data.balances, ), 200, ) ================================================ FILE: ethtx_ce/app/helpers.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. import importlib import logging import os import pkgutil from typing import Any, List, Tuple, Optional import pkg_resources import requests from flask import Blueprint, Flask from git import Repo log = logging.getLogger(__name__) def register_blueprints( app: Flask, package_name: str, package_path: str ) -> List[Blueprint]: """ Register all Blueprint instances on the specified Flask application found in all modules for the specified package. :param app: the Flask application :param package_name: the package name :param package_path: the package path """ rv = [] for _, name, _ in pkgutil.iter_modules(package_path): m = importlib.import_module("%s.%s" % (package_name, name)) for item in dir(m): item = getattr(m, item) if isinstance(item, Blueprint): app.register_blueprint(item) rv.append(item) return rv def class_import(name: str) -> Any: """Import class from string.""" d = name.rfind(".") classname = name[d + 1 : len(name)] m = __import__(name[0:d], globals(), locals(), [classname]) return getattr(m, classname) class Singleton(type): _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] def read_ethtx_versions(app: Flask) -> None: """Read ethtx and ethtx_ce versions.""" ethtx_version = pkg_resources.get_distribution("ethtx").version try: remote_url, sha = _get_version_from_git() except Exception: remote_url, sha = _get_version_from_docker() ethtx_ce_version = f"{_clean_up_git_link(remote_url)}/tree/{sha}" log.info( "%s: EthTx version: %s. EthTx CE version: %s", app.name, ethtx_version, ethtx_ce_version, ) app.config["ethtx_version"] = ethtx_version app.config["ethtx_ce_version"] = ethtx_ce_version def get_latest_ethtx_version() -> str: """Get latest EthTx version.""" package = "EthTx" response = requests.get(f"https://pypi.org/pypi/{package}/json") if response.status_code != 200: log.warning("Failed to get latest EthTx version from PyPI") return "" log.info("Latest EthTx version: %s", response.json()["info"]["version"]) return response.json()["info"]["version"] def _get_version_from_git() -> Tuple[str, str]: """Get EthTx CE version from .git""" repo = Repo(__file__, search_parent_directories=True) remote_url = repo.remote("origin").url sha = repo.head.commit.hexsha short_sha = repo.git.rev_parse(sha, short=True) return remote_url, short_sha def _get_version_from_docker() -> Tuple[str, str]: """Get EthTx CE version from env.""" return os.getenv("GIT_URL", ""), os.getenv("GIT_SHA", "") def _clean_up_git_link(git_link: str) -> str: """Clean up git link, delete .git extension, make https url.""" if "@" in git_link: git_link.replace(":", "/").replace("git@", "https://") if git_link.endswith(".git"): git_link = git_link[:-4] return git_link ================================================ FILE: ethtx_ce/app/logger.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. import json import logging import logging.config import os from flask import Flask def setup_logging(app: Flask): """Setup logging""" with open(app.config["LOGGING_CONFIG"], "r") as f: config = json.load(f) config["root"]["level"] = "DEBUG" if app.config["DEBUG"] else "INFO" filename = config["handlers"]["file_handler"]["filename"] if "/" not in filename: log_file_path = os.path.join(app.config["LOGGING_LOG_PATH"], filename) os.makedirs(os.path.dirname(log_file_path), exist_ok=True) config["handlers"]["file_handler"]["filename"] = log_file_path logging.config.dictConfig(config) setup_external_logging() def setup_external_logging() -> None: """Setup and override external libs loggers.""" logging.getLogger("web3").setLevel(logging.INFO) # web3 logger ================================================ FILE: ethtx_ce/app/wsgi.py ================================================ # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. # # The product contains trademarks and other branding elements of Token Flow Insights SA which are # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove # the trademark and/or other branding elements. import os from ethtx import EthTx, EthTxConfig from flask import Flask from werkzeug.middleware.dispatcher import DispatcherMiddleware from . import frontend, api app = Flask(__name__) ethtx_config = EthTxConfig( mongo_connection_string=os.getenv("MONGO_CONNECTION_STRING"), etherscan_api_key=os.getenv("ETHERSCAN_KEY"), web3nodes={ "mainnet": dict(hook=os.getenv("MAINNET_NODE_URL", ""), poa=False), "goerli": dict(hook=os.getenv("GOERLI_NODE_URL", ""), poa=True), }, default_chain="mainnet", etherscan_urls={ "mainnet": "https://api.etherscan.io/api", "goerli": "https://api-goerli.etherscan.io/api", }, ) ethtx = EthTx.initialize(ethtx_config) app.wsgi_app = DispatcherMiddleware( frontend.create_app(engine=ethtx, settings_override=EthTxConfig), {"/api": api.create_app(engine=ethtx, settings_override=EthTxConfig)}, ) # ethtx_ce/ as Source Root if __name__ == "__main__": app.run() ================================================ FILE: ethtx_ce/entrypoint.sh ================================================ #!/usr/bin/env sh set -e echo "ethtx_ce entrypoint.sh" if [ -f /app/app/wsgi.py ]; then DEFAULT_MODULE_NAME=app.wsgi elif [ -f /app/wsgi.py ]; then DEFAULT_MODULE_NAME=wsgi fi MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME} VARIABLE_NAME=${VARIABLE_NAME:-app} export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"} if [ -f /app/gunicorn_conf.py ]; then DEFAULT_GUNICORN_CONF=/app/gunicorn_conf.py elif [ -f /app/app/gunicorn_conf.py ]; then DEFAULT_GUNICORN_CONF=/app/app/gunicorn_conf.py else DEFAULT_GUNICORN_CONF=/gunicorn_conf.py fi export GUNICORN_CONF=${GUNICORN_CONF:-$DEFAULT_GUNICORN_CONF} exec "$@" ================================================ FILE: ethtx_ce/gunicorn_conf.py ================================================ import json import multiprocessing import os workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1") max_workers_str = os.getenv("MAX_WORKERS") use_max_workers = None if max_workers_str: use_max_workers = int(max_workers_str) web_concurrency_str = os.getenv("WEB_CONCURRENCY", None) host = os.getenv("HOST", "0.0.0.0") port = os.getenv("PORT", "5000") bind_env = os.getenv("BIND", None) use_loglevel = os.getenv("LOG_LEVEL", "info") if bind_env: use_bind = bind_env else: use_bind = f"{host}:{port}" cores = multiprocessing.cpu_count() workers_per_core = float(workers_per_core_str) default_web_concurrency = workers_per_core * cores if web_concurrency_str: web_concurrency = int(web_concurrency_str) assert web_concurrency > 0 else: web_concurrency = max(int(default_web_concurrency), 2) if use_max_workers: web_concurrency = min(web_concurrency, use_max_workers) accesslog_var = os.getenv("ACCESS_LOG", "-") use_accesslog = accesslog_var or None errorlog_var = os.getenv("ERROR_LOG", "-") use_errorlog = errorlog_var or None graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "600") timeout_str = os.getenv("TIMEOUT", "600") keepalive_str = os.getenv("KEEP_ALIVE", "5") # Gunicorn config variables loglevel = use_loglevel workers = web_concurrency bind = use_bind errorlog = use_errorlog worker_tmp_dir = "/dev/shm" accesslog = use_accesslog graceful_timeout = int(graceful_timeout_str) timeout = int(timeout_str) keepalive = int(keepalive_str) # For debugging and testing log_data = { "loglevel": loglevel, "workers": workers, "bind": bind, "graceful_timeout": graceful_timeout, "timeout": timeout, "keepalive": keepalive, "errorlog": errorlog, "accesslog": accesslog, # Additional, non-gunicorn variables "workers_per_core": workers_per_core, "use_max_workers": use_max_workers, "host": host, "port": port, } print(json.dumps(log_data)) ================================================ FILE: ethtx_ce/log_cfg.json ================================================ { "version": 1, "disable_existing_loggers": false, "formatters": { "extra": { "format": "%(asctime)s.%(msecs)03d %(levelname)-8s [%(filename)s:%(lineno)d - %(funcName)s %(process)d/%(processName)s] %(message)s", "datefmt": "%Y-%m-%d %H:%M:%S" } }, "handlers": { "console": { "class": "logging.StreamHandler", "level": "DEBUG", "formatter": "extra", "stream": "ext://sys.stdout" }, "file_handler": { "class": "logging.handlers.RotatingFileHandler", "level": "INFO", "formatter": "extra", "filename": "ethtx_ce.log", "maxBytes": 5242880, "backupCount": 5, "encoding": "utf8" } }, "root": { "level": "INFO", "handlers": [ "file_handler", "console" ] } } ================================================ FILE: ethtx_ce/start-reload.sh ================================================ #! /usr/bin/env sh set -e # Start Gunicorn echo "Starting Gunicorn..." exec pipenv run gunicorn "--reload" -c "$GUNICORN_CONF" "$APP_MODULE" ================================================ FILE: ethtx_ce/start.sh ================================================ #! /usr/bin/env sh set -e # Start Gunicorn echo "Starting Gunicorn..." exec pipenv run gunicorn -c "$GUNICORN_CONF" "$APP_MODULE" ================================================ FILE: ethtx_ce/tests/flask_test.py ================================================ import pytest from app.frontend import create_app class TestFlask: @pytest.fixture def client(self): ethtx = None app = create_app(ethtx) with app.test_client() as client: yield client def test_landing_page(self, client): resp = client.get("/") assert resp.status_code == 200 # it's a dump check, but it's something assert len(resp.data) > 500 def test_semantics_is_secured_with_basic_auth(self, client): tx_hash = "0xf9baa1792d644bbda985324a0bfdc052a806ca1a4b6a3df3578c73025f7fe544" url = f"/semantics/mainnet/{tx_hash}/" resp = client.get(url) assert resp.status_code == 401 ================================================ FILE: ethtx_ce/tests/mocks/__init__.py ================================================ ================================================ FILE: ethtx_ce/tests/mocks/mocks.py ================================================ from typing import Dict from ethtx.models.w3_model import W3Transaction, W3Block, W3Receipt, W3Log from ethtx.utils.attr_dict import AttrDict from hexbytes import HexBytes class MockWeb3Provider: blocks = { 1: { "difficulty": 123, # int "extraData": "test", # HexBytes "gasLimit": 123, # int "gasUsed": 123, # int "hash": HexBytes( b"\x88\xe9mE7\xbe\xa4\xd9\xc0]\x12T\x99\x07\xb3%a\xd3\xbf1\xf4Z\xaesL\xdc\x11\x9f\x13@l\xb6" ), # str "logsBloom": "test", # HexBytes "miner": "test", # str "nonce": "test", # HexBytes "number": 123, # int "parentHash": HexBytes( b"\x88\xe9mE7\xbe\xa4\xd9\xc0]\x12T\x99\x07\xb3%a\xd3\xbf1\xf4Z\xaesL\xdc\x11\x9f\x13@l\xb6" ), # str "receiptsRoot": "test", # HexBytes "sha3Uncles": "test", # HexBytes "size": 123, # int "stateRoot": "test", # HexBytes "timestamp": 123, # int, "totalDifficulty": 123, # int "transactions": [], # List "transactionsRoot": "test", # HexBytes "uncles": [], # List } } txs = { "0xd7701a0fc05593aee3a16f20cab605db7183f752ae942cc75fd0975feaf1072e": { "blockHash": HexBytes( b"\x88\xe9mE7\xbe\xa4\xd9\xc0]\x12T\x99\x07\xb3%a\xd3\xbf1\xf4Z\xaesL\xdc\x11\x9f\x13@l\xb6" ), # str "blockNumber": 1, # int "from_address": "fromasd", # str "gas": 420, # int "gasPrice": 1, # int "hash": "co", # HexBytes, "input": "jeszcze jak", # str "nonce": 123, # int "r": "ds", # HexBytes "s": "sdf", # HexBytes "to": "sdf", # str "transactionIndex": 1, # int "v": 1, # int "value": 1, # int } } def add_mocked_block_details(self, block_number, block_details: Dict): self.blocks[block_number] = block_details def get_transaction(self, tx_hash): return W3Transaction(**self.txs[tx_hash]) def get_transaction_receipt(self, tx_hash): log_values = AttrDict( { "address": "test", # str "blockHash": "test", # HexBytes "blockNumber": 123, # int "data": "test", # str "logIndex": 132, # int "removed": False, # bool, "topics": [HexBytes("d")], # List[HexBytes] "transactionHash": "test", # HexBytes "transactionIndex": 123, # int } ) values = { "blockHash": "test", # HexBytes "blockNumber": 123, # int "contractAddress": 123, # str "cumulativeGasUsed": 132, # int, "from_address": "from", # str "gasUsed": 123, # int "logs": [log_values], # List "logsBloom": "test", # HexBytes "root": "test", # str "status": 123, # int, "to_address": "test", # str "transactionHash": "test", # HexBytes "transactionIndex": 123, # int } return W3Receipt(**values) def get_block(self, block_number: int) -> W3Block: return W3Block(**self.blocks[block_number]) class Mocks: @staticmethod def get_mocked_w3_log(): log_data = { "address": "test", "blockHash": "test_block_hash", "blockNumber": 123, "data": "data", "logIndex": 1, "removed": False, "topics": [1, 2, 3, 4], "transactionHash": "txHash", "transactionIndex": 1, } log = W3Log(**log_data) return log class TestMocks: def test_can_create_w3_log(self): w3log = Mocks.get_mocked_w3_log() assert w3log is not None ================================================ FILE: scripts/git_version_for_docker.sh ================================================ #! /usr/bin/env bash # Execute in root project directory # Exit in case of error set -e remote_url=$(git config --get remote.origin.url) sha=$(git rev-parse HEAD) # set env variables export GIT_URL="${remote_url}" export GIT_SHA="${sha}" # url, sha url_sha="${remote_url},${sha}" # return echo "$url_sha"