├── .github └── workflows │ └── docker-release.yml ├── .gitignore ├── README.md ├── api ├── .dockerignore ├── .python-version ├── Dockerfile ├── hop.yml ├── poetry.lock ├── pyproject.toml ├── requirements.txt └── src │ ├── __init__.py │ ├── app.py │ ├── exceptions.py │ └── utils.py └── extension ├── .prettierrc ├── css └── popup.css ├── images ├── 128.png └── 64.png ├── js ├── background.js ├── inject.js ├── jquery.min.js └── popup.js ├── manifest.json └── popup.html /.github/workflows/docker-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker Image 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build-and-push: 10 | name: Build and Push Image 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up QEMU 17 | uses: docker/setup-qemu-action@v2 18 | 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v2 21 | 22 | - name: Login to Docker Hub 23 | uses: docker/login-action@v1 24 | with: 25 | username: prettyirrelevant 26 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 27 | 28 | - name: Decodify Version 29 | id: decodify_version 30 | run: echo "version=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT 31 | 32 | - name: Build and push Docker images 33 | uses: docker/build-push-action@v3 34 | with: 35 | context: ./api 36 | push: true 37 | platforms: linux/amd64,linux/arm64 38 | tags: | 39 | prettyirrelevant/decodify:${{ steps.decodify_version.outputs.version }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__ 3 | .mypy_cache 4 | .ruff_cache 5 | *.db 6 | .venv 7 | .env 8 | .cache 9 | hop.yml 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Decodify 2 | 3 | Decodify is a Chrome extension that enhances Etherscan and other EaaS (Explorers as a Service) platforms by leveraging rotki's powerful decoding capabilities for EVM chains. 4 | 5 | > **Note**: Decodify currently supports only EVM chains that are compatible with rotki. 6 | 7 | ## Running Your Own Decoding Server 8 | 9 | You can set up your own server to perform the decoding. Before you begin, obtain the required API keys: 10 | 11 | - [Optimism Etherscan API key](https://optimistic.etherscan.io/apis) 12 | - [Etherscan API key](https://docs.etherscan.io/getting-started/viewing-api-usage-statistics) 13 | - [Polygonscan API key](https://docs.polygonscan.com/getting-started/viewing-api-usage-statistics) 14 | 15 | ### Using Docker Image (Recommended) 16 | 17 | 1. Run the following command, replacing ``, `` (Etherscan), and `` (Optimism Etherscan) with your values: 18 | 19 | ``` 20 | docker run -d -p :2000 -e ETHEREUM_API_KEY= -e OPTIMISM_API_KEY= POLYGON_API_KEY= prettyirrelevant/decodify 21 | ``` 22 | 23 | 2. Ping `localhost:` to verify that the server is up and running. 24 | 25 | ### Local Setup 26 | 27 | 1. Set up a Python virtual environment and clone the repository. 28 | 2. Change the directory to `/api` and install the dependencies using `pip install -r requirements.txt`. 29 | 3. Set the environment variables `ETHEREUM_API_KEY`, `OPTIMISM_API_KEY` and `POLYGON_API_KEY` with your API keys. 30 | 4. Run the server with `PYTHONOPTIMIZE=1 flask run`. 31 | 32 | ## Acknowledgments 33 | 34 | - [rotki](https://github.com/rotki/rotki) for providing the decoding feature. 35 | - Icons8 for the extension's logo. 36 | -------------------------------------------------------------------------------- /api/.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | .venv 3 | .flask.env 4 | *.db 5 | 6 | .cache 7 | 8 | .DS_Store 9 | requirements-dev.txt 10 | -------------------------------------------------------------------------------- /api/.python-version: -------------------------------------------------------------------------------- 1 | 3.9.6 2 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.6-slim-buster 2 | 3 | ENV PYTHONDONTWRITEBYTECODE 1 4 | ENV PYTHONUNBUFFERED 1 5 | ENV PYTHONOPTIMIZE 1 6 | 7 | WORKDIR /opt/app 8 | 9 | COPY pyproject.toml poetry.lock ./ 10 | 11 | RUN apt-get update && \ 12 | apt-get install -y curl build-essential git && \ 13 | rm -rf /var/lib/apt/lists/* && \ 14 | pip install --upgrade pip && \ 15 | pip install poetry && \ 16 | poetry install 17 | 18 | COPY . . 19 | 20 | EXPOSE 2000 21 | 22 | CMD ["poetry", "run", "gunicorn", "--bind", "0.0.0.0:2000", "--worker-class", "gevent", "src.app:app"] 23 | -------------------------------------------------------------------------------- /api/hop.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | config: 4 | project_id: project_MTQ0NTYwNzE3ODIzMzE2MDA1 5 | deployment_id: deployment_MTUyNzYyNjY2ODM1ODk4ODM4 6 | -------------------------------------------------------------------------------- /api/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "decodify-api" 3 | version = "0.0.2" 4 | description = "Supercharges Etherscan and its derivatives by using rotki's powerful decoding feature" 5 | authors = ["Isaac Adewumi "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.9" 10 | rotkehlchen = {git = "https://github.com/rotki/rotki.git", rev = "42d7eae113274e3b18fe4b9a61160b155eefdf2d"} 11 | flask-caching = "^2.0.2" 12 | python-dotenv = "^1.0.0" 13 | gunicorn = "^20.1.0" 14 | 15 | 16 | [tool.poetry.group.dev.dependencies] 17 | isort = "^5.12.0" 18 | 19 | [build-system] 20 | requires = ["poetry-core"] 21 | build-backend = "poetry.core.masonry.api" 22 | -------------------------------------------------------------------------------- /api/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.4 ; python_version >= "3.9" and python_version < "4" 2 | aiosignal==1.3.1 ; python_version >= "3.9" and python_version < "4" 3 | asn1crypto==1.5.1 ; python_version >= "3.9" and python_version < "4.0" 4 | async-timeout==4.0.2 ; python_version >= "3.9" and python_version < "4" 5 | attrs==23.1.0 ; python_version >= "3.9" and python_version < "4" 6 | base58==2.1.1 ; python_version >= "3.9" and python_version < "4" 7 | base58check==1.0.2 ; python_version >= "3.9" and python_version < "4.0" 8 | bases==0.2.1 ; python_version >= "3.9" and python_version < "4.0" 9 | beautifulsoup4==4.12.2 ; python_version >= "3.9" and python_version < "4.0" 10 | bech32==1.2.0 ; python_version >= "3.9" and python_version < "4.0" 11 | bip-utils==2.7.1 ; python_version >= "3.9" and python_version < "4.0" 12 | bitarray==2.7.6 ; python_version >= "3.9" and python_version < "4" 13 | blinker==1.6.2 ; python_version >= "3.9" and python_version < "4.0" 14 | cachelib==0.9.0 ; python_version >= "3.9" and python_version < "4.0" 15 | cbor2==5.4.6 ; python_version >= "3.9" and python_version < "4.0" 16 | certifi==2023.5.7 ; python_version >= "3.9" and python_version < "4.0" 17 | cffi==1.15.1 ; python_version >= "3.9" and python_version < "4.0" 18 | charset-normalizer==3.2.0 ; python_version >= "3.9" and python_version < "4.0" 19 | click==8.1.5 ; python_version >= "3.9" and python_version < "4.0" 20 | coincurve==17.0.0 ; python_version >= "3.9" and python_version < "4.0" 21 | colorama==0.4.6 ; python_version >= "3.9" and python_version < "4.0" and platform_system == "Windows" 22 | crcmod==1.7 ; python_version >= "3.9" and python_version < "4.0" 23 | cryptography==41.0.1 ; python_version >= "3.9" and python_version < "4.0" 24 | cytoolz==0.12.1 ; python_version >= "3.9" and python_version < "4" 25 | ecdsa==0.18.0 ; python_version >= "3.9" and python_version < "4.0" 26 | ed25519-blake2b==1.4 ; python_version >= "3.9" and python_version < "4.0" 27 | eth-abi==2.2.0 ; python_version >= "3.9" and python_version < "4" 28 | eth-account==0.5.9 ; python_version >= "3.9" and python_version < "4" 29 | eth-hash==0.3.3 ; python_version >= "3.9" and python_version < "4" 30 | eth-hash[pycryptodome]==0.3.3 ; python_version >= "3.9" and python_version < "4" 31 | eth-keyfile==0.5.1 ; python_version >= "3.9" and python_version < "4" 32 | eth-keys==0.3.4 ; python_version >= "3.9" and python_version < "4" 33 | eth-rlp==0.2.1 ; python_version >= "3.9" and python_version < "4" 34 | eth-typing==2.3.0 ; python_version >= "3.9" and python_version < "4" 35 | eth-utils==1.10.0 ; python_version >= "3.9" and python_version < "4" 36 | filetype==1.2.0 ; python_version >= "3.9" and python_version < "4.0" 37 | flask-caching==2.0.2 ; python_version >= "3.9" and python_version < "4.0" 38 | flask-cors==3.0.10 ; python_version >= "3.9" and python_version < "4.0" 39 | flask==2.3.2 ; python_version >= "3.9" and python_version < "4.0" 40 | frozenlist==1.4.0 ; python_version >= "3.9" and python_version < "4" 41 | gevent-websocket==0.10.1 ; python_version >= "3.9" and python_version < "4.0" 42 | gevent==22.10.2 ; python_version >= "3.9" and python_version < "4.0" 43 | gql==2.0.0 ; python_version >= "3.9" and python_version < "4.0" 44 | graphql-core==2.3.2 ; python_version >= "3.9" and python_version < "4.0" 45 | greenlet==2.0.2 ; python_version >= "3.9" and python_version < "4.0" 46 | gunicorn==20.1.0 ; python_version >= "3.9" and python_version < "4.0" 47 | hexbytes==0.3.1 ; python_version >= "3.9" and python_version < "4" 48 | idna==3.4 ; python_version >= "3.9" and python_version < "4.0" 49 | importlib-metadata==6.8.0 ; python_version >= "3.9" and python_version < "3.10" 50 | ipfshttpclient==0.8.0a2 ; python_version >= "3.9" and python_version < "4" 51 | itsdangerous==2.1.2 ; python_version >= "3.9" and python_version < "4.0" 52 | jinja2==3.1.2 ; python_version >= "3.9" and python_version < "4.0" 53 | jsonschema-specifications==2023.6.1 ; python_version >= "3.9" and python_version < "4" 54 | jsonschema==4.18.3 ; python_version >= "3.9" and python_version < "4" 55 | lru-dict==1.2.0 ; python_version >= "3.9" and python_version < "4" 56 | markupsafe==2.1.3 ; python_version >= "3.9" and python_version < "4.0" 57 | marshmallow==3.19.0 ; python_version >= "3.9" and python_version < "4.0" 58 | maxminddb==2.2.0 ; python_version >= "3.9" and python_version < "4.0" 59 | miniupnpc==2.0.2 ; python_version >= "3.9" and python_version < "4.0" and sys_platform != "win32" 60 | miniupnpc==2.2.3 ; python_version >= "3.9" and python_version < "4.0" and sys_platform == "win32" 61 | more-itertools==9.1.0 ; python_version >= "3.9" and python_version < "4" 62 | multiaddr==0.0.9 ; python_version >= "3.9" and python_version < "4" 63 | multidict==6.0.4 ; python_version >= "3.9" and python_version < "4" 64 | multiformats-config==0.2.0.post4 ; python_version >= "3.9" and python_version < "4.0" 65 | multiformats==0.2.1 ; python_version >= "3.9" and python_version < "4.0" 66 | netaddr==0.8.0 ; python_version >= "3.9" and python_version < "4" 67 | packaging==23.1 ; python_version >= "3.9" and python_version < "4.0" 68 | parsimonious==0.8.1 ; python_version >= "3.9" and python_version < "4" 69 | polyleven==0.8 ; python_version >= "3.9" and python_version < "4.0" 70 | promise==2.3 ; python_version >= "3.9" and python_version < "4.0" 71 | protobuf==3.19.5 ; python_version >= "3.9" and python_version < "4" 72 | py-bip39-bindings==0.1.11 ; python_version >= "3.9" and python_version < "4.0" 73 | py-ed25519-zebra-bindings==1.0.1 ; python_version >= "3.9" and python_version < "4.0" 74 | py-sr25519-bindings==0.2.0 ; python_version >= "3.9" and python_version < "4.0" 75 | pycparser==2.17 ; python_version >= "3.9" and python_version < "4.0" 76 | pycryptodome==3.18.0 ; python_version >= "3.9" and python_version < "4.0" 77 | pynacl==1.5.0 ; python_version >= "3.9" and python_version < "4.0" 78 | pysha3==1.0.2 ; python_version >= "3.9" and python_version < "4.0" 79 | python-dotenv==1.0.0 ; python_version >= "3.9" and python_version < "4.0" 80 | pywin32==306 ; python_version >= "3.9" and python_version < "4" and platform_system == "Windows" 81 | referencing==0.29.1 ; python_version >= "3.9" and python_version < "4" 82 | requests==2.31.0 ; python_version >= "3.9" and python_version < "4.0" 83 | rlp==2.0.1 ; python_version >= "3.9" and python_version < "4" 84 | rotkehlchen @ git+https://github.com/rotki/rotki.git@42d7eae113274e3b18fe4b9a61160b155eefdf2d ; python_version >= "3.9" and python_version < "4.0" 85 | rotki-content-hash==0.0.3 ; python_version >= "3.9" and python_version < "4.0" 86 | rotki-pysqlcipher3==2022.8.1 ; python_version >= "3.9" and python_version < "4.0" 87 | rpds-py==0.8.10 ; python_version >= "3.9" and python_version < "4" 88 | rx==1.6.3 ; python_version >= "3.9" and python_version < "4.0" 89 | scalecodec==1.2.6 ; python_version >= "3.9" and python_version < "4" 90 | setuptools==68.0.0 ; python_version >= "3.9" and python_version < "4.0" 91 | six==1.16.0 ; python_version >= "3.9" and python_version < "4.0" 92 | soupsieve==2.4.1 ; python_version >= "3.9" and python_version < "4.0" 93 | substrate-interface==1.7.3 ; python_version >= "3.9" and python_version < "4" 94 | toolz==0.12.0 ; python_version >= "3.9" and python_version < "4" 95 | typing-extensions==4.5.0 ; python_version >= "3.9" and python_version < "4.0" 96 | typing-validation==1.0.0.post2 ; python_version >= "3.9" and python_version < "4.0" 97 | urllib3==1.26.14 ; python_version >= "3.9" and python_version < "4.0" 98 | varint==1.0.2 ; python_version >= "3.9" and python_version < "4" 99 | web3==5.31.3 ; python_version >= "3.9" and python_version < "4" 100 | webargs==8.2.0 ; python_version >= "3.9" and python_version < "4.0" 101 | websocket-client==1.6.1 ; python_version >= "3.9" and python_version < "4" 102 | websockets==9.1 ; python_version >= "3.9" and python_version < "4" 103 | werkzeug==2.3.3 ; python_version >= "3.9" and python_version < "4.0" 104 | wsaccel==0.6.3 ; python_version >= "3.9" and python_version < "4.0" 105 | xxhash==3.2.0 ; python_version >= "3.9" and python_version < "4" 106 | yarl==1.9.2 ; python_version >= "3.9" and python_version < "4" 107 | zipp==3.16.2 ; python_version >= "3.9" and python_version < "3.10" 108 | zope-event==5.0 ; python_version >= "3.9" and python_version < "4.0" 109 | zope-interface==6.0 ; python_version >= "3.9" and python_version < "4.0" 110 | -------------------------------------------------------------------------------- /api/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prettyirrelevant/decodify/c8581f7035e545f3e50272ea90489a1c153a9488/api/src/__init__.py -------------------------------------------------------------------------------- /api/src/app.py: -------------------------------------------------------------------------------- 1 | from gevent import monkey # isort: skip 2 | monkey.patch_all() # isort: skip 3 | 4 | from os import environ 5 | from pathlib import Path 6 | 7 | from dotenv import load_dotenv 8 | from flask import Flask, Response, jsonify 9 | from flask_caching import Cache 10 | from flask_cors import CORS 11 | from marshmallow import validate 12 | from rotkehlchen.api.v1.fields import (EvmAddressField, EvmChainNameField, 13 | EVMTransactionHashField) 14 | from rotkehlchen.chain.accounts import BlockchainAccountData 15 | from rotkehlchen.errors.misc import InputError 16 | from rotkehlchen.types import ChainID, EvmAddress, EVMTxHash 17 | from webargs.fields import DelimitedList 18 | from webargs.flaskparser import use_kwargs 19 | from werkzeug.exceptions import HTTPException 20 | 21 | from .utils import RotkiLite 22 | 23 | SUPPORTED_CHAIN_IDS = [ChainID.ETHEREUM, ChainID.OPTIMISM, ChainID.POLYGON_POS] 24 | 25 | BASE_DIR = Path(__file__).resolve().parent.parent 26 | load_dotenv(BASE_DIR / '.env', verbose=True) 27 | 28 | app = Flask(__name__) 29 | 30 | if not environ.get('REDIS_URL', None): 31 | app.config['CACHE_TYPE'] = 'FileSystemCache' 32 | 33 | cache_dir = BASE_DIR / '.cache' 34 | cache_dir.mkdir(exist_ok=True) 35 | 36 | app.config['CACHE_DIR'] = cache_dir 37 | app.config['CACHE_OPTIONS'] = {'mode': 511} 38 | else: 39 | app.config['CACHE_TYPE'] = 'RedisCache' 40 | app.config['CACHE_REDIS_URL'] = environ.get('REDIS_URL') 41 | 42 | cache = Cache(app) 43 | cors = CORS(app) 44 | rotki = RotkiLite( 45 | data_directory=BASE_DIR / 'data', 46 | password='deeznut', 47 | ethereum_api_key=environ.get('ETHEREUM_API_KEY'), 48 | optimism_api_key=environ.get('OPTIMISM_API_KEY'), 49 | polygon_api_key=environ.get('POLYGON_API_KEY') 50 | ) 51 | 52 | 53 | @app.errorhandler(Exception) 54 | def generic_errorhandler(e): 55 | return jsonify(errors=[str(e)]), 500 56 | 57 | 58 | @app.errorhandler(HTTPException) 59 | def http_errorhandler(e: HTTPException): 60 | resp = e.get_response() 61 | if resp.status_code in {400, 422}: 62 | messages = e.data.get('messages', ['Invalid request.']) 63 | else: 64 | messages = [e.description] 65 | 66 | return jsonify(errors=messages), resp.status_code 67 | 68 | 69 | @app.get('/') 70 | def index() -> Response: 71 | """This endpoint serves as a PING endpoint.""" 72 | return jsonify(message='welcome to decodify api'), 200 73 | 74 | 75 | @app.get('/transactions///addresses') 76 | @cache.cached() 77 | @use_kwargs({'tx_hash': EVMTransactionHashField(required=True), 'chain': EvmChainNameField(required=True, limit_to=SUPPORTED_CHAIN_IDS)}, location='view_args') # noqa: E501 78 | def fetch_transaction_addresses(tx_hash: EVMTxHash, chain: ChainID): 79 | addresses = rotki.fetch_transaction_addresses( 80 | chain=chain, 81 | tx_hash=tx_hash, 82 | ) 83 | return jsonify(data=addresses), 200 84 | 85 | 86 | @app.get('/transactions///decode') 87 | @cache.cached() 88 | @use_kwargs({'tx_hash': EVMTransactionHashField(required=True), 'chain': EvmChainNameField(required=True, limit_to=SUPPORTED_CHAIN_IDS)}, location='view_args') # noqa: E501 89 | @use_kwargs({'related_addresses': DelimitedList(EvmAddressField(), required=True, validate=validate.Length(max=2))}, location='query') # noqa: E501 90 | def decode_transaction( 91 | tx_hash: EVMTxHash, 92 | chain: ChainID, 93 | related_addresses: list[EvmAddress], 94 | ) -> Response: 95 | """This endpoint decodes a transaction.""" 96 | # step 1: add the address to the datbase. 97 | try: 98 | with rotki.database.user_write() as write_cursor: 99 | rotki.database.add_blockchain_accounts( 100 | write_cursor=write_cursor, 101 | account_data=[ 102 | BlockchainAccountData( 103 | chain=chain.to_blockchain(), 104 | address=addy, 105 | ) 106 | for addy in related_addresses 107 | ], 108 | ) 109 | except InputError: 110 | pass 111 | 112 | # step 2: decode the transaction. 113 | events = rotki.decode_transaction(chain=chain, tx_hash=tx_hash) 114 | 115 | # step 3: cleanup 116 | try: 117 | with rotki.database.user_write() as write_cursor: 118 | rotki.database.remove_single_blockchain_accounts( 119 | write_cursor=write_cursor, 120 | blockchain=chain.to_blockchain(), 121 | accounts=related_addresses, 122 | ) 123 | except InputError: 124 | pass 125 | 126 | # step 4: return the decoded transaction as JSON. 127 | return jsonify( 128 | data=[ 129 | event.serialize_for_api( 130 | customized_event_ids=[], 131 | ignored_ids_mapping={}, 132 | hidden_event_ids=[], 133 | ) 134 | for event in events 135 | ], 136 | ), 200 137 | -------------------------------------------------------------------------------- /api/src/exceptions.py: -------------------------------------------------------------------------------- 1 | class UnexpectedChainIDError(Exception): 2 | ... 3 | -------------------------------------------------------------------------------- /api/src/utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import TYPE_CHECKING, Any 3 | 4 | from rotkehlchen.chain.ethereum.constants import ETHEREUM_ETHERSCAN_NODE 5 | from rotkehlchen.chain.ethereum.node_inquirer import EthereumInquirer 6 | from rotkehlchen.chain.ethereum.transactions import EthereumTransactions 7 | from rotkehlchen.chain.optimism.constants import OPTIMISM_ETHERSCAN_NODE 8 | from rotkehlchen.chain.optimism.node_inquirer import OptimismInquirer 9 | from rotkehlchen.chain.optimism.transactions import OptimismTransactions 10 | from rotkehlchen.chain.polygon_pos.constants import POLYGON_POS_ETHERSCAN_NODE 11 | from rotkehlchen.chain.polygon_pos.node_inquirer import PolygonPOSInquirer 12 | from rotkehlchen.chain.polygon_pos.transactions import PolygonPOSTransactions 13 | from rotkehlchen.constants import DEFAULT_SQL_VM_INSTRUCTIONS_CB 14 | from rotkehlchen.db.dbhandler import DBHandler 15 | from rotkehlchen.globaldb.handler import GlobalDBHandler 16 | from rotkehlchen.greenlets.manager import GreenletManager 17 | from rotkehlchen.types import (ChainID, EvmAddress, EVMTxHash, ExternalService, 18 | ExternalServiceApiCredentials) 19 | from rotkehlchen.user_messages import MessagesAggregator 20 | 21 | from .exceptions import UnexpectedChainIDError 22 | 23 | if TYPE_CHECKING: 24 | from rotkehlchen.accounting.structures.evm_event import EvmEvent 25 | from rotkehlchen.chain.evm.decoding.decoder import EVMTransactionDecoder 26 | from rotkehlchen.chain.evm.transactions import EvmTransactions 27 | from rotkehlchen.db.drivers.gevent import DBCursor 28 | 29 | 30 | class RotkiLite: 31 | def __init__( 32 | self, 33 | data_directory: Path, 34 | password: str, 35 | polygon_api_key: str, 36 | ethereum_api_key: str, 37 | optimism_api_key: str, 38 | ) -> None: 39 | self.__msg_aggregator = MessagesAggregator() 40 | self.__greenlet_manager = GreenletManager(self.__msg_aggregator) 41 | self.__data_dir = data_directory 42 | self.__data_dir.mkdir(exist_ok=True) 43 | self.__user_data_dir = self.__data_dir / 'deezy' 44 | self.__user_data_dir.mkdir(exist_ok=True) 45 | 46 | GlobalDBHandler( 47 | data_dir=self.__data_dir, 48 | sql_vm_instructions_cb=DEFAULT_SQL_VM_INSTRUCTIONS_CB, 49 | ) 50 | self.database = DBHandler( 51 | user_data_dir=self.__user_data_dir, 52 | password=password, 53 | msg_aggregator=self.__msg_aggregator, 54 | initial_settings=None, 55 | sql_vm_instructions_cb=DEFAULT_SQL_VM_INSTRUCTIONS_CB, 56 | resume_from_backup=False, 57 | ) 58 | with self.database.user_write() as write_cursor: 59 | populate_db_with_rpc_nodes(write_cursor) 60 | self._add_api_keys_to_database( 61 | write_cursor=write_cursor, 62 | ethereum_etherscan_key=ethereum_api_key, 63 | optimism_etherscan_key=optimism_api_key, 64 | polygon_etherscan_key=polygon_api_key, 65 | ) 66 | 67 | # Initialise all node inquirers 68 | ethereum_inquirer = EthereumInquirer( 69 | greenlet_manager=self.__greenlet_manager, 70 | database=self.database, 71 | ) 72 | optimism_inquirer = OptimismInquirer( 73 | greenlet_manager=self.__greenlet_manager, 74 | database=self.database, 75 | ) 76 | polygon_inquirer = PolygonPOSInquirer( 77 | greenlet_manager=self.__greenlet_manager, 78 | database=self.database, 79 | ) 80 | 81 | # Initialise all transactions objects 82 | self.__ethereum_transactions = EthereumTransactions( 83 | ethereum_inquirer=ethereum_inquirer, 84 | database=self.database, 85 | ) 86 | self.__optimism_transactions = OptimismTransactions( 87 | optimism_inquirer=optimism_inquirer, 88 | database=self.database, 89 | ) 90 | self.__polygon_transactions = PolygonPOSTransactions( 91 | polygon_pos_inquirer=polygon_inquirer, 92 | database=self.database, 93 | ) 94 | 95 | # To prevent circular imports. 96 | from rotkehlchen.chain.ethereum.decoding.decoder import \ 97 | EthereumTransactionDecoder 98 | from rotkehlchen.chain.optimism.decoding.decoder import \ 99 | OptimismTransactionDecoder 100 | from rotkehlchen.chain.polygon_pos.decoding.decoder import \ 101 | PolygonPOSTransactionDecoder 102 | 103 | self.__ethereum_tx_decoder = EthereumTransactionDecoder( 104 | database=self.database, 105 | ethereum_inquirer=ethereum_inquirer, 106 | transactions=self.__ethereum_transactions, 107 | ) 108 | self.__optimism_tx_decoder = OptimismTransactionDecoder( 109 | database=self.database, 110 | optimism_inquirer=optimism_inquirer, 111 | transactions=self.__optimism_transactions, 112 | ) 113 | self.__polygon_tx_decoder = PolygonPOSTransactionDecoder( 114 | database=self.database, 115 | polygon_pos_inquirer=polygon_inquirer, 116 | transactions=self.__polygon_transactions, 117 | ) 118 | 119 | def _add_api_keys_to_database( 120 | self, 121 | write_cursor: 'DBCursor', 122 | ethereum_etherscan_key: str, 123 | optimism_etherscan_key: str, 124 | polygon_etherscan_key: str, 125 | ) -> None: 126 | """Add API keys to the database for various Etherscan services. 127 | 128 | Parameters: 129 | write_cursor: A database cursor with write access. 130 | ethereum_etherscan_key: The API key for Ethereum Etherscan service. 131 | optimism_etherscan_key: The API key for Optimism Etherscan service. 132 | polygon_etherscan_key: The API key for Polygon Etherscan service. 133 | """ 134 | self.database.add_external_service_credentials( 135 | write_cursor=write_cursor, 136 | credentials=[ 137 | ExternalServiceApiCredentials( 138 | service=ExternalService.ETHERSCAN, 139 | api_key=ethereum_etherscan_key, 140 | ), 141 | ExternalServiceApiCredentials( 142 | service=ExternalService.OPTIMISM_ETHERSCAN, 143 | api_key=optimism_etherscan_key, 144 | ), 145 | ExternalServiceApiCredentials( 146 | service=ExternalService.POLYGON_POS_ETHERSCAN, 147 | api_key=polygon_etherscan_key, 148 | ) 149 | ], 150 | ) 151 | 152 | def _get_transactions(self, chain_id: ChainID) -> 'EvmTransactions': 153 | """Get the EvmTransactions instance for the specified ChainID. 154 | 155 | Parameters: 156 | chain_id: The ID of the blockchain chain. 157 | 158 | Raises: 159 | UnexpectedChainIDError: if the `chain_id` is not supported yet. 160 | 161 | Returns: 162 | EvmTransactions: An instance of EvmTransactions 163 | for the specified chain, or None if the chain is not supported. 164 | """ 165 | if chain_id == ChainID.ETHEREUM: 166 | return self.__ethereum_transactions 167 | if chain_id == ChainID.OPTIMISM: 168 | return self.__optimism_transactions 169 | if chain_id == ChainID.POLYGON_POS: 170 | return self.__polygon_transactions 171 | 172 | raise UnexpectedChainIDError 173 | 174 | def _get_transactions_decoder(self, chain_id: ChainID) -> 'EVMTransactionDecoder': 175 | """Get the appropriate EVMTransactionDecoder based on the ChainID. 176 | 177 | Parameters: 178 | chain_id: The ID of the blockchain chain. 179 | 180 | Raises: 181 | UnexpectedChainIDError: if the `chain_id` is not supported yet. 182 | 183 | Returns: 184 | An instance of EVMTransactionDecoder for the specified chain. 185 | """ 186 | if chain_id == ChainID.ETHEREUM: 187 | return self.__ethereum_tx_decoder 188 | if chain_id == ChainID.OPTIMISM: 189 | return self.__optimism_tx_decoder 190 | if chain_id == ChainID.POLYGON_POS: 191 | return self.__polygon_tx_decoder 192 | 193 | raise UnexpectedChainIDError 194 | 195 | def _query_transaction(self, chain: ChainID, tx_hash: EVMTxHash) -> dict[str, Any]: 196 | """Get transaction information by its hash. 197 | 198 | Parameters: 199 | chain: The ID of the Ethereum chain. 200 | tx_hash: The hash of the transaction to query. 201 | 202 | Returns: 203 | A dictionary containing transaction information. 204 | """ 205 | transactions = self._get_transactions(chain) 206 | return transactions.evm_inquirer.etherscan.get_transaction_by_hash(tx_hash) 207 | 208 | def fetch_transaction_addresses(self, chain: ChainID, tx_hash: EVMTxHash) -> list[EvmAddress]: 209 | """Fetch the ``from`` and ``to`` in a transaction. 210 | 211 | Parameters: 212 | chain: The ID of the Ethereum chain. 213 | tx_hash: The hash of the transaction to query. 214 | 215 | Returns: 216 | A list of EvmAddress objects representing the addresses that are not contracts. 217 | """ 218 | addresses = [] 219 | transaction = self._query_transaction(chain, tx_hash) 220 | if not transaction: 221 | return addresses 222 | 223 | transactions = self._get_transactions(chain) 224 | for addy in (transaction['from'], transaction['to']): 225 | if transactions.evm_inquirer.etherscan.get_code(addy) == '0x': 226 | addresses.append(addy) 227 | 228 | return addresses 229 | 230 | def decode_transaction(self, chain: ChainID, tx_hash: EVMTxHash) -> list['EvmEvent']: 231 | """Decode an Ethereum transaction using its hash. 232 | 233 | Parameters: 234 | chain: The ID of the Ethereum chain. 235 | tx_hash: The hash of the transaction to decode. 236 | 237 | Returns: 238 | A list of EvmEvent objects representing the decoded events from the transaction. 239 | """ 240 | transactions = self._get_transactions(chain) 241 | transactions.get_or_query_transaction_receipt(tx_hash) 242 | 243 | decoder = self._get_transactions_decoder(chain) 244 | return decoder.decode_transaction_hashes(ignore_cache=False, tx_hashes=[tx_hash]) 245 | 246 | 247 | def populate_db_with_rpc_nodes(write_cursor: 'DBCursor') -> None: 248 | """Populates the database with RPC nodes' information. 249 | 250 | Parameters: 251 | write_cursor: A database cursor with write access. 252 | """ 253 | nodes = [ 254 | ETHEREUM_ETHERSCAN_NODE.serialize_for_db(), 255 | OPTIMISM_ETHERSCAN_NODE.serialize_for_db(), 256 | POLYGON_POS_ETHERSCAN_NODE.serialize_for_db(), 257 | ] 258 | 259 | write_cursor.executemany( 260 | 'INSERT OR IGNORE INTO rpc_nodes(name, endpoint, owned, active, weight, blockchain) ' 261 | 'VALUES (?, ?, ?, ?, ?, ?)', 262 | nodes, 263 | ) 264 | -------------------------------------------------------------------------------- /extension/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 190, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /extension/css/popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | text-align: center; 3 | font-family: monospace; 4 | margin: 0; 5 | padding: 0; 6 | width: 363px; 7 | } 8 | 9 | nav { 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | margin-bottom: 40px; 14 | margin-top: 10px; 15 | } 16 | 17 | nav img { 18 | width: 50px; 19 | height: 50px; 20 | margin-right: 10px; 21 | } 22 | 23 | input { 24 | width: 300px; 25 | padding: 10px; 26 | margin: 0 20px 20px 20px; 27 | font-family: monospace; 28 | } 29 | 30 | input:focus { 31 | outline: none; 32 | } 33 | 34 | .switch-container { 35 | display: flex; 36 | align-items: center; 37 | margin: 20px 20px 0 20px; 38 | justify-content: space-between; 39 | margin-bottom: 20px; 40 | } 41 | 42 | .switch-label { 43 | 44 | font-size: 12px; 45 | font-weight: normal; 46 | margin-right: 10px; 47 | } 48 | 49 | .switch { 50 | display: flex; 51 | align-items: center; 52 | } 53 | 54 | .switch input[type="checkbox"] { 55 | opacity: 0; 56 | width: 0; 57 | height: 0; 58 | } 59 | 60 | .slider { 61 | position: relative; 62 | cursor: pointer; 63 | width: 40px; 64 | height: 24px; 65 | background-color: #ccc; 66 | transition: .4s; 67 | border-radius: 24px; 68 | } 69 | 70 | .slider:before { 71 | content: ""; 72 | position: absolute; 73 | height: 18px; 74 | width: 18px; 75 | left: 3px; 76 | bottom: 3px; 77 | background-color: white; 78 | transition: .4s; 79 | border-radius: 50%; 80 | } 81 | 82 | input[type="checkbox"]:checked+.slider { 83 | background-color: #2196F3; 84 | } 85 | 86 | input[type="checkbox"]:checked+.slider:before { 87 | transform: translateX(16px); 88 | } 89 | 90 | footer { 91 | display: flex; 92 | align-items: center; 93 | justify-content: center; 94 | margin-top: 10px; 95 | } 96 | 97 | footer a { 98 | margin: 0 10px; 99 | color: #000; 100 | } 101 | 102 | #logoName { 103 | font-weight: 800; 104 | font-size: 20px; 105 | letter-spacing: 0.5px; 106 | } 107 | -------------------------------------------------------------------------------- /extension/images/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prettyirrelevant/decodify/c8581f7035e545f3e50272ea90489a1c153a9488/extension/images/128.png -------------------------------------------------------------------------------- /extension/images/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prettyirrelevant/decodify/c8581f7035e545f3e50272ea90489a1c153a9488/extension/images/64.png -------------------------------------------------------------------------------- /extension/js/background.js: -------------------------------------------------------------------------------- 1 | console.log('background.js loaded.'); 2 | 3 | const getDefaultSettings = () => { 4 | return { 5 | useCustomServer: false, 6 | baseServerURL: 'https://decodify.hop.sh/transactions', 7 | }; 8 | }; 9 | 10 | const initializeExtension = () => { 11 | chrome.storage.local.get(['useCustomServer', 'baseServerURL']).then(({ useCustomServer, baseServerURL }) => { 12 | const defaultSettings = getDefaultSettings(); 13 | 14 | defaultSettings.baseServerURL = baseServerURL || defaultSettings.baseServerURL; 15 | defaultSettings.useCustomServer = useCustomServer || defaultSettings.useCustomServer; 16 | 17 | chrome.storage.local.set(defaultSettings).then(() => console.log('default values for extension set!')); 18 | }); 19 | }; 20 | 21 | chrome.runtime.onInstalled.addListener(() => { 22 | initializeExtension(); 23 | }); 24 | 25 | chrome.runtime.onStartup.addListener(() => { 26 | initializeExtension(); 27 | }); 28 | -------------------------------------------------------------------------------- /extension/js/inject.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | console.log('inject.js is loaded.'); 3 | 4 | async function getTransactionAddresses(txHash, chain) { 5 | const { baseServerURL } = await chrome.storage.local.get('baseServerURL'); 6 | const url = `${baseServerURL}/${txHash}/${chain}/addresses`; 7 | try { 8 | return await $.ajax({ 9 | url: url, 10 | async: false, 11 | }); 12 | } catch (error) { 13 | console.error(`An error occurred while fetching the info of transaction ${txHash}`); 14 | console.error(error); 15 | return; 16 | } 17 | } 18 | 19 | async function getTransactionDecodedEvents(txHash, chain, ...addresses) { 20 | const { baseServerURL } = await chrome.storage.local.get('baseServerURL'); 21 | const url = `${baseServerURL}/${txHash}/${chain}/decode?related_addresses=${addresses.join(',')}`; 22 | try { 23 | return await $.ajax({ 24 | url: url, 25 | async: false, 26 | }); 27 | } catch (error) { 28 | console.error(`An error occurred while fetching decoded events of transaction ${txHash}`); 29 | console.error(error); 30 | return; 31 | } 32 | } 33 | 34 | // gets the icon tag for each event type for Ethereum. 35 | function getIconForEthereumEvent(e) { 36 | if (e.event_type === 'spend' && e.event_subtype === 'fee') { 37 | return ''; 38 | } else if (e.event_type === 'spend') { 39 | return ''; 40 | } else if (e.event_type === 'receive') { 41 | return ''; 42 | } else if (e.event_type === 'transfer') { 43 | return ''; 44 | } else if (e.event_type === 'deposit') { 45 | return ''; 46 | } else if (e.event_type === 'withdrawal') { 47 | return ''; 48 | } else { 49 | return ''; 50 | } 51 | } 52 | 53 | // gets the icon tag for each event type for Optimism. 54 | function getIconForOptimismAndPolygonEvent(e) { 55 | if (e.event_type === 'spend' && e.event_subtype === 'fee') { 56 | return ''; 57 | } else if (e.event_type === 'spend') { 58 | return ''; 59 | } else if (e.event_type === 'receive') { 60 | return ''; 61 | } else if (e.event_type === 'transfer') { 62 | return ''; 63 | } else if (e.event_type === 'deposit') { 64 | return ''; 65 | } else if (e.event_type === 'withdrawal') { 66 | return ''; 67 | } else { 68 | return ''; 69 | } 70 | } 71 | 72 | function transformAddressesToLinks(text, chain) { 73 | let url; 74 | if (chain === 'ethereum') { 75 | url = 'https://etherscan.io/address'; 76 | } else if (chain === 'optimism') { 77 | url = 'https://optimistic.etherscan.io/address'; 78 | } else if (chain === 'polygon_pos') { 79 | url = 'https://polygonscan.com/address'; 80 | } 81 | const addressRegex = /0x[0-9a-fA-F]{40}/g; 82 | const etherscanLink = (address) => `${address.slice(0, 8)}...${address.slice(-8)}`; 83 | return text.replace(addressRegex, etherscanLink); 84 | } 85 | 86 | async function injectDecodedEventsEthereum(txHash) { 87 | const chain = 'ethereum'; 88 | $('ul#ContentPlaceHolder1_myTab li:last').before(` 89 | 92 | `); 93 | 94 | $('div#pills-tabContent').append(` 95 |
96 |
Fetching data...
97 |
98 | `); 99 | 100 | const container = $('div#pills-tabContent').children().last(); 101 | const addresses = await getTransactionAddresses(txHash, chain); 102 | if (!addresses) { 103 | container.html(` 104 |
105 |

106 | 107 | An error occurred while attempting to retrieve transaction addresses. Check console for more errors. 108 |

109 |
110 | `); 111 | return; 112 | } 113 | 114 | const decodedEvents = await getTransactionDecodedEvents(txHash, chain, ...addresses.data); 115 | if (!decodedEvents || decodedEvents?.data?.length === 0) { 116 | container.html(` 117 |
118 |

119 | 120 | An error occurred while attempting to retrieve decoded events. Check console for more errors. 121 |

122 |
123 | `); 124 | return; 125 | } 126 | 127 | let data = ''; 128 | decodedEvents.data.forEach((el, i) => { 129 | const icon = getIconForEthereumEvent(el.entry); 130 | data += ` 131 |
132 | ${icon} 133 | ${transformAddressesToLinks(el.entry.notes, chain)} 134 |
135 | `; 136 | if (i !== decodedEvents.data.length - 1) { 137 | data += '\n'; 138 | } 139 | }); 140 | 141 | container.html(` 142 |
143 |
Transaction Receipt Event Logs decoded using rotki
144 |
145 | ${data} 146 |
147 | `); 148 | } 149 | 150 | async function injectDecodedEventsOptimismAndPolygon(txHash, chain) { 151 | $('ul#nav_tabs').append(` 152 | 155 | `); 156 | 157 | $('div#myTabContent').append(` 158 |
159 |
Fetching data...
160 |
161 | `); 162 | 163 | const container = $('div#myTabContent').children().last(); 164 | const addresses = await getTransactionAddresses(txHash, chain); 165 | if (!addresses) { 166 | container.html(` 167 |
168 |
169 | 170 | An error occurred while attempting to retrieve transaction details. Check console for more errors. 171 |
172 |
173 | `); 174 | return; 175 | } 176 | 177 | const decodedEvents = await getTransactionDecodedEvents(txHash, chain, ...addresses.data); 178 | if (!decodedEvents || decodedEvents?.data?.length === 0) { 179 | container.html(` 180 |
181 |
182 | 183 | An error occurred while attempting to retrieve decoded events. Check console for more errors. 184 |
185 |
186 | `); 187 | return; 188 | } 189 | 190 | let data = ''; 191 | decodedEvents.data.forEach((el, i) => { 192 | const icon = getIconForOptimismAndPolygonEvent(el.entry); 193 | data += ` 194 |
195 | ${icon} 196 | ${transformAddressesToLinks(el.entry.notes, chain)} 197 |
198 | `; 199 | if (i !== decodedEvents.data.length - 1) { 200 | data += '\n'; 201 | } 202 | }); 203 | 204 | container.html(` 205 |
206 |

Transaction Receipt Event Logs decoded using rotki

207 |
208 | ${data} 209 |
210 | `); 211 | } 212 | 213 | // Entrypoint 214 | const txHash = window.location.pathname.split('/')[2]; 215 | if (window.location.hostname === 'etherscan.io') { 216 | injectDecodedEventsEthereum(txHash); 217 | } else if (window.location.host === 'optimistic.etherscan.io') { 218 | injectDecodedEventsOptimismAndPolygon(txHash, 'optimism'); 219 | } else if (window.location.host === 'polygonscan.com') { 220 | injectDecodedEventsOptimismAndPolygon(txHash, 'polygon_pos'); 221 | } else { 222 | return; 223 | } 224 | }); 225 | -------------------------------------------------------------------------------- /extension/js/jquery.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v3.7.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ 2 | !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},C=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.0",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},R=function(){V()},M=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&z(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function X(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&M(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function U(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function z(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",R),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",le.option=!!xe.lastChild;var ke={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n",""]);var je=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function Me(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="
",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return R(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return R(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0 { 4 | const urlInput = document.getElementById('urlInput'); 5 | const slider = document.getElementById('switchId'); 6 | 7 | const { useCustomServer, baseServerURL } = await chrome.storage.local.get(['useCustomServer', 'baseServerURL']); 8 | if (!useCustomServer) { 9 | urlInput.style.display = 'none'; 10 | } 11 | 12 | slider.checked = useCustomServer; 13 | urlInput.value = baseServerURL; 14 | 15 | urlInput.addEventListener('keypress', (e) => { 16 | if (e.key !== 'Enter') return; 17 | 18 | // validate that the entry is a valid http/https url 19 | if (!isValidUrl(e.target.value)) { 20 | e.target.style.border = '2px solid'; 21 | e.target.style.borderColor = 'red'; 22 | 23 | removeOutline(e.target); 24 | return; 25 | } 26 | 27 | // store it inside chrome storage 28 | e.target.style.border = '2px solid'; 29 | e.target.style.borderColor = 'green'; 30 | chrome.storage.local.set({ baseServerURL: e.target.value }).then(() => console.log('updated the baseServerURL!')); 31 | removeOutline(e.target); 32 | }); 33 | 34 | slider.addEventListener('change', (e) => { 35 | if (e.target.checked) { 36 | urlInput.style.display = 'block'; 37 | } else { 38 | urlInput.style.display = 'none'; 39 | chrome.storage.local.set({ baseServerURL: 'https://decodify.hop.sh/transactions' }).then(() => console.log('updated the baseServerURL!')); 40 | } 41 | 42 | chrome.storage.local.set({ useCustomServer: e.target.checked }).then(() => console.log('updated the useCustomServer!')); 43 | }); 44 | 45 | const isValidUrl = (inputURL) => { 46 | try { 47 | const url = new URL(inputURL); 48 | return url.pathname == '/transactions' ? true : false; 49 | } catch (error) { 50 | return false; 51 | } 52 | }; 53 | 54 | const removeOutline = (el) => { 55 | setTimeout(() => { 56 | el.style.border = '1px solid'; 57 | el.style.borderColor = 'black'; 58 | }, 900); 59 | }; 60 | }); 61 | -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Decodify", 4 | "version": "0.0.2", 5 | "permissions": ["storage"], 6 | "description": "Supercharges Etherscan and its derivatives by using rotki's powerful decoding feature", 7 | "content_scripts": [ 8 | { 9 | "js": ["js/jquery.min.js", "js/inject.js"], 10 | "matches": ["https://etherscan.io/tx/*", "https://optimistic.etherscan.io/tx/*", "https://polygonscan.com/tx/*"], 11 | "run_at": "document_start" 12 | } 13 | ], 14 | "action": { 15 | "default_title": "decodify", 16 | "default_icon": "images/64.png", 17 | "default_popup": "popup.html" 18 | }, 19 | "icons": { 20 | "64": "images/64.png", 21 | "128": "images/128.png" 22 | }, 23 | "background": { 24 | "service_worker": "js/background.js" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /extension/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Popup 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 |
20 |
21 | 22 | 26 |
27 | 28 |
29 | 30 | 31 |
32 | 48 |

v0.0.1

49 | 50 | 51 | 52 | 53 | 54 | 55 | --------------------------------------------------------------------------------