├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config └── settings │ ├── __init__.py │ ├── base.py │ ├── local.py │ └── test.py ├── diagrams.mdj ├── django_eth_events ├── __init__.py ├── admin.py ├── apps.py ├── chainevents.py ├── decoder.py ├── event_listener.py ├── exceptions.py ├── factories.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── resync_daemon.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_daemon_last_error_block_number.py │ ├── 0003_block.py │ ├── 0004_auto_20171127_0501.py │ ├── 0005_daemon_status.py │ ├── 0006_block_timestamp.py │ ├── 0007_auto_20171207_0759.py │ ├── 0008_daemon_listener_lock.py │ ├── 0009_daemon_last_error_date_time.py │ ├── 0010_remove_daemon_status.py │ └── __init__.py ├── models.py ├── reorgs.py ├── singleton.py ├── tasks.py ├── tests │ ├── __init__.py │ ├── mocked_testrpc_reorg.py │ ├── test_celery.py │ ├── test_chainevents.py │ ├── test_daemon_model.py │ ├── test_decoder.py │ ├── test_event_listener_exec.py │ ├── test_event_listener_functions.py │ ├── test_reorg_detector.py │ ├── test_singleton.py │ ├── test_utils.py │ ├── test_web3_service.py │ └── utils.py ├── utils.py ├── version.py └── web3_service.py ├── manage.py ├── requirements.txt ├── run_tests.py ├── scripts └── deploy.sh ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | .coverage 58 | .vagrant 59 | .idea/ 60 | *.pyc 61 | *~ 62 | 63 | .python-version 64 | *.egg-info 65 | .pytest_cache/ 66 | 67 | .ipfs 68 | 69 | .idea 70 | .vscode 71 | 72 | venv/ 73 | config/settings/dev.py 74 | db.sqlite3 75 | 76 | build/ 77 | dist/ 78 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: pip 3 | python: 4 | - "3.6" 5 | dist: trusty 6 | env: 7 | global: 8 | - SOURCE_FOLDER=django_eth_events 9 | - PIP_USE_MIRRORS=true 10 | install: 11 | - pip install -r requirements.txt 12 | - pip install coveralls 13 | script: 14 | - coverage run --source=$SOURCE_FOLDER manage.py test --settings=config.settings.test 15 | after_success: 16 | - coveralls 17 | deploy: 18 | provider: script 19 | script: bash scripts/deploy.sh "${PYPI_USER}" "${PYPI_PASS}" 20 | skip_cleanup: true 21 | on: 22 | tags: true 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Gnosis Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/gnosis/django-eth-events.svg?branch=master)](https://travis-ci.org/gnosis/django-eth-events) 2 | [![Coverage Status](https://coveralls.io/repos/github/gnosis/django-eth-events/badge.svg?branch=master)](https://coveralls.io/github/gnosis/django-eth-events?branch=master) 3 | ![Python 3.6](https://img.shields.io/badge/Python-3.6-blue.svg) 4 | ![Django 2](https://img.shields.io/badge/Django-2-blue.svg) 5 | [![PyPI version](https://badge.fury.io/py/django-eth-events.svg)](https://badge.fury.io/py/django-eth-events) 6 | 7 | # django_eth_events 8 | A standalone Django app for decoding Ethereum events compatible with Python 3.x. This app provides methods 9 | and [Celery tasks](http://www.celeryproject.org/) to continuously decode events from ethereum blockchain and 10 | index then in a relational database. Too see an example of using this you can 11 | check [Gnosis TradingDB repository](https://github.com/gnosis/pm-trading-db) and 12 | [readthedocs documentation](https://gnosis-apollo.readthedocs.io/en/latest/pm-trading-db.html) 13 | 14 | # Setup 15 | If you want to install the latest stable release from PyPi: 16 | 17 | `$ pip install django-eth-events` 18 | 19 | If you want to install the latest development version from GitHub: 20 | 21 | `$ pip install -e git+https://github.com/gnosis/django-eth-events.git#egg=Package` 22 | 23 | Add django_eth_events to your INSTALLED_APPS: 24 | 25 | ```python 26 | INSTALLED_APPS = ( 27 | ... 28 | 'django_eth_events', 29 | ... 30 | ) 31 | ``` 32 | 33 | # Settings 34 | Provide an Ethereum _node url_. Provider will be detected depending on the protocol of the url. _http/s_, _ipc_ and 35 | _ws(websocket)_ providers are available. **Recommended protocol is HTTPS.** 36 | For example, using _https://localhost:8545_ communication with node will use **RPC through HTTPS** 37 | 38 | ```python 39 | ETHEREUM_NODE_URL = os.environ['ETHEREUM_NODE_URL'] 40 | ``` 41 | 42 | You can also provide an **IPC path** to a node running locally, which will be faster, using _ipc://PATH_TO_IPC_SOCKET_ 43 | 44 | Number of concurrent threads connected to the ethereum node can be configured: 45 | 46 | ```python 47 | ETHEREUM_MAX_WORKERS = os.environ['ETHEREUM_MAX_WORKERS'] 48 | ``` 49 | 50 | # IPFS 51 | Provide an IPFS host and port: 52 | 53 | ```python 54 | IPFS_HOST = os.environ['IPFS_HOST'] 55 | IPFS_PORT = os.environ['IPFS_PORT'] 56 | ``` 57 | 58 | # Listening to Events 59 | Create a new array `ETH_EVENTS` in your settings file and as follows: 60 | 61 | ``` 62 | ETH_EVENTS = [ 63 | { 64 | 'ADDRESSES': ['0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B'], 65 | 'EVENT_ABI': '... ABI ...', 66 | 'EVENT_DATA_RECEIVER': 'yourmodule.event_receivers.YourReceiverClass', 67 | 'NAME': 'Your Contract Name', 68 | 'PUBLISH': True, 69 | }, 70 | { 71 | 'ADDRESSES_GETTER': 'yourmodule.address_getters.YouCustomAddressGetter', 72 | 'EVENT_ABI': '... ABI ...', 73 | 'EVENT_DATA_RECEIVER': 'chainevents.event_receivers.MarketInstanceReceiver', 74 | 'NAME': 'Standard Markets Buy/Sell/Short Receiver' 75 | } 76 | ] 77 | ``` 78 | 79 | Take a look at [Gnosis TradingDB repository](https://github.com/gnosis/pm-trading-db) and check out the full documentation. 80 | 81 | 82 | # Django-eth-events development 83 | The easiest way to start development on django-eth-events is to install dependencies using pip and a virtualenv, python3.6 is 84 | required: 85 | 86 | ``` 87 | virtualenv -p python3.6 venv 88 | source venv/bin/activate 89 | pip install -r requirements 90 | ``` 91 | 92 | ## Tests 93 | You can launch tests using `python run_tests.py`. No additional services are required. 94 | 95 | Django tests can also be used 96 | ```bash 97 | DJANGO_SETTINGS_MODULE=config.settings.test python manage.py test 98 | ``` 99 | 100 | Coverage can be run using _coverage_ tool: 101 | ```bash 102 | pip install coverage 103 | DJANGO_SETTINGS_MODULE=config.settings.test coverage run --source=django_eth_events manage.py test 104 | ``` 105 | 106 | ### Run specific test cases 107 | 108 | ```DJANGO_SETTINGS_MODULE=config.settings.test python manage.py test django_eth_events.tests.test_file``` 109 | 110 | More information at [https://docs.djangoproject.com/en/2.2/topics/testing/overview/#running-tests](https://docs.djangoproject.com/en/2.2/topics/testing/overview/#running-tests). 111 | -------------------------------------------------------------------------------- /config/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnosis/django-eth-events/19869a0f5009bf6b519a1a0d8015809cdf834150/config/settings/__init__.py -------------------------------------------------------------------------------- /config/settings/base.py: -------------------------------------------------------------------------------- 1 | INSTALLED_APPS = ( 2 | 'solo', 3 | 'django_eth_events', 4 | ) 5 | 6 | # ------------------------------------------------------------------------------ 7 | # ETHEREUM CONFIGURATION 8 | # ------------------------------------------------------------------------------ 9 | ETHEREUM_NODE_URL = 'https://mainnet.infura.io:8545' 10 | ETHEREUM_MAX_WORKERS = 10 11 | ETHEREUM_MAX_BATCH_REQUESTS = 500 12 | 13 | ETH_BACKUP_BLOCKS = 100 14 | ETH_PROCESS_BLOCKS = 10000 15 | ETH_FILTER_PROCESS_BLOCKS = 100000 16 | 17 | # ------------------------------------------------------------------------------ 18 | # CELERY CONFIGURATION 19 | # ------------------------------------------------------------------------------ 20 | # BROKER_URL = 'django://' 21 | BROKER_POOL_LIMIT = 1 22 | BROKER_CONNECTION_TIMEOUT = 10 23 | 24 | # Celery configuration 25 | CELERY_RESULT_SERIALIZER = 'json' 26 | # configure queues, currently we have only one 27 | CELERY_DEFAULT_QUEUE = 'default' 28 | 29 | # Sensible settings for celery 30 | CELERY_ALWAYS_EAGER = False 31 | CELERY_ACKS_LATE = True 32 | CELERY_TASK_PUBLISH_RETRY = True 33 | CELERY_DISABLE_RATE_LIMITS = False 34 | 35 | # By default we will ignore result 36 | # If you want to see results and try out tasks interactively, change it to False 37 | # Or change this setting on tasks level 38 | CELERY_IGNORE_RESULT = False 39 | CELERY_SEND_TASK_ERROR_EMAILS = False 40 | CELERY_TASK_RESULT_EXPIRES = 600 41 | # Don't use pickle as serializer, json is much safer 42 | CELERY_TASK_SERIALIZER = "json" 43 | CELERY_ACCEPT_CONTENT = ['application/json'] 44 | CELERYD_HIJACK_ROOT_LOGGER = False 45 | CELERYD_PREFETCH_MULTIPLIER = 1 46 | CELERYD_MAX_TASKS_PER_CHILD = 1000 47 | CELERY_LOCK_EXPIRE = 60 # 1 minute 48 | 49 | # ------------------------------------------------------------------------------ 50 | # IPFS CONFIGURATION 51 | # ------------------------------------------------------------------------------ 52 | IPFS_HOST = 'http://ipfs.infura.io' 53 | IPFS_PORT = 5001 54 | 55 | ETH_EVENTS = [] 56 | -------------------------------------------------------------------------------- /config/settings/local.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | SECRET_KEY = 'localkey' 4 | DEBUG = True 5 | 6 | ETHEREUM_NODE_URL = 'http://localhost:8545' 7 | ETHEREUM_MAX_WORKERS = 5 8 | 9 | RABBIT_HOSTNAME = 'rabbit' 10 | RABBIT_USER = 'gnosisdb' 11 | RABBIT_PASSWORD = 'gnosisdb' 12 | RABBIT_PORT = '5672' 13 | BROKER_URL = 'amqp://{user}:{password}@{hostname}:{port}'.format( 14 | user=RABBIT_USER, 15 | password=RABBIT_PASSWORD, 16 | hostname=RABBIT_HOSTNAME, 17 | port=RABBIT_PORT 18 | ) 19 | 20 | # IPFS 21 | IPFS_HOST = 'http://ipfs' # 'ipfs' 22 | IPFS_PORT = 5001 23 | 24 | 25 | DATABASES = { 26 | 'default': { 27 | 'ENGINE': 'django.db.backends.sqlite3', 28 | 'NAME': ':memory:', 29 | } 30 | } 31 | CACHES = { 32 | 'default': { 33 | 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 34 | 'LOCATION': '/var/tmp/django_cache', 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config/settings/test.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | SECRET_KEY = 'testtest' 4 | DEBUG = True 5 | 6 | ETHEREUM_NODE_URL = 'http://localhost:8545' 7 | ETHEREUM_MAX_WORKERS = 5 8 | 9 | RABBIT_HOSTNAME = 'rabbit' 10 | RABBIT_USER = 'gnosisdb' 11 | RABBIT_PASSWORD = 'gnosisdb' 12 | RABBIT_PORT = '5672' 13 | BROKER_URL = 'amqp://{user}:{password}@{hostname}:{port}'.format( 14 | user=RABBIT_USER, 15 | password=RABBIT_PASSWORD, 16 | hostname=RABBIT_HOSTNAME, 17 | port=RABBIT_PORT 18 | ) 19 | 20 | # IPFS 21 | IPFS_HOST = 'http://ipfs' # 'ipfs' 22 | IPFS_PORT = 5001 23 | 24 | 25 | DATABASES = { 26 | 'default': { 27 | 'ENGINE': 'django.db.backends.sqlite3', 28 | 'NAME': ':memory:', 29 | } 30 | } 31 | CACHES = { 32 | 'default': { 33 | 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 34 | 'LOCATION': '/var/tmp/django_cache', 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /django_eth_events/__init__.py: -------------------------------------------------------------------------------- 1 | from .apps import app as celery_app 2 | default_app_config = 'django_eth_events.apps.DjangoEthEventsConfig' 3 | __all__ = ['celery_app'] 4 | -------------------------------------------------------------------------------- /django_eth_events/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.contrib import admin 3 | from solo.admin import SingletonModelAdmin 4 | 5 | from . import models 6 | 7 | 8 | class DaemonAdmin(SingletonModelAdmin): 9 | readonly_fields = ('created', 'modified') 10 | 11 | 12 | class BlockAdmin(admin.ModelAdmin): 13 | readonly_fields = ('created', 'modified') 14 | 15 | 16 | admin.site.register(models.Daemon, DaemonAdmin) 17 | admin.site.register(models.Block, BlockAdmin) 18 | -------------------------------------------------------------------------------- /django_eth_events/apps.py: -------------------------------------------------------------------------------- 1 | from celery import Celery 2 | from django.apps import AppConfig 3 | from django.conf import settings 4 | 5 | app = Celery('django_eth_events') 6 | 7 | 8 | class DjangoEthEventsConfig(AppConfig): 9 | name = 'django_eth_events' 10 | 11 | def ready(self): 12 | super().ready() 13 | app.config_from_object('django.conf:settings') 14 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS, force=True) 15 | -------------------------------------------------------------------------------- /django_eth_events/chainevents.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Dict, List, Optional 3 | 4 | from .singleton import SingletonABC 5 | 6 | 7 | class AbstractAddressesGetter(SingletonABC): 8 | """Abstract AddressesGetter class.""" 9 | 10 | @abstractmethod 11 | def get_addresses(self) -> List[str]: pass 12 | 13 | @abstractmethod 14 | def __contains__(self, address: str) -> bool: pass 15 | 16 | 17 | class AbstractEventReceiver(ABC): 18 | """Abstract EventReceiver class.""" 19 | 20 | @abstractmethod 21 | def save(self, decoded_event: Dict, block_info: Dict) -> Optional[object]: 22 | """ 23 | Let the inheriting EventReceiver save data. The way data is handled is up to the EventReceiver. 24 | :param decoded_event: See django_eth_events.decoder.Decoder 25 | :type decoded_event: dict 26 | :param block_info: Contains data representing a Block. See https://web3py.readthedocs.io/en/stable/examples.html#looking-up-blocks 27 | :type block_info: dict 28 | :return: the updated instance or None 29 | """ 30 | pass 31 | 32 | @abstractmethod 33 | def rollback(self, decoded_event: Dict, block_info: Dict) -> Optional[object]: 34 | """ 35 | Let the inheriting EventReceiver undo saved data. The way data is handled is up to the EventReceiver. 36 | :param decoded_event: See django_eth_events.decoder.Decoder 37 | :type decoded_event: dict 38 | :param block_info: Contains data representing a Block. See https://web3py.readthedocs.io/en/stable/examples.html#looking-up-blocks 39 | :type block_info: dict 40 | """ 41 | pass 42 | -------------------------------------------------------------------------------- /django_eth_events/decoder.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | from logging import getLogger 3 | 4 | from eth_abi import decode_abi 5 | from ethereum.utils import sha3 6 | from hexbytes import HexBytes 7 | 8 | from .singleton import Singleton 9 | from .utils import normalize_address_without_0x, remove_0x_head 10 | 11 | logger = getLogger(__name__) 12 | 13 | 14 | class Decoder(Singleton): 15 | """ 16 | This module allows to decode ethereum logs (hexadecimal) into readable dictionaries, by using 17 | Contract's ABIs 18 | """ 19 | 20 | def __init__(self): 21 | self.methods = {} 22 | self.added_abis = {} 23 | self.events = set() 24 | 25 | def reset(self): 26 | self.methods.clear() 27 | self.added_abis.clear() 28 | self.events.clear() 29 | 30 | @staticmethod 31 | def get_method_id(item): 32 | if item.get('inputs'): 33 | # Generate methodID and link it with the abi 34 | method_header = "{}({})".format(item['name'], 35 | ','.join(map(lambda method_input: method_input['type'], item['inputs']))) 36 | else: 37 | method_header = "{}()".format(item['name']) 38 | 39 | return binascii.hexlify(sha3(method_header)).decode('ascii') 40 | 41 | def add_abi(self, abi) -> int: 42 | """ 43 | Add ABI array into the decoder collection, in this step the method id is generated from: 44 | sha3(function_name + '(' + param_type1 + ... + param_typeN + ')') 45 | :param abi: Array of dictionaries 46 | :return: Items added 47 | :rtype: int 48 | """ 49 | added = 0 50 | abi_sha3 = sha3(str(abi)) 51 | # Check that abi was not processed before 52 | if abi_sha3 not in self.added_abis: 53 | for item in abi: 54 | if item.get('name'): 55 | method_id = self.get_method_id(item) 56 | self.methods[method_id] = item 57 | added += 1 58 | if item.get('type') == 'event': 59 | self.events.add(HexBytes(method_id).hex()) 60 | self.added_abis[abi_sha3] = None 61 | return added 62 | 63 | def remove_abi(self, abi): 64 | """ 65 | For testing purposes, we won't sometimes to remove the ABI methods from the decoder 66 | :param abi: Array of dictionaries 67 | :return: None 68 | """ 69 | self.added_abis = {} 70 | for item in abi: 71 | if item.get('name'): 72 | method_id = self.get_method_id(item) 73 | if self.methods.get(method_id): 74 | del self.methods[method_id] 75 | 76 | def decode_log(self, log): 77 | """ 78 | Decodes an ethereum log and returns the recovered parameters along with the method from the abi that was used 79 | in decoding. Raises a LookupError if the log's topic is unknown, 80 | :param log: ethereum log 81 | :return: dictionary of decoded parameters, decoding method reference 82 | """ 83 | method_id = remove_0x_head(log['topics'][0]) 84 | 85 | if method_id not in self.methods: 86 | raise LookupError("Unknown log topic.") 87 | 88 | # method item has the event name, inputs and types 89 | method = self.methods[method_id] 90 | decoded_params = [] 91 | data_i = 0 92 | topics_i = 1 93 | data_types = [] 94 | 95 | # get param types from properties not indexed 96 | for param in method['inputs']: 97 | if not param['indexed']: 98 | data_types.append(param['type']) 99 | 100 | # decode_abi expect data in bytes format instead of str starting by 0x 101 | log_data_bytes = HexBytes(log['data']) 102 | decoded_data = decode_abi(data_types, log_data_bytes) 103 | 104 | for param in method['inputs']: 105 | decoded_p = { 106 | 'name': param['name'] 107 | } 108 | if param['indexed']: 109 | decoded_p['value'] = log['topics'][topics_i] 110 | topics_i += 1 111 | else: 112 | decoded_p['value'] = decoded_data[data_i] 113 | data_i += 1 114 | 115 | if '[]' in param['type']: 116 | if 'address' in param['type']: 117 | decoded_p['value'] = [self.decode_address(address) for address in decoded_p['value']] 118 | else: 119 | decoded_p['value'] = list(decoded_p['value']) 120 | elif 'address' == param['type']: 121 | decoded_p['value'] = self.decode_address(decoded_p['value']) 122 | 123 | decoded_params.append(decoded_p) 124 | 125 | decoded_event = { 126 | 'params': decoded_params, 127 | 'name': method['name'], 128 | 'address': self.decode_address(log['address']), 129 | 'transaction_hash': self.decode_transaction(log['transactionHash']) 130 | } 131 | 132 | return decoded_event 133 | 134 | @staticmethod 135 | def decode_address(address): 136 | if not address: 137 | raise ValueError 138 | 139 | if isinstance(address, bytes): 140 | address = address.hex() 141 | 142 | # Address length must be 40 (42 with 0x), but usually it's packed on 32 bits (length of 66 with 0x) 143 | if len(address) == 66: 144 | address = address[26:] 145 | 146 | return normalize_address_without_0x(address) 147 | 148 | @staticmethod 149 | def decode_transaction(tx_hash): 150 | if not tx_hash: 151 | raise ValueError 152 | 153 | if isinstance(tx_hash, bytes): 154 | tx_hash = tx_hash.hex() 155 | 156 | tx_hash = tx_hash.strip() # Trim spaces 157 | if tx_hash.startswith('0x'): # Remove 0x prefix 158 | tx_hash = tx_hash[2:] 159 | 160 | if len(tx_hash) < 64: 161 | raise ValueError 162 | 163 | return tx_hash 164 | 165 | def decode_logs(self, logs): 166 | """ 167 | Processes and array of ethereum logs and returns an array of dictionaries of logs that could be decoded 168 | from the ABIs loaded. Logs that could not be decoded are omitted from the result. 169 | :param logs: array of ethereum logs 170 | :return: array of dictionaries 171 | """ 172 | decoded = [] 173 | for log in logs: 174 | try: 175 | decoded.append(self.decode_log(log)) 176 | except LookupError: 177 | pass 178 | 179 | return decoded 180 | -------------------------------------------------------------------------------- /django_eth_events/event_listener.py: -------------------------------------------------------------------------------- 1 | from json import dumps, loads 2 | from typing import Set 3 | 4 | from celery.utils.log import get_task_logger 5 | from django.conf import settings 6 | from django.db import transaction 7 | from django.utils.module_loading import import_string 8 | from ethereum.utils import checksum_encode 9 | 10 | from .decoder import Decoder 11 | from .exceptions import InvalidAddressException 12 | from .models import Block, Daemon 13 | from .reorgs import check_reorg 14 | from .utils import (JsonBytesEncoder, normalize_address_without_0x, 15 | remove_0x_head) 16 | from .web3_service import Web3Service, Web3ServiceProvider 17 | 18 | logger = get_task_logger(__name__) 19 | 20 | 21 | class SingletonListener: 22 | """ 23 | Singleton class decorator for EventListener 24 | """ 25 | def __init__(self, klass): 26 | self.klass = klass 27 | self.instance = None 28 | 29 | def __call__(self, *args, **kwargs): 30 | contract_map = kwargs.get('contract_map', None) 31 | provider = kwargs.get('provider', None) 32 | 33 | different_provider = self.instance and provider and not isinstance(provider, self.instance.provider.__class__) 34 | different_contract = self.instance and contract_map and (contract_map != self.instance.original_contract_map) 35 | 36 | if different_provider or different_contract: 37 | self.instance = self.klass(contract_map=contract_map, provider=provider) 38 | elif not self.instance: 39 | # In Python 3.4+ is not allowed to send args to __new__ if __init__ 40 | # is defined 41 | # cls._instance = super().__new__(cls, *args, **kwargs) 42 | self.instance = self.klass(contract_map=contract_map, provider=provider) 43 | return self.instance 44 | 45 | 46 | @SingletonListener 47 | class EventListener: 48 | max_blocks_to_backup = settings.ETH_BACKUP_BLOCKS 49 | max_blocks_to_process = settings.ETH_PROCESS_BLOCKS 50 | blocks_to_process_with_filters = settings.ETH_FILTER_PROCESS_BLOCKS 51 | 52 | def __init__(self, contract_map=None, provider=None): 53 | self.web3_service = Web3Service(provider=provider) if provider else Web3ServiceProvider() 54 | self.web3 = self.web3_service.web3 # Gets transaction and block info from ethereum 55 | 56 | if not contract_map: 57 | # Taken from settings, it's the contracts we listen to 58 | contract_map = settings.ETH_EVENTS 59 | 60 | self.original_contract_map = contract_map 61 | self.contract_map = self.parse_contract_map(contract_map) if contract_map else contract_map 62 | 63 | # Decodes Ethereum logs 64 | self.decoder = Decoder() 65 | 66 | # Prepare decoder for contracts 67 | for contract in self.contract_map: 68 | self.decoder.add_abi(contract['EVENT_ABI']) 69 | 70 | @property 71 | def provider(self): 72 | return self.web3_service.main_provider 73 | 74 | @staticmethod 75 | def import_class_from_string(class_string): 76 | try: 77 | return import_string(class_string) 78 | except ImportError as err: 79 | logger.error("Cannot load class for contract: %s", err.msg) 80 | raise err 81 | 82 | def get_current_block_number(self): 83 | return self.web3_service.get_current_block_number() 84 | 85 | @staticmethod 86 | def next_block(cls): 87 | return Daemon.get_solo().block_number 88 | 89 | def parse_contract_map(self, contract_map): 90 | """ 91 | Resolves contracts string to their corresponding classes 92 | :param contract_map: list of dictionaries 93 | :return: parsed list of dictionaries 94 | """ 95 | 96 | # Check no names repeated in contracts 97 | names = set() 98 | contracts_parsed = [] 99 | for contract in contract_map: 100 | name = contract.get('NAME') 101 | if not name: 102 | logger.error("Missing `NAME` for event listener") 103 | raise ValueError 104 | else: 105 | if name in names: 106 | logger.error("Duplicated `NAME` %s for event listener", name) 107 | raise ValueError 108 | else: 109 | names.add(name) 110 | 111 | contract_parsed = contract.copy() 112 | # Parse addresses (normalize and remove 0x). Throws exception if address is invalid 113 | if 'ADDRESSES' in contract: 114 | contract_parsed['ADDRESSES'] = [] 115 | for address in contract['ADDRESSES']: 116 | # TODO Wait for web3 to fix it https://github.com/ethereum/web3.py/issues/715 117 | if self.web3.isAddress('0x' + remove_0x_head(address)): 118 | contract_parsed['ADDRESSES'].append(normalize_address_without_0x(address)) 119 | else: 120 | logger.error("Address %s is not valid", address) 121 | raise InvalidAddressException(address) 122 | 123 | # Remove duplicated 124 | contract_parsed['ADDRESSES'] = list(set(contract_parsed['ADDRESSES'])) 125 | 126 | if 'ADDRESSES_GETTER' in contract: 127 | contract_parsed['ADDRESSES_GETTER_CLASS'] = self.import_class_from_string(contract['ADDRESSES_GETTER'])() 128 | contract_parsed['EVENT_DATA_RECEIVER_CLASS'] = self.import_class_from_string(contract['EVENT_DATA_RECEIVER'])() 129 | contracts_parsed.append(contract_parsed) 130 | return contracts_parsed 131 | 132 | def get_next_mined_block_numbers(self, daemon_block_number, current_block_number): 133 | """ 134 | Returns a range with the block numbers of blocks mined since last event_listener execution 135 | :return: iter(int) 136 | """ 137 | logger.debug('Blocks mined, daemon-block-number=%d node-block-number=%d', 138 | daemon_block_number, current_block_number) 139 | if daemon_block_number < current_block_number: 140 | if current_block_number - daemon_block_number > self.max_blocks_to_process: 141 | blocks_to_update = range(daemon_block_number + 1, daemon_block_number + self.max_blocks_to_process) 142 | else: 143 | blocks_to_update = range(daemon_block_number + 1, current_block_number + 1) 144 | return blocks_to_update 145 | else: 146 | return range(0) 147 | 148 | def get_watched_contract_addresses(self, contract) -> Set[str]: 149 | addresses = None 150 | try: 151 | if contract.get('ADDRESSES'): 152 | addresses = contract['ADDRESSES'] 153 | elif contract.get('ADDRESSES_GETTER_CLASS'): 154 | addresses = contract['ADDRESSES_GETTER_CLASS'].get_addresses() 155 | except Exception as e: 156 | raise LookupError("Could not retrieve watched addresses for contract {}".format(contract['NAME'])) from e 157 | 158 | normalized_addresses = {checksum_encode(address) for address in addresses} 159 | return normalized_addresses 160 | 161 | @transaction.atomic 162 | def save_event(self, contract, decoded_log, block_info): 163 | event_receiver = contract['EVENT_DATA_RECEIVER_CLASS'] 164 | return event_receiver.save(decoded_event=decoded_log, block_info=block_info) 165 | 166 | @transaction.atomic 167 | def revert_events(self, event_receiver_string, decoded_event, block_info): 168 | EventReceiver = import_string(event_receiver_string) 169 | EventReceiver().rollback(decoded_event=decoded_event, block_info=block_info) 170 | 171 | @transaction.atomic 172 | def rollback(self, daemon, block_number): 173 | """ 174 | Rollback blocks and set daemon block_number to current one 175 | :param daemon: 176 | :param block_number: 177 | :return: 178 | """ 179 | # get all blocks to rollback 180 | blocks = Block.objects.filter(block_number__gt=block_number).order_by('-block_number') 181 | logger.warning('Rolling back %d blocks, until block-number=%d', blocks.count(), block_number) 182 | for block in blocks: 183 | decoded_logs = loads(block.decoded_logs) 184 | logger.warning('Rolling back %d block and %d logs', block.block_number, len(decoded_logs)) 185 | if len(decoded_logs): 186 | # We loop decoded logs on inverse order because there might be dependencies inside the same block 187 | # And must be processed from last applied to first applied 188 | for log in reversed(decoded_logs): 189 | event = log['event'] 190 | block_info = { 191 | 'hash': block.block_hash, 192 | 'number': block.block_number, 193 | 'timestamp': block.timestamp 194 | } 195 | self.revert_events(log['event_receiver'], event, block_info) 196 | 197 | # Remove backups from future blocks (old chain) 198 | blocks.delete() 199 | 200 | # set daemon block_number to current one 201 | daemon.block_number = block_number 202 | daemon.save() 203 | 204 | def backup(self, block_hash, block_number, timestamp, decoded_event, 205 | event_receiver_string): 206 | # Get block or create new one 207 | block, _ = Block.objects.get_or_create(block_hash=block_hash, 208 | defaults={'block_number': block_number, 209 | 'timestamp': timestamp} 210 | ) 211 | 212 | saved_logs = loads(block.decoded_logs) 213 | saved_logs.append({'event_receiver': event_receiver_string, 214 | 'event': decoded_event}) 215 | 216 | block.decoded_logs = dumps(saved_logs, cls=JsonBytesEncoder) 217 | block.save() 218 | 219 | @transaction.atomic 220 | def backup_blocks(self, prefetched_blocks, last_block_number): 221 | """ 222 | Backup block at batch if haven't been backed up (no logs, but we saved the hash for reorg checking anyway) 223 | :param prefetched_blocks: Every prefetched block 224 | :param last_block_number: Number of last block mined 225 | :return: 226 | """ 227 | blocks_to_backup = [] 228 | block_numbers_to_delete = [] 229 | for block_number, prefetched_block in prefetched_blocks.items(): 230 | if (block_number - last_block_number) < self.max_blocks_to_backup: 231 | blocks_to_backup.append( 232 | Block( 233 | block_number=block_number, 234 | block_hash=remove_0x_head(prefetched_block['hash']), 235 | timestamp=prefetched_block['timestamp'], 236 | ) 237 | ) 238 | block_numbers_to_delete.append(block_number) 239 | Block.objects.filter(block_number__in=block_numbers_to_delete).delete() 240 | return Block.objects.bulk_create(blocks_to_backup) 241 | 242 | def clean_useless_blocks_backup(self, daemon_block_number): 243 | """ 244 | If there's an error during block processing, some blocks will be stored and will be detected 245 | as a reorg, so this method will clean blocks stored with bigger block number than daemon block number 246 | :param daemon_block_number: 247 | :return: 248 | """ 249 | return Block.objects.filter( 250 | block_number__gt=daemon_block_number 251 | ).delete() 252 | 253 | def clean_old_blocks_backup(self, daemon_block_number): 254 | return Block.objects.filter( 255 | block_number__lt=daemon_block_number - self.max_blocks_to_backup 256 | ).delete() 257 | 258 | @transaction.atomic 259 | def execute_with_filters(self, daemon: Daemon, end_block: int): 260 | start_block = daemon.block_number 261 | 262 | logger.info('Sync with filters, start-block=%d - end-block=%d', start_block, end_block) 263 | 264 | # Store addresses with retrieved logs 265 | block_number_with_logs = {} 266 | 267 | # Cache for contracts that need access to database 268 | contract_address_cache = {} 269 | 270 | # Every contract address. They will be used to know which blocks have to be retrieved for sure 271 | contract_addresses = set() 272 | block_numbers_to_be_prefetched = set() 273 | for contract in self.contract_map: 274 | contract_addresses.update(self.get_watched_contract_addresses(contract)) 275 | 276 | # Load logs for every event 277 | events = self.decoder.events 278 | for event in events: 279 | logger.info('Using filter to get logs for event=%s from block=%d to block=%d', 280 | event, 281 | start_block, 282 | end_block) 283 | logs = self.web3_service.get_logs_for_event_using_filter(start_block, end_block, event) 284 | logger.info('Found %d logs for event=%s', len(logs), event) 285 | for log in logs: 286 | block_number = log['blockNumber'] 287 | block_number_with_logs.setdefault(block_number, []).append(log) 288 | if log['address'] in contract_addresses: 289 | block_numbers_to_be_prefetched.add(block_number) 290 | 291 | logger.info('Start prefetching of %d blocks', len(block_numbers_to_be_prefetched)) 292 | prefetched_blocks = self.web3_service.get_blocks(list(block_numbers_to_be_prefetched)) 293 | logger.info('End block prefetching') 294 | 295 | # Start in the first block with logs. 296 | # For example, if we start in block 5 but no logs are found until 200, we start in the 200 297 | new_start_block = max(min(block_number_with_logs), start_block) if block_number_with_logs else end_block 298 | if new_start_block != start_block: 299 | logger.info('No logs found from block=%d to block=%d, so starting in block=%d', 300 | start_block, new_start_block, new_start_block) 301 | 302 | for block_number in range(new_start_block, end_block): 303 | logger.debug('Processing block %d', block_number) 304 | logs = block_number_with_logs.get(block_number) 305 | if not logs: 306 | continue 307 | 308 | # Don't load block if not needed, can be `None` if not prefetched 309 | current_block = prefetched_blocks.get(block_number) 310 | 311 | ########################### 312 | # Decode logs # 313 | ########################### 314 | for contract in self.contract_map: 315 | # Query cache before retrieving contract addresses from database 316 | if contract['NAME'] not in contract_address_cache: 317 | contract_address_cache[contract['NAME']] = self.get_watched_contract_addresses(contract) 318 | watched_addresses = contract_address_cache[contract['NAME']] 319 | 320 | # Filter logs by relevant addresses 321 | target_logs = [log for log in logs if log['address'] in watched_addresses] 322 | 323 | if target_logs: 324 | logger.info('Contract=%s Block=%d -> Found %d relevant logs', 325 | contract['NAME'], 326 | block_number, 327 | len(target_logs)) 328 | 329 | decoded_logs = self.decoder.decode_logs(target_logs) 330 | 331 | if decoded_logs: 332 | logger.info('Contract=%s Block=%d -> Decoded %d relevant logs', 333 | contract['NAME'], 334 | block_number, 335 | len(decoded_logs)) 336 | 337 | # Save events 338 | for decoded_log in decoded_logs: 339 | # Fetch block if not recovered yet 340 | if not current_block: 341 | current_block = self.web3_service.get_block(block_number) 342 | 343 | instance = self.save_event(contract, decoded_log, current_block) 344 | 345 | # Only valid data is saved in backup 346 | if instance is not None: 347 | # Clear cache, maybe new addresses are stored 348 | contract_address_cache.clear() 349 | 350 | if (end_block - block_number) < self.max_blocks_to_backup: 351 | self.backup( 352 | remove_0x_head(current_block['hash']), 353 | current_block['number'], 354 | current_block['timestamp'], 355 | decoded_log, 356 | contract['EVENT_DATA_RECEIVER'] 357 | ) 358 | 359 | logger.info('Contract=%s Block=%d -> Processed %d relevant logs', 360 | contract['NAME'], 361 | block_number, 362 | len(decoded_logs)) 363 | 364 | daemon.block_number = block_number 365 | logger.debug('Ended processing of block_number=%d', block_number) 366 | 367 | daemon.block_number = end_block 368 | daemon.save() 369 | 370 | def execute(self): 371 | """ 372 | :raises: Web3ConnectionException 373 | """ 374 | 375 | # When we have address getters caching can save us a lot of time 376 | contract_address_cache = {} 377 | 378 | daemon = Daemon.get_solo() 379 | 380 | self.clean_useless_blocks_backup(daemon.block_number) 381 | current_block_number = self.web3_service.get_current_block_number() 382 | 383 | # Use filters for first sync 384 | if (current_block_number - daemon.block_number) > self.max_blocks_to_backup: 385 | self.clean_old_blocks_backup(daemon.block_number) 386 | return self.execute_with_filters(daemon, 387 | min(daemon.block_number + self.blocks_to_process_with_filters, 388 | current_block_number - self.max_blocks_to_backup) 389 | ) 390 | 391 | had_reorg, reorg_block_number = check_reorg(daemon.block_number, 392 | current_block_number, 393 | provider=self.provider) 394 | if had_reorg: 395 | # Daemon block_number could be modified 396 | self.rollback(daemon, reorg_block_number) 397 | 398 | # Get block numbers of next mined blocks not processed yet 399 | next_mined_block_numbers = self.get_next_mined_block_numbers(daemon_block_number=daemon.block_number, 400 | current_block_number=current_block_number) 401 | if not next_mined_block_numbers: 402 | logger.info('No blocks mined, daemon-block-number=%d, node-block-number=%d', 403 | daemon.block_number, 404 | current_block_number) 405 | else: 406 | logger.info('Blocks mined from %d to %d, prefetching %d blocks, daemon-block-number=%d', 407 | next_mined_block_numbers[0], 408 | next_mined_block_numbers[-1], 409 | len(next_mined_block_numbers), 410 | daemon.block_number) 411 | 412 | last_mined_block_number = next_mined_block_numbers[-1] 413 | prefetched_blocks = self.web3_service.get_blocks(next_mined_block_numbers) 414 | logger.debug('Finished blocks prefetching') 415 | 416 | logger.info('Start log prefetching') 417 | prefetched_logs = self.web3_service.get_logs_for_blocks(prefetched_blocks.values()) 418 | logger.info('End log prefetching') 419 | 420 | self.backup_blocks(prefetched_blocks, last_mined_block_number) 421 | logger.debug('Finished blocks backup') 422 | 423 | for current_block_number in next_mined_block_numbers: 424 | self.process_block(daemon, 425 | prefetched_blocks[current_block_number], 426 | prefetched_logs[current_block_number], 427 | current_block_number, 428 | last_mined_block_number, 429 | contract_address_cache) 430 | 431 | # Remove older backups 432 | self.clean_old_blocks_backup(daemon.block_number) 433 | 434 | logger.info('Ended processing of chunk, daemon-block-number=%d', daemon.block_number) 435 | 436 | @transaction.atomic 437 | def process_block(self, daemon, current_block, logs, current_block_number, last_mined_block_number, 438 | contract_address_cache): 439 | # logger.debug('Getting every log for block_number=%d', current_block['number']) 440 | # logs = self.web3_service.get_logs(current_block) 441 | logger.debug('Got %d logs in block_number=%d', len(logs), current_block['number']) 442 | 443 | ########################### 444 | # Decode logs # 445 | ########################### 446 | if logs: 447 | for contract in self.contract_map: 448 | 449 | # Get watched contract addresses 450 | if contract['NAME'] not in contract_address_cache: 451 | contract_address_cache[contract['NAME']] = self.get_watched_contract_addresses(contract) 452 | watched_addresses = contract_address_cache[contract['NAME']] 453 | 454 | # Filter logs by relevant addresses 455 | target_logs = [log for log in logs if self.web3.toChecksumAddress(log['address']) in watched_addresses] 456 | 457 | if target_logs: 458 | logger.info('Found %d relevant logs in block %d', len(target_logs), current_block_number) 459 | 460 | # Decode logs 461 | decoded_logs = self.decoder.decode_logs(target_logs) 462 | 463 | if decoded_logs: 464 | # Clear cache, maybe new addresses are stored 465 | contract_address_cache.clear() 466 | 467 | logger.info('Decoded %d relevant logs in block %d', len(decoded_logs), current_block_number) 468 | 469 | for log in decoded_logs: 470 | # Save events 471 | instance = self.save_event(contract, log, current_block) 472 | 473 | # Only valid data is saved in backup 474 | if instance is not None: 475 | if (current_block_number - last_mined_block_number) < self.max_blocks_to_backup: 476 | self.backup( 477 | remove_0x_head(current_block['hash']), 478 | current_block['number'], 479 | current_block['timestamp'], 480 | log, 481 | contract['EVENT_DATA_RECEIVER'] 482 | ) 483 | 484 | logger.info('Processed %d relevant logs in block %d', len(decoded_logs), current_block_number) 485 | 486 | daemon.block_number = current_block_number 487 | # Make changes persistent, update block_number 488 | daemon.save() 489 | logger.debug('Ended processing of block_number=%d', current_block['number']) 490 | -------------------------------------------------------------------------------- /django_eth_events/exceptions.py: -------------------------------------------------------------------------------- 1 | class UnknownBlockReorgException(Exception): 2 | pass 3 | 4 | 5 | class NoBackupException(Exception): 6 | def __init__(self, message, errors): 7 | super().__init__(message) 8 | 9 | # Now for your custom code... 10 | self.errors = errors 11 | 12 | 13 | class Web3ConnectionException(Exception): 14 | pass 15 | 16 | 17 | class UnknownBlock(Exception): 18 | pass 19 | 20 | 21 | class UnknownTransaction(Exception): 22 | pass 23 | 24 | 25 | class InvalidAddressException(Exception): 26 | pass 27 | -------------------------------------------------------------------------------- /django_eth_events/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from .models import Daemon 4 | 5 | 6 | class DaemonFactory(factory.DjangoModelFactory): 7 | 8 | class Meta: 9 | model = Daemon 10 | 11 | block_number = 0 12 | -------------------------------------------------------------------------------- /django_eth_events/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnosis/django-eth-events/19869a0f5009bf6b519a1a0d8015809cdf834150/django_eth_events/management/__init__.py -------------------------------------------------------------------------------- /django_eth_events/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnosis/django-eth-events/19869a0f5009bf6b519a1a0d8015809cdf834150/django_eth_events/management/commands/__init__.py -------------------------------------------------------------------------------- /django_eth_events/management/commands/resync_daemon.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from ...models import Block, Daemon 4 | 5 | 6 | class Command(BaseCommand): 7 | help = 'Force daemon to sync deleting blocks and restoring status' 8 | 9 | def handle(self, *args, **options): 10 | Block.objects.all().delete() 11 | Daemon.objects.all().delete() 12 | self.stdout.write(self.style.SUCCESS('Forcing daemon sync')) 13 | -------------------------------------------------------------------------------- /django_eth_events/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-06-27 16:56 3 | from __future__ import unicode_literals 4 | 5 | import django.utils.timezone 6 | import model_utils.fields 7 | from django.db import migrations, models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Daemon', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), 23 | ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), 24 | ('block_number', models.IntegerField(default=0)), 25 | ], 26 | options={ 27 | 'abstract': False, 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /django_eth_events/migrations/0002_daemon_last_error_block_number.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-08-16 08:59 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_eth_events', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='daemon', 17 | name='last_error_block_number', 18 | field=models.IntegerField(default=0), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /django_eth_events/migrations/0003_block.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-11-23 09:22 3 | from __future__ import unicode_literals 4 | 5 | import django.utils.timezone 6 | import model_utils.fields 7 | from django.db import migrations, models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('django_eth_events', '0002_daemon_last_error_block_number'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Block', 19 | fields=[ 20 | ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), 21 | ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), 22 | ('block_number', models.IntegerField()), 23 | ('block_hash', models.CharField(max_length=64, primary_key=True, serialize=False)), 24 | ('decoded_logs', models.TextField(default='[]')), 25 | ], 26 | options={ 27 | 'abstract': False, 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /django_eth_events/migrations/0004_auto_20171127_0501.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-11-27 05:01 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_eth_events', '0003_block'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='block', 17 | name='decoded_logs', 18 | field=models.TextField(default='{}'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /django_eth_events/migrations/0005_daemon_status.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-11-29 08:26 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_eth_events', '0004_auto_20171127_0501'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='daemon', 17 | name='status', 18 | field=models.CharField(choices=[('EXECUTING', 'Normal execution'), ('HALTED', 'System halted, there was an error')], default='EXECUTING', max_length=9), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /django_eth_events/migrations/0006_block_timestamp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-11-29 14:46 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_eth_events', '0005_daemon_status'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='block', 17 | name='timestamp', 18 | field=models.IntegerField(default=0), 19 | preserve_default=False, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /django_eth_events/migrations/0007_auto_20171207_0759.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-12-07 07:59 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_eth_events', '0006_block_timestamp'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='block', 17 | name='decoded_logs', 18 | field=models.TextField(default='[]'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /django_eth_events/migrations/0008_daemon_listener_lock.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-12-08 09:34 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_eth_events', '0007_auto_20171207_0759'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='daemon', 17 | name='listener_lock', 18 | field=models.BooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /django_eth_events/migrations/0009_daemon_last_error_date_time.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-03-09 09:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_eth_events', '0008_daemon_listener_lock'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='daemon', 15 | name='last_error_date_time', 16 | field=models.DateTimeField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /django_eth_events/migrations/0010_remove_daemon_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-05-22 09:43 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_eth_events', '0009_daemon_last_error_date_time'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='daemon', 15 | name='status', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /django_eth_events/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnosis/django-eth-events/19869a0f5009bf6b519a1a0d8015809cdf834150/django_eth_events/migrations/__init__.py -------------------------------------------------------------------------------- /django_eth_events/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from model_utils.models import TimeStampedModel 3 | from solo.models import SingletonModel 4 | 5 | 6 | class Daemon(TimeStampedModel, SingletonModel): 7 | block_number = models.IntegerField(default=0) 8 | last_error_block_number = models.IntegerField(default=0) 9 | last_error_date_time = models.DateTimeField(null=True, blank=True) 10 | listener_lock = models.BooleanField(default=False) 11 | 12 | def __str__(self): 13 | return "Daemon at block {}".format(self.block_number) 14 | 15 | 16 | class Block(TimeStampedModel): 17 | block_number = models.IntegerField() 18 | block_hash = models.CharField(primary_key=True, max_length=64) 19 | decoded_logs = models.TextField(default="[]") 20 | timestamp = models.IntegerField() 21 | 22 | def __str__(self): 23 | with_logs = " with decoded logs" if len(self.decoded_logs) > 2 else "" 24 | return 'Block {}{}'.format(self.block_number, with_logs) 25 | -------------------------------------------------------------------------------- /django_eth_events/reorgs.py: -------------------------------------------------------------------------------- 1 | from .exceptions import NoBackupException, UnknownBlockReorgException 2 | from .models import Block 3 | from .utils import remove_0x_head 4 | from .web3_service import Web3Service, Web3ServiceProvider 5 | 6 | 7 | def check_reorg(daemon_block_number, current_block_number=None, provider=None): 8 | """ 9 | Checks if reorgs are happening 10 | :param daemon_block_number: daemon database block_number 11 | :param current_block_number: current block_number 12 | :param provider: optional Web3 provider instance 13 | :return: Tuple (True|False, None|Block number) 14 | :raise Web3ConnectionException 15 | :raise UnknownBlockReorg 16 | :raise NoBackup 17 | """ 18 | web3_service = Web3Service(provider=provider) if provider else Web3ServiceProvider() 19 | current_block_number = current_block_number if current_block_number else web3_service.get_current_block_number() 20 | 21 | if current_block_number >= daemon_block_number: 22 | # check last saved block hash haven't changed 23 | blocks = Block.objects.all().order_by('-block_number') 24 | if blocks.count(): 25 | # check if there was reorg 26 | for block in blocks: 27 | try: 28 | node_block_hash = remove_0x_head(web3_service.get_block(block.block_number)['hash']) 29 | except: 30 | raise UnknownBlockReorgException 31 | if block.block_hash == node_block_hash: 32 | # if is last saved block, no reorg 33 | if block.block_number == daemon_block_number: 34 | return False, None 35 | else: 36 | # there was a reorg from a saved block, we can do rollback 37 | return True, block.block_number 38 | 39 | # Exception, no saved history enough 40 | errors = { 41 | 'daemon_block_number': daemon_block_number, 42 | 'current_block_number': current_block_number, 43 | 'las_saved_block_hash': blocks[0].block_hash 44 | } 45 | raise NoBackupException(message='Not enough backup blocks, reorg cannot be rollback', errors=errors) 46 | 47 | else: 48 | # No backup data 49 | return False, None 50 | else: 51 | # check last common block hash haven't changed 52 | blocks = Block.objects.filter(block_number__lte=current_block_number).order_by('-block_number') 53 | if blocks: 54 | # check if there was reorg 55 | for block in blocks: 56 | try: 57 | node_block_hash = remove_0x_head(web3_service.get_block(block.block_number)['hash']) 58 | except: 59 | raise UnknownBlockReorgException 60 | if block.block_hash == node_block_hash: 61 | # if is last saved block, no reorg 62 | if block.block_number == daemon_block_number: 63 | return False, None 64 | else: 65 | # there was a reorg from a saved block, we can do rollback 66 | return True, block.block_number 67 | 68 | # Exception, no saved history enough 69 | errors = { 70 | 'daemon_block_number': daemon_block_number, 71 | 'current_block_number': current_block_number, 72 | 'las_saved_block_hash': blocks[0].block_hash 73 | } 74 | raise NoBackupException(message='Not enough backup blocks, reorg cannot be rolled back', errors=errors) 75 | else: 76 | # No backup data 77 | return False, None 78 | -------------------------------------------------------------------------------- /django_eth_events/singleton.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | 4 | class Singleton(object): 5 | _instance = None 6 | 7 | def __new__(cls, *args, **kwargs): 8 | if not isinstance(cls._instance, cls): 9 | # In Python 3.4+ is not allowed to send args to __new__ if __init__ 10 | # is defined 11 | # cls._instance = super().__new__(cls, *args, **kwargs) 12 | cls._instance = super().__new__(cls) 13 | return cls._instance 14 | 15 | 16 | class SingletonABC(ABC): 17 | _instances = {} 18 | 19 | def __call__(cls, *args, **kwargs): 20 | if cls not in cls._instances: 21 | cls._instances[cls] = super().__call__(*args, **kwargs) 22 | return cls._instances[cls] 23 | -------------------------------------------------------------------------------- /django_eth_events/tasks.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from datetime import timedelta 3 | 4 | from celery import shared_task 5 | from celery.utils.log import get_task_logger 6 | from django.core.mail import mail_admins 7 | from django.db import transaction 8 | from django.utils import timezone 9 | 10 | from .event_listener import EventListener 11 | from .exceptions import (UnknownBlock, UnknownBlockReorgException, 12 | UnknownTransaction, Web3ConnectionException) 13 | from .models import Daemon 14 | from .utils import is_network_error 15 | 16 | logger = get_task_logger(__name__) 17 | 18 | 19 | def send_email(message): 20 | logger.info('Sending email with text: %s', message) 21 | # send email 22 | mail_admins('[ETH Events Error] ', message) 23 | 24 | 25 | @shared_task 26 | def event_listener(provider=None): 27 | with transaction.atomic(): 28 | daemon = Daemon.objects.select_for_update().first() 29 | if not daemon: 30 | logger.debug('Daemon singleton row was not created, creating') 31 | daemon = Daemon.get_solo() 32 | locked = daemon.listener_lock 33 | if not locked: 34 | logger.debug('LOCK acquired') 35 | daemon.listener_lock = True 36 | daemon.save() 37 | if locked: 38 | logger.debug('LOCK already being imported by another worker') 39 | else: 40 | try: 41 | el = EventListener(provider=provider) 42 | el.execute() 43 | except UnknownTransaction: 44 | logger.warning('Unknown Transaction hash, might be a reorg', exc_info=True) 45 | except UnknownBlock: 46 | logger.warning('Cannot get block by number/hash, might be a reorg', exc_info=True) 47 | except UnknownBlockReorgException: 48 | logger.warning('Unknown Block hash, might be a reorg', exc_info=True) 49 | except Web3ConnectionException: 50 | logger.warning('Web3 cannot connect to provider/s', exc_info=True) 51 | except Exception as err: 52 | if is_network_error(err): 53 | logger.warning('Network error', exc_info=True) 54 | else: 55 | logger.error('An unhandled error occurred', exc_info=True) 56 | daemon = Daemon.get_solo() 57 | 58 | # get last error block number database 59 | last_error_block_number = daemon.last_error_block_number 60 | # get current block number from database 61 | current_block_number = daemon.block_number 62 | logger.error('Daemon block number: %d, Last error block number: %d', 63 | current_block_number, last_error_block_number) 64 | 65 | if last_error_block_number < current_block_number: 66 | # save block number into cache 67 | daemon.last_error_block_number = current_block_number 68 | daemon.last_error_date_time = timezone.now() 69 | 70 | daemon.save() 71 | finally: 72 | logger.debug('Releasing LOCK') 73 | with transaction.atomic(): 74 | daemon = Daemon.objects.select_for_update().first() 75 | daemon.listener_lock = False 76 | daemon.save() 77 | 78 | 79 | @shared_task 80 | def deadlock_checker(lock_interval=60000): 81 | """ 82 | Verifies whether celery tasks over the Daemon table are deadlocked. 83 | :param lock_interval: milliseconds 84 | """ 85 | try: 86 | logger.info("Deadlock checker, lock_interval %d" % lock_interval) 87 | daemon = Daemon.get_solo() 88 | valid_interval = timezone.now() - timedelta(milliseconds=lock_interval) 89 | if daemon.modified < valid_interval and daemon.listener_lock is True: 90 | # daemon is deadlocked 91 | logger.info('Found deadlocked Daemon task, block number %d' % daemon.block_number) 92 | daemon.listener_lock = False 93 | daemon.save() 94 | except Exception: 95 | logger.exception("Problem found using deadlock checker") 96 | send_email(traceback.format_exc()) 97 | -------------------------------------------------------------------------------- /django_eth_events/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnosis/django-eth-events/19869a0f5009bf6b519a1a0d8015809cdf834150/django_eth_events/tests/__init__.py -------------------------------------------------------------------------------- /django_eth_events/tests/mocked_testrpc_reorg.py: -------------------------------------------------------------------------------- 1 | from http.server import BaseHTTPRequestHandler 2 | from json import dumps, loads 3 | 4 | from django.core.cache import cache 5 | 6 | 7 | class MockedTestrpc(BaseHTTPRequestHandler): 8 | 9 | def _set_headers(self): 10 | self.send_response(200) 11 | self.send_header('Content-type', 'application/json') 12 | self.end_headers() 13 | 14 | def do_HEAD(self): 15 | self._set_headers() 16 | 17 | def do_POST(self): 18 | # Doesn't do anything with posted data 19 | self._set_headers() 20 | content_len = int(self.headers.get('content-length', 0)) 21 | post_body = loads(self.rfile.read(content_len)) 22 | print(post_body) 23 | print(post_body['method']) 24 | if post_body['method'] == 'eth_blockNumber': 25 | self.wfile.write(dumps({"result": 26 | cache.get('block_number')}).encode()) 27 | elif post_body['method'] == 'eth_getBlockByNumber': 28 | hash = cache.get(post_body['params'][0]) 29 | self.wfile.write(dumps({'result': {'hash': hash}}).encode()) 30 | elif post_body['method'] == 'web3_clientVersion': 31 | self.wfile.write(dumps({ 32 | 'jsonrpc': '2.0' 33 | }).encode()) 34 | elif post_body['method'] == 'net_version': 35 | self.wfile.write(dumps({ 36 | 'jsonrpc': '2.0', 37 | 'result': '5', 38 | }).encode()) 39 | else: 40 | self.wfile.write('{"code":32601}'.encode()) 41 | -------------------------------------------------------------------------------- /django_eth_events/tests/test_celery.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from time import sleep 3 | 4 | from django.conf import settings 5 | from django.test import TestCase 6 | from eth_tester import EthereumTester 7 | from web3.providers.eth_tester import EthereumTesterProvider 8 | 9 | from ..chainevents import AbstractEventReceiver 10 | from ..factories import DaemonFactory 11 | from ..models import Block, Daemon 12 | from ..tasks import deadlock_checker, event_listener 13 | from ..web3_service import Web3Service 14 | from .utils import centralized_oracle_abi, centralized_oracle_bytecode 15 | 16 | 17 | class DummyEventReceiver(AbstractEventReceiver): 18 | def save(self, decoded_event, block_info): 19 | return decoded_event 20 | 21 | def rollback(self, decoded_event, block_info): 22 | pass 23 | 24 | 25 | class TestCelery(TestCase): 26 | 27 | def setUp(self): 28 | self.web3 = Web3Service(provider=EthereumTesterProvider(EthereumTester())).web3 29 | self.provider = self.web3.providers[0] 30 | self.web3.eth.defaultAccount = self.web3.eth.coinbase 31 | self.tx_data = {'from': self.web3.eth.coinbase, 32 | 'gas': 1000000} 33 | self.event_receivers = [] 34 | 35 | def tearDown(self): 36 | # Delete centralized oracles 37 | self.provider.ethereum_tester.reset_to_genesis() 38 | self.assertEqual(0, self.web3.eth.blockNumber) 39 | 40 | def test_deadlock_checker(self): 41 | daemon = DaemonFactory(listener_lock=True) 42 | # sleep process to simulate old Daemon instance 43 | sleep(2) 44 | deadlock_checker(2000) # 2 seconds 45 | daemon_test = Daemon.get_solo() 46 | # Test deadlock detection 47 | self.assertEqual(daemon_test.listener_lock, False) 48 | 49 | daemon.listener_lock = True 50 | daemon.save() 51 | deadlock_checker() 52 | daemon_test = Daemon.get_solo() 53 | self.assertEqual(daemon_test.listener_lock, True) 54 | 55 | def test_event_listener(self): 56 | daemon_factory = DaemonFactory(listener_lock=False) 57 | # Number of blocks analyzed by Event Listener 58 | n_blocks = Block.objects.all().count() 59 | # Create centralized oracle factory contract 60 | centralized_contract_factory = self.web3.eth.contract(abi=centralized_oracle_abi, 61 | bytecode=centralized_oracle_bytecode) 62 | tx_hash = centralized_contract_factory.constructor().transact() 63 | centralized_oracle_factory_address = self.web3.eth.getTransactionReceipt(tx_hash).get('contractAddress') 64 | centralized_oracle_factory = self.web3.eth.contract(centralized_oracle_factory_address, 65 | abi=centralized_oracle_abi) 66 | 67 | # Event receiver 68 | centralized_event_receiver = { 69 | 'NAME': 'Centralized Oracle Factory', 70 | 'EVENT_ABI': centralized_oracle_abi, 71 | 'EVENT_DATA_RECEIVER': 'django_eth_events.tests.test_celery.DummyEventReceiver', 72 | 'ADDRESSES': [centralized_oracle_factory_address[2::]] 73 | } 74 | 75 | self.event_receivers.append(centralized_event_receiver) 76 | 77 | with self.settings(ETH_EVENTS=self.event_receivers): 78 | # Start Celery Task 79 | event_listener(self.provider) 80 | # Create centralized oracle 81 | centralized_oracle_factory.functions.createCentralizedOracle( 82 | b'QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG').transact(self.tx_data) 83 | # Run event listener again 84 | event_listener(self.provider) 85 | # Do checks 86 | daemon = Daemon.get_solo() 87 | self.assertEqual(daemon.block_number, daemon_factory.block_number + 2) 88 | self.assertEqual(Block.objects.all().count(), n_blocks + 2) 89 | self.assertFalse(daemon.listener_lock) 90 | 91 | def test_event_listener_with_no_blocks(self): 92 | with self.settings(ETH_EVENTS=self.event_receivers): 93 | # Start Celery Task 94 | event_listener(self.provider) 95 | # Do checks 96 | daemon = Daemon.get_solo() 97 | self.assertEqual(daemon.block_number, 0) 98 | self.assertEqual(Block.objects.all().count(), 0) 99 | self.assertFalse(daemon.listener_lock) 100 | -------------------------------------------------------------------------------- /django_eth_events/tests/test_chainevents.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from ..chainevents import AbstractEventReceiver, AbstractAddressesGetter 3 | 4 | 5 | class EventReceiverImpl(AbstractEventReceiver): 6 | pass 7 | 8 | 9 | class AddressGetterImpl(AbstractAddressesGetter): 10 | pass 11 | 12 | 13 | class TestAbstractClasses(TestCase): 14 | 15 | def test_abstract_class_detected(self): 16 | # Make sure abstract classes gets detected as abstracts. This to prevent 17 | # not desired behaviour when updating Python version. 18 | with self.assertRaises(TypeError): 19 | EventReceiverImpl() 20 | 21 | with self.assertRaises(TypeError): 22 | AddressGetterImpl() 23 | -------------------------------------------------------------------------------- /django_eth_events/tests/test_daemon_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.test import TestCase 3 | 4 | from ..factories import DaemonFactory 5 | from ..models import Daemon 6 | 7 | 8 | class TestDaemonModel(TestCase): 9 | def test_default_value(self): 10 | daemon = DaemonFactory() 11 | self.assertIsNotNone(daemon.pk) 12 | self.assertEqual(daemon.block_number, 0) 13 | 14 | def test_singleton(self): 15 | self.assertEqual(0, Daemon.objects.all().count()) 16 | DaemonFactory() 17 | d1 = Daemon.get_solo() 18 | self.assertEqual(1, Daemon.objects.all().count()) 19 | d2 = Daemon.get_solo() 20 | self.assertEqual(1, Daemon.objects.all().count()) 21 | self.assertEqual(d1.pk, d2.pk) 22 | -------------------------------------------------------------------------------- /django_eth_events/tests/test_decoder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from json import loads 3 | 4 | from django.test import TestCase 5 | from hexbytes import HexBytes 6 | 7 | from ..decoder import Decoder 8 | 9 | 10 | class TestDecoder(TestCase): 11 | test_abi = loads( 12 | '[{"inputs": [{"type": "address", "name": ""}], "constant": true, "name": "isInstantiation", "payable": ' 13 | 'false, "outputs": [{"type": "bool", "name": ""}], "type": "function"}, {"inputs": [{"type": "address[]", ' 14 | '"name": "_owners"}, {"type": "uint256", "name": "_required"}, {"type": "uint256", "name": "_dailyLimit"}], ' 15 | '"constant": false, "name": "create", "payable": false, "outputs": [{"type": "address", "name": "wallet"}], ' 16 | '"type": "function"}, {"inputs": [{"type": "address", "name": ""}, {"type": "uint256", "name": ""}], ' 17 | '"constant": true, "name": "instantiations", "payable": false, "outputs": [{"type": "address", "name": ""}], ' 18 | '"type": "function"}, {"inputs": [{"type": "address", "name": "creator"}], "constant": true, ' 19 | '"name": "getInstantiationCount", "payable": false, "outputs": [{"type": "uint256", "name": ""}], ' 20 | '"type": "function"}, {"inputs": [{"indexed": false, "type": "address", "name": "sender"}, {"indexed": false, ' 21 | '"type": "address", "name": "instantiation"}], "type": "event", "name": "ContractInstantiation", "anonymous": ' 22 | 'false}]') 23 | decoder = Decoder() 24 | 25 | def setUp(self): 26 | self.decoder.reset() 27 | 28 | def test_add_abis(self): 29 | self.decoder.add_abi([]) 30 | self.assertEqual(len(self.decoder.methods), 0) 31 | self.assertEqual(self.decoder.add_abi(self.test_abi), 5) 32 | # Make sure second time same abi is not processed 33 | self.assertEqual(self.decoder.add_abi(self.test_abi), 0) 34 | self.assertEqual(len(self.decoder.methods), 5) 35 | self.decoder.remove_abi([]) 36 | self.assertEqual(len(self.decoder.methods), 5) 37 | self.decoder.remove_abi(loads('[{"inputs": [{"type": "address", "name": ""}], "constant": true, "name": "isInstantiation", "payable": false, "outputs": [{"type": "bool", "name": ""}], "type": "function"}]')) 38 | self.assertEqual(len(self.decoder.methods), 4) 39 | self.decoder.remove_abi(self.test_abi) 40 | self.assertEqual(len(self.decoder.methods), 0) 41 | 42 | def test_decode_logs(self): 43 | logs = [ 44 | { 45 | 'address': '0xa6d9c5f7d4de3cef51ad3b7235d79ccc95114de5', 46 | 'data': u"0x00000000000000000000000065039084cc6f4773291a6ed7dcf5bc3a2e894ff300000000000000000000000017e054b16ca658789c927c854976450adbda7df0", 47 | 'transactionHash': '0x54041b3ce0976ee17212100f42b3793fa4ee5f869a6d107249a75caa5fc1b8aa', 48 | 'topics': [ 49 | HexBytes('0x4fb057ad4a26ed17a57957fa69c306f11987596069b89521c511fc9a894e6161') 50 | ] 51 | } 52 | ] 53 | self.assertListEqual([], self.decoder.decode_logs(logs)) 54 | self.decoder.add_abi(self.test_abi) 55 | decoded = self.decoder.decode_logs(logs) 56 | self.assertIsNotNone(decoded) 57 | self.assertListEqual( 58 | [ 59 | { 60 | 'address': 'a6d9c5f7d4de3cef51ad3b7235d79ccc95114de5', 61 | 'name': 'ContractInstantiation', 62 | 'transaction_hash': logs[0]['transactionHash'][2:], # without `0x` 63 | 'params': [ 64 | { 65 | 'name': 'sender', 66 | 'value': '65039084cc6f4773291a6ed7dcf5bc3a2e894ff3' 67 | }, 68 | { 69 | 'name': 'instantiation', 70 | 'value': '17e054b16ca658789c927c854976450adbda7df0' 71 | } 72 | ] 73 | } 74 | ], 75 | decoded 76 | ) 77 | 78 | def test_decode_transaction_hash(self): 79 | self.decoder.add_abi(self.test_abi) 80 | 81 | base_logs = [ 82 | { 83 | 'address': '0xa6d9c5f7d4de3cef51ad3b7235d79ccc95114de5', 84 | 'data': u"0x00000000000000000000000065039084cc6f4773291a6ed7dcf5bc3a2e894ff300000000000000000000000017e054b16ca658789c927c854976450adbda7df0", 85 | 'transactionHash': '0x54041b3ce0976ee17212100f42b3793fa4ee5f869a6d107249a75caa5fc1b8aa', 86 | 'topics': [ 87 | HexBytes('0x4fb057ad4a26ed17a57957fa69c306f11987596069b89521c511fc9a894e6161') 88 | ] 89 | } 90 | ] 91 | 92 | logs_with_hex_tx_hash = [{ 93 | **base_logs[0], 94 | 'transactionHash': HexBytes('0x54041b3ce0976ee17212100f42b3793fa4ee5f869a6d107249a75caa5fc1b8aa') 95 | }] 96 | decoded = self.decoder.decode_logs(logs_with_hex_tx_hash) 97 | # Test decoded transaction_hash is without `0x` prefix 98 | self.assertEqual(decoded[0]['transaction_hash'], logs_with_hex_tx_hash[0]['transactionHash'].hex()[2:]) 99 | self.assertFalse(decoded[0]['transaction_hash'].startswith('0x')) 100 | 101 | logs_with_hex_tx_hash = [{ 102 | **base_logs[0], 103 | 'transactionHash': bytes(HexBytes('54041b3ce0976ee17212100f42b3793fa4ee5f869a6d107249a75caa5fc1b8aa')).hex() 104 | }] 105 | decoded = self.decoder.decode_logs(logs_with_hex_tx_hash) 106 | # Test decoded transaction_hash is without `0x` prefix 107 | self.assertEqual(decoded[0]['transaction_hash'], logs_with_hex_tx_hash[0]['transactionHash']) 108 | self.assertFalse(decoded[0]['transaction_hash'].startswith('0x')) 109 | 110 | logs_with_hex_tx_hash = [{ 111 | **base_logs[0], 112 | 'transactionHash': bytes(HexBytes('54041b3ce0976ee17212100f42b3793fa4ee5f869a6d107249a75caa5fc1b8aa')) 113 | }] 114 | decoded = self.decoder.decode_logs(logs_with_hex_tx_hash) 115 | # Test decoded transaction_hash is without `0x` prefix 116 | self.assertEqual(decoded[0]['transaction_hash'], logs_with_hex_tx_hash[0]['transactionHash'].hex()) 117 | self.assertFalse(decoded[0]['transaction_hash'].startswith('0x')) 118 | 119 | logs_with_hex_tx_hash = [{ 120 | **base_logs[0], 121 | 'transactionHash': bytes(HexBytes('0x54041b3ce0976ee17212100f42b3793fa4ee5f869a6d107249a75caa5fc1b8aa')) 122 | }] 123 | decoded = self.decoder.decode_logs(logs_with_hex_tx_hash) 124 | # Test decoded transaction_hash is without `0x` prefix 125 | self.assertEqual(decoded[0]['transaction_hash'], logs_with_hex_tx_hash[0]['transactionHash'].hex()) 126 | self.assertFalse(decoded[0]['transaction_hash'].startswith('0x')) 127 | self.assertIsInstance(decoded[0]['transaction_hash'], str) 128 | 129 | def test_validation_errors(self): 130 | self.decoder.add_abi(self.test_abi) 131 | # Create base not decoded log, which contains camelcase `transactionHash` 132 | base_log = { 133 | 'address': '0xa6d9c5f7d4de3cef51ad3b7235d79ccc95114de5', 134 | 'transactionHash': '0x54041b3ce0976ee17212100f42b3793fa4ee5f869a6d107249a75caa5fc1b8aa', 135 | 'data': u"0x00000000000000000000000065039084cc6f4773291a6ed7dcf5bc3a2e894ff300000000000000000000000017e054b16ca658789c927c854976450adbda7df0", 136 | 'topics': [ 137 | HexBytes('0x4fb057ad4a26ed17a57957fa69c306f11987596069b89521c511fc9a894e6161') 138 | ] 139 | } 140 | 141 | logs_with_invalid_address = [ 142 | { 143 | **base_log, 144 | 'address': '0x' 145 | } 146 | ] 147 | self.assertRaises(ValueError, self.decoder.decode_logs, logs_with_invalid_address) 148 | 149 | logs_with_invalid_address = [ 150 | { 151 | **base_log, 152 | 'address': None 153 | } 154 | ] 155 | self.assertRaises(ValueError, self.decoder.decode_logs, logs_with_invalid_address) 156 | 157 | logs_with_valid_address = [base_log] 158 | self.assertIsNotNone(self.decoder.decode_logs(logs_with_valid_address)) 159 | 160 | logs_with_invalid_tx_hash = [ 161 | { 162 | **base_log, 163 | 'transactionHash': '0x' 164 | } 165 | ] 166 | self.assertRaises(ValueError, self.decoder.decode_logs, logs_with_invalid_tx_hash) 167 | 168 | logs_with_invalid_tx_hash = [ 169 | { 170 | **base_log, 171 | 'transactionHash': '0x0123456789' 172 | } 173 | ] 174 | self.assertRaises(ValueError, self.decoder.decode_logs, logs_with_invalid_tx_hash) 175 | 176 | logs_with_invalid_tx_hash = [ 177 | { 178 | **base_log, 179 | 'transactionHash': None 180 | } 181 | ] 182 | self.assertRaises(ValueError, self.decoder.decode_logs, logs_with_invalid_tx_hash) 183 | 184 | logs_with_valid_tx_hash = [ 185 | { 186 | **base_log, 187 | 'transactionHash': HexBytes('0x54041b3ce0976ee17212100f42b3793fa4ee5f869a6d107249a75caa5fc1b8aa') 188 | } 189 | ] 190 | self.assertIsNotNone(self.decoder.decode_logs(logs_with_valid_tx_hash)) 191 | 192 | logs_with_valid_tx_hash = [ 193 | { 194 | **base_log, 195 | 'transactionHash': HexBytes('54041b3ce0976ee17212100f42b3793fa4ee5f869a6d107249a75caa5fc1b8aa') 196 | } 197 | ] 198 | self.assertIsNotNone(self.decoder.decode_logs(logs_with_valid_tx_hash)) 199 | 200 | logs_with_valid_tx_hash = [ 201 | { 202 | **base_log, 203 | 'transactionHash': HexBytes('0x54041b3ce0976ee17212100f42b3793fa4ee5f869a6d107249a75caa5fc1b8aa').hex() 204 | } 205 | ] 206 | self.assertIsNotNone(self.decoder.decode_logs(logs_with_valid_tx_hash)) 207 | 208 | logs_with_valid_tx_hash = [ 209 | { 210 | **base_log, 211 | 'transactionHash': bytes(HexBytes('0x54041b3ce0976ee17212100f42b3793fa4ee5f869a6d107249a75caa5fc1b8aa')).hex() 212 | } 213 | ] 214 | self.assertIsNotNone(self.decoder.decode_logs(logs_with_valid_tx_hash)) 215 | 216 | logs_with_valid_tx_hash = [ 217 | { 218 | **base_log, 219 | 'transactionHash': bytes(HexBytes('54041b3ce0976ee17212100f42b3793fa4ee5f869a6d107249a75caa5fc1b8aa')).hex() 220 | } 221 | ] 222 | self.assertIsNotNone(self.decoder.decode_logs(logs_with_valid_tx_hash)) -------------------------------------------------------------------------------- /django_eth_events/tests/test_event_listener_exec.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from json import dumps, loads 3 | 4 | from django.test import TestCase 5 | from eth_tester import EthereumTester 6 | from web3.providers.eth_tester import EthereumTesterProvider 7 | 8 | from ..event_listener import EventListener 9 | from ..factories import DaemonFactory 10 | from ..models import Block, Daemon 11 | from ..utils import remove_0x_head 12 | from ..web3_service import Web3Service 13 | from .utils import (CentralizedOracle, centralized_oracle_abi, 14 | centralized_oracle_bytecode) 15 | 16 | 17 | class TestDaemonExec(TestCase): 18 | def setUp(self): 19 | self.provider = EthereumTesterProvider(EthereumTester()) 20 | self.web3 = Web3Service(provider=self.provider).web3 21 | self.web3.eth.defaultAccount = self.web3.eth.coinbase 22 | 23 | # Mock web3 24 | self.daemon = DaemonFactory() 25 | self.tx_data = {'from': self.web3.eth.accounts[0], 26 | 'gas': 1000000} 27 | 28 | # create oracles 29 | centralized_contract_factory = self.web3.eth.contract(abi=centralized_oracle_abi, 30 | bytecode=centralized_oracle_bytecode) 31 | tx_hash = centralized_contract_factory.constructor().transact() 32 | self.centralized_oracle_factory_address = self.web3.eth.getTransactionReceipt(tx_hash).get('contractAddress') 33 | self.centralized_oracle_factory = self.web3.eth.contract(self.centralized_oracle_factory_address, 34 | abi=centralized_oracle_abi) 35 | 36 | self.contracts = [ 37 | { 38 | 'NAME': 'Centralized Oracle Factory', 39 | 'EVENT_ABI': centralized_oracle_abi, 40 | 'EVENT_DATA_RECEIVER': 'django_eth_events.tests.utils.CentralizedOraclesReceiver', 41 | 'ADDRESSES': [self.centralized_oracle_factory_address[2::]] 42 | } 43 | ] 44 | EventListener.instance = None 45 | self.listener_under_test = EventListener(contract_map=self.contracts, 46 | provider=self.provider) 47 | CentralizedOracle().reset() 48 | self.assertEqual(CentralizedOracle().length(), 0) 49 | self.assertEqual(1, self.web3.eth.blockNumber) 50 | 51 | def tearDown(self): 52 | self.provider.ethereum_tester.reset_to_genesis() 53 | self.assertEqual(0, self.web3.eth.blockNumber) 54 | 55 | def test_create_centralized_oracle(self): 56 | self.assertEqual(CentralizedOracle().length(), 0) 57 | self.assertEqual(0, Daemon.get_solo().block_number) 58 | self.assertEqual(0, Block.objects.all().count()) 59 | 60 | # Create centralized oracle 61 | tx_hash = self.centralized_oracle_factory.functions.createCentralizedOracle( 62 | b'QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG').transact(self.tx_data) 63 | self.assertIsNotNone(tx_hash) 64 | self.listener_under_test.execute() 65 | self.assertEqual(CentralizedOracle().length(), 1) 66 | self.assertEqual(2, Daemon.get_solo().block_number) 67 | 68 | # Check backup 69 | self.assertEqual(2, Block.objects.all().count()) 70 | block = Block.objects.get(block_number=2) 71 | self.assertEqual(1, len(loads(block.decoded_logs))) 72 | 73 | def test_reorg_centralized_oracle(self): 74 | # initial transaction, to set reorg init 75 | accounts = self.web3.eth.accounts 76 | self.web3.eth.sendTransaction({'from': accounts[0], 'to': accounts[1], 'value': 5000000}) 77 | self.assertEqual(0, Block.objects.all().count()) 78 | self.assertEqual(CentralizedOracle().length(), 0) 79 | self.assertEqual(2, self.web3.eth.blockNumber) 80 | 81 | # Create centralized oracle 82 | tx_hash = self.centralized_oracle_factory.functions.createCentralizedOracle( 83 | b'QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG').transact(self.tx_data) 84 | self.assertIsNotNone(tx_hash) 85 | self.listener_under_test.execute() 86 | self.assertEqual(CentralizedOracle().length(), 1) 87 | self.assertEqual(3, Daemon.get_solo().block_number) 88 | self.assertEqual(3, Block.objects.all().count()) 89 | self.assertEqual(3, self.web3.eth.blockNumber) 90 | 91 | # Reset blockchain (simulates reorg) 92 | self.tearDown() 93 | 94 | self.web3.eth.sendTransaction({'from': accounts[0], 'to': accounts[1], 'value': 1000000}) 95 | self.web3.eth.sendTransaction({'from': accounts[0], 'to': accounts[1], 'value': 1000000}) 96 | self.web3.eth.sendTransaction({'from': accounts[0], 'to': accounts[1], 'value': 1000000}) 97 | self.assertEqual(3, self.web3.eth.blockNumber) 98 | 99 | # Force block_hash change (cannot recreate a real reorg with python testrpc) 100 | # TODO Check if it can be done with eth-tester 101 | block_hash = remove_0x_head(self.web3.eth.getBlock(1)['hash']) 102 | Block.objects.filter(block_number=1).update(block_hash=block_hash) 103 | 104 | self.listener_under_test.execute() 105 | self.assertEqual(CentralizedOracle().length(), 0) 106 | self.assertEqual(3, Daemon.get_solo().block_number) 107 | self.assertEqual(3, Block.objects.all().count()) 108 | 109 | def test_atomic_transaction(self): 110 | contracts = [ 111 | { 112 | 'NAME': 'Centralized Oracle Factory', 113 | 'EVENT_ABI': centralized_oracle_abi, 114 | 'EVENT_DATA_RECEIVER': 'django_eth_events.tests.utils.ErroredCentralizedOraclesReceiver', 115 | 'ADDRESSES': [self.centralized_oracle_factory_address[2::]] 116 | } 117 | ] 118 | atomic_listener = EventListener(contract_map=contracts, provider=self.provider) 119 | 120 | self.assertEqual(0, Block.objects.all().count()) 121 | self.assertEqual(0, CentralizedOracle().length()) 122 | self.assertEqual(0, Daemon.get_solo().block_number) 123 | # Create centralized oracle 124 | tx_hash = self.centralized_oracle_factory.functions.createCentralizedOracle( 125 | b'QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG').transact(self.tx_data) 126 | 127 | with self.assertRaises(Exception): 128 | # raising an exception would make the atomic transaction to fail 129 | atomic_listener.execute() 130 | 131 | # Test transaction atomic worked and updates where committed just for the first block 132 | self.assertEqual(0, CentralizedOracle().length()) 133 | self.assertEqual(1, Daemon.get_solo().block_number) 134 | self.assertEqual(2, self.web3.eth.blockNumber) 135 | 136 | # Reset daemon 137 | daemon = Daemon.get_solo() 138 | daemon.block_number = 0 139 | daemon.save() 140 | 141 | # Execute the listener correctly, this will save new blocks 142 | self.listener_under_test.execute() 143 | self.assertEqual(1, CentralizedOracle().length()) 144 | self.assertEqual(2, Daemon.get_solo().block_number) 145 | self.assertEqual(2, Block.objects.all().count()) 146 | self.assertEqual(2, self.web3.eth.blockNumber) 147 | 148 | # Reset blockchain (simulates reorg) 149 | self.tearDown() 150 | 151 | block = Block.objects.filter(block_number__gt=1).order_by('-block_number').first() 152 | logs = loads(block.decoded_logs) 153 | logs[0]['event_receiver'] = 'django_eth_events.tests.utils.ErroredCentralizedOraclesReceiver' 154 | block.decoded_logs = dumps(logs) 155 | block.save() 156 | 157 | accounts = self.web3.eth.accounts 158 | self.web3.eth.sendTransaction({'from': accounts[0], 'to': accounts[1], 'value': 1000000}) 159 | self.web3.eth.sendTransaction({'from': accounts[0], 'to': accounts[1], 'value': 1000000}) 160 | self.web3.eth.sendTransaction({'from': accounts[0], 'to': accounts[1], 'value': 1000000}) 161 | tx_hash = self.centralized_oracle_factory.functions.createCentralizedOracle( 162 | b'QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG').transact(self.tx_data) 163 | 164 | self.assertEqual(1, CentralizedOracle().length()) 165 | self.assertEqual(2, Daemon.get_solo().block_number) 166 | self.assertEqual(2, Block.objects.all().count()) 167 | self.assertEqual(4, self.web3.eth.blockNumber) 168 | 169 | block_hash1 = remove_0x_head(self.web3.eth.getBlock(1)['hash']) 170 | Block.objects.filter(block_number=1).update(block_hash=block_hash1) 171 | 172 | with self.assertRaises(Exception): 173 | atomic_listener.execute() 174 | # Test atomic rollback worked 175 | self.assertEqual(1, CentralizedOracle().length()) 176 | self.assertEqual(2, Daemon.get_solo().block_number) 177 | self.assertEqual(2, Block.objects.all().count()) 178 | self.assertEqual(4, self.web3.eth.blockNumber) 179 | -------------------------------------------------------------------------------- /django_eth_events/tests/test_event_listener_functions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.test import TestCase 3 | from eth_tester import EthereumTester 4 | from web3.providers.eth_tester import EthereumTesterProvider 5 | 6 | from ..event_listener import EventListener 7 | from ..exceptions import InvalidAddressException 8 | from ..factories import DaemonFactory 9 | from ..models import Daemon 10 | from ..utils import normalize_address_without_0x 11 | from .utils import abi, bin_hex 12 | 13 | 14 | class TestDaemon(TestCase): 15 | def setUp(self): 16 | self.daemon = DaemonFactory() 17 | self.el = EventListener(provider=EthereumTesterProvider(EthereumTester())) 18 | self.provider = self.el.provider 19 | self.el.web3.eth.defaultAccount = self.el.web3.eth.coinbase 20 | self.el.decoder.reset() 21 | self.maxDiff = None 22 | 23 | def tearDown(self): 24 | self.provider.ethereum_tester.reset_to_genesis() 25 | self.assertEqual(0, self.el.web3.eth.blockNumber) 26 | 27 | def test_next_block(self): 28 | daemon = Daemon.get_solo() 29 | self.assertSequenceEqual(self.el.get_next_mined_block_numbers(daemon.block_number, 30 | self.el.get_current_block_number()), 31 | []) 32 | factory = self.el.web3.eth.contract(abi=abi, bytecode=bin_hex) 33 | tx_hash = factory.constructor().transact() 34 | self.el.web3.eth.getTransactionReceipt(tx_hash) 35 | tx_hash2 = factory.constructor().transact() 36 | self.el.web3.eth.getTransactionReceipt(tx_hash2) 37 | self.assertSequenceEqual(self.el.get_next_mined_block_numbers(daemon.block_number, 38 | self.el.get_current_block_number()), 39 | [1, 2]) 40 | daemon.block_number = 2 41 | daemon.save() 42 | self.assertSequenceEqual(self.el.get_next_mined_block_numbers(daemon.block_number, 43 | self.el.get_current_block_number()), 44 | []) 45 | 46 | def test_load_abis(self): 47 | self.assertIsNotNone(self.el.decoder) 48 | self.assertEqual(len(self.el.decoder.methods), 0) 49 | self.assertEqual(self.el.decoder.add_abi([]), 0) 50 | self.assertEqual(len(self.el.decoder.methods), 0) 51 | # No ABIs 52 | self.assertEqual(self.el.decoder.add_abi(abi), 6) 53 | self.assertEqual(len(self.el.decoder.methods), 6) 54 | self.assertEqual(self.el.decoder.add_abi([{'nothing': 'wrong'}]), 0) 55 | 56 | self.assertEqual(self.el.decoder.add_abi(abi), 0) 57 | self.assertEqual(self.el.decoder.add_abi([{'nothing': 'wrong'}]), 0) 58 | 59 | def test_get_logs(self): 60 | # no logs before transactions 61 | block_info = self.el.web3_service.get_block(0) 62 | logs = self.el.web3_service.get_logs_for_block(block_info) 63 | self.assertListEqual([], logs) 64 | 65 | # create Wallet Factory contract 66 | factory = self.el.web3.eth.contract(abi=abi, bytecode=bin_hex) 67 | self.assertIsNotNone(factory) 68 | tx_hash = factory.constructor().transact() 69 | self.assertIsNotNone(tx_hash) 70 | receipt = self.el.web3.eth.getTransactionReceipt(tx_hash) 71 | self.assertIsNotNone(receipt) 72 | self.assertIsNotNone(receipt.get('contractAddress')) 73 | factory_address = receipt['contractAddress'] 74 | 75 | block_info = self.el.web3_service.get_block(0) 76 | logs = self.el.web3_service.get_logs_for_block(block_info) 77 | self.assertListEqual([], logs) 78 | 79 | # send.constructor().transact() function, will trigger two events 80 | self.el.decoder.add_abi(abi) 81 | factory_instance = self.el.web3.eth.contract(address=factory_address, abi=abi) 82 | owners = self.el.web3.eth.accounts[0:2] 83 | required_confirmations = 1 84 | daily_limit = 0 85 | tx_hash = factory_instance.functions.create(owners, required_confirmations, daily_limit).transact() 86 | receipt = self.el.web3.eth.getTransactionReceipt(tx_hash) 87 | self.assertIsNotNone(receipt) 88 | daemon = Daemon.get_solo() 89 | self.assertSequenceEqual(self.el.get_next_mined_block_numbers(daemon.block_number, 90 | self.el.get_current_block_number()), 91 | [1, 2]) 92 | daemon.block_number = 2 93 | daemon.save() 94 | self.assertSequenceEqual(self.el.get_next_mined_block_numbers(daemon.block_number, 95 | self.el.get_current_block_number()), 96 | []) 97 | 98 | block_info = self.el.web3_service.get_current_block() 99 | logs = self.el.web3_service.get_logs_for_block(block_info) 100 | self.assertEqual(2, len(logs)) 101 | decoded = self.el.decoder.decode_logs(logs) 102 | self.assertEqual(2, len(decoded)) 103 | self.assertDictEqual( 104 | { 105 | 'address': normalize_address_without_0x(factory_address), 106 | 'name': 'OwnersInit', 107 | 'transaction_hash': logs[0]['transactionHash'].hex()[2:], 108 | 'params': [ 109 | { 110 | 'name': 'owners', 111 | 'value': [normalize_address_without_0x(account) 112 | for account 113 | in self.el.web3.eth.accounts[0:2]] 114 | } 115 | ] 116 | }, 117 | decoded[0] 118 | ) 119 | self.assertDictEqual( 120 | { 121 | 'address': normalize_address_without_0x(factory_address), 122 | 'name': 'ContractInstantiation', 123 | 'transaction_hash': logs[1]['transactionHash'].hex()[2:], 124 | 'params': [ 125 | { 126 | 'name': 'sender', 127 | 'value': normalize_address_without_0x(self.el.web3.eth.coinbase) 128 | }, 129 | { 130 | 'name': 'instantiation', 131 | 'value': normalize_address_without_0x(decoded[1]['params'][1]['value']) 132 | } 133 | ] 134 | }, 135 | decoded[1] 136 | ) 137 | 138 | def test_parse_contract(self): 139 | contracts = [ 140 | { 141 | 'NAME': 'My custom event', 142 | 'EVENT_ABI': {}, 143 | 'EVENT_DATA_RECEIVER': 'django_eth_events.tests.utils.CentralizedOraclesReceiver', 144 | 'ADDRESSES': ['0xd3CDa913DeB6F67967B99d67fBdFA1712c293604', 145 | 'A823A913dEb6F67967B99D67fbdFa1712c293604' 146 | '0xd3Cda913DeB6F67967B99d67fBdFA1712c293604'] 147 | } 148 | ] 149 | with self.assertRaises(InvalidAddressException): 150 | EventListener(contract_map=contracts, 151 | provider=EthereumTesterProvider(EthereumTester())) 152 | 153 | contracts = [ 154 | { 155 | 'NAME': 'My custom event', 156 | 'EVENT_ABI': {}, 157 | 'EVENT_DATA_RECEIVER': 'made_up_function', 158 | 'ADDRESSES': ['d3CDa913DeB6F67967B99d67fBdFA1712c293604', 159 | 'A823A913dEb6F67967B99D67fbdFa1712c293604'] 160 | 161 | } 162 | ] 163 | 164 | with self.assertRaises(ImportError): 165 | EventListener(contract_map=contracts, 166 | provider=EthereumTesterProvider(EthereumTester())) 167 | 168 | contracts = [ 169 | { 170 | 'NAME': 'My custom event', 171 | 'EVENT_ABI': {}, 172 | 'EVENT_DATA_RECEIVER': 'django_eth_events.tests.utils.CentralizedOraclesReceiver', 173 | 'ADDRESSES': ['a823a913deb6f67967b99d67fbdfa1712c293604', 174 | 'A823A913dEb6F67967B99D67fbdFa1712c293604', 175 | '0xA823A913dEb6F67967B99D67fbdFa1712c293604', 176 | '0xa823a913deb6f67967b99d67fbdfa1712c293604'] 177 | } 178 | ] 179 | 180 | el = EventListener(contract_map=contracts, 181 | provider=EthereumTesterProvider(EthereumTester())) 182 | 183 | self.assertEqual(len(el.original_contract_map[0]['ADDRESSES']), 4) 184 | self.assertEqual(len(el.contract_map[0]['ADDRESSES']), 1) 185 | -------------------------------------------------------------------------------- /django_eth_events/tests/test_reorg_detector.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from http.server import HTTPServer 3 | from multiprocessing import Process 4 | from time import sleep 5 | 6 | from django.core.cache import cache 7 | from django.test import TestCase 8 | from eth_tester import EthereumTester 9 | from hexbytes import HexBytes 10 | from web3 import HTTPProvider 11 | from web3.providers.eth_tester import EthereumTesterProvider 12 | 13 | from ..chainevents import AbstractEventReceiver 14 | from ..exceptions import NoBackupException, Web3ConnectionException 15 | from ..factories import DaemonFactory 16 | from ..models import Block, Daemon 17 | from ..reorgs import check_reorg 18 | from ..utils import remove_0x_head 19 | from ..web3_service import Web3Service 20 | from .mocked_testrpc_reorg import MockedTestrpc 21 | 22 | 23 | def start_mock_server(): 24 | server_address = ('127.0.0.1', 8545) 25 | httpd = HTTPServer(server_address, MockedTestrpc) 26 | httpd.serve_forever() 27 | print('served internal') 28 | 29 | 30 | class DummyEventReceiver(AbstractEventReceiver): 31 | def __init__(self, *args, **kwars): 32 | super().__init__(args, kwars) 33 | self.stage = 'initial' 34 | 35 | def save(self, decoded_event, block_info): 36 | self.stage = 'processed' 37 | 38 | def rollback(self, decoded_event, block_info): 39 | self.stage = 'rollback' 40 | 41 | 42 | class TestReorgDetector(TestCase): 43 | 44 | def setUp(self): 45 | # Run mocked testrpc for reorgs 46 | print('Starting httpd...') 47 | self.server_process = Process(target=start_mock_server) 48 | self.server_process.start() 49 | cache.set('block_number', '0x0') 50 | sleep(1) 51 | print('served') 52 | self.provider = HTTPProvider('http://localhost:8545') 53 | self.web3_service = Web3Service(self.provider) 54 | # Mock web3 55 | self.daemon = DaemonFactory() 56 | 57 | def tearDown(self): 58 | self.server_process.terminate() 59 | self.server_process = None 60 | cache.clear() 61 | sleep(1) 62 | 63 | def test_mocked_block_number(self): 64 | self.assertEqual(self.web3_service.get_current_block_number(), 0) 65 | cache.set('block_number', '0x9') 66 | self.assertEqual(self.web3_service.get_current_block_number(), 9) 67 | 68 | def test_mocked_block_hash(self): 69 | block_hash_0 = remove_0x_head(HexBytes('0x000000000000000000000000')) 70 | cache.set('0x0', block_hash_0) 71 | self.assertEqual(self.web3_service.get_block(0)['hash'], HexBytes(block_hash_0)) 72 | block_hash_1 = remove_0x_head(HexBytes('0x111111111111111111111111')) 73 | cache.set('0x1', block_hash_1) 74 | self.assertEqual(self.web3_service.get_block(1)['hash'], HexBytes(block_hash_1)) 75 | self.assertNotEqual(block_hash_0, block_hash_1) 76 | 77 | def test_reorg_ok(self): 78 | # Last block hash haven't changed 79 | block_hash_0 = remove_0x_head(HexBytes('0x000000000000000000000000')) 80 | cache.set('0x0', block_hash_0) 81 | cache.set('block_number', '0x1') 82 | Block.objects.create(block_hash=block_hash_0, block_number=0, timestamp=0) 83 | Daemon.objects.all().update(block_number=0) 84 | (had_reorg, _) = check_reorg(Daemon.get_solo().block_number) 85 | self.assertFalse(had_reorg) 86 | 87 | block_hash_1 = remove_0x_head(HexBytes('0x111111111111111111111111')) 88 | cache.set('0x1', block_hash_1) 89 | cache.set('block_number', '0x2') 90 | Block.objects.create(block_hash=block_hash_1, block_number=1, timestamp=0) 91 | Daemon.objects.all().update(block_number=1) 92 | (had_reorg, _) = check_reorg(Daemon.get_solo().block_number) 93 | self.assertFalse(had_reorg) 94 | 95 | def test_reorg_happened(self): 96 | # Last block hash haven't changed 97 | block_hash_0 = remove_0x_head(HexBytes('0x000000000000000000000000')) 98 | cache.set('0x0', block_hash_0) 99 | cache.set('block_number', '0x1') 100 | Block.objects.create(block_hash=block_hash_0, block_number=0, timestamp=0) 101 | Daemon.objects.all().update(block_number=0) 102 | (had_reorg, _) = check_reorg(Daemon.get_solo().block_number) 103 | self.assertFalse(had_reorg) 104 | 105 | # Last block hash changed 106 | block_hash_1 = remove_0x_head(HexBytes('0x111111111111111111111111')) 107 | cache.set('0x1', block_hash_1) 108 | cache.set('block_number', '0x2') 109 | block_hash_reorg = '{:040d}'.format(1313) 110 | Block.objects.create(block_hash=block_hash_reorg, block_number=1, timestamp=0) 111 | Daemon.objects.all().update(block_number=1) 112 | (had_reorg, block_number) = check_reorg(Daemon.get_solo().block_number) 113 | self.assertTrue(had_reorg) 114 | self.assertEqual(block_number, 0) 115 | 116 | Block.objects.filter(block_number=1).update(block_hash=block_hash_1, timestamp=0) 117 | (had_reorg, _) = check_reorg(Daemon.get_solo().block_number) 118 | self.assertFalse(had_reorg) 119 | 120 | def test_reorg_exception(self): 121 | block_hash_0 = remove_0x_head(HexBytes('0x000000000000000000000000')) 122 | cache.set('0x0', block_hash_0) 123 | cache.set('block_number', '0x1') 124 | 125 | # Last block hash changed 126 | block_hash_1 = remove_0x_head(HexBytes('0x111111111111111111111111')) 127 | cache.set('0x1', block_hash_1) 128 | cache.set('block_number', '0x2') 129 | block_hash_reorg = '{:040d}'.format(1313) 130 | Block.objects.create(block_hash=block_hash_reorg, block_number=1, timestamp=0) 131 | Daemon.objects.all().update(block_number=1) 132 | self.assertRaises(NoBackupException, check_reorg, Daemon.get_solo().block_number) 133 | 134 | def test_network_connection_exception(self): 135 | (had_reorg, _) = check_reorg(Daemon.get_solo().block_number) 136 | self.assertFalse(had_reorg) 137 | self.server_process.terminate() 138 | self.assertRaises(Web3ConnectionException, check_reorg, Daemon.get_solo().block_number) 139 | 140 | def test_reorg_mined_multiple_blocks_ok(self): 141 | # Last block hash haven't changed 142 | block_hash_0 = remove_0x_head(HexBytes('0x000000000000000000000000')) 143 | cache.set('0x0', block_hash_0) 144 | cache.set('block_number', '0x1') 145 | Block.objects.create(block_hash=block_hash_0, block_number=0, timestamp=0) 146 | Daemon.objects.all().update(block_number=0) 147 | (had_reorg, _) = check_reorg(Daemon.get_solo().block_number) 148 | self.assertFalse(had_reorg) 149 | 150 | # new block number changed more than one unit 151 | block_hash_1 = remove_0x_head(HexBytes('0x111111111111111111111111')) 152 | cache.set('0x1', block_hash_1) # set_mocked_testrpc_block_hash 153 | cache.set('block_number', '0x9') 154 | Block.objects.create(block_hash=block_hash_1, block_number=1, timestamp=0) 155 | Daemon.objects.all().update(block_number=1) 156 | (had_reorg, _) = check_reorg(Daemon.get_solo().block_number) 157 | self.assertFalse(had_reorg) 158 | 159 | def test_mined_multiple_blocks_with_reorg(self): 160 | # Last block hash haven't changed 161 | block_hash_0 = remove_0x_head(HexBytes('0x000000000000000000000000')) 162 | cache.set('0x0', block_hash_0) 163 | cache.set('block_number', '0x1') 164 | Block.objects.create(block_hash=block_hash_0, block_number=0, timestamp=0) 165 | Daemon.objects.all().update(block_number=0) 166 | (had_reorg, _) = check_reorg(Daemon.get_solo().block_number) 167 | self.assertFalse(had_reorg) 168 | 169 | # Last block hash changed 170 | block_hash_1 = remove_0x_head(HexBytes('0x111111111111111111111111')) 171 | cache.set('0x1', block_hash_1) 172 | cache.set('block_number', '0x9') 173 | block_hash_reorg = '{:040d}'.format(1313) 174 | Block.objects.create(block_hash=block_hash_reorg, block_number=1, timestamp=0) 175 | Daemon.objects.all().update(block_number=1) 176 | (had_reorg, block_number) = check_reorg(Daemon.get_solo().block_number) 177 | self.assertTrue(had_reorg) 178 | self.assertEqual(block_number, 0) 179 | 180 | block_hash_2 = remove_0x_head(HexBytes('0x222222222222222222222222')) 181 | cache.set('0x2', block_hash_reorg) 182 | cache.set('block_number', '0x9') 183 | Block.objects.create(block_hash=block_hash_2, block_number=2, timestamp=0) 184 | Daemon.objects.all().update(block_number=2) 185 | (had_reorg, block_number) = check_reorg(Daemon.get_solo().block_number) 186 | self.assertTrue(had_reorg) 187 | self.assertEqual(block_number, 0) 188 | 189 | Block.objects.filter(block_number=1).update(block_hash=block_hash_1) 190 | 191 | (had_reorg, block_number) = check_reorg(Daemon.get_solo().block_number) 192 | self.assertTrue(had_reorg) 193 | self.assertEqual(block_number, 1) 194 | 195 | Block.objects.filter(block_number=2).update(block_hash=block_hash_2) 196 | cache.set('0x2', block_hash_2) 197 | (had_reorg, block_number) = check_reorg(Daemon.get_solo().block_number) 198 | self.assertFalse(had_reorg) 199 | 200 | def test_reorg_block_number_decreased(self): 201 | # block number of the node is lower than the one saved, we trust the node, we rollback to the common block 202 | # Last block hash haven't changed 203 | block_hash_0 = remove_0x_head(HexBytes('0x000000000000000000000000')) 204 | cache.set('0x0', block_hash_0) 205 | cache.set('block_number', '0x1') 206 | Block.objects.create(block_hash='wrong block', block_number=0, timestamp=0) 207 | Daemon.objects.all().update(block_number=3) 208 | # No common block 209 | self.assertRaises(NoBackupException, check_reorg, Daemon.get_solo().block_number) 210 | 211 | Block.objects.filter(block_number=0).update(block_hash=block_hash_0) 212 | 213 | block_hash_1 = remove_0x_head(HexBytes('0x111111111111111111111111')) 214 | cache.set('0x1', block_hash_1) 215 | cache.set('block_number', '0x2') 216 | Block.objects.create(block_hash=block_hash_1, block_number=1, timestamp=0) 217 | (had_reorg, block_number) = check_reorg(Daemon.get_solo().block_number) 218 | self.assertTrue(had_reorg) 219 | self.assertEqual(block_number, 1) 220 | 221 | block_hash_reorg = remove_0x_head(HexBytes('0x131313131313131313131313')) 222 | cache.set('0x1', block_hash_reorg) 223 | 224 | (had_reorg, block_number) = check_reorg(Daemon.get_solo().block_number) 225 | self.assertTrue(had_reorg) 226 | self.assertEqual(block_number, 0) 227 | 228 | def test_reorg_web3_provider(self): 229 | # Stop running server 230 | self.server_process.terminate() 231 | ethereum_tester = EthereumTester() 232 | ethereum_tester_provider = EthereumTesterProvider(ethereum_tester) 233 | # Run check_reorg, should not raise exceptions 234 | (had_reorg, block_number) = check_reorg(Daemon.get_solo().block_number, provider=ethereum_tester_provider) 235 | self.assertFalse(had_reorg) 236 | # Reset genesis block to simulate reorg 237 | ethereum_tester.reset_to_genesis() 238 | 239 | # Restart rpc server 240 | self.server_process = None 241 | self.server_process = Process(target=start_mock_server) 242 | self.server_process.start() 243 | sleep(1) 244 | 245 | # Simulate reorg 246 | block_hash_0 = remove_0x_head(HexBytes('0x000000000000000000000000')) 247 | block_hash_1 = remove_0x_head(HexBytes('0x111111111111111111111111')) 248 | 249 | Block.objects.create(block_hash=block_hash_0, block_number=0, timestamp=0) 250 | Daemon.objects.all().update(block_number=0) 251 | 252 | cache.set('0x0', block_hash_0) 253 | cache.set('0x1', block_hash_1) 254 | cache.set('block_number', '0x2') 255 | block_hash_reorg = remove_0x_head(HexBytes('0x131313131313131313131313')) 256 | Block.objects.create(block_hash=block_hash_reorg, block_number=1, timestamp=0) 257 | Daemon.objects.all().update(block_number=1) 258 | 259 | (had_reorg, _) = check_reorg(Daemon.get_solo().block_number) 260 | self.assertTrue(had_reorg) 261 | -------------------------------------------------------------------------------- /django_eth_events/tests/test_singleton.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.test import TestCase 3 | from eth_tester import EthereumTester 4 | from web3 import HTTPProvider, IPCProvider 5 | from web3.providers.eth_tester import EthereumTesterProvider 6 | 7 | from ..event_listener import EventListener 8 | from ..web3_service import Web3Service, Web3ServiceProvider 9 | 10 | 11 | class TestSingleton(TestCase): 12 | 13 | def test_single_instance(self): 14 | service1 = Web3ServiceProvider() 15 | service2 = Web3ServiceProvider() 16 | self.assertEqual(service1.web3, service2.web3) 17 | 18 | def test_arg_ipc_provider(self): 19 | ipc_provider = IPCProvider( 20 | ipc_path=None, 21 | testnet=True 22 | ) 23 | 24 | service1 = Web3ServiceProvider() 25 | self.assertIsInstance(service1.web3.providers[0], HTTPProvider) 26 | service2 = Web3Service(ipc_provider) 27 | self.assertIsInstance(service2.web3.providers[0], IPCProvider) 28 | self.assertEqual(service2.web3.providers[0], ipc_provider) 29 | 30 | def test_eth_tester_provider(self): 31 | eth_tester_provider = EthereumTesterProvider(EthereumTester()) 32 | 33 | service1 = Web3ServiceProvider() 34 | self.assertIsInstance(service1.web3.providers[0], HTTPProvider) 35 | service2 = Web3Service(eth_tester_provider) 36 | self.assertIsInstance(service2.web3.providers[0], EthereumTesterProvider) 37 | self.assertEqual(service2.web3.providers[0], eth_tester_provider) 38 | 39 | def test_event_listener_singleton(self): 40 | ipc_provider = IPCProvider( 41 | ipc_path=None, 42 | testnet=True 43 | ) 44 | 45 | listener1 = EventListener() 46 | listener2 = EventListener() 47 | self.assertEqual(listener1, listener2) 48 | listener3 = EventListener(provider=ipc_provider) 49 | self.assertNotEqual(listener2, listener3) 50 | 51 | # For a different contract we need a different instance of the singleton even if provider is the same 52 | contract_map = [ 53 | {'NAME': 'Tester Oracle Factory', 'EVENT_ABI': [], 54 | 'EVENT_DATA_RECEIVER': 'django_eth_events.tests.test_celery.DummyEventReceiver', 55 | 'ADDRESSES': ['c305c901078781C232A2a521C2aF7980f8385ee9'] 56 | } 57 | ] 58 | listener4 = EventListener(provider=ipc_provider, contract_map=contract_map) 59 | self.assertNotEqual(listener3, listener4) 60 | listener5 = EventListener(provider=ipc_provider, contract_map=contract_map) 61 | self.assertEqual(listener4, listener5) 62 | -------------------------------------------------------------------------------- /django_eth_events/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import errno 2 | from django.test import TestCase 3 | from hexbytes import HexBytes 4 | from urllib3.exceptions import HTTPError, LocationValueError, PoolError 5 | from json import dumps, loads 6 | 7 | from ..utils import normalize_address_without_0x, remove_0x_head, is_network_error, JsonBytesEncoder 8 | from ..exceptions import Web3ConnectionException 9 | 10 | 11 | class TestUtils(TestCase): 12 | 13 | def test_remove_0x_head(self): 14 | self.assertEqual('b58d5491D17ebF46E9DB7F18CeA7C556AE80d53B', 15 | remove_0x_head('0xb58d5491D17ebF46E9DB7F18CeA7C556AE80d53B')) 16 | 17 | self.assertEqual('b58d5491d17ebf46e9db7f18cea7c556ae80d53b', 18 | remove_0x_head('0xb58d5491d17ebf46e9db7f18cea7c556ae80d53b')) 19 | 20 | self.assertEqual('b58d5491d17ebf46e9db7f18cea7c556ae80d53B', 21 | remove_0x_head('0xb58d5491d17ebf46e9db7f18cea7c556ae80d53B')) 22 | 23 | self.assertEqual('b58d5491d17ebf46e9db7f18cea7c556ae80d53b', 24 | remove_0x_head(HexBytes('0xb58d5491D17ebF46E9DB7F18CeA7C556AE80d53B'))) 25 | 26 | self.assertEqual('b58d5491d17ebf46e9db7f18cea7c556ae80d53b', 27 | remove_0x_head(HexBytes('0xb58d5491d17ebf46e9db7f18cea7c556ae80d53B'))) 28 | 29 | self.assertEqual('b58d5491d17ebf46e9db7f18cea7c556ae80d53b', 30 | remove_0x_head(HexBytes('0xb58d5491d17ebf46e9db7f18cea7c556ae80d53B'))) 31 | 32 | self.assertEqual('b58d5491d17ebf46e9db7f18cea7c556ae80d53b', 33 | remove_0x_head(HexBytes('b58d5491d17ebf46e9db7f18cea7c556ae80d53B'))) 34 | 35 | def test_normalize_address_without_0x(self): 36 | self.assertEqual('b58d5491d17ebf46e9db7f18cea7c556ae80d53b', 37 | normalize_address_without_0x('0xb58d5491D17ebF46E9DB7F18CeA7C556AE80d53B')) 38 | 39 | self.assertEqual('b58d5491d17ebf46e9db7f18cea7c556ae80d53b', 40 | normalize_address_without_0x('0xb58d5491d17ebf46e9db7f18cea7c556ae80d53b')) 41 | 42 | self.assertEqual('b58d5491d17ebf46e9db7f18cea7c556ae80d53b', 43 | normalize_address_without_0x(HexBytes('0xb58d5491d17ebf46e9db7f18cea7c556ae80d53b'))) 44 | 45 | self.assertEqual('b58d5491d17ebf46e9db7f18cea7c556ae80d53b', 46 | normalize_address_without_0x(HexBytes('b58d5491d17ebf46e9db7f18cea7c556ae80d53b'))) 47 | 48 | def test_network_error_exception_detector(self): 49 | http_error = HTTPError() 50 | self.assertTrue(is_network_error(http_error)) 51 | 52 | location_value_error = LocationValueError() 53 | self.assertTrue(is_network_error(location_value_error)) 54 | 55 | pool_error = PoolError(None, 'an error') 56 | self.assertTrue(is_network_error(pool_error)) 57 | 58 | exception = Exception() 59 | self.assertFalse(is_network_error(exception)) 60 | 61 | w3_conn_error = Web3ConnectionException() 62 | self.assertFalse(is_network_error(w3_conn_error)) 63 | 64 | setattr(w3_conn_error, 'errno', errno.ECONNABORTED) 65 | self.assertTrue(is_network_error(w3_conn_error)) 66 | 67 | setattr(w3_conn_error, 'errno', errno.EPERM) 68 | self.assertFalse(is_network_error(w3_conn_error)) 69 | 70 | def test_json_encoder(self): 71 | base_address = 'b58d5491d17ebf46e9db7f18cea7c556ae80d53B' 72 | ipfs_hash_string = 'Qme4GBhwNJharbu83iNEsd5WnUhQYM1rBAgCgsSuFMdjcS' 73 | ipfs_hash_bytes = ipfs_hash_string.encode() # b'...' 74 | 75 | json = {'ipfs_hash': ipfs_hash_bytes} 76 | # Simulate string encoding and convert back to dict 77 | encoded_json = loads(dumps(json, cls=JsonBytesEncoder)) 78 | self.assertEqual(ipfs_hash_bytes.decode(), encoded_json['ipfs_hash']) 79 | 80 | json = {'ipfs_hash': ipfs_hash_string} 81 | # Simulate string encoding and convert back to dict 82 | encoded_json = loads(dumps(json, cls=JsonBytesEncoder)) 83 | self.assertEqual(ipfs_hash_string, encoded_json['ipfs_hash']) 84 | 85 | hex_bytes_address = HexBytes(base_address) 86 | json = {'address': hex_bytes_address} 87 | encoded_json = loads(dumps(json, cls=JsonBytesEncoder)) 88 | self.assertEqual(hex_bytes_address.hex(), encoded_json['address']) 89 | 90 | bytes_address = bytes.fromhex(base_address) 91 | json = {'address': bytes_address} 92 | encoded_json = loads(dumps(json, cls=JsonBytesEncoder)) 93 | self.assertEqual('0x' + bytes_address.hex(), encoded_json['address']) 94 | 95 | 96 | -------------------------------------------------------------------------------- /django_eth_events/tests/test_web3_service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from pathlib import Path 3 | 4 | from django.test import TestCase 5 | from eth_tester import EthereumTester 6 | from web3 import HTTPProvider, IPCProvider 7 | from web3.providers.eth_tester import EthereumTesterProvider 8 | 9 | from ..exceptions import UnknownBlock 10 | from ..web3_service import Web3Service, Web3ServiceProvider 11 | 12 | 13 | class TestSingleton(TestCase): 14 | 15 | def setUp(self): 16 | self.web3_service = Web3Service(provider=EthereumTesterProvider(EthereumTester())) 17 | self.web3 = self.web3_service.web3 18 | self.web3.eth.defaultAccount = self.web3.eth.coinbase 19 | self.provider = self.web3.providers[0] 20 | self.tx_data = {'from': self.web3.eth.coinbase, 21 | 'gas': 1000000} 22 | self.event_receivers = [] 23 | 24 | def tearDown(self): 25 | # Delete centralized oracles 26 | self.provider.ethereum_tester.reset_to_genesis() 27 | self.assertEqual(0, self.web3.eth.blockNumber) 28 | 29 | # Delete provider 30 | try: 31 | del Web3ServiceProvider.instance 32 | except AttributeError: 33 | pass 34 | 35 | def test_unknown_block(self): 36 | current_block_number = self.web3_service.get_current_block_number() 37 | self.assertRaises(UnknownBlock, self.web3_service.get_block, current_block_number + 10) 38 | 39 | def test_unknown_blocks(self): 40 | current_block_number = self.web3_service.get_current_block_number() 41 | self.assertRaises(UnknownBlock, self.web3_service.get_block, range(current_block_number + 10)) 42 | 43 | def test_provider_http(self): 44 | with self.settings(ETHEREUM_NODE_URL='http://localhost:8545'): 45 | web3_service = Web3ServiceProvider() 46 | provider = web3_service.web3.providers[0] 47 | self.assertTrue(isinstance(provider, HTTPProvider)) 48 | 49 | with self.settings(ETHEREUM_NODE_URL='https://localhost:8545'): 50 | web3_service = Web3ServiceProvider() 51 | provider = web3_service.web3.providers[0] 52 | self.assertTrue(isinstance(provider, HTTPProvider)) 53 | 54 | def test_provider_ipc(self): 55 | socket_path = str(Path('/tmp/socket.ipc').expanduser().resolve()) 56 | with self.settings(ETHEREUM_NODE_URL='ipc://' + socket_path): 57 | web3_service = Web3ServiceProvider() 58 | provider = web3_service.web3.providers[0] 59 | self.assertTrue(isinstance(provider, IPCProvider)) 60 | self.assertEqual(provider.ipc_path, socket_path) 61 | -------------------------------------------------------------------------------- /django_eth_events/tests/utils.py: -------------------------------------------------------------------------------- 1 | from json import loads 2 | 3 | from ..chainevents import AbstractEventReceiver 4 | 5 | abi = loads( 6 | '[{"inputs": [{"type": "address", "name": ""}], "constant": true, "name": "isInstantiation", "payable": false, ' 7 | '"outputs": [{"type": "bool", "name": ""}], "type": "function"}, {"inputs": [{"type": "address[]", ' 8 | '"name": "_owners"}, {"type": "uint256", "name": "_required"}, {"type": "uint256", "name": "_dailyLimit"}], ' 9 | '"constant": false, "name": "create", "payable": false, "outputs": [{"type": "address", "name": "wallet"}], ' 10 | '"type": "function"}, {"inputs": [{"type": "address", "name": ""}, {"type": "uint256", "name": ""}], "constant": ' 11 | 'true, "name": "instantiations", "payable": false, "outputs": [{"type": "address", "name": ""}], ' 12 | '"type": "function"}, {"inputs": [{"type": "address", "name": "creator"}], "constant": true, ' 13 | '"name": "getInstantiationCount", "payable": false, "outputs": [{"type": "uint256", "name": ""}], ' 14 | '"type": "function"}, {"inputs": [{"indexed": false, "type": "address[]", "name": "owners"}], "type": "event", ' 15 | '"name": "OwnersInit", "anonymous": false}, {"inputs": [{"indexed": false, "type": "address", "name": "sender"}, ' 16 | '{"indexed": false, "type": "address", "name": "instantiation"}], "type": "event", ' 17 | '"name": "ContractInstantiation", "anonymous": false}]') 18 | 19 | 20 | bin_hex = "6060604052611963806100126000396000f3606060405260e060020a60003504632f4f3316811461003f57806353d9d91014" \ 21 | "61005f57806357183c82146101fb5780638f8384781461023e575b610002565b346100025761027060043560006020819052" \ 22 | "908152604090205460ff1681565b346100025760408051602060048035808201358381028086018501909652808552610284" \ 23 | "95929460249490939285019282918501908490808284375094965050933593505060443591505060007fe1d216d1830e177b" \ 24 | "6fd03a19f026ec2c78fc953d60bae896ab63aaa1230ff9008460405180806020018281038252838181518152602001915080" \ 25 | "519060200190602002808383829060006004602084601f0104600302600f01f1509050019250505060405180910390a18383" \ 26 | "8360405161163080610333833901808060200184815260200183815260200182810382528581815181526020019150805190" \ 27 | "60200190602002808383829060006004602084601f0104600302600f01f150905001945050505050604051809103906000f0" \ 28 | "80156100025790506102a081600160a060020a03808216600090815260208181526040808320805460ff1916600190811790" \ 29 | "9155339094168352908390529020805491820180825590919082818380158290116102a7576000838152602090206102a791" \ 30 | "81019083015b8082111561032f57600081556001016101e7565b346100025761028460043560243560016020526000828152" \ 31 | "604090208054829081101561000257600091825260209091200154600160a060020a03169150829050565b34610002576001" \ 32 | "60a060020a036004351660009081526001602052604090205460408051918252519081900360200190f35b60408051911515" \ 33 | "8252519081900360200190f35b60408051600160a060020a039092168252519081900360200190f35b9392505050565b5050" \ 34 | "5060009283525060209182902001805473ffffffffffffffffffffffffffffffffffffffff19166c01000000000000000000" \ 35 | "000000848102041790556040805133600160a060020a03908116825284169281019290925280517f4fb057ad4a26ed17a579" \ 36 | "57fa69c306f11987596069b89521c511fc9a894e61619281900390910190a150565b50905660606040526040516116303803" \ 37 | "8061163083398101604052805160805160a05191909201919082826000825182603282118061003a57508181115b80610043" \ 38 | "575080155b8061004c575081155b1561005657610002565b600092505b84518310156100ce57600260005060008685815181" \ 39 | "1015610002576020908102909101810151600160a060020a031682528101919091526040016000205460ff16806100c45750" \ 40 | "848381518110156100025790602001906020020151600160a060020a03166000145b1561015357610002565b845160038054" \ 41 | "828255600082905290917fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b91820191602089" \ 42 | "0182156101cd579160200282015b828111156101cd5782518254600160a060020a0319166c01000000000000000000000000" \ 43 | "91820291909104178255602090920191600190910190610114565b6001600260005060008786815181101561000257906020" \ 44 | "01906020020151600160a060020a0316815260200190815260200160002060006101000a81548160ff02191690837f010000" \ 45 | "000000000000000000000000000000000000000000000000000000000090810204021790555060019092019161005b565b50" \ 46 | "6101f39291505b80821115610213578054600160a060020a03191681556001016101d5565b50505060049290925550505060" \ 47 | "0655506114199050806102176000396000f35b509056606060405236156101325760e060020a6000350463025e7c27811461" \ 48 | "0180578063173825d9146101b257806320ea8d86146101df5780632f54bf6e146102135780633411c81c146102335780634b" \ 49 | "c9fdc214610260578063547415251461028357806367eeba0c146102f75780636b0c932d146103055780637065cb48146103" \ 50 | "13578063784547a71461033e5780638b51d13f1461034e5780639ace38c2146103c2578063a0e67e2b146103fd578063a8ab" \ 51 | "e69a1461046e578063b5dc40c31461054d578063b77bf60014610659578063ba51a6df14610667578063c01a8c8414610693" \ 52 | "578063c6427474146106a3578063cea0862114610714578063d74f8edd1461073f578063dc8452cd1461074c578063e20056" \ 53 | "e61461075a578063ee22610b1461078a578063f059cf2b1461079a575b6107a8600034111561017e57604080513481529051" \ 54 | "600160a060020a033316917fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c919081900360" \ 55 | "200190a25b565b34610002576107aa60043560038054829081101561000257600091825260209091200154600160a060020a" \ 56 | "0316905081565b34610002576107a8600435600030600160a060020a031633600160a060020a0316141515610a1a57610002" \ 57 | "565b34610002576107a8600435600160a060020a033390811660009081526002602052604090205460ff161515610c5f5761" \ 58 | "0002565b34610002576107c660043560026020526000908152604090205460ff1681565b3461000257600160209081526004" \ 59 | "3560009081526040808220909252602435815220546107c69060ff1681565b34610002576107da6007546000906201518001" \ 60 | "421115610d0f5750600654610d18565b34610002576107da6004356024356000805b600554811015610d1b578380156102be" \ 61 | "575060008181526020819052604090206003015460ff16155b806102e257508280156102e257506000818152602081905260" \ 62 | "4090206003015460ff165b156102ef57600191909101905b600101610295565b34610002576107da60065481565b34610002" \ 63 | "576107da60075481565b34610002576107a860043530600160a060020a031633600160a060020a0316141515610d22576100" \ 64 | "02565b34610002576107c6600435610801565b34610002576107da6004356000805b600354811015610e5257600083815260" \ 65 | "0160205260408120600380549192918490811015610002576000918252602080832090910154600160a060020a0316835282" \ 66 | "019290925260400190205460ff16156103ba57600191909101905b60010161035d565b346100025760006020819052600435" \ 67 | "81526040902080546001820154600383015461087993600160a060020a03909316926002019060ff1684565b346100025760" \ 68 | "4080516020808201835260008252600380548451818402810184019095528085526109239492830182828015610462576020" \ 69 | "02820191906000526020600020905b8154600160a060020a03168152600190910190602001808311610444575b5050505050" \ 70 | "9050610d18565b34610002576109236004356024356044356064356040805160208181018352600080835283519182018452" \ 71 | "808252600554935192939192909182918059106104b35750595b9080825280602002602001820160405280156104ca575b50" \ 72 | "9250600091508190505b600554811015610e58578580156104fe575060008181526020819052604090206003015460ff1615" \ 73 | "5b806105225750848015610522575060008181526020819052604090206003015460ff165b15610545578083838151811015" \ 74 | "6100025760209081029091010152600191909101905b6001016104d5565b3461000257610923600435604080516020818101" \ 75 | "8352600080835283519182018452808252600354935192939192909182918059106105895750595b90808252806020026020" \ 76 | "01820160405280156105a0575b509250600091508190505b600354811015610ecd5760008581526001602052604081206003" \ 77 | "80549192918490811015610002576000918252602080832090910154600160a060020a031683528201929092526040019020" \ 78 | "5460ff161561065157600380548290811015610002576000918252602090912001548351600160a060020a03909116908490" \ 79 | "849081101561000257600160a060020a03909216602092830290910190910152600191909101905b6001016105ab565b3461" \ 80 | "0002576107da60055481565b34610002576107a86004355b30600160a060020a031633600160a060020a0316141515610f49" \ 81 | "57610002565b34610002576107a8600435610974565b3461000257604080516020600460443581810135601f810184900484" \ 82 | "02850184019095528484526107da948235946024803595606494929391909201918190840183828082843750949650505050" \ 83 | "505050600061096d848484600083600160a060020a0381161515610b6157610002565b34610002576107a860043530600160" \ 84 | "a060020a031633600160a060020a031614151561101457610002565b34610002576107da603281565b34610002576107da60" \ 85 | "045481565b34610002576107a8600435602435600030600160a060020a031633600160a060020a031614151561104f576100" \ 86 | "02565b34610002576107a86004356109f7565b34610002576107da60085481565b005b60408051600160a060020a03909216" \ 87 | "8252519081900360200190f35b604080519115158252519081900360200190f35b60408051918252519081900360200190f3" \ 88 | "5b600084815260208190526040902092506111c2845b600080805b6003548110156108725760008481526001602052604081" \ 89 | "20600380549192918490811015610002576000918252602080832090910154600160a060020a031683528201929092526040" \ 90 | "0190205460ff161561086357600191909101905b600454821415610e4a57600192505b5050919050565b60408051600160a0" \ 91 | "60020a0386168152602081018590528215156060820152608091810182815284546002600019610100600184161502019091" \ 92 | "1604928201839052909160a0830190859080156109115780601f106108e65761010080835404028352916020019161091156" \ 93 | "5b820191906000526020600020905b8154815290600101906020018083116108f457829003601f168201915b505095505050" \ 94 | "50505060405180910390f35b6040518080602001828103825283818151815260200191508051906020019060200280838382" \ 95 | "9060006004602084601f0104600302600f01f1509050019250505060405180910390f35b905061100d815b33600160a06002" \ 96 | "0a03811660009081526002602052604090205460ff161515610fb457610002565b6000858152600160208181526040808420" \ 97 | "600160a060020a0333168086529252808420805460ff1916909317909255905187927f4a504a94899432a9846e1aa406dceb" \ 98 | "1bcfd538bb839071d49d1e5e23f5be30ef91a3610ce6855b6000818152602081905260408120600301548190839060ff1615" \ 99 | "6107ec57610002565b600160a060020a038216600090815260026020526040902054829060ff161515610a4357610002565b" \ 100 | "600160a060020a0383166000908152600260205260408120805460ff1916905591505b60035460001901821015610b085782" \ 101 | "600160a060020a0316600360005083815481101561000257600091825260209091200154600160a060020a03161415610b38" \ 102 | "57600380546000198101908110156100025760009182526020909120015460038054600160a060020a039092169184908110" \ 103 | "156100025760009182526020909120018054600160a060020a031916606060020a928302929092049190911790555b600380" \ 104 | "546000198101808355919082908015829011610b4357600083815260209020610b43918101908301610c0e565b6001909101" \ 105 | "90610a66565b505060035460045411159150610c26905057600354610c2690610673565b6005546040805160808101825287" \ 106 | "8152602080820188815282840188815260006060850181905286815280845294852084518154606060020a91820291909104" \ 107 | "600160a060020a031990911617815591516001808401919091559051805160028085018054818a5298879020999b50969894" \ 108 | "97601f9481161561010002600019011604830185900484019490939291019083901061137e57805160ff1916838001178555" \ 109 | "5b506113ae9291505b80821115610c225760008155600101610c0e565b5090565b604051600160a060020a038416907f8001" \ 110 | "553a916ef2f495d26a907cc54d96ed840d7bda71e73194bf5a9df7a76b9090600090a2505050565b60008281526001602090" \ 111 | "8152604080832033600160a060020a038116855292529091205483919060ff161515610cee57610002565b60008581526001" \ 112 | "60209081526040808320600160a060020a0333168085529252808320805460ff191690555187927ff6a317157440607f3626" \ 113 | "9043eb55f1287a5a19ba2216afeab88cd46cbcfb88e991a35b505b50505050565b6000848152602081905260409020600301" \ 114 | "54849060ff1615610c9457610002565b50600854600654035b90565b5092915050565b600160a060020a0381166000908152" \ 115 | "60026020526040902054819060ff1615610d4a57610002565b81600160a060020a0381161515610d6057610002565b600354" \ 116 | "6004546001909101906032821180610d7a57508181115b80610d83575080155b80610d8c575081155b15610d965761000256" \ 117 | "5b600160a060020a0385166000908152600260205260409020805460ff191660019081179091556003805491820180825590" \ 118 | "91908281838015829011610dec57600083815260209020610dec918101908301610c0e565b50505060009283525060208220" \ 119 | "018054600160a060020a031916606060020a88810204179055604051600160a060020a038716917ff39e6e1eb0edcf53c221" \ 120 | "607b54b00cd28f3196fed0a24994dc308b8f611b682d91a25050505050565b600101610806565b50919050565b8787036040" \ 121 | "51805910610e685750595b908082528060200260200182016040528015610e7f575b5093508790505b86811015610ec25782" \ 122 | "81815181101561000257906020019060200201518489830381518110156100025760209081029091010152600101610e8656" \ 123 | "5b505050949350505050565b81604051805910610edb5750595b908082528060200260200182016040528015610ef2575b50" \ 124 | "9350600090505b81811015610f41578281815181101561000257906020019060200201518482815181101561000257600160" \ 125 | "a060020a03909216602092830290910190910152600101610efa565b505050919050565b600354816032821180610f5b5750" \ 126 | "8181115b80610f64575080155b80610f6d575081155b15610f7757610002565b60048390556040805184815290517fa3f1ee" \ 127 | "9126a074d9326c682f561767f710e927faa811f7a99829d49dc421797a9181900360200190a1505050565b60008281526020" \ 128 | "81905260409020548290600160a060020a03161515610fd957610002565b6000838152600160209081526040808320336001" \ 129 | "60a060020a038116855292529091205484919060ff161561099c57610002565b9392505050565b6006819055604080518281" \ 130 | "5290517fc71bdc6afaf9b1aa90a7078191d4fc1adf3bf680fca3183697df6b0dc226bca29181900360200190a150565b6001" \ 131 | "60a060020a038316600090815260026020526040902054839060ff16151561107857610002565b600160a060020a03831660" \ 132 | "0090815260026020526040902054839060ff16156110a057610002565b600092505b60035483101561111d5784600160a060" \ 133 | "020a0316600360005084815481101561000257600091825260209091200154600160a060020a031614156111b75783600360" \ 134 | "00508481548110156100025760009182526020909120018054600160a060020a031916606060020a92830292909204919091" \ 135 | "1790555b600160a060020a03808616600081815260026020526040808220805460ff19908116909155938816825280822080" \ 136 | "54909416600117909355915190917f8001553a916ef2f495d26a907cc54d96ed840d7bda71e73194bf5a9df7a76b9091a260" \ 137 | "4051600160a060020a038516907ff39e6e1eb0edcf53c221607b54b00cd28f3196fed0a24994dc308b8f611b682d90600090" \ 138 | "a25050505050565b6001909201916110a5565b91508180611231575060028084015460001961010060018316150201160415" \ 139 | "80156112315750600183015461123190600754600090620151800142111561120d574260075560006008555b600654600854" \ 140 | "830111806112245750600854828101105b1561141057506000611414565b15610ce85760038301805460ff19166001179055" \ 141 | "81151561125b5760018301546008805490910190555b825460018085015460405160028088018054600160a060020a039096" \ 142 | "169593949093839285926000199083161561010002019091160480156112de5780601f106112b35761010080835404028352" \ 143 | "91602001916112de565b820191906000526020600020905b8154815290600101906020018083116112c157829003601f1682" \ 144 | "01915b505091505060006040518083038185876185025a03f1925050501561132d5760405184907f33e13ecb54c3076d8e8b" \ 145 | "b8c2881800a4d972b792045ffae98fdf46df365fed7590600090a2610ce8565b60405184907f526441bb6c1aba3c9a4a6ca1" \ 146 | "d6545da9c2333c8c48343ef398eb858d72b7923690600090a260038301805460ff19169055811515610ce857505060010154" \ 147 | "6008805491909103905550565b82800160010185558215610c06579182015b82811115610c06578251826000505591602001" \ 148 | "919060010190611390565b5050606091909101516003909101805460ff191660f860020a9283029290920491909117905560" \ 149 | "058054600101905560405182907fc0ba8fe4b176c1714197d43b9cc6bcf797a4a7461c5fe8d0ef6e184ae7601e5190600090" \ 150 | "a2509392505050565b5060015b91905056" 151 | 152 | 153 | centralized_oracle_abi = loads('[{"inputs": [{"type": "bytes", "name": "ipfsHash"}], "constant": false, "name": "createCentralizedOracle", "payable": false, "outputs": [{"type": "address", "name": "centralizedOracle"}], "type": "function"}, {"inputs": [{"indexed": true, "type": "address", "name": "creator"}, {"indexed": false, "type": "address", "name": "centralizedOracle"}, {"indexed": false, "type": "bytes", "name": "ipfsHash"}], "type": "event", "name": "CentralizedOracleCreation", "anonymous": false}]') 154 | 155 | 156 | centralized_oracle_bytecode = "6060604052341561000c57fe5b5b6109ad8061001c6000396000f30060606040526000357c01000000000000000000000000000000" \ 157 | "00000000000000000000000000900463ffffffff1680634e2f220c1461003b575bfe5b341561004357fe5b61009360048080359060" \ 158 | "2001908201803590602001908080601f01602080910402602001604051908101604052809392919081815260200183838082843782" \ 159 | "0191505050505050919050506100d5565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffff" \ 160 | "ffffffffffffffffffffffff16815260200191505060405180910390f35b600033826100e16102a1565b808373ffffffffffffffff" \ 161 | "ffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200180602001828103825283818151" \ 162 | "81526020019150805190602001908083836000831461015f575b80518252602083111561015f576020820191506020810190506020" \ 163 | "8303925061013b565b505050905090810190601f16801561018b5780820380516001836020036101000a031916815260200191505b" \ 164 | "509350505050604051809103906000f08015156101a457fe5b90503373ffffffffffffffffffffffffffffffffffffffff167f33a1" \ 165 | "926cf5c2f7306ac1685bf19260d678fea874f5f59c00b69fa5e2643ecfd28284604051808373ffffffffffffffffffffffffffffff" \ 166 | "ffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018060200182810382528381815181526020019150" \ 167 | "8051906020019080838360008314610261575b8051825260208311156102615760208201915060208101905060208303925061023d" \ 168 | "565b505050905090810190601f16801561028d5780820380516001836020036101000a031916815260200191505b50935050505060" \ 169 | "405180910390a25b919050565b6040516106d0806102b28339019056006060604052341561000c57fe5b6040516106d03803806106" \ 170 | "d0833981016040528080519060200190919080518201919050505b602e81511415156100435760006000fd5b81600060006101000a" \ 171 | "81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217" \ 172 | "905550806001908051906020019061009a9291906100a3565b505b5050610148565b82805460018160011615610100020316600290" \ 173 | "0490600052602060002090601f016020900481019282601f106100e457805160ff1916838001178555610112565b82800160010185" \ 174 | "558215610112579182015b828111156101115782518255916020019190600101906100f6565b5b50905061011f9190610123565b50" \ 175 | "90565b61014591905b80821115610141576000816000905550600101610129565b5090565b90565b610579806101576000396000f3" \ 176 | "006060604052361561008c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16" \ 177 | "806327793f871461008e578063717a195a146100b45780637e7e4b47146100d45780638da5cb5b146100fa578063a39a45b7146101" \ 178 | "4c578063c623674f14610182578063c65fb3801461021b578063ccdf68f314610245575bfe5b341561009657fe5b61009e61026f56" \ 179 | "5b6040518082815260200191505060405180910390f35b34156100bc57fe5b6100d26004808035906020019091905050610275565b" \ 180 | "005b34156100dc57fe5b6100e461034d565b6040518082815260200191505060405180910390f35b341561010257fe5b61010a6103" \ 181 | "58565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681" \ 182 | "5260200191505060405180910390f35b341561015457fe5b610180600480803573ffffffffffffffffffffffffffffffffffffffff" \ 183 | "1690602001909190505061037e565b005b341561018a57fe5b610192610484565b6040518080602001828103825283818151815260" \ 184 | "2001915080519060200190808383600083146101e1575b8051825260208311156101e1576020820191506020810190506020830392" \ 185 | "506101bd565b505050905090810190601f16801561020d5780820380516001836020036101000a031916815260200191505b509250" \ 186 | "505060405180910390f35b341561022357fe5b61022b610522565b604051808215151515815260200191505060405180910390f35b" \ 187 | "341561024d57fe5b610255610535565b604051808215151515815260200191505060405180910390f35b60035481565b6000600090" \ 188 | "54906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1633" \ 189 | "73ffffffffffffffffffffffffffffffffffffffff161415156102d25760006000fd5b600260009054906101000a900460ff161515" \ 190 | "156102ef5760006000fd5b6001600260006101000a81548160ff021916908315150217905550806003819055507fb1aaa9f4484acc" \ 191 | "283375c8e495a44766e4026170797dc9280b4ae2ab5632fb71816040518082815260200191505060405180910390a15b5b50565b60" \ 192 | "0060035490505b90565b600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000600090" \ 193 | "54906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1633" \ 194 | "73ffffffffffffffffffffffffffffffffffffffff161415156103db5760006000fd5b600260009054906101000a900460ff161515" \ 195 | "156103f85760006000fd5b80600060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffff" \ 196 | "ffffffffffffffffffffffffffffffffff1602179055508073ffffffffffffffffffffffffffffffffffffffff167f191a2405c524" \ 197 | "52c381a62f3b7480f9d3e77a76d7737659fc1030aff54b395dd560405180905060405180910390a25b5b50565b6001805460018160" \ 198 | "0116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001" \ 199 | "161561010002031660029004801561051a5780601f106104ef5761010080835404028352916020019161051a565b82019190600052" \ 200 | "6020600020905b8154815290600101906020018083116104fd57829003601f168201915b505050505081565b600260009054906101" \ 201 | "000a900460ff1681565b6000600260009054906101000a900460ff1690505b905600a165627a7a72305820ed3e2c38177c4a88e910" \ 202 | "a15d89c1a7e9b0534395a3d76f02abe2fdc9cd57a9cb0029a165627a7a723058201542f2e1ea92f43165a6f655c469365bdc5bb05e" \ 203 | "075981b919d3b50c7d3468d80029" 204 | 205 | 206 | class CentralizedOracle(object): 207 | """ 208 | Allows to share a list of oracles between test cases and django-eth-events classes/functions 209 | """ 210 | instance = None 211 | 212 | class __Oracle: 213 | oracles = None 214 | 215 | def __init__(self): 216 | self.oracles = [] 217 | 218 | def append(self, value): 219 | self.oracles.append(value) 220 | 221 | def pop(self): 222 | self.oracles.pop() 223 | 224 | def length(self): 225 | return len(self.oracles) 226 | 227 | def reset(self): 228 | self.oracles = [] 229 | 230 | def __new__(cls): 231 | if not CentralizedOracle.instance: 232 | CentralizedOracle.instance = CentralizedOracle.__Oracle() 233 | return CentralizedOracle.instance 234 | 235 | def __getattr__(self, item): 236 | return getattr(self.instance, item) 237 | 238 | 239 | class CentralizedOraclesReceiver(AbstractEventReceiver): 240 | """ 241 | A dummy Centralized Oracle receiver 242 | """ 243 | def save(self, decoded_event, block_info): 244 | CentralizedOracle().append(decoded_event) 245 | return decoded_event 246 | 247 | def rollback(self, decoded_event, block_info): 248 | CentralizedOracle().pop() 249 | 250 | 251 | class ErroredCentralizedOraclesReceiver(AbstractEventReceiver): 252 | """ 253 | A dummy Centralized Oracle receiver useful for testing atomic transactions 254 | """ 255 | def save(self, decoded_event, block_info): 256 | 1/0 257 | 258 | def rollback(self, decoded_event, block_info): 259 | 1/0 260 | -------------------------------------------------------------------------------- /django_eth_events/utils.py: -------------------------------------------------------------------------------- 1 | import errno 2 | 3 | from hexbytes import HexBytes 4 | from json import JSONEncoder 5 | from requests.exceptions import RequestException 6 | from urllib3.exceptions import HTTPError 7 | 8 | from eth_utils import to_normalized_address 9 | from ethereum.utils import remove_0x_head as remove_0x 10 | 11 | 12 | def is_network_error(exception) -> bool: 13 | """ 14 | :param exception: an exception error instance 15 | :return: True if exception detected as a network error, False otherwise 16 | """ 17 | network_errors = [errno.ECONNABORTED, errno.ECONNREFUSED, errno.ENETRESET, errno.ECONNRESET, 18 | errno.ENETUNREACH, errno.ENETDOWN] 19 | 20 | if isinstance(exception, HTTPError) or isinstance(exception, RequestException) or \ 21 | hasattr(exception, 'errno') and exception.errno in network_errors: 22 | return True 23 | 24 | return False 25 | 26 | 27 | def remove_0x_head(address) -> str: 28 | address = address.hex() if isinstance(address, bytes) else address 29 | return remove_0x(address) 30 | 31 | 32 | def normalize_address_with_0x(address) -> str: 33 | address = address.hex() if isinstance(address, bytes) else address 34 | return to_normalized_address(address) 35 | 36 | 37 | def normalize_address_without_0x(address) -> str: 38 | address = address.hex() if isinstance(address, bytes) else address 39 | return remove_0x_head(to_normalized_address(address)) 40 | 41 | 42 | class JsonBytesEncoder(JSONEncoder): 43 | def default(self, obj): 44 | if isinstance(obj, bytes): 45 | try: 46 | return obj.decode() 47 | except UnicodeDecodeError: 48 | return HexBytes(obj).hex() 49 | 50 | # Let the base class default method raise the TypeError 51 | return super().default(self, obj) -------------------------------------------------------------------------------- /django_eth_events/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '3.3.1' 2 | __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')]) 3 | -------------------------------------------------------------------------------- /django_eth_events/web3_service.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | import logging 3 | import socket 4 | from typing import Dict, List, Tuple 5 | 6 | import requests 7 | from django.core.exceptions import ImproperlyConfigured 8 | from eth_tester import EthereumTester 9 | from requests.exceptions import ConnectionError 10 | from web3 import HTTPProvider, IPCProvider, Web3 11 | from web3.exceptions import UnhandledRequest 12 | from web3.middleware import geth_poa_middleware 13 | from web3.providers.eth_tester import EthereumTesterProvider 14 | 15 | from .exceptions import (UnknownBlock, UnknownTransaction, 16 | Web3ConnectionException) 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | RINKEBY_CHAIN_ID = 4 21 | 22 | 23 | class Web3ServiceProvider: 24 | def __new__(cls): 25 | if not hasattr(cls, 'instance'): 26 | from django.conf import settings 27 | provider = Web3Service.get_provider_from_uri(settings.ETHEREUM_NODE_URL) 28 | cls.instance = Web3Service(provider, 29 | settings.ETHEREUM_MAX_WORKERS, 30 | settings.ETHEREUM_MAX_BATCH_REQUESTS) 31 | return cls.instance 32 | 33 | 34 | class Web3Service: 35 | connection_exceptions: Tuple[Exception] = (UnhandledRequest, socket.timeout, ConnectionError) 36 | 37 | @staticmethod 38 | def get_provider_from_uri(node_uri: str): 39 | if node_uri.startswith('http'): 40 | return HTTPProvider(node_uri) 41 | elif node_uri.startswith('ipc'): 42 | path = node_uri.replace('ipc://', '') 43 | return IPCProvider(ipc_path=path) 44 | elif node_uri.startswith('test'): 45 | return EthereumTesterProvider(EthereumTester()) 46 | else: 47 | raise ValueError('%s uri is not supported. Must start by http, ipc, or test' % node_uri) 48 | 49 | def __init__(self, provider, 50 | max_workers: int=10, max_batch_requests: int=10, slow_provider_timeout: int=400): 51 | """ 52 | :param node_uri: Node http address. If uri starts with 'test', EthereumTester will be used 53 | :param max_workers: Max workers for multithread calls. 1 -> No multithread 54 | :param max_batch_requests: Max requests in the same batch for RPC 55 | :param self.slow_provider_timeout: Timeout for time lasting requests (like filters) 56 | """ 57 | self.provider = provider 58 | self.max_workers = max_workers 59 | self.max_batch_requests = max_batch_requests 60 | self.slow_provider_timeout = slow_provider_timeout 61 | self.node_uri = self.get_node_uri() 62 | 63 | self.web3 = Web3(provider) 64 | self.web3_slow = Web3(self.slow_provider) 65 | self.http_session = requests.session() 66 | 67 | # If rinkeby, inject Geth PoA middleware 68 | # http://web3py.readthedocs.io/en/latest/middleware.html#geth-style-proof-of-authority 69 | try: 70 | if int(self.web3.net.version) == RINKEBY_CHAIN_ID: 71 | self.web3.middleware_stack.inject(geth_poa_middleware, layer=0) 72 | # For tests using dummy connections (like IPC) 73 | except (UnhandledRequest, ConnectionError, ConnectionRefusedError, FileNotFoundError): 74 | pass 75 | 76 | @property 77 | def slow_provider(self): 78 | if isinstance(self.provider, HTTPProvider): 79 | return HTTPProvider(endpoint_uri=self.provider.endpoint_uri, 80 | request_kwargs={'timeout': self.slow_provider_timeout}) 81 | elif isinstance(self.provider, IPCProvider): 82 | return IPCProvider(ipc_path=self.provider.ipc_path, timeout=self.slow_provider_timeout) 83 | else: 84 | return self.provider 85 | 86 | @property 87 | def main_provider(self): 88 | return self.web3.providers[0] 89 | 90 | def get_node_uri(self) -> str: 91 | if isinstance(self.provider, HTTPProvider): 92 | return self.provider.endpoint_uri 93 | 94 | def has_http_provider(self): 95 | return isinstance(self.main_provider, HTTPProvider) 96 | 97 | def make_sure_cheksumed_address(self, address: str) -> str: 98 | """ 99 | Makes sure an address is checksumed. If not, returns it checksumed 100 | and logs a warning 101 | :param address: ethereum address 102 | :return: checksumed 0x address 103 | """ 104 | if self.web3.isChecksumAddress(address): 105 | return address 106 | else: 107 | checksumed_address = self.web3.toChecksumAddress(address) 108 | logger.warning("Address %s is not checksumed, should be %s", address, checksumed_address) 109 | return checksumed_address 110 | 111 | def is_connected(self) -> bool: 112 | try: 113 | return self.web3.isConnected() 114 | except socket.timeout: 115 | return False 116 | 117 | def get_current_block_number(self) -> int: 118 | """ 119 | :raises Web3ConnectionException 120 | :return: 121 | """ 122 | try: 123 | return self.web3.eth.blockNumber 124 | except self.connection_exceptions as e: 125 | raise Web3ConnectionException('Web3 provider is not connected') from e 126 | 127 | def get_transaction_receipt(self, transaction_hash): 128 | """ 129 | :param transaction_hash: 130 | :raises Web3ConnectionException 131 | :raises UnknownTransaction 132 | :return: 133 | """ 134 | try: 135 | receipt = self.web3.eth.getTransactionReceipt(transaction_hash) 136 | 137 | if not receipt: 138 | # Might be because a reorg 139 | raise UnknownTransaction 140 | return receipt 141 | except self.connection_exceptions as e: 142 | raise Web3ConnectionException('Web3 provider is not connected') from e 143 | except Exception as e: 144 | raise UnknownTransaction from e 145 | 146 | def get_transaction_receipts(self, tx_hashes): 147 | tx_with_receipt = {} 148 | 149 | if self.has_http_provider(): 150 | # Query limit for RPC is 131072 151 | for tx_hashes_chunk in self._chunks(tx_hashes, self.max_batch_requests): 152 | rpc_request = [self._build_tx_receipt_request(tx_hash) for tx_hash in tx_hashes_chunk] 153 | for rpc_response in self._do_request(rpc_request): 154 | tx = rpc_response['result'] 155 | if not tx: 156 | raise UnknownTransaction 157 | tx_hash = tx['transactionHash'] 158 | tx_with_receipt[tx_hash] = tx 159 | else: 160 | with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor: 161 | future_receipts_with_tx = {executor.submit(self.get_transaction_receipt, tx): tx 162 | for tx in tx_hashes} 163 | 164 | for future in concurrent.futures.as_completed(future_receipts_with_tx): 165 | tx = future_receipts_with_tx[future] 166 | receipt = future.result() 167 | if not receipt: 168 | raise UnknownTransaction 169 | tx_with_receipt[tx] = receipt 170 | 171 | return tx_with_receipt 172 | 173 | def get_block(self, block_identifier, full_transactions=False): 174 | """ 175 | :param block_identifier: 176 | :param full_transactions: 177 | :raises Web3ConnectionException 178 | :raises UnknownBlock 179 | :return: 180 | """ 181 | try: 182 | block = self.web3.eth.getBlock(block_identifier, full_transactions) 183 | if not block: 184 | raise UnknownBlock 185 | return block 186 | except self.connection_exceptions: 187 | raise Web3ConnectionException('Web3 provider is not connected') 188 | except Exception as e: 189 | raise UnknownBlock from e 190 | 191 | def get_blocks(self, block_identifiers, full_transactions=False): 192 | """ 193 | :param block_identifiers: 194 | :param full_transactions: 195 | :raises Web3ConnectionException 196 | :raises UnknownBlock 197 | :return: 198 | """ 199 | 200 | blocks = {} 201 | 202 | if self.has_http_provider(): 203 | # Query limit for RPC is 131072 204 | for block_numbers_chunk in self._chunks(block_identifiers, self.max_batch_requests): 205 | rpc_request = [self._build_block_request(block_number) for block_number in block_numbers_chunk] 206 | for rpc_response in self._do_request(rpc_request): 207 | block = rpc_response['result'] 208 | if not block: 209 | raise UnknownBlock 210 | 211 | block_number = int(block['number'], 16) 212 | block['number'] = block_number 213 | block['timestamp'] = int(block['timestamp'], 16) 214 | blocks[block_number] = block 215 | else: 216 | with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor: 217 | # Get blocks from ethereum node and mark each future with its block_id 218 | future_to_block_id = {executor.submit(self.get_block, block_id, full_transactions): block_id 219 | for block_id in block_identifiers} 220 | for future in concurrent.futures.as_completed(future_to_block_id): 221 | block_id = future_to_block_id[future] 222 | block = future.result() 223 | if not block: 224 | raise UnknownBlock 225 | blocks[block_id] = block 226 | 227 | return blocks 228 | 229 | def get_current_block(self, full_transactions=False): 230 | """ 231 | :param full_transactions: 232 | :raises Web3ConnectionException 233 | :raises UnknownBlock 234 | :return: 235 | """ 236 | return self.get_block(self.get_current_block_number(), full_transactions) 237 | 238 | def get_logs_for_block(self, block): 239 | """ 240 | Extract raw logs from web3 ethereum block 241 | :param block: web3 block to get logs from 242 | :return: list of log dictionaries 243 | """ 244 | 245 | logs = [] 246 | tx_with_receipt = self.get_transaction_receipts(block['transactions']) 247 | 248 | for tx in block['transactions']: 249 | receipt = tx_with_receipt[tx] 250 | logs.extend(receipt.get('logs', [])) 251 | 252 | return logs 253 | 254 | def get_logs_for_blocks(self, blocks): 255 | """ 256 | Recover logs for every block 257 | :param blocks: web3 blocks to get logs from 258 | :return: a dictionary, the key is the block number and value is list of 259 | """ 260 | 261 | block_number_with_logs = {} 262 | 263 | with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor: 264 | # Get blocks from ethereum node and mark each future with its block_id 265 | future_logs_to_block = {executor.submit(self.get_logs_for_block, block): block for block in blocks} 266 | for future in concurrent.futures.as_completed(future_logs_to_block): 267 | block = future_logs_to_block[future] 268 | logs = future.result() 269 | block_number_with_logs[block['number']] = logs 270 | 271 | return block_number_with_logs 272 | 273 | def get_logs_for_address_using_filter(self, from_block: int, to_block: int, address: str) -> List[any]: 274 | """ 275 | Recover logs using filter for address 276 | """ 277 | return self.web3_slow.eth.getLogs({'fromBlock': from_block, 278 | 'toBlock': to_block, 279 | 'address': address}) 280 | 281 | def get_logs_for_event_using_filter(self, from_block: int, to_block: int, event_hash: str) -> List[any]: 282 | """ 283 | Recover logs using filter for event 284 | """ 285 | logs_filter = {'fromBlock': from_block, 286 | 'toBlock': to_block, 287 | 'topics': [event_hash]} 288 | return self.web3_slow.eth.getLogs(logs_filter) 289 | 290 | def _do_request(self, rpc_request): 291 | if self.has_http_provider(): 292 | return self.http_session.post(self.node_uri, json=rpc_request).json() 293 | else: 294 | raise ImproperlyConfigured('Not valid provider') 295 | 296 | def _build_block_request(self, block_number: int, full_transactions: bool=False) -> Dict[str, any]: 297 | block_number_hex = '0x{:x}'.format(block_number) 298 | return {"jsonrpc": "2.0", 299 | "method": "eth_getBlockByNumber", 300 | "params": [block_number_hex, full_transactions], 301 | "id": 1 302 | } 303 | 304 | def _build_tx_receipt_request(self, tx_hash: str) -> Dict[str, any]: 305 | return {"jsonrpc": "2.0", 306 | "method": "eth_getTransactionReceipt", 307 | "params": [tx_hash], 308 | "id": 1} 309 | 310 | def _chunks(self, iterable, size): 311 | for i in range(0, len(iterable), size): 312 | yield iterable[i:i + size] 313 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.local') 7 | 8 | try: 9 | from django.core.management import execute_from_command_line 10 | except ImportError: 11 | # The above import may fail for some other reason. Ensure that the 12 | # issue is really that Django is missing to avoid masking other 13 | # exceptions on Python 2. 14 | try: 15 | import django # noqa 16 | except ImportError: 17 | raise ImportError( 18 | "Couldn't import Django. Are you sure it's installed and " 19 | "available on your PYTHONPATH environment variable? Did you " 20 | "forget to activate a virtual environment?" 21 | ) 22 | raise 23 | 24 | # This allows easy placement of apps within the interior 25 | # gnosisdb directory. 26 | current_path = os.path.dirname(os.path.abspath(__file__)) 27 | sys.path.append(os.path.join(current_path, 'django_eth_events')) 28 | 29 | execute_from_command_line(sys.argv) 30 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==2.1.7 2 | celery==4.2.1 3 | django-authtools==1.6.0 4 | django-model-utils==3.1.2 5 | django-solo==1.1.3 6 | eth-abi==1.3.0 7 | eth-utils==1.2.2 8 | ethereum==2.3.2 9 | factory-boy==2.11.1 10 | requests==2.18.4 11 | urllib3==1.22 12 | web3[tester]==4.9.2 13 | -------------------------------------------------------------------------------- /run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | import django 6 | from django.conf import settings 7 | from django.test.utils import get_runner 8 | 9 | if __name__ == "__main__": 10 | os.environ['DJANGO_SETTINGS_MODULE'] = 'config.settings.test' 11 | django.setup() 12 | TestRunner = get_runner(settings) 13 | test_runner = TestRunner() 14 | failures = test_runner.run_tests(["django_eth_events.tests"]) 15 | sys.exit(bool(failures)) 16 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # fail if any commands fails 3 | set -e 4 | 5 | python -m pip install --upgrade setuptools wheel 6 | python -m pip install --upgrade twine 7 | 8 | python setup.py sdist bdist_wheel 9 | twine upload -u "${1}" -p "${2}" dist/* 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from distutils.util import convert_path 3 | 4 | from setuptools import find_packages, setup 5 | 6 | # allow setup.py to be run from any path 7 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 8 | 9 | 10 | def read_version(): 11 | main_ns = {} 12 | ver_path = convert_path('django_eth_events/version.py') 13 | with open(ver_path) as ver_file: 14 | exec(ver_file.read(), main_ns) 15 | return main_ns['__version__'] 16 | 17 | 18 | version = read_version() 19 | 20 | requirements = [ 21 | "celery>=4.1.0", 22 | "Django>=2.0.0", 23 | "django-model-utils>=3", 24 | "django-solo>=1.1.3", 25 | "eth-abi>=1.0.0", 26 | "eth-utils>=1.0.0", 27 | "ethereum>=2", 28 | "kombu>=4.1.0", 29 | "requests>=2.0.0", 30 | "web3>=4", 31 | ] 32 | 33 | setup( 34 | name='django-eth-events', 35 | version=version, 36 | packages=find_packages(), 37 | include_package_data=True, 38 | install_requires=requirements, 39 | license='MIT License', 40 | description='A simple Django app to react to Ethereum events.', 41 | url='https://github.com/gnosis/django-eth-events', 42 | author='Gnosis', 43 | author_email='dev@gnosis.pm', 44 | keywords=['ethereum', 'gnosis'], 45 | classifiers=[ 46 | 'Environment :: Web Environment', 47 | 'Framework :: Django', 48 | 'Intended Audience :: Developers', 49 | 'License :: OSI Approved :: MIT License', 50 | 'Operating System :: OS Independent', 51 | 'Programming Language :: Python', 52 | 'Programming Language :: Python :: 3', 53 | 'Topic :: Internet :: WWW/HTTP', 54 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 55 | ], 56 | data_files=[("", ["LICENSE"])], 57 | ) 58 | --------------------------------------------------------------------------------