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

2 | EthTx Community Edition 3 |

4 |
5 |

6 | Community version of EthTx transaction decoder 7 |
8 | https://ethtx.info 9 |

10 |

11 | 12 | Python 13 | 14 | 15 | Black 16 | 17 | 18 | OpenSource 19 | 20 | 21 | Apache 22 | 23 |

24 | 25 | --- 26 | 27 | # Description 28 | 29 | This project represents usage of [EthTx](https://github.com/ethtx/ethtx) decoding library in form of a website. If you 30 | are looking for implementation of the said decoding functionalities, please refer 31 | to [EthTx](https://github.com/ethtx/ethtx) repository. 32 | 33 | # Local environment 34 | 35 | Here is a list of steps to recreate local environment on Ubuntu distribution. 36 | 37 | 1. Install needed packages using `apt`: 38 | 39 | ```shell 40 | apt install docker-compose python3-pip python3-dev pipenv make 41 | apt install libxml2-dev libxslt1-dev gcc 42 | ``` 43 | 2. Run: 44 | 45 | ```shell 46 | pipenv install 47 | ``` 48 | 49 | 3. Copy `.env_sample` to `.env` and fill required field according to description 50 | 51 | ``` 52 | # Proper nodes are required to run ethtx, provide connection strings for chains which will be used. 53 | MAINNET_NODE_URL=https://geth-erigon-node:8545 54 | # KOVAN_NODE_URL= 55 | # RINKEBY_NODE_URL= 56 | 57 | # EthTx supports multiple nodes, if one is unavailable, it will use others. You only need to specify them with a comma 58 | # Example: MAINNET_NODE_URL=https://geth-erigon-node:8545,https://geth1-erigon-node:8545 59 | 60 | 61 | # Etherscan API is used to get contract source code, required for decoding process 62 | # You can get free key here https://etherscan.io/apis 63 | ETHERSCAN_KEY= 64 | 65 | # Optional. Those represent data required for connecting to mongoDB. It's used for caching semantics 66 | # used in decoding process. But, it's not neccessary for running, If you don't want to use permanent 67 | # db or setup mongo, leave those values, mongomock package is used to simulate in-memory mongo. 68 | MONGO_CONNECTION_STRING=mongomock://localhost/ethtx 69 | 70 | # Optional. Credentials for accessing semantics editor page, available under '/semantics/' 71 | ETHTX_ADMIN_USERNAME=admin 72 | ETHTX_ADMIN_PASSWORD=admin 73 | 74 | # Optional. Api key used for exposing 75 | API_KEY= 76 | 77 | # Optional. Valid values are ['production', 'staging', 'development']. Those mainly 78 | # dictate what options are used for flask debugging and logging 79 | ENV=development 80 | ``` 81 | 82 | 4. Run 83 | ```shell 84 | PYTHONPATH=./ethtx_ce FLASK_APP=ethtx_ce/app/wsgi.py pipenv run flask run --host=0.0.0.0 --port 5000 85 | ``` 86 | or 87 | ```shell 88 | make run-local 89 | ``` 90 | This will setup new server on host 0.0.0.0 port 5000. 91 | 5. Now `ethtx_ce` should be accessible through link [http://localhost:5000](http://localhost:5000) 92 | 93 | Use can also provided `docker-compose` for running this locally: 94 | 95 | ```shell 96 | docker-compose up 97 | ``` 98 | 99 | Note, this also need proper `.env` file to function properly. 100 | 101 | # .env file 102 | 103 | For proper functioning, `.env` file is required containing all database and 3rd party providers configuration. 104 | `.env_sample` file is provided in repository with example values. 105 | 106 | Parameters `[CHAIN_ID]_NODE_URL` should hold valid urls to ethereum nodes; Parameter `ETHERSCAN_KEY` should be equal to 107 | Etherscan API key assigned to user. 108 | 109 | # API 110 | 111 | The EthTx APIs are provided as a community service and without warranty, so please use what you need and no more. We 112 | support `GET` requests. 113 | 114 | * **Decode transaction** 115 | 116 | Returns decoded EthTx transaction, based on `chain_id` and transaction hash `tx_hash` 117 | 118 | * **URL** 119 | ```shell 120 | /api/transactions/CHAIN_ID/TX_HASH 121 | ``` 122 | * **Method** 123 | `GET` 124 | * **Authorization** 125 | * Required: 126 | header: `x-api-key=[string]` **OR** query parameter: `api_key=[string]` 127 | * **URL Params** 128 | * Required: `chain_id=[string]`,`tx_hash=[string]` 129 | * **Example** 130 | ```shell 131 | curl --location --request GET 'http://0.0.0.0:5000/api/transactions/dsad/asd' \ 132 | --header 'x-api-key: 05a2212d-9985-48d2-b54f-0fbc5ba28766' 133 | ``` 134 | 135 | 136 | * **Get Raw Semantic** 137 | 138 | Returns raw semantic based on `chain_id` and sender/receiver `address` 139 | 140 | * **URL** 141 | ```shell 142 | /api/semantics/CHAIN_ID/ADDRESS 143 | ``` 144 | * **Method** 145 | `GET` 146 | * **Authorization** 147 | * Required: 148 | header: `x-api-key=[string]` **OR** query parameter: `api_key=[string]` 149 | * **URL Params** 150 | * Required:`chain_id=[string]`,`address=[string]` 151 | * **Example** 152 | ```shell 153 | curl --location --request GET 'http://0.0.0.0:5000/api/semantics/dsad/asd' \ 154 | --header 'x-api-key: 05a2212d-9985-48d2-b54f-0fbc5ba28766' 155 | ``` 156 | 157 | * **Info** 158 | 159 | Returns information about the `EthTx` 160 | 161 | * **URL** 162 | ```shell 163 | /api/info 164 | ``` 165 | * **Method** 166 | `GET` 167 | * **Authorization** 168 | * Required: 169 | header: `x-api-key=[string]` **OR** query parameter: `api_key=[string]` 170 | * **URL Params** 171 | * None 172 | * **Example** 173 | ```shell 174 | curl --location --request GET 'http://0.0.0.0:5000/api/info' \ 175 | --header 'x-api-key: 05a2212d-9985-48d2-b54f-0fbc5ba28766' 176 | ``` -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | services: 3 | ethtx_ce: 4 | ports: 5 | - "5000:5000" 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | 10 | command: /start.sh 11 | 12 | mongo: 13 | ports: 14 | - "27017:27017" 15 | mongo-express: 16 | depends_on: 17 | - mongo 18 | ports: 19 | - "8081:8081" -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | 3 | services: 4 | ethtx_ce: 5 | image: 'ethtx_ce:${TAG-latest}' 6 | env_file: 7 | - .env 8 | depends_on: 9 | - mongo 10 | build: 11 | context: . 12 | dockerfile: Dockerfile 13 | 14 | mongo: 15 | image: mongo 16 | environment: 17 | - MONGO_INITDB_DATABASE=${MONGODB_DB} 18 | 19 | mongo-express: 20 | image: mongo-express 21 | environment: 22 | - ME_CONFIG_MONGODB_SERVER=mongo 23 | - ME_CONFIG_MONGODB_PORT=27017 24 | - ME_CONFIG_MONGODB_ENABLE_ADMIN=false 25 | - ME_CONFIG_MONGODB_AUTH_DATABASE=${MONGODB_DB} 26 | - ME_CONFIG_BASICAUTH_USERNAME=${MONGOEXPRESS_LOGIN} 27 | - ME_CONFIG_BASICAUTH_PASSWORD=${MONGOEXPRESS_PASSWORD} 28 | depends_on: 29 | - mongo 30 | -------------------------------------------------------------------------------- /ethtx_ce/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 130 3 | exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache 4 | extend-ignore = 5 | # See https://github.com/PyCQA/pycodestyle/issues/373 6 | E203, F401, F403, F405 7 | -------------------------------------------------------------------------------- /ethtx_ce/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | app.egg-info 3 | -------------------------------------------------------------------------------- /ethtx_ce/app/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | -------------------------------------------------------------------------------- /ethtx_ce/app/api/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | 17 | from functools import wraps 18 | from typing import Dict, Type, Callable, Union, Optional 19 | 20 | from ethtx import EthTx 21 | from flask import Blueprint 22 | from flask import Flask 23 | 24 | from .. import factory 25 | from .decorators import auth_required 26 | from ..helpers import read_ethtx_versions 27 | 28 | 29 | def create_app( 30 | engine: EthTx, settings_override: Optional[Union[Dict, Type]] = None 31 | ) -> Flask: 32 | """Returns API application instance.""" 33 | 34 | app = factory.create_app(__name__, __path__, settings_override) 35 | app.name = "ethtx_ce/api" 36 | 37 | app.ethtx = engine # init ethtx engine 38 | read_ethtx_versions(app) 39 | 40 | return app 41 | 42 | 43 | def api_route(bp: Blueprint, *args, **kwargs): 44 | kwargs.setdefault("strict_slashes", False) 45 | 46 | def decorator(f: Callable): 47 | @bp.route(*args, **kwargs) 48 | @auth_required 49 | @wraps(f) 50 | def wrapper(*args, **kwargs): 51 | sc = 200 52 | rv = f(*args, **kwargs) 53 | if isinstance(rv, tuple): 54 | sc = rv[1] 55 | rv = rv[0] 56 | return rv, sc 57 | 58 | f.__name__ = str(id(f)) + f.__name__ 59 | return f 60 | 61 | return decorator 62 | 63 | 64 | # avoid circular 65 | from .endpoints import * 66 | from .exceptions import exceptions_bp 67 | -------------------------------------------------------------------------------- /ethtx_ce/app/api/decorators.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | 17 | import logging 18 | from functools import wraps 19 | from typing import Callable, Optional 20 | 21 | import jsonpickle 22 | from flask import request, current_app, jsonify 23 | 24 | from ..exceptions import ( 25 | AuthorizationError, 26 | PayloadTooLarge, 27 | UnexpectedError, 28 | InternalError, 29 | ) 30 | from .utils import enable_direct, delete_bstrings 31 | 32 | log = logging.getLogger(__name__) 33 | 34 | jsonpickle.set_decoder_options('simplejson', use_decimal=True) 35 | 36 | 37 | def auth_required(func: Callable): 38 | """api key verification.""" 39 | 40 | @wraps(func) 41 | def check_auth(**kwargs): 42 | api_key = request.headers.get("x-api-key") or request.args.get("api_key") 43 | if api_key != current_app.config.get("API_KEY"): 44 | raise AuthorizationError(api_key) 45 | 46 | return func(**kwargs) 47 | 48 | return check_auth 49 | 50 | 51 | def response(status: Optional[int] = 200): 52 | """ 53 | Return response with: 54 | :param status: response status code, default: `200` 55 | """ 56 | 57 | def _response(f: Callable): 58 | @wraps(f) 59 | def wrapped(*args, **kwargs): 60 | func = f(*args, **kwargs) 61 | 62 | try: 63 | data = jsonify( 64 | delete_bstrings( 65 | jsonpickle.decode( 66 | jsonpickle.encode(func, make_refs=False, unpicklable=False, use_decimal=True) 67 | ) 68 | ) 69 | ) 70 | except TypeError as e: 71 | log.critical("Response cannot be serialized. %s", e) 72 | raise InternalError() 73 | except Exception as e: 74 | log.exception(e) 75 | raise UnexpectedError() 76 | 77 | return data, status 78 | 79 | return wrapped 80 | 81 | return _response 82 | 83 | 84 | @enable_direct 85 | def limit_content_length(max_length: Optional[int] = None): 86 | """ 87 | Limit content length. If not given: 88 | The priority has app MAX_CONTENT_LENGTH value. 89 | """ 90 | 91 | def decorator(f): 92 | @wraps(f) 93 | def wrapper(*args, **kwargs): 94 | cl = request.content_length 95 | app_max_length = current_app.config.get("MAX_CONTENT_LENGTH") 96 | max_content_length = max_length if max_length else app_max_length 97 | 98 | if cl is not None and cl > max_content_length: 99 | raise PayloadTooLarge( 100 | content_length=cl, max_content_length=max_content_length 101 | ) 102 | return f(*args, **kwargs) 103 | 104 | return wrapper 105 | 106 | return decorator 107 | -------------------------------------------------------------------------------- /ethtx_ce/app/api/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | 17 | from .info import info_bp 18 | from .semantics import semantics_bp 19 | from .transactions import transactions_bp 20 | -------------------------------------------------------------------------------- /ethtx_ce/app/api/endpoints/info.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | 17 | from flask import Blueprint, current_app 18 | 19 | from .. import api_route 20 | from ..decorators import response 21 | from ...helpers import get_latest_ethtx_version 22 | 23 | info_bp = Blueprint("api_info", __name__) 24 | 25 | 26 | @api_route(info_bp, "/info") 27 | @response(200) 28 | def read_info(): 29 | """Get info.""" 30 | ethtx_version = current_app.config["ethtx_version"] 31 | latest_ethtx_version = get_latest_ethtx_version() 32 | 33 | ethtx_ce_version = current_app.config["ethtx_ce_version"] 34 | 35 | return { 36 | "ethtx": { 37 | "version": ethtx_version, 38 | "is_latest": ethtx_version == latest_ethtx_version, 39 | }, 40 | "ethtx_ce": { 41 | "version": ethtx_ce_version, 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /ethtx_ce/app/api/endpoints/semantics.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | 17 | from typing import Optional 18 | 19 | from flask import Blueprint, current_app 20 | 21 | from .. import api_route 22 | from ..decorators import response 23 | 24 | semantics_bp = Blueprint("api_semantics", __name__) 25 | 26 | 27 | @api_route(semantics_bp, "/semantics/") 28 | @api_route(semantics_bp, "/semantics//") 29 | @response(200) 30 | def read_raw_semantic(address: str, chain_id: Optional[str] = None): 31 | """Get raw semantic.""" 32 | raw_semantics = current_app.ethtx.semantics.get_semantics( 33 | chain_id=chain_id, address=address 34 | ) 35 | return raw_semantics.dict() 36 | -------------------------------------------------------------------------------- /ethtx_ce/app/api/endpoints/transactions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | 17 | import logging 18 | from typing import Optional 19 | 20 | from flask import Blueprint, current_app 21 | 22 | from .. import api_route 23 | from ..decorators import response 24 | 25 | log = logging.getLogger(__name__) 26 | transactions_bp = Blueprint("api_transactions", __name__) 27 | 28 | 29 | @api_route(transactions_bp, "/transactions/") 30 | @api_route(transactions_bp, "/transactions//") 31 | @response(200) 32 | def read_decoded_transaction(tx_hash: str, chain_id: Optional[str] = None): 33 | """Decode transaction.""" 34 | tx_hash = tx_hash if tx_hash.startswith("0x") else "0x" + tx_hash 35 | 36 | chain_id = chain_id or current_app.ethtx.default_chain 37 | decoded_transaction = current_app.ethtx.decoders.decode_transaction( 38 | chain_id=chain_id, tx_hash=tx_hash 39 | ) 40 | decoded_transaction.metadata.timestamp = ( 41 | decoded_transaction.metadata.timestamp.strftime("%Y-%m-%d %H:%M:%S") 42 | ) 43 | return decoded_transaction.dict() 44 | -------------------------------------------------------------------------------- /ethtx_ce/app/api/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | 17 | import datetime 18 | import logging 19 | from dataclasses import dataclass 20 | from http.client import responses 21 | from typing import Tuple, TypeVar 22 | 23 | from ethtx import exceptions as ethtx_exceptions 24 | from flask import Blueprint, request 25 | from web3.exceptions import TransactionNotFound 26 | from werkzeug.exceptions import HTTPException 27 | 28 | from .utils import as_dict 29 | from ..exceptions import * 30 | 31 | log = logging.getLogger(__name__) 32 | 33 | exceptions_bp = Blueprint("exceptions", __name__) 34 | 35 | 36 | @as_dict 37 | @dataclass 38 | class BaseRequestException: 39 | """Base Request Exception""" 40 | 41 | status: int 42 | error: str 43 | path: str 44 | message: str = "" 45 | timestamp: datetime.datetime.utcnow = None 46 | 47 | def __post_init__(self): 48 | """Post init values.""" 49 | self.error = str(self.error) 50 | self.message = responses[self.status] 51 | self.timestamp = datetime.datetime.utcnow() 52 | 53 | 54 | BaseErrorType = TypeVar("BaseErrorType", bound=Tuple[BaseRequestException, int]) 55 | 56 | 57 | @exceptions_bp.app_errorhandler(HTTPException) 58 | def handle_all_http_exceptions(error: HTTPException) -> BaseErrorType: 59 | """All HTTP Exceptions handler.""" 60 | return BaseRequestException(error.code, error.description, request.path), error.code 61 | 62 | 63 | @exceptions_bp.app_errorhandler(ethtx_exceptions.NodeConnectionException) 64 | def node_connection_error(error) -> BaseErrorType: 65 | """EthTx - Node connection error.""" 66 | return BaseRequestException(500, error, request.path), 500 67 | 68 | 69 | @exceptions_bp.app_errorhandler(ethtx_exceptions.ProcessingException) 70 | def processing_error(error) -> BaseErrorType: 71 | """EthTx - Processing error.""" 72 | return BaseRequestException(500, error, request.path), 500 73 | 74 | 75 | @exceptions_bp.app_errorhandler(ethtx_exceptions.InvalidTransactionHash) 76 | def invalid_transaction_hash(error) -> BaseErrorType: 77 | """EthTx - Invalid transaction hash.""" 78 | return BaseRequestException(400, error, request.path), 400 79 | 80 | 81 | @exceptions_bp.app_errorhandler(TransactionNotFound) 82 | def transaction_not_found(error) -> BaseErrorType: 83 | """Could not find transaction.""" 84 | return BaseRequestException(404, error, request.path), 404 85 | 86 | 87 | @exceptions_bp.app_errorhandler(AuthorizationError) 88 | def authorization_error(error) -> BaseErrorType: 89 | """Unauthorized request.""" 90 | return BaseRequestException(401, error, request.path), 401 91 | 92 | 93 | @exceptions_bp.app_errorhandler(MalformedRequest) 94 | def malformed_request(error) -> BaseErrorType: 95 | """Wrong request.""" 96 | return BaseRequestException(400, error, request.path), 400 97 | 98 | 99 | @exceptions_bp.app_errorhandler(PayloadTooLarge) 100 | def payload_too_large(error) -> BaseErrorType: 101 | """Payload is too large.""" 102 | return BaseRequestException(413, error, request.path), 413 103 | 104 | 105 | @exceptions_bp.app_errorhandler(ResourceLockedError) 106 | def resource_locked_error(error) -> BaseErrorType: 107 | """Resource is locked.""" 108 | return BaseRequestException(423, error, request.path), 423 109 | 110 | 111 | @exceptions_bp.app_errorhandler(Exception) 112 | def unexpected_error(error) -> BaseErrorType: 113 | """Unexpected error.""" 114 | log.exception(str(error)) 115 | 116 | return BaseRequestException(500, str(UnexpectedError()), request.path), 500 117 | -------------------------------------------------------------------------------- /ethtx_ce/app/api/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | 17 | from dataclasses import asdict 18 | from functools import wraps 19 | from typing import Dict 20 | from decimal import Decimal 21 | 22 | 23 | def enable_direct(decorator): 24 | """Decorator direct helper.""" 25 | 26 | @wraps(decorator) 27 | def wrapper(*args, **kwargs): 28 | f = args[0] 29 | if callable(f): 30 | return decorator()(f) # pass the function to be decorated 31 | else: 32 | return decorator(*args, **kwargs) # pass the specified params 33 | 34 | return wrapper 35 | 36 | 37 | def as_dict(cls): 38 | """Return object as dict.""" 39 | 40 | def wrapper(*args, **kwargs) -> Dict: 41 | instance = cls(*args, **kwargs) 42 | return asdict(instance) 43 | 44 | return wrapper 45 | 46 | 47 | def delete_bstrings(obj): 48 | primitive = (str, bool, float, type(None)) 49 | 50 | if isinstance(obj, primitive): 51 | return obj 52 | elif isinstance(obj, int): 53 | return str(obj) 54 | elif isinstance(obj, Decimal): 55 | if obj == obj.to_integral_value(): 56 | return str(obj) 57 | else: 58 | obj 59 | elif type(obj) == bytes: 60 | return obj.decode() 61 | elif type(obj) == list: 62 | for index, value in enumerate(obj): 63 | obj[index] = delete_bstrings(value) 64 | elif type(obj) == dict: 65 | for index, value in obj.items(): 66 | obj[index] = delete_bstrings(value) 67 | else: 68 | raise Exception("Unknown type:" + str(type(obj))) 69 | 70 | return obj 71 | -------------------------------------------------------------------------------- /ethtx_ce/app/config.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | 17 | import os 18 | 19 | from dotenv import load_dotenv, find_dotenv 20 | 21 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 22 | load_dotenv(find_dotenv(filename="../../.env")) 23 | 24 | 25 | class Config: 26 | """Base Config.""" 27 | 28 | LOGGING_CONFIG = os.environ.get( 29 | "LOGGING_CONFIG", os.path.join(BASE_DIR, "../log_cfg.json") 30 | ) 31 | LOGGING_LOG_PATH = os.environ.get( 32 | "LOGGING_CONFIG", os.path.join(BASE_DIR, "../../tmp") 33 | ) 34 | 35 | API_KEY = os.getenv("API_KEY", "") 36 | MAX_CONTENT_LENGTH = 10 * 1024 * 1024 37 | 38 | ETHTX_ADMIN_USERNAME = os.getenv("ETHTX_ADMIN_USERNAME") 39 | ETHTX_ADMIN_PASSWORD = os.getenv("ETHTX_ADMIN_PASSWORD") 40 | 41 | 42 | class ProductionConfig(Config): 43 | """Production Config.""" 44 | 45 | ENV = "production" 46 | FLASK_DEBUG = False 47 | TESTING = False 48 | PROPAGATE_EXCEPTIONS = True 49 | 50 | 51 | class StagingConfig(Config): 52 | """Staging Config.""" 53 | 54 | ENV = "staging" 55 | FLASK_DEBUG = True 56 | TESTING = False 57 | PROPAGATE_EXCEPTIONS = True 58 | 59 | 60 | class DevelopmentConfig(Config): 61 | """Development Config.""" 62 | 63 | ENV = "development" 64 | FLASK_DEBUG = True 65 | TESTING = True 66 | PROPAGATE_EXCEPTIONS = True 67 | -------------------------------------------------------------------------------- /ethtx_ce/app/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | 17 | from typing import Optional, Union 18 | 19 | __all__ = [ 20 | "AuthorizationError", 21 | "MalformedRequest", 22 | "PayloadTooLarge", 23 | "MethodNotAllowed", 24 | "ResourceLockedError", 25 | "InternalError", 26 | "UnexpectedError", 27 | "FactoryAppException", 28 | "EmptyResponseError", 29 | ] 30 | 31 | 32 | class FactoryAppException(Exception): 33 | """Basic Factory App exception.""" 34 | 35 | 36 | class UnexpectedError(Exception): 37 | """Internal Server Error.""" 38 | 39 | def __init__(self): 40 | super().__init__("Unexpected Error") 41 | 42 | 43 | class RequestError(Exception): 44 | """Request Error - basic class.""" 45 | 46 | 47 | class AuthorizationError(RequestError): 48 | """Unauthorized requests.""" 49 | 50 | def __init__(self, msg: Optional[str] = None): 51 | super().__init__( 52 | f"The provided api key is invalid : {msg}." 53 | if msg 54 | else "Api key is missing." 55 | ) 56 | 57 | 58 | class MalformedRequest(RequestError): 59 | """Malformed Request Error.""" 60 | 61 | def __init__(self, msg): 62 | super().__init__(msg) 63 | 64 | 65 | class PayloadTooLarge(RequestError): 66 | """Payload too large Error.""" 67 | 68 | def __init__(self, content_length: Union[float, int], max_content_length: int): 69 | super().__init__( 70 | f"The request is larger than the server is willing or able to process." 71 | f" Request length: {content_length}, but allowed is: {max_content_length}." 72 | ) 73 | 74 | 75 | class MethodNotAllowed(RequestError): 76 | """Method not allowed.""" 77 | 78 | def __init__(self, method: str): 79 | super().__init__(f"Method: {method} not allowed.") 80 | 81 | 82 | class ResourceLockedError(RequestError): 83 | """Resource is locked.""" 84 | 85 | def __init__(self): 86 | super().__init__("The resource that is being accessed is locked.") 87 | 88 | 89 | class EmptyResponseError(RequestError): 90 | """Response is empty.""" 91 | 92 | def __init__(self, msg: str): 93 | super().__init__(msg) 94 | 95 | 96 | class InternalError(RequestError): 97 | """Validation Error""" 98 | 99 | def __init__(self): 100 | super().__init__( 101 | "The request was well-formed but server could not properly decode transaction." 102 | ) 103 | -------------------------------------------------------------------------------- /ethtx_ce/app/factory.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | 17 | import os 18 | from typing import Optional, Dict 19 | 20 | from flask import Flask 21 | 22 | from .config import Config 23 | from .helpers import class_import, register_blueprints 24 | from .logger import setup_logging 25 | 26 | env = os.getenv("ENV", "development").capitalize() 27 | config_class = f"app.config.{env}Config" 28 | config: Config = class_import(config_class) 29 | 30 | 31 | def create_app( 32 | package_name: str, 33 | package_path: str, 34 | settings_override: Optional[Dict] = None, 35 | **app_kwargs, 36 | ) -> Flask: 37 | """ 38 | Returns a :class:`Flask` application instance 39 | :param package_name: application package name 40 | :param package_path: application package path 41 | :param settings_override: a dictionary of settings to override 42 | :param app_kwargs: additional app kwargs 43 | """ 44 | app = Flask(__name__, instance_relative_config=True, **app_kwargs) 45 | 46 | app.config.from_object(config) 47 | setup_logging(app=app) 48 | app.config.from_object(settings_override) 49 | 50 | register_blueprints(app, package_name, package_path) 51 | 52 | return app 53 | -------------------------------------------------------------------------------- /ethtx_ce/app/frontend/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | 17 | from functools import wraps 18 | from typing import Callable, Dict, Optional, Union, Type 19 | 20 | from ethtx import EthTx 21 | from flask import Blueprint, Flask 22 | 23 | 24 | from .. import factory 25 | from ..helpers import read_ethtx_versions 26 | 27 | 28 | def create_app( 29 | engine: EthTx, settings_override: Optional[Union[Dict, Type]] = None 30 | ) -> Flask: 31 | """Returns Frontend app instance.""" 32 | app = factory.create_app( 33 | __name__, 34 | __path__, 35 | settings_override, 36 | template_folder="frontend/templates", 37 | static_folder="frontend/static", 38 | ) 39 | app.name = "ethtx_ce/frontend" 40 | 41 | app.jinja_env.trim_blocks = True 42 | app.jinja_env.lstrip_blocks = True 43 | 44 | app.ethtx = engine # init ethtx engine 45 | read_ethtx_versions(app) 46 | 47 | return app 48 | 49 | 50 | def frontend_route(bp: Blueprint, *args, **kwargs): 51 | """Route in blueprint context.""" 52 | 53 | def decorator(f: Callable): 54 | @bp.route(*args, **kwargs) 55 | @wraps(f) 56 | def wrapper(*args, **kwargs): 57 | return f(*args, **kwargs) 58 | 59 | f.__name__ = str(id(f)) + f.__name__ 60 | return f 61 | 62 | return decorator 63 | -------------------------------------------------------------------------------- /ethtx_ce/app/frontend/deps.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | 17 | import json 18 | import logging 19 | import re 20 | import time 21 | from secrets import compare_digest 22 | from typing import Optional 23 | 24 | import requests 25 | from flask import request 26 | from flask_httpauth import HTTPBasicAuth 27 | 28 | from ..config import Config 29 | 30 | log = logging.getLogger(__name__) 31 | 32 | auth = HTTPBasicAuth() 33 | 34 | eth_price: Optional[float] = None 35 | eth_price_update: Optional[float] = None 36 | 37 | 38 | @auth.verify_password 39 | def verify_password(username: str, password: str) -> bool: 40 | """Verify user, return bool.""" 41 | return username == Config.ETHTX_ADMIN_USERNAME and compare_digest( 42 | password, Config.ETHTX_ADMIN_PASSWORD 43 | ) 44 | 45 | 46 | def get_eth_price() -> Optional[float]: 47 | """ 48 | Get current ETH price from coinbase.com 49 | Cache price for 60 seconds. 50 | """ 51 | global eth_price, eth_price_update 52 | 53 | current_time = time.time() 54 | if ( 55 | eth_price is None 56 | or eth_price_update is None 57 | or (current_time - eth_price_update) > 60 58 | ): 59 | response = requests.get( 60 | "https://api.coinbase.com/v2/prices/ETH-USD/buy", timeout=2 61 | ) 62 | if response.status_code == 200: 63 | eth_price = float(json.loads(response.content)["data"]["amount"]) 64 | eth_price_update = time.time() 65 | 66 | return eth_price 67 | 68 | 69 | def extract_tx_hash_from_req() -> str: 70 | """Extract tx hash from request url.""" 71 | hash_match = re.findall(r"(0x)?([A-Fa-f0-9]{64})", request.url) 72 | 73 | return ( 74 | f"{hash_match[0][0]}{hash_match[0][1]}" 75 | if hash_match and len(hash_match[0]) == 2 76 | else "" 77 | ) 78 | -------------------------------------------------------------------------------- /ethtx_ce/app/frontend/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | 17 | import logging 18 | from functools import wraps 19 | from typing import Callable, Optional 20 | 21 | from ethtx import exceptions as ethtx_exceptions 22 | from flask import Blueprint, render_template 23 | from web3.exceptions import TransactionNotFound 24 | from werkzeug.exceptions import HTTPException 25 | 26 | from .deps import extract_tx_hash_from_req 27 | from ..exceptions import * 28 | 29 | log = logging.getLogger(__name__) 30 | 31 | exceptions_bp = Blueprint("exceptions", __name__) 32 | 33 | 34 | def render_error_page(status: Optional[int] = 500): 35 | """Render error page.""" 36 | 37 | def _render_error_page(f: Callable): 38 | @wraps(f) 39 | def wrapper(*args, **kwargs): 40 | error = f(*args, **kwargs) 41 | status_code = status 42 | if isinstance(error, HTTPException): 43 | error, status_code = error.description, error.code 44 | return ( 45 | render_template( 46 | "exception.html", 47 | status_code=status_code, 48 | error=error, 49 | tx_hash=extract_tx_hash_from_req(), 50 | ), 51 | status_code, 52 | ) 53 | 54 | return wrapper 55 | 56 | return _render_error_page 57 | 58 | 59 | @exceptions_bp.app_errorhandler(HTTPException) 60 | @render_error_page() 61 | def handle_all_http_exceptions(error: HTTPException) -> HTTPException: 62 | """All HTTP Exceptions handler.""" 63 | return error 64 | 65 | 66 | @exceptions_bp.app_errorhandler(ethtx_exceptions.NodeConnectionException) 67 | @render_error_page(500) 68 | def node_connection_error(error) -> str: 69 | """EthTx - Node connection error.""" 70 | return error 71 | 72 | 73 | @exceptions_bp.app_errorhandler(ethtx_exceptions.ProcessingException) 74 | @render_error_page(500) 75 | def processing_error(error) -> str: 76 | """EthTx - Processing error.""" 77 | return error 78 | 79 | 80 | @exceptions_bp.app_errorhandler(ethtx_exceptions.InvalidTransactionHash) 81 | @render_error_page(400) 82 | def invalid_transaction_hash(error) -> str: 83 | """EthTx - Invalid transaction hash.""" 84 | return error 85 | 86 | 87 | @exceptions_bp.app_errorhandler(TransactionNotFound) 88 | @render_error_page(404) 89 | def transaction_not_found(error) -> str: 90 | """Could not find transaction.""" 91 | return error 92 | 93 | 94 | @exceptions_bp.app_errorhandler(AuthorizationError) 95 | @render_error_page(401) 96 | def authorization_error(error) -> str: 97 | """Unauthorized request.""" 98 | return error 99 | 100 | 101 | @exceptions_bp.app_errorhandler(MalformedRequest) 102 | @render_error_page(400) 103 | def malformed_request(error) -> str: 104 | """Wrong request.""" 105 | return error 106 | 107 | 108 | @exceptions_bp.app_errorhandler(PayloadTooLarge) 109 | @render_error_page(413) 110 | def payload_too_large(error) -> str: 111 | """Payload is too large.""" 112 | return error 113 | 114 | 115 | @exceptions_bp.app_errorhandler(ResourceLockedError) 116 | @render_error_page(423) 117 | def resource_locked_error(error) -> str: 118 | """Resource is locked.""" 119 | return error 120 | 121 | 122 | @exceptions_bp.app_errorhandler(EmptyResponseError) 123 | @render_error_page(404) 124 | def empty_response(error) -> str: 125 | """Response is empty.""" 126 | return error 127 | 128 | 129 | @exceptions_bp.app_errorhandler(Exception) 130 | @render_error_page(500) 131 | def unexpected_error(error) -> str: 132 | """Unexpected error.""" 133 | log.exception(str(error)) 134 | 135 | return str(UnexpectedError()) 136 | -------------------------------------------------------------------------------- /ethtx_ce/app/frontend/semantics.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | 17 | from __future__ import annotations 18 | 19 | import json 20 | import logging 21 | from typing import Optional, List, Dict 22 | 23 | from ethtx import EthTx 24 | from ethtx.models.semantics_model import ( 25 | AddressSemantics, 26 | ContractSemantics, 27 | ERC20Semantics, 28 | ) 29 | from ethtx.models.semantics_model import ( 30 | EventSemantics, 31 | FunctionSemantics, 32 | TransformationSemantics, 33 | ParameterSemantics, 34 | ) 35 | from flask import Blueprint, render_template, current_app, request, jsonify 36 | from web3 import Web3 37 | 38 | from . import frontend_route 39 | from .deps import auth 40 | from ..exceptions import EmptyResponseError 41 | 42 | bp = Blueprint("semantics", __name__) 43 | 44 | log = logging.getLogger(__name__) 45 | 46 | 47 | @frontend_route(bp, "/semantics//") 48 | @frontend_route(bp, "/semantics///") 49 | @auth.login_required 50 | def semantics(address: str, chain_id: Optional[str] = None) -> show_semantics_page: 51 | raw_semantics = current_app.ethtx.semantics.get_semantics( 52 | chain_id=chain_id or current_app.ethtx._default_chain, address=address 53 | ) 54 | 55 | return show_semantics_page(raw_semantics) 56 | 57 | 58 | @frontend_route(bp, "/reload", methods=["POST"]) 59 | @auth.login_required 60 | def reload_semantics(): 61 | """Reload raw semantic.""" 62 | data = json.loads(request.data) 63 | 64 | ethtx: EthTx = current_app.ethtx 65 | ethtx.semantics.database._addresses.delete_one({"address": data["address"]}) 66 | ethtx.semantics.get_semantics.cache_clear() 67 | ethtx.semantics.get_semantics( 68 | data["chain_id"] if data.get("chain_id") else current_app.ethtx._default_chain, 69 | data["address"], 70 | ) 71 | 72 | return "ok" 73 | 74 | 75 | @frontend_route(bp, "/save", methods=["POST"]) 76 | @auth.login_required 77 | def semantics_save(): 78 | data = json.loads(request.data) 79 | return _semantics_save(data) 80 | 81 | 82 | @frontend_route(bp, "/poke", methods=["POST"]) 83 | @auth.login_required 84 | def poke_abi(): 85 | data = json.loads(request.data) 86 | return _poke_abi(data) 87 | 88 | 89 | def show_semantics_page(data: AddressSemantics) -> render_template: 90 | if data: 91 | 92 | data_dict = data.dict() 93 | 94 | address = data.address 95 | chain_id = data.chain_id 96 | name = data.name or address 97 | 98 | if data.is_contract: 99 | events = data_dict["contract"]["events"] or {} 100 | functions = data_dict["contract"]["functions"] or {} 101 | transformations = data_dict["contract"]["transformations"] or {} 102 | code_hash = data.contract.code_hash 103 | contract_name = data.contract.name 104 | else: 105 | events = {} 106 | functions = {} 107 | transformations = {} 108 | code_hash = "EOA" 109 | contract_name = address 110 | 111 | standard = data.standard 112 | # ToDo: make it more universal 113 | 114 | if standard == "ERC20": 115 | standard_info = data_dict["erc20"] or {} 116 | elif standard == "ERC721": 117 | standard_info = {} 118 | else: 119 | standard_info = {} 120 | 121 | metadata = dict( 122 | label=name, 123 | chain=chain_id, 124 | contract=dict( 125 | name=contract_name, 126 | code_hash=code_hash, 127 | standard=dict(name=standard, data=standard_info), 128 | ), 129 | ) 130 | 131 | return ( 132 | render_template( 133 | "semantics.html", 134 | address=address, 135 | events=events, 136 | functions=functions, 137 | transformations=transformations, 138 | metadata=metadata, 139 | ), 140 | 200, 141 | ) 142 | 143 | raise EmptyResponseError( 144 | "The semantics are empty. It probably means that the given address " 145 | "has not been decoded before or address is incorrect." 146 | ) 147 | 148 | 149 | def _parameters_semantics(parameters: List[Dict]) -> List[ParameterSemantics]: 150 | parameters_semantics_list = [] 151 | if parameters: 152 | for parameter in parameters: 153 | parameters_semantics_list.append( 154 | ParameterSemantics( 155 | parameter_name=parameter.get("parameter_name"), 156 | parameter_type=parameter.get("parameter_type"), 157 | components=parameter.get("components", []), 158 | indexed=parameter.get("indexed", False), 159 | dynamic=parameter.get("dynamic", False), 160 | ) 161 | ) 162 | 163 | return parameters_semantics_list 164 | 165 | 166 | def _semantics_save(data): 167 | try: 168 | address = data.get("address") 169 | metadata = data.get("metadata") 170 | events = data.get("events") 171 | functions = data.get("functions") 172 | transformations = data.get("transformations") 173 | standard_name = None 174 | erc20_semantics = None 175 | 176 | if metadata.get("contract"): 177 | 178 | events_semantics = dict() 179 | functions_semantics = dict() 180 | transformations_semantics = dict() 181 | 182 | for event in events.values(): 183 | events_semantics[event.get("signature")] = EventSemantics( 184 | signature=event.get("signature"), 185 | anonymous=event.get("anonymous"), 186 | name=event.get("name"), 187 | parameters=_parameters_semantics(event.get("parameters")), 188 | ) 189 | 190 | for function in functions.values(): 191 | functions_semantics[function.get("signature")] = FunctionSemantics( 192 | signature=function.get("signature"), 193 | name=function.get("name"), 194 | inputs=_parameters_semantics(function.get("inputs")), 195 | outputs=_parameters_semantics(function.get("outputs")), 196 | ) 197 | 198 | for signature, transformation in transformations.items(): 199 | transformations_semantics[signature] = dict() 200 | for parameter_name, parameter_transformation in transformation: 201 | transformations_semantics[signature][ 202 | parameter_name 203 | ] = TransformationSemantics( 204 | transformed_name=parameter_transformation.get( 205 | "transformed_name" 206 | ), 207 | transformed_type=parameter_transformation.get( 208 | "transformed_type" 209 | ), 210 | transformation=parameter_transformation.get("transformation"), 211 | ) 212 | 213 | standard_name = metadata["contract"]["standard"]["name"] 214 | if standard_name == "ERC20": 215 | erc20_data = metadata["contract"]["standard"].get("data") 216 | if erc20_data: 217 | erc20_semantics = ERC20Semantics( 218 | name=erc20_data.get("name"), 219 | symbol=erc20_data.get("symbol"), 220 | decimals=erc20_data.get("decimals"), 221 | ) 222 | 223 | contract_semantics = ContractSemantics( 224 | code_hash=metadata["contract"].get("code_hash"), 225 | name=metadata["contract"].get("name"), 226 | events=events_semantics, 227 | functions=functions_semantics, 228 | transformations=transformations_semantics, 229 | ) 230 | 231 | else: 232 | contract_semantics = None 233 | 234 | address_semantics = AddressSemantics( 235 | chain_id=metadata.get("chain"), 236 | address=address, 237 | name=metadata.get("label"), 238 | is_contract=contract_semantics is not None, 239 | contract=contract_semantics, 240 | standard=standard_name, 241 | erc20=erc20_semantics, 242 | ) 243 | 244 | current_app.ethtx.semantics.update_semantics(semantics=address_semantics) 245 | current_app.ethtx.semantics.get_semantics.cache_clear() 246 | current_app.ethtx.semantics.get_event_abi.cache_clear() 247 | current_app.ethtx.semantics.get_anonymous_event_abi.cache_clear() 248 | current_app.ethtx.semantics.get_transformations.cache_clear() 249 | current_app.ethtx.semantics.get_function_abi.cache_clear() 250 | current_app.ethtx.semantics.get_constructor_abi.cache_clear() 251 | current_app.ethtx.semantics.check_is_contract.cache_clear() 252 | current_app.ethtx.semantics.get_standard.cache_clear() 253 | 254 | result = "ok" 255 | 256 | except Exception as e: 257 | logging.exception("Semantics save error: %s" % e) 258 | result = "error" 259 | 260 | return jsonify(result=result) 261 | 262 | 263 | def _poke_abi(data): 264 | # helper function decoding contract ABI 265 | def _parse_abi(json_abi): 266 | 267 | # helper function to recursively parse parameters 268 | def _parse_parameters(parameters): 269 | 270 | comp_canonical = "(" 271 | comp_inputs = list() 272 | 273 | for i, parameter in enumerate(parameters): 274 | argument = dict( 275 | parameter_name=parameter["name"], parameter_type=parameter["type"] 276 | ) 277 | 278 | if parameter["type"][:5] == "tuple": 279 | sub_canonical, sub_components = _parse_parameters( 280 | parameter["components"] 281 | ) 282 | comp_canonical += sub_canonical + parameter["type"][5:] 283 | argument["components"] = sub_components 284 | else: 285 | comp_canonical += parameter["type"] 286 | sub_components = [] 287 | 288 | if i < len(parameters) - 1: 289 | comp_canonical += "," 290 | 291 | if ( 292 | parameter["type"] in ("string", "bytes") 293 | or parameter["type"][-2:] == "[]" 294 | ): 295 | argument["dynamic"] = True 296 | elif parameter["type"] == "tuple": 297 | argument["dynamic"] = any(c["dynamic"] for c in sub_components) 298 | else: 299 | argument["dynamic"] = False 300 | 301 | if "indexed" in parameter: 302 | argument["indexed"] = parameter["indexed"] 303 | 304 | comp_inputs.append(argument) 305 | 306 | comp_canonical += ")" 307 | 308 | return comp_canonical, comp_inputs 309 | 310 | functions = dict() 311 | events = dict() 312 | 313 | for item in json_abi: 314 | if "type" in item: 315 | 316 | # parse contract functions 317 | if item["type"] == "constructor": 318 | _, inputs = _parse_parameters(item["inputs"]) 319 | functions["constructor"] = dict( 320 | signature="constructor", 321 | name="constructor", 322 | inputs=inputs, 323 | outputs=[], 324 | ) 325 | elif item["type"] == "fallback": 326 | functions["fallback"] = {} 327 | 328 | elif item["type"] == "function": 329 | canonical, inputs = _parse_parameters(item["inputs"]) 330 | canonical = item["name"] + canonical 331 | function_hash = Web3.sha3(text=canonical).hex() 332 | signature = function_hash[0:10] 333 | 334 | _, outputs = _parse_parameters(item["outputs"]) 335 | 336 | functions[signature] = dict( 337 | signature=signature, 338 | name=item["name"], 339 | inputs=inputs, 340 | outputs=outputs, 341 | ) 342 | 343 | # parse contract events 344 | elif item["type"] == "event": 345 | canonical, parameters = _parse_parameters(item["inputs"]) 346 | canonical = item["name"] + canonical 347 | event_hash = Web3.sha3(text=canonical).hex() 348 | signature = event_hash 349 | 350 | events[signature] = dict( 351 | signature=signature, 352 | name=item["name"], 353 | anonymous=item["anonymous"], 354 | parameters=parameters, 355 | ) 356 | 357 | return functions, events 358 | 359 | try: 360 | 361 | address = data["address"] 362 | chash = data["chash"] 363 | network = data["network"] 364 | name = data["name"] 365 | standard = json.loads(data["standard"]) 366 | abi = json.loads(data["abi"]) 367 | 368 | if abi and abi != []: 369 | 370 | is_contract = True 371 | functions, events = _parse_abi(abi) 372 | 373 | events_semantics = dict() 374 | for event in events.values(): 375 | events_semantics[event.get("signature")] = EventSemantics( 376 | signature=event.get("signature"), 377 | anonymous=event.get("anonymous"), 378 | name=event.get("name"), 379 | parameters=_parameters_semantics(event.get("parameters")), 380 | ) 381 | 382 | functions_semantics = dict() 383 | for function in functions.values(): 384 | functions_semantics[function.get("signature")] = FunctionSemantics( 385 | signature=function.get("signature"), 386 | name=function.get("name"), 387 | inputs=_parameters_semantics(function.get("inputs")), 388 | outputs=_parameters_semantics(function.get("outputs")), 389 | ) 390 | 391 | contract_semantics = ContractSemantics( 392 | code_hash=chash, 393 | name=name, 394 | events=events_semantics, 395 | functions=functions_semantics, 396 | transformations=dict(), 397 | ) 398 | 399 | else: 400 | 401 | is_contract = False 402 | contract_semantics = ContractSemantics( 403 | code_hash="0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", 404 | name="EOA", 405 | events=dict(), 406 | functions=dict(), 407 | transformations=dict(), 408 | ) 409 | 410 | address_semantics = AddressSemantics( 411 | chain_id=network, 412 | address=address, 413 | name=name, 414 | is_contract=is_contract, 415 | contract=contract_semantics, 416 | standard=standard.get("name"), 417 | erc20=ERC20Semantics( 418 | name=standard["data"].get("name"), 419 | symbol=standard["data"].get("symbol"), 420 | decimals=standard["data"].get("decimals"), 421 | ) 422 | if standard.get("name") == "ERC20" 423 | else None, 424 | ) 425 | 426 | current_app.ethtx.semantics.update_semantics(semantics=address_semantics) 427 | current_app.ethtx.semantics.get_semantics.cache_clear() 428 | current_app.ethtx.semantics.get_event_abi.cache_clear() 429 | current_app.ethtx.semantics.get_anonymous_event_abi.cache_clear() 430 | current_app.ethtx.semantics.get_transformations.cache_clear() 431 | current_app.ethtx.semantics.get_function_abi.cache_clear() 432 | current_app.ethtx.semantics.get_constructor_abi.cache_clear() 433 | current_app.ethtx.semantics.check_is_contract.cache_clear() 434 | current_app.ethtx.semantics.get_standard.cache_clear() 435 | 436 | logging.info(f"ABI for {address} decoded.") 437 | 438 | result = "ok" 439 | 440 | except Exception as e: 441 | logging.exception("ABI retrieval error: %s" % e) 442 | result = "error" 443 | 444 | return jsonify(result=result) 445 | -------------------------------------------------------------------------------- /ethtx_ce/app/frontend/static.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce) 2 | # Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded 3 | # in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and limitations under the License. 12 | # 13 | # The product contains trademarks and other branding elements of Token Flow Insights SA which are 14 | # not licensed under the Apache 2.0 license. When using or reproducing the code, please remove 15 | # the trademark and/or other branding elements. 16 | 17 | from flask import render_template, Blueprint, current_app 18 | 19 | from . import frontend_route 20 | 21 | bp = Blueprint("static", __name__) 22 | 23 | 24 | @frontend_route(bp, "/") 25 | def search_page() -> render_template: 26 | """Render search page - index.""" 27 | return ( 28 | render_template( 29 | "index.html", 30 | chains=current_app.ethtx.providers.web3provider.nodes.keys() 31 | if current_app.ethtx 32 | else [], 33 | ethtx_version=current_app.config["ethtx_version"], 34 | ethtx_ce_version=current_app.config["ethtx_ce_version"], 35 | ), 36 | 200, 37 | ) 38 | -------------------------------------------------------------------------------- /ethtx_ce/app/frontend/static/ethtx.new.css: -------------------------------------------------------------------------------- 1 | /*Copyright 2021 DAI FOUNDATION (the original version https://github.com/daifoundation/ethtx_ce)*/ 2 | /*Copyright 2021-2022 Token Flow Insights SA (modifications to the original software as recorded*/ 3 | /*in the changelog https://github.com/EthTx/ethtx/blob/master/CHANGELOG.md)*/ 4 | 5 | /*Licensed under the Apache License, Version 2.0 (the "License");*/ 6 | /*you may not use this file except in compliance with the License.*/ 7 | /*You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0*/ 8 | 9 | /*Unless required by applicable law or agreed to in writing, software distributed under the License is distributed*/ 10 | /*on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.*/ 11 | /*See the License for the specific language governing permissions and limitations under the License.*/ 12 | 13 | /*The product contains trademarks and other branding elements of Token Flow Insights SA which are*/ 14 | /*not licensed under the Apache 2.0 license. When using or reproducing the code, please remove*/ 15 | /*the trademark and/or other branding elements.*/ 16 | 17 | 18 | body { 19 | font-family: monospace; 20 | margin: 5px; 21 | background-color: whitesmoke; 22 | width: 100%; 23 | } 24 | 25 | * { 26 | margin: 0; 27 | padding: 0; 28 | } 29 | 30 | h3 { 31 | margin: 11px; 32 | } 33 | 34 | p { 35 | padding-bottom: 50px; 36 | } 37 | 38 | .my_p { 39 | margin: 2px 2px 2px 11px; 40 | border: 1px #ccc solid; 41 | border-radius: 5px; 42 | padding: 2px 5px; 43 | white-space: nowrap; 44 | display: inline-block; 45 | } 46 | 47 | a:link, 48 | a:visited { 49 | color: black; 50 | text-decoration: none; 51 | } 52 | 53 | .delegate a:link, 54 | .delegate a:visited { 55 | color: darkorange; 56 | text-decoration: none; 57 | } 58 | 59 | .table { 60 | margin-left: 11px; 61 | border: solid 1px #bbbbbb; 62 | border-collapse: collapse; 63 | border-spacing: 0; 64 | } 65 | 66 | .table thead th { 67 | background-color: #ddd; 68 | border: solid 1px #bbbbbb; 69 | color: #444444; 70 | padding: 3px 10px 3px 10px; 71 | text-align: left; 72 | text-shadow: 1px 1px 1px #fff; 73 | } 74 | 75 | .table tbody td { 76 | border: solid 1px #bbbbbb; 77 | color: #333; 78 | padding: 3px 10px 3px 10px; 79 | text-shadow: 1px 1px 1px #fff; 80 | white-space: nowrap; 81 | } 82 | 83 | .content { 84 | width: 500px; 85 | margin: auto; 86 | } 87 | 88 | .transaction-info { 89 | margin: 5px 0 4px 13px; 90 | } 91 | 92 | .transaction-hash { 93 | color: darkgreen; 94 | } 95 | 96 | .events tbody td { 97 | border: 1px #ccc solid; 98 | padding: 5px 10px 2px 10px; 99 | } 100 | 101 | .calls table { 102 | border: transparent; 103 | } 104 | 105 | .calls tbody td { 106 | border: transparent; 107 | padding: 5px 10px 2px 10px; 108 | } 109 | 110 | .l2txs table { 111 | border: transparent; 112 | } 113 | 114 | .l2txs tbody td { 115 | border: transparent; 116 | padding: 5px 10px 2px 10px; 117 | } 118 | 119 | #tree .ui-fancytree ul { 120 | padding-left: 20px; 121 | } 122 | 123 | #tree .ui-fancytree li { 124 | list-style-type: none; 125 | margin: 5px; 126 | position: relative; 127 | } 128 | 129 | #tree .ui-fancytree li::before { 130 | content: ""; 131 | position: absolute; 132 | top: -7px; 133 | left: 2px; 134 | border-left: 1px solid #ccc; 135 | border-bottom: 1px solid #ccc; 136 | border-radius: 0 0 0 0; 137 | width: 20px; 138 | height: 15px; 139 | } 140 | 141 | #tree .ui-fancytree li::after { 142 | position: absolute; 143 | content: ""; 144 | top: 8px; 145 | left: 2px; 146 | border-left: 1px solid #ccc; 147 | border-top: 1px solid #ccc; 148 | border-radius: 0 0 0 0; 149 | width: 20px; 150 | height: 100%; 151 | } 152 | 153 | #tree .ui-fancytree li:last-child::after { 154 | display: none; 155 | } 156 | 157 | #tree .ui-fancytree li:last-child:before { 158 | border-radius: 0 0 0 5px; 159 | } 160 | 161 | #tree ul.ui-fancytree > li:first-child::before { 162 | display: none; 163 | } 164 | 165 | #tree ul.ui-fancytree > li:first-child::after { 166 | border-radius: 5px 0 0 0; 167 | } 168 | 169 | #tree .ui-fancytree li p { 170 | border: 1px #ccc solid; 171 | border-radius: 5px; 172 | padding: 2px 5px; 173 | white-space: nowrap; 174 | display: inline-block; 175 | } 176 | 177 | #tree .ui-fancytree li p:hover, 178 | #tree .ui-fancytree li p:hover + ul li p, 179 | #tree .ui-fancytree li p:focus, 180 | #tree .ui-fancytree li p:focus + ul li p { 181 | background: #eee; 182 | color: #000; 183 | border: 1px solid #aaa; 184 | } 185 | 186 | #tree .ui-fancytree li p:hover + ul li::after, 187 | #tree .ui-fancytree li p:focus + ul li::after, 188 | #tree .ui-fancytree li p:hover + ul li::before, 189 | #tree .ui-fancytree li p:focus + ul li::before, 190 | #tree .ui-fancytree li p:hover + ul::before, 191 | #tree .ui-fancytree li p:focus + ul::before, 192 | #tree .ui-fancytree li p:hover + ul ul::before, 193 | #tree .ui-fancytree li p:focus + ul ul::before { 194 | border-color: #999; 195 | } 196 | 197 | #tree .ui-fancytree .fancytree-icon { 198 | display: none; 199 | } 200 | 201 | #tree .fancytree-plain.fancytree-container.fancytree-treefocus span.fancytree-focused span.fancytree-title { 202 | border-color: transparent; 203 | } 204 | 205 | #tree .fancytree-plain span.fancytree-node span.fancytree-title { 206 | background-color: transparent; 207 | border-color: transparent; 208 | } 209 | 210 | #tree ul.fancytree-container { 211 | background-color: transparent; 212 | border: none; 213 | outline: none; 214 | } 215 | 216 | #tree .fancytree-expander { 217 | position: relative; 218 | z-index: 2; 219 | top: -2px; 220 | left: -5px; 221 | } 222 | 223 | #tree * { 224 | font-family: monospace; 225 | } 226 | 227 | #tx_tree .ui-fancytree ul { 228 | padding-left: 20px; 229 | } 230 | 231 | #tx_tree .ui-fancytree li { 232 | list-style-type: none; 233 | margin: 5px; 234 | position: relative; 235 | } 236 | 237 | #tx_tree .ui-fancytree li::before { 238 | content: ""; 239 | position: absolute; 240 | top: -7px; 241 | left: 2px; 242 | border-left: 1px solid #ccc; 243 | border-bottom: 1px solid #ccc; 244 | border-radius: 0 0 0 0; 245 | width: 20px; 246 | height: 15px; 247 | } 248 | 249 | #tx_tree .ui-fancytree li::after { 250 | position: absolute; 251 | content: ""; 252 | top: 8px; 253 | left: 2px; 254 | border-left: 1px solid #ccc; 255 | border-top: 1px solid #ccc; 256 | border-radius: 0 0 0 0; 257 | width: 20px; 258 | height: 100%; 259 | } 260 | 261 | #tx_tree .ui-fancytree li:last-child::after { 262 | display: none; 263 | } 264 | 265 | #tx_tree .ui-fancytree li:last-child:before { 266 | border-radius: 0 0 0 5px; 267 | } 268 | 269 | #tx_tree ul.ui-fancytree > li:first-child::before { 270 | display: none; 271 | } 272 | 273 | #tx_tree ul.ui-fancytree > li:first-child::after { 274 | border-radius: 5px 0 0 0; 275 | } 276 | 277 | #tx_tree .ui-fancytree li p { 278 | border: 1px #ccc solid; 279 | border-radius: 5px; 280 | padding: 2px 5px; 281 | white-space: nowrap; 282 | display: inline-block; 283 | } 284 | 285 | #tx_tree .ui-fancytree li p:hover, 286 | #tx_tree .ui-fancytree li p:hover + ul li p, 287 | #tx_tree .ui-fancytree li p:focus, 288 | #tx_tree .ui-fancytree li p:focus + ul li p { 289 | background: #eee; 290 | color: #000; 291 | border: 1px solid #aaa; 292 | } 293 | 294 | #tx_tree .ui-fancytree li p:hover + ul li::after, 295 | #tx_tree .ui-fancytree li p:focus + ul li::after, 296 | #tx_tree .ui-fancytree li p:hover + ul li::before, 297 | #tx_tree .ui-fancytree li p:focus + ul li::before, 298 | #tx_tree .ui-fancytree li p:hover + ul::before, 299 | #tx_tree .ui-fancytree li p:focus + ul::before, 300 | #tx_tree .ui-fancytree li p:hover + ul ul::before, 301 | #tx_tree .ui-fancytree li p:focus + ul ul::before { 302 | border-color: #999; 303 | } 304 | 305 | #tx_tree .ui-fancytree .fancytree-icon { 306 | display: none; 307 | } 308 | 309 | #tx_tree 310 | .fancytree-plain.fancytree-container.fancytree-treefocus 311 | span.fancytree-focused 312 | span.fancytree-title { 313 | border-color: transparent; 314 | } 315 | 316 | #tx_tree .fancytree-plain span.fancytree-node span.fancytree-title { 317 | background-color: transparent; 318 | border-color: transparent; 319 | } 320 | 321 | #tx_tree ul.fancytree-container { 322 | background-color: transparent; 323 | border: none; 324 | outline: none; 325 | } 326 | 327 | #tx_tree .fancytree-expander { 328 | position: relative; 329 | z-index: 2; 330 | top: -2px; 331 | left: -5px; 332 | } 333 | 334 | #tx_tree * { 335 | font-family: monospace; 336 | } 337 | 338 | .back-button-container { 339 | display: flex; 340 | align-items: center; 341 | font-size: 16px; 342 | } 343 | 344 | .back-button-container img { 345 | margin-left: 20px; 346 | margin-right: 20px; 347 | } 348 | 349 | .with-back-button { 350 | display: flex; 351 | align-items: center; 352 | margin: 10px 11px 10px; 353 | } 354 | 355 | .with-back-button h3 { 356 | padding-bottom: 0; 357 | margin: 0; 358 | } 359 | 360 | .container-top { 361 | display: inline-block; 362 | } 363 | 364 | .container-info-logo { 365 | display: flex; 366 | justify-content: space-between; 367 | } 368 | 369 | @media screen and (max-width: 1281px) { 370 | .hosted-by { 371 | order: 1; 372 | margin: 0 0 0 13px; 373 | } 374 | 375 | .container-info-logo { 376 | flex-flow: column; 377 | } 378 | 379 | .transaction-info { 380 | order: 2; 381 | } 382 | 383 | } 384 | 385 | @media screen and (max-width: 1280px) { 386 | .with-back-button { 387 | flex-direction: column; 388 | align-items: flex-start; 389 | } 390 | 391 | .back-button-container { 392 | order: -1; 393 | margin-bottom: 20px; 394 | width: 100%; 395 | justify-content: space-between; 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /ethtx_ce/app/frontend/static/images/chevron_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthTx/ethtx_ce/f1f36fd5f447580dbd4a4c802eb77e09ea35dc20/ethtx_ce/app/frontend/static/images/chevron_down.png -------------------------------------------------------------------------------- /ethtx_ce/app/frontend/templates/exception.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | {% include './partials/headtags.html' %} 22 | Ethtx error 23 | 24 | 109 | 110 | 111 |
112 |
113 |

{{ status_code }}

114 |
115 |

{{ error }}

116 |
117 |
118 | {% if tx_hash %} 119 |
120 |

Tx hash: {{ tx_hash }}

121 |
122 | {%- endif -%} 123 |
124 | 125 | 126 | -------------------------------------------------------------------------------- /ethtx_ce/app/frontend/templates/index.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 23 | {% include './partials/headtags.html' %} 24 | 25 | EthDecoder 26 | 139 | 140 | 141 | 142 |
143 |
144 | EthTx Transaction Decoder 145 |
146 |
147 |
148 | 149 | 158 |
159 |
160 | 161 | 164 |
165 |
166 |
167 | 168 |
169 |
170 |
171 | 172 | 203 | 204 | -------------------------------------------------------------------------------- /ethtx_ce/app/frontend/templates/partials/headtags.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 23 | 25 | 26 | -------------------------------------------------------------------------------- /ethtx_ce/app/frontend/templates/semantics.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 23 | {% include './partials/headtags.html' %} 24 | Semantics editor 25 | 26 | 29 | 32 | 35 | 38 | 41 | 83 | 84 | 178 | 179 | 180 | 181 | 182 |

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

183 |
184 |
185 |
186 |
187 | 188 |
189 | 190 | 191 | 192 |
193 | 194 |
195 |
196 |
197 | 198 | 200 | 201 | 207 | 208 | 209 | 210 | 211 | 212 |
213 |
214 |
215 | 216 | 303 | 304 | 305 | 306 | 307 | -------------------------------------------------------------------------------- /ethtx_ce/app/frontend/templates/transaction.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | {% macro address_link(address, label, badge="") %} 20 | {%- if address and address != '0x0000000000000000000000000000000000000000' -%} 21 | 22 | {%- if badge -%} 23 | [{{ badge }}] 24 | {%- endif -%} 25 | {{- label -}} 26 | 27 | {%- else -%} 28 | {{- label -}} 29 | {%- endif -%} 30 | {% endmacro %} 31 | 32 | {% macro nft_link(address, label) %} 33 | {%- if address and address != '0x0000000000000000000000000000000000000000' -%} 34 | 35 | {{- label -}} 36 | 37 | {%- else -%} 38 | {{- label -}} 39 | {%- endif -%} 40 | {% endmacro %} 41 | 42 | {% macro print_event_arguments(arguments) %} 43 | {% for argument in arguments %} 44 | {% if argument.type != "ignore" %} 45 | {% if loop.index > 1 %}, {% endif %} 46 | {%- if argument.name == "[no ABI]" -%} 47 | no_ABI 48 | {%- else -%} 49 | {%- if argument.name -%} 50 | {{ argument.name }}= 51 | {%- endif -%} 52 | {%- if argument.type == "tuple" -%} 53 | ({{- print_event_arguments(argument.value) -}}) 54 | {%- elif argument.type == "tuple[]" -%} 55 | [ 56 | {%- for sub_arg in argument.value -%} 57 | {{- print_event_arguments(sub_arg) -}} 58 | {%- endfor -%} 59 | ] 60 | {%- elif argument.type == "address" -%} 61 | {{- address_link(argument.value.address, argument.value.name, argument.value.badge) -}} 62 | {%- elif argument.type == "nft" -%} 63 | {{- nft_link(argument.value.address, argument.value.name) -}} 64 | {%- elif argument.type == "call" -%} 65 | {{- address_link(argument.value.address, argument.value.name, argument.value.badge) -}}. 66 | {{- argument.value.function_name -}}({{ print_event_arguments(argument.value.arguments) }}) 67 | {%- else -%} 68 | {{- argument.value -}} 69 | {%- endif -%} 70 | {% endif %} 71 | {% endif %} 72 | {% endfor %} 73 | {% endmacro %} 74 | 75 | {% macro print_call_arguments(arguments) %} 76 | {% if arguments is not none %} 77 | {%- for argument in arguments -%} 78 | {% if argument.type != "ignore" %} 79 | {%- if loop.index > 1 -%}, {% endif %} 80 | {%- if argument.name == "[no ABI]" -%} 81 | no_ABI 82 | {%- else -%} 83 | {%- if argument.name %}{{- argument.name -}}={%- endif -%} 84 | {%- if argument.type == "tuple" -%} 85 | ({{- print_call_arguments(argument.value) -}}) 86 | {%- elif argument.type == "tuple[]" -%} 87 | [ 88 | {%- for sub_arg in argument.value -%} 89 | {%- if loop.index > 1 -%}, {% endif %} 90 | ({{- print_call_arguments(sub_arg) -}}) 91 | {%- endfor -%} 92 | ] 93 | {%- elif argument.type == "address" -%} 94 | {{- address_link(argument.value.address, argument.value.name, argument.value.badge) -}} 95 | {%- elif argument.type == "nft" -%} 96 | {{- nft_link(argument.value.address, argument.value.name) -}} 97 | {% elif argument.type == "call" %} 98 | {{- address_link(argument.value.address, argument.value.name, argument.value.badge) -}}. 99 | {{- argument.value.function_name -}}( 100 | {{- print_event_arguments(argument.value.arguments) -}}) 101 | {%- else -%} 102 | {{- argument.value -}} 103 | {%- endif -%} 104 | {%- endif -%} 105 | {%- endif -%} 106 | {%- endfor -%} 107 | {% endif %} 108 | {% endmacro %} 109 | 110 | {%- macro print_call_line(call) -%} 111 |
  • 113 |

    114 | [{{- call.gas_used if call.gas_used != None else "N/A" -}}]: 115 | {% if call.error %} 116 | ({{ call.error }}) 117 | {% endif %} 118 | {% if call.call_type == "delegatecall" %} 119 | (delegate) 120 | {% endif %} 121 | {% if call.value and call.call_type != "selfdestruct" %} 122 | ETH {{ call.value -}} 123 | {% endif %} 124 | {%- if call.call_type == "selfdestruct" -%} 125 | {{- address_link(call.from_address.address, call.from_address.name, call.from_address.badge) -}} 126 | .{{ call.call_type }}({% if call.value > 0 %} 127 | ETH {{ call.value }} => 128 | {{ address_link(call.to_address.address, call.to_address.name, call.to_address.badge) -}}{% endif %} 129 | ) 130 | {%- elif call.call_type == "create" -%} 131 | {{- address_link(call.to_address.address, call.to_address.name, call.to_address.badge) -}}.New() 133 | {%- else -%} 134 | 135 | {%- if call.call_type == "delegatecall" -%} 136 | {{- address_link(call.from_address.address, call.from_address.name, call.from_address.badge) -}} 137 | {%- else -%} 138 | {{- address_link(call.to_address.address, call.to_address.name, call.to_address.badge) -}} 139 | {%- endif -%} 140 | 141 | {%- if call.call_type == "delegatecall" -%} 142 | [{{- address_link(call.to_address.address, call.to_address.name, call.to_address.badge) -}} 144 | {%- endif -%} 145 | {%- if call.function_guessed -%} 146 | .{{- call.function_name -}} 147 | {%- elif call.function_name != "0x" -%} 148 | .{{- call.function_name -}} 149 | {%- else -%} 150 | .fallback 151 | {%- endif -%} 152 | {%- if call.call_type == "delegatecall" -%} 153 | ] 154 | {%- endif -%} 155 | 156 | ({{- print_call_arguments(call.arguments) -}}) => ({{- print_call_arguments(call.outputs) -}}) 157 | {%- endif -%} 158 |

    159 |
      160 | {% for sub_call in call.subcalls %} 161 | {{- print_call_line(sub_call) -}} 162 | {% endfor %} 163 |
    164 |
  • 165 | {%- endmacro -%} 166 | 167 | 168 | 169 | 170 | 171 | {% include './partials/headtags.html' %} 172 | Ethtx.info Analysis {{ transaction.tx_hash }} 173 | 176 | 180 | 183 | 184 | 185 | 186 | 187 | 188 |
    189 |
    190 |

    191 | 192 | 193 | Analysis for: 194 | {{ transaction.tx_hash }} / {{ transaction.chain_id }} 195 |

    196 | 220 |
    221 |
    222 | 223 | {% if events %} 224 |
    225 |

    Emitted events:

    226 | 227 | {% for event in events %} 228 | 229 | 239 | 240 | {% endfor %} 241 |
    230 | [{{- event.index -}}] 231 | {{ address_link(event.contract.address, event.contract.name, event.contract.badge) }} 232 | {%- if event.event_guessed -%} 233 | .{{ event.event_name }} 234 | {%- else -%} 235 | .{{ event.event_name }} 236 | {%- endif -%} 237 | ({{ print_event_arguments(event.parameters) }}) 238 |
    242 |
    243 | {% endif %} 244 | 245 | {% if balances %} 246 | 283 | {% endif %} 284 | 285 | {% if transfers %} 286 |
    287 |

    Token transfers:

    288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | {% for transfer in transfers %} 299 | 300 | 301 | {% if transfer.token_standard == 'ERC721' %} 302 | 303 | {% else %} 304 | 305 | {% endif %} 306 | 309 | 310 | 311 | {% endfor %} 312 | 313 |
    SenderTokenAmountReceiver
    {{- 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) -}} 307 | {{- "{:,.4f}".format(transfer.value) -}} 308 | {{- address_link(transfer.to_address.address, transfer.to_address.name, transfer.to_address.badge) -}}
    314 |
    315 | {% endif %} 316 | 317 | {% if call %} 318 |
    319 |

    Execution trace:

    320 |
    321 |
      322 |
    • 323 |

      324 | [{{- transaction.gas_used -}}]: 325 | {{- address_link(transaction.sender.address, transaction.sender.name, 'sender') -}} 326 |

      327 |
        328 | {{- print_call_line(call) -}} 329 |
      330 |
    • 331 |
    332 |
    333 |
    334 | {% else %} 335 |

    Trace decoding error...

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